Compare commits
1759 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c529e0afe | ||
|
|
b955473533 | ||
|
|
2026ce0ed2 | ||
|
|
42937d4bb6 | ||
|
|
cfd259ae6f | ||
|
|
493530ec6d | ||
|
|
4dea0892cb | ||
|
|
9deab6c0fd | ||
|
|
f9e9413f52 | ||
|
|
5171d167ce | ||
|
|
2981bece55 | ||
|
|
fd211bf490 | ||
|
|
9263b3a051 | ||
|
|
88209c6ac6 | ||
|
|
688f955f5e | ||
|
|
f3cee8f0b5 | ||
|
|
b7fd3f0031 | ||
|
|
d1e00f6e19 | ||
|
|
c9480c5b8b | ||
|
|
4829eac1e1 | ||
|
|
a10da0e540 | ||
|
|
7ac8a88a05 | ||
|
|
639ae0cfe0 | ||
|
|
c8f5a40fd9 | ||
|
|
f4f23e8e09 | ||
|
|
dc9ad12182 | ||
|
|
a808e09204 | ||
|
|
6ae16eba36 | ||
|
|
11f100813e | ||
|
|
b64470443f | ||
|
|
8633c4cefd | ||
|
|
e9240f7eee | ||
|
|
04deea3c6d | ||
|
|
6ca8923ec7 | ||
|
|
e3562be530 | ||
|
|
c5fd42b699 | ||
|
|
afdbb6b17a | ||
|
|
a0076460db | ||
|
|
787c866191 | ||
|
|
95245b935c | ||
|
|
f61f39efdd | ||
|
|
a73d517bee | ||
|
|
b3727438d6 | ||
|
|
b184f391c6 | ||
|
|
5f860181b6 | ||
|
|
6caa7083db | ||
|
|
839909f3f6 | ||
|
|
4194c9cce2 | ||
|
|
e8261e0c77 | ||
|
|
22e1f4946e | ||
|
|
971ebcbd77 | ||
|
|
03aca9ea79 | ||
|
|
1c0ffc5caf | ||
|
|
4e3a807733 | ||
|
|
d29084ec2c | ||
|
|
c5faa00ace | ||
|
|
910b25d416 | ||
|
|
cb4b5bd684 | ||
|
|
842aa5746f | ||
|
|
c2c2451788 | ||
|
|
cd7ca3de68 | ||
|
|
d52d0251b2 | ||
|
|
cdead1a991 | ||
|
|
031a20699c | ||
|
|
e4b18ea5c3 | ||
|
|
b72c3310bc | ||
|
|
748da0e5d7 | ||
|
|
bc501a28af | ||
|
|
9d2d2d17af | ||
|
|
b403395cdb | ||
|
|
fbce3bb48f | ||
|
|
bf23a0f4b8 | ||
|
|
448ec8b740 | ||
|
|
272f987b0c | ||
|
|
0ebac22bb2 | ||
|
|
c55e42f856 | ||
|
|
eb2caa554c | ||
|
|
7c5d2ea81d | ||
|
|
b09d3724a0 | ||
|
|
acb6b186f2 | ||
|
|
a3fdcd9b17 | ||
|
|
e5c76cb22d | ||
|
|
730114d088 | ||
|
|
c0c685a5de | ||
|
|
79ebca3f30 | ||
|
|
25d2cc4604 | ||
|
|
716fa97fa1 | ||
|
|
e1063678f1 | ||
|
|
661495e5c5 | ||
|
|
9d1ae0a149 | ||
|
|
7f776fe19a | ||
|
|
c10f483b9f | ||
|
|
ea9a491fb3 | ||
|
|
ca460e11e6 | ||
|
|
c3588b545f | ||
|
|
dc1ce51ac2 | ||
|
|
6e51918353 | ||
|
|
1d2afada83 | ||
|
|
29f4da93d4 | ||
|
|
5acbd7bc86 | ||
|
|
5ff75a41ea | ||
|
|
89badfec0c | ||
|
|
662d755974 | ||
|
|
bf9b94595c | ||
|
|
41d61ed221 | ||
|
|
e2f72ffed8 | ||
|
|
79c2abf531 | ||
|
|
a5ff345f7b | ||
|
|
f19ba6c2b1 | ||
|
|
745cd26850 | ||
|
|
71c5883d52 | ||
|
|
c30eafa254 | ||
|
|
7c3be72ac7 | ||
|
|
622e64320a | ||
|
|
0efcfeed17 | ||
|
|
65de43e67d | ||
|
|
cd4557cce8 | ||
|
|
714a97e452 | ||
|
|
80897f62a6 | ||
|
|
dfd83b59f8 | ||
|
|
bb42e43ee7 | ||
|
|
8fe5c704e3 | ||
|
|
b1c9b8b415 | ||
|
|
7658f60146 | ||
|
|
49c99a41ea | ||
|
|
f02d9425f9 | ||
|
|
1aca998f3f | ||
|
|
2ba0fe2876 | ||
|
|
546093175e | ||
|
|
e4d5f88257 | ||
|
|
3a8e375efe | ||
|
|
f5f3a2a928 | ||
|
|
6bca5a1c25 | ||
|
|
02a2272cfe | ||
|
|
fe8def98e4 | ||
|
|
f4bfd571ee | ||
|
|
f32f3f1980 | ||
|
|
baf5b5d005 | ||
|
|
3bcda48ba4 | ||
|
|
16f90ffc92 | ||
|
|
6520cf00e9 | ||
|
|
7eb1211192 | ||
|
|
f978b545c5 | ||
|
|
6192e74f03 | ||
|
|
7f71ce0ab2 | ||
|
|
a62d12634c | ||
|
|
886ac82c43 | ||
|
|
2575053697 | ||
|
|
130e6cf8a2 | ||
|
|
3841e99720 | ||
|
|
79d22bf334 | ||
|
|
45065e4e2e | ||
|
|
558e5406e8 | ||
|
|
1150e87e31 | ||
|
|
6676aeda5a | ||
|
|
9a90aaca96 | ||
|
|
bf74401fd3 | ||
|
|
9a84c9edb6 | ||
|
|
08a572086b | ||
|
|
2093889ac2 | ||
|
|
9c19728d2b | ||
|
|
859259ddae | ||
|
|
a0cefb3213 | ||
|
|
fdd23878ec | ||
|
|
b4c47c0ac0 | ||
|
|
5af262daa3 | ||
|
|
16720b96b4 | ||
|
|
b2b003dcac | ||
|
|
c5bf148ed7 | ||
|
|
81018f1996 | ||
|
|
a13f23d218 | ||
|
|
6d5641afce | ||
|
|
9396489b4c | ||
|
|
2eb3bdf132 | ||
|
|
a0dd478637 | ||
|
|
1d1eac4c6b | ||
|
|
d6bb5cb6c1 | ||
|
|
d79c89beba | ||
|
|
2cfc183029 | ||
|
|
e0645564fe | ||
|
|
cd475c7b27 | ||
|
|
c06ff2a992 | ||
|
|
1e107e6bd1 | ||
|
|
3d843edc69 | ||
|
|
2e87cf4a62 | ||
|
|
1de38a25fc | ||
|
|
e035f57535 | ||
|
|
e20216a1a8 | ||
|
|
c7888d1d97 | ||
|
|
41d2faccea | ||
|
|
dd69394598 | ||
|
|
c40947e651 | ||
|
|
b6f876e721 | ||
|
|
5a1c679f78 | ||
|
|
0b3f1b4a7c | ||
|
|
e622975ffd | ||
|
|
6ffb659282 | ||
|
|
8625eb643e | ||
|
|
76f0988551 | ||
|
|
348e519437 | ||
|
|
7292212d5a | ||
|
|
4ad56e84a8 | ||
|
|
e3734ef337 | ||
|
|
06a70b6d6d | ||
|
|
3fa261564b | ||
|
|
18d175708c | ||
|
|
19791546da | ||
|
|
885e461ae3 | ||
|
|
3a27cd87ce | ||
|
|
6380e19f07 | ||
|
|
c0eec5d61c | ||
|
|
c493f263b6 | ||
|
|
553aef57aa | ||
|
|
9f30ae9850 | ||
|
|
dd7112d5ea | ||
|
|
b49bedcf0c | ||
|
|
87cb5bc5b7 | ||
|
|
e59471766a | ||
|
|
4533a50542 | ||
|
|
ccf7584fac | ||
|
|
06b1b69fb7 | ||
|
|
333cb27272 | ||
|
|
d988d2006f | ||
|
|
42b43a7d7b | ||
|
|
e67d66a5d4 | ||
|
|
b25e41e348 | ||
|
|
70da93145d | ||
|
|
a20c6d072d | ||
|
|
06585f5bdd | ||
|
|
44d9365da0 | ||
|
|
2ddbac1f98 | ||
|
|
287df16c9c | ||
|
|
5f0e92a432 | ||
|
|
d7d418cd47 | ||
|
|
24212fd97f | ||
|
|
3e41c3cbb3 | ||
|
|
359f248729 | ||
|
|
f730291904 | ||
|
|
2ffd37b816 | ||
|
|
7758bcd141 | ||
|
|
740a97a8cc | ||
|
|
2c42f15e00 | ||
|
|
37ed391cc2 | ||
|
|
f22d14b105 | ||
|
|
d749021a31 | ||
|
|
93aee0f814 | ||
|
|
420823070b | ||
|
|
ddbdcab522 | ||
|
|
2174f3ce37 | ||
|
|
73fdda0e45 | ||
|
|
1d5215ab4f | ||
|
|
cafd71eb29 | ||
|
|
41cef6f5f2 | ||
|
|
681e502c12 | ||
|
|
bc509b42e8 | ||
|
|
e103932aad | ||
|
|
1c680210c2 | ||
|
|
eb989c8257 | ||
|
|
14625907ae | ||
|
|
353360dbe5 | ||
|
|
3497aa0766 | ||
|
|
d49fb8a2d5 | ||
|
|
e8583f01a0 | ||
|
|
e130a0257d | ||
|
|
729b5e9b2f | ||
|
|
36b86af4b9 | ||
|
|
40fa7b25c5 | ||
|
|
4e21d1d77b | ||
|
|
84fdba129a | ||
|
|
9ae40e393a | ||
|
|
18125c7d1f | ||
|
|
03d8a6c05d | ||
|
|
fd4d35d9a2 | ||
|
|
91a29932a6 | ||
|
|
b59376bea4 | ||
|
|
13c5456868 | ||
|
|
4f83586f55 | ||
|
|
77f54dbdae | ||
|
|
2bbe709bce | ||
|
|
39f1471e93 | ||
|
|
ab9befb197 | ||
|
|
6d446e7167 | ||
|
|
02e742b7a6 | ||
|
|
185e730feb | ||
|
|
e79413acc4 | ||
|
|
d761bfffd7 | ||
|
|
f7bacd169e | ||
|
|
8fca4781f1 | ||
|
|
657ca97dbd | ||
|
|
3868b61443 | ||
|
|
c605cf92f9 | ||
|
|
b097d8c0a6 | ||
|
|
db9809d6dc | ||
|
|
63bac67fb5 | ||
|
|
54a3d6210b | ||
|
|
15b865f502 | ||
|
|
9846953597 | ||
|
|
2fc7aede0b | ||
|
|
f6506fa6ca | ||
|
|
52b4eb6c46 | ||
|
|
aac371cf07 | ||
|
|
4d8e3cbcb0 | ||
|
|
7a918a1617 | ||
|
|
0b83b4076a | ||
|
|
4302972c23 | ||
|
|
235f9da432 | ||
|
|
9cf9900721 | ||
|
|
2035c5f8be | ||
|
|
5ebad5c96d | ||
|
|
2ab4bf13ab | ||
|
|
5098e5fc47 | ||
|
|
017ceffb76 | ||
|
|
e69e785b23 | ||
|
|
e8f4819876 | ||
|
|
985fd0cc2b | ||
|
|
97e5f7d7e9 | ||
|
|
011565ca10 | ||
|
|
e68fa641ff | ||
|
|
3e6bee2fc6 | ||
|
|
983d249680 | ||
|
|
8338c692a3 | ||
|
|
f64d5f1209 | ||
|
|
f38c632635 | ||
|
|
251fc68ef9 | ||
|
|
df8a83b2a1 | ||
|
|
0b92650494 | ||
|
|
9906a19e29 | ||
|
|
f78d87ee38 | ||
|
|
00e1a2122a | ||
|
|
80ee620459 | ||
|
|
023687d8d0 | ||
|
|
e3d60024aa | ||
|
|
a060b8ff73 | ||
|
|
ae28e4ba0f | ||
|
|
92c61b0de3 | ||
|
|
ed2c06c8cd | ||
|
|
033def0a7a | ||
|
|
9f94e443ff | ||
|
|
91e8e59cea | ||
|
|
4f1ae8c45e | ||
|
|
b84fcbc85e | ||
|
|
3cd19a7e45 | ||
|
|
d104487282 | ||
|
|
35bbcc39a2 | ||
|
|
ab9e246ab0 | ||
|
|
df29120abe | ||
|
|
5a17e797c7 | ||
|
|
ce96973ed5 | ||
|
|
77844ec5f3 | ||
|
|
0e26de2218 | ||
|
|
81f18f8a0f | ||
|
|
0769bb4898 | ||
|
|
31cfbac1f4 | ||
|
|
045274e647 | ||
|
|
6af7172204 | ||
|
|
412b43b216 | ||
|
|
de65737cb1 | ||
|
|
953916df49 | ||
|
|
2f7d0f8bd9 | ||
|
|
23aaa8a62c | ||
|
|
604a39f5ef | ||
|
|
a5bc98136d | ||
|
|
7dab2e1efe | ||
|
|
da1d5712cf | ||
|
|
30ff723d57 | ||
|
|
e227950b06 | ||
|
|
6719a818e7 | ||
|
|
08432c3e97 | ||
|
|
15497dcd77 | ||
|
|
8a0867c580 | ||
|
|
d4789f1ac4 | ||
|
|
26e7532756 | ||
|
|
570c4cc55a | ||
|
|
728d8fbdc5 | ||
|
|
fad569ae1b | ||
|
|
a4dbb27fab | ||
|
|
ec5286ea8c | ||
|
|
195afa0733 | ||
|
|
8bde0c5957 | ||
|
|
b18e6439bd | ||
|
|
5e0235946b | ||
|
|
360c09a1e7 | ||
|
|
85f7aa2f9b | ||
|
|
a7b4ed89da | ||
|
|
dcdc823dd2 | ||
|
|
30e7104b05 | ||
|
|
330de0a93d | ||
|
|
aa25fb05a9 | ||
|
|
4462b83258 | ||
|
|
94cdec2cfe | ||
|
|
ebf32ae8a9 | ||
|
|
74bc50e97c | ||
|
|
0f52856f99 | ||
|
|
077f0d3d66 | ||
|
|
37a09a6c30 | ||
|
|
4e98d2b7f1 | ||
|
|
d73c8e6a5e | ||
|
|
195422f9c0 | ||
|
|
62ca4ae963 | ||
|
|
35bea86c9f | ||
|
|
d774f3ca86 | ||
|
|
167625d24d | ||
|
|
3bc6c0f936 | ||
|
|
3cf82c6594 | ||
|
|
71a00c3223 | ||
|
|
e2e29284f0 | ||
|
|
26e64fc45c | ||
|
|
8f4f2c665d | ||
|
|
7a71b69a87 | ||
|
|
8910ec2870 | ||
|
|
c5d26ae1bb | ||
|
|
d6033037ac | ||
|
|
5750ae784a | ||
|
|
194581ab5f | ||
|
|
5ec2a17f08 | ||
|
|
7445c5bd70 | ||
|
|
1baa1a4d01 | ||
|
|
40ec0ec97d | ||
|
|
8a6aa5e17e | ||
|
|
854e603f84 | ||
|
|
65c56d4c00 | ||
|
|
e7374c39ba | ||
|
|
0ee3ee7333 | ||
|
|
0624a69964 | ||
|
|
fe897c81be | ||
|
|
ae4c8b8635 | ||
|
|
d70a31168b | ||
|
|
4fa64a962e | ||
|
|
438ea86137 | ||
|
|
d773c7498f | ||
|
|
4ef6cdfb68 | ||
|
|
d10054a38d | ||
|
|
9293062221 | ||
|
|
7fa27af408 | ||
|
|
2c419c4790 | ||
|
|
afcad74be8 | ||
|
|
6c395cb58c | ||
|
|
044d874c5b | ||
|
|
cb21d844d9 | ||
|
|
0282da9ddf | ||
|
|
e07144aeb4 | ||
|
|
7993afae46 | ||
|
|
bfc3e48fd5 | ||
|
|
76497c2542 | ||
|
|
27a98788a8 | ||
|
|
9fb1637ce2 | ||
|
|
75679f08c9 | ||
|
|
444cdfe6e3 | ||
|
|
89b00d714f | ||
|
|
3c2fd833ca | ||
|
|
01f5913826 | ||
|
|
0998170a01 | ||
|
|
28cf4c3226 | ||
|
|
08613b621e | ||
|
|
ee40ea5f6d | ||
|
|
e84c00efae | ||
|
|
f78b6df8bc | ||
|
|
53021ea264 | ||
|
|
01d067ec2b | ||
|
|
8e0589af69 | ||
|
|
8664fc4102 | ||
|
|
755d7b3787 | ||
|
|
cba1ca6244 | ||
|
|
cdd25c9512 | ||
|
|
898b63b665 | ||
|
|
e947732bde | ||
|
|
b0491e1a5e | ||
|
|
190326c186 | ||
|
|
0008bcb877 | ||
|
|
080dfb9841 | ||
|
|
02421790bf | ||
|
|
48d91def7e | ||
|
|
eb73db56c7 | ||
|
|
80b5ebc398 | ||
|
|
94332ed1d8 | ||
|
|
13c01193d6 | ||
|
|
26f590babe | ||
|
|
f90b2f845c | ||
|
|
9d4e0365da | ||
|
|
4d6fef36f4 | ||
|
|
0431b8bb74 | ||
|
|
2653024409 | ||
|
|
923174718e | ||
|
|
73f37d0989 | ||
|
|
0722b95e53 | ||
|
|
ae89436d97 | ||
|
|
af77a23501 | ||
|
|
2a1424e563 | ||
|
|
1871d526ac | ||
|
|
9346e18d8c | ||
|
|
1db5c95414 | ||
|
|
0c05b906d4 | ||
|
|
f04e01f55f | ||
|
|
a0da7b9774 | ||
|
|
0b0406fa85 | ||
|
|
f1245b094f | ||
|
|
c840e5c06e | ||
|
|
c7573a92b2 | ||
|
|
84a476af87 | ||
|
|
e890d60459 | ||
|
|
5dbe565b6d | ||
|
|
8f09904c06 | ||
|
|
c0db143d4a | ||
|
|
534dbacc80 | ||
|
|
af439e7d74 | ||
|
|
b38cadbecd | ||
|
|
edfadf3a7c | ||
|
|
ae66683998 | ||
|
|
4a8a4cb21b | ||
|
|
ccebf8e1d6 | ||
|
|
8663be34b4 | ||
|
|
502a372405 | ||
|
|
088ead2477 | ||
|
|
f2494ecb3d | ||
|
|
e9e01f2c9c | ||
|
|
d4e77faea7 | ||
|
|
ff58ae66c0 | ||
|
|
b40f760cc3 | ||
|
|
42e7456780 | ||
|
|
43d27ec7ed | ||
|
|
645ddc917f | ||
|
|
33a7f03ccc | ||
|
|
8b53a72dde | ||
|
|
8ee73e028f | ||
|
|
ca44e88e54 | ||
|
|
304fc4f222 | ||
|
|
09edb813bc | ||
|
|
f3ad2e4ad2 | ||
|
|
a2bb2ff164 | ||
|
|
b5fd3cfb84 | ||
|
|
c037f52878 | ||
|
|
584cccf7ec | ||
|
|
c92467df2f | ||
|
|
ad08bf79e7 | ||
|
|
dc727832a0 | ||
|
|
22c62dbd90 | ||
|
|
d220be8468 | ||
|
|
b09be4e3ef | ||
|
|
efce2ababa | ||
|
|
773b8b69dd | ||
|
|
ff8d787cd5 | ||
|
|
d299f0d99f | ||
|
|
bf837164b4 | ||
|
|
f6d589caa1 | ||
|
|
738c8762df | ||
|
|
3d990a6237 | ||
|
|
b024dbe794 | ||
|
|
83a93e5e9a | ||
|
|
504b6b72a9 | ||
|
|
3401049dea | ||
|
|
45851644c1 | ||
|
|
a7f880fa1f | ||
|
|
c4bd3c672b | ||
|
|
52354b9ab5 | ||
|
|
3c187c844e | ||
|
|
45ed4c726e | ||
|
|
d9a44098ce | ||
|
|
b93abfb3d1 | ||
|
|
bb8141e27c | ||
|
|
bddc88f09e | ||
|
|
5ea7f0342b | ||
|
|
13db045fc4 | ||
|
|
23538bcd31 | ||
|
|
a4f2236b36 | ||
|
|
88538f13ba | ||
|
|
920b5afe45 | ||
|
|
e4b9603fa8 | ||
|
|
ef7f627573 | ||
|
|
5a06330f61 | ||
|
|
93628fc0eb | ||
|
|
47936643c9 | ||
|
|
11ad61ba07 | ||
|
|
129549fd5b | ||
|
|
652a353319 | ||
|
|
d6d8d5b9d1 | ||
|
|
aa2ffb9805 | ||
|
|
7f554f50e3 | ||
|
|
25963e0544 | ||
|
|
5ee7ee0850 | ||
|
|
d6670bd6a8 | ||
|
|
9aec5cd52d | ||
|
|
4d5161444c | ||
|
|
db5649ec6a | ||
|
|
a8afba054a | ||
|
|
d3e363b97a | ||
|
|
2e3e1e9b7e | ||
|
|
424e5d1394 | ||
|
|
2d8f115d8c | ||
|
|
b2d66b9e7b | ||
|
|
c54afbe42e | ||
|
|
dea1c96031 | ||
|
|
16c922bf53 | ||
|
|
e94f5727c4 | ||
|
|
35da487d46 | ||
|
|
d2ea7387f2 | ||
|
|
8e009ee31c | ||
|
|
0c961deeaa | ||
|
|
32165d82b1 | ||
|
|
d422247433 | ||
|
|
a042c57227 | ||
|
|
3a1374e69c | ||
|
|
1c0582eaa7 | ||
|
|
1e883f5979 | ||
|
|
9c5495832c | ||
|
|
7364647f2f | ||
|
|
4e116ed503 | ||
|
|
e7a2e53108 | ||
|
|
2f8a1aed6e | ||
|
|
a63585dcab | ||
|
|
ea50ef1588 | ||
|
|
d2a5c7f99b | ||
|
|
bbbb6dc2e3 | ||
|
|
f41528433b | ||
|
|
02a4042dca | ||
|
|
b03c1342ac | ||
|
|
d8e91d9fee | ||
|
|
a4ddec1157 | ||
|
|
9020bf48b7 | ||
|
|
237ac8562f | ||
|
|
feb4b2249a | ||
|
|
95dd2eb1da | ||
|
|
8fde2f98ae | ||
|
|
2b80caf1af | ||
|
|
b4a655af52 | ||
|
|
e450218daa | ||
|
|
0cc9d7e6b6 | ||
|
|
6fe5493c3c | ||
|
|
a3aebfaefe | ||
|
|
9d5182eaad | ||
|
|
27589eb7e1 | ||
|
|
d5db9faba8 | ||
|
|
715a235b45 | ||
|
|
f746c190ac | ||
|
|
d632a0d5c2 | ||
|
|
894d7dca22 | ||
|
|
20d6c0b560 | ||
|
|
c08dfdc330 | ||
|
|
7049ebe4e2 | ||
|
|
1ef49ac5ab | ||
|
|
db650de372 | ||
|
|
d9885388d0 | ||
|
|
a6b5f583a7 | ||
|
|
cfe9c9c169 | ||
|
|
240cd1f28d | ||
|
|
31af381c56 | ||
|
|
bc0ecd1d06 | ||
|
|
c792ab8aed | ||
|
|
bac1ccd5b3 | ||
|
|
0b361c62c8 | ||
|
|
282f4ce920 | ||
|
|
5c8e5acf9d | ||
|
|
fb07a09964 | ||
|
|
6e2bc7712c | ||
|
|
173bd13ece | ||
|
|
e04273df57 | ||
|
|
15328c04eb | ||
|
|
c7a60af666 | ||
|
|
09dff73607 | ||
|
|
6591d7a209 | ||
|
|
220d177e76 | ||
|
|
ddffa9d1cc | ||
|
|
9cae9a2a97 | ||
|
|
129bb5393b | ||
|
|
bf0b70aa66 | ||
|
|
db08dcab14 | ||
|
|
edd6c0f9b3 | ||
|
|
a58cb3b95c | ||
|
|
40a682476d | ||
|
|
b7d43c5a5d | ||
|
|
236cac86b1 | ||
|
|
79c0515945 | ||
|
|
74019025b9 | ||
|
|
0e916460d8 | ||
|
|
1ab93a5f1d | ||
|
|
69a0eacb4f | ||
|
|
d9d932aef5 | ||
|
|
421c43a873 | ||
|
|
6558c7245f | ||
|
|
b3d646455a | ||
|
|
161d9e5971 | ||
|
|
30260361dd | ||
|
|
c1b935adbd | ||
|
|
7f05cd8bfc | ||
|
|
b55d66d0cf | ||
|
|
3ed3e51997 | ||
|
|
81d7883884 | ||
|
|
ad137d46a2 | ||
|
|
e16983d265 | ||
|
|
fd6ff9bc01 | ||
|
|
d2a8866bce | ||
|
|
c69978fbb0 | ||
|
|
c1eaa72883 | ||
|
|
55f3ffd2ba | ||
|
|
55eb82c434 | ||
|
|
d94a29e866 | ||
|
|
a7d2a82caa | ||
|
|
ce95437dee | ||
|
|
3ba3112a85 | ||
|
|
c019e489d2 | ||
|
|
43e4fcf6dd | ||
|
|
65ca554230 | ||
|
|
52a2999a91 | ||
|
|
f3580bee2d | ||
|
|
deb163d237 | ||
|
|
6f81602ba2 | ||
|
|
ac17f20d98 | ||
|
|
a9b7de8bf0 | ||
|
|
0fc75c9622 | ||
|
|
9726fafa98 | ||
|
|
cf7bb70809 | ||
|
|
03c3930734 | ||
|
|
25a4d7d882 | ||
|
|
eead0365b5 | ||
|
|
eb3b989286 | ||
|
|
cdb46e9f16 | ||
|
|
ead25634fe | ||
|
|
3f3521c826 | ||
|
|
0c2dfb9683 | ||
|
|
5d6b0a59c0 | ||
|
|
c629f6b18b | ||
|
|
b48eda6d7d | ||
|
|
81243657be | ||
|
|
24fe051803 | ||
|
|
16bcfa749e | ||
|
|
d083153548 | ||
|
|
41e6b04f0b | ||
|
|
33c2a3a3e5 | ||
|
|
97d5da5c99 | ||
|
|
d1005ebb8f | ||
|
|
c4623939af | ||
|
|
060609317a | ||
|
|
bae0f7f46a | ||
|
|
09a50b6e6d | ||
|
|
200fe072f1 | ||
|
|
ff9ec13ddb | ||
|
|
9384ff38c9 | ||
|
|
1e6e373913 | ||
|
|
e510eb0d45 | ||
|
|
2e2d6a8bef | ||
|
|
44cd1c9862 | ||
|
|
f6f084fa01 | ||
|
|
b7d212c0c9 | ||
|
|
bda3e53df6 | ||
|
|
9d4354c0a4 | ||
|
|
f99ddbfdf2 | ||
|
|
019dc14fc4 | ||
|
|
dbb58b39d9 | ||
|
|
c3ca44b46f | ||
|
|
f6df3b0b97 | ||
|
|
e9d4853296 | ||
|
|
28c2327f73 | ||
|
|
8ed5b77aba | ||
|
|
660b84a052 | ||
|
|
d73e689991 | ||
|
|
c773ea664b | ||
|
|
aa757f7715 | ||
|
|
4222a806f1 | ||
|
|
415c76b255 | ||
|
|
f1c9554f42 | ||
|
|
3d0b756877 | ||
|
|
ea4e9fa4e2 | ||
|
|
777bde7c60 | ||
|
|
e19f3c260f | ||
|
|
5025478ac2 | ||
|
|
5b73de92c2 | ||
|
|
6a237bd49f | ||
|
|
89710b4901 | ||
|
|
459d6e610c | ||
|
|
d1adbf4a5c | ||
|
|
c11cc36997 | ||
|
|
2b7fae2368 | ||
|
|
f2ca0b8170 | ||
|
|
912c8f0540 | ||
|
|
8f12269db7 | ||
|
|
7325a5fe2e | ||
|
|
9f4d0be895 | ||
|
|
394367e1d2 | ||
|
|
7e15fb7f2d | ||
|
|
8859936301 | ||
|
|
d580f5bac8 | ||
|
|
5941b2e071 | ||
|
|
bca19a1156 | ||
|
|
3df5989326 | ||
|
|
98b9de4ff1 | ||
|
|
0e248c9aed | ||
|
|
883b3ca2c1 | ||
|
|
e4bb48995a | ||
|
|
1d15bb2880 | ||
|
|
959395b796 | ||
|
|
f6caf230f8 | ||
|
|
f2cdbe776e | ||
|
|
97c9c10f3c | ||
|
|
f74930db79 | ||
|
|
5a88a9b22a | ||
|
|
d0d4759c96 | ||
|
|
749316623b | ||
|
|
60cdb875ed | ||
|
|
e0251ecb41 | ||
|
|
0834b63560 | ||
|
|
0c8569dcb0 | ||
|
|
5f2444d2a2 | ||
|
|
537dca477b | ||
|
|
8480111012 | ||
|
|
c63dc62294 | ||
|
|
8d19ccb56f | ||
|
|
eac4514227 | ||
|
|
16f0d100cc | ||
|
|
0b620c304b | ||
|
|
7371d436d2 | ||
|
|
b928928942 | ||
|
|
4337e9c4ba | ||
|
|
8dfd6c17e3 | ||
|
|
de7c97aa1e | ||
|
|
ace668ae8f | ||
|
|
857db415bc | ||
|
|
f8623a6668 | ||
|
|
4cadc600d5 | ||
|
|
d62ec2985d | ||
|
|
b2341899c5 | ||
|
|
2c84e9ddac | ||
|
|
b7dd8eac8e | ||
|
|
61d0209093 | ||
|
|
8b9bb5ff54 | ||
|
|
cb2c89a070 | ||
|
|
cea9367739 | ||
|
|
e1a66d54b9 | ||
|
|
a37f9f19d4 | ||
|
|
91aa6ab0df | ||
|
|
c85c5d05c5 | ||
|
|
34df678bc3 | ||
|
|
8c91ffc701 | ||
|
|
eda39f361d | ||
|
|
c58770bfef | ||
|
|
d9858844c3 | ||
|
|
04b8111f8f | ||
|
|
0253f7b8d5 | ||
|
|
5999368002 | ||
|
|
bf2f2dc2a6 | ||
|
|
787b0212d1 | ||
|
|
1de5af66da | ||
|
|
61caa4f776 | ||
|
|
9ca7e46a0a | ||
|
|
f66e0825b2 | ||
|
|
f5f924d293 | ||
|
|
c3d1986101 | ||
|
|
89de87dce1 | ||
|
|
ab005a4261 | ||
|
|
3d6ad054c0 | ||
|
|
b6e619413f | ||
|
|
a43fb9cd93 | ||
|
|
68f3818562 | ||
|
|
1ed1ef3c71 | ||
|
|
b6475b058f | ||
|
|
e39556ada0 | ||
|
|
966c63d477 | ||
|
|
a298077461 | ||
|
|
e1aed27c15 | ||
|
|
a759da0208 | ||
|
|
6ab36592ea | ||
|
|
c45b7012f5 | ||
|
|
1d10db4bab | ||
|
|
767e44ef29 | ||
|
|
4a11edfb68 | ||
|
|
e53bb1af45 | ||
|
|
e5d0862fa5 | ||
|
|
2ed4c2135a | ||
|
|
8cfec56a82 | ||
|
|
818a412d29 | ||
|
|
39158957f4 | ||
|
|
2cb0145bce | ||
|
|
dfda9acd6d | ||
|
|
c6951d5184 | ||
|
|
f85648bbf8 | ||
|
|
dd1ee22293 | ||
|
|
9def8522f3 | ||
|
|
a999fcd36a | ||
|
|
24468a6f34 | ||
|
|
ba8ae427e2 | ||
|
|
d6d2e6c615 | ||
|
|
7f93063945 | ||
|
|
63f070317c | ||
|
|
3d84f35850 | ||
|
|
3d794b6b38 | ||
|
|
d8f4663f49 | ||
|
|
34b9f90f35 | ||
|
|
5ec1dd2609 | ||
|
|
b691cbd7e2 | ||
|
|
d7ada40130 | ||
|
|
2f141bbc54 | ||
|
|
a098226ee4 | ||
|
|
fd2572a39d | ||
|
|
05d4a3f9eb | ||
|
|
2161b54555 | ||
|
|
69ff29bf44 | ||
|
|
de83d35994 | ||
|
|
c7c4f62f77 | ||
|
|
e635f9f9b2 | ||
|
|
f250e912eb | ||
|
|
351931d5ca | ||
|
|
28c785a0d1 | ||
|
|
3de715ec13 | ||
|
|
ea5fb0c153 | ||
|
|
9e549a1acf | ||
|
|
c7a45d6eaf | ||
|
|
37631dbfa0 | ||
|
|
e9f240a9a3 | ||
|
|
531be22a87 | ||
|
|
be93fdf11d | ||
|
|
f05cb934a9 | ||
|
|
82d806deb6 | ||
|
|
72e6f55b45 | ||
|
|
9a0f2a9fb7 | ||
|
|
49b7237581 | ||
|
|
814d348e7d | ||
|
|
88c14cad94 | ||
|
|
b57545bd21 | ||
|
|
2eef674615 | ||
|
|
bf7c5ea32c | ||
|
|
d99243c1a7 | ||
|
|
62b8712ca9 | ||
|
|
68105b3ae4 | ||
|
|
be503f1c43 | ||
|
|
5abca55412 | ||
|
|
79d2edcbff | ||
|
|
e2c6a77f06 | ||
|
|
35eec99d41 | ||
|
|
e4fde1048b | ||
|
|
bc4eecbbac | ||
|
|
1408c9f077 | ||
|
|
418607846a | ||
|
|
6aa5611ae5 | ||
|
|
c5c9fb93aa | ||
|
|
86961291e5 | ||
|
|
7c1dadee51 | ||
|
|
b16553e34e | ||
|
|
b6dcfd4377 | ||
|
|
1a80487e71 | ||
|
|
488720da8d | ||
|
|
0264121c10 | ||
|
|
d2f448ecee | ||
|
|
c692a0ee9c | ||
|
|
8844d9d04f | ||
|
|
4063b7f8e0 | ||
|
|
a8dfe34bfb | ||
|
|
7d9d502a01 | ||
|
|
09a44f4cac | ||
|
|
b14b9515a5 | ||
|
|
2a504a5bcd | ||
|
|
dfcdf644fe | ||
|
|
7dad3cb157 | ||
|
|
745737e337 | ||
|
|
eae1055fb0 | ||
|
|
2e1c36bbb6 | ||
|
|
e3cea41dcd | ||
|
|
c0f091d540 | ||
|
|
d4cd1aad8e | ||
|
|
39a297afe6 | ||
|
|
068548f7a9 | ||
|
|
0c163ce624 | ||
|
|
7760cf1881 | ||
|
|
37bd1e03cb | ||
|
|
fc95877622 | ||
|
|
03b694a1d0 | ||
|
|
79c2a47985 | ||
|
|
9289d77a80 | ||
|
|
2eee2d0099 | ||
|
|
0ea46e6aef | ||
|
|
54e00429e4 | ||
|
|
b0ac3581dd | ||
|
|
e7ed45a5d4 | ||
|
|
d1bde69c1e | ||
|
|
7cf859085e | ||
|
|
424479cf0f | ||
|
|
98dcd764bc | ||
|
|
ccd67733fb | ||
|
|
5873ca5146 | ||
|
|
d2903507d8 | ||
|
|
224b3a2eda | ||
|
|
5d024c7433 | ||
|
|
9232b88df3 | ||
|
|
e98f77037e | ||
|
|
672e42e558 | ||
|
|
36d7df4542 | ||
|
|
e5eaf02e19 | ||
|
|
c0d1f3f7ef | ||
|
|
9597d3dafe | ||
|
|
f7282b84bd | ||
|
|
1d7b574b31 | ||
|
|
d16fdb378c | ||
|
|
cc092afd3b | ||
|
|
fd4485a540 | ||
|
|
3b8779ad17 | ||
|
|
3e875178ad | ||
|
|
97b671057d | ||
|
|
d6fc489b2b | ||
|
|
4e8438afc8 | ||
|
|
b1f8038abf | ||
|
|
5603a0cd4b | ||
|
|
3c649cf91d | ||
|
|
82d573e391 | ||
|
|
b7b96b24d8 | ||
|
|
990e7bf3b9 | ||
|
|
bcdc3d0154 | ||
|
|
faea7e1407 | ||
|
|
7c701948d5 | ||
|
|
899b804ec1 | ||
|
|
66bd4e485a | ||
|
|
8ff8dd3ae9 | ||
|
|
0d17dc1e19 | ||
|
|
83e0b52294 | ||
|
|
1265612465 | ||
|
|
40eed363e8 | ||
|
|
5ccd3f2fc5 | ||
|
|
ba878c6d9d | ||
|
|
8792261df1 | ||
|
|
c2ed29070a | ||
|
|
38104dfc92 | ||
|
|
2e55c4ba61 | ||
|
|
e6ad6e02d2 | ||
|
|
d08c4ce0ad | ||
|
|
309e3d38a0 | ||
|
|
91e2b23258 | ||
|
|
6a4492a22d | ||
|
|
0dd378da30 | ||
|
|
7b273b8577 | ||
|
|
d98521b0d9 | ||
|
|
52011e84d3 | ||
|
|
de583ed7a3 | ||
|
|
f8480caae4 | ||
|
|
47d9e6ca1f | ||
|
|
6f0a33dfdc | ||
|
|
1641d00cb1 | ||
|
|
2d7a32f7ea | ||
|
|
06acbb9f5e | ||
|
|
7b630d9080 | ||
|
|
98de64badc | ||
|
|
1e241e1f2a | ||
|
|
ff2c18fedb | ||
|
|
bf64a800d6 | ||
|
|
efc57391eb | ||
|
|
dc65aa1fea | ||
|
|
80ad3fb8ed | ||
|
|
bc7110931a | ||
|
|
8c508612ec | ||
|
|
b7d046527e | ||
|
|
42804c52e8 | ||
|
|
d88a3712c5 | ||
|
|
abfedd692e | ||
|
|
d0b048c86d | ||
|
|
b6f069f4c3 | ||
|
|
edc2e5ab82 | ||
|
|
6da9a087f8 | ||
|
|
e19462d581 | ||
|
|
a811fabb43 | ||
|
|
07e76cbef2 | ||
|
|
76d5c9e4f4 | ||
|
|
abcadc4202 | ||
|
|
b7f6a9f3fd | ||
|
|
c8264385ea | ||
|
|
ad8131be9e | ||
|
|
54c88a6cf3 | ||
|
|
115f15600f | ||
|
|
65a145e2a7 | ||
|
|
5719a72eeb | ||
|
|
4bc4495115 | ||
|
|
c66e9f8f0f | ||
|
|
52eafdc21e | ||
|
|
85c0d5481b | ||
|
|
fd9055fd11 | ||
|
|
780bdda95a | ||
|
|
fd4b461290 | ||
|
|
040062e40c | ||
|
|
d853e9167a | ||
|
|
d50ad270f0 | ||
|
|
30c93701a7 | ||
|
|
b507e1754c | ||
|
|
748fce94fd | ||
|
|
d6281b4206 | ||
|
|
b61ed2cf7e | ||
|
|
9263f30c88 | ||
|
|
8f9a88ef7a | ||
|
|
c64af0d9ce | ||
|
|
9181df42da | ||
|
|
74e1a49dd7 | ||
|
|
468b1241a5 | ||
|
|
24744cf5cf | ||
|
|
ffc969b6c2 | ||
|
|
0567a8ee77 | ||
|
|
580c8525f0 | ||
|
|
d6010aa0c9 | ||
|
|
4e35c00ab0 | ||
|
|
c46e2cbbc7 | ||
|
|
c47835f5ec | ||
|
|
412b56f7cf | ||
|
|
faba432996 | ||
|
|
2ba23e8d08 | ||
|
|
d74f852fd6 | ||
|
|
1728798e81 | ||
|
|
53a8d20d88 | ||
|
|
61446faa17 | ||
|
|
9711e335d9 | ||
|
|
080a9d2f12 | ||
|
|
15af7e1662 | ||
|
|
e42cbc714f | ||
|
|
5e26e6e553 | ||
|
|
d0a4d348fe | ||
|
|
494be731e3 | ||
|
|
8ae244a06a | ||
|
|
f91049cec9 | ||
|
|
270d0f89ba | ||
|
|
e382ed4245 | ||
|
|
ef7cb47b1e | ||
|
|
6efde60b8b | ||
|
|
fd059359cc | ||
|
|
c2c504797e | ||
|
|
84f9f45f98 | ||
|
|
28aa4c891e | ||
|
|
6ff0fdb977 | ||
|
|
b0837693d0 | ||
|
|
52851e4388 | ||
|
|
cbe31f3748 | ||
|
|
c9bbdf4f10 | ||
|
|
f984e94fca | ||
|
|
b4fe91943d | ||
|
|
7d6317802e | ||
|
|
0365e5c3a0 | ||
|
|
e6859406f1 | ||
|
|
51cff6f106 | ||
|
|
68e58e1493 | ||
|
|
9f7eac0ba1 | ||
|
|
b0e31dca86 | ||
|
|
7d10a57514 | ||
|
|
2c0f6207e9 | ||
|
|
adb12d0d4f | ||
|
|
844c141d10 | ||
|
|
02d94e69f0 | ||
|
|
1bc56f9838 | ||
|
|
98ea8fae32 | ||
|
|
36288c5134 | ||
|
|
83a3cc9c94 | ||
|
|
0c04b44919 | ||
|
|
a5e8860feb | ||
|
|
8d95f89a6a | ||
|
|
3bca62e9e4 | ||
|
|
72b4534a0c | ||
|
|
21b4280126 | ||
|
|
30a9debaf1 | ||
|
|
4c5718c78d | ||
|
|
c93b949878 | ||
|
|
cf34adb75f | ||
|
|
f824a73143 | ||
|
|
e45a33f029 | ||
|
|
064e79761c | ||
|
|
f7713c47e8 | ||
|
|
8e4e2ba244 | ||
|
|
3b8935c533 | ||
|
|
ce8678e6d5 | ||
|
|
2e1f6c85f6 | ||
|
|
ca5e6830c6 | ||
|
|
69cbac8fb5 | ||
|
|
7301981f32 | ||
|
|
555ba4159d | ||
|
|
f47ae74981 | ||
|
|
e061ace099 | ||
|
|
85d52481b1 | ||
|
|
47379d4a79 | ||
|
|
0cb9d26d83 | ||
|
|
95cc114b34 | ||
|
|
9d716a39d6 | ||
|
|
923dcfd620 | ||
|
|
b5467645d3 | ||
|
|
a65380941d | ||
|
|
17d7c60735 | ||
|
|
81f822d528 | ||
|
|
0b340aa1f6 | ||
|
|
859915dc5e | ||
|
|
a8f4f49a82 | ||
|
|
e18c3ed494 | ||
|
|
2263fcf6b7 | ||
|
|
b96d552dbd | ||
|
|
2b2c1e5b7b | ||
|
|
7b63fa5966 | ||
|
|
b18040337a | ||
|
|
42e0d7970c | ||
|
|
ed0a4fe23b | ||
|
|
bb0632c7ad | ||
|
|
84ab194516 | ||
|
|
530cae9204 | ||
|
|
9dc43e84dc | ||
|
|
8af265da04 | ||
|
|
e3b9382122 | ||
|
|
c3f4eb6d57 | ||
|
|
892aa457be | ||
|
|
8f7b53e55b | ||
|
|
d27c377817 | ||
|
|
50abe43216 | ||
|
|
ddc67ca13a | ||
|
|
a1d3da4027 | ||
|
|
85e7b11ef5 | ||
|
|
77e1f93ca1 | ||
|
|
94b1ce65c6 | ||
|
|
f5b992f68a | ||
|
|
24a458b4c8 | ||
|
|
ac1d277225 | ||
|
|
9777703e03 | ||
|
|
f43cda9681 | ||
|
|
b47fdbe0a7 | ||
|
|
cde970be69 | ||
|
|
cfd43a9b02 | ||
|
|
ebd10aa6b4 | ||
|
|
24db492f53 | ||
|
|
1b387bea62 | ||
|
|
9528b64f7f | ||
|
|
b96559149c | ||
|
|
3388d82c1c | ||
|
|
9985a7cdca | ||
|
|
1d00c5e109 | ||
|
|
0559f11aa5 | ||
|
|
2893cddb68 | ||
|
|
6910641266 | ||
|
|
4b81a07303 | ||
|
|
7eb765578a | ||
|
|
e2cf3e0932 | ||
|
|
1830de2c13 | ||
|
|
25ab906b8b | ||
|
|
8a3b4b9c37 | ||
|
|
2b8a54d5d9 | ||
|
|
4eabfed651 | ||
|
|
505a34bb85 | ||
|
|
2dade6ed00 | ||
|
|
825ea9bfa1 | ||
|
|
cc1186242c | ||
|
|
86284689a3 | ||
|
|
c70d020bf3 | ||
|
|
4622c28ffd | ||
|
|
899998cf9c | ||
|
|
6f385fb4ea | ||
|
|
18e12cbd67 | ||
|
|
3cf4e133cc | ||
|
|
d53bfe0aa7 | ||
|
|
97da43d909 | ||
|
|
ee080ce8a5 | ||
|
|
741a3e8602 | ||
|
|
177637bfb9 | ||
|
|
ae64221c34 | ||
|
|
22017f11d0 | ||
|
|
c1eaad7d57 | ||
|
|
8e6d538a57 | ||
|
|
0836d74ae7 | ||
|
|
a9e5bd52d3 | ||
|
|
b4b5e358c3 | ||
|
|
4190809d1a | ||
|
|
4e2a820c6a | ||
|
|
14a43fffee | ||
|
|
245d39f52d | ||
|
|
ac0b862f8f | ||
|
|
c41db8ebbb | ||
|
|
75c124ea17 | ||
|
|
ab13c3f362 | ||
|
|
c9af19dae1 | ||
|
|
ffffac27f9 | ||
|
|
618bd56acf | ||
|
|
402232e60f | ||
|
|
400b51caf6 | ||
|
|
9aaf0fd340 | ||
|
|
8976b3ee0e | ||
|
|
ac2f2b1deb | ||
|
|
0be961a0f3 | ||
|
|
b4a397d153 | ||
|
|
d1a9ab3df0 | ||
|
|
0ede968ec0 | ||
|
|
5f21abc3a3 | ||
|
|
e2bb81124c | ||
|
|
ea18e9656b | ||
|
|
3cc58c2f78 | ||
|
|
0ac94134f5 | ||
|
|
c142f2551d | ||
|
|
cccfaa81fb | ||
|
|
d960fb78fc | ||
|
|
d02d0bb7b7 | ||
|
|
fe4cdd8a90 | ||
|
|
54a143e6a8 | ||
|
|
3a4f69734a | ||
|
|
fbdc6e8cc0 | ||
|
|
c2c44f0ffc | ||
|
|
e12fe64b54 | ||
|
|
5240252164 | ||
|
|
1ffc006363 | ||
|
|
836232e544 | ||
|
|
2539e5a352 | ||
|
|
8e457338ee | ||
|
|
d92ee8c3c3 | ||
|
|
492c60c202 | ||
|
|
86ef81454f | ||
|
|
675669c52e | ||
|
|
92c5d395a2 | ||
|
|
fa4ea1d9e8 | ||
|
|
657a395839 | ||
|
|
9fb2079458 | ||
|
|
83620ced2e | ||
|
|
1fb824cd28 | ||
|
|
0d35994fb8 | ||
|
|
3a37f33d99 | ||
|
|
ddfb2d5f3a | ||
|
|
d81c0e9a92 | ||
|
|
048cb71bf6 | ||
|
|
903fd144ff | ||
|
|
d51e27a5cb | ||
|
|
064290a606 | ||
|
|
e6ae68c0cc | ||
|
|
43f970ab6b | ||
|
|
49b9f9091a | ||
|
|
99277be25f | ||
|
|
699892bd03 | ||
|
|
70c1503afc | ||
|
|
ee5d2eb696 | ||
|
|
2058f11931 | ||
|
|
53a9ee21d4 | ||
|
|
04118a5761 | ||
|
|
c101c30690 | ||
|
|
0e664d3471 | ||
|
|
fcb1749f10 | ||
|
|
180eb098f1 | ||
|
|
cc9b3ec296 | ||
|
|
66bd71a5d7 | ||
|
|
3365907989 | ||
|
|
6b2040f98d | ||
|
|
90551c6ce2 | ||
|
|
d3b8390df3 | ||
|
|
9554fe0bf8 | ||
|
|
5a13f31bce | ||
|
|
41bddb48a1 | ||
|
|
b820cf2e39 | ||
|
|
fd8638652d | ||
|
|
01ae5dbb2e | ||
|
|
b4797d6295 | ||
|
|
c9195a0f45 | ||
|
|
86c56c829a | ||
|
|
ef023ebad3 | ||
|
|
0c737e3de0 | ||
|
|
1b7c70eab4 | ||
|
|
6a5456f873 | ||
|
|
28b1079548 | ||
|
|
fe01d1b0df | ||
|
|
54174c308f | ||
|
|
901f764825 | ||
|
|
c1759fc384 | ||
|
|
e843b028e6 | ||
|
|
305cbecb34 | ||
|
|
a986b3fb4a | ||
|
|
fd42133d89 | ||
|
|
d6d7f3821f | ||
|
|
e79b43eeb2 | ||
|
|
afba6ce907 | ||
|
|
d5948325d4 | ||
|
|
b9b44bb87c | ||
|
|
00b00ff931 | ||
|
|
6f54a6be1e | ||
|
|
d1faccb061 | ||
|
|
0f6bb3b3ef | ||
|
|
542b87fed3 | ||
|
|
33f1df2369 | ||
|
|
041a12fdf2 | ||
|
|
555999397b | ||
|
|
25a45f6bf7 | ||
|
|
82f017edeb | ||
|
|
b4842b20f6 | ||
|
|
e14e966da9 | ||
|
|
ebe0c34a02 | ||
|
|
7d711083ec | ||
|
|
0a4d5a423e | ||
|
|
b9ccb9d31f | ||
|
|
a056b41070 | ||
|
|
6d26c44895 | ||
|
|
1644cd2da5 | ||
|
|
98135a3d30 | ||
|
|
307a41339c | ||
|
|
72ebd74715 | ||
|
|
bfa53811d3 | ||
|
|
0fa77d58c4 | ||
|
|
fa80b8ad17 | ||
|
|
9cdb6fc724 | ||
|
|
cd8e69e33c | ||
|
|
7b87f7b6b5 | ||
|
|
dd0da4643a | ||
|
|
7766526992 | ||
|
|
5c3d692008 | ||
|
|
bdf9147ad4 | ||
|
|
ad2ac256de | ||
|
|
a4466342ae | ||
|
|
0d7af592c0 | ||
|
|
66ffc5e0f8 | ||
|
|
320137a4aa | ||
|
|
0278dc9b6f | ||
|
|
7d9297e929 | ||
|
|
9e03ea8215 | ||
|
|
60f5b15f20 | ||
|
|
e67047d629 | ||
|
|
10edfa65dc | ||
|
|
daec4c70b8 | ||
|
|
dbfbc2b222 | ||
|
|
426907eafb | ||
|
|
4f0879ff9b | ||
|
|
bec6ee5c29 | ||
|
|
23fa4cec61 | ||
|
|
4b9dbd3920 | ||
|
|
98c6ced46e | ||
|
|
cb485e5af4 | ||
|
|
817b175870 | ||
|
|
bd320951e6 | ||
|
|
0cfd873abe | ||
|
|
d30ad3f5ce | ||
|
|
5dbf4fc0c2 | ||
|
|
e3a945a0b5 | ||
|
|
a5c075c4e2 | ||
|
|
c0dd7c5975 | ||
|
|
4031588811 | ||
|
|
1dc2a45cb2 | ||
|
|
40b172ca5a | ||
|
|
94031f9cef | ||
|
|
a6783cd6f3 | ||
|
|
438d85b5ad | ||
|
|
90b6ccd321 | ||
|
|
db778fd456 | ||
|
|
08f3a0791d | ||
|
|
663f824fc4 | ||
|
|
2700a94d49 | ||
|
|
e31f40c2d0 | ||
|
|
fc073cb81c | ||
|
|
2e90aaf7af | ||
|
|
238b890d9b | ||
|
|
49119e31bf | ||
|
|
bb5f1e8173 | ||
|
|
05fbd490da | ||
|
|
5322f057a0 | ||
|
|
086cc03f05 | ||
|
|
d67514b657 | ||
|
|
a467fbea0d | ||
|
|
6686c67a41 | ||
|
|
73f36fc8b7 | ||
|
|
bd8a2cc18c | ||
|
|
6d1b7e94d1 | ||
|
|
9eff939b02 | ||
|
|
0a8b27ff49 | ||
|
|
72752165df | ||
|
|
9b21d3f206 | ||
|
|
dde0a81677 | ||
|
|
31576fac61 | ||
|
|
82846777a7 | ||
|
|
7f49e0fddc | ||
|
|
eda8b02a8d | ||
|
|
1fd1617427 | ||
|
|
97252a8b66 | ||
|
|
4a1cc792c9 | ||
|
|
581b3a110c | ||
|
|
e118682db1 | ||
|
|
4eeb1c4f31 | ||
|
|
e2c4730e17 | ||
|
|
c3e844e561 | ||
|
|
fde947e1a8 | ||
|
|
ce0af892aa | ||
|
|
3f389238f8 | ||
|
|
1faf95273c | ||
|
|
ba5d4ae42f | ||
|
|
d18124f5ed | ||
|
|
846cf781a1 | ||
|
|
85dd51ccc8 | ||
|
|
ded88700a3 | ||
|
|
a9d1f40c29 | ||
|
|
e2d19aab39 | ||
|
|
7210e443ee | ||
|
|
f674c57d1a | ||
|
|
9dec27371d | ||
|
|
75328b66e6 | ||
|
|
cf9d345382 | ||
|
|
0d8392bc45 | ||
|
|
47d2d20d81 | ||
|
|
b0a5740898 | ||
|
|
bc8c4b3ebd | ||
|
|
2eebe6c677 | ||
|
|
8c326c5e66 | ||
|
|
8e9034f074 | ||
|
|
612fb96d02 | ||
|
|
49d067d72e | ||
|
|
8e1301b6d7 | ||
|
|
8ac5af2896 | ||
|
|
8550ea0728 | ||
|
|
6e619e0a70 | ||
|
|
5b2b71bfd4 | ||
|
|
d92322a574 | ||
|
|
7e793b9419 | ||
|
|
d81b703f10 | ||
|
|
1265cb9952 | ||
|
|
124e58e42d | ||
|
|
2697b63bcd | ||
|
|
ee5b836e27 | ||
|
|
a4c17dfb19 | ||
|
|
00c0d62c9b | ||
|
|
a5d4c20905 | ||
|
|
0335c6d750 | ||
|
|
8b6e42317b | ||
|
|
56e6ae567c | ||
|
|
33b663e03d | ||
|
|
4bfbe7ec22 | ||
|
|
4b395d56cc | ||
|
|
018860d626 | ||
|
|
19a76337a4 | ||
|
|
0780f2573f | ||
|
|
cec7d47c1f | ||
|
|
3d00cd35fc | ||
|
|
5aa5b9748d | ||
|
|
cb65c56037 | ||
|
|
ae090740c5 | ||
|
|
2248a31a44 | ||
|
|
9fdfa155fb | ||
|
|
e49eca8d59 | ||
|
|
263b0e7d99 | ||
|
|
42b1033385 | ||
|
|
05f6422392 | ||
|
|
f83a65c8ae | ||
|
|
071960250f | ||
|
|
16d98541f2 | ||
|
|
2b8f4214c3 | ||
|
|
d3c9927fee | ||
|
|
acc5679bc8 | ||
|
|
2ba77cb1f4 | ||
|
|
8f65418d34 | ||
|
|
8ae79e09c0 | ||
|
|
c62bfaefd6 | ||
|
|
236fff00ad | ||
|
|
9b9355b8da | ||
|
|
446bcf44fb | ||
|
|
1db6fc87c7 | ||
|
|
895d52471b | ||
|
|
d226b2faf4 | ||
|
|
74ea94625a | ||
|
|
f9927e457b | ||
|
|
a71fdd26b3 | ||
|
|
2c7d00579b | ||
|
|
de35b077a2 | ||
|
|
086d4e4ced | ||
|
|
431ce79d94 | ||
|
|
db6f347db6 | ||
|
|
4b709037ab | ||
|
|
d946299b0a | ||
|
|
694c6fd0e7 | ||
|
|
8b1e53f6d3 | ||
|
|
a930f44e60 | ||
|
|
ac19212b2d | ||
|
|
03c314e3be | ||
|
|
fad7bd4393 | ||
|
|
b80e875525 | ||
|
|
1fc466e8ac | ||
|
|
209a0cd5b2 | ||
|
|
9ddfd62848 | ||
|
|
48838727ae | ||
|
|
39503932a4 | ||
|
|
da7133d201 | ||
|
|
684d7f8668 | ||
|
|
1327eb7cef | ||
|
|
030c337c68 | ||
|
|
3ab9b48782 | ||
|
|
2bdd034242 | ||
|
|
b5a83a6af1 | ||
|
|
d565df90ad | ||
|
|
88dc5f8204 | ||
|
|
cf37c477bb | ||
|
|
fb6282caaa | ||
|
|
f7f569f730 | ||
|
|
63a924b922 | ||
|
|
f5897498f3 | ||
|
|
c478027805 | ||
|
|
109e2f215f | ||
|
|
41df742faf | ||
|
|
86f70f5201 | ||
|
|
94ee37cdb3 | ||
|
|
8360c1e687 | ||
|
|
ca5d02df06 | ||
|
|
c3fcf4d928 | ||
|
|
517d498285 | ||
|
|
9686a4129c | ||
|
|
b7d31952eb | ||
|
|
b879074a64 | ||
|
|
4800aeaef7 | ||
|
|
ca7c1f5d8e | ||
|
|
f5a6a84314 | ||
|
|
2888988eb6 | ||
|
|
e27dbdd071 | ||
|
|
6c54ee03de | ||
|
|
0cd7effe35 | ||
|
|
5851a99b82 | ||
|
|
863a206727 | ||
|
|
722b35b0a6 | ||
|
|
51dd63d3e1 | ||
|
|
57f997b0b4 | ||
|
|
654212d93b | ||
|
|
88aa8f5435 | ||
|
|
0755d0605f | ||
|
|
4b88d6d2d7 | ||
|
|
407283ef81 | ||
|
|
3fddf99661 | ||
|
|
21f72b7163 | ||
|
|
719e89fc1a | ||
|
|
afa88a479b | ||
|
|
7d86827b5e | ||
|
|
b2ebb80878 | ||
|
|
7fc0d45a4c | ||
|
|
3b85a56db2 | ||
|
|
743711cd1f | ||
|
|
cfe1d4f7c9 | ||
|
|
2cdb54225c | ||
|
|
8f24e10571 | ||
|
|
3ac36f6572 | ||
|
|
e96da76c3b | ||
|
|
ca6f3d24c3 | ||
|
|
e24b56af6c | ||
|
|
007a77c2ba | ||
|
|
18fa7d866d | ||
|
|
9ccd52d538 | ||
|
|
cbbbfcd101 | ||
|
|
72a48d69cd | ||
|
|
7e4b21e9a7 | ||
|
|
b90d82c17f | ||
|
|
b60a8c12d5 | ||
|
|
a24c7b42f1 | ||
|
|
0350a1929d | ||
|
|
99783b6fba | ||
|
|
0e3abdc1af | ||
|
|
905c648b99 | ||
|
|
b81c257360 | ||
|
|
64e9956770 | ||
|
|
2385553790 | ||
|
|
4c6a11b8e1 | ||
|
|
1111d5b26b | ||
|
|
44d5524be2 | ||
|
|
1023390f53 | ||
|
|
02199c218d | ||
|
|
46901fb813 | ||
|
|
ef2ddb6f16 | ||
|
|
e3a2e1bbf8 | ||
|
|
8a0a880294 | ||
|
|
6817a56270 | ||
|
|
87860600fb | ||
|
|
377f63085a | ||
|
|
2a7c79dbf5 | ||
|
|
7a8134660f | ||
|
|
863a255c9d | ||
|
|
9b51105afa | ||
|
|
4b56b075c1 | ||
|
|
788303523e | ||
|
|
4b87810fc2 | ||
|
|
ad72e7f29d | ||
|
|
15ec5a898c | ||
|
|
9d9dd381bc | ||
|
|
997df928b7 | ||
|
|
6d145ac93a | ||
|
|
1280add047 | ||
|
|
c53556b88d | ||
|
|
60a53c75a6 | ||
|
|
ca8281f229 | ||
|
|
af7db5195b | ||
|
|
c2138e9733 | ||
|
|
31c1c69901 | ||
|
|
b7054495a7 | ||
|
|
fc03c35bbd | ||
|
|
8320232f38 | ||
|
|
52838a7834 | ||
|
|
87eae6213e | ||
|
|
5d8b950b7f | ||
|
|
7b54476d71 | ||
|
|
13ddce2381 | ||
|
|
be30eb22a6 | ||
|
|
589138ea71 | ||
|
|
f7a9beaefb | ||
|
|
214793f697 | ||
|
|
c36186ce65 | ||
|
|
e6a063ee47 | ||
|
|
723e414814 | ||
|
|
a00b516f9a | ||
|
|
31421cb6d7 | ||
|
|
a6f2af23fb | ||
|
|
ffa1bf726d | ||
|
|
199f0314b9 | ||
|
|
87df85f12d | ||
|
|
ae327ef435 | ||
|
|
1736242bd7 | ||
|
|
1a319056fc | ||
|
|
ea7a997afc | ||
|
|
2c7613c15c | ||
|
|
ab637e028b | ||
|
|
1934f515ef | ||
|
|
d9f0a28da2 | ||
|
|
ca88c02507 | ||
|
|
ea4a3adfd6 | ||
|
|
c4c966683c | ||
|
|
469830fffa | ||
|
|
f9720a38fe | ||
|
|
28b2859718 | ||
|
|
5e77eb23eb | ||
|
|
3bcd3317ad | ||
|
|
4af052b098 | ||
|
|
ac3d8800fd | ||
|
|
2951c3ace9 | ||
|
|
17e1106584 | ||
|
|
7e2d4daa68 | ||
|
|
0cfc4a49ea | ||
|
|
da1996b5f5 | ||
|
|
345b8391c4 | ||
|
|
b1595d3f61 | ||
|
|
c294a417bd | ||
|
|
bc5a5a63f2 | ||
|
|
655afba17d | ||
|
|
212f4b4d64 | ||
|
|
60906f7a46 | ||
|
|
72afbbbd71 | ||
|
|
583c736f0c | ||
|
|
8a0a18e9b3 | ||
|
|
bbc61c85ac | ||
|
|
02511d1564 | ||
|
|
f78408df77 | ||
|
|
f2175146a9 | ||
|
|
d8bc40271a | ||
|
|
fe27f3cc7d | ||
|
|
3aa0e4a526 | ||
|
|
ee65ca10f4 | ||
|
|
afbeb056f0 | ||
|
|
242b67de17 | ||
|
|
ed1095565b | ||
|
|
5373a63008 | ||
|
|
e6e86fa462 | ||
|
|
5a1ce3c45c | ||
|
|
67279418ff | ||
|
|
1f1c24fe15 | ||
|
|
7803bca335 | ||
|
|
c610c903f6 | ||
|
|
36e7cc1b9c | ||
|
|
d69c9da656 | ||
|
|
a113c4c6d3 | ||
|
|
9e3cd03721 | ||
|
|
0e5f2847f1 | ||
|
|
963b944e79 | ||
|
|
55cd3d8bf3 | ||
|
|
150ad0172f | ||
|
|
9d8645b45d | ||
|
|
bbd265184d | ||
|
|
3279cfa28b | ||
|
|
5fb4a100c9 | ||
|
|
71b4908233 | ||
|
|
77d2f6adde | ||
|
|
51688270ac | ||
|
|
19f3e06ab0 | ||
|
|
d2dc797779 | ||
|
|
56aa9962fc | ||
|
|
8e41ef5776 | ||
|
|
331bd84ef4 | ||
|
|
4ac3445056 | ||
|
|
c93fbb0e57 | ||
|
|
ba0100e057 | ||
|
|
37c47155e0 |
7
.coveragerc
Normal file
7
.coveragerc
Normal file
@@ -0,0 +1,7 @@
|
||||
[run]
|
||||
omit =
|
||||
# standlonetemplate is read dynamically and tested by test_genscript
|
||||
*standalonetemplate.py
|
||||
# oldinterpret could be removed, as it is no longer used in py26+
|
||||
*oldinterpret.py
|
||||
vendored_packages
|
||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
CHANGELOG merge=union
|
||||
14
.hgignore → .gitignore
vendored
14
.hgignore → .gitignore
vendored
@@ -1,28 +1,34 @@
|
||||
|
||||
# Automatically generated by `hgimportsvn`
|
||||
syntax:glob
|
||||
.svn
|
||||
.hgsvn
|
||||
|
||||
# Ignore local virtualenvs
|
||||
lib/
|
||||
bin/
|
||||
include/
|
||||
.Python/
|
||||
|
||||
# These lines are suggested according to the svn:ignore property
|
||||
# Feel free to enable them by uncommenting them
|
||||
syntax:glob
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.swp
|
||||
*.html
|
||||
*.class
|
||||
*.orig
|
||||
*~
|
||||
|
||||
.eggs/
|
||||
|
||||
doc/*/_build
|
||||
build/
|
||||
dist/
|
||||
*.egg-info
|
||||
issue/
|
||||
env/
|
||||
.env/
|
||||
3rdparty/
|
||||
.tox
|
||||
.cache
|
||||
.coverage
|
||||
.ropeproject
|
||||
.idea
|
||||
56
.hgtags
56
.hgtags
@@ -1,56 +0,0 @@
|
||||
52c6d9e78777a5a34e813123997dfc614a1a4767 1.0.0b3
|
||||
1c7aaa8c61f3b0945921a9acc7beb184201aed4b 1.0.0b4
|
||||
1c7aaa8c61f3b0945921a9acc7beb184201aed4b 1.0.0b4
|
||||
0000000000000000000000000000000000000000 1.0.0b4
|
||||
0000000000000000000000000000000000000000 1.0.0b4
|
||||
8cd6eb91eba313b012d6e568f37d844dc0751f2e 1.0.0b4
|
||||
8cd6eb91eba313b012d6e568f37d844dc0751f2e 1.0.0b4
|
||||
0000000000000000000000000000000000000000 1.0.0b4
|
||||
2cc0507f117ffe721dff7ee026648cfce00ec92f 1.0.0b6
|
||||
86f1e1b6e49bf5882a809f11edd1dbb08162cdad 1.0.0b8
|
||||
86f1e1b6e49bf5882a809f11edd1dbb08162cdad 1.0.0b8
|
||||
c63f35c266cbb26dad6b87b5e115d65685adf448 1.0.0b8
|
||||
c63f35c266cbb26dad6b87b5e115d65685adf448 1.0.0b8
|
||||
0eaa0fdf2ba0163cf534dc2eff4ba2e5fc66c261 1.0.0b8
|
||||
e2a60653cb490aeed81bbbd83c070b99401c211c 1.0.0b9
|
||||
5ea0cdf7854c3d4278d36eda94a2b68483a0e211 1.0.0
|
||||
5ea0cdf7854c3d4278d36eda94a2b68483a0e211 1.0.0
|
||||
7acde360d94b6a2690ce3d03ff39301da84c0a2b 1.0.0
|
||||
6bd221981ac99103002c1cb94fede400d23a96a1 1.0.1
|
||||
4816e8b80602a3fd3a0a120333ad85fbe7d8bab4 1.0.2
|
||||
60c44bdbf093285dc69d5462d4dbb4acad325ca6 1.1.0
|
||||
319187fcda66714c5eb1353492babeec3d3c826f 1.1.1
|
||||
4fc5212f7626a56b9eb6437b5c673f56dd7eb942 1.2.0
|
||||
c143a8c8840a1c68570890c8ac6165bbf92fd3c6 1.2.1
|
||||
eafd3c256e8732dfb0a4d49d051b5b4339858926 1.3.0
|
||||
d5eacf390af74553227122b85e20345d47b2f9e6 1.3.1
|
||||
d5eacf390af74553227122b85e20345d47b2f9e6 1.3.1
|
||||
8b8e7c25a13cf863f01b2dd955978285ae9daf6a 1.3.1
|
||||
3bff44b188a7ec1af328d977b9d39b6757bb38df 1.3.2
|
||||
c59d3fa8681a5b5966b8375b16fccd64a3a8dbeb 1.3.3
|
||||
79ef6377705184c55633d456832eea318fedcf61 1.3.4
|
||||
79ef6377705184c55633d456832eea318fedcf61 1.3.4
|
||||
90fffd35373e9f125af233f78b19416f0938d841 1.3.4
|
||||
e9e127acd6f0497324ef7f40cfb997cad4c4cd17 2.0.0
|
||||
e4497c2aed358c1988cf7be83ca9394c3c707fa2 2.0.1
|
||||
84e5c54b72448194a0f6f815da7e048ac8019d50 2.0.2
|
||||
2ef82d82daacb72733a3a532a95c5a37164e5819 2.0.3
|
||||
2ef82d82daacb72733a3a532a95c5a37164e5819 2.0.3
|
||||
c777dcad166548b7499564cb49ae5c8b4b07f935 2.0.3
|
||||
c777dcad166548b7499564cb49ae5c8b4b07f935 2.0.3
|
||||
49f11dbff725acdcc5fe3657cbcdf9ae04e25bbc 2.0.3
|
||||
49f11dbff725acdcc5fe3657cbcdf9ae04e25bbc 2.0.3
|
||||
363e5a5a59c803e6bc176a6f9cc4bf1a1ca2dab0 2.0.3
|
||||
e5e1746a197f0398356a43fbe2eebac9690f795d 2.1.0
|
||||
5864412c6f3c903384243bd315639d101d7ebc67 2.1.2
|
||||
12a05d59249f80276e25fd8b96e8e545b1332b7a 2.1.3
|
||||
1522710369337d96bf9568569d5f0ca9b38a74e0 2.2.0
|
||||
3da8cec6c5326ed27c144c9b6d7a64a648370005 2.2.1
|
||||
92b916483c1e65a80dc80e3f7816b39e84b36a4d 2.2.2
|
||||
3c11c5c9776f3c678719161e96cc0a08169c1cb8 2.2.3
|
||||
ad9fe504a371ad8eb613052d58f229aa66f53527 2.2.4
|
||||
c27a60097767c16a54ae56d9669a77925b213b9b 2.3.0
|
||||
acf0e1477fb19a1d35a4e40242b77fa6af32eb17 2.3.1
|
||||
8738b828dec53937765db71951ef955cca4c51f6 2.3.2
|
||||
7fe44182c434f8ac89149a3c340479872a5d5ccb 2.3.3
|
||||
ef299e57f24218dbdd949498d7e660723636bcc3 2.3.4
|
||||
40
.travis.yml
Normal file
40
.travis.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
sudo: false
|
||||
language: python
|
||||
python:
|
||||
- '3.5'
|
||||
# command to install dependencies
|
||||
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
|
||||
|
||||
script: tox --recreate -e $TESTENV
|
||||
|
||||
notifications:
|
||||
irc:
|
||||
channels:
|
||||
- "chat.freenode.net#pytest"
|
||||
on_success: change
|
||||
on_failure: change
|
||||
skip_join: true
|
||||
email:
|
||||
- pytest-commit@python.org
|
||||
87
AUTHORS
87
AUTHORS
@@ -1,27 +1,70 @@
|
||||
Holger Krekel, holger at merlinux eu
|
||||
merlinux GmbH, Germany, office at merlinux eu
|
||||
|
||||
Contributors include::
|
||||
Contributors include::
|
||||
|
||||
Ronny Pfannschmidt
|
||||
Benjamin Peterson
|
||||
Floris Bruynooghe
|
||||
Samuele Pedroni
|
||||
Carl Friedrich Bolz
|
||||
Armin Rigo
|
||||
Maciek Fijalkowski
|
||||
Guido Wesdorp
|
||||
Brian Dorsey
|
||||
Ross Lawley
|
||||
Ralf Schmitt
|
||||
Chris Lamb
|
||||
Harald Armin Massa
|
||||
Martijn Faassen
|
||||
Ian Bicking
|
||||
Jan Balster
|
||||
Grig Gheorghiu
|
||||
Bob Ippolito
|
||||
Christian Tismer
|
||||
Daniel Nuri
|
||||
Graham Horler
|
||||
Abhijeet Kasurde
|
||||
Anatoly Bubenkoff
|
||||
Andreas Zeidler
|
||||
Andy Freeland
|
||||
Anthon van der Neut
|
||||
Armin Rigo
|
||||
Aron Curzon
|
||||
Benjamin Peterson
|
||||
Bob Ippolito
|
||||
Brian Dorsey
|
||||
Brian Okken
|
||||
Brianna Laugher
|
||||
Bruno Oliveira
|
||||
Carl Friedrich Bolz
|
||||
Charles Cloud
|
||||
Chris Lamb
|
||||
Christian Theunert
|
||||
Christian Tismer
|
||||
Christopher Gilling
|
||||
Daniel Grana
|
||||
Daniel Nuri
|
||||
Dave Hunt
|
||||
David Mohr
|
||||
Edison Gustavo Muenz
|
||||
Eduardo Schettino
|
||||
Elizaveta Shashkova
|
||||
Eric Hunsberger
|
||||
Eric Siegerman
|
||||
Florian Bruhin
|
||||
Floris Bruynooghe
|
||||
Graham Horler
|
||||
Grig Gheorghiu
|
||||
Guido Wesdorp
|
||||
Harald Armin Massa
|
||||
Ian Bicking
|
||||
Jaap Broekhuizen
|
||||
Jan Balster
|
||||
Janne Vanhala
|
||||
Jason R. Coombs
|
||||
Jurko Gospodnetić
|
||||
Katarzyna Jachim
|
||||
Kevin Cox
|
||||
Maciek Fijalkowski
|
||||
Maho
|
||||
Marc Schlaich
|
||||
Mark Abramowitz
|
||||
Markus Unterwaditzer
|
||||
Martijn Faassen
|
||||
Michael Droettboom
|
||||
Nicolas Delaby
|
||||
Pieter Mulder
|
||||
Piotr Banaszkiewicz
|
||||
Punyashloka Biswal
|
||||
Ralf Schmitt
|
||||
Ronny Pfannschmidt
|
||||
Ross Lawley
|
||||
Samuele Pedroni
|
||||
Tom Viner
|
||||
Trevor Bekolay
|
||||
Wouter van Ackooy
|
||||
David Díaz-Barquero
|
||||
Eric Hunsberger
|
||||
Simon Gomizelj
|
||||
Russel Winder
|
||||
Ben Webb
|
||||
|
||||
221
CONTRIBUTING.rst
Normal file
221
CONTRIBUTING.rst
Normal file
@@ -0,0 +1,221 @@
|
||||
============================
|
||||
Contribution getting started
|
||||
============================
|
||||
|
||||
Contributions are highly welcomed and appreciated. Every little help counts,
|
||||
so do not hesitate!
|
||||
|
||||
.. contents:: Contribution links
|
||||
:depth: 2
|
||||
|
||||
|
||||
.. _submitplugin:
|
||||
|
||||
Submit a plugin, co-develop pytest
|
||||
----------------------------------
|
||||
|
||||
Pytest development of the core, some plugins and support code happens
|
||||
in repositories living under:
|
||||
|
||||
- `the pytest-dev github organisation <https://github.com/pytest-dev>`_
|
||||
|
||||
- `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.
|
||||
|
||||
|
||||
.. _reportbugs:
|
||||
|
||||
Report bugs
|
||||
-----------
|
||||
|
||||
Report bugs for pytest at https://github.com/pytest-dev/pytest/issues
|
||||
|
||||
If you are reporting a bug, please include:
|
||||
|
||||
* Your operating system name and version.
|
||||
* Any details about your local setup that might be helpful in troubleshooting,
|
||||
specifically Python interpreter version,
|
||||
installed libraries and pytest version.
|
||||
* Detailed steps to reproduce the bug.
|
||||
|
||||
If you can write a demonstration test that currently fails but should pass (xfail),
|
||||
that is a very useful commit to make as well, even if you can't find how
|
||||
to fix the bug yet.
|
||||
|
||||
.. _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:
|
||||
https://github.com/pytest-dev/pytest/labels/bug
|
||||
|
||||
:ref:`Talk <contact>` to developers to find out how you can fix specific bugs.
|
||||
|
||||
Don't forget to check the issue trackers of your favourite plugins, too!
|
||||
|
||||
.. _writeplugins:
|
||||
|
||||
Implement features
|
||||
------------------
|
||||
|
||||
Look through the GitHub issues for enhancements. Here is sample 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
|
||||
features.
|
||||
|
||||
Write documentation
|
||||
-------------------
|
||||
|
||||
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's never too much 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.
|
||||
|
||||
.. _`pull requests`:
|
||||
.. _pull-requests:
|
||||
|
||||
Preparing Pull Requests on GitHub
|
||||
---------------------------------
|
||||
|
||||
.. note::
|
||||
What is a "pull request"? It informs project's core developers about the
|
||||
changes you want to review and merge. Pull requests are stored on
|
||||
`GitHub servers <https://github.com/pytest-dev/pytest/pulls>`_.
|
||||
Once you send pull request, we can discuss it's potential modifications and
|
||||
even add more commits to it later on.
|
||||
|
||||
There's an excellent tutorial on how Pull Requests work in the
|
||||
`GitHub Help Center <https://help.github.com/articles/using-pull-requests/>`_,
|
||||
but here is a simple overview:
|
||||
|
||||
#. Fork the
|
||||
`pytest GitHub repository <https://github.com/pytest-dev/pytest>`__. It's
|
||||
fine to use ``pytest`` as your fork repository name because it will live
|
||||
under your user.
|
||||
|
||||
#. Clone your fork locally using `git <https://git-scm.com/>`_ and create a branch::
|
||||
|
||||
$ git clone git@github.com:YOUR_GITHUB_USERNAME/pytest.git
|
||||
$ cd pytest
|
||||
# now, to fix a bug create your own branch off "master":
|
||||
|
||||
$ git checkout -b your-bugfix-branch-name master
|
||||
|
||||
# or to instead add a feature create your own branch off "features":
|
||||
|
||||
$ git checkout -b your-feature-branch-name features
|
||||
|
||||
Given we have "major.minor.micro" version numbers, bugfixes will usually
|
||||
be released in micro releases whereas features will be released in
|
||||
minor releases and incompatible changes in major releases.
|
||||
|
||||
If you need some help with Git, follow this quick start
|
||||
guide: https://git.wiki.kernel.org/index.php/QuickStart
|
||||
|
||||
#. Install tox
|
||||
|
||||
Tox is used to run all the tests and will automatically setup virtualenvs
|
||||
to run the tests in.
|
||||
(will implicitly use http://www.virtualenv.org/en/latest/)::
|
||||
|
||||
$ pip install tox
|
||||
|
||||
#. Run all the tests
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
#. 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::
|
||||
|
||||
$ python runtox.py -e py27 -- --pdb
|
||||
|
||||
or to only run tests in a particular test module on py35::
|
||||
|
||||
$ python runtox.py -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.
|
||||
|
||||
#. Finally, submit a pull request through the GitHub website using this data::
|
||||
|
||||
head-fork: YOUR_GITHUB_USERNAME/pytest
|
||||
compare: your-branch-name
|
||||
|
||||
base-fork: pytest-dev/pytest
|
||||
base: master # if it's a bugfix
|
||||
base: feature # if it's a feature
|
||||
|
||||
|
||||
88
HOWTORELEASE.rst
Normal file
88
HOWTORELEASE.rst
Normal file
@@ -0,0 +1,88 @@
|
||||
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)
|
||||
|
||||
2. Check and finalize CHANGELOG
|
||||
|
||||
3. Write doc/en/announce/release-VERSION.txt and include
|
||||
it in doc/en/announce/index.txt
|
||||
|
||||
4. 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::
|
||||
|
||||
devpi use https://devpi.net/USER/dev
|
||||
devpi test pytest==VERSION
|
||||
|
||||
6. 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::
|
||||
|
||||
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
|
||||
|
||||
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>`_.
|
||||
|
||||
|
||||
12. Send release announcement to mailing lists:
|
||||
|
||||
- pytest-dev
|
||||
- testing-in-python
|
||||
- python-announce-list@python.org
|
||||
|
||||
|
||||
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``).
|
||||
126
ISSUES.txt
126
ISSUES.txt
@@ -1,10 +1,22 @@
|
||||
|
||||
improve / add to dependency/test resource injection
|
||||
|
||||
recorder = monkeypatch.function(".......")
|
||||
-------------------------------------------------------------
|
||||
tags: wish feature docs
|
||||
tags: nice feature
|
||||
|
||||
write up better examples showing the connection between
|
||||
the two.
|
||||
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
|
||||
-------------------------------------------------------------
|
||||
@@ -37,7 +49,7 @@ 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 neccessary
|
||||
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:
|
||||
|
||||
@@ -65,16 +77,7 @@ 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 ommitted?
|
||||
|
||||
checks / deprecations for next release
|
||||
---------------------------------------------------------------
|
||||
tags: bug 2.4 core xdist
|
||||
|
||||
* check oejskit plugin compatibility
|
||||
* move pytest_nose out of pylib because it implicitely extends
|
||||
the protocol now - setup/teardown is called at module level.
|
||||
consider making calling of setup/teardown configurable
|
||||
maybe the setupfunc could be omitted?
|
||||
|
||||
optimizations
|
||||
---------------------------------------------------------------
|
||||
@@ -94,20 +97,6 @@ appropriately to avoid this issue. Moreover/Alternatively, we could
|
||||
record which implementations of a hook succeeded and only call their
|
||||
teardown.
|
||||
|
||||
consider and document __init__ file usage in test directories
|
||||
---------------------------------------------------------------
|
||||
tags: bug core
|
||||
|
||||
Currently, a test module is imported with its fully qualified
|
||||
package path, determined by checking __init__ files upwards.
|
||||
This has the side effect that a source package at the root
|
||||
of the test dir could be imported as well. This is somewhat
|
||||
convenient but complicates the picture for running tests against
|
||||
different versions of a package. Also, implicit sys.path
|
||||
manipulations are problematic per-se. Maybe factorting out
|
||||
a pytest_addsyspath hook which can be disabled from the command line
|
||||
makes sense. In any case documentation/recommendations for
|
||||
certain scenarios makes sense.
|
||||
|
||||
relax requirement to have tests/testing contain an __init__
|
||||
----------------------------------------------------------------
|
||||
@@ -124,8 +113,8 @@ customize test function collection
|
||||
-------------------------------------------------------
|
||||
tags: feature
|
||||
|
||||
- introduce py.test.mark.nocollect for not considering a function for
|
||||
test collection at all. maybe also introduce a py.test.mark.test to
|
||||
- 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.
|
||||
|
||||
@@ -137,18 +126,18 @@ in addition to the imperative pytest.importorskip also introduce
|
||||
a pytest.mark.importorskip so that the test count is more correct.
|
||||
|
||||
|
||||
introduce py.test.mark.platform
|
||||
introduce pytest.mark.platform
|
||||
-------------------------------------------------------
|
||||
tags: feature
|
||||
|
||||
Introduce nice-to-spell platform-skipping, examples:
|
||||
|
||||
@py.test.mark.platform("python3")
|
||||
@py.test.mark.platform("not python3")
|
||||
@py.test.mark.platform("win32 and not python3")
|
||||
@py.test.mark.platform("darwin")
|
||||
@py.test.mark.platform("not (jython and win32)")
|
||||
@py.test.mark.platform("not (jython and win32)", xfail=True)
|
||||
@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
|
||||
@@ -183,8 +172,8 @@ tags: feature
|
||||
allow to name conftest.py files (in sub directories) that should
|
||||
be imported early, as to include command line options.
|
||||
|
||||
improve central py.test ini file
|
||||
----------------------------------
|
||||
improve central pytest ini file
|
||||
-------------------------------
|
||||
tags: feature
|
||||
|
||||
introduce more declarative configuration options:
|
||||
@@ -198,7 +187,7 @@ new documentation
|
||||
----------------------------------
|
||||
tags: feature
|
||||
|
||||
- logo py.test
|
||||
- logo pytest
|
||||
- examples for unittest or functional testing
|
||||
- resource management for functional testing
|
||||
- patterns: page object
|
||||
@@ -207,47 +196,25 @@ have imported module mismatch honour relative paths
|
||||
--------------------------------------------------------
|
||||
tags: bug
|
||||
|
||||
With 1.1.1 py.test fails at least on windows if an import
|
||||
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: py.test.ensuretemp and config
|
||||
consider globals: pytest.ensuretemp and config
|
||||
--------------------------------------------------------------
|
||||
tags: experimental-wish
|
||||
|
||||
consider deprecating py.test.ensuretemp and py.test.config
|
||||
to further reduce py.test globality. Also consider
|
||||
having py.test.config and ensuretemp coming from
|
||||
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 allowing funcargs for setup methods
|
||||
--------------------------------------------------------------
|
||||
tags: experimental-wish
|
||||
|
||||
Users have expressed the wish to have funcargs available to setup
|
||||
functions. Experiment with allowing funcargs there - it might
|
||||
also help to make the py.test.ensuretemp and config deprecation.
|
||||
For filling funcargs for setup methods, we could call funcarg
|
||||
factories with a request object that not have a cls/function
|
||||
attributes. However, how to handle parametrized test functions
|
||||
and funcargs?
|
||||
|
||||
maybe introduce a setup method like:
|
||||
|
||||
setup_invocation(self, request)
|
||||
|
||||
which has full access to the test invocation through "request"
|
||||
through which you can get funcargvalues, use cached_setup etc.
|
||||
Therefore, the access to funcargs would be indirect but it
|
||||
could be consistently implemented. setup_invocation() would
|
||||
be a "glue" function for bringing together the xUnit and funcargs
|
||||
world.
|
||||
|
||||
consider pytest_addsyspath hook
|
||||
-----------------------------------------
|
||||
tags:
|
||||
tags: wish
|
||||
|
||||
py.test could call a new pytest_addsyspath() in order to systematically
|
||||
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.
|
||||
|
||||
@@ -255,21 +222,14 @@ Alternatively it could also be done via the config object
|
||||
and pytest_configure.
|
||||
|
||||
|
||||
show plugin information in test header
|
||||
|
||||
deprecate global pytest.config usage
|
||||
----------------------------------------------------------------
|
||||
tags: feature
|
||||
|
||||
Now that external plugins are becoming more numerous
|
||||
it would be useful to have external plugins along with
|
||||
their versions displayed as a header line.
|
||||
|
||||
deprecate global py.test.config usage
|
||||
----------------------------------------------------------------
|
||||
tags: feature
|
||||
|
||||
py.test.ensuretemp and py.test.config are probably the last
|
||||
pytest.ensuretemp and pytest.config are probably the last
|
||||
objects containing global state. Often using them is not
|
||||
neccessary. This is about trying to get rid of them, i.e.
|
||||
necessary. This is about trying to get rid of them, i.e.
|
||||
deprecating them and checking with PyPy's usages as well
|
||||
as others.
|
||||
|
||||
@@ -338,7 +298,7 @@ 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 direcroty, file
|
||||
then just importing however, e.g. current working directory, file
|
||||
descriptors, ...
|
||||
|
||||
This would probably be done by marking::
|
||||
@@ -397,7 +357,7 @@ a few use-cases come to mind:
|
||||
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 loer level api and taking custom markers into account
|
||||
# there might be need for a lower level api and taking custom markers into account
|
||||
with pytest.section(id, bubble=False):
|
||||
call()
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
include CHANGELOG
|
||||
include README.rst
|
||||
include setup.py
|
||||
include distribute_setup.py
|
||||
include tox.ini
|
||||
include LICENSE
|
||||
graft doc
|
||||
|
||||
46
README.rst
46
README.rst
@@ -1,6 +1,27 @@
|
||||
======
|
||||
pytest
|
||||
======
|
||||
|
||||
The ``py.test`` testing tool makes it easy to write small tests, yet
|
||||
scales to support complex functional testing. It provides
|
||||
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
|
||||
:target: https://pypi.python.org/pypi/pytest
|
||||
.. image:: http://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/
|
||||
|
||||
Changelog: http://pytest.org/latest/changelog.html
|
||||
|
||||
Issues: https://github.com/pytest-dev/pytest/issues
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `auto-discovery
|
||||
<http://pytest.org/latest/goodpractises.html#python-test-discovery>`_
|
||||
@@ -8,15 +29,18 @@ scales to support complex functional testing. It provides
|
||||
- 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 ``py.test`` to run test suites based
|
||||
- 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 to Python2.4 all the way up to Python3.3,
|
||||
PyPy-1.9 and Jython-2.5.1.
|
||||
- single-source compatibility from Python2.6 all the way up to
|
||||
Python3.5, PyPy-2.3, (jython-2.5 untested)
|
||||
|
||||
|
||||
- many `external plugins <http://pytest.org/latest/plugins.html#installing-external-plugins-searching>`_.
|
||||
|
||||
A simple example for a test::
|
||||
A simple example for a test:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# content of test_module.py
|
||||
def test_function():
|
||||
@@ -31,6 +55,12 @@ For much more info, including PDF docs, see
|
||||
|
||||
and report bugs at:
|
||||
|
||||
http://bitbucket.org/hpk42/pytest/issues/
|
||||
https://github.com/pytest-dev/pytest/issues
|
||||
|
||||
Copyright Holger Krekel and others, 2004-2012
|
||||
and checkout or fork repo at:
|
||||
|
||||
https://github.com/pytest-dev/pytest
|
||||
|
||||
|
||||
Copyright Holger Krekel and others, 2004-2015
|
||||
Licensed under the MIT license.
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#
|
||||
__version__ = '2.3.5'
|
||||
__version__ = '2.8.2'
|
||||
|
||||
104
_pytest/_argcomplete.py
Normal file
104
_pytest/_argcomplete.py
Normal file
@@ -0,0 +1,104 @@
|
||||
|
||||
"""allow bash-completion for argparse with argcomplete if installed
|
||||
needs argcomplete>=0.5.6 for python 3.2/3.3 (older versions fail
|
||||
to find the magic string, so _ARGCOMPLETE env. var is never set, and
|
||||
this does not need special code.
|
||||
|
||||
argcomplete does not support python 2.5 (although the changes for that
|
||||
are minor).
|
||||
|
||||
Function try_argcomplete(parser) should be called directly before
|
||||
the call to ArgumentParser.parse_args().
|
||||
|
||||
The filescompleter is what you normally would use on the positional
|
||||
arguments specification, in order to get "dirname/" after "dirn<TAB>"
|
||||
instead of the default "dirname ":
|
||||
|
||||
optparser.add_argument(Config._file_or_dir, nargs='*'
|
||||
).completer=filescompleter
|
||||
|
||||
Other, application specific, completers should go in the file
|
||||
doing the add_argument calls as they need to be specified as .completer
|
||||
attributes as well. (If argcomplete is not installed, the function the
|
||||
attribute points to will not be used).
|
||||
|
||||
SPEEDUP
|
||||
=======
|
||||
The generic argcomplete script for bash-completion
|
||||
(/etc/bash_completion.d/python-argcomplete.sh )
|
||||
uses a python program to determine startup script generated by pip.
|
||||
You can speed up completion somewhat by changing this script to include
|
||||
# PYTHON_ARGCOMPLETE_OK
|
||||
so the the python-argcomplete-check-easy-install-script does not
|
||||
need to be called to find the entry point of the code and see if that is
|
||||
marked with PYTHON_ARGCOMPLETE_OK
|
||||
|
||||
INSTALL/DEBUGGING
|
||||
=================
|
||||
To include this support in another application that has setup.py generated
|
||||
scripts:
|
||||
- add the line:
|
||||
# PYTHON_ARGCOMPLETE_OK
|
||||
near the top of the main python entry point
|
||||
- include in the file calling parse_args():
|
||||
from _argcomplete import try_argcomplete, filescompleter
|
||||
, call try_argcomplete just before parse_args(), and optionally add
|
||||
filescompleter to the positional arguments' add_argument()
|
||||
If things do not work right away:
|
||||
- switch on argcomplete debugging with (also helpful when doing custom
|
||||
completers):
|
||||
export _ARC_DEBUG=1
|
||||
- run:
|
||||
python-argcomplete-check-easy-install-script $(which appname)
|
||||
echo $?
|
||||
will echo 0 if the magic line has been found, 1 if not
|
||||
- sometimes it helps to find early on errors using:
|
||||
_ARGCOMPLETE=1 _ARC_DEBUG=1 appname
|
||||
which should throw a KeyError: 'COMPLINE' (which is properly set by the
|
||||
global argcomplete script).
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from glob import glob
|
||||
|
||||
class FastFilesCompleter:
|
||||
'Fast file completer class'
|
||||
def __init__(self, directories=True):
|
||||
self.directories = directories
|
||||
|
||||
def __call__(self, prefix, **kwargs):
|
||||
"""only called on non option completions"""
|
||||
if os.path.sep in prefix[1:]: #
|
||||
prefix_dir = len(os.path.dirname(prefix) + os.path.sep)
|
||||
else:
|
||||
prefix_dir = 0
|
||||
completion = []
|
||||
globbed = []
|
||||
if '*' not in prefix and '?' not in prefix:
|
||||
if prefix[-1] == os.path.sep: # we are on unix, otherwise no bash
|
||||
globbed.extend(glob(prefix + '.*'))
|
||||
prefix += '*'
|
||||
globbed.extend(glob(prefix))
|
||||
for x in sorted(globbed):
|
||||
if os.path.isdir(x):
|
||||
x += '/'
|
||||
# append stripping the prefix (like bash, not like compgen)
|
||||
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:
|
||||
sys.exit(-1)
|
||||
filescompleter = FastFilesCompleter()
|
||||
|
||||
def try_argcomplete(parser):
|
||||
argcomplete.autocomplete(parser)
|
||||
else:
|
||||
def try_argcomplete(parser): pass
|
||||
filescompleter = None
|
||||
11
_pytest/_pluggy.py
Normal file
11
_pytest/_pluggy.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
imports symbols from vendored "pluggy" if available, otherwise
|
||||
falls back to importing "pluggy" from the default namespace.
|
||||
"""
|
||||
|
||||
try:
|
||||
from _pytest.vendored_packages.pluggy import * # noqa
|
||||
from _pytest.vendored_packages.pluggy import __version__ # noqa
|
||||
except ImportError:
|
||||
from pluggy import * # noqa
|
||||
from pluggy import __version__ # noqa
|
||||
@@ -3,24 +3,35 @@ support for presenting detailed information in failing assertions.
|
||||
"""
|
||||
import py
|
||||
import sys
|
||||
import pytest
|
||||
from _pytest.monkeypatch import monkeypatch
|
||||
from _pytest.assertion import util
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("debugconfig")
|
||||
group.addoption('--assert', action="store", dest="assertmode",
|
||||
group.addoption('--assert',
|
||||
action="store",
|
||||
dest="assertmode",
|
||||
choices=("rewrite", "reinterp", "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', action="store_true", default=False,
|
||||
dest="nomagic", help="DEPRECATED equivalent to --assert=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")
|
||||
|
||||
|
||||
class AssertionState:
|
||||
"""State for the assertion plugin."""
|
||||
@@ -29,40 +40,42 @@ class AssertionState:
|
||||
self.mode = mode
|
||||
self.trace = config.trace.root.get("assertion")
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
mode = config.getvalue("assertmode")
|
||||
if config.getvalue("noassert") or config.getvalue("nomagic"):
|
||||
mode = "plain"
|
||||
if mode == "rewrite":
|
||||
try:
|
||||
import ast
|
||||
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)):
|
||||
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)
|
||||
reinterpret.AssertionError) # noqa
|
||||
hook = None
|
||||
if mode == "rewrite":
|
||||
hook = rewrite.AssertionRewritingHook()
|
||||
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 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)
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
hook = config._assertstate.hook
|
||||
if hook is not None:
|
||||
sys.meta_path.remove(hook)
|
||||
|
||||
def pytest_collection(session):
|
||||
# this hook is only called when test modules are collected
|
||||
@@ -72,39 +85,65 @@ def pytest_collection(session):
|
||||
if hook is not None:
|
||||
hook.set_session(session)
|
||||
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
"""Setup the pytest_assertrepr_compare hook
|
||||
|
||||
The newinterpret and rewrite modules will use util._reprcompare if
|
||||
it exists to use custom reporting via the
|
||||
pytest_assertrepr_compare hook. This sets up this custom
|
||||
comparison for the test.
|
||||
"""
|
||||
def callbinrepr(op, left, right):
|
||||
"""Call the pytest_assertrepr_compare hook and prepare the result
|
||||
|
||||
This uses the first result from the hook and then ensures the
|
||||
following:
|
||||
* Overly verbose explanations are dropped unles -vv was used.
|
||||
* Embedded newlines are escaped to help util.format_explanation()
|
||||
later.
|
||||
* If the rewrite mode is used embedded %-characters are replaced
|
||||
to protect later % formatting.
|
||||
|
||||
The result can be formatted by util.format_explanation() for
|
||||
pretty printing.
|
||||
"""
|
||||
hook_result = item.ihook.pytest_assertrepr_compare(
|
||||
config=item.config, op=op, left=left, right=right)
|
||||
|
||||
for new_expl in hook_result:
|
||||
if new_expl:
|
||||
# Don't include pageloads of data unless we are very verbose (-vv)
|
||||
if len(''.join(new_expl[1:])) > 80*8 and item.config.option.verbose < 2:
|
||||
new_expl[1:] = ['Detailed information truncated, use "-vv" to see']
|
||||
res = '\n~'.join(new_expl)
|
||||
if (sum(len(p) for p in new_expl[1:]) > 80*8
|
||||
and item.config.option.verbose < 2):
|
||||
show_max = 10
|
||||
truncated_lines = len(new_expl) - show_max
|
||||
new_expl[show_max:] = [py.builtin._totext(
|
||||
'Detailed information truncated (%d more lines)'
|
||||
', use "-vv" to show' % truncated_lines)]
|
||||
new_expl = [line.replace("\n", "\\n") for line in new_expl]
|
||||
res = py.builtin._totext("\n~").join(new_expl)
|
||||
if item.config.getvalue("assertmode") == "rewrite":
|
||||
# The result will be fed back a python % formatting
|
||||
# operation, which will fail if there are extraneous
|
||||
# '%'s in the string. Escape them here.
|
||||
res = res.replace("%", "%%")
|
||||
return res
|
||||
util._reprcompare = callbinrepr
|
||||
|
||||
|
||||
def pytest_runtest_teardown(item):
|
||||
util._reprcompare = None
|
||||
|
||||
|
||||
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
|
||||
from _pytest.assertion import reinterpret # noqa
|
||||
if mode == "rewrite":
|
||||
from _pytest.assertion import rewrite
|
||||
from _pytest.assertion import rewrite # noqa
|
||||
|
||||
|
||||
def warn_about_missing_assertion(mode):
|
||||
try:
|
||||
@@ -119,8 +158,10 @@ def warn_about_missing_assertion(mode):
|
||||
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")
|
||||
" because assert statements are not executed "
|
||||
"by the underlying Python interpreter "
|
||||
"(are you using python -O?)\n")
|
||||
|
||||
|
||||
# Expose this plugin's implementation for the pytest_assertrepr_compare hook
|
||||
pytest_assertrepr_compare = util.assertrepr_compare
|
||||
|
||||
@@ -33,6 +33,12 @@ else:
|
||||
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."""
|
||||
@@ -232,24 +238,38 @@ class DebugInterpreter(ast.NodeVisitor):
|
||||
arguments = []
|
||||
for arg in call.args:
|
||||
arg_explanation, arg_result = self.visit(arg)
|
||||
arg_name = "__exprinfo_%s" % (len(ns),)
|
||||
ns[arg_name] = arg_result
|
||||
arguments.append(arg_name)
|
||||
arg_explanations.append(arg_explanation)
|
||||
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)
|
||||
arg_name = "__exprinfo_%s" % (len(ns),)
|
||||
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
|
||||
keyword_source = "%s=%%s" % (keyword.arg)
|
||||
arguments.append(keyword_source % (arg_name,))
|
||||
arg_explanations.append(keyword_source % (arg_explanation,))
|
||||
if call.starargs:
|
||||
|
||||
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 call.kwargs:
|
||||
|
||||
if getattr(call, 'kwargs', None):
|
||||
arg_explanation, arg_result = self.visit(call.kwargs)
|
||||
arg_name = "__exprinfo_kwds"
|
||||
ns[arg_name] = arg_result
|
||||
@@ -286,7 +306,19 @@ class DebugInterpreter(ast.NodeVisitor):
|
||||
source = "__exprinfo_expr.%s" % (attr.attr,)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
result = self.frame.eval(co, __exprinfo_expr=source_result)
|
||||
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),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import traceback
|
||||
import types
|
||||
import py
|
||||
import sys, inspect
|
||||
from compiler import parse, ast, pycodegen
|
||||
@@ -55,7 +57,7 @@ class View(object):
|
||||
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(self.__obj__, attr)
|
||||
return getattr(object.__getattribute__(self, '__obj__'), attr)
|
||||
|
||||
def __viewkey__(self):
|
||||
return self.__obj__.__class__
|
||||
@@ -355,7 +357,18 @@ class Getattr(Interpretable):
|
||||
expr.eval(frame)
|
||||
source = '__exprinfo_expr.%s' % self.attrname
|
||||
try:
|
||||
self.result = frame.eval(source, __exprinfo_expr=expr.result)
|
||||
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:
|
||||
@@ -466,7 +479,7 @@ def check(s, frame=None):
|
||||
def interpret(source, frame, should_fail=False):
|
||||
module = Interpretable(parse(source, 'exec').node)
|
||||
#print "got module", module
|
||||
if isinstance(frame, py.std.types.FrameType):
|
||||
if isinstance(frame, types.FrameType):
|
||||
frame = py.code.Frame(frame)
|
||||
try:
|
||||
module.run(frame)
|
||||
@@ -476,7 +489,6 @@ def interpret(source, frame, should_fail=False):
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
if should_fail:
|
||||
return ("(assertion failed, but when it was re-run for "
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
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 = str(args[0])
|
||||
except py.builtin._sysex:
|
||||
raise
|
||||
except:
|
||||
self.msg = "<[broken __repr__] %s at %0xd>" %(
|
||||
args[0].__class__, id(args[0]))
|
||||
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:
|
||||
@@ -37,10 +45,8 @@ class AssertionError(BuiltinAssertionError):
|
||||
|
||||
if sys.version_info > (3, 0):
|
||||
AssertionError.__module__ = "builtins"
|
||||
reinterpret_old = "old reinterpretation not available for py3"
|
||||
else:
|
||||
from _pytest.assertion.oldinterpret import interpret as reinterpret_old
|
||||
if sys.version_info >= (2, 6) or (sys.platform.startswith("java")):
|
||||
|
||||
if sys.version_info >= (2, 6) or sys.platform.startswith("java"):
|
||||
from _pytest.assertion.newinterpret import interpret as reinterpret
|
||||
else:
|
||||
reinterpret = reinterpret_old
|
||||
from _pytest.assertion.oldinterpret import interpret as reinterpret
|
||||
|
||||
@@ -15,13 +15,7 @@ import py
|
||||
from _pytest.assertion import util
|
||||
|
||||
|
||||
# Windows gives ENOENT in places *nix gives ENOTDIR.
|
||||
if sys.platform.startswith("win"):
|
||||
PATH_COMPONENT_NOT_DIR = errno.ENOENT
|
||||
else:
|
||||
PATH_COMPONENT_NOT_DIR = errno.ENOTDIR
|
||||
|
||||
# py.test caches rewritten pycs in __pycache__.
|
||||
# pytest caches rewritten pycs in __pycache__.
|
||||
if hasattr(imp, "get_tag"):
|
||||
PYTEST_TAG = imp.get_tag() + "-PYTEST"
|
||||
else:
|
||||
@@ -41,12 +35,19 @@ PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
|
||||
REWRITE_NEWLINES = sys.version_info[:2] != (2, 7) and sys.version_info < (3, 2)
|
||||
ASCII_IS_DEFAULT_ENCODING = sys.version_info[0] < 3
|
||||
|
||||
if sys.version_info >= (3,5):
|
||||
ast_Call = ast.Call
|
||||
else:
|
||||
ast_Call = lambda a,b,c: ast.Call(a, b, c, None, None)
|
||||
|
||||
|
||||
class AssertionRewritingHook(object):
|
||||
"""PEP302 Import hook which rewrites asserts."""
|
||||
|
||||
def __init__(self):
|
||||
self.session = None
|
||||
self.modules = {}
|
||||
self._register_with_pkg_resources()
|
||||
|
||||
def set_session(self, session):
|
||||
self.fnpats = session.config.getini("python_files")
|
||||
@@ -61,8 +62,12 @@ class AssertionRewritingHook(object):
|
||||
names = name.rsplit(".", 1)
|
||||
lastname = names[-1]
|
||||
pth = None
|
||||
if path is not None and len(path) == 1:
|
||||
pth = path[0]
|
||||
if path is not None:
|
||||
# Starting with Python 3.3, path is a _NamespacePath(), which
|
||||
# causes problems if not converted to list.
|
||||
path = list(path)
|
||||
if len(path) == 1:
|
||||
pth = path[0]
|
||||
if pth is None:
|
||||
try:
|
||||
fd, fn, desc = imp.find_module(lastname, path)
|
||||
@@ -103,7 +108,7 @@ class AssertionRewritingHook(object):
|
||||
# the most magical part of the process: load the source, rewrite the
|
||||
# asserts, and load the rewritten source. We also cache the rewritten
|
||||
# module code in a special pyc. We must be aware of the possibility of
|
||||
# concurrent py.test processes rewriting and loading pycs. To avoid
|
||||
# concurrent pytest processes rewriting and loading pycs. To avoid
|
||||
# tricky race conditions, we maintain the following invariant: The
|
||||
# cached pyc is always a complete, valid pyc. Operations on it must be
|
||||
# atomic. POSIX's atomic rename comes in handy.
|
||||
@@ -119,11 +124,11 @@ class AssertionRewritingHook(object):
|
||||
# common case) or it's blocked by a non-dir node. In the
|
||||
# latter case, we'll ignore it in _write_pyc.
|
||||
pass
|
||||
elif e == PATH_COMPONENT_NOT_DIR:
|
||||
elif e in [errno.ENOENT, errno.ENOTDIR]:
|
||||
# One of the path components was not a directory, likely
|
||||
# because we're in a zip file.
|
||||
write = False
|
||||
elif e == errno.EACCES:
|
||||
elif e in [errno.EACCES, errno.EROFS]:
|
||||
state.trace("read only directory: %r" % fn_pypath.dirname)
|
||||
write = False
|
||||
else:
|
||||
@@ -132,21 +137,27 @@ class AssertionRewritingHook(object):
|
||||
pyc = os.path.join(cache_dir, cache_name)
|
||||
# Notice that even if we're in a read-only directory, I'm going
|
||||
# to check for a cached pyc. This may not be optimal...
|
||||
co = _read_pyc(fn_pypath, pyc)
|
||||
co = _read_pyc(fn_pypath, pyc, state.trace)
|
||||
if co is None:
|
||||
state.trace("rewriting %r" % (fn,))
|
||||
co = _rewrite_test(state, fn_pypath)
|
||||
source_stat, co = _rewrite_test(state, fn_pypath)
|
||||
if co is None:
|
||||
# Probably a SyntaxError in the test.
|
||||
return None
|
||||
if write:
|
||||
_make_rewritten_pyc(state, fn_pypath, pyc, co)
|
||||
_make_rewritten_pyc(state, source_stat, pyc, co)
|
||||
else:
|
||||
state.trace("found cached rewritten pyc for %r" % (fn,))
|
||||
self.modules[name] = co, pyc
|
||||
return self
|
||||
|
||||
def load_module(self, name):
|
||||
# If there is an existing module object named 'fullname' in
|
||||
# sys.modules, the loader must use that existing module. (Otherwise,
|
||||
# the reload() builtin will not work correctly.)
|
||||
if name in sys.modules:
|
||||
return sys.modules[name]
|
||||
|
||||
co, pyc = self.modules.pop(name)
|
||||
# I wish I could just call imp.load_compiled here, but __file__ has to
|
||||
# be set properly. In Python 3.2+, this all would be handled correctly
|
||||
@@ -156,31 +167,69 @@ class AssertionRewritingHook(object):
|
||||
mod.__file__ = co.co_filename
|
||||
# Normally, this attribute is 3.2+.
|
||||
mod.__cached__ = pyc
|
||||
mod.__loader__ = self
|
||||
py.builtin.exec_(co, mod.__dict__)
|
||||
except:
|
||||
del sys.modules[name]
|
||||
raise
|
||||
return sys.modules[name]
|
||||
|
||||
def _write_pyc(co, source_path, pyc):
|
||||
|
||||
|
||||
def is_package(self, name):
|
||||
try:
|
||||
fd, fn, desc = imp.find_module(name)
|
||||
except ImportError:
|
||||
return False
|
||||
if fd is not None:
|
||||
fd.close()
|
||||
tp = desc[2]
|
||||
return tp == imp.PKG_DIRECTORY
|
||||
|
||||
@classmethod
|
||||
def _register_with_pkg_resources(cls):
|
||||
"""
|
||||
Ensure package resources can be loaded from this loader. May be called
|
||||
multiple times, as the operation is idempotent.
|
||||
"""
|
||||
try:
|
||||
import pkg_resources
|
||||
# access an attribute in case a deferred importer is present
|
||||
pkg_resources.__name__
|
||||
except ImportError:
|
||||
return
|
||||
|
||||
# Since pytest tests are always located in the file system, the
|
||||
# DefaultProvider is appropriate.
|
||||
pkg_resources.register_loader_type(cls, pkg_resources.DefaultProvider)
|
||||
|
||||
def get_data(self, pathname):
|
||||
"""Optional PEP302 get_data API.
|
||||
"""
|
||||
with open(pathname, 'rb') as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def _write_pyc(state, co, source_stat, pyc):
|
||||
# Technically, we don't have to have the same pyc format as
|
||||
# (C)Python, since these "pycs" should never be seen by builtin
|
||||
# import. However, there's little reason deviate, and I hope
|
||||
# sometime to be able to use imp.load_compiled to load them. (See
|
||||
# the comment in load_module above.)
|
||||
mtime = int(source_path.mtime())
|
||||
try:
|
||||
fp = open(pyc, "wb")
|
||||
except IOError:
|
||||
err = sys.exc_info()[1].errno
|
||||
if err == PATH_COMPONENT_NOT_DIR:
|
||||
# This happens when we get a EEXIST in find_module creating the
|
||||
# __pycache__ directory and __pycache__ is by some non-dir node.
|
||||
return False
|
||||
raise
|
||||
state.trace("error writing pyc file at %s: errno=%s" %(pyc, err))
|
||||
# we ignore any failure to write the cache file
|
||||
# there are many reasons, permission-denied, __pycache__ being a
|
||||
# file etc.
|
||||
return False
|
||||
try:
|
||||
fp.write(imp.get_magic())
|
||||
fp.write(struct.pack("<l", mtime))
|
||||
mtime = int(source_stat.mtime)
|
||||
size = source_stat.size & 0xFFFFFFFF
|
||||
fp.write(struct.pack("<ll", mtime, size))
|
||||
marshal.dump(co, fp)
|
||||
finally:
|
||||
fp.close()
|
||||
@@ -189,15 +238,16 @@ def _write_pyc(co, source_path, pyc):
|
||||
RN = "\r\n".encode("utf-8")
|
||||
N = "\n".encode("utf-8")
|
||||
|
||||
cookie_re = re.compile("coding[:=]\s*[-\w.]+")
|
||||
cookie_re = re.compile(r"^[ \t\f]*#.*coding[:=][ \t]*[-\w.]+")
|
||||
BOM_UTF8 = '\xef\xbb\xbf'
|
||||
|
||||
def _rewrite_test(state, fn):
|
||||
"""Try to read and rewrite *fn* and return the code object."""
|
||||
try:
|
||||
stat = fn.stat()
|
||||
source = fn.read("rb")
|
||||
except EnvironmentError:
|
||||
return None
|
||||
return None, None
|
||||
if ASCII_IS_DEFAULT_ENCODING:
|
||||
# ASCII is the default encoding in Python 2. Without a coding
|
||||
# declaration, Python 2 will complain about any bytes in the file
|
||||
@@ -213,17 +263,18 @@ def _rewrite_test(state, fn):
|
||||
end1 = source.find("\n")
|
||||
end2 = source.find("\n", end1 + 1)
|
||||
if (not source.startswith(BOM_UTF8) and
|
||||
(not cookie_re.match(source[0:end1]) or
|
||||
not cookie_re.match(source[end1:end2]))):
|
||||
cookie_re.match(source[0:end1]) is None and
|
||||
cookie_re.match(source[end1 + 1:end2]) is None):
|
||||
if hasattr(state, "_indecode"):
|
||||
return None # encodings imported us again, we don't rewrite
|
||||
# encodings imported us again, so don't rewrite.
|
||||
return None, None
|
||||
state._indecode = True
|
||||
try:
|
||||
try:
|
||||
source.decode("ascii")
|
||||
except UnicodeDecodeError:
|
||||
# Let it fail in real import.
|
||||
return None
|
||||
return None, None
|
||||
finally:
|
||||
del state._indecode
|
||||
# On Python versions which are not 2.7 and less than or equal to 3.1, the
|
||||
@@ -235,7 +286,7 @@ def _rewrite_test(state, fn):
|
||||
except SyntaxError:
|
||||
# Let this pop up again in the real import.
|
||||
state.trace("failed to parse: %r" % (fn,))
|
||||
return None
|
||||
return None, None
|
||||
rewrite_asserts(tree)
|
||||
try:
|
||||
co = compile(tree, fn.strpath, "exec")
|
||||
@@ -243,24 +294,24 @@ def _rewrite_test(state, fn):
|
||||
# It's possible that this error is from some bug in the
|
||||
# assertion rewriting, but I don't know of a fast way to tell.
|
||||
state.trace("failed to compile: %r" % (fn,))
|
||||
return None
|
||||
return co
|
||||
return None, None
|
||||
return stat, co
|
||||
|
||||
def _make_rewritten_pyc(state, fn, pyc, co):
|
||||
def _make_rewritten_pyc(state, source_stat, pyc, co):
|
||||
"""Try to dump rewritten code to *pyc*."""
|
||||
if sys.platform.startswith("win"):
|
||||
# Windows grants exclusive access to open files and doesn't have atomic
|
||||
# rename, so just write into the final file.
|
||||
_write_pyc(co, fn, pyc)
|
||||
_write_pyc(state, co, source_stat, pyc)
|
||||
else:
|
||||
# When not on windows, assume rename is atomic. Dump the code object
|
||||
# into a file specific to this process and atomically replace it.
|
||||
proc_pyc = pyc + "." + str(os.getpid())
|
||||
if _write_pyc(co, fn, proc_pyc):
|
||||
if _write_pyc(state, co, source_stat, proc_pyc):
|
||||
os.rename(proc_pyc, pyc)
|
||||
|
||||
def _read_pyc(source, pyc):
|
||||
"""Possibly read a py.test pyc containing rewritten code.
|
||||
def _read_pyc(source, pyc, trace=lambda x: None):
|
||||
"""Possibly read a pytest pyc containing rewritten code.
|
||||
|
||||
Return rewritten code if successful or None if not.
|
||||
"""
|
||||
@@ -268,23 +319,28 @@ def _read_pyc(source, pyc):
|
||||
fp = open(pyc, "rb")
|
||||
except IOError:
|
||||
return None
|
||||
try:
|
||||
with fp:
|
||||
try:
|
||||
mtime = int(source.mtime())
|
||||
data = fp.read(8)
|
||||
except EnvironmentError:
|
||||
size = source.size()
|
||||
data = fp.read(12)
|
||||
except EnvironmentError as e:
|
||||
trace('_read_pyc(%s): EnvironmentError %s' % (source, e))
|
||||
return None
|
||||
# Check for invalid or out of date pyc file.
|
||||
if (len(data) != 8 or data[:4] != imp.get_magic() or
|
||||
struct.unpack("<l", data[4:])[0] != mtime):
|
||||
if (len(data) != 12 or data[:4] != imp.get_magic() or
|
||||
struct.unpack("<ll", data[4:]) != (mtime, size)):
|
||||
trace('_read_pyc(%s): invalid or out of date pyc' % source)
|
||||
return None
|
||||
try:
|
||||
co = marshal.load(fp)
|
||||
except Exception as e:
|
||||
trace('_read_pyc(%s): marshal.load error %s' % (source, e))
|
||||
return None
|
||||
co = marshal.load(fp)
|
||||
if not isinstance(co, types.CodeType):
|
||||
# That's interesting....
|
||||
trace('_read_pyc(%s): not a code object' % source)
|
||||
return None
|
||||
return co
|
||||
finally:
|
||||
fp.close()
|
||||
|
||||
|
||||
def rewrite_asserts(mod):
|
||||
@@ -292,14 +348,64 @@ def rewrite_asserts(mod):
|
||||
AssertionRewriter().run(mod)
|
||||
|
||||
|
||||
_saferepr = py.io.saferepr
|
||||
from _pytest.assertion.util import format_explanation as _format_explanation
|
||||
def _saferepr(obj):
|
||||
"""Get a safe repr of an object for assertion error messages.
|
||||
|
||||
The assertion formatting (util.format_explanation()) requires
|
||||
newlines to be escaped since they are a special character for it.
|
||||
Normally assertion.util.format_explanation() does this but for a
|
||||
custom repr it is possible to contain one of the special escape
|
||||
sequences, especially '\n{' and '\n}' are likely to be present in
|
||||
JSON reprs.
|
||||
|
||||
"""
|
||||
repr = py.io.saferepr(obj)
|
||||
if py.builtin._istext(repr):
|
||||
t = py.builtin.text
|
||||
else:
|
||||
t = py.builtin.bytes
|
||||
return repr.replace(t("\n"), t("\\n"))
|
||||
|
||||
|
||||
from _pytest.assertion.util import format_explanation as _format_explanation # noqa
|
||||
|
||||
def _format_assertmsg(obj):
|
||||
"""Format the custom assertion message given.
|
||||
|
||||
For strings this simply replaces newlines with '\n~' so that
|
||||
util.format_explanation() will preserve them instead of escaping
|
||||
newlines. For other objects py.io.saferepr() is used first.
|
||||
|
||||
"""
|
||||
# reprlib appears to have a bug which means that if a string
|
||||
# contains a newline it gets escaped, however if an object has a
|
||||
# .__repr__() which contains newlines it does not get escaped.
|
||||
# However in either case we want to preserve the newline.
|
||||
if py.builtin._istext(obj) or py.builtin._isbytes(obj):
|
||||
s = obj
|
||||
is_repr = False
|
||||
else:
|
||||
s = py.io.saferepr(obj)
|
||||
is_repr = True
|
||||
if py.builtin._istext(s):
|
||||
t = py.builtin.text
|
||||
else:
|
||||
t = py.builtin.bytes
|
||||
s = s.replace(t("\n"), t("\n~")).replace(t("%"), t("%%"))
|
||||
if is_repr:
|
||||
s = s.replace(t("\\n"), t("\n~"))
|
||||
return s
|
||||
|
||||
def _should_repr_global_name(obj):
|
||||
return not hasattr(obj, "__name__") and not py.builtin.callable(obj)
|
||||
|
||||
def _format_boolop(explanations, is_or):
|
||||
return "(" + (is_or and " or " or " and ").join(explanations) + ")"
|
||||
explanation = "(" + (is_or and " or " or " and ").join(explanations) + ")"
|
||||
if py.builtin._istext(explanation):
|
||||
t = py.builtin.text
|
||||
else:
|
||||
t = py.builtin.bytes
|
||||
return explanation.replace(t('%'), t('%%'))
|
||||
|
||||
def _call_reprcompare(ops, results, expls, each_obj):
|
||||
for i, res, expl in zip(range(len(ops)), results, expls):
|
||||
@@ -348,6 +454,13 @@ binop_map = {
|
||||
ast.NotIn: "not in"
|
||||
}
|
||||
|
||||
# Python 3.4+ compatibility
|
||||
if hasattr(ast, "NameConstant"):
|
||||
_NameConstant = ast.NameConstant
|
||||
else:
|
||||
def _NameConstant(c):
|
||||
return ast.Name(str(c), ast.Load())
|
||||
|
||||
|
||||
def set_location(node, lineno, col_offset):
|
||||
"""Set node location information recursively."""
|
||||
@@ -363,6 +476,56 @@ def set_location(node, lineno, col_offset):
|
||||
|
||||
|
||||
class AssertionRewriter(ast.NodeVisitor):
|
||||
"""Assertion rewriting implementation.
|
||||
|
||||
The main entrypoint is to call .run() with an ast.Module instance,
|
||||
this will then find all the assert statements and re-write them to
|
||||
provide intermediate values and a detailed assertion error. See
|
||||
http://pybites.blogspot.be/2011/07/behind-scenes-of-pytests-new-assertion.html
|
||||
for an overview of how this works.
|
||||
|
||||
The entry point here is .run() which will iterate over all the
|
||||
statements in an ast.Module and for each ast.Assert statement it
|
||||
finds call .visit() with it. Then .visit_Assert() takes over and
|
||||
is responsible for creating new ast statements to replace the
|
||||
original assert statement: it re-writes the test of an assertion
|
||||
to provide intermediate values and replace it with an if statement
|
||||
which raises an assertion error with a detailed explanation in
|
||||
case the expression is false.
|
||||
|
||||
For this .visit_Assert() uses the visitor pattern to visit all the
|
||||
AST nodes of the ast.Assert.test field, each visit call returning
|
||||
an AST node and the corresponding explanation string. During this
|
||||
state is kept in several instance attributes:
|
||||
|
||||
:statements: All the AST statements which will replace the assert
|
||||
statement.
|
||||
|
||||
:variables: This is populated by .variable() with each variable
|
||||
used by the statements so that they can all be set to None at
|
||||
the end of the statements.
|
||||
|
||||
:variable_counter: Counter to create new unique variables needed
|
||||
by statements. Variables are created using .variable() and
|
||||
have the form of "@py_assert0".
|
||||
|
||||
:on_failure: The AST statements which will be executed if the
|
||||
assertion test fails. This is the code which will construct
|
||||
the failure message and raises the AssertionError.
|
||||
|
||||
:explanation_specifiers: A dict filled by .explanation_param()
|
||||
with %-formatting placeholders and their corresponding
|
||||
expressions to use in the building of an assertion message.
|
||||
This is used by .pop_format_context() to build a message.
|
||||
|
||||
:stack: A stack of the explanation_specifiers dicts maintained by
|
||||
.push_format_context() and .pop_format_context() which allows
|
||||
to build another %-formatted string while already building one.
|
||||
|
||||
This state is reset on every new assert statement visited and used
|
||||
by the other visitors.
|
||||
|
||||
"""
|
||||
|
||||
def run(self, mod):
|
||||
"""Find all assert statements in *mod* and rewrite them."""
|
||||
@@ -436,7 +599,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
"""Call a helper in this module."""
|
||||
py_name = ast.Name("@pytest_ar", ast.Load())
|
||||
attr = ast.Attribute(py_name, "_" + name, ast.Load())
|
||||
return ast.Call(attr, list(args), [], None, None)
|
||||
return ast_Call(attr, list(args), [])
|
||||
|
||||
def builtin(self, name):
|
||||
"""Return the builtin called *name*."""
|
||||
@@ -444,15 +607,41 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
return ast.Attribute(builtin_name, name, ast.Load())
|
||||
|
||||
def explanation_param(self, expr):
|
||||
"""Return a new named %-formatting placeholder for expr.
|
||||
|
||||
This creates a %-formatting placeholder for expr in the
|
||||
current formatting context, e.g. ``%(py0)s``. The placeholder
|
||||
and expr are placed in the current format context so that it
|
||||
can be used on the next call to .pop_format_context().
|
||||
|
||||
"""
|
||||
specifier = "py" + str(next(self.variable_counter))
|
||||
self.explanation_specifiers[specifier] = expr
|
||||
return "%(" + specifier + ")s"
|
||||
|
||||
def push_format_context(self):
|
||||
"""Create a new formatting context.
|
||||
|
||||
The format context is used for when an explanation wants to
|
||||
have a variable value formatted in the assertion message. In
|
||||
this case the value required can be added using
|
||||
.explanation_param(). Finally .pop_format_context() is used
|
||||
to format a string of %-formatted values as added by
|
||||
.explanation_param().
|
||||
|
||||
"""
|
||||
self.explanation_specifiers = {}
|
||||
self.stack.append(self.explanation_specifiers)
|
||||
|
||||
def pop_format_context(self, expl_expr):
|
||||
"""Format the %-formatted string with current format context.
|
||||
|
||||
The expl_expr should be an ast.Str instance constructed from
|
||||
the %-placeholders created by .explanation_param(). This will
|
||||
add the required code to format said string to .on_failure and
|
||||
return the ast.Name instance of the formatted string.
|
||||
|
||||
"""
|
||||
current = self.stack.pop()
|
||||
if self.stack:
|
||||
self.explanation_specifiers = self.stack[-1]
|
||||
@@ -470,11 +659,15 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
return res, self.explanation_param(self.display(res))
|
||||
|
||||
def visit_Assert(self, assert_):
|
||||
if assert_.msg:
|
||||
# There's already a message. Don't mess with it.
|
||||
return [assert_]
|
||||
"""Return the AST statements to replace the ast.Assert instance.
|
||||
|
||||
This re-writes the test of an assertion to provide
|
||||
intermediate values and replace it with an if statement which
|
||||
raises an assertion error with a detailed explanation in case
|
||||
the expression is false.
|
||||
|
||||
"""
|
||||
self.statements = []
|
||||
self.cond_chain = ()
|
||||
self.variables = []
|
||||
self.variable_counter = itertools.count()
|
||||
self.stack = []
|
||||
@@ -486,12 +679,17 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
body = self.on_failure
|
||||
negation = ast.UnaryOp(ast.Not(), top_condition)
|
||||
self.statements.append(ast.If(negation, body, []))
|
||||
explanation = "assert " + explanation
|
||||
template = ast.Str(explanation)
|
||||
if assert_.msg:
|
||||
assertmsg = self.helper('format_assertmsg', assert_.msg)
|
||||
explanation = "\n>assert " + explanation
|
||||
else:
|
||||
assertmsg = ast.Str("")
|
||||
explanation = "assert " + explanation
|
||||
template = ast.BinOp(assertmsg, ast.Add(), ast.Str(explanation))
|
||||
msg = self.pop_format_context(template)
|
||||
fmt = self.helper("format_explanation", msg)
|
||||
err_name = ast.Name("AssertionError", ast.Load())
|
||||
exc = ast.Call(err_name, [fmt], [], None, None)
|
||||
exc = ast_Call(err_name, [fmt], [])
|
||||
if sys.version_info[0] >= 3:
|
||||
raise_ = ast.Raise(exc, None)
|
||||
else:
|
||||
@@ -501,7 +699,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
if self.variables:
|
||||
variables = [ast.Name(name, ast.Store())
|
||||
for name in self.variables]
|
||||
clear = ast.Assign(variables, ast.Name("None", ast.Load()))
|
||||
clear = ast.Assign(variables, _NameConstant(None))
|
||||
self.statements.append(clear)
|
||||
# Fix line numbers.
|
||||
for stmt in self.statements:
|
||||
@@ -511,7 +709,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
def visit_Name(self, name):
|
||||
# Display the repr of the name if it's a local variable or
|
||||
# _should_repr_global_name() thinks it's acceptable.
|
||||
locs = ast.Call(self.builtin("locals"), [], [], None, None)
|
||||
locs = ast_Call(self.builtin("locals"), [], [])
|
||||
inlocs = ast.Compare(ast.Str(name.id), [ast.In()], [locs])
|
||||
dorepr = self.helper("should_repr_global_name", name)
|
||||
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
|
||||
@@ -531,13 +729,14 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
for i, v in enumerate(boolop.values):
|
||||
if i:
|
||||
fail_inner = []
|
||||
self.on_failure.append(ast.If(cond, fail_inner, []))
|
||||
# cond is set in a prior loop iteration below
|
||||
self.on_failure.append(ast.If(cond, fail_inner, [])) # noqa
|
||||
self.on_failure = fail_inner
|
||||
self.push_format_context()
|
||||
res, expl = self.visit(v)
|
||||
body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
|
||||
expl_format = self.pop_format_context(ast.Str(expl))
|
||||
call = ast.Call(app, [expl_format], [], None, None)
|
||||
call = ast_Call(app, [expl_format], [])
|
||||
self.on_failure.append(ast.Expr(call))
|
||||
if i < levels:
|
||||
cond = res
|
||||
@@ -566,7 +765,42 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
res = self.assign(ast.BinOp(left_expr, binop.op, right_expr))
|
||||
return res, explanation
|
||||
|
||||
def visit_Call(self, call):
|
||||
def visit_Call_35(self, call):
|
||||
"""
|
||||
visit `ast.Call` nodes on Python3.5 and after
|
||||
"""
|
||||
new_func, func_expl = self.visit(call.func)
|
||||
arg_expls = []
|
||||
new_args = []
|
||||
new_kwargs = []
|
||||
for arg in call.args:
|
||||
res, expl = self.visit(arg)
|
||||
arg_expls.append(expl)
|
||||
new_args.append(res)
|
||||
for keyword in call.keywords:
|
||||
res, expl = self.visit(keyword.value)
|
||||
new_kwargs.append(ast.keyword(keyword.arg, res))
|
||||
if keyword.arg:
|
||||
arg_expls.append(keyword.arg + "=" + expl)
|
||||
else: ## **args have `arg` keywords with an .arg of None
|
||||
arg_expls.append("**" + expl)
|
||||
|
||||
expl = "%s(%s)" % (func_expl, ', '.join(arg_expls))
|
||||
new_call = ast.Call(new_func, new_args, new_kwargs)
|
||||
res = self.assign(new_call)
|
||||
res_expl = self.explanation_param(self.display(res))
|
||||
outer_expl = "%s\n{%s = %s\n}" % (res_expl, res_expl, expl)
|
||||
return res, outer_expl
|
||||
|
||||
def visit_Starred(self, starred):
|
||||
# From Python 3.5, a Starred node can appear in a function call
|
||||
res, expl = self.visit(starred.value)
|
||||
return starred, '*' + expl
|
||||
|
||||
def visit_Call_legacy(self, call):
|
||||
"""
|
||||
visit `ast.Call nodes on 3.4 and below`
|
||||
"""
|
||||
new_func, func_expl = self.visit(call.func)
|
||||
arg_expls = []
|
||||
new_args = []
|
||||
@@ -594,6 +828,15 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
outer_expl = "%s\n{%s = %s\n}" % (res_expl, res_expl, expl)
|
||||
return res, outer_expl
|
||||
|
||||
# ast.Call signature changed on 3.5,
|
||||
# conditionally change which methods is named
|
||||
# visit_Call depending on Python version
|
||||
if sys.version_info >= (3, 5):
|
||||
visit_Call = visit_Call_35
|
||||
else:
|
||||
visit_Call = visit_Call_legacy
|
||||
|
||||
|
||||
def visit_Attribute(self, attr):
|
||||
if not isinstance(attr.ctx, ast.Load):
|
||||
return self.generic_visit(attr)
|
||||
@@ -624,7 +867,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
res_expr = ast.Compare(left_res, [op], [next_res])
|
||||
self.statements.append(ast.Assign([store_names[i]], res_expr))
|
||||
left_res, left_expl = next_res, next_expl
|
||||
# Use py.code._reprcompare if that's available.
|
||||
# Use pytest.assertion.util._reprcompare if that's available.
|
||||
expl_call = self.helper("call_reprcompare",
|
||||
ast.Tuple(syms, ast.Load()),
|
||||
ast.Tuple(load_names, ast.Load()),
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
"""Utilities for assertion debugging"""
|
||||
import pprint
|
||||
|
||||
import py
|
||||
try:
|
||||
from collections import Sequence
|
||||
except ImportError:
|
||||
Sequence = list
|
||||
|
||||
BuiltinAssertionError = py.builtin.builtins.AssertionError
|
||||
u = py.builtin._totext
|
||||
|
||||
# The _reprcompare attribute on the util module is used by the new assertion
|
||||
# interpretation code and assertion rewriter to detect this plugin was
|
||||
@@ -21,20 +27,33 @@ def format_explanation(explanation):
|
||||
for when one explanation needs to span multiple lines, e.g. when
|
||||
displaying diffs.
|
||||
"""
|
||||
# simplify 'assert False where False = ...'
|
||||
explanation = _collapse_false(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 c == "{":
|
||||
if prev_c + c == "\n{":
|
||||
level += 1
|
||||
elif c == "}":
|
||||
elif prev_c + c == "\n}":
|
||||
level -= 1
|
||||
if not level:
|
||||
break
|
||||
prev_c = c
|
||||
else:
|
||||
raise AssertionError("unbalanced braces: %r" % (explanation,))
|
||||
end = start + i
|
||||
@@ -43,38 +62,59 @@ def format_explanation(explanation):
|
||||
explanation = (explanation[:start] + explanation[start+15:end-1] +
|
||||
explanation[end+1:])
|
||||
where -= 17
|
||||
raw_lines = (explanation or '').split('\n')
|
||||
# escape newlines not followed by {, } and ~
|
||||
return explanation
|
||||
|
||||
|
||||
def _split_explanation(explanation):
|
||||
"""Return a list of individual lines in the explanation
|
||||
|
||||
This will return a list of lines split on '\n{', '\n}' and '\n~'.
|
||||
Any other newlines will be escaped and appear in the line as the
|
||||
literal '\n' characters.
|
||||
"""
|
||||
raw_lines = (explanation or u('')).split('\n')
|
||||
lines = [raw_lines[0]]
|
||||
for l in raw_lines[1:]:
|
||||
if l.startswith('{') or l.startswith('}') or l.startswith('~'):
|
||||
if l and l[0] in ['{', '}', '~', '>']:
|
||||
lines.append(l)
|
||||
else:
|
||||
lines[-1] += '\\n' + l
|
||||
return lines
|
||||
|
||||
|
||||
def _format_lines(lines):
|
||||
"""Format the individual lines
|
||||
|
||||
This will replace the '{', '}' and '~' characters of our mini
|
||||
formatting language with the proper 'where ...', 'and ...' and ' +
|
||||
...' text, taking care of indentation along the way.
|
||||
|
||||
Return a list of formatted lines.
|
||||
"""
|
||||
result = lines[:1]
|
||||
stack = [0]
|
||||
stackcnt = [0]
|
||||
for line in lines[1:]:
|
||||
if line.startswith('{'):
|
||||
if stackcnt[-1]:
|
||||
s = 'and '
|
||||
s = u('and ')
|
||||
else:
|
||||
s = 'where '
|
||||
s = u('where ')
|
||||
stack.append(len(result))
|
||||
stackcnt[-1] += 1
|
||||
stackcnt.append(0)
|
||||
result.append(' +' + ' '*(len(stack)-1) + s + line[1:])
|
||||
result.append(u(' +') + u(' ')*(len(stack)-1) + s + line[1:])
|
||||
elif line.startswith('}'):
|
||||
assert line.startswith('}')
|
||||
stack.pop()
|
||||
stackcnt.pop()
|
||||
result[stack[-1]] += line[1:]
|
||||
else:
|
||||
assert line.startswith('~')
|
||||
result.append(' '*len(stack) + line[1:])
|
||||
assert line[0] in ['~', '>']
|
||||
stack[-1] += 1
|
||||
indent = len(stack) if line.startswith('~') else len(stack) - 1
|
||||
result.append(u(' ')*indent + line[1:])
|
||||
assert len(stack) == 1
|
||||
return '\n'.join(result)
|
||||
return result
|
||||
|
||||
|
||||
# Provide basestring in python3
|
||||
@@ -89,35 +129,57 @@ def assertrepr_compare(config, op, left, right):
|
||||
width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op
|
||||
left_repr = py.io.saferepr(left, maxsize=int(width/2))
|
||||
right_repr = py.io.saferepr(right, maxsize=width-len(left_repr))
|
||||
summary = '%s %s %s' % (left_repr, op, right_repr)
|
||||
|
||||
issequence = lambda x: isinstance(x, (list, tuple))
|
||||
# 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))
|
||||
istext = lambda x: isinstance(x, basestring)
|
||||
isdict = lambda x: isinstance(x, dict)
|
||||
isset = lambda x: isinstance(x, (set, frozenset))
|
||||
|
||||
def isiterable(obj):
|
||||
try:
|
||||
iter(obj)
|
||||
return not istext(obj)
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
verbose = config.getoption('verbose')
|
||||
explanation = None
|
||||
try:
|
||||
if op == '==':
|
||||
if istext(left) and istext(right):
|
||||
explanation = _diff_text(left, right, verbose)
|
||||
elif issequence(left) and issequence(right):
|
||||
explanation = _compare_eq_sequence(left, right, verbose)
|
||||
elif isset(left) and isset(right):
|
||||
explanation = _compare_eq_set(left, right, verbose)
|
||||
elif isdict(left) and isdict(right):
|
||||
explanation = _compare_eq_dict(left, right, verbose)
|
||||
else:
|
||||
if issequence(left) and issequence(right):
|
||||
explanation = _compare_eq_sequence(left, right, verbose)
|
||||
elif isset(left) and isset(right):
|
||||
explanation = _compare_eq_set(left, right, verbose)
|
||||
elif isdict(left) and isdict(right):
|
||||
explanation = _compare_eq_dict(left, right, verbose)
|
||||
if isiterable(left) and isiterable(right):
|
||||
expl = _compare_eq_iterable(left, right, verbose)
|
||||
if explanation is not None:
|
||||
explanation.extend(expl)
|
||||
else:
|
||||
explanation = expl
|
||||
elif op == 'not in':
|
||||
if istext(left) and istext(right):
|
||||
explanation = _notin_text(left, right, verbose)
|
||||
except py.builtin._sysex:
|
||||
raise
|
||||
except:
|
||||
excinfo = py.code.ExceptionInfo()
|
||||
except Exception:
|
||||
explanation = [
|
||||
'(pytest_assertion plugin: representation of details failed. '
|
||||
'Probably an object has a faulty __repr__.)', str(excinfo)]
|
||||
u('(pytest_assertion plugin: representation of details failed. '
|
||||
'Probably an object has a faulty __repr__.)'),
|
||||
u(py.code.ExceptionInfo())]
|
||||
|
||||
if not explanation:
|
||||
return None
|
||||
@@ -126,12 +188,19 @@ def assertrepr_compare(config, op, left, right):
|
||||
|
||||
|
||||
def _diff_text(left, right, verbose=False):
|
||||
"""Return the explanation for the diff between text
|
||||
"""Return the explanation for the diff between text or bytes
|
||||
|
||||
Unless --verbose is used this will skip leading and trailing
|
||||
characters which are identical to keep the diff minimal.
|
||||
|
||||
If the input are bytes they will be safely converted to text.
|
||||
"""
|
||||
from difflib import ndiff
|
||||
explanation = []
|
||||
if isinstance(left, py.builtin.bytes):
|
||||
left = u(repr(left)[1:-1]).replace(r'\n', '\n')
|
||||
if isinstance(right, py.builtin.bytes):
|
||||
right = u(repr(right)[1:-1]).replace(r'\n', '\n')
|
||||
if not verbose:
|
||||
i = 0 # just in case left or right has zero length
|
||||
for i in range(min(len(left), len(right))):
|
||||
@@ -139,8 +208,8 @@ def _diff_text(left, right, verbose=False):
|
||||
break
|
||||
if i > 42:
|
||||
i -= 10 # Provide some context
|
||||
explanation = ['Skipping %s identical leading '
|
||||
'characters in diff, use -v to show' % i]
|
||||
explanation = [u('Skipping %s identical leading '
|
||||
'characters in diff, use -v to show') % i]
|
||||
left = left[i:]
|
||||
right = right[i:]
|
||||
if len(left) == len(right):
|
||||
@@ -149,13 +218,34 @@ def _diff_text(left, right, verbose=False):
|
||||
break
|
||||
if i > 42:
|
||||
i -= 10 # Provide some context
|
||||
explanation += ['Skipping %s identical trailing '
|
||||
'characters in diff, use -v to show' % i]
|
||||
explanation += [u('Skipping %s identical trailing '
|
||||
'characters in diff, use -v to show') % i]
|
||||
left = left[:-i]
|
||||
right = right[:-i]
|
||||
explanation += [line.strip('\n')
|
||||
for line in py.std.difflib.ndiff(left.splitlines(),
|
||||
right.splitlines())]
|
||||
for line in ndiff(left.splitlines(),
|
||||
right.splitlines())]
|
||||
return explanation
|
||||
|
||||
|
||||
def _compare_eq_iterable(left, right, verbose=False):
|
||||
if not verbose:
|
||||
return [u('Use -v to get the full diff')]
|
||||
# dynamic import to speedup pytest
|
||||
import difflib
|
||||
|
||||
try:
|
||||
left_formatting = pprint.pformat(left).splitlines()
|
||||
right_formatting = pprint.pformat(right).splitlines()
|
||||
explanation = [u('Full diff:')]
|
||||
except Exception:
|
||||
# hack: PrettyPrinter.pformat() in python 2 fails when formatting items that can't be sorted(), ie, calling
|
||||
# sorted() on a list would raise. See issue #718.
|
||||
# As a workaround, the full diff is generated by using the repr() string of each item of each container.
|
||||
left_formatting = sorted(repr(x) for x in left)
|
||||
right_formatting = sorted(repr(x) for x in right)
|
||||
explanation = [u('Full diff (fallback to calling repr on each item):')]
|
||||
explanation.extend(line.strip() for line in difflib.ndiff(left_formatting, right_formatting))
|
||||
return explanation
|
||||
|
||||
|
||||
@@ -163,19 +253,18 @@ def _compare_eq_sequence(left, right, verbose=False):
|
||||
explanation = []
|
||||
for i in range(min(len(left), len(right))):
|
||||
if left[i] != right[i]:
|
||||
explanation += ['At index %s diff: %r != %r' %
|
||||
(i, left[i], right[i])]
|
||||
explanation += [u('At index %s diff: %r != %r')
|
||||
% (i, left[i], right[i])]
|
||||
break
|
||||
if len(left) > len(right):
|
||||
explanation += [
|
||||
'Left contains more items, first extra item: %s' %
|
||||
py.io.saferepr(left[len(right)],)]
|
||||
explanation += [u('Left contains more items, first extra item: %s')
|
||||
% py.io.saferepr(left[len(right)],)]
|
||||
elif len(left) < len(right):
|
||||
explanation += [
|
||||
'Right contains more items, first extra item: %s' %
|
||||
u('Right contains more items, first extra item: %s') %
|
||||
py.io.saferepr(right[len(left)],)]
|
||||
return explanation # + _diff_text(py.std.pprint.pformat(left),
|
||||
# py.std.pprint.pformat(right))
|
||||
return explanation # + _diff_text(pprint.pformat(left),
|
||||
# pprint.pformat(right))
|
||||
|
||||
|
||||
def _compare_eq_set(left, right, verbose=False):
|
||||
@@ -183,11 +272,11 @@ def _compare_eq_set(left, right, verbose=False):
|
||||
diff_left = left - right
|
||||
diff_right = right - left
|
||||
if diff_left:
|
||||
explanation.append('Extra items in the left set:')
|
||||
explanation.append(u('Extra items in the left set:'))
|
||||
for item in diff_left:
|
||||
explanation.append(py.io.saferepr(item))
|
||||
if diff_right:
|
||||
explanation.append('Extra items in the right set:')
|
||||
explanation.append(u('Extra items in the right set:'))
|
||||
for item in diff_right:
|
||||
explanation.append(py.io.saferepr(item))
|
||||
return explanation
|
||||
@@ -198,26 +287,26 @@ def _compare_eq_dict(left, right, verbose=False):
|
||||
common = set(left).intersection(set(right))
|
||||
same = dict((k, left[k]) for k in common if left[k] == right[k])
|
||||
if same and not verbose:
|
||||
explanation += ['Hiding %s identical items, use -v to show' %
|
||||
explanation += [u('Omitting %s identical items, use -v to show') %
|
||||
len(same)]
|
||||
elif same:
|
||||
explanation += ['Common items:']
|
||||
explanation += py.std.pprint.pformat(same).splitlines()
|
||||
explanation += [u('Common items:')]
|
||||
explanation += pprint.pformat(same).splitlines()
|
||||
diff = set(k for k in common if left[k] != right[k])
|
||||
if diff:
|
||||
explanation += ['Differing items:']
|
||||
explanation += [u('Differing items:')]
|
||||
for k in diff:
|
||||
explanation += [py.io.saferepr({k: left[k]}) + ' != ' +
|
||||
py.io.saferepr({k: right[k]})]
|
||||
extra_left = set(left) - set(right)
|
||||
if extra_left:
|
||||
explanation.append('Left contains more items:')
|
||||
explanation.extend(py.std.pprint.pformat(
|
||||
explanation.append(u('Left contains more items:'))
|
||||
explanation.extend(pprint.pformat(
|
||||
dict((k, left[k]) for k in extra_left)).splitlines())
|
||||
extra_right = set(right) - set(left)
|
||||
if extra_right:
|
||||
explanation.append('Right contains more items:')
|
||||
explanation.extend(py.std.pprint.pformat(
|
||||
explanation.append(u('Right contains more items:'))
|
||||
explanation.extend(pprint.pformat(
|
||||
dict((k, right[k]) for k in extra_right)).splitlines())
|
||||
return explanation
|
||||
|
||||
@@ -228,14 +317,14 @@ def _notin_text(term, text, verbose=False):
|
||||
tail = text[index+len(term):]
|
||||
correct_text = head + tail
|
||||
diff = _diff_text(correct_text, text, verbose)
|
||||
newdiff = ['%s is contained here:' % py.io.saferepr(term, maxsize=42)]
|
||||
newdiff = [u('%s is contained here:') % py.io.saferepr(term, maxsize=42)]
|
||||
for line in diff:
|
||||
if line.startswith('Skipping'):
|
||||
if line.startswith(u('Skipping')):
|
||||
continue
|
||||
if line.startswith('- '):
|
||||
if line.startswith(u('- ')):
|
||||
continue
|
||||
if line.startswith('+ '):
|
||||
newdiff.append(' ' + line[2:])
|
||||
if line.startswith(u('+ ')):
|
||||
newdiff.append(u(' ') + line[2:])
|
||||
else:
|
||||
newdiff.append(line)
|
||||
return newdiff
|
||||
|
||||
243
_pytest/cacheprovider.py
Executable file
243
_pytest/cacheprovider.py
Executable file
@@ -0,0 +1,243 @@
|
||||
"""
|
||||
merged implementation of the cache provider
|
||||
|
||||
the name cache was not choosen to ensure pluggy automatically
|
||||
ignores the external pytest-cache
|
||||
"""
|
||||
|
||||
import py
|
||||
import pytest
|
||||
import json
|
||||
from os.path import sep as _sep, altsep as _altsep
|
||||
|
||||
|
||||
class Cache(object):
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self._cachedir = config.rootdir.join(".cache")
|
||||
self.trace = config.trace.root.get("cache")
|
||||
if config.getvalue("cacheclear"):
|
||||
self.trace("clearing cachedir")
|
||||
if self._cachedir.check():
|
||||
self._cachedir.remove()
|
||||
self._cachedir.mkdir()
|
||||
|
||||
def makedir(self, name):
|
||||
""" return a directory path object with the given name. If the
|
||||
directory does not yet exist, it will be created. You can use it
|
||||
to manage files likes e. g. store/retrieve database
|
||||
dumps across test sessions.
|
||||
|
||||
:param name: must be a string not containing a ``/`` separator.
|
||||
Make sure the name contains your plugin or application
|
||||
identifiers to prevent clashes with other cache users.
|
||||
"""
|
||||
if _sep in name or _altsep is not None and _altsep in name:
|
||||
raise ValueError("name is not allowed to contain path separators")
|
||||
return self._cachedir.ensure_dir("d", name)
|
||||
|
||||
def _getvaluepath(self, key):
|
||||
return self._cachedir.join('v', *key.split('/'))
|
||||
|
||||
def get(self, key, default):
|
||||
""" return cached value for the given key. If no value
|
||||
was yet cached or the value cannot be read, the specified
|
||||
default is returned.
|
||||
|
||||
:param key: must be a ``/`` separated value. Usually the first
|
||||
name is the name of your plugin or your application.
|
||||
:param default: must be provided in case of a cache-miss or
|
||||
invalid cache values.
|
||||
|
||||
"""
|
||||
path = self._getvaluepath(key)
|
||||
if path.check():
|
||||
try:
|
||||
with path.open("r") as f:
|
||||
return json.load(f)
|
||||
except ValueError:
|
||||
self.trace("cache-invalid at %s" % (path,))
|
||||
return default
|
||||
|
||||
def set(self, key, value):
|
||||
""" save value for the given key.
|
||||
|
||||
:param key: must be a ``/`` separated value. Usually the first
|
||||
name is the name of your plugin or your application.
|
||||
:param value: must be of any combination of basic
|
||||
python types, including nested types
|
||||
like e. g. lists of dictionaries.
|
||||
"""
|
||||
path = self._getvaluepath(key)
|
||||
try:
|
||||
path.dirpath().ensure_dir()
|
||||
except (py.error.EEXIST, py.error.EACCES):
|
||||
self.config.warn(
|
||||
code='I9', message='could not create cache path %s' % (path,)
|
||||
)
|
||||
return
|
||||
try:
|
||||
f = path.open('w')
|
||||
except py.error.ENOTDIR:
|
||||
self.config.warn(
|
||||
code='I9', message='cache could not write path %s' % (path,))
|
||||
else:
|
||||
with f:
|
||||
self.trace("cache-write %s: %r" % (key, value,))
|
||||
json.dump(value, f, indent=2, sort_keys=True)
|
||||
|
||||
|
||||
class LFPlugin:
|
||||
""" Plugin which implements the --lf (run last-failing) option """
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
active_keys = 'lf', 'failedfirst'
|
||||
self.active = any(config.getvalue(key) for key in active_keys)
|
||||
if self.active:
|
||||
self.lastfailed = config.cache.get("cache/lastfailed", {})
|
||||
else:
|
||||
self.lastfailed = {}
|
||||
|
||||
def pytest_report_header(self):
|
||||
if self.active:
|
||||
if not self.lastfailed:
|
||||
mode = "run all (no recorded failures)"
|
||||
else:
|
||||
mode = "rerun last %d failures%s" % (
|
||||
len(self.lastfailed),
|
||||
" first" if self.config.getvalue("failedfirst") else "")
|
||||
return "run-last-failure: %s" % mode
|
||||
|
||||
def pytest_runtest_logreport(self, report):
|
||||
if report.failed and "xfail" not in report.keywords:
|
||||
self.lastfailed[report.nodeid] = True
|
||||
elif not report.failed:
|
||||
if report.when == "call":
|
||||
self.lastfailed.pop(report.nodeid, None)
|
||||
|
||||
def pytest_collectreport(self, report):
|
||||
passed = report.outcome in ('passed', 'skipped')
|
||||
if passed:
|
||||
if report.nodeid in self.lastfailed:
|
||||
self.lastfailed.pop(report.nodeid)
|
||||
self.lastfailed.update(
|
||||
(item.nodeid, True)
|
||||
for item in report.result)
|
||||
else:
|
||||
self.lastfailed[report.nodeid] = True
|
||||
|
||||
def pytest_collection_modifyitems(self, session, config, items):
|
||||
if self.active and self.lastfailed:
|
||||
previously_failed = []
|
||||
previously_passed = []
|
||||
for item in items:
|
||||
if item.nodeid in self.lastfailed:
|
||||
previously_failed.append(item)
|
||||
else:
|
||||
previously_passed.append(item)
|
||||
if not previously_failed and previously_passed:
|
||||
# running a subset of all tests with recorded failures outside
|
||||
# of the set of tests currently executing
|
||||
pass
|
||||
elif self.config.getvalue("failedfirst"):
|
||||
items[:] = previously_failed + previously_passed
|
||||
else:
|
||||
items[:] = previously_failed
|
||||
config.hook.pytest_deselected(items=previously_passed)
|
||||
|
||||
def pytest_sessionfinish(self, session):
|
||||
config = self.config
|
||||
if config.getvalue("cacheshow") or hasattr(config, "slaveinput"):
|
||||
return
|
||||
config.cache.set("cache/lastfailed", self.lastfailed)
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("general")
|
||||
group.addoption(
|
||||
'--lf', 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",
|
||||
help="run all tests but run the last failures first. "
|
||||
"This may re-order tests and thus lead to "
|
||||
"repeated fixture setup/teardown")
|
||||
group.addoption(
|
||||
'--cache-show', action='store_true', dest="cacheshow",
|
||||
help="show cache contents, don't perform collection or tests")
|
||||
group.addoption(
|
||||
'--cache-clear', action='store_true', dest="cacheclear",
|
||||
help="remove all cache contents at start of test run.")
|
||||
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
if config.option.cacheshow:
|
||||
from _pytest.main import wrap_session
|
||||
return wrap_session(config, cacheshow)
|
||||
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_configure(config):
|
||||
config.cache = Cache(config)
|
||||
config.pluginmanager.register(LFPlugin(config), "lfplugin")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cache(request):
|
||||
"""
|
||||
Return a cache object that can persist state between testing sessions.
|
||||
|
||||
cache.get(key, default)
|
||||
cache.set(key, value)
|
||||
|
||||
Keys must be a ``/`` separated value, where the first part is usually the
|
||||
name of your plugin or application to avoid clashes with other cache users.
|
||||
|
||||
Values can be any object handled by the json stdlib module.
|
||||
"""
|
||||
return request.config.cache
|
||||
|
||||
|
||||
def pytest_report_header(config):
|
||||
if config.option.verbose:
|
||||
relpath = py.path.local().bestrelpath(config.cache._cachedir)
|
||||
return "cachedir: %s" % relpath
|
||||
|
||||
|
||||
def cacheshow(config, session):
|
||||
from pprint import pprint
|
||||
tw = py.io.TerminalWriter()
|
||||
tw.line("cachedir: " + str(config.cache._cachedir))
|
||||
if not config.cache._cachedir.check():
|
||||
tw.line("cache is empty")
|
||||
return 0
|
||||
dummy = object()
|
||||
basedir = config.cache._cachedir
|
||||
vdir = basedir.join("v")
|
||||
tw.sep("-", "cache values")
|
||||
for valpath in vdir.visit(lambda x: x.isfile()):
|
||||
key = valpath.relto(vdir).replace(valpath.sep, "/")
|
||||
val = config.cache.get(key, dummy)
|
||||
if val is dummy:
|
||||
tw.line("%s contains unreadable content, "
|
||||
"will be ignored" % key)
|
||||
else:
|
||||
tw.line("%s contains:" % key)
|
||||
stream = py.io.TextIO()
|
||||
pprint(val, stream=stream)
|
||||
for line in stream.getvalue().splitlines():
|
||||
tw.line(" " + line)
|
||||
|
||||
ddir = basedir.join("d")
|
||||
if ddir.isdir() and ddir.listdir():
|
||||
tw.sep("-", "cache directories")
|
||||
for p in basedir.join("d").visit():
|
||||
#if p.check(dir=1):
|
||||
# print("%s/" % p.relto(basedir))
|
||||
if p.isfile():
|
||||
key = p.relto(basedir)
|
||||
tw.line("%s is a file of length %d" % (
|
||||
key, p.size()))
|
||||
return 0
|
||||
@@ -1,224 +1,444 @@
|
||||
""" per-test stdout/stderr capturing mechanisms, ``capsys`` and ``capfd`` function arguments. """
|
||||
"""
|
||||
per-test stdout/stderr capturing mechanism.
|
||||
|
||||
import pytest, py
|
||||
"""
|
||||
from __future__ import with_statement
|
||||
|
||||
import sys
|
||||
import os
|
||||
from tempfile import TemporaryFile
|
||||
|
||||
import py
|
||||
import pytest
|
||||
|
||||
from py.io import TextIO
|
||||
unicode = py.builtin.text
|
||||
|
||||
patchsysdict = {0: 'stdin', 1: 'stdout', 2: 'stderr'}
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("general")
|
||||
group._addoption('--capture', action="store", default=None,
|
||||
metavar="method", type="choice", choices=['fd', 'sys', 'no'],
|
||||
help="per-test capturing method: one of fd (default)|sys|no.")
|
||||
group._addoption('-s', action="store_const", const="no", dest="capture",
|
||||
group._addoption(
|
||||
'--capture', action="store",
|
||||
default="fd" if hasattr(os, "dup") else "sys",
|
||||
metavar="method", choices=['fd', 'sys', 'no'],
|
||||
help="per-test capturing method: one of fd|sys|no.")
|
||||
group._addoption(
|
||||
'-s', action="store_const", const="no", dest="capture",
|
||||
help="shortcut for --capture=no.")
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
def pytest_cmdline_parse(pluginmanager, args):
|
||||
# we want to perform capturing already for plugin/conftest loading
|
||||
if '-s' in args or "--capture=no" in args:
|
||||
method = "no"
|
||||
elif hasattr(os, 'dup') and '--capture=sys' not in args:
|
||||
method = "fd"
|
||||
else:
|
||||
method = "sys"
|
||||
capman = CaptureManager(method)
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_load_initial_conftests(early_config, parser, args):
|
||||
ns = early_config.known_args_namespace
|
||||
pluginmanager = early_config.pluginmanager
|
||||
capman = CaptureManager(ns.capture)
|
||||
pluginmanager.register(capman, "capturemanager")
|
||||
|
||||
def addouterr(rep, outerr):
|
||||
for secname, content in zip(["out", "err"], outerr):
|
||||
if content:
|
||||
rep.sections.append(("Captured std%s" % secname, content))
|
||||
# make sure that capturemanager is properly reset at final shutdown
|
||||
early_config.add_cleanup(capman.reset_capturings)
|
||||
|
||||
# make sure logging does not raise exceptions at the end
|
||||
def silence_logging_at_shutdown():
|
||||
if "logging" in sys.modules:
|
||||
sys.modules["logging"].raiseExceptions = False
|
||||
early_config.add_cleanup(silence_logging_at_shutdown)
|
||||
|
||||
# finally trigger conftest loading but while capturing (issue93)
|
||||
capman.init_capturings()
|
||||
outcome = yield
|
||||
out, err = capman.suspendcapture()
|
||||
if outcome.excinfo is not None:
|
||||
sys.stdout.write(out)
|
||||
sys.stderr.write(err)
|
||||
|
||||
class NoCapture:
|
||||
def startall(self):
|
||||
pass
|
||||
def resume(self):
|
||||
pass
|
||||
def reset(self):
|
||||
pass
|
||||
def suspend(self):
|
||||
return "", ""
|
||||
|
||||
class CaptureManager:
|
||||
def __init__(self, defaultmethod=None):
|
||||
self._method2capture = {}
|
||||
self._defaultmethod = defaultmethod
|
||||
|
||||
def _maketempfile(self):
|
||||
f = py.std.tempfile.TemporaryFile()
|
||||
newf = py.io.dupfile(f, encoding="UTF-8")
|
||||
f.close()
|
||||
return newf
|
||||
|
||||
def _makestringio(self):
|
||||
return py.io.TextIO()
|
||||
def __init__(self, method):
|
||||
self._method = method
|
||||
|
||||
def _getcapture(self, method):
|
||||
if method == "fd":
|
||||
return py.io.StdCaptureFD(now=False,
|
||||
out=self._maketempfile(), err=self._maketempfile()
|
||||
)
|
||||
return MultiCapture(out=True, err=True, Capture=FDCapture)
|
||||
elif method == "sys":
|
||||
return py.io.StdCapture(now=False,
|
||||
out=self._makestringio(), err=self._makestringio()
|
||||
)
|
||||
return MultiCapture(out=True, err=True, Capture=SysCapture)
|
||||
elif method == "no":
|
||||
return NoCapture()
|
||||
return MultiCapture(out=False, err=False, in_=False)
|
||||
else:
|
||||
raise ValueError("unknown capturing method: %r" % method)
|
||||
|
||||
def _getmethod(self, config, fspath):
|
||||
if config.option.capture:
|
||||
method = config.option.capture
|
||||
else:
|
||||
try:
|
||||
method = config._conftest.rget("option_capture", path=fspath)
|
||||
except KeyError:
|
||||
method = "fd"
|
||||
if method == "fd" and not hasattr(os, 'dup'): # e.g. jython
|
||||
method = "sys"
|
||||
return method
|
||||
def init_capturings(self):
|
||||
assert not hasattr(self, "_capturing")
|
||||
self._capturing = self._getcapture(self._method)
|
||||
self._capturing.start_capturing()
|
||||
|
||||
def reset_capturings(self):
|
||||
for name, cap in self._method2capture.items():
|
||||
cap.reset()
|
||||
cap = self.__dict__.pop("_capturing", None)
|
||||
if cap is not None:
|
||||
cap.pop_outerr_to_orig()
|
||||
cap.stop_capturing()
|
||||
|
||||
def resumecapture_item(self, item):
|
||||
method = self._getmethod(item.config, item.fspath)
|
||||
if not hasattr(item, 'outerr'):
|
||||
item.outerr = ('', '') # we accumulate outerr on the item
|
||||
return self.resumecapture(method)
|
||||
def resumecapture(self):
|
||||
self._capturing.resume_capturing()
|
||||
|
||||
def resumecapture(self, method=None):
|
||||
if hasattr(self, '_capturing'):
|
||||
raise ValueError("cannot resume, already capturing with %r" %
|
||||
(self._capturing,))
|
||||
if method is None:
|
||||
method = self._defaultmethod
|
||||
cap = self._method2capture.get(method)
|
||||
self._capturing = method
|
||||
if cap is None:
|
||||
self._method2capture[method] = cap = self._getcapture(method)
|
||||
cap.startall()
|
||||
else:
|
||||
cap.resume()
|
||||
|
||||
def suspendcapture(self, item=None):
|
||||
def suspendcapture(self, in_=False):
|
||||
self.deactivate_funcargs()
|
||||
if hasattr(self, '_capturing'):
|
||||
method = self._capturing
|
||||
cap = self._method2capture.get(method)
|
||||
if cap is not None:
|
||||
outerr = cap.suspend()
|
||||
del self._capturing
|
||||
if item:
|
||||
outerr = (item.outerr[0] + outerr[0],
|
||||
item.outerr[1] + outerr[1])
|
||||
cap = getattr(self, "_capturing", None)
|
||||
if cap is not None:
|
||||
try:
|
||||
outerr = cap.readouterr()
|
||||
finally:
|
||||
cap.suspend_capturing(in_=in_)
|
||||
return outerr
|
||||
if hasattr(item, 'outerr'):
|
||||
return item.outerr
|
||||
return "", ""
|
||||
|
||||
def activate_funcargs(self, pyfuncitem):
|
||||
funcargs = getattr(pyfuncitem, "funcargs", None)
|
||||
if funcargs is not None:
|
||||
for name, capfuncarg in funcargs.items():
|
||||
if name in ('capsys', 'capfd'):
|
||||
assert not hasattr(self, '_capturing_funcarg')
|
||||
self._capturing_funcarg = capfuncarg
|
||||
capfuncarg._start()
|
||||
capfuncarg = pyfuncitem.__dict__.pop("_capfuncarg", None)
|
||||
if capfuncarg is not None:
|
||||
capfuncarg._start()
|
||||
self._capfuncarg = capfuncarg
|
||||
|
||||
def deactivate_funcargs(self):
|
||||
capturing_funcarg = getattr(self, '_capturing_funcarg', None)
|
||||
if capturing_funcarg:
|
||||
outerr = capturing_funcarg._finalize()
|
||||
del self._capturing_funcarg
|
||||
return outerr
|
||||
capfuncarg = self.__dict__.pop("_capfuncarg", None)
|
||||
if capfuncarg is not None:
|
||||
capfuncarg.close()
|
||||
|
||||
def pytest_make_collect_report(self, __multicall__, collector):
|
||||
method = self._getmethod(collector.config, collector.fspath)
|
||||
try:
|
||||
self.resumecapture(method)
|
||||
except ValueError:
|
||||
return # recursive collect, XXX refactor capturing
|
||||
# to allow for more lightweight recursive capturing
|
||||
try:
|
||||
rep = __multicall__.execute()
|
||||
finally:
|
||||
outerr = self.suspendcapture()
|
||||
addouterr(rep, outerr)
|
||||
return rep
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_make_collect_report(self, collector):
|
||||
if isinstance(collector, pytest.File):
|
||||
self.resumecapture()
|
||||
outcome = yield
|
||||
out, err = self.suspendcapture()
|
||||
rep = outcome.get_result()
|
||||
if out:
|
||||
rep.sections.append(("Captured stdout", out))
|
||||
if err:
|
||||
rep.sections.append(("Captured stderr", err))
|
||||
else:
|
||||
yield
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_setup(self, item):
|
||||
self.resumecapture_item(item)
|
||||
self.resumecapture()
|
||||
yield
|
||||
self.suspendcapture_item(item, "setup")
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_call(self, item):
|
||||
self.resumecapture_item(item)
|
||||
self.resumecapture()
|
||||
self.activate_funcargs(item)
|
||||
yield
|
||||
#self.deactivate_funcargs() called from suspendcapture()
|
||||
self.suspendcapture_item(item, "call")
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_teardown(self, item):
|
||||
self.resumecapture_item(item)
|
||||
self.resumecapture()
|
||||
yield
|
||||
self.suspendcapture_item(item, "teardown")
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_keyboard_interrupt(self, excinfo):
|
||||
if hasattr(self, '_capturing'):
|
||||
self.suspendcapture()
|
||||
self.reset_capturings()
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
def pytest_runtest_makereport(self, __multicall__, item, call):
|
||||
funcarg_outerr = self.deactivate_funcargs()
|
||||
rep = __multicall__.execute()
|
||||
outerr = self.suspendcapture(item)
|
||||
if funcarg_outerr is not None:
|
||||
outerr = (outerr[0] + funcarg_outerr[0],
|
||||
outerr[1] + funcarg_outerr[1])
|
||||
addouterr(rep, outerr)
|
||||
if not rep.passed or rep.when == "teardown":
|
||||
outerr = ('', '')
|
||||
item.outerr = outerr
|
||||
return rep
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_internalerror(self, excinfo):
|
||||
self.reset_capturings()
|
||||
|
||||
def suspendcapture_item(self, item, when):
|
||||
out, err = self.suspendcapture()
|
||||
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"
|
||||
|
||||
def pytest_funcarg__capsys(request):
|
||||
|
||||
@pytest.fixture
|
||||
def capsys(request):
|
||||
"""enables capturing of writes to sys.stdout/sys.stderr and makes
|
||||
captured output available via ``capsys.readouterr()`` method calls
|
||||
which return a ``(out, err)`` tuple.
|
||||
"""
|
||||
if "capfd" in request._funcargs:
|
||||
raise request.raiseerror(error_capsysfderror)
|
||||
return CaptureFixture(py.io.StdCapture)
|
||||
request.node._capfuncarg = c = CaptureFixture(SysCapture)
|
||||
return c
|
||||
|
||||
def pytest_funcarg__capfd(request):
|
||||
@pytest.fixture
|
||||
def capfd(request):
|
||||
"""enables capturing of writes to file descriptors 1 and 2 and makes
|
||||
captured output available via ``capsys.readouterr()`` method calls
|
||||
captured output available via ``capfd.readouterr()`` method calls
|
||||
which return a ``(out, err)`` tuple.
|
||||
"""
|
||||
if "capsys" in request._funcargs:
|
||||
request.raiseerror(error_capsysfderror)
|
||||
if not hasattr(os, 'dup'):
|
||||
pytest.skip("capfd funcarg needs os.dup")
|
||||
return CaptureFixture(py.io.StdCaptureFD)
|
||||
request.node._capfuncarg = c = CaptureFixture(FDCapture)
|
||||
return c
|
||||
|
||||
|
||||
class CaptureFixture:
|
||||
def __init__(self, captureclass):
|
||||
self.capture = captureclass(now=False)
|
||||
self.captureclass = captureclass
|
||||
|
||||
def _start(self):
|
||||
self.capture.startall()
|
||||
self._capture = MultiCapture(out=True, err=True, in_=False,
|
||||
Capture=self.captureclass)
|
||||
self._capture.start_capturing()
|
||||
|
||||
def _finalize(self):
|
||||
if hasattr(self, 'capture'):
|
||||
outerr = self._outerr = self.capture.reset()
|
||||
del self.capture
|
||||
return outerr
|
||||
def close(self):
|
||||
cap = self.__dict__.pop("_capture", None)
|
||||
if cap is not None:
|
||||
self._outerr = cap.pop_outerr_to_orig()
|
||||
cap.stop_capturing()
|
||||
|
||||
def readouterr(self):
|
||||
try:
|
||||
return self.capture.readouterr()
|
||||
return self._capture.readouterr()
|
||||
except AttributeError:
|
||||
return self._outerr
|
||||
|
||||
|
||||
def safe_text_dupfile(f, mode, default_encoding="UTF8"):
|
||||
""" return a open text file object that's a duplicate of f on the
|
||||
FD-level if possible.
|
||||
"""
|
||||
encoding = getattr(f, "encoding", None)
|
||||
try:
|
||||
fd = f.fileno()
|
||||
except Exception:
|
||||
if "b" not in getattr(f, "mode", "") and hasattr(f, "encoding"):
|
||||
# we seem to have a text stream, let's just use it
|
||||
return f
|
||||
else:
|
||||
newfd = os.dup(fd)
|
||||
if "b" not in mode:
|
||||
mode += "b"
|
||||
f = os.fdopen(newfd, mode, 0) # no buffering
|
||||
return EncodedFile(f, encoding or default_encoding)
|
||||
|
||||
|
||||
class EncodedFile(object):
|
||||
errors = "strict" # possibly needed by py3 code (issue555)
|
||||
def __init__(self, buffer, encoding):
|
||||
self.buffer = buffer
|
||||
self.encoding = encoding
|
||||
|
||||
def write(self, obj):
|
||||
if isinstance(obj, unicode):
|
||||
obj = obj.encode(self.encoding, "replace")
|
||||
self.buffer.write(obj)
|
||||
|
||||
def writelines(self, linelist):
|
||||
data = ''.join(linelist)
|
||||
self.write(data)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(object.__getattribute__(self, "buffer"), name)
|
||||
|
||||
|
||||
class MultiCapture(object):
|
||||
out = err = in_ = None
|
||||
|
||||
def __init__(self, out=True, err=True, in_=True, Capture=None):
|
||||
if in_:
|
||||
self.in_ = Capture(0)
|
||||
if out:
|
||||
self.out = Capture(1)
|
||||
if err:
|
||||
self.err = Capture(2)
|
||||
|
||||
def start_capturing(self):
|
||||
if self.in_:
|
||||
self.in_.start()
|
||||
if self.out:
|
||||
self.out.start()
|
||||
if self.err:
|
||||
self.err.start()
|
||||
|
||||
def pop_outerr_to_orig(self):
|
||||
""" pop current snapshot out/err capture and flush to orig streams. """
|
||||
out, err = self.readouterr()
|
||||
if out:
|
||||
self.out.writeorg(out)
|
||||
if err:
|
||||
self.err.writeorg(err)
|
||||
return out, err
|
||||
|
||||
def suspend_capturing(self, in_=False):
|
||||
if self.out:
|
||||
self.out.suspend()
|
||||
if self.err:
|
||||
self.err.suspend()
|
||||
if in_ and self.in_:
|
||||
self.in_.suspend()
|
||||
self._in_suspended = True
|
||||
|
||||
def resume_capturing(self):
|
||||
if self.out:
|
||||
self.out.resume()
|
||||
if self.err:
|
||||
self.err.resume()
|
||||
if hasattr(self, "_in_suspended"):
|
||||
self.in_.resume()
|
||||
del self._in_suspended
|
||||
|
||||
def stop_capturing(self):
|
||||
""" stop capturing and reset capturing streams """
|
||||
if hasattr(self, '_reset'):
|
||||
raise ValueError("was already stopped")
|
||||
self._reset = True
|
||||
if self.out:
|
||||
self.out.done()
|
||||
if self.err:
|
||||
self.err.done()
|
||||
if self.in_:
|
||||
self.in_.done()
|
||||
|
||||
def readouterr(self):
|
||||
""" return snapshot unicode value of stdout/stderr capturings. """
|
||||
return (self.out.snap() if self.out is not None else "",
|
||||
self.err.snap() if self.err is not None else "")
|
||||
|
||||
class NoCapture:
|
||||
__init__ = start = done = suspend = resume = lambda *args: None
|
||||
|
||||
class FDCapture:
|
||||
""" Capture IO to/from a given os-level filedescriptor. """
|
||||
|
||||
def __init__(self, targetfd, tmpfile=None):
|
||||
self.targetfd = targetfd
|
||||
try:
|
||||
self.targetfd_save = os.dup(self.targetfd)
|
||||
except OSError:
|
||||
self.start = lambda: None
|
||||
self.done = lambda: None
|
||||
else:
|
||||
if targetfd == 0:
|
||||
assert not tmpfile, "cannot set tmpfile with stdin"
|
||||
tmpfile = open(os.devnull, "r")
|
||||
self.syscapture = SysCapture(targetfd)
|
||||
else:
|
||||
if tmpfile is None:
|
||||
f = TemporaryFile()
|
||||
with f:
|
||||
tmpfile = safe_text_dupfile(f, mode="wb+")
|
||||
if targetfd in patchsysdict:
|
||||
self.syscapture = SysCapture(targetfd, tmpfile)
|
||||
else:
|
||||
self.syscapture = NoCapture()
|
||||
self.tmpfile = tmpfile
|
||||
self.tmpfile_fd = tmpfile.fileno()
|
||||
|
||||
def __repr__(self):
|
||||
return "<FDCapture %s oldfd=%s>" % (self.targetfd, self.targetfd_save)
|
||||
|
||||
def start(self):
|
||||
""" Start capturing on targetfd using memorized tmpfile. """
|
||||
try:
|
||||
os.fstat(self.targetfd_save)
|
||||
except (AttributeError, OSError):
|
||||
raise ValueError("saved filedescriptor not valid anymore")
|
||||
os.dup2(self.tmpfile_fd, self.targetfd)
|
||||
self.syscapture.start()
|
||||
|
||||
def snap(self):
|
||||
f = self.tmpfile
|
||||
f.seek(0)
|
||||
res = f.read()
|
||||
if res:
|
||||
enc = getattr(f, "encoding", None)
|
||||
if enc and isinstance(res, bytes):
|
||||
res = py.builtin._totext(res, enc, "replace")
|
||||
f.truncate(0)
|
||||
f.seek(0)
|
||||
return res
|
||||
return ''
|
||||
|
||||
def done(self):
|
||||
""" stop capturing, restore streams, return original capture file,
|
||||
seeked to position zero. """
|
||||
targetfd_save = self.__dict__.pop("targetfd_save")
|
||||
os.dup2(targetfd_save, self.targetfd)
|
||||
os.close(targetfd_save)
|
||||
self.syscapture.done()
|
||||
self.tmpfile.close()
|
||||
|
||||
def suspend(self):
|
||||
self.syscapture.suspend()
|
||||
os.dup2(self.targetfd_save, self.targetfd)
|
||||
|
||||
def resume(self):
|
||||
self.syscapture.resume()
|
||||
os.dup2(self.tmpfile_fd, self.targetfd)
|
||||
|
||||
def writeorg(self, data):
|
||||
""" write to original file descriptor. """
|
||||
if py.builtin._istext(data):
|
||||
data = data.encode("utf8") # XXX use encoding of original stream
|
||||
os.write(self.targetfd_save, data)
|
||||
|
||||
|
||||
class SysCapture:
|
||||
def __init__(self, fd, tmpfile=None):
|
||||
name = patchsysdict[fd]
|
||||
self._old = getattr(sys, name)
|
||||
self.name = name
|
||||
if tmpfile is None:
|
||||
if name == "stdin":
|
||||
tmpfile = DontReadFromInput()
|
||||
else:
|
||||
tmpfile = TextIO()
|
||||
self.tmpfile = tmpfile
|
||||
|
||||
def start(self):
|
||||
setattr(sys, self.name, self.tmpfile)
|
||||
|
||||
def snap(self):
|
||||
f = self.tmpfile
|
||||
res = f.getvalue()
|
||||
f.truncate(0)
|
||||
f.seek(0)
|
||||
return res
|
||||
|
||||
def done(self):
|
||||
setattr(sys, self.name, self._old)
|
||||
del self._old
|
||||
self.tmpfile.close()
|
||||
|
||||
def suspend(self):
|
||||
setattr(sys, self.name, self._old)
|
||||
|
||||
def resume(self):
|
||||
setattr(sys, self.name, self.tmpfile)
|
||||
|
||||
def writeorg(self, data):
|
||||
self._old.write(data)
|
||||
self._old.flush()
|
||||
|
||||
|
||||
class DontReadFromInput:
|
||||
"""Temporary stub class. Ideally when stdin is accessed, the
|
||||
capturing should be turned off, with possibly all data captured
|
||||
so far sent to the screen. This should be configurable, though,
|
||||
because in automated test runs it is better to crash than
|
||||
hang indefinitely.
|
||||
"""
|
||||
|
||||
encoding = None
|
||||
|
||||
def read(self, *args):
|
||||
raise IOError("reading from stdin while output is captured")
|
||||
readline = read
|
||||
readlines = read
|
||||
__iter__ = read
|
||||
|
||||
def fileno(self):
|
||||
raise ValueError("redirected Stdin is pseudofile, has no fileno()")
|
||||
|
||||
def isatty(self):
|
||||
return False
|
||||
|
||||
def close(self):
|
||||
self._finalize()
|
||||
pass
|
||||
|
||||
1095
_pytest/config.py
1095
_pytest/config.py
File diff suppressed because it is too large
Load Diff
498
_pytest/core.py
498
_pytest/core.py
@@ -1,498 +0,0 @@
|
||||
"""
|
||||
pytest PluginManager, basic initialization and tracing.
|
||||
(c) Holger Krekel 2004-2010
|
||||
"""
|
||||
import sys, os
|
||||
import inspect
|
||||
import py
|
||||
from _pytest import hookspec # the extension point definitions
|
||||
|
||||
assert py.__version__.split(".")[:2] >= ['1', '4'], ("installation problem: "
|
||||
"%s is too old, remove or upgrade 'py'" % (py.__version__))
|
||||
|
||||
default_plugins = (
|
||||
"config mark main terminal runner python pdb unittest capture skipping "
|
||||
"tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript "
|
||||
"junitxml resultlog doctest").split()
|
||||
|
||||
class TagTracer:
|
||||
def __init__(self):
|
||||
self._tag2proc = {}
|
||||
self.writer = None
|
||||
self.indent = 0
|
||||
|
||||
def get(self, name):
|
||||
return TagTracerSub(self, (name,))
|
||||
|
||||
def format_message(self, tags, args):
|
||||
if isinstance(args[-1], dict):
|
||||
extra = args[-1]
|
||||
args = args[:-1]
|
||||
else:
|
||||
extra = {}
|
||||
|
||||
content = " ".join(map(str, args))
|
||||
indent = " " * self.indent
|
||||
|
||||
lines = [
|
||||
"%s%s [%s]\n" %(indent, content, ":".join(tags))
|
||||
]
|
||||
|
||||
for name, value in extra.items():
|
||||
lines.append("%s %s: %s\n" % (indent, name, value))
|
||||
return lines
|
||||
|
||||
def processmessage(self, tags, args):
|
||||
if self.writer is not None and args:
|
||||
lines = self.format_message(tags, args)
|
||||
self.writer(''.join(lines))
|
||||
try:
|
||||
self._tag2proc[tags](tags, args)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def setwriter(self, writer):
|
||||
self.writer = writer
|
||||
|
||||
def setprocessor(self, tags, processor):
|
||||
if isinstance(tags, str):
|
||||
tags = tuple(tags.split(":"))
|
||||
else:
|
||||
assert isinstance(tags, tuple)
|
||||
self._tag2proc[tags] = processor
|
||||
|
||||
class TagTracerSub:
|
||||
def __init__(self, root, tags):
|
||||
self.root = root
|
||||
self.tags = tags
|
||||
def __call__(self, *args):
|
||||
self.root.processmessage(self.tags, args)
|
||||
def setmyprocessor(self, processor):
|
||||
self.root.setprocessor(self.tags, processor)
|
||||
def get(self, name):
|
||||
return self.__class__(self.root, self.tags + (name,))
|
||||
|
||||
class PluginManager(object):
|
||||
def __init__(self, load=False):
|
||||
self._name2plugin = {}
|
||||
self._listattrcache = {}
|
||||
self._plugins = []
|
||||
self._hints = []
|
||||
self.trace = TagTracer().get("pluginmanage")
|
||||
self._plugin_distinfo = []
|
||||
if os.environ.get('PYTEST_DEBUG'):
|
||||
err = sys.stderr
|
||||
encoding = getattr(err, 'encoding', 'utf8')
|
||||
try:
|
||||
err = py.io.dupfile(err, encoding=encoding)
|
||||
except Exception:
|
||||
pass
|
||||
self.trace.root.setwriter(err.write)
|
||||
self.hook = HookRelay([hookspec], pm=self)
|
||||
self.register(self)
|
||||
if load:
|
||||
for spec in default_plugins:
|
||||
self.import_plugin(spec)
|
||||
|
||||
def register(self, plugin, name=None, prepend=False):
|
||||
if self._name2plugin.get(name, None) == -1:
|
||||
return
|
||||
name = name or getattr(plugin, '__name__', str(id(plugin)))
|
||||
if self.isregistered(plugin, name):
|
||||
raise ValueError("Plugin already registered: %s=%s" %(name, plugin))
|
||||
#self.trace("registering", name, plugin)
|
||||
self._name2plugin[name] = plugin
|
||||
self.call_plugin(plugin, "pytest_addhooks", {'pluginmanager': self})
|
||||
self.hook.pytest_plugin_registered(manager=self, plugin=plugin)
|
||||
if not prepend:
|
||||
self._plugins.append(plugin)
|
||||
else:
|
||||
self._plugins.insert(0, plugin)
|
||||
return True
|
||||
|
||||
def unregister(self, plugin=None, name=None):
|
||||
if plugin is None:
|
||||
plugin = self.getplugin(name=name)
|
||||
self._plugins.remove(plugin)
|
||||
self.hook.pytest_plugin_unregistered(plugin=plugin)
|
||||
for name, value in list(self._name2plugin.items()):
|
||||
if value == plugin:
|
||||
del self._name2plugin[name]
|
||||
|
||||
def isregistered(self, plugin, name=None):
|
||||
if self.getplugin(name) is not None:
|
||||
return True
|
||||
for val in self._name2plugin.values():
|
||||
if plugin == val:
|
||||
return True
|
||||
|
||||
def addhooks(self, spec):
|
||||
self.hook._addhooks(spec, prefix="pytest_")
|
||||
|
||||
def getplugins(self):
|
||||
return list(self._plugins)
|
||||
|
||||
def skipifmissing(self, name):
|
||||
if not self.hasplugin(name):
|
||||
py.test.skip("plugin %r is missing" % name)
|
||||
|
||||
def hasplugin(self, name):
|
||||
return bool(self.getplugin(name))
|
||||
|
||||
def getplugin(self, name):
|
||||
if name is None:
|
||||
return None
|
||||
try:
|
||||
return self._name2plugin[name]
|
||||
except KeyError:
|
||||
return self._name2plugin.get("_pytest." + name, None)
|
||||
|
||||
# API for bootstrapping
|
||||
#
|
||||
def _envlist(self, varname):
|
||||
val = py.std.os.environ.get(varname, None)
|
||||
if val is not None:
|
||||
return val.split(',')
|
||||
return ()
|
||||
|
||||
def consider_env(self):
|
||||
for spec in self._envlist("PYTEST_PLUGINS"):
|
||||
self.import_plugin(spec)
|
||||
|
||||
def consider_setuptools_entrypoints(self):
|
||||
try:
|
||||
from pkg_resources import iter_entry_points, DistributionNotFound
|
||||
except ImportError:
|
||||
return # XXX issue a warning
|
||||
for ep in iter_entry_points('pytest11'):
|
||||
name = ep.name
|
||||
if name.startswith("pytest_"):
|
||||
name = name[7:]
|
||||
if ep.name in self._name2plugin or name in self._name2plugin:
|
||||
continue
|
||||
try:
|
||||
plugin = ep.load()
|
||||
except DistributionNotFound:
|
||||
continue
|
||||
self._plugin_distinfo.append((ep.dist, plugin))
|
||||
self.register(plugin, name=name)
|
||||
|
||||
def consider_preparse(self, args):
|
||||
for opt1,opt2 in zip(args, args[1:]):
|
||||
if opt1 == "-p":
|
||||
self.consider_pluginarg(opt2)
|
||||
|
||||
def consider_pluginarg(self, arg):
|
||||
if arg.startswith("no:"):
|
||||
name = arg[3:]
|
||||
if self.getplugin(name) is not None:
|
||||
self.unregister(None, name=name)
|
||||
self._name2plugin[name] = -1
|
||||
else:
|
||||
if self.getplugin(arg) is None:
|
||||
self.import_plugin(arg)
|
||||
|
||||
def consider_conftest(self, conftestmodule):
|
||||
if self.register(conftestmodule, name=conftestmodule.__file__):
|
||||
self.consider_module(conftestmodule)
|
||||
|
||||
def consider_module(self, mod):
|
||||
attr = getattr(mod, "pytest_plugins", ())
|
||||
if attr:
|
||||
if not isinstance(attr, (list, tuple)):
|
||||
attr = (attr,)
|
||||
for spec in attr:
|
||||
self.import_plugin(spec)
|
||||
|
||||
def import_plugin(self, modname):
|
||||
assert isinstance(modname, str)
|
||||
if self.getplugin(modname) is not None:
|
||||
return
|
||||
try:
|
||||
#self.trace("importing", modname)
|
||||
mod = importplugin(modname)
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except ImportError:
|
||||
if modname.startswith("pytest_"):
|
||||
return self.import_plugin(modname[7:])
|
||||
raise
|
||||
except:
|
||||
e = py.std.sys.exc_info()[1]
|
||||
if not hasattr(py.test, 'skip'):
|
||||
raise
|
||||
elif not isinstance(e, py.test.skip.Exception):
|
||||
raise
|
||||
self._hints.append("skipped plugin %r: %s" %((modname, e.msg)))
|
||||
else:
|
||||
self.register(mod, modname)
|
||||
self.consider_module(mod)
|
||||
|
||||
def pytest_configure(self, config):
|
||||
config.addinivalue_line("markers",
|
||||
"tryfirst: mark a hook implementation function such that the "
|
||||
"plugin machinery will try to call it first/as early as possible.")
|
||||
config.addinivalue_line("markers",
|
||||
"trylast: mark a hook implementation function such that the "
|
||||
"plugin machinery will try to call it last/as late as possible.")
|
||||
|
||||
def pytest_plugin_registered(self, plugin):
|
||||
import pytest
|
||||
dic = self.call_plugin(plugin, "pytest_namespace", {}) or {}
|
||||
if dic:
|
||||
self._setns(pytest, dic)
|
||||
if hasattr(self, '_config'):
|
||||
self.call_plugin(plugin, "pytest_addoption",
|
||||
{'parser': self._config._parser})
|
||||
self.call_plugin(plugin, "pytest_configure",
|
||||
{'config': self._config})
|
||||
|
||||
def _setns(self, obj, dic):
|
||||
import pytest
|
||||
for name, value in dic.items():
|
||||
if isinstance(value, dict):
|
||||
mod = getattr(obj, name, None)
|
||||
if mod is None:
|
||||
modname = "pytest.%s" % name
|
||||
mod = py.std.types.ModuleType(modname)
|
||||
sys.modules[modname] = mod
|
||||
mod.__all__ = []
|
||||
setattr(obj, name, mod)
|
||||
obj.__all__.append(name)
|
||||
self._setns(mod, value)
|
||||
else:
|
||||
setattr(obj, name, value)
|
||||
obj.__all__.append(name)
|
||||
#if obj != pytest:
|
||||
# pytest.__all__.append(name)
|
||||
setattr(pytest, name, value)
|
||||
|
||||
def pytest_terminal_summary(self, terminalreporter):
|
||||
tw = terminalreporter._tw
|
||||
if terminalreporter.config.option.traceconfig:
|
||||
for hint in self._hints:
|
||||
tw.line("hint: %s" % hint)
|
||||
|
||||
def do_addoption(self, parser):
|
||||
mname = "pytest_addoption"
|
||||
methods = reversed(self.listattr(mname))
|
||||
MultiCall(methods, {'parser': parser}).execute()
|
||||
|
||||
def do_configure(self, config):
|
||||
assert not hasattr(self, '_config')
|
||||
self._config = config
|
||||
config.hook.pytest_configure(config=self._config)
|
||||
|
||||
def do_unconfigure(self, config):
|
||||
config = self._config
|
||||
del self._config
|
||||
config.hook.pytest_unconfigure(config=config)
|
||||
config.pluginmanager.unregister(self)
|
||||
|
||||
def notify_exception(self, excinfo, option=None):
|
||||
if option and option.fulltrace:
|
||||
style = "long"
|
||||
else:
|
||||
style = "native"
|
||||
excrepr = excinfo.getrepr(funcargs=True,
|
||||
showlocals=getattr(option, 'showlocals', False),
|
||||
style=style,
|
||||
)
|
||||
res = self.hook.pytest_internalerror(excrepr=excrepr)
|
||||
if not py.builtin.any(res):
|
||||
for line in str(excrepr).split("\n"):
|
||||
sys.stderr.write("INTERNALERROR> %s\n" %line)
|
||||
sys.stderr.flush()
|
||||
|
||||
def listattr(self, attrname, plugins=None):
|
||||
if plugins is None:
|
||||
plugins = self._plugins
|
||||
key = (attrname,) + tuple(plugins)
|
||||
try:
|
||||
return list(self._listattrcache[key])
|
||||
except KeyError:
|
||||
pass
|
||||
l = []
|
||||
last = []
|
||||
for plugin in plugins:
|
||||
try:
|
||||
meth = getattr(plugin, attrname)
|
||||
if hasattr(meth, 'tryfirst'):
|
||||
last.append(meth)
|
||||
elif hasattr(meth, 'trylast'):
|
||||
l.insert(0, meth)
|
||||
else:
|
||||
l.append(meth)
|
||||
except AttributeError:
|
||||
continue
|
||||
l.extend(last)
|
||||
self._listattrcache[key] = list(l)
|
||||
return l
|
||||
|
||||
def call_plugin(self, plugin, methname, kwargs):
|
||||
return MultiCall(methods=self.listattr(methname, plugins=[plugin]),
|
||||
kwargs=kwargs, firstresult=True).execute()
|
||||
|
||||
|
||||
def importplugin(importspec):
|
||||
name = importspec
|
||||
try:
|
||||
mod = "_pytest." + name
|
||||
__import__(mod)
|
||||
return sys.modules[mod]
|
||||
except ImportError:
|
||||
#e = py.std.sys.exc_info()[1]
|
||||
#if str(e).find(name) == -1:
|
||||
# raise
|
||||
pass #
|
||||
try:
|
||||
__import__(importspec)
|
||||
except ImportError:
|
||||
raise ImportError(importspec)
|
||||
return sys.modules[importspec]
|
||||
|
||||
class MultiCall:
|
||||
""" execute a call into multiple python functions/methods. """
|
||||
def __init__(self, methods, kwargs, firstresult=False):
|
||||
self.methods = list(methods)
|
||||
self.kwargs = kwargs
|
||||
self.results = []
|
||||
self.firstresult = firstresult
|
||||
|
||||
def __repr__(self):
|
||||
status = "%d results, %d meths" % (len(self.results), len(self.methods))
|
||||
return "<MultiCall %s, kwargs=%r>" %(status, self.kwargs)
|
||||
|
||||
def execute(self):
|
||||
while self.methods:
|
||||
method = self.methods.pop()
|
||||
kwargs = self.getkwargs(method)
|
||||
res = method(**kwargs)
|
||||
if res is not None:
|
||||
self.results.append(res)
|
||||
if self.firstresult:
|
||||
return res
|
||||
if not self.firstresult:
|
||||
return self.results
|
||||
|
||||
def getkwargs(self, method):
|
||||
kwargs = {}
|
||||
for argname in varnames(method):
|
||||
try:
|
||||
kwargs[argname] = self.kwargs[argname]
|
||||
except KeyError:
|
||||
if argname == "__multicall__":
|
||||
kwargs[argname] = self
|
||||
return kwargs
|
||||
|
||||
def varnames(func):
|
||||
try:
|
||||
return func._varnames
|
||||
except AttributeError:
|
||||
pass
|
||||
if not inspect.isfunction(func) and not inspect.ismethod(func):
|
||||
func = getattr(func, '__call__', func)
|
||||
ismethod = inspect.ismethod(func)
|
||||
rawcode = py.code.getrawcode(func)
|
||||
try:
|
||||
x = rawcode.co_varnames[ismethod:rawcode.co_argcount]
|
||||
except AttributeError:
|
||||
x = ()
|
||||
py.builtin._getfuncdict(func)['_varnames'] = x
|
||||
return x
|
||||
|
||||
class HookRelay:
|
||||
def __init__(self, hookspecs, pm, prefix="pytest_"):
|
||||
if not isinstance(hookspecs, list):
|
||||
hookspecs = [hookspecs]
|
||||
self._hookspecs = []
|
||||
self._pm = pm
|
||||
self.trace = pm.trace.root.get("hook")
|
||||
for hookspec in hookspecs:
|
||||
self._addhooks(hookspec, prefix)
|
||||
|
||||
def _addhooks(self, hookspecs, prefix):
|
||||
self._hookspecs.append(hookspecs)
|
||||
added = False
|
||||
for name, method in vars(hookspecs).items():
|
||||
if name.startswith(prefix):
|
||||
firstresult = getattr(method, 'firstresult', False)
|
||||
hc = HookCaller(self, name, firstresult=firstresult)
|
||||
setattr(self, name, hc)
|
||||
added = True
|
||||
#print ("setting new hook", name)
|
||||
if not added:
|
||||
raise ValueError("did not find new %r hooks in %r" %(
|
||||
prefix, hookspecs,))
|
||||
|
||||
|
||||
class HookCaller:
|
||||
def __init__(self, hookrelay, name, firstresult):
|
||||
self.hookrelay = hookrelay
|
||||
self.name = name
|
||||
self.firstresult = firstresult
|
||||
self.trace = self.hookrelay.trace
|
||||
|
||||
def __repr__(self):
|
||||
return "<HookCaller %r>" %(self.name,)
|
||||
|
||||
def __call__(self, **kwargs):
|
||||
methods = self.hookrelay._pm.listattr(self.name)
|
||||
return self._docall(methods, kwargs)
|
||||
|
||||
def pcall(self, plugins, **kwargs):
|
||||
methods = self.hookrelay._pm.listattr(self.name, plugins=plugins)
|
||||
return self._docall(methods, kwargs)
|
||||
|
||||
def _docall(self, methods, kwargs):
|
||||
self.trace(self.name, kwargs)
|
||||
self.trace.root.indent += 1
|
||||
mc = MultiCall(methods, kwargs, firstresult=self.firstresult)
|
||||
try:
|
||||
res = mc.execute()
|
||||
if res:
|
||||
self.trace("finish", self.name, "-->", res)
|
||||
finally:
|
||||
self.trace.root.indent -= 1
|
||||
return res
|
||||
|
||||
_preinit = []
|
||||
|
||||
def _preloadplugins():
|
||||
_preinit.append(PluginManager(load=True))
|
||||
|
||||
def _prepareconfig(args=None, plugins=None):
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
elif isinstance(args, py.path.local):
|
||||
args = [str(args)]
|
||||
elif not isinstance(args, (tuple, list)):
|
||||
if not isinstance(args, str):
|
||||
raise ValueError("not a string or argument list: %r" % (args,))
|
||||
args = py.std.shlex.split(args)
|
||||
if _preinit:
|
||||
_pluginmanager = _preinit.pop(0)
|
||||
else: # subsequent calls to main will create a fresh instance
|
||||
_pluginmanager = PluginManager(load=True)
|
||||
hook = _pluginmanager.hook
|
||||
if plugins:
|
||||
for plugin in plugins:
|
||||
_pluginmanager.register(plugin)
|
||||
return hook.pytest_cmdline_parse(
|
||||
pluginmanager=_pluginmanager, args=args)
|
||||
|
||||
def main(args=None, plugins=None):
|
||||
""" return exit code, after performing an in-process test run.
|
||||
|
||||
:arg args: list of command line arguments.
|
||||
|
||||
:arg plugins: list of plugin objects to be auto-registered during
|
||||
initialization.
|
||||
"""
|
||||
config = _prepareconfig(args, plugins)
|
||||
exitstatus = config.hook.pytest_cmdline_main(config=config)
|
||||
return exitstatus
|
||||
|
||||
class UsageError(Exception):
|
||||
""" error in py.test usage or invocation"""
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
""" discover and run doctests in modules and test files."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
import traceback
|
||||
import pytest, py
|
||||
from _pytest.python import FixtureRequest, FuncFixtureInfo
|
||||
from _pytest.python import FixtureRequest
|
||||
from py._code.code import TerminalRepr, ReprFileLocation
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addini('doctest_optionflags', 'option flags for doctests',
|
||||
type="args", default=["ELLIPSIS"])
|
||||
group = parser.getgroup("collect")
|
||||
group.addoption("--doctest-modules",
|
||||
action="store_true", default=False,
|
||||
@@ -14,6 +17,10 @@ def pytest_addoption(parser):
|
||||
action="store", default="test*.txt", metavar="pat",
|
||||
help="doctests file matching pattern, default: test*.txt",
|
||||
dest="doctestglob")
|
||||
group.addoption("--doctest-ignore-import-errors",
|
||||
action="store_true", default=False,
|
||||
help="ignore doctest ImportErrors",
|
||||
dest="doctest_ignore_import_errors")
|
||||
|
||||
def pytest_collect_file(path, parent):
|
||||
config = parent.config
|
||||
@@ -34,8 +41,17 @@ class ReprFailDoctest(TerminalRepr):
|
||||
self.reprlocation.toterminal(tw)
|
||||
|
||||
class DoctestItem(pytest.Item):
|
||||
def __init__(self, name, parent, runner=None, dtest=None):
|
||||
super(DoctestItem, self).__init__(name, parent)
|
||||
self.runner = runner
|
||||
self.dtest = dtest
|
||||
|
||||
def runtest(self):
|
||||
_check_all_skipped(self.dtest)
|
||||
self.runner.run(self.dtest)
|
||||
|
||||
def repr_failure(self, excinfo):
|
||||
doctest = py.std.doctest
|
||||
import doctest
|
||||
if excinfo.errisinstance((doctest.DocTestFailure,
|
||||
doctest.UnexpectedException)):
|
||||
doctestfailure = excinfo.value
|
||||
@@ -48,8 +64,8 @@ class DoctestItem(pytest.Item):
|
||||
lineno = test.lineno + example.lineno + 1
|
||||
message = excinfo.type.__name__
|
||||
reprlocation = ReprFileLocation(filename, lineno, message)
|
||||
checker = py.std.doctest.OutputChecker()
|
||||
REPORT_UDIFF = py.std.doctest.REPORT_UDIFF
|
||||
checker = _get_unicode_checker()
|
||||
REPORT_UDIFF = doctest.REPORT_UDIFF
|
||||
filelines = py.path.local(filename).readlines(cr=0)
|
||||
lines = []
|
||||
if lineno is not None:
|
||||
@@ -70,39 +86,162 @@ class DoctestItem(pytest.Item):
|
||||
inner_excinfo = py.code.ExceptionInfo(excinfo.value.exc_info)
|
||||
lines += ["UNEXPECTED EXCEPTION: %s" %
|
||||
repr(inner_excinfo.value)]
|
||||
lines += py.std.traceback.format_exception(*excinfo.value.exc_info)
|
||||
lines += traceback.format_exception(*excinfo.value.exc_info)
|
||||
return ReprFailDoctest(reprlocation, lines)
|
||||
else:
|
||||
return super(DoctestItem, self).repr_failure(excinfo)
|
||||
|
||||
def reportinfo(self):
|
||||
return self.fspath, None, "[doctest]"
|
||||
return self.fspath, None, "[doctest] %s" % self.name
|
||||
|
||||
def _get_flag_lookup():
|
||||
import doctest
|
||||
return dict(DONT_ACCEPT_TRUE_FOR_1=doctest.DONT_ACCEPT_TRUE_FOR_1,
|
||||
DONT_ACCEPT_BLANKLINE=doctest.DONT_ACCEPT_BLANKLINE,
|
||||
NORMALIZE_WHITESPACE=doctest.NORMALIZE_WHITESPACE,
|
||||
ELLIPSIS=doctest.ELLIPSIS,
|
||||
IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL,
|
||||
COMPARISON_FLAGS=doctest.COMPARISON_FLAGS,
|
||||
ALLOW_UNICODE=_get_allow_unicode_flag())
|
||||
|
||||
def get_optionflags(parent):
|
||||
optionflags_str = parent.config.getini("doctest_optionflags")
|
||||
flag_lookup_table = _get_flag_lookup()
|
||||
flag_acc = 0
|
||||
for flag in optionflags_str:
|
||||
flag_acc |= flag_lookup_table[flag]
|
||||
return flag_acc
|
||||
|
||||
|
||||
class DoctestTextfile(DoctestItem, pytest.File):
|
||||
def runtest(self):
|
||||
doctest = py.std.doctest
|
||||
# satisfy `FixtureRequest` constructor...
|
||||
self.funcargs = {}
|
||||
self._fixtureinfo = FuncFixtureInfo((), [], {})
|
||||
fixture_request = FixtureRequest(self)
|
||||
failed, tot = doctest.testfile(
|
||||
str(self.fspath), module_relative=False,
|
||||
optionflags=doctest.ELLIPSIS,
|
||||
extraglobs=dict(getfixture=fixture_request.getfuncargvalue),
|
||||
raise_on_error=True, verbose=0)
|
||||
|
||||
class DoctestModule(DoctestItem, pytest.File):
|
||||
def runtest(self):
|
||||
doctest = py.std.doctest
|
||||
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__'
|
||||
|
||||
optionflags = get_optionflags(self)
|
||||
runner = doctest.DebugRunner(verbose=0, optionflags=optionflags,
|
||||
checker=_get_unicode_checker())
|
||||
|
||||
parser = doctest.DocTestParser()
|
||||
test = parser.get_doctest(text, globs, name, filename, 0)
|
||||
_check_all_skipped(test)
|
||||
runner.run(test)
|
||||
|
||||
|
||||
def _check_all_skipped(test):
|
||||
"""raises pytest.skip() if all examples in the given DocTest have the SKIP
|
||||
option set.
|
||||
"""
|
||||
import doctest
|
||||
all_skipped = all(x.options.get(doctest.SKIP, False) for x in test.examples)
|
||||
if all_skipped:
|
||||
pytest.skip('all tests skipped by +SKIP option')
|
||||
|
||||
|
||||
class DoctestModule(pytest.File):
|
||||
def collect(self):
|
||||
import doctest
|
||||
if self.fspath.basename == "conftest.py":
|
||||
module = self.config._conftest.importconftest(self.fspath)
|
||||
module = self.config.pluginmanager._importconftest(self.fspath)
|
||||
else:
|
||||
module = self.fspath.pyimport()
|
||||
try:
|
||||
module = self.fspath.pyimport()
|
||||
except ImportError:
|
||||
if self.config.getvalue('doctest_ignore_import_errors'):
|
||||
pytest.skip('unable to import module %r' % self.fspath)
|
||||
else:
|
||||
raise
|
||||
# satisfy `FixtureRequest` constructor...
|
||||
self.funcargs = {}
|
||||
self._fixtureinfo = FuncFixtureInfo((), [], {})
|
||||
fixture_request = FixtureRequest(self)
|
||||
failed, tot = doctest.testmod(
|
||||
module, raise_on_error=True, verbose=0,
|
||||
extraglobs=dict(getfixture=fixture_request.getfuncargvalue),
|
||||
optionflags=doctest.ELLIPSIS)
|
||||
fixture_request = _setup_fixtures(self)
|
||||
doctest_globals = dict(getfixture=fixture_request.getfuncargvalue)
|
||||
# uses internal doctest module parsing mechanism
|
||||
finder = doctest.DocTestFinder()
|
||||
optionflags = get_optionflags(self)
|
||||
runner = doctest.DebugRunner(verbose=0, optionflags=optionflags,
|
||||
checker=_get_unicode_checker())
|
||||
for test in finder.find(module, module.__name__,
|
||||
extraglobs=doctest_globals):
|
||||
if test.examples: # skip empty doctests
|
||||
yield DoctestItem(test.name, self, runner, test)
|
||||
|
||||
|
||||
def _setup_fixtures(doctest_item):
|
||||
"""
|
||||
Used by DoctestTextfile and DoctestModule to setup fixture information.
|
||||
"""
|
||||
def func():
|
||||
pass
|
||||
|
||||
doctest_item.funcargs = {}
|
||||
fm = doctest_item.session._fixturemanager
|
||||
doctest_item._fixtureinfo = fm.getfixtureinfo(node=doctest_item, func=func,
|
||||
cls=None, funcargs=False)
|
||||
fixture_request = FixtureRequest(doctest_item)
|
||||
fixture_request._fillfixtures()
|
||||
return fixture_request
|
||||
|
||||
|
||||
def _get_unicode_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.
|
||||
|
||||
An inner class is used to avoid importing "doctest" at the module
|
||||
level.
|
||||
"""
|
||||
if hasattr(_get_unicode_checker, 'UnicodeOutputChecker'):
|
||||
return _get_unicode_checker.UnicodeOutputChecker()
|
||||
|
||||
import doctest
|
||||
import re
|
||||
|
||||
class UnicodeOutputChecker(doctest.OutputChecker):
|
||||
"""
|
||||
Copied from doctest_nose_plugin.py from the nltk project:
|
||||
https://github.com/nltk/nltk
|
||||
"""
|
||||
|
||||
_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE)
|
||||
|
||||
def check_output(self, want, got, optionflags):
|
||||
res = doctest.OutputChecker.check_output(self, want, got,
|
||||
optionflags)
|
||||
if res:
|
||||
return True
|
||||
|
||||
if not (optionflags & _get_allow_unicode_flag()):
|
||||
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)
|
||||
|
||||
want = remove_u_prefixes(want)
|
||||
got = remove_u_prefixes(got)
|
||||
res = doctest.OutputChecker.check_output(self, want, got,
|
||||
optionflags)
|
||||
return res
|
||||
|
||||
_get_unicode_checker.UnicodeOutputChecker = UnicodeOutputChecker
|
||||
return _get_unicode_checker.UnicodeOutputChecker()
|
||||
|
||||
|
||||
def _get_allow_unicode_flag():
|
||||
"""
|
||||
Registers and returns the ALLOW_UNICODE flag.
|
||||
"""
|
||||
import doctest
|
||||
return doctest.register_optionflag('ALLOW_UNICODE')
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
""" generate a single-file self-contained version of py.test """
|
||||
""" (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 py.std.sys.path:
|
||||
for syspath in sys.path:
|
||||
base = py.path.local(syspath)
|
||||
lib = base/name
|
||||
if lib.check(dir=1):
|
||||
@@ -25,12 +32,16 @@ def pkg_to_mapping(name):
|
||||
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):
|
||||
data = py.std.pickle.dumps(mapping, 2)
|
||||
data = py.std.zlib.compress(data, 9)
|
||||
data = py.std.base64.encodestring(data)
|
||||
import base64, pickle, zlib
|
||||
data = pickle.dumps(mapping, 2)
|
||||
data = zlib.compress(data, 9)
|
||||
data = base64.encodestring(data)
|
||||
data = data.decode('ascii')
|
||||
return data
|
||||
|
||||
@@ -54,16 +65,68 @@ def pytest_addoption(parser):
|
||||
group = parser.getgroup("debugconfig")
|
||||
group.addoption("--genscript", action="store", default=None,
|
||||
dest="genscript", metavar="path",
|
||||
help="create standalone py.test script at given target 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 py; raise SystemExit(py.test.cmdline.main())',
|
||||
['py', '_pytest', 'pytest'],
|
||||
'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
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
""" version info, help messages, tracing configuration. """
|
||||
import py
|
||||
import pytest
|
||||
import os, inspect, sys
|
||||
from _pytest.core import varnames
|
||||
import os, sys
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup('debugconfig')
|
||||
@@ -12,41 +11,44 @@ def pytest_addoption(parser):
|
||||
help="show help message and configuration info")
|
||||
group._addoption('-p', action="append", dest="plugins", default = [],
|
||||
metavar="name",
|
||||
help="early-load given plugin (multi-allowed).")
|
||||
group.addoption('--traceconfig',
|
||||
action="store_true", dest="traceconfig", default=False,
|
||||
help="early-load given plugin (multi-allowed). "
|
||||
"To avoid loading of plugins, use the `no:` prefix, e.g. "
|
||||
"`no:doctest`.")
|
||||
group.addoption('--traceconfig', '--trace-config',
|
||||
action="store_true", default=False,
|
||||
help="trace considerations of conftest.py files."),
|
||||
group.addoption('--debug',
|
||||
action="store_true", dest="debug", default=False,
|
||||
help="store internal tracing debug information in 'pytestdebug.log'.")
|
||||
|
||||
|
||||
def pytest_cmdline_parse(__multicall__):
|
||||
config = __multicall__.execute()
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_cmdline_parse():
|
||||
outcome = yield
|
||||
config = outcome.get_result()
|
||||
if config.option.debug:
|
||||
path = os.path.abspath("pytestdebug.log")
|
||||
f = open(path, 'w')
|
||||
config._debugfile = f
|
||||
f.write("versions pytest-%s, py-%s, python-%s\ncwd=%s\nargs=%s\n\n" %(
|
||||
pytest.__version__, py.__version__, ".".join(map(str, sys.version_info)),
|
||||
debugfile = open(path, 'w')
|
||||
debugfile.write("versions pytest-%s, py-%s, "
|
||||
"python-%s\ncwd=%s\nargs=%s\n\n" %(
|
||||
pytest.__version__, py.__version__,
|
||||
".".join(map(str, sys.version_info)),
|
||||
os.getcwd(), config._origargs))
|
||||
config.trace.root.setwriter(f.write)
|
||||
config.trace.root.setwriter(debugfile.write)
|
||||
undo_tracing = config.pluginmanager.enable_tracing()
|
||||
sys.stderr.write("writing pytestdebug information to %s\n" % path)
|
||||
return config
|
||||
|
||||
@pytest.mark.trylast
|
||||
def pytest_unconfigure(config):
|
||||
if hasattr(config, '_debugfile'):
|
||||
config._debugfile.close()
|
||||
sys.stderr.write("wrote pytestdebug information to %s\n" %
|
||||
config._debugfile.name)
|
||||
config.trace.root.setwriter(None)
|
||||
|
||||
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):
|
||||
if config.option.version:
|
||||
p = py.path.local(pytest.__file__)
|
||||
sys.stderr.write("This is py.test version %s, imported from %s\n" %
|
||||
sys.stderr.write("This is pytest version %s, imported from %s\n" %
|
||||
(pytest.__version__, p))
|
||||
plugininfo = getpluginversioninfo(config)
|
||||
if plugininfo:
|
||||
@@ -54,13 +56,14 @@ def pytest_cmdline_main(config):
|
||||
sys.stderr.write(line + "\n")
|
||||
return 0
|
||||
elif config.option.help:
|
||||
config.pluginmanager.do_configure(config)
|
||||
config._do_configure()
|
||||
showhelp(config)
|
||||
config.pluginmanager.do_unconfigure(config)
|
||||
config._ensure_unconfigure()
|
||||
return 0
|
||||
|
||||
def showhelp(config):
|
||||
tw = py.io.TerminalWriter()
|
||||
reporter = config.pluginmanager.get_plugin('terminalreporter')
|
||||
tw = reporter._tw
|
||||
tw.write(config._parser.optparser.format_help())
|
||||
tw.line()
|
||||
tw.line()
|
||||
@@ -77,20 +80,27 @@ def showhelp(config):
|
||||
line = " %-24s %s" %(spec, help)
|
||||
tw.line(line[:tw.fullwidth])
|
||||
|
||||
tw.line() ; tw.line()
|
||||
#tw.sep("=")
|
||||
tw.line()
|
||||
tw.line("environment variables:")
|
||||
vars = [
|
||||
("PYTEST_ADDOPTS", "extra command line options"),
|
||||
("PYTEST_PLUGINS", "comma-separated plugins to load during startup"),
|
||||
("PYTEST_DEBUG", "set to enable debug tracing of pytest's internals")
|
||||
]
|
||||
for name, help in vars:
|
||||
tw.line(" %-24s %s" % (name, help))
|
||||
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("(shown according to specified file_or_dir or current dir "
|
||||
"if not specified)")
|
||||
|
||||
for warningreport in reporter.stats.get('warnings', []):
|
||||
tw.line("warning : " + warningreport.message, red=True)
|
||||
return
|
||||
|
||||
tw.line("conftest.py options:")
|
||||
tw.line()
|
||||
conftestitems = sorted(config._parser._conftestdict.items())
|
||||
for name, help in conftest_options + conftestitems:
|
||||
line = " %-15s %s" %(name, help)
|
||||
tw.line(line[:tw.fullwidth])
|
||||
tw.line()
|
||||
#tw.sep( "=")
|
||||
|
||||
conftest_options = [
|
||||
('pytest_plugins', 'list of plugin names to load'),
|
||||
@@ -98,10 +108,10 @@ conftest_options = [
|
||||
|
||||
def getpluginversioninfo(config):
|
||||
lines = []
|
||||
plugininfo = config.pluginmanager._plugin_distinfo
|
||||
plugininfo = config.pluginmanager.list_plugin_distinfo()
|
||||
if plugininfo:
|
||||
lines.append("setuptools registered plugins:")
|
||||
for dist, plugin in plugininfo:
|
||||
for plugin, dist in plugininfo:
|
||||
loc = getattr(plugin, '__file__', repr(plugin))
|
||||
content = "%s-%s at %s" % (dist.project_name, dist.version, loc)
|
||||
lines.append(" " + content)
|
||||
@@ -119,8 +129,7 @@ def pytest_report_header(config):
|
||||
|
||||
if config.option.traceconfig:
|
||||
lines.append("active plugins:")
|
||||
plugins = []
|
||||
items = config.pluginmanager._name2plugin.items()
|
||||
items = config.pluginmanager.list_name_plugin()
|
||||
for name, plugin in items:
|
||||
if hasattr(plugin, '__file__'):
|
||||
r = plugin.__file__
|
||||
@@ -128,72 +137,3 @@ def pytest_report_header(config):
|
||||
r = repr(plugin)
|
||||
lines.append(" %-20s: %s" %(name, r))
|
||||
return lines
|
||||
|
||||
|
||||
# =====================================================
|
||||
# validate plugin syntax and hooks
|
||||
# =====================================================
|
||||
|
||||
def pytest_plugin_registered(manager, plugin):
|
||||
methods = collectattr(plugin)
|
||||
hooks = {}
|
||||
for hookspec in manager.hook._hookspecs:
|
||||
hooks.update(collectattr(hookspec))
|
||||
|
||||
stringio = py.io.TextIO()
|
||||
def Print(*args):
|
||||
if args:
|
||||
stringio.write(" ".join(map(str, args)))
|
||||
stringio.write("\n")
|
||||
|
||||
fail = False
|
||||
while methods:
|
||||
name, method = methods.popitem()
|
||||
#print "checking", name
|
||||
if isgenerichook(name):
|
||||
continue
|
||||
if name not in hooks:
|
||||
if not getattr(method, 'optionalhook', False):
|
||||
Print("found unknown hook:", name)
|
||||
fail = True
|
||||
else:
|
||||
#print "checking", method
|
||||
method_args = list(varnames(method))
|
||||
if '__multicall__' in method_args:
|
||||
method_args.remove('__multicall__')
|
||||
hook = hooks[name]
|
||||
hookargs = varnames(hook)
|
||||
for arg in method_args:
|
||||
if arg not in hookargs:
|
||||
Print("argument %r not available" %(arg, ))
|
||||
Print("actual definition: %s" %(formatdef(method)))
|
||||
Print("available hook arguments: %s" %
|
||||
", ".join(hookargs))
|
||||
fail = True
|
||||
break
|
||||
#if not fail:
|
||||
# print "matching hook:", formatdef(method)
|
||||
if fail:
|
||||
name = getattr(plugin, '__name__', plugin)
|
||||
raise PluginValidationError("%s:\n%s" % (name, stringio.getvalue()))
|
||||
|
||||
class PluginValidationError(Exception):
|
||||
""" plugin failed validation. """
|
||||
|
||||
def isgenerichook(name):
|
||||
return name == "pytest_plugins" or \
|
||||
name.startswith("pytest_funcarg__")
|
||||
|
||||
def collectattr(obj):
|
||||
methods = {}
|
||||
for apiname in dir(obj):
|
||||
if apiname.startswith("pytest_"):
|
||||
methods[apiname] = getattr(obj, apiname)
|
||||
return methods
|
||||
|
||||
def formatdef(func):
|
||||
return "%s%s" % (
|
||||
func.__name__,
|
||||
inspect.formatargspec(*inspect.getargspec(func))
|
||||
)
|
||||
|
||||
|
||||
@@ -1,32 +1,44 @@
|
||||
""" hook specifications for pytest plugins, invoked from main.py and builtin plugins. """
|
||||
|
||||
from _pytest._pluggy import HookspecMarker
|
||||
|
||||
hookspec = HookspecMarker("pytest")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Initialization
|
||||
# Initialization hooks called for every plugin
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@hookspec(historic=True)
|
||||
def pytest_addhooks(pluginmanager):
|
||||
"""called at plugin load time to allow adding new hooks via a call to
|
||||
pluginmanager.registerhooks(module)."""
|
||||
"""called at plugin registration time to allow adding new hooks via a call to
|
||||
pluginmanager.add_hookspecs(module_or_class, prefix)."""
|
||||
|
||||
|
||||
@hookspec(historic=True)
|
||||
def pytest_namespace():
|
||||
"""return dict of name->object to be made globally available in
|
||||
the py.test/pytest namespace. This hook is called before command
|
||||
line options are parsed.
|
||||
the pytest namespace. This hook is called at plugin registration
|
||||
time.
|
||||
"""
|
||||
|
||||
def pytest_cmdline_parse(pluginmanager, args):
|
||||
"""return initialized config object, parsing the specified args. """
|
||||
pytest_cmdline_parse.firstresult = True
|
||||
@hookspec(historic=True)
|
||||
def pytest_plugin_registered(plugin, manager):
|
||||
""" a new pytest plugin got registered. """
|
||||
|
||||
def pytest_cmdline_preparse(config, args):
|
||||
"""modify command line arguments before option parsing. """
|
||||
|
||||
@hookspec(historic=True)
|
||||
def pytest_addoption(parser):
|
||||
"""register optparse-style options and ini-style config values.
|
||||
"""register argparse-style options and ini-style config values.
|
||||
|
||||
This function must be implemented in a :ref:`plugin <pluginorder>` and is
|
||||
called once at the beginning of a test run.
|
||||
.. warning::
|
||||
|
||||
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.
|
||||
|
||||
:arg parser: To add command line options, call
|
||||
:py:func:`parser.addoption(...) <_pytest.config.Parser.addoption>`.
|
||||
@@ -47,31 +59,43 @@ def pytest_addoption(parser):
|
||||
via (deprecated) ``pytest.config``.
|
||||
"""
|
||||
|
||||
@hookspec(historic=True)
|
||||
def pytest_configure(config):
|
||||
""" called after command line options have been parsed
|
||||
and all plugins and initial conftest files been loaded.
|
||||
This hook is called for every plugin.
|
||||
"""
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Bootstrapping hooks called for plugins registered early enough:
|
||||
# internal and 3rd party plugins as well as directly
|
||||
# discoverable conftest.py local plugins.
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_cmdline_parse(pluginmanager, args):
|
||||
"""return initialized config object, parsing the specified args. """
|
||||
|
||||
def pytest_cmdline_preparse(config, args):
|
||||
"""(deprecated) modify command line arguments before option parsing. """
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_cmdline_main(config):
|
||||
""" called for performing the main command line action. The default
|
||||
implementation will invoke the configure hooks and runtest_mainloop. """
|
||||
pytest_cmdline_main.firstresult = True
|
||||
|
||||
def pytest_configure(config):
|
||||
""" called after command line options have been parsed
|
||||
and all plugins and initial conftest files been loaded.
|
||||
"""
|
||||
def pytest_load_initial_conftests(args, early_config, parser):
|
||||
""" implements the loading of initial conftest files ahead
|
||||
of command line option parsing. """
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
""" called before test process is exited. """
|
||||
|
||||
def pytest_runtestloop(session):
|
||||
""" called for performing the main runtest loop
|
||||
(after collection finished). """
|
||||
pytest_runtestloop.firstresult = True
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# collection hooks
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_collection(session):
|
||||
""" perform the collection protocol for the given session. """
|
||||
pytest_collection.firstresult = True
|
||||
|
||||
def pytest_collection_modifyitems(session, config, items):
|
||||
""" called after collection has been performed, may filter or re-order
|
||||
@@ -80,16 +104,16 @@ def pytest_collection_modifyitems(session, config, items):
|
||||
def pytest_collection_finish(session):
|
||||
""" called after collection has been performed and modified. """
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_ignore_collect(path, config):
|
||||
""" return True to prevent considering this path for collection.
|
||||
This hook is consulted for all files and directories prior to calling
|
||||
more specific hooks.
|
||||
"""
|
||||
pytest_ignore_collect.firstresult = True
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_collect_directory(path, parent):
|
||||
""" called before traversing a directory for collection files. """
|
||||
pytest_collect_directory.firstresult = True
|
||||
|
||||
def pytest_collect_file(path, parent):
|
||||
""" return collection Node or None for the given path. Any new node
|
||||
@@ -108,29 +132,29 @@ def pytest_collectreport(report):
|
||||
def pytest_deselected(items):
|
||||
""" called for test items deselected by keyword. """
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_make_collect_report(collector):
|
||||
""" perform ``collector.collect()`` and return a CollectReport. """
|
||||
pytest_make_collect_report.firstresult = True
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Python test function related hooks
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_pycollect_makemodule(path, parent):
|
||||
""" return a Module collector or None for the given path.
|
||||
This hook will be called for each matching test module path.
|
||||
The pytest_collect_file hook needs to be used if you want to
|
||||
create test modules for files that do not match as a test module.
|
||||
"""
|
||||
pytest_pycollect_makemodule.firstresult = True
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_pycollect_makeitem(collector, name, obj):
|
||||
""" return custom item/collector for a python object in a module, or None. """
|
||||
pytest_pycollect_makeitem.firstresult = True
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_pyfunc_call(pyfuncitem):
|
||||
""" call underlying test function. """
|
||||
pytest_pyfunc_call.firstresult = True
|
||||
|
||||
def pytest_generate_tests(metafunc):
|
||||
""" generate (multiple) parametrized calls to a test function."""
|
||||
@@ -138,9 +162,16 @@ def pytest_generate_tests(metafunc):
|
||||
# -------------------------------------------------------------------------
|
||||
# generic runtest related hooks
|
||||
# -------------------------------------------------------------------------
|
||||
def pytest_itemstart(item, node=None):
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_runtestloop(session):
|
||||
""" called for performing the main runtest loop
|
||||
(after collection finished). """
|
||||
|
||||
def pytest_itemstart(item, node):
|
||||
""" (deprecated, use pytest_runtest_logstart). """
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_runtest_protocol(item, nextitem):
|
||||
""" implements the runtest_setup/call/teardown protocol for
|
||||
the given test item, including capturing exceptions and calling
|
||||
@@ -148,13 +179,12 @@ def pytest_runtest_protocol(item, nextitem):
|
||||
|
||||
:arg item: test item for which the runtest protocol is performed.
|
||||
|
||||
:arg nexitem: the scheduled-to-be-next test item (or None if this
|
||||
is the end my friend). This argument is passed on to
|
||||
:py:func:`pytest_runtest_teardown`.
|
||||
:arg nextitem: the scheduled-to-be-next test item (or None if this
|
||||
is the end my friend). This argument is passed on to
|
||||
:py:func:`pytest_runtest_teardown`.
|
||||
|
||||
:return boolean: True if no further hook implementations should be invoked.
|
||||
"""
|
||||
pytest_runtest_protocol.firstresult = True
|
||||
|
||||
def pytest_runtest_logstart(nodeid, location):
|
||||
""" signal the start of running a single test item. """
|
||||
@@ -168,18 +198,18 @@ def pytest_runtest_call(item):
|
||||
def pytest_runtest_teardown(item, nextitem):
|
||||
""" called after ``pytest_runtest_call``.
|
||||
|
||||
:arg nexitem: the scheduled-to-be-next test item (None if no further
|
||||
test item is scheduled). This argument can be used to
|
||||
perform exact teardowns, i.e. calling just enough finalizers
|
||||
so that nextitem only needs to call setup-functions.
|
||||
:arg nextitem: the scheduled-to-be-next test item (None if no further
|
||||
test item is scheduled). This argument can be used to
|
||||
perform exact teardowns, i.e. calling just enough finalizers
|
||||
so that nextitem only needs to call setup-functions.
|
||||
"""
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
""" return a :py:class:`_pytest.runner.TestReport` object
|
||||
for the given :py:class:`pytest.Item` and
|
||||
:py:class:`_pytest.runner.CallInfo`.
|
||||
"""
|
||||
pytest_runtest_makereport.firstresult = True
|
||||
|
||||
def pytest_runtest_logreport(report):
|
||||
""" process a test setup/call/teardown report relating to
|
||||
@@ -195,6 +225,9 @@ def pytest_sessionstart(session):
|
||||
def pytest_sessionfinish(session, exitstatus):
|
||||
""" whole test run finishes. """
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
""" called before test process is exited. """
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# hooks for customising the assert methods
|
||||
@@ -216,33 +249,46 @@ 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."""
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_report_teststatus(report):
|
||||
""" return result-category, shortletter and verbose word for reporting."""
|
||||
pytest_report_teststatus.firstresult = True
|
||||
|
||||
def pytest_terminal_summary(terminalreporter):
|
||||
""" add additional section in terminal summary reporting. """
|
||||
""" add additional section in terminal summary reporting. """
|
||||
|
||||
|
||||
@hookspec(historic=True)
|
||||
def pytest_logwarning(message, code, nodeid, fslocation):
|
||||
""" process a warning specified by a message, a code string,
|
||||
a nodeid and fslocation (both of which may be None
|
||||
if the warning is not tied to a partilar node/location)."""
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# doctest hooks
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_doctest_prepare_content(content):
|
||||
""" return processed content for a given doctest"""
|
||||
pytest_doctest_prepare_content.firstresult = True
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# error handling and internal debugging hooks
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def pytest_plugin_registered(plugin, manager):
|
||||
""" a new py lib plugin got registered. """
|
||||
|
||||
def pytest_plugin_unregistered(plugin):
|
||||
""" a py lib plugin got unregistered. """
|
||||
|
||||
def pytest_internalerror(excrepr):
|
||||
def pytest_internalerror(excrepr, excinfo):
|
||||
""" called for internal errors. """
|
||||
|
||||
def pytest_keyboard_interrupt(excinfo):
|
||||
""" called for keyboard interrupt. """
|
||||
|
||||
def pytest_exception_interact(node, call, report):
|
||||
""" (experimental, new in 2.4) 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".
|
||||
"""
|
||||
|
||||
def pytest_enter_pdb():
|
||||
""" called upon pdb.set_trace()"""
|
||||
|
||||
@@ -1,30 +1,24 @@
|
||||
""" report test results in JUnit-XML format, for use with Hudson 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.
|
||||
"""
|
||||
|
||||
import py
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
# Python 2.X and 3.X compatibility
|
||||
try:
|
||||
unichr(65)
|
||||
except NameError:
|
||||
if sys.version_info[0] < 3:
|
||||
from codecs import open
|
||||
else:
|
||||
unichr = chr
|
||||
try:
|
||||
unicode('A')
|
||||
except NameError:
|
||||
unicode = str
|
||||
try:
|
||||
long(1)
|
||||
except NameError:
|
||||
long = int
|
||||
|
||||
|
||||
class Junit(py.xml.Namespace):
|
||||
pass
|
||||
|
||||
@@ -36,7 +30,8 @@ class Junit(py.xml.Namespace):
|
||||
# | [#x10000-#x10FFFF]
|
||||
_legal_chars = (0x09, 0x0A, 0x0d)
|
||||
_legal_ranges = (
|
||||
(0x20, 0xD7FF),
|
||||
(0x20, 0x7E),
|
||||
(0x80, 0xD7FF),
|
||||
(0xE000, 0xFFFD),
|
||||
(0x10000, 0x10FFFF),
|
||||
)
|
||||
@@ -59,12 +54,26 @@ def bin_xml_escape(arg):
|
||||
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', action="store", dest="xmlpath",
|
||||
metavar="path", default=None,
|
||||
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', action="store", dest="junitprefix",
|
||||
group.addoption('--junitprefix', '--junit-prefix', action="store",
|
||||
metavar="str", default=None,
|
||||
help="prepend prefix to classnames in junit-xml output")
|
||||
|
||||
@@ -81,7 +90,6 @@ def pytest_unconfigure(config):
|
||||
del config._xml
|
||||
config.pluginmanager.unregister(xml)
|
||||
|
||||
|
||||
def mangle_testnames(names):
|
||||
names = [x.replace(".py", "") for x in names if x != '()']
|
||||
names[0] = names[0].replace("/", '.')
|
||||
@@ -93,31 +101,63 @@ class LogXML(object):
|
||||
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 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("::"))
|
||||
classnames = names[:-1]
|
||||
if self.prefix:
|
||||
classnames.insert(0, self.prefix)
|
||||
self.tests.append(Junit.testcase(
|
||||
classname=".".join(classnames),
|
||||
name=names[-1],
|
||||
time=getattr(report, 'duration', 0)
|
||||
))
|
||||
attrs = {
|
||||
"classname": ".".join(classnames),
|
||||
"name": bin_xml_escape(names[-1]),
|
||||
"file": report.location[0],
|
||||
"time": self.durations.get(report.nodeid, 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
|
||||
|
||||
def _write_captured_output(self, report):
|
||||
sec = dict(report.sections)
|
||||
for name in ('out', 'err'):
|
||||
content = sec.get("Captured std%s" % name)
|
||||
if content:
|
||||
tag = getattr(Junit, 'system-'+name)
|
||||
self.append(tag(bin_xml_escape(content)))
|
||||
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
|
||||
|
||||
def append_pass(self, report):
|
||||
self.passed += 1
|
||||
self._write_captured_output(report)
|
||||
@@ -129,39 +169,46 @@ class LogXML(object):
|
||||
Junit.skipped(message="xfail-marked test passes unexpectedly"))
|
||||
self.skipped += 1
|
||||
else:
|
||||
fail = Junit.failure(message="test failure")
|
||||
fail.append(str(report.longrepr))
|
||||
if hasattr(report.longrepr, "reprcrash"):
|
||||
message = report.longrepr.reprcrash.message
|
||||
elif isinstance(report.longrepr, (unicode, str)):
|
||||
message = report.longrepr
|
||||
else:
|
||||
message = str(report.longrepr)
|
||||
message = bin_xml_escape(message)
|
||||
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_failure(self, report):
|
||||
def append_collect_error(self, report):
|
||||
#msg = str(report.longrepr.reprtraceback.extraline)
|
||||
self.append(Junit.failure(str(report.longrepr),
|
||||
message="collection failure"))
|
||||
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(str(report.longrepr),
|
||||
self.append(Junit.skipped(bin_xml_escape(report.longrepr),
|
||||
message="collection skipped"))
|
||||
self.skipped += 1
|
||||
|
||||
def append_error(self, report):
|
||||
self.append(Junit.error(str(report.longrepr),
|
||||
self.append(Junit.error(bin_xml_escape(report.longrepr),
|
||||
message="test setup failure"))
|
||||
self.errors += 1
|
||||
|
||||
def append_skipped(self, report):
|
||||
if hasattr(report, "wasxfail"):
|
||||
self.append(Junit.skipped(str(report.wasxfail),
|
||||
self.append(Junit.skipped(bin_xml_escape(report.wasxfail),
|
||||
message="expected test failure"))
|
||||
else:
|
||||
filename, lineno, skipreason = report.longrepr
|
||||
if skipreason.startswith("Skipped: "):
|
||||
skipreason = skipreason[9:]
|
||||
skipreason = bin_xml_escape(skipreason[9:])
|
||||
self.append(
|
||||
Junit.skipped("%s:%s: %s" % report.longrepr,
|
||||
Junit.skipped("%s:%s: %s" % (filename, lineno, skipreason),
|
||||
type="pytest.skip",
|
||||
message=skipreason
|
||||
))
|
||||
@@ -169,8 +216,30 @@ class LogXML(object):
|
||||
self._write_captured_output(report)
|
||||
|
||||
def pytest_runtest_logreport(self, report):
|
||||
"""handle a setup/call/teardown report, generating the appropriate
|
||||
xml tags as necessary.
|
||||
|
||||
note: due to plugins like xdist, this hook may be called in interlaced
|
||||
order with reports from other nodes. for example:
|
||||
|
||||
usual call order:
|
||||
-> setup node1
|
||||
-> call node1
|
||||
-> teardown node1
|
||||
-> setup node2
|
||||
-> call node2
|
||||
-> teardown node2
|
||||
|
||||
possible call order in xdist:
|
||||
-> setup node1
|
||||
-> call node1
|
||||
-> setup node2
|
||||
-> call node2
|
||||
-> teardown node2
|
||||
-> teardown node1
|
||||
"""
|
||||
if report.passed:
|
||||
if report.when == "call": # ignore setup/teardown
|
||||
if report.when == "call": # ignore setup/teardown
|
||||
self._opentestcase(report)
|
||||
self.append_pass(report)
|
||||
elif report.failed:
|
||||
@@ -182,33 +251,45 @@ class LogXML(object):
|
||||
elif report.skipped:
|
||||
self._opentestcase(report)
|
||||
self.append_skipped(report)
|
||||
self.update_testcase_duration(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
|
||||
|
||||
def pytest_collectreport(self, report):
|
||||
if not report.passed:
|
||||
self._opentestcase(report)
|
||||
if report.failed:
|
||||
self.append_collect_failure(report)
|
||||
self.append_collect_error(report)
|
||||
else:
|
||||
self.append_collect_skipped(report)
|
||||
|
||||
def pytest_internalerror(self, excrepr):
|
||||
self.errors += 1
|
||||
data = py.xml.escape(excrepr)
|
||||
data = bin_xml_escape(excrepr)
|
||||
self.tests.append(
|
||||
Junit.testcase(
|
||||
Junit.error(data, message="internal error"),
|
||||
classname="pytest",
|
||||
name="internal"))
|
||||
|
||||
def pytest_sessionstart(self, session):
|
||||
def pytest_sessionstart(self):
|
||||
self.suite_start_time = time.time()
|
||||
|
||||
def pytest_sessionfinish(self, session, exitstatus, __multicall__):
|
||||
if py.std.sys.version_info[0] < 3:
|
||||
logfile = py.std.codecs.open(self.logfile, 'w', encoding='utf-8')
|
||||
else:
|
||||
logfile = open(self.logfile, 'w', encoding='utf-8')
|
||||
|
||||
def pytest_sessionfinish(self):
|
||||
dirname = os.path.dirname(os.path.abspath(self.logfile))
|
||||
if not os.path.isdir(dirname):
|
||||
os.makedirs(dirname)
|
||||
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
|
||||
@@ -216,7 +297,7 @@ class LogXML(object):
|
||||
logfile.write('<?xml version="1.0" encoding="utf-8"?>')
|
||||
logfile.write(Junit.testsuite(
|
||||
self.tests,
|
||||
name="",
|
||||
name="pytest",
|
||||
errors=self.errors,
|
||||
failures=self.failed,
|
||||
skips=self.skipped,
|
||||
|
||||
271
_pytest/main.py
271
_pytest/main.py
@@ -1,15 +1,15 @@
|
||||
""" core implementation of testing process: init, session, runtest loop. """
|
||||
import re
|
||||
|
||||
import py
|
||||
import pytest, _pytest
|
||||
import inspect
|
||||
import os, sys, imp
|
||||
try:
|
||||
from collections import MutableMapping as MappingMixin
|
||||
except ImportError:
|
||||
from UserDict import DictMixin as MappingMixin
|
||||
|
||||
from _pytest.mark import MarkInfo
|
||||
from _pytest.runner import collect_one_node
|
||||
|
||||
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
|
||||
|
||||
@@ -19,12 +19,15 @@ EXIT_TESTSFAILED = 1
|
||||
EXIT_INTERRUPTED = 2
|
||||
EXIT_INTERNALERROR = 3
|
||||
EXIT_USAGEERROR = 4
|
||||
EXIT_NOTESTSCOLLECTED = 5
|
||||
|
||||
name_re = py.std.re.compile("^[a-zA-Z_]\w*$")
|
||||
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}'))
|
||||
type="args", default=['.*', '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",
|
||||
# "patterns specifying possible locations of test files",
|
||||
# type="linelist", default=["**/test_*.txt",
|
||||
@@ -35,22 +38,28 @@ def pytest_addoption(parser):
|
||||
dest="exitfirst",
|
||||
help="exit instantly on first error or failed test."),
|
||||
group._addoption('--maxfail', metavar="num",
|
||||
action="store", type="int", dest="maxfail", default=0,
|
||||
action="store", type=int, dest="maxfail", default=0,
|
||||
help="exit after first num failures or errors.")
|
||||
group._addoption('--strict', action="store_true",
|
||||
help="run pytest in strict mode, warnings become errors.")
|
||||
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 = parser.getgroup("collect", "collection")
|
||||
group.addoption('--collectonly',
|
||||
action="store_true", dest="collectonly",
|
||||
group.addoption('--collectonly', '--collect-only', action="store_true",
|
||||
help="only collect tests, don't execute them."),
|
||||
group.addoption('--pyargs', action="store_true",
|
||||
help="try to interpret all arguments as python packages.")
|
||||
group.addoption("--ignore", action="append", metavar="path",
|
||||
help="ignore path during collection (multi-allowed).")
|
||||
# when changing this to --conf-cut-dir, config.py Conftest.setinitial
|
||||
# needs upgrading as well
|
||||
group.addoption('--confcutdir', dest="confcutdir", default=None,
|
||||
metavar="dir",
|
||||
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 = parser.getgroup("debugconfig",
|
||||
"test session debugging and configuration")
|
||||
@@ -63,7 +72,7 @@ def pytest_namespace():
|
||||
return dict(collect=collect)
|
||||
|
||||
def pytest_configure(config):
|
||||
py.test.config = config # compatibiltiy
|
||||
pytest.config = config # compatibiltiy
|
||||
if config.option.exitfirst:
|
||||
config.option.maxfail = 1
|
||||
|
||||
@@ -74,35 +83,32 @@ def wrap_session(config, doit):
|
||||
initstate = 0
|
||||
try:
|
||||
try:
|
||||
config.pluginmanager.do_configure(config)
|
||||
config._do_configure()
|
||||
initstate = 1
|
||||
config.hook.pytest_sessionstart(session=session)
|
||||
initstate = 2
|
||||
doit(config, session)
|
||||
session.exitstatus = doit(config, session) or 0
|
||||
except pytest.UsageError:
|
||||
msg = sys.exc_info()[1].args[0]
|
||||
sys.stderr.write("ERROR: %s\n" %(msg,))
|
||||
session.exitstatus = EXIT_USAGEERROR
|
||||
raise
|
||||
except KeyboardInterrupt:
|
||||
excinfo = py.code.ExceptionInfo()
|
||||
config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
|
||||
session.exitstatus = EXIT_INTERRUPTED
|
||||
except:
|
||||
excinfo = py.code.ExceptionInfo()
|
||||
config.pluginmanager.notify_exception(excinfo, config.option)
|
||||
config.notify_exception(excinfo, config.option)
|
||||
session.exitstatus = EXIT_INTERNALERROR
|
||||
if excinfo.errisinstance(SystemExit):
|
||||
sys.stderr.write("mainloop: caught Spurious SystemExit!\n")
|
||||
else:
|
||||
if session._testsfailed:
|
||||
session.exitstatus = EXIT_TESTSFAILED
|
||||
|
||||
finally:
|
||||
excinfo = None # Explicitly break reference cycle.
|
||||
session.startdir.chdir()
|
||||
if initstate >= 2:
|
||||
config.hook.pytest_sessionfinish(
|
||||
session=session,
|
||||
exitstatus=session.exitstatus)
|
||||
if initstate >= 1:
|
||||
config.pluginmanager.do_unconfigure(config)
|
||||
config._ensure_unconfigure()
|
||||
return session.exitstatus
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
@@ -114,6 +120,11 @@ def _main(config, session):
|
||||
config.hook.pytest_collection(session=session)
|
||||
config.hook.pytest_runtestloop(session=session)
|
||||
|
||||
if session.testsfailed:
|
||||
return EXIT_TESTSFAILED
|
||||
elif session.testscollected == 0:
|
||||
return EXIT_NOTESTSCOLLECTED
|
||||
|
||||
def pytest_collection(session):
|
||||
return session.perform_collect()
|
||||
|
||||
@@ -141,23 +152,21 @@ def pytest_ignore_collect(path, config):
|
||||
p = path.dirpath()
|
||||
ignore_paths = config._getconftest_pathlist("collect_ignore", path=p)
|
||||
ignore_paths = ignore_paths or []
|
||||
excludeopt = config.getvalue("ignore")
|
||||
excludeopt = config.getoption("ignore")
|
||||
if excludeopt:
|
||||
ignore_paths.extend([py.path.local(x) for x in excludeopt])
|
||||
return path in ignore_paths
|
||||
|
||||
class HookProxy:
|
||||
def __init__(self, fspath, config):
|
||||
class FSHookProxy:
|
||||
def __init__(self, fspath, pm, remove_mods):
|
||||
self.fspath = fspath
|
||||
self.config = config
|
||||
self.pm = pm
|
||||
self.remove_mods = remove_mods
|
||||
|
||||
def __getattr__(self, name):
|
||||
hookmethod = getattr(self.config.hook, name)
|
||||
|
||||
def call_matching_hooks(**kwargs):
|
||||
plugins = self.config._getmatchingplugins(self.fspath)
|
||||
return hookmethod.pcall(plugins, **kwargs)
|
||||
return call_matching_hooks
|
||||
x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods)
|
||||
self.__dict__[name] = x
|
||||
return x
|
||||
|
||||
def compatproperty(name):
|
||||
def fget(self):
|
||||
@@ -168,30 +177,39 @@ def compatproperty(name):
|
||||
|
||||
class NodeKeywords(MappingMixin):
|
||||
def __init__(self, node):
|
||||
parent = node.parent
|
||||
bases = parent and (parent.keywords._markers,) or ()
|
||||
self._markers = type("dynmarker", bases, {node.name: True})
|
||||
self.node = node
|
||||
self.parent = node.parent
|
||||
self._markers = {node.name: True}
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return getattr(self._markers, key)
|
||||
except AttributeError:
|
||||
raise KeyError(key)
|
||||
return self._markers[key]
|
||||
except KeyError:
|
||||
if self.parent is None:
|
||||
raise
|
||||
return self.parent.keywords[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
setattr(self._markers, key, value)
|
||||
self._markers[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
delattr(self._markers, key)
|
||||
raise ValueError("cannot delete key in keywords dict")
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.keys())
|
||||
seen = set(self._markers)
|
||||
if self.parent is not None:
|
||||
seen.update(self.parent.keywords)
|
||||
return iter(seen)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.keys())
|
||||
return len(self.__iter__())
|
||||
|
||||
def keys(self):
|
||||
return dir(self._markers)
|
||||
return list(self)
|
||||
|
||||
def __repr__(self):
|
||||
return "<NodeKeywords for node %s>" % (self.node, )
|
||||
|
||||
|
||||
class Node(object):
|
||||
""" base class for Collector and Item the test collection tree.
|
||||
@@ -216,6 +234,12 @@ class Node(object):
|
||||
#: keywords/markers collected from all scopes
|
||||
self.keywords = NodeKeywords(self)
|
||||
|
||||
#: allow adding of extra keywords to use for matching
|
||||
self.extra_keyword_matches = set()
|
||||
|
||||
# used for storing artificial fixturedefs for direct parametrization
|
||||
self._name2pseudofixturedef = {}
|
||||
|
||||
#self.extrainit()
|
||||
|
||||
@property
|
||||
@@ -246,6 +270,20 @@ class Node(object):
|
||||
return "<%s %r>" %(self.__class__.__name__,
|
||||
getattr(self, 'name', None))
|
||||
|
||||
def warn(self, code, message):
|
||||
""" generate a warning with the given code and message for this
|
||||
item. """
|
||||
assert isinstance(code, str)
|
||||
fslocation = getattr(self, "location", None)
|
||||
if fslocation is None:
|
||||
fslocation = getattr(self, "fspath", None)
|
||||
else:
|
||||
fslocation = "%s:%s" % fslocation[:2]
|
||||
|
||||
self.ihook.pytest_logwarning.call_historic(kwargs=dict(
|
||||
code=code, message=message,
|
||||
nodeid=self.nodeid, fslocation=fslocation))
|
||||
|
||||
# methods for ordering nodes
|
||||
@property
|
||||
def nodeid(self):
|
||||
@@ -256,21 +294,11 @@ class Node(object):
|
||||
self._nodeid = x = self._makeid()
|
||||
return x
|
||||
|
||||
|
||||
def _makeid(self):
|
||||
return self.parent.nodeid + "::" + self.name
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Node):
|
||||
return False
|
||||
return (self.__class__ == other.__class__ and
|
||||
self.name == other.name and self.parent == other.parent)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.parent))
|
||||
return hash(self.nodeid)
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
@@ -290,7 +318,7 @@ class Node(object):
|
||||
except py.builtin._sysex:
|
||||
raise
|
||||
except:
|
||||
failure = py.std.sys.exc_info()
|
||||
failure = sys.exc_info()
|
||||
setattr(self, exattrname, failure)
|
||||
raise
|
||||
setattr(self, attrname, res)
|
||||
@@ -307,13 +335,49 @@ class Node(object):
|
||||
chain.reverse()
|
||||
return chain
|
||||
|
||||
def add_marker(self, marker):
|
||||
""" dynamically add a marker object to the node.
|
||||
|
||||
``marker`` can be a string or pytest.mark.* instance.
|
||||
"""
|
||||
from _pytest.mark import MarkDecorator
|
||||
if isinstance(marker, py.builtin._basestring):
|
||||
marker = MarkDecorator(marker)
|
||||
elif not isinstance(marker, MarkDecorator):
|
||||
raise ValueError("is not a string or pytest.mark.* Marker")
|
||||
self.keywords[marker.name] = marker
|
||||
|
||||
def get_marker(self, name):
|
||||
""" get a marker object from this node or None if
|
||||
the node doesn't have a marker with that name. """
|
||||
val = self.keywords.get(name, None)
|
||||
if val is not None:
|
||||
from _pytest.mark import MarkInfo, MarkDecorator
|
||||
if isinstance(val, (MarkDecorator, MarkInfo)):
|
||||
return val
|
||||
|
||||
def listextrakeywords(self):
|
||||
""" Return a set of all extra keywords in self and any parents."""
|
||||
extra_keywords = set()
|
||||
item = self
|
||||
for item in self.listchain():
|
||||
extra_keywords.update(item.extra_keyword_matches)
|
||||
return extra_keywords
|
||||
|
||||
def listnames(self):
|
||||
return [x.name for x in self.listchain()]
|
||||
|
||||
def getplugins(self):
|
||||
return self.config._getmatchingplugins(self.fspath)
|
||||
def addfinalizer(self, fin):
|
||||
""" register a function to be called when this node is finalized.
|
||||
|
||||
This method can only be called when this node is active
|
||||
in a setup chain, for example during self.setup().
|
||||
"""
|
||||
self.session._setupstate.addfinalizer(fin, self)
|
||||
|
||||
def getparent(self, cls):
|
||||
""" get the next parent node (including ourself)
|
||||
which is an instance of the given class"""
|
||||
current = self
|
||||
while current and not isinstance(current, cls):
|
||||
current = current.parent
|
||||
@@ -326,20 +390,24 @@ class Node(object):
|
||||
fm = self.session._fixturemanager
|
||||
if excinfo.errisinstance(fm.FixtureLookupError):
|
||||
return excinfo.value.formatrepr()
|
||||
tbfilter = True
|
||||
if self.config.option.fulltrace:
|
||||
style="long"
|
||||
else:
|
||||
self._prunetraceback(excinfo)
|
||||
# XXX should excinfo.getrepr record all data and toterminal()
|
||||
# process it?
|
||||
tbfilter = False # prunetraceback already does it
|
||||
if style == "auto":
|
||||
style = "long"
|
||||
# XXX should excinfo.getrepr record all data and toterminal() process it?
|
||||
if style is None:
|
||||
if self.config.option.tbstyle == "short":
|
||||
style = "short"
|
||||
else:
|
||||
style = "long"
|
||||
|
||||
return excinfo.getrepr(funcargs=True,
|
||||
showlocals=self.config.option.showlocals,
|
||||
style=style)
|
||||
style=style, tbfilter=tbfilter)
|
||||
|
||||
repr_failure = _repr_failure_py
|
||||
|
||||
@@ -347,6 +415,7 @@ class Collector(Node):
|
||||
""" Collector instances create children through collect()
|
||||
and thus iteratively build a tree.
|
||||
"""
|
||||
|
||||
class CollectError(Exception):
|
||||
""" an error during collection, contains a custom message. """
|
||||
|
||||
@@ -369,7 +438,6 @@ class Collector(Node):
|
||||
|
||||
def _prunetraceback(self, excinfo):
|
||||
if hasattr(self, 'fspath'):
|
||||
path = self.fspath
|
||||
traceback = excinfo.traceback
|
||||
ntraceback = traceback.cut(path=self.fspath)
|
||||
if ntraceback == traceback:
|
||||
@@ -389,9 +457,7 @@ class FSCollector(Collector):
|
||||
self.fspath = fspath
|
||||
|
||||
def _makeid(self):
|
||||
if self == self.session:
|
||||
return "."
|
||||
relpath = self.session.fspath.bestrelpath(self.fspath)
|
||||
relpath = self.fspath.relto(self.config.rootdir)
|
||||
if os.sep != "/":
|
||||
relpath = relpath.replace(os.sep, "/")
|
||||
return relpath
|
||||
@@ -405,6 +471,14 @@ class Item(Node):
|
||||
"""
|
||||
nextitem = None
|
||||
|
||||
def __init__(self, name, parent=None, config=None, session=None):
|
||||
super(Item, self).__init__(name, parent, config, session)
|
||||
self._report_sections = []
|
||||
|
||||
def add_report_section(self, when, key, content):
|
||||
if content:
|
||||
self._report_sections.append((when, key, content))
|
||||
|
||||
def reportinfo(self):
|
||||
return self.fspath, None, ""
|
||||
|
||||
@@ -428,38 +502,64 @@ class Item(Node):
|
||||
class NoMatch(Exception):
|
||||
""" raised if matching cannot locate a matching names. """
|
||||
|
||||
class Interrupted(KeyboardInterrupt):
|
||||
""" signals an interrupted test run. """
|
||||
__module__ = 'builtins' # for py3
|
||||
|
||||
class Session(FSCollector):
|
||||
class Interrupted(KeyboardInterrupt):
|
||||
""" signals an interrupted test run. """
|
||||
__module__ = 'builtins' # for py3
|
||||
Interrupted = Interrupted
|
||||
|
||||
def __init__(self, config):
|
||||
FSCollector.__init__(self, py.path.local(), parent=None,
|
||||
FSCollector.__init__(self, config.rootdir, parent=None,
|
||||
config=config, session=self)
|
||||
self.config.pluginmanager.register(self, name="session", prepend=True)
|
||||
self._testsfailed = 0
|
||||
self._fs2hookproxy = {}
|
||||
self.testsfailed = 0
|
||||
self.testscollected = 0
|
||||
self.shouldstop = False
|
||||
self.trace = config.trace.root.get("collection")
|
||||
self._norecursepatterns = config.getini("norecursedirs")
|
||||
self.startdir = py.path.local()
|
||||
self.config.pluginmanager.register(self, name="session")
|
||||
|
||||
def _makeid(self):
|
||||
return ""
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_collectstart(self):
|
||||
if self.shouldstop:
|
||||
raise self.Interrupted(self.shouldstop)
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_runtest_logreport(self, report):
|
||||
if report.failed and not hasattr(report, 'wasxfail'):
|
||||
self._testsfailed += 1
|
||||
self.testsfailed += 1
|
||||
maxfail = self.config.getvalue("maxfail")
|
||||
if maxfail and self._testsfailed >= maxfail:
|
||||
if maxfail and self.testsfailed >= maxfail:
|
||||
self.shouldstop = "stopping after %d failures" % (
|
||||
self._testsfailed)
|
||||
self.testsfailed)
|
||||
pytest_collectreport = pytest_runtest_logreport
|
||||
|
||||
def isinitpath(self, path):
|
||||
return path in self._initialpaths
|
||||
|
||||
def gethookproxy(self, fspath):
|
||||
return HookProxy(fspath, self.config)
|
||||
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
|
||||
|
||||
def perform_collect(self, args=None, genitems=True):
|
||||
hook = self.config.hook
|
||||
@@ -469,6 +569,7 @@ class Session(FSCollector):
|
||||
config=self.config, items=items)
|
||||
finally:
|
||||
hook.pytest_collection_finish(session=self)
|
||||
self.testscollected = len(items)
|
||||
return items
|
||||
|
||||
def _perform_collect(self, args, genitems):
|
||||
@@ -484,14 +585,16 @@ class Session(FSCollector):
|
||||
parts = self._parsearg(arg)
|
||||
self._initialparts.append(parts)
|
||||
self._initialpaths.add(parts[0])
|
||||
self.ihook.pytest_collectstart(collector=self)
|
||||
rep = self.ihook.pytest_make_collect_report(collector=self)
|
||||
rep = collect_one_node(self)
|
||||
self.ihook.pytest_collectreport(report=rep)
|
||||
self.trace.root.indent -= 1
|
||||
if self._notfound:
|
||||
errors = []
|
||||
for arg, exc in self._notfound:
|
||||
line = "(no name %r in any of %r)" % (arg, exc.args[0])
|
||||
raise pytest.UsageError("not found: %s\n%s" %(arg, line))
|
||||
errors.append("not found: %s\n%s" % (arg, line))
|
||||
#XXX: test this
|
||||
raise pytest.UsageError(*errors)
|
||||
if not genitems:
|
||||
return rep.result
|
||||
else:
|
||||
@@ -512,8 +615,7 @@ class Session(FSCollector):
|
||||
# we are inside a make_report hook so
|
||||
# we cannot directly pass through the exception
|
||||
self._notfound.append((arg, sys.exc_info()[1]))
|
||||
self.trace.root.indent -= 1
|
||||
break
|
||||
|
||||
self.trace.root.indent -= 1
|
||||
|
||||
def _collect(self, arg):
|
||||
@@ -580,7 +682,7 @@ class Session(FSCollector):
|
||||
arg = self._tryconvertpyarg(arg)
|
||||
parts = str(arg).split("::")
|
||||
relpath = parts[0].replace("/", os.sep)
|
||||
path = self.fspath.join(relpath, abs=True)
|
||||
path = self.config.invocation_dir.join(relpath, abs=True)
|
||||
if not path.check():
|
||||
if self.config.option.pyargs:
|
||||
msg = "file or package not found: "
|
||||
@@ -614,8 +716,7 @@ class Session(FSCollector):
|
||||
resultnodes.append(node)
|
||||
continue
|
||||
assert isinstance(node, pytest.Collector)
|
||||
node.ihook.pytest_collectstart(collector=node)
|
||||
rep = node.ihook.pytest_make_collect_report(collector=node)
|
||||
rep = collect_one_node(node)
|
||||
if rep.passed:
|
||||
has_matched = False
|
||||
for x in rep.result:
|
||||
@@ -636,19 +737,9 @@ class Session(FSCollector):
|
||||
yield node
|
||||
else:
|
||||
assert isinstance(node, pytest.Collector)
|
||||
node.ihook.pytest_collectstart(collector=node)
|
||||
rep = node.ihook.pytest_make_collect_report(collector=node)
|
||||
rep = collect_one_node(node)
|
||||
if rep.passed:
|
||||
for subnode in rep.result:
|
||||
for x in self.genitems(subnode):
|
||||
yield x
|
||||
node.ihook.pytest_collectreport(report=rep)
|
||||
|
||||
def getfslineno(obj):
|
||||
# xxx let decorators etc specify a sane ordering
|
||||
if hasattr(obj, 'place_as'):
|
||||
obj = obj.place_as
|
||||
fslineno = py.code.getfslineno(obj)
|
||||
assert isinstance(fslineno[1], int), obj
|
||||
return fslineno
|
||||
|
||||
|
||||
207
_pytest/mark.py
207
_pytest/mark.py
@@ -1,49 +1,72 @@
|
||||
""" generic mechanism for marking and selecting python functions. """
|
||||
import pytest, py
|
||||
import inspect
|
||||
|
||||
|
||||
class MarkerError(Exception):
|
||||
|
||||
"""Error in use of a pytest marker/attribute."""
|
||||
|
||||
|
||||
def pytest_namespace():
|
||||
return {'mark': MarkGenerator()}
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("general")
|
||||
group._addoption('-k',
|
||||
group._addoption(
|
||||
'-k',
|
||||
action="store", dest="keyword", default='', metavar="EXPRESSION",
|
||||
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 keywords. Example: -k 'test_method or test_other' "
|
||||
"matches all test functions whose name contains "
|
||||
"'test_method' or 'test_other'.")
|
||||
"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 "
|
||||
"containing extra names in their 'extra_keyword_matches' set, "
|
||||
"as well as functions which have names assigned directly to them."
|
||||
)
|
||||
|
||||
group._addoption("-m",
|
||||
group._addoption(
|
||||
"-m",
|
||||
action="store", dest="markexpr", default="", metavar="MARKEXPR",
|
||||
help="only run tests matching given mark expression. "
|
||||
"example: -m 'mark1 and not mark2'."
|
||||
)
|
||||
)
|
||||
|
||||
group.addoption("--markers", action="store_true", help=
|
||||
"show markers (builtin, plugin and per-project ones).")
|
||||
group.addoption(
|
||||
"--markers", action="store_true",
|
||||
help="show markers (builtin, plugin and per-project ones)."
|
||||
)
|
||||
|
||||
parser.addini("markers", "markers for test functions", 'linelist')
|
||||
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
import _pytest.config
|
||||
if config.option.markers:
|
||||
config.pluginmanager.do_configure(config)
|
||||
tw = py.io.TerminalWriter()
|
||||
config._do_configure()
|
||||
tw = _pytest.config.create_terminal_writer(config)
|
||||
for line in config.getini("markers"):
|
||||
name, rest = line.split(":", 1)
|
||||
tw.write("@pytest.mark.%s:" % name, bold=True)
|
||||
tw.write("@pytest.mark.%s:" % name, bold=True)
|
||||
tw.line(rest)
|
||||
tw.line()
|
||||
config.pluginmanager.do_unconfigure(config)
|
||||
config._ensure_unconfigure()
|
||||
return 0
|
||||
pytest_cmdline_main.tryfirst = True
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(items, config):
|
||||
keywordexpr = config.option.keyword
|
||||
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
|
||||
if keywordexpr.startswith("-"):
|
||||
keywordexpr = "not " + keywordexpr[1:]
|
||||
selectuntil = False
|
||||
if keywordexpr[-1:] == ":":
|
||||
selectuntil = True
|
||||
@@ -67,38 +90,87 @@ def pytest_collection_modifyitems(items, config):
|
||||
config.hook.pytest_deselected(items=deselected)
|
||||
items[:] = remaining
|
||||
|
||||
class BoolDict:
|
||||
def __init__(self, mydict):
|
||||
self._mydict = mydict
|
||||
def __getitem__(self, name):
|
||||
return name in self._mydict
|
||||
|
||||
class SubstringDict:
|
||||
def __init__(self, mydict):
|
||||
self._mydict = mydict
|
||||
class MarkMapping:
|
||||
"""Provides a local mapping for markers where item access
|
||||
resolves to True if the marker is present. """
|
||||
def __init__(self, keywords):
|
||||
mymarks = set()
|
||||
for key, value in keywords.items():
|
||||
if isinstance(value, MarkInfo) or isinstance(value, MarkDecorator):
|
||||
mymarks.add(key)
|
||||
self._mymarks = mymarks
|
||||
|
||||
def __getitem__(self, name):
|
||||
for key in self._mydict:
|
||||
if name in key:
|
||||
return name in self._mymarks
|
||||
|
||||
|
||||
class KeywordMapping:
|
||||
"""Provides a local mapping for keywords.
|
||||
Given a list of names, map any substring of one of these names to True.
|
||||
"""
|
||||
def __init__(self, names):
|
||||
self._names = names
|
||||
|
||||
def __getitem__(self, subname):
|
||||
for name in self._names:
|
||||
if subname in name:
|
||||
return True
|
||||
return False
|
||||
|
||||
def matchmark(colitem, matchexpr):
|
||||
return eval(matchexpr, {}, BoolDict(colitem.keywords))
|
||||
|
||||
def matchmark(colitem, markexpr):
|
||||
"""Tries to match on any marker names, attached to the given colitem."""
|
||||
return eval(markexpr, {}, MarkMapping(colitem.keywords))
|
||||
|
||||
|
||||
def matchkeyword(colitem, keywordexpr):
|
||||
keywordexpr = keywordexpr.replace("-", "not ")
|
||||
return eval(keywordexpr, {}, SubstringDict(colitem.keywords))
|
||||
"""Tries to match given keyword expression to given collector item.
|
||||
|
||||
Will match on the name of colitem, including the names of its parents.
|
||||
Only matches names of items which are either a :class:`Class` or a
|
||||
:class:`Function`.
|
||||
Additionally, matches on names in the 'extra_keyword_matches' set of
|
||||
any item, as well as names directly assigned to test functions.
|
||||
"""
|
||||
mapped_names = set()
|
||||
|
||||
# Add the names of the current item and any parent items
|
||||
import pytest
|
||||
for item in colitem.listchain():
|
||||
if not isinstance(item, pytest.Instance):
|
||||
mapped_names.add(item.name)
|
||||
|
||||
# Add the names added as extra keywords to current or parent items
|
||||
for name in colitem.listextrakeywords():
|
||||
mapped_names.add(name)
|
||||
|
||||
# Add the names attached to the current function through direct assignment
|
||||
if hasattr(colitem, 'function'):
|
||||
for name in colitem.function.__dict__:
|
||||
mapped_names.add(name)
|
||||
|
||||
mapping = KeywordMapping(mapped_names)
|
||||
if " " not in keywordexpr:
|
||||
# special case to allow for simple "-k pass" and "-k 1.3"
|
||||
return mapping[keywordexpr]
|
||||
elif keywordexpr.startswith("not ") and " " not in keywordexpr[4:]:
|
||||
return not mapping[keywordexpr[4:]]
|
||||
return eval(keywordexpr, {}, mapping)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
import pytest
|
||||
if config.option.strict:
|
||||
pytest.mark._config = config
|
||||
|
||||
|
||||
class MarkGenerator:
|
||||
""" Factory for :class:`MarkDecorator` objects - exposed as
|
||||
a ``py.test.mark`` singleton instance. Example::
|
||||
a ``pytest.mark`` singleton instance. Example::
|
||||
|
||||
import py
|
||||
@py.test.mark.slowtest
|
||||
@pytest.mark.slowtest
|
||||
def test_function():
|
||||
pass
|
||||
|
||||
@@ -107,7 +179,7 @@ class MarkGenerator:
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name[0] == "_":
|
||||
raise AttributeError(name)
|
||||
raise AttributeError("Marker name must NOT start with underscore")
|
||||
if hasattr(self, '_config'):
|
||||
self._check(name)
|
||||
return MarkDecorator(name)
|
||||
@@ -126,59 +198,90 @@ class MarkGenerator:
|
||||
if name not in self._markers:
|
||||
raise AttributeError("%r not a registered marker" % (name,))
|
||||
|
||||
def istestfunc(func):
|
||||
return hasattr(func, "__call__") and \
|
||||
getattr(func, "__name__", "<lambda>") != "<lambda>"
|
||||
|
||||
class MarkDecorator:
|
||||
""" A decorator for test functions and test classes. When applied
|
||||
it will create :class:`MarkInfo` objects which may be
|
||||
:ref:`retrieved by hooks as item keywords <excontrolskip>`.
|
||||
MarkDecorator instances are often created like this::
|
||||
|
||||
mark1 = py.test.mark.NAME # simple MarkDecorator
|
||||
mark2 = py.test.mark.NAME(name1=value) # parametrized MarkDecorator
|
||||
mark1 = pytest.mark.NAME # simple MarkDecorator
|
||||
mark2 = pytest.mark.NAME(name1=value) # parametrized MarkDecorator
|
||||
|
||||
and can then be applied as decorators to test functions::
|
||||
|
||||
@mark2
|
||||
def test_function():
|
||||
pass
|
||||
|
||||
When a MarkDecorator instance is called it does the following:
|
||||
1. If called with a single class as its only positional argument and no
|
||||
additional keyword arguments, it attaches itself to the class so it
|
||||
gets applied automatically to all test cases found in that class.
|
||||
2. If called with a single function as its only positional argument and
|
||||
no additional keyword arguments, it attaches a MarkInfo object to the
|
||||
function, containing all the arguments already stored internally in
|
||||
the MarkDecorator.
|
||||
3. When called in any other case, it performs a 'fake construction' call,
|
||||
i.e. it returns a new MarkDecorator instance with the original
|
||||
MarkDecorator's content updated with the arguments passed to this
|
||||
call.
|
||||
|
||||
Note: The rules above prevent MarkDecorator objects from storing only a
|
||||
single function or class reference as their positional argument with no
|
||||
additional keyword or positional arguments.
|
||||
|
||||
"""
|
||||
def __init__(self, name, args=None, kwargs=None):
|
||||
self.markname = name
|
||||
self.name = name
|
||||
self.args = args or ()
|
||||
self.kwargs = kwargs or {}
|
||||
|
||||
@property
|
||||
def markname(self):
|
||||
return self.name # for backward-compat (2.4.1 had this attr)
|
||||
|
||||
def __repr__(self):
|
||||
d = self.__dict__.copy()
|
||||
name = d.pop('markname')
|
||||
return "<MarkDecorator %r %r>" %(name, d)
|
||||
name = d.pop('name')
|
||||
return "<MarkDecorator %r %r>" % (name, d)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
""" if passed a single callable argument: decorate it with mark info.
|
||||
otherwise add *args/**kwargs in-place to mark information. """
|
||||
if args:
|
||||
if args and not kwargs:
|
||||
func = args[0]
|
||||
if len(args) == 1 and hasattr(func, '__call__') or \
|
||||
hasattr(func, '__bases__'):
|
||||
if hasattr(func, '__bases__'):
|
||||
is_class = inspect.isclass(func)
|
||||
if len(args) == 1 and (istestfunc(func) or is_class):
|
||||
if is_class:
|
||||
if hasattr(func, 'pytestmark'):
|
||||
l = func.pytestmark
|
||||
if not isinstance(l, list):
|
||||
func.pytestmark = [l, self]
|
||||
else:
|
||||
l.append(self)
|
||||
mark_list = func.pytestmark
|
||||
if not isinstance(mark_list, list):
|
||||
mark_list = [mark_list]
|
||||
# always work on a copy to avoid updating pytestmark
|
||||
# from a superclass by accident
|
||||
mark_list = mark_list + [self]
|
||||
func.pytestmark = mark_list
|
||||
else:
|
||||
func.pytestmark = [self]
|
||||
func.pytestmark = [self]
|
||||
else:
|
||||
holder = getattr(func, self.markname, None)
|
||||
holder = getattr(func, self.name, None)
|
||||
if holder is None:
|
||||
holder = MarkInfo(self.markname, self.args, self.kwargs)
|
||||
setattr(func, self.markname, holder)
|
||||
holder = MarkInfo(
|
||||
self.name, self.args, self.kwargs
|
||||
)
|
||||
setattr(func, self.name, holder)
|
||||
else:
|
||||
holder.add(self.args, self.kwargs)
|
||||
return func
|
||||
kw = self.kwargs.copy()
|
||||
kw.update(kwargs)
|
||||
args = self.args + args
|
||||
return self.__class__(self.markname, args=args, kwargs=kw)
|
||||
return self.__class__(self.name, args=args, kwargs=kw)
|
||||
|
||||
|
||||
class MarkInfo:
|
||||
""" Marking object created by :class:`MarkDecorator` instances. """
|
||||
@@ -188,12 +291,13 @@ class MarkInfo:
|
||||
#: positional argument list, empty if none specified
|
||||
self.args = args
|
||||
#: keyword argument dictionary, empty if nothing specified
|
||||
self.kwargs = kwargs
|
||||
self.kwargs = kwargs.copy()
|
||||
self._arglist = [(args, kwargs.copy())]
|
||||
|
||||
def __repr__(self):
|
||||
return "<MarkInfo %r args=%r kwargs=%r>" % (
|
||||
self.name, self.args, self.kwargs)
|
||||
self.name, self.args, self.kwargs
|
||||
)
|
||||
|
||||
def add(self, args, kwargs):
|
||||
""" add a MarkInfo with the given args and kwargs. """
|
||||
@@ -205,4 +309,3 @@ class MarkInfo:
|
||||
""" yield MarkInfo objects each relating to a marking-call. """
|
||||
for args, kwargs in self._arglist:
|
||||
yield MarkInfo(self.name, args, kwargs)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
""" monkeypatching and mocking functionality. """
|
||||
|
||||
import os, sys, inspect
|
||||
import os, sys
|
||||
from py.builtin import _basestring
|
||||
|
||||
def pytest_funcarg__monkeypatch(request):
|
||||
"""The returned ``monkeypatch`` funcarg provides these
|
||||
@@ -24,45 +25,128 @@ def pytest_funcarg__monkeypatch(request):
|
||||
request.addfinalizer(mpatch.undo)
|
||||
return mpatch
|
||||
|
||||
notset = object()
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
class Notset:
|
||||
def __repr__(self):
|
||||
return "<notset>"
|
||||
|
||||
notset = Notset()
|
||||
|
||||
class monkeypatch:
|
||||
""" object keeping a record of setattr/item/env/syspath changes. """
|
||||
""" Object keeping a record of setattr/item/env/syspath changes. """
|
||||
def __init__(self):
|
||||
self._setattr = []
|
||||
self._setitem = []
|
||||
self._cwd = None
|
||||
self._savesyspath = None
|
||||
|
||||
def setattr(self, obj, name, value, raising=True):
|
||||
""" set attribute ``name`` on ``obj`` to ``value``, by default
|
||||
raise AttributeEror if the attribute did not exist. """
|
||||
oldval = getattr(obj, name, notset)
|
||||
def setattr(self, target, name, value=notset, raising=True):
|
||||
""" Set attribute value on target, memorizing the old value.
|
||||
By default raise AttributeError if the attribute did not exist.
|
||||
|
||||
For convenience you can specify a string as ``target`` which
|
||||
will be interpreted as a dotted import path, with the last part
|
||||
being the attribute name. Example:
|
||||
``monkeypatch.setattr("os.getcwd", lambda x: "/")``
|
||||
would set the ``getcwd`` function of the ``os`` module.
|
||||
|
||||
The ``raising`` value determines if the setattr should fail
|
||||
if the attribute is not already present (defaults to True
|
||||
which means it will raise).
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
import inspect
|
||||
|
||||
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")
|
||||
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" %(obj, name))
|
||||
raise AttributeError("%r has no attribute %r" %(target, name))
|
||||
|
||||
# avoid class descriptors like staticmethod/classmethod
|
||||
if inspect.isclass(obj):
|
||||
oldval = obj.__dict__.get(name, notset)
|
||||
self._setattr.insert(0, (obj, name, oldval))
|
||||
setattr(obj, name, value)
|
||||
if inspect.isclass(target):
|
||||
oldval = target.__dict__.get(name, notset)
|
||||
self._setattr.insert(0, (target, name, oldval))
|
||||
setattr(target, name, value)
|
||||
|
||||
def delattr(self, obj, name, raising=True):
|
||||
""" delete attribute ``name`` from ``obj``, by default raise
|
||||
AttributeError it the attribute did not previously exist. """
|
||||
if not hasattr(obj, name):
|
||||
def delattr(self, target, name=notset, raising=True):
|
||||
""" Delete attribute ``name`` from ``target``, by default raise
|
||||
AttributeError it the attribute did not previously exist.
|
||||
|
||||
If no ``name`` is specified and ``target`` is a string
|
||||
it will be interpreted as a dotted import path with the
|
||||
last part being the attribute name.
|
||||
|
||||
If ``raising`` is set to False, no exception will be raised if the
|
||||
attribute is missing.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
if name is notset:
|
||||
if not isinstance(target, _basestring):
|
||||
raise TypeError("use delattr(target, name) or "
|
||||
"delattr(target) with target being a dotted "
|
||||
"import string")
|
||||
name, target = derive_importpath(target, raising)
|
||||
|
||||
if not hasattr(target, name):
|
||||
if raising:
|
||||
raise AttributeError(name)
|
||||
else:
|
||||
self._setattr.insert(0, (obj, name, getattr(obj, name, notset)))
|
||||
delattr(obj, name)
|
||||
self._setattr.insert(0, (target, name,
|
||||
getattr(target, name, notset)))
|
||||
delattr(target, name)
|
||||
|
||||
def setitem(self, dic, name, value):
|
||||
""" set dictionary entry ``name`` to value. """
|
||||
""" Set dictionary entry ``name`` to value. """
|
||||
self._setitem.insert(0, (dic, name, dic.get(name, notset)))
|
||||
dic[name] = value
|
||||
|
||||
def delitem(self, dic, name, raising=True):
|
||||
""" delete ``name`` from dict, raise KeyError if it doesn't exist."""
|
||||
""" Delete ``name`` from dict. Raise KeyError if it doesn't exist.
|
||||
|
||||
If ``raising`` is set to False, no exception will be raised if the
|
||||
key is missing.
|
||||
"""
|
||||
if name not in dic:
|
||||
if raising:
|
||||
raise KeyError(name)
|
||||
@@ -71,7 +155,7 @@ class monkeypatch:
|
||||
del dic[name]
|
||||
|
||||
def setenv(self, name, value, prepend=None):
|
||||
""" set environment variable ``name`` to ``value``. if ``prepend``
|
||||
""" Set environment variable ``name`` to ``value``. If ``prepend``
|
||||
is a character, read the current environment variable value
|
||||
and prepend the ``value`` adjoined with the ``prepend`` character."""
|
||||
value = str(value)
|
||||
@@ -80,18 +164,23 @@ class monkeypatch:
|
||||
self.setitem(os.environ, name, value)
|
||||
|
||||
def delenv(self, name, raising=True):
|
||||
""" delete ``name`` from environment, raise KeyError it not exists."""
|
||||
""" Delete ``name`` from the environment. Raise KeyError it does not
|
||||
exist.
|
||||
|
||||
If ``raising`` is set to False, no exception will be raised if the
|
||||
environment variable is missing.
|
||||
"""
|
||||
self.delitem(os.environ, name, raising=raising)
|
||||
|
||||
def syspath_prepend(self, path):
|
||||
""" prepend ``path`` to ``sys.path`` list of import locations. """
|
||||
if not hasattr(self, '_savesyspath'):
|
||||
""" Prepend ``path`` to ``sys.path`` list of import locations. """
|
||||
if self._savesyspath is None:
|
||||
self._savesyspath = sys.path[:]
|
||||
sys.path.insert(0, str(path))
|
||||
|
||||
def chdir(self, path):
|
||||
""" change the current working directory to the specified path
|
||||
path can be a string or a py.path.local object
|
||||
""" Change the current working directory to the specified path.
|
||||
Path can be a string or a py.path.local object.
|
||||
"""
|
||||
if self._cwd is None:
|
||||
self._cwd = os.getcwd()
|
||||
@@ -101,9 +190,19 @@ class monkeypatch:
|
||||
os.chdir(path)
|
||||
|
||||
def undo(self):
|
||||
""" 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."""
|
||||
""" 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:
|
||||
if value is not notset:
|
||||
setattr(obj, name, value)
|
||||
@@ -119,9 +218,9 @@ class monkeypatch:
|
||||
else:
|
||||
dictionary[name] = value
|
||||
self._setitem[:] = []
|
||||
if hasattr(self, '_savesyspath'):
|
||||
if self._savesyspath is not None:
|
||||
sys.path[:] = self._savesyspath
|
||||
del self._savesyspath
|
||||
self._savesyspath = None
|
||||
|
||||
if self._cwd is not None:
|
||||
os.chdir(self._cwd)
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
""" run test suites written for nose. """
|
||||
|
||||
import pytest, py
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
import py
|
||||
import pytest
|
||||
from _pytest import unittest
|
||||
|
||||
|
||||
def pytest_runtest_makereport(__multicall__, item, call):
|
||||
SkipTest = getattr(sys.modules.get('nose', None), 'SkipTest', None)
|
||||
if SkipTest:
|
||||
if call.excinfo and call.excinfo.errisinstance(SkipTest):
|
||||
# let's substitute the excinfo with a py.test.skip one
|
||||
call2 = call.__class__(lambda: py.test.skip(str(call.excinfo.value)), call.when)
|
||||
call.excinfo = call2.excinfo
|
||||
def get_skip_exceptions():
|
||||
skip_classes = set()
|
||||
for module_name in ('unittest', 'unittest2', 'nose'):
|
||||
mod = sys.modules.get(module_name)
|
||||
if hasattr(mod, 'SkipTest'):
|
||||
skip_classes.add(mod.SkipTest)
|
||||
return tuple(skip_classes)
|
||||
|
||||
|
||||
@pytest.mark.trylast
|
||||
def pytest_runtest_makereport(item, call):
|
||||
if call.excinfo and call.excinfo.errisinstance(get_skip_exceptions()):
|
||||
# let's substitute the excinfo with a pytest.skip one
|
||||
call2 = call.__class__(lambda:
|
||||
pytest.skip(str(call.excinfo.value)), call.when)
|
||||
call.excinfo = call2.excinfo
|
||||
|
||||
|
||||
@pytest.hookimpl(trylast=True)
|
||||
def pytest_runtest_setup(item):
|
||||
if is_potential_nosetest(item):
|
||||
if isinstance(item.parent, pytest.Generator):
|
||||
@@ -39,6 +48,7 @@ def teardown_nose(item):
|
||||
# #call_optional(item._nosegensetup, 'teardown')
|
||||
# del item.parent._nosegensetup
|
||||
|
||||
|
||||
def pytest_make_collect_report(collector):
|
||||
if isinstance(collector, pytest.Generator):
|
||||
call_optional(collector.obj, 'setup')
|
||||
|
||||
@@ -1,67 +1,88 @@
|
||||
""" submit failure or test session information to a pastebin service. """
|
||||
import py, sys
|
||||
import pytest
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
class url:
|
||||
base = "http://bpaste.net"
|
||||
xmlrpc = base + "/xmlrpc/"
|
||||
show = base + "/show/"
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("terminal reporting")
|
||||
group._addoption('--pastebin', metavar="mode",
|
||||
action='store', dest="pastebin", default=None,
|
||||
type="choice", choices=['failed', 'all'],
|
||||
choices=['failed', 'all'],
|
||||
help="send failed|all info to bpaste.net pastebin service.")
|
||||
|
||||
def pytest_configure(__multicall__, config):
|
||||
import tempfile
|
||||
__multicall__.execute()
|
||||
@pytest.hookimpl(trylast=True)
|
||||
def pytest_configure(config):
|
||||
if config.option.pastebin == "all":
|
||||
config._pastebinfile = tempfile.TemporaryFile('w+')
|
||||
tr = config.pluginmanager.getplugin('terminalreporter')
|
||||
oldwrite = tr._tw.write
|
||||
def tee_write(s, **kwargs):
|
||||
oldwrite(s, **kwargs)
|
||||
config._pastebinfile.write(str(s))
|
||||
tr._tw.write = tee_write
|
||||
# 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+')
|
||||
oldwrite = tr._tw.write
|
||||
def tee_write(s, **kwargs):
|
||||
oldwrite(s, **kwargs)
|
||||
config._pastebinfile.write(str(s))
|
||||
tr._tw.write = tee_write
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
if hasattr(config, '_pastebinfile'):
|
||||
# get terminal contents and delete file
|
||||
config._pastebinfile.seek(0)
|
||||
sessionlog = config._pastebinfile.read()
|
||||
config._pastebinfile.close()
|
||||
del config._pastebinfile
|
||||
proxyid = getproxy().newPaste("python", sessionlog)
|
||||
pastebinurl = "%s%s" % (url.show, proxyid)
|
||||
sys.stderr.write("pastebin session-log: %s\n" % pastebinurl)
|
||||
# undo our patching in the terminal reporter
|
||||
tr = config.pluginmanager.getplugin('terminalreporter')
|
||||
del tr._tw.__dict__['write']
|
||||
# write summary
|
||||
tr.write_sep("=", "Sending information to Paste Service")
|
||||
pastebinurl = create_new_paste(sessionlog)
|
||||
tr.write_line("pastebin session-log: %s\n" % pastebinurl)
|
||||
|
||||
def getproxy():
|
||||
def create_new_paste(contents):
|
||||
"""
|
||||
Creates a new paste using bpaste.net service.
|
||||
|
||||
:contents: paste contents
|
||||
:returns: url to the pasted contents
|
||||
"""
|
||||
import re
|
||||
if sys.version_info < (3, 0):
|
||||
from xmlrpclib import ServerProxy
|
||||
from urllib import urlopen, urlencode
|
||||
else:
|
||||
from xmlrpc.client import ServerProxy
|
||||
return ServerProxy(url.xmlrpc).pastes
|
||||
from urllib.request import urlopen
|
||||
from urllib.parse import urlencode
|
||||
|
||||
params = {
|
||||
'code': contents,
|
||||
'lexer': 'python3' if sys.version_info[0] == 3 else 'python',
|
||||
'expiry': '1week',
|
||||
}
|
||||
url = 'https://bpaste.net'
|
||||
response = urlopen(url, data=urlencode(params)).read()
|
||||
m = re.search(r'href="/raw/(\w+)"', response)
|
||||
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":
|
||||
return
|
||||
tr = terminalreporter
|
||||
if 'failed' in tr.stats:
|
||||
terminalreporter.write_sep("=", "Sending information to Paste Service")
|
||||
if tr.config.option.debug:
|
||||
terminalreporter.write_line("xmlrpcurl: %s" %(url.xmlrpc,))
|
||||
serverproxy = getproxy()
|
||||
for rep in terminalreporter.stats.get('failed'):
|
||||
try:
|
||||
msg = rep.longrepr.reprtraceback.reprentries[-1].reprfileloc
|
||||
except AttributeError:
|
||||
msg = tr._getfailureheadline(rep)
|
||||
tw = py.io.TerminalWriter(stringio=True)
|
||||
tw = _pytest.config.create_terminal_writer(terminalreporter.config, stringio=True)
|
||||
rep.toterminal(tw)
|
||||
s = tw.stringio.getvalue()
|
||||
assert len(s)
|
||||
proxyid = serverproxy.newPaste("python", s)
|
||||
pastebinurl = "%s%s" % (url.show, proxyid)
|
||||
pastebinurl = create_new_paste(s)
|
||||
tr.write_line("%s --> %s" %(msg, pastebinurl))
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
""" interactive debugging with PDB, the Python Debugger. """
|
||||
|
||||
import pytest, py
|
||||
from __future__ import absolute_import
|
||||
import pdb
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("general")
|
||||
group._addoption('--pdb',
|
||||
@@ -16,67 +19,60 @@ def pytest_configure(config):
|
||||
if config.getvalue("usepdb"):
|
||||
config.pluginmanager.register(PdbInvoke(), 'pdbinvoke')
|
||||
|
||||
old = (pdb.set_trace, pytestPDB._pluginmanager)
|
||||
def fin():
|
||||
pdb.set_trace, pytestPDB._pluginmanager = old
|
||||
pytestPDB._config = None
|
||||
pdb.set_trace = pytest.set_trace
|
||||
pytestPDB._pluginmanager = config.pluginmanager
|
||||
pytestPDB._config = config
|
||||
config._cleanup.append(fin)
|
||||
|
||||
class pytestPDB:
|
||||
""" Pseudo PDB that defers to the real pdb. """
|
||||
item = None
|
||||
collector = None
|
||||
_pluginmanager = None
|
||||
_config = None
|
||||
|
||||
def set_trace(self):
|
||||
""" invoke PDB set_trace debugging, dropping any IO capturing. """
|
||||
import _pytest.config
|
||||
frame = sys._getframe().f_back
|
||||
item = self.item or self.collector
|
||||
|
||||
if item is not None:
|
||||
capman = item.config.pluginmanager.getplugin("capturemanager")
|
||||
out, err = capman.suspendcapture()
|
||||
if hasattr(item, 'outerr'):
|
||||
item.outerr = (item.outerr[0] + out, item.outerr[1] + err)
|
||||
tw = py.io.TerminalWriter()
|
||||
capman = None
|
||||
if self._pluginmanager is not None:
|
||||
capman = self._pluginmanager.getplugin("capturemanager")
|
||||
if capman:
|
||||
capman.suspendcapture(in_=True)
|
||||
tw = _pytest.config.create_terminal_writer(self._config)
|
||||
tw.line()
|
||||
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
|
||||
py.std.pdb.Pdb().set_trace(frame)
|
||||
self._pluginmanager.hook.pytest_enter_pdb()
|
||||
pdb.Pdb().set_trace(frame)
|
||||
|
||||
def pdbitem(item):
|
||||
pytestPDB.item = item
|
||||
pytest_runtest_setup = pytest_runtest_call = pytest_runtest_teardown = pdbitem
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
def pytest_make_collect_report(__multicall__, collector):
|
||||
try:
|
||||
pytestPDB.collector = collector
|
||||
return __multicall__.execute()
|
||||
finally:
|
||||
pytestPDB.collector = None
|
||||
|
||||
def pytest_runtest_makereport():
|
||||
pytestPDB.item = None
|
||||
|
||||
class PdbInvoke:
|
||||
@pytest.mark.tryfirst
|
||||
def pytest_runtest_makereport(self, item, call, __multicall__):
|
||||
rep = __multicall__.execute()
|
||||
if not call.excinfo or \
|
||||
call.excinfo.errisinstance(pytest.skip.Exception) or \
|
||||
call.excinfo.errisinstance(py.std.bdb.BdbQuit):
|
||||
return rep
|
||||
if hasattr(rep, "wasxfail"):
|
||||
return rep
|
||||
return _enter_pdb(item, call.excinfo, rep)
|
||||
def pytest_exception_interact(self, node, call, report):
|
||||
capman = node.config.pluginmanager.getplugin("capturemanager")
|
||||
if capman:
|
||||
capman.suspendcapture(in_=True)
|
||||
_enter_pdb(node, call.excinfo, report)
|
||||
|
||||
def pytest_internalerror(self, excrepr, excinfo):
|
||||
for line in str(excrepr).split("\n"):
|
||||
sys.stderr.write("INTERNALERROR> %s\n" %line)
|
||||
sys.stderr.flush()
|
||||
tb = _postmortem_traceback(excinfo)
|
||||
post_mortem(tb)
|
||||
|
||||
|
||||
|
||||
|
||||
def _enter_pdb(item, excinfo, rep):
|
||||
# we assume that the above execute() suspended capturing
|
||||
def _enter_pdb(node, excinfo, rep):
|
||||
# XXX we re-use the TerminalReporter's terminalwriter
|
||||
# because this seems to avoid some encoding related troubles
|
||||
# for not completely clear reasons.
|
||||
tw = item.config.pluginmanager.getplugin("terminalreporter")._tw
|
||||
tw = node.config.pluginmanager.getplugin("terminalreporter")._tw
|
||||
tw.line()
|
||||
tw.sep(">", "traceback")
|
||||
rep.toterminal(tw)
|
||||
tw.sep(">", "entering PDB")
|
||||
|
||||
tb = _postmortem_traceback(excinfo)
|
||||
post_mortem(tb)
|
||||
rep._pdbshown = True
|
||||
@@ -86,7 +82,8 @@ def _enter_pdb(item, excinfo, rep):
|
||||
def _postmortem_traceback(excinfo):
|
||||
# A doctest.UnexpectedException is not useful for post_mortem.
|
||||
# Use the underlying exception instead:
|
||||
if isinstance(excinfo.value, py.std.doctest.UnexpectedException):
|
||||
from doctest import UnexpectedException
|
||||
if isinstance(excinfo.value, UnexpectedException):
|
||||
return excinfo.value.exc_info[2]
|
||||
else:
|
||||
return excinfo._excinfo[2]
|
||||
@@ -100,7 +97,6 @@ def _find_last_non_hidden_frame(stack):
|
||||
|
||||
|
||||
def post_mortem(t):
|
||||
pdb = py.std.pdb
|
||||
class Pdb(pdb.Pdb):
|
||||
def get_stack(self, f, t):
|
||||
stack, i = pdb.Pdb.get_stack(self, f, t)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1533
_pytest/python.py
1533
_pytest/python.py
File diff suppressed because it is too large
Load Diff
@@ -1,99 +1,190 @@
|
||||
""" recording warnings during test function execution. """
|
||||
|
||||
import inspect
|
||||
import py
|
||||
import sys, os
|
||||
import sys
|
||||
import warnings
|
||||
import pytest
|
||||
|
||||
def pytest_funcarg__recwarn(request):
|
||||
|
||||
@pytest.yield_fixture
|
||||
def recwarn(request):
|
||||
"""Return a WarningsRecorder instance that provides these methods:
|
||||
|
||||
* ``pop(category=None)``: return last warning matching the category.
|
||||
* ``clear()``: clear list of warnings
|
||||
|
||||
|
||||
See http://docs.python.org/library/warnings.html for information
|
||||
on warning categories.
|
||||
"""
|
||||
if sys.version_info >= (2,7):
|
||||
import warnings
|
||||
oldfilters = warnings.filters[:]
|
||||
warnings.simplefilter('default')
|
||||
def reset_filters():
|
||||
warnings.filters[:] = oldfilters
|
||||
request.addfinalizer(reset_filters)
|
||||
wrec = WarningsRecorder()
|
||||
request.addfinalizer(wrec.finalize)
|
||||
return wrec
|
||||
with wrec:
|
||||
warnings.simplefilter('default')
|
||||
yield wrec
|
||||
|
||||
|
||||
def pytest_namespace():
|
||||
return {'deprecated_call': deprecated_call}
|
||||
return {'deprecated_call': deprecated_call,
|
||||
'warns': warns}
|
||||
|
||||
|
||||
def deprecated_call(func, *args, **kwargs):
|
||||
""" assert that calling ``func(*args, **kwargs)``
|
||||
triggers a DeprecationWarning.
|
||||
"""Assert that ``func(*args, **kwargs)`` triggers a DeprecationWarning.
|
||||
"""
|
||||
warningmodule = py.std.warnings
|
||||
l = []
|
||||
oldwarn_explicit = getattr(warningmodule, 'warn_explicit')
|
||||
def warn_explicit(*args, **kwargs):
|
||||
l.append(args)
|
||||
oldwarn_explicit(*args, **kwargs)
|
||||
oldwarn = getattr(warningmodule, 'warn')
|
||||
def warn(*args, **kwargs):
|
||||
l.append(args)
|
||||
oldwarn(*args, **kwargs)
|
||||
|
||||
warningmodule.warn_explicit = warn_explicit
|
||||
warningmodule.warn = warn
|
||||
try:
|
||||
wrec = WarningsRecorder()
|
||||
with wrec:
|
||||
warnings.simplefilter('always') # ensure all warnings are triggered
|
||||
ret = func(*args, **kwargs)
|
||||
finally:
|
||||
warningmodule.warn_explicit = warn_explicit
|
||||
warningmodule.warn = warn
|
||||
if not l:
|
||||
#print warningmodule
|
||||
|
||||
depwarnings = (DeprecationWarning, PendingDeprecationWarning)
|
||||
if not any(r.category in depwarnings for r in wrec):
|
||||
__tracebackhide__ = True
|
||||
raise AssertionError("%r did not produce DeprecationWarning" %(func,))
|
||||
raise AssertionError("%r did not produce DeprecationWarning" % (func,))
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
class RecordedWarning:
|
||||
def __init__(self, message, category, filename, lineno, line):
|
||||
def warns(expected_warning, *args, **kwargs):
|
||||
"""Assert that code raises a particular class of warning.
|
||||
|
||||
Specifically, the input @expected_warning can be a warning class or
|
||||
tuple of warning classes, and the code must return that warning
|
||||
(if a single class) or one of those warnings (if a tuple).
|
||||
|
||||
This helper produces a list of ``warnings.WarningMessage`` objects,
|
||||
one for each warning raised.
|
||||
|
||||
This function can be used as a context manager, or any of the other ways
|
||||
``pytest.raises`` can be used::
|
||||
|
||||
>>> with warns(RuntimeWarning):
|
||||
... warnings.warn("my warning", RuntimeWarning)
|
||||
"""
|
||||
wcheck = WarningsChecker(expected_warning)
|
||||
if not args:
|
||||
return wcheck
|
||||
elif isinstance(args[0], str):
|
||||
code, = args
|
||||
assert isinstance(code, str)
|
||||
frame = sys._getframe(1)
|
||||
loc = frame.f_locals.copy()
|
||||
loc.update(kwargs)
|
||||
|
||||
with wcheck:
|
||||
code = py.code.Source(code).compile()
|
||||
py.builtin.exec_(code, frame.f_globals, loc)
|
||||
else:
|
||||
func = args[0]
|
||||
with wcheck:
|
||||
return func(*args[1:], **kwargs)
|
||||
|
||||
|
||||
class RecordedWarning(object):
|
||||
def __init__(self, message, category, filename, lineno, file, line):
|
||||
self.message = message
|
||||
self.category = category
|
||||
self.filename = filename
|
||||
self.lineno = lineno
|
||||
self.file = file
|
||||
self.line = line
|
||||
|
||||
class WarningsRecorder:
|
||||
def __init__(self):
|
||||
warningmodule = py.std.warnings
|
||||
self.list = []
|
||||
def showwarning(message, category, filename, lineno, line=0):
|
||||
self.list.append(RecordedWarning(
|
||||
message, category, filename, lineno, line))
|
||||
try:
|
||||
self.old_showwarning(message, category,
|
||||
filename, lineno, line=line)
|
||||
except TypeError:
|
||||
# < python2.6
|
||||
self.old_showwarning(message, category, filename, lineno)
|
||||
self.old_showwarning = warningmodule.showwarning
|
||||
warningmodule.showwarning = showwarning
|
||||
|
||||
class WarningsRecorder(object):
|
||||
"""A context manager to record raised warnings.
|
||||
|
||||
Adapted from `warnings.catch_warnings`.
|
||||
"""
|
||||
|
||||
def __init__(self, module=None):
|
||||
self._module = sys.modules['warnings'] if module is None else module
|
||||
self._entered = False
|
||||
self._list = []
|
||||
|
||||
@property
|
||||
def list(self):
|
||||
"""The list of recorded warnings."""
|
||||
return self._list
|
||||
|
||||
def __getitem__(self, i):
|
||||
"""Get a recorded warning by index."""
|
||||
return self._list[i]
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate through the recorded warnings."""
|
||||
return iter(self._list)
|
||||
|
||||
def __len__(self):
|
||||
"""The number of recorded warnings."""
|
||||
return len(self._list)
|
||||
|
||||
def pop(self, cls=Warning):
|
||||
""" pop the first recorded warning, raise exception if not exists."""
|
||||
for i, w in enumerate(self.list):
|
||||
"""Pop the first recorded warning, raise exception if not exists."""
|
||||
for i, w in enumerate(self._list):
|
||||
if issubclass(w.category, cls):
|
||||
return self.list.pop(i)
|
||||
return self._list.pop(i)
|
||||
__tracebackhide__ = True
|
||||
assert 0, "%r not found in %r" %(cls, self.list)
|
||||
|
||||
#def resetregistry(self):
|
||||
# import warnings
|
||||
# warnings.onceregistry.clear()
|
||||
# warnings.__warningregistry__.clear()
|
||||
raise AssertionError("%r not found in warning list" % cls)
|
||||
|
||||
def clear(self):
|
||||
self.list[:] = []
|
||||
"""Clear the list of recorded warnings."""
|
||||
self._list[:] = []
|
||||
|
||||
def finalize(self):
|
||||
py.std.warnings.showwarning = self.old_showwarning
|
||||
def __enter__(self):
|
||||
if self._entered:
|
||||
__tracebackhide__ = True
|
||||
raise RuntimeError("Cannot enter %r twice" % self)
|
||||
self._entered = True
|
||||
self._filters = self._module.filters
|
||||
self._module.filters = self._filters[:]
|
||||
self._showwarning = self._module.showwarning
|
||||
|
||||
def showwarning(message, category, filename, lineno,
|
||||
file=None, line=None):
|
||||
self._list.append(RecordedWarning(
|
||||
message, category, filename, lineno, file, line))
|
||||
|
||||
# still perform old showwarning functionality
|
||||
self._showwarning(
|
||||
message, category, filename, lineno, file=file, line=line)
|
||||
|
||||
self._module.showwarning = showwarning
|
||||
|
||||
# allow the same warning to be raised more than once
|
||||
self._module.simplefilter('always', append=True)
|
||||
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc_info):
|
||||
if not self._entered:
|
||||
__tracebackhide__ = True
|
||||
raise RuntimeError("Cannot exit %r without entering first" % self)
|
||||
self._module.filters = self._filters
|
||||
self._module.showwarning = self._showwarning
|
||||
|
||||
|
||||
class WarningsChecker(WarningsRecorder):
|
||||
def __init__(self, expected_warning=None, module=None):
|
||||
super(WarningsChecker, self).__init__(module=module)
|
||||
|
||||
msg = ("exceptions must be old-style classes or "
|
||||
"derived from Warning, not %s")
|
||||
if isinstance(expected_warning, tuple):
|
||||
for exc in expected_warning:
|
||||
if not inspect.isclass(exc):
|
||||
raise TypeError(msg % type(exc))
|
||||
elif inspect.isclass(expected_warning):
|
||||
expected_warning = (expected_warning,)
|
||||
elif expected_warning is not None:
|
||||
raise TypeError(msg % type(expected_warning))
|
||||
|
||||
self.expected_warning = expected_warning
|
||||
|
||||
def __exit__(self, *exc_info):
|
||||
super(WarningsChecker, self).__exit__(*exc_info)
|
||||
|
||||
# only check if we're not currently handling an exception
|
||||
if all(a is None for a in exc_info):
|
||||
if self.expected_warning is not None:
|
||||
if not any(r.category in self.expected_warning for r in self):
|
||||
__tracebackhide__ = True
|
||||
pytest.fail("DID NOT WARN")
|
||||
|
||||
@@ -3,10 +3,11 @@ text file.
|
||||
"""
|
||||
|
||||
import py
|
||||
import os
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("terminal reporting", "resultlog plugin options")
|
||||
group.addoption('--resultlog', action="store", dest="resultlog",
|
||||
group.addoption('--resultlog', '--result-log', action="store",
|
||||
metavar="path", default=None,
|
||||
help="path for machine-readable result log.")
|
||||
|
||||
@@ -14,6 +15,9 @@ def pytest_configure(config):
|
||||
resultlog = config.option.resultlog
|
||||
# prevent opening resultlog on slave nodes (xdist)
|
||||
if resultlog and not hasattr(config, 'slaveinput'):
|
||||
dirname = os.path.dirname(os.path.abspath(resultlog))
|
||||
if not os.path.isdir(dirname):
|
||||
os.makedirs(dirname)
|
||||
logfile = open(resultlog, 'w', 1) # line buffered
|
||||
config._resultlog = ResultLog(config, logfile)
|
||||
config.pluginmanager.register(config._resultlog)
|
||||
@@ -85,7 +89,7 @@ class ResultLog(object):
|
||||
if not report.passed:
|
||||
if report.failed:
|
||||
code = "F"
|
||||
longrepr = str(report.longrepr.reprcrash)
|
||||
longrepr = str(report.longrepr)
|
||||
else:
|
||||
assert report.skipped
|
||||
code = "S"
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
""" basic collect and runtest protocol implementations """
|
||||
|
||||
import py, sys
|
||||
import bdb
|
||||
import sys
|
||||
from time import time
|
||||
|
||||
import py
|
||||
import pytest
|
||||
from py._code.code import TerminalRepr
|
||||
|
||||
def pytest_namespace():
|
||||
@@ -18,7 +21,7 @@ def pytest_namespace():
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("terminal reporting", "reporting", after="general")
|
||||
group.addoption('--durations',
|
||||
action="store", type="int", default=None, metavar="N",
|
||||
action="store", type=int, default=None, metavar="N",
|
||||
help="show N slowest setup/test durations (N=0 for all)."),
|
||||
|
||||
def pytest_terminal_summary(terminalreporter):
|
||||
@@ -83,7 +86,17 @@ def pytest_runtest_setup(item):
|
||||
item.session._setupstate.prepare(item)
|
||||
|
||||
def pytest_runtest_call(item):
|
||||
item.runtest()
|
||||
try:
|
||||
item.runtest()
|
||||
except Exception:
|
||||
# Store trace info to allow postmortem debugging
|
||||
type, value, tb = sys.exc_info()
|
||||
tb = tb.tb_next # Skip *this* frame
|
||||
sys.last_type = type
|
||||
sys.last_value = value
|
||||
sys.last_traceback = tb
|
||||
del tb # Get rid of it in this namespace
|
||||
raise
|
||||
|
||||
def pytest_runtest_teardown(item, nextitem):
|
||||
item.session._setupstate.teardown_exact(item, nextitem)
|
||||
@@ -108,8 +121,16 @@ def call_and_report(item, when, log=True, **kwds):
|
||||
report = hook.pytest_runtest_makereport(item=item, call=call)
|
||||
if log:
|
||||
hook.pytest_runtest_logreport(report=report)
|
||||
if check_interactive_exception(call, report):
|
||||
hook.pytest_exception_interact(node=item, call=call, report=report)
|
||||
return report
|
||||
|
||||
def check_interactive_exception(call, report):
|
||||
return call.excinfo and not (
|
||||
hasattr(report, "wasxfail") or
|
||||
call.excinfo.errisinstance(skip.Exception) or
|
||||
call.excinfo.errisinstance(bdb.BdbQuit))
|
||||
|
||||
def call_runtest_hook(item, when, **kwds):
|
||||
hookname = "pytest_runtest_" + when
|
||||
ihook = getattr(item.ihook, hookname)
|
||||
@@ -125,14 +146,13 @@ class CallInfo:
|
||||
self.when = when
|
||||
self.start = time()
|
||||
try:
|
||||
try:
|
||||
self.result = func()
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
self.excinfo = py.code.ExceptionInfo()
|
||||
finally:
|
||||
self.result = func()
|
||||
except KeyboardInterrupt:
|
||||
self.stop = time()
|
||||
raise
|
||||
except:
|
||||
self.excinfo = py.code.ExceptionInfo()
|
||||
self.stop = time()
|
||||
|
||||
def __repr__(self):
|
||||
if self.excinfo:
|
||||
@@ -168,6 +188,11 @@ class BaseReport(object):
|
||||
except UnicodeEncodeError:
|
||||
out.line("<unprintable longrepr>")
|
||||
|
||||
def get_sections(self, prefix):
|
||||
for name, content in self.sections:
|
||||
if name.startswith(prefix):
|
||||
yield prefix, content
|
||||
|
||||
passed = property(lambda x: x.outcome == "passed")
|
||||
failed = property(lambda x: x.outcome == "failed")
|
||||
skipped = property(lambda x: x.outcome == "skipped")
|
||||
@@ -181,15 +206,15 @@ def pytest_runtest_makereport(item, call):
|
||||
duration = call.stop-call.start
|
||||
keywords = dict([(x,1) for x in item.keywords])
|
||||
excinfo = call.excinfo
|
||||
sections = []
|
||||
if not call.excinfo:
|
||||
outcome = "passed"
|
||||
longrepr = None
|
||||
else:
|
||||
excinfo = call.excinfo
|
||||
if not isinstance(excinfo, py.code.ExceptionInfo):
|
||||
outcome = "failed"
|
||||
longrepr = excinfo
|
||||
elif excinfo.errisinstance(py.test.skip.Exception):
|
||||
elif excinfo.errisinstance(pytest.skip.Exception):
|
||||
outcome = "skipped"
|
||||
r = excinfo._getreprcrash()
|
||||
longrepr = (str(r.path), r.lineno, r.message)
|
||||
@@ -198,17 +223,20 @@ def pytest_runtest_makereport(item, call):
|
||||
if call.when == "call":
|
||||
longrepr = item.repr_failure(excinfo)
|
||||
else: # exception in setup or teardown
|
||||
longrepr = item._repr_failure_py(excinfo)
|
||||
longrepr = item._repr_failure_py(excinfo,
|
||||
style=item.config.option.tbstyle)
|
||||
for rwhen, key, content in item._report_sections:
|
||||
sections.append(("Captured %s %s" %(key, rwhen), content))
|
||||
return TestReport(item.nodeid, item.location,
|
||||
keywords, outcome, longrepr, when,
|
||||
duration=duration)
|
||||
sections, duration)
|
||||
|
||||
class TestReport(BaseReport):
|
||||
""" Basic test report object (also used for setup and teardown calls if
|
||||
they fail).
|
||||
"""
|
||||
def __init__(self, nodeid, location,
|
||||
keywords, outcome, longrepr, when, sections=(), duration=0, **extra):
|
||||
def __init__(self, nodeid, location, keywords, outcome,
|
||||
longrepr, when, sections=(), duration=0, **extra):
|
||||
#: normalized collection node id
|
||||
self.nodeid = nodeid
|
||||
|
||||
@@ -257,7 +285,9 @@ def pytest_make_collect_report(collector):
|
||||
if not call.excinfo:
|
||||
outcome = "passed"
|
||||
else:
|
||||
if call.excinfo.errisinstance(py.test.skip.Exception):
|
||||
from _pytest import nose
|
||||
skip_exceptions = (Skipped,) + nose.get_skip_exceptions()
|
||||
if call.excinfo.errisinstance(skip_exceptions):
|
||||
outcome = "skipped"
|
||||
r = collector._repr_failure_py(call.excinfo, "line").reprcrash
|
||||
longrepr = (str(r.path), r.lineno, r.message)
|
||||
@@ -267,11 +297,15 @@ def pytest_make_collect_report(collector):
|
||||
if not hasattr(errorinfo, "toterminal"):
|
||||
errorinfo = CollectErrorRepr(errorinfo)
|
||||
longrepr = errorinfo
|
||||
return CollectReport(collector.nodeid, outcome, longrepr,
|
||||
rep = CollectReport(collector.nodeid, outcome, longrepr,
|
||||
getattr(call, 'result', None))
|
||||
rep.call = call # see collect_one_node
|
||||
return rep
|
||||
|
||||
|
||||
class CollectReport(BaseReport):
|
||||
def __init__(self, nodeid, outcome, longrepr, result, sections=(), **extra):
|
||||
def __init__(self, nodeid, outcome, longrepr, result,
|
||||
sections=(), **extra):
|
||||
self.nodeid = nodeid
|
||||
self.outcome = outcome
|
||||
self.longrepr = longrepr
|
||||
@@ -303,11 +337,10 @@ class SetupState(object):
|
||||
""" attach a finalizer to the given colitem.
|
||||
if colitem is None, this will add a finalizer that
|
||||
is called at the end of teardown_all().
|
||||
if colitem is a tuple, it will be used as a key
|
||||
and needs an explicit call to _callfinalizers(key) later on.
|
||||
"""
|
||||
assert hasattr(finalizer, '__call__')
|
||||
#assert colitem in self.stack
|
||||
assert colitem and not isinstance(colitem, tuple)
|
||||
assert py.builtin.callable(finalizer)
|
||||
#assert colitem in self.stack # some unit tests don't setup stack :/
|
||||
self._finalizers.setdefault(colitem, []).append(finalizer)
|
||||
|
||||
def _pop_and_teardown(self):
|
||||
@@ -316,9 +349,18 @@ class SetupState(object):
|
||||
|
||||
def _callfinalizers(self, colitem):
|
||||
finalizers = self._finalizers.pop(colitem, None)
|
||||
exc = None
|
||||
while finalizers:
|
||||
fin = finalizers.pop()
|
||||
fin()
|
||||
try:
|
||||
fin()
|
||||
except Exception:
|
||||
# XXX Only first exception will be seen by user,
|
||||
# ideally all should be reported.
|
||||
if exc is None:
|
||||
exc = sys.exc_info()
|
||||
if exc:
|
||||
py.builtin._reraise(*exc)
|
||||
|
||||
def _teardown_with_finalization(self, colitem):
|
||||
self._callfinalizers(colitem)
|
||||
@@ -363,6 +405,16 @@ class SetupState(object):
|
||||
col._prepare_exc = sys.exc_info()
|
||||
raise
|
||||
|
||||
def collect_one_node(collector):
|
||||
ihook = collector.ihook
|
||||
ihook.pytest_collectstart(collector=collector)
|
||||
rep = ihook.pytest_make_collect_report(collector=collector)
|
||||
call = rep.__dict__.pop("call", None)
|
||||
if call and check_interactive_exception(call, rep):
|
||||
ihook.pytest_exception_interact(node=collector, call=call, report=rep)
|
||||
return rep
|
||||
|
||||
|
||||
# =============================================================
|
||||
# Test OutcomeExceptions and helpers for creating them.
|
||||
|
||||
@@ -388,7 +440,7 @@ class Skipped(OutcomeException):
|
||||
__module__ = 'builtins'
|
||||
|
||||
class Failed(OutcomeException):
|
||||
""" raised from an explicit call to py.test.fail() """
|
||||
""" raised from an explicit call to pytest.fail() """
|
||||
__module__ = 'builtins'
|
||||
|
||||
class Exit(KeyboardInterrupt):
|
||||
@@ -408,7 +460,7 @@ exit.Exception = Exit
|
||||
|
||||
def skip(msg=""):
|
||||
""" skip an executing test with the given message. Note: it's usually
|
||||
better to use the py.test.mark.skipif marker to declare a test to be
|
||||
better to use the pytest.mark.skipif marker to declare a test to be
|
||||
skipped under certain conditions like mismatching platforms or
|
||||
dependencies. See the pytest_skipping plugin for details.
|
||||
"""
|
||||
@@ -428,25 +480,28 @@ fail.Exception = Failed
|
||||
|
||||
|
||||
def importorskip(modname, minversion=None):
|
||||
""" return imported module if it has a higher __version__ than the
|
||||
optionally specified 'minversion' - otherwise call py.test.skip()
|
||||
with a message detailing the mismatch.
|
||||
""" return imported module if it has at least "minversion" as its
|
||||
__version__ attribute. If no minversion is specified the a skip
|
||||
is only triggered if the module can not be imported.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
compile(modname, '', 'eval') # to catch syntaxerrors
|
||||
try:
|
||||
__import__(modname)
|
||||
except ImportError:
|
||||
py.test.skip("could not import %r" %(modname,))
|
||||
skip("could not import %r" %(modname,))
|
||||
mod = sys.modules[modname]
|
||||
if minversion is None:
|
||||
return mod
|
||||
verattr = getattr(mod, '__version__', None)
|
||||
if isinstance(minversion, str):
|
||||
minver = minversion.split(".")
|
||||
else:
|
||||
minver = list(minversion)
|
||||
if verattr is None or verattr.split(".") < minver:
|
||||
py.test.skip("module %r has __version__ %r, required is: %r" %(
|
||||
modname, verattr, minversion))
|
||||
if minversion is not 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,))
|
||||
if verattr is None or pv(verattr) < pv(minversion):
|
||||
skip("module %r has __version__ %r, required is: %r" %(
|
||||
modname, verattr, minversion))
|
||||
return mod
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
""" support for skip/xfail functions and markers. """
|
||||
|
||||
import py, pytest
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import py
|
||||
import pytest
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("general")
|
||||
@@ -10,6 +13,14 @@ def pytest_addoption(parser):
|
||||
help="run tests even if they are marked xfail")
|
||||
|
||||
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",
|
||||
"skipif(condition): skip the given test function if eval(condition) "
|
||||
"results in a True value. Evaluation happens within the "
|
||||
@@ -18,18 +29,20 @@ def pytest_configure(config):
|
||||
"http://pytest.org/latest/skipping.html"
|
||||
)
|
||||
config.addinivalue_line("markers",
|
||||
"xfail(condition, reason=None, run=True): mark the the test function "
|
||||
"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. See "
|
||||
"http://pytest.org/latest/skipping.html"
|
||||
"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 py.test.xfail() """
|
||||
""" raised from an explicit call to pytest.xfail() """
|
||||
|
||||
def xfail(reason=""):
|
||||
""" xfail an executing test or setup functions with the given reason."""
|
||||
@@ -44,7 +57,8 @@ class MarkEvaluator:
|
||||
|
||||
@property
|
||||
def holder(self):
|
||||
return self.item.keywords.get(self.name, None)
|
||||
return self.item.keywords.get(self.name)
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self.holder)
|
||||
__nonzero__ = __bool__
|
||||
@@ -52,18 +66,22 @@ class MarkEvaluator:
|
||||
def wasvalid(self):
|
||||
return not hasattr(self, 'exc')
|
||||
|
||||
def invalidraise(self, exc):
|
||||
raises = self.get('raises')
|
||||
if not raises:
|
||||
return
|
||||
return not isinstance(exc, raises)
|
||||
|
||||
def istrue(self):
|
||||
try:
|
||||
return self._istrue()
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
except Exception:
|
||||
self.exc = sys.exc_info()
|
||||
if isinstance(self.exc[1], SyntaxError):
|
||||
msg = [" " * (self.exc[1].offset + 4) + "^",]
|
||||
msg.append("SyntaxError: invalid syntax")
|
||||
else:
|
||||
msg = py.std.traceback.format_exception_only(*self.exc[:2])
|
||||
msg = traceback.format_exception_only(*self.exc[:2])
|
||||
pytest.fail("Error evaluating %r expression\n"
|
||||
" %s\n"
|
||||
"%s"
|
||||
@@ -71,7 +89,7 @@ class MarkEvaluator:
|
||||
pytrace=False)
|
||||
|
||||
def _getglobals(self):
|
||||
d = {'os': py.std.os, 'sys': py.std.sys, 'config': self.item.config}
|
||||
d = {'os': os, 'sys': sys, 'config': self.item.config}
|
||||
func = self.item.obj
|
||||
try:
|
||||
d.update(func.__globals__)
|
||||
@@ -80,20 +98,36 @@ class MarkEvaluator:
|
||||
return d
|
||||
|
||||
def _istrue(self):
|
||||
if hasattr(self, 'result'):
|
||||
return self.result
|
||||
if self.holder:
|
||||
d = self._getglobals()
|
||||
if self.holder.args:
|
||||
self.result = False
|
||||
for expr in self.holder.args:
|
||||
self.expr = expr
|
||||
if isinstance(expr, py.builtin._basestring):
|
||||
result = cached_eval(self.item.config, expr, d)
|
||||
else:
|
||||
pytest.fail("expression is not a string")
|
||||
if result:
|
||||
self.result = True
|
||||
# "holder" might be a MarkInfo or a MarkDecorator; only
|
||||
# MarkInfo keeps track of all parameters it received in an
|
||||
# _arglist attribute
|
||||
if hasattr(self.holder, '_arglist'):
|
||||
arglist = self.holder._arglist
|
||||
else:
|
||||
arglist = [(self.holder.args, self.holder.kwargs)]
|
||||
for args, kwargs in arglist:
|
||||
for expr in args:
|
||||
self.expr = expr
|
||||
break
|
||||
if isinstance(expr, py.builtin._basestring):
|
||||
result = cached_eval(self.item.config, expr, d)
|
||||
else:
|
||||
if "reason" not in kwargs:
|
||||
# XXX better be checked at collection time
|
||||
msg = "you need to specify reason=STRING " \
|
||||
"when using booleans as conditions."
|
||||
pytest.fail(msg)
|
||||
result = bool(expr)
|
||||
if result:
|
||||
self.result = True
|
||||
self.reason = kwargs.get('reason', None)
|
||||
self.expr = expr
|
||||
return self.result
|
||||
else:
|
||||
self.result = True
|
||||
return getattr(self, 'result', False)
|
||||
@@ -102,7 +136,7 @@ class MarkEvaluator:
|
||||
return self.holder.kwargs.get(attr, default)
|
||||
|
||||
def getexplanation(self):
|
||||
expl = self.get('reason', None)
|
||||
expl = getattr(self, 'reason', None) or self.get('reason', None)
|
||||
if not expl:
|
||||
if not hasattr(self, 'expr'):
|
||||
return ""
|
||||
@@ -111,13 +145,12 @@ class MarkEvaluator:
|
||||
return expl
|
||||
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_runtest_setup(item):
|
||||
if not isinstance(item, pytest.Function):
|
||||
return
|
||||
evalskip = MarkEvaluator(item, 'skipif')
|
||||
if evalskip.istrue():
|
||||
py.test.skip(evalskip.getexplanation())
|
||||
item._evalskip = evalskip
|
||||
pytest.skip(evalskip.getexplanation())
|
||||
item._evalxfail = MarkEvaluator(item, 'xfail')
|
||||
check_xfail_no_run(item)
|
||||
|
||||
@@ -129,44 +162,42 @@ def check_xfail_no_run(item):
|
||||
evalxfail = item._evalxfail
|
||||
if evalxfail.istrue():
|
||||
if not evalxfail.get('run', True):
|
||||
py.test.xfail("[NOTRUN] " + evalxfail.getexplanation())
|
||||
pytest.xfail("[NOTRUN] " + evalxfail.getexplanation())
|
||||
|
||||
def pytest_runtest_makereport(__multicall__, item, call):
|
||||
if not isinstance(item, pytest.Function):
|
||||
return
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
outcome = yield
|
||||
rep = outcome.get_result()
|
||||
evalxfail = getattr(item, '_evalxfail', None)
|
||||
evalskip = getattr(item, '_evalskip', None)
|
||||
# unitttest special case, see setting of _unexpectedsuccess
|
||||
if hasattr(item, '_unexpectedsuccess'):
|
||||
rep = __multicall__.execute()
|
||||
if rep.when == "call":
|
||||
# we need to translate into how py.test encodes xpass
|
||||
rep.wasxfail = "reason: " + repr(item._unexpectedsuccess)
|
||||
rep.outcome = "failed"
|
||||
return rep
|
||||
if not (call.excinfo and
|
||||
call.excinfo.errisinstance(py.test.xfail.Exception)):
|
||||
evalxfail = getattr(item, '_evalxfail', None)
|
||||
if not evalxfail:
|
||||
return
|
||||
if call.excinfo and call.excinfo.errisinstance(py.test.xfail.Exception):
|
||||
if not item.config.getvalue("runxfail"):
|
||||
rep = __multicall__.execute()
|
||||
rep.wasxfail = "reason: " + call.excinfo.value.msg
|
||||
rep.outcome = "skipped"
|
||||
return rep
|
||||
rep = __multicall__.execute()
|
||||
evalxfail = item._evalxfail
|
||||
if not rep.skipped:
|
||||
if not item.config.option.runxfail:
|
||||
if evalxfail.wasvalid() and evalxfail.istrue():
|
||||
if call.excinfo:
|
||||
rep.outcome = "skipped"
|
||||
elif call.when == "call":
|
||||
rep.outcome = "failed"
|
||||
else:
|
||||
return rep
|
||||
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"
|
||||
elif item.config.option.runxfail:
|
||||
pass # don't interefere
|
||||
elif call.excinfo and call.excinfo.errisinstance(pytest.xfail.Exception):
|
||||
rep.wasxfail = "reason: " + call.excinfo.value.msg
|
||||
rep.outcome = "skipped"
|
||||
elif evalxfail and not rep.skipped and evalxfail.wasvalid() and \
|
||||
evalxfail.istrue():
|
||||
if call.excinfo:
|
||||
if evalxfail.invalidraise(call.excinfo.value):
|
||||
rep.outcome = "failed"
|
||||
else:
|
||||
rep.outcome = "skipped"
|
||||
rep.wasxfail = evalxfail.getexplanation()
|
||||
return rep
|
||||
return rep
|
||||
elif call.when == "call":
|
||||
rep.outcome = "failed" # xpass outcome
|
||||
rep.wasxfail = evalxfail.getexplanation()
|
||||
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
|
||||
# the location of where the skip exception was raised within pytest
|
||||
filename, line, reason = rep.longrepr
|
||||
filename, line = item.location[:2]
|
||||
rep.longrepr = filename, line, reason
|
||||
|
||||
# called by terminalreporter progress reporting
|
||||
def pytest_report_teststatus(report):
|
||||
@@ -174,7 +205,7 @@ def pytest_report_teststatus(report):
|
||||
if report.skipped:
|
||||
return "xfailed", "x", "xfail"
|
||||
elif report.failed:
|
||||
return "xpassed", "X", "XPASS"
|
||||
return "xpassed", "X", ("XPASS", {'yellow': True})
|
||||
|
||||
# called by the terminalreporter instance/plugin
|
||||
def pytest_terminal_summary(terminalreporter):
|
||||
@@ -205,18 +236,17 @@ def pytest_terminal_summary(terminalreporter):
|
||||
tr._tw.line(line)
|
||||
|
||||
def show_simple(terminalreporter, lines, stat, format):
|
||||
tw = terminalreporter._tw
|
||||
failed = terminalreporter.stats.get(stat)
|
||||
if failed:
|
||||
for rep in failed:
|
||||
pos = rep.nodeid
|
||||
lines.append(format %(pos, ))
|
||||
pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid)
|
||||
lines.append(format %(pos,))
|
||||
|
||||
def show_xfailed(terminalreporter, lines):
|
||||
xfailed = terminalreporter.stats.get("xfailed")
|
||||
if xfailed:
|
||||
for rep in xfailed:
|
||||
pos = rep.nodeid
|
||||
pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid)
|
||||
reason = rep.wasxfail
|
||||
lines.append("XFAIL %s" % (pos,))
|
||||
if reason:
|
||||
@@ -226,7 +256,7 @@ def show_xpassed(terminalreporter, lines):
|
||||
xpassed = terminalreporter.stats.get("xpassed")
|
||||
if xpassed:
|
||||
for rep in xpassed:
|
||||
pos = rep.nodeid
|
||||
pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid)
|
||||
reason = rep.wasxfail
|
||||
lines.append("XPASS %s %s" %(pos, reason))
|
||||
|
||||
|
||||
@@ -1,18 +1,40 @@
|
||||
#! /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
|
||||
import imp
|
||||
|
||||
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:
|
||||
@@ -36,7 +58,7 @@ class DictImporter(object):
|
||||
if is_pkg:
|
||||
module.__path__ = [fullname]
|
||||
|
||||
do_exec(co, module.__dict__)
|
||||
do_exec(co, module.__dict__) # noqa
|
||||
return sys.modules[fullname]
|
||||
|
||||
def get_source(self, name):
|
||||
@@ -46,6 +68,11 @@ class DictImporter(object):
|
||||
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
|
||||
@@ -58,6 +85,5 @@ if __name__ == "__main__":
|
||||
|
||||
importer = DictImporter(sources)
|
||||
sys.meta_path.insert(0, importer)
|
||||
|
||||
entry = "@ENTRY@"
|
||||
do_exec(entry, locals())
|
||||
do_exec(entry, locals()) # noqa
|
||||
|
||||
@@ -2,10 +2,16 @@
|
||||
|
||||
This is a good source for looking at the various reporting hooks.
|
||||
"""
|
||||
from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, \
|
||||
EXIT_USAGEERROR, EXIT_NOTESTSCOLLECTED
|
||||
import pytest
|
||||
import py
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import platform
|
||||
|
||||
import _pytest._pluggy as pluggy
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("terminal reporting", "reporting", after="general")
|
||||
@@ -16,7 +22,7 @@ def pytest_addoption(parser):
|
||||
group._addoption('-r',
|
||||
action="store", dest="reportchars", default=None, metavar="chars",
|
||||
help="show extra test summary info as specified by chars (f)ailed, "
|
||||
"(E)error, (s)skipped, (x)failed, (X)passed.")
|
||||
"(E)error, (s)skipped, (x)failed, (X)passed (w)pytest-warnings (a)all.")
|
||||
group._addoption('-l', '--showlocals',
|
||||
action="store_true", dest="showlocals", default=False,
|
||||
help="show locals in tracebacks (disabled by default).")
|
||||
@@ -24,30 +30,20 @@ def pytest_addoption(parser):
|
||||
action="store", dest="report", default=None, metavar="opts",
|
||||
help="(deprecated, use -r)")
|
||||
group._addoption('--tb', metavar="style",
|
||||
action="store", dest="tbstyle", default='long',
|
||||
type="choice", choices=['long', 'short', 'no', 'line', 'native'],
|
||||
help="traceback print mode (long/short/line/native/no).")
|
||||
group._addoption('--fulltrace',
|
||||
action="store_true", dest="fulltrace", default=False,
|
||||
action="store", dest="tbstyle", default='auto',
|
||||
choices=['auto', 'long', 'short', 'no', 'line', 'native'],
|
||||
help="traceback print mode (auto/long/short/line/native/no).")
|
||||
group._addoption('--fulltrace', '--full-trace',
|
||||
action="store_true", default=False,
|
||||
help="don't cut any tracebacks (default is to cut).")
|
||||
group._addoption('--color', metavar="color",
|
||||
action="store", dest="color", default='auto',
|
||||
choices=['yes', 'no', 'auto'],
|
||||
help="color terminal output (yes/no/auto).")
|
||||
|
||||
def pytest_configure(config):
|
||||
config.option.verbose -= config.option.quiet
|
||||
# we try hard to make printing resilient against
|
||||
# later changes on FD level.
|
||||
stdout = py.std.sys.stdout
|
||||
if hasattr(os, 'dup') and hasattr(stdout, 'fileno'):
|
||||
try:
|
||||
newstdout = py.io.dupfile(stdout, buffering=1,
|
||||
encoding=stdout.encoding)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
config._cleanup.append(lambda: newstdout.close())
|
||||
assert stdout.encoding == newstdout.encoding
|
||||
stdout = newstdout
|
||||
|
||||
reporter = TerminalReporter(config, stdout)
|
||||
reporter = TerminalReporter(config, sys.stdout)
|
||||
config.pluginmanager.register(reporter, 'terminalreporter')
|
||||
if config.option.debug or config.option.traceconfig:
|
||||
def mywriter(tags, args):
|
||||
@@ -60,7 +56,7 @@ def getreportopt(config):
|
||||
optvalue = config.option.report
|
||||
if optvalue:
|
||||
py.builtin.print_("DEPRECATED: use -r instead of --report option.",
|
||||
file=py.std.sys.stderr)
|
||||
file=sys.stderr)
|
||||
if optvalue:
|
||||
for setting in optvalue.split(","):
|
||||
setting = setting.strip()
|
||||
@@ -71,8 +67,10 @@ def getreportopt(config):
|
||||
reportchars = config.option.reportchars
|
||||
if reportchars:
|
||||
for char in reportchars:
|
||||
if char not in reportopts:
|
||||
if char not in reportopts and char != 'a':
|
||||
reportopts += char
|
||||
elif char == 'a':
|
||||
reportopts = 'fEsxXw'
|
||||
return reportopts
|
||||
|
||||
def pytest_report_teststatus(report):
|
||||
@@ -86,8 +84,17 @@ def pytest_report_teststatus(report):
|
||||
letter = "f"
|
||||
return report.outcome, letter, report.outcome.upper()
|
||||
|
||||
class WarningReport:
|
||||
def __init__(self, code, message, nodeid=None, fslocation=None):
|
||||
self.code = code
|
||||
self.message = message
|
||||
self.nodeid = nodeid
|
||||
self.fslocation = fslocation
|
||||
|
||||
|
||||
class TerminalReporter:
|
||||
def __init__(self, config, file=None):
|
||||
import _pytest.config
|
||||
self.config = config
|
||||
self.verbosity = self.config.option.verbose
|
||||
self.showheader = self.verbosity >= 0
|
||||
@@ -96,24 +103,25 @@ class TerminalReporter:
|
||||
self._numcollected = 0
|
||||
|
||||
self.stats = {}
|
||||
self.startdir = self.curdir = py.path.local()
|
||||
self.startdir = py.path.local()
|
||||
if file is None:
|
||||
file = py.std.sys.stdout
|
||||
self._tw = py.io.TerminalWriter(file)
|
||||
file = sys.stdout
|
||||
self._tw = self.writer = _pytest.config.create_terminal_writer(config,
|
||||
file)
|
||||
self.currentfspath = None
|
||||
self.reportchars = getreportopt(config)
|
||||
self.hasmarkup = self._tw.hasmarkup
|
||||
|
||||
def hasopt(self, char):
|
||||
char = {'xfailed': 'x', 'skipped': 's'}.get(char,char)
|
||||
char = {'xfailed': 'x', 'skipped': 's'}.get(char, char)
|
||||
return char in self.reportchars
|
||||
|
||||
def write_fspath_result(self, fspath, res):
|
||||
def write_fspath_result(self, nodeid, res):
|
||||
fspath = self.config.rootdir.join(nodeid.split("::")[0])
|
||||
if fspath != self.currentfspath:
|
||||
self.currentfspath = fspath
|
||||
#fspath = self.startdir.bestrelpath(fspath)
|
||||
fspath = self.startdir.bestrelpath(fspath)
|
||||
self._tw.line()
|
||||
#relpath = self.startdir.bestrelpath(fspath)
|
||||
self._tw.write(fspath + " ")
|
||||
self._tw.write(res)
|
||||
|
||||
@@ -135,7 +143,8 @@ class TerminalReporter:
|
||||
self._tw.write(content, **markup)
|
||||
|
||||
def write_line(self, line, **markup):
|
||||
line = str(line)
|
||||
if not py.builtin._istext(line):
|
||||
line = py.builtin.text(line, errors="replace")
|
||||
self.ensure_newline()
|
||||
self._tw.line(line, **markup)
|
||||
|
||||
@@ -147,14 +156,28 @@ class TerminalReporter:
|
||||
self.ensure_newline()
|
||||
self._tw.sep(sep, title, **markup)
|
||||
|
||||
def section(self, title, sep="=", **kw):
|
||||
self._tw.sep(sep, title, **kw)
|
||||
|
||||
def line(self, msg, **kw):
|
||||
self._tw.line(msg, **kw)
|
||||
|
||||
def pytest_internalerror(self, excrepr):
|
||||
for line in str(excrepr).split("\n"):
|
||||
for line in py.builtin.text(excrepr).split("\n"):
|
||||
self.write_line("INTERNALERROR> " + line)
|
||||
return 1
|
||||
|
||||
def pytest_logwarning(self, code, fslocation, message, nodeid):
|
||||
warnings = self.stats.setdefault("warnings", [])
|
||||
if isinstance(fslocation, tuple):
|
||||
fslocation = "%s:%d" % fslocation
|
||||
warning = WarningReport(code=code, fslocation=fslocation,
|
||||
message=message, nodeid=nodeid)
|
||||
warnings.append(warning)
|
||||
|
||||
def pytest_plugin_registered(self, plugin):
|
||||
if self.config.option.traceconfig:
|
||||
msg = "PLUGIN registered: %s" %(plugin,)
|
||||
msg = "PLUGIN registered: %s" % (plugin,)
|
||||
# XXX this event may happen during setup/teardown time
|
||||
# which unfortunately captures our output here
|
||||
# which garbles our output if we use self.write_line
|
||||
@@ -166,24 +189,25 @@ class TerminalReporter:
|
||||
def pytest_runtest_logstart(self, nodeid, location):
|
||||
# ensure that the path is printed before the
|
||||
# 1st test of a module starts running
|
||||
fspath = nodeid.split("::")[0]
|
||||
if self.showlongtestinfo:
|
||||
line = self._locationline(fspath, *location)
|
||||
line = self._locationline(nodeid, *location)
|
||||
self.write_ensure_prefix(line, "")
|
||||
elif self.showfspath:
|
||||
self.write_fspath_result(fspath, "")
|
||||
fsid = nodeid.split("::")[0]
|
||||
self.write_fspath_result(fsid, "")
|
||||
|
||||
def pytest_runtest_logreport(self, report):
|
||||
rep = report
|
||||
res = self.config.hook.pytest_report_teststatus(report=rep)
|
||||
cat, letter, word = res
|
||||
self.stats.setdefault(cat, []).append(rep)
|
||||
self._tests_ran = True
|
||||
if not letter and not word:
|
||||
# probably passed setup/teardown
|
||||
return
|
||||
if self.verbosity <= 0:
|
||||
if not hasattr(rep, 'node') and self.showfspath:
|
||||
self.write_fspath_result(rep.fspath, letter)
|
||||
self.write_fspath_result(rep.nodeid, letter)
|
||||
else:
|
||||
self._tw.write(letter)
|
||||
else:
|
||||
@@ -196,7 +220,7 @@ class TerminalReporter:
|
||||
markup = {'red':True}
|
||||
elif rep.skipped:
|
||||
markup = {'yellow':True}
|
||||
line = self._locationline(str(rep.fspath), *rep.location)
|
||||
line = self._locationline(rep.nodeid, *rep.location)
|
||||
if not hasattr(rep, 'node'):
|
||||
self.write_ensure_prefix(line, word, **markup)
|
||||
#self._tw.write(word, **markup)
|
||||
@@ -209,7 +233,7 @@ class TerminalReporter:
|
||||
self.currentfspath = -2
|
||||
|
||||
def pytest_collection(self):
|
||||
if not self.hasmarkup and self.config.option.verbose >=1:
|
||||
if not self.hasmarkup and self.config.option.verbose >= 1:
|
||||
self.write("collecting ... ", bold=True)
|
||||
|
||||
def pytest_collectreport(self, report):
|
||||
@@ -220,7 +244,7 @@ class TerminalReporter:
|
||||
items = [x for x in report.result if isinstance(x, pytest.Item)]
|
||||
self._numcollected += len(items)
|
||||
if self.hasmarkup:
|
||||
#self.write_fspath_result(report.fspath, 'E')
|
||||
#self.write_fspath_result(report.nodeid, 'E')
|
||||
self.report_collect()
|
||||
|
||||
def report_collect(self, final=False):
|
||||
@@ -248,18 +272,19 @@ class TerminalReporter:
|
||||
def pytest_collection_modifyitems(self):
|
||||
self.report_collect(True)
|
||||
|
||||
@pytest.mark.trylast
|
||||
@pytest.hookimpl(trylast=True)
|
||||
def pytest_sessionstart(self, session):
|
||||
self._sessionstarttime = py.std.time.time()
|
||||
self._sessionstarttime = time.time()
|
||||
if not self.showheader:
|
||||
return
|
||||
self.write_sep("=", "test session starts", bold=True)
|
||||
verinfo = ".".join(map(str, sys.version_info[:3]))
|
||||
verinfo = platform.python_version()
|
||||
msg = "platform %s -- Python %s" % (sys.platform, verinfo)
|
||||
if hasattr(sys, 'pypy_version_info'):
|
||||
verinfo = ".".join(map(str, sys.pypy_version_info[:3]))
|
||||
msg += "[pypy-%s-%s]" % (verinfo, sys.pypy_version_info[3])
|
||||
msg += " -- pytest-%s" % (py.test.__version__)
|
||||
msg += ", pytest-%s, py-%s, pluggy-%s" % (
|
||||
pytest.__version__, py.__version__, pluggy.__version__)
|
||||
if self.verbosity > 0 or self.config.option.debug or \
|
||||
getattr(self.config.option, 'pastebin', None):
|
||||
msg += " -- " + str(sys.executable)
|
||||
@@ -271,15 +296,17 @@ class TerminalReporter:
|
||||
self.write_line(line)
|
||||
|
||||
def pytest_report_header(self, config):
|
||||
plugininfo = config.pluginmanager._plugin_distinfo
|
||||
inifile = ""
|
||||
if config.inifile:
|
||||
inifile = config.rootdir.bestrelpath(config.inifile)
|
||||
lines = ["rootdir: %s, inifile: %s" %(config.rootdir, inifile)]
|
||||
|
||||
plugininfo = config.pluginmanager.list_plugin_distinfo()
|
||||
if plugininfo:
|
||||
l = []
|
||||
for dist, plugin in plugininfo:
|
||||
name = dist.project_name
|
||||
if name.startswith("pytest-"):
|
||||
name = name[7:]
|
||||
l.append(name)
|
||||
return "plugins: %s" % ", ".join(l)
|
||||
|
||||
lines.append(
|
||||
"plugins: %s" % ", ".join(_plugin_nameversions(plugininfo)))
|
||||
return lines
|
||||
|
||||
def pytest_collection_finish(self, session):
|
||||
if self.config.option.collectonly:
|
||||
@@ -325,17 +352,23 @@ class TerminalReporter:
|
||||
stack.append(col)
|
||||
#if col.name == "()":
|
||||
# continue
|
||||
indent = (len(stack)-1) * " "
|
||||
self._tw.line("%s%s" %(indent, col))
|
||||
indent = (len(stack) - 1) * " "
|
||||
self._tw.line("%s%s" % (indent, col))
|
||||
|
||||
def pytest_sessionfinish(self, exitstatus, __multicall__):
|
||||
__multicall__.execute()
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_sessionfinish(self, exitstatus):
|
||||
outcome = yield
|
||||
outcome.get_result()
|
||||
self._tw.line("")
|
||||
if exitstatus in (0, 1, 2, 4):
|
||||
summary_exit_codes = (
|
||||
EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, EXIT_USAGEERROR,
|
||||
EXIT_NOTESTSCOLLECTED)
|
||||
if exitstatus in summary_exit_codes:
|
||||
self.summary_errors()
|
||||
self.summary_failures()
|
||||
self.summary_warnings()
|
||||
self.config.hook.pytest_terminal_summary(terminalreporter=self)
|
||||
if exitstatus == 2:
|
||||
if exitstatus == EXIT_INTERRUPTED:
|
||||
self._report_keyboardinterrupt()
|
||||
del self._keyboardinterrupt_memo
|
||||
self.summary_deselected()
|
||||
@@ -358,20 +391,24 @@ class TerminalReporter:
|
||||
else:
|
||||
excrepr.reprcrash.toterminal(self._tw)
|
||||
|
||||
def _locationline(self, collect_fspath, fspath, lineno, domain):
|
||||
def _locationline(self, nodeid, fspath, lineno, domain):
|
||||
def mkrel(nodeid):
|
||||
line = self.config.cwd_relative_nodeid(nodeid)
|
||||
if domain and line.endswith(domain):
|
||||
line = line[:-len(domain)]
|
||||
l = domain.split("[")
|
||||
l[0] = l[0].replace('.', '::') # don't replace '.' in params
|
||||
line += "[".join(l)
|
||||
return line
|
||||
# collect_fspath comes from testid which has a "/"-normalized path
|
||||
if fspath and fspath.replace("\\", "/") != collect_fspath:
|
||||
fspath = "%s <- %s" % (collect_fspath, fspath)
|
||||
|
||||
if fspath:
|
||||
line = str(fspath)
|
||||
if lineno is not None:
|
||||
lineno += 1
|
||||
line += ":" + str(lineno)
|
||||
if domain:
|
||||
line += ": " + str(domain)
|
||||
res = mkrel(nodeid).replace("::()", "") # parens-normalization
|
||||
if nodeid.split("::")[0] != fspath.replace("\\", "/"):
|
||||
res += " <- " + self.startdir.bestrelpath(fspath)
|
||||
else:
|
||||
line = "[location]"
|
||||
return line + " "
|
||||
res = "[location]"
|
||||
return res + " "
|
||||
|
||||
def _getfailureheadline(self, rep):
|
||||
if hasattr(rep, 'location'):
|
||||
@@ -399,6 +436,16 @@ class TerminalReporter:
|
||||
l.append(x)
|
||||
return l
|
||||
|
||||
def summary_warnings(self):
|
||||
if self.hasopt("w"):
|
||||
warnings = self.stats.get("warnings")
|
||||
if not warnings:
|
||||
return
|
||||
self.write_sep("=", "pytest-warning summary")
|
||||
for w in warnings:
|
||||
self._tw.line("W%s %s %s" % (w.code,
|
||||
w.fslocation, w.message))
|
||||
|
||||
def summary_failures(self):
|
||||
if self.config.option.tbstyle != "no":
|
||||
reports = self.getreports('failed')
|
||||
@@ -441,25 +488,15 @@ class TerminalReporter:
|
||||
self._tw.line(content)
|
||||
|
||||
def summary_stats(self):
|
||||
session_duration = py.std.time.time() - self._sessionstarttime
|
||||
session_duration = time.time() - self._sessionstarttime
|
||||
(line, color) = build_summary_stats_line(self.stats)
|
||||
msg = "%s in %.2f seconds" % (line, session_duration)
|
||||
markup = {color: True, 'bold': True}
|
||||
|
||||
keys = "failed passed skipped deselected xfailed xpassed".split()
|
||||
for key in self.stats.keys():
|
||||
if key not in keys:
|
||||
keys.append(key)
|
||||
parts = []
|
||||
for key in keys:
|
||||
if key: # setup/teardown reports have an empty key, ignore them
|
||||
val = self.stats.get(key, None)
|
||||
if val:
|
||||
parts.append("%d %s" %(len(val), key))
|
||||
line = ", ".join(parts)
|
||||
# XXX coloring
|
||||
msg = "%s in %.2f seconds" %(line, session_duration)
|
||||
if self.verbosity >= 0:
|
||||
self.write_sep("=", msg, bold=True)
|
||||
#else:
|
||||
# self.write_line(msg, bold=True)
|
||||
self.write_sep("=", msg, **markup)
|
||||
if self.verbosity == -1:
|
||||
self.write_line(msg, **markup)
|
||||
|
||||
def summary_deselected(self):
|
||||
if 'deselected' in self.stats:
|
||||
@@ -471,7 +508,7 @@ class TerminalReporter:
|
||||
if m:
|
||||
l.append("-m %r" % m)
|
||||
if l:
|
||||
self.write_sep("=", "%d tests deselected by %r" %(
|
||||
self.write_sep("=", "%d tests deselected by %r" % (
|
||||
len(self.stats['deselected']), " ".join(l)), bold=True)
|
||||
|
||||
def repr_pythonversion(v=None):
|
||||
@@ -490,3 +527,46 @@ def flatten(l):
|
||||
else:
|
||||
yield x
|
||||
|
||||
def build_summary_stats_line(stats):
|
||||
keys = ("failed passed skipped deselected "
|
||||
"xfailed xpassed warnings error").split()
|
||||
key_translation = {'warnings': 'pytest-warnings'}
|
||||
unknown_key_seen = False
|
||||
for key in stats.keys():
|
||||
if key not in keys:
|
||||
if key: # setup/teardown reports have an empty key, ignore them
|
||||
keys.append(key)
|
||||
unknown_key_seen = True
|
||||
parts = []
|
||||
for key in keys:
|
||||
val = stats.get(key, None)
|
||||
if val:
|
||||
key_name = key_translation.get(key, key)
|
||||
parts.append("%d %s" % (len(val), key_name))
|
||||
line = ", ".join(parts)
|
||||
|
||||
if 'failed' in stats or 'error' in stats:
|
||||
color = 'red'
|
||||
elif 'warnings' in stats or unknown_key_seen:
|
||||
color = 'yellow'
|
||||
elif 'passed' in stats:
|
||||
color = 'green'
|
||||
else:
|
||||
color = 'yellow'
|
||||
|
||||
return (line, color)
|
||||
|
||||
|
||||
def _plugin_nameversions(plugininfo):
|
||||
l = []
|
||||
for plugin, dist in plugininfo:
|
||||
# gets us name and version!
|
||||
name = '{dist.project_name}-{dist.version}'.format(dist=dist)
|
||||
# questionable convenience, but it keeps things short
|
||||
if name.startswith("pytest-"):
|
||||
name = name[7:]
|
||||
# we decided to print python package names
|
||||
# they can have more than one plugin
|
||||
if name not in l:
|
||||
l.append(name)
|
||||
return l
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
""" support for providing temporary directories to test functions. """
|
||||
import pytest, py
|
||||
import re
|
||||
|
||||
import pytest
|
||||
import py
|
||||
from _pytest.monkeypatch import monkeypatch
|
||||
|
||||
class TempdirHandler:
|
||||
|
||||
class TempdirFactory:
|
||||
"""Factory for temporary directories under the common base temp directory.
|
||||
|
||||
The base directory can be configured using the ``--basetemp`` option.
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.trace = config.trace.get("tmpdir")
|
||||
@@ -18,6 +27,10 @@ class TempdirHandler:
|
||||
return self.getbasetemp().ensure(string, dir=dir)
|
||||
|
||||
def mktemp(self, basename, numbered=True):
|
||||
"""Create a subdirectory of the base temporary directory and return it.
|
||||
If ``numbered``, ensure the directory is unique by adding a number
|
||||
prefix greater than any existing one.
|
||||
"""
|
||||
basetemp = self.getbasetemp()
|
||||
if not numbered:
|
||||
p = basetemp.mkdir(basename)
|
||||
@@ -39,7 +52,17 @@ class TempdirHandler:
|
||||
basetemp.remove()
|
||||
basetemp.mkdir()
|
||||
else:
|
||||
basetemp = py.path.local.make_numbered_dir(prefix='pytest-')
|
||||
temproot = py.path.local.get_temproot()
|
||||
user = get_user()
|
||||
if user:
|
||||
# use a sub-directory in the temproot to speed-up
|
||||
# make_numbered_dir() call
|
||||
rootdir = temproot.join('pytest-of-%s' % user)
|
||||
else:
|
||||
rootdir = temproot
|
||||
rootdir.ensure(dir=1)
|
||||
basetemp = py.path.local.make_numbered_dir(prefix='pytest-',
|
||||
rootdir=rootdir)
|
||||
self._basetemp = t = basetemp.realpath()
|
||||
self.trace("new basetemp", t)
|
||||
return t
|
||||
@@ -47,15 +70,44 @@ class TempdirHandler:
|
||||
def finish(self):
|
||||
self.trace("finish")
|
||||
|
||||
|
||||
def get_user():
|
||||
"""Return the current user name, or None if getuser() does not work
|
||||
in the current environment (see #1010).
|
||||
"""
|
||||
import getpass
|
||||
try:
|
||||
return getpass.getuser()
|
||||
except (ImportError, KeyError):
|
||||
return None
|
||||
|
||||
# backward compatibility
|
||||
TempdirHandler = TempdirFactory
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
"""Create a TempdirFactory and attach it to the config object.
|
||||
|
||||
This is to comply with existing plugins which expect the handler to be
|
||||
available at pytest_configure time, but ideally should be moved entirely
|
||||
to the tmpdir_factory session fixture.
|
||||
"""
|
||||
mp = monkeypatch()
|
||||
t = TempdirHandler(config)
|
||||
t = TempdirFactory(config)
|
||||
config._cleanup.extend([mp.undo, t.finish])
|
||||
mp.setattr(config, '_tmpdirhandler', t, raising=False)
|
||||
mp.setattr(pytest, 'ensuretemp', t.ensuretemp, raising=False)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def tmpdir_factory(request):
|
||||
"""Return a TempdirFactory instance for the test session.
|
||||
"""
|
||||
return request.config._tmpdirhandler
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tmpdir(request):
|
||||
def tmpdir(request, tmpdir_factory):
|
||||
"""return a temporary directory path object
|
||||
which is unique to each test function invocation,
|
||||
created as a sub directory of the base temporary
|
||||
@@ -63,6 +115,9 @@ def tmpdir(request):
|
||||
path object.
|
||||
"""
|
||||
name = request.node.name
|
||||
name = py.std.re.sub("[\W]", "_", name)
|
||||
x = request.config._tmpdirhandler.mktemp(name, numbered=True)
|
||||
name = re.sub("[\W]", "_", name)
|
||||
MAXVAL = 30
|
||||
if len(name) > MAXVAL:
|
||||
name = name[:MAXVAL]
|
||||
x = tmpdir_factory.mktemp(name, numbered=True)
|
||||
return x
|
||||
|
||||
@@ -1,40 +1,57 @@
|
||||
""" discovery and running of std-library "unittest" style tests. """
|
||||
import pytest, py
|
||||
import sys, pdb
|
||||
from __future__ import absolute_import
|
||||
import traceback
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import py
|
||||
|
||||
|
||||
# for transfering markers
|
||||
from _pytest.python import transfer_markers
|
||||
from _pytest.skipping import MarkEvaluator
|
||||
|
||||
|
||||
def pytest_pycollect_makeitem(collector, name, obj):
|
||||
unittest = sys.modules.get('unittest')
|
||||
if unittest is None:
|
||||
return # nobody can have derived unittest.TestCase
|
||||
# has unittest been imported and is obj a subclass of its TestCase?
|
||||
try:
|
||||
isunit = issubclass(obj, unittest.TestCase)
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
if not issubclass(obj, sys.modules["unittest"].TestCase):
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
if isunit:
|
||||
return UnitTestCase(name, parent=collector)
|
||||
return
|
||||
# yes, so let's collect it
|
||||
return UnitTestCase(name, parent=collector)
|
||||
|
||||
|
||||
class UnitTestCase(pytest.Class):
|
||||
nofuncargs = True # marker for fixturemanger.getfixtureinfo()
|
||||
# to declare that our children do not support funcargs
|
||||
#
|
||||
def setup(self):
|
||||
cls = self.obj
|
||||
if getattr(cls, '__unittest_skip__', False):
|
||||
return # skipped
|
||||
setup = getattr(cls, 'setUpClass', None)
|
||||
if setup is not None:
|
||||
setup()
|
||||
teardown = getattr(cls, 'tearDownClass', None)
|
||||
if teardown is not None:
|
||||
self.addfinalizer(teardown)
|
||||
super(UnitTestCase, self).setup()
|
||||
|
||||
def collect(self):
|
||||
self.session._fixturemanager.parsefactories(self, unittest=True)
|
||||
loader = py.std.unittest.TestLoader()
|
||||
module = self.getparent(pytest.Module).obj
|
||||
from unittest import TestLoader
|
||||
cls = self.obj
|
||||
if not getattr(cls, "__test__", True):
|
||||
return
|
||||
self.session._fixturemanager.parsefactories(self, unittest=True)
|
||||
loader = TestLoader()
|
||||
module = self.getparent(pytest.Module).obj
|
||||
foundsomething = False
|
||||
for name in loader.getTestCaseNames(self.obj):
|
||||
x = getattr(self.obj, name)
|
||||
funcobj = getattr(x, 'im_func', x)
|
||||
transfer_markers(funcobj, cls, module)
|
||||
if hasattr(funcobj, 'todo'):
|
||||
pytest.mark.xfail(reason=str(funcobj.todo))(funcobj)
|
||||
yield TestCaseFunction(name, parent=self)
|
||||
foundsomething = True
|
||||
|
||||
@@ -45,21 +62,7 @@ class UnitTestCase(pytest.Class):
|
||||
if ut is None or runtest != ut.TestCase.runTest:
|
||||
yield TestCaseFunction('runTest', parent=self)
|
||||
|
||||
def setup(self):
|
||||
if getattr(self.obj, '__unittest_skip__', False):
|
||||
return
|
||||
meth = getattr(self.obj, 'setUpClass', None)
|
||||
if meth is not None:
|
||||
meth()
|
||||
super(UnitTestCase, self).setup()
|
||||
|
||||
def teardown(self):
|
||||
if getattr(self.obj, '__unittest_skip__', False):
|
||||
return
|
||||
meth = getattr(self.obj, 'tearDownClass', None)
|
||||
if meth is not None:
|
||||
meth()
|
||||
super(UnitTestCase, self).teardown()
|
||||
|
||||
class TestCaseFunction(pytest.Function):
|
||||
_excinfo = None
|
||||
@@ -67,10 +70,6 @@ class TestCaseFunction(pytest.Function):
|
||||
def setup(self):
|
||||
self._testcase = self.parent.obj(self.name)
|
||||
self._obj = getattr(self._testcase, self.name)
|
||||
if hasattr(self._testcase, 'skip'):
|
||||
pytest.skip(self._testcase.skip)
|
||||
if hasattr(self._obj, 'skip'):
|
||||
pytest.skip(self._obj.skip)
|
||||
if hasattr(self._testcase, 'setup_method'):
|
||||
self._testcase.setup_method(self._obj)
|
||||
if hasattr(self, "_request"):
|
||||
@@ -91,7 +90,7 @@ class TestCaseFunction(pytest.Function):
|
||||
except TypeError:
|
||||
try:
|
||||
try:
|
||||
l = py.std.traceback.format_exception(*rawexcinfo)
|
||||
l = traceback.format_exception(*rawexcinfo)
|
||||
l.insert(0, "NOTE: Incompatible Exception Representation, "
|
||||
"displaying natively:\n\n")
|
||||
pytest.fail("".join(l), pytrace=False)
|
||||
@@ -115,6 +114,8 @@ class TestCaseFunction(pytest.Function):
|
||||
try:
|
||||
pytest.skip(reason)
|
||||
except pytest.skip.Exception:
|
||||
self._evalskip = MarkEvaluator(self, 'SkipTest')
|
||||
self._evalskip.result = True
|
||||
self._addexcinfo(sys.exc_info())
|
||||
|
||||
def addExpectedFailure(self, testcase, rawexcinfo, reason=""):
|
||||
@@ -142,38 +143,44 @@ class TestCaseFunction(pytest.Function):
|
||||
if traceback:
|
||||
excinfo.traceback = traceback
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
if isinstance(item, TestCaseFunction):
|
||||
if item._excinfo:
|
||||
call.excinfo = item._excinfo.pop(0)
|
||||
del call.result
|
||||
try:
|
||||
del call.result
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# twisted trial support
|
||||
def pytest_runtest_protocol(item, __multicall__):
|
||||
if isinstance(item, TestCaseFunction):
|
||||
if 'twisted.trial.unittest' in sys.modules:
|
||||
ut = sys.modules['twisted.python.failure']
|
||||
Failure__init__ = ut.Failure.__init__.im_func
|
||||
check_testcase_implements_trial_reporter()
|
||||
def excstore(self, exc_value=None, exc_type=None, exc_tb=None,
|
||||
captureVars=None):
|
||||
if exc_value is None:
|
||||
self._rawexcinfo = sys.exc_info()
|
||||
else:
|
||||
if exc_type is None:
|
||||
exc_type = type(exc_value)
|
||||
self._rawexcinfo = (exc_type, exc_value, exc_tb)
|
||||
try:
|
||||
Failure__init__(self, exc_value, exc_type, exc_tb,
|
||||
captureVars=captureVars)
|
||||
except TypeError:
|
||||
Failure__init__(self, exc_value, exc_type, exc_tb)
|
||||
ut.Failure.__init__ = excstore
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_protocol(item):
|
||||
if isinstance(item, TestCaseFunction) and \
|
||||
'twisted.trial.unittest' in sys.modules:
|
||||
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:
|
||||
self._rawexcinfo = sys.exc_info()
|
||||
else:
|
||||
if exc_type is None:
|
||||
exc_type = type(exc_value)
|
||||
self._rawexcinfo = (exc_type, exc_value, exc_tb)
|
||||
try:
|
||||
return __multicall__.execute()
|
||||
finally:
|
||||
ut.Failure.__init__ = Failure__init__
|
||||
Failure__init__(self, exc_value, exc_type, exc_tb,
|
||||
captureVars=captureVars)
|
||||
except TypeError:
|
||||
Failure__init__(self, exc_value, exc_type, exc_tb)
|
||||
ut.Failure.__init__ = excstore
|
||||
yield
|
||||
ut.Failure.__init__ = Failure__init__
|
||||
else:
|
||||
yield
|
||||
|
||||
|
||||
def check_testcase_implements_trial_reporter(done=[]):
|
||||
if done:
|
||||
|
||||
13
_pytest/vendored_packages/README.md
Normal file
13
_pytest/vendored_packages/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
This directory vendors the `pluggy` module.
|
||||
|
||||
For a more detailed discussion for the reasons to vendoring this
|
||||
package, please see [this issue](https://github.com/pytest-dev/pytest/issues/944).
|
||||
|
||||
To update the current version, execute:
|
||||
|
||||
```
|
||||
$ pip install -U pluggy==<version> --no-compile --target=_pytest/vendored_packages
|
||||
```
|
||||
|
||||
And commit the modified files. The `pluggy-<version>.dist-info` directory
|
||||
created by `pip` should be ignored.
|
||||
@@ -0,0 +1,10 @@
|
||||
Plugin registration and hook calling for Python
|
||||
===============================================
|
||||
|
||||
This is the plugin manager as used by pytest but stripped
|
||||
of pytest specific details.
|
||||
|
||||
During the 0.x series this plugin does not have much documentation
|
||||
except extensive docstrings in the pluggy.py module.
|
||||
|
||||
|
||||
39
_pytest/vendored_packages/pluggy-0.3.1.dist-info/METADATA
Normal file
39
_pytest/vendored_packages/pluggy-0.3.1.dist-info/METADATA
Normal file
@@ -0,0 +1,39 @@
|
||||
Metadata-Version: 2.0
|
||||
Name: pluggy
|
||||
Version: 0.3.1
|
||||
Summary: plugin and hook calling mechanisms for python
|
||||
Home-page: UNKNOWN
|
||||
Author: Holger Krekel
|
||||
Author-email: holger at merlinux.eu
|
||||
License: MIT license
|
||||
Platform: unix
|
||||
Platform: linux
|
||||
Platform: osx
|
||||
Platform: win32
|
||||
Classifier: Development Status :: 4 - Beta
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Operating System :: POSIX
|
||||
Classifier: Operating System :: Microsoft :: Windows
|
||||
Classifier: Operating System :: MacOS :: MacOS X
|
||||
Classifier: Topic :: Software Development :: Testing
|
||||
Classifier: Topic :: Software Development :: Libraries
|
||||
Classifier: Topic :: Utilities
|
||||
Classifier: Programming Language :: Python :: 2
|
||||
Classifier: Programming Language :: Python :: 2.6
|
||||
Classifier: Programming Language :: Python :: 2.7
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.3
|
||||
Classifier: Programming Language :: Python :: 3.4
|
||||
Classifier: Programming Language :: Python :: 3.5
|
||||
|
||||
Plugin registration and hook calling for Python
|
||||
===============================================
|
||||
|
||||
This is the plugin manager as used by pytest but stripped
|
||||
of pytest specific details.
|
||||
|
||||
During the 0.x series this plugin does not have much documentation
|
||||
except extensive docstrings in the pluggy.py module.
|
||||
|
||||
|
||||
8
_pytest/vendored_packages/pluggy-0.3.1.dist-info/RECORD
Normal file
8
_pytest/vendored_packages/pluggy-0.3.1.dist-info/RECORD
Normal file
@@ -0,0 +1,8 @@
|
||||
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
|
||||
6
_pytest/vendored_packages/pluggy-0.3.1.dist-info/WHEEL
Normal file
6
_pytest/vendored_packages/pluggy-0.3.1.dist-info/WHEEL
Normal file
@@ -0,0 +1,6 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.24.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py2-none-any
|
||||
Tag: py3-none-any
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"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"]}
|
||||
@@ -0,0 +1 @@
|
||||
{"is_release": false, "git_version": "7d4c9cd"}
|
||||
@@ -0,0 +1 @@
|
||||
pluggy
|
||||
777
_pytest/vendored_packages/pluggy.py
Normal file
777
_pytest/vendored_packages/pluggy.py
Normal file
@@ -0,0 +1,777 @@
|
||||
"""
|
||||
PluginManager, basic initialization and tracing.
|
||||
|
||||
pluggy is the cristallized core of plugin management as used
|
||||
by some 150 plugins for pytest.
|
||||
|
||||
Pluggy uses semantic versioning. Breaking changes are only foreseen for
|
||||
Major releases (incremented X in "X.Y.Z"). If you want to use pluggy in
|
||||
your project you should thus use a dependency restriction like
|
||||
"pluggy>=0.1.0,<1.0" to avoid surprises.
|
||||
|
||||
pluggy is concerned with hook specification, hook implementations and hook
|
||||
calling. For any given hook specification a hook call invokes up to N implementations.
|
||||
A hook implementation can influence its position and type of execution:
|
||||
if attributed "tryfirst" or "trylast" it will be tried to execute
|
||||
first or last. However, if attributed "hookwrapper" an implementation
|
||||
can wrap all calls to non-hookwrapper implementations. A hookwrapper
|
||||
can thus execute some code ahead and after the execution of other hooks.
|
||||
|
||||
Hook specification is done by way of a regular python function where
|
||||
both the function name and the names of all its arguments are significant.
|
||||
Each hook implementation function is verified against the original specification
|
||||
function, including the names of all its arguments. To allow for hook specifications
|
||||
to evolve over the livetime of a project, hook implementations can
|
||||
accept less arguments. One can thus add new arguments and semantics to
|
||||
a hook specification by adding another argument typically without breaking
|
||||
existing hook implementations.
|
||||
|
||||
The chosen approach is meant to let a hook designer think carefuly about
|
||||
which objects are needed by an extension writer. By contrast, subclass-based
|
||||
extension mechanisms often expose a lot more state and behaviour than needed,
|
||||
thus restricting future developments.
|
||||
|
||||
Pluggy currently consists of functionality for:
|
||||
|
||||
- a way to register new hook specifications. Without a hook
|
||||
specification no hook calling can be performed.
|
||||
|
||||
- a registry of plugins which contain hook implementation functions. It
|
||||
is possible to register plugins for which a hook specification is not yet
|
||||
known and validate all hooks when the system is in a more referentially
|
||||
consistent state. Setting an "optionalhook" attribution to a hook
|
||||
implementation will avoid PluginValidationError's if a specification
|
||||
is missing. This allows to have optional integration between plugins.
|
||||
|
||||
- a "hook" relay object from which you can launch 1:N calls to
|
||||
registered hook implementation functions
|
||||
|
||||
- a mechanism for ordering hook implementation functions
|
||||
|
||||
- mechanisms for two different type of 1:N calls: "firstresult" for when
|
||||
the call should stop when the first implementation returns a non-None result.
|
||||
And the other (default) way of guaranteeing that all hook implementations
|
||||
will be called and their non-None result collected.
|
||||
|
||||
- mechanisms for "historic" extension points such that all newly
|
||||
registered functions will receive all hook calls that happened
|
||||
before their registration.
|
||||
|
||||
- a mechanism for discovering plugin objects which are based on
|
||||
setuptools based entry points.
|
||||
|
||||
- a simple tracing mechanism, including tracing of plugin calls and
|
||||
their arguments.
|
||||
|
||||
"""
|
||||
import sys
|
||||
import inspect
|
||||
|
||||
__version__ = '0.3.1'
|
||||
__all__ = ["PluginManager", "PluginValidationError",
|
||||
"HookspecMarker", "HookimplMarker"]
|
||||
|
||||
_py3 = sys.version_info > (3, 0)
|
||||
|
||||
|
||||
class HookspecMarker:
|
||||
""" Decorator helper class for marking functions as hook specifications.
|
||||
|
||||
You can instantiate it with a project_name to get a decorator.
|
||||
Calling PluginManager.add_hookspecs later will discover all marked functions
|
||||
if the PluginManager uses the same project_name.
|
||||
"""
|
||||
|
||||
def __init__(self, project_name):
|
||||
self.project_name = project_name
|
||||
|
||||
def __call__(self, function=None, firstresult=False, historic=False):
|
||||
""" if passed a function, directly sets attributes on the function
|
||||
which will make it discoverable to add_hookspecs(). If passed no
|
||||
function, returns a decorator which can be applied to a function
|
||||
later using the attributes supplied.
|
||||
|
||||
If firstresult is True the 1:N hook call (N being the number of registered
|
||||
hook implementation functions) will stop at I<=N when the I'th function
|
||||
returns a non-None result.
|
||||
|
||||
If historic is True calls to a hook will be memorized and replayed
|
||||
on later registered plugins.
|
||||
|
||||
"""
|
||||
def setattr_hookspec_opts(func):
|
||||
if historic and firstresult:
|
||||
raise ValueError("cannot have a historic firstresult hook")
|
||||
setattr(func, self.project_name + "_spec",
|
||||
dict(firstresult=firstresult, historic=historic))
|
||||
return func
|
||||
|
||||
if function is not None:
|
||||
return setattr_hookspec_opts(function)
|
||||
else:
|
||||
return setattr_hookspec_opts
|
||||
|
||||
|
||||
class HookimplMarker:
|
||||
""" Decorator helper class for marking functions as hook implementations.
|
||||
|
||||
You can instantiate with a project_name to get a decorator.
|
||||
Calling PluginManager.register later will discover all marked functions
|
||||
if the PluginManager uses the same project_name.
|
||||
"""
|
||||
def __init__(self, project_name):
|
||||
self.project_name = project_name
|
||||
|
||||
def __call__(self, function=None, hookwrapper=False, optionalhook=False,
|
||||
tryfirst=False, trylast=False):
|
||||
|
||||
""" if passed a function, directly sets attributes on the function
|
||||
which will make it discoverable to register(). If passed no function,
|
||||
returns a decorator which can be applied to a function later using
|
||||
the attributes supplied.
|
||||
|
||||
If optionalhook is True a missing matching hook specification will not result
|
||||
in an error (by default it is an error if no matching spec is found).
|
||||
|
||||
If tryfirst is True this hook implementation will run as early as possible
|
||||
in the chain of N hook implementations for a specfication.
|
||||
|
||||
If trylast is True this hook implementation will run as late as possible
|
||||
in the chain of N hook implementations.
|
||||
|
||||
If hookwrapper is True the hook implementations needs to execute exactly
|
||||
one "yield". The code before the yield is run early before any non-hookwrapper
|
||||
function is run. The code after the yield is run after all non-hookwrapper
|
||||
function have run. The yield receives an ``_CallOutcome`` object representing
|
||||
the exception or result outcome of the inner calls (including other hookwrapper
|
||||
calls).
|
||||
|
||||
"""
|
||||
def setattr_hookimpl_opts(func):
|
||||
setattr(func, self.project_name + "_impl",
|
||||
dict(hookwrapper=hookwrapper, optionalhook=optionalhook,
|
||||
tryfirst=tryfirst, trylast=trylast))
|
||||
return func
|
||||
|
||||
if function is None:
|
||||
return setattr_hookimpl_opts
|
||||
else:
|
||||
return setattr_hookimpl_opts(function)
|
||||
|
||||
|
||||
def normalize_hookimpl_opts(opts):
|
||||
opts.setdefault("tryfirst", False)
|
||||
opts.setdefault("trylast", False)
|
||||
opts.setdefault("hookwrapper", False)
|
||||
opts.setdefault("optionalhook", False)
|
||||
|
||||
|
||||
class _TagTracer:
|
||||
def __init__(self):
|
||||
self._tag2proc = {}
|
||||
self.writer = None
|
||||
self.indent = 0
|
||||
|
||||
def get(self, name):
|
||||
return _TagTracerSub(self, (name,))
|
||||
|
||||
def format_message(self, tags, args):
|
||||
if isinstance(args[-1], dict):
|
||||
extra = args[-1]
|
||||
args = args[:-1]
|
||||
else:
|
||||
extra = {}
|
||||
|
||||
content = " ".join(map(str, args))
|
||||
indent = " " * self.indent
|
||||
|
||||
lines = [
|
||||
"%s%s [%s]\n" % (indent, content, ":".join(tags))
|
||||
]
|
||||
|
||||
for name, value in extra.items():
|
||||
lines.append("%s %s: %s\n" % (indent, name, value))
|
||||
return lines
|
||||
|
||||
def processmessage(self, tags, args):
|
||||
if self.writer is not None and args:
|
||||
lines = self.format_message(tags, args)
|
||||
self.writer(''.join(lines))
|
||||
try:
|
||||
self._tag2proc[tags](tags, args)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def setwriter(self, writer):
|
||||
self.writer = writer
|
||||
|
||||
def setprocessor(self, tags, processor):
|
||||
if isinstance(tags, str):
|
||||
tags = tuple(tags.split(":"))
|
||||
else:
|
||||
assert isinstance(tags, tuple)
|
||||
self._tag2proc[tags] = processor
|
||||
|
||||
|
||||
class _TagTracerSub:
|
||||
def __init__(self, root, tags):
|
||||
self.root = root
|
||||
self.tags = tags
|
||||
|
||||
def __call__(self, *args):
|
||||
self.root.processmessage(self.tags, args)
|
||||
|
||||
def setmyprocessor(self, processor):
|
||||
self.root.setprocessor(self.tags, processor)
|
||||
|
||||
def get(self, name):
|
||||
return self.__class__(self.root, self.tags + (name,))
|
||||
|
||||
|
||||
def _raise_wrapfail(wrap_controller, msg):
|
||||
co = wrap_controller.gi_code
|
||||
raise RuntimeError("wrap_controller at %r %s:%d %s" %
|
||||
(co.co_name, co.co_filename, co.co_firstlineno, msg))
|
||||
|
||||
|
||||
def _wrapped_call(wrap_controller, func):
|
||||
""" Wrap calling to a function with a generator which needs to yield
|
||||
exactly once. The yield point will trigger calling the wrapped function
|
||||
and return its _CallOutcome to the yield point. The generator then needs
|
||||
to finish (raise StopIteration) in order for the wrapped call to complete.
|
||||
"""
|
||||
try:
|
||||
next(wrap_controller) # first yield
|
||||
except StopIteration:
|
||||
_raise_wrapfail(wrap_controller, "did not yield")
|
||||
call_outcome = _CallOutcome(func)
|
||||
try:
|
||||
wrap_controller.send(call_outcome)
|
||||
_raise_wrapfail(wrap_controller, "has second yield")
|
||||
except StopIteration:
|
||||
pass
|
||||
return call_outcome.get_result()
|
||||
|
||||
|
||||
class _CallOutcome:
|
||||
""" Outcome of a function call, either an exception or a proper result.
|
||||
Calling the ``get_result`` method will return the result or reraise
|
||||
the exception raised when the function was called. """
|
||||
excinfo = None
|
||||
|
||||
def __init__(self, func):
|
||||
try:
|
||||
self.result = func()
|
||||
except BaseException:
|
||||
self.excinfo = sys.exc_info()
|
||||
|
||||
def force_result(self, result):
|
||||
self.result = result
|
||||
self.excinfo = None
|
||||
|
||||
def get_result(self):
|
||||
if self.excinfo is None:
|
||||
return self.result
|
||||
else:
|
||||
ex = self.excinfo
|
||||
if _py3:
|
||||
raise ex[1].with_traceback(ex[2])
|
||||
_reraise(*ex) # noqa
|
||||
|
||||
if not _py3:
|
||||
exec("""
|
||||
def _reraise(cls, val, tb):
|
||||
raise cls, val, tb
|
||||
""")
|
||||
|
||||
|
||||
class _TracedHookExecution:
|
||||
def __init__(self, pluginmanager, before, after):
|
||||
self.pluginmanager = pluginmanager
|
||||
self.before = before
|
||||
self.after = after
|
||||
self.oldcall = pluginmanager._inner_hookexec
|
||||
assert not isinstance(self.oldcall, _TracedHookExecution)
|
||||
self.pluginmanager._inner_hookexec = self
|
||||
|
||||
def __call__(self, hook, hook_impls, kwargs):
|
||||
self.before(hook.name, hook_impls, kwargs)
|
||||
outcome = _CallOutcome(lambda: self.oldcall(hook, hook_impls, kwargs))
|
||||
self.after(outcome, hook.name, hook_impls, kwargs)
|
||||
return outcome.get_result()
|
||||
|
||||
def undo(self):
|
||||
self.pluginmanager._inner_hookexec = self.oldcall
|
||||
|
||||
|
||||
class PluginManager(object):
|
||||
""" Core Pluginmanager class which manages registration
|
||||
of plugin objects and 1:N hook calling.
|
||||
|
||||
You can register new hooks by calling ``addhooks(module_or_class)``.
|
||||
You can register plugin objects (which contain hooks) by calling
|
||||
``register(plugin)``. The Pluginmanager is initialized with a
|
||||
prefix that is searched for in the names of the dict of registered
|
||||
plugin objects. An optional excludefunc allows to blacklist names which
|
||||
are not considered as hooks despite a matching prefix.
|
||||
|
||||
For debugging purposes you can call ``enable_tracing()``
|
||||
which will subsequently send debug information to the trace helper.
|
||||
"""
|
||||
|
||||
def __init__(self, project_name, implprefix=None):
|
||||
""" if implprefix is given implementation functions
|
||||
will be recognized if their name matches the implprefix. """
|
||||
self.project_name = project_name
|
||||
self._name2plugin = {}
|
||||
self._plugin2hookcallers = {}
|
||||
self._plugin_distinfo = []
|
||||
self.trace = _TagTracer().get("pluginmanage")
|
||||
self.hook = _HookRelay(self.trace.root.get("hook"))
|
||||
self._implprefix = implprefix
|
||||
self._inner_hookexec = lambda hook, methods, kwargs: \
|
||||
_MultiCall(methods, kwargs, hook.spec_opts).execute()
|
||||
|
||||
def _hookexec(self, hook, methods, kwargs):
|
||||
# called from all hookcaller instances.
|
||||
# enable_tracing will set its own wrapping function at self._inner_hookexec
|
||||
return self._inner_hookexec(hook, methods, kwargs)
|
||||
|
||||
def register(self, plugin, name=None):
|
||||
""" Register a plugin and return its canonical name or None if the name
|
||||
is blocked from registering. Raise a ValueError if the plugin is already
|
||||
registered. """
|
||||
plugin_name = name or self.get_canonical_name(plugin)
|
||||
|
||||
if plugin_name in self._name2plugin or plugin in self._plugin2hookcallers:
|
||||
if self._name2plugin.get(plugin_name, -1) is None:
|
||||
return # blocked plugin, return None to indicate no registration
|
||||
raise ValueError("Plugin already registered: %s=%s\n%s" %
|
||||
(plugin_name, plugin, self._name2plugin))
|
||||
|
||||
# XXX if an error happens we should make sure no state has been
|
||||
# changed at point of return
|
||||
self._name2plugin[plugin_name] = plugin
|
||||
|
||||
# register matching hook implementations of the plugin
|
||||
self._plugin2hookcallers[plugin] = hookcallers = []
|
||||
for name in dir(plugin):
|
||||
hookimpl_opts = self.parse_hookimpl_opts(plugin, name)
|
||||
if hookimpl_opts is not None:
|
||||
normalize_hookimpl_opts(hookimpl_opts)
|
||||
method = getattr(plugin, name)
|
||||
hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_opts)
|
||||
hook = getattr(self.hook, name, None)
|
||||
if hook is None:
|
||||
hook = _HookCaller(name, self._hookexec)
|
||||
setattr(self.hook, name, hook)
|
||||
elif hook.has_spec():
|
||||
self._verify_hook(hook, hookimpl)
|
||||
hook._maybe_apply_history(hookimpl)
|
||||
hook._add_hookimpl(hookimpl)
|
||||
hookcallers.append(hook)
|
||||
return plugin_name
|
||||
|
||||
def parse_hookimpl_opts(self, plugin, name):
|
||||
method = getattr(plugin, name)
|
||||
res = getattr(method, self.project_name + "_impl", None)
|
||||
if res is not None and not isinstance(res, dict):
|
||||
# false positive
|
||||
res = None
|
||||
elif res is None and self._implprefix and name.startswith(self._implprefix):
|
||||
res = {}
|
||||
return res
|
||||
|
||||
def unregister(self, plugin=None, name=None):
|
||||
""" unregister a plugin object and all its contained hook implementations
|
||||
from internal data structures. """
|
||||
if name is None:
|
||||
assert plugin is not None, "one of name or plugin needs to be specified"
|
||||
name = self.get_name(plugin)
|
||||
|
||||
if plugin is None:
|
||||
plugin = self.get_plugin(name)
|
||||
|
||||
# if self._name2plugin[name] == None registration was blocked: ignore
|
||||
if self._name2plugin.get(name):
|
||||
del self._name2plugin[name]
|
||||
|
||||
for hookcaller in self._plugin2hookcallers.pop(plugin, []):
|
||||
hookcaller._remove_plugin(plugin)
|
||||
|
||||
return plugin
|
||||
|
||||
def set_blocked(self, name):
|
||||
""" block registrations of the given name, unregister if already registered. """
|
||||
self.unregister(name=name)
|
||||
self._name2plugin[name] = None
|
||||
|
||||
def is_blocked(self, name):
|
||||
""" return True if the name blogs registering plugins of that name. """
|
||||
return name in self._name2plugin and self._name2plugin[name] is None
|
||||
|
||||
def add_hookspecs(self, module_or_class):
|
||||
""" add new hook specifications defined in the given module_or_class.
|
||||
Functions are recognized if they have been decorated accordingly. """
|
||||
names = []
|
||||
for name in dir(module_or_class):
|
||||
spec_opts = self.parse_hookspec_opts(module_or_class, name)
|
||||
if spec_opts is not None:
|
||||
hc = getattr(self.hook, name, None)
|
||||
if hc is None:
|
||||
hc = _HookCaller(name, self._hookexec, module_or_class, spec_opts)
|
||||
setattr(self.hook, name, hc)
|
||||
else:
|
||||
# plugins registered this hook without knowing the spec
|
||||
hc.set_specification(module_or_class, spec_opts)
|
||||
for hookfunction in (hc._wrappers + hc._nonwrappers):
|
||||
self._verify_hook(hc, hookfunction)
|
||||
names.append(name)
|
||||
|
||||
if not names:
|
||||
raise ValueError("did not find any %r hooks in %r" %
|
||||
(self.project_name, module_or_class))
|
||||
|
||||
def parse_hookspec_opts(self, module_or_class, name):
|
||||
method = getattr(module_or_class, name)
|
||||
return getattr(method, self.project_name + "_spec", None)
|
||||
|
||||
def get_plugins(self):
|
||||
""" return the set of registered plugins. """
|
||||
return set(self._plugin2hookcallers)
|
||||
|
||||
def is_registered(self, plugin):
|
||||
""" Return True if the plugin is already registered. """
|
||||
return plugin in self._plugin2hookcallers
|
||||
|
||||
def get_canonical_name(self, plugin):
|
||||
""" Return canonical name for a plugin object. Note that a plugin
|
||||
may be registered under a different name which was specified
|
||||
by the caller of register(plugin, name). To obtain the name
|
||||
of an registered plugin use ``get_name(plugin)`` instead."""
|
||||
return getattr(plugin, "__name__", None) or str(id(plugin))
|
||||
|
||||
def get_plugin(self, name):
|
||||
""" Return a plugin or None for the given name. """
|
||||
return self._name2plugin.get(name)
|
||||
|
||||
def get_name(self, plugin):
|
||||
""" Return name for registered plugin or None if not registered. """
|
||||
for name, val in self._name2plugin.items():
|
||||
if plugin == val:
|
||||
return name
|
||||
|
||||
def _verify_hook(self, hook, hookimpl):
|
||||
if hook.is_historic() and hookimpl.hookwrapper:
|
||||
raise PluginValidationError(
|
||||
"Plugin %r\nhook %r\nhistoric incompatible to hookwrapper" %
|
||||
(hookimpl.plugin_name, hook.name))
|
||||
|
||||
for arg in hookimpl.argnames:
|
||||
if arg not in hook.argnames:
|
||||
raise PluginValidationError(
|
||||
"Plugin %r\nhook %r\nargument %r not available\n"
|
||||
"plugin definition: %s\n"
|
||||
"available hookargs: %s" %
|
||||
(hookimpl.plugin_name, hook.name, arg,
|
||||
_formatdef(hookimpl.function), ", ".join(hook.argnames)))
|
||||
|
||||
def check_pending(self):
|
||||
""" Verify that all hooks which have not been verified against
|
||||
a hook specification are optional, otherwise raise PluginValidationError"""
|
||||
for name in self.hook.__dict__:
|
||||
if name[0] != "_":
|
||||
hook = getattr(self.hook, name)
|
||||
if not hook.has_spec():
|
||||
for hookimpl in (hook._wrappers + hook._nonwrappers):
|
||||
if not hookimpl.optionalhook:
|
||||
raise PluginValidationError(
|
||||
"unknown hook %r in plugin %r" %
|
||||
(name, hookimpl.plugin))
|
||||
|
||||
def load_setuptools_entrypoints(self, entrypoint_name):
|
||||
""" Load modules from querying the specified setuptools entrypoint name.
|
||||
Return the number of loaded plugins. """
|
||||
from pkg_resources import iter_entry_points, DistributionNotFound
|
||||
for ep in iter_entry_points(entrypoint_name):
|
||||
# is the plugin registered or blocked?
|
||||
if self.get_plugin(ep.name) or self.is_blocked(ep.name):
|
||||
continue
|
||||
try:
|
||||
plugin = ep.load()
|
||||
except DistributionNotFound:
|
||||
continue
|
||||
self.register(plugin, name=ep.name)
|
||||
self._plugin_distinfo.append((plugin, ep.dist))
|
||||
return len(self._plugin_distinfo)
|
||||
|
||||
def list_plugin_distinfo(self):
|
||||
""" return list of distinfo/plugin tuples for all setuptools registered
|
||||
plugins. """
|
||||
return list(self._plugin_distinfo)
|
||||
|
||||
def list_name_plugin(self):
|
||||
""" return list of name/plugin pairs. """
|
||||
return list(self._name2plugin.items())
|
||||
|
||||
def get_hookcallers(self, plugin):
|
||||
""" get all hook callers for the specified plugin. """
|
||||
return self._plugin2hookcallers.get(plugin)
|
||||
|
||||
def add_hookcall_monitoring(self, before, after):
|
||||
""" add before/after tracing functions for all hooks
|
||||
and return an undo function which, when called,
|
||||
will remove the added tracers.
|
||||
|
||||
``before(hook_name, hook_impls, kwargs)`` will be called ahead
|
||||
of all hook calls and receive a hookcaller instance, a list
|
||||
of HookImpl instances and the keyword arguments for the hook call.
|
||||
|
||||
``after(outcome, hook_name, hook_impls, kwargs)`` receives the
|
||||
same arguments as ``before`` but also a :py:class:`_CallOutcome`` object
|
||||
which represents the result of the overall hook call.
|
||||
"""
|
||||
return _TracedHookExecution(self, before, after).undo
|
||||
|
||||
def enable_tracing(self):
|
||||
""" enable tracing of hook calls and return an undo function. """
|
||||
hooktrace = self.hook._trace
|
||||
|
||||
def before(hook_name, methods, kwargs):
|
||||
hooktrace.root.indent += 1
|
||||
hooktrace(hook_name, kwargs)
|
||||
|
||||
def after(outcome, hook_name, methods, kwargs):
|
||||
if outcome.excinfo is None:
|
||||
hooktrace("finish", hook_name, "-->", outcome.result)
|
||||
hooktrace.root.indent -= 1
|
||||
|
||||
return self.add_hookcall_monitoring(before, after)
|
||||
|
||||
def subset_hook_caller(self, name, remove_plugins):
|
||||
""" Return a new _HookCaller instance for the named method
|
||||
which manages calls to all registered plugins except the
|
||||
ones from remove_plugins. """
|
||||
orig = getattr(self.hook, name)
|
||||
plugins_to_remove = [plug for plug in remove_plugins if hasattr(plug, name)]
|
||||
if plugins_to_remove:
|
||||
hc = _HookCaller(orig.name, orig._hookexec, orig._specmodule_or_class,
|
||||
orig.spec_opts)
|
||||
for hookimpl in (orig._wrappers + orig._nonwrappers):
|
||||
plugin = hookimpl.plugin
|
||||
if plugin not in plugins_to_remove:
|
||||
hc._add_hookimpl(hookimpl)
|
||||
# we also keep track of this hook caller so it
|
||||
# gets properly removed on plugin unregistration
|
||||
self._plugin2hookcallers.setdefault(plugin, []).append(hc)
|
||||
return hc
|
||||
return orig
|
||||
|
||||
|
||||
class _MultiCall:
|
||||
""" execute a call into multiple python functions/methods. """
|
||||
|
||||
# XXX note that the __multicall__ argument is supported only
|
||||
# for pytest compatibility reasons. It was never officially
|
||||
# supported there and is explicitely deprecated since 2.8
|
||||
# so we can remove it soon, allowing to avoid the below recursion
|
||||
# in execute() and simplify/speed up the execute loop.
|
||||
|
||||
def __init__(self, hook_impls, kwargs, specopts={}):
|
||||
self.hook_impls = hook_impls
|
||||
self.kwargs = kwargs
|
||||
self.kwargs["__multicall__"] = self
|
||||
self.specopts = specopts
|
||||
|
||||
def execute(self):
|
||||
all_kwargs = self.kwargs
|
||||
self.results = results = []
|
||||
firstresult = self.specopts.get("firstresult")
|
||||
|
||||
while self.hook_impls:
|
||||
hook_impl = self.hook_impls.pop()
|
||||
args = [all_kwargs[argname] for argname in hook_impl.argnames]
|
||||
if hook_impl.hookwrapper:
|
||||
return _wrapped_call(hook_impl.function(*args), self.execute)
|
||||
res = hook_impl.function(*args)
|
||||
if res is not None:
|
||||
if firstresult:
|
||||
return res
|
||||
results.append(res)
|
||||
|
||||
if not firstresult:
|
||||
return results
|
||||
|
||||
def __repr__(self):
|
||||
status = "%d meths" % (len(self.hook_impls),)
|
||||
if hasattr(self, "results"):
|
||||
status = ("%d results, " % len(self.results)) + status
|
||||
return "<_MultiCall %s, kwargs=%r>" % (status, self.kwargs)
|
||||
|
||||
|
||||
def varnames(func, startindex=None):
|
||||
""" return argument name tuple for a function, method, class or callable.
|
||||
|
||||
In case of a class, its "__init__" method is considered.
|
||||
For methods the "self" parameter is not included unless you are passing
|
||||
an unbound method with Python3 (which has no supports for unbound methods)
|
||||
"""
|
||||
cache = getattr(func, "__dict__", {})
|
||||
try:
|
||||
return cache["_varnames"]
|
||||
except KeyError:
|
||||
pass
|
||||
if inspect.isclass(func):
|
||||
try:
|
||||
func = func.__init__
|
||||
except AttributeError:
|
||||
return ()
|
||||
startindex = 1
|
||||
else:
|
||||
if not inspect.isfunction(func) and not inspect.ismethod(func):
|
||||
func = getattr(func, '__call__', func)
|
||||
if startindex is None:
|
||||
startindex = int(inspect.ismethod(func))
|
||||
|
||||
try:
|
||||
rawcode = func.__code__
|
||||
except AttributeError:
|
||||
return ()
|
||||
try:
|
||||
x = rawcode.co_varnames[startindex:rawcode.co_argcount]
|
||||
except AttributeError:
|
||||
x = ()
|
||||
else:
|
||||
defaults = func.__defaults__
|
||||
if defaults:
|
||||
x = x[:-len(defaults)]
|
||||
try:
|
||||
cache["_varnames"] = x
|
||||
except TypeError:
|
||||
pass
|
||||
return x
|
||||
|
||||
|
||||
class _HookRelay:
|
||||
""" hook holder object for performing 1:N hook calls where N is the number
|
||||
of registered plugins.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, trace):
|
||||
self._trace = trace
|
||||
|
||||
|
||||
class _HookCaller(object):
|
||||
def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None):
|
||||
self.name = name
|
||||
self._wrappers = []
|
||||
self._nonwrappers = []
|
||||
self._hookexec = hook_execute
|
||||
if specmodule_or_class is not None:
|
||||
assert spec_opts is not None
|
||||
self.set_specification(specmodule_or_class, spec_opts)
|
||||
|
||||
def has_spec(self):
|
||||
return hasattr(self, "_specmodule_or_class")
|
||||
|
||||
def set_specification(self, specmodule_or_class, spec_opts):
|
||||
assert not self.has_spec()
|
||||
self._specmodule_or_class = specmodule_or_class
|
||||
specfunc = getattr(specmodule_or_class, self.name)
|
||||
argnames = varnames(specfunc, startindex=inspect.isclass(specmodule_or_class))
|
||||
assert "self" not in argnames # sanity check
|
||||
self.argnames = ["__multicall__"] + list(argnames)
|
||||
self.spec_opts = spec_opts
|
||||
if spec_opts.get("historic"):
|
||||
self._call_history = []
|
||||
|
||||
def is_historic(self):
|
||||
return hasattr(self, "_call_history")
|
||||
|
||||
def _remove_plugin(self, plugin):
|
||||
def remove(wrappers):
|
||||
for i, method in enumerate(wrappers):
|
||||
if method.plugin == plugin:
|
||||
del wrappers[i]
|
||||
return True
|
||||
if remove(self._wrappers) is None:
|
||||
if remove(self._nonwrappers) is None:
|
||||
raise ValueError("plugin %r not found" % (plugin,))
|
||||
|
||||
def _add_hookimpl(self, hookimpl):
|
||||
if hookimpl.hookwrapper:
|
||||
methods = self._wrappers
|
||||
else:
|
||||
methods = self._nonwrappers
|
||||
|
||||
if hookimpl.trylast:
|
||||
methods.insert(0, hookimpl)
|
||||
elif hookimpl.tryfirst:
|
||||
methods.append(hookimpl)
|
||||
else:
|
||||
# find last non-tryfirst method
|
||||
i = len(methods) - 1
|
||||
while i >= 0 and methods[i].tryfirst:
|
||||
i -= 1
|
||||
methods.insert(i + 1, hookimpl)
|
||||
|
||||
def __repr__(self):
|
||||
return "<_HookCaller %r>" % (self.name,)
|
||||
|
||||
def __call__(self, **kwargs):
|
||||
assert not self.is_historic()
|
||||
return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
|
||||
|
||||
def call_historic(self, proc=None, kwargs=None):
|
||||
self._call_history.append((kwargs or {}, proc))
|
||||
# historizing hooks don't return results
|
||||
self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
|
||||
|
||||
def call_extra(self, methods, kwargs):
|
||||
""" Call the hook with some additional temporarily participating
|
||||
methods using the specified kwargs as call parameters. """
|
||||
old = list(self._nonwrappers), list(self._wrappers)
|
||||
for method in methods:
|
||||
opts = dict(hookwrapper=False, trylast=False, tryfirst=False)
|
||||
hookimpl = HookImpl(None, "<temp>", method, opts)
|
||||
self._add_hookimpl(hookimpl)
|
||||
try:
|
||||
return self(**kwargs)
|
||||
finally:
|
||||
self._nonwrappers, self._wrappers = old
|
||||
|
||||
def _maybe_apply_history(self, method):
|
||||
if self.is_historic():
|
||||
for kwargs, proc in self._call_history:
|
||||
res = self._hookexec(self, [method], kwargs)
|
||||
if res and proc is not None:
|
||||
proc(res[0])
|
||||
|
||||
|
||||
class HookImpl:
|
||||
def __init__(self, plugin, plugin_name, function, hook_impl_opts):
|
||||
self.function = function
|
||||
self.argnames = varnames(self.function)
|
||||
self.plugin = plugin
|
||||
self.opts = hook_impl_opts
|
||||
self.plugin_name = plugin_name
|
||||
self.__dict__.update(hook_impl_opts)
|
||||
|
||||
|
||||
class PluginValidationError(Exception):
|
||||
""" plugin failed validation. """
|
||||
|
||||
|
||||
if hasattr(inspect, 'signature'):
|
||||
def _formatdef(func):
|
||||
return "%s%s" % (
|
||||
func.__name__,
|
||||
str(inspect.signature(func))
|
||||
)
|
||||
else:
|
||||
def _formatdef(func):
|
||||
return "%s%s" % (
|
||||
func.__name__,
|
||||
inspect.formatargspec(*inspect.getargspec(func))
|
||||
)
|
||||
98
appveyor.yml
Normal file
98
appveyor.yml
Normal file
@@ -0,0 +1,98 @@
|
||||
environment:
|
||||
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"
|
||||
|
||||
|
||||
install:
|
||||
- ECHO "Filesystem root:"
|
||||
- ps: "ls \"C:/\""
|
||||
|
||||
- ECHO "Installed SDKs:"
|
||||
- ps: "ls \"C:/Program Files/Microsoft SDKs/Windows\""
|
||||
|
||||
# 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
|
||||
|
||||
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%
|
||||
180
appveyor/install.ps1
Normal file
180
appveyor/install.ps1
Normal file
@@ -0,0 +1,180 @@
|
||||
# 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
|
||||
@@ -1,10 +1,12 @@
|
||||
import sys
|
||||
|
||||
if __name__ == '__main__':
|
||||
import cProfile
|
||||
import py
|
||||
import pytest
|
||||
import pstats
|
||||
stats = cProfile.run('py.test.cmdline.main(["empty.py", ])', 'prof')
|
||||
script = sys.argv[1:] if len(sys.argv) > 1 else "empty.py"
|
||||
stats = cProfile.run('pytest.cmdline.main(%r)' % script, 'prof')
|
||||
p = pstats.Stats("prof")
|
||||
p.strip_dirs()
|
||||
p.sort_stats('cumulative')
|
||||
print(p.print_stats(30))
|
||||
print(p.print_stats(500))
|
||||
|
||||
19
bench/bench_argcomplete.py
Normal file
19
bench/bench_argcomplete.py
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
|
||||
# 10000 iterations, just for relative comparison
|
||||
# 2.7.5 3.3.2
|
||||
# FilesCompleter 75.1109 69.2116
|
||||
# FastFilesCompleter 0.7383 1.0760
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
import timeit
|
||||
from argcomplete.completers import FilesCompleter
|
||||
from _pytest._argcomplete import FastFilesCompleter
|
||||
count = 1000 # only a few seconds
|
||||
setup = 'from __main__ import FastFilesCompleter\nfc = FastFilesCompleter()'
|
||||
run = 'fc("/d")'
|
||||
sys.stdout.write('%s\n' % (timeit.timeit(run,
|
||||
setup=setup.replace('Fast', ''), number=count)))
|
||||
sys.stdout.write('%s\n' % (timeit.timeit(run, setup=setup, number=count)))
|
||||
12
bench/manyparam.py
Normal file
12
bench/manyparam.py
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
import pytest
|
||||
|
||||
@pytest.fixture(scope='module', params=range(966))
|
||||
def foo(request):
|
||||
return request.param
|
||||
|
||||
def test_it(foo):
|
||||
pass
|
||||
def test_it2(foo):
|
||||
pass
|
||||
|
||||
10
bench/skip.py
Normal file
10
bench/skip.py
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
SKIP = True
|
||||
|
||||
@pytest.mark.parametrize("x", xrange(5000))
|
||||
def test_foo(x):
|
||||
if SKIP:
|
||||
pytest.skip("heh")
|
||||
@@ -1,497 +0,0 @@
|
||||
#!python
|
||||
"""Bootstrap distribute installation
|
||||
|
||||
If you want to use setuptools in your package's setup.py, just include this
|
||||
file in the same directory with it, and add this to the top of your setup.py::
|
||||
|
||||
from distribute_setup import use_setuptools
|
||||
use_setuptools()
|
||||
|
||||
If you want to require a specific version of setuptools, set a download
|
||||
mirror, or use an alternate download directory, you can do so by supplying
|
||||
the appropriate options to ``use_setuptools()``.
|
||||
|
||||
This file can also be run as a script to install or upgrade setuptools.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import fnmatch
|
||||
import tempfile
|
||||
import tarfile
|
||||
from distutils import log
|
||||
|
||||
try:
|
||||
from site import USER_SITE
|
||||
except ImportError:
|
||||
USER_SITE = None
|
||||
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
def _python_cmd(*args):
|
||||
args = (sys.executable,) + args
|
||||
return subprocess.call(args) == 0
|
||||
|
||||
except ImportError:
|
||||
# will be used for python 2.3
|
||||
def _python_cmd(*args):
|
||||
args = (sys.executable,) + args
|
||||
# quoting arguments if windows
|
||||
if sys.platform == 'win32':
|
||||
def quote(arg):
|
||||
if ' ' in arg:
|
||||
return '"%s"' % arg
|
||||
return arg
|
||||
args = [quote(arg) for arg in args]
|
||||
return os.spawnl(os.P_WAIT, sys.executable, *args) == 0
|
||||
|
||||
DEFAULT_VERSION = "0.6.27"
|
||||
DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/"
|
||||
SETUPTOOLS_FAKED_VERSION = "0.6c11"
|
||||
|
||||
SETUPTOOLS_PKG_INFO = """\
|
||||
Metadata-Version: 1.0
|
||||
Name: setuptools
|
||||
Version: %s
|
||||
Summary: xxxx
|
||||
Home-page: xxx
|
||||
Author: xxx
|
||||
Author-email: xxx
|
||||
License: xxx
|
||||
Description: xxx
|
||||
""" % SETUPTOOLS_FAKED_VERSION
|
||||
|
||||
|
||||
def _install(tarball, install_args=()):
|
||||
# extracting the tarball
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
log.warn('Extracting in %s', tmpdir)
|
||||
old_wd = os.getcwd()
|
||||
try:
|
||||
os.chdir(tmpdir)
|
||||
tar = tarfile.open(tarball)
|
||||
_extractall(tar)
|
||||
tar.close()
|
||||
|
||||
# going in the directory
|
||||
subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
|
||||
os.chdir(subdir)
|
||||
log.warn('Now working in %s', subdir)
|
||||
|
||||
# installing
|
||||
log.warn('Installing Distribute')
|
||||
if not _python_cmd('setup.py', 'install', *install_args):
|
||||
log.warn('Something went wrong during the installation.')
|
||||
log.warn('See the error message above.')
|
||||
finally:
|
||||
os.chdir(old_wd)
|
||||
|
||||
|
||||
def _build_egg(egg, tarball, to_dir):
|
||||
# extracting the tarball
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
log.warn('Extracting in %s', tmpdir)
|
||||
old_wd = os.getcwd()
|
||||
try:
|
||||
os.chdir(tmpdir)
|
||||
tar = tarfile.open(tarball)
|
||||
_extractall(tar)
|
||||
tar.close()
|
||||
|
||||
# going in the directory
|
||||
subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
|
||||
os.chdir(subdir)
|
||||
log.warn('Now working in %s', subdir)
|
||||
|
||||
# building an egg
|
||||
log.warn('Building a Distribute egg in %s', to_dir)
|
||||
_python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
|
||||
|
||||
finally:
|
||||
os.chdir(old_wd)
|
||||
# returning the result
|
||||
log.warn(egg)
|
||||
if not os.path.exists(egg):
|
||||
raise IOError('Could not build the egg.')
|
||||
|
||||
|
||||
def _do_download(version, download_base, to_dir, download_delay):
|
||||
egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg'
|
||||
% (version, sys.version_info[0], sys.version_info[1]))
|
||||
if not os.path.exists(egg):
|
||||
tarball = download_setuptools(version, download_base,
|
||||
to_dir, download_delay)
|
||||
_build_egg(egg, tarball, to_dir)
|
||||
sys.path.insert(0, egg)
|
||||
import setuptools
|
||||
setuptools.bootstrap_install_from = egg
|
||||
|
||||
|
||||
def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
|
||||
to_dir=os.curdir, download_delay=15, no_fake=True):
|
||||
# making sure we use the absolute path
|
||||
to_dir = os.path.abspath(to_dir)
|
||||
was_imported = 'pkg_resources' in sys.modules or \
|
||||
'setuptools' in sys.modules
|
||||
try:
|
||||
try:
|
||||
import pkg_resources
|
||||
if not hasattr(pkg_resources, '_distribute'):
|
||||
if not no_fake:
|
||||
_fake_setuptools()
|
||||
raise ImportError
|
||||
except ImportError:
|
||||
return _do_download(version, download_base, to_dir, download_delay)
|
||||
try:
|
||||
pkg_resources.require("distribute>="+version)
|
||||
return
|
||||
except pkg_resources.VersionConflict:
|
||||
e = sys.exc_info()[1]
|
||||
if was_imported:
|
||||
sys.stderr.write(
|
||||
"The required version of distribute (>=%s) is not available,\n"
|
||||
"and can't be installed while this script is running. Please\n"
|
||||
"install a more recent version first, using\n"
|
||||
"'easy_install -U distribute'."
|
||||
"\n\n(Currently using %r)\n" % (version, e.args[0]))
|
||||
sys.exit(2)
|
||||
else:
|
||||
del pkg_resources, sys.modules['pkg_resources'] # reload ok
|
||||
return _do_download(version, download_base, to_dir,
|
||||
download_delay)
|
||||
except pkg_resources.DistributionNotFound:
|
||||
return _do_download(version, download_base, to_dir,
|
||||
download_delay)
|
||||
finally:
|
||||
if not no_fake:
|
||||
_create_fake_setuptools_pkg_info(to_dir)
|
||||
|
||||
def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
|
||||
to_dir=os.curdir, delay=15):
|
||||
"""Download distribute from a specified location and return its filename
|
||||
|
||||
`version` should be a valid distribute version number that is available
|
||||
as an egg for download under the `download_base` URL (which should end
|
||||
with a '/'). `to_dir` is the directory where the egg will be downloaded.
|
||||
`delay` is the number of seconds to pause before an actual download
|
||||
attempt.
|
||||
"""
|
||||
# making sure we use the absolute path
|
||||
to_dir = os.path.abspath(to_dir)
|
||||
try:
|
||||
from urllib.request import urlopen
|
||||
except ImportError:
|
||||
from urllib2 import urlopen
|
||||
tgz_name = "distribute-%s.tar.gz" % version
|
||||
url = download_base + tgz_name
|
||||
saveto = os.path.join(to_dir, tgz_name)
|
||||
src = dst = None
|
||||
if not os.path.exists(saveto): # Avoid repeated downloads
|
||||
try:
|
||||
log.warn("Downloading %s", url)
|
||||
src = urlopen(url)
|
||||
# Read/write all in one block, so we don't create a corrupt file
|
||||
# if the download is interrupted.
|
||||
data = src.read()
|
||||
dst = open(saveto, "wb")
|
||||
dst.write(data)
|
||||
finally:
|
||||
if src:
|
||||
src.close()
|
||||
if dst:
|
||||
dst.close()
|
||||
return os.path.realpath(saveto)
|
||||
|
||||
def _no_sandbox(function):
|
||||
def __no_sandbox(*args, **kw):
|
||||
try:
|
||||
from setuptools.sandbox import DirectorySandbox
|
||||
if not hasattr(DirectorySandbox, '_old'):
|
||||
def violation(*args):
|
||||
pass
|
||||
DirectorySandbox._old = DirectorySandbox._violation
|
||||
DirectorySandbox._violation = violation
|
||||
patched = True
|
||||
else:
|
||||
patched = False
|
||||
except ImportError:
|
||||
patched = False
|
||||
|
||||
try:
|
||||
return function(*args, **kw)
|
||||
finally:
|
||||
if patched:
|
||||
DirectorySandbox._violation = DirectorySandbox._old
|
||||
del DirectorySandbox._old
|
||||
|
||||
return __no_sandbox
|
||||
|
||||
def _patch_file(path, content):
|
||||
"""Will backup the file then patch it"""
|
||||
existing_content = open(path).read()
|
||||
if existing_content == content:
|
||||
# already patched
|
||||
log.warn('Already patched.')
|
||||
return False
|
||||
log.warn('Patching...')
|
||||
_rename_path(path)
|
||||
f = open(path, 'w')
|
||||
try:
|
||||
f.write(content)
|
||||
finally:
|
||||
f.close()
|
||||
return True
|
||||
|
||||
_patch_file = _no_sandbox(_patch_file)
|
||||
|
||||
def _same_content(path, content):
|
||||
return open(path).read() == content
|
||||
|
||||
def _rename_path(path):
|
||||
new_name = path + '.OLD.%s' % time.time()
|
||||
log.warn('Renaming %s into %s', path, new_name)
|
||||
os.rename(path, new_name)
|
||||
return new_name
|
||||
|
||||
def _remove_flat_installation(placeholder):
|
||||
if not os.path.isdir(placeholder):
|
||||
log.warn('Unkown installation at %s', placeholder)
|
||||
return False
|
||||
found = False
|
||||
for file in os.listdir(placeholder):
|
||||
if fnmatch.fnmatch(file, 'setuptools*.egg-info'):
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
log.warn('Could not locate setuptools*.egg-info')
|
||||
return
|
||||
|
||||
log.warn('Removing elements out of the way...')
|
||||
pkg_info = os.path.join(placeholder, file)
|
||||
if os.path.isdir(pkg_info):
|
||||
patched = _patch_egg_dir(pkg_info)
|
||||
else:
|
||||
patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO)
|
||||
|
||||
if not patched:
|
||||
log.warn('%s already patched.', pkg_info)
|
||||
return False
|
||||
# now let's move the files out of the way
|
||||
for element in ('setuptools', 'pkg_resources.py', 'site.py'):
|
||||
element = os.path.join(placeholder, element)
|
||||
if os.path.exists(element):
|
||||
_rename_path(element)
|
||||
else:
|
||||
log.warn('Could not find the %s element of the '
|
||||
'Setuptools distribution', element)
|
||||
return True
|
||||
|
||||
_remove_flat_installation = _no_sandbox(_remove_flat_installation)
|
||||
|
||||
def _after_install(dist):
|
||||
log.warn('After install bootstrap.')
|
||||
placeholder = dist.get_command_obj('install').install_purelib
|
||||
_create_fake_setuptools_pkg_info(placeholder)
|
||||
|
||||
def _create_fake_setuptools_pkg_info(placeholder):
|
||||
if not placeholder or not os.path.exists(placeholder):
|
||||
log.warn('Could not find the install location')
|
||||
return
|
||||
pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1])
|
||||
setuptools_file = 'setuptools-%s-py%s.egg-info' % \
|
||||
(SETUPTOOLS_FAKED_VERSION, pyver)
|
||||
pkg_info = os.path.join(placeholder, setuptools_file)
|
||||
if os.path.exists(pkg_info):
|
||||
log.warn('%s already exists', pkg_info)
|
||||
return
|
||||
|
||||
if not os.access(pkg_info, os.W_OK):
|
||||
log.warn("Don't have permissions to write %s, skipping", pkg_info)
|
||||
|
||||
log.warn('Creating %s', pkg_info)
|
||||
f = open(pkg_info, 'w')
|
||||
try:
|
||||
f.write(SETUPTOOLS_PKG_INFO)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
pth_file = os.path.join(placeholder, 'setuptools.pth')
|
||||
log.warn('Creating %s', pth_file)
|
||||
f = open(pth_file, 'w')
|
||||
try:
|
||||
f.write(os.path.join(os.curdir, setuptools_file))
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
_create_fake_setuptools_pkg_info = _no_sandbox(_create_fake_setuptools_pkg_info)
|
||||
|
||||
def _patch_egg_dir(path):
|
||||
# let's check if it's already patched
|
||||
pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
|
||||
if os.path.exists(pkg_info):
|
||||
if _same_content(pkg_info, SETUPTOOLS_PKG_INFO):
|
||||
log.warn('%s already patched.', pkg_info)
|
||||
return False
|
||||
_rename_path(path)
|
||||
os.mkdir(path)
|
||||
os.mkdir(os.path.join(path, 'EGG-INFO'))
|
||||
pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
|
||||
f = open(pkg_info, 'w')
|
||||
try:
|
||||
f.write(SETUPTOOLS_PKG_INFO)
|
||||
finally:
|
||||
f.close()
|
||||
return True
|
||||
|
||||
_patch_egg_dir = _no_sandbox(_patch_egg_dir)
|
||||
|
||||
def _before_install():
|
||||
log.warn('Before install bootstrap.')
|
||||
_fake_setuptools()
|
||||
|
||||
|
||||
def _under_prefix(location):
|
||||
if 'install' not in sys.argv:
|
||||
return True
|
||||
args = sys.argv[sys.argv.index('install')+1:]
|
||||
for index, arg in enumerate(args):
|
||||
for option in ('--root', '--prefix'):
|
||||
if arg.startswith('%s=' % option):
|
||||
top_dir = arg.split('root=')[-1]
|
||||
return location.startswith(top_dir)
|
||||
elif arg == option:
|
||||
if len(args) > index:
|
||||
top_dir = args[index+1]
|
||||
return location.startswith(top_dir)
|
||||
if arg == '--user' and USER_SITE is not None:
|
||||
return location.startswith(USER_SITE)
|
||||
return True
|
||||
|
||||
|
||||
def _fake_setuptools():
|
||||
log.warn('Scanning installed packages')
|
||||
try:
|
||||
import pkg_resources
|
||||
except ImportError:
|
||||
# we're cool
|
||||
log.warn('Setuptools or Distribute does not seem to be installed.')
|
||||
return
|
||||
ws = pkg_resources.working_set
|
||||
try:
|
||||
setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools',
|
||||
replacement=False))
|
||||
except TypeError:
|
||||
# old distribute API
|
||||
setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools'))
|
||||
|
||||
if setuptools_dist is None:
|
||||
log.warn('No setuptools distribution found')
|
||||
return
|
||||
# detecting if it was already faked
|
||||
setuptools_location = setuptools_dist.location
|
||||
log.warn('Setuptools installation detected at %s', setuptools_location)
|
||||
|
||||
# if --root or --preix was provided, and if
|
||||
# setuptools is not located in them, we don't patch it
|
||||
if not _under_prefix(setuptools_location):
|
||||
log.warn('Not patching, --root or --prefix is installing Distribute'
|
||||
' in another location')
|
||||
return
|
||||
|
||||
# let's see if its an egg
|
||||
if not setuptools_location.endswith('.egg'):
|
||||
log.warn('Non-egg installation')
|
||||
res = _remove_flat_installation(setuptools_location)
|
||||
if not res:
|
||||
return
|
||||
else:
|
||||
log.warn('Egg installation')
|
||||
pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO')
|
||||
if (os.path.exists(pkg_info) and
|
||||
_same_content(pkg_info, SETUPTOOLS_PKG_INFO)):
|
||||
log.warn('Already patched.')
|
||||
return
|
||||
log.warn('Patching...')
|
||||
# let's create a fake egg replacing setuptools one
|
||||
res = _patch_egg_dir(setuptools_location)
|
||||
if not res:
|
||||
return
|
||||
log.warn('Patched done.')
|
||||
_relaunch()
|
||||
|
||||
|
||||
def _relaunch():
|
||||
log.warn('Relaunching...')
|
||||
# we have to relaunch the process
|
||||
# pip marker to avoid a relaunch bug
|
||||
if sys.argv[:3] == ['-c', 'install', '--single-version-externally-managed']:
|
||||
sys.argv[0] = 'setup.py'
|
||||
args = [sys.executable] + sys.argv
|
||||
sys.exit(subprocess.call(args))
|
||||
|
||||
|
||||
def _extractall(self, path=".", members=None):
|
||||
"""Extract all members from the archive to the current working
|
||||
directory and set owner, modification time and permissions on
|
||||
directories afterwards. `path' specifies a different directory
|
||||
to extract to. `members' is optional and must be a subset of the
|
||||
list returned by getmembers().
|
||||
"""
|
||||
import copy
|
||||
import operator
|
||||
from tarfile import ExtractError
|
||||
directories = []
|
||||
|
||||
if members is None:
|
||||
members = self
|
||||
|
||||
for tarinfo in members:
|
||||
if tarinfo.isdir():
|
||||
# Extract directories with a safe mode.
|
||||
directories.append(tarinfo)
|
||||
tarinfo = copy.copy(tarinfo)
|
||||
tarinfo.mode = 448 # decimal for oct 0700
|
||||
self.extract(tarinfo, path)
|
||||
|
||||
# Reverse sort directories.
|
||||
if sys.version_info < (2, 4):
|
||||
def sorter(dir1, dir2):
|
||||
return cmp(dir1.name, dir2.name)
|
||||
directories.sort(sorter)
|
||||
directories.reverse()
|
||||
else:
|
||||
directories.sort(key=operator.attrgetter('name'), reverse=True)
|
||||
|
||||
# Set correct owner, mtime and filemode on directories.
|
||||
for tarinfo in directories:
|
||||
dirpath = os.path.join(path, tarinfo.name)
|
||||
try:
|
||||
self.chown(tarinfo, dirpath)
|
||||
self.utime(tarinfo, dirpath)
|
||||
self.chmod(tarinfo, dirpath)
|
||||
except ExtractError:
|
||||
e = sys.exc_info()[1]
|
||||
if self.errorlevel > 1:
|
||||
raise
|
||||
else:
|
||||
self._dbg(1, "tarfile: %s" % e)
|
||||
|
||||
def _build_install_args(argv):
|
||||
install_args = []
|
||||
user_install = '--user' in argv
|
||||
if user_install and sys.version_info < (2,6):
|
||||
log.warn("--user requires Python 2.6 or later")
|
||||
raise SystemExit(1)
|
||||
if user_install:
|
||||
install_args.append('--user')
|
||||
return install_args
|
||||
|
||||
def main(argv, version=DEFAULT_VERSION):
|
||||
"""Install or upgrade setuptools and EasyInstall"""
|
||||
tarball = download_setuptools()
|
||||
_install(tarball, _build_install_args(argv))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv[1:])
|
||||
@@ -12,46 +12,49 @@ PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
SITETARGET=latest
|
||||
REGENDOC_ARGS := \
|
||||
--normalize "/={8,} (.*) ={8,}/======= \1 ========/" \
|
||||
--normalize "/_{8,} (.*) _{8,}/_______ \1 ________/" \
|
||||
--normalize "/in \d+.\d+ seconds/in 0.12 seconds/" \
|
||||
--normalize "@/tmp/pytest-of-.*/pytest-\d+@PYTEST_TMPDIR@" \
|
||||
|
||||
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
|
||||
|
||||
regen:
|
||||
PYTHONDONTWRITEBYTECODE=1 COLUMNS=76 regendoc --update *.txt */*.txt
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " showtarget to show the pytest.org target directory"
|
||||
@echo " install to install docs to pytest.org/SITETARGET"
|
||||
@echo " install-ldf to install the doc pdf to pytest.org/SITETARGET"
|
||||
@echo " regen to regenerate pytest examples using the installed pytest"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
|
||||
clean:
|
||||
-rm -rf $(BUILDDIR)/*
|
||||
|
||||
SITETARGET=$(shell ./_getdoctarget.py)
|
||||
|
||||
showtarget:
|
||||
@echo $(SITETARGET)
|
||||
|
||||
install: html
|
||||
rsync -avz _build/html/ pytest.org:/www/pytest.org/$(SITETARGET)
|
||||
# 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.org:/www/pytest.org/$(SITETARGET)
|
||||
@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}
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
|
||||
16
doc/en/_getdoctarget.py
Executable file
16
doc/en/_getdoctarget.py
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/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())
|
||||
@@ -1,339 +0,0 @@
|
||||
/*
|
||||
* sphinxdoc.css_t
|
||||
* ~~~~~~~~~~~~~~~
|
||||
*
|
||||
* Sphinx stylesheet -- sphinxdoc theme. Originally created by
|
||||
* Armin Ronacher for Werkzeug.
|
||||
*
|
||||
* :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
|
||||
* :license: BSD, see LICENSE for details.
|
||||
*
|
||||
*/
|
||||
|
||||
@import url("basic.css");
|
||||
|
||||
/* -- page layout ----------------------------------------------------------- */
|
||||
|
||||
body {
|
||||
font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva',
|
||||
'Verdana', sans-serif;
|
||||
font-size: 1.1em;
|
||||
letter-spacing: -0.01em;
|
||||
line-height: 150%;
|
||||
text-align: center;
|
||||
background-color: #BFD1D4;
|
||||
color: black;
|
||||
padding: 0;
|
||||
border: 1px solid #aaa;
|
||||
|
||||
margin: 0px 80px 0px 80px;
|
||||
min-width: 740px;
|
||||
}
|
||||
|
||||
div.document {
|
||||
background-color: white;
|
||||
text-align: left;
|
||||
background-image: url(contents.png);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
|
||||
div.bodywrapper {
|
||||
margin: 0 240px 0 0;
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
||||
|
||||
div.body {
|
||||
margin: 0;
|
||||
padding: 0.5em 20px 20px 20px;
|
||||
}
|
||||
|
||||
div.related {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
div.related ul {
|
||||
background-image: url(navigation.png);
|
||||
height: 2em;
|
||||
border-top: 1px solid #ddd;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
div.related ul li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 2em;
|
||||
float: left;
|
||||
}
|
||||
|
||||
div.related ul li.right {
|
||||
float: right;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
div.related ul li a {
|
||||
margin: 0;
|
||||
padding: 0 5px 0 5px;
|
||||
line-height: 1.75em;
|
||||
color: #EE9816;
|
||||
}
|
||||
|
||||
div.related ul li a:hover {
|
||||
color: #3CA8E7;
|
||||
}
|
||||
|
||||
div.sphinxsidebarwrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.sphinxsidebar {
|
||||
margin: 0;
|
||||
padding: 0.5em 15px 15px 0;
|
||||
width: 210px;
|
||||
float: right;
|
||||
font-size: 1em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3, div.sphinxsidebar h4 {
|
||||
margin: 1em 0 0.5em 0;
|
||||
font-size: 1em;
|
||||
padding: 0.1em 0 0.1em 0.5em;
|
||||
color: white;
|
||||
border: 1px solid #86989B;
|
||||
background-color: #AFC1C4;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3 a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
div.sphinxsidebar ul {
|
||||
padding-left: 1.5em;
|
||||
margin-top: 7px;
|
||||
padding: 0;
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
div.sphinxsidebar ul ul {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
div.footer {
|
||||
background-color: #E3EFF1;
|
||||
color: #86989B;
|
||||
padding: 3px 8px 3px 0;
|
||||
clear: both;
|
||||
font-size: 0.8em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
div.footer a {
|
||||
color: #86989B;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* -- body styles ----------------------------------------------------------- */
|
||||
|
||||
p {
|
||||
margin: 0.8em 0 0.5em 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #CA7900;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #2491CF;
|
||||
}
|
||||
|
||||
div.body a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
padding: 0.7em 0 0.3em 0;
|
||||
font-size: 1.5em;
|
||||
color: #11557C;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 1.3em 0 0.2em 0;
|
||||
font-size: 1.35em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 1em 0 -0.3em 0;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
div.body h1 a, div.body h2 a, div.body h3 a, div.body h4 a, div.body h5 a, div.body h6 a {
|
||||
color: black!important;
|
||||
}
|
||||
|
||||
h1 a.anchor, h2 a.anchor, h3 a.anchor, h4 a.anchor, h5 a.anchor, h6 a.anchor {
|
||||
display: none;
|
||||
margin: 0 0 0 0.3em;
|
||||
padding: 0 0.2em 0 0.2em;
|
||||
color: #aaa!important;
|
||||
}
|
||||
|
||||
h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor,
|
||||
h5:hover a.anchor, h6:hover a.anchor {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
h1 a.anchor:hover, h2 a.anchor:hover, h3 a.anchor:hover, h4 a.anchor:hover,
|
||||
h5 a.anchor:hover, h6 a.anchor:hover {
|
||||
color: #777;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
a.headerlink {
|
||||
color: #c60f0f!important;
|
||||
font-size: 1em;
|
||||
margin-left: 6px;
|
||||
padding: 0 4px 0 4px;
|
||||
text-decoration: none!important;
|
||||
}
|
||||
|
||||
a.headerlink:hover {
|
||||
background-color: #ccc;
|
||||
color: white!important;
|
||||
}
|
||||
|
||||
cite, code, tt {
|
||||
font-family: 'Consolas', 'Deja Vu Sans Mono',
|
||||
'Bitstream Vera Sans Mono', monospace;
|
||||
font-size: 0.95em;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
tt {
|
||||
background-color: #f2f2f2;
|
||||
border-bottom: 1px solid #ddd;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
tt.descname, tt.descclassname, tt.xref {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 1px solid #abc;
|
||||
margin: 2em;
|
||||
}
|
||||
|
||||
a tt {
|
||||
border: 0;
|
||||
color: #CA7900;
|
||||
}
|
||||
|
||||
a tt:hover {
|
||||
color: #2491CF;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: 'Consolas', 'Deja Vu Sans Mono',
|
||||
'Bitstream Vera Sans Mono', monospace;
|
||||
font-size: 0.95em;
|
||||
letter-spacing: 0.015em;
|
||||
line-height: 120%;
|
||||
padding: 0.5em;
|
||||
border: 1px solid #ccc;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
pre a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
td.linenos pre {
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
|
||||
div.quotebar {
|
||||
background-color: #f8f8f8;
|
||||
max-width: 250px;
|
||||
float: right;
|
||||
padding: 2px 7px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
div.topic {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
margin: 0 -0.5em 0 -0.5em;
|
||||
}
|
||||
|
||||
table td, table th {
|
||||
padding: 0.2em 0.5em 0.2em 0.5em;
|
||||
}
|
||||
|
||||
div.admonition, div.warning {
|
||||
font-size: 0.9em;
|
||||
margin: 1em 0 1em 0;
|
||||
border: 1px solid #86989B;
|
||||
background-color: #f7f7f7;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.admonition p, div.warning p {
|
||||
margin: 0.5em 1em 0.5em 1em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.admonition pre, div.warning pre {
|
||||
margin: 0.4em 1em 0.4em 1em;
|
||||
}
|
||||
|
||||
div.admonition p.admonition-title,
|
||||
div.warning p.admonition-title {
|
||||
margin: 0;
|
||||
padding: 0.1em 0 0.1em 0.5em;
|
||||
color: white;
|
||||
border-bottom: 1px solid #86989B;
|
||||
font-weight: bold;
|
||||
background-color: #AFC1C4;
|
||||
}
|
||||
|
||||
div.warning {
|
||||
border: 1px solid #940000;
|
||||
}
|
||||
|
||||
div.warning p.admonition-title {
|
||||
background-color: #CF0000;
|
||||
border-bottom-color: #940000;
|
||||
}
|
||||
|
||||
div.admonition ul, div.admonition ol,
|
||||
div.warning ul, div.warning ol {
|
||||
margin: 0.1em 0.5em 0.5em 3em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.versioninfo {
|
||||
margin: 1em 0 0 0;
|
||||
border: 1px solid #ccc;
|
||||
background-color: #DDEAF0;
|
||||
padding: 8px;
|
||||
line-height: 1.3em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.viewcode-back {
|
||||
font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva',
|
||||
'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
div.viewcode-block:target {
|
||||
background-color: #f4debf;
|
||||
border-top: 1px solid #ac9;
|
||||
border-bottom: 1px solid #ac9;
|
||||
}
|
||||
17
doc/en/_templates/globaltoc.html
Normal file
17
doc/en/_templates/globaltoc.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<h3><a href="{{ pathto(master_doc) }}">{{ _('Table Of Contents') }}</a></h3>
|
||||
|
||||
<ul>
|
||||
<li><a href="{{ pathto('index') }}">Home</a></li>
|
||||
<li><a href="{{ pathto('contents') }}">Contents</a></li>
|
||||
<li><a href="{{ pathto('getting-started') }}">Install</a></li>
|
||||
<li><a href="{{ pathto('example/index') }}">Examples</a></li>
|
||||
<li><a href="{{ pathto('customize') }}">Customize</a></li>
|
||||
<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>
|
||||
</ul>
|
||||
|
||||
{%- if display_toc %}
|
||||
<hr>
|
||||
{{ toc }}
|
||||
{%- endif %}
|
||||
@@ -1,22 +0,0 @@
|
||||
<h3>Download</h3>
|
||||
{% if version.endswith('(hg)') %}
|
||||
<p>This documentation is for version <b>{{ version }}</b>, which is
|
||||
not released yet.</p>
|
||||
<p>You can use it from the
|
||||
<a href="http://bitbucket.org/hpk42/pytest">Bitbucket Repo</a> or look for
|
||||
released versions in the <a href="http://pypi.python.org/pypi/pytest">Python
|
||||
Package Index</a>.</p>
|
||||
{% else %}
|
||||
<p><b><a href="{{ pathto('announce/index')}}">{{ release }} release</a></b>
|
||||
[<a href="{{ pathto('changelog') }}">Changelog</a>]</p>
|
||||
<p>
|
||||
<a href="http://pypi.python.org/pypi/pytest">pytest/PyPI</a>
|
||||
</p>
|
||||
<pre>easy_install pytest</pre>
|
||||
<pre>pip install pytest</pre>
|
||||
{% endif %}
|
||||
|
||||
<h3>Questions? Suggestions?</h3>
|
||||
|
||||
<p><a href="{{ pathto('contact') }}">contact channels</a>
|
||||
</p>
|
||||
@@ -1,10 +1,5 @@
|
||||
{% extends "!layout.html" %}
|
||||
|
||||
{% block relbaritems %}
|
||||
{{ super() }}
|
||||
<g:plusone></g:plusone>
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
{{ super() }}
|
||||
<script type="text/javascript">
|
||||
@@ -20,5 +15,4 @@
|
||||
})();
|
||||
|
||||
</script>
|
||||
<script type="text/javascript" src="https://apis.google.com/js/plusone.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
11
doc/en/_templates/links.html
Normal file
11
doc/en/_templates/links.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<h3>Useful Links</h3>
|
||||
<ul>
|
||||
<li><a href="{{ pathto('index') }}">The pytest Website</a></li>
|
||||
<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="https://github.com/pytest-dev/pytest/issues">Issue Tracker</a></li>
|
||||
<li><a href="http://pytest.org/latest/pytest.pdf">PDF Documentation</a>
|
||||
</ul>
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
|
||||
{%- if pagename != "search" %}
|
||||
<div id="searchbox" style="display: none">
|
||||
<form class="search" action="{{ pathto('search') }}" method="get">
|
||||
<input type="text" name="q" size="18" />
|
||||
<input type="submit" value="{{ _('Search') }}" />
|
||||
<input type="hidden" name="check_keywords" value="yes" />
|
||||
<input type="hidden" name="area" value="default" />
|
||||
</form>
|
||||
</div>
|
||||
<script type="text/javascript">$('#searchbox').show(0);</script>
|
||||
{%- endif %}
|
||||
|
||||
<h3>quicklinks</h3>
|
||||
<div style="text-align: left; font-size: 100%; vertical-align: middle;">
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ pathto('index') }}">home</a>
|
||||
</td><td>
|
||||
<a href="{{ pathto('contents') }}">TOC/contents</a>
|
||||
</td></tr><tr><td>
|
||||
<a href="{{ pathto('getting-started') }}">install</a>
|
||||
</td><td>
|
||||
<a href="{{ pathto('changelog') }}">changelog</a>
|
||||
</td></tr><tr><td>
|
||||
<a href="{{ pathto('example/index') }}">examples</a>
|
||||
</td><td>
|
||||
<a href="{{ pathto('customize') }}">customize</a>
|
||||
</td></tr><tr><td>
|
||||
<a href="https://bitbucket.org/hpk42/pytest/issues?status=new&status=open">issues[bb]</a>
|
||||
</td><td>
|
||||
<a href="{{ pathto('contact') }}">contact</a>
|
||||
</td></tr></table>
|
||||
</div>
|
||||
{% extends "basic/localtoc.html" %}
|
||||
|
||||
5
doc/en/_templates/sidebarintro.html
Normal file
5
doc/en/_templates/sidebarintro.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<h3>About pytest</h3>
|
||||
<p>
|
||||
pytest is a mature full-featured Python testing tool that helps
|
||||
you write better programs.
|
||||
</p>
|
||||
3
doc/en/_themes/.gitignore
vendored
Normal file
3
doc/en/_themes/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*.pyc
|
||||
*.pyo
|
||||
.DS_Store
|
||||
37
doc/en/_themes/LICENSE
Normal file
37
doc/en/_themes/LICENSE
Normal file
@@ -0,0 +1,37 @@
|
||||
Copyright (c) 2010 by Armin Ronacher.
|
||||
|
||||
Some rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms of the theme, with or
|
||||
without modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
|
||||
* The names of the contributors may not be used to endorse or
|
||||
promote products derived from this software without specific
|
||||
prior written permission.
|
||||
|
||||
We kindly ask you to only use these themes in an unmodified manner just
|
||||
for Flask and Flask-related products, not for unrelated projects. If you
|
||||
like the visual style and want to use it for your own projects, please
|
||||
consider making some larger changes to the themes (such as changing
|
||||
font faces, sizes, colors or margins).
|
||||
|
||||
THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
31
doc/en/_themes/README
Normal file
31
doc/en/_themes/README
Normal file
@@ -0,0 +1,31 @@
|
||||
Flask Sphinx Styles
|
||||
===================
|
||||
|
||||
This repository contains sphinx styles for Flask and Flask related
|
||||
projects. To use this style in your Sphinx documentation, follow
|
||||
this guide:
|
||||
|
||||
1. put this folder as _themes into your docs folder. Alternatively
|
||||
you can also use git submodules to check out the contents there.
|
||||
2. add this to your conf.py:
|
||||
|
||||
sys.path.append(os.path.abspath('_themes'))
|
||||
html_theme_path = ['_themes']
|
||||
html_theme = 'flask'
|
||||
|
||||
The following themes exist:
|
||||
|
||||
- 'flask' - the standard flask documentation theme for large
|
||||
projects
|
||||
- 'flask_small' - small one-page theme. Intended to be used by
|
||||
very small addon libraries for flask.
|
||||
|
||||
The following options exist for the flask_small theme:
|
||||
|
||||
[options]
|
||||
index_logo = '' filename of a picture in _static
|
||||
to be used as replacement for the
|
||||
h1 in the index.rst file.
|
||||
index_logo_height = 120px height of the index logo
|
||||
github_fork = '' repository name on github for the
|
||||
"fork me" badge
|
||||
24
doc/en/_themes/flask/layout.html
Normal file
24
doc/en/_themes/flask/layout.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{%- extends "basic/layout.html" %}
|
||||
{%- block extrahead %}
|
||||
{{ super() }}
|
||||
{% if theme_touch_icon %}
|
||||
<link rel="apple-touch-icon" href="{{ pathto('_static/' ~ theme_touch_icon, 1) }}" />
|
||||
{% endif %}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9">
|
||||
{% endblock %}
|
||||
{%- block relbar2 %}{% endblock %}
|
||||
{% block header %}
|
||||
{{ super() }}
|
||||
{% if pagename == 'index' %}
|
||||
<div class=indexwrapper>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{%- block footer %}
|
||||
<div class="footer">
|
||||
© Copyright {{ copyright }}.
|
||||
Created using <a href="http://sphinx.pocoo.org/">Sphinx</a>.
|
||||
</div>
|
||||
{% if pagename == 'index' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{%- endblock %}
|
||||
19
doc/en/_themes/flask/relations.html
Normal file
19
doc/en/_themes/flask/relations.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<h3>Related Topics</h3>
|
||||
<ul>
|
||||
<li><a href="{{ pathto(master_doc) }}">Documentation overview</a><ul>
|
||||
{%- for parent in parents %}
|
||||
<li><a href="{{ parent.link|e }}">{{ parent.title }}</a><ul>
|
||||
{%- endfor %}
|
||||
{%- if prev %}
|
||||
<li>Previous: <a href="{{ prev.link|e }}" title="{{ _('previous chapter')
|
||||
}}">{{ prev.title }}</a></li>
|
||||
{%- endif %}
|
||||
{%- if next %}
|
||||
<li>Next: <a href="{{ next.link|e }}" title="{{ _('next chapter')
|
||||
}}">{{ next.title }}</a></li>
|
||||
{%- endif %}
|
||||
{%- for parent in parents %}
|
||||
</ul></li>
|
||||
{%- endfor %}
|
||||
</ul></li>
|
||||
</ul>
|
||||
557
doc/en/_themes/flask/static/flasky.css_t
Normal file
557
doc/en/_themes/flask/static/flasky.css_t
Normal file
@@ -0,0 +1,557 @@
|
||||
/*
|
||||
* flasky.css_t
|
||||
* ~~~~~~~~~~~~
|
||||
*
|
||||
* :copyright: Copyright 2010 by Armin Ronacher.
|
||||
* :license: Flask Design License, see LICENSE for details.
|
||||
*/
|
||||
|
||||
{% set page_width = '1020px' %}
|
||||
{% set sidebar_width = '220px' %}
|
||||
/* orange of logo is #d67c29 but we use black for links for now */
|
||||
{% set link_color = '#000' %}
|
||||
{% set link_hover_color = '#000' %}
|
||||
{% set base_font = 'sans-serif' %}
|
||||
{% set header_font = 'serif' %}
|
||||
|
||||
@import url("basic.css");
|
||||
|
||||
/* -- page layout ----------------------------------------------------------- */
|
||||
|
||||
body {
|
||||
font-family: {{ base_font }};
|
||||
font-size: 17px;
|
||||
background-color: white;
|
||||
color: #000;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.document {
|
||||
width: {{ page_width }};
|
||||
margin: 30px auto 0 auto;
|
||||
}
|
||||
|
||||
div.documentwrapper {
|
||||
float: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.bodywrapper {
|
||||
margin: 0 0 0 {{ sidebar_width }};
|
||||
}
|
||||
|
||||
div.sphinxsidebar {
|
||||
width: {{ sidebar_width }};
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-top: 1px solid #B1B4B6;
|
||||
}
|
||||
|
||||
div.body {
|
||||
background-color: #ffffff;
|
||||
color: #3E4349;
|
||||
padding: 0 30px 0 30px;
|
||||
}
|
||||
|
||||
img.floatingflask {
|
||||
padding: 0 0 10px 10px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
div.footer {
|
||||
width: {{ page_width }};
|
||||
margin: 20px auto 30px auto;
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
div.footer a {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
div.related {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.sphinxsidebar a {
|
||||
color: #444;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted #999;
|
||||
}
|
||||
|
||||
div.sphinxsidebar a:hover {
|
||||
border-bottom: 1px solid #999;
|
||||
}
|
||||
|
||||
div.sphinxsidebar {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
div.sphinxsidebarwrapper {
|
||||
padding: 18px 10px;
|
||||
}
|
||||
|
||||
div.sphinxsidebarwrapper p.logo {
|
||||
padding: 0 0 20px 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3,
|
||||
div.sphinxsidebar h4 {
|
||||
font-family: {{ header_font }};
|
||||
color: #444;
|
||||
font-size: 24px;
|
||||
font-weight: normal;
|
||||
margin: 0 0 5px 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h4 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3 a {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
div.sphinxsidebar p.logo a,
|
||||
div.sphinxsidebar h3 a,
|
||||
div.sphinxsidebar p.logo a:hover,
|
||||
div.sphinxsidebar h3 a:hover {
|
||||
border: none;
|
||||
}
|
||||
|
||||
div.sphinxsidebar p {
|
||||
color: #555;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
div.sphinxsidebar ul {
|
||||
margin: 10px 0;
|
||||
padding: 0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
div.sphinxsidebar input {
|
||||
border: 1px solid #ccc;
|
||||
font-family: {{ base_font }};
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* -- body styles ----------------------------------------------------------- */
|
||||
|
||||
a {
|
||||
color: {{ link_color }};
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: {{ link_hover_color }};
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a.reference.internal em {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
div.body h1,
|
||||
div.body h2,
|
||||
div.body h3,
|
||||
div.body h4,
|
||||
div.body h5,
|
||||
div.body h6 {
|
||||
font-family: {{ header_font }};
|
||||
font-weight: normal;
|
||||
margin: 30px 0px 10px 0px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
{% if theme_index_logo %}
|
||||
div.indexwrapper h1 {
|
||||
text-indent: -999999px;
|
||||
background: url({{ theme_index_logo }}) no-repeat center center;
|
||||
height: {{ theme_index_logo_height }};
|
||||
}
|
||||
{% else %}
|
||||
div.indexwrapper div.body h1 {
|
||||
font-size: 200%;
|
||||
}
|
||||
{% endif %}
|
||||
div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; }
|
||||
div.body h2 { font-size: 180%; }
|
||||
div.body h3 { font-size: 150%; }
|
||||
div.body h4 { font-size: 130%; }
|
||||
div.body h5 { font-size: 100%; }
|
||||
div.body h6 { font-size: 100%; }
|
||||
|
||||
a.headerlink {
|
||||
color: #ddd;
|
||||
padding: 0 4px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.headerlink:hover {
|
||||
color: #444;
|
||||
background: #eaeaea;
|
||||
}
|
||||
|
||||
div.body p, div.body dd, div.body li {
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
div.admonition {
|
||||
background: #fafafa;
|
||||
margin: 20px -30px;
|
||||
padding: 10px 30px;
|
||||
border-top: 1px solid #ccc;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
div.admonition tt.xref, div.admonition a tt {
|
||||
border-bottom: 1px solid #fafafa;
|
||||
}
|
||||
|
||||
dd div.admonition {
|
||||
margin-left: -60px;
|
||||
padding-left: 60px;
|
||||
}
|
||||
|
||||
div.admonition p.admonition-title {
|
||||
font-family: {{ header_font }};
|
||||
font-weight: normal;
|
||||
font-size: 24px;
|
||||
margin: 0 0 10px 0;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
div.admonition p.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
div.highlight {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
dt:target, .highlight {
|
||||
background: #FAF3E8;
|
||||
}
|
||||
|
||||
div.note {
|
||||
background-color: #eee;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
div.seealso {
|
||||
background-color: #ffc;
|
||||
border: 1px solid #ff6;
|
||||
}
|
||||
|
||||
div.topic {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
p.admonition-title {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
p.admonition-title:after {
|
||||
content: ":";
|
||||
}
|
||||
|
||||
pre, tt, code {
|
||||
font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
|
||||
font-size: 0.9em;
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
img.screenshot {
|
||||
}
|
||||
|
||||
tt.descname, tt.descclassname {
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
tt.descname {
|
||||
padding-right: 0.08em;
|
||||
}
|
||||
|
||||
img.screenshot {
|
||||
-moz-box-shadow: 2px 2px 4px #eee;
|
||||
-webkit-box-shadow: 2px 2px 4px #eee;
|
||||
box-shadow: 2px 2px 4px #eee;
|
||||
}
|
||||
|
||||
table.docutils {
|
||||
border: 1px solid #888;
|
||||
-moz-box-shadow: 2px 2px 4px #eee;
|
||||
-webkit-box-shadow: 2px 2px 4px #eee;
|
||||
box-shadow: 2px 2px 4px #eee;
|
||||
}
|
||||
|
||||
table.docutils td, table.docutils th {
|
||||
border: 1px solid #888;
|
||||
padding: 0.25em 0.7em;
|
||||
}
|
||||
|
||||
table.field-list, table.footnote {
|
||||
border: none;
|
||||
-moz-box-shadow: none;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
table.footnote {
|
||||
margin: 15px 0;
|
||||
width: 100%;
|
||||
border: 1px solid #eee;
|
||||
background: #fdfdfd;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
table.footnote + table.footnote {
|
||||
margin-top: -15px;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
table.field-list th {
|
||||
padding: 0 0.8em 0 0;
|
||||
}
|
||||
|
||||
table.field-list td {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table.footnote td.label {
|
||||
width: 0px;
|
||||
padding: 0.3em 0 0.3em 0.5em;
|
||||
}
|
||||
|
||||
table.footnote td {
|
||||
padding: 0.3em 0.5em;
|
||||
}
|
||||
|
||||
dl {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
dl dd {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 0 30px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin: 10px 0 10px 30px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #eee;
|
||||
padding: 7px 30px;
|
||||
margin: 15px -30px;
|
||||
line-height: 1.3em;
|
||||
}
|
||||
|
||||
dl pre, blockquote pre, li pre {
|
||||
margin-left: -60px;
|
||||
padding-left: 60px;
|
||||
}
|
||||
|
||||
dl dl pre {
|
||||
margin-left: -90px;
|
||||
padding-left: 90px;
|
||||
}
|
||||
|
||||
tt {
|
||||
background-color: #ecf0f3;
|
||||
color: #222;
|
||||
/* padding: 1px 2px; */
|
||||
}
|
||||
|
||||
tt.xref, a tt {
|
||||
background-color: #FBFBFB;
|
||||
border-bottom: 1px solid white;
|
||||
}
|
||||
|
||||
a.reference {
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted {{ link_color }};
|
||||
}
|
||||
|
||||
a.reference:hover {
|
||||
border-bottom: 1px solid {{ link_hover_color }};
|
||||
}
|
||||
|
||||
a.footnote-reference {
|
||||
text-decoration: none;
|
||||
font-size: 0.7em;
|
||||
vertical-align: top;
|
||||
border-bottom: 1px dotted {{ link_color }};
|
||||
}
|
||||
|
||||
a.footnote-reference:hover {
|
||||
border-bottom: 1px solid {{ link_hover_color }};
|
||||
}
|
||||
|
||||
a:hover tt {
|
||||
background: #EEE;
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 870px) {
|
||||
|
||||
div.sphinxsidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.document {
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
|
||||
div.documentwrapper {
|
||||
margin-left: 0;
|
||||
margin-top: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
div.bodywrapper {
|
||||
margin-top: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.document {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.bodywrapper {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.github {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@media screen and (max-width: 875px) {
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px 30px;
|
||||
}
|
||||
|
||||
div.documentwrapper {
|
||||
float: none;
|
||||
background: white;
|
||||
}
|
||||
|
||||
div.sphinxsidebar {
|
||||
display: block;
|
||||
float: none;
|
||||
width: 102.5%;
|
||||
margin: 50px -30px -20px -30px;
|
||||
padding: 10px 20px;
|
||||
background: #333;
|
||||
color: white;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p,
|
||||
div.sphinxsidebar h3 a, div.sphinxsidebar ul {
|
||||
color: white;
|
||||
}
|
||||
|
||||
div.sphinxsidebar a {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
div.sphinxsidebar p.logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.document {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
div.related {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 10px 0 20px 0;
|
||||
}
|
||||
|
||||
div.related ul,
|
||||
div.related ul li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.bodywrapper {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
div.body {
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.rtd_doc_footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.document {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.github {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* misc. */
|
||||
|
||||
.revsys-inline {
|
||||
display: none!important;
|
||||
}
|
||||
9
doc/en/_themes/flask/theme.conf
Normal file
9
doc/en/_themes/flask/theme.conf
Normal file
@@ -0,0 +1,9 @@
|
||||
[theme]
|
||||
inherit = basic
|
||||
stylesheet = flasky.css
|
||||
pygments_style = flask_theme_support.FlaskyStyle
|
||||
|
||||
[options]
|
||||
index_logo = ''
|
||||
index_logo_height = 120px
|
||||
touch_icon =
|
||||
86
doc/en/_themes/flask_theme_support.py
Normal file
86
doc/en/_themes/flask_theme_support.py
Normal file
@@ -0,0 +1,86 @@
|
||||
# flasky extensions. flasky pygments style based on tango style
|
||||
from pygments.style import Style
|
||||
from pygments.token import Keyword, Name, Comment, String, Error, \
|
||||
Number, Operator, Generic, Whitespace, Punctuation, Other, Literal
|
||||
|
||||
|
||||
class FlaskyStyle(Style):
|
||||
background_color = "#f8f8f8"
|
||||
default_style = ""
|
||||
|
||||
styles = {
|
||||
# No corresponding class for the following:
|
||||
#Text: "", # class: ''
|
||||
Whitespace: "underline #f8f8f8", # class: 'w'
|
||||
Error: "#a40000 border:#ef2929", # class: 'err'
|
||||
Other: "#000000", # class 'x'
|
||||
|
||||
Comment: "italic #8f5902", # class: 'c'
|
||||
Comment.Preproc: "noitalic", # class: 'cp'
|
||||
|
||||
Keyword: "bold #004461", # class: 'k'
|
||||
Keyword.Constant: "bold #004461", # class: 'kc'
|
||||
Keyword.Declaration: "bold #004461", # class: 'kd'
|
||||
Keyword.Namespace: "bold #004461", # class: 'kn'
|
||||
Keyword.Pseudo: "bold #004461", # class: 'kp'
|
||||
Keyword.Reserved: "bold #004461", # class: 'kr'
|
||||
Keyword.Type: "bold #004461", # class: 'kt'
|
||||
|
||||
Operator: "#582800", # class: 'o'
|
||||
Operator.Word: "bold #004461", # class: 'ow' - like keywords
|
||||
|
||||
Punctuation: "bold #000000", # class: 'p'
|
||||
|
||||
# because special names such as Name.Class, Name.Function, etc.
|
||||
# are not recognized as such later in the parsing, we choose them
|
||||
# to look the same as ordinary variables.
|
||||
Name: "#000000", # class: 'n'
|
||||
Name.Attribute: "#c4a000", # class: 'na' - to be revised
|
||||
Name.Builtin: "#004461", # class: 'nb'
|
||||
Name.Builtin.Pseudo: "#3465a4", # class: 'bp'
|
||||
Name.Class: "#000000", # class: 'nc' - to be revised
|
||||
Name.Constant: "#000000", # class: 'no' - to be revised
|
||||
Name.Decorator: "#888", # class: 'nd' - to be revised
|
||||
Name.Entity: "#ce5c00", # class: 'ni'
|
||||
Name.Exception: "bold #cc0000", # class: 'ne'
|
||||
Name.Function: "#000000", # class: 'nf'
|
||||
Name.Property: "#000000", # class: 'py'
|
||||
Name.Label: "#f57900", # class: 'nl'
|
||||
Name.Namespace: "#000000", # class: 'nn' - to be revised
|
||||
Name.Other: "#000000", # class: 'nx'
|
||||
Name.Tag: "bold #004461", # class: 'nt' - like a keyword
|
||||
Name.Variable: "#000000", # class: 'nv' - to be revised
|
||||
Name.Variable.Class: "#000000", # class: 'vc' - to be revised
|
||||
Name.Variable.Global: "#000000", # class: 'vg' - to be revised
|
||||
Name.Variable.Instance: "#000000", # class: 'vi' - to be revised
|
||||
|
||||
Number: "#990000", # class: 'm'
|
||||
|
||||
Literal: "#000000", # class: 'l'
|
||||
Literal.Date: "#000000", # class: 'ld'
|
||||
|
||||
String: "#4e9a06", # class: 's'
|
||||
String.Backtick: "#4e9a06", # class: 'sb'
|
||||
String.Char: "#4e9a06", # class: 'sc'
|
||||
String.Doc: "italic #8f5902", # class: 'sd' - like a comment
|
||||
String.Double: "#4e9a06", # class: 's2'
|
||||
String.Escape: "#4e9a06", # class: 'se'
|
||||
String.Heredoc: "#4e9a06", # class: 'sh'
|
||||
String.Interpol: "#4e9a06", # class: 'si'
|
||||
String.Other: "#4e9a06", # class: 'sx'
|
||||
String.Regex: "#4e9a06", # class: 'sr'
|
||||
String.Single: "#4e9a06", # class: 's1'
|
||||
String.Symbol: "#4e9a06", # class: 'ss'
|
||||
|
||||
Generic: "#000000", # class: 'g'
|
||||
Generic.Deleted: "#a40000", # class: 'gd'
|
||||
Generic.Emph: "italic #000000", # class: 'ge'
|
||||
Generic.Error: "#ef2929", # class: 'gr'
|
||||
Generic.Heading: "bold #000080", # class: 'gh'
|
||||
Generic.Inserted: "#00A000", # class: 'gi'
|
||||
Generic.Output: "#888", # class: 'go'
|
||||
Generic.Prompt: "#745334", # class: 'gp'
|
||||
Generic.Strong: "bold #000000", # class: 'gs'
|
||||
Generic.Subheading: "bold #800080", # class: 'gu'
|
||||
Generic.Traceback: "bold #a40000", # class: 'gt'
|
||||
}
|
||||
78
doc/en/adopt.rst
Normal file
78
doc/en/adopt.rst
Normal file
@@ -0,0 +1,78 @@
|
||||
|
||||
April 2015 is "adopt pytest month"
|
||||
=============================================
|
||||
|
||||
Are you an enthusiastic pytest user, the local testing guru in your workplace? Or are you considering using pytest for your open source project, but not sure how to get started? Then you may be interested in "adopt pytest month"!
|
||||
|
||||
We will pair experienced pytest users with open source projects, for a month's effort of getting new development teams started with pytest.
|
||||
|
||||
In 2015 we are trying this for the first time. In February and March 2015 we will gather volunteers on both sides, in April we will do the work, and in May we will evaluate how it went. This effort is being coordinated by Brianna Laugher. If you have any questions or comments, you can raise them on the `@pytestdotorg twitter account <https://twitter.com/pytestdotorg>`_ the `issue tracker`_ or the `pytest-dev mailing list`_.
|
||||
|
||||
|
||||
.. _`issue tracker`: https://github.com/pytest-dev/pytest/issues/676
|
||||
.. _`pytest-dev mailing list`: https://mail.python.org/mailman/listinfo/pytest-dev
|
||||
|
||||
|
||||
The ideal pytest helper
|
||||
-----------------------------------------
|
||||
|
||||
- will be able to commit 2-4 hours a week to working with their particular project (this might involve joining their mailing list, installing the software and exploring any existing tests, offering advice, writing some example tests)
|
||||
- feels confident in using pytest (e.g. has explored command line options, knows how to write parametrized tests, has an idea about conftest contents)
|
||||
- does not need to be an expert in every aspect!
|
||||
|
||||
`Pytest helpers, sign up here`_! (preferably in February, hard deadline 22 March)
|
||||
|
||||
|
||||
.. _`Pytest helpers, sign up here`: http://goo.gl/forms/nxqAhqWt1P
|
||||
|
||||
|
||||
The ideal partner project
|
||||
-----------------------------------------
|
||||
|
||||
- is open source, and predominantly written in Python
|
||||
- has an automated/documented install process for developers
|
||||
- has more than one core developer
|
||||
- has at least one official release (e.g. is available on pypi)
|
||||
- has the support of the core development team, in trying out pytest adoption
|
||||
- has no tests... or 100% test coverage... or somewhere in between!
|
||||
|
||||
`Partner projects, sign up here`_! (by 22 March)
|
||||
|
||||
|
||||
.. _`Partner projects, sign up here`: http://goo.gl/forms/ZGyqlHiwk3
|
||||
|
||||
|
||||
What does it mean to "adopt pytest"?
|
||||
-----------------------------------------
|
||||
|
||||
There can be many different definitions of "success". Pytest can run many `nose and unittest`_ tests by default, so using pytest as your testrunner may be possible from day 1. Job done, right?
|
||||
|
||||
Progressive success might look like:
|
||||
|
||||
- tests can be run (by pytest) without errors (there may be failures)
|
||||
- tests can be run (by pytest) without failures
|
||||
- test runner is integrated into CI server
|
||||
- existing tests are rewritten to take advantage of pytest features - this can happen in several iterations, for example:
|
||||
- changing to native assert_ statements (pycmd_ has a script to help with that, ``pyconvert_unittest.py``)
|
||||
- changing `setUp/tearDown methods`_ to fixtures_
|
||||
- adding markers_
|
||||
- other changes to reduce boilerplate
|
||||
- assess needs for future tests to be written, e.g. new fixtures, distributed_ testing tweaks
|
||||
|
||||
"Success" should also include that the development team feels comfortable with their knowledge of how to use pytest. In fact this is probably more important than anything else. So spending a lot of time on communication, giving examples, etc will probably be important - both in running the tests, and in writing them.
|
||||
|
||||
It may be after the month is up, the partner project decides that pytest is not right for it. That's okay - hopefully the pytest team will also learn something about its weaknesses or deficiencies.
|
||||
|
||||
.. _`nose and unittest`: faq.html#how-does-pytest-relate-to-nose-and-unittest
|
||||
.. _assert: asserts.html
|
||||
.. _pycmd: https://bitbucket.org/hpk42/pycmd/overview
|
||||
.. _`setUp/tearDown methods`: xunit_setup.html
|
||||
.. _fixtures: fixture.html
|
||||
.. _markers: markers.html
|
||||
.. _distributed: xdist.html
|
||||
|
||||
|
||||
Other ways to help
|
||||
-----------------------------------------
|
||||
|
||||
Promote! Do your favourite open source Python projects use pytest? If not, why not tell them about this page?
|
||||
@@ -5,6 +5,21 @@ Release announcements
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
release-2.8.2
|
||||
release-2.7.2
|
||||
release-2.7.1
|
||||
release-2.7.0
|
||||
release-2.6.3
|
||||
release-2.6.2
|
||||
release-2.6.1
|
||||
release-2.6.0
|
||||
release-2.5.2
|
||||
release-2.5.1
|
||||
release-2.5.0
|
||||
release-2.4.2
|
||||
release-2.4.1
|
||||
release-2.4.0
|
||||
release-2.3.5
|
||||
release-2.3.4
|
||||
release-2.3.3
|
||||
release-2.3.2
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user