Compare commits
1867 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
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 | ||
|
|
8c7ae7f7a5 | ||
|
|
05c4ecf892 | ||
|
|
c5f9958783 | ||
|
|
7a90515d49 | ||
|
|
3ab94544b9 | ||
|
|
3c317dc35e | ||
|
|
b2cb93e06d | ||
|
|
c93fbb0e57 | ||
|
|
cf7cae0780 | ||
|
|
55c349a9eb | ||
|
|
73446e98be | ||
|
|
0bc98eb9d2 | ||
|
|
bfe9779b37 | ||
|
|
bb6f3ebd31 | ||
|
|
ee69b43c7a | ||
|
|
63a6936d82 | ||
|
|
1cbd2db621 | ||
|
|
94aa76fec0 | ||
|
|
265a4de06e | ||
|
|
712898cfe1 | ||
|
|
f31dc7a8b7 | ||
|
|
9c9679945e | ||
|
|
ba79c1926c | ||
|
|
76fb51a4ba | ||
|
|
93da606763 | ||
|
|
5e479c94ce | ||
|
|
1884be0121 | ||
|
|
8f8466ee40 | ||
|
|
dfcb0e322c | ||
|
|
da3b42ce46 | ||
|
|
fa9bd8443f | ||
|
|
5a3547dd7e | ||
|
|
c4b3a09886 | ||
|
|
f747d363b0 | ||
|
|
65c69a34ac | ||
|
|
ba0100e057 | ||
|
|
37c47155e0 | ||
|
|
5ba2a7f628 | ||
|
|
0cf79b29cd | ||
|
|
6d1662e4b7 | ||
|
|
850fd2b7f7 | ||
|
|
48e6aa9dc7 | ||
|
|
0dd05023b8 | ||
|
|
aeba66ac6a | ||
|
|
d23f9fab46 | ||
|
|
69ef750091 | ||
|
|
ca8b3c2307 | ||
|
|
857c99d354 | ||
|
|
3785f1aae3 | ||
|
|
d0e18ac63f | ||
|
|
296f752cca | ||
|
|
456731ed0f | ||
|
|
c8653b4c02 | ||
|
|
e7a86caac2 | ||
|
|
162c3689c6 | ||
|
|
b94c3084a6 | ||
|
|
570ad36eaf | ||
|
|
9d107523a1 | ||
|
|
06ab38a2fc | ||
|
|
e007f2dc54 | ||
|
|
25547e3afb | ||
|
|
64e6c71bf6 | ||
|
|
80f590288b | ||
|
|
570688f701 | ||
|
|
c5f587d6db | ||
|
|
ee713ad036 | ||
|
|
51b40dd22c | ||
|
|
65edf87ea6 | ||
|
|
4d4b551079 | ||
|
|
e13fedc256 | ||
|
|
97f9bc2e46 | ||
|
|
d0bf65e6c8 | ||
|
|
8d25e52e1e | ||
|
|
6fefab0e3a | ||
|
|
1e94d900f2 | ||
|
|
5f99511ab7 | ||
|
|
22dd5e29e2 | ||
|
|
725e63db66 | ||
|
|
3d79e7060e | ||
|
|
1d7c71884e | ||
|
|
ffb5b8efa1 | ||
|
|
68786a6434 | ||
|
|
b97de57ebe | ||
|
|
03445913e0 | ||
|
|
8580058ffb | ||
|
|
1c9ef2443f | ||
|
|
cac1a48fc7 | ||
|
|
b5955c5979 | ||
|
|
765b053984 | ||
|
|
a9adfa9114 | ||
|
|
7f403950ad | ||
|
|
f263f54889 | ||
|
|
d66ff7e63e | ||
|
|
f3e03fc298 | ||
|
|
2ef350aede | ||
|
|
b940ed11a0 | ||
|
|
e15da7cbef | ||
|
|
5b64b0130d | ||
|
|
af89a9667f | ||
|
|
c64c567b75 | ||
|
|
d31f4dcba8 | ||
|
|
d9ce7f143e | ||
|
|
4ac465acfb | ||
|
|
a4909a0ae4 | ||
|
|
c790490387 | ||
|
|
664b01ca42 | ||
|
|
ff0c75aa34 | ||
|
|
476d210d09 | ||
|
|
eedc4242ef | ||
|
|
370f5dd5cb | ||
|
|
79f45928a4 | ||
|
|
dbff4ae034 | ||
|
|
d6f10d502c | ||
|
|
65d6ebe7d1 | ||
|
|
33cd414420 | ||
|
|
dba2a8bc64 | ||
|
|
f203401964 | ||
|
|
c64699bba6 | ||
|
|
002c5072af | ||
|
|
b3c8991b22 | ||
|
|
1a41c9c001 | ||
|
|
df444906d6 | ||
|
|
04754f6748 | ||
|
|
d5ad91c64f | ||
|
|
7e831b66ec | ||
|
|
ba9b27fcd3 | ||
|
|
ca281b7c1b | ||
|
|
fb173a97a8 | ||
|
|
983b2d2475 | ||
|
|
e7e5ee805f | ||
|
|
67f8dd0cf2 | ||
|
|
07cc48517d | ||
|
|
fce13c3e46 | ||
|
|
573599beb3 | ||
|
|
6ebf39e9a6 | ||
|
|
6b6080ae6c | ||
|
|
427cf6f66d | ||
|
|
2fc8ee0839 | ||
|
|
6ad16936bb | ||
|
|
bcb8dc71d2 | ||
|
|
b8277bfed8 | ||
|
|
2637326782 | ||
|
|
aa79c0a4b9 | ||
|
|
05c86aeb28 | ||
|
|
f28f073c7c | ||
|
|
036557ac18 | ||
|
|
1b61fbc8ed | ||
|
|
97f03edcd6 | ||
|
|
7e5efa0005 | ||
|
|
d55fc611c4 | ||
|
|
720fe3405b | ||
|
|
c894b2b459 | ||
|
|
6d5bf4b908 | ||
|
|
d4d213f83d | ||
|
|
289ee1c6ea | ||
|
|
f41f7fda68 | ||
|
|
9ed127b5da | ||
|
|
525b08bc5c | ||
|
|
fae34ca5e3 | ||
|
|
0852e84d9f | ||
|
|
76db624639 | ||
|
|
1e6ec9941c | ||
|
|
a5ce481022 | ||
|
|
dca5fa2241 | ||
|
|
586befb945 | ||
|
|
b0b6695538 | ||
|
|
024df6e00b | ||
|
|
5e28f461c8 | ||
|
|
64544bee1a | ||
|
|
7c8755cc89 | ||
|
|
7d747a1cde | ||
|
|
dbaedbacde | ||
|
|
cf17f1d628 | ||
|
|
67de2c53ac | ||
|
|
26ab80c4cd | ||
|
|
20849a44f5 | ||
|
|
51644a116c | ||
|
|
98513b995a | ||
|
|
dc4e205876 | ||
|
|
2855a2f6cb | ||
|
|
cc2337af3a | ||
|
|
ab4183d400 | ||
|
|
37965657d0 | ||
|
|
ccaa1af534 | ||
|
|
2f3bbdafda | ||
|
|
021c087701 | ||
|
|
4541456a96 | ||
|
|
f5d796b093 | ||
|
|
40a55a640c | ||
|
|
6eec2f5893 | ||
|
|
0594265adc | ||
|
|
fb3af07ef4 | ||
|
|
39b8a19cf7 | ||
|
|
916c1c170e | ||
|
|
df643f65f0 | ||
|
|
d630d02c5b | ||
|
|
30b10a6950 | ||
|
|
cda84fb566 | ||
|
|
d3893dd5d1 | ||
|
|
55a8bfd174 | ||
|
|
f588eae4f5 | ||
|
|
d8c365ef2c | ||
|
|
4cbb2ab3b3 | ||
|
|
d1a3f5c3a6 | ||
|
|
bb07ba7807 | ||
|
|
8282efbb40 | ||
|
|
9251e747af | ||
|
|
439cc1238f | ||
|
|
3049af618c | ||
|
|
7bc7a9b702 | ||
|
|
5173647b4d | ||
|
|
57a832812b | ||
|
|
bee7543716 | ||
|
|
b9767fd74c | ||
|
|
dbe66f468a | ||
|
|
35cbb5791d | ||
|
|
a18fd61a20 | ||
|
|
a1c3d60747 | ||
|
|
fe4ccdff0e | ||
|
|
cd1ead4f7b | ||
|
|
9568ff3b23 | ||
|
|
6e5f491a42 | ||
|
|
7768972ec5 | ||
|
|
754fab9b55 | ||
|
|
253a87b2dc | ||
|
|
81082ed3d3 | ||
|
|
465cfff6f9 | ||
|
|
738f14a48a | ||
|
|
22dc47d9f9 | ||
|
|
6cb3281ddd | ||
|
|
a5e7e441d3 | ||
|
|
a7c6688bd6 | ||
|
|
d9c24552fc | ||
|
|
631d311e89 | ||
|
|
c2480f5c54 | ||
|
|
a94bb0a8bb | ||
|
|
646c2c6001 | ||
|
|
f6b555f5ad | ||
|
|
bf5b226474 | ||
|
|
084c617b67 | ||
|
|
bfaf8e50b6 | ||
|
|
848c749d1a | ||
|
|
41ad7dbae1 | ||
|
|
93eac240a0 | ||
|
|
a6060dfb6d | ||
|
|
7f36649763 | ||
|
|
f07ebc6615 | ||
|
|
e876ad9abd | ||
|
|
503addbf09 | ||
|
|
1318df4f5b | ||
|
|
45693c2847 | ||
|
|
0e8cd9297a | ||
|
|
0cca20bef9 | ||
|
|
1446b4b4e6 | ||
|
|
aa84359bd9 | ||
|
|
f275830ca7 | ||
|
|
627e068516 | ||
|
|
f472f21406 | ||
|
|
f4963270c6 | ||
|
|
08c3b1b80f | ||
|
|
935761f098 | ||
|
|
dd268c1b2b | ||
|
|
172505f703 | ||
|
|
6746a00cb8 | ||
|
|
46dc7eeacb | ||
|
|
ae241a5071 | ||
|
|
5fd84c35dd | ||
|
|
535d892f27 | ||
|
|
cb2eb9ba33 | ||
|
|
4f94ab4e42 | ||
|
|
449b55cc70 | ||
|
|
9dc79fd187 | ||
|
|
b57fb9fd47 | ||
|
|
d68c65b493 | ||
|
|
fa61927c6b | ||
|
|
d4a487c725 | ||
|
|
76584b53a1 | ||
|
|
6b0f0adf5b | ||
|
|
396045e53f | ||
|
|
80db25822c | ||
|
|
f358fe7154 | ||
|
|
e14459d45c | ||
|
|
4e4b507472 | ||
|
|
c7ee6e71ab | ||
|
|
4766497515 | ||
|
|
38b18c44e9 | ||
|
|
a73c27da13 | ||
|
|
dbaf7ee9d0 | ||
|
|
7a90bed19b | ||
|
|
8adac2878f | ||
|
|
66ed2d123a | ||
|
|
b902c36bfc | ||
|
|
099ac1e1f4 | ||
|
|
1aca6c9d7c | ||
|
|
fe24e01a03 | ||
|
|
838e758cf7 | ||
|
|
ddd4467fdd | ||
|
|
5574e45749 | ||
|
|
74e55493d1 | ||
|
|
ecec653e98 | ||
|
|
0ba0f91720 | ||
|
|
ea49993459 | ||
|
|
b4b86159cd | ||
|
|
91b6f2bda8 | ||
|
|
227d847216 | ||
|
|
6e0c30d67d | ||
|
|
65cbf591d8 | ||
|
|
e79a312b92 | ||
|
|
42d44bfd43 | ||
|
|
ccc04b9fc4 | ||
|
|
18306a4644 | ||
|
|
1bbe1d086c | ||
|
|
672919a8e2 | ||
|
|
f176ee3a1c | ||
|
|
474b177da8 | ||
|
|
b2e87ce027 | ||
|
|
2e163e4aae | ||
|
|
63eacd9dd5 | ||
|
|
b008e489ba | ||
|
|
d5078001c9 | ||
|
|
8b3ac3b03a | ||
|
|
6af20a5290 | ||
|
|
eb1b1005ae | ||
|
|
6fd57ec786 | ||
|
|
03a814a859 | ||
|
|
b4c2161e35 | ||
|
|
9198069739 | ||
|
|
4d77653bb0 | ||
|
|
3f17784386 | ||
|
|
971f96468c | ||
|
|
c11202b549 | ||
|
|
42d63832b7 | ||
|
|
f5f3fe54d5 | ||
|
|
76ec623b22 | ||
|
|
69fc6987ad | ||
|
|
0790f7a75f | ||
|
|
db8fbe7661 | ||
|
|
91c41cd6b3 | ||
|
|
1bf1cfd07a | ||
|
|
51d94a4a6e | ||
|
|
e18abfd013 | ||
|
|
6c7ea8191f | ||
|
|
329dca42a7 | ||
|
|
0362aaba5a | ||
|
|
948dea8bb4 | ||
|
|
6155e9139d | ||
|
|
6dd8405aed | ||
|
|
c076f4e789 | ||
|
|
d32a132b51 | ||
|
|
0e3779b14f | ||
|
|
fe1c35f8d0 | ||
|
|
b4588f1798 | ||
|
|
64c7c1be15 | ||
|
|
1c817aa7bd | ||
|
|
d02eaa8881 | ||
|
|
b92176024c | ||
|
|
1c746e0819 | ||
|
|
166aae4418 | ||
|
|
58933aac2a | ||
|
|
45aa4e5229 | ||
|
|
e643e99586 | ||
|
|
9f6d6f630d | ||
|
|
812ba87f37 | ||
|
|
2b0887fa5f | ||
|
|
ee8d2f9950 | ||
|
|
51d29cf4c6 | ||
|
|
e378496b24 | ||
|
|
4d21274a29 | ||
|
|
705442cf4e | ||
|
|
87b4cb283f | ||
|
|
83505b790d | ||
|
|
2ca6d9f039 | ||
|
|
87b8769680 | ||
|
|
78e7d7aed0 | ||
|
|
68b353be0d | ||
|
|
a756dc8106 | ||
|
|
604e27658c | ||
|
|
dfa273dc25 | ||
|
|
5263656df6 | ||
|
|
d88fe07377 | ||
|
|
2e23057804 | ||
|
|
303f49a5ad | ||
|
|
adbbd164ff | ||
|
|
93424b0f9c | ||
|
|
fb7706d4c7 | ||
|
|
4131923c0f | ||
|
|
7b95af2400 | ||
|
|
eb6481c663 | ||
|
|
c126cac98d | ||
|
|
e3a8b1e062 | ||
|
|
fa6d5bd15b | ||
|
|
f2c8a837af | ||
|
|
ccc1b21ebd | ||
|
|
85f2a78005 | ||
|
|
e21202b730 | ||
|
|
dc0535f7d5 | ||
|
|
f2791988f9 | ||
|
|
8e83af1c33 | ||
|
|
268c051eba | ||
|
|
03cb37b1eb | ||
|
|
d5c3265763 | ||
|
|
13e0340350 | ||
|
|
5093d8b925 | ||
|
|
40187ec9bb | ||
|
|
f5f8695587 | ||
|
|
27f5213718 | ||
|
|
b83a3bcc80 | ||
|
|
3a3f69372f | ||
|
|
4a08ee2b74 | ||
|
|
82ba764bb6 | ||
|
|
94e31e414a | ||
|
|
a94a6b4282 | ||
|
|
af0edf0d10 | ||
|
|
8307270cec | ||
|
|
c4fe622b82 | ||
|
|
b28977fbaf | ||
|
|
96cb1208d3 | ||
|
|
0c8e71faa5 | ||
|
|
d965101f6a | ||
|
|
d15ee2fb87 | ||
|
|
826d1e6153 | ||
|
|
50c9e3f654 | ||
|
|
59b8ea1746 | ||
|
|
cf02fb60c1 | ||
|
|
679d72eedf | ||
|
|
03b23e2587 | ||
|
|
48e6823c7a | ||
|
|
6b4e6eee09 | ||
|
|
f7648e11d8 | ||
|
|
7bb7d1205c | ||
|
|
a1d41c6811 | ||
|
|
58e0301f87 | ||
|
|
a5e7b2760d | ||
|
|
efe438d3e8 | ||
|
|
ec0565fac5 | ||
|
|
48a6a504b6 | ||
|
|
8f55425898 | ||
|
|
a51e52aee3 | ||
|
|
69dfc75572 | ||
|
|
9d3e51af9f | ||
|
|
f7c1b9087a | ||
|
|
36c42b5c15 | ||
|
|
bc8ee95e72 | ||
|
|
979dfd20f2 | ||
|
|
67fbd24ebf | ||
|
|
7f7589afa9 | ||
|
|
4f01cda2a7 | ||
|
|
bd296c796f | ||
|
|
7144cec580 | ||
|
|
99a1188287 | ||
|
|
0b18b6094e | ||
|
|
ae53d04780 | ||
|
|
a324826dfd | ||
|
|
29bf205f3a | ||
|
|
3b9fd3abd8 | ||
|
|
974e4e3a9d | ||
|
|
369b7709f7 | ||
|
|
78438db752 | ||
|
|
a2f4a11301 | ||
|
|
077c468589 | ||
|
|
d4fe273b2f | ||
|
|
761a95e542 | ||
|
|
5ae04397bd | ||
|
|
2c230f910d | ||
|
|
ae54151467 | ||
|
|
05af53d160 | ||
|
|
448f1c0d9c | ||
|
|
346da57a8a | ||
|
|
9d92b19ed1 | ||
|
|
e2201fe3a9 | ||
|
|
45b98d6e70 | ||
|
|
29b4082b00 | ||
|
|
6ac638ba87 | ||
|
|
f2512017ea | ||
|
|
3bd3ba133f | ||
|
|
be249dcfe5 | ||
|
|
45afb1b7d1 | ||
|
|
922a283f99 | ||
|
|
7e857e9068 | ||
|
|
172d46abd0 | ||
|
|
ac9192e4f8 | ||
|
|
b490047b1c | ||
|
|
ad785a476c | ||
|
|
d37af98db3 | ||
|
|
4316cf2121 | ||
|
|
fb6fc673b8 | ||
|
|
eaec527a60 | ||
|
|
2bc4065a00 | ||
|
|
5c32421f2e | ||
|
|
fab7615c8a | ||
|
|
2315de8321 | ||
|
|
25711a0879 | ||
|
|
0e05a4fbcf | ||
|
|
8675cf640d | ||
|
|
8b211983ff | ||
|
|
661a8a4a92 | ||
|
|
abe080c6b4 | ||
|
|
574d230c22 | ||
|
|
88c5299a94 | ||
|
|
09933b8b04 | ||
|
|
fb1b1d9aae | ||
|
|
68a08840e1 | ||
|
|
41b8a03b05 | ||
|
|
fba2079292 | ||
|
|
9675b0f65c | ||
|
|
c426a67b0e | ||
|
|
6ca3c980bf | ||
|
|
5bd34f8ecc | ||
|
|
7636dc76e0 | ||
|
|
c5dee7b549 | ||
|
|
643ab120f4 | ||
|
|
f86c8469f5 | ||
|
|
22335acd09 | ||
|
|
8b866aa065 | ||
|
|
2c4964d290 | ||
|
|
a70293fdb7 | ||
|
|
43113f9a9d | ||
|
|
650c3bcfde | ||
|
|
ade9b9aa8e | ||
|
|
7576b3c7d0 | ||
|
|
85415135a4 | ||
|
|
3cc8697744 | ||
|
|
703da22831 | ||
|
|
14ceaf2459 | ||
|
|
f3bc197afb | ||
|
|
aafe6a8e34 | ||
|
|
46b1348b79 | ||
|
|
709da3fe84 | ||
|
|
a59c2c9e17 | ||
|
|
6096aeca53 | ||
|
|
8cd68494bf | ||
|
|
dd4b252749 | ||
|
|
59067684cd | ||
|
|
81f4a07548 | ||
|
|
50c8218501 | ||
|
|
814c6c70f1 | ||
|
|
85118e9019 | ||
|
|
c2cdc66eca | ||
|
|
bc66cd85b1 | ||
|
|
639f35bbc4 | ||
|
|
8c683acad1 | ||
|
|
dc8225afea | ||
|
|
8713f4ba60 | ||
|
|
c40dc9f779 | ||
|
|
d1684e8052 | ||
|
|
c25ea2cbe2 | ||
|
|
6a523b4f59 | ||
|
|
fb043c355e | ||
|
|
0ef23dd31f | ||
|
|
c13fa886d9 | ||
|
|
79ac8c6681 | ||
|
|
418cd482b1 | ||
|
|
491f58ab26 | ||
|
|
df85ddf0d2 | ||
|
|
e7c8fc7db9 | ||
|
|
92f8eef836 | ||
|
|
758b5e3511 | ||
|
|
e91dc7c895 | ||
|
|
4e8b9fab3c | ||
|
|
d105e75d87 | ||
|
|
46950ef19a | ||
|
|
407ca5b120 | ||
|
|
a4fe63c08d | ||
|
|
fefdca5787 | ||
|
|
c7d120ec1c | ||
|
|
ae8ee08ac0 | ||
|
|
1707168b62 | ||
|
|
4fcd346838 | ||
|
|
aa7f7a1c71 | ||
|
|
48b76c7544 | ||
|
|
d52ff3e2b9 | ||
|
|
f286a02582 | ||
|
|
c6e3606c6b | ||
|
|
f4eb15632d | ||
|
|
d027f9d546 | ||
|
|
8de50347fb | ||
|
|
4b4a2c8162 | ||
|
|
29d58ffdf2 | ||
|
|
9ea58242d4 | ||
|
|
8772b8c928 | ||
|
|
8e81ed693a | ||
|
|
d853d9a9af | ||
|
|
57a3d4d6d8 | ||
|
|
8f6477f695 | ||
|
|
2618e3640f | ||
|
|
43de6c270f | ||
|
|
ce1b456762 | ||
|
|
332bceeb7a | ||
|
|
e3b2792677 | ||
|
|
67859158d4 | ||
|
|
5dfce4a0ca | ||
|
|
6c90059342 | ||
|
|
5690beab5a | ||
|
|
8bc9fdc8d3 | ||
|
|
00dee742b0 | ||
|
|
5e31624315 | ||
|
|
5e311d3bfc | ||
|
|
4b7293428b | ||
|
|
6fdcecb864 | ||
|
|
f63ff5267c | ||
|
|
5498fe960f | ||
|
|
4c885cf0d2 | ||
|
|
326b63adf8 | ||
|
|
70dc7a976d | ||
|
|
89a98e3276 | ||
|
|
410438f187 | ||
|
|
8dc4e732f0 | ||
|
|
e98057130d | ||
|
|
70d22fbe9a | ||
|
|
56b40ebd75 | ||
|
|
5f75c5851f | ||
|
|
606ea870f0 | ||
|
|
e56838cb6c | ||
|
|
e22d3e03fe | ||
|
|
d53feaf6f0 | ||
|
|
914f689ee8 | ||
|
|
971f34147a | ||
|
|
16b4f54545 | ||
|
|
abb07fc732 | ||
|
|
cf6949c9a3 | ||
|
|
2f984e0c23 | ||
|
|
0a7237b72f | ||
|
|
f684a9ed56 | ||
|
|
196cece338 | ||
|
|
241ff0b43a | ||
|
|
411e9b136b | ||
|
|
96521ada68 | ||
|
|
7cf8afef47 | ||
|
|
657522b629 | ||
|
|
dd199d255c | ||
|
|
89d6defd68 | ||
|
|
c4d761fe99 | ||
|
|
bf3d9f3737 | ||
|
|
32a67f9622 | ||
|
|
d438a0bd83 | ||
|
|
d3645758ea | ||
|
|
15b9e8ed7d | ||
|
|
ee64da4bad | ||
|
|
4fe13e59a7 | ||
|
|
250160b4b0 | ||
|
|
f423ce9c01 | ||
|
|
491c05cea7 | ||
|
|
e02d22aa4f | ||
|
|
c0910abf2f | ||
|
|
b061e71da9 | ||
|
|
fa412675fc | ||
|
|
0bb84abca7 | ||
|
|
f5decc90ca | ||
|
|
7fc2f8786f | ||
|
|
76cede83c0 | ||
|
|
993efe927b | ||
|
|
9c4f6791e5 | ||
|
|
7ba8fee3dc | ||
|
|
265b7458cb | ||
|
|
dc3e39e95c | ||
|
|
4f2166c997 | ||
|
|
e0c128beec | ||
|
|
bf039fea74 | ||
|
|
78be3db9bb | ||
|
|
aae89cd021 | ||
|
|
9ac818fb5c | ||
|
|
9e6dfaefd9 | ||
|
|
c742e47de0 | ||
|
|
95ddd5059f | ||
|
|
b6815538c5 | ||
|
|
ea936213bc | ||
|
|
7e65f346f4 | ||
|
|
07e870dc14 | ||
|
|
ee53b1f591 | ||
|
|
63ccec90be | ||
|
|
c666aeabbb | ||
|
|
f8137390c2 | ||
|
|
2589aa183c | ||
|
|
b316bc6723 | ||
|
|
420bbfd9a9 | ||
|
|
942ae47cd1 | ||
|
|
06ca7090f9 | ||
|
|
bc4e4b38a9 | ||
|
|
1c1918eb22 | ||
|
|
60ff2e8529 | ||
|
|
b7ba4d4e70 | ||
|
|
3a9788fc6f | ||
|
|
cf4e14baed | ||
|
|
1d40abadc4 | ||
|
|
ed6d2537bc | ||
|
|
a9f1f26a39 | ||
|
|
a7131dc911 | ||
|
|
6aaaaa8e67 | ||
|
|
6d06f55543 | ||
|
|
527bc472a8 | ||
|
|
007f0daeb9 | ||
|
|
55657d6c51 | ||
|
|
1a7c6ecc42 | ||
|
|
f2670651b3 | ||
|
|
5470cadbff | ||
|
|
f8e3fe8fbf | ||
|
|
c552b58dc5 | ||
|
|
18e784c9c9 | ||
|
|
5bef795ba7 | ||
|
|
a6c518e68c | ||
|
|
7e44c38570 | ||
|
|
9c952b3ce0 | ||
|
|
bfe6e98abb | ||
|
|
07cee24122 | ||
|
|
22fac92ca0 | ||
|
|
318e8a404b | ||
|
|
fadd1a2313 | ||
|
|
070c73ff2f | ||
|
|
682773e0cb | ||
|
|
6f3b84da9f | ||
|
|
f1b5dae1fb | ||
|
|
8d62e4c71c | ||
|
|
f6c1e49287 | ||
|
|
27577170e1 | ||
|
|
2f2586af72 | ||
|
|
70ceb946e4 | ||
|
|
d2f9b41519 | ||
|
|
2bd0c98801 | ||
|
|
5a5a618dcb | ||
|
|
98cd8edb71 | ||
|
|
e7b69a2ac0 | ||
|
|
74b9ebc1cd | ||
|
|
3004fe3915 | ||
|
|
eb225456d7 | ||
|
|
b04f87b1a6 | ||
|
|
35b0b376f0 | ||
|
|
762ea71f67 | ||
|
|
adacd3491d | ||
|
|
709d5e3f2c | ||
|
|
d8d88ede65 | ||
|
|
b8f0d10f80 | ||
|
|
aea4d1bd7a | ||
|
|
2b750074f4 | ||
|
|
88cfaebbcb | ||
|
|
426e056d2b | ||
|
|
4445685285 | ||
|
|
ae9b7a8bea | ||
|
|
5daef51000 | ||
|
|
647b56614a | ||
|
|
1b3fb3d229 | ||
|
|
170c78cef9 | ||
|
|
8f5d837ef6 | ||
|
|
0ec5f3fd6c | ||
|
|
8631c1f57a | ||
|
|
821f493378 | ||
|
|
4086d46378 | ||
|
|
a15983cb33 | ||
|
|
9ab256c296 | ||
|
|
7db9e98b55 | ||
|
|
e6541ed14e | ||
|
|
fc4f72cb1f | ||
|
|
feea4ea3d5 | ||
|
|
513482f4f7 | ||
|
|
2e80512bb8 | ||
|
|
c7531705fc | ||
|
|
752965c298 | ||
|
|
96a687b97c | ||
|
|
d894bae281 | ||
|
|
f1fc6e5eb6 | ||
|
|
ca72c162c8 | ||
|
|
9bcb66d9a5 | ||
|
|
f741d6a01e | ||
|
|
b622c85bbf | ||
|
|
9e7ef58cfd | ||
|
|
0efa6e5aea | ||
|
|
f6894ce550 | ||
|
|
1df0eaa387 | ||
|
|
8d4c9ec343 | ||
|
|
8a527b95f2 | ||
|
|
b28438171b | ||
|
|
ab08cb2176 | ||
|
|
4cb2c74159 | ||
|
|
03ee8b7fe0 | ||
|
|
539f828cdd | ||
|
|
c36b20b137 | ||
|
|
10d4544267 | ||
|
|
ff27d299cc | ||
|
|
233baecd2d | ||
|
|
9be1cd8007 | ||
|
|
b40a0c18b1 | ||
|
|
ac5992f9a1 | ||
|
|
e2068927f9 | ||
|
|
840eed28be | ||
|
|
6ebd5f2900 | ||
|
|
4accc4aa68 | ||
|
|
11e8e5570e | ||
|
|
4fa7a2e8ce | ||
|
|
695bffc83d | ||
|
|
6e6b0ab5d9 | ||
|
|
2458c139e4 | ||
|
|
0357d3afda | ||
|
|
bc42cf8ffb | ||
|
|
f456e376b9 | ||
|
|
158e160823 | ||
|
|
bd5a9ba392 | ||
|
|
9a21a81740 | ||
|
|
093bef0a08 | ||
|
|
eaf68c1ffd | ||
|
|
5a2295ada5 | ||
|
|
0325441099 | ||
|
|
582486d531 | ||
|
|
a698465487 | ||
|
|
93a436542c | ||
|
|
2a825169b2 | ||
|
|
acd286f82f | ||
|
|
fb102a2ddb | ||
|
|
a298cf753d | ||
|
|
0323c5247f | ||
|
|
82ba645a2e | ||
|
|
1bc444d5c8 | ||
|
|
868848a9a6 | ||
|
|
076e03e90f | ||
|
|
929291775e | ||
|
|
2e4e9eb745 | ||
|
|
323dd8a25a | ||
|
|
d44ff035d0 | ||
|
|
5bec71edc4 | ||
|
|
51fa358d8a | ||
|
|
07b67d36c4 | ||
|
|
3845ea821f | ||
|
|
3a53d86988 | ||
|
|
55dff651f4 | ||
|
|
fefac66079 | ||
|
|
6461295ab4 | ||
|
|
722e20c7d6 | ||
|
|
582a2100b1 | ||
|
|
9bed4bb31c | ||
|
|
d9ad2e7cce | ||
|
|
8716b391c7 | ||
|
|
107b04d462 | ||
|
|
885c7ce281 | ||
|
|
d0ac4135a2 | ||
|
|
707775dcfa | ||
|
|
1a7f2e77e8 | ||
|
|
b3628daa62 | ||
|
|
1899443744 | ||
|
|
fcebf4f557 | ||
|
|
6dac77433e | ||
|
|
f181c70d8e | ||
|
|
d108235095 | ||
|
|
6a734efe44 | ||
|
|
132eeeeade | ||
|
|
99dfb8ad65 | ||
|
|
bb732a4e75 | ||
|
|
b1e4301457 | ||
|
|
49319ba729 | ||
|
|
fed8f19156 | ||
|
|
5251653fc3 | ||
|
|
28d51e26a0 | ||
|
|
c18cca9d54 | ||
|
|
7d495cc250 | ||
|
|
53d1cfc3a1 | ||
|
|
32ac7a7c6e | ||
|
|
85c24b7fa1 | ||
|
|
cf8dd64703 | ||
|
|
c3ec2718a2 | ||
|
|
209140fea0 | ||
|
|
5616874823 | ||
|
|
14b2b128c2 | ||
|
|
f1e3dde2ec | ||
|
|
8871ca5bfa | ||
|
|
bb50ec89a9 | ||
|
|
868670b5f2 | ||
|
|
23f8d8bce7 | ||
|
|
03924d205d | ||
|
|
35969e13ae | ||
|
|
c92365f8dd | ||
|
|
f73ab23003 | ||
|
|
7138df5b51 | ||
|
|
bc574f4d94 | ||
|
|
b6ec5a575d | ||
|
|
1280041f0c | ||
|
|
2d8bcbdf55 | ||
|
|
c9e629c870 | ||
|
|
b86b1628bb | ||
|
|
f7b4f70a16 | ||
|
|
65675d5a95 | ||
|
|
33951d48f1 | ||
|
|
3ea082f63f | ||
|
|
8248b5e31d | ||
|
|
29222dffc9 | ||
|
|
90c1084a88 | ||
|
|
34c5c5d878 | ||
|
|
5fc87acf9b | ||
|
|
4480401119 | ||
|
|
a6f10a6d80 | ||
|
|
603ff3a64f | ||
|
|
b4210f3ae0 | ||
|
|
f466d35771 | ||
|
|
6cddd7e793 | ||
|
|
c2c8471f3d | ||
|
|
1999180dfd | ||
|
|
76a1bf391e | ||
|
|
47e56e0dee | ||
|
|
56afcfc9f3 | ||
|
|
dc5e2f5ed3 | ||
|
|
e3f48a81c5 | ||
|
|
5701ffa8d6 | ||
|
|
87e9cb9bec | ||
|
|
6411fa69bc | ||
|
|
c229e5459f | ||
|
|
039037701a | ||
|
|
80778eb3ae | ||
|
|
170346654c | ||
|
|
5d798feaf0 | ||
|
|
2a579217b8 | ||
|
|
976549cc88 | ||
|
|
bf1cd25831 | ||
|
|
5a0ef7355e | ||
|
|
1b7d2b07ab | ||
|
|
8853c5bdef | ||
|
|
2e4391d28e | ||
|
|
9925ac883e | ||
|
|
f3fb91e296 | ||
|
|
17719b99a1 | ||
|
|
3a5d28f3fe | ||
|
|
270deb015e | ||
|
|
22282eedb9 | ||
|
|
04c41cb672 | ||
|
|
7453fc107c | ||
|
|
07c835fdf3 | ||
|
|
9555d427ae | ||
|
|
6631447161 | ||
|
|
6efc6dcb62 | ||
|
|
9b4cca2bf4 | ||
|
|
251fb0ab1c | ||
|
|
a82a6bb058 | ||
|
|
b5b8e5f0c2 | ||
|
|
431a582132 | ||
|
|
aa70d9073c | ||
|
|
eee0e14334 | ||
|
|
ce3b260b0b | ||
|
|
c614adcf48 | ||
|
|
d89d0e8b26 | ||
|
|
4ee3831ac9 | ||
|
|
854f6a98ae | ||
|
|
7a461a2f3b | ||
|
|
652d0ca636 | ||
|
|
32fce34825 | ||
|
|
51bb0f53c5 | ||
|
|
d1aff902d5 | ||
|
|
f488da5cc8 | ||
|
|
98bdf022d3 | ||
|
|
09a9ce1da1 | ||
|
|
6b0db18eca | ||
|
|
253c173a88 | ||
|
|
7e3ff100f6 | ||
|
|
ec5ea5c05e | ||
|
|
c62ed0cd93 | ||
|
|
eccc2a868c | ||
|
|
60a9b60634 | ||
|
|
94c2fd4033 | ||
|
|
fe54762b93 | ||
|
|
89c53de084 | ||
|
|
eead8f9ab4 | ||
|
|
7c6e47f715 | ||
|
|
cebcdb83cf | ||
|
|
a054b63bac | ||
|
|
6892dc47a3 | ||
|
|
f6da7ea0a5 | ||
|
|
29051458fc | ||
|
|
4eb45dab08 | ||
|
|
939a53c436 | ||
|
|
a6003ac332 | ||
|
|
63bb9efd29 | ||
|
|
77cacb99ee | ||
|
|
1ff173baee | ||
|
|
b56d3c223d | ||
|
|
cd5676adc4 | ||
|
|
e2c11f1ddb | ||
|
|
81ec29a597 | ||
|
|
88915aa57d | ||
|
|
e2e01a8585 | ||
|
|
a60e470573 | ||
|
|
f779d3f863 | ||
|
|
2718fccfa0 | ||
|
|
a2fe6714f8 | ||
|
|
1c020c3d32 | ||
|
|
7d1585215d | ||
|
|
2cf22e3124 | ||
|
|
c3166ee84a | ||
|
|
56b955dfb5 | ||
|
|
4b2cb3acbe | ||
|
|
ca84a5e8e0 | ||
|
|
b86207a6c1 | ||
|
|
abab8f6f63 | ||
|
|
0af90e0962 | ||
|
|
58169edc8e | ||
|
|
e2683f4538 | ||
|
|
350ebbd9ad | ||
|
|
bb6e9848b3 | ||
|
|
489faf26f2 | ||
|
|
9ca7ed647b | ||
|
|
79734420df | ||
|
|
b81e48507c | ||
|
|
04b3b9a3da | ||
|
|
af412d993c | ||
|
|
6fb56443a9 | ||
|
|
6f40441ef8 | ||
|
|
7903fbb8ce | ||
|
|
2b59200786 | ||
|
|
2b3ac35780 | ||
|
|
c17bb32f70 | ||
|
|
f194b16a09 | ||
|
|
cd013746cf | ||
|
|
95bafbccd1 | ||
|
|
a2f9fbb178 | ||
|
|
f814cb5346 | ||
|
|
b690290c3f | ||
|
|
c542806396 | ||
|
|
faf0fe8887 | ||
|
|
d8fcc96563 | ||
|
|
31c91796c6 | ||
|
|
8f2b0d0889 | ||
|
|
ba1f6336af | ||
|
|
782430c614 | ||
|
|
3654592959 | ||
|
|
6aab9bcfb9 | ||
|
|
efeae72509 | ||
|
|
92bfb58798 | ||
|
|
74523a9d09 | ||
|
|
e5d09b771a | ||
|
|
677f7c0a6a | ||
|
|
1693b9c407 | ||
|
|
b14f8505d0 | ||
|
|
ed8e24312c | ||
|
|
71cb42d263 | ||
|
|
74d3acea02 | ||
|
|
caf5bdbf89 | ||
|
|
6ea944a350 | ||
|
|
9d47521cf0 | ||
|
|
86440b1853 | ||
|
|
e98b15eb67 | ||
|
|
1ffe0e7b82 | ||
|
|
3c069544fc | ||
|
|
4d4344212f | ||
|
|
eddd16d9fd | ||
|
|
37a2898f18 | ||
|
|
af5e18e26c | ||
|
|
320835d43f | ||
|
|
2664230fad | ||
|
|
48d818742c | ||
|
|
2b13836efa | ||
|
|
195d066ff8 | ||
|
|
1e8b59e39f | ||
|
|
b28c439494 | ||
|
|
223a04be27 | ||
|
|
3aefaff44f | ||
|
|
f9c5b00ffa | ||
|
|
e533e63bbf | ||
|
|
be582b5f53 | ||
|
|
4a489af0ff | ||
|
|
c19f51a3d7 | ||
|
|
ca9b320c9c | ||
|
|
ace2f975ea | ||
|
|
e3250f4846 | ||
|
|
29217a47f4 | ||
|
|
5f9876d54e | ||
|
|
8c0dfb525d | ||
|
|
aa4308883c | ||
|
|
381b81b0e1 | ||
|
|
7335c4d06d | ||
|
|
f554fa03ae | ||
|
|
6fa58fd8c9 | ||
|
|
c57a24774d | ||
|
|
d51000b15d | ||
|
|
b8db15a94f | ||
|
|
2f50ed3e99 | ||
|
|
da52304a6e | ||
|
|
f8d3a80af5 | ||
|
|
df744e9182 | ||
|
|
d82d65d95e | ||
|
|
f856db29dc | ||
|
|
4d75c703a0 | ||
|
|
3a8d13599e | ||
|
|
504e42a62e | ||
|
|
149f9e1042 | ||
|
|
3f1efe1b57 | ||
|
|
72de7d94fd | ||
|
|
c3233b9c15 | ||
|
|
2995d65720 | ||
|
|
77a7d576ec | ||
|
|
3b30c5b67a | ||
|
|
610cde6f85 | ||
|
|
add518e6b6 | ||
|
|
bc6ead1a3c | ||
|
|
0c04577f9f | ||
|
|
523704f890 | ||
|
|
6951da7da0 | ||
|
|
d83bf93154 | ||
|
|
4437ecb385 | ||
|
|
d1c8209875 | ||
|
|
6231bb0da3 | ||
|
|
64388832d9 | ||
|
|
d00b62e0f4 | ||
|
|
804dcd3521 | ||
|
|
c1d0fc9aaf | ||
|
|
8ece058256 | ||
|
|
10b8de060a | ||
|
|
10baa7f8af | ||
|
|
740a668f52 | ||
|
|
c56f4f9444 | ||
|
|
bdd1006e06 | ||
|
|
46f72d9350 | ||
|
|
f8404be1b2 | ||
|
|
2e82ca5fde | ||
|
|
a07e494554 | ||
|
|
75d80ca183 | ||
|
|
395bee4bc0 | ||
|
|
b66b5e2715 | ||
|
|
73d9900844 | ||
|
|
6cc89c9fcf | ||
|
|
2e36e2619f | ||
|
|
3042d1442a | ||
|
|
312238c023 | ||
|
|
ff2b893d31 | ||
|
|
c953c7d313 | ||
|
|
fbcf9ec543 | ||
|
|
9173b60677 | ||
|
|
7fc7b4307b | ||
|
|
545aab85f2 | ||
|
|
fa074da5a9 | ||
|
|
29a5b7452e | ||
|
|
94ce5b0a5a | ||
|
|
93712a3ce6 | ||
|
|
6f697294b2 | ||
|
|
7cba3a07af | ||
|
|
4f7ef0b63f | ||
|
|
67ec87e7f9 | ||
|
|
578cba20d4 | ||
|
|
93f91c9607 | ||
|
|
925f75088d | ||
|
|
eac0345689 | ||
|
|
20424a9c76 | ||
|
|
2229d2d947 | ||
|
|
c3bd29b490 | ||
|
|
f552749de6 | ||
|
|
cf255cd643 | ||
|
|
10296faff1 | ||
|
|
9f5e6f9761 | ||
|
|
c790288a5f | ||
|
|
da097c9d67 | ||
|
|
4f5d7948f7 | ||
|
|
1a97c59439 | ||
|
|
e71685736e | ||
|
|
5876736890 | ||
|
|
f97e082543 | ||
|
|
91880ffc19 | ||
|
|
169d8d1e54 | ||
|
|
11bf293972 | ||
|
|
9cc6b602b9 | ||
|
|
3d70917758 | ||
|
|
0cebee2d24 | ||
|
|
379390a8aa | ||
|
|
8ba2a98e11 | ||
|
|
0f5ed3abc7 | ||
|
|
74b8fdf28a | ||
|
|
f266c8f92f | ||
|
|
afdb928b12 | ||
|
|
1706947edd | ||
|
|
54afd26c7e | ||
|
|
05ab5ad6d5 | ||
|
|
a127767da6 | ||
|
|
a7d646c5e7 | ||
|
|
87fcea7eb7 | ||
|
|
ee036223ce | ||
|
|
325cb0aa49 | ||
|
|
c933ada7fb | ||
|
|
28150c7486 | ||
|
|
dd7fd97810 | ||
|
|
1a8b2838fa | ||
|
|
82d4aae571 | ||
|
|
fd473d4002 | ||
|
|
45e10f4c48 | ||
|
|
3efb8028fb | ||
|
|
b8247bc91e | ||
|
|
2630a452b4 | ||
|
|
b3ce06bbf9 | ||
|
|
962d0fe2be | ||
|
|
811408959f | ||
|
|
25ed74a77a | ||
|
|
5ece3858e4 | ||
|
|
1c1623885f | ||
|
|
5dc66bb4ca | ||
|
|
030548bc73 | ||
|
|
78d67c007b | ||
|
|
d93016d85f | ||
|
|
f2be437daa | ||
|
|
22a50a5b88 | ||
|
|
37cdf17e0e | ||
|
|
78d33a2f28 | ||
|
|
d5e463605e | ||
|
|
0d0a7b7fec | ||
|
|
f691292aaa | ||
|
|
ed7a2d2da3 | ||
|
|
8131f5bdc0 | ||
|
|
c8d78177b9 | ||
|
|
f6a04b92d2 | ||
|
|
7084313408 | ||
|
|
7629b8fda7 | ||
|
|
1771cb0af8 | ||
|
|
d1b45ef3d4 | ||
|
|
f16d54f9a8 | ||
|
|
d909aead4e | ||
|
|
0b24a70279 | ||
|
|
b3a05b545e | ||
|
|
221ac3e466 | ||
|
|
2ee6653ff7 | ||
|
|
4337702a6a | ||
|
|
ff90b1b4fb | ||
|
|
85d35f7418 | ||
|
|
cbb4c0dadc | ||
|
|
c10f0c2c36 | ||
|
|
6e84b487ca | ||
|
|
061f4c1515 | ||
|
|
fe34a8a15a | ||
|
|
5715bbd6f5 | ||
|
|
10f6c3a432 | ||
|
|
536252cb2e | ||
|
|
7cd899fd3c | ||
|
|
e45cd7e35e | ||
|
|
75a6bf2395 | ||
|
|
1d28dcf140 | ||
|
|
d994c51ccd | ||
|
|
fe95ad0aa6 | ||
|
|
c75d0faa6f | ||
|
|
3608d722fa | ||
|
|
18b5ddc4dd | ||
|
|
a2fbe31a26 | ||
|
|
222a08ec03 | ||
|
|
c7326f1949 | ||
|
|
3bca6be46d | ||
|
|
13488dd540 | ||
|
|
ac1eed545c | ||
|
|
e09e7148a3 | ||
|
|
d163d92b33 | ||
|
|
105ed6dcaa | ||
|
|
a3d15b2c60 | ||
|
|
3234e6e978 | ||
|
|
02c129df7a | ||
|
|
f95877a09b | ||
|
|
7bd60b5abb | ||
|
|
33d78f41b2 | ||
|
|
9d64d7e27a | ||
|
|
a2af204687 | ||
|
|
4d5ea7be43 | ||
|
|
98608611af | ||
|
|
b18ab6e03b | ||
|
|
17bd875444 | ||
|
|
aa6d2a37e6 | ||
|
|
14feeb9ca1 | ||
|
|
f7c562e492 | ||
|
|
53fc3204fb | ||
|
|
6756416d69 | ||
|
|
d3c0ff3a1f | ||
|
|
30b756a1a2 | ||
|
|
49f7972d48 | ||
|
|
1e76202b65 | ||
|
|
55fcc5a219 | ||
|
|
866255e1f5 | ||
|
|
0149771997 | ||
|
|
89068d9471 | ||
|
|
d483f18374 | ||
|
|
95de17b652 | ||
|
|
f5e9d91f7b | ||
|
|
09671eb6fc | ||
|
|
6f0db1d193 | ||
|
|
76e49b57bf | ||
|
|
4a568f43fe | ||
|
|
ee2f292efa | ||
|
|
1ff37207a2 | ||
|
|
7747655905 | ||
|
|
030986dcc4 | ||
|
|
9da1ba40ed | ||
|
|
5c27076d32 | ||
|
|
40f41496d8 | ||
|
|
d4f5073076 | ||
|
|
59f3adb46b | ||
|
|
d53572a710 | ||
|
|
676081b87a | ||
|
|
a9fe84d9b9 | ||
|
|
1b0d6296dd | ||
|
|
8d9e0712be | ||
|
|
3296939eda | ||
|
|
b6909e61a0 | ||
|
|
ba1451330e | ||
|
|
352e305431 | ||
|
|
3a23baf484 | ||
|
|
99301a0dae | ||
|
|
3029aa6558 | ||
|
|
45c1517580 | ||
|
|
3239bd250b | ||
|
|
ff5c1b6611 | ||
|
|
018254a907 | ||
|
|
9fcd108091 | ||
|
|
27aa14c20f | ||
|
|
d541713dca | ||
|
|
0b2d9a5520 | ||
|
|
c6c7d041b7 | ||
|
|
1b34492108 | ||
|
|
eebeb1b257 | ||
|
|
56a936993c | ||
|
|
1b6391d814 | ||
|
|
f3e62e38aa | ||
|
|
a20e60aeae | ||
|
|
fd76cd8f41 | ||
|
|
e0dd171e45 | ||
|
|
47df1e16b6 | ||
|
|
f8b5951103 | ||
|
|
b62978a88f | ||
|
|
2752168a58 | ||
|
|
eb4249322e | ||
|
|
9fa6ca885a | ||
|
|
587951966f | ||
|
|
fa0c7b18bf | ||
|
|
1103f2ad62 | ||
|
|
a42d9eb9f6 | ||
|
|
6495007aba | ||
|
|
94bc770786 | ||
|
|
77667c11d2 | ||
|
|
f5ea19858c | ||
|
|
d3b20e8d24 | ||
|
|
30bbf3b042 | ||
|
|
89f178bf4d | ||
|
|
4656bc4c97 | ||
|
|
f02b84d528 | ||
|
|
a15afb5e48 | ||
|
|
ae63605ac0 | ||
|
|
631dfe9f13 | ||
|
|
9d01975c78 | ||
|
|
77b640d1b7 | ||
|
|
9be7d78fb1 | ||
|
|
c348cec481 | ||
|
|
79af98fc29 | ||
|
|
7780e74016 | ||
|
|
db21cac694 | ||
|
|
080fd2880e | ||
|
|
71e332c9c4 | ||
|
|
425e4849f3 | ||
|
|
8737254a74 | ||
|
|
0361b73d75 | ||
|
|
27bcd2dbda | ||
|
|
cc82f1601c | ||
|
|
9239d2dd06 | ||
|
|
0bfd3819c8 | ||
|
|
abb05d9384 | ||
|
|
516cee2a94 | ||
|
|
d28838bbb6 | ||
|
|
88e61467f1 | ||
|
|
6d46efa87a | ||
|
|
cd96e52144 | ||
|
|
7864f6a4fd | ||
|
|
51a684f488 | ||
|
|
f254b6f7c1 | ||
|
|
98863d1d01 | ||
|
|
8a5c3c0c69 | ||
|
|
1580b2c8da | ||
|
|
4b1db63b35 | ||
|
|
ee1064b13c | ||
|
|
2ca39443a3 | ||
|
|
7637a6ecda | ||
|
|
15ed79cb08 | ||
|
|
1f45edf121 | ||
|
|
213c9fa119 | ||
|
|
b44e04f28d | ||
|
|
f93c7e0cc4 | ||
|
|
90306e0089 | ||
|
|
ec96ab5286 | ||
|
|
56c1391a16 | ||
|
|
bb755ae009 | ||
|
|
31bba4e087 | ||
|
|
9d5b313aad | ||
|
|
8c6593cc08 | ||
|
|
9652be0ac1 | ||
|
|
79a9a99d1e | ||
|
|
ed03eef81b | ||
|
|
0e03ae1ee8 | ||
|
|
8292ff7d0f | ||
|
|
bcede77e45 | ||
|
|
ecb19b751a | ||
|
|
d1dcf0fa92 | ||
|
|
0060869e79 | ||
|
|
452ce50d7d | ||
|
|
6d9e3ac686 | ||
|
|
3adf6687c9 | ||
|
|
41a572ee1e | ||
|
|
1a86d09da4 | ||
|
|
f4ec2d1ecd | ||
|
|
a4a652af85 | ||
|
|
e6f2258409 | ||
|
|
e0bca8fe51 | ||
|
|
a5a94c4e8f | ||
|
|
b04a04cabd | ||
|
|
4dd6c7679d | ||
|
|
e584892c12 | ||
|
|
6a82cdb37f | ||
|
|
609776bf26 | ||
|
|
30710a9cd6 | ||
|
|
c02719f44c | ||
|
|
7aee121bd7 | ||
|
|
270ac2bc0d | ||
|
|
92d482069c | ||
|
|
cc15685015 | ||
|
|
455b0afdfe | ||
|
|
690ccaedc1 | ||
|
|
58e1693af0 | ||
|
|
69dd2d7a78 | ||
|
|
86fc12dd15 | ||
|
|
1f01fafec7 | ||
|
|
92cab2bae7 | ||
|
|
37295dff0a | ||
|
|
84efdacfc0 | ||
|
|
d2e6cd0523 | ||
|
|
33bd39053f | ||
|
|
cc3404b832 | ||
|
|
2b1505c0f3 | ||
|
|
09ba42a1bb | ||
|
|
a161a865c8 | ||
|
|
fb159b0d40 | ||
|
|
6f80c985fb | ||
|
|
5f3bdf2d0b | ||
|
|
8e5efa7d6d | ||
|
|
82caacd633 | ||
|
|
6c2b1c4363 | ||
|
|
861f34fe90 | ||
|
|
4a76c096da | ||
|
|
9ac4faf3af | ||
|
|
118eebb190 | ||
|
|
9910db2ca6 | ||
|
|
fabd967595 | ||
|
|
c38bb72205 | ||
|
|
80f3e33e41 | ||
|
|
3795b08e95 | ||
|
|
eab7e039eb | ||
|
|
6f5918f03b | ||
|
|
d8b9b5f1c8 | ||
|
|
3ca770b420 | ||
|
|
5e21e39125 | ||
|
|
df8aedba47 | ||
|
|
1bdc0896ca | ||
|
|
90f39426b4 | ||
|
|
f10bfbb7e5 | ||
|
|
a603021757 | ||
|
|
c15a1b698c | ||
|
|
6e11f8cd2a | ||
|
|
5791c06bf2 | ||
|
|
db1ff48996 | ||
|
|
1f29529a24 | ||
|
|
ab9f6a75ad | ||
|
|
496e3b1138 | ||
|
|
aed66120a2 | ||
|
|
5914277f92 | ||
|
|
98b2300266 | ||
|
|
2986c5dc74 | ||
|
|
7466516673 | ||
|
|
2c523cd0d6 | ||
|
|
40e91dcd85 | ||
|
|
3d2975f38e | ||
|
|
c3fd7f0247 | ||
|
|
cb5bd868d9 | ||
|
|
0f96be372d | ||
|
|
4d598370b4 | ||
|
|
8f69d23f18 | ||
|
|
1e71a5c392 | ||
|
|
b3ca12d435 | ||
|
|
d80f37f14a | ||
|
|
8af3ede092 | ||
|
|
6ddea4a1bc | ||
|
|
e3b34c9da3 | ||
|
|
1b97d06a09 | ||
|
|
9fd1367845 | ||
|
|
62a4cf68e8 | ||
|
|
81062c5e2f | ||
|
|
47bad98c07 | ||
|
|
d4d0226058 | ||
|
|
22c1ad9f7b | ||
|
|
6d84da39e4 | ||
|
|
5df58c619d | ||
|
|
8ea2364039 | ||
|
|
b70c7a209d | ||
|
|
f9eadc6440 | ||
|
|
0f29b503ef | ||
|
|
29d437489d | ||
|
|
3c3002ccd5 | ||
|
|
c8119d89b6 | ||
|
|
5cf27098cf | ||
|
|
734a40eb28 | ||
|
|
518194537e | ||
|
|
783c714a2c | ||
|
|
7ab98c1b25 | ||
|
|
bde56a8246 | ||
|
|
1e51844519 | ||
|
|
32e2bf7d08 | ||
|
|
a0d7ab2244 | ||
|
|
6823fa634b | ||
|
|
5851471009 | ||
|
|
45b98d4915 | ||
|
|
99af33b26d | ||
|
|
1c9760d123 | ||
|
|
86da34b874 | ||
|
|
499a982860 | ||
|
|
c7f11745cd | ||
|
|
6c3e961bc5 | ||
|
|
73fc2f01f2 | ||
|
|
e30aeed876 | ||
|
|
5d2504df0a | ||
|
|
f636ed8ced | ||
|
|
c1fcf9c4d8 | ||
|
|
43b8bd7df7 | ||
|
|
54709bcae1 | ||
|
|
c791610998 | ||
|
|
e336683cdb | ||
|
|
749bfa46c4 | ||
|
|
318a935b89 | ||
|
|
04dd0810c3 | ||
|
|
ee17858cce | ||
|
|
ee86950af4 | ||
|
|
a051eb1a05 | ||
|
|
ad0c2edfd2 | ||
|
|
e63abd631f | ||
|
|
1781347999 | ||
|
|
01848ca821 | ||
|
|
39eac1be28 | ||
|
|
c95504e738 | ||
|
|
59892b8532 | ||
|
|
b3e8b2f6ab | ||
|
|
7a4bd92e33 | ||
|
|
711552e84c | ||
|
|
9af223e6cb | ||
|
|
8a6a3183ae | ||
|
|
9018fe40e3 | ||
|
|
4369c65790 | ||
|
|
fb365e47dc | ||
|
|
45a9aa536f | ||
|
|
ee1747fcb4 | ||
|
|
6f4c6d36a4 | ||
|
|
78d0d4656b | ||
|
|
b930565d56 | ||
|
|
0f7a9e2da2 | ||
|
|
96ec12902d | ||
|
|
1dafcc6b37 | ||
|
|
d75f7b2dd7 | ||
|
|
c2d0c52086 | ||
|
|
0014e65c1d | ||
|
|
fc3178a394 | ||
|
|
ac934bb2b6 | ||
|
|
695c8038e0 | ||
|
|
e596d9df13 | ||
|
|
130046d245 | ||
|
|
e0e9953be2 | ||
|
|
3bdbb29c6f | ||
|
|
c23cc3656c | ||
|
|
783e6aeb4d | ||
|
|
5e95feaf90 | ||
|
|
91f90d27ee | ||
|
|
d1932a30ed | ||
|
|
681d344eac | ||
|
|
13932b7f4b | ||
|
|
e391662cff | ||
|
|
47d56e41ba | ||
|
|
c981ead40e | ||
|
|
e7b86d808f | ||
|
|
f522838c77 | ||
|
|
81f7891539 | ||
|
|
3be875bfd2 | ||
|
|
c024dc6417 | ||
|
|
8ee7bef638 | ||
|
|
94aef0b771 | ||
|
|
e810e1774d | ||
|
|
d43d69e3db | ||
|
|
739edc26b4 | ||
|
|
58a9e71e81 | ||
|
|
27c08ac235 | ||
|
|
2b8f489d60 | ||
|
|
1fcd373bd5 | ||
|
|
046ac957ab | ||
|
|
5118821c10 | ||
|
|
f3fcb5e6d3 | ||
|
|
561fdca3a2 | ||
|
|
079a2327ec | ||
|
|
e051c9a4b8 | ||
|
|
ab83c4e05c | ||
|
|
9c6d5080e3 | ||
|
|
523380432a | ||
|
|
4b710010a6 | ||
|
|
4f50ae1336 | ||
|
|
d668acfd99 | ||
|
|
36288223b4 | ||
|
|
b1feb81b8a | ||
|
|
5e4fcdd14e | ||
|
|
30e87e887d | ||
|
|
36189a7aa7 | ||
|
|
38180ffa5f | ||
|
|
822b69a4e8 | ||
|
|
58d7f236a7 | ||
|
|
d2f497084e | ||
|
|
d702f4da14 | ||
|
|
7b906ca763 | ||
|
|
f0181c35a5 | ||
|
|
37976be529 | ||
|
|
b552f6eb46 | ||
|
|
a01e4769cc | ||
|
|
5c8df1d4ca | ||
|
|
58db35e70c | ||
|
|
d41949a6e3 | ||
|
|
58c6971dc8 | ||
|
|
cea46a86aa | ||
|
|
afc8e6bd91 | ||
|
|
8fcdac9dd6 | ||
|
|
91597f4100 | ||
|
|
cb9b8e4632 | ||
|
|
818de59080 | ||
|
|
cd5a89bec9 | ||
|
|
03970e0c4a | ||
|
|
8336df9929 | ||
|
|
4cf46c2723 |
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
# Automatically generated by `hgimportsvn`
|
||||
.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
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.swp
|
||||
*.html
|
||||
*.class
|
||||
*.orig
|
||||
*~
|
||||
|
||||
doc/*/_build
|
||||
build/
|
||||
dist/
|
||||
*.egg-info
|
||||
issue/
|
||||
env/
|
||||
3rdparty/
|
||||
.tox
|
||||
.cache
|
||||
.coverage
|
||||
.ropeproject
|
||||
.idea
|
||||
|
||||
23
.hgignore
23
.hgignore
@@ -1,9 +1,15 @@
|
||||
|
||||
# Automatically generated by `hgimportsvn`
|
||||
syntax:glob
|
||||
.svn
|
||||
.hgsvn
|
||||
|
||||
# Ignore local virtualenvs
|
||||
syntax:glob
|
||||
lib/
|
||||
bin/
|
||||
include/
|
||||
.Python/
|
||||
|
||||
# These lines are suggested according to the svn:ignore property
|
||||
# Feel free to enable them by uncommenting them
|
||||
syntax:glob
|
||||
@@ -11,6 +17,19 @@ syntax:glob
|
||||
*.pyo
|
||||
*.swp
|
||||
*.html
|
||||
*.class
|
||||
*.orig
|
||||
*~
|
||||
|
||||
doc/*/_build
|
||||
build/
|
||||
py.egg-info
|
||||
dist/
|
||||
*.egg-info
|
||||
issue/
|
||||
env/
|
||||
3rdparty/
|
||||
.tox
|
||||
.cache
|
||||
.coverage
|
||||
.ropeproject
|
||||
*.sublime-*
|
||||
|
||||
48
.hgtags
48
.hgtags
@@ -14,3 +14,51 @@ 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
|
||||
fc3a793e87ec907000a47ea0d3a372a2fe218c0a 2.3.5
|
||||
b93ac0cdae02effaa3c136a681cc45bba757fe46 1.4.14
|
||||
b93ac0cdae02effaa3c136a681cc45bba757fe46 1.4.14
|
||||
0000000000000000000000000000000000000000 1.4.14
|
||||
0000000000000000000000000000000000000000 1.4.14
|
||||
0000000000000000000000000000000000000000 1.4.14
|
||||
af860de70cc3f157ac34ca1d4bf557a057bff775 2.4.0
|
||||
8828c924acae0b4cad2e2cb92943d51da7cb744a 2.4.1
|
||||
|
||||
10
.travis.yml
Normal file
10
.travis.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
language: python
|
||||
# command to install dependencies
|
||||
install: "pip install -U detox"
|
||||
# # command to run tests
|
||||
script: detox --recreate
|
||||
notifications:
|
||||
irc:
|
||||
- "chat.freenode.net#pytest-dev"
|
||||
email:
|
||||
- pytest-commit@python.org
|
||||
37
AUTHORS
Normal file
37
AUTHORS
Normal file
@@ -0,0 +1,37 @@
|
||||
Holger Krekel, holger at merlinux eu
|
||||
merlinux GmbH, Germany, office at merlinux eu
|
||||
|
||||
Contributors include::
|
||||
|
||||
Ronny Pfannschmidt
|
||||
Benjamin Peterson
|
||||
Floris Bruynooghe
|
||||
Jason R. Coombs
|
||||
Wouter van Ackooy
|
||||
Samuele Pedroni
|
||||
Anatoly Bubenkoff
|
||||
Brianna Laugher
|
||||
Carl Friedrich Bolz
|
||||
Armin Rigo
|
||||
Maho
|
||||
Jaap Broekhuizen
|
||||
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
|
||||
Andreas Zeidler
|
||||
Brian Okken
|
||||
Katarzyna Jachim
|
||||
Christian Theunert
|
||||
Anthon van der Neut
|
||||
373
ISSUES.txt
Normal file
373
ISSUES.txt
Normal file
@@ -0,0 +1,373 @@
|
||||
|
||||
|
||||
recorder = monkeypatch.function(".......")
|
||||
-------------------------------------------------------------
|
||||
tags: nice feature
|
||||
|
||||
Like monkeypatch.replace but sets a mock-like call recorder:
|
||||
|
||||
recorder = monkeypatch.function("os.path.abspath")
|
||||
recorder.set_return("/hello")
|
||||
os.path.abspath("hello")
|
||||
call, = recorder.calls
|
||||
assert call.args.path == "hello"
|
||||
assert call.returned == "/hello"
|
||||
...
|
||||
|
||||
Unlike mock, "args.path" acts on the parsed auto-spec'ed ``os.path.abspath``
|
||||
so it's independent from if the client side called "os.path.abspath(path=...)"
|
||||
or "os.path.abspath('positional')".
|
||||
|
||||
refine parametrize API
|
||||
-------------------------------------------------------------
|
||||
tags: critical feature
|
||||
|
||||
extend metafunc.parametrize to directly support indirection, example:
|
||||
|
||||
def setupdb(request, config):
|
||||
# setup "resource" based on test request and the values passed
|
||||
# in to parametrize. setupfunc is called for each such value.
|
||||
# you may use request.addfinalizer() or request.cached_setup ...
|
||||
return dynamic_setup_database(val)
|
||||
|
||||
@pytest.mark.parametrize("db", ["pg", "mysql"], setupfunc=setupdb)
|
||||
def test_heavy_functional_test(db):
|
||||
...
|
||||
|
||||
There would be no need to write or explain funcarg factories and
|
||||
their special __ syntax.
|
||||
|
||||
The examples and improvements should also show how to put the parametrize
|
||||
decorator to a class, to a module or even to a directory. For the directory
|
||||
part a conftest.py content like this::
|
||||
|
||||
pytestmark = [
|
||||
@pytest.mark.parametrize_setup("db", ...),
|
||||
]
|
||||
|
||||
probably makes sense in order to keep the declarative nature. This mirrors
|
||||
the marker-mechanism with respect to a test module but puts it to a directory
|
||||
scale.
|
||||
|
||||
When doing larger scoped parametrization it probably becomes neccessary
|
||||
to allow parametrization to be ignored if the according parameter is not
|
||||
used (currently any parametrized argument that is not present in a function will cause a ValueError). Example:
|
||||
|
||||
@pytest.mark.parametrize("db", ..., mustmatch=False)
|
||||
|
||||
means to not raise an error but simply ignore the parametrization
|
||||
if the signature of a decorated function does not match. XXX is it
|
||||
not sufficient to always allow non-matches?
|
||||
|
||||
|
||||
allow parametrized attributes on classes
|
||||
--------------------------------------------------
|
||||
|
||||
tags: wish 2.4
|
||||
|
||||
example:
|
||||
|
||||
@pytest.mark.parametrize_attr("db", setupfunc, [1,2,3], scope="class")
|
||||
@pytest.mark.parametrize_attr("tmp", setupfunc, scope="...")
|
||||
class TestMe:
|
||||
def test_hello(self):
|
||||
access self.db ...
|
||||
|
||||
this would run the test_hello() function three times with three
|
||||
different values for self.db. This could also work with unittest/nose
|
||||
style tests, i.e. it leverages existing test suites without needing
|
||||
to rewrite them. Together with the previously mentioned setup_test()
|
||||
maybe the setupfunc could be 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
|
||||
|
||||
optimizations
|
||||
---------------------------------------------------------------
|
||||
tags: 2.4 core
|
||||
|
||||
- look at ihook optimization such that all lookups for
|
||||
hooks relating to the same fspath are cached.
|
||||
|
||||
fix start/finish partial finailization problem
|
||||
---------------------------------------------------------------
|
||||
tags: bug core
|
||||
|
||||
if a configure/runtest_setup/sessionstart/... hook invocation partially
|
||||
fails the sessionfinishes is not called. Each hook implementation
|
||||
should better be repsonsible for registering a cleanup/finalizer
|
||||
appropriately to avoid this issue. Moreover/Alternatively, we could
|
||||
record which implementations of a hook succeeded and only call their
|
||||
teardown.
|
||||
|
||||
|
||||
relax requirement to have tests/testing contain an __init__
|
||||
----------------------------------------------------------------
|
||||
tags: feature
|
||||
bb: http://bitbucket.org/hpk42/py-trunk/issue/64
|
||||
|
||||
A local test run of a "tests" directory may work
|
||||
but a remote one fail because the tests directory
|
||||
does not contain an "__init__.py". Either give
|
||||
an error or make it work without the __init__.py
|
||||
i.e. port the nose-logic of unloading a test module.
|
||||
|
||||
customize test function collection
|
||||
-------------------------------------------------------
|
||||
tags: feature
|
||||
|
||||
- introduce py.test.mark.nocollect for not considering a function for
|
||||
test collection at all. maybe also introduce a py.test.mark.test to
|
||||
explicitely mark a function to become a tested one. Lookup JUnit ways
|
||||
of tagging tests.
|
||||
|
||||
introduce pytest.mark.importorskip
|
||||
-------------------------------------------------------
|
||||
tags: feature
|
||||
|
||||
in addition to the imperative pytest.importorskip also introduce
|
||||
a pytest.mark.importorskip so that the test count is more correct.
|
||||
|
||||
|
||||
introduce py.test.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)
|
||||
|
||||
etc. Idea is to allow Python expressions which can operate
|
||||
on common spellings for operating systems and python
|
||||
interpreter versions.
|
||||
|
||||
pytest.mark.xfail signature change
|
||||
-------------------------------------------------------
|
||||
tags: feature
|
||||
|
||||
change to pytest.mark.xfail(reason, (optional)condition)
|
||||
to better implement the word meaning. It also signals
|
||||
better that we always have some kind of an implementation
|
||||
reason that can be formualated.
|
||||
Compatibility? how to introduce a new name/keep compat?
|
||||
|
||||
allow to non-intrusively apply skipfs/xfail/marks
|
||||
---------------------------------------------------
|
||||
tags: feature
|
||||
|
||||
use case: mark a module or directory structures
|
||||
to be skipped on certain platforms (i.e. no import
|
||||
attempt will be made).
|
||||
|
||||
consider introducing a hook/mechanism that allows to apply marks
|
||||
from conftests or plugins. (See extended parametrization)
|
||||
|
||||
|
||||
explicit referencing of conftest.py files
|
||||
-----------------------------------------
|
||||
tags: feature
|
||||
|
||||
allow to name conftest.py files (in sub directories) that should
|
||||
be imported early, as to include command line options.
|
||||
|
||||
improve central py.test ini file
|
||||
----------------------------------
|
||||
tags: feature
|
||||
|
||||
introduce more declarative configuration options:
|
||||
- (to-be-collected test directories)
|
||||
- required plugins
|
||||
- test func/class/file matching patterns
|
||||
- skip/xfail (non-intrusive)
|
||||
- pytest.ini and tox.ini and setup.cfg configuration in the same file
|
||||
|
||||
new documentation
|
||||
----------------------------------
|
||||
tags: feature
|
||||
|
||||
- logo py.test
|
||||
- examples for unittest or functional testing
|
||||
- resource management for functional testing
|
||||
- patterns: page object
|
||||
|
||||
have imported module mismatch honour relative paths
|
||||
--------------------------------------------------------
|
||||
tags: bug
|
||||
|
||||
With 1.1.1 py.test 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
|
||||
--------------------------------------------------------------
|
||||
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
|
||||
a plugin rather than being there from the start.
|
||||
|
||||
|
||||
consider pytest_addsyspath hook
|
||||
-----------------------------------------
|
||||
tags: wish
|
||||
|
||||
py.test could call a new pytest_addsyspath() in order to systematically
|
||||
allow manipulation of sys.path and to inhibit it via --no-addsyspath
|
||||
in order to more easily run against installed packages.
|
||||
|
||||
Alternatively it could also be done via the config object
|
||||
and pytest_configure.
|
||||
|
||||
|
||||
|
||||
deprecate global py.test.config usage
|
||||
----------------------------------------------------------------
|
||||
tags: feature
|
||||
|
||||
py.test.ensuretemp and py.test.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.
|
||||
deprecating them and checking with PyPy's usages as well
|
||||
as others.
|
||||
|
||||
remove deprecated bits in collect.py
|
||||
-------------------------------------------------------------------
|
||||
tags: feature
|
||||
|
||||
In an effort to further simplify code, review and remove deprecated bits
|
||||
in collect.py. Probably good:
|
||||
- inline consider_file/dir methods, no need to have them
|
||||
subclass-overridable because of hooks
|
||||
|
||||
implement fslayout decorator
|
||||
---------------------------------
|
||||
tags: feature
|
||||
|
||||
Improve the way how tests can work with pre-made examples,
|
||||
keeping the layout close to the test function:
|
||||
|
||||
@pytest.mark.fslayout("""
|
||||
conftest.py:
|
||||
# empty
|
||||
tests/
|
||||
test_%(NAME)s: # becomes test_run1.py
|
||||
def test_function(self):
|
||||
pass
|
||||
""")
|
||||
def test_run(pytester, fslayout):
|
||||
p = fslayout.findone("test_*.py")
|
||||
result = pytester.runpytest(p)
|
||||
assert result.ret == 0
|
||||
assert result.passed == 1
|
||||
|
||||
Another idea is to allow to define a full scenario including the run
|
||||
in one content string::
|
||||
|
||||
runscenario("""
|
||||
test_{TESTNAME}.py:
|
||||
import pytest
|
||||
@pytest.mark.xfail
|
||||
def test_that_fails():
|
||||
assert 0
|
||||
|
||||
@pytest.mark.skipif("True")
|
||||
def test_hello():
|
||||
pass
|
||||
|
||||
conftest.py:
|
||||
import pytest
|
||||
def pytest_runsetup_setup(item):
|
||||
pytest.skip("abc")
|
||||
|
||||
runpytest -rsxX
|
||||
*SKIP*{TESTNAME}*
|
||||
*1 skipped*
|
||||
""")
|
||||
|
||||
This could be run with at least three different ways to invoke pytest:
|
||||
through the shell, through "python -m pytest" and inlined. As inlined
|
||||
would be the fastest it could be run first (or "--fast" mode).
|
||||
|
||||
|
||||
Create isolate plugin
|
||||
---------------------
|
||||
tags: feature
|
||||
|
||||
The idea is that you can e.g. import modules in a test and afterwards
|
||||
sys.modules, sys.meta_path etc would be reverted. It can go further
|
||||
then just importing however, e.g. current working direcroty, file
|
||||
descriptors, ...
|
||||
|
||||
This would probably be done by marking::
|
||||
|
||||
@pytest.mark.isolate(importing=True, cwd=True, fds=False)
|
||||
def test_foo():
|
||||
...
|
||||
|
||||
With the possibility of doing this globally in an ini-file.
|
||||
|
||||
|
||||
fnmatch for test names
|
||||
----------------------
|
||||
tags: feature-wish
|
||||
|
||||
various testsuites use suffixes instead of prefixes for test classes
|
||||
also it lends itself to bdd style test names::
|
||||
|
||||
class UserBehaviour:
|
||||
def anonymous_should_not_have_inbox(user):
|
||||
...
|
||||
def registred_should_have_inbox(user):
|
||||
..
|
||||
|
||||
using the following in pytest.ini::
|
||||
|
||||
[pytest]
|
||||
python_classes = Test *Behaviour *Test
|
||||
python_functions = test *_should_*
|
||||
|
||||
|
||||
mechanism for running named parts of tests with different reporting behaviour
|
||||
------------------------------------------------------------------------------
|
||||
tags: feature-wish-incomplete
|
||||
|
||||
a few use-cases come to mind:
|
||||
|
||||
* fail assertions and record that without stopping a complete test
|
||||
|
||||
* this is in particular hepfull if a small bit of a test is known to fail/xfail::
|
||||
|
||||
def test_fun():
|
||||
with pytest.section('fdcheck, marks=pytest.mark.xfail_if(...)):
|
||||
breaks_on_windows()
|
||||
|
||||
* divide functional/acceptance tests into sections
|
||||
* provide a different mechanism for generators, maybe something like::
|
||||
|
||||
def pytest_runtest_call(item)
|
||||
if not generator:
|
||||
...
|
||||
prepare_check = GeneratorCheckprepare()
|
||||
|
||||
gen = item.obj(**fixtures)
|
||||
for check in gen
|
||||
id, call = prepare_check(check)
|
||||
# bubble should only prevent exception propagation after a failure
|
||||
# the whole test should still fail
|
||||
# there might be need for a loer level api and taking custom markers into account
|
||||
with pytest.section(id, bubble=False):
|
||||
call()
|
||||
|
||||
|
||||
19
LICENSE
Normal file
19
LICENSE
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
413
MANIFEST
413
MANIFEST
@@ -1,413 +0,0 @@
|
||||
CHANGELOG
|
||||
LICENSE
|
||||
MANIFEST
|
||||
README.txt
|
||||
_findpy.py
|
||||
doc/announce/release-0.9.0.txt
|
||||
doc/announce/release-0.9.2.txt
|
||||
doc/announce/release-1.0.0.txt
|
||||
doc/announce/releases.txt
|
||||
doc/bin.txt
|
||||
doc/code.txt
|
||||
doc/confrest.py
|
||||
doc/conftest.py
|
||||
doc/contact.txt
|
||||
doc/download.txt
|
||||
doc/execnet.txt
|
||||
doc/img/pylib.png
|
||||
doc/index.txt
|
||||
doc/io.txt
|
||||
doc/log.txt
|
||||
doc/misc.txt
|
||||
doc/path.txt
|
||||
doc/style.css
|
||||
doc/test/attic.txt
|
||||
doc/test/config.txt
|
||||
doc/test/dist.txt
|
||||
doc/test/examples.txt
|
||||
doc/test/extend.txt
|
||||
doc/test/features.txt
|
||||
doc/test/funcargs.txt
|
||||
doc/test/plugin/capture.txt
|
||||
doc/test/plugin/doctest.txt
|
||||
doc/test/plugin/figleaf.txt
|
||||
doc/test/plugin/hooklog.txt
|
||||
doc/test/plugin/hookspec.txt
|
||||
doc/test/plugin/index.txt
|
||||
doc/test/plugin/keyword.txt
|
||||
doc/test/plugin/links.txt
|
||||
doc/test/plugin/monkeypatch.txt
|
||||
doc/test/plugin/oejskit.txt
|
||||
doc/test/plugin/pastebin.txt
|
||||
doc/test/plugin/pdb.txt
|
||||
doc/test/plugin/recwarn.txt
|
||||
doc/test/plugin/restdoc.txt
|
||||
doc/test/plugin/resultlog.txt
|
||||
doc/test/plugin/terminal.txt
|
||||
doc/test/plugin/unittest.txt
|
||||
doc/test/plugin/xfail.txt
|
||||
doc/test/quickstart.txt
|
||||
doc/test/talks.txt
|
||||
doc/test/test.txt
|
||||
doc/test/xunit_setup.txt
|
||||
doc/xml.txt
|
||||
example/assertion/failure_demo.py
|
||||
example/assertion/test_failures.py
|
||||
example/assertion/test_setup_flow_example.py
|
||||
example/execnet/popen_read_multiple.py
|
||||
example/execnet/redirect_remote_output.py
|
||||
example/execnet/svn-sync-repo.py
|
||||
example/execnet/sysinfo.py
|
||||
example/funcarg/conftest.py
|
||||
example/funcarg/costlysetup/conftest.py
|
||||
example/funcarg/costlysetup/sub1/__init__.py
|
||||
example/funcarg/costlysetup/sub1/test_quick.py
|
||||
example/funcarg/costlysetup/sub2/__init__.py
|
||||
example/funcarg/costlysetup/sub2/test_two.py
|
||||
example/funcarg/mysetup/__init__.py
|
||||
example/funcarg/mysetup/conftest.py
|
||||
example/funcarg/mysetup/myapp.py
|
||||
example/funcarg/mysetup/test_sample.py
|
||||
example/funcarg/mysetup2/__init__.py
|
||||
example/funcarg/mysetup2/conftest.py
|
||||
example/funcarg/mysetup2/myapp.py
|
||||
example/funcarg/mysetup2/test_sample.py
|
||||
example/funcarg/mysetup2/test_ssh.py
|
||||
example/funcarg/parametrize/test_parametrize.py
|
||||
example/funcarg/parametrize/test_parametrize2.py
|
||||
example/funcarg/parametrize/test_parametrize3.py
|
||||
example/funcarg/test_simpleprovider.py
|
||||
example/genhtml.py
|
||||
example/genhtmlcss.py
|
||||
example/genxml.py
|
||||
makepluginlist.py
|
||||
py/LICENSE
|
||||
py/__init__.py
|
||||
py/_com.py
|
||||
py/bin/_findpy.py
|
||||
py/bin/_genscripts.py
|
||||
py/bin/gendoc.py
|
||||
py/bin/py.cleanup
|
||||
py/bin/py.countloc
|
||||
py/bin/py.lookup
|
||||
py/bin/py.rest
|
||||
py/bin/py.svnwcrevert
|
||||
py/bin/py.test
|
||||
py/bin/py.which
|
||||
py/bin/win32/py.cleanup.cmd
|
||||
py/bin/win32/py.countloc.cmd
|
||||
py/bin/win32/py.lookup.cmd
|
||||
py/bin/win32/py.rest.cmd
|
||||
py/bin/win32/py.svnwcrevert.cmd
|
||||
py/bin/win32/py.test.cmd
|
||||
py/bin/win32/py.which.cmd
|
||||
py/builtin/__init__.py
|
||||
py/builtin/enumerate.py
|
||||
py/builtin/exception.py
|
||||
py/builtin/reversed.py
|
||||
py/builtin/set.py
|
||||
py/builtin/sorted.py
|
||||
py/builtin/testing/__init__.py
|
||||
py/builtin/testing/test_enumerate.py
|
||||
py/builtin/testing/test_exception.py
|
||||
py/builtin/testing/test_reversed.py
|
||||
py/builtin/testing/test_set.py
|
||||
py/builtin/testing/test_sorted.py
|
||||
py/cmdline/__init__.py
|
||||
py/cmdline/pycleanup.py
|
||||
py/cmdline/pycountloc.py
|
||||
py/cmdline/pylookup.py
|
||||
py/cmdline/pyrest.py
|
||||
py/cmdline/pysvnwcrevert.py
|
||||
py/cmdline/pytest.py
|
||||
py/cmdline/pywhich.py
|
||||
py/cmdline/testing/__init__.py
|
||||
py/cmdline/testing/test_cmdline.py
|
||||
py/cmdline/testing/test_generic.py
|
||||
py/code/__init__.py
|
||||
py/code/code.py
|
||||
py/code/excinfo.py
|
||||
py/code/frame.py
|
||||
py/code/safe_repr.py
|
||||
py/code/source.py
|
||||
py/code/testing/__init__.py
|
||||
py/code/testing/test_code.py
|
||||
py/code/testing/test_excinfo.py
|
||||
py/code/testing/test_frame.py
|
||||
py/code/testing/test_safe_repr.py
|
||||
py/code/testing/test_source.py
|
||||
py/code/traceback2.py
|
||||
py/compat/LICENSE
|
||||
py/compat/__init__.py
|
||||
py/compat/conftest.py
|
||||
py/compat/doctest.py
|
||||
py/compat/optparse.py
|
||||
py/compat/subprocess.py
|
||||
py/compat/testing/__init__.py
|
||||
py/compat/testing/test_doctest.py
|
||||
py/compat/testing/test_doctest.txt
|
||||
py/compat/testing/test_doctest2.py
|
||||
py/compat/testing/test_doctest2.txt
|
||||
py/compat/testing/test_optparse.py
|
||||
py/compat/testing/test_subprocess.py
|
||||
py/compat/testing/test_textwrap.py
|
||||
py/compat/textwrap.py
|
||||
py/conftest.py
|
||||
py/env.cmd
|
||||
py/env.py
|
||||
py/execnet/NOTES
|
||||
py/execnet/__init__.py
|
||||
py/execnet/channel.py
|
||||
py/execnet/gateway.py
|
||||
py/execnet/gwmanage.py
|
||||
py/execnet/improve-remote-tracebacks.txt
|
||||
py/execnet/inputoutput.py
|
||||
py/execnet/message.py
|
||||
py/execnet/multi.py
|
||||
py/execnet/register.py
|
||||
py/execnet/rsync.py
|
||||
py/execnet/rsync_remote.py
|
||||
py/execnet/script/__init__.py
|
||||
py/execnet/script/loop_socketserver.py
|
||||
py/execnet/script/quitserver.py
|
||||
py/execnet/script/shell.py
|
||||
py/execnet/script/socketserver.py
|
||||
py/execnet/script/socketserverservice.py
|
||||
py/execnet/script/xx.py
|
||||
py/execnet/testing/__init__.py
|
||||
py/execnet/testing/conftest.py
|
||||
py/execnet/testing/test_event.py
|
||||
py/execnet/testing/test_gateway.py
|
||||
py/execnet/testing/test_gwmanage.py
|
||||
py/execnet/testing/test_multi.py
|
||||
py/execnet/testing/test_pickle.py
|
||||
py/execnet/testing/test_rsync.py
|
||||
py/execnet/testing/test_xspec.py
|
||||
py/execnet/xspec.py
|
||||
py/initpkg.py
|
||||
py/io/__init__.py
|
||||
py/io/dupfile.py
|
||||
py/io/fdcapture.py
|
||||
py/io/stdcapture.py
|
||||
py/io/terminalwriter.py
|
||||
py/io/testing/__init__.py
|
||||
py/io/testing/test_dupfile.py
|
||||
py/io/testing/test_fdcapture.py
|
||||
py/io/testing/test_stdcapture.py
|
||||
py/io/testing/test_terminalwriter.py
|
||||
py/log/__init__.py
|
||||
py/log/consumer.py
|
||||
py/log/logger.py
|
||||
py/log/producer.py
|
||||
py/log/testing/__init__.py
|
||||
py/log/testing/test_log.py
|
||||
py/log/testing/test_logger.py
|
||||
py/log/testing/test_warning.py
|
||||
py/log/warning.py
|
||||
py/magic/__init__.py
|
||||
py/magic/assertion.py
|
||||
py/magic/autopath.py
|
||||
py/magic/exprinfo.py
|
||||
py/magic/invoke.py
|
||||
py/magic/patch.py
|
||||
py/magic/testing/__init__.py
|
||||
py/magic/testing/test_assertion.py
|
||||
py/magic/testing/test_autopath.py
|
||||
py/magic/testing/test_exprinfo.py
|
||||
py/magic/testing/test_invoke.py
|
||||
py/magic/testing/test_patch.py
|
||||
py/magic/testing/test_viewtype.py
|
||||
py/magic/viewtype.py
|
||||
py/misc/__init__.py
|
||||
py/misc/_dist.py
|
||||
py/misc/buildcmodule.py
|
||||
py/misc/cache.py
|
||||
py/misc/cmdline/__init__.py
|
||||
py/misc/cmdline/countloc.py
|
||||
py/misc/difftime.py
|
||||
py/misc/dynpkg.py
|
||||
py/misc/error.py
|
||||
py/misc/findmissingdocstrings.py
|
||||
py/misc/rest.py
|
||||
py/misc/std.py
|
||||
py/misc/svnlook.py
|
||||
py/misc/terminal_helper.py
|
||||
py/misc/testing/__init__.py
|
||||
py/misc/testing/data/svnlookrepo.dump
|
||||
py/misc/testing/test_api.py
|
||||
py/misc/testing/test_cache.py
|
||||
py/misc/testing/test_com.py
|
||||
py/misc/testing/test_error.py
|
||||
py/misc/testing/test_initpkg.py
|
||||
py/misc/testing/test_std.py
|
||||
py/misc/testing/test_svnlook.py
|
||||
py/misc/testing/test_terminal.py
|
||||
py/path/__init__.py
|
||||
py/path/common.py
|
||||
py/path/gateway/TODO.txt
|
||||
py/path/gateway/__init__.py
|
||||
py/path/gateway/channeltest.py
|
||||
py/path/gateway/channeltest2.py
|
||||
py/path/gateway/remotepath.py
|
||||
py/path/local/__init__.py
|
||||
py/path/local/common.py
|
||||
py/path/local/local.py
|
||||
py/path/local/posix.py
|
||||
py/path/local/testing/__init__.py
|
||||
py/path/local/testing/test_local.py
|
||||
py/path/local/testing/test_posix.py
|
||||
py/path/local/testing/test_win.py
|
||||
py/path/local/win.py
|
||||
py/path/svn/__init__.py
|
||||
py/path/svn/cache.py
|
||||
py/path/svn/quoting.txt
|
||||
py/path/svn/svncommon.py
|
||||
py/path/svn/testing/__init__.py
|
||||
py/path/svn/testing/repotest.dump
|
||||
py/path/svn/testing/svntestbase.py
|
||||
py/path/svn/testing/test_auth.py
|
||||
py/path/svn/testing/test_test_repo.py
|
||||
py/path/svn/testing/test_urlcommand.py
|
||||
py/path/svn/testing/test_wccommand.py
|
||||
py/path/svn/urlcommand.py
|
||||
py/path/svn/wccommand.py
|
||||
py/path/testing/__init__.py
|
||||
py/path/testing/common.py
|
||||
py/path/testing/fscommon.py
|
||||
py/path/testing/test_api.py
|
||||
py/process/__init__.py
|
||||
py/process/cmdexec.py
|
||||
py/process/forkedfunc.py
|
||||
py/process/killproc.py
|
||||
py/process/testing/__init__.py
|
||||
py/process/testing/test_cmdexec.py
|
||||
py/process/testing/test_forkedfunc.py
|
||||
py/process/testing/test_killproc.py
|
||||
py/rest/__init__.py
|
||||
py/rest/convert.py
|
||||
py/rest/directive.py
|
||||
py/rest/latex.py
|
||||
py/rest/rest.sty.template
|
||||
py/rest/rst.py
|
||||
py/rest/testing/__init__.py
|
||||
py/rest/testing/data/example.rst2pdfconfig
|
||||
py/rest/testing/data/example1.dot
|
||||
py/rest/testing/data/formula.txt
|
||||
py/rest/testing/data/formula1.txt
|
||||
py/rest/testing/data/graphviz.txt
|
||||
py/rest/testing/data/part1.txt
|
||||
py/rest/testing/data/part2.txt
|
||||
py/rest/testing/data/tocdepth.rst2pdfconfig
|
||||
py/rest/testing/setup.py
|
||||
py/rest/testing/test_convert.py
|
||||
py/rest/testing/test_directive.py
|
||||
py/rest/testing/test_htmlrest.py
|
||||
py/rest/testing/test_rst.py
|
||||
py/rest/testing/test_rst2pdf.py
|
||||
py/rest/testing/test_transform.py
|
||||
py/rest/transform.py
|
||||
py/test/__init__.py
|
||||
py/test/cmdline.py
|
||||
py/test/collect.py
|
||||
py/test/compat.py
|
||||
py/test/config.py
|
||||
py/test/conftesthandle.py
|
||||
py/test/defaultconftest.py
|
||||
py/test/dist/__init__.py
|
||||
py/test/dist/dsession.py
|
||||
py/test/dist/mypickle.py
|
||||
py/test/dist/nodemanage.py
|
||||
py/test/dist/testing/__init__.py
|
||||
py/test/dist/testing/acceptance_test.py
|
||||
py/test/dist/testing/test_dsession.py
|
||||
py/test/dist/testing/test_mypickle.py
|
||||
py/test/dist/testing/test_nodemanage.py
|
||||
py/test/dist/testing/test_txnode.py
|
||||
py/test/dist/txnode.py
|
||||
py/test/funcargs.py
|
||||
py/test/looponfail/__init__.py
|
||||
py/test/looponfail/remote.py
|
||||
py/test/looponfail/testing/__init__.py
|
||||
py/test/looponfail/testing/test_remote.py
|
||||
py/test/looponfail/testing/test_util.py
|
||||
py/test/looponfail/util.py
|
||||
py/test/outcome.py
|
||||
py/test/parseopt.py
|
||||
py/test/plugin/__init__.py
|
||||
py/test/plugin/conftest.py
|
||||
py/test/plugin/hookspec.py
|
||||
py/test/plugin/pytest__pytest.py
|
||||
py/test/plugin/pytest_capture.py
|
||||
py/test/plugin/pytest_default.py
|
||||
py/test/plugin/pytest_doctest.py
|
||||
py/test/plugin/pytest_execnetcleanup.py
|
||||
py/test/plugin/pytest_figleaf.py
|
||||
py/test/plugin/pytest_hooklog.py
|
||||
py/test/plugin/pytest_keyword.py
|
||||
py/test/plugin/pytest_monkeypatch.py
|
||||
py/test/plugin/pytest_pastebin.py
|
||||
py/test/plugin/pytest_pdb.py
|
||||
py/test/plugin/pytest_pylint.py
|
||||
py/test/plugin/pytest_pytester.py
|
||||
py/test/plugin/pytest_recwarn.py
|
||||
py/test/plugin/pytest_restdoc.py
|
||||
py/test/plugin/pytest_resultlog.py
|
||||
py/test/plugin/pytest_runner.py
|
||||
py/test/plugin/pytest_terminal.py
|
||||
py/test/plugin/pytest_tmpdir.py
|
||||
py/test/plugin/pytest_unittest.py
|
||||
py/test/plugin/pytest_xfail.py
|
||||
py/test/plugin/test_pytest_capture.py
|
||||
py/test/plugin/test_pytest_runner.py
|
||||
py/test/plugin/test_pytest_runner_xunit.py
|
||||
py/test/plugin/test_pytest_terminal.py
|
||||
py/test/pluginmanager.py
|
||||
py/test/pycollect.py
|
||||
py/test/session.py
|
||||
py/test/testing/__init__.py
|
||||
py/test/testing/acceptance_test.py
|
||||
py/test/testing/conftest.py
|
||||
py/test/testing/import_test/package/__init__.py
|
||||
py/test/testing/import_test/package/absolute_import_shared_lib.py
|
||||
py/test/testing/import_test/package/module_that_imports_shared_lib.py
|
||||
py/test/testing/import_test/package/shared_lib.py
|
||||
py/test/testing/import_test/package/test_import.py
|
||||
py/test/testing/test_collect.py
|
||||
py/test/testing/test_compat.py
|
||||
py/test/testing/test_config.py
|
||||
py/test/testing/test_conftesthandle.py
|
||||
py/test/testing/test_deprecated_api.py
|
||||
py/test/testing/test_funcargs.py
|
||||
py/test/testing/test_genitems.py
|
||||
py/test/testing/test_install.py
|
||||
py/test/testing/test_outcome.py
|
||||
py/test/testing/test_parseopt.py
|
||||
py/test/testing/test_pickling.py
|
||||
py/test/testing/test_pluginmanager.py
|
||||
py/test/testing/test_pycollect.py
|
||||
py/test/testing/test_recording.py
|
||||
py/test/testing/test_session.py
|
||||
py/test/testing/test_traceback.py
|
||||
py/test/web/__init__.py
|
||||
py/test/web/exception.py
|
||||
py/test/web/post_multipart.py
|
||||
py/test/web/webcheck.py
|
||||
py/thread/__init__.py
|
||||
py/thread/io.py
|
||||
py/thread/pool.py
|
||||
py/thread/testing/__init__.py
|
||||
py/thread/testing/test_io.py
|
||||
py/thread/testing/test_pool.py
|
||||
py/tool/__init__.py
|
||||
py/tool/testing/__init__.py
|
||||
py/tool/testing/test_utestconvert.py
|
||||
py/tool/utestconvert.py
|
||||
py/xmlobj/__init__.py
|
||||
py/xmlobj/html.py
|
||||
py/xmlobj/misc.py
|
||||
py/xmlobj/testing/__init__.py
|
||||
py/xmlobj/testing/test_html.py
|
||||
py/xmlobj/testing/test_xml.py
|
||||
py/xmlobj/visit.py
|
||||
py/xmlobj/xml.py
|
||||
setup.py
|
||||
7
MANIFEST.in
Normal file
7
MANIFEST.in
Normal file
@@ -0,0 +1,7 @@
|
||||
include CHANGELOG
|
||||
include README.rst
|
||||
include setup.py
|
||||
include tox.ini
|
||||
include LICENSE
|
||||
graft doc
|
||||
graft testing
|
||||
48
README.rst
Normal file
48
README.rst
Normal file
@@ -0,0 +1,48 @@
|
||||
|
||||
Changelog: http://pytest.org/latest/changelog.html
|
||||
|
||||
The ``py.test`` testing tool makes it easy to write small tests, yet
|
||||
scales to support complex functional testing. It provides
|
||||
|
||||
- `auto-discovery
|
||||
<http://pytest.org/latest/goodpractises.html#python-test-discovery>`_
|
||||
of test modules and functions,
|
||||
- detailed info on failing `assert statements <http://pytest.org/latest/assert.html>`_ (no need to remember ``self.assert*`` names)
|
||||
- `modular fixtures <http://pytest.org/latest/fixture.html>`_ for
|
||||
managing small or parametrized long-lived test resources.
|
||||
- multi-paradigm support: you can use ``py.test`` 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.
|
||||
|
||||
- many `external plugins <http://pytest.org/latest/plugins.html#installing-external-plugins-searching>`_.
|
||||
|
||||
.. image:: https://secure.travis-ci.org/hpk42/pytest.png
|
||||
:target: http://travis-ci.org/hpk42/pytest
|
||||
|
||||
A simple example for a test::
|
||||
|
||||
# content of test_module.py
|
||||
def test_function():
|
||||
i = 4
|
||||
assert i == 3
|
||||
|
||||
which can be run with ``py.test test_module.py``. See `getting-started <http://pytest.org/latest/getting-started.html#our-first-test-run>`_ for more examples.
|
||||
|
||||
For much more info, including PDF docs, see
|
||||
|
||||
http://pytest.org
|
||||
|
||||
and report bugs at:
|
||||
|
||||
http://bitbucket.org/hpk42/pytest/issues/
|
||||
|
||||
and checkout repos at:
|
||||
|
||||
http://github.com/hpk42/pytest/ (mirror)
|
||||
http://bitbucket.org/hpk42/pytest/
|
||||
|
||||
|
||||
Copyright Holger Krekel and others, 2004-2013
|
||||
Licensed under the MIT license.
|
||||
19
README.txt
19
README.txt
@@ -1,19 +0,0 @@
|
||||
The py lib is a Python development support library featuring
|
||||
the following tools and modules:
|
||||
|
||||
* py.test: tool for distributed automated testing
|
||||
* py.execnet: ad-hoc distributed execution
|
||||
* py.code: dynamic code generation and introspection
|
||||
* py.path: uniform local and svn path objects
|
||||
|
||||
It includes code and contributions from several people,
|
||||
listed in the LICENSE file.
|
||||
|
||||
For questions, please see py/doc/index.txt, refer to the website
|
||||
http://pylib.org or come to the #pylib IRC freenode channel or subscribe to
|
||||
http://codespeak.net/mailman/listinfo/py-dev .
|
||||
|
||||
have fun,
|
||||
|
||||
holger krekel, holger at merlinux eu
|
||||
|
||||
39
_findpy.py
39
_findpy.py
@@ -1,39 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
#
|
||||
# try to find and import a nearby version of the 'py' package.
|
||||
# otherwise use the system global default
|
||||
# XXX turn this into a developer-only thing?
|
||||
#
|
||||
import sys
|
||||
import os
|
||||
from os.path import dirname as opd, exists, join, basename, abspath
|
||||
|
||||
def searchpy(current):
|
||||
while 1:
|
||||
last = current
|
||||
initpy = join(current, '__init__.py')
|
||||
if not exists(initpy):
|
||||
pydir = join(current, 'py')
|
||||
# recognize py-package and ensure it is importable
|
||||
if exists(pydir) and exists(join(pydir, '__init__.py')):
|
||||
#for p in sys.path:
|
||||
# if p == current:
|
||||
# return True
|
||||
if current != sys.path[0]: # if we are already first, then ok
|
||||
print >>sys.stderr, "inserting into sys.path:", current
|
||||
sys.path.insert(0, current)
|
||||
return True
|
||||
current = opd(current)
|
||||
if last == current:
|
||||
return False
|
||||
|
||||
if not searchpy(abspath(os.curdir)):
|
||||
if not searchpy(opd(abspath(sys.argv[0]))):
|
||||
if not searchpy(opd(__file__)):
|
||||
pass # let's hope it is just on sys.path
|
||||
|
||||
import py
|
||||
|
||||
if __name__ == '__main__':
|
||||
print "py lib is at", py.__file__
|
||||
2
_pytest/__init__.py
Normal file
2
_pytest/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
#
|
||||
__version__ = '2.4.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
|
||||
125
_pytest/assertion/__init__.py
Normal file
125
_pytest/assertion/__init__.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""
|
||||
support for presenting detailed information in failing assertions.
|
||||
"""
|
||||
import py
|
||||
import sys
|
||||
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",
|
||||
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', '--no-magic', action="store_true",
|
||||
default=False, help="DEPRECATED equivalent to --assert=plain")
|
||||
|
||||
class AssertionState:
|
||||
"""State for the assertion plugin."""
|
||||
|
||||
def __init__(self, config, mode):
|
||||
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
|
||||
except ImportError:
|
||||
mode = "reinterp"
|
||||
else:
|
||||
# Both Jython and CPython 2.6.0 have AST bugs that make the
|
||||
# assertion rewriting hook malfunction.
|
||||
if (sys.platform.startswith('java') or
|
||||
sys.version_info[:3] == (2, 6, 0)):
|
||||
mode = "reinterp"
|
||||
if mode != "plain":
|
||||
_load_modules(mode)
|
||||
m = monkeypatch()
|
||||
config._cleanup.append(m.undo)
|
||||
m.setattr(py.builtin.builtins, 'AssertionError',
|
||||
reinterpret.AssertionError)
|
||||
hook = None
|
||||
if mode == "rewrite":
|
||||
hook = rewrite.AssertionRewritingHook()
|
||||
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 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
|
||||
# so for example not in the master process of pytest-xdist
|
||||
# (which does not collect test modules)
|
||||
hook = session.config._assertstate.hook
|
||||
if hook is not None:
|
||||
hook.set_session(session)
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
def callbinrepr(op, left, right):
|
||||
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 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
|
||||
if mode == "rewrite":
|
||||
from _pytest.assertion import rewrite
|
||||
|
||||
def warn_about_missing_assertion(mode):
|
||||
try:
|
||||
assert False
|
||||
except AssertionError:
|
||||
pass
|
||||
else:
|
||||
if mode == "rewrite":
|
||||
specifically = ("assertions which are not in test modules "
|
||||
"will be ignored")
|
||||
else:
|
||||
specifically = "failing tests may report as passing"
|
||||
|
||||
sys.stderr.write("WARNING: " + specifically +
|
||||
" because assert statements are not executed "
|
||||
"by the underlying Python interpreter "
|
||||
"(are you using python -O?)\n")
|
||||
|
||||
pytest_assertrepr_compare = util.assertrepr_compare
|
||||
333
_pytest/assertion/newinterpret.py
Normal file
333
_pytest/assertion/newinterpret.py
Normal file
@@ -0,0 +1,333 @@
|
||||
"""
|
||||
Find intermediate evalutation results in assert statements through builtin AST.
|
||||
This should replace oldinterpret.py eventually.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import ast
|
||||
|
||||
import py
|
||||
from _pytest.assertion import util
|
||||
from _pytest.assertion.reinterpret import BuiltinAssertionError
|
||||
|
||||
|
||||
if sys.platform.startswith("java"):
|
||||
# See http://bugs.jython.org/issue1497
|
||||
_exprs = ("BoolOp", "BinOp", "UnaryOp", "Lambda", "IfExp", "Dict",
|
||||
"ListComp", "GeneratorExp", "Yield", "Compare", "Call",
|
||||
"Repr", "Num", "Str", "Attribute", "Subscript", "Name",
|
||||
"List", "Tuple")
|
||||
_stmts = ("FunctionDef", "ClassDef", "Return", "Delete", "Assign",
|
||||
"AugAssign", "Print", "For", "While", "If", "With", "Raise",
|
||||
"TryExcept", "TryFinally", "Assert", "Import", "ImportFrom",
|
||||
"Exec", "Global", "Expr", "Pass", "Break", "Continue")
|
||||
_expr_nodes = set(getattr(ast, name) for name in _exprs)
|
||||
_stmt_nodes = set(getattr(ast, name) for name in _stmts)
|
||||
def _is_ast_expr(node):
|
||||
return node.__class__ in _expr_nodes
|
||||
def _is_ast_stmt(node):
|
||||
return node.__class__ in _stmt_nodes
|
||||
else:
|
||||
def _is_ast_expr(node):
|
||||
return isinstance(node, ast.expr)
|
||||
def _is_ast_stmt(node):
|
||||
return isinstance(node, ast.stmt)
|
||||
|
||||
|
||||
class Failure(Exception):
|
||||
"""Error found while interpreting AST."""
|
||||
|
||||
def __init__(self, explanation=""):
|
||||
self.cause = sys.exc_info()
|
||||
self.explanation = explanation
|
||||
|
||||
|
||||
def interpret(source, frame, should_fail=False):
|
||||
mod = ast.parse(source)
|
||||
visitor = DebugInterpreter(frame)
|
||||
try:
|
||||
visitor.visit(mod)
|
||||
except Failure:
|
||||
failure = sys.exc_info()[1]
|
||||
return getfailure(failure)
|
||||
if should_fail:
|
||||
return ("(assertion failed, but when it was re-run for "
|
||||
"printing intermediate values, it did not fail. Suggestions: "
|
||||
"compute assert expression before the assert or use --assert=plain)")
|
||||
|
||||
def run(offending_line, frame=None):
|
||||
if frame is None:
|
||||
frame = py.code.Frame(sys._getframe(1))
|
||||
return interpret(offending_line, frame)
|
||||
|
||||
def getfailure(e):
|
||||
explanation = util.format_explanation(e.explanation)
|
||||
value = e.cause[1]
|
||||
if str(value):
|
||||
lines = explanation.split('\n')
|
||||
lines[0] += " << %s" % (value,)
|
||||
explanation = '\n'.join(lines)
|
||||
text = "%s: %s" % (e.cause[0].__name__, explanation)
|
||||
if text.startswith('AssertionError: assert '):
|
||||
text = text[16:]
|
||||
return text
|
||||
|
||||
operator_map = {
|
||||
ast.BitOr : "|",
|
||||
ast.BitXor : "^",
|
||||
ast.BitAnd : "&",
|
||||
ast.LShift : "<<",
|
||||
ast.RShift : ">>",
|
||||
ast.Add : "+",
|
||||
ast.Sub : "-",
|
||||
ast.Mult : "*",
|
||||
ast.Div : "/",
|
||||
ast.FloorDiv : "//",
|
||||
ast.Mod : "%",
|
||||
ast.Eq : "==",
|
||||
ast.NotEq : "!=",
|
||||
ast.Lt : "<",
|
||||
ast.LtE : "<=",
|
||||
ast.Gt : ">",
|
||||
ast.GtE : ">=",
|
||||
ast.Pow : "**",
|
||||
ast.Is : "is",
|
||||
ast.IsNot : "is not",
|
||||
ast.In : "in",
|
||||
ast.NotIn : "not in"
|
||||
}
|
||||
|
||||
unary_map = {
|
||||
ast.Not : "not %s",
|
||||
ast.Invert : "~%s",
|
||||
ast.USub : "-%s",
|
||||
ast.UAdd : "+%s"
|
||||
}
|
||||
|
||||
|
||||
class DebugInterpreter(ast.NodeVisitor):
|
||||
"""Interpret AST nodes to gleam useful debugging information. """
|
||||
|
||||
def __init__(self, frame):
|
||||
self.frame = frame
|
||||
|
||||
def generic_visit(self, node):
|
||||
# Fallback when we don't have a special implementation.
|
||||
if _is_ast_expr(node):
|
||||
mod = ast.Expression(node)
|
||||
co = self._compile(mod)
|
||||
try:
|
||||
result = self.frame.eval(co)
|
||||
except Exception:
|
||||
raise Failure()
|
||||
explanation = self.frame.repr(result)
|
||||
return explanation, result
|
||||
elif _is_ast_stmt(node):
|
||||
mod = ast.Module([node])
|
||||
co = self._compile(mod, "exec")
|
||||
try:
|
||||
self.frame.exec_(co)
|
||||
except Exception:
|
||||
raise Failure()
|
||||
return None, None
|
||||
else:
|
||||
raise AssertionError("can't handle %s" %(node,))
|
||||
|
||||
def _compile(self, source, mode="eval"):
|
||||
return compile(source, "<assertion interpretation>", mode)
|
||||
|
||||
def visit_Expr(self, expr):
|
||||
return self.visit(expr.value)
|
||||
|
||||
def visit_Module(self, mod):
|
||||
for stmt in mod.body:
|
||||
self.visit(stmt)
|
||||
|
||||
def visit_Name(self, name):
|
||||
explanation, result = self.generic_visit(name)
|
||||
# See if the name is local.
|
||||
source = "%r in locals() is not globals()" % (name.id,)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
local = self.frame.eval(co)
|
||||
except Exception:
|
||||
# have to assume it isn't
|
||||
local = None
|
||||
if local is None or not self.frame.is_true(local):
|
||||
return name.id, result
|
||||
return explanation, result
|
||||
|
||||
def visit_Compare(self, comp):
|
||||
left = comp.left
|
||||
left_explanation, left_result = self.visit(left)
|
||||
for op, next_op in zip(comp.ops, comp.comparators):
|
||||
next_explanation, next_result = self.visit(next_op)
|
||||
op_symbol = operator_map[op.__class__]
|
||||
explanation = "%s %s %s" % (left_explanation, op_symbol,
|
||||
next_explanation)
|
||||
source = "__exprinfo_left %s __exprinfo_right" % (op_symbol,)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
result = self.frame.eval(co, __exprinfo_left=left_result,
|
||||
__exprinfo_right=next_result)
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
try:
|
||||
if not self.frame.is_true(result):
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
break
|
||||
left_explanation, left_result = next_explanation, next_result
|
||||
|
||||
if util._reprcompare is not None:
|
||||
res = util._reprcompare(op_symbol, left_result, next_result)
|
||||
if res:
|
||||
explanation = res
|
||||
return explanation, result
|
||||
|
||||
def visit_BoolOp(self, boolop):
|
||||
is_or = isinstance(boolop.op, ast.Or)
|
||||
explanations = []
|
||||
for operand in boolop.values:
|
||||
explanation, result = self.visit(operand)
|
||||
explanations.append(explanation)
|
||||
if result == is_or:
|
||||
break
|
||||
name = is_or and " or " or " and "
|
||||
explanation = "(" + name.join(explanations) + ")"
|
||||
return explanation, result
|
||||
|
||||
def visit_UnaryOp(self, unary):
|
||||
pattern = unary_map[unary.op.__class__]
|
||||
operand_explanation, operand_result = self.visit(unary.operand)
|
||||
explanation = pattern % (operand_explanation,)
|
||||
co = self._compile(pattern % ("__exprinfo_expr",))
|
||||
try:
|
||||
result = self.frame.eval(co, __exprinfo_expr=operand_result)
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
return explanation, result
|
||||
|
||||
def visit_BinOp(self, binop):
|
||||
left_explanation, left_result = self.visit(binop.left)
|
||||
right_explanation, right_result = self.visit(binop.right)
|
||||
symbol = operator_map[binop.op.__class__]
|
||||
explanation = "(%s %s %s)" % (left_explanation, symbol,
|
||||
right_explanation)
|
||||
source = "__exprinfo_left %s __exprinfo_right" % (symbol,)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
result = self.frame.eval(co, __exprinfo_left=left_result,
|
||||
__exprinfo_right=right_result)
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
return explanation, result
|
||||
|
||||
def visit_Call(self, call):
|
||||
func_explanation, func = self.visit(call.func)
|
||||
arg_explanations = []
|
||||
ns = {"__exprinfo_func" : func}
|
||||
arguments = []
|
||||
for arg in call.args:
|
||||
arg_explanation, arg_result = self.visit(arg)
|
||||
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),)
|
||||
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:
|
||||
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:
|
||||
arg_explanation, arg_result = self.visit(call.kwargs)
|
||||
arg_name = "__exprinfo_kwds"
|
||||
ns[arg_name] = arg_result
|
||||
arguments.append("**%s" % (arg_name,))
|
||||
arg_explanations.append("**%s" % (arg_explanation,))
|
||||
args_explained = ", ".join(arg_explanations)
|
||||
explanation = "%s(%s)" % (func_explanation, args_explained)
|
||||
args = ", ".join(arguments)
|
||||
source = "__exprinfo_func(%s)" % (args,)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
result = self.frame.eval(co, **ns)
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
pattern = "%s\n{%s = %s\n}"
|
||||
rep = self.frame.repr(result)
|
||||
explanation = pattern % (rep, rep, explanation)
|
||||
return explanation, result
|
||||
|
||||
def _is_builtin_name(self, name):
|
||||
pattern = "%r not in globals() and %r not in locals()"
|
||||
source = pattern % (name.id, name.id)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
return self.frame.eval(co)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def visit_Attribute(self, attr):
|
||||
if not isinstance(attr.ctx, ast.Load):
|
||||
return self.generic_visit(attr)
|
||||
source_explanation, source_result = self.visit(attr.value)
|
||||
explanation = "%s.%s" % (source_explanation, attr.attr)
|
||||
source = "__exprinfo_expr.%s" % (attr.attr,)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
result = self.frame.eval(co, __exprinfo_expr=source_result)
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
explanation = "%s\n{%s = %s.%s\n}" % (self.frame.repr(result),
|
||||
self.frame.repr(result),
|
||||
source_explanation, attr.attr)
|
||||
# Check if the attr is from an instance.
|
||||
source = "%r in getattr(__exprinfo_expr, '__dict__', {})"
|
||||
source = source % (attr.attr,)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
from_instance = self.frame.eval(co, __exprinfo_expr=source_result)
|
||||
except Exception:
|
||||
from_instance = None
|
||||
if from_instance is None or self.frame.is_true(from_instance):
|
||||
rep = self.frame.repr(result)
|
||||
pattern = "%s\n{%s = %s\n}"
|
||||
explanation = pattern % (rep, rep, explanation)
|
||||
return explanation, result
|
||||
|
||||
def visit_Assert(self, assrt):
|
||||
test_explanation, test_result = self.visit(assrt.test)
|
||||
explanation = "assert %s" % (test_explanation,)
|
||||
if not self.frame.is_true(test_result):
|
||||
try:
|
||||
raise BuiltinAssertionError
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
return explanation, test_result
|
||||
|
||||
def visit_Assign(self, assign):
|
||||
value_explanation, value_result = self.visit(assign.value)
|
||||
explanation = "... = %s" % (value_explanation,)
|
||||
name = ast.Name("__exprinfo_expr", ast.Load(),
|
||||
lineno=assign.value.lineno,
|
||||
col_offset=assign.value.col_offset)
|
||||
new_assign = ast.Assign(assign.targets, name, lineno=assign.lineno,
|
||||
col_offset=assign.col_offset)
|
||||
mod = ast.Module([new_assign])
|
||||
co = self._compile(mod, "exec")
|
||||
try:
|
||||
self.frame.exec_(co, __exprinfo_expr=value_result)
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
return explanation, value_result
|
||||
@@ -1,17 +1,100 @@
|
||||
from compiler import parse, ast, pycodegen
|
||||
import py
|
||||
import __builtin__, sys
|
||||
import sys, inspect
|
||||
from compiler import parse, ast, pycodegen
|
||||
from _pytest.assertion.util import format_explanation, BuiltinAssertionError
|
||||
|
||||
passthroughex = (KeyboardInterrupt, SystemExit, MemoryError)
|
||||
passthroughex = py.builtin._sysex
|
||||
|
||||
class Failure:
|
||||
def __init__(self, node):
|
||||
self.exc, self.value, self.tb = sys.exc_info()
|
||||
self.node = node
|
||||
#import traceback
|
||||
#traceback.print_exc()
|
||||
|
||||
from py.__.magic.viewtype import View
|
||||
class View(object):
|
||||
"""View base class.
|
||||
|
||||
If C is a subclass of View, then C(x) creates a proxy object around
|
||||
the object x. The actual class of the proxy is not C in general,
|
||||
but a *subclass* of C determined by the rules below. To avoid confusion
|
||||
we call view class the class of the proxy (a subclass of C, so of View)
|
||||
and object class the class of x.
|
||||
|
||||
Attributes and methods not found in the proxy are automatically read on x.
|
||||
Other operations like setting attributes are performed on the proxy, as
|
||||
determined by its view class. The object x is available from the proxy
|
||||
as its __obj__ attribute.
|
||||
|
||||
The view class selection is determined by the __view__ tuples and the
|
||||
optional __viewkey__ method. By default, the selected view class is the
|
||||
most specific subclass of C whose __view__ mentions the class of x.
|
||||
If no such subclass is found, the search proceeds with the parent
|
||||
object classes. For example, C(True) will first look for a subclass
|
||||
of C with __view__ = (..., bool, ...) and only if it doesn't find any
|
||||
look for one with __view__ = (..., int, ...), and then ..., object,...
|
||||
If everything fails the class C itself is considered to be the default.
|
||||
|
||||
Alternatively, the view class selection can be driven by another aspect
|
||||
of the object x, instead of the class of x, by overriding __viewkey__.
|
||||
See last example at the end of this module.
|
||||
"""
|
||||
|
||||
_viewcache = {}
|
||||
__view__ = ()
|
||||
|
||||
def __new__(rootclass, obj, *args, **kwds):
|
||||
self = object.__new__(rootclass)
|
||||
self.__obj__ = obj
|
||||
self.__rootclass__ = rootclass
|
||||
key = self.__viewkey__()
|
||||
try:
|
||||
self.__class__ = self._viewcache[key]
|
||||
except KeyError:
|
||||
self.__class__ = self._selectsubclass(key)
|
||||
return self
|
||||
|
||||
def __getattr__(self, attr):
|
||||
# attributes not found in the normal hierarchy rooted on View
|
||||
# are looked up in the object's real class
|
||||
return getattr(self.__obj__, attr)
|
||||
|
||||
def __viewkey__(self):
|
||||
return self.__obj__.__class__
|
||||
|
||||
def __matchkey__(self, key, subclasses):
|
||||
if inspect.isclass(key):
|
||||
keys = inspect.getmro(key)
|
||||
else:
|
||||
keys = [key]
|
||||
for key in keys:
|
||||
result = [C for C in subclasses if key in C.__view__]
|
||||
if result:
|
||||
return result
|
||||
return []
|
||||
|
||||
def _selectsubclass(self, key):
|
||||
subclasses = list(enumsubclasses(self.__rootclass__))
|
||||
for C in subclasses:
|
||||
if not isinstance(C.__view__, tuple):
|
||||
C.__view__ = (C.__view__,)
|
||||
choices = self.__matchkey__(key, subclasses)
|
||||
if not choices:
|
||||
return self.__rootclass__
|
||||
elif len(choices) == 1:
|
||||
return choices[0]
|
||||
else:
|
||||
# combine the multiple choices
|
||||
return type('?', tuple(choices), {})
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%r)' % (self.__rootclass__.__name__, self.__obj__)
|
||||
|
||||
|
||||
def enumsubclasses(cls):
|
||||
for subcls in cls.__subclasses__():
|
||||
for subsubclass in enumsubclasses(subcls):
|
||||
yield subsubclass
|
||||
yield cls
|
||||
|
||||
|
||||
class Interpretable(View):
|
||||
"""A parse tree node with a few extra methods."""
|
||||
@@ -48,63 +131,35 @@ class Interpretable(View):
|
||||
raise Failure(self)
|
||||
|
||||
def nice_explanation(self):
|
||||
# uck! See CallFunc for where \n{ and \n} escape sequences are used
|
||||
raw_lines = (self.explanation or '').split('\n')
|
||||
# escape newlines not followed by { and }
|
||||
lines = [raw_lines[0]]
|
||||
for l in raw_lines[1:]:
|
||||
if l.startswith('{') or l.startswith('}'):
|
||||
lines.append(l)
|
||||
else:
|
||||
lines[-1] += '\\n' + l
|
||||
|
||||
result = lines[:1]
|
||||
stack = [0]
|
||||
stackcnt = [0]
|
||||
for line in lines[1:]:
|
||||
if line.startswith('{'):
|
||||
if stackcnt[-1]:
|
||||
s = 'and '
|
||||
else:
|
||||
s = 'where '
|
||||
stack.append(len(result))
|
||||
stackcnt[-1] += 1
|
||||
stackcnt.append(0)
|
||||
result.append(' +' + ' '*(len(stack)-1) + s + line[1:])
|
||||
else:
|
||||
assert line.startswith('}')
|
||||
stack.pop()
|
||||
stackcnt.pop()
|
||||
result[stack[-1]] += line[1:]
|
||||
assert len(stack) == 1
|
||||
return '\n'.join(result)
|
||||
return format_explanation(self.explanation)
|
||||
|
||||
|
||||
class Name(Interpretable):
|
||||
__view__ = ast.Name
|
||||
|
||||
def is_local(self, frame):
|
||||
co = compile('%r in locals() is not globals()' % self.name, '?', 'eval')
|
||||
source = '%r in locals() is not globals()' % self.name
|
||||
try:
|
||||
return frame.is_true(frame.eval(co))
|
||||
return frame.is_true(frame.eval(source))
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
return False
|
||||
|
||||
def is_global(self, frame):
|
||||
co = compile('%r in globals()' % self.name, '?', 'eval')
|
||||
source = '%r in globals()' % self.name
|
||||
try:
|
||||
return frame.is_true(frame.eval(co))
|
||||
return frame.is_true(frame.eval(source))
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
return False
|
||||
|
||||
def is_builtin(self, frame):
|
||||
co = compile('%r not in locals() and %r not in globals()' % (
|
||||
self.name, self.name), '?', 'eval')
|
||||
source = '%r not in locals() and %r not in globals()' % (
|
||||
self.name, self.name)
|
||||
try:
|
||||
return frame.is_true(frame.eval(co))
|
||||
return frame.is_true(frame.eval(source))
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
@@ -130,11 +185,11 @@ class Compare(Interpretable):
|
||||
expr2.eval(frame)
|
||||
self.explanation = "%s %s %s" % (
|
||||
expr.explanation, operation, expr2.explanation)
|
||||
co = compile("__exprinfo_left %s __exprinfo_right" % operation,
|
||||
'?', 'eval')
|
||||
source = "__exprinfo_left %s __exprinfo_right" % operation
|
||||
try:
|
||||
self.result = frame.eval(co, __exprinfo_left=expr.result,
|
||||
__exprinfo_right=expr2.result)
|
||||
self.result = frame.eval(source,
|
||||
__exprinfo_left=expr.result,
|
||||
__exprinfo_right=expr2.result)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
@@ -180,14 +235,14 @@ for astclass, astpattern in {
|
||||
class UnaryArith(Interpretable):
|
||||
__view__ = astclass
|
||||
|
||||
def eval(self, frame, astpattern=astpattern,
|
||||
co=compile(astpattern, '?', 'eval')):
|
||||
def eval(self, frame, astpattern=astpattern):
|
||||
expr = Interpretable(self.expr)
|
||||
expr.eval(frame)
|
||||
self.explanation = astpattern.replace('__exprinfo_expr',
|
||||
expr.explanation)
|
||||
try:
|
||||
self.result = frame.eval(co, __exprinfo_expr=expr.result)
|
||||
self.result = frame.eval(astpattern,
|
||||
__exprinfo_expr=expr.result)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
@@ -208,8 +263,7 @@ for astclass, astpattern in {
|
||||
class BinaryArith(Interpretable):
|
||||
__view__ = astclass
|
||||
|
||||
def eval(self, frame, astpattern=astpattern,
|
||||
co=compile(astpattern, '?', 'eval')):
|
||||
def eval(self, frame, astpattern=astpattern):
|
||||
left = Interpretable(self.left)
|
||||
left.eval(frame)
|
||||
right = Interpretable(self.right)
|
||||
@@ -218,8 +272,9 @@ for astclass, astpattern in {
|
||||
.replace('__exprinfo_left', left .explanation)
|
||||
.replace('__exprinfo_right', right.explanation))
|
||||
try:
|
||||
self.result = frame.eval(co, __exprinfo_left=left.result,
|
||||
__exprinfo_right=right.result)
|
||||
self.result = frame.eval(astpattern,
|
||||
__exprinfo_left=left.result,
|
||||
__exprinfo_right=right.result)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
@@ -232,9 +287,10 @@ class CallFunc(Interpretable):
|
||||
__view__ = ast.CallFunc
|
||||
|
||||
def is_bool(self, frame):
|
||||
co = compile('isinstance(__exprinfo_value, bool)', '?', 'eval')
|
||||
source = 'isinstance(__exprinfo_value, bool)'
|
||||
try:
|
||||
return frame.is_true(frame.eval(co, __exprinfo_value=self.result))
|
||||
return frame.is_true(frame.eval(source,
|
||||
__exprinfo_value=self.result))
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
@@ -281,9 +337,8 @@ class CallFunc(Interpretable):
|
||||
if source.endswith(','):
|
||||
source = source[:-1]
|
||||
source += ')'
|
||||
co = compile(source, '?', 'eval')
|
||||
try:
|
||||
self.result = frame.eval(co, **vars)
|
||||
self.result = frame.eval(source, **vars)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
@@ -298,21 +353,20 @@ class Getattr(Interpretable):
|
||||
def eval(self, frame):
|
||||
expr = Interpretable(self.expr)
|
||||
expr.eval(frame)
|
||||
co = compile('__exprinfo_expr.%s' % self.attrname, '?', 'eval')
|
||||
source = '__exprinfo_expr.%s' % self.attrname
|
||||
try:
|
||||
self.result = frame.eval(co, __exprinfo_expr=expr.result)
|
||||
self.result = frame.eval(source, __exprinfo_expr=expr.result)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
self.explanation = '%s.%s' % (expr.explanation, self.attrname)
|
||||
# if the attribute comes from the instance, its value is interesting
|
||||
co = compile('hasattr(__exprinfo_expr, "__dict__") and '
|
||||
'%r in __exprinfo_expr.__dict__' % self.attrname,
|
||||
'?', 'eval')
|
||||
source = ('hasattr(__exprinfo_expr, "__dict__") and '
|
||||
'%r in __exprinfo_expr.__dict__' % self.attrname)
|
||||
try:
|
||||
from_instance = frame.is_true(
|
||||
frame.eval(co, __exprinfo_expr=expr.result))
|
||||
frame.eval(source, __exprinfo_expr=expr.result))
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
@@ -322,8 +376,6 @@ class Getattr(Interpretable):
|
||||
self.explanation = '%s\n{%s = %s\n}' % (r, r, self.explanation)
|
||||
|
||||
# == Re-interpretation of full statements ==
|
||||
import __builtin__
|
||||
BuiltinAssertionError = __builtin__.AssertionError
|
||||
|
||||
class Assert(Interpretable):
|
||||
__view__ = ast.Assert
|
||||
@@ -331,10 +383,6 @@ class Assert(Interpretable):
|
||||
def run(self, frame):
|
||||
test = Interpretable(self.test)
|
||||
test.eval(frame)
|
||||
# simplify 'assert False where False = ...'
|
||||
if (test.explanation.startswith('False\n{False = ') and
|
||||
test.explanation.endswith('\n}')):
|
||||
test.explanation = test.explanation[15:-2]
|
||||
# print the result as 'assert <explanation>'
|
||||
self.result = test.result
|
||||
self.explanation = 'assert ' + test.explanation
|
||||
@@ -390,11 +438,10 @@ def report_failure(e):
|
||||
explanation = ", in: " + explanation
|
||||
else:
|
||||
explanation = ""
|
||||
print "%s: %s%s" % (e.exc.__name__, e.value, explanation)
|
||||
sys.stdout.write("%s: %s%s\n" % (e.exc.__name__, e.value, explanation))
|
||||
|
||||
def check(s, frame=None):
|
||||
if frame is None:
|
||||
import sys
|
||||
frame = sys._getframe(1)
|
||||
frame = py.code.Frame(frame)
|
||||
expr = parse(s, 'eval')
|
||||
@@ -404,11 +451,12 @@ def check(s, frame=None):
|
||||
node.eval(frame)
|
||||
except passthroughex:
|
||||
raise
|
||||
except Failure, e:
|
||||
except Failure:
|
||||
e = sys.exc_info()[1]
|
||||
report_failure(e)
|
||||
else:
|
||||
if not frame.is_true(node.result):
|
||||
print "assertion failed:", node.nice_explanation()
|
||||
sys.stderr.write("assertion failed: %s\n" % node.nice_explanation())
|
||||
|
||||
|
||||
###########################################################
|
||||
@@ -422,7 +470,8 @@ def interpret(source, frame, should_fail=False):
|
||||
frame = py.code.Frame(frame)
|
||||
try:
|
||||
module.run(frame)
|
||||
except Failure, e:
|
||||
except Failure:
|
||||
e = sys.exc_info()[1]
|
||||
return getfailure(e)
|
||||
except passthroughex:
|
||||
raise
|
||||
@@ -432,7 +481,7 @@ def interpret(source, frame, should_fail=False):
|
||||
if should_fail:
|
||||
return ("(assertion failed, but when it was re-run for "
|
||||
"printing intermediate values, it did not fail. Suggestions: "
|
||||
"compute assert expression before the assert or use --nomagic)")
|
||||
"compute assert expression before the assert or use --assert=plain)")
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -443,11 +492,11 @@ def getmsg(excinfo):
|
||||
#frame = py.code.Frame(frame)
|
||||
#return interpret(line, frame)
|
||||
|
||||
tb = excinfo.traceback[-1]
|
||||
tb = excinfo.traceback[-1]
|
||||
source = str(tb.statement).strip()
|
||||
x = interpret(source, tb.frame, should_fail=True)
|
||||
if not isinstance(x, str):
|
||||
raise TypeError, "interpret returned non-string %r" % (x,)
|
||||
raise TypeError("interpret returned non-string %r" % (x,))
|
||||
return x
|
||||
|
||||
def getfailure(e):
|
||||
@@ -463,13 +512,13 @@ def getfailure(e):
|
||||
|
||||
def run(s, frame=None):
|
||||
if frame is None:
|
||||
import sys
|
||||
frame = sys._getframe(1)
|
||||
frame = py.code.Frame(frame)
|
||||
module = Interpretable(parse(s, 'exec').node)
|
||||
try:
|
||||
module.run(frame)
|
||||
except Failure, e:
|
||||
except Failure:
|
||||
e = sys.exc_info()[1]
|
||||
report_failure(e)
|
||||
|
||||
|
||||
@@ -477,10 +526,13 @@ if __name__ == '__main__':
|
||||
# example:
|
||||
def f():
|
||||
return 5
|
||||
|
||||
def g():
|
||||
return 3
|
||||
|
||||
def h(x):
|
||||
return 'never'
|
||||
|
||||
check("f() * g() == 5")
|
||||
check("not f()")
|
||||
check("not (f() and g() or 0)")
|
||||
46
_pytest/assertion/reinterpret.py
Normal file
46
_pytest/assertion/reinterpret.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import sys
|
||||
import py
|
||||
from _pytest.assertion.util import BuiltinAssertionError
|
||||
|
||||
class AssertionError(BuiltinAssertionError):
|
||||
def __init__(self, *args):
|
||||
BuiltinAssertionError.__init__(self, *args)
|
||||
if args:
|
||||
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]))
|
||||
else:
|
||||
f = py.code.Frame(sys._getframe(1))
|
||||
try:
|
||||
source = f.code.fullsource
|
||||
if source is not None:
|
||||
try:
|
||||
source = source.getstatement(f.lineno, assertion=True)
|
||||
except IndexError:
|
||||
source = None
|
||||
else:
|
||||
source = str(source.deindent()).strip()
|
||||
except py.error.ENOENT:
|
||||
source = None
|
||||
# this can also occur during reinterpretation, when the
|
||||
# co_filename is set to "<run>".
|
||||
if source:
|
||||
self.msg = reinterpret(source, f, should_fail=True)
|
||||
else:
|
||||
self.msg = "<could not determine information>"
|
||||
if not self.args:
|
||||
self.args = (self.msg,)
|
||||
|
||||
if sys.version_info > (3, 0):
|
||||
AssertionError.__module__ = "builtins"
|
||||
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")):
|
||||
from _pytest.assertion.newinterpret import interpret as reinterpret
|
||||
else:
|
||||
reinterpret = reinterpret_old
|
||||
644
_pytest/assertion/rewrite.py
Normal file
644
_pytest/assertion/rewrite.py
Normal file
@@ -0,0 +1,644 @@
|
||||
"""Rewrite assertion AST to produce nice error messages"""
|
||||
|
||||
import ast
|
||||
import errno
|
||||
import itertools
|
||||
import imp
|
||||
import marshal
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
import types
|
||||
|
||||
import py
|
||||
from _pytest.assertion import util
|
||||
|
||||
|
||||
# py.test caches rewritten pycs in __pycache__.
|
||||
if hasattr(imp, "get_tag"):
|
||||
PYTEST_TAG = imp.get_tag() + "-PYTEST"
|
||||
else:
|
||||
if hasattr(sys, "pypy_version_info"):
|
||||
impl = "pypy"
|
||||
elif sys.platform == "java":
|
||||
impl = "jython"
|
||||
else:
|
||||
impl = "cpython"
|
||||
ver = sys.version_info
|
||||
PYTEST_TAG = "%s-%s%s-PYTEST" % (impl, ver[0], ver[1])
|
||||
del ver, impl
|
||||
|
||||
PYC_EXT = ".py" + (__debug__ and "c" or "o")
|
||||
PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
|
||||
|
||||
REWRITE_NEWLINES = sys.version_info[:2] != (2, 7) and sys.version_info < (3, 2)
|
||||
ASCII_IS_DEFAULT_ENCODING = sys.version_info[0] < 3
|
||||
|
||||
class AssertionRewritingHook(object):
|
||||
"""PEP302 Import hook which rewrites asserts."""
|
||||
|
||||
def __init__(self):
|
||||
self.session = None
|
||||
self.modules = {}
|
||||
|
||||
def set_session(self, session):
|
||||
self.fnpats = session.config.getini("python_files")
|
||||
self.session = session
|
||||
|
||||
def find_module(self, name, path=None):
|
||||
if self.session is None:
|
||||
return None
|
||||
sess = self.session
|
||||
state = sess.config._assertstate
|
||||
state.trace("find_module called for: %s" % name)
|
||||
names = name.rsplit(".", 1)
|
||||
lastname = names[-1]
|
||||
pth = None
|
||||
if path is not None and len(path) == 1:
|
||||
pth = path[0]
|
||||
if pth is None:
|
||||
try:
|
||||
fd, fn, desc = imp.find_module(lastname, path)
|
||||
except ImportError:
|
||||
return None
|
||||
if fd is not None:
|
||||
fd.close()
|
||||
tp = desc[2]
|
||||
if tp == imp.PY_COMPILED:
|
||||
if hasattr(imp, "source_from_cache"):
|
||||
fn = imp.source_from_cache(fn)
|
||||
else:
|
||||
fn = fn[:-1]
|
||||
elif tp != imp.PY_SOURCE:
|
||||
# Don't know what this is.
|
||||
return None
|
||||
else:
|
||||
fn = os.path.join(pth, name.rpartition(".")[2] + ".py")
|
||||
fn_pypath = py.path.local(fn)
|
||||
# Is this a test file?
|
||||
if not sess.isinitpath(fn):
|
||||
# We have to be very careful here because imports in this code can
|
||||
# trigger a cycle.
|
||||
self.session = None
|
||||
try:
|
||||
for pat in self.fnpats:
|
||||
if fn_pypath.fnmatch(pat):
|
||||
state.trace("matched test file %r" % (fn,))
|
||||
break
|
||||
else:
|
||||
return None
|
||||
finally:
|
||||
self.session = sess
|
||||
else:
|
||||
state.trace("matched test file (was specified on cmdline): %r" %
|
||||
(fn,))
|
||||
# The requested module looks like a test file, so rewrite it. This is
|
||||
# the most magical part of the process: load the source, rewrite the
|
||||
# asserts, and load the rewritten source. We also cache the rewritten
|
||||
# module code in a special pyc. We must be aware of the possibility of
|
||||
# concurrent py.test 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.
|
||||
write = not sys.dont_write_bytecode
|
||||
cache_dir = os.path.join(fn_pypath.dirname, "__pycache__")
|
||||
if write:
|
||||
try:
|
||||
os.mkdir(cache_dir)
|
||||
except OSError:
|
||||
e = sys.exc_info()[1].errno
|
||||
if e == errno.EEXIST:
|
||||
# Either the __pycache__ directory already exists (the
|
||||
# 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 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:
|
||||
state.trace("read only directory: %r" % fn_pypath.dirname)
|
||||
write = False
|
||||
else:
|
||||
raise
|
||||
cache_name = fn_pypath.basename[:-3] + PYC_TAIL
|
||||
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)
|
||||
if co is None:
|
||||
state.trace("rewriting %r" % (fn,))
|
||||
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)
|
||||
else:
|
||||
state.trace("found cached rewritten pyc for %r" % (fn,))
|
||||
self.modules[name] = co, pyc
|
||||
return self
|
||||
|
||||
def load_module(self, name):
|
||||
co, pyc = self.modules.pop(name)
|
||||
# I wish I could just call imp.load_compiled here, but __file__ has to
|
||||
# be set properly. In Python 3.2+, this all would be handled correctly
|
||||
# by load_compiled.
|
||||
mod = sys.modules[name] = imp.new_module(name)
|
||||
try:
|
||||
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 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
|
||||
|
||||
def _write_pyc(state, co, source_path, 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
|
||||
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))
|
||||
marshal.dump(co, fp)
|
||||
finally:
|
||||
fp.close()
|
||||
return True
|
||||
|
||||
RN = "\r\n".encode("utf-8")
|
||||
N = "\n".encode("utf-8")
|
||||
|
||||
cookie_re = re.compile("coding[:=]\s*[-\w.]+")
|
||||
BOM_UTF8 = '\xef\xbb\xbf'
|
||||
|
||||
def _rewrite_test(state, fn):
|
||||
"""Try to read and rewrite *fn* and return the code object."""
|
||||
try:
|
||||
source = fn.read("rb")
|
||||
except EnvironmentError:
|
||||
return 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
|
||||
# outside the ASCII range. Sadly, this behavior does not extend to
|
||||
# compile() or ast.parse(), which prefer to interpret the bytes as
|
||||
# latin-1. (At least they properly handle explicit coding cookies.) To
|
||||
# preserve this error behavior, we could force ast.parse() to use ASCII
|
||||
# as the encoding by inserting a coding cookie. Unfortunately, that
|
||||
# messes up line numbers. Thus, we have to check ourselves if anything
|
||||
# is outside the ASCII range in the case no encoding is explicitly
|
||||
# declared. For more context, see issue #269. Yay for Python 3 which
|
||||
# gets this right.
|
||||
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]))):
|
||||
if hasattr(state, "_indecode"):
|
||||
return None # encodings imported us again, we don't rewrite
|
||||
state._indecode = True
|
||||
try:
|
||||
try:
|
||||
source.decode("ascii")
|
||||
except UnicodeDecodeError:
|
||||
# Let it fail in real import.
|
||||
return None
|
||||
finally:
|
||||
del state._indecode
|
||||
# On Python versions which are not 2.7 and less than or equal to 3.1, the
|
||||
# parser expects *nix newlines.
|
||||
if REWRITE_NEWLINES:
|
||||
source = source.replace(RN, N) + N
|
||||
try:
|
||||
tree = ast.parse(source)
|
||||
except SyntaxError:
|
||||
# Let this pop up again in the real import.
|
||||
state.trace("failed to parse: %r" % (fn,))
|
||||
return None
|
||||
rewrite_asserts(tree)
|
||||
try:
|
||||
co = compile(tree, fn.strpath, "exec")
|
||||
except SyntaxError:
|
||||
# It's possible that this error is from some bug in the
|
||||
# assertion rewriting, but I don't know of a fast way to tell.
|
||||
state.trace("failed to compile: %r" % (fn,))
|
||||
return None
|
||||
return co
|
||||
|
||||
def _make_rewritten_pyc(state, fn, 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(state, co, fn, 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(state, co, fn, proc_pyc):
|
||||
os.rename(proc_pyc, pyc)
|
||||
|
||||
def _read_pyc(source, pyc):
|
||||
"""Possibly read a py.test pyc containing rewritten code.
|
||||
|
||||
Return rewritten code if successful or None if not.
|
||||
"""
|
||||
try:
|
||||
fp = open(pyc, "rb")
|
||||
except IOError:
|
||||
return None
|
||||
try:
|
||||
try:
|
||||
mtime = int(source.mtime())
|
||||
data = fp.read(8)
|
||||
except EnvironmentError:
|
||||
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):
|
||||
return None
|
||||
co = marshal.load(fp)
|
||||
if not isinstance(co, types.CodeType):
|
||||
# That's interesting....
|
||||
return None
|
||||
return co
|
||||
finally:
|
||||
fp.close()
|
||||
|
||||
|
||||
def rewrite_asserts(mod):
|
||||
"""Rewrite the assert statements in mod."""
|
||||
AssertionRewriter().run(mod)
|
||||
|
||||
|
||||
_saferepr = py.io.saferepr
|
||||
from _pytest.assertion.util import format_explanation as _format_explanation
|
||||
|
||||
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) + ")"
|
||||
|
||||
def _call_reprcompare(ops, results, expls, each_obj):
|
||||
for i, res, expl in zip(range(len(ops)), results, expls):
|
||||
try:
|
||||
done = not res
|
||||
except Exception:
|
||||
done = True
|
||||
if done:
|
||||
break
|
||||
if util._reprcompare is not None:
|
||||
custom = util._reprcompare(ops[i], each_obj[i], each_obj[i + 1])
|
||||
if custom is not None:
|
||||
return custom
|
||||
return expl
|
||||
|
||||
|
||||
unary_map = {
|
||||
ast.Not: "not %s",
|
||||
ast.Invert: "~%s",
|
||||
ast.USub: "-%s",
|
||||
ast.UAdd: "+%s"
|
||||
}
|
||||
|
||||
binop_map = {
|
||||
ast.BitOr: "|",
|
||||
ast.BitXor: "^",
|
||||
ast.BitAnd: "&",
|
||||
ast.LShift: "<<",
|
||||
ast.RShift: ">>",
|
||||
ast.Add: "+",
|
||||
ast.Sub: "-",
|
||||
ast.Mult: "*",
|
||||
ast.Div: "/",
|
||||
ast.FloorDiv: "//",
|
||||
ast.Mod: "%%", # escaped for string formatting
|
||||
ast.Eq: "==",
|
||||
ast.NotEq: "!=",
|
||||
ast.Lt: "<",
|
||||
ast.LtE: "<=",
|
||||
ast.Gt: ">",
|
||||
ast.GtE: ">=",
|
||||
ast.Pow: "**",
|
||||
ast.Is: "is",
|
||||
ast.IsNot: "is not",
|
||||
ast.In: "in",
|
||||
ast.NotIn: "not in"
|
||||
}
|
||||
|
||||
|
||||
def set_location(node, lineno, col_offset):
|
||||
"""Set node location information recursively."""
|
||||
def _fix(node, lineno, col_offset):
|
||||
if "lineno" in node._attributes:
|
||||
node.lineno = lineno
|
||||
if "col_offset" in node._attributes:
|
||||
node.col_offset = col_offset
|
||||
for child in ast.iter_child_nodes(node):
|
||||
_fix(child, lineno, col_offset)
|
||||
_fix(node, lineno, col_offset)
|
||||
return node
|
||||
|
||||
|
||||
class AssertionRewriter(ast.NodeVisitor):
|
||||
|
||||
def run(self, mod):
|
||||
"""Find all assert statements in *mod* and rewrite them."""
|
||||
if not mod.body:
|
||||
# Nothing to do.
|
||||
return
|
||||
# Insert some special imports at the top of the module but after any
|
||||
# docstrings and __future__ imports.
|
||||
aliases = [ast.alias(py.builtin.builtins.__name__, "@py_builtins"),
|
||||
ast.alias("_pytest.assertion.rewrite", "@pytest_ar")]
|
||||
expect_docstring = True
|
||||
pos = 0
|
||||
lineno = 0
|
||||
for item in mod.body:
|
||||
if (expect_docstring and isinstance(item, ast.Expr) and
|
||||
isinstance(item.value, ast.Str)):
|
||||
doc = item.value.s
|
||||
if "PYTEST_DONT_REWRITE" in doc:
|
||||
# The module has disabled assertion rewriting.
|
||||
return
|
||||
lineno += len(doc) - 1
|
||||
expect_docstring = False
|
||||
elif (not isinstance(item, ast.ImportFrom) or item.level > 0 or
|
||||
item.module != "__future__"):
|
||||
lineno = item.lineno
|
||||
break
|
||||
pos += 1
|
||||
imports = [ast.Import([alias], lineno=lineno, col_offset=0)
|
||||
for alias in aliases]
|
||||
mod.body[pos:pos] = imports
|
||||
# Collect asserts.
|
||||
nodes = [mod]
|
||||
while nodes:
|
||||
node = nodes.pop()
|
||||
for name, field in ast.iter_fields(node):
|
||||
if isinstance(field, list):
|
||||
new = []
|
||||
for i, child in enumerate(field):
|
||||
if isinstance(child, ast.Assert):
|
||||
# Transform assert.
|
||||
new.extend(self.visit(child))
|
||||
else:
|
||||
new.append(child)
|
||||
if isinstance(child, ast.AST):
|
||||
nodes.append(child)
|
||||
setattr(node, name, new)
|
||||
elif (isinstance(field, ast.AST) and
|
||||
# Don't recurse into expressions as they can't contain
|
||||
# asserts.
|
||||
not isinstance(field, ast.expr)):
|
||||
nodes.append(field)
|
||||
|
||||
def variable(self):
|
||||
"""Get a new variable."""
|
||||
# Use a character invalid in python identifiers to avoid clashing.
|
||||
name = "@py_assert" + str(next(self.variable_counter))
|
||||
self.variables.append(name)
|
||||
return name
|
||||
|
||||
def assign(self, expr):
|
||||
"""Give *expr* a name."""
|
||||
name = self.variable()
|
||||
self.statements.append(ast.Assign([ast.Name(name, ast.Store())], expr))
|
||||
return ast.Name(name, ast.Load())
|
||||
|
||||
def display(self, expr):
|
||||
"""Call py.io.saferepr on the expression."""
|
||||
return self.helper("saferepr", expr)
|
||||
|
||||
def helper(self, name, *args):
|
||||
"""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)
|
||||
|
||||
def builtin(self, name):
|
||||
"""Return the builtin called *name*."""
|
||||
builtin_name = ast.Name("@py_builtins", ast.Load())
|
||||
return ast.Attribute(builtin_name, name, ast.Load())
|
||||
|
||||
def explanation_param(self, expr):
|
||||
specifier = "py" + str(next(self.variable_counter))
|
||||
self.explanation_specifiers[specifier] = expr
|
||||
return "%(" + specifier + ")s"
|
||||
|
||||
def push_format_context(self):
|
||||
self.explanation_specifiers = {}
|
||||
self.stack.append(self.explanation_specifiers)
|
||||
|
||||
def pop_format_context(self, expl_expr):
|
||||
current = self.stack.pop()
|
||||
if self.stack:
|
||||
self.explanation_specifiers = self.stack[-1]
|
||||
keys = [ast.Str(key) for key in current.keys()]
|
||||
format_dict = ast.Dict(keys, list(current.values()))
|
||||
form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
|
||||
name = "@py_format" + str(next(self.variable_counter))
|
||||
self.on_failure.append(ast.Assign([ast.Name(name, ast.Store())], form))
|
||||
return ast.Name(name, ast.Load())
|
||||
|
||||
def generic_visit(self, node):
|
||||
"""Handle expressions we don't have custom code for."""
|
||||
assert isinstance(node, ast.expr)
|
||||
res = self.assign(node)
|
||||
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_]
|
||||
self.statements = []
|
||||
self.cond_chain = ()
|
||||
self.variables = []
|
||||
self.variable_counter = itertools.count()
|
||||
self.stack = []
|
||||
self.on_failure = []
|
||||
self.push_format_context()
|
||||
# Rewrite assert into a bunch of statements.
|
||||
top_condition, explanation = self.visit(assert_.test)
|
||||
# Create failure message.
|
||||
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)
|
||||
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)
|
||||
if sys.version_info[0] >= 3:
|
||||
raise_ = ast.Raise(exc, None)
|
||||
else:
|
||||
raise_ = ast.Raise(exc, None, None)
|
||||
body.append(raise_)
|
||||
# Clear temporary variables by setting them to None.
|
||||
if self.variables:
|
||||
variables = [ast.Name(name, ast.Store())
|
||||
for name in self.variables]
|
||||
clear = ast.Assign(variables, ast.Name("None", ast.Load()))
|
||||
self.statements.append(clear)
|
||||
# Fix line numbers.
|
||||
for stmt in self.statements:
|
||||
set_location(stmt, assert_.lineno, assert_.col_offset)
|
||||
return self.statements
|
||||
|
||||
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)
|
||||
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])
|
||||
expr = ast.IfExp(test, self.display(name), ast.Str(name.id))
|
||||
return name, self.explanation_param(expr)
|
||||
|
||||
def visit_BoolOp(self, boolop):
|
||||
res_var = self.variable()
|
||||
expl_list = self.assign(ast.List([], ast.Load()))
|
||||
app = ast.Attribute(expl_list, "append", ast.Load())
|
||||
is_or = int(isinstance(boolop.op, ast.Or))
|
||||
body = save = self.statements
|
||||
fail_save = self.on_failure
|
||||
levels = len(boolop.values) - 1
|
||||
self.push_format_context()
|
||||
# Process each operand, short-circuting if needed.
|
||||
for i, v in enumerate(boolop.values):
|
||||
if i:
|
||||
fail_inner = []
|
||||
self.on_failure.append(ast.If(cond, fail_inner, []))
|
||||
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)
|
||||
self.on_failure.append(ast.Expr(call))
|
||||
if i < levels:
|
||||
cond = res
|
||||
if is_or:
|
||||
cond = ast.UnaryOp(ast.Not(), cond)
|
||||
inner = []
|
||||
self.statements.append(ast.If(cond, inner, []))
|
||||
self.statements = body = inner
|
||||
self.statements = save
|
||||
self.on_failure = fail_save
|
||||
expl_template = self.helper("format_boolop", expl_list, ast.Num(is_or))
|
||||
expl = self.pop_format_context(expl_template)
|
||||
return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
|
||||
|
||||
def visit_UnaryOp(self, unary):
|
||||
pattern = unary_map[unary.op.__class__]
|
||||
operand_res, operand_expl = self.visit(unary.operand)
|
||||
res = self.assign(ast.UnaryOp(unary.op, operand_res))
|
||||
return res, pattern % (operand_expl,)
|
||||
|
||||
def visit_BinOp(self, binop):
|
||||
symbol = binop_map[binop.op.__class__]
|
||||
left_expr, left_expl = self.visit(binop.left)
|
||||
right_expr, right_expl = self.visit(binop.right)
|
||||
explanation = "(%s %s %s)" % (left_expl, symbol, right_expl)
|
||||
res = self.assign(ast.BinOp(left_expr, binop.op, right_expr))
|
||||
return res, explanation
|
||||
|
||||
def visit_Call(self, call):
|
||||
new_func, func_expl = self.visit(call.func)
|
||||
arg_expls = []
|
||||
new_args = []
|
||||
new_kwargs = []
|
||||
new_star = new_kwarg = None
|
||||
for arg in call.args:
|
||||
res, expl = self.visit(arg)
|
||||
new_args.append(res)
|
||||
arg_expls.append(expl)
|
||||
for keyword in call.keywords:
|
||||
res, expl = self.visit(keyword.value)
|
||||
new_kwargs.append(ast.keyword(keyword.arg, res))
|
||||
arg_expls.append(keyword.arg + "=" + expl)
|
||||
if call.starargs:
|
||||
new_star, expl = self.visit(call.starargs)
|
||||
arg_expls.append("*" + expl)
|
||||
if call.kwargs:
|
||||
new_kwarg, expl = self.visit(call.kwargs)
|
||||
arg_expls.append("**" + expl)
|
||||
expl = "%s(%s)" % (func_expl, ', '.join(arg_expls))
|
||||
new_call = ast.Call(new_func, new_args, new_kwargs,
|
||||
new_star, new_kwarg)
|
||||
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_Attribute(self, attr):
|
||||
if not isinstance(attr.ctx, ast.Load):
|
||||
return self.generic_visit(attr)
|
||||
value, value_expl = self.visit(attr.value)
|
||||
res = self.assign(ast.Attribute(value, attr.attr, ast.Load()))
|
||||
res_expl = self.explanation_param(self.display(res))
|
||||
pat = "%s\n{%s = %s.%s\n}"
|
||||
expl = pat % (res_expl, res_expl, value_expl, attr.attr)
|
||||
return res, expl
|
||||
|
||||
def visit_Compare(self, comp):
|
||||
self.push_format_context()
|
||||
left_res, left_expl = self.visit(comp.left)
|
||||
res_variables = [self.variable() for i in range(len(comp.ops))]
|
||||
load_names = [ast.Name(v, ast.Load()) for v in res_variables]
|
||||
store_names = [ast.Name(v, ast.Store()) for v in res_variables]
|
||||
it = zip(range(len(comp.ops)), comp.ops, comp.comparators)
|
||||
expls = []
|
||||
syms = []
|
||||
results = [left_res]
|
||||
for i, op, next_operand in it:
|
||||
next_res, next_expl = self.visit(next_operand)
|
||||
results.append(next_res)
|
||||
sym = binop_map[op.__class__]
|
||||
syms.append(ast.Str(sym))
|
||||
expl = "%s %s %s" % (left_expl, sym, next_expl)
|
||||
expls.append(ast.Str(expl))
|
||||
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.
|
||||
expl_call = self.helper("call_reprcompare",
|
||||
ast.Tuple(syms, ast.Load()),
|
||||
ast.Tuple(load_names, ast.Load()),
|
||||
ast.Tuple(expls, ast.Load()),
|
||||
ast.Tuple(results, ast.Load()))
|
||||
if len(comp.ops) > 1:
|
||||
res = ast.BoolOp(ast.And(), load_names)
|
||||
else:
|
||||
res = load_names[0]
|
||||
return res, self.explanation_param(self.pop_format_context(expl_call))
|
||||
250
_pytest/assertion/util.py
Normal file
250
_pytest/assertion/util.py
Normal file
@@ -0,0 +1,250 @@
|
||||
"""Utilities for assertion debugging"""
|
||||
|
||||
import py
|
||||
try:
|
||||
from collections.abc import Sequence
|
||||
except ImportError:
|
||||
try:
|
||||
from collections import Sequence
|
||||
except ImportError:
|
||||
Sequence = list
|
||||
|
||||
|
||||
BuiltinAssertionError = py.builtin.builtins.AssertionError
|
||||
|
||||
# The _reprcompare attribute on the util module is used by the new assertion
|
||||
# interpretation code and assertion rewriter to detect this plugin was
|
||||
# loaded and in turn call the hooks defined here as part of the
|
||||
# DebugInterpreter.
|
||||
_reprcompare = None
|
||||
|
||||
|
||||
def format_explanation(explanation):
|
||||
"""This formats an explanation
|
||||
|
||||
Normally all embedded newlines are escaped, however there are
|
||||
three exceptions: \n{, \n} and \n~. The first two are intended
|
||||
cover nested explanations, see function and attribute explanations
|
||||
for examples (.visit_Call(), visit_Attribute()). The last one is
|
||||
for when one explanation needs to span multiple lines, e.g. when
|
||||
displaying diffs.
|
||||
"""
|
||||
# simplify 'assert False where False = ...'
|
||||
where = 0
|
||||
while True:
|
||||
start = where = explanation.find("False\n{False = ", where)
|
||||
if where == -1:
|
||||
break
|
||||
level = 0
|
||||
for i, c in enumerate(explanation[start:]):
|
||||
if c == "{":
|
||||
level += 1
|
||||
elif c == "}":
|
||||
level -= 1
|
||||
if not level:
|
||||
break
|
||||
else:
|
||||
raise AssertionError("unbalanced braces: %r" % (explanation,))
|
||||
end = start + i
|
||||
where = end
|
||||
if explanation[end - 1] == '\n':
|
||||
explanation = (explanation[:start] + explanation[start+15:end-1] +
|
||||
explanation[end+1:])
|
||||
where -= 17
|
||||
raw_lines = (explanation or '').split('\n')
|
||||
# escape newlines not followed by {, } and ~
|
||||
lines = [raw_lines[0]]
|
||||
for l in raw_lines[1:]:
|
||||
if l.startswith('{') or l.startswith('}') or l.startswith('~'):
|
||||
lines.append(l)
|
||||
else:
|
||||
lines[-1] += '\\n' + l
|
||||
|
||||
result = lines[:1]
|
||||
stack = [0]
|
||||
stackcnt = [0]
|
||||
for line in lines[1:]:
|
||||
if line.startswith('{'):
|
||||
if stackcnt[-1]:
|
||||
s = 'and '
|
||||
else:
|
||||
s = 'where '
|
||||
stack.append(len(result))
|
||||
stackcnt[-1] += 1
|
||||
stackcnt.append(0)
|
||||
result.append(' +' + ' '*(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 len(stack) == 1
|
||||
return '\n'.join(result)
|
||||
|
||||
|
||||
# Provide basestring in python3
|
||||
try:
|
||||
basestring = basestring
|
||||
except NameError:
|
||||
basestring = str
|
||||
|
||||
|
||||
def assertrepr_compare(config, op, left, right):
|
||||
"""Return specialised explanations for some operators/operands"""
|
||||
width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op
|
||||
left_repr = py.io.saferepr(left, maxsize=int(width/2))
|
||||
right_repr = py.io.saferepr(right, maxsize=width-len(left_repr))
|
||||
summary = '%s %s %s' % (left_repr, op, 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))
|
||||
|
||||
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)
|
||||
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()
|
||||
explanation = [
|
||||
'(pytest_assertion plugin: representation of details failed. '
|
||||
'Probably an object has a faulty __repr__.)', str(excinfo)]
|
||||
|
||||
if not explanation:
|
||||
return None
|
||||
|
||||
return [summary] + explanation
|
||||
|
||||
|
||||
def _diff_text(left, right, verbose=False):
|
||||
"""Return the explanation for the diff between text
|
||||
|
||||
Unless --verbose is used this will skip leading and trailing
|
||||
characters which are identical to keep the diff minimal.
|
||||
"""
|
||||
explanation = []
|
||||
if not verbose:
|
||||
i = 0 # just in case left or right has zero length
|
||||
for i in range(min(len(left), len(right))):
|
||||
if left[i] != right[i]:
|
||||
break
|
||||
if i > 42:
|
||||
i -= 10 # Provide some context
|
||||
explanation = ['Skipping %s identical leading '
|
||||
'characters in diff, use -v to show' % i]
|
||||
left = left[i:]
|
||||
right = right[i:]
|
||||
if len(left) == len(right):
|
||||
for i in range(len(left)):
|
||||
if left[-i] != right[-i]:
|
||||
break
|
||||
if i > 42:
|
||||
i -= 10 # Provide some context
|
||||
explanation += ['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())]
|
||||
return explanation
|
||||
|
||||
|
||||
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])]
|
||||
break
|
||||
if len(left) > len(right):
|
||||
explanation += [
|
||||
'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' %
|
||||
py.io.saferepr(right[len(left)],)]
|
||||
return explanation # + _diff_text(py.std.pprint.pformat(left),
|
||||
# py.std.pprint.pformat(right))
|
||||
|
||||
|
||||
def _compare_eq_set(left, right, verbose=False):
|
||||
explanation = []
|
||||
diff_left = left - right
|
||||
diff_right = right - left
|
||||
if diff_left:
|
||||
explanation.append('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:')
|
||||
for item in diff_right:
|
||||
explanation.append(py.io.saferepr(item))
|
||||
return explanation
|
||||
|
||||
|
||||
def _compare_eq_dict(left, right, verbose=False):
|
||||
explanation = []
|
||||
common = set(left).intersection(set(right))
|
||||
same = dict((k, left[k]) for k in common if left[k] == right[k])
|
||||
if same and not verbose:
|
||||
explanation += ['Omitting %s identical items, use -v to show' %
|
||||
len(same)]
|
||||
elif same:
|
||||
explanation += ['Common items:']
|
||||
explanation += py.std.pprint.pformat(same).splitlines()
|
||||
diff = set(k for k in common if left[k] != right[k])
|
||||
if diff:
|
||||
explanation += ['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(
|
||||
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(
|
||||
dict((k, right[k]) for k in extra_right)).splitlines())
|
||||
return explanation
|
||||
|
||||
|
||||
def _notin_text(term, text, verbose=False):
|
||||
index = text.find(term)
|
||||
head = text[:index]
|
||||
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)]
|
||||
for line in diff:
|
||||
if line.startswith('Skipping'):
|
||||
continue
|
||||
if line.startswith('- '):
|
||||
continue
|
||||
if line.startswith('+ '):
|
||||
newdiff.append(' ' + line[2:])
|
||||
else:
|
||||
newdiff.append(line)
|
||||
return newdiff
|
||||
248
_pytest/capture.py
Normal file
248
_pytest/capture.py
Normal file
@@ -0,0 +1,248 @@
|
||||
""" per-test stdout/stderr capturing mechanisms, ``capsys`` and ``capfd`` function arguments. """
|
||||
|
||||
import pytest, py
|
||||
import sys
|
||||
import os
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("general")
|
||||
group._addoption('--capture', action="store", default=None,
|
||||
metavar="method", 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",
|
||||
help="shortcut for --capture=no.")
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
def pytest_load_initial_conftests(early_config, parser, args, __multicall__):
|
||||
ns = parser.parse_known_args(args)
|
||||
method = ns.capture
|
||||
if not method:
|
||||
method = "fd"
|
||||
if method == "fd" and not hasattr(os, "dup"):
|
||||
method = "sys"
|
||||
capman = CaptureManager(method)
|
||||
early_config.pluginmanager.register(capman, "capturemanager")
|
||||
# make sure that capturemanager is properly reset at final shutdown
|
||||
def teardown():
|
||||
try:
|
||||
capman.reset_capturings()
|
||||
except ValueError:
|
||||
pass
|
||||
early_config.pluginmanager.add_shutdown(teardown)
|
||||
# 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.pluginmanager.add_shutdown(silence_logging_at_shutdown)
|
||||
|
||||
# finally trigger conftest loading but while capturing (issue93)
|
||||
capman.resumecapture()
|
||||
try:
|
||||
try:
|
||||
return __multicall__.execute()
|
||||
finally:
|
||||
out, err = capman.suspendcapture()
|
||||
except:
|
||||
sys.stdout.write(out)
|
||||
sys.stderr.write(err)
|
||||
raise
|
||||
|
||||
def addouterr(rep, outerr):
|
||||
for secname, content in zip(["out", "err"], outerr):
|
||||
if content:
|
||||
rep.sections.append(("Captured std%s" % secname, content))
|
||||
|
||||
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 _getcapture(self, method):
|
||||
if method == "fd":
|
||||
return py.io.StdCaptureFD(now=False,
|
||||
out=self._maketempfile(), err=self._maketempfile()
|
||||
)
|
||||
elif method == "sys":
|
||||
return py.io.StdCapture(now=False,
|
||||
out=self._makestringio(), err=self._makestringio()
|
||||
)
|
||||
elif method == "no":
|
||||
return NoCapture()
|
||||
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 reset_capturings(self):
|
||||
for name, cap in self._method2capture.items():
|
||||
cap.reset()
|
||||
|
||||
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, 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):
|
||||
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])
|
||||
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()
|
||||
|
||||
def deactivate_funcargs(self):
|
||||
capturing_funcarg = getattr(self, '_capturing_funcarg', None)
|
||||
if capturing_funcarg:
|
||||
outerr = capturing_funcarg._finalize()
|
||||
del self._capturing_funcarg
|
||||
return outerr
|
||||
|
||||
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.mark.tryfirst
|
||||
def pytest_runtest_setup(self, item):
|
||||
self.resumecapture_item(item)
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
def pytest_runtest_call(self, item):
|
||||
self.resumecapture_item(item)
|
||||
self.activate_funcargs(item)
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
def pytest_runtest_teardown(self, item):
|
||||
self.resumecapture_item(item)
|
||||
|
||||
def pytest_keyboard_interrupt(self, excinfo):
|
||||
if hasattr(self, '_capturing'):
|
||||
self.suspendcapture()
|
||||
|
||||
@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
|
||||
|
||||
error_capsysfderror = "cannot use capsys and capfd at the same time"
|
||||
|
||||
def pytest_funcarg__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)
|
||||
|
||||
def pytest_funcarg__capfd(request):
|
||||
"""enables capturing of writes to file descriptors 1 and 2 and makes
|
||||
captured output available via ``capsys.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)
|
||||
|
||||
class CaptureFixture:
|
||||
def __init__(self, captureclass):
|
||||
self.capture = captureclass(now=False)
|
||||
|
||||
def _start(self):
|
||||
self.capture.startall()
|
||||
|
||||
def _finalize(self):
|
||||
if hasattr(self, 'capture'):
|
||||
outerr = self._outerr = self.capture.reset()
|
||||
del self.capture
|
||||
return outerr
|
||||
|
||||
def readouterr(self):
|
||||
try:
|
||||
return self.capture.readouterr()
|
||||
except AttributeError:
|
||||
return self._outerr
|
||||
|
||||
def close(self):
|
||||
self._finalize()
|
||||
862
_pytest/config.py
Normal file
862
_pytest/config.py
Normal file
@@ -0,0 +1,862 @@
|
||||
""" command line options, ini-file and conftest.py processing. """
|
||||
|
||||
import py
|
||||
import sys, os
|
||||
from _pytest import hookspec # the extension point definitions
|
||||
from _pytest.core import PluginManager
|
||||
|
||||
# pytest startup
|
||||
|
||||
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 cmdline: # compatibility namespace
|
||||
main = staticmethod(main)
|
||||
|
||||
class UsageError(Exception):
|
||||
""" error in py.test usage or invocation"""
|
||||
|
||||
_preinit = []
|
||||
|
||||
default_plugins = (
|
||||
"mark main terminal runner python pdb unittest capture skipping "
|
||||
"tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript "
|
||||
"junitxml resultlog doctest").split()
|
||||
|
||||
def _preloadplugins():
|
||||
assert not _preinit
|
||||
_preinit.append(get_plugin_manager())
|
||||
|
||||
def get_plugin_manager():
|
||||
if _preinit:
|
||||
return _preinit.pop(0)
|
||||
# subsequent calls to main will create a fresh instance
|
||||
pluginmanager = PytestPluginManager()
|
||||
pluginmanager.config = config = Config(pluginmanager) # XXX attr needed?
|
||||
for spec in default_plugins:
|
||||
pluginmanager.import_plugin(spec)
|
||||
return pluginmanager
|
||||
|
||||
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)
|
||||
pluginmanager = get_plugin_manager()
|
||||
if plugins:
|
||||
for plugin in plugins:
|
||||
pluginmanager.register(plugin)
|
||||
return pluginmanager.hook.pytest_cmdline_parse(
|
||||
pluginmanager=pluginmanager, args=args)
|
||||
|
||||
class PytestPluginManager(PluginManager):
|
||||
def __init__(self, hookspecs=[hookspec]):
|
||||
super(PytestPluginManager, self).__init__(hookspecs=hookspecs)
|
||||
self.register(self)
|
||||
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)
|
||||
|
||||
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.")
|
||||
|
||||
|
||||
class Parser:
|
||||
""" Parser for command line arguments and ini-file values. """
|
||||
|
||||
def __init__(self, usage=None, processopt=None):
|
||||
self._anonymous = OptionGroup("custom options", parser=self)
|
||||
self._groups = []
|
||||
self._processopt = processopt
|
||||
self._usage = usage
|
||||
self._inidict = {}
|
||||
self._ininames = []
|
||||
self.hints = []
|
||||
|
||||
def processoption(self, option):
|
||||
if self._processopt:
|
||||
if option.dest:
|
||||
self._processopt(option)
|
||||
|
||||
def getgroup(self, name, description="", after=None):
|
||||
""" get (or create) a named option Group.
|
||||
|
||||
:name: name of the option group.
|
||||
:description: long description for --help output.
|
||||
:after: name of other group, used for ordering --help output.
|
||||
|
||||
The returned group object has an ``addoption`` method with the same
|
||||
signature as :py:func:`parser.addoption
|
||||
<_pytest.config.Parser.addoption>` but will be shown in the
|
||||
respective group in the output of ``pytest. --help``.
|
||||
"""
|
||||
for group in self._groups:
|
||||
if group.name == name:
|
||||
return group
|
||||
group = OptionGroup(name, description, parser=self)
|
||||
i = 0
|
||||
for i, grp in enumerate(self._groups):
|
||||
if grp.name == after:
|
||||
break
|
||||
self._groups.insert(i+1, group)
|
||||
return group
|
||||
|
||||
def addoption(self, *opts, **attrs):
|
||||
""" register a command line option.
|
||||
|
||||
:opts: option names, can be short or long options.
|
||||
:attrs: same attributes which the ``add_option()`` function of the
|
||||
`optparse library
|
||||
<http://docs.python.org/library/optparse.html#module-optparse>`_
|
||||
accepts.
|
||||
|
||||
After command line parsing options are available on the pytest config
|
||||
object via ``config.option.NAME`` where ``NAME`` is usually set
|
||||
by passing a ``dest`` attribute, for example
|
||||
``addoption("--long", dest="NAME", ...)``.
|
||||
"""
|
||||
self._anonymous.addoption(*opts, **attrs)
|
||||
|
||||
def parse(self, args):
|
||||
from _pytest._argcomplete import try_argcomplete
|
||||
self.optparser = self._getparser()
|
||||
try_argcomplete(self.optparser)
|
||||
return self.optparser.parse_args([str(x) for x in args])
|
||||
|
||||
def _getparser(self):
|
||||
from _pytest._argcomplete import filescompleter
|
||||
optparser = MyOptionParser(self)
|
||||
groups = self._groups + [self._anonymous]
|
||||
for group in groups:
|
||||
if group.options:
|
||||
desc = group.description or group.name
|
||||
arggroup = optparser.add_argument_group(desc)
|
||||
for option in group.options:
|
||||
n = option.names()
|
||||
a = option.attrs()
|
||||
arggroup.add_argument(*n, **a)
|
||||
# bash like autocompletion for dirs (appending '/')
|
||||
optparser.add_argument(FILE_OR_DIR, nargs='*'
|
||||
).completer=filescompleter
|
||||
return optparser
|
||||
|
||||
def parse_setoption(self, args, option):
|
||||
parsedoption = self.parse(args)
|
||||
for name, value in parsedoption.__dict__.items():
|
||||
setattr(option, name, value)
|
||||
return getattr(parsedoption, FILE_OR_DIR)
|
||||
|
||||
def parse_known_args(self, args):
|
||||
optparser = self._getparser()
|
||||
args = [str(x) for x in args]
|
||||
return optparser.parse_known_args(args)[0]
|
||||
|
||||
def addini(self, name, help, type=None, default=None):
|
||||
""" register an ini-file option.
|
||||
|
||||
:name: name of the ini-variable
|
||||
:type: type of the variable, can be ``pathlist``, ``args`` or ``linelist``.
|
||||
:default: default value if no ini-file option exists but is queried.
|
||||
|
||||
The value of ini-variables can be retrieved via a call to
|
||||
:py:func:`config.getini(name) <_pytest.config.Config.getini>`.
|
||||
"""
|
||||
assert type in (None, "pathlist", "args", "linelist")
|
||||
self._inidict[name] = (help, type, default)
|
||||
self._ininames.append(name)
|
||||
|
||||
|
||||
class ArgumentError(Exception):
|
||||
"""
|
||||
Raised if an Argument instance is created with invalid or
|
||||
inconsistent arguments.
|
||||
"""
|
||||
|
||||
def __init__(self, msg, option):
|
||||
self.msg = msg
|
||||
self.option_id = str(option)
|
||||
|
||||
def __str__(self):
|
||||
if self.option_id:
|
||||
return "option %s: %s" % (self.option_id, self.msg)
|
||||
else:
|
||||
return self.msg
|
||||
|
||||
|
||||
class Argument:
|
||||
"""class that mimics the necessary behaviour of py.std.optparse.Option """
|
||||
_typ_map = {
|
||||
'int': int,
|
||||
'string': str,
|
||||
}
|
||||
# enable after some grace period for plugin writers
|
||||
TYPE_WARN = False
|
||||
|
||||
def __init__(self, *names, **attrs):
|
||||
"""store parms in private vars for use in add_argument"""
|
||||
self._attrs = attrs
|
||||
self._short_opts = []
|
||||
self._long_opts = []
|
||||
self.dest = attrs.get('dest')
|
||||
if self.TYPE_WARN:
|
||||
try:
|
||||
help = attrs['help']
|
||||
if '%default' in help:
|
||||
py.std.warnings.warn(
|
||||
'py.test now uses argparse. "%default" should be'
|
||||
' changed to "%(default)s" ',
|
||||
FutureWarning,
|
||||
stacklevel=3)
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
typ = attrs['type']
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
# this might raise a keyerror as well, don't want to catch that
|
||||
if isinstance(typ, py.builtin._basestring):
|
||||
if typ == 'choice':
|
||||
if self.TYPE_WARN:
|
||||
py.std.warnings.warn(
|
||||
'type argument to addoption() is a string %r.'
|
||||
' For parsearg this is optional and when supplied '
|
||||
' should be a type.'
|
||||
' (options: %s)' % (typ, names),
|
||||
FutureWarning,
|
||||
stacklevel=3)
|
||||
# argparse expects a type here take it from
|
||||
# the type of the first element
|
||||
attrs['type'] = type(attrs['choices'][0])
|
||||
else:
|
||||
if self.TYPE_WARN:
|
||||
py.std.warnings.warn(
|
||||
'type argument to addoption() is a string %r.'
|
||||
' For parsearg this should be a type.'
|
||||
' (options: %s)' % (typ, names),
|
||||
FutureWarning,
|
||||
stacklevel=3)
|
||||
attrs['type'] = Argument._typ_map[typ]
|
||||
# used in test_parseopt -> test_parse_defaultgetter
|
||||
self.type = attrs['type']
|
||||
else:
|
||||
self.type = typ
|
||||
try:
|
||||
# attribute existence is tested in Config._processopt
|
||||
self.default = attrs['default']
|
||||
except KeyError:
|
||||
pass
|
||||
self._set_opt_strings(names)
|
||||
if not self.dest:
|
||||
if self._long_opts:
|
||||
self.dest = self._long_opts[0][2:].replace('-', '_')
|
||||
else:
|
||||
try:
|
||||
self.dest = self._short_opts[0][1:]
|
||||
except IndexError:
|
||||
raise ArgumentError(
|
||||
'need a long or short option', self)
|
||||
|
||||
def names(self):
|
||||
return self._short_opts + self._long_opts
|
||||
|
||||
def attrs(self):
|
||||
# update any attributes set by processopt
|
||||
attrs = 'default dest help'.split()
|
||||
if self.dest:
|
||||
attrs.append(self.dest)
|
||||
for attr in attrs:
|
||||
try:
|
||||
self._attrs[attr] = getattr(self, attr)
|
||||
except AttributeError:
|
||||
pass
|
||||
if self._attrs.get('help'):
|
||||
a = self._attrs['help']
|
||||
a = a.replace('%default', '%(default)s')
|
||||
#a = a.replace('%prog', '%(prog)s')
|
||||
self._attrs['help'] = a
|
||||
return self._attrs
|
||||
|
||||
def _set_opt_strings(self, opts):
|
||||
"""directly from optparse
|
||||
|
||||
might not be necessary as this is passed to argparse later on"""
|
||||
for opt in opts:
|
||||
if len(opt) < 2:
|
||||
raise ArgumentError(
|
||||
"invalid option string %r: "
|
||||
"must be at least two characters long" % opt, self)
|
||||
elif len(opt) == 2:
|
||||
if not (opt[0] == "-" and opt[1] != "-"):
|
||||
raise ArgumentError(
|
||||
"invalid short option string %r: "
|
||||
"must be of the form -x, (x any non-dash char)" % opt,
|
||||
self)
|
||||
self._short_opts.append(opt)
|
||||
else:
|
||||
if not (opt[0:2] == "--" and opt[2] != "-"):
|
||||
raise ArgumentError(
|
||||
"invalid long option string %r: "
|
||||
"must start with --, followed by non-dash" % opt,
|
||||
self)
|
||||
self._long_opts.append(opt)
|
||||
|
||||
def __repr__(self):
|
||||
retval = 'Argument('
|
||||
if self._short_opts:
|
||||
retval += '_short_opts: ' + repr(self._short_opts) + ', '
|
||||
if self._long_opts:
|
||||
retval += '_long_opts: ' + repr(self._long_opts) + ', '
|
||||
retval += 'dest: ' + repr(self.dest) + ', '
|
||||
if hasattr(self, 'type'):
|
||||
retval += 'type: ' + repr(self.type) + ', '
|
||||
if hasattr(self, 'default'):
|
||||
retval += 'default: ' + repr(self.default) + ', '
|
||||
if retval[-2:] == ', ': # always long enough to test ("Argument(" )
|
||||
retval = retval[:-2]
|
||||
retval += ')'
|
||||
return retval
|
||||
|
||||
|
||||
class OptionGroup:
|
||||
def __init__(self, name, description="", parser=None):
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.options = []
|
||||
self.parser = parser
|
||||
|
||||
def addoption(self, *optnames, **attrs):
|
||||
""" add an option to this group.
|
||||
|
||||
if a shortened version of a long option is specified it will
|
||||
be suppressed in the help. addoption('--twowords', '--two-words')
|
||||
results in help showing '--two-words' only, but --twowords gets
|
||||
accepted **and** the automatic destination is in args.twowords
|
||||
"""
|
||||
option = Argument(*optnames, **attrs)
|
||||
self._addoption_instance(option, shortupper=False)
|
||||
|
||||
def _addoption(self, *optnames, **attrs):
|
||||
option = Argument(*optnames, **attrs)
|
||||
self._addoption_instance(option, shortupper=True)
|
||||
|
||||
def _addoption_instance(self, option, shortupper=False):
|
||||
if not shortupper:
|
||||
for opt in option._short_opts:
|
||||
if opt[0] == '-' and opt[1].islower():
|
||||
raise ValueError("lowercase shortoptions reserved")
|
||||
if self.parser:
|
||||
self.parser.processoption(option)
|
||||
self.options.append(option)
|
||||
|
||||
|
||||
class MyOptionParser(py.std.argparse.ArgumentParser):
|
||||
def __init__(self, parser):
|
||||
self._parser = parser
|
||||
py.std.argparse.ArgumentParser.__init__(self, usage=parser._usage,
|
||||
add_help=False, formatter_class=DropShorterLongHelpFormatter)
|
||||
|
||||
def format_epilog(self, formatter):
|
||||
hints = self._parser.hints
|
||||
if hints:
|
||||
s = "\n".join(["hint: " + x for x in hints]) + "\n"
|
||||
s = "\n" + s + "\n"
|
||||
return s
|
||||
return ""
|
||||
|
||||
def parse_args(self, args=None, namespace=None):
|
||||
"""allow splitting of positional arguments"""
|
||||
args, argv = self.parse_known_args(args, namespace)
|
||||
if argv:
|
||||
for arg in argv:
|
||||
if arg and arg[0] == '-':
|
||||
msg = py.std.argparse._('unrecognized arguments: %s')
|
||||
self.error(msg % ' '.join(argv))
|
||||
getattr(args, FILE_OR_DIR).extend(argv)
|
||||
return args
|
||||
|
||||
class DropShorterLongHelpFormatter(py.std.argparse.HelpFormatter):
|
||||
"""shorten help for long options that differ only in extra hyphens
|
||||
|
||||
- collapse **long** options that are the same except for extra hyphens
|
||||
- special action attribute map_long_option allows surpressing additional
|
||||
long options
|
||||
- shortcut if there are only two options and one of them is a short one
|
||||
- cache result on action object as this is called at least 2 times
|
||||
"""
|
||||
def _format_action_invocation(self, action):
|
||||
orgstr = py.std.argparse.HelpFormatter._format_action_invocation(self, action)
|
||||
if orgstr and orgstr[0] != '-': # only optional arguments
|
||||
return orgstr
|
||||
res = getattr(action, '_formatted_action_invocation', None)
|
||||
if res:
|
||||
return res
|
||||
options = orgstr.split(', ')
|
||||
if len(options) == 2 and (len(options[0]) == 2 or len(options[1]) == 2):
|
||||
# a shortcut for '-h, --help' or '--abc', '-a'
|
||||
action._formatted_action_invocation = orgstr
|
||||
return orgstr
|
||||
return_list = []
|
||||
option_map = getattr(action, 'map_long_option', {})
|
||||
if option_map is None:
|
||||
option_map = {}
|
||||
short_long = {}
|
||||
for option in options:
|
||||
if len(option) == 2 or option[2] == ' ':
|
||||
continue
|
||||
if not option.startswith('--'):
|
||||
raise ArgumentError('long optional argument without "--": [%s]'
|
||||
% (option), self)
|
||||
xxoption = option[2:]
|
||||
if xxoption.split()[0] not in option_map:
|
||||
shortened = xxoption.replace('-', '')
|
||||
if shortened not in short_long or \
|
||||
len(short_long[shortened]) < len(xxoption):
|
||||
short_long[shortened] = xxoption
|
||||
# now short_long has been filled out to the longest with dashes
|
||||
# **and** we keep the right option ordering from add_argument
|
||||
for option in options: #
|
||||
if len(option) == 2 or option[2] == ' ':
|
||||
return_list.append(option)
|
||||
if option[2:] == short_long.get(option.replace('-', '')):
|
||||
return_list.append(option)
|
||||
action._formatted_action_invocation = ', '.join(return_list)
|
||||
return action._formatted_action_invocation
|
||||
|
||||
|
||||
class Conftest(object):
|
||||
""" the single place for accessing values and interacting
|
||||
towards conftest modules from py.test objects.
|
||||
"""
|
||||
def __init__(self, onimport=None, confcutdir=None):
|
||||
self._path2confmods = {}
|
||||
self._onimport = onimport
|
||||
self._conftestpath2mod = {}
|
||||
self._confcutdir = confcutdir
|
||||
|
||||
def setinitial(self, args):
|
||||
""" try to find a first anchor path for looking up global values
|
||||
from conftests. This function is usually called _before_
|
||||
argument parsing. conftest files may add command line options
|
||||
and we thus have no completely safe way of determining
|
||||
which parts of the arguments are actually related to options
|
||||
and which are file system paths. We just try here to get
|
||||
bootstrapped ...
|
||||
"""
|
||||
current = py.path.local()
|
||||
opt = '--confcutdir'
|
||||
for i in range(len(args)):
|
||||
opt1 = str(args[i])
|
||||
if opt1.startswith(opt):
|
||||
if opt1 == opt:
|
||||
if len(args) > i:
|
||||
p = current.join(args[i+1], abs=True)
|
||||
elif opt1.startswith(opt + "="):
|
||||
p = current.join(opt1[len(opt)+1:], abs=1)
|
||||
self._confcutdir = p
|
||||
break
|
||||
foundanchor = False
|
||||
for arg in args:
|
||||
if hasattr(arg, 'startswith') and arg.startswith("--"):
|
||||
continue
|
||||
anchor = current.join(arg, abs=1)
|
||||
if exists(anchor): # we found some file object
|
||||
self._try_load_conftest(anchor)
|
||||
foundanchor = True
|
||||
if not foundanchor:
|
||||
self._try_load_conftest(current)
|
||||
|
||||
def _try_load_conftest(self, anchor):
|
||||
self._path2confmods[None] = self.getconftestmodules(anchor)
|
||||
# let's also consider test* subdirs
|
||||
if anchor.check(dir=1):
|
||||
for x in anchor.listdir("test*"):
|
||||
if x.check(dir=1):
|
||||
self.getconftestmodules(x)
|
||||
|
||||
def getconftestmodules(self, path):
|
||||
try:
|
||||
clist = self._path2confmods[path]
|
||||
except KeyError:
|
||||
if path is None:
|
||||
raise ValueError("missing default conftest.")
|
||||
clist = []
|
||||
for parent in path.parts():
|
||||
if self._confcutdir and self._confcutdir.relto(parent):
|
||||
continue
|
||||
conftestpath = parent.join("conftest.py")
|
||||
if conftestpath.check(file=1):
|
||||
clist.append(self.importconftest(conftestpath))
|
||||
self._path2confmods[path] = clist
|
||||
return clist
|
||||
|
||||
def rget(self, name, path=None):
|
||||
mod, value = self.rget_with_confmod(name, path)
|
||||
return value
|
||||
|
||||
def rget_with_confmod(self, name, path=None):
|
||||
modules = self.getconftestmodules(path)
|
||||
modules.reverse()
|
||||
for mod in modules:
|
||||
try:
|
||||
return mod, getattr(mod, name)
|
||||
except AttributeError:
|
||||
continue
|
||||
raise KeyError(name)
|
||||
|
||||
def importconftest(self, conftestpath):
|
||||
assert conftestpath.check(), conftestpath
|
||||
try:
|
||||
return self._conftestpath2mod[conftestpath]
|
||||
except KeyError:
|
||||
pkgpath = conftestpath.pypkgpath()
|
||||
if pkgpath is None:
|
||||
_ensure_removed_sysmodule(conftestpath.purebasename)
|
||||
self._conftestpath2mod[conftestpath] = mod = conftestpath.pyimport()
|
||||
dirpath = conftestpath.dirpath()
|
||||
if dirpath in self._path2confmods:
|
||||
for path, mods in self._path2confmods.items():
|
||||
if path and path.relto(dirpath) or path == dirpath:
|
||||
assert mod not in mods
|
||||
mods.append(mod)
|
||||
self._postimport(mod)
|
||||
return mod
|
||||
|
||||
def _postimport(self, mod):
|
||||
if self._onimport:
|
||||
self._onimport(mod)
|
||||
return mod
|
||||
|
||||
def _ensure_removed_sysmodule(modname):
|
||||
try:
|
||||
del sys.modules[modname]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
class CmdOptions(object):
|
||||
""" holds cmdline options as attributes."""
|
||||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(kwargs)
|
||||
def __repr__(self):
|
||||
return "<CmdOptions %r>" %(self.__dict__,)
|
||||
|
||||
FILE_OR_DIR = 'file_or_dir'
|
||||
class Config(object):
|
||||
""" access to configuration values, pluginmanager and plugin hooks. """
|
||||
|
||||
def __init__(self, pluginmanager):
|
||||
#: access to command line option as attributes.
|
||||
#: (deprecated), use :py:func:`getoption() <_pytest.config.Config.getoption>` instead
|
||||
self.option = CmdOptions()
|
||||
_a = FILE_OR_DIR
|
||||
self._parser = Parser(
|
||||
usage="%%(prog)s [options] [%s] [%s] [...]" % (_a, _a),
|
||||
processopt=self._processopt,
|
||||
)
|
||||
#: a pluginmanager instance
|
||||
self.pluginmanager = pluginmanager
|
||||
self.trace = self.pluginmanager.trace.root.get("config")
|
||||
self._conftest = Conftest(onimport=self._onimportconftest)
|
||||
self.hook = self.pluginmanager.hook
|
||||
self._inicache = {}
|
||||
self._opt2dest = {}
|
||||
self._cleanup = []
|
||||
self.pluginmanager.register(self, "pytestconfig")
|
||||
self.pluginmanager.set_register_callback(self._register_plugin)
|
||||
self._configured = False
|
||||
|
||||
def _register_plugin(self, plugin, name):
|
||||
call_plugin = self.pluginmanager.call_plugin
|
||||
call_plugin(plugin, "pytest_addhooks",
|
||||
{'pluginmanager': self.pluginmanager})
|
||||
self.hook.pytest_plugin_registered(plugin=plugin,
|
||||
manager=self.pluginmanager)
|
||||
dic = call_plugin(plugin, "pytest_namespace", {}) or {}
|
||||
if dic:
|
||||
import pytest
|
||||
setns(pytest, dic)
|
||||
call_plugin(plugin, "pytest_addoption", {'parser': self._parser})
|
||||
if self._configured:
|
||||
call_plugin(plugin, "pytest_configure", {'config': self})
|
||||
|
||||
def do_configure(self):
|
||||
assert not self._configured
|
||||
self._configured = True
|
||||
self.hook.pytest_configure(config=self)
|
||||
|
||||
def do_unconfigure(self):
|
||||
assert self._configured
|
||||
self._configured = False
|
||||
self.hook.pytest_unconfigure(config=self)
|
||||
self.pluginmanager.ensure_shutdown()
|
||||
|
||||
def pytest_cmdline_parse(self, pluginmanager, args):
|
||||
assert self == pluginmanager.config, (self, pluginmanager.config)
|
||||
self.parse(args)
|
||||
return self
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
while config._cleanup:
|
||||
fin = config._cleanup.pop()
|
||||
fin()
|
||||
|
||||
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,
|
||||
excinfo=excinfo)
|
||||
if not py.builtin.any(res):
|
||||
for line in str(excrepr).split("\n"):
|
||||
sys.stderr.write("INTERNALERROR> %s\n" %line)
|
||||
sys.stderr.flush()
|
||||
|
||||
|
||||
@classmethod
|
||||
def fromdictargs(cls, option_dict, args):
|
||||
""" constructor useable for subprocesses. """
|
||||
pluginmanager = get_plugin_manager()
|
||||
config = pluginmanager.config
|
||||
config._preparse(args, addopts=False)
|
||||
config.option.__dict__.update(option_dict)
|
||||
for x in config.option.plugins:
|
||||
config.pluginmanager.consider_pluginarg(x)
|
||||
return config
|
||||
|
||||
def _onimportconftest(self, conftestmodule):
|
||||
self.trace("loaded conftestmodule %r" %(conftestmodule,))
|
||||
self.pluginmanager.consider_conftest(conftestmodule)
|
||||
|
||||
def _processopt(self, opt):
|
||||
for name in opt._short_opts + opt._long_opts:
|
||||
self._opt2dest[name] = opt.dest
|
||||
|
||||
if hasattr(opt, 'default') and opt.dest:
|
||||
if not hasattr(self.option, opt.dest):
|
||||
setattr(self.option, opt.dest, opt.default)
|
||||
|
||||
def _getmatchingplugins(self, fspath):
|
||||
allconftests = self._conftest._conftestpath2mod.values()
|
||||
plugins = [x for x in self.pluginmanager.getplugins()
|
||||
if x not in allconftests]
|
||||
plugins += self._conftest.getconftestmodules(fspath)
|
||||
return plugins
|
||||
|
||||
def pytest_load_initial_conftests(self, parser, args):
|
||||
self._conftest.setinitial(args)
|
||||
pytest_load_initial_conftests.trylast = True
|
||||
|
||||
def _initini(self, args):
|
||||
self.inicfg = getcfg(args, ["pytest.ini", "tox.ini", "setup.cfg"])
|
||||
self._parser.addini('addopts', 'extra command line options', 'args')
|
||||
self._parser.addini('minversion', 'minimally required pytest version')
|
||||
|
||||
def _preparse(self, args, addopts=True):
|
||||
self._initini(args)
|
||||
if addopts:
|
||||
args[:] = self.getini("addopts") + args
|
||||
self._checkversion()
|
||||
self.pluginmanager.consider_preparse(args)
|
||||
self.pluginmanager.consider_setuptools_entrypoints()
|
||||
self.pluginmanager.consider_env()
|
||||
self.hook.pytest_load_initial_conftests(early_config=self,
|
||||
args=args, parser=self._parser)
|
||||
|
||||
def _checkversion(self):
|
||||
import pytest
|
||||
minver = self.inicfg.get('minversion', None)
|
||||
if minver:
|
||||
ver = minver.split(".")
|
||||
myver = pytest.__version__.split(".")
|
||||
if myver < ver:
|
||||
raise pytest.UsageError(
|
||||
"%s:%d: requires pytest-%s, actual pytest-%s'" %(
|
||||
self.inicfg.config.path, self.inicfg.lineof('minversion'),
|
||||
minver, pytest.__version__))
|
||||
|
||||
def parse(self, args):
|
||||
# parse given cmdline arguments into this config object.
|
||||
# Note that this can only be called once per testing process.
|
||||
assert not hasattr(self, 'args'), (
|
||||
"can only parse cmdline args at most once per Config object")
|
||||
self._origargs = args
|
||||
self._preparse(args)
|
||||
# XXX deprecated hook:
|
||||
self.hook.pytest_cmdline_preparse(config=self, args=args)
|
||||
self._parser.hints.extend(self.pluginmanager._hints)
|
||||
args = self._parser.parse_setoption(args, self.option)
|
||||
if not args:
|
||||
args.append(py.std.os.getcwd())
|
||||
self.args = args
|
||||
|
||||
def addinivalue_line(self, name, line):
|
||||
""" add a line to an ini-file option. The option must have been
|
||||
declared but might not yet be set in which case the line becomes the
|
||||
the first line in its value. """
|
||||
x = self.getini(name)
|
||||
assert isinstance(x, list)
|
||||
x.append(line) # modifies the cached list inline
|
||||
|
||||
def getini(self, name):
|
||||
""" return configuration value from an :ref:`ini file <inifiles>`. If the
|
||||
specified name hasn't been registered through a prior
|
||||
:py:func:`parser.addini <pytest.config.Parser.addini>`
|
||||
call (usually from a plugin), a ValueError is raised. """
|
||||
try:
|
||||
return self._inicache[name]
|
||||
except KeyError:
|
||||
self._inicache[name] = val = self._getini(name)
|
||||
return val
|
||||
|
||||
def _getini(self, name):
|
||||
try:
|
||||
description, type, default = self._parser._inidict[name]
|
||||
except KeyError:
|
||||
raise ValueError("unknown configuration value: %r" %(name,))
|
||||
try:
|
||||
value = self.inicfg[name]
|
||||
except KeyError:
|
||||
if default is not None:
|
||||
return default
|
||||
if type is None:
|
||||
return ''
|
||||
return []
|
||||
if type == "pathlist":
|
||||
dp = py.path.local(self.inicfg.config.path).dirpath()
|
||||
l = []
|
||||
for relpath in py.std.shlex.split(value):
|
||||
l.append(dp.join(relpath, abs=True))
|
||||
return l
|
||||
elif type == "args":
|
||||
return py.std.shlex.split(value)
|
||||
elif type == "linelist":
|
||||
return [t for t in map(lambda x: x.strip(), value.split("\n")) if t]
|
||||
else:
|
||||
assert type is None
|
||||
return value
|
||||
|
||||
def _getconftest_pathlist(self, name, path=None):
|
||||
try:
|
||||
mod, relroots = self._conftest.rget_with_confmod(name, path)
|
||||
except KeyError:
|
||||
return None
|
||||
modpath = py.path.local(mod.__file__).dirpath()
|
||||
l = []
|
||||
for relroot in relroots:
|
||||
if not isinstance(relroot, py.path.local):
|
||||
relroot = relroot.replace("/", py.path.local.sep)
|
||||
relroot = modpath.join(relroot, abs=True)
|
||||
l.append(relroot)
|
||||
return l
|
||||
|
||||
def _getconftest(self, name, path=None, check=False):
|
||||
if check:
|
||||
self._checkconftest(name)
|
||||
return self._conftest.rget(name, path)
|
||||
|
||||
def getoption(self, name):
|
||||
""" return command line option value.
|
||||
|
||||
:arg name: name of the option. You may also specify
|
||||
the literal ``--OPT`` option instead of the "dest" option name.
|
||||
"""
|
||||
name = self._opt2dest.get(name, name)
|
||||
try:
|
||||
return getattr(self.option, name)
|
||||
except AttributeError:
|
||||
raise ValueError("no option named %r" % (name,))
|
||||
|
||||
def getvalue(self, name, path=None):
|
||||
""" return command line option value.
|
||||
|
||||
:arg name: name of the command line option
|
||||
|
||||
(deprecated) if we can't find the option also lookup
|
||||
the name in a matching conftest file.
|
||||
"""
|
||||
try:
|
||||
return getattr(self.option, name)
|
||||
except AttributeError:
|
||||
return self._getconftest(name, path, check=False)
|
||||
|
||||
def getvalueorskip(self, name, path=None):
|
||||
""" (deprecated) return getvalue(name) or call
|
||||
py.test.skip if no value exists. """
|
||||
__tracebackhide__ = True
|
||||
try:
|
||||
val = self.getvalue(name, path)
|
||||
if val is None:
|
||||
raise KeyError(name)
|
||||
return val
|
||||
except KeyError:
|
||||
py.test.skip("no %r value found" %(name,))
|
||||
|
||||
def exists(path, ignore=EnvironmentError):
|
||||
try:
|
||||
return path.check()
|
||||
except ignore:
|
||||
return False
|
||||
|
||||
def getcfg(args, inibasenames):
|
||||
args = [x for x in args if not str(x).startswith("-")]
|
||||
if not args:
|
||||
args = [py.path.local()]
|
||||
for arg in args:
|
||||
arg = py.path.local(arg)
|
||||
for base in arg.parts(reverse=True):
|
||||
for inibasename in inibasenames:
|
||||
p = base.join(inibasename)
|
||||
if exists(p):
|
||||
iniconfig = py.iniconfig.IniConfig(p)
|
||||
if 'pytest' in iniconfig.sections:
|
||||
return iniconfig['pytest']
|
||||
return {}
|
||||
|
||||
|
||||
def setns(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)
|
||||
setns(mod, value)
|
||||
else:
|
||||
setattr(obj, name, value)
|
||||
obj.__all__.append(name)
|
||||
#if obj != pytest:
|
||||
# pytest.__all__.append(name)
|
||||
setattr(pytest, name, value)
|
||||
385
_pytest/core.py
Normal file
385
_pytest/core.py
Normal file
@@ -0,0 +1,385 @@
|
||||
"""
|
||||
pytest PluginManager, basic initialization and tracing.
|
||||
"""
|
||||
import sys
|
||||
import inspect
|
||||
import py
|
||||
|
||||
assert py.__version__.split(".")[:2] >= ['1', '4'], ("installation problem: "
|
||||
"%s is too old, remove or upgrade 'py'" % (py.__version__))
|
||||
|
||||
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, hookspecs=None):
|
||||
self._name2plugin = {}
|
||||
self._listattrcache = {}
|
||||
self._plugins = []
|
||||
self._hints = []
|
||||
self.trace = TagTracer().get("pluginmanage")
|
||||
self._plugin_distinfo = []
|
||||
self._shutdown = []
|
||||
self.hook = HookRelay(hookspecs or [], pm=self)
|
||||
|
||||
def do_configure(self, config):
|
||||
# backward compatibility
|
||||
config.do_configure()
|
||||
|
||||
def set_register_callback(self, callback):
|
||||
assert not hasattr(self, "_registercallback")
|
||||
self._registercallback = callback
|
||||
|
||||
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\n%s" %(
|
||||
name, plugin, self._name2plugin))
|
||||
#self.trace("registering", name, plugin)
|
||||
self._name2plugin[name] = plugin
|
||||
reg = getattr(self, "_registercallback", None)
|
||||
if reg is not None:
|
||||
reg(plugin, name)
|
||||
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)
|
||||
for name, value in list(self._name2plugin.items()):
|
||||
if value == plugin:
|
||||
del self._name2plugin[name]
|
||||
|
||||
def add_shutdown(self, func):
|
||||
self._shutdown.append(func)
|
||||
|
||||
def ensure_shutdown(self):
|
||||
while self._shutdown:
|
||||
func = self._shutdown.pop()
|
||||
func()
|
||||
self._plugins = []
|
||||
self._name2plugin.clear()
|
||||
self._listattrcache.clear()
|
||||
|
||||
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, prefix="pytest_"):
|
||||
self.hook._addhooks(spec, prefix=prefix)
|
||||
|
||||
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:
|
||||
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 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
|
||||
#print >>sys.stderr, "tryimport", mod
|
||||
__import__(mod)
|
||||
return sys.modules[mod]
|
||||
except ImportError:
|
||||
#e = py.std.sys.exc_info()[1]
|
||||
#if str(e).find(name) == -1:
|
||||
# raise
|
||||
pass #
|
||||
try:
|
||||
#print >>sys.stderr, "tryimport", importspec
|
||||
__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
|
||||
|
||||
120
_pytest/doctest.py
Normal file
120
_pytest/doctest.py
Normal file
@@ -0,0 +1,120 @@
|
||||
""" discover and run doctests in modules and test files."""
|
||||
|
||||
import pytest, py
|
||||
from _pytest.python import FixtureRequest, FuncFixtureInfo
|
||||
from py._code.code import TerminalRepr, ReprFileLocation
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("collect")
|
||||
group.addoption("--doctest-modules",
|
||||
action="store_true", default=False,
|
||||
help="run doctests in all .py modules",
|
||||
dest="doctestmodules")
|
||||
group.addoption("--doctest-glob",
|
||||
action="store", default="test*.txt", metavar="pat",
|
||||
help="doctests file matching pattern, default: test*.txt",
|
||||
dest="doctestglob")
|
||||
|
||||
def pytest_collect_file(path, parent):
|
||||
config = parent.config
|
||||
if path.ext == ".py":
|
||||
if config.option.doctestmodules:
|
||||
return DoctestModule(path, parent)
|
||||
elif (path.ext in ('.txt', '.rst') and parent.session.isinitpath(path)) or \
|
||||
path.check(fnmatch=config.getvalue("doctestglob")):
|
||||
return DoctestTextfile(path, parent)
|
||||
|
||||
class ReprFailDoctest(TerminalRepr):
|
||||
def __init__(self, reprlocation, lines):
|
||||
self.reprlocation = reprlocation
|
||||
self.lines = lines
|
||||
def toterminal(self, tw):
|
||||
for line in self.lines:
|
||||
tw.line(line)
|
||||
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):
|
||||
self.runner.run(self.dtest)
|
||||
|
||||
def repr_failure(self, excinfo):
|
||||
doctest = py.std.doctest
|
||||
if excinfo.errisinstance((doctest.DocTestFailure,
|
||||
doctest.UnexpectedException)):
|
||||
doctestfailure = excinfo.value
|
||||
example = doctestfailure.example
|
||||
test = doctestfailure.test
|
||||
filename = test.filename
|
||||
if test.lineno is None:
|
||||
lineno = None
|
||||
else:
|
||||
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
|
||||
filelines = py.path.local(filename).readlines(cr=0)
|
||||
lines = []
|
||||
if lineno is not None:
|
||||
i = max(test.lineno, max(0, lineno - 10)) # XXX?
|
||||
for line in filelines[i:lineno]:
|
||||
lines.append("%03d %s" % (i+1, line))
|
||||
i += 1
|
||||
else:
|
||||
lines.append('EXAMPLE LOCATION UNKNOWN, not showing all tests of that example')
|
||||
indent = '>>>'
|
||||
for line in example.source.splitlines():
|
||||
lines.append('??? %s %s' % (indent, line))
|
||||
indent = '...'
|
||||
if excinfo.errisinstance(doctest.DocTestFailure):
|
||||
lines += checker.output_difference(example,
|
||||
doctestfailure.got, REPORT_UDIFF).split("\n")
|
||||
else:
|
||||
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)
|
||||
return ReprFailDoctest(reprlocation, lines)
|
||||
else:
|
||||
return super(DoctestItem, self).repr_failure(excinfo)
|
||||
|
||||
def reportinfo(self):
|
||||
return self.fspath, None, "[doctest] %s" % self.name
|
||||
|
||||
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(pytest.File):
|
||||
def collect(self):
|
||||
doctest = py.std.doctest
|
||||
if self.fspath.basename == "conftest.py":
|
||||
module = self.config._conftest.importconftest(self.fspath)
|
||||
else:
|
||||
module = self.fspath.pyimport()
|
||||
# satisfy `FixtureRequest` constructor...
|
||||
self.funcargs = {}
|
||||
self._fixtureinfo = FuncFixtureInfo((), [], {})
|
||||
fixture_request = FixtureRequest(self)
|
||||
doctest_globals = dict(getfixture=fixture_request.getfuncargvalue)
|
||||
# uses internal doctest module parsing mechanism
|
||||
finder = doctest.DocTestFinder()
|
||||
runner = doctest.DebugRunner(verbose=0, optionflags=doctest.ELLIPSIS)
|
||||
for test in finder.find(module, module.__name__,
|
||||
extraglobs=doctest_globals):
|
||||
if test.examples: # skip empty doctests
|
||||
yield DoctestItem(test.name, self, runner, test)
|
||||
80
_pytest/genscript.py
Executable file
80
_pytest/genscript.py
Executable file
@@ -0,0 +1,80 @@
|
||||
""" generate a single-file self-contained version of py.test """
|
||||
import py
|
||||
import sys
|
||||
|
||||
def find_toplevel(name):
|
||||
for syspath in py.std.sys.path:
|
||||
base = py.path.local(syspath)
|
||||
lib = base/name
|
||||
if lib.check(dir=1):
|
||||
return lib
|
||||
mod = base.join("%s.py" % name)
|
||||
if mod.check(file=1):
|
||||
return mod
|
||||
raise LookupError(name)
|
||||
|
||||
def pkgname(toplevel, rootpath, path):
|
||||
parts = path.parts()[len(rootpath.parts()):]
|
||||
return '.'.join([toplevel] + [x.purebasename for x in parts])
|
||||
|
||||
def pkg_to_mapping(name):
|
||||
toplevel = find_toplevel(name)
|
||||
name2src = {}
|
||||
if toplevel.check(file=1): # module
|
||||
name2src[toplevel.purebasename] = toplevel.read()
|
||||
else: # package
|
||||
for pyfile in toplevel.visit('*.py'):
|
||||
pkg = pkgname(name, toplevel, pyfile)
|
||||
name2src[pkg] = pyfile.read()
|
||||
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)
|
||||
data = data.decode('ascii')
|
||||
return data
|
||||
|
||||
|
||||
def compress_packages(names):
|
||||
mapping = {}
|
||||
for name in names:
|
||||
mapping.update(pkg_to_mapping(name))
|
||||
return compress_mapping(mapping)
|
||||
|
||||
def generate_script(entry, packages):
|
||||
data = compress_packages(packages)
|
||||
tmpl = py.path.local(__file__).dirpath().join('standalonetemplate.py')
|
||||
exe = tmpl.read()
|
||||
exe = exe.replace('@SOURCES@', data)
|
||||
exe = exe.replace('@ENTRY@', entry)
|
||||
return exe
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("debugconfig")
|
||||
group.addoption("--genscript", action="store", default=None,
|
||||
dest="genscript", metavar="path",
|
||||
help="create standalone py.test script at given target path.")
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
genscript = config.getvalue("genscript")
|
||||
if genscript:
|
||||
tw = py.io.TerminalWriter()
|
||||
deps = ['py', '_pytest', 'pytest']
|
||||
if sys.version_info < (2,7):
|
||||
deps.append("argparse")
|
||||
tw.line("generated script will run on python2.5-python3.3++")
|
||||
else:
|
||||
tw.line("WARNING: generated script will not run on python2.6 "
|
||||
"or below due to 'argparse' dependency. Use python2.6 "
|
||||
"to generate a python2.5/6 compatible script", red=True)
|
||||
script = generate_script(
|
||||
'import py; raise SystemExit(py.test.cmdline.main())',
|
||||
deps,
|
||||
)
|
||||
genscript = py.path.local(genscript)
|
||||
genscript.write(script)
|
||||
tw.line("generated pytest standalone script: %s" % genscript,
|
||||
bold=True)
|
||||
return 0
|
||||
200
_pytest/helpconfig.py
Normal file
200
_pytest/helpconfig.py
Normal file
@@ -0,0 +1,200 @@
|
||||
""" version info, help messages, tracing configuration. """
|
||||
import py
|
||||
import pytest
|
||||
import os, inspect, sys
|
||||
from _pytest.core import varnames
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup('debugconfig')
|
||||
group.addoption('--version', action="store_true",
|
||||
help="display pytest lib version and import information.")
|
||||
group._addoption("-h", "--help", action="store_true", dest="help",
|
||||
help="show help message and configuration info")
|
||||
group._addoption('-p', action="append", dest="plugins", default = [],
|
||||
metavar="name",
|
||||
help="early-load given plugin (multi-allowed).")
|
||||
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()
|
||||
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)),
|
||||
os.getcwd(), config._origargs))
|
||||
config.trace.root.setwriter(f.write)
|
||||
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 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" %
|
||||
(pytest.__version__, p))
|
||||
plugininfo = getpluginversioninfo(config)
|
||||
if plugininfo:
|
||||
for line in plugininfo:
|
||||
sys.stderr.write(line + "\n")
|
||||
return 0
|
||||
elif config.option.help:
|
||||
config.do_configure()
|
||||
showhelp(config)
|
||||
config.do_unconfigure()
|
||||
return 0
|
||||
|
||||
def showhelp(config):
|
||||
tw = py.io.TerminalWriter()
|
||||
tw.write(config._parser.optparser.format_help())
|
||||
tw.write(config._parser.optparser.format_epilog(None))
|
||||
tw.line()
|
||||
tw.line()
|
||||
#tw.sep( "=", "config file settings")
|
||||
tw.line("[pytest] ini-options in the next "
|
||||
"pytest.ini|tox.ini|setup.cfg file:")
|
||||
tw.line()
|
||||
|
||||
for name in config._parser._ininames:
|
||||
help, type, default = config._parser._inidict[name]
|
||||
if type is None:
|
||||
type = "string"
|
||||
spec = "%s (%s)" % (name, type)
|
||||
line = " %-24s %s" %(spec, help)
|
||||
tw.line(line[:tw.fullwidth])
|
||||
|
||||
tw.line() ; tw.line()
|
||||
#tw.sep("=")
|
||||
tw.line("to see available markers type: py.test --markers")
|
||||
tw.line("to see available fixtures type: py.test --fixtures")
|
||||
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'),
|
||||
]
|
||||
|
||||
def getpluginversioninfo(config):
|
||||
lines = []
|
||||
plugininfo = config.pluginmanager._plugin_distinfo
|
||||
if plugininfo:
|
||||
lines.append("setuptools registered plugins:")
|
||||
for dist, plugin in plugininfo:
|
||||
loc = getattr(plugin, '__file__', repr(plugin))
|
||||
content = "%s-%s at %s" % (dist.project_name, dist.version, loc)
|
||||
lines.append(" " + content)
|
||||
return lines
|
||||
|
||||
def pytest_report_header(config):
|
||||
lines = []
|
||||
if config.option.debug or config.option.traceconfig:
|
||||
lines.append("using: pytest-%s pylib-%s" %
|
||||
(pytest.__version__,py.__version__))
|
||||
|
||||
verinfo = getpluginversioninfo(config)
|
||||
if verinfo:
|
||||
lines.extend(verinfo)
|
||||
|
||||
if config.option.traceconfig:
|
||||
lines.append("active plugins:")
|
||||
plugins = []
|
||||
items = config.pluginmanager._name2plugin.items()
|
||||
for name, plugin in items:
|
||||
if hasattr(plugin, '__file__'):
|
||||
r = plugin.__file__
|
||||
else:
|
||||
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))
|
||||
)
|
||||
|
||||
259
_pytest/hookspec.py
Normal file
259
_pytest/hookspec.py
Normal file
@@ -0,0 +1,259 @@
|
||||
""" hook specifications for pytest plugins, invoked from main.py and builtin plugins. """
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Initialization
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def pytest_addhooks(pluginmanager):
|
||||
"""called at plugin load time to allow adding new hooks via a call to
|
||||
pluginmanager.registerhooks(module)."""
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
def pytest_cmdline_parse(pluginmanager, args):
|
||||
"""return initialized config object, parsing the specified args. """
|
||||
pytest_cmdline_parse.firstresult = True
|
||||
|
||||
def pytest_cmdline_preparse(config, args):
|
||||
"""(deprecated) modify command line arguments before option parsing. """
|
||||
|
||||
def pytest_addoption(parser):
|
||||
"""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.
|
||||
|
||||
:arg parser: To add command line options, call
|
||||
:py:func:`parser.addoption(...) <_pytest.config.Parser.addoption>`.
|
||||
To add ini-file values call :py:func:`parser.addini(...)
|
||||
<_pytest.config.Parser.addini>`.
|
||||
|
||||
Options can later be accessed through the
|
||||
:py:class:`config <_pytest.config.Config>` object, respectively:
|
||||
|
||||
- :py:func:`config.getoption(name) <_pytest.config.Config.getoption>` to
|
||||
retrieve the value of a command line option.
|
||||
|
||||
- :py:func:`config.getini(name) <_pytest.config.Config.getini>` to retrieve
|
||||
a value read from an ini-style file.
|
||||
|
||||
The config object is passed around on many internal objects via the ``.config``
|
||||
attribute or can be retrieved as the ``pytestconfig`` fixture or accessed
|
||||
via (deprecated) ``pytest.config``.
|
||||
"""
|
||||
|
||||
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_load_initial_conftests(args, early_config, parser):
|
||||
""" implements loading initial conftests.
|
||||
"""
|
||||
|
||||
def pytest_configure(config):
|
||||
""" called after command line options have been parsed
|
||||
and all plugins and initial conftest files been loaded.
|
||||
"""
|
||||
|
||||
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
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
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
|
||||
the items in-place."""
|
||||
|
||||
def pytest_collection_finish(session):
|
||||
""" called after collection has been performed and modified. """
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
needs to have the specified ``parent`` as a parent."""
|
||||
|
||||
# logging hooks for collection
|
||||
def pytest_collectstart(collector):
|
||||
""" collector starts collecting. """
|
||||
|
||||
def pytest_itemcollected(item):
|
||||
""" we just collected a test item. """
|
||||
|
||||
def pytest_collectreport(report):
|
||||
""" collector finished collecting. """
|
||||
|
||||
def pytest_deselected(items):
|
||||
""" called for test items deselected by keyword. """
|
||||
|
||||
def pytest_make_collect_report(collector):
|
||||
""" perform ``collector.collect()`` and return a CollectReport. """
|
||||
pytest_make_collect_report.firstresult = True
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Python test function related hooks
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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."""
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# generic runtest related hooks
|
||||
# -------------------------------------------------------------------------
|
||||
def pytest_itemstart(item, node=None):
|
||||
""" (deprecated, use pytest_runtest_logstart). """
|
||||
|
||||
def pytest_runtest_protocol(item, nextitem):
|
||||
""" implements the runtest_setup/call/teardown protocol for
|
||||
the given test item, including capturing exceptions and calling
|
||||
reporting hooks.
|
||||
|
||||
: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`.
|
||||
|
||||
: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. """
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
""" called before ``pytest_runtest_call(item)``. """
|
||||
|
||||
def pytest_runtest_call(item):
|
||||
""" called to execute the test ``item``. """
|
||||
|
||||
def pytest_runtest_teardown(item, nextitem):
|
||||
""" called after ``pytest_runtest_call``.
|
||||
|
||||
: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.
|
||||
"""
|
||||
|
||||
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
|
||||
the respective phase of executing a test. """
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# test session related hooks
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def pytest_sessionstart(session):
|
||||
""" before session.main() is called. """
|
||||
|
||||
def pytest_sessionfinish(session, exitstatus):
|
||||
""" whole test run finishes. """
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# hooks for customising the assert methods
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def pytest_assertrepr_compare(config, op, left, right):
|
||||
"""return explanation for comparisons in failing assert expressions.
|
||||
|
||||
Return None for no custom explanation, otherwise return a list
|
||||
of strings. The strings will be joined by newlines but any newlines
|
||||
*in* a string will be escaped. Note that all but the first line will
|
||||
be indented sligthly, the intention is for the first line to be a summary.
|
||||
"""
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# hooks for influencing reporting (invoked from _pytest_terminal)
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def pytest_report_header(config, startdir):
|
||||
""" return a string to be displayed as header info for terminal reporting."""
|
||||
|
||||
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. """
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# doctest hooks
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
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 pytest plugin got registered. """
|
||||
|
||||
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".
|
||||
"""
|
||||
|
||||
254
_pytest/impl
Normal file
254
_pytest/impl
Normal file
@@ -0,0 +1,254 @@
|
||||
Sorting per-resource
|
||||
-----------------------------
|
||||
|
||||
for any given set of items:
|
||||
|
||||
- collect items per session-scoped parametrized funcarg
|
||||
- re-order until items no parametrizations are mixed
|
||||
|
||||
examples:
|
||||
|
||||
test()
|
||||
test1(s1)
|
||||
test1(s2)
|
||||
test2()
|
||||
test3(s1)
|
||||
test3(s2)
|
||||
|
||||
gets sorted to:
|
||||
|
||||
test()
|
||||
test2()
|
||||
test1(s1)
|
||||
test3(s1)
|
||||
test1(s2)
|
||||
test3(s2)
|
||||
|
||||
|
||||
the new @setup functions
|
||||
--------------------------------------
|
||||
|
||||
Consider a given @setup-marked function::
|
||||
|
||||
@pytest.mark.setup(maxscope=SCOPE)
|
||||
def mysetup(request, arg1, arg2, ...)
|
||||
...
|
||||
request.addfinalizer(fin)
|
||||
...
|
||||
|
||||
then FUNCARGSET denotes the set of (arg1, arg2, ...) funcargs and
|
||||
all of its dependent funcargs. The mysetup function will execute
|
||||
for any matching test item once per scope.
|
||||
|
||||
The scope is determined as the minimum scope of all scopes of the args
|
||||
in FUNCARGSET and the given "maxscope".
|
||||
|
||||
If mysetup has been called and no finalizers have been called it is
|
||||
called "active".
|
||||
|
||||
Furthermore the following rules apply:
|
||||
|
||||
- if an arg value in FUNCARGSET is about to be torn down, the
|
||||
mysetup-registered finalizers will execute as well.
|
||||
|
||||
- There will never be two active mysetup invocations.
|
||||
|
||||
Example 1, session scope::
|
||||
|
||||
@pytest.mark.funcarg(scope="session", params=[1,2])
|
||||
def db(request):
|
||||
request.addfinalizer(db_finalize)
|
||||
|
||||
@pytest.mark.setup
|
||||
def mysetup(request, db):
|
||||
request.addfinalizer(mysetup_finalize)
|
||||
...
|
||||
|
||||
And a given test module:
|
||||
|
||||
def test_something():
|
||||
...
|
||||
def test_otherthing():
|
||||
pass
|
||||
|
||||
Here is what happens::
|
||||
|
||||
db(request) executes with request.param == 1
|
||||
mysetup(request, db) executes
|
||||
test_something() executes
|
||||
test_otherthing() executes
|
||||
mysetup_finalize() executes
|
||||
db_finalize() executes
|
||||
db(request) executes with request.param == 2
|
||||
mysetup(request, db) executes
|
||||
test_something() executes
|
||||
test_otherthing() executes
|
||||
mysetup_finalize() executes
|
||||
db_finalize() executes
|
||||
|
||||
Example 2, session/function scope::
|
||||
|
||||
@pytest.mark.funcarg(scope="session", params=[1,2])
|
||||
def db(request):
|
||||
request.addfinalizer(db_finalize)
|
||||
|
||||
@pytest.mark.setup(scope="function")
|
||||
def mysetup(request, db):
|
||||
...
|
||||
request.addfinalizer(mysetup_finalize)
|
||||
...
|
||||
|
||||
And a given test module:
|
||||
|
||||
def test_something():
|
||||
...
|
||||
def test_otherthing():
|
||||
pass
|
||||
|
||||
Here is what happens::
|
||||
|
||||
db(request) executes with request.param == 1
|
||||
mysetup(request, db) executes
|
||||
test_something() executes
|
||||
mysetup_finalize() executes
|
||||
mysetup(request, db) executes
|
||||
test_otherthing() executes
|
||||
mysetup_finalize() executes
|
||||
db_finalize() executes
|
||||
db(request) executes with request.param == 2
|
||||
mysetup(request, db) executes
|
||||
test_something() executes
|
||||
mysetup_finalize() executes
|
||||
mysetup(request, db) executes
|
||||
test_otherthing() executes
|
||||
mysetup_finalize() executes
|
||||
db_finalize() executes
|
||||
|
||||
|
||||
Example 3 - funcargs session-mix
|
||||
----------------------------------------
|
||||
|
||||
Similar with funcargs, an example::
|
||||
|
||||
@pytest.mark.funcarg(scope="session", params=[1,2])
|
||||
def db(request):
|
||||
request.addfinalizer(db_finalize)
|
||||
|
||||
@pytest.mark.funcarg(scope="function")
|
||||
def table(request, db):
|
||||
...
|
||||
request.addfinalizer(table_finalize)
|
||||
...
|
||||
|
||||
And a given test module:
|
||||
|
||||
def test_something(table):
|
||||
...
|
||||
def test_otherthing(table):
|
||||
pass
|
||||
def test_thirdthing():
|
||||
pass
|
||||
|
||||
Here is what happens::
|
||||
|
||||
db(request) executes with param == 1
|
||||
table(request, db)
|
||||
test_something(table)
|
||||
table_finalize()
|
||||
table(request, db)
|
||||
test_otherthing(table)
|
||||
table_finalize()
|
||||
db_finalize
|
||||
db(request) executes with param == 2
|
||||
table(request, db)
|
||||
test_something(table)
|
||||
table_finalize()
|
||||
table(request, db)
|
||||
test_otherthing(table)
|
||||
table_finalize()
|
||||
db_finalize
|
||||
test_thirdthing()
|
||||
|
||||
Data structures
|
||||
--------------------
|
||||
|
||||
pytest internally maintains a dict of active funcargs with cache, param,
|
||||
finalizer, (scopeitem?) information:
|
||||
|
||||
active_funcargs = dict()
|
||||
|
||||
if a parametrized "db" is activated:
|
||||
|
||||
active_funcargs["db"] = FuncargInfo(dbvalue, paramindex,
|
||||
FuncargFinalize(...), scopeitem)
|
||||
|
||||
if a test is torn down and the next test requires a differently
|
||||
parametrized "db":
|
||||
|
||||
for argname in item.callspec.params:
|
||||
if argname in active_funcargs:
|
||||
funcarginfo = active_funcargs[argname]
|
||||
if funcarginfo.param != item.callspec.params[argname]:
|
||||
funcarginfo.callfinalizer()
|
||||
del node2funcarg[funcarginfo.scopeitem]
|
||||
del active_funcargs[argname]
|
||||
nodes_to_be_torn_down = ...
|
||||
for node in nodes_to_be_torn_down:
|
||||
if node in node2funcarg:
|
||||
argname = node2funcarg[node]
|
||||
active_funcargs[argname].callfinalizer()
|
||||
del node2funcarg[node]
|
||||
del active_funcargs[argname]
|
||||
|
||||
if a test is setup requiring a "db" funcarg:
|
||||
|
||||
if "db" in active_funcargs:
|
||||
return active_funcargs["db"][0]
|
||||
funcarginfo = setup_funcarg()
|
||||
active_funcargs["db"] = funcarginfo
|
||||
node2funcarg[funcarginfo.scopeitem] = "db"
|
||||
|
||||
Implementation plan for resources
|
||||
------------------------------------------
|
||||
|
||||
1. Revert FuncargRequest to the old form, unmerge item/request
|
||||
(done)
|
||||
2. make funcarg factories be discovered at collection time
|
||||
3. Introduce funcarg marker
|
||||
4. Introduce funcarg scope parameter
|
||||
5. Introduce funcarg parametrize parameter
|
||||
6. make setup functions be discovered at collection time
|
||||
7. (Introduce a pytest_fixture_protocol/setup_funcargs hook)
|
||||
|
||||
methods and data structures
|
||||
--------------------------------
|
||||
|
||||
A FuncarcManager holds all information about funcarg definitions
|
||||
including parametrization and scope definitions. It implements
|
||||
a pytest_generate_tests hook which performs parametrization as appropriate.
|
||||
|
||||
as a simple example, let's consider a tree where a test function requires
|
||||
a "abc" funcarg and its factory defines it as parametrized and scoped
|
||||
for Modules. When collections hits the function item, it creates
|
||||
the metafunc object, and calls funcargdb.pytest_generate_tests(metafunc)
|
||||
which looks up available funcarg factories and their scope and parametrization.
|
||||
This information is equivalent to what can be provided today directly
|
||||
at the function site and it should thus be relatively straight forward
|
||||
to implement the additional way of defining parametrization/scoping.
|
||||
|
||||
conftest loading:
|
||||
each funcarg-factory will populate the session.funcargmanager
|
||||
|
||||
When a test item is collected, it grows a dictionary
|
||||
(funcargname2factorycalllist). A factory lookup is performed
|
||||
for each required funcarg. The resulting factory call is stored
|
||||
with the item. If a function is parametrized multiple items are
|
||||
created with respective factory calls. Else if a factory is parametrized
|
||||
multiple items and calls to the factory function are created as well.
|
||||
|
||||
At setup time, an item populates a funcargs mapping, mapping names
|
||||
to values. If a value is funcarg factories are queried for a given item
|
||||
test functions and setup functions are put in a class
|
||||
which looks up required funcarg factories.
|
||||
|
||||
|
||||
230
_pytest/junitxml.py
Normal file
230
_pytest/junitxml.py
Normal file
@@ -0,0 +1,230 @@
|
||||
""" report test results in JUnit-XML format, for use with Hudson and build integration servers.
|
||||
|
||||
Based on initial code from Ross Lawley.
|
||||
"""
|
||||
|
||||
import py
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
# Python 2.X and 3.X compatibility
|
||||
try:
|
||||
unichr(65)
|
||||
except NameError:
|
||||
unichr = chr
|
||||
try:
|
||||
unicode('A')
|
||||
except NameError:
|
||||
unicode = str
|
||||
try:
|
||||
long(1)
|
||||
except NameError:
|
||||
long = int
|
||||
|
||||
|
||||
class Junit(py.xml.Namespace):
|
||||
pass
|
||||
|
||||
|
||||
# We need to get the subset of the invalid unicode ranges according to
|
||||
# XML 1.0 which are valid in this python build. Hence we calculate
|
||||
# this dynamically instead of hardcoding it. The spec range of valid
|
||||
# chars is: Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD]
|
||||
# | [#x10000-#x10FFFF]
|
||||
_legal_chars = (0x09, 0x0A, 0x0d)
|
||||
_legal_ranges = (
|
||||
(0x20, 0x7E),
|
||||
(0x80, 0xD7FF),
|
||||
(0xE000, 0xFFFD),
|
||||
(0x10000, 0x10FFFF),
|
||||
)
|
||||
_legal_xml_re = [unicode("%s-%s") % (unichr(low), unichr(high))
|
||||
for (low, high) in _legal_ranges
|
||||
if low < sys.maxunicode]
|
||||
_legal_xml_re = [unichr(x) for x in _legal_chars] + _legal_xml_re
|
||||
illegal_xml_re = re.compile(unicode('[^%s]') %
|
||||
unicode('').join(_legal_xml_re))
|
||||
del _legal_chars
|
||||
del _legal_ranges
|
||||
del _legal_xml_re
|
||||
|
||||
def bin_xml_escape(arg):
|
||||
def repl(matchobj):
|
||||
i = ord(matchobj.group())
|
||||
if i <= 0xFF:
|
||||
return unicode('#x%02X') % i
|
||||
else:
|
||||
return unicode('#x%04X') % i
|
||||
return py.xml.raw(illegal_xml_re.sub(repl, py.xml.escape(arg)))
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("terminal reporting")
|
||||
group.addoption('--junitxml', '--junit-xml', action="store",
|
||||
dest="xmlpath", metavar="path", default=None,
|
||||
help="create junit-xml style report file at given path.")
|
||||
group.addoption('--junitprefix', '--junit-prefix', action="store",
|
||||
metavar="str", default=None,
|
||||
help="prepend prefix to classnames in junit-xml output")
|
||||
|
||||
def pytest_configure(config):
|
||||
xmlpath = config.option.xmlpath
|
||||
# prevent opening xmllog on slave nodes (xdist)
|
||||
if xmlpath and not hasattr(config, 'slaveinput'):
|
||||
config._xml = LogXML(xmlpath, config.option.junitprefix)
|
||||
config.pluginmanager.register(config._xml)
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
xml = getattr(config, '_xml', None)
|
||||
if xml:
|
||||
del config._xml
|
||||
config.pluginmanager.unregister(xml)
|
||||
|
||||
|
||||
def mangle_testnames(names):
|
||||
names = [x.replace(".py", "") for x in names if x != '()']
|
||||
names[0] = names[0].replace("/", '.')
|
||||
return names
|
||||
|
||||
class LogXML(object):
|
||||
def __init__(self, logfile, prefix):
|
||||
logfile = os.path.expanduser(os.path.expandvars(logfile))
|
||||
self.logfile = os.path.normpath(os.path.abspath(logfile))
|
||||
self.prefix = prefix
|
||||
self.tests = []
|
||||
self.passed = self.skipped = 0
|
||||
self.failed = self.errors = 0
|
||||
|
||||
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=bin_xml_escape(names[-1]),
|
||||
time=getattr(report, 'duration', 0)
|
||||
))
|
||||
|
||||
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)))
|
||||
|
||||
def append(self, obj):
|
||||
self.tests[-1].append(obj)
|
||||
|
||||
def append_pass(self, report):
|
||||
self.passed += 1
|
||||
self._write_captured_output(report)
|
||||
|
||||
def append_failure(self, report):
|
||||
#msg = str(report.longrepr.reprtraceback.extraline)
|
||||
if hasattr(report, "wasxfail"):
|
||||
self.append(
|
||||
Junit.skipped(message="xfail-marked test passes unexpectedly"))
|
||||
self.skipped += 1
|
||||
else:
|
||||
fail = Junit.failure(message="test failure")
|
||||
fail.append(str(report.longrepr))
|
||||
self.append(fail)
|
||||
self.failed += 1
|
||||
self._write_captured_output(report)
|
||||
|
||||
def append_collect_failure(self, report):
|
||||
#msg = str(report.longrepr.reprtraceback.extraline)
|
||||
self.append(Junit.failure(str(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),
|
||||
message="collection skipped"))
|
||||
self.skipped += 1
|
||||
|
||||
def append_error(self, report):
|
||||
self.append(Junit.error(str(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),
|
||||
message="expected test failure"))
|
||||
else:
|
||||
filename, lineno, skipreason = report.longrepr
|
||||
if skipreason.startswith("Skipped: "):
|
||||
skipreason = skipreason[9:]
|
||||
self.append(
|
||||
Junit.skipped("%s:%s: %s" % report.longrepr,
|
||||
type="pytest.skip",
|
||||
message=skipreason
|
||||
))
|
||||
self.skipped += 1
|
||||
self._write_captured_output(report)
|
||||
|
||||
def pytest_runtest_logreport(self, report):
|
||||
if report.passed:
|
||||
if report.when == "call": # ignore setup/teardown
|
||||
self._opentestcase(report)
|
||||
self.append_pass(report)
|
||||
elif report.failed:
|
||||
self._opentestcase(report)
|
||||
if report.when != "call":
|
||||
self.append_error(report)
|
||||
else:
|
||||
self.append_failure(report)
|
||||
elif report.skipped:
|
||||
self._opentestcase(report)
|
||||
self.append_skipped(report)
|
||||
|
||||
def pytest_collectreport(self, report):
|
||||
if not report.passed:
|
||||
self._opentestcase(report)
|
||||
if report.failed:
|
||||
self.append_collect_failure(report)
|
||||
else:
|
||||
self.append_collect_skipped(report)
|
||||
|
||||
def pytest_internalerror(self, excrepr):
|
||||
self.errors += 1
|
||||
data = py.xml.escape(excrepr)
|
||||
self.tests.append(
|
||||
Junit.testcase(
|
||||
Junit.error(data, message="internal error"),
|
||||
classname="pytest",
|
||||
name="internal"))
|
||||
|
||||
def pytest_sessionstart(self, session):
|
||||
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')
|
||||
|
||||
suite_stop_time = time.time()
|
||||
suite_time_delta = suite_stop_time - self.suite_start_time
|
||||
numtests = self.passed + self.failed
|
||||
|
||||
logfile.write('<?xml version="1.0" encoding="utf-8"?>')
|
||||
logfile.write(Junit.testsuite(
|
||||
self.tests,
|
||||
name="pytest",
|
||||
errors=self.errors,
|
||||
failures=self.failed,
|
||||
skips=self.skipped,
|
||||
tests=numtests,
|
||||
time="%.3f" % suite_time_delta,
|
||||
).unicode(indent=0))
|
||||
logfile.close()
|
||||
|
||||
def pytest_terminal_summary(self, terminalreporter):
|
||||
terminalreporter.write_sep("-", "generated xml file: %s" % (self.logfile))
|
||||
708
_pytest/main.py
Normal file
708
_pytest/main.py
Normal file
@@ -0,0 +1,708 @@
|
||||
""" core implementation of testing process: init, session, runtest loop. """
|
||||
|
||||
import py
|
||||
import pytest, _pytest
|
||||
import os, sys, imp
|
||||
try:
|
||||
from collections import MutableMapping as MappingMixin
|
||||
except ImportError:
|
||||
from UserDict import DictMixin as MappingMixin
|
||||
|
||||
from _pytest.runner import collect_one_node, Skipped
|
||||
|
||||
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
|
||||
|
||||
# exitcodes for the command line
|
||||
EXIT_OK = 0
|
||||
EXIT_TESTSFAILED = 1
|
||||
EXIT_INTERRUPTED = 2
|
||||
EXIT_INTERNALERROR = 3
|
||||
EXIT_USAGEERROR = 4
|
||||
|
||||
name_re = py.std.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}'))
|
||||
#parser.addini("dirpatterns",
|
||||
# "patterns specifying possible locations of test files",
|
||||
# type="linelist", default=["**/test_*.txt",
|
||||
# "**/test_*.py", "**/*_test.py"]
|
||||
#)
|
||||
group = parser.getgroup("general", "running and selection options")
|
||||
group._addoption('-x', '--exitfirst', action="store_true", default=False,
|
||||
dest="exitfirst",
|
||||
help="exit instantly on first error or failed test."),
|
||||
group._addoption('--maxfail', metavar="num",
|
||||
action="store", type=int, dest="maxfail", default=0,
|
||||
help="exit after first num failures or errors.")
|
||||
group._addoption('--strict', action="store_true",
|
||||
help="run pytest in strict mode, warnings become errors.")
|
||||
|
||||
group = parser.getgroup("collect", "collection")
|
||||
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 = parser.getgroup("debugconfig",
|
||||
"test session debugging and configuration")
|
||||
group.addoption('--basetemp', dest="basetemp", default=None, metavar="dir",
|
||||
help="base temporary directory for this test run.")
|
||||
|
||||
|
||||
def pytest_namespace():
|
||||
collect = dict(Item=Item, Collector=Collector, File=File, Session=Session)
|
||||
return dict(collect=collect)
|
||||
|
||||
def pytest_configure(config):
|
||||
py.test.config = config # compatibiltiy
|
||||
if config.option.exitfirst:
|
||||
config.option.maxfail = 1
|
||||
|
||||
def wrap_session(config, doit):
|
||||
"""Skeleton command line program"""
|
||||
session = Session(config)
|
||||
session.exitstatus = EXIT_OK
|
||||
initstate = 0
|
||||
try:
|
||||
try:
|
||||
config.do_configure()
|
||||
initstate = 1
|
||||
config.hook.pytest_sessionstart(session=session)
|
||||
initstate = 2
|
||||
doit(config, session)
|
||||
except pytest.UsageError:
|
||||
msg = sys.exc_info()[1].args[0]
|
||||
sys.stderr.write("ERROR: %s\n" %(msg,))
|
||||
session.exitstatus = EXIT_USAGEERROR
|
||||
except KeyboardInterrupt:
|
||||
excinfo = py.code.ExceptionInfo()
|
||||
config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
|
||||
session.exitstatus = EXIT_INTERRUPTED
|
||||
except:
|
||||
excinfo = py.code.ExceptionInfo()
|
||||
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:
|
||||
session.startdir.chdir()
|
||||
if initstate >= 2:
|
||||
config.hook.pytest_sessionfinish(
|
||||
session=session,
|
||||
exitstatus=session.exitstatus)
|
||||
if initstate >= 1:
|
||||
config.do_unconfigure()
|
||||
config.pluginmanager.ensure_shutdown()
|
||||
return session.exitstatus
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
return wrap_session(config, _main)
|
||||
|
||||
def _main(config, session):
|
||||
""" default command line protocol for initialization, session,
|
||||
running tests and reporting. """
|
||||
config.hook.pytest_collection(session=session)
|
||||
config.hook.pytest_runtestloop(session=session)
|
||||
|
||||
def pytest_collection(session):
|
||||
return session.perform_collect()
|
||||
|
||||
def pytest_runtestloop(session):
|
||||
if session.config.option.collectonly:
|
||||
return True
|
||||
|
||||
def getnextitem(i):
|
||||
# this is a function to avoid python2
|
||||
# keeping sys.exc_info set when calling into a test
|
||||
# python2 keeps sys.exc_info till the frame is left
|
||||
try:
|
||||
return session.items[i+1]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
for i, item in enumerate(session.items):
|
||||
nextitem = getnextitem(i)
|
||||
item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
|
||||
if session.shouldstop:
|
||||
raise session.Interrupted(session.shouldstop)
|
||||
return True
|
||||
|
||||
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")
|
||||
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):
|
||||
self.fspath = fspath
|
||||
self.config = config
|
||||
|
||||
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
|
||||
|
||||
def compatproperty(name):
|
||||
def fget(self):
|
||||
# deprecated - use pytest.name
|
||||
return getattr(pytest, name)
|
||||
|
||||
return property(fget)
|
||||
|
||||
class NodeKeywords(MappingMixin):
|
||||
def __init__(self, node):
|
||||
self.node = node
|
||||
self.parent = node.parent
|
||||
self._markers = {node.name: True}
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return self._markers[key]
|
||||
except KeyError:
|
||||
if self.parent is None:
|
||||
raise
|
||||
return self.parent.keywords[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._markers[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
raise ValueError("cannot delete key in keywords dict")
|
||||
|
||||
def __iter__(self):
|
||||
seen = set(self._markers)
|
||||
if self.parent is not None:
|
||||
seen.update(self.parent.keywords)
|
||||
return iter(seen)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.__iter__())
|
||||
|
||||
def keys(self):
|
||||
return list(self)
|
||||
|
||||
def __repr__(self):
|
||||
return "<NodeKeywords for node %s>" % (self.node, )
|
||||
|
||||
|
||||
class Node(object):
|
||||
""" base class for Collector and Item the test collection tree.
|
||||
Collector subclasses have children, Items are terminal nodes."""
|
||||
|
||||
def __init__(self, name, parent=None, config=None, session=None):
|
||||
#: a unique name within the scope of the parent node
|
||||
self.name = name
|
||||
|
||||
#: the parent collector node.
|
||||
self.parent = parent
|
||||
|
||||
#: the pytest config object
|
||||
self.config = config or parent.config
|
||||
|
||||
#: the session this node is part of
|
||||
self.session = session or parent.session
|
||||
|
||||
#: filesystem path where this node was collected from (can be None)
|
||||
self.fspath = getattr(parent, 'fspath', None)
|
||||
|
||||
#: keywords/markers collected from all scopes
|
||||
self.keywords = NodeKeywords(self)
|
||||
|
||||
#: allow adding of extra keywords to use for matching
|
||||
self.extra_keyword_matches = set()
|
||||
|
||||
#self.extrainit()
|
||||
|
||||
@property
|
||||
def ihook(self):
|
||||
""" fspath sensitive hook proxy used to call pytest hooks"""
|
||||
return self.session.gethookproxy(self.fspath)
|
||||
|
||||
#def extrainit(self):
|
||||
# """"extra initialization after Node is initialized. Implemented
|
||||
# by some subclasses. """
|
||||
|
||||
Module = compatproperty("Module")
|
||||
Class = compatproperty("Class")
|
||||
Instance = compatproperty("Instance")
|
||||
Function = compatproperty("Function")
|
||||
File = compatproperty("File")
|
||||
Item = compatproperty("Item")
|
||||
|
||||
def _getcustomclass(self, name):
|
||||
cls = getattr(self, name)
|
||||
if cls != getattr(pytest, name):
|
||||
py.log._apiwarn("2.0", "use of node.%s is deprecated, "
|
||||
"use pytest_pycollect_makeitem(...) to create custom "
|
||||
"collection nodes" % name)
|
||||
return cls
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s %r>" %(self.__class__.__name__,
|
||||
getattr(self, 'name', None))
|
||||
|
||||
# methods for ordering nodes
|
||||
@property
|
||||
def nodeid(self):
|
||||
""" a ::-separated string denoting its collection tree address. """
|
||||
try:
|
||||
return self._nodeid
|
||||
except AttributeError:
|
||||
self._nodeid = x = self._makeid()
|
||||
return x
|
||||
|
||||
|
||||
def _makeid(self):
|
||||
return self.parent.nodeid + "::" + self.name
|
||||
|
||||
def __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))
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
|
||||
def teardown(self):
|
||||
pass
|
||||
|
||||
def _memoizedcall(self, attrname, function):
|
||||
exattrname = "_ex_" + attrname
|
||||
failure = getattr(self, exattrname, None)
|
||||
if failure is not None:
|
||||
py.builtin._reraise(failure[0], failure[1], failure[2])
|
||||
if hasattr(self, attrname):
|
||||
return getattr(self, attrname)
|
||||
try:
|
||||
res = function()
|
||||
except py.builtin._sysex:
|
||||
raise
|
||||
except:
|
||||
failure = py.std.sys.exc_info()
|
||||
setattr(self, exattrname, failure)
|
||||
raise
|
||||
setattr(self, attrname, res)
|
||||
return res
|
||||
|
||||
def listchain(self):
|
||||
""" return list of all parent collectors up to self,
|
||||
starting from root of collection tree. """
|
||||
chain = []
|
||||
item = self
|
||||
while item is not None:
|
||||
chain.append(item)
|
||||
item = item.parent
|
||||
chain.reverse()
|
||||
return chain
|
||||
|
||||
def add_marker(self, marker):
|
||||
""" dynamically add a marker object to the node.
|
||||
|
||||
``marker`` can be a string or pytest.mark.* instance.
|
||||
"""
|
||||
from _pytest.mark import MarkDecorator
|
||||
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):
|
||||
current = self
|
||||
while current and not isinstance(current, cls):
|
||||
current = current.parent
|
||||
return current
|
||||
|
||||
def _prunetraceback(self, excinfo):
|
||||
pass
|
||||
|
||||
def _repr_failure_py(self, excinfo, style=None):
|
||||
fm = self.session._fixturemanager
|
||||
if excinfo.errisinstance(fm.FixtureLookupError):
|
||||
return excinfo.value.formatrepr()
|
||||
if self.config.option.fulltrace:
|
||||
style="long"
|
||||
else:
|
||||
self._prunetraceback(excinfo)
|
||||
# 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)
|
||||
|
||||
repr_failure = _repr_failure_py
|
||||
|
||||
class Collector(Node):
|
||||
""" Collector instances create children through collect()
|
||||
and thus iteratively build a tree.
|
||||
"""
|
||||
|
||||
# the set of exceptions to interpret as "Skip the whole module" during
|
||||
# collection
|
||||
skip_exceptions = (Skipped,)
|
||||
|
||||
class CollectError(Exception):
|
||||
""" an error during collection, contains a custom message. """
|
||||
|
||||
def collect(self):
|
||||
""" returns a list of children (items and collectors)
|
||||
for this collection node.
|
||||
"""
|
||||
raise NotImplementedError("abstract")
|
||||
|
||||
def repr_failure(self, excinfo):
|
||||
""" represent a collection failure. """
|
||||
if excinfo.errisinstance(self.CollectError):
|
||||
exc = excinfo.value
|
||||
return str(exc.args[0])
|
||||
return self._repr_failure_py(excinfo, style="short")
|
||||
|
||||
def _memocollect(self):
|
||||
""" internal helper method to cache results of calling collect(). """
|
||||
return self._memoizedcall('_collected', lambda: list(self.collect()))
|
||||
|
||||
def _prunetraceback(self, excinfo):
|
||||
if hasattr(self, 'fspath'):
|
||||
path = self.fspath
|
||||
traceback = excinfo.traceback
|
||||
ntraceback = traceback.cut(path=self.fspath)
|
||||
if ntraceback == traceback:
|
||||
ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
|
||||
excinfo.traceback = ntraceback.filter()
|
||||
|
||||
class FSCollector(Collector):
|
||||
def __init__(self, fspath, parent=None, config=None, session=None):
|
||||
fspath = py.path.local(fspath) # xxx only for test_resultlog.py?
|
||||
name = fspath.basename
|
||||
if parent is not None:
|
||||
rel = fspath.relto(parent.fspath)
|
||||
if rel:
|
||||
name = rel
|
||||
name = name.replace(os.sep, "/")
|
||||
super(FSCollector, self).__init__(name, parent, config, session)
|
||||
self.fspath = fspath
|
||||
|
||||
def _makeid(self):
|
||||
if self == self.session:
|
||||
return "."
|
||||
relpath = self.session.fspath.bestrelpath(self.fspath)
|
||||
if os.sep != "/":
|
||||
relpath = relpath.replace(os.sep, "/")
|
||||
return relpath
|
||||
|
||||
class File(FSCollector):
|
||||
""" base class for collecting tests from a file. """
|
||||
|
||||
class Item(Node):
|
||||
""" a basic test invocation item. Note that for a single function
|
||||
there might be multiple test invocation items.
|
||||
"""
|
||||
nextitem = None
|
||||
|
||||
def reportinfo(self):
|
||||
return self.fspath, None, ""
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
try:
|
||||
return self._location
|
||||
except AttributeError:
|
||||
location = self.reportinfo()
|
||||
# bestrelpath is a quite slow function
|
||||
cache = self.config.__dict__.setdefault("_bestrelpathcache", {})
|
||||
try:
|
||||
fspath = cache[location[0]]
|
||||
except KeyError:
|
||||
fspath = self.session.fspath.bestrelpath(location[0])
|
||||
cache[location[0]] = fspath
|
||||
location = (fspath, location[1], str(location[2]))
|
||||
self._location = location
|
||||
return location
|
||||
|
||||
class NoMatch(Exception):
|
||||
""" raised if matching cannot locate a matching names. """
|
||||
|
||||
class Session(FSCollector):
|
||||
class Interrupted(KeyboardInterrupt):
|
||||
""" signals an interrupted test run. """
|
||||
__module__ = 'builtins' # for py3
|
||||
|
||||
def __init__(self, config):
|
||||
FSCollector.__init__(self, py.path.local(), parent=None,
|
||||
config=config, session=self)
|
||||
self.config.pluginmanager.register(self, name="session", prepend=True)
|
||||
self._testsfailed = 0
|
||||
self.shouldstop = False
|
||||
self.trace = config.trace.root.get("collection")
|
||||
self._norecursepatterns = config.getini("norecursedirs")
|
||||
self.startdir = py.path.local()
|
||||
|
||||
def pytest_collectstart(self):
|
||||
if self.shouldstop:
|
||||
raise self.Interrupted(self.shouldstop)
|
||||
|
||||
def pytest_runtest_logreport(self, report):
|
||||
if report.failed and not hasattr(report, 'wasxfail'):
|
||||
self._testsfailed += 1
|
||||
maxfail = self.config.getvalue("maxfail")
|
||||
if maxfail and self._testsfailed >= maxfail:
|
||||
self.shouldstop = "stopping after %d failures" % (
|
||||
self._testsfailed)
|
||||
pytest_collectreport = pytest_runtest_logreport
|
||||
|
||||
def isinitpath(self, path):
|
||||
return path in self._initialpaths
|
||||
|
||||
def gethookproxy(self, fspath):
|
||||
return HookProxy(fspath, self.config)
|
||||
|
||||
def perform_collect(self, args=None, genitems=True):
|
||||
hook = self.config.hook
|
||||
try:
|
||||
items = self._perform_collect(args, genitems)
|
||||
hook.pytest_collection_modifyitems(session=self,
|
||||
config=self.config, items=items)
|
||||
finally:
|
||||
hook.pytest_collection_finish(session=self)
|
||||
return items
|
||||
|
||||
def _perform_collect(self, args, genitems):
|
||||
if args is None:
|
||||
args = self.config.args
|
||||
self.trace("perform_collect", self, args)
|
||||
self.trace.root.indent += 1
|
||||
self._notfound = []
|
||||
self._initialpaths = set()
|
||||
self._initialparts = []
|
||||
self.items = items = []
|
||||
for arg in args:
|
||||
parts = self._parsearg(arg)
|
||||
self._initialparts.append(parts)
|
||||
self._initialpaths.add(parts[0])
|
||||
rep = collect_one_node(self)
|
||||
self.ihook.pytest_collectreport(report=rep)
|
||||
self.trace.root.indent -= 1
|
||||
if self._notfound:
|
||||
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))
|
||||
if not genitems:
|
||||
return rep.result
|
||||
else:
|
||||
if rep.passed:
|
||||
for node in rep.result:
|
||||
self.items.extend(self.genitems(node))
|
||||
return items
|
||||
|
||||
def collect(self):
|
||||
for parts in self._initialparts:
|
||||
arg = "::".join(map(str, parts))
|
||||
self.trace("processing argument", arg)
|
||||
self.trace.root.indent += 1
|
||||
try:
|
||||
for x in self._collect(arg):
|
||||
yield x
|
||||
except NoMatch:
|
||||
# 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):
|
||||
names = self._parsearg(arg)
|
||||
path = names.pop(0)
|
||||
if path.check(dir=1):
|
||||
assert not names, "invalid arg %r" %(arg,)
|
||||
for path in path.visit(fil=lambda x: x.check(file=1),
|
||||
rec=self._recurse, bf=True, sort=True):
|
||||
for x in self._collectfile(path):
|
||||
yield x
|
||||
else:
|
||||
assert path.check(file=1)
|
||||
for x in self.matchnodes(self._collectfile(path), names):
|
||||
yield x
|
||||
|
||||
def _collectfile(self, path):
|
||||
ihook = self.gethookproxy(path)
|
||||
if not self.isinitpath(path):
|
||||
if ihook.pytest_ignore_collect(path=path, config=self.config):
|
||||
return ()
|
||||
return ihook.pytest_collect_file(path=path, parent=self)
|
||||
|
||||
def _recurse(self, path):
|
||||
ihook = self.gethookproxy(path.dirpath())
|
||||
if ihook.pytest_ignore_collect(path=path, config=self.config):
|
||||
return
|
||||
for pat in self._norecursepatterns:
|
||||
if path.check(fnmatch=pat):
|
||||
return False
|
||||
ihook = self.gethookproxy(path)
|
||||
ihook.pytest_collect_directory(path=path, parent=self)
|
||||
return True
|
||||
|
||||
def _tryconvertpyarg(self, x):
|
||||
mod = None
|
||||
path = [os.path.abspath('.')] + sys.path
|
||||
for name in x.split('.'):
|
||||
# ignore anything that's not a proper name here
|
||||
# else something like --pyargs will mess up '.'
|
||||
# since imp.find_module will actually sometimes work for it
|
||||
# but it's supposed to be considered a filesystem path
|
||||
# not a package
|
||||
if name_re.match(name) is None:
|
||||
return x
|
||||
try:
|
||||
fd, mod, type_ = imp.find_module(name, path)
|
||||
except ImportError:
|
||||
return x
|
||||
else:
|
||||
if fd is not None:
|
||||
fd.close()
|
||||
|
||||
if type_[2] != imp.PKG_DIRECTORY:
|
||||
path = [os.path.dirname(mod)]
|
||||
else:
|
||||
path = [mod]
|
||||
return mod
|
||||
|
||||
def _parsearg(self, arg):
|
||||
""" return (fspath, names) tuple after checking the file exists. """
|
||||
arg = str(arg)
|
||||
if self.config.option.pyargs:
|
||||
arg = self._tryconvertpyarg(arg)
|
||||
parts = str(arg).split("::")
|
||||
relpath = parts[0].replace("/", os.sep)
|
||||
path = self.fspath.join(relpath, abs=True)
|
||||
if not path.check():
|
||||
if self.config.option.pyargs:
|
||||
msg = "file or package not found: "
|
||||
else:
|
||||
msg = "file not found: "
|
||||
raise pytest.UsageError(msg + arg)
|
||||
parts[0] = path
|
||||
return parts
|
||||
|
||||
def matchnodes(self, matching, names):
|
||||
self.trace("matchnodes", matching, names)
|
||||
self.trace.root.indent += 1
|
||||
nodes = self._matchnodes(matching, names)
|
||||
num = len(nodes)
|
||||
self.trace("matchnodes finished -> ", num, "nodes")
|
||||
self.trace.root.indent -= 1
|
||||
if num == 0:
|
||||
raise NoMatch(matching, names[:1])
|
||||
return nodes
|
||||
|
||||
def _matchnodes(self, matching, names):
|
||||
if not matching or not names:
|
||||
return matching
|
||||
name = names[0]
|
||||
assert name
|
||||
nextnames = names[1:]
|
||||
resultnodes = []
|
||||
for node in matching:
|
||||
if isinstance(node, pytest.Item):
|
||||
if not names:
|
||||
resultnodes.append(node)
|
||||
continue
|
||||
assert isinstance(node, pytest.Collector)
|
||||
rep = collect_one_node(node)
|
||||
if rep.passed:
|
||||
has_matched = False
|
||||
for x in rep.result:
|
||||
if x.name == name:
|
||||
resultnodes.extend(self.matchnodes([x], nextnames))
|
||||
has_matched = True
|
||||
# XXX accept IDs that don't have "()" for class instances
|
||||
if not has_matched and len(rep.result) == 1 and x.name == "()":
|
||||
nextnames.insert(0, name)
|
||||
resultnodes.extend(self.matchnodes([x], nextnames))
|
||||
node.ihook.pytest_collectreport(report=rep)
|
||||
return resultnodes
|
||||
|
||||
def genitems(self, node):
|
||||
self.trace("genitems", node)
|
||||
if isinstance(node, pytest.Item):
|
||||
node.ihook.pytest_itemcollected(item=node)
|
||||
yield node
|
||||
else:
|
||||
assert isinstance(node, pytest.Collector)
|
||||
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
|
||||
|
||||
272
_pytest/mark.py
Normal file
272
_pytest/mark.py
Normal file
@@ -0,0 +1,272 @@
|
||||
""" generic mechanism for marking and selecting python functions. """
|
||||
import py
|
||||
|
||||
|
||||
def pytest_namespace():
|
||||
return {'mark': MarkGenerator()}
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("general")
|
||||
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 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",
|
||||
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)."
|
||||
)
|
||||
|
||||
parser.addini("markers", "markers for test functions", 'linelist')
|
||||
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
if config.option.markers:
|
||||
config.do_configure()
|
||||
tw = py.io.TerminalWriter()
|
||||
for line in config.getini("markers"):
|
||||
name, rest = line.split(":", 1)
|
||||
tw.write("@pytest.mark.%s:" % name, bold=True)
|
||||
tw.line(rest)
|
||||
tw.line()
|
||||
config.do_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
|
||||
selectuntil = False
|
||||
if keywordexpr[-1:] == ":":
|
||||
selectuntil = True
|
||||
keywordexpr = keywordexpr[:-1]
|
||||
|
||||
remaining = []
|
||||
deselected = []
|
||||
for colitem in items:
|
||||
if keywordexpr and not matchkeyword(colitem, keywordexpr):
|
||||
deselected.append(colitem)
|
||||
else:
|
||||
if selectuntil:
|
||||
keywordexpr = None
|
||||
if matchexpr:
|
||||
if not matchmark(colitem, matchexpr):
|
||||
deselected.append(colitem)
|
||||
continue
|
||||
remaining.append(colitem)
|
||||
|
||||
if deselected:
|
||||
config.hook.pytest_deselected(items=deselected)
|
||||
items[:] = remaining
|
||||
|
||||
|
||||
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):
|
||||
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, markexpr):
|
||||
"""Tries to match on any marker names, attached to the given colitem."""
|
||||
return eval(markexpr, {}, MarkMapping(colitem.keywords))
|
||||
|
||||
|
||||
def matchkeyword(colitem, keywordexpr):
|
||||
"""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.
|
||||
"""
|
||||
keywordexpr = keywordexpr.replace("-", "not ")
|
||||
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)
|
||||
|
||||
return eval(keywordexpr, {}, KeywordMapping(mapped_names))
|
||||
|
||||
|
||||
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::
|
||||
|
||||
import py
|
||||
@py.test.mark.slowtest
|
||||
def test_function():
|
||||
pass
|
||||
|
||||
will set a 'slowtest' :class:`MarkInfo` object
|
||||
on the ``test_function`` object. """
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name[0] == "_":
|
||||
raise AttributeError(name)
|
||||
if hasattr(self, '_config'):
|
||||
self._check(name)
|
||||
return MarkDecorator(name)
|
||||
|
||||
def _check(self, name):
|
||||
try:
|
||||
if name in self._markers:
|
||||
return
|
||||
except AttributeError:
|
||||
pass
|
||||
self._markers = l = set()
|
||||
for line in self._config.getini("markers"):
|
||||
beginning = line.split(":", 1)
|
||||
x = beginning[0].split("(", 1)[0]
|
||||
l.add(x)
|
||||
if name not in self._markers:
|
||||
raise AttributeError("%r not a registered marker" % (name,))
|
||||
|
||||
|
||||
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
|
||||
|
||||
and can then be applied as decorators to test functions::
|
||||
|
||||
@mark2
|
||||
def test_function():
|
||||
pass
|
||||
"""
|
||||
def __init__(self, name, args=None, kwargs=None):
|
||||
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('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:
|
||||
func = args[0]
|
||||
if len(args) == 1 and hasattr(func, '__call__') or \
|
||||
hasattr(func, '__bases__'):
|
||||
if hasattr(func, '__bases__'):
|
||||
if hasattr(func, 'pytestmark'):
|
||||
l = func.pytestmark
|
||||
if not isinstance(l, list):
|
||||
func.pytestmark = [l, self]
|
||||
else:
|
||||
l.append(self)
|
||||
else:
|
||||
func.pytestmark = [self]
|
||||
else:
|
||||
holder = getattr(func, self.name, None)
|
||||
if holder is None:
|
||||
holder = MarkInfo(
|
||||
self.name, self.args, self.kwargs
|
||||
)
|
||||
setattr(func, self.name, holder)
|
||||
else:
|
||||
holder.add(self.args, self.kwargs)
|
||||
return func
|
||||
kw = self.kwargs.copy()
|
||||
kw.update(kwargs)
|
||||
args = self.args + args
|
||||
return self.__class__(self.name, args=args, kwargs=kw)
|
||||
|
||||
|
||||
class MarkInfo:
|
||||
""" Marking object created by :class:`MarkDecorator` instances. """
|
||||
def __init__(self, name, args, kwargs):
|
||||
#: name of attribute
|
||||
self.name = name
|
||||
#: positional argument list, empty if none specified
|
||||
self.args = args
|
||||
#: keyword argument dictionary, empty if nothing specified
|
||||
self.kwargs = kwargs
|
||||
self._arglist = [(args, kwargs.copy())]
|
||||
|
||||
def __repr__(self):
|
||||
return "<MarkInfo %r args=%r kwargs=%r>" % (
|
||||
self.name, self.args, self.kwargs
|
||||
)
|
||||
|
||||
def add(self, args, kwargs):
|
||||
""" add a MarkInfo with the given args and kwargs. """
|
||||
self._arglist.append((args, kwargs))
|
||||
self.args += args
|
||||
self.kwargs.update(kwargs)
|
||||
|
||||
def __iter__(self):
|
||||
""" yield MarkInfo objects each relating to a marking-call. """
|
||||
for args, kwargs in self._arglist:
|
||||
yield MarkInfo(self.name, args, kwargs)
|
||||
201
_pytest/monkeypatch.py
Normal file
201
_pytest/monkeypatch.py
Normal file
@@ -0,0 +1,201 @@
|
||||
""" monkeypatching and mocking functionality. """
|
||||
|
||||
import os, sys
|
||||
|
||||
def pytest_funcarg__monkeypatch(request):
|
||||
"""The returned ``monkeypatch`` funcarg provides these
|
||||
helper methods to modify objects, dictionaries or os.environ::
|
||||
|
||||
monkeypatch.setattr(obj, name, value, raising=True)
|
||||
monkeypatch.delattr(obj, name, raising=True)
|
||||
monkeypatch.setitem(mapping, name, value)
|
||||
monkeypatch.delitem(obj, name, raising=True)
|
||||
monkeypatch.setenv(name, value, prepend=False)
|
||||
monkeypatch.delenv(name, value, raising=True)
|
||||
monkeypatch.syspath_prepend(path)
|
||||
monkeypatch.chdir(path)
|
||||
|
||||
All modifications will be undone after the requesting
|
||||
test function has finished. The ``raising``
|
||||
parameter determines if a KeyError or AttributeError
|
||||
will be raised if the set/deletion operation has no target.
|
||||
"""
|
||||
mpatch = monkeypatch()
|
||||
request.addfinalizer(mpatch.undo)
|
||||
return mpatch
|
||||
|
||||
|
||||
|
||||
def derive_importpath(import_path):
|
||||
import pytest
|
||||
if not isinstance(import_path, str) 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]
|
||||
getattr(obj, attr)
|
||||
except AttributeError:
|
||||
__tracebackhide__ = True
|
||||
pytest.fail("object %r has no attribute %r" % (obj, attr))
|
||||
return attr, obj
|
||||
|
||||
|
||||
|
||||
notset = object()
|
||||
|
||||
class monkeypatch:
|
||||
""" object keeping a record of setattr/item/env/syspath changes. """
|
||||
def __init__(self):
|
||||
self._setattr = []
|
||||
self._setitem = []
|
||||
self._cwd = None
|
||||
|
||||
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, str):
|
||||
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)
|
||||
|
||||
oldval = getattr(target, name, notset)
|
||||
if raising and oldval is notset:
|
||||
raise AttributeError("%r has no attribute %r" %(target, name))
|
||||
|
||||
# avoid class descriptors like staticmethod/classmethod
|
||||
if inspect.isclass(target):
|
||||
oldval = target.__dict__.get(name, notset)
|
||||
self._setattr.insert(0, (target, name, oldval))
|
||||
setattr(target, name, value)
|
||||
|
||||
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, the attribute is allowed to not
|
||||
pre-exist.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
if name is notset:
|
||||
if not isinstance(target, str):
|
||||
raise TypeError("use delattr(target, name) or "
|
||||
"delattr(target) with target being a dotted "
|
||||
"import string")
|
||||
name, target = derive_importpath(target)
|
||||
|
||||
if not hasattr(target, name):
|
||||
if raising:
|
||||
raise AttributeError(name)
|
||||
else:
|
||||
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. """
|
||||
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."""
|
||||
if name not in dic:
|
||||
if raising:
|
||||
raise KeyError(name)
|
||||
else:
|
||||
self._setitem.insert(0, (dic, name, dic.get(name, notset)))
|
||||
del dic[name]
|
||||
|
||||
def setenv(self, name, value, prepend=None):
|
||||
""" set environment variable ``name`` to ``value``. if ``prepend``
|
||||
is a character, read the current environment variable value
|
||||
and prepend the ``value`` adjoined with the ``prepend`` character."""
|
||||
value = str(value)
|
||||
if prepend and name in os.environ:
|
||||
value = value + prepend + os.environ[name]
|
||||
self.setitem(os.environ, name, value)
|
||||
|
||||
def delenv(self, name, raising=True):
|
||||
""" delete ``name`` from environment, raise KeyError it not exists."""
|
||||
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'):
|
||||
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
|
||||
"""
|
||||
if self._cwd is None:
|
||||
self._cwd = os.getcwd()
|
||||
if hasattr(path, "chdir"):
|
||||
path.chdir()
|
||||
else:
|
||||
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."""
|
||||
for obj, name, value in self._setattr:
|
||||
if value is not notset:
|
||||
setattr(obj, name, value)
|
||||
else:
|
||||
delattr(obj, name)
|
||||
self._setattr[:] = []
|
||||
for dictionary, name, value in self._setitem:
|
||||
if value is notset:
|
||||
try:
|
||||
del dictionary[name]
|
||||
except KeyError:
|
||||
pass # was already deleted, so we have the desired state
|
||||
else:
|
||||
dictionary[name] = value
|
||||
self._setitem[:] = []
|
||||
if hasattr(self, '_savesyspath'):
|
||||
sys.path[:] = self._savesyspath
|
||||
del self._savesyspath
|
||||
|
||||
if self._cwd is not None:
|
||||
os.chdir(self._cwd)
|
||||
self._cwd = None
|
||||
66
_pytest/nose.py
Normal file
66
_pytest/nose.py
Normal file
@@ -0,0 +1,66 @@
|
||||
""" run test suites written for nose. """
|
||||
|
||||
import pytest, py
|
||||
import sys
|
||||
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:
|
||||
pytest.skip(str(call.excinfo.value)), call.when)
|
||||
call.excinfo = call2.excinfo
|
||||
|
||||
|
||||
@pytest.mark.trylast
|
||||
def pytest_runtest_setup(item):
|
||||
if is_potential_nosetest(item):
|
||||
if isinstance(item.parent, pytest.Generator):
|
||||
gen = item.parent
|
||||
if not hasattr(gen, '_nosegensetup'):
|
||||
call_optional(gen.obj, 'setup')
|
||||
if isinstance(gen.parent, pytest.Instance):
|
||||
call_optional(gen.parent.obj, 'setup')
|
||||
gen._nosegensetup = True
|
||||
if not call_optional(item.obj, 'setup'):
|
||||
# call module level setup if there is no object level one
|
||||
call_optional(item.parent.obj, 'setup')
|
||||
#XXX this implies we only call teardown when setup worked
|
||||
item.session._setupstate.addfinalizer((lambda: teardown_nose(item)), item)
|
||||
|
||||
def teardown_nose(item):
|
||||
if is_potential_nosetest(item):
|
||||
if not call_optional(item.obj, 'teardown'):
|
||||
call_optional(item.parent.obj, 'teardown')
|
||||
#if hasattr(item.parent, '_nosegensetup'):
|
||||
# #call_optional(item._nosegensetup, 'teardown')
|
||||
# del item.parent._nosegensetup
|
||||
|
||||
def pytest_make_collect_report(collector):
|
||||
SkipTest = getattr(sys.modules.get('unittest', None), 'SkipTest', None)
|
||||
if SkipTest is not None:
|
||||
collector.skip_exceptions += (SkipTest,)
|
||||
SkipTest = getattr(sys.modules.get('nose', None), 'SkipTest', None)
|
||||
if SkipTest is not None:
|
||||
collector.skip_exceptions += (SkipTest,)
|
||||
if isinstance(collector, pytest.Generator):
|
||||
call_optional(collector.obj, 'setup')
|
||||
|
||||
|
||||
def is_potential_nosetest(item):
|
||||
# extra check needed since we do not do nose style setup/teardown
|
||||
# on direct unittest style classes
|
||||
return isinstance(item, pytest.Function) and \
|
||||
not isinstance(item, unittest.TestCaseFunction)
|
||||
|
||||
|
||||
def call_optional(obj, name):
|
||||
method = getattr(obj, name, None)
|
||||
isfixture = hasattr(method, "_pytestfixturefunction")
|
||||
if method is not None and not isfixture and py.builtin.callable(method):
|
||||
# If there's any problems allow the exception to raise rather than
|
||||
# silently ignoring them
|
||||
method()
|
||||
return True
|
||||
67
_pytest/pastebin.py
Normal file
67
_pytest/pastebin.py
Normal file
@@ -0,0 +1,67 @@
|
||||
""" submit failure or test session information to a pastebin service. """
|
||||
import py, sys
|
||||
|
||||
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,
|
||||
choices=['failed', 'all'],
|
||||
help="send failed|all info to bpaste.net pastebin service.")
|
||||
|
||||
def pytest_configure(__multicall__, config):
|
||||
import tempfile
|
||||
__multicall__.execute()
|
||||
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
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
if hasattr(config, '_pastebinfile'):
|
||||
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)
|
||||
tr = config.pluginmanager.getplugin('terminalreporter')
|
||||
del tr._tw.__dict__['write']
|
||||
|
||||
def getproxy():
|
||||
if sys.version_info < (3, 0):
|
||||
from xmlrpclib import ServerProxy
|
||||
else:
|
||||
from xmlrpc.client import ServerProxy
|
||||
return ServerProxy(url.xmlrpc).pastes
|
||||
|
||||
def pytest_terminal_summary(terminalreporter):
|
||||
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)
|
||||
rep.toterminal(tw)
|
||||
s = tw.stringio.getvalue()
|
||||
assert len(s)
|
||||
proxyid = serverproxy.newPaste("python", s)
|
||||
pastebinurl = "%s%s" % (url.show, proxyid)
|
||||
tr.write_line("%s --> %s" %(msg, pastebinurl))
|
||||
113
_pytest/pdb.py
Normal file
113
_pytest/pdb.py
Normal file
@@ -0,0 +1,113 @@
|
||||
""" interactive debugging with PDB, the Python Debugger. """
|
||||
|
||||
import pytest, py
|
||||
import sys
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("general")
|
||||
group._addoption('--pdb',
|
||||
action="store_true", dest="usepdb", default=False,
|
||||
help="start the interactive Python debugger on errors.")
|
||||
|
||||
def pytest_namespace():
|
||||
return {'set_trace': pytestPDB().set_trace}
|
||||
|
||||
def pytest_configure(config):
|
||||
if config.getvalue("usepdb"):
|
||||
config.pluginmanager.register(PdbInvoke(), 'pdbinvoke')
|
||||
|
||||
old_trace = py.std.pdb.set_trace
|
||||
def fin():
|
||||
py.std.pdb.set_trace = old_trace
|
||||
py.std.pdb.set_trace = pytest.set_trace
|
||||
config._cleanup.append(fin)
|
||||
|
||||
class pytestPDB:
|
||||
""" Pseudo PDB that defers to the real pdb. """
|
||||
item = None
|
||||
collector = None
|
||||
|
||||
def set_trace(self):
|
||||
""" invoke PDB set_trace debugging, dropping any IO capturing. """
|
||||
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()
|
||||
tw.line()
|
||||
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
|
||||
py.std.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:
|
||||
def pytest_exception_interact(self, node, call, report):
|
||||
return _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(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 = 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
|
||||
return 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):
|
||||
return excinfo.value.exc_info[2]
|
||||
else:
|
||||
return excinfo._excinfo[2]
|
||||
|
||||
|
||||
def _find_last_non_hidden_frame(stack):
|
||||
i = max(0, len(stack) - 1)
|
||||
while i and stack[i][0].f_locals.get("__tracebackhide__", False):
|
||||
i -= 1
|
||||
return i
|
||||
|
||||
|
||||
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)
|
||||
if f is None:
|
||||
i = _find_last_non_hidden_frame(stack)
|
||||
return stack, i
|
||||
p = Pdb()
|
||||
p.reset()
|
||||
p.interaction(None, t)
|
||||
695
_pytest/pytester.py
Normal file
695
_pytest/pytester.py
Normal file
@@ -0,0 +1,695 @@
|
||||
""" (disabled by default) support for testing py.test and py.test plugins. """
|
||||
|
||||
import py, pytest
|
||||
import sys, os
|
||||
import codecs
|
||||
import re
|
||||
import time
|
||||
from fnmatch import fnmatch
|
||||
from _pytest.main import Session, EXIT_OK
|
||||
from py.builtin import print_
|
||||
from _pytest.core import HookRelay
|
||||
|
||||
|
||||
def get_public_names(l):
|
||||
"""Only return names from iterator l without a leading underscore."""
|
||||
return [x for x in l if x[0] != "_"]
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("pylib")
|
||||
group.addoption('--no-tools-on-path',
|
||||
action="store_true", dest="notoolsonpath", default=False,
|
||||
help=("discover tools on PATH instead of going through py.cmdline.")
|
||||
)
|
||||
|
||||
def pytest_configure(config):
|
||||
# This might be called multiple times. Only take the first.
|
||||
global _pytest_fullpath
|
||||
import pytest
|
||||
try:
|
||||
_pytest_fullpath
|
||||
except NameError:
|
||||
_pytest_fullpath = os.path.abspath(pytest.__file__.rstrip("oc"))
|
||||
_pytest_fullpath = _pytest_fullpath.replace("$py.class", ".py")
|
||||
|
||||
def pytest_funcarg___pytest(request):
|
||||
return PytestArg(request)
|
||||
|
||||
class PytestArg:
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
|
||||
def gethookrecorder(self, hook):
|
||||
hookrecorder = HookRecorder(hook._pm)
|
||||
hookrecorder.start_recording(hook._hookspecs)
|
||||
self.request.addfinalizer(hookrecorder.finish_recording)
|
||||
return hookrecorder
|
||||
|
||||
class ParsedCall:
|
||||
def __init__(self, name, locals):
|
||||
assert '_name' not in locals
|
||||
self.__dict__.update(locals)
|
||||
self.__dict__.pop('self')
|
||||
self._name = name
|
||||
|
||||
def __repr__(self):
|
||||
d = self.__dict__.copy()
|
||||
del d['_name']
|
||||
return "<ParsedCall %r(**%r)>" %(self._name, d)
|
||||
|
||||
class HookRecorder:
|
||||
def __init__(self, pluginmanager):
|
||||
self._pluginmanager = pluginmanager
|
||||
self.calls = []
|
||||
self._recorders = {}
|
||||
|
||||
def start_recording(self, hookspecs):
|
||||
if not isinstance(hookspecs, (list, tuple)):
|
||||
hookspecs = [hookspecs]
|
||||
for hookspec in hookspecs:
|
||||
assert hookspec not in self._recorders
|
||||
class RecordCalls:
|
||||
_recorder = self
|
||||
for name, method in vars(hookspec).items():
|
||||
if name[0] != "_":
|
||||
setattr(RecordCalls, name, self._makecallparser(method))
|
||||
recorder = RecordCalls()
|
||||
self._recorders[hookspec] = recorder
|
||||
self._pluginmanager.register(recorder)
|
||||
self.hook = HookRelay(hookspecs, pm=self._pluginmanager,
|
||||
prefix="pytest_")
|
||||
|
||||
def finish_recording(self):
|
||||
for recorder in self._recorders.values():
|
||||
if self._pluginmanager.isregistered(recorder):
|
||||
self._pluginmanager.unregister(recorder)
|
||||
self._recorders.clear()
|
||||
|
||||
def _makecallparser(self, method):
|
||||
name = method.__name__
|
||||
args, varargs, varkw, default = py.std.inspect.getargspec(method)
|
||||
if not args or args[0] != "self":
|
||||
args.insert(0, 'self')
|
||||
fspec = py.std.inspect.formatargspec(args, varargs, varkw, default)
|
||||
# we use exec because we want to have early type
|
||||
# errors on wrong input arguments, using
|
||||
# *args/**kwargs delays this and gives errors
|
||||
# elsewhere
|
||||
exec (py.code.compile("""
|
||||
def %(name)s%(fspec)s:
|
||||
self._recorder.calls.append(
|
||||
ParsedCall(%(name)r, locals()))
|
||||
""" % locals()))
|
||||
return locals()[name]
|
||||
|
||||
def getcalls(self, names):
|
||||
if isinstance(names, str):
|
||||
names = names.split()
|
||||
for name in names:
|
||||
for cls in self._recorders:
|
||||
if name in vars(cls):
|
||||
break
|
||||
else:
|
||||
raise ValueError("callname %r not found in %r" %(
|
||||
name, self._recorders.keys()))
|
||||
l = []
|
||||
for call in self.calls:
|
||||
if call._name in names:
|
||||
l.append(call)
|
||||
return l
|
||||
|
||||
def contains(self, entries):
|
||||
__tracebackhide__ = True
|
||||
from py.builtin import print_
|
||||
i = 0
|
||||
entries = list(entries)
|
||||
backlocals = py.std.sys._getframe(1).f_locals
|
||||
while entries:
|
||||
name, check = entries.pop(0)
|
||||
for ind, call in enumerate(self.calls[i:]):
|
||||
if call._name == name:
|
||||
print_("NAMEMATCH", name, call)
|
||||
if eval(check, backlocals, call.__dict__):
|
||||
print_("CHECKERMATCH", repr(check), "->", call)
|
||||
else:
|
||||
print_("NOCHECKERMATCH", repr(check), "-", call)
|
||||
continue
|
||||
i += ind + 1
|
||||
break
|
||||
print_("NONAMEMATCH", name, "with", call)
|
||||
else:
|
||||
py.test.fail("could not find %r check %r" % (name, check))
|
||||
|
||||
def popcall(self, name):
|
||||
__tracebackhide__ = True
|
||||
for i, call in enumerate(self.calls):
|
||||
if call._name == name:
|
||||
del self.calls[i]
|
||||
return call
|
||||
lines = ["could not find call %r, in:" % (name,)]
|
||||
lines.extend([" %s" % str(x) for x in self.calls])
|
||||
py.test.fail("\n".join(lines))
|
||||
|
||||
def getcall(self, name):
|
||||
l = self.getcalls(name)
|
||||
assert len(l) == 1, (name, l)
|
||||
return l[0]
|
||||
|
||||
|
||||
def pytest_funcarg__linecomp(request):
|
||||
return LineComp()
|
||||
|
||||
def pytest_funcarg__LineMatcher(request):
|
||||
return LineMatcher
|
||||
|
||||
def pytest_funcarg__testdir(request):
|
||||
tmptestdir = TmpTestdir(request)
|
||||
return tmptestdir
|
||||
|
||||
rex_outcome = re.compile("(\d+) (\w+)")
|
||||
class RunResult:
|
||||
def __init__(self, ret, outlines, errlines, duration):
|
||||
self.ret = ret
|
||||
self.outlines = outlines
|
||||
self.errlines = errlines
|
||||
self.stdout = LineMatcher(outlines)
|
||||
self.stderr = LineMatcher(errlines)
|
||||
self.duration = duration
|
||||
|
||||
def parseoutcomes(self):
|
||||
for line in reversed(self.outlines):
|
||||
if 'seconds' in line:
|
||||
outcomes = rex_outcome.findall(line)
|
||||
if outcomes:
|
||||
d = {}
|
||||
for num, cat in outcomes:
|
||||
d[cat] = int(num)
|
||||
return d
|
||||
|
||||
class TmpTestdir:
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
self.Config = request.config.__class__
|
||||
self._pytest = request.getfuncargvalue("_pytest")
|
||||
# XXX remove duplication with tmpdir plugin
|
||||
basetmp = request.config._tmpdirhandler.ensuretemp("testdir")
|
||||
name = request.function.__name__
|
||||
for i in range(100):
|
||||
try:
|
||||
tmpdir = basetmp.mkdir(name + str(i))
|
||||
except py.error.EEXIST:
|
||||
continue
|
||||
break
|
||||
self.tmpdir = tmpdir
|
||||
self.plugins = []
|
||||
self._syspathremove = []
|
||||
self.chdir() # always chdir
|
||||
self.request.addfinalizer(self.finalize)
|
||||
|
||||
def __repr__(self):
|
||||
return "<TmpTestdir %r>" % (self.tmpdir,)
|
||||
|
||||
def finalize(self):
|
||||
for p in self._syspathremove:
|
||||
py.std.sys.path.remove(p)
|
||||
if hasattr(self, '_olddir'):
|
||||
self._olddir.chdir()
|
||||
# delete modules that have been loaded from tmpdir
|
||||
for name, mod in list(sys.modules.items()):
|
||||
if mod:
|
||||
fn = getattr(mod, '__file__', None)
|
||||
if fn and fn.startswith(str(self.tmpdir)):
|
||||
del sys.modules[name]
|
||||
|
||||
def getreportrecorder(self, obj):
|
||||
if hasattr(obj, 'config'):
|
||||
obj = obj.config
|
||||
if hasattr(obj, 'hook'):
|
||||
obj = obj.hook
|
||||
assert hasattr(obj, '_hookspecs'), obj
|
||||
reprec = ReportRecorder(obj)
|
||||
reprec.hookrecorder = self._pytest.gethookrecorder(obj)
|
||||
reprec.hook = reprec.hookrecorder.hook
|
||||
return reprec
|
||||
|
||||
def chdir(self):
|
||||
old = self.tmpdir.chdir()
|
||||
if not hasattr(self, '_olddir'):
|
||||
self._olddir = old
|
||||
|
||||
def _makefile(self, ext, args, kwargs):
|
||||
items = list(kwargs.items())
|
||||
if args:
|
||||
source = py.builtin._totext("\n").join(
|
||||
map(py.builtin._totext, args)) + py.builtin._totext("\n")
|
||||
basename = self.request.function.__name__
|
||||
items.insert(0, (basename, source))
|
||||
ret = None
|
||||
for name, value in items:
|
||||
p = self.tmpdir.join(name).new(ext=ext)
|
||||
source = py.builtin._totext(py.code.Source(value)).strip()
|
||||
content = source.encode("utf-8") # + "\n"
|
||||
#content = content.rstrip() + "\n"
|
||||
p.write(content, "wb")
|
||||
if ret is None:
|
||||
ret = p
|
||||
return ret
|
||||
|
||||
|
||||
def makefile(self, ext, *args, **kwargs):
|
||||
return self._makefile(ext, args, kwargs)
|
||||
|
||||
def makeini(self, source):
|
||||
return self.makefile('cfg', setup=source)
|
||||
|
||||
def makeconftest(self, source):
|
||||
return self.makepyfile(conftest=source)
|
||||
|
||||
def makeini(self, source):
|
||||
return self.makefile('.ini', tox=source)
|
||||
|
||||
def getinicfg(self, source):
|
||||
p = self.makeini(source)
|
||||
return py.iniconfig.IniConfig(p)['pytest']
|
||||
|
||||
def makepyfile(self, *args, **kwargs):
|
||||
return self._makefile('.py', args, kwargs)
|
||||
|
||||
def maketxtfile(self, *args, **kwargs):
|
||||
return self._makefile('.txt', args, kwargs)
|
||||
|
||||
def syspathinsert(self, path=None):
|
||||
if path is None:
|
||||
path = self.tmpdir
|
||||
py.std.sys.path.insert(0, str(path))
|
||||
self._syspathremove.append(str(path))
|
||||
|
||||
def mkdir(self, name):
|
||||
return self.tmpdir.mkdir(name)
|
||||
|
||||
def mkpydir(self, name):
|
||||
p = self.mkdir(name)
|
||||
p.ensure("__init__.py")
|
||||
return p
|
||||
|
||||
Session = Session
|
||||
def getnode(self, config, arg):
|
||||
session = Session(config)
|
||||
assert '::' not in str(arg)
|
||||
p = py.path.local(arg)
|
||||
x = session.fspath.bestrelpath(p)
|
||||
config.hook.pytest_sessionstart(session=session)
|
||||
res = session.perform_collect([x], genitems=False)[0]
|
||||
config.hook.pytest_sessionfinish(session=session, exitstatus=EXIT_OK)
|
||||
return res
|
||||
|
||||
def getpathnode(self, path):
|
||||
config = self.parseconfigure(path)
|
||||
session = Session(config)
|
||||
x = session.fspath.bestrelpath(path)
|
||||
config.hook.pytest_sessionstart(session=session)
|
||||
res = session.perform_collect([x], genitems=False)[0]
|
||||
config.hook.pytest_sessionfinish(session=session, exitstatus=EXIT_OK)
|
||||
return res
|
||||
|
||||
def genitems(self, colitems):
|
||||
session = colitems[0].session
|
||||
result = []
|
||||
for colitem in colitems:
|
||||
result.extend(session.genitems(colitem))
|
||||
return result
|
||||
|
||||
def runitem(self, source):
|
||||
# used from runner functional tests
|
||||
item = self.getitem(source)
|
||||
# the test class where we are called from wants to provide the runner
|
||||
testclassinstance = self.request.instance
|
||||
runner = testclassinstance.getrunner()
|
||||
return runner(item)
|
||||
|
||||
def inline_runsource(self, source, *cmdlineargs):
|
||||
p = self.makepyfile(source)
|
||||
l = list(cmdlineargs) + [p]
|
||||
return self.inline_run(*l)
|
||||
|
||||
def inline_runsource1(self, *args):
|
||||
args = list(args)
|
||||
source = args.pop()
|
||||
p = self.makepyfile(source)
|
||||
l = list(args) + [p]
|
||||
reprec = self.inline_run(*l)
|
||||
reports = reprec.getreports("pytest_runtest_logreport")
|
||||
assert len(reports) == 3, reports # setup/call/teardown
|
||||
return reports[1]
|
||||
|
||||
def inline_genitems(self, *args):
|
||||
return self.inprocess_run(list(args) + ['--collectonly'])
|
||||
|
||||
def inline_run(self, *args):
|
||||
items, rec = self.inprocess_run(args)
|
||||
return rec
|
||||
|
||||
def inprocess_run(self, args, plugins=None):
|
||||
rec = []
|
||||
items = []
|
||||
class Collect:
|
||||
def pytest_configure(x, config):
|
||||
rec.append(self.getreportrecorder(config))
|
||||
def pytest_itemcollected(self, item):
|
||||
items.append(item)
|
||||
if not plugins:
|
||||
plugins = []
|
||||
plugins.append(Collect())
|
||||
ret = pytest.main(list(args), plugins=plugins)
|
||||
reprec = rec[0]
|
||||
reprec.ret = ret
|
||||
assert len(rec) == 1
|
||||
return items, reprec
|
||||
|
||||
def parseconfig(self, *args):
|
||||
args = [str(x) for x in args]
|
||||
for x in args:
|
||||
if str(x).startswith('--basetemp'):
|
||||
break
|
||||
else:
|
||||
args.append("--basetemp=%s" % self.tmpdir.dirpath('basetemp'))
|
||||
import _pytest.config
|
||||
config = _pytest.config._prepareconfig(args, self.plugins)
|
||||
# we don't know what the test will do with this half-setup config
|
||||
# object and thus we make sure it gets unconfigured properly in any
|
||||
# case (otherwise capturing could still be active, for example)
|
||||
def ensure_unconfigure():
|
||||
if hasattr(config.pluginmanager, "_config"):
|
||||
config.pluginmanager.do_unconfigure(config)
|
||||
config.pluginmanager.ensure_shutdown()
|
||||
|
||||
self.request.addfinalizer(ensure_unconfigure)
|
||||
return config
|
||||
|
||||
def parseconfigure(self, *args):
|
||||
config = self.parseconfig(*args)
|
||||
config.do_configure()
|
||||
self.request.addfinalizer(lambda:
|
||||
config.do_unconfigure())
|
||||
return config
|
||||
|
||||
def getitem(self, source, funcname="test_func"):
|
||||
items = self.getitems(source)
|
||||
for item in items:
|
||||
if item.name == funcname:
|
||||
return item
|
||||
assert 0, "%r item not found in module:\n%s\nitems: %s" %(
|
||||
funcname, source, items)
|
||||
|
||||
def getitems(self, source):
|
||||
modcol = self.getmodulecol(source)
|
||||
return self.genitems([modcol])
|
||||
|
||||
def getmodulecol(self, source, configargs=(), withinit=False):
|
||||
kw = {self.request.function.__name__: py.code.Source(source).strip()}
|
||||
path = self.makepyfile(**kw)
|
||||
if withinit:
|
||||
self.makepyfile(__init__ = "#")
|
||||
self.config = config = self.parseconfigure(path, *configargs)
|
||||
node = self.getnode(config, path)
|
||||
return node
|
||||
|
||||
def collect_by_name(self, modcol, name):
|
||||
for colitem in modcol._memocollect():
|
||||
if colitem.name == name:
|
||||
return colitem
|
||||
|
||||
def popen(self, cmdargs, stdout, stderr, **kw):
|
||||
env = os.environ.copy()
|
||||
env['PYTHONPATH'] = os.pathsep.join(filter(None, [
|
||||
str(os.getcwd()), env.get('PYTHONPATH', '')]))
|
||||
kw['env'] = env
|
||||
#print "env", env
|
||||
return py.std.subprocess.Popen(cmdargs,
|
||||
stdout=stdout, stderr=stderr, **kw)
|
||||
|
||||
def run(self, *cmdargs):
|
||||
return self._run(*cmdargs)
|
||||
|
||||
def _run(self, *cmdargs):
|
||||
cmdargs = [str(x) for x in cmdargs]
|
||||
p1 = self.tmpdir.join("stdout")
|
||||
p2 = self.tmpdir.join("stderr")
|
||||
print_("running", cmdargs, "curdir=", py.path.local())
|
||||
f1 = codecs.open(str(p1), "w", encoding="utf8")
|
||||
f2 = codecs.open(str(p2), "w", encoding="utf8")
|
||||
try:
|
||||
now = time.time()
|
||||
popen = self.popen(cmdargs, stdout=f1, stderr=f2,
|
||||
close_fds=(sys.platform != "win32"))
|
||||
ret = popen.wait()
|
||||
finally:
|
||||
f1.close()
|
||||
f2.close()
|
||||
f1 = codecs.open(str(p1), "r", encoding="utf8")
|
||||
f2 = codecs.open(str(p2), "r", encoding="utf8")
|
||||
try:
|
||||
out = f1.read().splitlines()
|
||||
err = f2.read().splitlines()
|
||||
finally:
|
||||
f1.close()
|
||||
f2.close()
|
||||
self._dump_lines(out, sys.stdout)
|
||||
self._dump_lines(err, sys.stderr)
|
||||
return RunResult(ret, out, err, time.time()-now)
|
||||
|
||||
def _dump_lines(self, lines, fp):
|
||||
try:
|
||||
for line in lines:
|
||||
py.builtin.print_(line, file=fp)
|
||||
except UnicodeEncodeError:
|
||||
print("couldn't print to %s because of encoding" % (fp,))
|
||||
|
||||
def runpybin(self, scriptname, *args):
|
||||
fullargs = self._getpybinargs(scriptname) + args
|
||||
return self.run(*fullargs)
|
||||
|
||||
def _getpybinargs(self, scriptname):
|
||||
if not self.request.config.getvalue("notoolsonpath"):
|
||||
# XXX we rely on script refering to the correct environment
|
||||
# we cannot use "(py.std.sys.executable,script)"
|
||||
# becaue on windows the script is e.g. a py.test.exe
|
||||
return (py.std.sys.executable, _pytest_fullpath,)
|
||||
else:
|
||||
py.test.skip("cannot run %r with --no-tools-on-path" % scriptname)
|
||||
|
||||
def runpython(self, script, prepend=True):
|
||||
if prepend:
|
||||
s = self._getsysprepend()
|
||||
if s:
|
||||
script.write(s + "\n" + script.read())
|
||||
return self.run(sys.executable, script)
|
||||
|
||||
def _getsysprepend(self):
|
||||
if self.request.config.getvalue("notoolsonpath"):
|
||||
s = "import sys;sys.path.insert(0,%r);" % str(py._pydir.dirpath())
|
||||
else:
|
||||
s = ""
|
||||
return s
|
||||
|
||||
def runpython_c(self, command):
|
||||
command = self._getsysprepend() + command
|
||||
return self.run(py.std.sys.executable, "-c", command)
|
||||
|
||||
def runpytest(self, *args):
|
||||
p = py.path.local.make_numbered_dir(prefix="runpytest-",
|
||||
keep=None, rootdir=self.tmpdir)
|
||||
args = ('--basetemp=%s' % p, ) + args
|
||||
#for x in args:
|
||||
# if '--confcutdir' in str(x):
|
||||
# break
|
||||
#else:
|
||||
# pass
|
||||
# args = ('--confcutdir=.',) + args
|
||||
plugins = [x for x in self.plugins if isinstance(x, str)]
|
||||
if plugins:
|
||||
args = ('-p', plugins[0]) + args
|
||||
return self.runpybin("py.test", *args)
|
||||
|
||||
def spawn_pytest(self, string, expect_timeout=10.0):
|
||||
if self.request.config.getvalue("notoolsonpath"):
|
||||
py.test.skip("--no-tools-on-path prevents running pexpect-spawn tests")
|
||||
basetemp = self.tmpdir.mkdir("pexpect")
|
||||
invoke = " ".join(map(str, self._getpybinargs("py.test")))
|
||||
cmd = "%s --basetemp=%s %s" % (invoke, basetemp, string)
|
||||
return self.spawn(cmd, expect_timeout=expect_timeout)
|
||||
|
||||
def spawn(self, cmd, expect_timeout=10.0):
|
||||
pexpect = py.test.importorskip("pexpect", "2.4")
|
||||
if hasattr(sys, 'pypy_version_info') and '64' in py.std.platform.machine():
|
||||
pytest.skip("pypy-64 bit not supported")
|
||||
if sys.platform == "darwin":
|
||||
pytest.xfail("pexpect does not work reliably on darwin?!")
|
||||
if sys.platform.startswith("freebsd"):
|
||||
pytest.xfail("pexpect does not work reliably on freebsd")
|
||||
logfile = self.tmpdir.join("spawn.out")
|
||||
child = pexpect.spawn(cmd, logfile=logfile.open("w"))
|
||||
child.timeout = expect_timeout
|
||||
return child
|
||||
|
||||
def getdecoded(out):
|
||||
try:
|
||||
return out.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return "INTERNAL not-utf8-decodeable, truncated string:\n%s" % (
|
||||
py.io.saferepr(out),)
|
||||
|
||||
class ReportRecorder(object):
|
||||
def __init__(self, hook):
|
||||
self.hook = hook
|
||||
self.pluginmanager = hook._pm
|
||||
self.pluginmanager.register(self)
|
||||
|
||||
def getcall(self, name):
|
||||
return self.hookrecorder.getcall(name)
|
||||
|
||||
def popcall(self, name):
|
||||
return self.hookrecorder.popcall(name)
|
||||
|
||||
def getcalls(self, names):
|
||||
""" return list of ParsedCall instances matching the given eventname. """
|
||||
return self.hookrecorder.getcalls(names)
|
||||
|
||||
# functionality for test reports
|
||||
|
||||
def getreports(self, names="pytest_runtest_logreport pytest_collectreport"):
|
||||
return [x.report for x in self.getcalls(names)]
|
||||
|
||||
def matchreport(self, inamepart="",
|
||||
names="pytest_runtest_logreport pytest_collectreport", when=None):
|
||||
""" return a testreport whose dotted import path matches """
|
||||
l = []
|
||||
for rep in self.getreports(names=names):
|
||||
try:
|
||||
if not when and rep.when != "call" and rep.passed:
|
||||
# setup/teardown passing reports - let's ignore those
|
||||
continue
|
||||
except AttributeError:
|
||||
pass
|
||||
if when and getattr(rep, 'when', None) != when:
|
||||
continue
|
||||
if not inamepart or inamepart in rep.nodeid.split("::"):
|
||||
l.append(rep)
|
||||
if not l:
|
||||
raise ValueError("could not find test report matching %r: no test reports at all!" %
|
||||
(inamepart,))
|
||||
if len(l) > 1:
|
||||
raise ValueError("found more than one testreport matching %r: %s" %(
|
||||
inamepart, l))
|
||||
return l[0]
|
||||
|
||||
def getfailures(self, names='pytest_runtest_logreport pytest_collectreport'):
|
||||
return [rep for rep in self.getreports(names) if rep.failed]
|
||||
|
||||
def getfailedcollections(self):
|
||||
return self.getfailures('pytest_collectreport')
|
||||
|
||||
def listoutcomes(self):
|
||||
passed = []
|
||||
skipped = []
|
||||
failed = []
|
||||
for rep in self.getreports(
|
||||
"pytest_collectreport pytest_runtest_logreport"):
|
||||
if rep.passed:
|
||||
if getattr(rep, "when", None) == "call":
|
||||
passed.append(rep)
|
||||
elif rep.skipped:
|
||||
skipped.append(rep)
|
||||
elif rep.failed:
|
||||
failed.append(rep)
|
||||
return passed, skipped, failed
|
||||
|
||||
def countoutcomes(self):
|
||||
return [len(x) for x in self.listoutcomes()]
|
||||
|
||||
def assertoutcome(self, passed=0, skipped=0, failed=0):
|
||||
realpassed, realskipped, realfailed = self.listoutcomes()
|
||||
assert passed == len(realpassed)
|
||||
assert skipped == len(realskipped)
|
||||
assert failed == len(realfailed)
|
||||
|
||||
def clear(self):
|
||||
self.hookrecorder.calls[:] = []
|
||||
|
||||
def unregister(self):
|
||||
self.pluginmanager.unregister(self)
|
||||
self.hookrecorder.finish_recording()
|
||||
|
||||
class LineComp:
|
||||
def __init__(self):
|
||||
self.stringio = py.io.TextIO()
|
||||
|
||||
def assert_contains_lines(self, lines2):
|
||||
""" assert that lines2 are contained (linearly) in lines1.
|
||||
return a list of extralines found.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
val = self.stringio.getvalue()
|
||||
self.stringio.truncate(0)
|
||||
self.stringio.seek(0)
|
||||
lines1 = val.split("\n")
|
||||
return LineMatcher(lines1).fnmatch_lines(lines2)
|
||||
|
||||
class LineMatcher:
|
||||
def __init__(self, lines):
|
||||
self.lines = lines
|
||||
|
||||
def str(self):
|
||||
return "\n".join(self.lines)
|
||||
|
||||
def _getlines(self, lines2):
|
||||
if isinstance(lines2, str):
|
||||
lines2 = py.code.Source(lines2)
|
||||
if isinstance(lines2, py.code.Source):
|
||||
lines2 = lines2.strip().lines
|
||||
return lines2
|
||||
|
||||
def fnmatch_lines_random(self, lines2):
|
||||
lines2 = self._getlines(lines2)
|
||||
for line in lines2:
|
||||
for x in self.lines:
|
||||
if line == x or fnmatch(x, line):
|
||||
print_("matched: ", repr(line))
|
||||
break
|
||||
else:
|
||||
raise ValueError("line %r not found in output" % line)
|
||||
|
||||
def get_lines_after(self, fnline):
|
||||
for i, line in enumerate(self.lines):
|
||||
if fnline == line or fnmatch(line, fnline):
|
||||
return self.lines[i+1:]
|
||||
raise ValueError("line %r not found in output" % fnline)
|
||||
|
||||
def fnmatch_lines(self, lines2):
|
||||
def show(arg1, arg2):
|
||||
py.builtin.print_(arg1, arg2, file=py.std.sys.stderr)
|
||||
lines2 = self._getlines(lines2)
|
||||
lines1 = self.lines[:]
|
||||
nextline = None
|
||||
extralines = []
|
||||
__tracebackhide__ = True
|
||||
for line in lines2:
|
||||
nomatchprinted = False
|
||||
while lines1:
|
||||
nextline = lines1.pop(0)
|
||||
if line == nextline:
|
||||
show("exact match:", repr(line))
|
||||
break
|
||||
elif fnmatch(nextline, line):
|
||||
show("fnmatch:", repr(line))
|
||||
show(" with:", repr(nextline))
|
||||
break
|
||||
else:
|
||||
if not nomatchprinted:
|
||||
show("nomatch:", repr(line))
|
||||
nomatchprinted = True
|
||||
show(" and:", repr(nextline))
|
||||
extralines.append(nextline)
|
||||
else:
|
||||
py.test.fail("remains unmatched: %r, see stderr" % (line,))
|
||||
1895
_pytest/python.py
Normal file
1895
_pytest/python.py
Normal file
File diff suppressed because it is too large
Load Diff
99
_pytest/recwarn.py
Normal file
99
_pytest/recwarn.py
Normal file
@@ -0,0 +1,99 @@
|
||||
""" recording warnings during test function execution. """
|
||||
|
||||
import py
|
||||
import sys
|
||||
|
||||
def pytest_funcarg__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
|
||||
|
||||
def pytest_namespace():
|
||||
return {'deprecated_call': deprecated_call}
|
||||
|
||||
def deprecated_call(func, *args, **kwargs):
|
||||
""" assert that calling ``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:
|
||||
ret = func(*args, **kwargs)
|
||||
finally:
|
||||
warningmodule.warn_explicit = warn_explicit
|
||||
warningmodule.warn = warn
|
||||
if not l:
|
||||
#print warningmodule
|
||||
__tracebackhide__ = True
|
||||
raise AssertionError("%r did not produce DeprecationWarning" %(func,))
|
||||
return ret
|
||||
|
||||
|
||||
class RecordedWarning:
|
||||
def __init__(self, message, category, filename, lineno, line):
|
||||
self.message = message
|
||||
self.category = category
|
||||
self.filename = filename
|
||||
self.lineno = lineno
|
||||
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
|
||||
|
||||
def pop(self, cls=Warning):
|
||||
""" 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)
|
||||
__tracebackhide__ = True
|
||||
assert 0, "%r not found in %r" %(cls, self.list)
|
||||
|
||||
#def resetregistry(self):
|
||||
# import warnings
|
||||
# warnings.onceregistry.clear()
|
||||
# warnings.__warningregistry__.clear()
|
||||
|
||||
def clear(self):
|
||||
self.list[:] = []
|
||||
|
||||
def finalize(self):
|
||||
py.std.warnings.showwarning = self.old_showwarning
|
||||
100
_pytest/resultlog.py
Normal file
100
_pytest/resultlog.py
Normal file
@@ -0,0 +1,100 @@
|
||||
""" log machine-parseable test session result information in a plain
|
||||
text file.
|
||||
"""
|
||||
|
||||
import py
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("terminal reporting", "resultlog plugin options")
|
||||
group.addoption('--resultlog', '--result-log', action="store",
|
||||
metavar="path", default=None,
|
||||
help="path for machine-readable result log.")
|
||||
|
||||
def pytest_configure(config):
|
||||
resultlog = config.option.resultlog
|
||||
# prevent opening resultlog on slave nodes (xdist)
|
||||
if resultlog and not hasattr(config, 'slaveinput'):
|
||||
logfile = open(resultlog, 'w', 1) # line buffered
|
||||
config._resultlog = ResultLog(config, logfile)
|
||||
config.pluginmanager.register(config._resultlog)
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
resultlog = getattr(config, '_resultlog', None)
|
||||
if resultlog:
|
||||
resultlog.logfile.close()
|
||||
del config._resultlog
|
||||
config.pluginmanager.unregister(resultlog)
|
||||
|
||||
def generic_path(item):
|
||||
chain = item.listchain()
|
||||
gpath = [chain[0].name]
|
||||
fspath = chain[0].fspath
|
||||
fspart = False
|
||||
for node in chain[1:]:
|
||||
newfspath = node.fspath
|
||||
if newfspath == fspath:
|
||||
if fspart:
|
||||
gpath.append(':')
|
||||
fspart = False
|
||||
else:
|
||||
gpath.append('.')
|
||||
else:
|
||||
gpath.append('/')
|
||||
fspart = True
|
||||
name = node.name
|
||||
if name[0] in '([':
|
||||
gpath.pop()
|
||||
gpath.append(name)
|
||||
fspath = newfspath
|
||||
return ''.join(gpath)
|
||||
|
||||
class ResultLog(object):
|
||||
def __init__(self, config, logfile):
|
||||
self.config = config
|
||||
self.logfile = logfile # preferably line buffered
|
||||
|
||||
def write_log_entry(self, testpath, lettercode, longrepr):
|
||||
py.builtin.print_("%s %s" % (lettercode, testpath), file=self.logfile)
|
||||
for line in longrepr.splitlines():
|
||||
py.builtin.print_(" %s" % line, file=self.logfile)
|
||||
|
||||
def log_outcome(self, report, lettercode, longrepr):
|
||||
testpath = getattr(report, 'nodeid', None)
|
||||
if testpath is None:
|
||||
testpath = report.fspath
|
||||
self.write_log_entry(testpath, lettercode, longrepr)
|
||||
|
||||
def pytest_runtest_logreport(self, report):
|
||||
if report.when != "call" and report.passed:
|
||||
return
|
||||
res = self.config.hook.pytest_report_teststatus(report=report)
|
||||
code = res[1]
|
||||
if code == 'x':
|
||||
longrepr = str(report.longrepr)
|
||||
elif code == 'X':
|
||||
longrepr = ''
|
||||
elif report.passed:
|
||||
longrepr = ""
|
||||
elif report.failed:
|
||||
longrepr = str(report.longrepr)
|
||||
elif report.skipped:
|
||||
longrepr = str(report.longrepr[2])
|
||||
self.log_outcome(report, code, longrepr)
|
||||
|
||||
def pytest_collectreport(self, report):
|
||||
if not report.passed:
|
||||
if report.failed:
|
||||
code = "F"
|
||||
longrepr = str(report.longrepr.reprcrash)
|
||||
else:
|
||||
assert report.skipped
|
||||
code = "S"
|
||||
longrepr = "%s:%d: %s" % report.longrepr
|
||||
self.log_outcome(report, code, longrepr)
|
||||
|
||||
def pytest_internalerror(self, excrepr):
|
||||
reprcrash = getattr(excrepr, 'reprcrash', None)
|
||||
path = getattr(reprcrash, "path", None)
|
||||
if path is None:
|
||||
path = "cwd:%s" % py.path.local()
|
||||
self.write_log_entry(path, '!', str(excrepr))
|
||||
474
_pytest/runner.py
Normal file
474
_pytest/runner.py
Normal file
@@ -0,0 +1,474 @@
|
||||
""" basic collect and runtest protocol implementations """
|
||||
|
||||
import py, sys
|
||||
from time import time
|
||||
from py._code.code import TerminalRepr
|
||||
|
||||
def pytest_namespace():
|
||||
return {
|
||||
'fail' : fail,
|
||||
'skip' : skip,
|
||||
'importorskip' : importorskip,
|
||||
'exit' : exit,
|
||||
}
|
||||
|
||||
#
|
||||
# pytest plugin hooks
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("terminal reporting", "reporting", after="general")
|
||||
group.addoption('--durations',
|
||||
action="store", type=int, default=None, metavar="N",
|
||||
help="show N slowest setup/test durations (N=0 for all)."),
|
||||
|
||||
def pytest_terminal_summary(terminalreporter):
|
||||
durations = terminalreporter.config.option.durations
|
||||
if durations is None:
|
||||
return
|
||||
tr = terminalreporter
|
||||
dlist = []
|
||||
for replist in tr.stats.values():
|
||||
for rep in replist:
|
||||
if hasattr(rep, 'duration'):
|
||||
dlist.append(rep)
|
||||
if not dlist:
|
||||
return
|
||||
dlist.sort(key=lambda x: x.duration)
|
||||
dlist.reverse()
|
||||
if not durations:
|
||||
tr.write_sep("=", "slowest test durations")
|
||||
else:
|
||||
tr.write_sep("=", "slowest %s test durations" % durations)
|
||||
dlist = dlist[:durations]
|
||||
|
||||
for rep in dlist:
|
||||
nodeid = rep.nodeid.replace("::()::", "::")
|
||||
tr.write_line("%02.2fs %-8s %s" %
|
||||
(rep.duration, rep.when, nodeid))
|
||||
|
||||
def pytest_sessionstart(session):
|
||||
session._setupstate = SetupState()
|
||||
def pytest_sessionfinish(session):
|
||||
session._setupstate.teardown_all()
|
||||
|
||||
class NodeInfo:
|
||||
def __init__(self, location):
|
||||
self.location = location
|
||||
|
||||
def pytest_runtest_protocol(item, nextitem):
|
||||
item.ihook.pytest_runtest_logstart(
|
||||
nodeid=item.nodeid, location=item.location,
|
||||
)
|
||||
runtestprotocol(item, nextitem=nextitem)
|
||||
return True
|
||||
|
||||
def runtestprotocol(item, log=True, nextitem=None):
|
||||
hasrequest = hasattr(item, "_request")
|
||||
if hasrequest and not item._request:
|
||||
item._initrequest()
|
||||
rep = call_and_report(item, "setup", log)
|
||||
reports = [rep]
|
||||
if rep.passed:
|
||||
reports.append(call_and_report(item, "call", log))
|
||||
reports.append(call_and_report(item, "teardown", log,
|
||||
nextitem=nextitem))
|
||||
# after all teardown hooks have been called
|
||||
# want funcargs and request info to go away
|
||||
if hasrequest:
|
||||
item._request = False
|
||||
item.funcargs = None
|
||||
return reports
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
item.session._setupstate.prepare(item)
|
||||
|
||||
def pytest_runtest_call(item):
|
||||
item.runtest()
|
||||
|
||||
def pytest_runtest_teardown(item, nextitem):
|
||||
item.session._setupstate.teardown_exact(item, nextitem)
|
||||
|
||||
def pytest_report_teststatus(report):
|
||||
if report.when in ("setup", "teardown"):
|
||||
if report.failed:
|
||||
# category, shortletter, verbose-word
|
||||
return "error", "E", "ERROR"
|
||||
elif report.skipped:
|
||||
return "skipped", "s", "SKIPPED"
|
||||
else:
|
||||
return "", "", ""
|
||||
|
||||
|
||||
#
|
||||
# Implementation
|
||||
|
||||
def call_and_report(item, when, log=True, **kwds):
|
||||
call = call_runtest_hook(item, when, **kwds)
|
||||
hook = item.ihook
|
||||
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(py.std.bdb.BdbQuit))
|
||||
|
||||
def call_runtest_hook(item, when, **kwds):
|
||||
hookname = "pytest_runtest_" + when
|
||||
ihook = getattr(item.ihook, hookname)
|
||||
return CallInfo(lambda: ihook(item=item, **kwds), when=when)
|
||||
|
||||
class CallInfo:
|
||||
""" Result/Exception info a function invocation. """
|
||||
#: None or ExceptionInfo object.
|
||||
excinfo = None
|
||||
def __init__(self, func, when):
|
||||
#: context of invocation: one of "setup", "call",
|
||||
#: "teardown", "memocollect"
|
||||
self.when = when
|
||||
self.start = time()
|
||||
try:
|
||||
try:
|
||||
self.result = func()
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
self.excinfo = py.code.ExceptionInfo()
|
||||
finally:
|
||||
self.stop = time()
|
||||
|
||||
def __repr__(self):
|
||||
if self.excinfo:
|
||||
status = "exception: %s" % str(self.excinfo.value)
|
||||
else:
|
||||
status = "result: %r" % (self.result,)
|
||||
return "<CallInfo when=%r %s>" % (self.when, status)
|
||||
|
||||
def getslaveinfoline(node):
|
||||
try:
|
||||
return node._slaveinfocache
|
||||
except AttributeError:
|
||||
d = node.slaveinfo
|
||||
ver = "%s.%s.%s" % d['version_info'][:3]
|
||||
node._slaveinfocache = s = "[%s] %s -- Python %s %s" % (
|
||||
d['id'], d['sysplatform'], ver, d['executable'])
|
||||
return s
|
||||
|
||||
class BaseReport(object):
|
||||
|
||||
def __init__(self, **kw):
|
||||
self.__dict__.update(kw)
|
||||
|
||||
def toterminal(self, out):
|
||||
longrepr = self.longrepr
|
||||
if hasattr(self, 'node'):
|
||||
out.line(getslaveinfoline(self.node))
|
||||
if hasattr(longrepr, 'toterminal'):
|
||||
longrepr.toterminal(out)
|
||||
else:
|
||||
try:
|
||||
out.line(longrepr)
|
||||
except UnicodeEncodeError:
|
||||
out.line("<unprintable longrepr>")
|
||||
|
||||
passed = property(lambda x: x.outcome == "passed")
|
||||
failed = property(lambda x: x.outcome == "failed")
|
||||
skipped = property(lambda x: x.outcome == "skipped")
|
||||
|
||||
@property
|
||||
def fspath(self):
|
||||
return self.nodeid.split("::")[0]
|
||||
|
||||
def pytest_runtest_makereport(item, call):
|
||||
when = call.when
|
||||
duration = call.stop-call.start
|
||||
keywords = dict([(x,1) for x in item.keywords])
|
||||
excinfo = call.excinfo
|
||||
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):
|
||||
outcome = "skipped"
|
||||
r = excinfo._getreprcrash()
|
||||
longrepr = (str(r.path), r.lineno, r.message)
|
||||
else:
|
||||
outcome = "failed"
|
||||
if call.when == "call":
|
||||
longrepr = item.repr_failure(excinfo)
|
||||
else: # exception in setup or teardown
|
||||
longrepr = item._repr_failure_py(excinfo,
|
||||
style=item.config.option.tbstyle)
|
||||
return TestReport(item.nodeid, item.location,
|
||||
keywords, outcome, longrepr, when,
|
||||
duration=duration)
|
||||
|
||||
class TestReport(BaseReport):
|
||||
""" Basic test report object (also used for setup and teardown calls if
|
||||
they fail).
|
||||
"""
|
||||
def __init__(self, nodeid, location,
|
||||
keywords, outcome, longrepr, when, sections=(), duration=0, **extra):
|
||||
#: normalized collection node id
|
||||
self.nodeid = nodeid
|
||||
|
||||
#: a (filesystempath, lineno, domaininfo) tuple indicating the
|
||||
#: actual location of a test item - it might be different from the
|
||||
#: collected one e.g. if a method is inherited from a different module.
|
||||
self.location = location
|
||||
|
||||
#: a name -> value dictionary containing all keywords and
|
||||
#: markers associated with a test invocation.
|
||||
self.keywords = keywords
|
||||
|
||||
#: test outcome, always one of "passed", "failed", "skipped".
|
||||
self.outcome = outcome
|
||||
|
||||
#: None or a failure representation.
|
||||
self.longrepr = longrepr
|
||||
|
||||
#: one of 'setup', 'call', 'teardown' to indicate runtest phase.
|
||||
self.when = when
|
||||
|
||||
#: list of (secname, data) extra information which needs to
|
||||
#: marshallable
|
||||
self.sections = list(sections)
|
||||
|
||||
#: time it took to run just the test
|
||||
self.duration = duration
|
||||
|
||||
self.__dict__.update(extra)
|
||||
|
||||
def __repr__(self):
|
||||
return "<TestReport %r when=%r outcome=%r>" % (
|
||||
self.nodeid, self.when, self.outcome)
|
||||
|
||||
class TeardownErrorReport(BaseReport):
|
||||
outcome = "failed"
|
||||
when = "teardown"
|
||||
def __init__(self, longrepr, **extra):
|
||||
self.longrepr = longrepr
|
||||
self.sections = []
|
||||
self.__dict__.update(extra)
|
||||
|
||||
def pytest_make_collect_report(collector):
|
||||
call = CallInfo(collector._memocollect, "memocollect")
|
||||
longrepr = None
|
||||
if not call.excinfo:
|
||||
outcome = "passed"
|
||||
else:
|
||||
if call.excinfo.errisinstance(collector.skip_exceptions):
|
||||
outcome = "skipped"
|
||||
r = collector._repr_failure_py(call.excinfo, "line").reprcrash
|
||||
longrepr = (str(r.path), r.lineno, r.message)
|
||||
else:
|
||||
outcome = "failed"
|
||||
errorinfo = collector.repr_failure(call.excinfo)
|
||||
if not hasattr(errorinfo, "toterminal"):
|
||||
errorinfo = CollectErrorRepr(errorinfo)
|
||||
longrepr = errorinfo
|
||||
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):
|
||||
self.nodeid = nodeid
|
||||
self.outcome = outcome
|
||||
self.longrepr = longrepr
|
||||
self.result = result or []
|
||||
self.sections = list(sections)
|
||||
self.__dict__.update(extra)
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
return (self.fspath, None, self.fspath)
|
||||
|
||||
def __repr__(self):
|
||||
return "<CollectReport %r lenresult=%s outcome=%r>" % (
|
||||
self.nodeid, len(self.result), self.outcome)
|
||||
|
||||
class CollectErrorRepr(TerminalRepr):
|
||||
def __init__(self, msg):
|
||||
self.longrepr = msg
|
||||
def toterminal(self, out):
|
||||
out.line(self.longrepr, red=True)
|
||||
|
||||
class SetupState(object):
|
||||
""" shared state for setting up/tearing down test items or collectors. """
|
||||
def __init__(self):
|
||||
self.stack = []
|
||||
self._finalizers = {}
|
||||
|
||||
def addfinalizer(self, finalizer, colitem):
|
||||
""" 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
|
||||
self._finalizers.setdefault(colitem, []).append(finalizer)
|
||||
|
||||
def _pop_and_teardown(self):
|
||||
colitem = self.stack.pop()
|
||||
self._teardown_with_finalization(colitem)
|
||||
|
||||
def _callfinalizers(self, colitem):
|
||||
finalizers = self._finalizers.pop(colitem, None)
|
||||
while finalizers:
|
||||
fin = finalizers.pop()
|
||||
fin()
|
||||
|
||||
def _teardown_with_finalization(self, colitem):
|
||||
self._callfinalizers(colitem)
|
||||
if hasattr(colitem, "teardown"):
|
||||
colitem.teardown()
|
||||
for colitem in self._finalizers:
|
||||
assert colitem is None or colitem in self.stack \
|
||||
or isinstance(colitem, tuple)
|
||||
|
||||
def teardown_all(self):
|
||||
while self.stack:
|
||||
self._pop_and_teardown()
|
||||
for key in list(self._finalizers):
|
||||
self._teardown_with_finalization(key)
|
||||
assert not self._finalizers
|
||||
|
||||
def teardown_exact(self, item, nextitem):
|
||||
needed_collectors = nextitem and nextitem.listchain() or []
|
||||
self._teardown_towards(needed_collectors)
|
||||
|
||||
def _teardown_towards(self, needed_collectors):
|
||||
while self.stack:
|
||||
if self.stack == needed_collectors[:len(self.stack)]:
|
||||
break
|
||||
self._pop_and_teardown()
|
||||
|
||||
def prepare(self, colitem):
|
||||
""" setup objects along the collector chain to the test-method
|
||||
and teardown previously setup objects."""
|
||||
needed_collectors = colitem.listchain()
|
||||
self._teardown_towards(needed_collectors)
|
||||
|
||||
# check if the last collection node has raised an error
|
||||
for col in self.stack:
|
||||
if hasattr(col, '_prepare_exc'):
|
||||
py.builtin._reraise(*col._prepare_exc)
|
||||
for col in needed_collectors[len(self.stack):]:
|
||||
self.stack.append(col)
|
||||
try:
|
||||
col.setup()
|
||||
except Exception:
|
||||
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.
|
||||
|
||||
|
||||
class OutcomeException(Exception):
|
||||
""" OutcomeException and its subclass instances indicate and
|
||||
contain info about test and collection outcomes.
|
||||
"""
|
||||
def __init__(self, msg=None, pytrace=True):
|
||||
Exception.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.pytrace = pytrace
|
||||
|
||||
def __repr__(self):
|
||||
if self.msg:
|
||||
return str(self.msg)
|
||||
return "<%s instance>" %(self.__class__.__name__,)
|
||||
__str__ = __repr__
|
||||
|
||||
class Skipped(OutcomeException):
|
||||
# XXX hackish: on 3k we fake to live in the builtins
|
||||
# in order to have Skipped exception printing shorter/nicer
|
||||
__module__ = 'builtins'
|
||||
|
||||
class Failed(OutcomeException):
|
||||
""" raised from an explicit call to py.test.fail() """
|
||||
__module__ = 'builtins'
|
||||
|
||||
class Exit(KeyboardInterrupt):
|
||||
""" raised for immediate program exits (no tracebacks/summaries)"""
|
||||
def __init__(self, msg="unknown reason"):
|
||||
self.msg = msg
|
||||
KeyboardInterrupt.__init__(self, msg)
|
||||
|
||||
# exposed helper methods
|
||||
|
||||
def exit(msg):
|
||||
""" exit testing process as if KeyboardInterrupt was triggered. """
|
||||
__tracebackhide__ = True
|
||||
raise Exit(msg)
|
||||
|
||||
exit.Exception = Exit
|
||||
|
||||
def skip(msg=""):
|
||||
""" skip an executing test with the given message. Note: it's usually
|
||||
better to use the py.test.mark.skipif marker to declare a test to be
|
||||
skipped under certain conditions like mismatching platforms or
|
||||
dependencies. See the pytest_skipping plugin for details.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
raise Skipped(msg=msg)
|
||||
skip.Exception = Skipped
|
||||
|
||||
def fail(msg="", pytrace=True):
|
||||
""" explicitely fail an currently-executing test with the given Message.
|
||||
|
||||
:arg pytrace: if false the msg represents the full failure information
|
||||
and no python traceback will be reported.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
raise Failed(msg=msg, pytrace=pytrace)
|
||||
fail.Exception = Failed
|
||||
|
||||
|
||||
def importorskip(modname, minversion=None):
|
||||
""" return imported module if it has a higher __version__ than the
|
||||
optionally specified 'minversion' - otherwise call py.test.skip()
|
||||
with a message detailing the mismatch.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
compile(modname, '', 'eval') # to catch syntaxerrors
|
||||
try:
|
||||
__import__(modname)
|
||||
except ImportError:
|
||||
py.test.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))
|
||||
return mod
|
||||
277
_pytest/skipping.py
Normal file
277
_pytest/skipping.py
Normal file
@@ -0,0 +1,277 @@
|
||||
""" support for skip/xfail functions and markers. """
|
||||
|
||||
import py, pytest
|
||||
import sys
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("general")
|
||||
group.addoption('--runxfail',
|
||||
action="store_true", dest="runxfail", default=False,
|
||||
help="run tests even if they are marked xfail")
|
||||
|
||||
def pytest_configure(config):
|
||||
config.addinivalue_line("markers",
|
||||
"skipif(condition): skip the given test function if eval(condition) "
|
||||
"results in a True value. Evaluation happens within the "
|
||||
"module global context. Example: skipif('sys.platform == \"win32\"') "
|
||||
"skips the test if we are on the win32 platform. see "
|
||||
"http://pytest.org/latest/skipping.html"
|
||||
)
|
||||
config.addinivalue_line("markers",
|
||||
"xfail(condition, reason=None, run=True): 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"
|
||||
)
|
||||
|
||||
def pytest_namespace():
|
||||
return dict(xfail=xfail)
|
||||
|
||||
class XFailed(pytest.fail.Exception):
|
||||
""" raised from an explicit call to py.test.xfail() """
|
||||
|
||||
def xfail(reason=""):
|
||||
""" xfail an executing test or setup functions with the given reason."""
|
||||
__tracebackhide__ = True
|
||||
raise XFailed(reason)
|
||||
xfail.Exception = XFailed
|
||||
|
||||
class MarkEvaluator:
|
||||
def __init__(self, item, name):
|
||||
self.item = item
|
||||
self.name = name
|
||||
|
||||
@property
|
||||
def holder(self):
|
||||
return self.item.keywords.get(self.name, None)
|
||||
def __bool__(self):
|
||||
return bool(self.holder)
|
||||
__nonzero__ = __bool__
|
||||
|
||||
def wasvalid(self):
|
||||
return not hasattr(self, 'exc')
|
||||
|
||||
def istrue(self):
|
||||
try:
|
||||
return self._istrue()
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
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])
|
||||
pytest.fail("Error evaluating %r expression\n"
|
||||
" %s\n"
|
||||
"%s"
|
||||
%(self.name, self.expr, "\n".join(msg)),
|
||||
pytrace=False)
|
||||
|
||||
def _getglobals(self):
|
||||
d = {'os': py.std.os, 'sys': py.std.sys, 'config': self.item.config}
|
||||
func = self.item.obj
|
||||
try:
|
||||
d.update(func.__globals__)
|
||||
except AttributeError:
|
||||
d.update(func.func_globals)
|
||||
return d
|
||||
|
||||
def _istrue(self):
|
||||
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:
|
||||
if self.get("reason") is None:
|
||||
# XXX better be checked at collection time
|
||||
pytest.fail("you need to specify reason=STRING "
|
||||
"when using booleans as conditions.")
|
||||
result = bool(expr)
|
||||
if result:
|
||||
self.result = True
|
||||
self.expr = expr
|
||||
break
|
||||
else:
|
||||
self.result = True
|
||||
return getattr(self, 'result', False)
|
||||
|
||||
def get(self, attr, default=None):
|
||||
return self.holder.kwargs.get(attr, default)
|
||||
|
||||
def getexplanation(self):
|
||||
expl = self.get('reason', None)
|
||||
if not expl:
|
||||
if not hasattr(self, 'expr'):
|
||||
return ""
|
||||
else:
|
||||
return "condition: " + str(self.expr)
|
||||
return expl
|
||||
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
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._evalxfail = MarkEvaluator(item, 'xfail')
|
||||
check_xfail_no_run(item)
|
||||
|
||||
def pytest_pyfunc_call(pyfuncitem):
|
||||
check_xfail_no_run(pyfuncitem)
|
||||
|
||||
def check_xfail_no_run(item):
|
||||
if not item.config.option.runxfail:
|
||||
evalxfail = item._evalxfail
|
||||
if evalxfail.istrue():
|
||||
if not evalxfail.get('run', True):
|
||||
py.test.xfail("[NOTRUN] " + evalxfail.getexplanation())
|
||||
|
||||
def pytest_runtest_makereport(__multicall__, item, call):
|
||||
if not isinstance(item, pytest.Function):
|
||||
return
|
||||
# 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
|
||||
rep.wasxfail = evalxfail.getexplanation()
|
||||
return rep
|
||||
return rep
|
||||
|
||||
# called by terminalreporter progress reporting
|
||||
def pytest_report_teststatus(report):
|
||||
if hasattr(report, "wasxfail"):
|
||||
if report.skipped:
|
||||
return "xfailed", "x", "xfail"
|
||||
elif report.failed:
|
||||
return "xpassed", "X", "XPASS"
|
||||
|
||||
# called by the terminalreporter instance/plugin
|
||||
def pytest_terminal_summary(terminalreporter):
|
||||
tr = terminalreporter
|
||||
if not tr.reportchars:
|
||||
#for name in "xfailed skipped failed xpassed":
|
||||
# if not tr.stats.get(name, 0):
|
||||
# tr.write_line("HINT: use '-r' option to see extra "
|
||||
# "summary info about tests")
|
||||
# break
|
||||
return
|
||||
|
||||
lines = []
|
||||
for char in tr.reportchars:
|
||||
if char == "x":
|
||||
show_xfailed(terminalreporter, lines)
|
||||
elif char == "X":
|
||||
show_xpassed(terminalreporter, lines)
|
||||
elif char in "fF":
|
||||
show_simple(terminalreporter, lines, 'failed', "FAIL %s")
|
||||
elif char in "sS":
|
||||
show_skipped(terminalreporter, lines)
|
||||
elif char == "E":
|
||||
show_simple(terminalreporter, lines, 'error', "ERROR %s")
|
||||
if lines:
|
||||
tr._tw.sep("=", "short test summary info")
|
||||
for line in lines:
|
||||
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, ))
|
||||
|
||||
def show_xfailed(terminalreporter, lines):
|
||||
xfailed = terminalreporter.stats.get("xfailed")
|
||||
if xfailed:
|
||||
for rep in xfailed:
|
||||
pos = rep.nodeid
|
||||
reason = rep.wasxfail
|
||||
lines.append("XFAIL %s" % (pos,))
|
||||
if reason:
|
||||
lines.append(" " + str(reason))
|
||||
|
||||
def show_xpassed(terminalreporter, lines):
|
||||
xpassed = terminalreporter.stats.get("xpassed")
|
||||
if xpassed:
|
||||
for rep in xpassed:
|
||||
pos = rep.nodeid
|
||||
reason = rep.wasxfail
|
||||
lines.append("XPASS %s %s" %(pos, reason))
|
||||
|
||||
def cached_eval(config, expr, d):
|
||||
if not hasattr(config, '_evalcache'):
|
||||
config._evalcache = {}
|
||||
try:
|
||||
return config._evalcache[expr]
|
||||
except KeyError:
|
||||
#import sys
|
||||
#print >>sys.stderr, ("cache-miss: %r" % expr)
|
||||
exprcode = py.code.compile(expr, mode="eval")
|
||||
config._evalcache[expr] = x = eval(exprcode, d)
|
||||
return x
|
||||
|
||||
|
||||
def folded_skips(skipped):
|
||||
d = {}
|
||||
for event in skipped:
|
||||
key = event.longrepr
|
||||
assert len(key) == 3, (event, key)
|
||||
d.setdefault(key, []).append(event)
|
||||
l = []
|
||||
for key, events in d.items():
|
||||
l.append((len(events),) + key)
|
||||
return l
|
||||
|
||||
def show_skipped(terminalreporter, lines):
|
||||
tr = terminalreporter
|
||||
skipped = tr.stats.get('skipped', [])
|
||||
if skipped:
|
||||
#if not tr.hasopt('skipped'):
|
||||
# tr.write_line(
|
||||
# "%d skipped tests, specify -rs for more info" %
|
||||
# len(skipped))
|
||||
# return
|
||||
fskips = folded_skips(skipped)
|
||||
if fskips:
|
||||
#tr.write_sep("_", "skipped test summary")
|
||||
for num, fspath, lineno, reason in fskips:
|
||||
if reason.startswith("Skipped: "):
|
||||
reason = reason[9:]
|
||||
lines.append("SKIP [%d] %s:%d: %s" %
|
||||
(num, fspath, lineno, reason))
|
||||
66
_pytest/standalonetemplate.py
Executable file
66
_pytest/standalonetemplate.py
Executable file
@@ -0,0 +1,66 @@
|
||||
#! /usr/bin/env python
|
||||
|
||||
sources = """
|
||||
@SOURCES@"""
|
||||
|
||||
import sys
|
||||
import base64
|
||||
import zlib
|
||||
|
||||
class DictImporter(object):
|
||||
def __init__(self, sources):
|
||||
self.sources = sources
|
||||
|
||||
def find_module(self, fullname, path=None):
|
||||
if fullname == "argparse" and sys.version_info >= (2,7):
|
||||
# we were generated with <python2.7 (which pulls in argparse)
|
||||
# but we are running now on a stdlib which has it, so use that.
|
||||
return None
|
||||
if fullname in self.sources:
|
||||
return self
|
||||
if fullname + '.__init__' in self.sources:
|
||||
return self
|
||||
return None
|
||||
|
||||
def load_module(self, fullname):
|
||||
# print "load_module:", fullname
|
||||
from types import ModuleType
|
||||
try:
|
||||
s = self.sources[fullname]
|
||||
is_pkg = False
|
||||
except KeyError:
|
||||
s = self.sources[fullname + '.__init__']
|
||||
is_pkg = True
|
||||
|
||||
co = compile(s, fullname, 'exec')
|
||||
module = sys.modules.setdefault(fullname, ModuleType(fullname))
|
||||
module.__file__ = "%s/%s" % (__file__, fullname)
|
||||
module.__loader__ = self
|
||||
if is_pkg:
|
||||
module.__path__ = [fullname]
|
||||
|
||||
do_exec(co, module.__dict__)
|
||||
return sys.modules[fullname]
|
||||
|
||||
def get_source(self, name):
|
||||
res = self.sources.get(name)
|
||||
if res is None:
|
||||
res = self.sources.get(name + '.__init__')
|
||||
return res
|
||||
|
||||
if __name__ == "__main__":
|
||||
if sys.version_info >= (3, 0):
|
||||
exec("def do_exec(co, loc): exec(co, loc)\n")
|
||||
import pickle
|
||||
sources = sources.encode("ascii") # ensure bytes
|
||||
sources = pickle.loads(zlib.decompress(base64.decodebytes(sources)))
|
||||
else:
|
||||
import cPickle as pickle
|
||||
exec("def do_exec(co, loc): exec co in loc\n")
|
||||
sources = pickle.loads(zlib.decompress(base64.decodestring(sources)))
|
||||
|
||||
importer = DictImporter(sources)
|
||||
sys.meta_path.insert(0, importer)
|
||||
|
||||
entry = "@ENTRY@"
|
||||
do_exec(entry, locals())
|
||||
497
_pytest/terminal.py
Normal file
497
_pytest/terminal.py
Normal file
@@ -0,0 +1,497 @@
|
||||
""" terminal reporting of the full testing process.
|
||||
|
||||
This is a good source for looking at the various reporting hooks.
|
||||
"""
|
||||
import pytest
|
||||
import py
|
||||
import sys
|
||||
import os
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("terminal reporting", "reporting", after="general")
|
||||
group._addoption('-v', '--verbose', action="count",
|
||||
dest="verbose", default=0, help="increase verbosity."),
|
||||
group._addoption('-q', '--quiet', action="count",
|
||||
dest="quiet", default=0, help="decrease verbosity."),
|
||||
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.")
|
||||
group._addoption('-l', '--showlocals',
|
||||
action="store_true", dest="showlocals", default=False,
|
||||
help="show locals in tracebacks (disabled by default).")
|
||||
group._addoption('--report',
|
||||
action="store", dest="report", default=None, metavar="opts",
|
||||
help="(deprecated, use -r)")
|
||||
group._addoption('--tb', metavar="style",
|
||||
action="store", dest="tbstyle", default='long',
|
||||
choices=['long', 'short', 'no', 'line', 'native'],
|
||||
help="traceback print mode (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).")
|
||||
|
||||
def pytest_configure(config):
|
||||
config.option.verbose -= config.option.quiet
|
||||
reporter = TerminalReporter(config, sys.stdout)
|
||||
config.pluginmanager.register(reporter, 'terminalreporter')
|
||||
if config.option.debug or config.option.traceconfig:
|
||||
def mywriter(tags, args):
|
||||
msg = " ".join(map(str, args))
|
||||
reporter.write_line("[traceconfig] " + msg)
|
||||
config.trace.root.setprocessor("pytest:config", mywriter)
|
||||
|
||||
def getreportopt(config):
|
||||
reportopts = ""
|
||||
optvalue = config.option.report
|
||||
if optvalue:
|
||||
py.builtin.print_("DEPRECATED: use -r instead of --report option.",
|
||||
file=py.std.sys.stderr)
|
||||
if optvalue:
|
||||
for setting in optvalue.split(","):
|
||||
setting = setting.strip()
|
||||
if setting == "skipped":
|
||||
reportopts += "s"
|
||||
elif setting == "xfailed":
|
||||
reportopts += "x"
|
||||
reportchars = config.option.reportchars
|
||||
if reportchars:
|
||||
for char in reportchars:
|
||||
if char not in reportopts:
|
||||
reportopts += char
|
||||
return reportopts
|
||||
|
||||
def pytest_report_teststatus(report):
|
||||
if report.passed:
|
||||
letter = "."
|
||||
elif report.skipped:
|
||||
letter = "s"
|
||||
elif report.failed:
|
||||
letter = "F"
|
||||
if report.when != "call":
|
||||
letter = "f"
|
||||
return report.outcome, letter, report.outcome.upper()
|
||||
|
||||
class TerminalReporter:
|
||||
def __init__(self, config, file=None):
|
||||
self.config = config
|
||||
self.verbosity = self.config.option.verbose
|
||||
self.showheader = self.verbosity >= 0
|
||||
self.showfspath = self.verbosity >= 0
|
||||
self.showlongtestinfo = self.verbosity > 0
|
||||
self._numcollected = 0
|
||||
|
||||
self.stats = {}
|
||||
self.startdir = self.curdir = py.path.local()
|
||||
if file is None:
|
||||
file = py.std.sys.stdout
|
||||
self._tw = self.writer = py.io.TerminalWriter(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)
|
||||
return char in self.reportchars
|
||||
|
||||
def write_fspath_result(self, fspath, res):
|
||||
if fspath != self.currentfspath:
|
||||
self.currentfspath = fspath
|
||||
#fspath = self.startdir.bestrelpath(fspath)
|
||||
self._tw.line()
|
||||
#relpath = self.startdir.bestrelpath(fspath)
|
||||
self._tw.write(fspath + " ")
|
||||
self._tw.write(res)
|
||||
|
||||
def write_ensure_prefix(self, prefix, extra="", **kwargs):
|
||||
if self.currentfspath != prefix:
|
||||
self._tw.line()
|
||||
self.currentfspath = prefix
|
||||
self._tw.write(prefix)
|
||||
if extra:
|
||||
self._tw.write(extra, **kwargs)
|
||||
self.currentfspath = -2
|
||||
|
||||
def ensure_newline(self):
|
||||
if self.currentfspath:
|
||||
self._tw.line()
|
||||
self.currentfspath = None
|
||||
|
||||
def write(self, content, **markup):
|
||||
self._tw.write(content, **markup)
|
||||
|
||||
def write_line(self, line, **markup):
|
||||
line = str(line)
|
||||
self.ensure_newline()
|
||||
self._tw.line(line, **markup)
|
||||
|
||||
def rewrite(self, line, **markup):
|
||||
line = str(line)
|
||||
self._tw.write("\r" + line, **markup)
|
||||
|
||||
def write_sep(self, sep, title=None, **markup):
|
||||
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"):
|
||||
self.write_line("INTERNALERROR> " + line)
|
||||
return 1
|
||||
|
||||
def pytest_plugin_registered(self, plugin):
|
||||
if self.config.option.traceconfig:
|
||||
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
|
||||
self.write_line(msg)
|
||||
|
||||
def pytest_deselected(self, items):
|
||||
self.stats.setdefault('deselected', []).extend(items)
|
||||
|
||||
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)
|
||||
self.write_ensure_prefix(line, "")
|
||||
elif self.showfspath:
|
||||
self.write_fspath_result(fspath, "")
|
||||
|
||||
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)
|
||||
else:
|
||||
self._tw.write(letter)
|
||||
else:
|
||||
if isinstance(word, tuple):
|
||||
word, markup = word
|
||||
else:
|
||||
if rep.passed:
|
||||
markup = {'green':True}
|
||||
elif rep.failed:
|
||||
markup = {'red':True}
|
||||
elif rep.skipped:
|
||||
markup = {'yellow':True}
|
||||
line = self._locationline(str(rep.fspath), *rep.location)
|
||||
if not hasattr(rep, 'node'):
|
||||
self.write_ensure_prefix(line, word, **markup)
|
||||
#self._tw.write(word, **markup)
|
||||
else:
|
||||
self.ensure_newline()
|
||||
if hasattr(rep, 'node'):
|
||||
self._tw.write("[%s] " % rep.node.gateway.id)
|
||||
self._tw.write(word, **markup)
|
||||
self._tw.write(" " + line)
|
||||
self.currentfspath = -2
|
||||
|
||||
def pytest_collection(self):
|
||||
if not self.hasmarkup and self.config.option.verbose >= 1:
|
||||
self.write("collecting ... ", bold=True)
|
||||
|
||||
def pytest_collectreport(self, report):
|
||||
if report.failed:
|
||||
self.stats.setdefault("error", []).append(report)
|
||||
elif report.skipped:
|
||||
self.stats.setdefault("skipped", []).append(report)
|
||||
items = [x for x in report.result if isinstance(x, pytest.Item)]
|
||||
self._numcollected += len(items)
|
||||
if self.hasmarkup:
|
||||
#self.write_fspath_result(report.fspath, 'E')
|
||||
self.report_collect()
|
||||
|
||||
def report_collect(self, final=False):
|
||||
if self.config.option.verbose < 0:
|
||||
return
|
||||
|
||||
errors = len(self.stats.get('error', []))
|
||||
skipped = len(self.stats.get('skipped', []))
|
||||
if final:
|
||||
line = "collected "
|
||||
else:
|
||||
line = "collecting "
|
||||
line += str(self._numcollected) + " items"
|
||||
if errors:
|
||||
line += " / %d errors" % errors
|
||||
if skipped:
|
||||
line += " / %d skipped" % skipped
|
||||
if self.hasmarkup:
|
||||
if final:
|
||||
line += " \n"
|
||||
self.rewrite(line, bold=True)
|
||||
else:
|
||||
self.write_line(line)
|
||||
|
||||
def pytest_collection_modifyitems(self):
|
||||
self.report_collect(True)
|
||||
|
||||
@pytest.mark.trylast
|
||||
def pytest_sessionstart(self, session):
|
||||
self._sessionstarttime = py.std.time.time()
|
||||
if not self.showheader:
|
||||
return
|
||||
self.write_sep("=", "test session starts", bold=True)
|
||||
verinfo = ".".join(map(str, sys.version_info[:3]))
|
||||
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__)
|
||||
if self.verbosity > 0 or self.config.option.debug or \
|
||||
getattr(self.config.option, 'pastebin', None):
|
||||
msg += " -- " + str(sys.executable)
|
||||
self.write_line(msg)
|
||||
lines = self.config.hook.pytest_report_header(
|
||||
config=self.config, startdir=self.startdir)
|
||||
lines.reverse()
|
||||
for line in flatten(lines):
|
||||
self.write_line(line)
|
||||
|
||||
def pytest_report_header(self, config):
|
||||
plugininfo = config.pluginmanager._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)
|
||||
|
||||
def pytest_collection_finish(self, session):
|
||||
if self.config.option.collectonly:
|
||||
self._printcollecteditems(session.items)
|
||||
if self.stats.get('failed'):
|
||||
self._tw.sep("!", "collection failures")
|
||||
for rep in self.stats.get('failed'):
|
||||
rep.toterminal(self._tw)
|
||||
return 1
|
||||
return 0
|
||||
if not self.showheader:
|
||||
return
|
||||
#for i, testarg in enumerate(self.config.args):
|
||||
# self.write_line("test path %d: %s" %(i+1, testarg))
|
||||
|
||||
def _printcollecteditems(self, items):
|
||||
# to print out items and their parent collectors
|
||||
# we take care to leave out Instances aka ()
|
||||
# because later versions are going to get rid of them anyway
|
||||
if self.config.option.verbose < 0:
|
||||
if self.config.option.verbose < -1:
|
||||
counts = {}
|
||||
for item in items:
|
||||
name = item.nodeid.split('::', 1)[0]
|
||||
counts[name] = counts.get(name, 0) + 1
|
||||
for name, count in sorted(counts.items()):
|
||||
self._tw.line("%s: %d" % (name, count))
|
||||
else:
|
||||
for item in items:
|
||||
nodeid = item.nodeid
|
||||
nodeid = nodeid.replace("::()::", "::")
|
||||
self._tw.line(nodeid)
|
||||
return
|
||||
stack = []
|
||||
indent = ""
|
||||
for item in items:
|
||||
needed_collectors = item.listchain()[1:] # strip root node
|
||||
while stack:
|
||||
if stack == needed_collectors[:len(stack)]:
|
||||
break
|
||||
stack.pop()
|
||||
for col in needed_collectors[len(stack):]:
|
||||
stack.append(col)
|
||||
#if col.name == "()":
|
||||
# continue
|
||||
indent = (len(stack) - 1) * " "
|
||||
self._tw.line("%s%s" % (indent, col))
|
||||
|
||||
def pytest_sessionfinish(self, exitstatus, __multicall__):
|
||||
__multicall__.execute()
|
||||
self._tw.line("")
|
||||
if exitstatus in (0, 1, 2, 4):
|
||||
self.summary_errors()
|
||||
self.summary_failures()
|
||||
self.summary_hints()
|
||||
self.config.hook.pytest_terminal_summary(terminalreporter=self)
|
||||
if exitstatus == 2:
|
||||
self._report_keyboardinterrupt()
|
||||
del self._keyboardinterrupt_memo
|
||||
self.summary_deselected()
|
||||
self.summary_stats()
|
||||
|
||||
def pytest_keyboard_interrupt(self, excinfo):
|
||||
self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True)
|
||||
|
||||
def pytest_unconfigure(self):
|
||||
if hasattr(self, '_keyboardinterrupt_memo'):
|
||||
self._report_keyboardinterrupt()
|
||||
|
||||
def _report_keyboardinterrupt(self):
|
||||
excrepr = self._keyboardinterrupt_memo
|
||||
msg = excrepr.reprcrash.message
|
||||
self.write_sep("!", msg)
|
||||
if "KeyboardInterrupt" in msg:
|
||||
if self.config.option.fulltrace:
|
||||
excrepr.toterminal(self._tw)
|
||||
else:
|
||||
excrepr.reprcrash.toterminal(self._tw)
|
||||
|
||||
def _locationline(self, collect_fspath, fspath, lineno, domain):
|
||||
# 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)
|
||||
else:
|
||||
line = "[location]"
|
||||
return line + " "
|
||||
|
||||
def _getfailureheadline(self, rep):
|
||||
if hasattr(rep, 'location'):
|
||||
fspath, lineno, domain = rep.location
|
||||
return domain
|
||||
else:
|
||||
return "test session" # XXX?
|
||||
|
||||
def _getcrashline(self, rep):
|
||||
try:
|
||||
return str(rep.longrepr.reprcrash)
|
||||
except AttributeError:
|
||||
try:
|
||||
return str(rep.longrepr)[:50]
|
||||
except AttributeError:
|
||||
return ""
|
||||
|
||||
#
|
||||
# summaries for sessionfinish
|
||||
#
|
||||
def getreports(self, name):
|
||||
l = []
|
||||
for x in self.stats.get(name, []):
|
||||
if not hasattr(x, '_pdbshown'):
|
||||
l.append(x)
|
||||
return l
|
||||
|
||||
def summary_hints(self):
|
||||
if self.config.option.traceconfig:
|
||||
for hint in self.config.pluginmanager._hints:
|
||||
self._tw.line("hint: %s" % hint)
|
||||
|
||||
def summary_failures(self):
|
||||
if self.config.option.tbstyle != "no":
|
||||
reports = self.getreports('failed')
|
||||
if not reports:
|
||||
return
|
||||
self.write_sep("=", "FAILURES")
|
||||
for rep in reports:
|
||||
if self.config.option.tbstyle == "line":
|
||||
line = self._getcrashline(rep)
|
||||
self.write_line(line)
|
||||
else:
|
||||
msg = self._getfailureheadline(rep)
|
||||
self.write_sep("_", msg)
|
||||
self._outrep_summary(rep)
|
||||
|
||||
def summary_errors(self):
|
||||
if self.config.option.tbstyle != "no":
|
||||
reports = self.getreports('error')
|
||||
if not reports:
|
||||
return
|
||||
self.write_sep("=", "ERRORS")
|
||||
for rep in self.stats['error']:
|
||||
msg = self._getfailureheadline(rep)
|
||||
if not hasattr(rep, 'when'):
|
||||
# collect
|
||||
msg = "ERROR collecting " + msg
|
||||
elif rep.when == "setup":
|
||||
msg = "ERROR at setup of " + msg
|
||||
elif rep.when == "teardown":
|
||||
msg = "ERROR at teardown of " + msg
|
||||
self.write_sep("_", msg)
|
||||
self._outrep_summary(rep)
|
||||
|
||||
def _outrep_summary(self, rep):
|
||||
rep.toterminal(self._tw)
|
||||
for secname, content in rep.sections:
|
||||
self._tw.sep("-", secname)
|
||||
if content[-1:] == "\n":
|
||||
content = content[:-1]
|
||||
self._tw.line(content)
|
||||
|
||||
def summary_stats(self):
|
||||
session_duration = py.std.time.time() - self._sessionstarttime
|
||||
|
||||
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)
|
||||
msg = "%s in %.2f seconds" % (line, session_duration)
|
||||
|
||||
markup = {'bold': True}
|
||||
if 'failed' in self.stats or 'error' in self.stats:
|
||||
markup = {'red': True, 'bold': True}
|
||||
else:
|
||||
markup = {'green': True, 'bold': True}
|
||||
|
||||
if self.verbosity >= 0:
|
||||
self.write_sep("=", msg, **markup)
|
||||
if self.verbosity == -1:
|
||||
self.write_line(msg, **markup)
|
||||
|
||||
def summary_deselected(self):
|
||||
if 'deselected' in self.stats:
|
||||
l = []
|
||||
k = self.config.option.keyword
|
||||
if k:
|
||||
l.append("-k%s" % k)
|
||||
m = self.config.option.markexpr
|
||||
if m:
|
||||
l.append("-m %r" % m)
|
||||
if l:
|
||||
self.write_sep("=", "%d tests deselected by %r" % (
|
||||
len(self.stats['deselected']), " ".join(l)), bold=True)
|
||||
|
||||
def repr_pythonversion(v=None):
|
||||
if v is None:
|
||||
v = sys.version_info
|
||||
try:
|
||||
return "%s.%s.%s-%s-%s" % v
|
||||
except (TypeError, ValueError):
|
||||
return str(v)
|
||||
|
||||
def flatten(l):
|
||||
for x in l:
|
||||
if isinstance(x, (list, tuple)):
|
||||
for y in flatten(x):
|
||||
yield y
|
||||
else:
|
||||
yield x
|
||||
|
||||
71
_pytest/tmpdir.py
Normal file
71
_pytest/tmpdir.py
Normal file
@@ -0,0 +1,71 @@
|
||||
""" support for providing temporary directories to test functions. """
|
||||
import pytest, py
|
||||
from _pytest.monkeypatch import monkeypatch
|
||||
|
||||
class TempdirHandler:
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.trace = config.trace.get("tmpdir")
|
||||
|
||||
def ensuretemp(self, string, dir=1):
|
||||
""" (deprecated) return temporary directory path with
|
||||
the given string as the trailing part. It is usually
|
||||
better to use the 'tmpdir' function argument which
|
||||
provides an empty unique-per-test-invocation directory
|
||||
and is guaranteed to be empty.
|
||||
"""
|
||||
#py.log._apiwarn(">1.1", "use tmpdir function argument")
|
||||
return self.getbasetemp().ensure(string, dir=dir)
|
||||
|
||||
def mktemp(self, basename, numbered=True):
|
||||
basetemp = self.getbasetemp()
|
||||
if not numbered:
|
||||
p = basetemp.mkdir(basename)
|
||||
else:
|
||||
p = py.path.local.make_numbered_dir(prefix=basename,
|
||||
keep=0, rootdir=basetemp, lock_timeout=None)
|
||||
self.trace("mktemp", p)
|
||||
return p
|
||||
|
||||
def getbasetemp(self):
|
||||
""" return base temporary directory. """
|
||||
try:
|
||||
return self._basetemp
|
||||
except AttributeError:
|
||||
basetemp = self.config.option.basetemp
|
||||
if basetemp:
|
||||
basetemp = py.path.local(basetemp)
|
||||
if basetemp.check():
|
||||
basetemp.remove()
|
||||
basetemp.mkdir()
|
||||
else:
|
||||
basetemp = py.path.local.make_numbered_dir(prefix='pytest-')
|
||||
self._basetemp = t = basetemp.realpath()
|
||||
self.trace("new basetemp", t)
|
||||
return t
|
||||
|
||||
def finish(self):
|
||||
self.trace("finish")
|
||||
|
||||
def pytest_configure(config):
|
||||
mp = monkeypatch()
|
||||
t = TempdirHandler(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
|
||||
def tmpdir(request):
|
||||
"""return a temporary directory path object
|
||||
which is unique to each test function invocation,
|
||||
created as a sub directory of the base temporary
|
||||
directory. The returned object is a `py.path.local`_
|
||||
path object.
|
||||
"""
|
||||
name = request.node.name
|
||||
name = py.std.re.sub("[\W]", "_", name)
|
||||
MAXVAL = 30
|
||||
if len(name) > MAXVAL:
|
||||
name = name[:MAXVAL]
|
||||
x = request.config._tmpdirhandler.mktemp(name, numbered=True)
|
||||
return x
|
||||
190
_pytest/unittest.py
Normal file
190
_pytest/unittest.py
Normal file
@@ -0,0 +1,190 @@
|
||||
""" discovery and running of std-library "unittest" style tests. """
|
||||
import pytest, py
|
||||
import sys
|
||||
|
||||
# for transfering markers
|
||||
from _pytest.python import transfer_markers
|
||||
|
||||
|
||||
def is_unittest(obj):
|
||||
"""Is obj a subclass of unittest.TestCase?"""
|
||||
unittest = sys.modules.get('unittest')
|
||||
if unittest is None:
|
||||
return # nobody can have derived unittest.TestCase
|
||||
try:
|
||||
return issubclass(obj, unittest.TestCase)
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def pytest_pycollect_makeitem(collector, name, obj):
|
||||
if is_unittest(obj):
|
||||
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
|
||||
cls = self.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
|
||||
|
||||
if not foundsomething:
|
||||
runtest = getattr(self.obj, 'runTest', None)
|
||||
if runtest is not None:
|
||||
ut = sys.modules.get("twisted.trial.unittest", None)
|
||||
if ut is None or runtest != ut.TestCase.runTest:
|
||||
yield TestCaseFunction('runTest', parent=self)
|
||||
|
||||
|
||||
|
||||
class TestCaseFunction(pytest.Function):
|
||||
_excinfo = None
|
||||
|
||||
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"):
|
||||
self._request._fillfixtures()
|
||||
|
||||
def teardown(self):
|
||||
if hasattr(self._testcase, 'teardown_method'):
|
||||
self._testcase.teardown_method(self._obj)
|
||||
|
||||
def startTest(self, testcase):
|
||||
pass
|
||||
|
||||
def _addexcinfo(self, rawexcinfo):
|
||||
# unwrap potential exception info (see twisted trial support below)
|
||||
rawexcinfo = getattr(rawexcinfo, '_rawexcinfo', rawexcinfo)
|
||||
try:
|
||||
excinfo = py.code.ExceptionInfo(rawexcinfo)
|
||||
except TypeError:
|
||||
try:
|
||||
try:
|
||||
l = py.std.traceback.format_exception(*rawexcinfo)
|
||||
l.insert(0, "NOTE: Incompatible Exception Representation, "
|
||||
"displaying natively:\n\n")
|
||||
pytest.fail("".join(l), pytrace=False)
|
||||
except (pytest.fail.Exception, KeyboardInterrupt):
|
||||
raise
|
||||
except:
|
||||
pytest.fail("ERROR: Unknown Incompatible Exception "
|
||||
"representation:\n%r" %(rawexcinfo,), pytrace=False)
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except pytest.fail.Exception:
|
||||
excinfo = py.code.ExceptionInfo()
|
||||
self.__dict__.setdefault('_excinfo', []).append(excinfo)
|
||||
|
||||
def addError(self, testcase, rawexcinfo):
|
||||
self._addexcinfo(rawexcinfo)
|
||||
def addFailure(self, testcase, rawexcinfo):
|
||||
self._addexcinfo(rawexcinfo)
|
||||
|
||||
def addSkip(self, testcase, reason):
|
||||
try:
|
||||
pytest.skip(reason)
|
||||
except pytest.skip.Exception:
|
||||
self._addexcinfo(sys.exc_info())
|
||||
|
||||
def addExpectedFailure(self, testcase, rawexcinfo, reason=""):
|
||||
try:
|
||||
pytest.xfail(str(reason))
|
||||
except pytest.xfail.Exception:
|
||||
self._addexcinfo(sys.exc_info())
|
||||
|
||||
def addUnexpectedSuccess(self, testcase, reason=""):
|
||||
self._unexpectedsuccess = reason
|
||||
|
||||
def addSuccess(self, testcase):
|
||||
pass
|
||||
|
||||
def stopTest(self, testcase):
|
||||
pass
|
||||
|
||||
def runtest(self):
|
||||
self._testcase(result=self)
|
||||
|
||||
def _prunetraceback(self, excinfo):
|
||||
pytest.Function._prunetraceback(self, excinfo)
|
||||
traceback = excinfo.traceback.filter(
|
||||
lambda x:not x.frame.f_globals.get('__unittest'))
|
||||
if traceback:
|
||||
excinfo.traceback = traceback
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
def pytest_runtest_makereport(item, call):
|
||||
if isinstance(item, TestCaseFunction):
|
||||
if item._excinfo:
|
||||
call.excinfo = item._excinfo.pop(0)
|
||||
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
|
||||
try:
|
||||
return __multicall__.execute()
|
||||
finally:
|
||||
ut.Failure.__init__ = Failure__init__
|
||||
|
||||
def check_testcase_implements_trial_reporter(done=[]):
|
||||
if done:
|
||||
return
|
||||
from zope.interface import classImplements
|
||||
from twisted.trial.itrial import IReporter
|
||||
classImplements(TestCaseFunction, IReporter)
|
||||
done.append(1)
|
||||
10
bench/bench.py
Normal file
10
bench/bench.py
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
if __name__ == '__main__':
|
||||
import cProfile
|
||||
import py
|
||||
import pstats
|
||||
stats = cProfile.run('py.test.cmdline.main(["empty.py", ])', 'prof')
|
||||
p = pstats.Stats("prof")
|
||||
p.strip_dirs()
|
||||
p.sort_stats('cumulative')
|
||||
print(p.print_stats(50))
|
||||
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)))
|
||||
3
bench/empty.py
Normal file
3
bench/empty.py
Normal file
@@ -0,0 +1,3 @@
|
||||
import py
|
||||
for i in range(1000):
|
||||
py.builtin.exec_("def test_func_%d(): pass" % i)
|
||||
@@ -1,329 +0,0 @@
|
||||
"""
|
||||
Tested with coverage 2.85 and pygments 1.0
|
||||
|
||||
TODO:
|
||||
+ 'html-output/*,cover' should be deleted
|
||||
+ credits for coverage
|
||||
+ credits for pygments
|
||||
+ 'Install pygments' after ImportError is to less
|
||||
+ is the way of determining DIR_CSS_RESOURCE ok?
|
||||
+ write plugin test
|
||||
+ '.coverage' still exists in py.test execution dir
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import shutil
|
||||
from StringIO import StringIO
|
||||
|
||||
import py
|
||||
|
||||
try:
|
||||
from pygments import highlight
|
||||
from pygments.lexers import get_lexer_by_name
|
||||
from pygments.formatters import HtmlFormatter
|
||||
except ImportError:
|
||||
print "Install pygments" # XXX
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
DIR_CUR = str(py.path.local())
|
||||
REPORT_FILE = os.path.join(DIR_CUR, '.coverage')
|
||||
DIR_ANNOTATE_OUTPUT = os.path.join(DIR_CUR, '.coverage_annotate')
|
||||
COVERAGE_MODULES = set()
|
||||
# coverage output parsing
|
||||
REG_COVERAGE_SUMMARY = re.compile('([a-z_\.]+) +([0-9]+) +([0-9]+) +([0-9]+%)')
|
||||
REG_COVERAGE_SUMMARY_TOTAL = re.compile('(TOTAL) +([0-9]+) +([0-9]+) +([0-9]+%)')
|
||||
DEFAULT_COVERAGE_OUTPUT = '.coverage_annotation'
|
||||
# HTML output specific
|
||||
DIR_CSS_RESOURCE = os.path.dirname(__import__('pytest_coverage').__file__)
|
||||
CSS_RESOURCE_FILES = ['header_bg.jpg', 'links.gif']
|
||||
|
||||
COVERAGE_TERM_HEADER = "\nCOVERAGE INFORMATION\n" \
|
||||
"====================\n"
|
||||
HTML_INDEX_HEADER = '''<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
||||
<html>
|
||||
<head>
|
||||
<title>py.test - Coverage Index</title>
|
||||
|
||||
<style type="text/css">
|
||||
table {
|
||||
font-size:0.9em;
|
||||
font-family: Arial, Helvetica, verdana sans-serif;
|
||||
background-color:#fff;
|
||||
border-collapse: collapse;
|
||||
width: 500px;
|
||||
}
|
||||
caption {
|
||||
font-size: 25px;
|
||||
color: #1ba6b2;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
background: url(header_bg.jpg) no-repeat top left;
|
||||
padding: 10px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
thead th {
|
||||
border-right: 1px solid #fff;
|
||||
color:#fff;
|
||||
text-align:center;
|
||||
padding:2px;
|
||||
text-transform:uppercase;
|
||||
height:25px;
|
||||
background-color: #a3c159;
|
||||
font-weight: normal;
|
||||
}
|
||||
tfoot {
|
||||
color:#1ba6b2;
|
||||
padding:2px;
|
||||
text-transform:uppercase;
|
||||
font-size:1.2em;
|
||||
font-weigth: bold;
|
||||
margin-top:6px;
|
||||
border-top: 6px solid #e9f7f6;
|
||||
}
|
||||
tfoot td {
|
||||
text-align: left;
|
||||
}
|
||||
tbody tr {
|
||||
background-color:#fff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
tbody td {
|
||||
color:#414141;
|
||||
padding:5px;
|
||||
text-align:left;
|
||||
}
|
||||
tbody th {
|
||||
text-align:left;
|
||||
padding:2px;
|
||||
}
|
||||
tbody td a, tbody th a {
|
||||
color:#6C8C37;
|
||||
text-decoration:none;
|
||||
font-weight:normal;
|
||||
display:block;
|
||||
background: transparent url(links.gif) no-repeat 0% 50%;
|
||||
padding-left:15px;
|
||||
}
|
||||
tbody td a:hover, tbody th a:hover {
|
||||
color:#009193;
|
||||
text-decoration:none;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body
|
||||
<table >
|
||||
<caption>Module Coverage</caption>
|
||||
<tbody>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Module</th>
|
||||
<th>Statements</th>
|
||||
<th>Executed</th>
|
||||
<th>Coverage</th>
|
||||
</tr>
|
||||
</thead>'''
|
||||
HTML_INDEX_FOOTER = ''' </tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>'''
|
||||
|
||||
|
||||
class CoverageHtmlFormatter(HtmlFormatter):
|
||||
"""XXX: doc"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
HtmlFormatter.__init__(self,*args, **kwargs)
|
||||
self.annotation_infos = kwargs.get('annotation_infos')
|
||||
|
||||
def _highlight_lines(self, tokensource):
|
||||
"""
|
||||
XXX: doc
|
||||
"""
|
||||
|
||||
hls = self.hl_lines
|
||||
self.annotation_infos = [None] + self.annotation_infos
|
||||
hls = [l for l, i in enumerate(self.annotation_infos) if i]
|
||||
for i, (t, value) in enumerate(tokensource):
|
||||
if t != 1:
|
||||
yield t, value
|
||||
if i + 1 in hls: # i + 1 because Python indexes start at 0
|
||||
if self.annotation_infos[i+1] == "!":
|
||||
yield 1, '<span style="background-color:#FFE5E5">%s</span>' \
|
||||
% value
|
||||
elif self.annotation_infos[i+1] == ">":
|
||||
yield 1, '<span style="background-color:#CCFFEB">%s</span>' \
|
||||
% value
|
||||
else:
|
||||
raise ValueError("HHAHA: %s" % self.annotation_infos[i+1])
|
||||
else:
|
||||
yield 1, value
|
||||
|
||||
|
||||
def _rename_annotation_files(module_list, dir_annotate_output):
|
||||
for m in module_list:
|
||||
mod_fpath = os.path.basename(m.__file__)
|
||||
if mod_fpath.endswith('pyc'):
|
||||
mod_fpath = mod_fpath[:-1]
|
||||
old = os.path.join(dir_annotate_output, '%s,cover'% mod_fpath)
|
||||
new = os.path.join(dir_annotate_output, '%s,cover'% m.__name__)
|
||||
if os.path.isfile(old):
|
||||
shutil.move(old, new)
|
||||
yield new
|
||||
|
||||
def _generate_module_coverage(mc_path, anotation_infos, src_lines):
|
||||
#XXX: doc
|
||||
|
||||
code = "".join(src_lines)
|
||||
mc_path = "%s.html" % mc_path
|
||||
lexer = get_lexer_by_name("python", stripall=True)
|
||||
formatter = CoverageHtmlFormatter(linenos=True, noclasses=True,
|
||||
hl_lines=[1], annotation_infos=anotation_infos)
|
||||
result = highlight(code, lexer, formatter)
|
||||
fp = open(mc_path, 'w')
|
||||
fp.write(result)
|
||||
fp.close()
|
||||
|
||||
def _parse_modulecoverage(mc_fpath):
|
||||
#XXX: doc
|
||||
|
||||
fd = open(mc_fpath, 'r')
|
||||
anotate_infos = []
|
||||
src_lines = []
|
||||
for line in fd.readlines():
|
||||
anotate_info = line[0:2].strip()
|
||||
if not anotate_info:
|
||||
anotate_info = None
|
||||
src_line = line[2:]
|
||||
anotate_infos.append(anotate_info)
|
||||
src_lines.append(src_line)
|
||||
return mc_fpath, anotate_infos, src_lines
|
||||
|
||||
def _parse_coverage_summary(fd):
|
||||
"""Parses coverage summary output."""
|
||||
|
||||
if hasattr(fd, 'readlines'):
|
||||
fd.seek(0)
|
||||
for l in fd.readlines():
|
||||
m = REG_COVERAGE_SUMMARY.match(l)
|
||||
if m:
|
||||
# yield name, stmts, execs, cover
|
||||
yield m.group(1), m.group(2), m.group(3), m.group(4)
|
||||
else:
|
||||
m = REG_COVERAGE_SUMMARY_TOTAL.match(l)
|
||||
if m:
|
||||
# yield name, stmts, execs, cover
|
||||
yield m.group(1), m.group(2), m.group(3), m.group(4)
|
||||
|
||||
|
||||
def _get_coverage_index(mod_name, stmts, execs, cover, annotation_dir):
|
||||
"""
|
||||
Generates the index page where are all modulare coverage reports are
|
||||
linked.
|
||||
"""
|
||||
|
||||
if mod_name == 'TOTAL':
|
||||
return '<tfoot><tr style="text-align: center;font-weigth:bold;font-size:1.2em; "><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr></tfoot>\n' % (mod_name, stmts, execs, cover)
|
||||
covrep_fpath = os.path.join(annotation_dir, '%s,cover.html' % mod_name)
|
||||
assert os.path.isfile(covrep_fpath) == True
|
||||
fname = os.path.basename(covrep_fpath)
|
||||
modlink = '<a href="%s">%s</a>' % (fname, mod_name)
|
||||
return '<tr style="text-align: center;"><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>\n' % (modlink, stmts, execs, cover)
|
||||
|
||||
|
||||
class CoveragePlugin:
|
||||
def pytest_addoption(self, parser):
|
||||
group = parser.addgroup('coverage options')
|
||||
group.addoption('-C', action='store_true', default=False,
|
||||
dest = 'coverage',
|
||||
help=('displays coverage information.'))
|
||||
group.addoption('--coverage-html', action='store', default=False,
|
||||
dest='coverage_annotation',
|
||||
help='path to the coverage HTML output dir.')
|
||||
group.addoption('--coverage-css-resourcesdir', action='store',
|
||||
default=DIR_CSS_RESOURCE,
|
||||
dest='coverage_css_ressourcedir',
|
||||
help='path to dir with css-resources (%s) for '
|
||||
'being copied to the HTML output dir.' % \
|
||||
", ".join(CSS_RESOURCE_FILES))
|
||||
|
||||
def pytest_configure(self, config):
|
||||
if config.getvalue('coverage'):
|
||||
try:
|
||||
import coverage
|
||||
except ImportError:
|
||||
raise config.Error("To run use the coverage option you have to install " \
|
||||
"Ned Batchelder's coverage: "\
|
||||
"http://nedbatchelder.com/code/modules/coverage.html")
|
||||
self.coverage = coverage
|
||||
self.summary = None
|
||||
|
||||
def pytest_terminal_summary(self, terminalreporter):
|
||||
if hasattr(self, 'coverage'):
|
||||
self.coverage.stop()
|
||||
module_list = [sys.modules[mod] for mod in COVERAGE_MODULES]
|
||||
module_list.sort()
|
||||
summary_fd = StringIO()
|
||||
# get coverage reports by module list
|
||||
self.coverage.report(module_list, file=summary_fd)
|
||||
summary = COVERAGE_TERM_HEADER + summary_fd.getvalue()
|
||||
terminalreporter._tw.write(summary)
|
||||
|
||||
config = terminalreporter.config
|
||||
dir_annotate_output = config.getvalue('coverage_annotation')
|
||||
if dir_annotate_output:
|
||||
if dir_annotate_output == "":
|
||||
dir_annotate_output = DIR_ANNOTATE_OUTPUT
|
||||
# create dir
|
||||
if os.path.isdir(dir_annotate_output):
|
||||
shutil.rmtree(dir_annotate_output)
|
||||
os.mkdir(dir_annotate_output)
|
||||
# generate annotation text files for later parsing
|
||||
self.coverage.annotate(module_list, dir_annotate_output)
|
||||
# generate the separate module coverage reports
|
||||
for mc_fpath in _rename_annotation_files(module_list, \
|
||||
dir_annotate_output):
|
||||
# mc_fpath, anotate_infos, src_lines from _parse_do
|
||||
_generate_module_coverage(*_parse_modulecoverage(mc_fpath))
|
||||
# creating contents for the index pagee for coverage report
|
||||
idxpage_html = StringIO()
|
||||
idxpage_html.write(HTML_INDEX_HEADER)
|
||||
total_sum = None
|
||||
for args in _parse_coverage_summary(summary_fd):
|
||||
# mod_name, stmts, execs, cover = args
|
||||
idxpage_html.write(_get_coverage_index(*args, \
|
||||
**dict(annotation_dir=dir_annotate_output)))
|
||||
idxpage_html.write(HTML_INDEX_FOOTER)
|
||||
idx_fpath = os.path.join(dir_annotate_output, 'index.html')
|
||||
idx_fd = open(idx_fpath, 'w')
|
||||
idx_fd.write(idxpage_html.getvalue())
|
||||
idx_fd.close()
|
||||
|
||||
dir_css_resource_dir = config.getvalue('coverage_css_ressourcedir')
|
||||
if dir_annotate_output and dir_css_resource_dir != "":
|
||||
if not os.path.isdir(dir_css_resource_dir):
|
||||
raise config.Error("CSS resource dir not found: '%s'" % \
|
||||
dir_css_resource_dir)
|
||||
for r in CSS_RESOURCE_FILES:
|
||||
src = os.path.join(dir_css_resource_dir, r)
|
||||
if os.path.isfile(src):
|
||||
dest = os.path.join(dir_annotate_output, r)
|
||||
shutil.copy(src, dest)
|
||||
|
||||
def pytest_collectstart(self, collector):
|
||||
if isinstance(collector, py.__.test.pycollect.Module):
|
||||
COVERAGE_MODULES.update(getattr(collector.obj,
|
||||
'COVERAGE_MODULES', []))
|
||||
|
||||
def pytest_testrunstart(self):
|
||||
print "self.coverage", self.coverage
|
||||
if hasattr(self, 'coverage'):
|
||||
print "START coverage"
|
||||
self.coverage.erase()
|
||||
self.coverage.start()
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 75 B |
@@ -1,374 +0,0 @@
|
||||
"""XXX in progress: resultdb plugin for database logging of test results.
|
||||
|
||||
Saves test results to a datastore.
|
||||
|
||||
XXX this needs to be merged with resultlog plugin
|
||||
|
||||
Also mixes in some early ideas about an archive abstraction for test
|
||||
results.
|
||||
"""
|
||||
import py
|
||||
|
||||
py.test.skip("XXX needs to be merged with resultlog")
|
||||
|
||||
from pytest_resultlog import ResultLog
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.addgroup("resultdb", "resultdb plugin options")
|
||||
group.addoption('--resultdb', action="store", dest="resultdb",
|
||||
metavar="path",
|
||||
help="path to the file to store test results.")
|
||||
group.addoption('--resultdb_format', action="store",
|
||||
dest="resultdbformat", default='json',
|
||||
help="data format (json, sqlite)")
|
||||
|
||||
def pytest_configure(config):
|
||||
# XXX using config.XYZ is not good
|
||||
if config.getvalue('resultdb'):
|
||||
if config.option.resultdb:
|
||||
# local import so missing module won't crash py.test
|
||||
try:
|
||||
import sqlite3
|
||||
except ImportError:
|
||||
raise config.Error('Could not import sqlite3 module')
|
||||
try:
|
||||
import simplejson
|
||||
except ImportError:
|
||||
raise config.Error('Could not import simplejson module')
|
||||
if config.option.resultdbformat.lower() == 'json':
|
||||
resultdb = ResultDB(JSONResultArchive,
|
||||
config.option.resultdb)
|
||||
elif config.option.resultdbformat.lower() == 'sqlite':
|
||||
resultdb = ResultDB(SQLiteResultArchive,
|
||||
config.option.resultdb)
|
||||
else:
|
||||
raise config.Error('Unknown --resultdb_format: %s' %
|
||||
config.option.resultdbformat)
|
||||
|
||||
config.pluginmanager.register(resultdb)
|
||||
|
||||
class JSONResultArchive(object):
|
||||
def __init__(self, archive_path):
|
||||
self.archive_path = archive_path
|
||||
import simplejson
|
||||
self.simplejson = simplejson
|
||||
|
||||
def init_db(self):
|
||||
if os.path.exists(self.archive_path):
|
||||
data_file = open(self.archive_path)
|
||||
archive = self.simplejson.load(data_file)
|
||||
self.archive = archive
|
||||
else:
|
||||
self.archive = []
|
||||
self._flush()
|
||||
|
||||
def append_data(self, data):
|
||||
runid = py.std.uuid.uuid4()
|
||||
for item in data:
|
||||
item = item.copy()
|
||||
item['runid'] = str(runid)
|
||||
self.archive.append(item)
|
||||
self._flush()
|
||||
|
||||
def get_all_data(self):
|
||||
return self.archive
|
||||
|
||||
def _flush(self):
|
||||
data_file = open(self.archive_path, 'w')
|
||||
self.simplejson.dump(self.archive, data_file)
|
||||
data_file.close()
|
||||
|
||||
|
||||
class SQLiteResultArchive(object):
|
||||
def __init__(self, archive_path):
|
||||
self.archive_path = archive_path
|
||||
import sqlite3
|
||||
self.sqlite3 = sqlite3
|
||||
|
||||
def init_db(self):
|
||||
if not os.path.exists(self.archive_path):
|
||||
conn = self.sqlite3.connect(self.archive_path)
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
cursor.execute(SQL_CREATE_TABLES)
|
||||
conn.commit()
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
def append_data(self, data):
|
||||
flat_data = []
|
||||
runid = py.std.uuid.uuid4()
|
||||
for item in data:
|
||||
item = item.copy()
|
||||
item['runid'] = str(runid)
|
||||
flat_data.append(self.flatten(item))
|
||||
conn = self.sqlite3.connect(self.archive_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.executemany(SQL_INSERT_DATA, flat_data)
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
def get_all_data(self):
|
||||
conn = self.sqlite3.connect(self.archive_path)
|
||||
conn.row_factory = self.sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(SQL_SELECT_DATA)
|
||||
data = cursor.fetchall()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
data = [self.unflatten(item) for item in data]
|
||||
return data
|
||||
|
||||
def flatten(self, item):
|
||||
return (item.get('runid', None),
|
||||
item.get('name', None),
|
||||
item.get('passed', False),
|
||||
item.get('skipped', False),
|
||||
item.get('failed', False),
|
||||
item.get('shortrepr', None),
|
||||
item.get('longrepr', None),
|
||||
item.get('fspath', None),
|
||||
item.get('itemname', None),
|
||||
)
|
||||
|
||||
def unflatten(self, item):
|
||||
names = ("runid name passed skipped failed shortrepr "
|
||||
"longrepr fspath itemname").split()
|
||||
d = {}
|
||||
for i, name in enumerate(names):
|
||||
d[name] = item[i]
|
||||
return d
|
||||
|
||||
|
||||
class ResultDB(ResultLog):
|
||||
def __init__(self, cls, db_path):
|
||||
self.archive = cls(db_path)
|
||||
self.archive.init_db()
|
||||
|
||||
def write_log_entry(self, testpath, shortrepr, longrepr):
|
||||
data = {}
|
||||
event_excludes = ['colitem', 'longrepr']
|
||||
for item in vars(event).keys():
|
||||
if item not in event_excludes:
|
||||
data[item] = getattr(event, item)
|
||||
# use the locally calculated longrepr & shortrepr
|
||||
data['longrepr'] = longrepr
|
||||
data['shortrepr'] = shortrepr
|
||||
|
||||
data['testpath'] = unicode(testpath)
|
||||
self.archive.append_data([data])
|
||||
|
||||
|
||||
SQL_CREATE_TABLES = """
|
||||
create table pytest_results (
|
||||
runid varchar(36),
|
||||
name varchar,
|
||||
passed int,
|
||||
skipped int,
|
||||
failed int,
|
||||
shortrepr varchar,
|
||||
longrepr varchar,
|
||||
fspath varchar,
|
||||
itemname varchar
|
||||
);
|
||||
"""
|
||||
SQL_INSERT_DATA = """
|
||||
insert into pytest_results (
|
||||
runid,
|
||||
name,
|
||||
passed,
|
||||
skipped,
|
||||
failed,
|
||||
shortrepr,
|
||||
longrepr,
|
||||
fspath,
|
||||
itemname)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
"""
|
||||
SQL_SELECT_DATA = """
|
||||
select
|
||||
runid,
|
||||
name,
|
||||
passed,
|
||||
skipped,
|
||||
failed,
|
||||
shortrepr,
|
||||
longrepr,
|
||||
fspath,
|
||||
itemname
|
||||
from pytest_results;
|
||||
"""
|
||||
|
||||
|
||||
# ===============================================================================
|
||||
#
|
||||
# plugin tests
|
||||
#
|
||||
# ===============================================================================
|
||||
|
||||
import os, StringIO
|
||||
|
||||
class BaseResultArchiveTests(object):
|
||||
cls = None
|
||||
|
||||
def setup_class(cls):
|
||||
# XXX refactor setup into a funcarg?
|
||||
cls.tempdb = "test_tempdb"
|
||||
|
||||
def test_init_db(self, testdir):
|
||||
tempdb_path = unicode(testdir.tmpdir.join(self.tempdb))
|
||||
archive = self.cls(tempdb_path)
|
||||
archive.init_db()
|
||||
assert os.path.exists(tempdb_path)
|
||||
|
||||
def test_db_insert(self, testdir):
|
||||
tempdb_path = unicode(testdir.tmpdir.join(self.tempdb))
|
||||
archive = self.cls(tempdb_path)
|
||||
archive.init_db()
|
||||
assert len(archive.get_all_data()) == 0
|
||||
|
||||
data = [{'name': 'tmppackage/test_whatever.py:test_hello',
|
||||
'fspath': '/Users/brian/work/tmppackage/test_whatever.py',
|
||||
'name': 'test_hello',
|
||||
'longrepr': '',
|
||||
'passed': True,
|
||||
'shortrepr': '.'
|
||||
}]
|
||||
archive.append_data(data)
|
||||
result = archive.get_all_data()
|
||||
print result
|
||||
assert len(result) == 1
|
||||
for key, value in data[0].items():
|
||||
assert value == result[0][key]
|
||||
assert 'runid' in result[0]
|
||||
|
||||
# make sure the data is persisted
|
||||
tempdb_path = unicode(testdir.tmpdir.join(self.tempdb))
|
||||
archive = self.cls(tempdb_path)
|
||||
archive.init_db()
|
||||
assert len(archive.get_all_data()) == 1
|
||||
|
||||
|
||||
class TestJSONResultArchive(BaseResultArchiveTests):
|
||||
cls = JSONResultArchive
|
||||
|
||||
def setup_method(self, method):
|
||||
py.test.importorskip("simplejson")
|
||||
|
||||
|
||||
class TestSQLiteResultArchive(BaseResultArchiveTests):
|
||||
cls = SQLiteResultArchive
|
||||
|
||||
def setup_method(self, method):
|
||||
py.test.importorskip("sqlite3")
|
||||
|
||||
def test_init_db_sql(self, testdir):
|
||||
py.test.importorskip("sqlite3")
|
||||
tempdb_path = unicode(testdir.tmpdir.join(self.tempdb))
|
||||
archive = self.cls(tempdb_path)
|
||||
archive.init_db()
|
||||
assert os.path.exists(tempdb_path)
|
||||
|
||||
# is table in the database?
|
||||
import sqlite3
|
||||
conn = sqlite3.connect(tempdb_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""SELECT name FROM sqlite_master
|
||||
ORDER BY name;""")
|
||||
tables = cursor.fetchall()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
assert len(tables) == 1
|
||||
|
||||
def verify_archive_item_shape(item):
|
||||
names = ("runid name passed skipped failed shortrepr "
|
||||
"longrepr fspath itemname").split()
|
||||
for name in names:
|
||||
assert name in item
|
||||
|
||||
class TestWithFunctionIntegration:
|
||||
def getarchive(self, testdir, arg):
|
||||
py.test.importorskip("sqlite3")
|
||||
py.test.importorskip("simplejson")
|
||||
resultdb = testdir.tmpdir.join("resultdb")
|
||||
args = ["--resultdb=%s" % resultdb, "--resultdb_format=sqlite"] + [arg]
|
||||
testdir.runpytest(*args)
|
||||
assert resultdb.check(file=1)
|
||||
archive = SQLiteResultArchive(unicode(resultdb))
|
||||
archive.init_db()
|
||||
return archive
|
||||
|
||||
def test_collection_report(self, testdir):
|
||||
py.test.skip("Needs a rewrite for db version.")
|
||||
ok = testdir.makepyfile(test_collection_ok="")
|
||||
skip = testdir.makepyfile(test_collection_skip="import py ; py.test.skip('hello')")
|
||||
fail = testdir.makepyfile(test_collection_fail="XXX")
|
||||
|
||||
lines = self.getresultdb(testdir, ok)
|
||||
assert not lines
|
||||
|
||||
lines = self.getresultdb(testdir, skip)
|
||||
assert len(lines) == 2
|
||||
assert lines[0].startswith("S ")
|
||||
assert lines[0].endswith("test_collection_skip.py")
|
||||
assert lines[1].startswith(" ")
|
||||
assert lines[1].endswith("test_collection_skip.py:1: Skipped: 'hello'")
|
||||
|
||||
lines = self.getresultdb(testdir, fail)
|
||||
assert lines
|
||||
assert lines[0].startswith("F ")
|
||||
assert lines[0].endswith("test_collection_fail.py"), lines[0]
|
||||
for x in lines[1:]:
|
||||
assert x.startswith(" ")
|
||||
assert "XXX" in "".join(lines[1:])
|
||||
|
||||
def test_log_test_outcomes(self, testdir):
|
||||
mod = testdir.makepyfile(test_mod="""
|
||||
import py
|
||||
def test_pass(): pass
|
||||
def test_skip(): py.test.skip("hello")
|
||||
def test_fail(): raise ValueError("val")
|
||||
""")
|
||||
|
||||
archive = self.getarchive(testdir, mod)
|
||||
data = archive.get_all_data()
|
||||
for item in data:
|
||||
verify_archive_item_shape(item)
|
||||
assert len(data) == 3
|
||||
assert len([item for item in data if item['passed'] == True]) == 1
|
||||
assert len([item for item in data if item['skipped'] == True]) == 1
|
||||
assert len([item for item in data if item['failed'] == True]) == 1
|
||||
|
||||
def test_internal_exception(self):
|
||||
py.test.skip("Needs a rewrite for db version.")
|
||||
# they are produced for example by a teardown failing
|
||||
# at the end of the run
|
||||
try:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
excinfo = py.code.ExceptionInfo()
|
||||
reslog = ResultDB(StringIO.StringIO())
|
||||
reslog.pytest_internalerror(excinfo.getrepr)
|
||||
entry = reslog.logfile.getvalue()
|
||||
entry_lines = entry.splitlines()
|
||||
|
||||
assert entry_lines[0].startswith('! ')
|
||||
assert os.path.basename(__file__)[:-1] in entry_lines[0] #.py/.pyc
|
||||
assert entry_lines[-1][0] == ' '
|
||||
assert 'ValueError' in entry
|
||||
|
||||
def test_generic(testdir):
|
||||
testdir.makepyfile("""
|
||||
import py
|
||||
def test_pass():
|
||||
pass
|
||||
def test_fail():
|
||||
assert 0
|
||||
def test_skip():
|
||||
py.test.skip("")
|
||||
""")
|
||||
testdir.runpytest("--resultdb=result.sqlite")
|
||||
#testdir.tmpdir.join("result.sqlite")
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
"""
|
||||
Allows to test twisted applications with pytest.
|
||||
|
||||
Notes: twisted's asynchronous behavior may have influence on the order of test-functions
|
||||
|
||||
TODO:
|
||||
+ credits to Ralf Schmitt See: http://twistedmatrix.com/pipermail/twisted-python/2007-February/014872.html
|
||||
+ get test to work
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
try:
|
||||
from twisted.internet import reactor, defer
|
||||
from twisted.python import failure, log
|
||||
except ImportError:
|
||||
print "To use the twisted option you have to install twisted."
|
||||
sys.exit(10)
|
||||
try:
|
||||
from greenlet import greenlet
|
||||
except ImportError:
|
||||
print "Since pylib 1.0 greenlet are removed and separately packaged: " \
|
||||
"http://pypi.python.org/pypi/greenlet"
|
||||
sys.exit(10)
|
||||
|
||||
|
||||
def _start_twisted_logging():
|
||||
"""Enables twisted internal logging"""
|
||||
|
||||
class Logger(object):
|
||||
"""late-bound sys.stdout"""
|
||||
def write(self, msg):
|
||||
sys.stdout.write(msg)
|
||||
|
||||
def flush(self):
|
||||
sys.stdout.flush()
|
||||
# sys.stdout will be changed by py.test later.
|
||||
log.startLogging(Logger(), setStdout=0)
|
||||
|
||||
def _run_twisted(logging=False):
|
||||
"""Start twisted mainloop and initialize recursive calling of doit()."""
|
||||
|
||||
# make twisted copy traceback...
|
||||
failure.Failure.cleanFailure = lambda *args: None
|
||||
if logging:
|
||||
_start_twisted_logging()
|
||||
|
||||
def fix_signal_handling():
|
||||
# see http://twistedmatrix.com/trac/ticket/733
|
||||
import signal
|
||||
if hasattr(signal, "siginterrupt"):
|
||||
signal.siginterrupt(signal.SIGCHLD, False)
|
||||
|
||||
def start():
|
||||
fix_signal_handling()
|
||||
doit(None)
|
||||
|
||||
# recursively called for each test-function/method due done()
|
||||
def doit(val): # val always None
|
||||
# switch context to wait that wrapper() passes back to test-method
|
||||
res = gr_tests.switch(val)
|
||||
if res is None:
|
||||
reactor.stop()
|
||||
return
|
||||
|
||||
def done(res):
|
||||
reactor.callLater(0.0, doit, None) # recursive call of doit()
|
||||
|
||||
def err(res):
|
||||
reactor.callLater(0.0, doit, res)
|
||||
|
||||
# the test-function *may* return a deferred
|
||||
# here the test-function will actually been called
|
||||
# done() is finalizing a test-process by assuring recursive invoking
|
||||
# of doit()
|
||||
defer.maybeDeferred(res).addCallback(done).addErrback(err)
|
||||
# initially preparing the calling of doit() and starting the reactor
|
||||
reactor.callLater(0.0, start)
|
||||
reactor.run()
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.addgroup('twisted options')
|
||||
group.addoption('--twisted-logging', action='store_true', default=False,
|
||||
dest='twisted_logging',
|
||||
help="switch on twisted internal logging")
|
||||
|
||||
def pytest_configure(config):
|
||||
twisted_logging = config.getvalue("twisted_logging")
|
||||
gr_twisted.switch(twisted_logging)
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
gr_twisted.switch(None)
|
||||
|
||||
def pytest_pyfunc_call(pyfuncitem):
|
||||
# XXX1 kwargs?
|
||||
# XXX2 we want to delegate actual call to next plugin
|
||||
# (which may want to produce test coverage, etc.)
|
||||
res = gr_twisted.switch(lambda: pyfuncitem.call())
|
||||
if res:
|
||||
res.raiseException()
|
||||
return True # indicates that we performed the function call
|
||||
|
||||
gr_twisted = greenlet(_run_twisted)
|
||||
gr_tests = greenlet.getcurrent()
|
||||
|
||||
# ===============================================================================
|
||||
# plugin tests
|
||||
# ===============================================================================
|
||||
|
||||
def test_generic(testdir):
|
||||
testdir.makepyfile('''
|
||||
def test_pass():
|
||||
pass
|
||||
from twisted.internet import defer, reactor
|
||||
from twisted.python import failure
|
||||
from twisted.python import log
|
||||
|
||||
|
||||
def test_no_deferred():
|
||||
assert True is True
|
||||
|
||||
def test_deferred():
|
||||
log.msg("test_deferred() called")
|
||||
d = defer.Deferred()
|
||||
def done():
|
||||
log.msg("test_deferred.done() CALLBACK DONE")
|
||||
d.callback(None)
|
||||
|
||||
reactor.callLater(2.5, done)
|
||||
log.msg("test_deferred() returning deferred: %r" % (d,))
|
||||
return d
|
||||
|
||||
def test_deferred2():
|
||||
log.msg("test_deferred2() called")
|
||||
d = defer.Deferred()
|
||||
def done():
|
||||
log.msg("test_deferred2.done() CALLBACK DONE")
|
||||
d.callback(None)
|
||||
|
||||
reactor.callLater(2.5, done)
|
||||
log.msg("test_deferred2() returning deferred: %r" % (d,))
|
||||
return d
|
||||
|
||||
def test_deferred4():
|
||||
log.msg("test_deferred4() called")
|
||||
from twisted.web.client import getPage
|
||||
def printContents(contents):
|
||||
assert contents == ""
|
||||
|
||||
deferred = getPage('http://twistedmatrix.com/')
|
||||
deferred.addCallback(printContents)
|
||||
return deferred
|
||||
|
||||
def test_deferred3():
|
||||
log.msg("test_deferred3() called")
|
||||
d = defer.Deferred()
|
||||
def done():
|
||||
log.msg("test_deferred3.done() CALLBACK DONE")
|
||||
d.callback(None)
|
||||
|
||||
reactor.callLater(2.5, done)
|
||||
log.msg("test_deferred3() returning deferred: %r" % (d,))
|
||||
return d
|
||||
|
||||
class TestTwistedSetupMethod:
|
||||
def setup_method(self, method):
|
||||
log.msg("TestTwistedSetupMethod.setup_method() called")
|
||||
|
||||
def test_deferred(self):
|
||||
log.msg("TestTwistedSetupMethod.test_deferred() called")
|
||||
d = defer.Deferred()
|
||||
def done():
|
||||
log.msg("TestTwistedSetupMethod.test_deferred() CALLBACK DONE")
|
||||
d.callback(None)
|
||||
|
||||
reactor.callLater(2.5, done)
|
||||
log.msg("TestTwistedSetupMethod.test_deferred() returning deferred: %r" % (d,))
|
||||
return d
|
||||
|
||||
|
||||
def test_defer_fail():
|
||||
def fun():
|
||||
log.msg("provoking NameError")
|
||||
rsdfg
|
||||
return defer.maybeDeferred(fun)
|
||||
''')
|
||||
testdir.runpytest("-T")
|
||||
# XXX: what to do?
|
||||
# s = testdir.tmpdir.join("event.log").read()
|
||||
# assert s.find("TestrunFinish") != -1
|
||||
@@ -1,2 +0,0 @@
|
||||
|
||||
pygreen: experimental IO and execnet operations through greenlets
|
||||
@@ -1,116 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
|
||||
small utility for hot-syncing a svn repository through ssh.
|
||||
uses py.execnet.
|
||||
|
||||
"""
|
||||
|
||||
import py
|
||||
import sys, os
|
||||
|
||||
def usage():
|
||||
arg0 = sys.argv[0]
|
||||
print """%s [user@]remote-host:/repo/location localrepo [identity keyfile]""" % (arg0,)
|
||||
|
||||
|
||||
def main(args):
|
||||
remote = args[0]
|
||||
localrepo = py.path.local(args[1])
|
||||
if not localrepo.check(dir=1):
|
||||
raise SystemExit("localrepo %s does not exist" %(localrepo,))
|
||||
if len(args) == 3:
|
||||
keyfile = py.path.local(args[2])
|
||||
else:
|
||||
keyfile = None
|
||||
remote_host, path = remote.split(':', 1)
|
||||
print "ssh-connecting to", remote_host
|
||||
gw = getgateway(remote_host, keyfile)
|
||||
|
||||
local_rev = get_svn_youngest(localrepo)
|
||||
|
||||
# local protocol
|
||||
# 1. client sends rev/repo -> server
|
||||
# 2. server checks for newer revisions and sends dumps
|
||||
# 3. client receives dumps, updates local repo
|
||||
# 4. client goes back to step 1
|
||||
c = gw.remote_exec("""
|
||||
import py
|
||||
import os
|
||||
remote_rev, repopath = channel.receive()
|
||||
while 1:
|
||||
rev = py.process.cmdexec('svnlook youngest "%s"' % repopath)
|
||||
rev = int(rev)
|
||||
if rev > remote_rev:
|
||||
revrange = (remote_rev+1, rev)
|
||||
dumpchannel = channel.gateway.newchannel()
|
||||
channel.send(revrange)
|
||||
channel.send(dumpchannel)
|
||||
|
||||
f = os.popen(
|
||||
"svnadmin dump -q --incremental -r %s:%s %s"
|
||||
% (revrange[0], revrange[1], repopath), 'r')
|
||||
try:
|
||||
maxcount = dumpchannel.receive()
|
||||
count = maxcount
|
||||
while 1:
|
||||
s = f.read(8192)
|
||||
if not s:
|
||||
raise EOFError
|
||||
dumpchannel.send(s)
|
||||
count = count - 1
|
||||
if count <= 0:
|
||||
ack = dumpchannel.receive()
|
||||
count = maxcount
|
||||
|
||||
except EOFError:
|
||||
dumpchannel.close()
|
||||
remote_rev = rev
|
||||
else:
|
||||
# using svn-hook instead would be nice here
|
||||
py.std.time.sleep(30)
|
||||
""")
|
||||
|
||||
c.send((local_rev, path))
|
||||
print "checking revisions from %d in %s" %(local_rev, remote)
|
||||
while 1:
|
||||
revstart, revend = c.receive()
|
||||
dumpchannel = c.receive()
|
||||
print "receiving revisions", revstart, "-", revend, "replaying..."
|
||||
svn_load(localrepo, dumpchannel)
|
||||
print "current revision", revend
|
||||
|
||||
def svn_load(repo, dumpchannel, maxcount=100):
|
||||
# every maxcount we will send an ACK to the other
|
||||
# side in order to synchronise and avoid our side
|
||||
# growing buffers (py.execnet does not control
|
||||
# RAM usage or receive queue sizes)
|
||||
dumpchannel.send(maxcount)
|
||||
f = os.popen("svnadmin load -q %s" %(repo, ), "w")
|
||||
count = maxcount
|
||||
for x in dumpchannel:
|
||||
sys.stdout.write(".")
|
||||
sys.stdout.flush()
|
||||
f.write(x)
|
||||
count = count - 1
|
||||
if count <= 0:
|
||||
dumpchannel.send(maxcount)
|
||||
count = maxcount
|
||||
print >>sys.stdout
|
||||
f.close()
|
||||
|
||||
def get_svn_youngest(repo):
|
||||
rev = py.process.cmdexec('svnlook youngest "%s"' % repo)
|
||||
return int(rev)
|
||||
|
||||
def getgateway(host, keyfile=None):
|
||||
return py.execnet.SshGateway(host, identity=keyfile)
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) < 3:
|
||||
usage()
|
||||
raise SystemExit(1)
|
||||
|
||||
main(sys.argv[1:])
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
"""
|
||||
sysinfo.py [host1] [host2] [options]
|
||||
|
||||
obtain system info from remote machine.
|
||||
"""
|
||||
|
||||
import py
|
||||
import sys
|
||||
|
||||
optparse = py.compat.optparse
|
||||
|
||||
parser = optparse.OptionParser(usage=__doc__)
|
||||
parser.add_option("-f", "--sshconfig", action="store", dest="ssh_config", default=None,
|
||||
help="use given ssh config file, and add info all contained hosts for getting info")
|
||||
parser.add_option("-i", "--ignore", action="store", dest="ignores", default=None,
|
||||
help="ignore hosts (useful if the list of hostnames come from a file list)")
|
||||
|
||||
def parsehosts(path):
|
||||
path = py.path.local(path)
|
||||
l = []
|
||||
rex = py.std.re.compile(r'Host\s*(\S+)')
|
||||
for line in path.readlines():
|
||||
m = rex.match(line)
|
||||
if m is not None:
|
||||
sshname, = m.groups()
|
||||
l.append(sshname)
|
||||
return l
|
||||
|
||||
class RemoteInfo:
|
||||
def __init__(self, gateway):
|
||||
self.gw = gateway
|
||||
self._cache = {}
|
||||
|
||||
def exreceive(self, execstring):
|
||||
if execstring not in self._cache:
|
||||
channel = self.gw.remote_exec(execstring)
|
||||
self._cache[execstring] = channel.receive()
|
||||
return self._cache[execstring]
|
||||
|
||||
def getmodattr(self, modpath):
|
||||
module = modpath.split(".")[0]
|
||||
return self.exreceive("""
|
||||
import %s
|
||||
channel.send(%s)
|
||||
""" %(module, modpath))
|
||||
|
||||
def islinux(self):
|
||||
return self.getmodattr('sys.platform').find("linux") != -1
|
||||
|
||||
def getfqdn(self):
|
||||
return self.exreceive("""
|
||||
import socket
|
||||
channel.send(socket.getfqdn())
|
||||
""")
|
||||
|
||||
def getmemswap(self):
|
||||
if self.islinux():
|
||||
return self.exreceive("""
|
||||
import commands, re
|
||||
out = commands.getoutput("free")
|
||||
mem = re.search(r"Mem:\s+(\S*)", out).group(1)
|
||||
swap = re.search(r"Swap:\s+(\S*)", out).group(1)
|
||||
channel.send((mem, swap))
|
||||
""")
|
||||
|
||||
def getcpuinfo(self):
|
||||
if self.islinux():
|
||||
return self.exreceive("""
|
||||
# a hyperthreaded cpu core only counts as 1, although it
|
||||
# is present as 2 in /proc/cpuinfo. Counting it as 2 is
|
||||
# misleading because it is *by far* not as efficient as
|
||||
# two independent cores.
|
||||
cpus = {}
|
||||
cpuinfo = {}
|
||||
f = open("/proc/cpuinfo")
|
||||
lines = f.readlines()
|
||||
f.close()
|
||||
for line in lines + ['']:
|
||||
if line.strip():
|
||||
key, value = line.split(":", 1)
|
||||
cpuinfo[key.strip()] = value.strip()
|
||||
else:
|
||||
corekey = (cpuinfo.get("physical id"),
|
||||
cpuinfo.get("core id"))
|
||||
cpus[corekey] = 1
|
||||
numcpus = len(cpus)
|
||||
model = cpuinfo.get("model name")
|
||||
channel.send((numcpus, model))
|
||||
""")
|
||||
|
||||
def debug(*args):
|
||||
print >>sys.stderr, " ".join(map(str, args))
|
||||
def error(*args):
|
||||
debug("ERROR", args[0] + ":", *args[1:])
|
||||
|
||||
def getinfo(sshname, ssh_config=None, loginfo=sys.stdout):
|
||||
debug("connecting to", sshname)
|
||||
try:
|
||||
gw = py.execnet.SshGateway(sshname, ssh_config=ssh_config)
|
||||
except IOError:
|
||||
error("could not get sshagteway", sshname)
|
||||
else:
|
||||
ri = RemoteInfo(gw)
|
||||
#print "%s info:" % sshname
|
||||
prefix = sshname.upper() + " "
|
||||
print >>loginfo, prefix, "fqdn:", ri.getfqdn()
|
||||
for attr in (
|
||||
"sys.platform",
|
||||
"sys.version_info",
|
||||
):
|
||||
loginfo.write("%s %s: " %(prefix, attr,))
|
||||
loginfo.flush()
|
||||
value = ri.getmodattr(attr)
|
||||
loginfo.write(str(value))
|
||||
loginfo.write("\n")
|
||||
loginfo.flush()
|
||||
memswap = ri.getmemswap()
|
||||
if memswap:
|
||||
mem,swap = memswap
|
||||
print >>loginfo, prefix, "Memory:", mem, "Swap:", swap
|
||||
cpuinfo = ri.getcpuinfo()
|
||||
if cpuinfo:
|
||||
numcpu, model = cpuinfo
|
||||
print >>loginfo, prefix, "number of cpus:", numcpu
|
||||
print >>loginfo, prefix, "cpu model", model
|
||||
return ri
|
||||
|
||||
if __name__ == '__main__':
|
||||
options, args = parser.parse_args()
|
||||
hosts = list(args)
|
||||
ssh_config = options.ssh_config
|
||||
if ssh_config:
|
||||
hosts.extend(parsehosts(ssh_config))
|
||||
ignores = options.ignores or ()
|
||||
if ignores:
|
||||
ignores = ignores.split(",")
|
||||
for host in hosts:
|
||||
if host not in ignores:
|
||||
getinfo(host, ssh_config=ssh_config)
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
py lib 1.0.0: XXX
|
||||
======================================================================
|
||||
|
||||
Welcome to the 1.0.0 py lib release - a library aiming to
|
||||
support agile and test-driven python development on various levels.
|
||||
|
||||
XXX
|
||||
@@ -1,27 +0,0 @@
|
||||
py lib 0.9.2: bugfix release
|
||||
=============================
|
||||
|
||||
Welcome to the 0.9.2 py lib and py.test release -
|
||||
mainly fixing Windows issues, providing better
|
||||
packaging and integration with setuptools.
|
||||
|
||||
Here is a quick summary of what the py lib provides:
|
||||
|
||||
* py.test: cross-project testing tool with many advanced features
|
||||
* py.execnet: ad-hoc code distribution to SSH, Socket and local sub processes
|
||||
* py.magic.greenlet: micro-threads on standard CPython ("stackless-light")
|
||||
* py.path: path abstractions over local and subversion files
|
||||
* rich documentation of py's exported API
|
||||
* tested against Linux, Win32, OSX, works on python 2.3-2.6
|
||||
|
||||
See here for more information:
|
||||
|
||||
Pypi pages: http://pypi.python.org/pypi/py/
|
||||
|
||||
Download/Install: http://codespeak.net/py/0.9.2/download.html
|
||||
|
||||
Documentation/API: http://codespeak.net/py/0.9.2/index.html
|
||||
|
||||
best and have fun,
|
||||
|
||||
holger krekel
|
||||
@@ -1,63 +0,0 @@
|
||||
|
||||
pylib 1.0.0 released: testing-with-python innovations continue
|
||||
--------------------------------------------------------------------
|
||||
|
||||
Took a few betas but finally i uploaded a `1.0.0 py lib release`_,
|
||||
featuring the mature and powerful py.test tool and "execnet-style"
|
||||
*elastic* distributed programming. With the new release, there are
|
||||
many new advanced automated testing features - here is a quick summary:
|
||||
|
||||
* funcargs_ - pythonic zero-boilerplate fixtures for Python test functions :
|
||||
|
||||
- totally separates test code, test configuration and test setup
|
||||
- ideal for integration and functional tests
|
||||
- allows for flexible and natural test parametrization schemes
|
||||
|
||||
* new `plugin architecture`_, allowing easy-to-write project-specific and cross-project single-file plugins. The most notable new external plugin is `oejskit`_ which naturally enables **running and reporting of javascript-unittests in real-life browsers**.
|
||||
|
||||
* many new features done in easy-to-improve `default plugins`_, highlights:
|
||||
|
||||
* xfail: mark tests as "expected to fail" and report separately.
|
||||
* pastebin: automatically send tracebacks to pocoo paste service
|
||||
* capture: flexibly capture stdout/stderr of subprocesses, per-test ...
|
||||
* monkeypatch: safely monkeypatch modules/classes from within tests
|
||||
* unittest: run and integrate traditional unittest.py tests
|
||||
* figleaf: generate html coverage reports with the figleaf module
|
||||
* resultlog: generate buildbot-friendly reporting output
|
||||
* ...
|
||||
|
||||
* `distributed testing`_ and `elastic distributed execution`_:
|
||||
|
||||
- new unified "TX" URL scheme for specifying remote processes
|
||||
- new distribution modes "--dist=each" and "--dist=load"
|
||||
- new sync/async ways to handle 1:N communication
|
||||
- improved documentation
|
||||
|
||||
The py lib continues to offer most of the functionality used by
|
||||
the testing tool in `independent namespaces`_.
|
||||
|
||||
Some non-test related code, notably greenlets/co-routines and
|
||||
api-generation now live as their own projects which simplifies the
|
||||
installation procedure because no C-Extensions are required anymore.
|
||||
|
||||
The whole package should work well with Linux, Win32 and OSX, on Python
|
||||
2.3, 2.4, 2.5 and 2.6. (Expect Python3 compatibility soon!)
|
||||
|
||||
For more info, see the py.test and py lib documentation:
|
||||
|
||||
http://pytest.org
|
||||
|
||||
http://pylib.org
|
||||
|
||||
have fun,
|
||||
holger
|
||||
|
||||
.. _`independent namespaces`: http://pylib.org
|
||||
.. _`funcargs`: http://codespeak.net/py/dist/test/funcargs.html
|
||||
.. _`plugin architecture`: http://codespeak.net/py/dist/test/extend.html
|
||||
.. _`default plugins`: http://codespeak.net/py/dist/test/plugin/index.html
|
||||
.. _`distributed testing`: http://codespeak.net/py/dist/test/dist.html
|
||||
.. _`elastic distributed execution`: http://codespeak.net/py/dist/execnet.html
|
||||
.. _`1.0.0 py lib release`: http://pypi.python.org/pypi/py
|
||||
.. _`oejskit`: http://codespeak.net/py/dist/test/plugin/oejskit.html
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
=============
|
||||
Release notes
|
||||
=============
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
announce/release-1.0.0
|
||||
announce/release-0.9.2
|
||||
announce/release-0.9.0
|
||||
69
doc/bin.txt
69
doc/bin.txt
@@ -1,69 +0,0 @@
|
||||
======================
|
||||
``py/bin/`` scripts
|
||||
======================
|
||||
|
||||
The py-lib contains some scripts, most of which are
|
||||
small ones (apart from ``py.test``) that help during
|
||||
the python development process. If working
|
||||
from a svn-checkout of py lib you may add ``py/bin``
|
||||
to your shell ``PATH`` which should make the scripts
|
||||
available on your command prompt.
|
||||
|
||||
``py.test``
|
||||
===========
|
||||
|
||||
The ``py.test`` executable is the main entry point into the py-lib testing tool,
|
||||
see the `py.test documentation`_.
|
||||
|
||||
.. _`py.test documentation`: test/test.html
|
||||
|
||||
``py.cleanup``
|
||||
==============
|
||||
|
||||
Usage: ``py.cleanup [PATH]``
|
||||
|
||||
Delete pyc file recursively, starting from ``PATH`` (which defaults to the
|
||||
current working directory). Don't follow links and don't recurse into
|
||||
directories with a ".".
|
||||
|
||||
|
||||
``py.countloc``
|
||||
===============
|
||||
|
||||
Usage: ``py.countloc [PATHS]``
|
||||
|
||||
Count (non-empty) lines of python code and number of python files recursively
|
||||
starting from a ``PATHS`` given on the command line (starting from the current
|
||||
working directory). Distinguish between test files and normal ones and report
|
||||
them separately.
|
||||
|
||||
``py.lookup``
|
||||
=============
|
||||
|
||||
Usage: ``py.lookup SEARCH_STRING [options]``
|
||||
|
||||
Looks recursively at Python files for a ``SEARCH_STRING``, starting from the
|
||||
present working directory. Prints the line, with the filename and line-number
|
||||
prepended.
|
||||
|
||||
``py.rest``
|
||||
===========
|
||||
|
||||
Usage: ``py.rest [PATHS] [options]``
|
||||
|
||||
Loot recursively for .txt files starting from ``PATHS`` and convert them to
|
||||
html using docutils or to pdf files, if the ``--pdf`` option is used. For
|
||||
conversion to PDF you will need several command line tools, on Ubuntu Linux
|
||||
this is **texlive** and **texlive-extra-utils**.
|
||||
|
||||
``py.rest`` has some extra features over rst2html (which is shipped with
|
||||
docutils). Most of these are still experimental, the one which is most likely
|
||||
not going to change is the `graphviz`_ directive. With that you can embed .dot
|
||||
files into your document and have them be converted to png (when outputting
|
||||
html) and to eps (when outputting pdf). Otherwise the directive works mostly
|
||||
like the image directive::
|
||||
|
||||
.. graphviz:: example.dot
|
||||
:scale: 90
|
||||
|
||||
.. _`graphviz`: http://www.graphviz.org
|
||||
141
doc/code.txt
141
doc/code.txt
@@ -1,141 +0,0 @@
|
||||
================================================================================
|
||||
py.code: higher level python code and introspection objects
|
||||
================================================================================
|
||||
|
||||
The :api:`py.code` part of the pylib contains some functionality to help
|
||||
dealing with Python code objects. Even though working with Python's internal
|
||||
code objects (as found on frames and callables) can be very powerful, it's
|
||||
usually also quite cumbersome, because the API provided by core Python is
|
||||
relatively low level and not very accessible.
|
||||
|
||||
The :api:`py.code` library tries to simplify accessing the code objects as well
|
||||
as creating them. There is a small set of interfaces a user needs to deal with,
|
||||
all nicely bundled together, and with a rich set of 'Pythonic' functionality.
|
||||
|
||||
source: :source:`py/code/`
|
||||
|
||||
Contents of the library
|
||||
=======================
|
||||
|
||||
Every object in the :api:`py.code` library wraps a code Python object related
|
||||
to code objects, source code, frames and tracebacks: the :api:`py.code.Code`
|
||||
class wraps code objects, :api:`py.code.Source` source snippets,
|
||||
:api:`py.code.Traceback` exception tracebacks, :api:`py.code.Frame` frame
|
||||
objects (as found in e.g. tracebacks) and :api:`py.code.ExceptionInfo` the
|
||||
tuple provided by sys.exc_info() (containing exception and traceback
|
||||
information when an exception occurs). Also in the library is a helper function
|
||||
:api:`py.code.compile()` that provides the same functionality as Python's
|
||||
built-in 'compile()' function, but returns a wrapped code object.
|
||||
|
||||
The wrappers
|
||||
============
|
||||
|
||||
:api:`py.code.Code`
|
||||
-------------------
|
||||
|
||||
Code objects are instantiated with a code object or a callable as argument,
|
||||
and provide functionality to compare themselves with other Code objects, get to
|
||||
the source file or its contents, create new Code objects from scratch, etc.
|
||||
|
||||
A quick example::
|
||||
|
||||
>>> import py
|
||||
>>> c = py.code.Code(py.path.local.read)
|
||||
>>> c.path.basename
|
||||
'common.py'
|
||||
>>> isinstance(c.source(), py.code.Source)
|
||||
True
|
||||
>>> str(c.source()).split('\n')[0]
|
||||
"def read(self, mode='rb'):"
|
||||
|
||||
source: :source:`py/code/code.py`
|
||||
|
||||
:api:`py.code.Source`
|
||||
---------------------
|
||||
|
||||
Source objects wrap snippets of Python source code, providing a simple yet
|
||||
powerful interface to read, deindent, slice, compare, compile and manipulate
|
||||
them, things that are not so easy in core Python.
|
||||
|
||||
Example::
|
||||
|
||||
>>> s = py.code.Source("""\
|
||||
... def foo():
|
||||
... print "foo"
|
||||
... """)
|
||||
>>> str(s).startswith('def') # automatic de-indentation!
|
||||
True
|
||||
>>> s.isparseable()
|
||||
True
|
||||
>>> sub = s.getstatement(1) # get the statement starting at line 1
|
||||
>>> str(sub).strip() # XXX why is the strip() required?!?
|
||||
'print "foo"'
|
||||
|
||||
source: :source:`py/code/source.py`
|
||||
|
||||
:api:`py.code.Traceback`
|
||||
------------------------
|
||||
|
||||
Tracebacks are usually not very easy to examine, you need to access certain
|
||||
somewhat hidden attributes of the traceback's items (resulting in expressions
|
||||
such as 'fname = tb.tb_next.tb_frame.f_code.co_filename'). The Traceback
|
||||
interface (and its TracebackItem children) tries to improve this.
|
||||
|
||||
Example::
|
||||
|
||||
>>> import sys
|
||||
>>> try:
|
||||
... py.path.local(100) # illegal argument
|
||||
... except:
|
||||
... exc, e, tb = sys.exc_info()
|
||||
>>> t = py.code.Traceback(tb)
|
||||
>>> first = t[1] # get the second entry (first is in this doc)
|
||||
>>> first.path.basename # second is in py/path/local.py
|
||||
'local.py'
|
||||
>>> isinstance(first.statement, py.code.Source)
|
||||
True
|
||||
>>> str(first.statement).strip().startswith('raise ValueError')
|
||||
True
|
||||
|
||||
source: :source:`py/code/traceback2.py`
|
||||
|
||||
:api:`py.code.Frame`
|
||||
--------------------
|
||||
|
||||
Frame wrappers are used in :api:`py.code.Traceback` items, and will usually not
|
||||
directly be instantiated. They provide some nice methods to evaluate code
|
||||
'inside' the frame (using the frame's local variables), get to the underlying
|
||||
code (frames have a code attribute that points to a :api:`py.code.Code` object)
|
||||
and examine the arguments.
|
||||
|
||||
Example (using the 'first' TracebackItem instance created above)::
|
||||
|
||||
>>> frame = first.frame
|
||||
>>> isinstance(frame.code, py.code.Code)
|
||||
True
|
||||
>>> isinstance(frame.eval('self'), py.__.path.local.local.LocalPath)
|
||||
True
|
||||
>>> [namevalue[0] for namevalue in frame.getargs()]
|
||||
['cls', 'path']
|
||||
|
||||
:api:`py.code.ExceptionInfo`
|
||||
----------------------------
|
||||
|
||||
A wrapper around the tuple returned by sys.exc_info() (will call sys.exc_info()
|
||||
itself if the tuple is not provided as an argument), provides some handy
|
||||
attributes to easily access the traceback and exception string.
|
||||
|
||||
Example::
|
||||
|
||||
>>> import sys
|
||||
>>> try:
|
||||
... foobar()
|
||||
... except:
|
||||
... excinfo = py.code.ExceptionInfo()
|
||||
>>> excinfo.typename
|
||||
'NameError'
|
||||
>>> isinstance(excinfo.traceback, py.code.Traceback)
|
||||
True
|
||||
>>> excinfo.exconly()
|
||||
"NameError: name 'foobar' is not defined"
|
||||
|
||||
278
doc/confrest.py
278
doc/confrest.py
@@ -1,278 +0,0 @@
|
||||
import py
|
||||
from py.__.misc.rest import convert_rest_html, strip_html_header
|
||||
from py.__.misc.difftime import worded_time
|
||||
|
||||
html = py.xml.html
|
||||
|
||||
class css:
|
||||
#pagetitle = "pagetitle"
|
||||
contentspace = "contentspace"
|
||||
menubar = "menubar"
|
||||
navspace = "navspace"
|
||||
versioninfo = "versioninfo"
|
||||
|
||||
class Page(object):
|
||||
doctype = ('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
|
||||
' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n')
|
||||
googlefragment = """
|
||||
<script type="text/javascript">
|
||||
var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
|
||||
document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
try {
|
||||
var pageTracker = _gat._getTracker("UA-7597274-3");
|
||||
pageTracker._trackPageview();
|
||||
} catch(err) {}</script>
|
||||
"""
|
||||
|
||||
def __init__(self, project, title, targetpath, stylesheeturl=None,
|
||||
type="text/html", encoding="ISO-8859-1"):
|
||||
self.project = project
|
||||
self.title = project.prefix_title + title
|
||||
self.targetpath = targetpath
|
||||
self.stylesheeturl = stylesheeturl
|
||||
self.type = type
|
||||
self.encoding = encoding
|
||||
|
||||
self.body = html.body()
|
||||
self.head = html.head()
|
||||
self._root = html.html(self.head, self.body)
|
||||
self.fill()
|
||||
|
||||
def a_href(self, name, url, **kwargs):
|
||||
return html.a(name, class_="menu", href=url, **kwargs)
|
||||
|
||||
def a_docref(self, name, relhtmlpath):
|
||||
docpath = self.project.docpath
|
||||
return html.a(name, class_="menu",
|
||||
href=relpath(self.targetpath.strpath,
|
||||
docpath.join(relhtmlpath).strpath))
|
||||
|
||||
def a_apigenref(self, name, relhtmlpath):
|
||||
apipath = self.project.apigenpath
|
||||
return html.a(name, class_="menu",
|
||||
href=relpath(self.targetpath.strpath,
|
||||
apipath.join(relhtmlpath).strpath))
|
||||
|
||||
def fill_menubar(self):
|
||||
items = [
|
||||
self.a_docref("pylib index", "index.html"),
|
||||
self.a_docref("test doc-index", "test/test.html"),
|
||||
self.a_docref("test quickstart", "test/quickstart.html"),
|
||||
self.a_docref("test features", "test/features.html"),
|
||||
self.a_docref("test plugins", "test/plugin/index.html"),
|
||||
self.a_docref("py.execnet", "execnet.html"),
|
||||
#self.a_docref("py.code", "code.html"),
|
||||
#self.a_apigenref("api", "api/index.html"),
|
||||
#self.a_apigenref("source", "source/index.html"),
|
||||
#self.a_href("source", "http://bitbucket.org/hpk42/py-trunk/src/"),
|
||||
self.a_href("issues", "http://bitbucket.org/hpk42/py-trunk/issues/"),
|
||||
self.a_docref("contact", "contact.html"),
|
||||
self.a_docref("install", "download.html"),
|
||||
]
|
||||
self.menubar = html.div(id=css.menubar, *[
|
||||
html.div(item) for item in items])
|
||||
version = py.version
|
||||
self.menubar.insert(0,
|
||||
html.div("%s" % (py.version), style="font-style: italic;")
|
||||
)
|
||||
#self.a_href("%s-%s" % (self.title, py.version),
|
||||
# "http://pypi.python.org/pypi/py/%s" % version,
|
||||
#id="versioninfo",
|
||||
|
||||
def fill(self):
|
||||
content_type = "%s;charset=%s" %(self.type, self.encoding)
|
||||
self.head.append(html.title(self.title))
|
||||
self.head.append(html.meta(name="Content-Type", content=content_type))
|
||||
if self.stylesheeturl:
|
||||
self.head.append(
|
||||
html.link(href=self.stylesheeturl,
|
||||
media="screen", rel="stylesheet",
|
||||
type="text/css"))
|
||||
self.fill_menubar()
|
||||
|
||||
self.body.append(html.div(
|
||||
self.project.logo,
|
||||
self.menubar,
|
||||
id=css.navspace,
|
||||
))
|
||||
|
||||
#self.body.append(html.div(self.title, id=css.pagetitle))
|
||||
self.contentspace = html.div(id=css.contentspace)
|
||||
self.body.append(self.contentspace)
|
||||
|
||||
def unicode(self, doctype=True):
|
||||
page = self._root.unicode()
|
||||
page = page.replace("</body>", self.googlefragment + "</body>")
|
||||
if doctype:
|
||||
return self.doctype + page
|
||||
else:
|
||||
return page
|
||||
|
||||
class PyPage(Page):
|
||||
def get_menubar(self):
|
||||
menubar = super(PyPage, self).get_menubar()
|
||||
# base layout
|
||||
menubar.append(
|
||||
html.a("issue", href="https://codespeak.net/issue/py-dev/",
|
||||
class_="menu"),
|
||||
)
|
||||
return menubar
|
||||
|
||||
|
||||
def getrealname(username):
|
||||
try:
|
||||
import uconf
|
||||
except ImportError:
|
||||
return username
|
||||
try:
|
||||
user = uconf.system.User(username)
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
try:
|
||||
return user.realname or username
|
||||
except KeyError:
|
||||
return username
|
||||
|
||||
|
||||
class Project:
|
||||
mydir = py.magic.autopath().dirpath()
|
||||
title = "py lib"
|
||||
prefix_title = "" # we have a logo already containing "py lib"
|
||||
encoding = 'latin1'
|
||||
logo = html.div(
|
||||
html.a(
|
||||
html.img(alt="py lib", id='pyimg', height=114/2, width=154/2,
|
||||
src="http://codespeak.net/img/pylib.png"),
|
||||
href="http://pylib.org"))
|
||||
Page = PyPage
|
||||
|
||||
def __init__(self, sourcepath=None):
|
||||
if sourcepath is None:
|
||||
sourcepath = self.mydir
|
||||
self.setpath(sourcepath)
|
||||
|
||||
def setpath(self, sourcepath, docpath=None,
|
||||
apigenpath=None, stylesheet=None):
|
||||
self.sourcepath = sourcepath
|
||||
if docpath is None:
|
||||
docpath = sourcepath
|
||||
self.docpath = docpath
|
||||
if apigenpath is None:
|
||||
apigenpath = docpath
|
||||
self.apigenpath = apigenpath
|
||||
if stylesheet is None:
|
||||
p = sourcepath.join("style.css")
|
||||
if p.check():
|
||||
self.stylesheet = p
|
||||
else:
|
||||
self.stylesheet = None
|
||||
else:
|
||||
p = sourcepath.join(stylesheet)
|
||||
if p.check():
|
||||
stylesheet = p
|
||||
self.stylesheet = stylesheet
|
||||
#assert self.stylesheet
|
||||
self.apigen_relpath = relpath(
|
||||
self.docpath.strpath + '/', self.apigenpath.strpath + '/')
|
||||
|
||||
def get_content(self, txtpath, encoding):
|
||||
return unicode(txtpath.read(), encoding)
|
||||
|
||||
def get_htmloutputpath(self, txtpath):
|
||||
reloutputpath = txtpath.new(ext='.html').relto(self.sourcepath)
|
||||
return self.docpath.join(reloutputpath)
|
||||
|
||||
def process(self, txtpath):
|
||||
encoding = self.encoding
|
||||
content = self.get_content(txtpath, encoding)
|
||||
outputpath = self.get_htmloutputpath(txtpath)
|
||||
|
||||
stylesheet = self.stylesheet
|
||||
if isinstance(stylesheet, py.path.local):
|
||||
if not self.docpath.join(stylesheet.basename).check():
|
||||
docpath.ensure(dir=True)
|
||||
stylesheet.copy(docpath)
|
||||
stylesheet = relpath(outputpath.strpath,
|
||||
self.docpath.join(stylesheet.basename).strpath)
|
||||
|
||||
content = convert_rest_html(content, txtpath,
|
||||
stylesheet=stylesheet, encoding=encoding)
|
||||
content = strip_html_header(content, encoding=encoding)
|
||||
|
||||
title = txtpath.purebasename
|
||||
if txtpath.dirpath().basename == "test":
|
||||
title = "py.test " + title
|
||||
# title = "[%s] %s" % (txtpath.purebasename, py.version)
|
||||
page = self.Page(self, title,
|
||||
outputpath, stylesheeturl=stylesheet)
|
||||
|
||||
try:
|
||||
modified = py.process.cmdexec(
|
||||
"hg tip --template 'modified {date|shortdate}'"
|
||||
)
|
||||
except py.process.cmdexec.Error:
|
||||
modified = " "
|
||||
|
||||
#page.body.append(html.div(modified, id="docinfoline"))
|
||||
|
||||
page.contentspace.append(py.xml.raw(content))
|
||||
outputpath.ensure().write(page.unicode().encode(encoding))
|
||||
|
||||
# XXX this function comes from apigen/linker.py, put it
|
||||
# somewhere in py lib
|
||||
import os
|
||||
def relpath(p1, p2, sep=os.path.sep, back='..', normalize=True):
|
||||
""" create a relative path from p1 to p2
|
||||
|
||||
sep is the seperator used for input and (depending
|
||||
on the setting of 'normalize', see below) output
|
||||
|
||||
back is the string used to indicate the parent directory
|
||||
|
||||
when 'normalize' is True, any backslashes (\) in the path
|
||||
will be replaced with forward slashes, resulting in a consistent
|
||||
output on Windows and the rest of the world
|
||||
|
||||
paths to directories must end on a / (URL style)
|
||||
"""
|
||||
if normalize:
|
||||
p1 = p1.replace(sep, '/')
|
||||
p2 = p2.replace(sep, '/')
|
||||
sep = '/'
|
||||
# XXX would be cool to be able to do long filename
|
||||
# expansion and drive
|
||||
# letter fixes here, and such... iow: windows sucks :(
|
||||
if (p1.startswith(sep) ^ p2.startswith(sep)):
|
||||
raise ValueError("mixed absolute relative path: %r -> %r" %(p1, p2))
|
||||
fromlist = p1.split(sep)
|
||||
tolist = p2.split(sep)
|
||||
|
||||
# AA
|
||||
# AA BB -> AA/BB
|
||||
#
|
||||
# AA BB
|
||||
# AA CC -> CC
|
||||
#
|
||||
# AA BB
|
||||
# AA -> ../AA
|
||||
|
||||
diffindex = 0
|
||||
for x1, x2 in zip(fromlist, tolist):
|
||||
if x1 != x2:
|
||||
break
|
||||
diffindex += 1
|
||||
commonindex = diffindex - 1
|
||||
|
||||
fromlist_diff = fromlist[diffindex:]
|
||||
tolist_diff = tolist[diffindex:]
|
||||
|
||||
if not fromlist_diff:
|
||||
return sep.join(tolist[commonindex:])
|
||||
backcount = len(fromlist_diff)
|
||||
if tolist_diff:
|
||||
return sep.join([back,]*(backcount-1) + tolist_diff)
|
||||
return sep.join([back,]*(backcount) + tolist[commonindex:])
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
#XXX make work: excludedirs = ['_build']
|
||||
import py
|
||||
#py.test.importorskip("pygments")
|
||||
pytest_plugins = ['pytest_restdoc']
|
||||
rsyncdirs = ['.']
|
||||
@@ -1,46 +0,0 @@
|
||||
Contact and Communication points
|
||||
===================================
|
||||
|
||||
- `py-dev developers list`_ announcements and discussions.
|
||||
|
||||
- #pylib on irc.freenode.net IRC channel for random questions.
|
||||
|
||||
- `tetamap`_: Holger Krekel's blog, often about testing and py.test related news.
|
||||
|
||||
- `py-svn general commit mailing list`_ to follow development commits,
|
||||
|
||||
- `bitbucket issue tracker`_ use this bitbucket issue tracker to report
|
||||
bugs or request features.
|
||||
|
||||
- `merlinux.eu`_ offers on-site teaching and consulting services.
|
||||
|
||||
.. _`bitbucket issue tracker`: http://bitbucket.org/hpk42/py-trunk/issues/
|
||||
|
||||
.. _`merlinux.eu`: http://merlinux.eu
|
||||
|
||||
.. _`get an account`:
|
||||
|
||||
.. _tetamap: http://tetamap.wordpress.com
|
||||
|
||||
|
||||
..
|
||||
get an account on codespeak
|
||||
---------------------------
|
||||
|
||||
codespeak_ is where the subversion repository is hosted. If you know
|
||||
someone who is active on codespeak already or you are otherwise known in
|
||||
the community (see also: FOAF_) you will get access. But even if
|
||||
you are new to the python developer community please come to the IRC
|
||||
or the mailing list and ask questions, get involved.
|
||||
|
||||
.. _FOAF: http://en.wikipedia.org/wiki/FOAF
|
||||
.. _us: http://codespeak.net/mailman/listinfo/py-dev
|
||||
.. _codespeak: http://codespeak.net/
|
||||
.. _`py-dev`:
|
||||
.. _`development mailing list`:
|
||||
.. _`py-dev developers list`: http://codespeak.net/mailman/listinfo/py-dev
|
||||
.. _`subversion commit mailing list`:
|
||||
.. _`py-svn`:
|
||||
.. _`py-svn general commit mailing list`: http://codespeak.net/mailman/listinfo/py-svn
|
||||
.. _`development bug/feature tracker`: https://codespeak.net/issue/py-dev/
|
||||
|
||||
128
doc/download.txt
128
doc/download.txt
@@ -1,128 +0,0 @@
|
||||
..
|
||||
==============
|
||||
Downloading
|
||||
==============
|
||||
|
||||
.. _`PyPI project page`: http://pypi.python.org/pypi/py/
|
||||
|
||||
Latest Release, see `PyPI project page`_
|
||||
|
||||
using setuptools / easy_install
|
||||
===================================================
|
||||
|
||||
With a working `setuptools installation`_ you can type::
|
||||
|
||||
easy_install -U py
|
||||
|
||||
to get the latest release of the py lib. The ``-U`` switch
|
||||
will trigger an upgrade if you already have an older version installed.
|
||||
The py lib and its tools are expected to work well on Linux,
|
||||
Windows and OSX, Python versions 2.3, 2.4, 2.5 and 2.6.
|
||||
|
||||
We provide binary eggs for Windows machines.
|
||||
|
||||
On other systems you need a working C-compiler in order to
|
||||
install the full py lib. If you don't have a compiler available
|
||||
you can still install the py lib but without greenlets - look
|
||||
below for the ``install_lib`` target.
|
||||
|
||||
**IMPORTANT NOTE**: if you are using Windows and have
|
||||
0.8 versions of the py lib on your system, please download
|
||||
and execute http://codespeak.net/svn/py/build/winpathclean.py
|
||||
This will check that no previous files are getting in the way.
|
||||
You can find out the py lib version with::
|
||||
|
||||
import py
|
||||
print py.version
|
||||
|
||||
|
||||
.. _`checkout`:
|
||||
|
||||
Installing from version control / develop mode
|
||||
=================================================
|
||||
|
||||
To follow development or help with fixing things
|
||||
for the next release, checkout the complete code
|
||||
and documentation source with mercurial_::
|
||||
|
||||
hg clone https://bitbucket.org/hpk42/py-trunk/
|
||||
|
||||
With a working `setuptools installation`_ you can then issue::
|
||||
|
||||
python setup.py develop
|
||||
|
||||
in order to work with your checkout version.
|
||||
|
||||
For enhancing one of the plugins you may go to
|
||||
the ``py/test/plugin/`` sub directory.
|
||||
|
||||
.. _mercurial: http://mercurial.selenic.com/wiki/
|
||||
|
||||
.. _`no-setuptools`:
|
||||
|
||||
Working without setuptools / from source
|
||||
==========================================
|
||||
|
||||
If you have a checkout_ or a tarball_ it is actually not neccessary to issue
|
||||
``setup.py`` commands in order to use py lib and its tools. You can
|
||||
simply add the root directory to ``PYTHONPATH`` and ``py/bin`` or
|
||||
``py\bin\win32`` to your ``PATH`` settings.
|
||||
|
||||
There are also helper scripts to set the environment
|
||||
on windows::
|
||||
|
||||
c:\\path\to\checkout\py\env.cmd
|
||||
|
||||
and on linux/osx you can add something like this to
|
||||
your shell initialization::
|
||||
|
||||
eval `python ~/path/to/checkout/py/env.py`
|
||||
|
||||
both of which which will get you good settings
|
||||
for ``PYTHONPATH`` and ``PATH``.
|
||||
|
||||
Note also that the command line scripts will look
|
||||
for "nearby" py libs, so if you have a layout like this::
|
||||
|
||||
mypkg/
|
||||
subpkg1/
|
||||
tests/
|
||||
tests/
|
||||
py/
|
||||
|
||||
then issuing ``py.test subpkg1`` will use the py lib
|
||||
from that projects root directory.
|
||||
|
||||
Debian and RPM packages
|
||||
===================================
|
||||
|
||||
As of July 2009 pytest/pylib 1.0 RPMs and Debian packages
|
||||
are not yet available. So you will only find older
|
||||
versions.
|
||||
|
||||
On Debian systems look for ``python-codespeak-lib``.
|
||||
*But this package is probably outdated - if somebody
|
||||
can help with bringing this up to date,
|
||||
that would be very much appreciated.*
|
||||
|
||||
Dwayne Bailey has thankfully put together a Fedora `RPM`_.
|
||||
|
||||
.. _`RPM`: http://translate.sourceforge.net/releases/testing/fedora/pylib-0.9.2-1.fc9.noarch.rpm
|
||||
|
||||
.. _`setuptools installation`: http://pypi.python.org/pypi/setuptools
|
||||
|
||||
.. _tarball:
|
||||
|
||||
Installing from a TAR archive
|
||||
===================================================
|
||||
|
||||
You need a working `setuptools installation`_.
|
||||
|
||||
Go to the python package index (pypi) and download a tar file:
|
||||
|
||||
http://pypi.python.org/pypi/py/
|
||||
|
||||
and unpack it to a directory, where you then type::
|
||||
|
||||
python setup.py install
|
||||
|
||||
161
doc/en/Makefile
Normal file
161
doc/en/Makefile
Normal file
@@ -0,0 +1,161 @@
|
||||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = _build
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
SITETARGET=latest
|
||||
|
||||
.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 " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
|
||||
clean:
|
||||
-rm -rf $(BUILDDIR)/*
|
||||
|
||||
install: html
|
||||
rsync -avz _build/html/ pytest.org:/www/pytest.org/$(SITETARGET)
|
||||
|
||||
installpdf: latexpdf
|
||||
@scp $(BUILDDIR)/latex/pytest.pdf pytest.org:/www/pytest.org/$(SITETARGET)
|
||||
|
||||
installall: clean install installpdf
|
||||
@echo "done"
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pytest.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pytest.qhc"
|
||||
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/pytest"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pytest"
|
||||
@echo "# devhelp"
|
||||
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
make -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
||||
|
||||
texinfo:
|
||||
mkdir -p $(BUILDDIR)/texinfo
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
info:
|
||||
mkdir -p $(BUILDDIR)/texinfo
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
339
doc/en/_static/sphinxdoc.css
Normal file
339
doc/en/_static/sphinxdoc.css
Normal file
@@ -0,0 +1,339 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
22
doc/en/_templates/indexsidebar.html
Normal file
22
doc/en/_templates/indexsidebar.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<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>
|
||||
27
doc/en/_templates/layout.html
Normal file
27
doc/en/_templates/layout.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{% extends "!layout.html" %}
|
||||
|
||||
{% block relbaritems %}
|
||||
{{ super() }}
|
||||
<g:plusone></g:plusone>
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
{{ super() }}
|
||||
<script type="text/javascript">
|
||||
|
||||
var _gaq = _gaq || [];
|
||||
_gaq.push(['_setAccount', 'UA-7597274-13']);
|
||||
_gaq.push(['_trackPageview']);
|
||||
|
||||
(function() {
|
||||
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
|
||||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
||||
})();
|
||||
|
||||
</script>
|
||||
<script type="text/javascript" src="https://apis.google.com/js/plusone.js"></script>
|
||||
{% endblock %}
|
||||
39
doc/en/_templates/localtoc.html
Normal file
39
doc/en/_templates/localtoc.html
Normal file
@@ -0,0 +1,39 @@
|
||||
|
||||
{%- 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><tr><td>
|
||||
<a href="{{ pathto('talks') }}">Talks/Posts</a>
|
||||
</td></tr></table>
|
||||
</div>
|
||||
{% extends "basic/localtoc.html" %}
|
||||
|
||||
29
doc/en/announce/index.txt
Normal file
29
doc/en/announce/index.txt
Normal file
@@ -0,0 +1,29 @@
|
||||
|
||||
Release announcements
|
||||
===========================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
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
|
||||
release-2.3.1
|
||||
release-2.3.0
|
||||
release-2.2.4
|
||||
release-2.2.2
|
||||
release-2.2.1
|
||||
release-2.2.0
|
||||
release-2.1.3
|
||||
release-2.1.2
|
||||
release-2.1.1
|
||||
release-2.1.0
|
||||
release-2.0.3
|
||||
release-2.0.2
|
||||
release-2.0.1
|
||||
release-2.0.0
|
||||
|
||||
129
doc/en/announce/release-2.0.0.txt
Normal file
129
doc/en/announce/release-2.0.0.txt
Normal file
@@ -0,0 +1,129 @@
|
||||
py.test 2.0.0: asserts++, unittest++, reporting++, config++, docs++
|
||||
===========================================================================
|
||||
|
||||
Welcome to pytest-2.0.0, a major new release of "py.test", the rapid
|
||||
easy Python testing tool. There are many new features and enhancements,
|
||||
see below for summary and detailed lists. A lot of long-deprecated code
|
||||
has been removed, resulting in a much smaller and cleaner
|
||||
implementation. See the new docs with examples here:
|
||||
|
||||
http://pytest.org/2.0.0/index.html
|
||||
|
||||
A note on packaging: pytest used to part of the "py" distribution up
|
||||
until version py-1.3.4 but this has changed now: pytest-2.0.0 only
|
||||
contains py.test related code and is expected to be backward-compatible
|
||||
to existing test code. If you want to install pytest, just type one of::
|
||||
|
||||
pip install -U pytest
|
||||
easy_install -U pytest
|
||||
|
||||
Many thanks to all issue reporters and people asking questions or
|
||||
complaining. Particular thanks to Floris Bruynooghe and Ronny Pfannschmidt
|
||||
for their great coding contributions and many others for feedback and help.
|
||||
|
||||
best,
|
||||
holger krekel
|
||||
|
||||
|
||||
New Features
|
||||
-----------------------
|
||||
|
||||
- new invocations through Python interpreter and from Python::
|
||||
|
||||
python -m pytest # on all pythons >= 2.5
|
||||
|
||||
or from a python program::
|
||||
|
||||
import pytest ; pytest.main(arglist, pluginlist)
|
||||
|
||||
see http://pytest.org/2.0.0/usage.html for details.
|
||||
|
||||
- new and better reporting information in assert expressions
|
||||
if comparing lists, sequences or strings.
|
||||
|
||||
see http://pytest.org/2.0.0/assert.html#newreport
|
||||
|
||||
- new configuration through ini-files (setup.cfg or tox.ini recognized),
|
||||
for example::
|
||||
|
||||
[pytest]
|
||||
norecursedirs = .hg data* # don't ever recurse in such dirs
|
||||
addopts = -x --pyargs # add these command line options by default
|
||||
|
||||
see http://pytest.org/2.0.0/customize.html
|
||||
|
||||
- improved standard unittest support. In general py.test should now
|
||||
better be able to run custom unittest.TestCases like twisted trial
|
||||
or Django based TestCases. Also you can now run the tests of an
|
||||
installed 'unittest' package with py.test::
|
||||
|
||||
py.test --pyargs unittest
|
||||
|
||||
- new "-q" option which decreases verbosity and prints a more
|
||||
nose/unittest-style "dot" output.
|
||||
|
||||
- many many more detailed improvements details
|
||||
|
||||
Fixes
|
||||
-----------------------
|
||||
|
||||
- fix issue126 - introduce py.test.set_trace() to trace execution via
|
||||
PDB during the running of tests even if capturing is ongoing.
|
||||
- fix issue124 - make reporting more resilient against tests opening
|
||||
files on filedescriptor 1 (stdout).
|
||||
- fix issue109 - sibling conftest.py files will not be loaded.
|
||||
(and Directory collectors cannot be customized anymore from a Directory's
|
||||
conftest.py - this needs to happen at least one level up).
|
||||
- fix issue88 (finding custom test nodes from command line arg)
|
||||
- fix issue93 stdout/stderr is captured while importing conftest.py
|
||||
- fix bug: unittest collected functions now also can have "pytestmark"
|
||||
applied at class/module level
|
||||
|
||||
Important Notes
|
||||
--------------------
|
||||
|
||||
* The usual way in pre-2.0 times to use py.test in python code was
|
||||
to import "py" and then e.g. use "py.test.raises" for the helper.
|
||||
This remains valid and is not planned to be deprecated. However,
|
||||
in most examples and internal code you'll find "import pytest"
|
||||
and "pytest.raises" used as the recommended default way.
|
||||
|
||||
* pytest now first performs collection of the complete test suite
|
||||
before running any test. This changes for example the semantics of when
|
||||
pytest_collectstart/pytest_collectreport are called. Some plugins may
|
||||
need upgrading.
|
||||
|
||||
* The pytest package consists of a 400 LOC core.py and about 20 builtin plugins,
|
||||
summing up to roughly 5000 LOCs, including docstrings. To be fair, it also
|
||||
uses generic code from the "pylib", and the new "py" package to help
|
||||
with filesystem and introspection/code manipulation.
|
||||
|
||||
(Incompatible) Removals
|
||||
-----------------------------
|
||||
|
||||
- py.test.config is now only available if you are in a test run.
|
||||
|
||||
- the following (mostly already deprecated) functionality was removed:
|
||||
|
||||
- removed support for Module/Class/... collection node definitions
|
||||
in conftest.py files. They will cause nothing special.
|
||||
- removed support for calling the pre-1.0 collection API of "run()" and "join"
|
||||
- removed reading option values from conftest.py files or env variables.
|
||||
This can now be done much much better and easier through the ini-file
|
||||
mechanism and the "addopts" entry in particular.
|
||||
- removed the "disabled" attribute in test classes. Use the skipping
|
||||
and pytestmark mechanism to skip or xfail a test class.
|
||||
|
||||
- py.test.collect.Directory does not exist anymore and it
|
||||
is not possible to provide an own "Directory" object.
|
||||
If you have used this and don't know what to do, get
|
||||
in contact. We'll figure something out.
|
||||
|
||||
Note that pytest_collect_directory() is still called but
|
||||
any return value will be ignored. This allows to keep
|
||||
old code working that performed for example "py.test.skip()"
|
||||
in collect() to prevent recursion into directory trees
|
||||
if a certain dependency or command line option is missing.
|
||||
|
||||
|
||||
see :ref:`changelog` for more detailed changes.
|
||||
67
doc/en/announce/release-2.0.1.txt
Normal file
67
doc/en/announce/release-2.0.1.txt
Normal file
@@ -0,0 +1,67 @@
|
||||
py.test 2.0.1: bug fixes
|
||||
===========================================================================
|
||||
|
||||
Welcome to pytest-2.0.1, a maintenance and bug fix release of pytest,
|
||||
a mature testing tool for Python, supporting CPython 2.4-3.2, Jython
|
||||
and latest PyPy interpreters. See extensive docs with tested examples here:
|
||||
|
||||
http://pytest.org/
|
||||
|
||||
If you want to install or upgrade pytest, just type one of::
|
||||
|
||||
pip install -U pytest # or
|
||||
easy_install -U pytest
|
||||
|
||||
Many thanks to all issue reporters and people asking questions or
|
||||
complaining. Particular thanks to Floris Bruynooghe and Ronny Pfannschmidt
|
||||
for their great coding contributions and many others for feedback and help.
|
||||
|
||||
best,
|
||||
holger krekel
|
||||
|
||||
Changes between 2.0.0 and 2.0.1
|
||||
----------------------------------------------
|
||||
|
||||
- refine and unify initial capturing so that it works nicely
|
||||
even if the logging module is used on an early-loaded conftest.py
|
||||
file or plugin.
|
||||
- fix issue12 - show plugin versions with "--version" and
|
||||
"--traceconfig" and also document how to add extra information
|
||||
to reporting test header
|
||||
- fix issue17 (import-* reporting issue on python3) by
|
||||
requiring py>1.4.0 (1.4.1 is going to include it)
|
||||
- fix issue10 (numpy arrays truth checking) by refining
|
||||
assertion interpretation in py lib
|
||||
- fix issue15: make nose compatibility tests compatible
|
||||
with python3 (now that nose-1.0 supports python3)
|
||||
- remove somewhat surprising "same-conftest" detection because
|
||||
it ignores conftest.py when they appear in several subdirs.
|
||||
- improve assertions ("not in"), thanks Floris Bruynooghe
|
||||
- improve behaviour/warnings when running on top of "python -OO"
|
||||
(assertions and docstrings are turned off, leading to potential
|
||||
false positives)
|
||||
- introduce a pytest_cmdline_processargs(args) hook
|
||||
to allow dynamic computation of command line arguments.
|
||||
This fixes a regression because py.test prior to 2.0
|
||||
allowed to set command line options from conftest.py
|
||||
files which so far pytest-2.0 only allowed from ini-files now.
|
||||
- fix issue7: assert failures in doctest modules.
|
||||
unexpected failures in doctests will not generally
|
||||
show nicer, i.e. within the doctest failing context.
|
||||
- fix issue9: setup/teardown functions for an xfail-marked
|
||||
test will report as xfail if they fail but report as normally
|
||||
passing (not xpassing) if they succeed. This only is true
|
||||
for "direct" setup/teardown invocations because teardown_class/
|
||||
teardown_module cannot closely relate to a single test.
|
||||
- fix issue14: no logging errors at process exit
|
||||
- refinements to "collecting" output on non-ttys
|
||||
- refine internal plugin registration and --traceconfig output
|
||||
- introduce a mechanism to prevent/unregister plugins from the
|
||||
command line, see http://pytest.org/latest/plugins.html#cmdunregister
|
||||
- activate resultlog plugin by default
|
||||
- fix regression wrt yielded tests which due to the
|
||||
collection-before-running semantics were not
|
||||
setup as with pytest 1.3.4. Note, however, that
|
||||
the recommended and much cleaner way to do test
|
||||
parametrization remains the "pytest_generate_tests"
|
||||
mechanism, see the docs.
|
||||
73
doc/en/announce/release-2.0.2.txt
Normal file
73
doc/en/announce/release-2.0.2.txt
Normal file
@@ -0,0 +1,73 @@
|
||||
py.test 2.0.2: bug fixes, improved xfail/skip expressions, speed ups
|
||||
===========================================================================
|
||||
|
||||
Welcome to pytest-2.0.2, a maintenance and bug fix release of pytest,
|
||||
a mature testing tool for Python, supporting CPython 2.4-3.2, Jython
|
||||
and latest PyPy interpreters. See the extensive docs with tested examples here:
|
||||
|
||||
http://pytest.org/
|
||||
|
||||
If you want to install or upgrade pytest, just type one of::
|
||||
|
||||
pip install -U pytest # or
|
||||
easy_install -U pytest
|
||||
|
||||
Many thanks to all issue reporters and people asking questions
|
||||
or complaining, particularly Jurko for his insistence,
|
||||
Laura, Victor and Brianna for helping with improving
|
||||
and Ronny for his general advise.
|
||||
|
||||
best,
|
||||
holger krekel
|
||||
|
||||
Changes between 2.0.1 and 2.0.2
|
||||
----------------------------------------------
|
||||
|
||||
- tackle issue32 - speed up test runs of very quick test functions
|
||||
by reducing the relative overhead
|
||||
|
||||
- fix issue30 - extended xfail/skipif handling and improved reporting.
|
||||
If you have a syntax error in your skip/xfail
|
||||
expressions you now get nice error reports.
|
||||
|
||||
Also you can now access module globals from xfail/skipif
|
||||
expressions so that this for example works now::
|
||||
|
||||
import pytest
|
||||
import mymodule
|
||||
@pytest.mark.skipif("mymodule.__version__[0] == "1")
|
||||
def test_function():
|
||||
pass
|
||||
|
||||
This will not run the test function if the module's version string
|
||||
does not start with a "1". Note that specifying a string instead
|
||||
of a boolean expressions allows py.test to report meaningful information
|
||||
when summarizing a test run as to what conditions lead to skipping
|
||||
(or xfail-ing) tests.
|
||||
|
||||
- fix issue28 - setup_method and pytest_generate_tests work together
|
||||
The setup_method fixture method now gets called also for
|
||||
test function invocations generated from the pytest_generate_tests
|
||||
hook.
|
||||
|
||||
- fix issue27 - collectonly and keyword-selection (-k) now work together
|
||||
Also, if you do "py.test --collectonly -q" you now get a flat list
|
||||
of test ids that you can use to paste to the py.test commandline
|
||||
in order to execute a particular test.
|
||||
|
||||
- fix issue25 avoid reported problems with --pdb and python3.2/encodings output
|
||||
|
||||
- fix issue23 - tmpdir argument now works on Python3.2 and WindowsXP
|
||||
Starting with Python3.2 os.symlink may be supported. By requiring
|
||||
a newer py lib version the py.path.local() implementation acknowledges
|
||||
this.
|
||||
|
||||
- fixed typos in the docs (thanks Victor Garcia, Brianna Laugher) and particular
|
||||
thanks to Laura Creighton who also revieved parts of the documentation.
|
||||
|
||||
- fix slighly wrong output of verbose progress reporting for classes
|
||||
(thanks Amaury)
|
||||
|
||||
- more precise (avoiding of) deprecation warnings for node.Class|Function accesses
|
||||
|
||||
- avoid std unittest assertion helper code in tracebacks (thanks Ronny)
|
||||
40
doc/en/announce/release-2.0.3.txt
Normal file
40
doc/en/announce/release-2.0.3.txt
Normal file
@@ -0,0 +1,40 @@
|
||||
py.test 2.0.3: bug fixes and speed ups
|
||||
===========================================================================
|
||||
|
||||
Welcome to pytest-2.0.3, a maintenance and bug fix release of pytest,
|
||||
a mature testing tool for Python, supporting CPython 2.4-3.2, Jython
|
||||
and latest PyPy interpreters. See the extensive docs with tested examples here:
|
||||
|
||||
http://pytest.org/
|
||||
|
||||
If you want to install or upgrade pytest, just type one of::
|
||||
|
||||
pip install -U pytest # or
|
||||
easy_install -U pytest
|
||||
|
||||
There also is a bugfix release 1.6 of pytest-xdist, the plugin
|
||||
that enables seemless distributed and "looponfail" testing for Python.
|
||||
|
||||
best,
|
||||
holger krekel
|
||||
|
||||
Changes between 2.0.2 and 2.0.3
|
||||
----------------------------------------------
|
||||
|
||||
- fix issue38: nicer tracebacks on calls to hooks, particularly early
|
||||
configure/sessionstart ones
|
||||
|
||||
- fix missing skip reason/meta information in junitxml files, reported
|
||||
via http://lists.idyll.org/pipermail/testing-in-python/2011-March/003928.html
|
||||
|
||||
- fix issue34: avoid collection failure with "test" prefixed classes
|
||||
deriving from object.
|
||||
|
||||
- don't require zlib (and other libs) for genscript plugin without
|
||||
--genscript actually being used.
|
||||
|
||||
- speed up skips (by not doing a full traceback represenation
|
||||
internally)
|
||||
|
||||
- fix issue37: avoid invalid characters in junitxml's output
|
||||
|
||||
47
doc/en/announce/release-2.1.0.txt
Normal file
47
doc/en/announce/release-2.1.0.txt
Normal file
@@ -0,0 +1,47 @@
|
||||
py.test 2.1.0: perfected assertions and bug fixes
|
||||
===========================================================================
|
||||
|
||||
Welcome to the release of pytest-2.1, a mature testing tool for Python,
|
||||
supporting CPython 2.4-3.2, Jython and latest PyPy interpreters. See
|
||||
the improved extensive docs (now also as PDF!) with tested examples here:
|
||||
|
||||
http://pytest.org/
|
||||
|
||||
The single biggest news about this release are **perfected assertions**
|
||||
courtesy of Benjamin Peterson. You can now safely use ``assert``
|
||||
statements in test modules without having to worry about side effects
|
||||
or python optimization ("-OO") options. This is achieved by rewriting
|
||||
assert statements in test modules upon import, using a PEP302 hook.
|
||||
See http://pytest.org/assert.html#advanced-assertion-introspection for
|
||||
detailed information. The work has been partly sponsored by my company,
|
||||
merlinux GmbH.
|
||||
|
||||
For further details on bug fixes and smaller enhancements see below.
|
||||
|
||||
If you want to install or upgrade pytest, just type one of::
|
||||
|
||||
pip install -U pytest # or
|
||||
easy_install -U pytest
|
||||
|
||||
best,
|
||||
holger krekel / http://merlinux.eu
|
||||
|
||||
Changes between 2.0.3 and 2.1.0
|
||||
----------------------------------------------
|
||||
|
||||
- fix issue53 call nosestyle setup functions with correct ordering
|
||||
- fix issue58 and issue59: new assertion code fixes
|
||||
- merge Benjamin's assertionrewrite branch: now assertions
|
||||
for test modules on python 2.6 and above are done by rewriting
|
||||
the AST and saving the pyc file before the test module is imported.
|
||||
see doc/assert.txt for more info.
|
||||
- fix issue43: improve doctests with better traceback reporting on
|
||||
unexpected exceptions
|
||||
- fix issue47: timing output in junitxml for test cases is now correct
|
||||
- fix issue48: typo in MarkInfo repr leading to exception
|
||||
- fix issue49: avoid confusing error when initialization partially fails
|
||||
- fix issue44: env/username expansion for junitxml file path
|
||||
- show releaselevel information in test runs for pypy
|
||||
- reworked doc pages for better navigation and PDF generation
|
||||
- report KeyboardInterrupt even if interrupted during session startup
|
||||
- fix issue 35 - provide PDF doc version and download link from index page
|
||||
37
doc/en/announce/release-2.1.1.txt
Normal file
37
doc/en/announce/release-2.1.1.txt
Normal file
@@ -0,0 +1,37 @@
|
||||
py.test 2.1.1: assertion fixes and improved junitxml output
|
||||
===========================================================================
|
||||
|
||||
pytest-2.1.1 is a backward compatible maintenance release of the
|
||||
popular py.test testing tool. See extensive docs with examples here:
|
||||
|
||||
http://pytest.org/
|
||||
|
||||
Most bug fixes address remaining issues with the perfected assertions
|
||||
introduced with 2.1.0 - many thanks to the bug reporters and to Benjamin
|
||||
Peterson for helping to fix them. Also, junitxml output now produces
|
||||
system-out/err tags which lead to better displays of tracebacks with Jenkins.
|
||||
|
||||
Also a quick note to package maintainers and others interested: there now
|
||||
is a "pytest" man page which can be generated with "make man" in doc/.
|
||||
|
||||
If you want to install or upgrade pytest, just type one of::
|
||||
|
||||
pip install -U pytest # or
|
||||
easy_install -U pytest
|
||||
|
||||
best,
|
||||
holger krekel / http://merlinux.eu
|
||||
|
||||
Changes between 2.1.0 and 2.1.1
|
||||
----------------------------------------------
|
||||
|
||||
- fix issue64 / pytest.set_trace now works within pytest_generate_tests hooks
|
||||
- fix issue60 / fix error conditions involving the creation of __pycache__
|
||||
- fix issue63 / assertion rewriting on inserts involving strings containing '%'
|
||||
- fix assertion rewriting on calls with a ** arg
|
||||
- don't cache rewritten modules if bytecode generation is disabled
|
||||
- fix assertion rewriting in read-only directories
|
||||
- fix issue59: provide system-out/err tags for junitxml output
|
||||
- fix issue61: assertion rewriting on boolean operations with 3 or more operands
|
||||
- you can now build a man page with "cd doc ; make man"
|
||||
|
||||
33
doc/en/announce/release-2.1.2.txt
Normal file
33
doc/en/announce/release-2.1.2.txt
Normal file
@@ -0,0 +1,33 @@
|
||||
py.test 2.1.2: bug fixes and fixes for jython
|
||||
===========================================================================
|
||||
|
||||
pytest-2.1.2 is a minor backward compatible maintenance release of the
|
||||
popular py.test testing tool. pytest is commonly used for unit,
|
||||
functional- and integration testing. See extensive docs with examples
|
||||
here:
|
||||
|
||||
http://pytest.org/
|
||||
|
||||
Most bug fixes address remaining issues with the perfected assertions
|
||||
introduced in the 2.1 series - many thanks to the bug reporters and to Benjamin
|
||||
Peterson for helping to fix them. pytest should also work better with
|
||||
Jython-2.5.1 (and Jython trunk).
|
||||
|
||||
If you want to install or upgrade pytest, just type one of::
|
||||
|
||||
pip install -U pytest # or
|
||||
easy_install -U pytest
|
||||
|
||||
best,
|
||||
holger krekel / http://merlinux.eu
|
||||
|
||||
Changes between 2.1.1 and 2.1.2
|
||||
----------------------------------------
|
||||
|
||||
- fix assertion rewriting on files with windows newlines on some Python versions
|
||||
- refine test discovery by package/module name (--pyargs), thanks Florian Mayer
|
||||
- fix issue69 / assertion rewriting fixed on some boolean operations
|
||||
- fix issue68 / packages now work with assertion rewriting
|
||||
- fix issue66: use different assertion rewriting caches when the -O option is passed
|
||||
- don't try assertion rewriting on Jython, use reinterp
|
||||
|
||||
32
doc/en/announce/release-2.1.3.txt
Normal file
32
doc/en/announce/release-2.1.3.txt
Normal file
@@ -0,0 +1,32 @@
|
||||
py.test 2.1.3: just some more fixes
|
||||
===========================================================================
|
||||
|
||||
pytest-2.1.3 is a minor backward compatible maintenance release of the
|
||||
popular py.test testing tool. It is commonly used for unit, functional-
|
||||
and integration testing. See extensive docs with examples here:
|
||||
|
||||
http://pytest.org/
|
||||
|
||||
The release contains another fix to the perfected assertions introduced
|
||||
with the 2.1 series as well as the new possibility to customize reporting
|
||||
for assertion expressions on a per-directory level.
|
||||
|
||||
If you want to install or upgrade pytest, just type one of::
|
||||
|
||||
pip install -U pytest # or
|
||||
easy_install -U pytest
|
||||
|
||||
Thanks to the bug reporters and to Ronny Pfannschmidt, Benjamin Peterson
|
||||
and Floris Bruynooghe who implemented the fixes.
|
||||
|
||||
best,
|
||||
holger krekel
|
||||
|
||||
Changes between 2.1.2 and 2.1.3
|
||||
----------------------------------------
|
||||
|
||||
- fix issue79: assertion rewriting failed on some comparisons in boolops,
|
||||
- correctly handle zero length arguments (a la pytest '')
|
||||
- fix issue67 / junitxml now contains correct test durations
|
||||
- fix issue75 / skipping test failure on jython
|
||||
- fix issue77 / Allow assertrepr_compare hook to apply to a subset of tests
|
||||
95
doc/en/announce/release-2.2.0.txt
Normal file
95
doc/en/announce/release-2.2.0.txt
Normal file
@@ -0,0 +1,95 @@
|
||||
py.test 2.2.0: test marking++, parametrization++ and duration profiling
|
||||
===========================================================================
|
||||
|
||||
pytest-2.2.0 is a test-suite compatible release of the popular
|
||||
py.test testing tool. Plugins might need upgrades. It comes
|
||||
with these improvements:
|
||||
|
||||
* easier and more powerful parametrization of tests:
|
||||
|
||||
- new @pytest.mark.parametrize decorator to run tests with different arguments
|
||||
- new metafunc.parametrize() API for parametrizing arguments independently
|
||||
- see examples at http://pytest.org/latest/example/parametrize.html
|
||||
- NOTE that parametrize() related APIs are still a bit experimental
|
||||
and might change in future releases.
|
||||
|
||||
* improved handling of test markers and refined marking mechanism:
|
||||
|
||||
- "-m markexpr" option for selecting tests according to their mark
|
||||
- a new "markers" ini-variable for registering test markers for your project
|
||||
- the new "--strict" bails out with an error if using unregistered markers.
|
||||
- see examples at http://pytest.org/latest/example/markers.html
|
||||
|
||||
* duration profiling: new "--duration=N" option showing the N slowest test
|
||||
execution or setup/teardown calls. This is most useful if you want to
|
||||
find out where your slowest test code is.
|
||||
|
||||
* also 2.2.0 performs more eager calling of teardown/finalizers functions
|
||||
resulting in better and more accurate reporting when they fail
|
||||
|
||||
Besides there is the usual set of bug fixes along with a cleanup of
|
||||
pytest's own test suite allowing it to run on a wider range of environments.
|
||||
|
||||
For general information, see extensive docs with examples here:
|
||||
|
||||
http://pytest.org/
|
||||
|
||||
If you want to install or upgrade pytest you might just type::
|
||||
|
||||
pip install -U pytest # or
|
||||
easy_install -U pytest
|
||||
|
||||
Thanks to Ronny Pfannschmidt, David Burns, Jeff Donner, Daniel Nouri, Alfredo Deza and all who gave feedback or sent bug reports.
|
||||
|
||||
best,
|
||||
holger krekel
|
||||
|
||||
|
||||
notes on incompatibility
|
||||
------------------------------
|
||||
|
||||
While test suites should work unchanged you might need to upgrade plugins:
|
||||
|
||||
* You need a new version of the pytest-xdist plugin (1.7) for distributing
|
||||
test runs.
|
||||
|
||||
* Other plugins might need an upgrade if they implement
|
||||
the ``pytest_runtest_logreport`` hook which now is called unconditionally
|
||||
for the setup/teardown fixture phases of a test. You may choose to
|
||||
ignore setup/teardown failures by inserting "if rep.when != 'call': return"
|
||||
or something similar. Note that most code probably "just" works because
|
||||
the hook was already called for failing setup/teardown phases of a test
|
||||
so a plugin should have been ready to grok such reports already.
|
||||
|
||||
|
||||
Changes between 2.1.3 and 2.2.0
|
||||
----------------------------------------
|
||||
|
||||
- fix issue90: introduce eager tearing down of test items so that
|
||||
teardown function are called earlier.
|
||||
- add an all-powerful metafunc.parametrize function which allows to
|
||||
parametrize test function arguments in multiple steps and therefore
|
||||
from independent plugins and places.
|
||||
- add a @pytest.mark.parametrize helper which allows to easily
|
||||
call a test function with different argument values.
|
||||
- Add examples to the "parametrize" example page, including a quick port
|
||||
of Test scenarios and the new parametrize function and decorator.
|
||||
- introduce registration for "pytest.mark.*" helpers via ini-files
|
||||
or through plugin hooks. Also introduce a "--strict" option which
|
||||
will treat unregistered markers as errors
|
||||
allowing to avoid typos and maintain a well described set of markers
|
||||
for your test suite. See examples at http://pytest.org/latest/mark.html
|
||||
and its links.
|
||||
- issue50: introduce "-m marker" option to select tests based on markers
|
||||
(this is a stricter and more predictable version of "-k" in that "-m"
|
||||
only matches complete markers and has more obvious rules for and/or
|
||||
semantics.
|
||||
- new feature to help optimizing the speed of your tests:
|
||||
--durations=N option for displaying N slowest test calls
|
||||
and setup/teardown methods.
|
||||
- fix issue87: --pastebin now works with python3
|
||||
- fix issue89: --pdb with unexpected exceptions in doctest work more sensibly
|
||||
- fix and cleanup pytest's own test suite to not leak FDs
|
||||
- fix issue83: link to generated funcarg list
|
||||
- fix issue74: pyarg module names are now checked against imp.find_module false positives
|
||||
- fix compatibility with twisted/trial-11.1.0 use cases
|
||||
41
doc/en/announce/release-2.2.1.txt
Normal file
41
doc/en/announce/release-2.2.1.txt
Normal file
@@ -0,0 +1,41 @@
|
||||
pytest-2.2.1: bug fixes, perfect teardowns
|
||||
===========================================================================
|
||||
|
||||
|
||||
pytest-2.2.1 is a minor backward-compatible release of the the py.test
|
||||
testing tool. It contains bug fixes and little improvements, including
|
||||
documentation fixes. If you are using the distributed testing
|
||||
pluginmake sure to upgrade it to pytest-xdist-1.8.
|
||||
|
||||
For general information see here:
|
||||
|
||||
http://pytest.org/
|
||||
|
||||
To install or upgrade pytest:
|
||||
|
||||
pip install -U pytest # or
|
||||
easy_install -U pytest
|
||||
|
||||
Special thanks for helping on this release to Ronny Pfannschmidt, Jurko
|
||||
Gospodnetic and Ralf Schmitt.
|
||||
|
||||
best,
|
||||
holger krekel
|
||||
|
||||
|
||||
Changes between 2.2.0 and 2.2.1
|
||||
----------------------------------------
|
||||
|
||||
- fix issue99 (in pytest and py) internallerrors with resultlog now
|
||||
produce better output - fixed by normalizing pytest_internalerror
|
||||
input arguments.
|
||||
- fix issue97 / traceback issues (in pytest and py) improve traceback output
|
||||
in conjunction with jinja2 and cython which hack tracebacks
|
||||
- fix issue93 (in pytest and pytest-xdist) avoid "delayed teardowns":
|
||||
the final test in a test node will now run its teardown directly
|
||||
instead of waiting for the end of the session. Thanks Dave Hunt for
|
||||
the good reporting and feedback. The pytest_runtest_protocol as well
|
||||
as the pytest_runtest_teardown hooks now have "nextitem" available
|
||||
which will be None indicating the end of the test run.
|
||||
- fix collection crash due to unknown-source collected items, thanks
|
||||
to Ralf Schmitt (fixed by depending on a more recent pylib)
|
||||
43
doc/en/announce/release-2.2.2.txt
Normal file
43
doc/en/announce/release-2.2.2.txt
Normal file
@@ -0,0 +1,43 @@
|
||||
pytest-2.2.2: bug fixes
|
||||
===========================================================================
|
||||
|
||||
pytest-2.2.2 (updated to 2.2.3 to fix packaging issues) is a minor
|
||||
backward-compatible release of the versatile py.test testing tool. It
|
||||
contains bug fixes and a few refinements particularly to reporting with
|
||||
"--collectonly", see below for betails.
|
||||
|
||||
For general information see here:
|
||||
|
||||
http://pytest.org/
|
||||
|
||||
To install or upgrade pytest:
|
||||
|
||||
pip install -U pytest # or
|
||||
easy_install -U pytest
|
||||
|
||||
Special thanks for helping on this release to Ronny Pfannschmidt
|
||||
and Ralf Schmitt and the contributors of issues.
|
||||
|
||||
best,
|
||||
holger krekel
|
||||
|
||||
|
||||
Changes between 2.2.1 and 2.2.2
|
||||
----------------------------------------
|
||||
|
||||
- fix issue101: wrong args to unittest.TestCase test function now
|
||||
produce better output
|
||||
- fix issue102: report more useful errors and hints for when a
|
||||
test directory was renamed and some pyc/__pycache__ remain
|
||||
- fix issue106: allow parametrize to be applied multiple times
|
||||
e.g. from module, class and at function level.
|
||||
- fix issue107: actually perform session scope finalization
|
||||
- don't check in parametrize if indirect parameters are funcarg names
|
||||
- add chdir method to monkeypatch funcarg
|
||||
- fix crash resulting from calling monkeypatch undo a second time
|
||||
- fix issue115: make --collectonly robust against early failure
|
||||
(missing files/directories)
|
||||
- "-qq --collectonly" now shows only files and the number of tests in them
|
||||
- "-q --collectonly" now shows test ids
|
||||
- allow adding of attributes to test reports such that it also works
|
||||
with distributed testing (no upgrade of pytest-xdist needed)
|
||||
39
doc/en/announce/release-2.2.4.txt
Normal file
39
doc/en/announce/release-2.2.4.txt
Normal file
@@ -0,0 +1,39 @@
|
||||
pytest-2.2.4: bug fixes, better junitxml/unittest/python3 compat
|
||||
===========================================================================
|
||||
|
||||
pytest-2.2.4 is a minor backward-compatible release of the versatile
|
||||
py.test testing tool. It contains bug fixes and a few refinements
|
||||
to junitxml reporting, better unittest- and python3 compatibility.
|
||||
|
||||
For general information see here:
|
||||
|
||||
http://pytest.org/
|
||||
|
||||
To install or upgrade pytest:
|
||||
|
||||
pip install -U pytest # or
|
||||
easy_install -U pytest
|
||||
|
||||
Special thanks for helping on this release to Ronny Pfannschmidt
|
||||
and Benjamin Peterson and the contributors of issues.
|
||||
|
||||
best,
|
||||
holger krekel
|
||||
|
||||
Changes between 2.2.3 and 2.2.4
|
||||
-----------------------------------
|
||||
|
||||
- fix error message for rewritten assertions involving the % operator
|
||||
- fix issue 126: correctly match all invalid xml characters for junitxml
|
||||
binary escape
|
||||
- fix issue with unittest: now @unittest.expectedFailure markers should
|
||||
be processed correctly (you can also use @pytest.mark markers)
|
||||
- document integration with the extended distribute/setuptools test commands
|
||||
- fix issue 140: propperly get the real functions
|
||||
of bound classmethods for setup/teardown_class
|
||||
- fix issue #141: switch from the deceased paste.pocoo.org to bpaste.net
|
||||
- fix issue #143: call unconfigure/sessionfinish always when
|
||||
configure/sessionstart where called
|
||||
- fix issue #144: better mangle test ids to junitxml classnames
|
||||
- upgrade distribute_setup.py to 0.6.27
|
||||
|
||||
134
doc/en/announce/release-2.3.0.txt
Normal file
134
doc/en/announce/release-2.3.0.txt
Normal file
@@ -0,0 +1,134 @@
|
||||
pytest-2.3: improved fixtures / better unittest integration
|
||||
=============================================================================
|
||||
|
||||
pytest-2.3 comes with many major improvements for fixture/funcarg management
|
||||
and parametrized testing in Python. It is now easier, more efficient and
|
||||
more predicatable to re-run the same tests with different fixture
|
||||
instances. Also, you can directly declare the caching "scope" of
|
||||
fixtures so that dependent tests throughout your whole test suite can
|
||||
re-use database or other expensive fixture objects with ease. Lastly,
|
||||
it's possible for fixture functions (formerly known as funcarg
|
||||
factories) to use other fixtures, allowing for a completely modular and
|
||||
re-useable fixture design.
|
||||
|
||||
For detailed info and tutorial-style examples, see:
|
||||
|
||||
http://pytest.org/latest/fixture.html
|
||||
|
||||
Moreover, there is now support for using pytest fixtures/funcargs with
|
||||
unittest-style suites, see here for examples:
|
||||
|
||||
http://pytest.org/latest/unittest.html
|
||||
|
||||
Besides, more unittest-test suites are now expected to "simply work"
|
||||
with pytest.
|
||||
|
||||
All changes are backward compatible and you should be able to continue
|
||||
to run your test suites and 3rd party plugins that worked with
|
||||
pytest-2.2.4.
|
||||
|
||||
If you are interested in the precise reasoning (including examples) of the
|
||||
pytest-2.3 fixture evolution, please consult
|
||||
http://pytest.org/latest/funcarg_compare.html
|
||||
|
||||
For general info on installation and getting started:
|
||||
|
||||
http://pytest.org/latest/getting-started.html
|
||||
|
||||
Docs and PDF access as usual at:
|
||||
|
||||
http://pytest.org
|
||||
|
||||
and more details for those already in the knowing of pytest can be found
|
||||
in the CHANGELOG below.
|
||||
|
||||
Particular thanks for this release go to Floris Bruynooghe, Alex Okrushko
|
||||
Carl Meyer, Ronny Pfannschmidt, Benjamin Peterson and Alex Gaynor for helping
|
||||
to get the new features right and well integrated. Ronny and Floris
|
||||
also helped to fix a number of bugs and yet more people helped by
|
||||
providing bug reports.
|
||||
|
||||
have fun,
|
||||
holger krekel
|
||||
|
||||
|
||||
Changes between 2.2.4 and 2.3.0
|
||||
-----------------------------------
|
||||
|
||||
- fix issue202 - better automatic names for parametrized test functions
|
||||
- fix issue139 - introduce @pytest.fixture which allows direct scoping
|
||||
and parametrization of funcarg factories. Introduce new @pytest.setup
|
||||
marker to allow the writing of setup functions which accept funcargs.
|
||||
- fix issue198 - conftest fixtures were not found on windows32 in some
|
||||
circumstances with nested directory structures due to path manipulation issues
|
||||
- fix issue193 skip test functions with were parametrized with empty
|
||||
parameter sets
|
||||
- fix python3.3 compat, mostly reporting bits that previously depended
|
||||
on dict ordering
|
||||
- introduce re-ordering of tests by resource and parametrization setup
|
||||
which takes precedence to the usual file-ordering
|
||||
- fix issue185 monkeypatching time.time does not cause pytest to fail
|
||||
- fix issue172 duplicate call of pytest.setup-decoratored setup_module
|
||||
functions
|
||||
- fix junitxml=path construction so that if tests change the
|
||||
current working directory and the path is a relative path
|
||||
it is constructed correctly from the original current working dir.
|
||||
- fix "python setup.py test" example to cause a proper "errno" return
|
||||
- fix issue165 - fix broken doc links and mention stackoverflow for FAQ
|
||||
- catch unicode-issues when writing failure representations
|
||||
to terminal to prevent the whole session from crashing
|
||||
- fix xfail/skip confusion: a skip-mark or an imperative pytest.skip
|
||||
will now take precedence before xfail-markers because we
|
||||
can't determine xfail/xpass status in case of a skip. see also:
|
||||
http://stackoverflow.com/questions/11105828/in-py-test-when-i-explicitly-skip-a-test-that-is-marked-as-xfail-how-can-i-get
|
||||
|
||||
- always report installed 3rd party plugins in the header of a test run
|
||||
|
||||
- fix issue160: a failing setup of an xfail-marked tests should
|
||||
be reported as xfail (not xpass)
|
||||
|
||||
- fix issue128: show captured output when capsys/capfd are used
|
||||
|
||||
- fix issue179: propperly show the dependency chain of factories
|
||||
|
||||
- pluginmanager.register(...) now raises ValueError if the
|
||||
plugin has been already registered or the name is taken
|
||||
|
||||
- fix issue159: improve http://pytest.org/latest/faq.html
|
||||
especially with respect to the "magic" history, also mention
|
||||
pytest-django, trial and unittest integration.
|
||||
|
||||
- make request.keywords and node.keywords writable. All descendant
|
||||
collection nodes will see keyword values. Keywords are dictionaries
|
||||
containing markers and other info.
|
||||
|
||||
- fix issue 178: xml binary escapes are now wrapped in py.xml.raw
|
||||
|
||||
- fix issue 176: correctly catch the builtin AssertionError
|
||||
even when we replaced AssertionError with a subclass on the
|
||||
python level
|
||||
|
||||
- factory discovery no longer fails with magic global callables
|
||||
that provide no sane __code__ object (mock.call for example)
|
||||
|
||||
- fix issue 182: testdir.inprocess_run now considers passed plugins
|
||||
|
||||
- fix issue 188: ensure sys.exc_info is clear on python2
|
||||
before calling into a test
|
||||
|
||||
- fix issue 191: add unittest TestCase runTest method support
|
||||
- fix issue 156: monkeypatch correctly handles class level descriptors
|
||||
|
||||
- reporting refinements:
|
||||
|
||||
- pytest_report_header now receives a "startdir" so that
|
||||
you can use startdir.bestrelpath(yourpath) to show
|
||||
nice relative path
|
||||
|
||||
- allow plugins to implement both pytest_report_header and
|
||||
pytest_sessionstart (sessionstart is invoked first).
|
||||
|
||||
- don't show deselected reason line if there is none
|
||||
|
||||
- py.test -vv will show all of assert comparisations instead of truncating
|
||||
|
||||
39
doc/en/announce/release-2.3.1.txt
Normal file
39
doc/en/announce/release-2.3.1.txt
Normal file
@@ -0,0 +1,39 @@
|
||||
pytest-2.3.1: fix regression with factory functions
|
||||
===========================================================================
|
||||
|
||||
pytest-2.3.1 is a quick follow-up release:
|
||||
|
||||
- fix issue202 - regression with fixture functions/funcarg factories:
|
||||
using "self" is now safe again and works as in 2.2.4. Thanks
|
||||
to Eduard Schettino for the quick bug report.
|
||||
|
||||
- disable pexpect pytest self tests on Freebsd - thanks Koob for the
|
||||
quick reporting
|
||||
|
||||
- fix/improve interactive docs with --markers
|
||||
|
||||
See
|
||||
|
||||
http://pytest.org/
|
||||
|
||||
for general information. To install or upgrade pytest:
|
||||
|
||||
pip install -U pytest # or
|
||||
easy_install -U pytest
|
||||
|
||||
best,
|
||||
holger krekel
|
||||
|
||||
|
||||
Changes between 2.3.0 and 2.3.1
|
||||
-----------------------------------
|
||||
|
||||
- fix issue202 - fix regression: using "self" from fixture functions now
|
||||
works as expected (it's the same "self" instance that a test method
|
||||
which uses the fixture sees)
|
||||
|
||||
- skip pexpect using tests (test_pdb.py mostly) on freebsd* systems
|
||||
due to pexpect not supporting it properly (hanging)
|
||||
|
||||
- link to web pages from --markers output which provides help for
|
||||
pytest.mark.* usage.
|
||||
57
doc/en/announce/release-2.3.2.txt
Normal file
57
doc/en/announce/release-2.3.2.txt
Normal file
@@ -0,0 +1,57 @@
|
||||
pytest-2.3.2: some fixes and more traceback-printing speed
|
||||
===========================================================================
|
||||
|
||||
pytest-2.3.2 is a another stabilization release:
|
||||
|
||||
- issue 205: fixes a regression with conftest detection
|
||||
- issue 208/29: fixes traceback-printing speed in some bad cases
|
||||
- fix teardown-ordering for parametrized setups
|
||||
- fix unittest and trial compat behaviour with respect to runTest() methods
|
||||
- issue 206 and others: some improvements to packaging
|
||||
- fix issue127 and others: improve some docs
|
||||
|
||||
See
|
||||
|
||||
http://pytest.org/
|
||||
|
||||
for general information. To install or upgrade pytest:
|
||||
|
||||
pip install -U pytest # or
|
||||
easy_install -U pytest
|
||||
|
||||
best,
|
||||
holger krekel
|
||||
|
||||
|
||||
Changes between 2.3.1 and 2.3.2
|
||||
-----------------------------------
|
||||
|
||||
- fix issue208 and fix issue29 use new py version to avoid long pauses
|
||||
when printing tracebacks in long modules
|
||||
|
||||
- fix issue205 - conftests in subdirs customizing
|
||||
pytest_pycollect_makemodule and pytest_pycollect_makeitem
|
||||
now work properly
|
||||
|
||||
- fix teardown-ordering for parametrized setups
|
||||
|
||||
- fix issue127 - better documentation for pytest_addoption
|
||||
and related objects.
|
||||
|
||||
- fix unittest behaviour: TestCase.runtest only called if there are
|
||||
test methods defined
|
||||
|
||||
- improve trial support: don't collect its empty
|
||||
unittest.TestCase.runTest() method
|
||||
|
||||
- "python setup.py test" now works with pytest itself
|
||||
|
||||
- fix/improve internal/packaging related bits:
|
||||
|
||||
- exception message check of test_nose.py now passes on python33 as well
|
||||
|
||||
- issue206 - fix test_assertrewrite.py to work when a global
|
||||
PYTHONDONTWRITEBYTECODE=1 is present
|
||||
|
||||
- add tox.ini to pytest distribution so that ignore-dirs and others config
|
||||
bits are properly distributed for maintainers who run pytest-own tests
|
||||
62
doc/en/announce/release-2.3.3.txt
Normal file
62
doc/en/announce/release-2.3.3.txt
Normal file
@@ -0,0 +1,62 @@
|
||||
pytest-2.3.3: integration fixes, py24 suport, ``*/**`` shown in traceback
|
||||
===========================================================================
|
||||
|
||||
pytest-2.3.3 is a another stabilization release of the py.test tool
|
||||
which offers uebersimple assertions, scalable fixture mechanisms
|
||||
and deep customization for testing with Python. Particularly,
|
||||
this release provides:
|
||||
|
||||
- integration fixes and improvements related to flask, numpy, nose,
|
||||
unittest, mock
|
||||
|
||||
- makes pytest work on py24 again (yes, people sometimes still need to use it)
|
||||
|
||||
- show ``*,**`` args in pytest tracebacks
|
||||
|
||||
Thanks to Manuel Jacob, Thomas Waldmann, Ronny Pfannschmidt, Pavel Repin
|
||||
and Andreas Taumoefolau for providing patches and all for the issues.
|
||||
|
||||
See
|
||||
|
||||
http://pytest.org/
|
||||
|
||||
for general information. To install or upgrade pytest:
|
||||
|
||||
pip install -U pytest # or
|
||||
easy_install -U pytest
|
||||
|
||||
best,
|
||||
holger krekel
|
||||
|
||||
Changes between 2.3.2 and 2.3.3
|
||||
-----------------------------------
|
||||
|
||||
- fix issue214 - parse modules that contain special objects like e. g.
|
||||
flask's request object which blows up on getattr access if no request
|
||||
is active. thanks Thomas Waldmann.
|
||||
|
||||
- fix issue213 - allow to parametrize with values like numpy arrays that
|
||||
do not support an __eq__ operator
|
||||
|
||||
- fix issue215 - split test_python.org into multiple files
|
||||
|
||||
- fix issue148 - @unittest.skip on classes is now recognized and avoids
|
||||
calling setUpClass/tearDownClass, thanks Pavel Repin
|
||||
|
||||
- fix issue209 - reintroduce python2.4 support by depending on newer
|
||||
pylib which re-introduced statement-finding for pre-AST interpreters
|
||||
|
||||
- nose support: only call setup if its a callable, thanks Andrew
|
||||
Taumoefolau
|
||||
|
||||
- fix issue219 - add py2.4-3.3 classifiers to TROVE list
|
||||
|
||||
- in tracebacks *,** arg values are now shown next to normal arguments
|
||||
(thanks Manuel Jacob)
|
||||
|
||||
- fix issue217 - support mock.patch with pytest's fixtures - note that
|
||||
you need either mock-1.0.1 or the python3.3 builtin unittest.mock.
|
||||
|
||||
- fix issue127 - improve documentation for pytest_addoption() and
|
||||
add a ``config.getoption(name)`` helper function for consistency.
|
||||
|
||||
39
doc/en/announce/release-2.3.4.txt
Normal file
39
doc/en/announce/release-2.3.4.txt
Normal file
@@ -0,0 +1,39 @@
|
||||
pytest-2.3.4: stabilization, more flexible selection via "-k expr"
|
||||
===========================================================================
|
||||
|
||||
pytest-2.3.4 is a small stabilization release of the py.test tool
|
||||
which offers uebersimple assertions, scalable fixture mechanisms
|
||||
and deep customization for testing with Python. This release
|
||||
comes with the following fixes and features:
|
||||
|
||||
- make "-k" option accept an expressions the same as with "-m" so that one
|
||||
can write: -k "name1 or name2" etc. This is a slight usage incompatibility
|
||||
if you used special syntax like "TestClass.test_method" which you now
|
||||
need to write as -k "TestClass and test_method" to match a certain
|
||||
method in a certain test class.
|
||||
- allow to dynamically define markers via
|
||||
item.keywords[...]=assignment integrating with "-m" option
|
||||
- yielded test functions will now have autouse-fixtures active but
|
||||
cannot accept fixtures as funcargs - it's anyway recommended to
|
||||
rather use the post-2.0 parametrize features instead of yield, see:
|
||||
http://pytest.org/latest/example/parametrize.html
|
||||
- fix autouse-issue where autouse-fixtures would not be discovered
|
||||
if defined in a a/conftest.py file and tests in a/tests/test_some.py
|
||||
- fix issue226 - LIFO ordering for fixture teardowns
|
||||
- fix issue224 - invocations with >256 char arguments now work
|
||||
- fix issue91 - add/discuss package/directory level setups in example
|
||||
- fixes related to autouse discovery and calling
|
||||
|
||||
Thanks in particular to Thomas Waldmann for spotting and reporting issues.
|
||||
|
||||
See
|
||||
|
||||
http://pytest.org/
|
||||
|
||||
for general information. To install or upgrade pytest:
|
||||
|
||||
pip install -U pytest # or
|
||||
easy_install -U pytest
|
||||
|
||||
best,
|
||||
holger krekel
|
||||
97
doc/en/announce/release-2.3.5.txt
Normal file
97
doc/en/announce/release-2.3.5.txt
Normal file
@@ -0,0 +1,97 @@
|
||||
pytest-2.3.5: bug fixes and little improvements
|
||||
===========================================================================
|
||||
|
||||
pytest-2.3.5 is a maintenance release with many bug fixes and little
|
||||
improvements. See the changelog below for details. No backward
|
||||
compatibility issues are foreseen and all plugins which worked with the
|
||||
prior version are expected to work unmodified. Speaking of which, a
|
||||
few interesting new plugins saw the light last month:
|
||||
|
||||
- pytest-instafail: show failure information while tests are running
|
||||
- pytest-qt: testing of GUI applications written with QT/Pyside
|
||||
- pytest-xprocess: managing external processes across test runs
|
||||
- pytest-random: randomize test ordering
|
||||
|
||||
And several others like pytest-django saw maintenance releases.
|
||||
For a more complete list, check out
|
||||
https://pypi.python.org/pypi?%3Aaction=search&term=pytest&submit=search.
|
||||
|
||||
For general information see:
|
||||
|
||||
http://pytest.org/
|
||||
|
||||
To install or upgrade pytest:
|
||||
|
||||
pip install -U pytest # or
|
||||
easy_install -U pytest
|
||||
|
||||
Particular thanks to Floris, Ronny, Benjamin and the many bug reporters
|
||||
and fix providers.
|
||||
|
||||
may the fixtures be with you,
|
||||
holger krekel
|
||||
|
||||
|
||||
Changes between 2.3.4 and 2.3.5
|
||||
-----------------------------------
|
||||
|
||||
- never consider a fixture function for test function collection
|
||||
|
||||
- allow re-running of test items / helps to fix pytest-reruntests plugin
|
||||
and also help to keep less fixture/resource references alive
|
||||
|
||||
- put captured stdout/stderr into junitxml output even for passing tests
|
||||
(thanks Adam Goucher)
|
||||
|
||||
- Issue 265 - integrate nose setup/teardown with setupstate
|
||||
so it doesnt try to teardown if it did not setup
|
||||
|
||||
- issue 271 - dont write junitxml on slave nodes
|
||||
|
||||
- Issue 274 - dont try to show full doctest example
|
||||
when doctest does not know the example location
|
||||
|
||||
- issue 280 - disable assertion rewriting on buggy CPython 2.6.0
|
||||
|
||||
- inject "getfixture()" helper to retrieve fixtures from doctests,
|
||||
thanks Andreas Zeidler
|
||||
|
||||
- issue 259 - when assertion rewriting, be consistent with the default
|
||||
source encoding of ASCII on Python 2
|
||||
|
||||
- issue 251 - report a skip instead of ignoring classes with init
|
||||
|
||||
- issue250 unicode/str mixes in parametrization names and values now works
|
||||
|
||||
- issue257, assertion-triggered compilation of source ending in a
|
||||
comment line doesn't blow up in python2.5 (fixed through py>=1.4.13.dev6)
|
||||
|
||||
- fix --genscript option to generate standalone scripts that also
|
||||
work with python3.3 (importer ordering)
|
||||
|
||||
- issue171 - in assertion rewriting, show the repr of some
|
||||
global variables
|
||||
|
||||
- fix option help for "-k"
|
||||
|
||||
- move long description of distribution into README.rst
|
||||
|
||||
- improve docstring for metafunc.parametrize()
|
||||
|
||||
- fix bug where using capsys with pytest.set_trace() in a test
|
||||
function would break when looking at capsys.readouterr()
|
||||
|
||||
- allow to specify prefixes starting with "_" when
|
||||
customizing python_functions test discovery. (thanks Graham Horler)
|
||||
|
||||
- improve PYTEST_DEBUG tracing output by puting
|
||||
extra data on a new lines with additional indent
|
||||
|
||||
- ensure OutcomeExceptions like skip/fail have initialized exception attributes
|
||||
|
||||
- issue 260 - don't use nose special setup on plain unittest cases
|
||||
|
||||
- fix issue134 - print the collect errors that prevent running specified test items
|
||||
|
||||
- fix issue266 - accept unicode in MarkEvaluator expressions
|
||||
|
||||
225
doc/en/announce/release-2.4.0.txt
Normal file
225
doc/en/announce/release-2.4.0.txt
Normal file
@@ -0,0 +1,225 @@
|
||||
pytest-2.4.0: new fixture features/hooks and bug fixes
|
||||
===========================================================================
|
||||
|
||||
The just released pytest-2.4.0 brings many improvements and numerous
|
||||
bug fixes while remaining plugin- and test-suite compatible apart
|
||||
from a few supposedly very minor incompatibilities. See below for
|
||||
a full list of details. A few feature highlights:
|
||||
|
||||
- new yield-style fixtures `pytest.yield_fixture
|
||||
<http://pytest.org/latest/yieldfixture.html>`_, allowing to use
|
||||
existing with-style context managers in fixture functions.
|
||||
|
||||
- improved pdb support: ``import pdb ; pdb.set_trace()`` now works
|
||||
without requiring prior disabling of stdout/stderr capturing.
|
||||
Also the ``--pdb`` options works now on collection and internal errors
|
||||
and we introduced a new experimental hook for IDEs/plugins to
|
||||
intercept debugging: ``pytest_exception_interact(node, call, report)``.
|
||||
|
||||
- shorter monkeypatch variant to allow specifying an import path as
|
||||
a target, for example: ``monkeypatch.setattr("requests.get", myfunc)``
|
||||
|
||||
- better unittest/nose compatibility: all teardown methods are now only
|
||||
called if the corresponding setup method succeeded.
|
||||
|
||||
- integrate tab-completion on command line options if you
|
||||
have `argcomplete <http://pypi.python.org/pypi/argcomplete>`_
|
||||
configured.
|
||||
|
||||
- allow boolean expression directly with skipif/xfail
|
||||
if a "reason" is also specified.
|
||||
|
||||
- a new hook ``pytest_load_initial_conftests`` allows plugins like
|
||||
`pytest-django <http://pypi.python.org/pypi/pytest-django>`_ to
|
||||
influence the environment before conftest files import ``django``.
|
||||
|
||||
- reporting: color the last line red or green depending if
|
||||
failures/errors occured or everything passed.
|
||||
|
||||
The documentation has been updated to accomodate the changes,
|
||||
see `http://pytest.org <http://pytest.org>`_
|
||||
|
||||
To install or upgrade pytest::
|
||||
|
||||
pip install -U pytest # or
|
||||
easy_install -U pytest
|
||||
|
||||
|
||||
**Many thanks to all who helped, including Floris Bruynooghe,
|
||||
Brianna Laugher, Andreas Pelme, Anthon van der Neut, Anatoly Bubenkoff,
|
||||
Vladimir Keleshev, Mathieu Agopian, Ronny Pfannschmidt, Christian
|
||||
Theunert and many others.**
|
||||
|
||||
may passing tests be with you,
|
||||
|
||||
holger krekel
|
||||
|
||||
Changes between 2.3.5 and 2.4
|
||||
-----------------------------------
|
||||
|
||||
known incompatibilities:
|
||||
|
||||
- if calling --genscript from python2.7 or above, you only get a
|
||||
standalone script which works on python2.7 or above. Use Python2.6
|
||||
to also get a python2.5 compatible version.
|
||||
|
||||
- all xunit-style teardown methods (nose-style, pytest-style,
|
||||
unittest-style) will not be called if the corresponding setup method failed,
|
||||
see issue322 below.
|
||||
|
||||
- the pytest_plugin_unregister hook wasn't ever properly called
|
||||
and there is no known implementation of the hook - so it got removed.
|
||||
|
||||
- pytest.fixture-decorated functions cannot be generators (i.e. use
|
||||
yield) anymore. This change might be reversed in 2.4.1 if it causes
|
||||
unforeseen real-life issues. However, you can always write and return
|
||||
an inner function/generator and change the fixture consumer to iterate
|
||||
over the returned generator. This change was done in lieu of the new
|
||||
``pytest.yield_fixture`` decorator, see below.
|
||||
|
||||
new features:
|
||||
|
||||
- experimentally introduce a new ``pytest.yield_fixture`` decorator
|
||||
which accepts exactly the same parameters as pytest.fixture but
|
||||
mandates a ``yield`` statement instead of a ``return statement`` from
|
||||
fixture functions. This allows direct integration with "with-style"
|
||||
context managers in fixture functions and generally avoids registering
|
||||
of finalization callbacks in favour of treating the "after-yield" as
|
||||
teardown code. Thanks Andreas Pelme, Vladimir Keleshev, Floris
|
||||
Bruynooghe, Ronny Pfannschmidt and many others for discussions.
|
||||
|
||||
- allow boolean expression directly with skipif/xfail
|
||||
if a "reason" is also specified. Rework skipping documentation
|
||||
to recommend "condition as booleans" because it prevents surprises
|
||||
when importing markers between modules. Specifying conditions
|
||||
as strings will remain fully supported.
|
||||
|
||||
- reporting: color the last line red or green depending if
|
||||
failures/errors occured or everything passed. thanks Christian
|
||||
Theunert.
|
||||
|
||||
- make "import pdb ; pdb.set_trace()" work natively wrt capturing (no
|
||||
"-s" needed anymore), making ``pytest.set_trace()`` a mere shortcut.
|
||||
|
||||
- fix issue181: --pdb now also works on collect errors (and
|
||||
on internal errors) . This was implemented by a slight internal
|
||||
refactoring and the introduction of a new hook
|
||||
``pytest_exception_interact`` hook (see next item).
|
||||
|
||||
- fix issue341: introduce new experimental hook for IDEs/terminals to
|
||||
intercept debugging: ``pytest_exception_interact(node, call, report)``.
|
||||
|
||||
- new monkeypatch.setattr() variant to provide a shorter
|
||||
invocation for patching out classes/functions from modules:
|
||||
|
||||
monkeypatch.setattr("requests.get", myfunc)
|
||||
|
||||
will replace the "get" function of the "requests" module with ``myfunc``.
|
||||
|
||||
- fix issue322: tearDownClass is not run if setUpClass failed. Thanks
|
||||
Mathieu Agopian for the initial fix. Also make all of pytest/nose
|
||||
finalizer mimick the same generic behaviour: if a setupX exists and
|
||||
fails, don't run teardownX. This internally introduces a new method
|
||||
"node.addfinalizer()" helper which can only be called during the setup
|
||||
phase of a node.
|
||||
|
||||
- simplify pytest.mark.parametrize() signature: allow to pass a
|
||||
CSV-separated string to specify argnames. For example:
|
||||
``pytest.mark.parametrize("input,expected", [(1,2), (2,3)])``
|
||||
works as well as the previous:
|
||||
``pytest.mark.parametrize(("input", "expected"), ...)``.
|
||||
|
||||
- add support for setUpModule/tearDownModule detection, thanks Brian Okken.
|
||||
|
||||
- integrate tab-completion on options through use of "argcomplete".
|
||||
Thanks Anthon van der Neut for the PR.
|
||||
|
||||
- change option names to be hyphen-separated long options but keep the
|
||||
old spelling backward compatible. py.test -h will only show the
|
||||
hyphenated version, for example "--collect-only" but "--collectonly"
|
||||
will remain valid as well (for backward-compat reasons). Many thanks to
|
||||
Anthon van der Neut for the implementation and to Hynek Schlawack for
|
||||
pushing us.
|
||||
|
||||
- fix issue 308 - allow to mark/xfail/skip individual parameter sets
|
||||
when parametrizing. Thanks Brianna Laugher.
|
||||
|
||||
- call new experimental pytest_load_initial_conftests hook to allow
|
||||
3rd party plugins to do something before a conftest is loaded.
|
||||
|
||||
Bug fixes:
|
||||
|
||||
- fix issue358 - capturing options are now parsed more properly
|
||||
by using a new parser.parse_known_args method.
|
||||
|
||||
- pytest now uses argparse instead of optparse (thanks Anthon) which
|
||||
means that "argparse" is added as a dependency if installing into python2.6
|
||||
environments or below.
|
||||
|
||||
- fix issue333: fix a case of bad unittest/pytest hook interaction.
|
||||
|
||||
- PR27: correctly handle nose.SkipTest during collection. Thanks
|
||||
Antonio Cuni, Ronny Pfannschmidt.
|
||||
|
||||
- fix issue355: junitxml puts name="pytest" attribute to testsuite tag.
|
||||
|
||||
- fix issue336: autouse fixture in plugins should work again.
|
||||
|
||||
- fix issue279: improve object comparisons on assertion failure
|
||||
for standard datatypes and recognise collections.abc. Thanks to
|
||||
Brianna Laugher and Mathieu Agopian.
|
||||
|
||||
- fix issue317: assertion rewriter support for the is_package method
|
||||
|
||||
- fix issue335: document py.code.ExceptionInfo() object returned
|
||||
from pytest.raises(), thanks Mathieu Agopian.
|
||||
|
||||
- remove implicit distribute_setup support from setup.py.
|
||||
|
||||
- fix issue305: ignore any problems when writing pyc files.
|
||||
|
||||
- SO-17664702: call fixture finalizers even if the fixture function
|
||||
partially failed (finalizers would not always be called before)
|
||||
|
||||
- fix issue320 - fix class scope for fixtures when mixed with
|
||||
module-level functions. Thanks Anatloy Bubenkoff.
|
||||
|
||||
- you can specify "-q" or "-qq" to get different levels of "quieter"
|
||||
reporting (thanks Katarzyna Jachim)
|
||||
|
||||
- fix issue300 - Fix order of conftest loading when starting py.test
|
||||
in a subdirectory.
|
||||
|
||||
- fix issue323 - sorting of many module-scoped arg parametrizations
|
||||
|
||||
- make sessionfinish hooks execute with the same cwd-context as at
|
||||
session start (helps fix plugin behaviour which write output files
|
||||
with relative path such as pytest-cov)
|
||||
|
||||
- fix issue316 - properly reference collection hooks in docs
|
||||
|
||||
- fix issue 306 - cleanup of -k/-m options to only match markers/test
|
||||
names/keywords respectively. Thanks Wouter van Ackooy.
|
||||
|
||||
- improved doctest counting for doctests in python modules --
|
||||
files without any doctest items will not show up anymore
|
||||
and doctest examples are counted as separate test items.
|
||||
thanks Danilo Bellini.
|
||||
|
||||
- fix issue245 by depending on the released py-1.4.14
|
||||
which fixes py.io.dupfile to work with files with no
|
||||
mode. Thanks Jason R. Coombs.
|
||||
|
||||
- fix junitxml generation when test output contains control characters,
|
||||
addressing issue267, thanks Jaap Broekhuizen
|
||||
|
||||
- fix issue338: honor --tb style for setup/teardown errors as well. Thanks Maho.
|
||||
|
||||
- fix issue307 - use yaml.safe_load in example, thanks Mark Eichin.
|
||||
|
||||
- better parametrize error messages, thanks Brianna Laugher
|
||||
|
||||
- pytest_terminal_summary(terminalreporter) hooks can now use
|
||||
".section(title)" and ".line(msg)" methods to print extra
|
||||
information at the end of a test run.
|
||||
|
||||
25
doc/en/announce/release-2.4.1.txt
Normal file
25
doc/en/announce/release-2.4.1.txt
Normal file
@@ -0,0 +1,25 @@
|
||||
pytest-2.4.1: fixing three regressions compared to 2.3.5
|
||||
===========================================================================
|
||||
|
||||
pytest-2.4.1 is a quick follow up release to fix three regressions
|
||||
compared to 2.3.5 before they hit more people:
|
||||
|
||||
- When using parser.addoption() unicode arguments to the
|
||||
"type" keyword should also be converted to the respective types.
|
||||
thanks Floris Bruynooghe, @dnozay. (fixes issue360 and issue362)
|
||||
|
||||
- fix dotted filename completion when using argcomplete
|
||||
thanks Anthon van der Neuth. (fixes issue361)
|
||||
|
||||
- fix regression when a 1-tuple ("arg",) is used for specifying
|
||||
parametrization (the values of the parametrization were passed
|
||||
nested in a tuple). Thanks Donald Stufft.
|
||||
|
||||
- also merge doc typo fixes, thanks Andy Dirnberger
|
||||
|
||||
as usual, docs at http://pytest.org and upgrades via::
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
have fun,
|
||||
holger krekel
|
||||
39
doc/en/announce/release-2.4.2.txt
Normal file
39
doc/en/announce/release-2.4.2.txt
Normal file
@@ -0,0 +1,39 @@
|
||||
pytest-2.4.2: colorama on windows, plugin/tmpdir fixes
|
||||
===========================================================================
|
||||
|
||||
pytest-2.4.2 is another bug-fixing release:
|
||||
|
||||
- on Windows require colorama and a newer py lib so that py.io.TerminalWriter()
|
||||
now uses colorama instead of its own ctypes hacks. (fixes issue365)
|
||||
thanks Paul Moore for bringing it up.
|
||||
|
||||
- fix "-k" matching of tests where "repr" and "attr" and other names would
|
||||
cause wrong matches because of an internal implementation quirk
|
||||
(don't ask) which is now properly implemented. fixes issue345.
|
||||
|
||||
- avoid tmpdir fixture to create too long filenames especially
|
||||
when parametrization is used (issue354)
|
||||
|
||||
- fix pytest-pep8 and pytest-flakes / pytest interactions
|
||||
(collection names in mark plugin was assuming an item always
|
||||
has a function which is not true for those plugins etc.)
|
||||
Thanks Andi Zeidler.
|
||||
|
||||
- introduce node.get_marker/node.add_marker API for plugins
|
||||
like pytest-pep8 and pytest-flakes to avoid the messy
|
||||
details of the node.keywords pseudo-dicts. Adapated
|
||||
docs.
|
||||
|
||||
- remove attempt to "dup" stdout at startup as it's icky.
|
||||
the normal capturing should catch enough possibilities
|
||||
of tests messing up standard FDs.
|
||||
|
||||
- add pluginmanager.do_configure(config) as a link to
|
||||
config.do_configure() for plugin-compatibility
|
||||
|
||||
as usual, docs at http://pytest.org and upgrades via::
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
have fun,
|
||||
holger krekel
|
||||
27
doc/en/apiref.txt
Normal file
27
doc/en/apiref.txt
Normal file
@@ -0,0 +1,27 @@
|
||||
|
||||
.. _apiref:
|
||||
|
||||
py.test reference documentation
|
||||
================================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
builtin.txt
|
||||
customize.txt
|
||||
assert.txt
|
||||
fixture.txt
|
||||
yieldfixture.txt
|
||||
parametrize.txt
|
||||
xunit_setup.txt
|
||||
capture.txt
|
||||
monkeypatch.txt
|
||||
xdist.txt
|
||||
tmpdir.txt
|
||||
mark.txt
|
||||
skipping.txt
|
||||
recwarn.txt
|
||||
unittest.txt
|
||||
nose.txt
|
||||
doctest.txt
|
||||
|
||||
260
doc/en/assert.txt
Normal file
260
doc/en/assert.txt
Normal file
@@ -0,0 +1,260 @@
|
||||
|
||||
The writing and reporting of assertions in tests
|
||||
==================================================
|
||||
|
||||
.. _`assertfeedback`:
|
||||
.. _`assert with the assert statement`:
|
||||
.. _`assert`:
|
||||
|
||||
|
||||
Asserting with the ``assert`` statement
|
||||
---------------------------------------------------------
|
||||
|
||||
``py.test`` allows you to use the standard python ``assert`` for verifying
|
||||
expectations and values in Python tests. For example, you can write the
|
||||
following::
|
||||
|
||||
# content of test_assert1.py
|
||||
def f():
|
||||
return 3
|
||||
|
||||
def test_function():
|
||||
assert f() == 4
|
||||
|
||||
to assert that your function returns a certain value. If this assertion fails
|
||||
you will see the return value of the function call::
|
||||
|
||||
$ py.test test_assert1.py
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
collected 1 items
|
||||
|
||||
test_assert1.py F
|
||||
|
||||
================================= FAILURES =================================
|
||||
______________________________ test_function _______________________________
|
||||
|
||||
def test_function():
|
||||
> assert f() == 4
|
||||
E assert 3 == 4
|
||||
E + where 3 = f()
|
||||
|
||||
test_assert1.py:5: AssertionError
|
||||
========================= 1 failed in 0.01 seconds =========================
|
||||
|
||||
py.test has support for showing the values of the most common subexpressions
|
||||
including calls, attributes, comparisons, and binary and unary
|
||||
operators. (See :ref:`tbreportdemo`). This allows you to use the
|
||||
idiomatic python constructs without boilerplate code while not losing
|
||||
introspection information.
|
||||
|
||||
However, if you specify a message with the assertion like this::
|
||||
|
||||
assert a % 2 == 0, "value was odd, should be even"
|
||||
|
||||
then no assertion introspection takes places at all and the message
|
||||
will be simply shown in the traceback.
|
||||
|
||||
See :ref:`assert-details` for more information on assertion introspection.
|
||||
|
||||
.. _`assertraises`:
|
||||
|
||||
Assertions about expected exceptions
|
||||
------------------------------------------
|
||||
|
||||
In order to write assertions about raised exceptions, you can use
|
||||
``pytest.raises`` as a context manager like this::
|
||||
|
||||
import pytest
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
1 / 0
|
||||
|
||||
and if you need to have access to the actual exception info you may use::
|
||||
|
||||
with pytest.raises(RuntimeError) as excinfo:
|
||||
def f():
|
||||
f()
|
||||
f()
|
||||
|
||||
# do checks related to excinfo.type, excinfo.value, excinfo.traceback
|
||||
|
||||
``excinfo`` is a `py.code.ExceptionInfo`_ instance, which is a wrapper around
|
||||
the actual exception raised.
|
||||
|
||||
.. _py.code.ExceptionInfo:
|
||||
http://pylib.readthedocs.org/en/latest/code.html#py-code-exceptioninfo
|
||||
|
||||
If you want to write test code that works on Python 2.4 as well,
|
||||
you may also use two other ways to test for an expected exception::
|
||||
|
||||
pytest.raises(ExpectedException, func, *args, **kwargs)
|
||||
pytest.raises(ExpectedException, "func(*args, **kwargs)")
|
||||
|
||||
both of which execute the specified function with args and kwargs and
|
||||
asserts that the given ``ExpectedException`` is raised. The reporter will
|
||||
provide you with helpful output in case of failures such as *no
|
||||
exception* or *wrong exception*.
|
||||
|
||||
.. _newreport:
|
||||
|
||||
Making use of context-sensitive comparisons
|
||||
-------------------------------------------------
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
py.test has rich support for providing context-sensitive information
|
||||
when it encounters comparisons. For example::
|
||||
|
||||
# content of test_assert2.py
|
||||
|
||||
def test_set_comparison():
|
||||
set1 = set("1308")
|
||||
set2 = set("8035")
|
||||
assert set1 == set2
|
||||
|
||||
if you run this module::
|
||||
|
||||
$ py.test test_assert2.py
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
collected 1 items
|
||||
|
||||
test_assert2.py F
|
||||
|
||||
================================= FAILURES =================================
|
||||
___________________________ test_set_comparison ____________________________
|
||||
|
||||
def test_set_comparison():
|
||||
set1 = set("1308")
|
||||
set2 = set("8035")
|
||||
> assert set1 == set2
|
||||
E assert set(['0', '1', '3', '8']) == set(['0', '3', '5', '8'])
|
||||
E Extra items in the left set:
|
||||
E '1'
|
||||
E Extra items in the right set:
|
||||
E '5'
|
||||
|
||||
test_assert2.py:5: AssertionError
|
||||
========================= 1 failed in 0.01 seconds =========================
|
||||
|
||||
Special comparisons are done for a number of cases:
|
||||
|
||||
* comparing long strings: a context diff is shown
|
||||
* comparing long sequences: first failing indices
|
||||
* comparing dicts: different entries
|
||||
|
||||
See the :ref:`reporting demo <tbreportdemo>` for many more examples.
|
||||
|
||||
Defining your own assertion comparison
|
||||
----------------------------------------------
|
||||
|
||||
It is possible to add your own detailed explanations by implementing
|
||||
the ``pytest_assertrepr_compare`` hook.
|
||||
|
||||
.. autofunction:: _pytest.hookspec.pytest_assertrepr_compare
|
||||
|
||||
As an example consider adding the following hook in a conftest.py which
|
||||
provides an alternative explanation for ``Foo`` objects::
|
||||
|
||||
# content of conftest.py
|
||||
from test_foocompare import Foo
|
||||
def pytest_assertrepr_compare(op, left, right):
|
||||
if isinstance(left, Foo) and isinstance(right, Foo) and op == "==":
|
||||
return ['Comparing Foo instances:',
|
||||
' vals: %s != %s' % (left.val, right.val)]
|
||||
|
||||
now, given this test module::
|
||||
|
||||
# content of test_foocompare.py
|
||||
class Foo:
|
||||
def __init__(self, val):
|
||||
self.val = val
|
||||
|
||||
def test_compare():
|
||||
f1 = Foo(1)
|
||||
f2 = Foo(2)
|
||||
assert f1 == f2
|
||||
|
||||
you can run the test module and get the custom output defined in
|
||||
the conftest file::
|
||||
|
||||
$ py.test -q test_foocompare.py
|
||||
F
|
||||
================================= FAILURES =================================
|
||||
_______________________________ test_compare _______________________________
|
||||
|
||||
def test_compare():
|
||||
f1 = Foo(1)
|
||||
f2 = Foo(2)
|
||||
> assert f1 == f2
|
||||
E assert Comparing Foo instances:
|
||||
E vals: 1 != 2
|
||||
|
||||
test_foocompare.py:8: AssertionError
|
||||
1 failed in 0.01 seconds
|
||||
|
||||
.. _assert-details:
|
||||
.. _`assert introspection`:
|
||||
|
||||
Advanced assertion introspection
|
||||
----------------------------------
|
||||
|
||||
.. versionadded:: 2.1
|
||||
|
||||
|
||||
Reporting details about a failing assertion is achieved either by rewriting
|
||||
assert statements before they are run or re-evaluating the assert expression and
|
||||
recording the intermediate values. Which technique is used depends on the
|
||||
location of the assert, py.test's configuration, and Python version being used
|
||||
to run py.test. Note that for assert statements with a manually provided
|
||||
message, i.e. ``assert expr, message``, no assertion introspection takes place
|
||||
and the manually provided message will be rendered in tracebacks.
|
||||
|
||||
By default, if the Python version is greater than or equal to 2.6, py.test
|
||||
rewrites assert statements in test modules. Rewritten assert statements put
|
||||
introspection information into the assertion failure message. py.test only
|
||||
rewrites test modules directly discovered by its test collection process, so
|
||||
asserts in supporting modules which are not themselves test modules will not be
|
||||
rewritten.
|
||||
|
||||
.. note::
|
||||
|
||||
py.test rewrites test modules on import. It does this by using an import hook
|
||||
to write a new pyc files. Most of the time this works transparently. However,
|
||||
if you are messing with import yourself, the import hook may interfere. If
|
||||
this is the case, simply use ``--assert=reinterp`` or
|
||||
``--assert=plain``. Additionally, rewriting will fail silently if it cannot
|
||||
write new pycs, i.e. in a read-only filesystem or a zipfile.
|
||||
|
||||
If an assert statement has not been rewritten or the Python version is less than
|
||||
2.6, py.test falls back on assert reinterpretation. In assert reinterpretation,
|
||||
py.test walks the frame of the function containing the assert statement to
|
||||
discover sub-expression results of the failing assert statement. You can force
|
||||
py.test to always use assertion reinterpretation by passing the
|
||||
``--assert=reinterp`` option.
|
||||
|
||||
Assert reinterpretation has a caveat not present with assert rewriting: If
|
||||
evaluating the assert expression has side effects you may get a warning that the
|
||||
intermediate values could not be determined safely. A common example of this
|
||||
issue is an assertion which reads from a file::
|
||||
|
||||
assert f.read() != '...'
|
||||
|
||||
If this assertion fails then the re-evaluation will probably succeed!
|
||||
This is because ``f.read()`` will return an empty string when it is
|
||||
called the second time during the re-evaluation. However, it is
|
||||
easy to rewrite the assertion and avoid any trouble::
|
||||
|
||||
content = f.read()
|
||||
assert content != '...'
|
||||
|
||||
All assert introspection can be turned off by passing ``--assert=plain``.
|
||||
|
||||
For further information, Benjamin Peterson wrote up `Behind the scenes of py.test's new assertion rewriting <http://pybites.blogspot.com/2011/07/behind-scenes-of-pytests-new-assertion.html>`_.
|
||||
|
||||
.. versionadded:: 2.1
|
||||
Add assert rewriting as an alternate introspection technique.
|
||||
|
||||
.. versionchanged:: 2.1
|
||||
Introduce the ``--assert`` option. Deprecate ``--no-assert`` and
|
||||
``--nomagic``.
|
||||
188
doc/en/attic_fixtures.txt
Normal file
188
doc/en/attic_fixtures.txt
Normal file
@@ -0,0 +1,188 @@
|
||||
|
||||
**Test classes, modules or whole projects can make use of
|
||||
one or more fixtures**. All required fixture functions will execute
|
||||
before a test from the specifying context executes. As You can use this
|
||||
to make tests operate from a pre-initialized directory or with
|
||||
certain environment variables or with pre-configured global application
|
||||
settings.
|
||||
|
||||
For example, the Django_ project requires database
|
||||
initialization to be able to import from and use its model objects.
|
||||
For that, the `pytest-django`_ plugin provides fixtures which your
|
||||
project can then easily depend or extend on, simply by referencing the
|
||||
name of the particular fixture.
|
||||
|
||||
Fixture functions have limited visilibity which depends on where they
|
||||
are defined. If they are defined on a test class, only its test methods
|
||||
may use it. A fixture defined in a module can only be used
|
||||
from that test module. A fixture defined in a conftest.py file
|
||||
can only be used by the tests below the directory of that file.
|
||||
Lastly, plugins can define fixtures which are available across all
|
||||
projects.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Python, Java and many other languages support a so called xUnit_ style
|
||||
for providing a fixed state, `test fixtures`_, for running tests. It
|
||||
typically involves calling a autouse function ahead and a teardown
|
||||
function after test execute. In 2005 pytest introduced a scope-specific
|
||||
model of automatically detecting and calling autouse and teardown
|
||||
functions on a per-module, class or function basis. The Python unittest
|
||||
package and nose have subsequently incorporated them. This model
|
||||
remains supported by pytest as :ref:`classic xunit`.
|
||||
|
||||
One property of xunit fixture functions is that they work implicitely
|
||||
by preparing global state or setting attributes on TestCase objects.
|
||||
By contrast, pytest provides :ref:`funcargs` which allow to
|
||||
dependency-inject application test state into test functions or
|
||||
methods as function arguments. If your application is sufficiently modular
|
||||
or if you are creating a new project, we recommend you now rather head over to
|
||||
:ref:`funcargs` instead because many pytest users agree that using this
|
||||
paradigm leads to better application and test organisation.
|
||||
|
||||
However, not all programs and frameworks work and can be tested in
|
||||
a fully modular way. They rather require preparation of global state
|
||||
like database autouse on which further fixtures like preparing application
|
||||
specific tables or wrapping tests in transactions can take place. For those
|
||||
needs, pytest-2.3 now supports new **fixture functions** which come with
|
||||
a ton of improvements over classic xunit fixture writing. Fixture functions:
|
||||
|
||||
- allow to separate different autouse concerns into multiple modular functions
|
||||
|
||||
- can receive and fully interoperate with :ref:`funcargs <resources>`,
|
||||
|
||||
- are called multiple times if its funcargs are parametrized,
|
||||
|
||||
- don't need to be defined directly in your test classes or modules,
|
||||
they can also be defined in a plugin or :ref:`conftest.py <conftest.py>` files and get called
|
||||
|
||||
- are called on a per-session, per-module, per-class or per-function basis
|
||||
by means of a simple "scope" declaration.
|
||||
|
||||
- can access the :ref:`request <request>` object which allows to
|
||||
introspect and interact with the (scoped) testcontext.
|
||||
|
||||
- can add cleanup functions which will be invoked when the last test
|
||||
of the fixture test context has finished executing.
|
||||
|
||||
All of these features are now demonstrated by little examples.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
test modules accessing a global resource
|
||||
-------------------------------------------------------
|
||||
|
||||
.. note::
|
||||
|
||||
Relying on `global state is considered bad programming practise <http://en.wikipedia.org/wiki/Global_variable>`_ but when you work with an application
|
||||
that relies on it you often have no choice.
|
||||
|
||||
If you want test modules to access a global resource,
|
||||
you can stick the resource to the module globals in
|
||||
a per-module autouse function. We use a :ref:`resource factory
|
||||
<@pytest.fixture>` to create our global resource::
|
||||
|
||||
# content of conftest.py
|
||||
import pytest
|
||||
|
||||
class GlobalResource:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def globresource():
|
||||
return GlobalResource()
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def setresource(request, globresource):
|
||||
request.module.globresource = globresource
|
||||
|
||||
Now any test module can access ``globresource`` as a module global::
|
||||
|
||||
# content of test_glob.py
|
||||
|
||||
def test_1():
|
||||
print ("test_1 %s" % globresource)
|
||||
def test_2():
|
||||
print ("test_2 %s" % globresource)
|
||||
|
||||
Let's run this module without output-capturing::
|
||||
|
||||
$ py.test -qs test_glob.py
|
||||
FF
|
||||
================================= FAILURES =================================
|
||||
__________________________________ test_1 __________________________________
|
||||
|
||||
def test_1():
|
||||
> print ("test_1 %s" % globresource)
|
||||
E NameError: global name 'globresource' is not defined
|
||||
|
||||
test_glob.py:3: NameError
|
||||
__________________________________ test_2 __________________________________
|
||||
|
||||
def test_2():
|
||||
> print ("test_2 %s" % globresource)
|
||||
E NameError: global name 'globresource' is not defined
|
||||
|
||||
test_glob.py:5: NameError
|
||||
2 failed in 0.01 seconds
|
||||
|
||||
The two tests see the same global ``globresource`` object.
|
||||
|
||||
Parametrizing the global resource
|
||||
+++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
We extend the previous example and add parametrization to the globresource
|
||||
factory and also add a finalizer::
|
||||
|
||||
# content of conftest.py
|
||||
|
||||
import pytest
|
||||
|
||||
class GlobalResource:
|
||||
def __init__(self, param):
|
||||
self.param = param
|
||||
|
||||
@pytest.fixture(scope="session", params=[1,2])
|
||||
def globresource(request):
|
||||
g = GlobalResource(request.param)
|
||||
def fin():
|
||||
print "finalizing", g
|
||||
request.addfinalizer(fin)
|
||||
return g
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def setresource(request, globresource):
|
||||
request.module.globresource = globresource
|
||||
|
||||
And then re-run our test module::
|
||||
|
||||
$ py.test -qs test_glob.py
|
||||
FF
|
||||
================================= FAILURES =================================
|
||||
__________________________________ test_1 __________________________________
|
||||
|
||||
def test_1():
|
||||
> print ("test_1 %s" % globresource)
|
||||
E NameError: global name 'globresource' is not defined
|
||||
|
||||
test_glob.py:3: NameError
|
||||
__________________________________ test_2 __________________________________
|
||||
|
||||
def test_2():
|
||||
> print ("test_2 %s" % globresource)
|
||||
E NameError: global name 'globresource' is not defined
|
||||
|
||||
test_glob.py:5: NameError
|
||||
2 failed in 0.01 seconds
|
||||
|
||||
We are now running the two tests twice with two different global resource
|
||||
instances. Note that the tests are ordered such that only
|
||||
one instance is active at any given time: the finalizer of
|
||||
the first globresource instance is called before the second
|
||||
instance is created and sent to the autouse functions.
|
||||
|
||||
28
doc/en/bash-completion.txt
Normal file
28
doc/en/bash-completion.txt
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
.. _bash_completion:
|
||||
|
||||
Setting up bash completion
|
||||
==========================
|
||||
|
||||
When using bash as your shell, ``py.test`` can use argcomplete
|
||||
(https://argcomplete.readthedocs.org/) for auto-completion.
|
||||
For this ``argcomplete`` needs to be installed **and** enabled.
|
||||
|
||||
Install argcomplete using::
|
||||
|
||||
sudo pip install 'argcomplete>=0.5.7'
|
||||
|
||||
For global activation of all argcomplete enabled python applications run::
|
||||
|
||||
sudo activate-global-python-argcomplete
|
||||
|
||||
For permanent (but not global) ``py.test`` activation, use::
|
||||
|
||||
register-python-argcomplete py.test >> ~/.bashrc
|
||||
|
||||
For one-time activation of argcomplete for ``py.test`` only, use::
|
||||
|
||||
eval "$(register-python-argcomplete py.test)"
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user