Compare commits
585 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8368e6c2e | ||
|
|
fac8208e8f | ||
|
|
51abdb80db | ||
|
|
65534682aa | ||
|
|
e980fbbe39 | ||
|
|
07e768ab68 | ||
|
|
9a2e0c061d | ||
|
|
056d9e8dc2 | ||
|
|
46f5d7a1bb | ||
|
|
30453057e8 | ||
|
|
3b757b1b8c | ||
|
|
14a9b1ec83 | ||
|
|
9fcbf57163 | ||
|
|
1fb2457018 | ||
|
|
92219e576b | ||
|
|
143ac5af99 | ||
|
|
409b919fc0 | ||
|
|
eadd15fe45 | ||
|
|
3f7223af44 | ||
|
|
20085542e2 | ||
|
|
cfaf3600c1 | ||
|
|
188df8100c | ||
|
|
44fa5a77d4 | ||
|
|
31476c69ab | ||
|
|
6200920dc3 | ||
|
|
46c5d5355e | ||
|
|
39024a7536 | ||
|
|
ae62ced080 | ||
|
|
6166151ee4 | ||
|
|
bedceaacc4 | ||
|
|
1127d519db | ||
|
|
b5ac61657a | ||
|
|
48548767fc | ||
|
|
7536e949b1 | ||
|
|
287c003cfd | ||
|
|
aa53e37fa2 | ||
|
|
dd97c94035 | ||
|
|
264e455410 | ||
|
|
75f11f0b65 | ||
|
|
45d0a21294 | ||
|
|
147b43f832 | ||
|
|
6e14585ca2 | ||
|
|
dae74b674e | ||
|
|
2a99e5dd2a | ||
|
|
9361d48b61 | ||
|
|
ebddac6a5c | ||
|
|
b319375592 | ||
|
|
f8fdf0ae91 | ||
|
|
053fc118b7 | ||
|
|
b486e1294b | ||
|
|
9d879bee36 | ||
|
|
4131d3f300 | ||
|
|
254e357076 | ||
|
|
0f6879bf5e | ||
|
|
bfe2cbe875 | ||
|
|
00d3001138 | ||
|
|
6bc45d158d | ||
|
|
371eb8c6af | ||
|
|
435b8ddc7c | ||
|
|
3cbf0c8ec0 | ||
|
|
9849022eb2 | ||
|
|
18c84a1904 | ||
|
|
247cdb835a | ||
|
|
ce1872e7e8 | ||
|
|
d845af7b24 | ||
|
|
391553887b | ||
|
|
7656fc8320 | ||
|
|
b6166dccb4 | ||
|
|
e7bcc854d9 | ||
|
|
ffee213c85 | ||
|
|
e5b527d0e3 | ||
|
|
c04e248de5 | ||
|
|
3685c1bc01 | ||
|
|
eea169e515 | ||
|
|
16c52f05f1 | ||
|
|
f72182977d | ||
|
|
40d0ade2d9 | ||
|
|
7152707280 | ||
|
|
42c1f85257 | ||
|
|
0d15a46863 | ||
|
|
570edb466b | ||
|
|
7c80c81433 | ||
|
|
9202ba91cf | ||
|
|
ea8997a108 | ||
|
|
867344d0d7 | ||
|
|
e64feaba7a | ||
|
|
74633815aa | ||
|
|
e289c60c3a | ||
|
|
8c81722a0c | ||
|
|
3425edd2a5 | ||
|
|
4458e65fe7 | ||
|
|
653abad27b | ||
|
|
89a55d85a9 | ||
|
|
ef7df8f167 | ||
|
|
90a8faabba | ||
|
|
e12a588c39 | ||
|
|
547070e2d8 | ||
|
|
f2fb841b29 | ||
|
|
3256fa9ee9 | ||
|
|
dc9a9ec4c2 | ||
|
|
527845ef29 | ||
|
|
489e638b4e | ||
|
|
13ee1cffed | ||
|
|
4c148bd0ef | ||
|
|
71a7b3c062 | ||
|
|
ebab1b6c69 | ||
|
|
49773b573f | ||
|
|
32979def7d | ||
|
|
ab00c3e911 | ||
|
|
6e4efccc38 | ||
|
|
269eeec702 | ||
|
|
0e1be01b7a | ||
|
|
aff463a3c4 | ||
|
|
b3247c1d03 | ||
|
|
169635e889 | ||
|
|
cd0b2ace67 | ||
|
|
17a1ed5edf | ||
|
|
a54cd4c2fd | ||
|
|
77de45cce3 | ||
|
|
d550c33cd0 | ||
|
|
a58099022a | ||
|
|
3bc7ced97a | ||
|
|
8979b2a9d7 | ||
|
|
d4c11e58aa | ||
|
|
7f83605c81 | ||
|
|
37b41de779 | ||
|
|
bf2c10c810 | ||
|
|
4285325cb8 | ||
|
|
2a1b1107c5 | ||
|
|
cbbd606b6c | ||
|
|
a24ca9872f | ||
|
|
b8be339632 | ||
|
|
2aad8c0fce | ||
|
|
15cbd61159 | ||
|
|
2f955e0c99 | ||
|
|
af37778b0d | ||
|
|
3f5e9ea71e | ||
|
|
443275f025 | ||
|
|
8426c57a9e | ||
|
|
30ca9f9d38 | ||
|
|
46d87deb5d | ||
|
|
203508d9f3 | ||
|
|
2c7f94fdb9 | ||
|
|
ff90c9e237 | ||
|
|
b4e8861aa5 | ||
|
|
baa189f5a3 | ||
|
|
113bfb6be8 | ||
|
|
9f4688e549 | ||
|
|
3a9d0b26d5 | ||
|
|
a5e60b6a2d | ||
|
|
0df42b4426 | ||
|
|
0d96a5bf90 | ||
|
|
060f68bd90 | ||
|
|
e5739a3115 | ||
|
|
8a8797df80 | ||
|
|
8994603d46 | ||
|
|
5c0b340a4b | ||
|
|
196dcc37a8 | ||
|
|
0ab57c4139 | ||
|
|
29a7b5e064 | ||
|
|
27ae270159 | ||
|
|
2e40a8b3ca | ||
|
|
18e053546c | ||
|
|
9dbcac9af3 | ||
|
|
4a436572a8 | ||
|
|
97a4967b03 | ||
|
|
8f6a5928f7 | ||
|
|
5d89a93977 | ||
|
|
c53b72fd7b | ||
|
|
6bb739516f | ||
|
|
8d735f3e1d | ||
|
|
aca1b06747 | ||
|
|
8dcd2718aa | ||
|
|
5ad1313b8a | ||
|
|
3b3d237f07 | ||
|
|
c4c968fe69 | ||
|
|
0b6df94b12 | ||
|
|
c3d420bf75 | ||
|
|
ebb4c47155 | ||
|
|
7ea5a22657 | ||
|
|
cd76366d87 | ||
|
|
931e8830ba | ||
|
|
621374679b | ||
|
|
8be1136d03 | ||
|
|
e0b63e34fa | ||
|
|
1fd67c9000 | ||
|
|
e3406e0818 | ||
|
|
dc79116de3 | ||
|
|
8433e2ba04 | ||
|
|
86e1b44230 | ||
|
|
5d3f7d7142 | ||
|
|
648d5d0c6b | ||
|
|
dff597dcd0 | ||
|
|
076fb56f85 | ||
|
|
150537d5e0 | ||
|
|
d8c23fd39b | ||
|
|
b748576358 | ||
|
|
f555a3a76c | ||
|
|
1f4831a23f | ||
|
|
0a0d97aeb5 | ||
|
|
4a3863c2e2 | ||
|
|
d314691fd3 | ||
|
|
01e37fe892 | ||
|
|
abbdb60051 | ||
|
|
5939b336cd | ||
|
|
3d289b803d | ||
|
|
4a704bbb55 | ||
|
|
ee6c9f50a2 | ||
|
|
3181718fe0 | ||
|
|
2674f352e8 | ||
|
|
6fb46a0e79 | ||
|
|
820ea6d68f | ||
|
|
b0032ba2b3 | ||
|
|
cf9b31bd5a | ||
|
|
b68b80aec9 | ||
|
|
bd1d17e8de | ||
|
|
93306f6a5e | ||
|
|
962aede290 | ||
|
|
a8d3d329ec | ||
|
|
b256cd2a6a | ||
|
|
b6b36bc167 | ||
|
|
3dd24f8d21 | ||
|
|
794fb193ba | ||
|
|
d7e1f037d9 | ||
|
|
29ff9301d8 | ||
|
|
f3c666db3c | ||
|
|
b93aa5e35f | ||
|
|
bc66f7e43f | ||
|
|
cb6b851780 | ||
|
|
afb8a4e35d | ||
|
|
06a182386b | ||
|
|
fac07c1b3f | ||
|
|
e8c0ca4f08 | ||
|
|
ac6f257efc | ||
|
|
554cb8d09c | ||
|
|
25b504b4f0 | ||
|
|
0a6e086f9d | ||
|
|
f24c470403 | ||
|
|
52a7ccef57 | ||
|
|
bd2d0d2c3c | ||
|
|
08997279f4 | ||
|
|
205e29d843 | ||
|
|
672c901c70 | ||
|
|
c70efaa0fb | ||
|
|
d370e7788d | ||
|
|
0d83dd1b31 | ||
|
|
ed293ec3e9 | ||
|
|
2f8427bb4e | ||
|
|
94608c6110 | ||
|
|
afc607cfd8 | ||
|
|
30729b7c3c | ||
|
|
dfc5399cd7 | ||
|
|
76489d30f7 | ||
|
|
d6f75d2836 | ||
|
|
d85a3ca19a | ||
|
|
f3c9c6e8a8 | ||
|
|
67bd60d5c6 | ||
|
|
28a93b9eeb | ||
|
|
70461d1ead | ||
|
|
73eccb4c36 | ||
|
|
d7a76a4d07 | ||
|
|
924b5e2e3d | ||
|
|
d87279115d | ||
|
|
0a2735a275 | ||
|
|
c90e76c371 | ||
|
|
370daf0441 | ||
|
|
586ecea6f2 | ||
|
|
db4df5833a | ||
|
|
b17c6e5f89 | ||
|
|
c3f63ac143 | ||
|
|
3862b0b28d | ||
|
|
476d4df1b7 | ||
|
|
52449903c3 | ||
|
|
506c9c91c0 | ||
|
|
38f34e2fa1 | ||
|
|
ba41015ef6 | ||
|
|
ebfc1c49d1 | ||
|
|
45e7734b1a | ||
|
|
8ce6e39b1c | ||
|
|
c8e7d1ae34 | ||
|
|
7b5d4d01ed | ||
|
|
a4f4579f19 | ||
|
|
e4da9bacdf | ||
|
|
1e295535c3 | ||
|
|
3ca1e4b7f0 | ||
|
|
dc19624248 | ||
|
|
f8f1a52ea0 | ||
|
|
5c6d7739bc | ||
|
|
771b5c8852 | ||
|
|
fc5ec5807e | ||
|
|
fc544dc660 | ||
|
|
7792587b3f | ||
|
|
c2cd239d35 | ||
|
|
cb0ba18f53 | ||
|
|
3b85e0c3a9 | ||
|
|
73bc6bacfa | ||
|
|
8e8a953ac6 | ||
|
|
852b96714e | ||
|
|
dd64f1a4a9 | ||
|
|
41a6ec6f31 | ||
|
|
7feab7391d | ||
|
|
f0bfe9de3d | ||
|
|
596937e610 | ||
|
|
57fcd3f57e | ||
|
|
65f5383106 | ||
|
|
964c29cb93 | ||
|
|
38fb6aae78 | ||
|
|
1c5b887dfd | ||
|
|
ba209b5230 | ||
|
|
b62fd79c0c | ||
|
|
655146e522 | ||
|
|
88f2cc9b64 | ||
|
|
ed2bb9d723 | ||
|
|
2a111ff700 | ||
|
|
5c6758fde4 | ||
|
|
9bd8420a6b | ||
|
|
cbdab02d05 | ||
|
|
ce30896cd2 | ||
|
|
2e8b0a83fe | ||
|
|
369c711f14 | ||
|
|
cf0cac3b73 | ||
|
|
a9dd37f429 | ||
|
|
294729962d | ||
|
|
4de433e280 | ||
|
|
652936f47f | ||
|
|
1fe2e2cb03 | ||
|
|
e66473853c | ||
|
|
70f1e3b4b0 | ||
|
|
fdd4abb88a | ||
|
|
fdfc1946da | ||
|
|
5085aa2bce | ||
|
|
912330a7e2 | ||
|
|
88ed1ab648 | ||
|
|
191e8c6d9b | ||
|
|
6bbd741039 | ||
|
|
0f5fb7ed05 | ||
|
|
5f1a7330b2 | ||
|
|
2a75ae46c3 | ||
|
|
89cf943e04 | ||
|
|
833f33fa0c | ||
|
|
3dbac17d75 | ||
|
|
6843d45c51 | ||
|
|
4a840a7c09 | ||
|
|
9b7e4ab0c6 | ||
|
|
4ea7bbc197 | ||
|
|
4d2f05e4b9 | ||
|
|
454b60b6c5 | ||
|
|
f6be23b68b | ||
|
|
644fdc5237 | ||
|
|
4b5f0d5ffa | ||
|
|
9f7ba00611 | ||
|
|
796db80ca4 | ||
|
|
d95c8a2204 | ||
|
|
4678cbeb91 | ||
|
|
67ad0fa364 | ||
|
|
6cdd851227 | ||
|
|
c58715371c | ||
|
|
d5f038e29a | ||
|
|
6eeacaba3e | ||
|
|
6b90ad4d4b | ||
|
|
e273f5399d | ||
|
|
0de1a65644 | ||
|
|
95de11a44e | ||
|
|
05cfdcc8cb | ||
|
|
0ddd3e2839 | ||
|
|
aa9a02ec44 | ||
|
|
e97c774f8e | ||
|
|
f50ace7c0a | ||
|
|
49c0c599b0 | ||
|
|
e0d236c031 | ||
|
|
b533c2600a | ||
|
|
dc574c60ef | ||
|
|
1d26f3730f | ||
|
|
27935ebec9 | ||
|
|
378eb5d67b | ||
|
|
5e71ffab87 | ||
|
|
8df7ed12c1 | ||
|
|
f05333ab75 | ||
|
|
c8d52b633b | ||
|
|
2455f8670e | ||
|
|
3a5dbabf60 | ||
|
|
3441084bd2 | ||
|
|
8b92527d7d | ||
|
|
dab889304e | ||
|
|
7a7cb8c8c5 | ||
|
|
77bd0aa02f | ||
|
|
b0f558da44 | ||
|
|
ca1f4bc537 | ||
|
|
219b758949 | ||
|
|
6161bcff6e | ||
|
|
99a4a93dbc | ||
|
|
99ba3c9700 | ||
|
|
c19708b193 | ||
|
|
1f08d990d5 | ||
|
|
e2c59d3282 | ||
|
|
f9029f11af | ||
|
|
c7be83ac47 | ||
|
|
74aaf91653 | ||
|
|
1aeb58b531 | ||
|
|
7b3febd314 | ||
|
|
e87ff07370 | ||
|
|
8f90812481 | ||
|
|
3b3bf9f53d | ||
|
|
685387a43e | ||
|
|
a6f2d2d2c9 | ||
|
|
6d3fe0b826 | ||
|
|
bdad345f99 | ||
|
|
063335a715 | ||
|
|
f074fd9ac6 | ||
|
|
6550b9911b | ||
|
|
258031afe5 | ||
|
|
d1af369800 | ||
|
|
99496d9e5b | ||
|
|
983a09a2d4 | ||
|
|
0108f262b1 | ||
|
|
c47dcaa713 | ||
|
|
c33074c8b9 | ||
|
|
e351976ef4 | ||
|
|
b18a9deb4c | ||
|
|
d7e8eeef56 | ||
|
|
e58e8faf47 | ||
|
|
7d43225c36 | ||
|
|
460cae02b0 | ||
|
|
f3a119c06a | ||
|
|
d1aa553f73 | ||
|
|
07b2b18a01 | ||
|
|
766de67392 | ||
|
|
821f9a94d8 | ||
|
|
cb30848e5a | ||
|
|
8e178e9f9b | ||
|
|
8e28815d44 | ||
|
|
b27dde24d6 | ||
|
|
4a436f2255 | ||
|
|
def471b975 | ||
|
|
f743e95cfc | ||
|
|
4e581b637f | ||
|
|
6b86b0dbfe | ||
|
|
6821d36ca5 | ||
|
|
083084fcbc | ||
|
|
f7387e45ea | ||
|
|
3da28067f3 | ||
|
|
5c71151967 | ||
|
|
3f9f4be070 | ||
|
|
e81b275eda | ||
|
|
537fc3c315 | ||
|
|
00d3abe6dc | ||
|
|
71c76d96d3 | ||
|
|
843872b501 | ||
|
|
eaf38c7239 | ||
|
|
b29a9711c4 | ||
|
|
c750a5beec | ||
|
|
df37cdf51f | ||
|
|
af75ca435b | ||
|
|
8aed5fecd9 | ||
|
|
f3261d9418 | ||
|
|
775f4a6f2f | ||
|
|
502652ff02 | ||
|
|
0e83511d6d | ||
|
|
815dd19fb4 | ||
|
|
1f3ab118fa | ||
|
|
0ec72d0745 | ||
|
|
69f3bd8336 | ||
|
|
10a3b9118b | ||
|
|
ce8c829945 | ||
|
|
ed7aa074aa | ||
|
|
66e9a79472 | ||
|
|
1480aed781 | ||
|
|
be0e2132b7 | ||
|
|
7113c76f0d | ||
|
|
ef732fc51d | ||
|
|
dd45f8ba6c | ||
|
|
c486598440 | ||
|
|
059455b45d | ||
|
|
73ff53c742 | ||
|
|
88366b393c | ||
|
|
9b0ce535c9 | ||
|
|
8a6bdb282f | ||
|
|
459cc40192 | ||
|
|
e3b73682b2 | ||
|
|
8480075f01 | ||
|
|
9ad2b75038 | ||
|
|
a33650953a | ||
|
|
667e70f555 | ||
|
|
0668a6c6d3 | ||
|
|
03ce0adb79 | ||
|
|
e7a4d3d8cf | ||
|
|
61eb20df71 | ||
|
|
df6d5cd4e7 | ||
|
|
fbb9e9328b | ||
|
|
9824499396 | ||
|
|
59f66933cd | ||
|
|
c1aa63c0bb | ||
|
|
e4a6e52b81 | ||
|
|
06307be15d | ||
|
|
79d3353081 | ||
|
|
f9589f7b64 | ||
|
|
d132c502e6 | ||
|
|
3b30c93f73 | ||
|
|
c0c859ce99 | ||
|
|
22f338d74d | ||
|
|
9919269ed0 | ||
|
|
87596714bf | ||
|
|
296ac5c476 | ||
|
|
ad21d5cac4 | ||
|
|
2559ec8bdb | ||
|
|
207f153ec1 | ||
|
|
3a4011585f | ||
|
|
57f66a455a | ||
|
|
e41fd52e8c | ||
|
|
08f6b5f4ea | ||
|
|
d13e17cf51 | ||
|
|
f1f6109255 | ||
|
|
87b8dc5afb | ||
|
|
fc965c1dc5 | ||
|
|
a1bd54e4ea | ||
|
|
36cceeb10e | ||
|
|
3e71a50403 | ||
|
|
98209e92ee | ||
|
|
1bea7e6985 | ||
|
|
1ba219e0da | ||
|
|
a8e3effb6c | ||
|
|
ca46f4fe2a | ||
|
|
5130f5707f | ||
|
|
6607478b23 | ||
|
|
8eafbd05ca | ||
|
|
de0d19ca09 | ||
|
|
d96869ff66 | ||
|
|
b57a84d065 | ||
|
|
c89827b9f2 | ||
|
|
062a0e3e68 | ||
|
|
a2da5a691a | ||
|
|
afe7966683 | ||
|
|
3ebfb881c9 | ||
|
|
9273e11f21 | ||
|
|
6967f3070e | ||
|
|
a0c6758202 | ||
|
|
80d165475b | ||
|
|
aa6a67044f | ||
|
|
b55a4f805f | ||
|
|
d01f08e96f | ||
|
|
e1f2254fc2 | ||
|
|
f825b4979b | ||
|
|
3d70727021 | ||
|
|
7d59b2e350 | ||
|
|
d9992558fc | ||
|
|
d1f71b0575 | ||
|
|
de6b41e318 | ||
|
|
8d1903fed3 | ||
|
|
13eac944ae | ||
|
|
d8ecca5ebd | ||
|
|
9bbf14d0f6 | ||
|
|
c42d966a40 | ||
|
|
3dc0da9339 | ||
|
|
11ec6aeafb | ||
|
|
9d373d83ac | ||
|
|
221797c609 | ||
|
|
78a027e128 | ||
|
|
488bbd2aeb | ||
|
|
312891daa6 | ||
|
|
fe415e3ff8 | ||
|
|
ff35c17ecf | ||
|
|
9ab83083d1 | ||
|
|
756db2131f | ||
|
|
cb700208e8 | ||
|
|
333a9ad7fa | ||
|
|
5c0feb2877 | ||
|
|
5f17caa156 | ||
|
|
98bf5fc9be | ||
|
|
eb462582af | ||
|
|
9e62a31b63 | ||
|
|
2e33d9b35e | ||
|
|
dc563e4954 | ||
|
|
40254b64e5 | ||
|
|
6e7547244b | ||
|
|
333ec8ba5a | ||
|
|
dcaeef7c10 | ||
|
|
8a2e6a8d51 | ||
|
|
74d536314f | ||
|
|
ceb016514b | ||
|
|
e90f876b34 | ||
|
|
c68a89b4a7 | ||
|
|
1f0d06641a | ||
|
|
a63b34c685 | ||
|
|
522d59e844 | ||
|
|
4c62cd451a |
@@ -1,7 +1,4 @@
|
||||
[run]
|
||||
omit =
|
||||
omit =
|
||||
# standlonetemplate is read dynamically and tested by test_genscript
|
||||
*standalonetemplate.py
|
||||
# oldinterpret could be removed, as it is no longer used in py26+
|
||||
*oldinterpret.py
|
||||
vendored_packages
|
||||
|
||||
19
.github/PULL_REQUEST_TEMPLATE.md
vendored
19
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,15 +1,14 @@
|
||||
Thanks for submitting a PR, your contribution is really appreciated!
|
||||
|
||||
Here's a quick checklist that should be present in PRs:
|
||||
Here's a quick checklist that should be present in PRs (you can delete this text from the final description, this is
|
||||
just a guideline):
|
||||
|
||||
- [ ] Add a new news fragment into the changelog folder
|
||||
* name it `$issue_id.$type` for example (588.bug)
|
||||
* if you don't have an issue_id change it to the pr id after creating the pr
|
||||
* ensure type is one of `removal`, `feature`, `bugfix`, `vendor`, `doc` or `trivial`
|
||||
* Make sure to use full sentences with correct case and punctuation, for example: "Fix issue with non-ascii contents in doctest text files."
|
||||
- [ ] Target: for `bugfix`, `vendor`, `doc` or `trivial` fixes, target `master`; for removals or features target `features`;
|
||||
- [ ] Make sure to include reasonable tests for your change if necessary
|
||||
- [ ] Create a new changelog file in the `changelog` folder, with a name like `<ISSUE NUMBER>.<TYPE>.rst`. See [changelog/README.rst](/changelog/README.rst) for details.
|
||||
- [ ] Target the `master` branch for bug fixes, documentation updates and trivial changes.
|
||||
- [ ] Target the `features` branch for new features and removals/deprecations.
|
||||
- [ ] Include documentation when adding new features.
|
||||
- [ ] Include new tests or update existing tests when applicable.
|
||||
|
||||
Unless your change is a trivial or a documentation fix (e.g., a typo or reword of a small section) please:
|
||||
Unless your change is trivial or a small documentation fix (e.g., a typo or reword of a small section) please:
|
||||
|
||||
- [ ] Add yourself to `AUTHORS`, in alphabetical order;
|
||||
- [ ] Add yourself to `AUTHORS` in alphabetical order;
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -33,6 +33,7 @@ env/
|
||||
3rdparty/
|
||||
.tox
|
||||
.cache
|
||||
.pytest_cache
|
||||
.coverage
|
||||
.ropeproject
|
||||
.idea
|
||||
|
||||
29
.travis.yml
29
.travis.yml
@@ -2,10 +2,8 @@ sudo: false
|
||||
language: python
|
||||
python:
|
||||
- '3.6'
|
||||
# command to install dependencies
|
||||
install:
|
||||
- pip install --upgrade --pre tox
|
||||
# # command to run tests
|
||||
env:
|
||||
matrix:
|
||||
# coveralls is not listed in tox's envlist, but should run in travis
|
||||
@@ -19,20 +17,18 @@ env:
|
||||
- TOXENV=py27-xdist
|
||||
- TOXENV=py27-trial
|
||||
- TOXENV=py27-numpy
|
||||
- TOXENV=py27-pluggymaster
|
||||
- TOXENV=py36-pexpect
|
||||
- TOXENV=py36-xdist
|
||||
- TOXENV=py36-trial
|
||||
- TOXENV=py36-numpy
|
||||
- TOXENV=py36-pluggymaster
|
||||
- TOXENV=py27-nobyte
|
||||
- TOXENV=doctesting
|
||||
- TOXENV=docs
|
||||
|
||||
matrix:
|
||||
jobs:
|
||||
include:
|
||||
- env: TOXENV=py26
|
||||
python: '2.6'
|
||||
- env: TOXENV=py33
|
||||
python: '3.3'
|
||||
- env: TOXENV=pypy
|
||||
python: 'pypy-5.4'
|
||||
- env: TOXENV=py35
|
||||
@@ -41,9 +37,22 @@ matrix:
|
||||
python: '3.5'
|
||||
- env: TOXENV=py37
|
||||
python: 'nightly'
|
||||
allow_failures:
|
||||
- env: TOXENV=py37
|
||||
python: 'nightly'
|
||||
|
||||
- stage: deploy
|
||||
python: '3.6'
|
||||
env:
|
||||
install: pip install -U setuptools setuptools_scm
|
||||
script: skip
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: nicoddemus
|
||||
distributions: sdist bdist_wheel
|
||||
skip_upload_docs: true
|
||||
password:
|
||||
secure: xanTgTUu6XDQVqB/0bwJQXoDMnU5tkwZc5koz6mBkkqZhKdNOi2CLoC1XhiSZ+ah24l4V1E0GAqY5kBBcy9d7NVe4WNg4tD095LsHw+CRU6/HCVIFfyk2IZ+FPAlguesCcUiJSXOrlBF+Wj68wEvLoK7EoRFbJeiZ/f91Ww1sbtDlqXABWGHrmhPJL5Wva7o7+wG7JwJowqdZg1pbQExsCc7b53w4v2RBu3D6TJaTAzHiVsW+nUSI67vKI/uf+cR/OixsTfy37wlHgSwihYmrYLFls3V0bSpahCim3bCgMaFZx8S8xrdgJ++PzBCof2HeflFKvW+VCkoYzGEG4NrTWJoNz6ni4red9GdvfjGH3YCjAKS56h9x58zp2E5rpsb/kVq5/45xzV+dq6JRuhQ1nJWjBC6fSKAc/bfwnuFK3EBxNLkvBssLHvsNjj5XG++cB8DdS9wVGUqjpoK4puaXUWFqy4q3S9F86HEsKNgExtieA9qNx+pCIZVs6JCXZNjr0I5eVNzqJIyggNgJG6RyravsU35t9Zd9doL5g4Y7UKmAGTn1Sz24HQ4sMQgXdm2SyD8gEK5je4tlhUvfGtDvMSlstq71kIn9nRpFnqB6MFlbYSEAZmo8dGbCquoUc++6Rum208wcVbrzzVtGlXB/Ow9AbFMYeAGA0+N/K1e59c=
|
||||
on:
|
||||
tags: true
|
||||
repo: pytest-dev/pytest
|
||||
|
||||
script: tox --recreate
|
||||
|
||||
|
||||
20
AUTHORS
20
AUTHORS
@@ -3,12 +3,15 @@ merlinux GmbH, Germany, office at merlinux eu
|
||||
|
||||
Contributors include::
|
||||
|
||||
Aaron Coleman
|
||||
Abdeali JK
|
||||
Abhijeet Kasurde
|
||||
Ahn Ki-Wook
|
||||
Alan Velasco
|
||||
Alexander Johnson
|
||||
Alexei Kozlenok
|
||||
Anatoly Bubenkoff
|
||||
Anders Hovmöller
|
||||
Andras Tim
|
||||
Andreas Zeidler
|
||||
Andrzej Ostrowski
|
||||
@@ -17,6 +20,7 @@ Anthon van der Neut
|
||||
Anthony Sottile
|
||||
Antony Lee
|
||||
Armin Rigo
|
||||
Aron Coyle
|
||||
Aron Curzon
|
||||
Aviv Palivoda
|
||||
Barney Gale
|
||||
@@ -25,11 +29,13 @@ Benjamin Peterson
|
||||
Bernard Pratz
|
||||
Bob Ippolito
|
||||
Brian Dorsey
|
||||
Brian Maissy
|
||||
Brian Okken
|
||||
Brianna Laugher
|
||||
Bruno Oliveira
|
||||
Cal Leeming
|
||||
Carl Friedrich Bolz
|
||||
Ceridwen
|
||||
Charles Cloud
|
||||
Charnjit SiNGH (CCSJ)
|
||||
Chris Lamb
|
||||
@@ -37,6 +43,7 @@ Christian Boelsen
|
||||
Christian Theunert
|
||||
Christian Tismer
|
||||
Christopher Gilling
|
||||
Cyrus Maden
|
||||
Daniel Grana
|
||||
Daniel Hahler
|
||||
Daniel Nuri
|
||||
@@ -65,6 +72,7 @@ Feng Ma
|
||||
Florian Bruhin
|
||||
Floris Bruynooghe
|
||||
Gabriel Reis
|
||||
George Kussumoto
|
||||
Georgy Dyuldin
|
||||
Graham Horler
|
||||
Greg Price
|
||||
@@ -72,8 +80,11 @@ Grig Gheorghiu
|
||||
Grigorii Eremeev (budulianin)
|
||||
Guido Wesdorp
|
||||
Harald Armin Massa
|
||||
Henk-Jaap Wagenaar
|
||||
Hugo van Kemenade
|
||||
Hui Wang (coldnight)
|
||||
Ian Bicking
|
||||
Ian Lesperance
|
||||
Jaap Broekhuizen
|
||||
Jan Balster
|
||||
Janne Vanhala
|
||||
@@ -81,6 +92,7 @@ Jason R. Coombs
|
||||
Javier Domingo Cansino
|
||||
Javier Romero
|
||||
Jeff Widman
|
||||
John Eddie Ayson
|
||||
John Towler
|
||||
Jon Sonesen
|
||||
Jonas Obrist
|
||||
@@ -118,6 +130,7 @@ Matt Bachmann
|
||||
Matt Duck
|
||||
Matt Williams
|
||||
Matthias Hafner
|
||||
Maxim Filipenko
|
||||
mbyt
|
||||
Michael Aquilina
|
||||
Michael Birtwell
|
||||
@@ -142,6 +155,7 @@ Punyashloka Biswal
|
||||
Quentin Pradet
|
||||
Ralf Schmitt
|
||||
Ran Benita
|
||||
Raphael Castaneda
|
||||
Raphael Pierzina
|
||||
Raquel Alegre
|
||||
Ravi Chandra
|
||||
@@ -152,6 +166,7 @@ Ronny Pfannschmidt
|
||||
Ross Lawley
|
||||
Russel Winder
|
||||
Ryan Wooden
|
||||
Samuel Dion-Girardeau
|
||||
Samuele Pedroni
|
||||
Segev Finer
|
||||
Simon Gomizelj
|
||||
@@ -162,13 +177,16 @@ Stefan Zimmermann
|
||||
Stefano Taschini
|
||||
Steffen Allner
|
||||
Stephan Obermann
|
||||
Tarcisio Fischer
|
||||
Tareq Alayan
|
||||
Ted Xiao
|
||||
Thomas Grainger
|
||||
Thomas Hisch
|
||||
Tom Dalton
|
||||
Tom Viner
|
||||
Trevor Bekolay
|
||||
Tyler Goodlet
|
||||
Tzu-ping Chung
|
||||
Vasily Kuznetsov
|
||||
Victor Uriarte
|
||||
Vidar T. Fauske
|
||||
@@ -178,3 +196,5 @@ Wouter van Ackooy
|
||||
Xuan Luong
|
||||
Xuecong Liao
|
||||
Zoltán Máté
|
||||
Roland Puntaier
|
||||
Allan Feldman
|
||||
|
||||
490
CHANGELOG.rst
490
CHANGELOG.rst
@@ -8,6 +8,492 @@
|
||||
|
||||
.. towncrier release notes start
|
||||
|
||||
Pytest 3.4.2 (2018-03-04)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- Removed progress information when capture option is ``no``. (`#3203
|
||||
<https://github.com/pytest-dev/pytest/issues/3203>`_)
|
||||
|
||||
- Refactor check of bindir from ``exists`` to ``isdir``. (`#3241
|
||||
<https://github.com/pytest-dev/pytest/issues/3241>`_)
|
||||
|
||||
- Fix ``TypeError`` issue when using ``approx`` with a ``Decimal`` value.
|
||||
(`#3247 <https://github.com/pytest-dev/pytest/issues/3247>`_)
|
||||
|
||||
- Fix reference cycle generated when using the ``request`` fixture. (`#3249
|
||||
<https://github.com/pytest-dev/pytest/issues/3249>`_)
|
||||
|
||||
- ``[tool:pytest]`` sections in ``*.cfg`` files passed by the ``-c`` option are
|
||||
now properly recognized. (`#3260
|
||||
<https://github.com/pytest-dev/pytest/issues/3260>`_)
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- Add logging plugin to plugins list. (`#3209
|
||||
<https://github.com/pytest-dev/pytest/issues/3209>`_)
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- Fix minor typo in fixture.rst (`#3259
|
||||
<https://github.com/pytest-dev/pytest/issues/3259>`_)
|
||||
|
||||
|
||||
Pytest 3.4.1 (2018-02-20)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- Move import of ``doctest.UnexpectedException`` to top-level to avoid possible
|
||||
errors when using ``--pdb``. (`#1810
|
||||
<https://github.com/pytest-dev/pytest/issues/1810>`_)
|
||||
|
||||
- Added printing of captured stdout/stderr before entering pdb, and improved a
|
||||
test which was giving false negatives about output capturing. (`#3052
|
||||
<https://github.com/pytest-dev/pytest/issues/3052>`_)
|
||||
|
||||
- Fix ordering of tests using parametrized fixtures which can lead to fixtures
|
||||
being created more than necessary. (`#3161
|
||||
<https://github.com/pytest-dev/pytest/issues/3161>`_)
|
||||
|
||||
- Fix bug where logging happening at hooks outside of "test run" hooks would
|
||||
cause an internal error. (`#3184
|
||||
<https://github.com/pytest-dev/pytest/issues/3184>`_)
|
||||
|
||||
- Detect arguments injected by ``unittest.mock.patch`` decorator correctly when
|
||||
pypi ``mock.patch`` is installed and imported. (`#3206
|
||||
<https://github.com/pytest-dev/pytest/issues/3206>`_)
|
||||
|
||||
- Errors shown when a ``pytest.raises()`` with ``match=`` fails are now cleaner
|
||||
on what happened: When no exception was raised, the "matching '...'" part got
|
||||
removed as it falsely implies that an exception was raised but it didn't
|
||||
match. When a wrong exception was raised, it's now thrown (like
|
||||
``pytest.raised()`` without ``match=`` would) instead of complaining about
|
||||
the unmatched text. (`#3222
|
||||
<https://github.com/pytest-dev/pytest/issues/3222>`_)
|
||||
|
||||
- Fixed output capture handling in doctests on macOS. (`#985
|
||||
<https://github.com/pytest-dev/pytest/issues/985>`_)
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- Add Sphinx parameter docs for ``match`` and ``message`` args to
|
||||
``pytest.raises``. (`#3202
|
||||
<https://github.com/pytest-dev/pytest/issues/3202>`_)
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- pytest has changed the publication procedure and is now being published to
|
||||
PyPI directly from Travis. (`#3060
|
||||
<https://github.com/pytest-dev/pytest/issues/3060>`_)
|
||||
|
||||
- Rename ``ParameterSet._for_parameterize()`` to ``_for_parametrize()`` in
|
||||
order to comply with the naming convention. (`#3166
|
||||
<https://github.com/pytest-dev/pytest/issues/3166>`_)
|
||||
|
||||
- Skip failing pdb/doctest test on mac. (`#985
|
||||
<https://github.com/pytest-dev/pytest/issues/985>`_)
|
||||
|
||||
|
||||
Pytest 3.4.0 (2018-01-30)
|
||||
=========================
|
||||
|
||||
Deprecations and Removals
|
||||
-------------------------
|
||||
|
||||
- All pytest classes now subclass ``object`` for better Python 2/3 compatibility.
|
||||
This should not affect user code except in very rare edge cases. (`#2147
|
||||
<https://github.com/pytest-dev/pytest/issues/2147>`_)
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- Introduce ``empty_parameter_set_mark`` ini option to select which mark to
|
||||
apply when ``@pytest.mark.parametrize`` is given an empty set of parameters.
|
||||
Valid options are ``skip`` (default) and ``xfail``. Note that it is planned
|
||||
to change the default to ``xfail`` in future releases as this is considered
|
||||
less error prone. (`#2527
|
||||
<https://github.com/pytest-dev/pytest/issues/2527>`_)
|
||||
|
||||
- **Incompatible change**: after community feedback the `logging
|
||||
<https://docs.pytest.org/en/latest/logging.html>`_ functionality has
|
||||
undergone some changes. Please consult the `logging documentation
|
||||
<https://docs.pytest.org/en/latest/logging.html#incompatible-changes-in-pytest-3-4>`_
|
||||
for details. (`#3013 <https://github.com/pytest-dev/pytest/issues/3013>`_)
|
||||
|
||||
- Console output falls back to "classic" mode when capturing is disabled (``-s``),
|
||||
otherwise the output gets garbled to the point of being useless. (`#3038
|
||||
<https://github.com/pytest-dev/pytest/issues/3038>`_)
|
||||
|
||||
- New `pytest_runtest_logfinish
|
||||
<https://docs.pytest.org/en/latest/writing_plugins.html#_pytest.hookspec.pytest_runtest_logfinish>`_
|
||||
hook which is called when a test item has finished executing, analogous to
|
||||
`pytest_runtest_logstart
|
||||
<https://docs.pytest.org/en/latest/writing_plugins.html#_pytest.hookspec.pytest_runtest_start>`_.
|
||||
(`#3101 <https://github.com/pytest-dev/pytest/issues/3101>`_)
|
||||
|
||||
- Improve performance when collecting tests using many fixtures. (`#3107
|
||||
<https://github.com/pytest-dev/pytest/issues/3107>`_)
|
||||
|
||||
- New ``caplog.get_records(when)`` method which provides access to the captured
|
||||
records for the ``"setup"``, ``"call"`` and ``"teardown"``
|
||||
testing stages. (`#3117 <https://github.com/pytest-dev/pytest/issues/3117>`_)
|
||||
|
||||
- New fixture ``record_xml_attribute`` that allows modifying and inserting
|
||||
attributes on the ``<testcase>`` xml node in JUnit reports. (`#3130
|
||||
<https://github.com/pytest-dev/pytest/issues/3130>`_)
|
||||
|
||||
- The default cache directory has been renamed from ``.cache`` to
|
||||
``.pytest_cache`` after community feedback that the name ``.cache`` did not
|
||||
make it clear that it was used by pytest. (`#3138
|
||||
<https://github.com/pytest-dev/pytest/issues/3138>`_)
|
||||
|
||||
- Colorize the levelname column in the live-log output. (`#3142
|
||||
<https://github.com/pytest-dev/pytest/issues/3142>`_)
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- Fix hanging pexpect test on MacOS by using flush() instead of wait().
|
||||
(`#2022 <https://github.com/pytest-dev/pytest/issues/2022>`_)
|
||||
|
||||
- Fix restoring Python state after in-process pytest runs with the
|
||||
``pytester`` plugin; this may break tests using multiple inprocess
|
||||
pytest runs if later ones depend on earlier ones leaking global interpreter
|
||||
changes. (`#3016 <https://github.com/pytest-dev/pytest/issues/3016>`_)
|
||||
|
||||
- Fix skipping plugin reporting hook when test aborted before plugin setup
|
||||
hook. (`#3074 <https://github.com/pytest-dev/pytest/issues/3074>`_)
|
||||
|
||||
- Fix progress percentage reported when tests fail during teardown. (`#3088
|
||||
<https://github.com/pytest-dev/pytest/issues/3088>`_)
|
||||
|
||||
- **Incompatible change**: ``-o/--override`` option no longer eats all the
|
||||
remaining options, which can lead to surprising behavior: for example,
|
||||
``pytest -o foo=1 /path/to/test.py`` would fail because ``/path/to/test.py``
|
||||
would be considered as part of the ``-o`` command-line argument. One
|
||||
consequence of this is that now multiple configuration overrides need
|
||||
multiple ``-o`` flags: ``pytest -o foo=1 -o bar=2``. (`#3103
|
||||
<https://github.com/pytest-dev/pytest/issues/3103>`_)
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- Document hooks (defined with ``historic=True``) which cannot be used with
|
||||
``hookwrapper=True``. (`#2423
|
||||
<https://github.com/pytest-dev/pytest/issues/2423>`_)
|
||||
|
||||
- Clarify that warning capturing doesn't change the warning filter by default.
|
||||
(`#2457 <https://github.com/pytest-dev/pytest/issues/2457>`_)
|
||||
|
||||
- Clarify a possible confusion when using pytest_fixture_setup with fixture
|
||||
functions that return None. (`#2698
|
||||
<https://github.com/pytest-dev/pytest/issues/2698>`_)
|
||||
|
||||
- Fix the wording of a sentence on doctest flags used in pytest. (`#3076
|
||||
<https://github.com/pytest-dev/pytest/issues/3076>`_)
|
||||
|
||||
- Prefer ``https://*.readthedocs.io`` over ``http://*.rtfd.org`` for links in
|
||||
the documentation. (`#3092
|
||||
<https://github.com/pytest-dev/pytest/issues/3092>`_)
|
||||
|
||||
- Improve readability (wording, grammar) of Getting Started guide (`#3131
|
||||
<https://github.com/pytest-dev/pytest/issues/3131>`_)
|
||||
|
||||
- Added note that calling pytest.main multiple times from the same process is
|
||||
not recommended because of import caching. (`#3143
|
||||
<https://github.com/pytest-dev/pytest/issues/3143>`_)
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- Show a simple and easy error when keyword expressions trigger a syntax error
|
||||
(for example, ``"-k foo and import"`` will show an error that you can not use
|
||||
the ``import`` keyword in expressions). (`#2953
|
||||
<https://github.com/pytest-dev/pytest/issues/2953>`_)
|
||||
|
||||
- Change parametrized automatic test id generation to use the ``__name__``
|
||||
attribute of functions instead of the fallback argument name plus counter.
|
||||
(`#2976 <https://github.com/pytest-dev/pytest/issues/2976>`_)
|
||||
|
||||
- Replace py.std with stdlib imports. (`#3067
|
||||
<https://github.com/pytest-dev/pytest/issues/3067>`_)
|
||||
|
||||
- Corrected 'you' to 'your' in logging docs. (`#3129
|
||||
<https://github.com/pytest-dev/pytest/issues/3129>`_)
|
||||
|
||||
|
||||
Pytest 3.3.2 (2017-12-25)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- pytester: ignore files used to obtain current user metadata in the fd leak
|
||||
detector. (`#2784 <https://github.com/pytest-dev/pytest/issues/2784>`_)
|
||||
|
||||
- Fix **memory leak** where objects returned by fixtures were never destructed
|
||||
by the garbage collector. (`#2981
|
||||
<https://github.com/pytest-dev/pytest/issues/2981>`_)
|
||||
|
||||
- Fix conversion of pyargs to filename to not convert symlinks on Python 2. (`#2985
|
||||
<https://github.com/pytest-dev/pytest/issues/2985>`_)
|
||||
|
||||
- ``PYTEST_DONT_REWRITE`` is now checked for plugins too rather than only for
|
||||
test modules. (`#2995 <https://github.com/pytest-dev/pytest/issues/2995>`_)
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- Add clarifying note about behavior of multiple parametrized arguments (`#3001
|
||||
<https://github.com/pytest-dev/pytest/issues/3001>`_)
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- Code cleanup. (`#3015 <https://github.com/pytest-dev/pytest/issues/3015>`_,
|
||||
`#3021 <https://github.com/pytest-dev/pytest/issues/3021>`_)
|
||||
|
||||
- Clean up code by replacing imports and references of `_ast` to `ast`. (`#3018
|
||||
<https://github.com/pytest-dev/pytest/issues/3018>`_)
|
||||
|
||||
|
||||
Pytest 3.3.1 (2017-12-05)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- Fix issue about ``-p no:<plugin>`` having no effect. (`#2920
|
||||
<https://github.com/pytest-dev/pytest/issues/2920>`_)
|
||||
|
||||
- Fix regression with warnings that contained non-strings in their arguments in
|
||||
Python 2. (`#2956 <https://github.com/pytest-dev/pytest/issues/2956>`_)
|
||||
|
||||
- Always escape null bytes when setting ``PYTEST_CURRENT_TEST``. (`#2957
|
||||
<https://github.com/pytest-dev/pytest/issues/2957>`_)
|
||||
|
||||
- Fix ``ZeroDivisionError`` when using the ``testmon`` plugin when no tests
|
||||
were actually collected. (`#2971
|
||||
<https://github.com/pytest-dev/pytest/issues/2971>`_)
|
||||
|
||||
- Bring back ``TerminalReporter.writer`` as an alias to
|
||||
``TerminalReporter._tw``. This alias was removed by accident in the ``3.3.0``
|
||||
release. (`#2984 <https://github.com/pytest-dev/pytest/issues/2984>`_)
|
||||
|
||||
- The ``pytest-capturelog`` plugin is now also blacklisted, avoiding errors when
|
||||
running pytest with it still installed. (`#3004
|
||||
<https://github.com/pytest-dev/pytest/issues/3004>`_)
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- Fix broken link to plugin ``pytest-localserver``. (`#2963
|
||||
<https://github.com/pytest-dev/pytest/issues/2963>`_)
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- Update github "bugs" link in ``CONTRIBUTING.rst`` (`#2949
|
||||
<https://github.com/pytest-dev/pytest/issues/2949>`_)
|
||||
|
||||
|
||||
Pytest 3.3.0 (2017-11-23)
|
||||
=========================
|
||||
|
||||
Deprecations and Removals
|
||||
-------------------------
|
||||
|
||||
- Pytest no longer supports Python **2.6** and **3.3**. Those Python versions
|
||||
are EOL for some time now and incur maintenance and compatibility costs on
|
||||
the pytest core team, and following up with the rest of the community we
|
||||
decided that they will no longer be supported starting on this version. Users
|
||||
which still require those versions should pin pytest to ``<3.3``. (`#2812
|
||||
<https://github.com/pytest-dev/pytest/issues/2812>`_)
|
||||
|
||||
- Remove internal ``_preloadplugins()`` function. This removal is part of the
|
||||
``pytest_namespace()`` hook deprecation. (`#2636
|
||||
<https://github.com/pytest-dev/pytest/issues/2636>`_)
|
||||
|
||||
- Internally change ``CallSpec2`` to have a list of marks instead of a broken
|
||||
mapping of keywords. This removes the keywords attribute of the internal
|
||||
``CallSpec2`` class. (`#2672
|
||||
<https://github.com/pytest-dev/pytest/issues/2672>`_)
|
||||
|
||||
- Remove ParameterSet.deprecated_arg_dict - its not a public api and the lack
|
||||
of the underscore was a naming error. (`#2675
|
||||
<https://github.com/pytest-dev/pytest/issues/2675>`_)
|
||||
|
||||
- Remove the internal multi-typed attribute ``Node._evalskip`` and replace it
|
||||
with the boolean ``Node._skipped_by_mark``. (`#2767
|
||||
<https://github.com/pytest-dev/pytest/issues/2767>`_)
|
||||
|
||||
- The ``params`` list passed to ``pytest.fixture`` is now for
|
||||
all effects considered immutable and frozen at the moment of the ``pytest.fixture``
|
||||
call. Previously the list could be changed before the first invocation of the fixture
|
||||
allowing for a form of dynamic parametrization (for example, updated from command-line options),
|
||||
but this was an unwanted implementation detail which complicated the internals and prevented
|
||||
some internal cleanup. See issue `#2959 <https://github.com/pytest-dev/pytest/issues/2959>`_
|
||||
for details and a recommended workaround.
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- ``pytest_fixture_post_finalizer`` hook can now receive a ``request``
|
||||
argument. (`#2124 <https://github.com/pytest-dev/pytest/issues/2124>`_)
|
||||
|
||||
- Replace the old introspection code in compat.py that determines the available
|
||||
arguments of fixtures with inspect.signature on Python 3 and
|
||||
funcsigs.signature on Python 2. This should respect ``__signature__``
|
||||
declarations on functions. (`#2267
|
||||
<https://github.com/pytest-dev/pytest/issues/2267>`_)
|
||||
|
||||
- Report tests with global ``pytestmark`` variable only once. (`#2549
|
||||
<https://github.com/pytest-dev/pytest/issues/2549>`_)
|
||||
|
||||
- Now pytest displays the total progress percentage while running tests. The
|
||||
previous output style can be set by configuring the ``console_output_style``
|
||||
setting to ``classic``. (`#2657 <https://github.com/pytest-dev/pytest/issues/2657>`_)
|
||||
|
||||
- Match ``warns`` signature to ``raises`` by adding ``match`` keyword. (`#2708
|
||||
<https://github.com/pytest-dev/pytest/issues/2708>`_)
|
||||
|
||||
- Pytest now captures and displays output from the standard `logging` module.
|
||||
The user can control the logging level to be captured by specifying options
|
||||
in ``pytest.ini``, the command line and also during individual tests using
|
||||
markers. Also, a ``caplog`` fixture is available that enables users to test
|
||||
the captured log during specific tests (similar to ``capsys`` for example).
|
||||
For more information, please see the `logging docs
|
||||
<https://docs.pytest.org/en/latest/logging.html>`_. This feature was
|
||||
introduced by merging the popular `pytest-catchlog
|
||||
<https://pypi.org/project/pytest-catchlog/>`_ plugin, thanks to `Thomas Hisch
|
||||
<https://github.com/thisch>`_. Be advised that during the merging the
|
||||
backward compatibility interface with the defunct ``pytest-capturelog`` has
|
||||
been dropped. (`#2794 <https://github.com/pytest-dev/pytest/issues/2794>`_)
|
||||
|
||||
- Add ``allow_module_level`` kwarg to ``pytest.skip()``, enabling to skip the
|
||||
whole module. (`#2808 <https://github.com/pytest-dev/pytest/issues/2808>`_)
|
||||
|
||||
- Allow setting ``file_or_dir``, ``-c``, and ``-o`` in PYTEST_ADDOPTS. (`#2824
|
||||
<https://github.com/pytest-dev/pytest/issues/2824>`_)
|
||||
|
||||
- Return stdout/stderr capture results as a ``namedtuple``, so ``out`` and
|
||||
``err`` can be accessed by attribute. (`#2879
|
||||
<https://github.com/pytest-dev/pytest/issues/2879>`_)
|
||||
|
||||
- Add ``capfdbinary``, a version of ``capfd`` which returns bytes from
|
||||
``readouterr()``. (`#2923
|
||||
<https://github.com/pytest-dev/pytest/issues/2923>`_)
|
||||
|
||||
- Add ``capsysbinary`` a version of ``capsys`` which returns bytes from
|
||||
``readouterr()``. (`#2934
|
||||
<https://github.com/pytest-dev/pytest/issues/2934>`_)
|
||||
|
||||
- Implement feature to skip ``setup.py`` files when run with
|
||||
``--doctest-modules``. (`#502
|
||||
<https://github.com/pytest-dev/pytest/issues/502>`_)
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- Resume output capturing after ``capsys/capfd.disabled()`` context manager.
|
||||
(`#1993 <https://github.com/pytest-dev/pytest/issues/1993>`_)
|
||||
|
||||
- ``pytest_fixture_setup`` and ``pytest_fixture_post_finalizer`` hooks are now
|
||||
called for all ``conftest.py`` files. (`#2124
|
||||
<https://github.com/pytest-dev/pytest/issues/2124>`_)
|
||||
|
||||
- If an exception happens while loading a plugin, pytest no longer hides the
|
||||
original traceback. In Python 2 it will show the original traceback with a new
|
||||
message that explains in which plugin. In Python 3 it will show 2 canonized
|
||||
exceptions, the original exception while loading the plugin in addition to an
|
||||
exception that pytest throws about loading a plugin. (`#2491
|
||||
<https://github.com/pytest-dev/pytest/issues/2491>`_)
|
||||
|
||||
- ``capsys`` and ``capfd`` can now be used by other fixtures. (`#2709
|
||||
<https://github.com/pytest-dev/pytest/issues/2709>`_)
|
||||
|
||||
- Internal ``pytester`` plugin properly encodes ``bytes`` arguments to
|
||||
``utf-8``. (`#2738 <https://github.com/pytest-dev/pytest/issues/2738>`_)
|
||||
|
||||
- ``testdir`` now uses use the same method used by ``tmpdir`` to create its
|
||||
temporary directory. This changes the final structure of the ``testdir``
|
||||
directory slightly, but should not affect usage in normal scenarios and
|
||||
avoids a number of potential problems. (`#2751
|
||||
<https://github.com/pytest-dev/pytest/issues/2751>`_)
|
||||
|
||||
- Pytest no longer complains about warnings with unicode messages being
|
||||
non-ascii compatible even for ascii-compatible messages. As a result of this,
|
||||
warnings with unicode messages are converted first to an ascii representation
|
||||
for safety. (`#2809 <https://github.com/pytest-dev/pytest/issues/2809>`_)
|
||||
|
||||
- Change return value of pytest command when ``--maxfail`` is reached from
|
||||
``2`` (interrupted) to ``1`` (failed). (`#2845
|
||||
<https://github.com/pytest-dev/pytest/issues/2845>`_)
|
||||
|
||||
- Fix issue in assertion rewriting which could lead it to rewrite modules which
|
||||
should not be rewritten. (`#2939
|
||||
<https://github.com/pytest-dev/pytest/issues/2939>`_)
|
||||
|
||||
- Handle marks without description in ``pytest.ini``. (`#2942
|
||||
<https://github.com/pytest-dev/pytest/issues/2942>`_)
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- pytest now depends on `attrs <https://pypi.org/project/attrs/>`_ for internal
|
||||
structures to ease code maintainability. (`#2641
|
||||
<https://github.com/pytest-dev/pytest/issues/2641>`_)
|
||||
|
||||
- Refactored internal Python 2/3 compatibility code to use ``six``. (`#2642
|
||||
<https://github.com/pytest-dev/pytest/issues/2642>`_)
|
||||
|
||||
- Stop vendoring ``pluggy`` - we're missing out on its latest changes for not
|
||||
much benefit (`#2719 <https://github.com/pytest-dev/pytest/issues/2719>`_)
|
||||
|
||||
- Internal refactor: simplify ascii string escaping by using the
|
||||
backslashreplace error handler in newer Python 3 versions. (`#2734
|
||||
<https://github.com/pytest-dev/pytest/issues/2734>`_)
|
||||
|
||||
- Remove unnecessary mark evaluator in unittest plugin (`#2767
|
||||
<https://github.com/pytest-dev/pytest/issues/2767>`_)
|
||||
|
||||
- Calls to ``Metafunc.addcall`` now emit a deprecation warning. This function
|
||||
is scheduled to be removed in ``pytest-4.0``. (`#2876
|
||||
<https://github.com/pytest-dev/pytest/issues/2876>`_)
|
||||
|
||||
- Internal move of the parameterset extraction to a more maintainable place.
|
||||
(`#2877 <https://github.com/pytest-dev/pytest/issues/2877>`_)
|
||||
|
||||
- Internal refactoring to simplify scope node lookup. (`#2910
|
||||
<https://github.com/pytest-dev/pytest/issues/2910>`_)
|
||||
|
||||
- Configure ``pytest`` to prevent pip from installing pytest in unsupported
|
||||
Python versions. (`#2922
|
||||
<https://github.com/pytest-dev/pytest/issues/2922>`_)
|
||||
|
||||
|
||||
Pytest 3.2.5 (2017-11-15)
|
||||
=========================
|
||||
|
||||
@@ -192,7 +678,7 @@ Deprecations and Removals
|
||||
-------------------------
|
||||
|
||||
- ``pytest.approx`` no longer supports ``>``, ``>=``, ``<`` and ``<=``
|
||||
operators to avoid surprising/inconsistent behavior. See `the docs
|
||||
operators to avoid surprising/inconsistent behavior. See `the approx docs
|
||||
<https://docs.pytest.org/en/latest/builtin.html#pytest.approx>`_ for more
|
||||
information. (`#2003 <https://github.com/pytest-dev/pytest/issues/2003>`_)
|
||||
|
||||
@@ -2079,7 +2565,7 @@ time or change existing behaviors in order to make them less surprising/more use
|
||||
- fix issue655: work around different ways that cause python2/3
|
||||
to leak sys.exc_info into fixtures/tests causing failures in 3rd party code
|
||||
|
||||
- fix issue615: assertion re-writing did not correctly escape % signs
|
||||
- fix issue615: assertion rewriting did not correctly escape % signs
|
||||
when formatting boolean operations, which tripped over mixing
|
||||
booleans with modulo operators. Thanks to Tom Viner for the report,
|
||||
triaging and fix.
|
||||
|
||||
@@ -49,7 +49,7 @@ Fix bugs
|
||||
--------
|
||||
|
||||
Look through the GitHub issues for bugs. Here is a filter you can use:
|
||||
https://github.com/pytest-dev/pytest/labels/bug
|
||||
https://github.com/pytest-dev/pytest/labels/type%3A%20bug
|
||||
|
||||
:ref:`Talk <contact>` to developers to find out how you can fix specific bugs.
|
||||
|
||||
@@ -276,3 +276,15 @@ Here is a simple overview, with pytest-specific bits:
|
||||
base: features # if it's a feature
|
||||
|
||||
|
||||
Joining the Development Team
|
||||
----------------------------
|
||||
|
||||
Anyone who has successfully seen through a pull request which did not
|
||||
require any extra work from the development team to merge will
|
||||
themselves gain commit access if they so wish (if we forget to ask please send a friendly
|
||||
reminder). This does not mean your workflow to contribute changes,
|
||||
everyone goes through the same pull-request-and-review process and
|
||||
no-one merges their own pull requests unless already approved. It does however mean you can
|
||||
participate in the development process more fully since you can merge
|
||||
pull requests from other contributors yourself after having reviewed
|
||||
them.
|
||||
|
||||
@@ -12,7 +12,7 @@ taking a lot of time to make a new one.
|
||||
|
||||
#. Install development dependencies in a virtual environment with::
|
||||
|
||||
pip3 install -r tasks/requirements.txt
|
||||
pip3 install -U -r tasks/requirements.txt
|
||||
|
||||
#. Create a branch ``release-X.Y.Z`` with the version for the release.
|
||||
|
||||
@@ -22,44 +22,28 @@ taking a lot of time to make a new one.
|
||||
|
||||
Ensure your are in a clean work tree.
|
||||
|
||||
#. Generate docs, changelog, announcements and upload a package to
|
||||
your ``devpi`` staging server::
|
||||
#. Generate docs, changelog, announcements and a **local** tag::
|
||||
|
||||
invoke generate.pre-release <VERSION> <DEVPI USER> --password <DEVPI PASSWORD>
|
||||
|
||||
If ``--password`` is not given, it is assumed the user is already logged in ``devpi``.
|
||||
If you don't have an account, please ask for one.
|
||||
invoke generate.pre-release <VERSION>
|
||||
|
||||
#. Open a PR for this branch targeting ``master``.
|
||||
|
||||
#. Test the package
|
||||
#. After all tests pass and the PR has been approved, publish to PyPI by pushing the tag::
|
||||
|
||||
* **Manual method**
|
||||
git push git@github.com:pytest-dev/pytest.git <VERSION>
|
||||
|
||||
Run from multiple machines::
|
||||
Wait for the deploy to complete, then make sure it is `available on PyPI <https://pypi.org/project/pytest>`_.
|
||||
|
||||
devpi use https://devpi.net/USER/dev
|
||||
devpi test pytest==VERSION
|
||||
#. Send an email announcement with the contents from::
|
||||
|
||||
Check that tests pass for relevant combinations with::
|
||||
doc/en/announce/release-<VERSION>.rst
|
||||
|
||||
devpi list pytest
|
||||
To the following mailing lists:
|
||||
|
||||
* **CI servers**
|
||||
* pytest-dev@python.org (all releases)
|
||||
* python-announce-list@python.org (all releases)
|
||||
* testing-in-python@lists.idyll.org (only major/minor releases)
|
||||
|
||||
Configure a repository as per-instructions on
|
||||
devpi-cloud-test_ to test the package on Travis_ and AppVeyor_.
|
||||
All test environments should pass.
|
||||
|
||||
#. Publish to PyPI::
|
||||
|
||||
invoke generate.publish-release <VERSION> <DEVPI USER> <PYPI_NAME>
|
||||
|
||||
where PYPI_NAME is the name of pypi.python.org as configured in your ``~/.pypirc``
|
||||
file `for devpi <http://doc.devpi.net/latest/quickstart-releaseprocess.html?highlight=pypirc#devpi-push-releasing-to-an-external-index>`_.
|
||||
And announce it on `Twitter <https://twitter.com/>`_ with the ``#pytest`` hashtag.
|
||||
|
||||
#. After a minor/major release, merge ``release-X.Y.Z`` into ``master`` and push (or open a PR).
|
||||
|
||||
.. _devpi-cloud-test: https://github.com/obestwalter/devpi-cloud-test
|
||||
.. _AppVeyor: https://www.appveyor.com/
|
||||
.. _Travis: https://travis-ci.org
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
.. image:: https://ci.appveyor.com/api/projects/status/mrgbjaua7t33pg6b?svg=true
|
||||
:target: https://ci.appveyor.com/project/pytestbot/pytest
|
||||
|
||||
.. image:: https://www.codetriage.com/pytest-dev/pytest/badges/users.svg
|
||||
:target: https://www.codetriage.com/pytest-dev/pytest
|
||||
|
||||
The ``pytest`` framework makes it easy to write small tests, yet
|
||||
scales to support complex functional testing for applications and libraries.
|
||||
|
||||
@@ -76,7 +79,7 @@ Features
|
||||
- Can run `unittest <http://docs.pytest.org/en/latest/unittest.html>`_ (or trial),
|
||||
`nose <http://docs.pytest.org/en/latest/nose.html>`_ test suites out of the box;
|
||||
|
||||
- Python2.6+, Python3.3+, PyPy-2.3, Jython-2.5 (untested);
|
||||
- Python 2.7, Python 3.4+, PyPy 2.3, Jython 2.5 (untested);
|
||||
|
||||
- Rich plugin architecture, with over 315+ `external plugins <http://plugincompat.herokuapp.com>`_ and thriving community;
|
||||
|
||||
|
||||
@@ -4,9 +4,6 @@ needs argcomplete>=0.5.6 for python 3.2/3.3 (older versions fail
|
||||
to find the magic string, so _ARGCOMPLETE env. var is never set, and
|
||||
this does not need special code.
|
||||
|
||||
argcomplete does not support python 2.5 (although the changes for that
|
||||
are minor).
|
||||
|
||||
Function try_argcomplete(parser) should be called directly before
|
||||
the call to ArgumentParser.parse_args().
|
||||
|
||||
@@ -63,7 +60,7 @@ import os
|
||||
from glob import glob
|
||||
|
||||
|
||||
class FastFilesCompleter:
|
||||
class FastFilesCompleter(object):
|
||||
'Fast file completer class'
|
||||
|
||||
def __init__(self, directories=True):
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import inspect
|
||||
import sys
|
||||
import traceback
|
||||
from inspect import CO_VARARGS, CO_VARKEYWORDS
|
||||
import re
|
||||
from weakref import ref
|
||||
@@ -8,8 +10,6 @@ from _pytest.compat import _PY2, _PY3, PY35, safe_str
|
||||
import py
|
||||
builtin_repr = repr
|
||||
|
||||
reprlib = py.builtin._tryimport('repr', 'reprlib')
|
||||
|
||||
if _PY3:
|
||||
from traceback import format_exception_only
|
||||
else:
|
||||
@@ -235,7 +235,7 @@ class TracebackEntry(object):
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
if py.builtin.callable(tbh):
|
||||
if callable(tbh):
|
||||
return tbh(None if self._excinfo is None else self._excinfo())
|
||||
else:
|
||||
return tbh
|
||||
@@ -424,7 +424,7 @@ class ExceptionInfo(object):
|
||||
"""
|
||||
if style == 'native':
|
||||
return ReprExceptionInfo(ReprTracebackNative(
|
||||
py.std.traceback.format_exception(
|
||||
traceback.format_exception(
|
||||
self.type,
|
||||
self.value,
|
||||
self.traceback[0]._rawentry,
|
||||
@@ -558,7 +558,7 @@ class FormattedExcinfo(object):
|
||||
# else:
|
||||
# self._line("%-10s =\\" % (name,))
|
||||
# # XXX
|
||||
# py.std.pprint.pprint(value, stream=self.excinfowriter)
|
||||
# pprint.pprint(value, stream=self.excinfowriter)
|
||||
return ReprLocals(lines)
|
||||
|
||||
def repr_traceback_entry(self, entry, excinfo=None):
|
||||
@@ -671,7 +671,7 @@ class FormattedExcinfo(object):
|
||||
else:
|
||||
# fallback to native repr if the exception doesn't have a traceback:
|
||||
# ExceptionInfo objects require a full traceback to work
|
||||
reprtraceback = ReprTracebackNative(py.std.traceback.format_exception(type(e), e, None))
|
||||
reprtraceback = ReprTracebackNative(traceback.format_exception(type(e), e, None))
|
||||
reprcrash = None
|
||||
|
||||
repr_chain += [(reprtraceback, reprcrash, descr)]
|
||||
@@ -888,7 +888,7 @@ def getrawcode(obj, trycall=True):
|
||||
obj = getattr(obj, 'f_code', obj)
|
||||
obj = getattr(obj, '__code__', obj)
|
||||
if trycall and not hasattr(obj, 'co_firstlineno'):
|
||||
if hasattr(obj, '__call__') and not py.std.inspect.isclass(obj):
|
||||
if hasattr(obj, '__call__') and not inspect.isclass(obj):
|
||||
x = getrawcode(obj.__call__, trycall=False)
|
||||
if hasattr(x, 'co_firstlineno'):
|
||||
return x
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
from __future__ import absolute_import, division, generators, print_function
|
||||
|
||||
import ast
|
||||
from ast import PyCF_ONLY_AST as _AST_FLAG
|
||||
from bisect import bisect_right
|
||||
import linecache
|
||||
import sys
|
||||
import six
|
||||
import inspect
|
||||
import tokenize
|
||||
import py
|
||||
cpy_compile = compile
|
||||
|
||||
try:
|
||||
import _ast
|
||||
from _ast import PyCF_ONLY_AST as _AST_FLAG
|
||||
except ImportError:
|
||||
_AST_FLAG = 0
|
||||
_ast = None
|
||||
cpy_compile = compile
|
||||
|
||||
|
||||
class Source(object):
|
||||
@@ -32,7 +30,7 @@ class Source(object):
|
||||
partlines = part.lines
|
||||
elif isinstance(part, (tuple, list)):
|
||||
partlines = [x.rstrip("\n") for x in part]
|
||||
elif isinstance(part, py.builtin._basestring):
|
||||
elif isinstance(part, six.string_types):
|
||||
partlines = part.split('\n')
|
||||
if rstrip:
|
||||
while partlines:
|
||||
@@ -194,7 +192,7 @@ class Source(object):
|
||||
if flag & _AST_FLAG:
|
||||
return co
|
||||
lines = [(x + "\n") for x in self.lines]
|
||||
py.std.linecache.cache[filename] = (1, None, lines, filename)
|
||||
linecache.cache[filename] = (1, None, lines, filename)
|
||||
return co
|
||||
|
||||
#
|
||||
@@ -208,7 +206,7 @@ def compile_(source, filename=None, mode='exec', flags=generators.compiler_flag,
|
||||
retrieval of the source code for the code object
|
||||
and any recursively created code objects.
|
||||
"""
|
||||
if _ast is not None and isinstance(source, _ast.AST):
|
||||
if isinstance(source, ast.AST):
|
||||
# XXX should Source support having AST?
|
||||
return cpy_compile(source, filename, mode, flags, dont_inherit)
|
||||
_genframe = sys._getframe(1) # the caller
|
||||
@@ -226,8 +224,7 @@ def getfslineno(obj):
|
||||
code = _pytest._code.Code(obj)
|
||||
except TypeError:
|
||||
try:
|
||||
fn = (py.std.inspect.getsourcefile(obj) or
|
||||
py.std.inspect.getfile(obj))
|
||||
fn = inspect.getsourcefile(obj) or inspect.getfile(obj)
|
||||
except TypeError:
|
||||
return "", -1
|
||||
|
||||
@@ -251,7 +248,7 @@ def getfslineno(obj):
|
||||
|
||||
def findsource(obj):
|
||||
try:
|
||||
sourcelines, lineno = py.std.inspect.findsource(obj)
|
||||
sourcelines, lineno = inspect.findsource(obj)
|
||||
except py.builtin._sysex:
|
||||
raise
|
||||
except: # noqa
|
||||
@@ -321,7 +318,7 @@ def get_statement_startend2(lineno, node):
|
||||
# AST's line numbers start indexing at 1
|
||||
values = []
|
||||
for x in ast.walk(node):
|
||||
if isinstance(x, _ast.stmt) or isinstance(x, _ast.ExceptHandler):
|
||||
if isinstance(x, ast.stmt) or isinstance(x, ast.ExceptHandler):
|
||||
values.append(x.lineno - 1)
|
||||
for name in "finalbody", "orelse":
|
||||
val = getattr(x, name, None)
|
||||
@@ -341,8 +338,6 @@ def get_statement_startend2(lineno, node):
|
||||
def getstatementrange_ast(lineno, source, assertion=False, astnode=None):
|
||||
if astnode is None:
|
||||
content = str(source)
|
||||
if sys.version_info < (2, 7):
|
||||
content += "\n"
|
||||
try:
|
||||
astnode = compile(content, "source", "exec", 1024) # 1024 for AST
|
||||
except ValueError:
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
"""
|
||||
imports symbols from vendored "pluggy" if available, otherwise
|
||||
falls back to importing "pluggy" from the default namespace.
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
try:
|
||||
from _pytest.vendored_packages.pluggy import * # noqa
|
||||
from _pytest.vendored_packages.pluggy import __version__ # noqa
|
||||
except ImportError:
|
||||
from pluggy import * # noqa
|
||||
from pluggy import __version__ # noqa
|
||||
@@ -2,8 +2,8 @@
|
||||
support for presenting detailed information in failing assertions.
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import py
|
||||
import sys
|
||||
import six
|
||||
|
||||
from _pytest.assertion import util
|
||||
from _pytest.assertion import rewrite
|
||||
@@ -56,7 +56,7 @@ class DummyRewriteHook(object):
|
||||
pass
|
||||
|
||||
|
||||
class AssertionState:
|
||||
class AssertionState(object):
|
||||
"""State for the assertion plugin."""
|
||||
|
||||
def __init__(self, config, mode):
|
||||
@@ -67,10 +67,8 @@ class AssertionState:
|
||||
|
||||
def install_importhook(config):
|
||||
"""Try to install the rewrite hook, raise SystemError if it fails."""
|
||||
# Both Jython and CPython 2.6.0 have AST bugs that make the
|
||||
# assertion rewriting hook malfunction.
|
||||
if (sys.platform.startswith('java') or
|
||||
sys.version_info[:3] == (2, 6, 0)):
|
||||
# Jython has an AST bug that make the assertion rewriting hook malfunction.
|
||||
if (sys.platform.startswith('java')):
|
||||
raise SystemError('rewrite not supported')
|
||||
|
||||
config._assertstate = AssertionState(config, 'rewrite')
|
||||
@@ -126,7 +124,7 @@ def pytest_runtest_setup(item):
|
||||
if new_expl:
|
||||
new_expl = truncate.truncate_if_required(new_expl, item)
|
||||
new_expl = [line.replace("\n", "\\n") for line in new_expl]
|
||||
res = py.builtin._totext("\n~").join(new_expl)
|
||||
res = six.text_type("\n~").join(new_expl)
|
||||
if item.config.getvalue("assertmode") == "rewrite":
|
||||
res = res.replace("%", "%%")
|
||||
return res
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"""Rewrite assertion AST to produce nice error messages"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import ast
|
||||
import _ast
|
||||
import errno
|
||||
import itertools
|
||||
import imp
|
||||
import marshal
|
||||
import os
|
||||
import re
|
||||
import six
|
||||
import struct
|
||||
import sys
|
||||
import types
|
||||
@@ -33,7 +33,6 @@ else:
|
||||
PYC_EXT = ".py" + (__debug__ and "c" or "o")
|
||||
PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
|
||||
|
||||
REWRITE_NEWLINES = sys.version_info[:2] != (2, 7) and sys.version_info < (3, 2)
|
||||
ASCII_IS_DEFAULT_ENCODING = sys.version_info[0] < 3
|
||||
|
||||
if sys.version_info >= (3, 5):
|
||||
@@ -168,29 +167,31 @@ class AssertionRewritingHook(object):
|
||||
return True
|
||||
|
||||
for marked in self._must_rewrite:
|
||||
if name.startswith(marked):
|
||||
if name == marked or name.startswith(marked + '.'):
|
||||
state.trace("matched marked file %r (from %r)" % (name, marked))
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def mark_rewrite(self, *names):
|
||||
"""Mark import names as needing to be re-written.
|
||||
"""Mark import names as needing to be rewritten.
|
||||
|
||||
The named module or package as well as any nested modules will
|
||||
be re-written on import.
|
||||
be rewritten on import.
|
||||
"""
|
||||
already_imported = set(names).intersection(set(sys.modules))
|
||||
if already_imported:
|
||||
for name in already_imported:
|
||||
if name not in self._rewritten_names:
|
||||
self._warn_already_imported(name)
|
||||
already_imported = (set(names)
|
||||
.intersection(sys.modules)
|
||||
.difference(self._rewritten_names))
|
||||
for name in already_imported:
|
||||
if not AssertionRewriter.is_rewrite_disabled(
|
||||
sys.modules[name].__doc__ or ""):
|
||||
self._warn_already_imported(name)
|
||||
self._must_rewrite.update(names)
|
||||
|
||||
def _warn_already_imported(self, name):
|
||||
self.config.warn(
|
||||
'P1',
|
||||
'Module already imported so can not be re-written: %s' % name)
|
||||
'Module already imported so cannot be rewritten: %s' % name)
|
||||
|
||||
def load_module(self, name):
|
||||
# If there is an existing module object named 'fullname' in
|
||||
@@ -320,10 +321,6 @@ def _rewrite_test(config, fn):
|
||||
return None, None
|
||||
finally:
|
||||
del state._indecode
|
||||
# On Python versions which are not 2.7 and less than or equal to 3.1, the
|
||||
# parser expects *nix newlines.
|
||||
if REWRITE_NEWLINES:
|
||||
source = source.replace(RN, N) + N
|
||||
try:
|
||||
tree = ast.parse(source)
|
||||
except SyntaxError:
|
||||
@@ -405,10 +402,10 @@ def _saferepr(obj):
|
||||
|
||||
"""
|
||||
repr = py.io.saferepr(obj)
|
||||
if py.builtin._istext(repr):
|
||||
t = py.builtin.text
|
||||
if isinstance(repr, six.text_type):
|
||||
t = six.text_type
|
||||
else:
|
||||
t = py.builtin.bytes
|
||||
t = six.binary_type
|
||||
return repr.replace(t("\n"), t("\\n"))
|
||||
|
||||
|
||||
@@ -427,16 +424,16 @@ def _format_assertmsg(obj):
|
||||
# contains a newline it gets escaped, however if an object has a
|
||||
# .__repr__() which contains newlines it does not get escaped.
|
||||
# However in either case we want to preserve the newline.
|
||||
if py.builtin._istext(obj) or py.builtin._isbytes(obj):
|
||||
if isinstance(obj, six.text_type) or isinstance(obj, six.binary_type):
|
||||
s = obj
|
||||
is_repr = False
|
||||
else:
|
||||
s = py.io.saferepr(obj)
|
||||
is_repr = True
|
||||
if py.builtin._istext(s):
|
||||
t = py.builtin.text
|
||||
if isinstance(s, six.text_type):
|
||||
t = six.text_type
|
||||
else:
|
||||
t = py.builtin.bytes
|
||||
t = six.binary_type
|
||||
s = s.replace(t("\n"), t("\n~")).replace(t("%"), t("%%"))
|
||||
if is_repr:
|
||||
s = s.replace(t("\\n"), t("\n~"))
|
||||
@@ -444,15 +441,15 @@ def _format_assertmsg(obj):
|
||||
|
||||
|
||||
def _should_repr_global_name(obj):
|
||||
return not hasattr(obj, "__name__") and not py.builtin.callable(obj)
|
||||
return not hasattr(obj, "__name__") and not callable(obj)
|
||||
|
||||
|
||||
def _format_boolop(explanations, is_or):
|
||||
explanation = "(" + (is_or and " or " or " and ").join(explanations) + ")"
|
||||
if py.builtin._istext(explanation):
|
||||
t = py.builtin.text
|
||||
if isinstance(explanation, six.text_type):
|
||||
t = six.text_type
|
||||
else:
|
||||
t = py.builtin.bytes
|
||||
t = six.binary_type
|
||||
return explanation.replace(t('%'), t('%%'))
|
||||
|
||||
|
||||
@@ -533,7 +530,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
"""Assertion rewriting implementation.
|
||||
|
||||
The main entrypoint is to call .run() with an ast.Module instance,
|
||||
this will then find all the assert statements and re-write them to
|
||||
this will then find all the assert statements and rewrite them to
|
||||
provide intermediate values and a detailed assertion error. See
|
||||
http://pybites.blogspot.be/2011/07/behind-scenes-of-pytests-new-assertion.html
|
||||
for an overview of how this works.
|
||||
@@ -542,7 +539,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
statements in an ast.Module and for each ast.Assert statement it
|
||||
finds call .visit() with it. Then .visit_Assert() takes over and
|
||||
is responsible for creating new ast statements to replace the
|
||||
original assert statement: it re-writes the test of an assertion
|
||||
original assert statement: it rewrites the test of an assertion
|
||||
to provide intermediate values and replace it with an if statement
|
||||
which raises an assertion error with a detailed explanation in
|
||||
case the expression is false.
|
||||
@@ -640,7 +637,8 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
not isinstance(field, ast.expr)):
|
||||
nodes.append(field)
|
||||
|
||||
def is_rewrite_disabled(self, docstring):
|
||||
@staticmethod
|
||||
def is_rewrite_disabled(docstring):
|
||||
return "PYTEST_DONT_REWRITE" in docstring
|
||||
|
||||
def variable(self):
|
||||
@@ -726,7 +724,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
def visit_Assert(self, assert_):
|
||||
"""Return the AST statements to replace the ast.Assert instance.
|
||||
|
||||
This re-writes the test of an assertion to provide
|
||||
This rewrites the test of an assertion to provide
|
||||
intermediate values and replace it with an if statement which
|
||||
raises an assertion error with a detailed explanation in case
|
||||
the expression is false.
|
||||
@@ -918,7 +916,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
def visit_Compare(self, comp):
|
||||
self.push_format_context()
|
||||
left_res, left_expl = self.visit(comp.left)
|
||||
if isinstance(comp.left, (_ast.Compare, _ast.BoolOp)):
|
||||
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
|
||||
left_expl = "({0})".format(left_expl)
|
||||
res_variables = [self.variable() for i in range(len(comp.ops))]
|
||||
load_names = [ast.Name(v, ast.Load()) for v in res_variables]
|
||||
@@ -929,7 +927,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
results = [left_res]
|
||||
for i, op, next_operand in it:
|
||||
next_res, next_expl = self.visit(next_operand)
|
||||
if isinstance(next_operand, (_ast.Compare, _ast.BoolOp)):
|
||||
if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
|
||||
next_expl = "({0})".format(next_expl)
|
||||
results.append(next_res)
|
||||
sym = binop_map[op.__class__]
|
||||
|
||||
@@ -7,7 +7,7 @@ Current default behaviour is to truncate assertion explanations at
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import os
|
||||
|
||||
import py
|
||||
import six
|
||||
|
||||
|
||||
DEFAULT_MAX_LINES = 8
|
||||
@@ -74,8 +74,8 @@ def _truncate_explanation(input_lines, max_lines=None, max_chars=None):
|
||||
msg += ' ({0} lines hidden)'.format(truncated_line_count)
|
||||
msg += ", {0}" .format(USAGE_MSG)
|
||||
truncated_explanation.extend([
|
||||
py.builtin._totext(""),
|
||||
py.builtin._totext(msg),
|
||||
six.text_type(""),
|
||||
six.text_type(msg),
|
||||
])
|
||||
return truncated_explanation
|
||||
|
||||
|
||||
@@ -4,13 +4,10 @@ import pprint
|
||||
|
||||
import _pytest._code
|
||||
import py
|
||||
try:
|
||||
from collections import Sequence
|
||||
except ImportError:
|
||||
Sequence = list
|
||||
import six
|
||||
from collections import Sequence
|
||||
|
||||
|
||||
u = py.builtin._totext
|
||||
u = six.text_type
|
||||
|
||||
# The _reprcompare attribute on the util module is used by the new assertion
|
||||
# interpretation code and assertion rewriter to detect this plugin was
|
||||
@@ -112,7 +109,7 @@ def assertrepr_compare(config, op, left, right):
|
||||
summary = u('%s %s %s') % (ecu(left_repr), op, ecu(right_repr))
|
||||
|
||||
def issequence(x):
|
||||
return (isinstance(x, (list, tuple, Sequence)) and not isinstance(x, basestring))
|
||||
return isinstance(x, Sequence) and not isinstance(x, basestring)
|
||||
|
||||
def istext(x):
|
||||
return isinstance(x, basestring)
|
||||
@@ -174,9 +171,9 @@ def _diff_text(left, right, verbose=False):
|
||||
"""
|
||||
from difflib import ndiff
|
||||
explanation = []
|
||||
if isinstance(left, py.builtin.bytes):
|
||||
if isinstance(left, six.binary_type):
|
||||
left = u(repr(left)[1:-1]).replace(r'\n', '\n')
|
||||
if isinstance(right, py.builtin.bytes):
|
||||
if isinstance(right, six.binary_type):
|
||||
right = u(repr(right)[1:-1]).replace(r'\n', '\n')
|
||||
if not verbose:
|
||||
i = 0 # just in case left or right has zero length
|
||||
|
||||
@@ -17,7 +17,7 @@ class Cache(object):
|
||||
self.config = config
|
||||
self._cachedir = Cache.cache_dir_from_config(config)
|
||||
self.trace = config.trace.root.get("cache")
|
||||
if config.getvalue("cacheclear"):
|
||||
if config.getoption("cacheclear"):
|
||||
self.trace("clearing cachedir")
|
||||
if self._cachedir.check():
|
||||
self._cachedir.remove()
|
||||
@@ -98,13 +98,13 @@ class Cache(object):
|
||||
json.dump(value, f, indent=2, sort_keys=True)
|
||||
|
||||
|
||||
class LFPlugin:
|
||||
class LFPlugin(object):
|
||||
""" Plugin which implements the --lf (run last-failing) option """
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
active_keys = 'lf', 'failedfirst'
|
||||
self.active = any(config.getvalue(key) for key in active_keys)
|
||||
self.active = any(config.getoption(key) for key in active_keys)
|
||||
self.lastfailed = config.cache.get("cache/lastfailed", {})
|
||||
self._previously_failed_count = None
|
||||
|
||||
@@ -114,7 +114,8 @@ class LFPlugin:
|
||||
mode = "run all (no recorded failures)"
|
||||
else:
|
||||
noun = 'failure' if self._previously_failed_count == 1 else 'failures'
|
||||
suffix = " first" if self.config.getvalue("failedfirst") else ""
|
||||
suffix = " first" if self.config.getoption(
|
||||
"failedfirst") else ""
|
||||
mode = "rerun previous {count} {noun}{suffix}".format(
|
||||
count=self._previously_failed_count, suffix=suffix, noun=noun
|
||||
)
|
||||
@@ -151,7 +152,7 @@ class LFPlugin:
|
||||
# running a subset of all tests with recorded failures outside
|
||||
# of the set of tests currently executing
|
||||
return
|
||||
if self.config.getvalue("lf"):
|
||||
if self.config.getoption("lf"):
|
||||
items[:] = previously_failed
|
||||
config.hook.pytest_deselected(items=previously_passed)
|
||||
else:
|
||||
@@ -159,7 +160,7 @@ class LFPlugin:
|
||||
|
||||
def pytest_sessionfinish(self, session):
|
||||
config = self.config
|
||||
if config.getvalue("cacheshow") or hasattr(config, "slaveinput"):
|
||||
if config.getoption("cacheshow") or hasattr(config, "slaveinput"):
|
||||
return
|
||||
|
||||
saved_lastfailed = config.cache.get("cache/lastfailed", {})
|
||||
@@ -185,7 +186,7 @@ def pytest_addoption(parser):
|
||||
'--cache-clear', action='store_true', dest="cacheclear",
|
||||
help="remove all cache contents at start of test run.")
|
||||
parser.addini(
|
||||
"cache_dir", default='.cache',
|
||||
"cache_dir", default='.pytest_cache',
|
||||
help="cache directory path.")
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ per-test stdout/stderr capturing mechanism.
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import collections
|
||||
import contextlib
|
||||
import sys
|
||||
import os
|
||||
@@ -11,11 +12,10 @@ import io
|
||||
from io import UnsupportedOperation
|
||||
from tempfile import TemporaryFile
|
||||
|
||||
import py
|
||||
import six
|
||||
import pytest
|
||||
from _pytest.compat import CaptureIO
|
||||
|
||||
unicode = py.builtin.text
|
||||
|
||||
patchsysdict = {0: 'stdin', 1: 'stdout', 2: 'stderr'}
|
||||
|
||||
@@ -44,7 +44,7 @@ def pytest_load_initial_conftests(early_config, parser, args):
|
||||
pluginmanager.register(capman, "capturemanager")
|
||||
|
||||
# make sure that capturemanager is properly reset at final shutdown
|
||||
early_config.add_cleanup(capman.reset_capturings)
|
||||
early_config.add_cleanup(capman.stop_global_capturing)
|
||||
|
||||
# make sure logging does not raise exceptions at the end
|
||||
def silence_logging_at_shutdown():
|
||||
@@ -53,17 +53,30 @@ def pytest_load_initial_conftests(early_config, parser, args):
|
||||
early_config.add_cleanup(silence_logging_at_shutdown)
|
||||
|
||||
# finally trigger conftest loading but while capturing (issue93)
|
||||
capman.init_capturings()
|
||||
capman.start_global_capturing()
|
||||
outcome = yield
|
||||
out, err = capman.suspendcapture()
|
||||
out, err = capman.suspend_global_capture()
|
||||
if outcome.excinfo is not None:
|
||||
sys.stdout.write(out)
|
||||
sys.stderr.write(err)
|
||||
|
||||
|
||||
class CaptureManager:
|
||||
class CaptureManager(object):
|
||||
"""
|
||||
Capture plugin, manages that the appropriate capture method is enabled/disabled during collection and each
|
||||
test phase (setup, call, teardown). After each of those points, the captured output is obtained and
|
||||
attached to the collection/runtest report.
|
||||
|
||||
There are two levels of capture:
|
||||
* global: which is enabled by default and can be suppressed by the ``-s`` option. This is always enabled/disabled
|
||||
during collection and each test phase.
|
||||
* fixture: when a test function or one of its fixture depend on the ``capsys`` or ``capfd`` fixtures. In this
|
||||
case special handling is needed to ensure the fixtures take precedence over the global capture.
|
||||
"""
|
||||
|
||||
def __init__(self, method):
|
||||
self._method = method
|
||||
self._global_capturing = None
|
||||
|
||||
def _getcapture(self, method):
|
||||
if method == "fd":
|
||||
@@ -75,23 +88,24 @@ class CaptureManager:
|
||||
else:
|
||||
raise ValueError("unknown capturing method: %r" % method)
|
||||
|
||||
def init_capturings(self):
|
||||
assert not hasattr(self, "_capturing")
|
||||
self._capturing = self._getcapture(self._method)
|
||||
self._capturing.start_capturing()
|
||||
def start_global_capturing(self):
|
||||
assert self._global_capturing is None
|
||||
self._global_capturing = self._getcapture(self._method)
|
||||
self._global_capturing.start_capturing()
|
||||
|
||||
def reset_capturings(self):
|
||||
cap = self.__dict__.pop("_capturing", None)
|
||||
if cap is not None:
|
||||
cap.pop_outerr_to_orig()
|
||||
cap.stop_capturing()
|
||||
def stop_global_capturing(self):
|
||||
if self._global_capturing is not None:
|
||||
self._global_capturing.pop_outerr_to_orig()
|
||||
self._global_capturing.stop_capturing()
|
||||
self._global_capturing = None
|
||||
|
||||
def resumecapture(self):
|
||||
self._capturing.resume_capturing()
|
||||
def resume_global_capture(self):
|
||||
self._global_capturing.resume_capturing()
|
||||
|
||||
def suspendcapture(self, in_=False):
|
||||
self.deactivate_funcargs()
|
||||
cap = getattr(self, "_capturing", None)
|
||||
def suspend_global_capture(self, item=None, in_=False):
|
||||
if item is not None:
|
||||
self.deactivate_fixture(item)
|
||||
cap = getattr(self, "_global_capturing", None)
|
||||
if cap is not None:
|
||||
try:
|
||||
outerr = cap.readouterr()
|
||||
@@ -99,23 +113,26 @@ class CaptureManager:
|
||||
cap.suspend_capturing(in_=in_)
|
||||
return outerr
|
||||
|
||||
def activate_funcargs(self, pyfuncitem):
|
||||
capfuncarg = pyfuncitem.__dict__.pop("_capfuncarg", None)
|
||||
if capfuncarg is not None:
|
||||
capfuncarg._start()
|
||||
self._capfuncarg = capfuncarg
|
||||
def activate_fixture(self, item):
|
||||
"""If the current item is using ``capsys`` or ``capfd``, activate them so they take precedence over
|
||||
the global capture.
|
||||
"""
|
||||
fixture = getattr(item, "_capture_fixture", None)
|
||||
if fixture is not None:
|
||||
fixture._start()
|
||||
|
||||
def deactivate_funcargs(self):
|
||||
capfuncarg = self.__dict__.pop("_capfuncarg", None)
|
||||
if capfuncarg is not None:
|
||||
capfuncarg.close()
|
||||
def deactivate_fixture(self, item):
|
||||
"""Deactivates the ``capsys`` or ``capfd`` fixture of this item, if any."""
|
||||
fixture = getattr(item, "_capture_fixture", None)
|
||||
if fixture is not None:
|
||||
fixture.close()
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_make_collect_report(self, collector):
|
||||
if isinstance(collector, pytest.File):
|
||||
self.resumecapture()
|
||||
self.resume_global_capture()
|
||||
outcome = yield
|
||||
out, err = self.suspendcapture()
|
||||
out, err = self.suspend_global_capture()
|
||||
rep = outcome.get_result()
|
||||
if out:
|
||||
rep.sections.append(("Captured stdout", out))
|
||||
@@ -126,68 +143,135 @@ class CaptureManager:
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_setup(self, item):
|
||||
self.resumecapture()
|
||||
self.resume_global_capture()
|
||||
# no need to activate a capture fixture because they activate themselves during creation; this
|
||||
# only makes sense when a fixture uses a capture fixture, otherwise the capture fixture will
|
||||
# be activated during pytest_runtest_call
|
||||
yield
|
||||
self.suspendcapture_item(item, "setup")
|
||||
self.suspend_capture_item(item, "setup")
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_call(self, item):
|
||||
self.resumecapture()
|
||||
self.activate_funcargs(item)
|
||||
self.resume_global_capture()
|
||||
# it is important to activate this fixture during the call phase so it overwrites the "global"
|
||||
# capture
|
||||
self.activate_fixture(item)
|
||||
yield
|
||||
# self.deactivate_funcargs() called from suspendcapture()
|
||||
self.suspendcapture_item(item, "call")
|
||||
self.suspend_capture_item(item, "call")
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_teardown(self, item):
|
||||
self.resumecapture()
|
||||
self.resume_global_capture()
|
||||
self.activate_fixture(item)
|
||||
yield
|
||||
self.suspendcapture_item(item, "teardown")
|
||||
self.suspend_capture_item(item, "teardown")
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_keyboard_interrupt(self, excinfo):
|
||||
self.reset_capturings()
|
||||
self.stop_global_capturing()
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_internalerror(self, excinfo):
|
||||
self.reset_capturings()
|
||||
self.stop_global_capturing()
|
||||
|
||||
def suspendcapture_item(self, item, when, in_=False):
|
||||
out, err = self.suspendcapture(in_=in_)
|
||||
def suspend_capture_item(self, item, when, in_=False):
|
||||
out, err = self.suspend_global_capture(item, in_=in_)
|
||||
item.add_report_section(when, "stdout", out)
|
||||
item.add_report_section(when, "stderr", err)
|
||||
|
||||
|
||||
error_capsysfderror = "cannot use capsys and capfd at the same time"
|
||||
capture_fixtures = {'capfd', 'capfdbinary', 'capsys', 'capsysbinary'}
|
||||
|
||||
|
||||
def _ensure_only_one_capture_fixture(request, name):
|
||||
fixtures = set(request.fixturenames) & capture_fixtures - set((name,))
|
||||
if fixtures:
|
||||
fixtures = sorted(fixtures)
|
||||
fixtures = fixtures[0] if len(fixtures) == 1 else fixtures
|
||||
raise request.raiseerror(
|
||||
"cannot use {0} and {1} at the same time".format(
|
||||
fixtures, name,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def capsys(request):
|
||||
"""Enable capturing of writes to sys.stdout/sys.stderr and make
|
||||
captured output available via ``capsys.readouterr()`` method calls
|
||||
which return a ``(out, err)`` tuple.
|
||||
which return a ``(out, err)`` tuple. ``out`` and ``err`` will be ``text``
|
||||
objects.
|
||||
"""
|
||||
if "capfd" in request.fixturenames:
|
||||
raise request.raiseerror(error_capsysfderror)
|
||||
request.node._capfuncarg = c = CaptureFixture(SysCapture, request)
|
||||
return c
|
||||
_ensure_only_one_capture_fixture(request, 'capsys')
|
||||
with _install_capture_fixture_on_item(request, SysCapture) as fixture:
|
||||
yield fixture
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def capsysbinary(request):
|
||||
"""Enable capturing of writes to sys.stdout/sys.stderr and make
|
||||
captured output available via ``capsys.readouterr()`` method calls
|
||||
which return a ``(out, err)`` tuple. ``out`` and ``err`` will be ``bytes``
|
||||
objects.
|
||||
"""
|
||||
_ensure_only_one_capture_fixture(request, 'capsysbinary')
|
||||
# Currently, the implementation uses the python3 specific `.buffer`
|
||||
# property of CaptureIO.
|
||||
if sys.version_info < (3,):
|
||||
raise request.raiseerror('capsysbinary is only supported on python 3')
|
||||
with _install_capture_fixture_on_item(request, SysCaptureBinary) as fixture:
|
||||
yield fixture
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def capfd(request):
|
||||
"""Enable capturing of writes to file descriptors 1 and 2 and make
|
||||
captured output available via ``capfd.readouterr()`` method calls
|
||||
which return a ``(out, err)`` tuple.
|
||||
which return a ``(out, err)`` tuple. ``out`` and ``err`` will be ``text``
|
||||
objects.
|
||||
"""
|
||||
if "capsys" in request.fixturenames:
|
||||
request.raiseerror(error_capsysfderror)
|
||||
_ensure_only_one_capture_fixture(request, 'capfd')
|
||||
if not hasattr(os, 'dup'):
|
||||
pytest.skip("capfd funcarg needs os.dup")
|
||||
request.node._capfuncarg = c = CaptureFixture(FDCapture, request)
|
||||
return c
|
||||
pytest.skip("capfd fixture needs os.dup function which is not available in this system")
|
||||
with _install_capture_fixture_on_item(request, FDCapture) as fixture:
|
||||
yield fixture
|
||||
|
||||
|
||||
class CaptureFixture:
|
||||
@pytest.fixture
|
||||
def capfdbinary(request):
|
||||
"""Enable capturing of write to file descriptors 1 and 2 and make
|
||||
captured output available via ``capfdbinary.readouterr`` method calls
|
||||
which return a ``(out, err)`` tuple. ``out`` and ``err`` will be
|
||||
``bytes`` objects.
|
||||
"""
|
||||
_ensure_only_one_capture_fixture(request, 'capfdbinary')
|
||||
if not hasattr(os, 'dup'):
|
||||
pytest.skip("capfdbinary fixture needs os.dup function which is not available in this system")
|
||||
with _install_capture_fixture_on_item(request, FDCaptureBinary) as fixture:
|
||||
yield fixture
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _install_capture_fixture_on_item(request, capture_class):
|
||||
"""
|
||||
Context manager which creates a ``CaptureFixture`` instance and "installs" it on
|
||||
the item/node of the given request. Used by ``capsys`` and ``capfd``.
|
||||
|
||||
The CaptureFixture is added as attribute of the item because it needs to accessed
|
||||
by ``CaptureManager`` during its ``pytest_runtest_*`` hooks.
|
||||
"""
|
||||
request.node._capture_fixture = fixture = CaptureFixture(capture_class, request)
|
||||
capmanager = request.config.pluginmanager.getplugin('capturemanager')
|
||||
# need to active this fixture right away in case it is being used by another fixture (setup phase)
|
||||
# if this fixture is being used only by a test function (call phase), then we wouldn't need this
|
||||
# activation, but it doesn't hurt
|
||||
capmanager.activate_fixture(request.node)
|
||||
yield fixture
|
||||
fixture.close()
|
||||
del request.node._capture_fixture
|
||||
|
||||
|
||||
class CaptureFixture(object):
|
||||
def __init__(self, captureclass, request):
|
||||
self.captureclass = captureclass
|
||||
self.request = request
|
||||
@@ -211,12 +295,14 @@ class CaptureFixture:
|
||||
|
||||
@contextlib.contextmanager
|
||||
def disabled(self):
|
||||
self._capture.suspend_capturing()
|
||||
capmanager = self.request.config.pluginmanager.getplugin('capturemanager')
|
||||
capmanager.suspendcapture_item(self.request.node, "call", in_=True)
|
||||
capmanager.suspend_global_capture(item=None, in_=False)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
capmanager.resumecapture()
|
||||
capmanager.resume_global_capture()
|
||||
self._capture.resume_capturing()
|
||||
|
||||
|
||||
def safe_text_dupfile(f, mode, default_encoding="UTF8"):
|
||||
@@ -246,7 +332,7 @@ class EncodedFile(object):
|
||||
self.encoding = encoding
|
||||
|
||||
def write(self, obj):
|
||||
if isinstance(obj, unicode):
|
||||
if isinstance(obj, six.text_type):
|
||||
obj = obj.encode(self.encoding, "replace")
|
||||
self.buffer.write(obj)
|
||||
|
||||
@@ -263,6 +349,9 @@ class EncodedFile(object):
|
||||
return getattr(object.__getattribute__(self, "buffer"), name)
|
||||
|
||||
|
||||
CaptureResult = collections.namedtuple("CaptureResult", ["out", "err"])
|
||||
|
||||
|
||||
class MultiCapture(object):
|
||||
out = err = in_ = None
|
||||
|
||||
@@ -323,16 +412,19 @@ class MultiCapture(object):
|
||||
|
||||
def readouterr(self):
|
||||
""" return snapshot unicode value of stdout/stderr capturings. """
|
||||
return (self.out.snap() if self.out is not None else "",
|
||||
self.err.snap() if self.err is not None else "")
|
||||
return CaptureResult(self.out.snap() if self.out is not None else "",
|
||||
self.err.snap() if self.err is not None else "")
|
||||
|
||||
|
||||
class NoCapture:
|
||||
class NoCapture(object):
|
||||
__init__ = start = done = suspend = resume = lambda *args: None
|
||||
|
||||
|
||||
class FDCapture:
|
||||
""" Capture IO to/from a given os-level filedescriptor. """
|
||||
class FDCaptureBinary(object):
|
||||
"""Capture IO to/from a given os-level filedescriptor.
|
||||
|
||||
snap() produces `bytes`
|
||||
"""
|
||||
|
||||
def __init__(self, targetfd, tmpfile=None):
|
||||
self.targetfd = targetfd
|
||||
@@ -371,17 +463,11 @@ class FDCapture:
|
||||
self.syscapture.start()
|
||||
|
||||
def snap(self):
|
||||
f = self.tmpfile
|
||||
f.seek(0)
|
||||
res = f.read()
|
||||
if res:
|
||||
enc = getattr(f, "encoding", None)
|
||||
if enc and isinstance(res, bytes):
|
||||
res = py.builtin._totext(res, enc, "replace")
|
||||
f.truncate(0)
|
||||
f.seek(0)
|
||||
return res
|
||||
return ''
|
||||
self.tmpfile.seek(0)
|
||||
res = self.tmpfile.read()
|
||||
self.tmpfile.seek(0)
|
||||
self.tmpfile.truncate()
|
||||
return res
|
||||
|
||||
def done(self):
|
||||
""" stop capturing, restore streams, return original capture file,
|
||||
@@ -402,12 +488,25 @@ class FDCapture:
|
||||
|
||||
def writeorg(self, data):
|
||||
""" write to original file descriptor. """
|
||||
if py.builtin._istext(data):
|
||||
if isinstance(data, six.text_type):
|
||||
data = data.encode("utf8") # XXX use encoding of original stream
|
||||
os.write(self.targetfd_save, data)
|
||||
|
||||
|
||||
class SysCapture:
|
||||
class FDCapture(FDCaptureBinary):
|
||||
"""Capture IO to/from a given os-level filedescriptor.
|
||||
|
||||
snap() produces text
|
||||
"""
|
||||
def snap(self):
|
||||
res = FDCaptureBinary.snap(self)
|
||||
enc = getattr(self.tmpfile, "encoding", None)
|
||||
if enc and isinstance(res, bytes):
|
||||
res = six.text_type(res, enc, "replace")
|
||||
return res
|
||||
|
||||
|
||||
class SysCapture(object):
|
||||
def __init__(self, fd, tmpfile=None):
|
||||
name = patchsysdict[fd]
|
||||
self._old = getattr(sys, name)
|
||||
@@ -423,10 +522,9 @@ class SysCapture:
|
||||
setattr(sys, self.name, self.tmpfile)
|
||||
|
||||
def snap(self):
|
||||
f = self.tmpfile
|
||||
res = f.getvalue()
|
||||
f.truncate(0)
|
||||
f.seek(0)
|
||||
res = self.tmpfile.getvalue()
|
||||
self.tmpfile.seek(0)
|
||||
self.tmpfile.truncate()
|
||||
return res
|
||||
|
||||
def done(self):
|
||||
@@ -445,7 +543,15 @@ class SysCapture:
|
||||
self._old.flush()
|
||||
|
||||
|
||||
class DontReadFromInput:
|
||||
class SysCaptureBinary(SysCapture):
|
||||
def snap(self):
|
||||
res = self.tmpfile.buffer.getvalue()
|
||||
self.tmpfile.seek(0)
|
||||
self.tmpfile.truncate()
|
||||
return res
|
||||
|
||||
|
||||
class DontReadFromInput(object):
|
||||
"""Temporary stub class. Ideally when stdin is accessed, the
|
||||
capturing should be turned off, with possibly all data captured
|
||||
so far sent to the screen. This should be configurable, though,
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
python version compatibility code
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import sys
|
||||
import inspect
|
||||
import types
|
||||
import re
|
||||
|
||||
import codecs
|
||||
import functools
|
||||
import inspect
|
||||
import re
|
||||
import sys
|
||||
|
||||
import py
|
||||
|
||||
import _pytest
|
||||
from _pytest.outcomes import TEST_OUTCOME
|
||||
|
||||
|
||||
try:
|
||||
import enum
|
||||
except ImportError: # pragma: no cover
|
||||
@@ -25,6 +25,12 @@ _PY3 = sys.version_info > (3, 0)
|
||||
_PY2 = not _PY3
|
||||
|
||||
|
||||
if _PY3:
|
||||
from inspect import signature, Parameter as Parameter
|
||||
else:
|
||||
from funcsigs import signature, Parameter as Parameter
|
||||
|
||||
|
||||
NoneType = type(None)
|
||||
NOTSET = object()
|
||||
|
||||
@@ -32,12 +38,10 @@ PY35 = sys.version_info[:2] >= (3, 5)
|
||||
PY36 = sys.version_info[:2] >= (3, 6)
|
||||
MODULE_NOT_FOUND_ERROR = 'ModuleNotFoundError' if PY36 else 'ImportError'
|
||||
|
||||
if hasattr(inspect, 'signature'):
|
||||
def _format_args(func):
|
||||
return str(inspect.signature(func))
|
||||
else:
|
||||
def _format_args(func):
|
||||
return inspect.formatargspec(*inspect.getargspec(func))
|
||||
|
||||
def _format_args(func):
|
||||
return str(signature(func))
|
||||
|
||||
|
||||
isfunction = inspect.isfunction
|
||||
isclass = inspect.isclass
|
||||
@@ -63,7 +67,6 @@ def iscoroutinefunction(func):
|
||||
|
||||
|
||||
def getlocation(function, curdir):
|
||||
import inspect
|
||||
fn = py.path.local(inspect.getfile(function))
|
||||
lineno = py.builtin._getcode(function).co_firstlineno
|
||||
if fn.relto(curdir):
|
||||
@@ -76,67 +79,72 @@ def num_mock_patch_args(function):
|
||||
patchings = getattr(function, "patchings", None)
|
||||
if not patchings:
|
||||
return 0
|
||||
mock = sys.modules.get("mock", sys.modules.get("unittest.mock", None))
|
||||
if mock is not None:
|
||||
mock_modules = [sys.modules.get("mock"), sys.modules.get("unittest.mock")]
|
||||
if any(mock_modules):
|
||||
sentinels = [m.DEFAULT for m in mock_modules if m is not None]
|
||||
return len([p for p in patchings
|
||||
if not p.attribute_name and p.new is mock.DEFAULT])
|
||||
if not p.attribute_name and p.new in sentinels])
|
||||
return len(patchings)
|
||||
|
||||
|
||||
def getfuncargnames(function, startindex=None, cls=None):
|
||||
"""
|
||||
@RonnyPfannschmidt: This function should be refactored when we revisit fixtures. The
|
||||
fixture mechanism should ask the node for the fixture names, and not try to obtain
|
||||
directly from the function object well after collection has occurred.
|
||||
"""
|
||||
if startindex is None and cls is not None:
|
||||
is_staticmethod = isinstance(cls.__dict__.get(function.__name__, None), staticmethod)
|
||||
startindex = 0 if is_staticmethod else 1
|
||||
# XXX merge with main.py's varnames
|
||||
# assert not isclass(function)
|
||||
realfunction = function
|
||||
while hasattr(realfunction, "__wrapped__"):
|
||||
realfunction = realfunction.__wrapped__
|
||||
if startindex is None:
|
||||
startindex = inspect.ismethod(function) and 1 or 0
|
||||
if realfunction != function:
|
||||
startindex += num_mock_patch_args(function)
|
||||
function = realfunction
|
||||
if isinstance(function, functools.partial):
|
||||
argnames = inspect.getargs(_pytest._code.getrawcode(function.func))[0]
|
||||
partial = function
|
||||
argnames = argnames[len(partial.args):]
|
||||
if partial.keywords:
|
||||
for kw in partial.keywords:
|
||||
argnames.remove(kw)
|
||||
else:
|
||||
argnames = inspect.getargs(_pytest._code.getrawcode(function))[0]
|
||||
defaults = getattr(function, 'func_defaults',
|
||||
getattr(function, '__defaults__', None)) or ()
|
||||
numdefaults = len(defaults)
|
||||
if numdefaults:
|
||||
return tuple(argnames[startindex:-numdefaults])
|
||||
return tuple(argnames[startindex:])
|
||||
def getfuncargnames(function, is_method=False, cls=None):
|
||||
"""Returns the names of a function's mandatory arguments.
|
||||
|
||||
This should return the names of all function arguments that:
|
||||
* Aren't bound to an instance or type as in instance or class methods.
|
||||
* Don't have default values.
|
||||
* Aren't bound with functools.partial.
|
||||
* Aren't replaced with mocks.
|
||||
|
||||
if sys.version_info[:2] == (2, 6):
|
||||
def isclass(object):
|
||||
""" Return true if the object is a class. Overrides inspect.isclass for
|
||||
python 2.6 because it will return True for objects which always return
|
||||
something on __getattr__ calls (see #1035).
|
||||
Backport of https://hg.python.org/cpython/rev/35bf8f7a8edc
|
||||
"""
|
||||
return isinstance(object, (type, types.ClassType))
|
||||
The is_method and cls arguments indicate that the function should
|
||||
be treated as a bound method even though it's not unless, only in
|
||||
the case of cls, the function is a static method.
|
||||
|
||||
@RonnyPfannschmidt: This function should be refactored when we
|
||||
revisit fixtures. The fixture mechanism should ask the node for
|
||||
the fixture names, and not try to obtain directly from the
|
||||
function object well after collection has occurred.
|
||||
|
||||
"""
|
||||
# The parameters attribute of a Signature object contains an
|
||||
# ordered mapping of parameter names to Parameter instances. This
|
||||
# creates a tuple of the names of the parameters that don't have
|
||||
# defaults.
|
||||
arg_names = tuple(p.name for p in signature(function).parameters.values()
|
||||
if (p.kind is Parameter.POSITIONAL_OR_KEYWORD or
|
||||
p.kind is Parameter.KEYWORD_ONLY) and
|
||||
p.default is Parameter.empty)
|
||||
# If this function should be treated as a bound method even though
|
||||
# it's passed as an unbound method or function, remove the first
|
||||
# parameter name.
|
||||
if (is_method or
|
||||
(cls and not isinstance(cls.__dict__.get(function.__name__, None),
|
||||
staticmethod))):
|
||||
arg_names = arg_names[1:]
|
||||
# Remove any names that will be replaced with mocks.
|
||||
if hasattr(function, "__wrapped__"):
|
||||
arg_names = arg_names[num_mock_patch_args(function):]
|
||||
return arg_names
|
||||
|
||||
|
||||
if _PY3:
|
||||
import codecs
|
||||
imap = map
|
||||
izip = zip
|
||||
STRING_TYPES = bytes, str
|
||||
UNICODE_TYPES = str,
|
||||
|
||||
def _ascii_escaped(val):
|
||||
if PY35:
|
||||
def _bytes_to_ascii(val):
|
||||
return val.decode('ascii', 'backslashreplace')
|
||||
else:
|
||||
def _bytes_to_ascii(val):
|
||||
if val:
|
||||
# source: http://goo.gl/bGsnwC
|
||||
encoded_bytes, _ = codecs.escape_encode(val)
|
||||
return encoded_bytes.decode('ascii')
|
||||
else:
|
||||
# empty bytes crashes codecs.escape_encode (#1087)
|
||||
return ''
|
||||
|
||||
def ascii_escaped(val):
|
||||
"""If val is pure ascii, returns it as a str(). Otherwise, escapes
|
||||
bytes objects into a sequence of escaped bytes:
|
||||
|
||||
@@ -155,22 +163,14 @@ if _PY3:
|
||||
|
||||
"""
|
||||
if isinstance(val, bytes):
|
||||
if val:
|
||||
# source: http://goo.gl/bGsnwC
|
||||
encoded_bytes, _ = codecs.escape_encode(val)
|
||||
return encoded_bytes.decode('ascii')
|
||||
else:
|
||||
# empty bytes crashes codecs.escape_encode (#1087)
|
||||
return ''
|
||||
return _bytes_to_ascii(val)
|
||||
else:
|
||||
return val.encode('unicode_escape').decode('ascii')
|
||||
else:
|
||||
STRING_TYPES = bytes, str, unicode
|
||||
UNICODE_TYPES = unicode,
|
||||
|
||||
from itertools import imap, izip # NOQA
|
||||
|
||||
def _ascii_escaped(val):
|
||||
def ascii_escaped(val):
|
||||
"""In py2 bytes and str are the same type, so return if it's a bytes
|
||||
object, return it unchanged if it is a full ascii string,
|
||||
otherwise escape it into its binary form.
|
||||
@@ -223,10 +223,7 @@ def getimfunc(func):
|
||||
try:
|
||||
return func.__func__
|
||||
except AttributeError:
|
||||
try:
|
||||
return func.im_func
|
||||
except AttributeError:
|
||||
return func
|
||||
return func
|
||||
|
||||
|
||||
def safe_getattr(object, name, default):
|
||||
|
||||
@@ -6,6 +6,7 @@ import traceback
|
||||
import types
|
||||
import warnings
|
||||
|
||||
import six
|
||||
import py
|
||||
# DON't import pytest here because it causes import cycle troubles
|
||||
import sys
|
||||
@@ -13,7 +14,7 @@ import os
|
||||
import _pytest._code
|
||||
import _pytest.hookspec # the extension point definitions
|
||||
import _pytest.assertion
|
||||
from _pytest._pluggy import PluginManager, HookimplMarker, HookspecMarker
|
||||
from pluggy import PluginManager, HookimplMarker, HookspecMarker
|
||||
from _pytest.compat import safe_str
|
||||
|
||||
hookimpl = HookimplMarker("pytest")
|
||||
@@ -59,12 +60,13 @@ def main(args=None, plugins=None):
|
||||
finally:
|
||||
config._ensure_unconfigure()
|
||||
except UsageError as e:
|
||||
tw = py.io.TerminalWriter(sys.stderr)
|
||||
for msg in e.args:
|
||||
sys.stderr.write("ERROR: %s\n" % (msg,))
|
||||
tw.line("ERROR: {}\n".format(msg), red=True)
|
||||
return 4
|
||||
|
||||
|
||||
class cmdline: # compatibility namespace
|
||||
class cmdline(object): # compatibility namespace
|
||||
main = staticmethod(main)
|
||||
|
||||
|
||||
@@ -100,27 +102,18 @@ def directory_arg(path, optname):
|
||||
return path
|
||||
|
||||
|
||||
_preinit = []
|
||||
|
||||
default_plugins = (
|
||||
"mark main terminal runner python fixtures debugging unittest capture skipping "
|
||||
"tmpdir monkeypatch recwarn pastebin helpconfig nose assertion "
|
||||
"junitxml resultlog doctest cacheprovider freeze_support "
|
||||
"setuponly setupplan warnings").split()
|
||||
"setuponly setupplan warnings logging").split()
|
||||
|
||||
|
||||
builtin_plugins = set(default_plugins)
|
||||
builtin_plugins.add("pytester")
|
||||
|
||||
|
||||
def _preloadplugins():
|
||||
assert not _preinit
|
||||
_preinit.append(get_config())
|
||||
|
||||
|
||||
def get_config():
|
||||
if _preinit:
|
||||
return _preinit.pop(0)
|
||||
# subsequent calls to main will create a fresh instance
|
||||
pluginmanager = PytestPluginManager()
|
||||
config = Config(pluginmanager)
|
||||
@@ -158,7 +151,7 @@ def _prepareconfig(args=None, plugins=None):
|
||||
try:
|
||||
if plugins:
|
||||
for plugin in plugins:
|
||||
if isinstance(plugin, py.builtin._basestring):
|
||||
if isinstance(plugin, six.string_types):
|
||||
pluginmanager.consider_pluginarg(plugin)
|
||||
else:
|
||||
pluginmanager.register(plugin)
|
||||
@@ -173,7 +166,7 @@ def _prepareconfig(args=None, plugins=None):
|
||||
|
||||
class PytestPluginManager(PluginManager):
|
||||
"""
|
||||
Overwrites :py:class:`pluggy.PluginManager <_pytest.vendored_packages.pluggy.PluginManager>` to add pytest-specific
|
||||
Overwrites :py:class:`pluggy.PluginManager <pluggy.PluginManager>` to add pytest-specific
|
||||
functionality:
|
||||
|
||||
* loading plugins from the command line, ``PYTEST_PLUGIN`` env variable and
|
||||
@@ -211,7 +204,7 @@ class PytestPluginManager(PluginManager):
|
||||
"""
|
||||
.. deprecated:: 2.8
|
||||
|
||||
Use :py:meth:`pluggy.PluginManager.add_hookspecs <_pytest.vendored_packages.pluggy.PluginManager.add_hookspecs>`
|
||||
Use :py:meth:`pluggy.PluginManager.add_hookspecs <PluginManager.add_hookspecs>`
|
||||
instead.
|
||||
"""
|
||||
warning = dict(code="I2",
|
||||
@@ -249,18 +242,12 @@ class PytestPluginManager(PluginManager):
|
||||
"historic": hasattr(method, "historic")}
|
||||
return opts
|
||||
|
||||
def _verify_hook(self, hook, hookmethod):
|
||||
super(PytestPluginManager, self)._verify_hook(hook, hookmethod)
|
||||
if "__multicall__" in hookmethod.argnames:
|
||||
fslineno = _pytest._code.getfslineno(hookmethod.function)
|
||||
warning = dict(code="I1",
|
||||
fslocation=fslineno,
|
||||
nodeid=None,
|
||||
message="%r hook uses deprecated __multicall__ "
|
||||
"argument" % (hook.name))
|
||||
self._warn(warning)
|
||||
|
||||
def register(self, plugin, name=None):
|
||||
if name in ['pytest_catchlog', 'pytest_capturelog']:
|
||||
self._warn('{0} plugin has been merged into the core, '
|
||||
'please remove it from your requirements.'.format(
|
||||
name.replace('_', '-')))
|
||||
return
|
||||
ret = super(PytestPluginManager, self).register(plugin, name)
|
||||
if ret:
|
||||
self.hook.pytest_plugin_registered.call_historic(
|
||||
@@ -430,9 +417,9 @@ class PytestPluginManager(PluginManager):
|
||||
# "terminal" or "capture". Those plugins are registered under their
|
||||
# basename for historic purposes but must be imported with the
|
||||
# _pytest prefix.
|
||||
assert isinstance(modname, (py.builtin.text, str)), "module name as text required, got %r" % modname
|
||||
assert isinstance(modname, (six.text_type, str)), "module name as text required, got %r" % modname
|
||||
modname = str(modname)
|
||||
if self.get_plugin(modname) is not None:
|
||||
if self.is_blocked(modname) or self.get_plugin(modname) is not None:
|
||||
return
|
||||
if modname in builtin_plugins:
|
||||
importspec = "_pytest." + modname
|
||||
@@ -442,12 +429,12 @@ class PytestPluginManager(PluginManager):
|
||||
try:
|
||||
__import__(importspec)
|
||||
except ImportError as e:
|
||||
new_exc = ImportError('Error importing plugin "%s": %s' % (modname, safe_str(e.args[0])))
|
||||
# copy over name and path attributes
|
||||
for attr in ('name', 'path'):
|
||||
if hasattr(e, attr):
|
||||
setattr(new_exc, attr, getattr(e, attr))
|
||||
raise new_exc
|
||||
new_exc_type = ImportError
|
||||
new_exc_message = 'Error importing plugin "%s": %s' % (modname, safe_str(e.args[0]))
|
||||
new_exc = new_exc_type(new_exc_message)
|
||||
|
||||
six.reraise(new_exc_type, new_exc, sys.exc_info()[2])
|
||||
|
||||
except Exception as e:
|
||||
import pytest
|
||||
if not hasattr(pytest, 'skip') or not isinstance(e, pytest.skip.Exception):
|
||||
@@ -476,7 +463,7 @@ def _get_plugin_specs_as_list(specs):
|
||||
return []
|
||||
|
||||
|
||||
class Parser:
|
||||
class Parser(object):
|
||||
""" Parser for command line arguments and ini-file values.
|
||||
|
||||
:ivar extra_info: dict of generic param -> value to display in case
|
||||
@@ -611,7 +598,7 @@ class ArgumentError(Exception):
|
||||
return self.msg
|
||||
|
||||
|
||||
class Argument:
|
||||
class Argument(object):
|
||||
"""class that mimics the necessary behaviour of optparse.Option
|
||||
|
||||
its currently a least effort implementation
|
||||
@@ -643,7 +630,7 @@ class Argument:
|
||||
pass
|
||||
else:
|
||||
# this might raise a keyerror as well, don't want to catch that
|
||||
if isinstance(typ, py.builtin._basestring):
|
||||
if isinstance(typ, six.string_types):
|
||||
if typ == 'choice':
|
||||
warnings.warn(
|
||||
'type argument to addoption() is a string %r.'
|
||||
@@ -741,7 +728,7 @@ class Argument:
|
||||
return 'Argument({0})'.format(', '.join(args))
|
||||
|
||||
|
||||
class OptionGroup:
|
||||
class OptionGroup(object):
|
||||
def __init__(self, name, description="", parser=None):
|
||||
self.name = name
|
||||
self.description = description
|
||||
@@ -872,7 +859,7 @@ class CmdOptions(object):
|
||||
return CmdOptions(self.__dict__)
|
||||
|
||||
|
||||
class Notset:
|
||||
class Notset(object):
|
||||
def __repr__(self):
|
||||
return "<NOTSET>"
|
||||
|
||||
@@ -968,7 +955,7 @@ class Config(object):
|
||||
)
|
||||
res = self.hook.pytest_internalerror(excrepr=excrepr,
|
||||
excinfo=excinfo)
|
||||
if not py.builtin.any(res):
|
||||
if not any(res):
|
||||
for line in str(excrepr).split("\n"):
|
||||
sys.stderr.write("INTERNALERROR> %s\n" % line)
|
||||
sys.stderr.flush()
|
||||
@@ -1014,10 +1001,10 @@ class Config(object):
|
||||
self._override_ini = ns.override_ini or ()
|
||||
|
||||
def _consider_importhook(self, args):
|
||||
"""Install the PEP 302 import hook if using assertion re-writing.
|
||||
"""Install the PEP 302 import hook if using assertion rewriting.
|
||||
|
||||
Needs to parse the --assert=<mode> option from the commandline
|
||||
and find all the installed plugins to mark them for re-writing
|
||||
and find all the installed plugins to mark them for rewriting
|
||||
by the importhook.
|
||||
"""
|
||||
ns, unknown_args = self._parser.parse_known_and_unknown_args(args)
|
||||
@@ -1074,9 +1061,10 @@ class Config(object):
|
||||
"(are you using python -O?)\n")
|
||||
|
||||
def _preparse(self, args, addopts=True):
|
||||
self._initini(args)
|
||||
if addopts:
|
||||
args[:] = shlex.split(os.environ.get('PYTEST_ADDOPTS', '')) + args
|
||||
self._initini(args)
|
||||
if addopts:
|
||||
args[:] = self.getini("addopts") + args
|
||||
self._checkversion()
|
||||
self._consider_importhook(args)
|
||||
@@ -1200,16 +1188,15 @@ class Config(object):
|
||||
|
||||
def _get_override_ini_value(self, name):
|
||||
value = None
|
||||
# override_ini is a list of list, to support both -o foo1=bar1 foo2=bar2 and
|
||||
# and -o foo1=bar1 -o foo2=bar2 options
|
||||
# always use the last item if multiple value set for same ini-name,
|
||||
# override_ini is a list of "ini=value" options
|
||||
# always use the last item if multiple values are set for same ini-name,
|
||||
# e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2
|
||||
for ini_config_list in self._override_ini:
|
||||
for ini_config in ini_config_list:
|
||||
try:
|
||||
(key, user_ini_value) = ini_config.split("=", 1)
|
||||
except ValueError:
|
||||
raise UsageError("-o/--override-ini expects option=value style.")
|
||||
for ini_config in self._override_ini:
|
||||
try:
|
||||
key, user_ini_value = ini_config.split("=", 1)
|
||||
except ValueError:
|
||||
raise UsageError("-o/--override-ini expects option=value style.")
|
||||
else:
|
||||
if key == name:
|
||||
value = user_ini_value
|
||||
return value
|
||||
@@ -1340,10 +1327,14 @@ def determine_setup(inifile, args, warnfunc=None):
|
||||
dirs = get_dirs_from_args(args)
|
||||
if inifile:
|
||||
iniconfig = py.iniconfig.IniConfig(inifile)
|
||||
try:
|
||||
inicfg = iniconfig["pytest"]
|
||||
except KeyError:
|
||||
inicfg = None
|
||||
is_cfg_file = str(inifile).endswith('.cfg')
|
||||
sections = ['tool:pytest', 'pytest'] if is_cfg_file else ['pytest']
|
||||
for section in sections:
|
||||
try:
|
||||
inicfg = iniconfig[section]
|
||||
break
|
||||
except KeyError:
|
||||
inicfg = None
|
||||
rootdir = get_common_ancestor(dirs)
|
||||
else:
|
||||
ancestor = get_common_ancestor(dirs)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import pdb
|
||||
import sys
|
||||
from doctest import UnexpectedException
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
@@ -40,7 +41,7 @@ def pytest_configure(config):
|
||||
config._cleanup.append(fin)
|
||||
|
||||
|
||||
class pytestPDB:
|
||||
class pytestPDB(object):
|
||||
""" Pseudo PDB that defers to the real pdb. """
|
||||
_pluginmanager = None
|
||||
_config = None
|
||||
@@ -54,7 +55,7 @@ class pytestPDB:
|
||||
if cls._pluginmanager is not None:
|
||||
capman = cls._pluginmanager.getplugin("capturemanager")
|
||||
if capman:
|
||||
capman.suspendcapture(in_=True)
|
||||
capman.suspend_global_capture(in_=True)
|
||||
tw = _pytest.config.create_terminal_writer(cls._config)
|
||||
tw.line()
|
||||
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
|
||||
@@ -62,11 +63,11 @@ class pytestPDB:
|
||||
cls._pdb_cls().set_trace(frame)
|
||||
|
||||
|
||||
class PdbInvoke:
|
||||
class PdbInvoke(object):
|
||||
def pytest_exception_interact(self, node, call, report):
|
||||
capman = node.config.pluginmanager.getplugin("capturemanager")
|
||||
if capman:
|
||||
out, err = capman.suspendcapture(in_=True)
|
||||
out, err = capman.suspend_global_capture(in_=True)
|
||||
sys.stdout.write(out)
|
||||
sys.stdout.write(err)
|
||||
_enter_pdb(node, call.excinfo, report)
|
||||
@@ -85,6 +86,17 @@ def _enter_pdb(node, excinfo, rep):
|
||||
# for not completely clear reasons.
|
||||
tw = node.config.pluginmanager.getplugin("terminalreporter")._tw
|
||||
tw.line()
|
||||
|
||||
captured_stdout = rep.capstdout
|
||||
if len(captured_stdout) > 0:
|
||||
tw.sep(">", "captured stdout")
|
||||
tw.line(captured_stdout)
|
||||
|
||||
captured_stderr = rep.capstderr
|
||||
if len(captured_stderr) > 0:
|
||||
tw.sep(">", "captured stderr")
|
||||
tw.line(captured_stderr)
|
||||
|
||||
tw.sep(">", "traceback")
|
||||
rep.toterminal(tw)
|
||||
tw.sep(">", "entering PDB")
|
||||
@@ -95,10 +107,9 @@ def _enter_pdb(node, excinfo, rep):
|
||||
|
||||
|
||||
def _postmortem_traceback(excinfo):
|
||||
# A doctest.UnexpectedException is not useful for post_mortem.
|
||||
# Use the underlying exception instead:
|
||||
from doctest import UnexpectedException
|
||||
if isinstance(excinfo.value, UnexpectedException):
|
||||
# A doctest.UnexpectedException is not useful for post_mortem.
|
||||
# Use the underlying exception instead:
|
||||
return excinfo.value.exc_info[2]
|
||||
else:
|
||||
return excinfo._excinfo[2]
|
||||
|
||||
@@ -40,3 +40,13 @@ MARK_PARAMETERSET_UNPACKING = RemovedInPytest4Warning(
|
||||
" please use pytest.param(..., marks=...) instead.\n"
|
||||
"For more details, see: https://docs.pytest.org/en/latest/parametrize.html"
|
||||
)
|
||||
|
||||
COLLECTOR_MAKEITEM = RemovedInPytest4Warning(
|
||||
"pycollector makeitem was removed "
|
||||
"as it is an accidentially leaked internal api"
|
||||
)
|
||||
|
||||
METAFUNC_ADD_CALL = (
|
||||
"Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0.\n"
|
||||
"Please use Metafunc.parametrize instead."
|
||||
)
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import traceback
|
||||
import sys
|
||||
import platform
|
||||
|
||||
import pytest
|
||||
from _pytest._code.code import ExceptionInfo, ReprFileLocation, TerminalRepr
|
||||
@@ -50,12 +52,19 @@ def pytest_addoption(parser):
|
||||
def pytest_collect_file(path, parent):
|
||||
config = parent.config
|
||||
if path.ext == ".py":
|
||||
if config.option.doctestmodules:
|
||||
if config.option.doctestmodules and not _is_setup_py(config, path, parent):
|
||||
return DoctestModule(path, parent)
|
||||
elif _is_doctest(config, path, parent):
|
||||
return DoctestTextfile(path, parent)
|
||||
|
||||
|
||||
def _is_setup_py(config, path, parent):
|
||||
if path.basename != "setup.py":
|
||||
return False
|
||||
contents = path.read()
|
||||
return 'setuptools' in contents or 'distutils' in contents
|
||||
|
||||
|
||||
def _is_doctest(config, path, parent):
|
||||
if path.ext in ('.txt', '.rst') and parent.session.isinitpath(path):
|
||||
return True
|
||||
@@ -96,8 +105,21 @@ class DoctestItem(pytest.Item):
|
||||
|
||||
def runtest(self):
|
||||
_check_all_skipped(self.dtest)
|
||||
self._disable_output_capturing_for_darwin()
|
||||
self.runner.run(self.dtest)
|
||||
|
||||
def _disable_output_capturing_for_darwin(self):
|
||||
"""
|
||||
Disable output capturing. Otherwise, stdout is lost to doctest (#985)
|
||||
"""
|
||||
if platform.system() != 'Darwin':
|
||||
return
|
||||
capman = self.config.pluginmanager.getplugin("capturemanager")
|
||||
if capman:
|
||||
out, err = capman.suspend_global_capture(in_=True)
|
||||
sys.stdout.write(out)
|
||||
sys.stderr.write(err)
|
||||
|
||||
def repr_failure(self, excinfo):
|
||||
import doctest
|
||||
if excinfo.errisinstance((doctest.DocTestFailure,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import sys
|
||||
import warnings
|
||||
from collections import OrderedDict, deque, defaultdict
|
||||
|
||||
import attr
|
||||
import py
|
||||
from py._code.code import FormattedExcinfo
|
||||
|
||||
@@ -21,18 +24,21 @@ from _pytest.compat import (
|
||||
from _pytest.outcomes import fail, TEST_OUTCOME
|
||||
|
||||
|
||||
if sys.version_info[:2] == (2, 6):
|
||||
from ordereddict import OrderedDict
|
||||
else:
|
||||
from collections import OrderedDict
|
||||
@attr.s(frozen=True)
|
||||
class PseudoFixtureDef(object):
|
||||
cached_result = attr.ib()
|
||||
scope = attr.ib()
|
||||
|
||||
|
||||
def pytest_sessionstart(session):
|
||||
import _pytest.python
|
||||
import _pytest.nodes
|
||||
|
||||
scopename2class.update({
|
||||
'class': _pytest.python.Class,
|
||||
'module': _pytest.python.Module,
|
||||
'function': _pytest.main.Item,
|
||||
'function': _pytest.nodes.Item,
|
||||
'session': _pytest.main.Session,
|
||||
})
|
||||
session._fixturemanager = FixtureManager(session)
|
||||
|
||||
@@ -64,8 +70,6 @@ def scopeproperty(name=None, doc=None):
|
||||
def get_scope_node(node, scope):
|
||||
cls = scopename2class.get(scope)
|
||||
if cls is None:
|
||||
if scope == "session":
|
||||
return node.session
|
||||
raise ValueError("unknown scope")
|
||||
return node.getparent(cls)
|
||||
|
||||
@@ -165,62 +169,59 @@ def get_parametrized_fixture_keys(item, scopenum):
|
||||
|
||||
def reorder_items(items):
|
||||
argkeys_cache = {}
|
||||
items_by_argkey = {}
|
||||
for scopenum in range(0, scopenum_function):
|
||||
argkeys_cache[scopenum] = d = {}
|
||||
items_by_argkey[scopenum] = item_d = defaultdict(deque)
|
||||
for item in items:
|
||||
keys = OrderedDict.fromkeys(get_parametrized_fixture_keys(item, scopenum))
|
||||
if keys:
|
||||
d[item] = keys
|
||||
return reorder_items_atscope(items, set(), argkeys_cache, 0)
|
||||
for key in keys:
|
||||
item_d[key].append(item)
|
||||
items = OrderedDict.fromkeys(items)
|
||||
return list(reorder_items_atscope(items, argkeys_cache, items_by_argkey, 0))
|
||||
|
||||
|
||||
def reorder_items_atscope(items, ignore, argkeys_cache, scopenum):
|
||||
def fix_cache_order(item, argkeys_cache, items_by_argkey):
|
||||
for scopenum in range(0, scopenum_function):
|
||||
for key in argkeys_cache[scopenum].get(item, []):
|
||||
items_by_argkey[scopenum][key].appendleft(item)
|
||||
|
||||
|
||||
def reorder_items_atscope(items, argkeys_cache, items_by_argkey, scopenum):
|
||||
if scopenum >= scopenum_function or len(items) < 3:
|
||||
return items
|
||||
items_done = []
|
||||
while 1:
|
||||
items_before, items_same, items_other, newignore = \
|
||||
slice_items(items, ignore, argkeys_cache[scopenum])
|
||||
items_before = reorder_items_atscope(
|
||||
items_before, ignore, argkeys_cache, scopenum + 1)
|
||||
if items_same is None:
|
||||
# nothing to reorder in this scope
|
||||
assert items_other is None
|
||||
return items_done + items_before
|
||||
items_done.extend(items_before)
|
||||
items = items_same + items_other
|
||||
ignore = newignore
|
||||
|
||||
|
||||
def slice_items(items, ignore, scoped_argkeys_cache):
|
||||
# we pick the first item which uses a fixture instance in the
|
||||
# requested scope and which we haven't seen yet. We slice the input
|
||||
# items list into a list of items_nomatch, items_same and
|
||||
# items_other
|
||||
if scoped_argkeys_cache: # do we need to do work at all?
|
||||
it = iter(items)
|
||||
# first find a slicing key
|
||||
for i, item in enumerate(it):
|
||||
argkeys = scoped_argkeys_cache.get(item)
|
||||
if argkeys is not None:
|
||||
newargkeys = OrderedDict.fromkeys(k for k in argkeys if k not in ignore)
|
||||
if newargkeys: # found a slicing key
|
||||
slicing_argkey, _ = newargkeys.popitem()
|
||||
items_before = items[:i]
|
||||
items_same = [item]
|
||||
items_other = []
|
||||
# now slice the remainder of the list
|
||||
for item in it:
|
||||
argkeys = scoped_argkeys_cache.get(item)
|
||||
if argkeys and slicing_argkey in argkeys and \
|
||||
slicing_argkey not in ignore:
|
||||
items_same.append(item)
|
||||
else:
|
||||
items_other.append(item)
|
||||
newignore = ignore.copy()
|
||||
newignore.add(slicing_argkey)
|
||||
return (items_before, items_same, items_other, newignore)
|
||||
return items, None, None, None
|
||||
ignore = set()
|
||||
items_deque = deque(items)
|
||||
items_done = OrderedDict()
|
||||
scoped_items_by_argkey = items_by_argkey[scopenum]
|
||||
scoped_argkeys_cache = argkeys_cache[scopenum]
|
||||
while items_deque:
|
||||
no_argkey_group = OrderedDict()
|
||||
slicing_argkey = None
|
||||
while items_deque:
|
||||
item = items_deque.popleft()
|
||||
if item in items_done or item in no_argkey_group:
|
||||
continue
|
||||
argkeys = OrderedDict.fromkeys(k for k in scoped_argkeys_cache.get(item, []) if k not in ignore)
|
||||
if not argkeys:
|
||||
no_argkey_group[item] = None
|
||||
else:
|
||||
slicing_argkey, _ = argkeys.popitem()
|
||||
# we don't have to remove relevant items from later in the deque because they'll just be ignored
|
||||
matching_items = [i for i in scoped_items_by_argkey[slicing_argkey] if i in items]
|
||||
for i in reversed(matching_items):
|
||||
fix_cache_order(i, argkeys_cache, items_by_argkey)
|
||||
items_deque.appendleft(i)
|
||||
break
|
||||
if no_argkey_group:
|
||||
no_argkey_group = reorder_items_atscope(
|
||||
no_argkey_group, argkeys_cache, items_by_argkey, scopenum + 1)
|
||||
for item in no_argkey_group:
|
||||
items_done[item] = None
|
||||
ignore.add(slicing_argkey)
|
||||
return items_done
|
||||
|
||||
|
||||
def fillfixtures(function):
|
||||
@@ -249,7 +250,7 @@ def get_direct_param_fixture_func(request):
|
||||
return request.param
|
||||
|
||||
|
||||
class FuncFixtureInfo:
|
||||
class FuncFixtureInfo(object):
|
||||
def __init__(self, argnames, names_closure, name2fixturedefs):
|
||||
self.argnames = argnames
|
||||
self.names_closure = names_closure
|
||||
@@ -270,7 +271,6 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
self.fixturename = None
|
||||
#: Scope string, one of "function", "class", "module", "session"
|
||||
self.scope = "function"
|
||||
self._fixture_values = {} # argname -> fixture value
|
||||
self._fixture_defs = {} # argname -> FixtureDef
|
||||
fixtureinfo = pyfuncitem._fixtureinfo
|
||||
self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy()
|
||||
@@ -446,15 +446,13 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
fixturedef = self._getnextfixturedef(argname)
|
||||
except FixtureLookupError:
|
||||
if argname == "request":
|
||||
class PseudoFixtureDef:
|
||||
cached_result = (self, [0], None)
|
||||
scope = "function"
|
||||
return PseudoFixtureDef
|
||||
cached_result = (self, [0], None)
|
||||
scope = "function"
|
||||
return PseudoFixtureDef(cached_result, scope)
|
||||
raise
|
||||
# remove indent to prevent the python3 exception
|
||||
# from leaking into the call
|
||||
result = self._getfixturevalue(fixturedef)
|
||||
self._fixture_values[argname] = result
|
||||
self._compute_fixture_value(fixturedef)
|
||||
self._fixture_defs[argname] = fixturedef
|
||||
return fixturedef
|
||||
|
||||
@@ -469,7 +467,14 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
values.append(fixturedef)
|
||||
current = current._parent_request
|
||||
|
||||
def _getfixturevalue(self, fixturedef):
|
||||
def _compute_fixture_value(self, fixturedef):
|
||||
"""
|
||||
Creates a SubRequest based on "self" and calls the execute method of the given fixturedef object. This will
|
||||
force the FixtureDef object to throw away any previous results and compute a new fixture value, which
|
||||
will be stored into the FixtureDef object itself.
|
||||
|
||||
:param FixtureDef fixturedef:
|
||||
"""
|
||||
# prepare a subrequest object before calling fixture function
|
||||
# (latter managed by fixturedef)
|
||||
argname = fixturedef.argname
|
||||
@@ -518,12 +523,11 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
exc_clear()
|
||||
try:
|
||||
# call the fixture function
|
||||
val = fixturedef.execute(request=subrequest)
|
||||
fixturedef.execute(request=subrequest)
|
||||
finally:
|
||||
# if fixture function failed it might have registered finalizers
|
||||
self.session._setupstate.addfinalizer(fixturedef.finish,
|
||||
self.session._setupstate.addfinalizer(functools.partial(fixturedef.finish, request=subrequest),
|
||||
subrequest.node)
|
||||
return val
|
||||
|
||||
def _check_scope(self, argname, invoking_scope, requested_scope):
|
||||
if argname == "request":
|
||||
@@ -556,7 +560,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
if node is None and scope == "class":
|
||||
# fallback to function item itself
|
||||
node = self._pyfuncitem
|
||||
assert node
|
||||
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(scope, self._pyfuncitem)
|
||||
return node
|
||||
|
||||
def __repr__(self):
|
||||
@@ -576,7 +580,6 @@ class SubRequest(FixtureRequest):
|
||||
self.scope = scope
|
||||
self._fixturedef = fixturedef
|
||||
self._pyfuncitem = request._pyfuncitem
|
||||
self._fixture_values = request._fixture_values
|
||||
self._fixture_defs = request._fixture_defs
|
||||
self._arg2fixturedefs = request._arg2fixturedefs
|
||||
self._arg2index = request._arg2index
|
||||
@@ -718,7 +721,7 @@ def call_fixture_func(fixturefunc, request, kwargs):
|
||||
return res
|
||||
|
||||
|
||||
class FixtureDef:
|
||||
class FixtureDef(object):
|
||||
""" A container for a factory definition. """
|
||||
|
||||
def __init__(self, fixturemanager, baseid, argname, func, scope, params,
|
||||
@@ -735,21 +738,20 @@ class FixtureDef:
|
||||
where=baseid
|
||||
)
|
||||
self.params = params
|
||||
startindex = unittest and 1 or None
|
||||
self.argnames = getfuncargnames(func, startindex=startindex)
|
||||
self.argnames = getfuncargnames(func, is_method=unittest)
|
||||
self.unittest = unittest
|
||||
self.ids = ids
|
||||
self._finalizer = []
|
||||
self._finalizers = []
|
||||
|
||||
def addfinalizer(self, finalizer):
|
||||
self._finalizer.append(finalizer)
|
||||
self._finalizers.append(finalizer)
|
||||
|
||||
def finish(self):
|
||||
def finish(self, request):
|
||||
exceptions = []
|
||||
try:
|
||||
while self._finalizer:
|
||||
while self._finalizers:
|
||||
try:
|
||||
func = self._finalizer.pop()
|
||||
func = self._finalizers.pop()
|
||||
func()
|
||||
except: # noqa
|
||||
exceptions.append(sys.exc_info())
|
||||
@@ -759,12 +761,15 @@ class FixtureDef:
|
||||
py.builtin._reraise(*e)
|
||||
|
||||
finally:
|
||||
ihook = self._fixturemanager.session.ihook
|
||||
ihook.pytest_fixture_post_finalizer(fixturedef=self)
|
||||
hook = self._fixturemanager.session.gethookproxy(request.node.fspath)
|
||||
hook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
|
||||
# even if finalization fails, we invalidate
|
||||
# the cached fixture value
|
||||
# the cached fixture value and remove
|
||||
# all finalizers because they may be bound methods which will
|
||||
# keep instances alive
|
||||
if hasattr(self, "cached_result"):
|
||||
del self.cached_result
|
||||
self._finalizers = []
|
||||
|
||||
def execute(self, request):
|
||||
# get required arguments and register our own finish()
|
||||
@@ -772,7 +777,7 @@ class FixtureDef:
|
||||
for argname in self.argnames:
|
||||
fixturedef = request._get_active_fixturedef(argname)
|
||||
if argname != "request":
|
||||
fixturedef.addfinalizer(self.finish)
|
||||
fixturedef.addfinalizer(functools.partial(self.finish, request=request))
|
||||
|
||||
my_cache_key = request.param_index
|
||||
cached_result = getattr(self, "cached_result", None)
|
||||
@@ -785,11 +790,11 @@ class FixtureDef:
|
||||
return result
|
||||
# we have a previous but differently parametrized fixture instance
|
||||
# so we need to tear it down before creating a new one
|
||||
self.finish()
|
||||
self.finish(request)
|
||||
assert not hasattr(self, "cached_result")
|
||||
|
||||
ihook = self._fixturemanager.session.ihook
|
||||
return ihook.pytest_fixture_setup(fixturedef=self, request=request)
|
||||
hook = self._fixturemanager.session.gethookproxy(request.node.fspath)
|
||||
return hook.pytest_fixture_setup(fixturedef=self, request=request)
|
||||
|
||||
def __repr__(self):
|
||||
return ("<FixtureDef name=%r scope=%r baseid=%r >" %
|
||||
@@ -828,13 +833,21 @@ def pytest_fixture_setup(fixturedef, request):
|
||||
return result
|
||||
|
||||
|
||||
class FixtureFunctionMarker:
|
||||
def __init__(self, scope, params, autouse=False, ids=None, name=None):
|
||||
self.scope = scope
|
||||
self.params = params
|
||||
self.autouse = autouse
|
||||
self.ids = ids
|
||||
self.name = name
|
||||
def _ensure_immutable_ids(ids):
|
||||
if ids is None:
|
||||
return
|
||||
if callable(ids):
|
||||
return ids
|
||||
return tuple(ids)
|
||||
|
||||
|
||||
@attr.s(frozen=True)
|
||||
class FixtureFunctionMarker(object):
|
||||
scope = attr.ib()
|
||||
params = attr.ib(convert=attr.converters.optional(tuple))
|
||||
autouse = attr.ib(default=False)
|
||||
ids = attr.ib(default=None, convert=_ensure_immutable_ids)
|
||||
name = attr.ib(default=None)
|
||||
|
||||
def __call__(self, function):
|
||||
if isclass(function):
|
||||
@@ -914,7 +927,7 @@ def pytestconfig(request):
|
||||
return request.config
|
||||
|
||||
|
||||
class FixtureManager:
|
||||
class FixtureManager(object):
|
||||
"""
|
||||
pytest fixtures definitions and information is stored and managed
|
||||
from this class.
|
||||
|
||||
@@ -57,9 +57,9 @@ def pytest_addoption(parser):
|
||||
action="store_true", dest="debug", default=False,
|
||||
help="store internal tracing debug information in 'pytestdebug.log'.")
|
||||
group._addoption(
|
||||
'-o', '--override-ini', nargs='*', dest="override_ini",
|
||||
'-o', '--override-ini', dest="override_ini",
|
||||
action="append",
|
||||
help="override config option with option=value style, e.g. `-o xfail_strict=True`.")
|
||||
help='override ini option with "option=value" style, e.g. `-o xfail_strict=True -o cache_dir=cache`.')
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
""" hook specifications for pytest plugins, invoked from main.py and builtin plugins. """
|
||||
|
||||
from _pytest._pluggy import HookspecMarker
|
||||
from pluggy import HookspecMarker
|
||||
|
||||
hookspec = HookspecMarker("pytest")
|
||||
|
||||
@@ -12,22 +12,40 @@ hookspec = HookspecMarker("pytest")
|
||||
@hookspec(historic=True)
|
||||
def pytest_addhooks(pluginmanager):
|
||||
"""called at plugin registration time to allow adding new hooks via a call to
|
||||
pluginmanager.add_hookspecs(module_or_class, prefix)."""
|
||||
``pluginmanager.add_hookspecs(module_or_class, prefix)``.
|
||||
|
||||
|
||||
:param _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager
|
||||
|
||||
.. note::
|
||||
This hook is incompatible with ``hookwrapper=True``.
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(historic=True)
|
||||
def pytest_namespace():
|
||||
"""
|
||||
DEPRECATED: this hook causes direct monkeypatching on pytest, its use is strongly discouraged
|
||||
(**Deprecated**) this hook causes direct monkeypatching on pytest, its use is strongly discouraged
|
||||
return dict of name->object to be made globally available in
|
||||
the pytest namespace. This hook is called at plugin registration
|
||||
time.
|
||||
the pytest namespace.
|
||||
|
||||
This hook is called at plugin registration time.
|
||||
|
||||
.. note::
|
||||
This hook is incompatible with ``hookwrapper=True``.
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(historic=True)
|
||||
def pytest_plugin_registered(plugin, manager):
|
||||
""" a new pytest plugin got registered. """
|
||||
""" a new pytest plugin got registered.
|
||||
|
||||
:param plugin: the plugin module or instance
|
||||
:param _pytest.config.PytestPluginManager manager: pytest plugin manager
|
||||
|
||||
.. note::
|
||||
This hook is incompatible with ``hookwrapper=True``.
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(historic=True)
|
||||
@@ -41,7 +59,7 @@ def pytest_addoption(parser):
|
||||
files situated at the tests root directory due to how pytest
|
||||
:ref:`discovers plugins during startup <pluginorder>`.
|
||||
|
||||
:arg parser: To add command line options, call
|
||||
:arg _pytest.config.Parser parser: To add command line options, call
|
||||
:py:func:`parser.addoption(...) <_pytest.config.Parser.addoption>`.
|
||||
To add ini-file values call :py:func:`parser.addini(...)
|
||||
<_pytest.config.Parser.addini>`.
|
||||
@@ -56,8 +74,10 @@ def pytest_addoption(parser):
|
||||
a value read from an ini-style file.
|
||||
|
||||
The config object is passed around on many internal objects via the ``.config``
|
||||
attribute or can be retrieved as the ``pytestconfig`` fixture or accessed
|
||||
via (deprecated) ``pytest.config``.
|
||||
attribute or can be retrieved as the ``pytestconfig`` fixture.
|
||||
|
||||
.. note::
|
||||
This hook is incompatible with ``hookwrapper=True``.
|
||||
"""
|
||||
|
||||
|
||||
@@ -72,14 +92,15 @@ def pytest_configure(config):
|
||||
After that, the hook is called for other conftest files as they are
|
||||
imported.
|
||||
|
||||
:arg config: pytest config object
|
||||
:type config: _pytest.config.Config
|
||||
.. note::
|
||||
This hook is incompatible with ``hookwrapper=True``.
|
||||
|
||||
:arg _pytest.config.Config config: pytest config object
|
||||
"""
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Bootstrapping hooks called for plugins registered early enough:
|
||||
# internal and 3rd party plugins as well as directly
|
||||
# discoverable conftest.py local plugins.
|
||||
# internal and 3rd party plugins.
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -87,11 +108,28 @@ def pytest_configure(config):
|
||||
def pytest_cmdline_parse(pluginmanager, args):
|
||||
"""return initialized config object, parsing the specified args.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult` """
|
||||
Stops at first non-None result, see :ref:`firstresult`
|
||||
|
||||
.. note::
|
||||
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
|
||||
|
||||
:param _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager
|
||||
:param list[str] args: list of arguments passed on the command line
|
||||
"""
|
||||
|
||||
|
||||
def pytest_cmdline_preparse(config, args):
|
||||
"""(deprecated) modify command line arguments before option parsing. """
|
||||
"""(**Deprecated**) modify command line arguments before option parsing.
|
||||
|
||||
This hook is considered deprecated and will be removed in a future pytest version. Consider
|
||||
using :func:`pytest_load_initial_conftests` instead.
|
||||
|
||||
.. note::
|
||||
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
|
||||
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
:param list[str] args: list of arguments passed on the command line
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
@@ -99,12 +137,26 @@ def pytest_cmdline_main(config):
|
||||
""" called for performing the main command line action. The default
|
||||
implementation will invoke the configure hooks and runtest_mainloop.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult` """
|
||||
.. note::
|
||||
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult`
|
||||
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
"""
|
||||
|
||||
|
||||
def pytest_load_initial_conftests(early_config, parser, args):
|
||||
""" implements the loading of initial conftest files ahead
|
||||
of command line option parsing. """
|
||||
of command line option parsing.
|
||||
|
||||
.. note::
|
||||
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
|
||||
|
||||
:param _pytest.config.Config early_config: pytest config object
|
||||
:param list[str] args: list of arguments passed on the command line
|
||||
:param _pytest.config.Parser parser: to add command line options
|
||||
"""
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -113,18 +165,29 @@ def pytest_load_initial_conftests(early_config, parser, args):
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_collection(session):
|
||||
""" perform the collection protocol for the given session.
|
||||
"""Perform the collection protocol for the given session.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult` """
|
||||
Stops at first non-None result, see :ref:`firstresult`.
|
||||
|
||||
:param _pytest.main.Session session: the pytest session object
|
||||
"""
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(session, config, items):
|
||||
""" called after collection has been performed, may filter or re-order
|
||||
the items in-place."""
|
||||
the items in-place.
|
||||
|
||||
:param _pytest.main.Session session: the pytest session object
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
:param List[_pytest.nodes.Item] items: list of item objects
|
||||
"""
|
||||
|
||||
|
||||
def pytest_collection_finish(session):
|
||||
""" called after collection has been performed and modified. """
|
||||
""" called after collection has been performed and modified.
|
||||
|
||||
:param _pytest.main.Session session: the pytest session object
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
@@ -134,6 +197,9 @@ def pytest_ignore_collect(path, config):
|
||||
more specific hooks.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult`
|
||||
|
||||
:param str path: the path to analyze
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
"""
|
||||
|
||||
|
||||
@@ -141,12 +207,18 @@ def pytest_ignore_collect(path, config):
|
||||
def pytest_collect_directory(path, parent):
|
||||
""" called before traversing a directory for collection files.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult` """
|
||||
Stops at first non-None result, see :ref:`firstresult`
|
||||
|
||||
:param str path: the path to analyze
|
||||
"""
|
||||
|
||||
|
||||
def pytest_collect_file(path, parent):
|
||||
""" return collection Node or None for the given path. Any new node
|
||||
needs to have the specified ``parent`` as a parent."""
|
||||
needs to have the specified ``parent`` as a parent.
|
||||
|
||||
:param str path: the path to collect
|
||||
"""
|
||||
|
||||
# logging hooks for collection
|
||||
|
||||
@@ -212,7 +284,12 @@ def pytest_make_parametrize_id(config, val, argname):
|
||||
by @pytest.mark.parametrize calls. Return None if the hook doesn't know about ``val``.
|
||||
The parameter name is available as ``argname``, if required.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult` """
|
||||
Stops at first non-None result, see :ref:`firstresult`
|
||||
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
:param val: the parametrized value
|
||||
:param str argname: the automatic parameter name produced by pytest
|
||||
"""
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# generic runtest related hooks
|
||||
@@ -224,11 +301,14 @@ def pytest_runtestloop(session):
|
||||
""" called for performing the main runtest loop
|
||||
(after collection finished).
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult` """
|
||||
Stops at first non-None result, see :ref:`firstresult`
|
||||
|
||||
:param _pytest.main.Session session: the pytest session object
|
||||
"""
|
||||
|
||||
|
||||
def pytest_itemstart(item, node):
|
||||
""" (deprecated, use pytest_runtest_logstart). """
|
||||
"""(**Deprecated**) use pytest_runtest_logstart. """
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
@@ -250,7 +330,25 @@ def pytest_runtest_protocol(item, nextitem):
|
||||
|
||||
|
||||
def pytest_runtest_logstart(nodeid, location):
|
||||
""" signal the start of running a single test item. """
|
||||
""" signal the start of running a single test item.
|
||||
|
||||
This hook will be called **before** :func:`pytest_runtest_setup`, :func:`pytest_runtest_call` and
|
||||
:func:`pytest_runtest_teardown` hooks.
|
||||
|
||||
:param str nodeid: full id of the item
|
||||
:param location: a triple of ``(filename, linenum, testname)``
|
||||
"""
|
||||
|
||||
|
||||
def pytest_runtest_logfinish(nodeid, location):
|
||||
""" signal the complete finish of running a single test item.
|
||||
|
||||
This hook will be called **after** :func:`pytest_runtest_setup`, :func:`pytest_runtest_call` and
|
||||
:func:`pytest_runtest_teardown` hooks.
|
||||
|
||||
:param str nodeid: full id of the item
|
||||
:param location: a triple of ``(filename, linenum, testname)``
|
||||
"""
|
||||
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
@@ -293,10 +391,18 @@ def pytest_runtest_logreport(report):
|
||||
def pytest_fixture_setup(fixturedef, request):
|
||||
""" performs fixture setup execution.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult` """
|
||||
:return: The return value of the call to the fixture function
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult`
|
||||
|
||||
.. note::
|
||||
If the fixture function returns None, other implementations of
|
||||
this hook function will continue to be called, according to the
|
||||
behavior of the :ref:`firstresult` option.
|
||||
"""
|
||||
|
||||
|
||||
def pytest_fixture_post_finalizer(fixturedef):
|
||||
def pytest_fixture_post_finalizer(fixturedef, request):
|
||||
""" called after fixture teardown, but before the cache is cleared so
|
||||
the fixture result cache ``fixturedef.cached_result`` can
|
||||
still be accessed."""
|
||||
@@ -307,15 +413,25 @@ def pytest_fixture_post_finalizer(fixturedef):
|
||||
|
||||
|
||||
def pytest_sessionstart(session):
|
||||
""" before session.main() is called. """
|
||||
""" before session.main() is called.
|
||||
|
||||
:param _pytest.main.Session session: the pytest session object
|
||||
"""
|
||||
|
||||
|
||||
def pytest_sessionfinish(session, exitstatus):
|
||||
""" whole test run finishes. """
|
||||
""" whole test run finishes.
|
||||
|
||||
:param _pytest.main.Session session: the pytest session object
|
||||
:param int exitstatus: the status which pytest will return to the system
|
||||
"""
|
||||
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
""" called before test process is exited. """
|
||||
""" called before test process is exited.
|
||||
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
"""
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -329,6 +445,8 @@ def pytest_assertrepr_compare(config, op, left, right):
|
||||
of strings. The strings will be joined by newlines but any newlines
|
||||
*in* a string will be escaped. Note that all but the first line will
|
||||
be indented slightly, the intention is for the first line to be a summary.
|
||||
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
"""
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -339,7 +457,7 @@ def pytest_assertrepr_compare(config, op, left, right):
|
||||
def pytest_report_header(config, startdir):
|
||||
""" return a string or list of strings to be displayed as header info for terminal reporting.
|
||||
|
||||
:param config: the pytest config object.
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
:param startdir: py.path object with the starting dir
|
||||
|
||||
.. note::
|
||||
@@ -358,7 +476,7 @@ def pytest_report_collectionfinish(config, startdir, items):
|
||||
|
||||
This strings will be displayed after the standard "collected X items" message.
|
||||
|
||||
:param config: the pytest config object.
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
:param startdir: py.path object with the starting dir
|
||||
:param items: list of pytest items that are going to be executed; this list should not be modified.
|
||||
"""
|
||||
@@ -379,7 +497,11 @@ def pytest_terminal_summary(terminalreporter, exitstatus):
|
||||
def pytest_logwarning(message, code, nodeid, fslocation):
|
||||
""" process a warning specified by a message, a code string,
|
||||
a nodeid and fslocation (both of which may be None
|
||||
if the warning is not tied to a partilar node/location)."""
|
||||
if the warning is not tied to a particular node/location).
|
||||
|
||||
.. note::
|
||||
This hook is incompatible with ``hookwrapper=True``.
|
||||
"""
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# doctest hooks
|
||||
@@ -418,6 +540,5 @@ def pytest_enter_pdb(config):
|
||||
""" called upon pdb.set_trace(), can be used by plugins to take special
|
||||
action just before the python debugger enters in interactive mode.
|
||||
|
||||
:arg config: pytest config object
|
||||
:type config: _pytest.config.Config
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
"""
|
||||
|
||||
@@ -85,6 +85,9 @@ class _NodeReporter(object):
|
||||
def add_property(self, name, value):
|
||||
self.properties.append((str(name), bin_xml_escape(value)))
|
||||
|
||||
def add_attribute(self, name, value):
|
||||
self.attrs[str(name)] = bin_xml_escape(value)
|
||||
|
||||
def make_properties_node(self):
|
||||
"""Return a Junit node containing custom properties, if any.
|
||||
"""
|
||||
@@ -98,6 +101,7 @@ class _NodeReporter(object):
|
||||
def record_testreport(self, testreport):
|
||||
assert not self.testcase
|
||||
names = mangle_test_address(testreport.nodeid)
|
||||
existing_attrs = self.attrs
|
||||
classnames = names[:-1]
|
||||
if self.xml.prefix:
|
||||
classnames.insert(0, self.xml.prefix)
|
||||
@@ -111,6 +115,7 @@ class _NodeReporter(object):
|
||||
if hasattr(testreport, "url"):
|
||||
attrs["url"] = testreport.url
|
||||
self.attrs = attrs
|
||||
self.attrs.update(existing_attrs) # restore any user-defined attributes
|
||||
|
||||
def to_xml(self):
|
||||
testcase = Junit.testcase(time=self.duration, **self.attrs)
|
||||
@@ -211,6 +216,27 @@ def record_xml_property(request):
|
||||
return add_property_noop
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def record_xml_attribute(request):
|
||||
"""Add extra xml attributes to the tag for the calling test.
|
||||
The fixture is callable with ``(name, value)``, with value being automatically
|
||||
xml-encoded
|
||||
"""
|
||||
request.node.warn(
|
||||
code='C3',
|
||||
message='record_xml_attribute is an experimental feature',
|
||||
)
|
||||
xml = getattr(request.config, "_xml", None)
|
||||
if xml is not None:
|
||||
node_reporter = xml.node_reporter(request.node.nodeid)
|
||||
return node_reporter.add_attribute
|
||||
else:
|
||||
def add_attr_noop(name, value):
|
||||
pass
|
||||
|
||||
return add_attr_noop
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("terminal reporting")
|
||||
group.addoption(
|
||||
|
||||
487
_pytest/logging.py
Normal file
487
_pytest/logging.py
Normal file
@@ -0,0 +1,487 @@
|
||||
""" Access and control log capturing. """
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import logging
|
||||
from contextlib import closing, contextmanager
|
||||
import re
|
||||
import six
|
||||
|
||||
from _pytest.config import create_terminal_writer
|
||||
import pytest
|
||||
import py
|
||||
|
||||
|
||||
DEFAULT_LOG_FORMAT = '%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s'
|
||||
DEFAULT_LOG_DATE_FORMAT = '%H:%M:%S'
|
||||
|
||||
|
||||
class ColoredLevelFormatter(logging.Formatter):
|
||||
"""
|
||||
Colorize the %(levelname)..s part of the log format passed to __init__.
|
||||
"""
|
||||
|
||||
LOGLEVEL_COLOROPTS = {
|
||||
logging.CRITICAL: {'red'},
|
||||
logging.ERROR: {'red', 'bold'},
|
||||
logging.WARNING: {'yellow'},
|
||||
logging.WARN: {'yellow'},
|
||||
logging.INFO: {'green'},
|
||||
logging.DEBUG: {'purple'},
|
||||
logging.NOTSET: set(),
|
||||
}
|
||||
LEVELNAME_FMT_REGEX = re.compile(r'%\(levelname\)([+-]?\d*s)')
|
||||
|
||||
def __init__(self, terminalwriter, *args, **kwargs):
|
||||
super(ColoredLevelFormatter, self).__init__(
|
||||
*args, **kwargs)
|
||||
if six.PY2:
|
||||
self._original_fmt = self._fmt
|
||||
else:
|
||||
self._original_fmt = self._style._fmt
|
||||
self._level_to_fmt_mapping = {}
|
||||
|
||||
levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt)
|
||||
if not levelname_fmt_match:
|
||||
return
|
||||
levelname_fmt = levelname_fmt_match.group()
|
||||
|
||||
for level, color_opts in self.LOGLEVEL_COLOROPTS.items():
|
||||
formatted_levelname = levelname_fmt % {
|
||||
'levelname': logging.getLevelName(level)}
|
||||
|
||||
# add ANSI escape sequences around the formatted levelname
|
||||
color_kwargs = {name: True for name in color_opts}
|
||||
colorized_formatted_levelname = terminalwriter.markup(
|
||||
formatted_levelname, **color_kwargs)
|
||||
self._level_to_fmt_mapping[level] = self.LEVELNAME_FMT_REGEX.sub(
|
||||
colorized_formatted_levelname,
|
||||
self._fmt)
|
||||
|
||||
def format(self, record):
|
||||
fmt = self._level_to_fmt_mapping.get(
|
||||
record.levelno, self._original_fmt)
|
||||
if six.PY2:
|
||||
self._fmt = fmt
|
||||
else:
|
||||
self._style._fmt = fmt
|
||||
return super(ColoredLevelFormatter, self).format(record)
|
||||
|
||||
|
||||
def get_option_ini(config, *names):
|
||||
for name in names:
|
||||
ret = config.getoption(name) # 'default' arg won't work as expected
|
||||
if ret is None:
|
||||
ret = config.getini(name)
|
||||
if ret:
|
||||
return ret
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
"""Add options to control log capturing."""
|
||||
group = parser.getgroup('logging')
|
||||
|
||||
def add_option_ini(option, dest, default=None, type=None, **kwargs):
|
||||
parser.addini(dest, default=default, type=type,
|
||||
help='default value for ' + option)
|
||||
group.addoption(option, dest=dest, **kwargs)
|
||||
|
||||
add_option_ini(
|
||||
'--no-print-logs',
|
||||
dest='log_print', action='store_const', const=False, default=True,
|
||||
type='bool',
|
||||
help='disable printing caught logs on failed tests.')
|
||||
add_option_ini(
|
||||
'--log-level',
|
||||
dest='log_level', default=None,
|
||||
help='logging level used by the logging module')
|
||||
add_option_ini(
|
||||
'--log-format',
|
||||
dest='log_format', default=DEFAULT_LOG_FORMAT,
|
||||
help='log format as used by the logging module.')
|
||||
add_option_ini(
|
||||
'--log-date-format',
|
||||
dest='log_date_format', default=DEFAULT_LOG_DATE_FORMAT,
|
||||
help='log date format as used by the logging module.')
|
||||
parser.addini(
|
||||
'log_cli', default=False, type='bool',
|
||||
help='enable log display during test run (also known as "live logging").')
|
||||
add_option_ini(
|
||||
'--log-cli-level',
|
||||
dest='log_cli_level', default=None,
|
||||
help='cli logging level.')
|
||||
add_option_ini(
|
||||
'--log-cli-format',
|
||||
dest='log_cli_format', default=None,
|
||||
help='log format as used by the logging module.')
|
||||
add_option_ini(
|
||||
'--log-cli-date-format',
|
||||
dest='log_cli_date_format', default=None,
|
||||
help='log date format as used by the logging module.')
|
||||
add_option_ini(
|
||||
'--log-file',
|
||||
dest='log_file', default=None,
|
||||
help='path to a file when logging will be written to.')
|
||||
add_option_ini(
|
||||
'--log-file-level',
|
||||
dest='log_file_level', default=None,
|
||||
help='log file logging level.')
|
||||
add_option_ini(
|
||||
'--log-file-format',
|
||||
dest='log_file_format', default=DEFAULT_LOG_FORMAT,
|
||||
help='log format as used by the logging module.')
|
||||
add_option_ini(
|
||||
'--log-file-date-format',
|
||||
dest='log_file_date_format', default=DEFAULT_LOG_DATE_FORMAT,
|
||||
help='log date format as used by the logging module.')
|
||||
|
||||
|
||||
@contextmanager
|
||||
def catching_logs(handler, formatter=None, level=None):
|
||||
"""Context manager that prepares the whole logging machinery properly."""
|
||||
root_logger = logging.getLogger()
|
||||
|
||||
if formatter is not None:
|
||||
handler.setFormatter(formatter)
|
||||
if level is not None:
|
||||
handler.setLevel(level)
|
||||
|
||||
# Adding the same handler twice would confuse logging system.
|
||||
# Just don't do that.
|
||||
add_new_handler = handler not in root_logger.handlers
|
||||
|
||||
if add_new_handler:
|
||||
root_logger.addHandler(handler)
|
||||
if level is not None:
|
||||
orig_level = root_logger.level
|
||||
root_logger.setLevel(level)
|
||||
try:
|
||||
yield handler
|
||||
finally:
|
||||
if level is not None:
|
||||
root_logger.setLevel(orig_level)
|
||||
if add_new_handler:
|
||||
root_logger.removeHandler(handler)
|
||||
|
||||
|
||||
class LogCaptureHandler(logging.StreamHandler):
|
||||
"""A logging handler that stores log records and the log text."""
|
||||
|
||||
def __init__(self):
|
||||
"""Creates a new log handler."""
|
||||
logging.StreamHandler.__init__(self, py.io.TextIO())
|
||||
self.records = []
|
||||
|
||||
def emit(self, record):
|
||||
"""Keep the log records in a list in addition to the log text."""
|
||||
self.records.append(record)
|
||||
logging.StreamHandler.emit(self, record)
|
||||
|
||||
|
||||
class LogCaptureFixture(object):
|
||||
"""Provides access and control of log capturing."""
|
||||
|
||||
def __init__(self, item):
|
||||
"""Creates a new funcarg."""
|
||||
self._item = item
|
||||
self._initial_log_levels = {} # type: Dict[str, int] # dict of log name -> log level
|
||||
|
||||
def _finalize(self):
|
||||
"""Finalizes the fixture.
|
||||
|
||||
This restores the log levels changed by :meth:`set_level`.
|
||||
"""
|
||||
# restore log levels
|
||||
for logger_name, level in self._initial_log_levels.items():
|
||||
logger = logging.getLogger(logger_name)
|
||||
logger.setLevel(level)
|
||||
|
||||
@property
|
||||
def handler(self):
|
||||
return self._item.catch_log_handler
|
||||
|
||||
def get_records(self, when):
|
||||
"""
|
||||
Get the logging records for one of the possible test phases.
|
||||
|
||||
:param str when:
|
||||
Which test phase to obtain the records from. Valid values are: "setup", "call" and "teardown".
|
||||
|
||||
:rtype: List[logging.LogRecord]
|
||||
:return: the list of captured records at the given stage
|
||||
|
||||
.. versionadded:: 3.4
|
||||
"""
|
||||
handler = self._item.catch_log_handlers.get(when)
|
||||
if handler:
|
||||
return handler.records
|
||||
else:
|
||||
return []
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
"""Returns the log text."""
|
||||
return self.handler.stream.getvalue()
|
||||
|
||||
@property
|
||||
def records(self):
|
||||
"""Returns the list of log records."""
|
||||
return self.handler.records
|
||||
|
||||
@property
|
||||
def record_tuples(self):
|
||||
"""Returns a list of a striped down version of log records intended
|
||||
for use in assertion comparison.
|
||||
|
||||
The format of the tuple is:
|
||||
|
||||
(logger_name, log_level, message)
|
||||
"""
|
||||
return [(r.name, r.levelno, r.getMessage()) for r in self.records]
|
||||
|
||||
def clear(self):
|
||||
"""Reset the list of log records."""
|
||||
self.handler.records = []
|
||||
|
||||
def set_level(self, level, logger=None):
|
||||
"""Sets the level for capturing of logs. The level will be restored to its previous value at the end of
|
||||
the test.
|
||||
|
||||
:param int level: the logger to level.
|
||||
:param str logger: the logger to update the level. If not given, the root logger level is updated.
|
||||
|
||||
.. versionchanged:: 3.4
|
||||
The levels of the loggers changed by this function will be restored to their initial values at the
|
||||
end of the test.
|
||||
"""
|
||||
logger_name = logger
|
||||
logger = logging.getLogger(logger_name)
|
||||
# save the original log-level to restore it during teardown
|
||||
self._initial_log_levels.setdefault(logger_name, logger.level)
|
||||
logger.setLevel(level)
|
||||
|
||||
@contextmanager
|
||||
def at_level(self, level, logger=None):
|
||||
"""Context manager that sets the level for capturing of logs. After the end of the 'with' statement the
|
||||
level is restored to its original value.
|
||||
|
||||
:param int level: the logger to level.
|
||||
:param str logger: the logger to update the level. If not given, the root logger level is updated.
|
||||
"""
|
||||
logger = logging.getLogger(logger)
|
||||
orig_level = logger.level
|
||||
logger.setLevel(level)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
logger.setLevel(orig_level)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def caplog(request):
|
||||
"""Access and control log capturing.
|
||||
|
||||
Captured logs are available through the following methods::
|
||||
|
||||
* caplog.text() -> string containing formatted log output
|
||||
* caplog.records() -> list of logging.LogRecord instances
|
||||
* caplog.record_tuples() -> list of (logger_name, level, message) tuples
|
||||
"""
|
||||
result = LogCaptureFixture(request.node)
|
||||
yield result
|
||||
result._finalize()
|
||||
|
||||
|
||||
def get_actual_log_level(config, *setting_names):
|
||||
"""Return the actual logging level."""
|
||||
|
||||
for setting_name in setting_names:
|
||||
log_level = config.getoption(setting_name)
|
||||
if log_level is None:
|
||||
log_level = config.getini(setting_name)
|
||||
if log_level:
|
||||
break
|
||||
else:
|
||||
return
|
||||
|
||||
if isinstance(log_level, six.string_types):
|
||||
log_level = log_level.upper()
|
||||
try:
|
||||
return int(getattr(logging, log_level, log_level))
|
||||
except ValueError:
|
||||
# Python logging does not recognise this as a logging level
|
||||
raise pytest.UsageError(
|
||||
"'{0}' is not recognized as a logging level name for "
|
||||
"'{1}'. Please consider passing the "
|
||||
"logging level num instead.".format(
|
||||
log_level,
|
||||
setting_name))
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
config.pluginmanager.register(LoggingPlugin(config), 'logging-plugin')
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _dummy_context_manager():
|
||||
yield
|
||||
|
||||
|
||||
class LoggingPlugin(object):
|
||||
"""Attaches to the logging module and captures log messages for each test.
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Creates a new plugin to capture log messages.
|
||||
|
||||
The formatter can be safely shared across all handlers so
|
||||
create a single one for the entire test session here.
|
||||
"""
|
||||
self._config = config
|
||||
|
||||
# enable verbose output automatically if live logging is enabled
|
||||
if self._config.getini('log_cli') and not config.getoption('verbose'):
|
||||
# sanity check: terminal reporter should not have been loaded at this point
|
||||
assert self._config.pluginmanager.get_plugin('terminalreporter') is None
|
||||
config.option.verbose = 1
|
||||
|
||||
self.print_logs = get_option_ini(config, 'log_print')
|
||||
self.formatter = logging.Formatter(get_option_ini(config, 'log_format'),
|
||||
get_option_ini(config, 'log_date_format'))
|
||||
self.log_level = get_actual_log_level(config, 'log_level')
|
||||
|
||||
log_file = get_option_ini(config, 'log_file')
|
||||
if log_file:
|
||||
self.log_file_level = get_actual_log_level(config, 'log_file_level')
|
||||
|
||||
log_file_format = get_option_ini(config, 'log_file_format', 'log_format')
|
||||
log_file_date_format = get_option_ini(config, 'log_file_date_format', 'log_date_format')
|
||||
# Each pytest runtests session will write to a clean logfile
|
||||
self.log_file_handler = logging.FileHandler(log_file, mode='w')
|
||||
log_file_formatter = logging.Formatter(log_file_format, datefmt=log_file_date_format)
|
||||
self.log_file_handler.setFormatter(log_file_formatter)
|
||||
else:
|
||||
self.log_file_handler = None
|
||||
|
||||
# initialized during pytest_runtestloop
|
||||
self.log_cli_handler = None
|
||||
|
||||
@contextmanager
|
||||
def _runtest_for(self, item, when):
|
||||
"""Implements the internals of pytest_runtest_xxx() hook."""
|
||||
with catching_logs(LogCaptureHandler(),
|
||||
formatter=self.formatter, level=self.log_level) as log_handler:
|
||||
if self.log_cli_handler:
|
||||
self.log_cli_handler.set_when(when)
|
||||
if not hasattr(item, 'catch_log_handlers'):
|
||||
item.catch_log_handlers = {}
|
||||
item.catch_log_handlers[when] = log_handler
|
||||
item.catch_log_handler = log_handler
|
||||
try:
|
||||
yield # run test
|
||||
finally:
|
||||
del item.catch_log_handler
|
||||
if when == 'teardown':
|
||||
del item.catch_log_handlers
|
||||
|
||||
if self.print_logs:
|
||||
# Add a captured log section to the report.
|
||||
log = log_handler.stream.getvalue().strip()
|
||||
item.add_report_section(when, 'log', log)
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_setup(self, item):
|
||||
with self._runtest_for(item, 'setup'):
|
||||
yield
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_call(self, item):
|
||||
with self._runtest_for(item, 'call'):
|
||||
yield
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_teardown(self, item):
|
||||
with self._runtest_for(item, 'teardown'):
|
||||
yield
|
||||
|
||||
def pytest_runtest_logstart(self):
|
||||
if self.log_cli_handler:
|
||||
self.log_cli_handler.reset()
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtestloop(self, session):
|
||||
"""Runs all collected test items."""
|
||||
self._setup_cli_logging()
|
||||
with self.live_logs_context:
|
||||
if self.log_file_handler is not None:
|
||||
with closing(self.log_file_handler):
|
||||
with catching_logs(self.log_file_handler,
|
||||
level=self.log_file_level):
|
||||
yield # run all the tests
|
||||
else:
|
||||
yield # run all the tests
|
||||
|
||||
def _setup_cli_logging(self):
|
||||
"""Sets up the handler and logger for the Live Logs feature, if enabled.
|
||||
|
||||
This must be done right before starting the loop so we can access the terminal reporter plugin.
|
||||
"""
|
||||
terminal_reporter = self._config.pluginmanager.get_plugin('terminalreporter')
|
||||
if self._config.getini('log_cli') and terminal_reporter is not None:
|
||||
capture_manager = self._config.pluginmanager.get_plugin('capturemanager')
|
||||
log_cli_handler = _LiveLoggingStreamHandler(terminal_reporter, capture_manager)
|
||||
log_cli_format = get_option_ini(self._config, 'log_cli_format', 'log_format')
|
||||
log_cli_date_format = get_option_ini(self._config, 'log_cli_date_format', 'log_date_format')
|
||||
if self._config.option.color != 'no' and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(log_cli_format):
|
||||
log_cli_formatter = ColoredLevelFormatter(create_terminal_writer(self._config),
|
||||
log_cli_format, datefmt=log_cli_date_format)
|
||||
else:
|
||||
log_cli_formatter = logging.Formatter(log_cli_format, datefmt=log_cli_date_format)
|
||||
log_cli_level = get_actual_log_level(self._config, 'log_cli_level', 'log_level')
|
||||
self.log_cli_handler = log_cli_handler
|
||||
self.live_logs_context = catching_logs(log_cli_handler, formatter=log_cli_formatter, level=log_cli_level)
|
||||
else:
|
||||
self.live_logs_context = _dummy_context_manager()
|
||||
|
||||
|
||||
class _LiveLoggingStreamHandler(logging.StreamHandler):
|
||||
"""
|
||||
Custom StreamHandler used by the live logging feature: it will write a newline before the first log message
|
||||
in each test.
|
||||
|
||||
During live logging we must also explicitly disable stdout/stderr capturing otherwise it will get captured
|
||||
and won't appear in the terminal.
|
||||
"""
|
||||
|
||||
def __init__(self, terminal_reporter, capture_manager):
|
||||
"""
|
||||
:param _pytest.terminal.TerminalReporter terminal_reporter:
|
||||
:param _pytest.capture.CaptureManager capture_manager:
|
||||
"""
|
||||
logging.StreamHandler.__init__(self, stream=terminal_reporter)
|
||||
self.capture_manager = capture_manager
|
||||
self.reset()
|
||||
self.set_when(None)
|
||||
|
||||
def reset(self):
|
||||
"""Reset the handler; should be called before the start of each test"""
|
||||
self._first_record_emitted = False
|
||||
|
||||
def set_when(self, when):
|
||||
"""Prepares for the given test phase (setup/call/teardown)"""
|
||||
self._when = when
|
||||
self._section_name_shown = False
|
||||
|
||||
def emit(self, record):
|
||||
if self.capture_manager is not None:
|
||||
self.capture_manager.suspend_global_capture()
|
||||
try:
|
||||
if not self._first_record_emitted or self._when == 'teardown':
|
||||
self.stream.write('\n')
|
||||
self._first_record_emitted = True
|
||||
if not self._section_name_shown and self._when:
|
||||
self.stream.section('live log ' + self._when, sep='-', bold=True)
|
||||
self._section_name_shown = True
|
||||
logging.StreamHandler.emit(self, record)
|
||||
finally:
|
||||
if self.capture_manager is not None:
|
||||
self.capture_manager.resume_global_capture()
|
||||
466
_pytest/main.py
466
_pytest/main.py
@@ -1,24 +1,22 @@
|
||||
""" core implementation of testing process: init, session, runtest loop. """
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import contextlib
|
||||
import functools
|
||||
import os
|
||||
import pkgutil
|
||||
import six
|
||||
import sys
|
||||
|
||||
import _pytest
|
||||
from _pytest import nodes
|
||||
import _pytest._code
|
||||
import py
|
||||
try:
|
||||
from collections import MutableMapping as MappingMixin
|
||||
except ImportError:
|
||||
from UserDict import DictMixin as MappingMixin
|
||||
|
||||
from _pytest.config import directory_arg, UsageError, hookimpl
|
||||
from _pytest.outcomes import exit
|
||||
from _pytest.runner import collect_one_node
|
||||
|
||||
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
|
||||
|
||||
# exitcodes for the command line
|
||||
EXIT_OK = 0
|
||||
@@ -84,15 +82,6 @@ def pytest_addoption(parser):
|
||||
help="base temporary directory for this test run.")
|
||||
|
||||
|
||||
def pytest_namespace():
|
||||
"""keeping this one works around a deeper startup issue in pytest
|
||||
|
||||
i tried to find it for a while but the amount of time turned unsustainable,
|
||||
so i put a hack in to revisit later
|
||||
"""
|
||||
return {}
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
__import__('pytest').config = config # compatibiltiy
|
||||
|
||||
@@ -111,6 +100,8 @@ def wrap_session(config, doit):
|
||||
session.exitstatus = doit(config, session) or 0
|
||||
except UsageError:
|
||||
raise
|
||||
except Failed:
|
||||
session.exitstatus = EXIT_TESTSFAILED
|
||||
except KeyboardInterrupt:
|
||||
excinfo = _pytest._code.ExceptionInfo()
|
||||
if initstate < 2 and isinstance(excinfo.value, exit.Exception):
|
||||
@@ -168,6 +159,8 @@ def pytest_runtestloop(session):
|
||||
for i, item in enumerate(session.items):
|
||||
nextitem = session.items[i + 1] if i + 1 < len(session.items) else None
|
||||
item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
|
||||
if session.shouldfail:
|
||||
raise session.Failed(session.shouldfail)
|
||||
if session.shouldstop:
|
||||
raise session.Interrupted(session.shouldstop)
|
||||
return True
|
||||
@@ -177,7 +170,7 @@ def _in_venv(path):
|
||||
"""Attempts to detect if ``path`` is the root of a Virtual Environment by
|
||||
checking for the existence of the appropriate activate script"""
|
||||
bindir = path.join('Scripts' if sys.platform.startswith('win') else 'bin')
|
||||
if not bindir.exists():
|
||||
if not bindir.isdir():
|
||||
return False
|
||||
activates = ('activate', 'activate.csh', 'activate.fish',
|
||||
'Activate', 'Activate.bat', 'Activate.ps1')
|
||||
@@ -210,7 +203,47 @@ def pytest_ignore_collect(path, config):
|
||||
return False
|
||||
|
||||
|
||||
class FSHookProxy:
|
||||
@contextlib.contextmanager
|
||||
def _patched_find_module():
|
||||
"""Patch bug in pkgutil.ImpImporter.find_module
|
||||
|
||||
When using pkgutil.find_loader on python<3.4 it removes symlinks
|
||||
from the path due to a call to os.path.realpath. This is not consistent
|
||||
with actually doing the import (in these versions, pkgutil and __import__
|
||||
did not share the same underlying code). This can break conftest
|
||||
discovery for pytest where symlinks are involved.
|
||||
|
||||
The only supported python<3.4 by pytest is python 2.7.
|
||||
"""
|
||||
if six.PY2: # python 3.4+ uses importlib instead
|
||||
def find_module_patched(self, fullname, path=None):
|
||||
# Note: we ignore 'path' argument since it is only used via meta_path
|
||||
subname = fullname.split(".")[-1]
|
||||
if subname != fullname and self.path is None:
|
||||
return None
|
||||
if self.path is None:
|
||||
path = None
|
||||
else:
|
||||
# original: path = [os.path.realpath(self.path)]
|
||||
path = [self.path]
|
||||
try:
|
||||
file, filename, etc = pkgutil.imp.find_module(subname,
|
||||
path)
|
||||
except ImportError:
|
||||
return None
|
||||
return pkgutil.ImpLoader(fullname, file, filename, etc)
|
||||
|
||||
old_find_module = pkgutil.ImpImporter.find_module
|
||||
pkgutil.ImpImporter.find_module = find_module_patched
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
pkgutil.ImpImporter.find_module = old_find_module
|
||||
else:
|
||||
yield
|
||||
|
||||
|
||||
class FSHookProxy(object):
|
||||
def __init__(self, fspath, pm, remove_mods):
|
||||
self.fspath = fspath
|
||||
self.pm = pm
|
||||
@@ -222,374 +255,6 @@ class FSHookProxy:
|
||||
return x
|
||||
|
||||
|
||||
class _CompatProperty(object):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __get__(self, obj, owner):
|
||||
if obj is None:
|
||||
return self
|
||||
|
||||
# TODO: reenable in the features branch
|
||||
# warnings.warn(
|
||||
# "usage of {owner!r}.{name} is deprecated, please use pytest.{name} instead".format(
|
||||
# name=self.name, owner=type(owner).__name__),
|
||||
# PendingDeprecationWarning, stacklevel=2)
|
||||
return getattr(__import__('pytest'), self.name)
|
||||
|
||||
|
||||
class NodeKeywords(MappingMixin):
|
||||
def __init__(self, node):
|
||||
self.node = node
|
||||
self.parent = node.parent
|
||||
self._markers = {node.name: True}
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return self._markers[key]
|
||||
except KeyError:
|
||||
if self.parent is None:
|
||||
raise
|
||||
return self.parent.keywords[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._markers[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
raise ValueError("cannot delete key in keywords dict")
|
||||
|
||||
def __iter__(self):
|
||||
seen = set(self._markers)
|
||||
if self.parent is not None:
|
||||
seen.update(self.parent.keywords)
|
||||
return iter(seen)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.__iter__())
|
||||
|
||||
def keys(self):
|
||||
return list(self)
|
||||
|
||||
def __repr__(self):
|
||||
return "<NodeKeywords for node %s>" % (self.node, )
|
||||
|
||||
|
||||
class Node(object):
|
||||
""" base class for Collector and Item the test collection tree.
|
||||
Collector subclasses have children, Items are terminal nodes."""
|
||||
|
||||
def __init__(self, name, parent=None, config=None, session=None):
|
||||
#: a unique name within the scope of the parent node
|
||||
self.name = name
|
||||
|
||||
#: the parent collector node.
|
||||
self.parent = parent
|
||||
|
||||
#: the pytest config object
|
||||
self.config = config or parent.config
|
||||
|
||||
#: the session this node is part of
|
||||
self.session = session or parent.session
|
||||
|
||||
#: filesystem path where this node was collected from (can be None)
|
||||
self.fspath = getattr(parent, 'fspath', None)
|
||||
|
||||
#: keywords/markers collected from all scopes
|
||||
self.keywords = NodeKeywords(self)
|
||||
|
||||
#: allow adding of extra keywords to use for matching
|
||||
self.extra_keyword_matches = set()
|
||||
|
||||
# used for storing artificial fixturedefs for direct parametrization
|
||||
self._name2pseudofixturedef = {}
|
||||
|
||||
@property
|
||||
def ihook(self):
|
||||
""" fspath sensitive hook proxy used to call pytest hooks"""
|
||||
return self.session.gethookproxy(self.fspath)
|
||||
|
||||
Module = _CompatProperty("Module")
|
||||
Class = _CompatProperty("Class")
|
||||
Instance = _CompatProperty("Instance")
|
||||
Function = _CompatProperty("Function")
|
||||
File = _CompatProperty("File")
|
||||
Item = _CompatProperty("Item")
|
||||
|
||||
def _getcustomclass(self, name):
|
||||
maybe_compatprop = getattr(type(self), name)
|
||||
if isinstance(maybe_compatprop, _CompatProperty):
|
||||
return getattr(__import__('pytest'), name)
|
||||
else:
|
||||
cls = getattr(self, name)
|
||||
# TODO: reenable in the features branch
|
||||
# warnings.warn("use of node.%s is deprecated, "
|
||||
# "use pytest_pycollect_makeitem(...) to create custom "
|
||||
# "collection nodes" % name, category=DeprecationWarning)
|
||||
return cls
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s %r>" % (self.__class__.__name__,
|
||||
getattr(self, 'name', None))
|
||||
|
||||
def warn(self, code, message):
|
||||
""" generate a warning with the given code and message for this
|
||||
item. """
|
||||
assert isinstance(code, str)
|
||||
fslocation = getattr(self, "location", None)
|
||||
if fslocation is None:
|
||||
fslocation = getattr(self, "fspath", None)
|
||||
self.ihook.pytest_logwarning.call_historic(kwargs=dict(
|
||||
code=code, message=message,
|
||||
nodeid=self.nodeid, fslocation=fslocation))
|
||||
|
||||
# methods for ordering nodes
|
||||
@property
|
||||
def nodeid(self):
|
||||
""" a ::-separated string denoting its collection tree address. """
|
||||
try:
|
||||
return self._nodeid
|
||||
except AttributeError:
|
||||
self._nodeid = x = self._makeid()
|
||||
return x
|
||||
|
||||
def _makeid(self):
|
||||
return self.parent.nodeid + "::" + self.name
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.nodeid)
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
|
||||
def teardown(self):
|
||||
pass
|
||||
|
||||
def _memoizedcall(self, attrname, function):
|
||||
exattrname = "_ex_" + attrname
|
||||
failure = getattr(self, exattrname, None)
|
||||
if failure is not None:
|
||||
py.builtin._reraise(failure[0], failure[1], failure[2])
|
||||
if hasattr(self, attrname):
|
||||
return getattr(self, attrname)
|
||||
try:
|
||||
res = function()
|
||||
except py.builtin._sysex:
|
||||
raise
|
||||
except: # noqa
|
||||
failure = sys.exc_info()
|
||||
setattr(self, exattrname, failure)
|
||||
raise
|
||||
setattr(self, attrname, res)
|
||||
return res
|
||||
|
||||
def listchain(self):
|
||||
""" return list of all parent collectors up to self,
|
||||
starting from root of collection tree. """
|
||||
chain = []
|
||||
item = self
|
||||
while item is not None:
|
||||
chain.append(item)
|
||||
item = item.parent
|
||||
chain.reverse()
|
||||
return chain
|
||||
|
||||
def add_marker(self, marker):
|
||||
""" dynamically add a marker object to the node.
|
||||
|
||||
``marker`` can be a string or pytest.mark.* instance.
|
||||
"""
|
||||
from _pytest.mark import MarkDecorator, MARK_GEN
|
||||
if isinstance(marker, py.builtin._basestring):
|
||||
marker = getattr(MARK_GEN, marker)
|
||||
elif not isinstance(marker, MarkDecorator):
|
||||
raise ValueError("is not a string or pytest.mark.* Marker")
|
||||
self.keywords[marker.name] = marker
|
||||
|
||||
def get_marker(self, name):
|
||||
""" get a marker object from this node or None if
|
||||
the node doesn't have a marker with that name. """
|
||||
val = self.keywords.get(name, None)
|
||||
if val is not None:
|
||||
from _pytest.mark import MarkInfo, MarkDecorator
|
||||
if isinstance(val, (MarkDecorator, MarkInfo)):
|
||||
return val
|
||||
|
||||
def listextrakeywords(self):
|
||||
""" Return a set of all extra keywords in self and any parents."""
|
||||
extra_keywords = set()
|
||||
item = self
|
||||
for item in self.listchain():
|
||||
extra_keywords.update(item.extra_keyword_matches)
|
||||
return extra_keywords
|
||||
|
||||
def listnames(self):
|
||||
return [x.name for x in self.listchain()]
|
||||
|
||||
def addfinalizer(self, fin):
|
||||
""" register a function to be called when this node is finalized.
|
||||
|
||||
This method can only be called when this node is active
|
||||
in a setup chain, for example during self.setup().
|
||||
"""
|
||||
self.session._setupstate.addfinalizer(fin, self)
|
||||
|
||||
def getparent(self, cls):
|
||||
""" get the next parent node (including ourself)
|
||||
which is an instance of the given class"""
|
||||
current = self
|
||||
while current and not isinstance(current, cls):
|
||||
current = current.parent
|
||||
return current
|
||||
|
||||
def _prunetraceback(self, excinfo):
|
||||
pass
|
||||
|
||||
def _repr_failure_py(self, excinfo, style=None):
|
||||
fm = self.session._fixturemanager
|
||||
if excinfo.errisinstance(fm.FixtureLookupError):
|
||||
return excinfo.value.formatrepr()
|
||||
tbfilter = True
|
||||
if self.config.option.fulltrace:
|
||||
style = "long"
|
||||
else:
|
||||
tb = _pytest._code.Traceback([excinfo.traceback[-1]])
|
||||
self._prunetraceback(excinfo)
|
||||
if len(excinfo.traceback) == 0:
|
||||
excinfo.traceback = tb
|
||||
tbfilter = False # prunetraceback already does it
|
||||
if style == "auto":
|
||||
style = "long"
|
||||
# XXX should excinfo.getrepr record all data and toterminal() process it?
|
||||
if style is None:
|
||||
if self.config.option.tbstyle == "short":
|
||||
style = "short"
|
||||
else:
|
||||
style = "long"
|
||||
|
||||
try:
|
||||
os.getcwd()
|
||||
abspath = False
|
||||
except OSError:
|
||||
abspath = True
|
||||
|
||||
return excinfo.getrepr(funcargs=True, abspath=abspath,
|
||||
showlocals=self.config.option.showlocals,
|
||||
style=style, tbfilter=tbfilter)
|
||||
|
||||
repr_failure = _repr_failure_py
|
||||
|
||||
|
||||
class Collector(Node):
|
||||
""" Collector instances create children through collect()
|
||||
and thus iteratively build a tree.
|
||||
"""
|
||||
|
||||
class CollectError(Exception):
|
||||
""" an error during collection, contains a custom message. """
|
||||
|
||||
def collect(self):
|
||||
""" returns a list of children (items and collectors)
|
||||
for this collection node.
|
||||
"""
|
||||
raise NotImplementedError("abstract")
|
||||
|
||||
def repr_failure(self, excinfo):
|
||||
""" represent a collection failure. """
|
||||
if excinfo.errisinstance(self.CollectError):
|
||||
exc = excinfo.value
|
||||
return str(exc.args[0])
|
||||
return self._repr_failure_py(excinfo, style="short")
|
||||
|
||||
def _prunetraceback(self, excinfo):
|
||||
if hasattr(self, 'fspath'):
|
||||
traceback = excinfo.traceback
|
||||
ntraceback = traceback.cut(path=self.fspath)
|
||||
if ntraceback == traceback:
|
||||
ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
|
||||
excinfo.traceback = ntraceback.filter()
|
||||
|
||||
|
||||
class FSCollector(Collector):
|
||||
def __init__(self, fspath, parent=None, config=None, session=None):
|
||||
fspath = py.path.local(fspath) # xxx only for test_resultlog.py?
|
||||
name = fspath.basename
|
||||
if parent is not None:
|
||||
rel = fspath.relto(parent.fspath)
|
||||
if rel:
|
||||
name = rel
|
||||
name = name.replace(os.sep, nodes.SEP)
|
||||
super(FSCollector, self).__init__(name, parent, config, session)
|
||||
self.fspath = fspath
|
||||
|
||||
def _check_initialpaths_for_relpath(self):
|
||||
for initialpath in self.session._initialpaths:
|
||||
if self.fspath.common(initialpath) == initialpath:
|
||||
return self.fspath.relto(initialpath.dirname)
|
||||
|
||||
def _makeid(self):
|
||||
relpath = self.fspath.relto(self.config.rootdir)
|
||||
|
||||
if not relpath:
|
||||
relpath = self._check_initialpaths_for_relpath()
|
||||
if os.sep != nodes.SEP:
|
||||
relpath = relpath.replace(os.sep, nodes.SEP)
|
||||
return relpath
|
||||
|
||||
|
||||
class File(FSCollector):
|
||||
""" base class for collecting tests from a file. """
|
||||
|
||||
|
||||
class Item(Node):
|
||||
""" a basic test invocation item. Note that for a single function
|
||||
there might be multiple test invocation items.
|
||||
"""
|
||||
nextitem = None
|
||||
|
||||
def __init__(self, name, parent=None, config=None, session=None):
|
||||
super(Item, self).__init__(name, parent, config, session)
|
||||
self._report_sections = []
|
||||
|
||||
def add_report_section(self, when, key, content):
|
||||
"""
|
||||
Adds a new report section, similar to what's done internally to add stdout and
|
||||
stderr captured output::
|
||||
|
||||
item.add_report_section("call", "stdout", "report section contents")
|
||||
|
||||
:param str when:
|
||||
One of the possible capture states, ``"setup"``, ``"call"``, ``"teardown"``.
|
||||
:param str key:
|
||||
Name of the section, can be customized at will. Pytest uses ``"stdout"`` and
|
||||
``"stderr"`` internally.
|
||||
|
||||
:param str content:
|
||||
The full contents as a string.
|
||||
"""
|
||||
if content:
|
||||
self._report_sections.append((when, key, content))
|
||||
|
||||
def reportinfo(self):
|
||||
return self.fspath, None, ""
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
try:
|
||||
return self._location
|
||||
except AttributeError:
|
||||
location = self.reportinfo()
|
||||
# bestrelpath is a quite slow function
|
||||
cache = self.config.__dict__.setdefault("_bestrelpathcache", {})
|
||||
try:
|
||||
fspath = cache[location[0]]
|
||||
except KeyError:
|
||||
fspath = self.session.fspath.bestrelpath(location[0])
|
||||
cache[location[0]] = fspath
|
||||
location = (fspath, location[1], str(location[2]))
|
||||
self._location = location
|
||||
return location
|
||||
|
||||
|
||||
class NoMatch(Exception):
|
||||
""" raised if matching cannot locate a matching names. """
|
||||
|
||||
@@ -599,15 +264,22 @@ class Interrupted(KeyboardInterrupt):
|
||||
__module__ = 'builtins' # for py3
|
||||
|
||||
|
||||
class Session(FSCollector):
|
||||
class Failed(Exception):
|
||||
""" signals an stop as failed test run. """
|
||||
|
||||
|
||||
class Session(nodes.FSCollector):
|
||||
Interrupted = Interrupted
|
||||
Failed = Failed
|
||||
|
||||
def __init__(self, config):
|
||||
FSCollector.__init__(self, config.rootdir, parent=None,
|
||||
config=config, session=self)
|
||||
nodes.FSCollector.__init__(
|
||||
self, config.rootdir, parent=None,
|
||||
config=config, session=self)
|
||||
self.testsfailed = 0
|
||||
self.testscollected = 0
|
||||
self.shouldstop = False
|
||||
self.shouldfail = False
|
||||
self.trace = config.trace.root.get("collection")
|
||||
self._norecursepatterns = config.getini("norecursedirs")
|
||||
self.startdir = py.path.local()
|
||||
@@ -618,6 +290,8 @@ class Session(FSCollector):
|
||||
|
||||
@hookimpl(tryfirst=True)
|
||||
def pytest_collectstart(self):
|
||||
if self.shouldfail:
|
||||
raise self.Failed(self.shouldfail)
|
||||
if self.shouldstop:
|
||||
raise self.Interrupted(self.shouldstop)
|
||||
|
||||
@@ -627,7 +301,7 @@ class Session(FSCollector):
|
||||
self.testsfailed += 1
|
||||
maxfail = self.config.getvalue("maxfail")
|
||||
if maxfail and self.testsfailed >= maxfail:
|
||||
self.shouldstop = "stopping after %d failures" % (
|
||||
self.shouldfail = "stopping after %d failures" % (
|
||||
self.testsfailed)
|
||||
pytest_collectreport = pytest_runtest_logreport
|
||||
|
||||
@@ -742,9 +416,10 @@ class Session(FSCollector):
|
||||
"""Convert a dotted module name to path.
|
||||
|
||||
"""
|
||||
import pkgutil
|
||||
|
||||
try:
|
||||
loader = pkgutil.find_loader(x)
|
||||
with _patched_find_module():
|
||||
loader = pkgutil.find_loader(x)
|
||||
except ImportError:
|
||||
return x
|
||||
if loader is None:
|
||||
@@ -752,7 +427,8 @@ class Session(FSCollector):
|
||||
# This method is sometimes invoked when AssertionRewritingHook, which
|
||||
# does not define a get_filename method, is already in place:
|
||||
try:
|
||||
path = loader.get_filename(x)
|
||||
with _patched_find_module():
|
||||
path = loader.get_filename(x)
|
||||
except AttributeError:
|
||||
# Retrieve path from AssertionRewritingHook:
|
||||
path = loader.modules[x][0].co_filename
|
||||
@@ -796,11 +472,11 @@ class Session(FSCollector):
|
||||
nextnames = names[1:]
|
||||
resultnodes = []
|
||||
for node in matching:
|
||||
if isinstance(node, Item):
|
||||
if isinstance(node, nodes.Item):
|
||||
if not names:
|
||||
resultnodes.append(node)
|
||||
continue
|
||||
assert isinstance(node, Collector)
|
||||
assert isinstance(node, nodes.Collector)
|
||||
rep = collect_one_node(node)
|
||||
if rep.passed:
|
||||
has_matched = False
|
||||
@@ -822,11 +498,11 @@ class Session(FSCollector):
|
||||
|
||||
def genitems(self, node):
|
||||
self.trace("genitems", node)
|
||||
if isinstance(node, Item):
|
||||
if isinstance(node, nodes.Item):
|
||||
node.ihook.pytest_itemcollected(item=node)
|
||||
yield node
|
||||
else:
|
||||
assert isinstance(node, Collector)
|
||||
assert isinstance(node, nodes.Collector)
|
||||
rep = collect_one_node(node)
|
||||
if rep.passed:
|
||||
for subnode in rep.result:
|
||||
|
||||
126
_pytest/mark.py
126
_pytest/mark.py
@@ -2,11 +2,18 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import inspect
|
||||
import keyword
|
||||
import warnings
|
||||
import attr
|
||||
from collections import namedtuple
|
||||
from operator import attrgetter
|
||||
from .compat import imap
|
||||
from six.moves import map
|
||||
|
||||
from _pytest.config import UsageError
|
||||
from .deprecated import MARK_PARAMETERSET_UNPACKING
|
||||
from .compat import NOTSET, getfslineno
|
||||
|
||||
EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
|
||||
|
||||
|
||||
def alias(name, warning=None):
|
||||
@@ -67,9 +74,40 @@ class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')):
|
||||
|
||||
return cls(argval, marks=newmarks, id=None)
|
||||
|
||||
@property
|
||||
def deprecated_arg_dict(self):
|
||||
return dict((mark.name, mark) for mark in self.marks)
|
||||
@classmethod
|
||||
def _for_parametrize(cls, argnames, argvalues, function, config):
|
||||
if not isinstance(argnames, (tuple, list)):
|
||||
argnames = [x.strip() for x in argnames.split(",") if x.strip()]
|
||||
force_tuple = len(argnames) == 1
|
||||
else:
|
||||
force_tuple = False
|
||||
parameters = [
|
||||
ParameterSet.extract_from(x, legacy_force_tuple=force_tuple)
|
||||
for x in argvalues]
|
||||
del argvalues
|
||||
|
||||
if not parameters:
|
||||
mark = get_empty_parameterset_mark(config, argnames, function)
|
||||
parameters.append(ParameterSet(
|
||||
values=(NOTSET,) * len(argnames),
|
||||
marks=[mark],
|
||||
id=None,
|
||||
))
|
||||
return argnames, parameters
|
||||
|
||||
|
||||
def get_empty_parameterset_mark(config, argnames, function):
|
||||
requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION)
|
||||
if requested_mark in ('', None, 'skip'):
|
||||
mark = MARK_GEN.skip
|
||||
elif requested_mark == 'xfail':
|
||||
mark = MARK_GEN.xfail(run=False)
|
||||
else:
|
||||
raise LookupError(requested_mark)
|
||||
fs, lineno = getfslineno(function)
|
||||
reason = "got empty parameter set %r, function %s at %s:%d" % (
|
||||
argnames, function.__name__, fs, lineno)
|
||||
return mark(reason=reason)
|
||||
|
||||
|
||||
class MarkerError(Exception):
|
||||
@@ -111,6 +149,9 @@ def pytest_addoption(parser):
|
||||
)
|
||||
|
||||
parser.addini("markers", "markers for test functions", 'linelist')
|
||||
parser.addini(
|
||||
EMPTY_PARAMETERSET_OPTION,
|
||||
"default marker for empty parametersets")
|
||||
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
@@ -119,7 +160,9 @@ def pytest_cmdline_main(config):
|
||||
config._do_configure()
|
||||
tw = _pytest.config.create_terminal_writer(config)
|
||||
for line in config.getini("markers"):
|
||||
name, rest = line.split(":", 1)
|
||||
parts = line.split(":", 1)
|
||||
name = parts[0]
|
||||
rest = parts[1] if len(parts) == 2 else ''
|
||||
tw.write("@pytest.mark.%s:" % name, bold=True)
|
||||
tw.line(rest)
|
||||
tw.line()
|
||||
@@ -164,22 +207,26 @@ def pytest_collection_modifyitems(items, config):
|
||||
items[:] = remaining
|
||||
|
||||
|
||||
class MarkMapping:
|
||||
@attr.s
|
||||
class MarkMapping(object):
|
||||
"""Provides a local mapping for markers where item access
|
||||
resolves to True if the marker is present. """
|
||||
|
||||
def __init__(self, keywords):
|
||||
mymarks = set()
|
||||
own_mark_names = attr.ib()
|
||||
|
||||
@classmethod
|
||||
def from_keywords(cls, keywords):
|
||||
mark_names = set()
|
||||
for key, value in keywords.items():
|
||||
if isinstance(value, MarkInfo) or isinstance(value, MarkDecorator):
|
||||
mymarks.add(key)
|
||||
self._mymarks = mymarks
|
||||
mark_names.add(key)
|
||||
return cls(mark_names)
|
||||
|
||||
def __getitem__(self, name):
|
||||
return name in self._mymarks
|
||||
return name in self.own_mark_names
|
||||
|
||||
|
||||
class KeywordMapping:
|
||||
class KeywordMapping(object):
|
||||
"""Provides a local mapping for keywords.
|
||||
Given a list of names, map any substring of one of these names to True.
|
||||
"""
|
||||
@@ -194,9 +241,12 @@ class KeywordMapping:
|
||||
return False
|
||||
|
||||
|
||||
python_keywords_allowed_list = ["or", "and", "not"]
|
||||
|
||||
|
||||
def matchmark(colitem, markexpr):
|
||||
"""Tries to match on any marker names, attached to the given colitem."""
|
||||
return eval(markexpr, {}, MarkMapping(colitem.keywords))
|
||||
return eval(markexpr, {}, MarkMapping.from_keywords(colitem.keywords))
|
||||
|
||||
|
||||
def matchkeyword(colitem, keywordexpr):
|
||||
@@ -231,7 +281,13 @@ def matchkeyword(colitem, keywordexpr):
|
||||
return mapping[keywordexpr]
|
||||
elif keywordexpr.startswith("not ") and " " not in keywordexpr[4:]:
|
||||
return not mapping[keywordexpr[4:]]
|
||||
return eval(keywordexpr, {}, mapping)
|
||||
for kwd in keywordexpr.split():
|
||||
if keyword.iskeyword(kwd) and kwd not in python_keywords_allowed_list:
|
||||
raise UsageError("Python keyword '{}' not accepted in expressions passed to '-k'".format(kwd))
|
||||
try:
|
||||
return eval(keywordexpr, {}, mapping)
|
||||
except SyntaxError:
|
||||
raise UsageError("Wrong expression passed to '-k': {}".format(keywordexpr))
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
@@ -239,12 +295,19 @@ def pytest_configure(config):
|
||||
if config.option.strict:
|
||||
MARK_GEN._config = config
|
||||
|
||||
empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION)
|
||||
|
||||
if empty_parameterset not in ('skip', 'xfail', None, ''):
|
||||
raise UsageError(
|
||||
"{!s} must be one of skip and xfail,"
|
||||
" but it is {!r}".format(EMPTY_PARAMETERSET_OPTION, empty_parameterset))
|
||||
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
MARK_GEN._config = getattr(config, '_old_mark_config', None)
|
||||
|
||||
|
||||
class MarkGenerator:
|
||||
class MarkGenerator(object):
|
||||
""" Factory for :class:`MarkDecorator` objects - exposed as
|
||||
a ``pytest.mark`` singleton instance. Example::
|
||||
|
||||
@@ -272,7 +335,7 @@ class MarkGenerator:
|
||||
pass
|
||||
self._markers = values = set()
|
||||
for line in self._config.getini("markers"):
|
||||
marker, _ = line.split(":", 1)
|
||||
marker = line.split(":", 1)[0]
|
||||
marker = marker.rstrip()
|
||||
x = marker.split("(", 1)[0]
|
||||
values.add(x)
|
||||
@@ -285,7 +348,21 @@ def istestfunc(func):
|
||||
getattr(func, "__name__", "<lambda>") != "<lambda>"
|
||||
|
||||
|
||||
class MarkDecorator:
|
||||
@attr.s(frozen=True)
|
||||
class Mark(object):
|
||||
name = attr.ib()
|
||||
args = attr.ib()
|
||||
kwargs = attr.ib()
|
||||
|
||||
def combined_with(self, other):
|
||||
assert self.name == other.name
|
||||
return Mark(
|
||||
self.name, self.args + other.args,
|
||||
dict(self.kwargs, **other.kwargs))
|
||||
|
||||
|
||||
@attr.s
|
||||
class MarkDecorator(object):
|
||||
""" A decorator for test functions and test classes. When applied
|
||||
it will create :class:`MarkInfo` objects which may be
|
||||
:ref:`retrieved by hooks as item keywords <excontrolskip>`.
|
||||
@@ -319,9 +396,7 @@ class MarkDecorator:
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, mark):
|
||||
assert isinstance(mark, Mark), repr(mark)
|
||||
self.mark = mark
|
||||
mark = attr.ib(validator=attr.validators.instance_of(Mark))
|
||||
|
||||
name = alias('mark.name')
|
||||
args = alias('mark.args')
|
||||
@@ -401,15 +476,6 @@ def store_legacy_markinfo(func, mark):
|
||||
holder.add_mark(mark)
|
||||
|
||||
|
||||
class Mark(namedtuple('Mark', 'name, args, kwargs')):
|
||||
|
||||
def combined_with(self, other):
|
||||
assert self.name == other.name
|
||||
return Mark(
|
||||
self.name, self.args + other.args,
|
||||
dict(self.kwargs, **other.kwargs))
|
||||
|
||||
|
||||
class MarkInfo(object):
|
||||
""" Marking object created by :class:`MarkDecorator` instances. """
|
||||
|
||||
@@ -432,7 +498,7 @@ class MarkInfo(object):
|
||||
|
||||
def __iter__(self):
|
||||
""" yield MarkInfo objects each relating to a marking-call. """
|
||||
return imap(MarkInfo, self._marks)
|
||||
return map(MarkInfo, self._marks)
|
||||
|
||||
|
||||
MARK_GEN = MarkGenerator()
|
||||
|
||||
@@ -4,8 +4,7 @@ from __future__ import absolute_import, division, print_function
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
|
||||
from py.builtin import _basestring
|
||||
import six
|
||||
from _pytest.fixtures import fixture
|
||||
|
||||
RE_IMPORT_ERROR_NAME = re.compile("^No module named (.*)$")
|
||||
@@ -79,7 +78,7 @@ def annotated_getattr(obj, name, ann):
|
||||
|
||||
|
||||
def derive_importpath(import_path, raising):
|
||||
if not isinstance(import_path, _basestring) or "." not in import_path:
|
||||
if not isinstance(import_path, six.string_types) or "." not in import_path:
|
||||
raise TypeError("must be absolute import path string, not %r" %
|
||||
(import_path,))
|
||||
module, attr = import_path.rsplit('.', 1)
|
||||
@@ -89,7 +88,7 @@ def derive_importpath(import_path, raising):
|
||||
return attr, target
|
||||
|
||||
|
||||
class Notset:
|
||||
class Notset(object):
|
||||
def __repr__(self):
|
||||
return "<notset>"
|
||||
|
||||
@@ -97,7 +96,7 @@ class Notset:
|
||||
notset = Notset()
|
||||
|
||||
|
||||
class MonkeyPatch:
|
||||
class MonkeyPatch(object):
|
||||
""" Object returned by the ``monkeypatch`` fixture keeping a record of setattr/item/env/syspath changes.
|
||||
"""
|
||||
|
||||
@@ -125,7 +124,7 @@ class MonkeyPatch:
|
||||
import inspect
|
||||
|
||||
if value is notset:
|
||||
if not isinstance(target, _basestring):
|
||||
if not isinstance(target, six.string_types):
|
||||
raise TypeError("use setattr(target, name, value) or "
|
||||
"setattr(target, value) with target being a dotted "
|
||||
"import string")
|
||||
@@ -155,7 +154,7 @@ class MonkeyPatch:
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
if name is notset:
|
||||
if not isinstance(target, _basestring):
|
||||
if not isinstance(target, six.string_types):
|
||||
raise TypeError("use delattr(target, name) or "
|
||||
"delattr(target) with target being a dotted "
|
||||
"import string")
|
||||
|
||||
363
_pytest/nodes.py
363
_pytest/nodes.py
@@ -1,5 +1,18 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
from collections import MutableMapping as MappingMixin
|
||||
import os
|
||||
|
||||
import six
|
||||
import py
|
||||
import attr
|
||||
|
||||
import _pytest
|
||||
|
||||
|
||||
SEP = "/"
|
||||
|
||||
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
|
||||
|
||||
|
||||
def _splitnode(nodeid):
|
||||
"""Split a nodeid into constituent 'parts'.
|
||||
@@ -35,3 +48,353 @@ def ischildnode(baseid, nodeid):
|
||||
if len(node_parts) < len(base_parts):
|
||||
return False
|
||||
return node_parts[:len(base_parts)] == base_parts
|
||||
|
||||
|
||||
@attr.s
|
||||
class _CompatProperty(object):
|
||||
name = attr.ib()
|
||||
|
||||
def __get__(self, obj, owner):
|
||||
if obj is None:
|
||||
return self
|
||||
|
||||
# TODO: reenable in the features branch
|
||||
# warnings.warn(
|
||||
# "usage of {owner!r}.{name} is deprecated, please use pytest.{name} instead".format(
|
||||
# name=self.name, owner=type(owner).__name__),
|
||||
# PendingDeprecationWarning, stacklevel=2)
|
||||
return getattr(__import__('pytest'), self.name)
|
||||
|
||||
|
||||
class NodeKeywords(MappingMixin):
|
||||
def __init__(self, node):
|
||||
self.node = node
|
||||
self.parent = node.parent
|
||||
self._markers = {node.name: True}
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return self._markers[key]
|
||||
except KeyError:
|
||||
if self.parent is None:
|
||||
raise
|
||||
return self.parent.keywords[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._markers[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
raise ValueError("cannot delete key in keywords dict")
|
||||
|
||||
def __iter__(self):
|
||||
seen = set(self._markers)
|
||||
if self.parent is not None:
|
||||
seen.update(self.parent.keywords)
|
||||
return iter(seen)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.__iter__())
|
||||
|
||||
def keys(self):
|
||||
return list(self)
|
||||
|
||||
def __repr__(self):
|
||||
return "<NodeKeywords for node %s>" % (self.node, )
|
||||
|
||||
|
||||
class Node(object):
|
||||
""" base class for Collector and Item the test collection tree.
|
||||
Collector subclasses have children, Items are terminal nodes."""
|
||||
|
||||
def __init__(self, name, parent=None, config=None, session=None):
|
||||
#: a unique name within the scope of the parent node
|
||||
self.name = name
|
||||
|
||||
#: the parent collector node.
|
||||
self.parent = parent
|
||||
|
||||
#: the pytest config object
|
||||
self.config = config or parent.config
|
||||
|
||||
#: the session this node is part of
|
||||
self.session = session or parent.session
|
||||
|
||||
#: filesystem path where this node was collected from (can be None)
|
||||
self.fspath = getattr(parent, 'fspath', None)
|
||||
|
||||
#: keywords/markers collected from all scopes
|
||||
self.keywords = NodeKeywords(self)
|
||||
|
||||
#: allow adding of extra keywords to use for matching
|
||||
self.extra_keyword_matches = set()
|
||||
|
||||
# used for storing artificial fixturedefs for direct parametrization
|
||||
self._name2pseudofixturedef = {}
|
||||
|
||||
@property
|
||||
def ihook(self):
|
||||
""" fspath sensitive hook proxy used to call pytest hooks"""
|
||||
return self.session.gethookproxy(self.fspath)
|
||||
|
||||
Module = _CompatProperty("Module")
|
||||
Class = _CompatProperty("Class")
|
||||
Instance = _CompatProperty("Instance")
|
||||
Function = _CompatProperty("Function")
|
||||
File = _CompatProperty("File")
|
||||
Item = _CompatProperty("Item")
|
||||
|
||||
def _getcustomclass(self, name):
|
||||
maybe_compatprop = getattr(type(self), name)
|
||||
if isinstance(maybe_compatprop, _CompatProperty):
|
||||
return getattr(__import__('pytest'), name)
|
||||
else:
|
||||
cls = getattr(self, name)
|
||||
# TODO: reenable in the features branch
|
||||
# warnings.warn("use of node.%s is deprecated, "
|
||||
# "use pytest_pycollect_makeitem(...) to create custom "
|
||||
# "collection nodes" % name, category=DeprecationWarning)
|
||||
return cls
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s %r>" % (self.__class__.__name__,
|
||||
getattr(self, 'name', None))
|
||||
|
||||
def warn(self, code, message):
|
||||
""" generate a warning with the given code and message for this
|
||||
item. """
|
||||
assert isinstance(code, str)
|
||||
fslocation = getattr(self, "location", None)
|
||||
if fslocation is None:
|
||||
fslocation = getattr(self, "fspath", None)
|
||||
self.ihook.pytest_logwarning.call_historic(kwargs=dict(
|
||||
code=code, message=message,
|
||||
nodeid=self.nodeid, fslocation=fslocation))
|
||||
|
||||
# methods for ordering nodes
|
||||
@property
|
||||
def nodeid(self):
|
||||
""" a ::-separated string denoting its collection tree address. """
|
||||
try:
|
||||
return self._nodeid
|
||||
except AttributeError:
|
||||
self._nodeid = x = self._makeid()
|
||||
return x
|
||||
|
||||
def _makeid(self):
|
||||
return self.parent.nodeid + "::" + self.name
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.nodeid)
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
|
||||
def teardown(self):
|
||||
pass
|
||||
|
||||
def listchain(self):
|
||||
""" return list of all parent collectors up to self,
|
||||
starting from root of collection tree. """
|
||||
chain = []
|
||||
item = self
|
||||
while item is not None:
|
||||
chain.append(item)
|
||||
item = item.parent
|
||||
chain.reverse()
|
||||
return chain
|
||||
|
||||
def add_marker(self, marker):
|
||||
""" dynamically add a marker object to the node.
|
||||
|
||||
``marker`` can be a string or pytest.mark.* instance.
|
||||
"""
|
||||
from _pytest.mark import MarkDecorator, MARK_GEN
|
||||
if isinstance(marker, six.string_types):
|
||||
marker = getattr(MARK_GEN, marker)
|
||||
elif not isinstance(marker, MarkDecorator):
|
||||
raise ValueError("is not a string or pytest.mark.* Marker")
|
||||
self.keywords[marker.name] = marker
|
||||
|
||||
def get_marker(self, name):
|
||||
""" get a marker object from this node or None if
|
||||
the node doesn't have a marker with that name. """
|
||||
val = self.keywords.get(name, None)
|
||||
if val is not None:
|
||||
from _pytest.mark import MarkInfo, MarkDecorator
|
||||
if isinstance(val, (MarkDecorator, MarkInfo)):
|
||||
return val
|
||||
|
||||
def listextrakeywords(self):
|
||||
""" Return a set of all extra keywords in self and any parents."""
|
||||
extra_keywords = set()
|
||||
item = self
|
||||
for item in self.listchain():
|
||||
extra_keywords.update(item.extra_keyword_matches)
|
||||
return extra_keywords
|
||||
|
||||
def listnames(self):
|
||||
return [x.name for x in self.listchain()]
|
||||
|
||||
def addfinalizer(self, fin):
|
||||
""" register a function to be called when this node is finalized.
|
||||
|
||||
This method can only be called when this node is active
|
||||
in a setup chain, for example during self.setup().
|
||||
"""
|
||||
self.session._setupstate.addfinalizer(fin, self)
|
||||
|
||||
def getparent(self, cls):
|
||||
""" get the next parent node (including ourself)
|
||||
which is an instance of the given class"""
|
||||
current = self
|
||||
while current and not isinstance(current, cls):
|
||||
current = current.parent
|
||||
return current
|
||||
|
||||
def _prunetraceback(self, excinfo):
|
||||
pass
|
||||
|
||||
def _repr_failure_py(self, excinfo, style=None):
|
||||
fm = self.session._fixturemanager
|
||||
if excinfo.errisinstance(fm.FixtureLookupError):
|
||||
return excinfo.value.formatrepr()
|
||||
tbfilter = True
|
||||
if self.config.option.fulltrace:
|
||||
style = "long"
|
||||
else:
|
||||
tb = _pytest._code.Traceback([excinfo.traceback[-1]])
|
||||
self._prunetraceback(excinfo)
|
||||
if len(excinfo.traceback) == 0:
|
||||
excinfo.traceback = tb
|
||||
tbfilter = False # prunetraceback already does it
|
||||
if style == "auto":
|
||||
style = "long"
|
||||
# XXX should excinfo.getrepr record all data and toterminal() process it?
|
||||
if style is None:
|
||||
if self.config.option.tbstyle == "short":
|
||||
style = "short"
|
||||
else:
|
||||
style = "long"
|
||||
|
||||
try:
|
||||
os.getcwd()
|
||||
abspath = False
|
||||
except OSError:
|
||||
abspath = True
|
||||
|
||||
return excinfo.getrepr(funcargs=True, abspath=abspath,
|
||||
showlocals=self.config.option.showlocals,
|
||||
style=style, tbfilter=tbfilter)
|
||||
|
||||
repr_failure = _repr_failure_py
|
||||
|
||||
|
||||
class Collector(Node):
|
||||
""" Collector instances create children through collect()
|
||||
and thus iteratively build a tree.
|
||||
"""
|
||||
|
||||
class CollectError(Exception):
|
||||
""" an error during collection, contains a custom message. """
|
||||
|
||||
def collect(self):
|
||||
""" returns a list of children (items and collectors)
|
||||
for this collection node.
|
||||
"""
|
||||
raise NotImplementedError("abstract")
|
||||
|
||||
def repr_failure(self, excinfo):
|
||||
""" represent a collection failure. """
|
||||
if excinfo.errisinstance(self.CollectError):
|
||||
exc = excinfo.value
|
||||
return str(exc.args[0])
|
||||
return self._repr_failure_py(excinfo, style="short")
|
||||
|
||||
def _prunetraceback(self, excinfo):
|
||||
if hasattr(self, 'fspath'):
|
||||
traceback = excinfo.traceback
|
||||
ntraceback = traceback.cut(path=self.fspath)
|
||||
if ntraceback == traceback:
|
||||
ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
|
||||
excinfo.traceback = ntraceback.filter()
|
||||
|
||||
|
||||
class FSCollector(Collector):
|
||||
def __init__(self, fspath, parent=None, config=None, session=None):
|
||||
fspath = py.path.local(fspath) # xxx only for test_resultlog.py?
|
||||
name = fspath.basename
|
||||
if parent is not None:
|
||||
rel = fspath.relto(parent.fspath)
|
||||
if rel:
|
||||
name = rel
|
||||
name = name.replace(os.sep, SEP)
|
||||
super(FSCollector, self).__init__(name, parent, config, session)
|
||||
self.fspath = fspath
|
||||
|
||||
def _check_initialpaths_for_relpath(self):
|
||||
for initialpath in self.session._initialpaths:
|
||||
if self.fspath.common(initialpath) == initialpath:
|
||||
return self.fspath.relto(initialpath.dirname)
|
||||
|
||||
def _makeid(self):
|
||||
relpath = self.fspath.relto(self.config.rootdir)
|
||||
|
||||
if not relpath:
|
||||
relpath = self._check_initialpaths_for_relpath()
|
||||
if os.sep != SEP:
|
||||
relpath = relpath.replace(os.sep, SEP)
|
||||
return relpath
|
||||
|
||||
|
||||
class File(FSCollector):
|
||||
""" base class for collecting tests from a file. """
|
||||
|
||||
|
||||
class Item(Node):
|
||||
""" a basic test invocation item. Note that for a single function
|
||||
there might be multiple test invocation items.
|
||||
"""
|
||||
nextitem = None
|
||||
|
||||
def __init__(self, name, parent=None, config=None, session=None):
|
||||
super(Item, self).__init__(name, parent, config, session)
|
||||
self._report_sections = []
|
||||
|
||||
def add_report_section(self, when, key, content):
|
||||
"""
|
||||
Adds a new report section, similar to what's done internally to add stdout and
|
||||
stderr captured output::
|
||||
|
||||
item.add_report_section("call", "stdout", "report section contents")
|
||||
|
||||
:param str when:
|
||||
One of the possible capture states, ``"setup"``, ``"call"``, ``"teardown"``.
|
||||
:param str key:
|
||||
Name of the section, can be customized at will. Pytest uses ``"stdout"`` and
|
||||
``"stderr"`` internally.
|
||||
|
||||
:param str content:
|
||||
The full contents as a string.
|
||||
"""
|
||||
if content:
|
||||
self._report_sections.append((when, key, content))
|
||||
|
||||
def reportinfo(self):
|
||||
return self.fspath, None, ""
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
try:
|
||||
return self._location
|
||||
except AttributeError:
|
||||
location = self.reportinfo()
|
||||
# bestrelpath is a quite slow function
|
||||
cache = self.config.__dict__.setdefault("_bestrelpathcache", {})
|
||||
try:
|
||||
fspath = cache[location[0]]
|
||||
except KeyError:
|
||||
fspath = self.session.fspath.bestrelpath(location[0])
|
||||
cache[location[0]] = fspath
|
||||
location = (fspath, location[1], str(location[2]))
|
||||
self._location = location
|
||||
return location
|
||||
|
||||
@@ -3,7 +3,6 @@ from __future__ import absolute_import, division, print_function
|
||||
|
||||
import sys
|
||||
|
||||
import py
|
||||
from _pytest import unittest, runner, python
|
||||
from _pytest.config import hookimpl
|
||||
|
||||
@@ -66,7 +65,7 @@ def is_potential_nosetest(item):
|
||||
def call_optional(obj, name):
|
||||
method = getattr(obj, name, None)
|
||||
isfixture = hasattr(method, "_pytestfixturefunction")
|
||||
if method is not None and not isfixture and py.builtin.callable(method):
|
||||
if method is not None and not isfixture and callable(method):
|
||||
# If there's any problems allow the exception to raise rather than
|
||||
# silently ignoring them
|
||||
method()
|
||||
|
||||
@@ -62,14 +62,21 @@ def exit(msg):
|
||||
exit.Exception = Exit
|
||||
|
||||
|
||||
def skip(msg=""):
|
||||
def skip(msg="", **kwargs):
|
||||
""" skip an executing test with the given message. Note: it's usually
|
||||
better to use the pytest.mark.skipif marker to declare a test to be
|
||||
skipped under certain conditions like mismatching platforms or
|
||||
dependencies. See the pytest_skipping plugin for details.
|
||||
|
||||
:kwarg bool allow_module_level: allows this function to be called at
|
||||
module level, skipping the rest of the module. Default to False.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
raise Skipped(msg=msg)
|
||||
allow_module_level = kwargs.pop('allow_module_level', False)
|
||||
if kwargs:
|
||||
keys = [k for k in kwargs.keys()]
|
||||
raise TypeError('unexpected keyword arguments: {0}'.format(keys))
|
||||
raise Skipped(msg=msg, allow_module_level=allow_module_level)
|
||||
|
||||
|
||||
skip.Exception = Skipped
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import pytest
|
||||
import six
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
@@ -16,7 +17,6 @@ def pytest_addoption(parser):
|
||||
|
||||
@pytest.hookimpl(trylast=True)
|
||||
def pytest_configure(config):
|
||||
import py
|
||||
if config.option.pastebin == "all":
|
||||
tr = config.pluginmanager.getplugin('terminalreporter')
|
||||
# if no terminal reporter plugin is present, nothing we can do here;
|
||||
@@ -29,7 +29,7 @@ def pytest_configure(config):
|
||||
|
||||
def tee_write(s, **kwargs):
|
||||
oldwrite(s, **kwargs)
|
||||
if py.builtin._istext(s):
|
||||
if isinstance(s, six.text_type):
|
||||
s = s.encode('utf-8')
|
||||
config._pastebinfile.write(s)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
""" (disabled by default) support for testing pytest and pytest plugins. """
|
||||
"""(disabled by default) support for testing pytest and pytest plugins."""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import codecs
|
||||
@@ -7,6 +7,7 @@ import os
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
import six
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
@@ -22,27 +23,26 @@ from _pytest.main import Session, EXIT_OK
|
||||
from _pytest.assertion.rewrite import AssertionRewritingHook
|
||||
|
||||
|
||||
PYTEST_FULLPATH = os.path.abspath(pytest.__file__.rstrip("oc")).replace("$py.class", ".py")
|
||||
|
||||
|
||||
IGNORE_PAM = [ # filenames added when obtaining details about the current user
|
||||
u'/var/lib/sss/mc/passwd'
|
||||
]
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
# group = parser.getgroup("pytester", "pytester (self-tests) options")
|
||||
parser.addoption('--lsof',
|
||||
action="store_true", dest="lsof", default=False,
|
||||
help=("run FD checks if lsof is available"))
|
||||
|
||||
parser.addoption('--runpytest', default="inprocess", dest="runpytest",
|
||||
choices=("inprocess", "subprocess", ),
|
||||
choices=("inprocess", "subprocess"),
|
||||
help=("run pytest sub runs in tests using an 'inprocess' "
|
||||
"or 'subprocess' (python -m main) method"))
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
# This might be called multiple times. Only take the first.
|
||||
global _pytest_fullpath
|
||||
try:
|
||||
_pytest_fullpath
|
||||
except NameError:
|
||||
_pytest_fullpath = os.path.abspath(pytest.__file__.rstrip("oc"))
|
||||
_pytest_fullpath = _pytest_fullpath.replace("$py.class", ".py")
|
||||
|
||||
if config.getvalue("lsof"):
|
||||
checker = LsofFdLeakChecker()
|
||||
if checker.matching_platform():
|
||||
@@ -71,6 +71,8 @@ class LsofFdLeakChecker(object):
|
||||
fields = line.split('\0')
|
||||
fd = fields[0][1:]
|
||||
filename = fields[1][1:]
|
||||
if filename in IGNORE_PAM:
|
||||
continue
|
||||
if filename.startswith('/'):
|
||||
open_files.append((fd, filename))
|
||||
|
||||
@@ -80,8 +82,8 @@ class LsofFdLeakChecker(object):
|
||||
try:
|
||||
py.process.cmdexec("lsof -v")
|
||||
except (py.process.cmdexec.Error, UnicodeDecodeError):
|
||||
# cmdexec may raise UnicodeDecodeError on Windows systems
|
||||
# with locale other than english:
|
||||
# cmdexec may raise UnicodeDecodeError on Windows systems with
|
||||
# locale other than English:
|
||||
# https://bitbucket.org/pytest-dev/py/issues/66
|
||||
return False
|
||||
else:
|
||||
@@ -114,12 +116,9 @@ class LsofFdLeakChecker(object):
|
||||
# XXX copied from execnet's conftest.py - needs to be merged
|
||||
winpymap = {
|
||||
'python2.7': r'C:\Python27\python.exe',
|
||||
'python2.6': r'C:\Python26\python.exe',
|
||||
'python3.1': r'C:\Python31\python.exe',
|
||||
'python3.2': r'C:\Python32\python.exe',
|
||||
'python3.3': r'C:\Python33\python.exe',
|
||||
'python3.4': r'C:\Python34\python.exe',
|
||||
'python3.5': r'C:\Python35\python.exe',
|
||||
'python3.6': r'C:\Python36\python.exe',
|
||||
}
|
||||
|
||||
|
||||
@@ -139,14 +138,13 @@ def getexecutable(name, cache={}):
|
||||
if "2.5.2" in err:
|
||||
executable = None # http://bugs.jython.org/issue1790
|
||||
elif popen.returncode != 0:
|
||||
# Handle pyenv's 127.
|
||||
# handle pyenv's 127
|
||||
executable = None
|
||||
cache[name] = executable
|
||||
return executable
|
||||
|
||||
|
||||
@pytest.fixture(params=['python2.6', 'python2.7', 'python3.3', "python3.4",
|
||||
'pypy', 'pypy3'])
|
||||
@pytest.fixture(params=['python2.7', 'python3.4', 'pypy', 'pypy3'])
|
||||
def anypython(request):
|
||||
name = request.param
|
||||
executable = getexecutable(name)
|
||||
@@ -165,14 +163,15 @@ def anypython(request):
|
||||
|
||||
@pytest.fixture
|
||||
def _pytest(request):
|
||||
""" Return a helper which offers a gethookrecorder(hook)
|
||||
method which returns a HookRecorder instance which helps
|
||||
to make assertions about called hooks.
|
||||
"""Return a helper which offers a gethookrecorder(hook) method which
|
||||
returns a HookRecorder instance which helps to make assertions about called
|
||||
hooks.
|
||||
|
||||
"""
|
||||
return PytestArg(request)
|
||||
|
||||
|
||||
class PytestArg:
|
||||
class PytestArg(object):
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
|
||||
@@ -187,7 +186,7 @@ def get_public_names(values):
|
||||
return [x for x in values if x[0] != "_"]
|
||||
|
||||
|
||||
class ParsedCall:
|
||||
class ParsedCall(object):
|
||||
def __init__(self, name, kwargs):
|
||||
self.__dict__.update(kwargs)
|
||||
self._name = name
|
||||
@@ -198,11 +197,11 @@ class ParsedCall:
|
||||
return "<ParsedCall %r(**%r)>" % (self._name, d)
|
||||
|
||||
|
||||
class HookRecorder:
|
||||
class HookRecorder(object):
|
||||
"""Record all hooks called in a plugin manager.
|
||||
|
||||
This wraps all the hook calls in the plugin manager, recording
|
||||
each call before propagating the normal calls.
|
||||
This wraps all the hook calls in the plugin manager, recording each call
|
||||
before propagating the normal calls.
|
||||
|
||||
"""
|
||||
|
||||
@@ -270,7 +269,7 @@ class HookRecorder:
|
||||
|
||||
def matchreport(self, inamepart="",
|
||||
names="pytest_runtest_logreport pytest_collectreport", when=None):
|
||||
""" return a testreport whose dotted import path matches """
|
||||
"""return a testreport whose dotted import path matches"""
|
||||
values = []
|
||||
for rep in self.getreports(names=names):
|
||||
try:
|
||||
@@ -344,19 +343,19 @@ def testdir(request, tmpdir_factory):
|
||||
rex_outcome = re.compile(r"(\d+) ([\w-]+)")
|
||||
|
||||
|
||||
class RunResult:
|
||||
class RunResult(object):
|
||||
"""The result of running a command.
|
||||
|
||||
Attributes:
|
||||
|
||||
:ret: The return value.
|
||||
:outlines: List of lines captured from stdout.
|
||||
:errlines: List of lines captures from stderr.
|
||||
:ret: the return value
|
||||
:outlines: list of lines captured from stdout
|
||||
:errlines: list of lines captures from stderr
|
||||
:stdout: :py:class:`LineMatcher` of stdout, use ``stdout.str()`` to
|
||||
reconstruct stdout or the commonly used
|
||||
``stdout.fnmatch_lines()`` method.
|
||||
:stderrr: :py:class:`LineMatcher` of stderr.
|
||||
:duration: Duration in seconds.
|
||||
reconstruct stdout or the commonly used ``stdout.fnmatch_lines()``
|
||||
method
|
||||
:stderr: :py:class:`LineMatcher` of stderr
|
||||
:duration: duration in seconds
|
||||
|
||||
"""
|
||||
|
||||
@@ -369,8 +368,10 @@ class RunResult:
|
||||
self.duration = duration
|
||||
|
||||
def parseoutcomes(self):
|
||||
""" Return a dictionary of outcomestring->num from parsing
|
||||
the terminal output that the test process produced."""
|
||||
"""Return a dictionary of outcomestring->num from parsing the terminal
|
||||
output that the test process produced.
|
||||
|
||||
"""
|
||||
for line in reversed(self.outlines):
|
||||
if 'seconds' in line:
|
||||
outcomes = rex_outcome.findall(line)
|
||||
@@ -382,8 +383,10 @@ class RunResult:
|
||||
raise ValueError("Pytest terminal report not found")
|
||||
|
||||
def assert_outcomes(self, passed=0, skipped=0, failed=0, error=0):
|
||||
""" assert that the specified outcomes appear with the respective
|
||||
numbers (0 means it didn't occur) in the text output from a test run."""
|
||||
"""Assert that the specified outcomes appear with the respective
|
||||
numbers (0 means it didn't occur) in the text output from a test run.
|
||||
|
||||
"""
|
||||
d = self.parseoutcomes()
|
||||
obtained = {
|
||||
'passed': d.get('passed', 0),
|
||||
@@ -394,44 +397,63 @@ class RunResult:
|
||||
assert obtained == dict(passed=passed, skipped=skipped, failed=failed, error=error)
|
||||
|
||||
|
||||
class Testdir:
|
||||
class CwdSnapshot(object):
|
||||
def __init__(self):
|
||||
self.__saved = os.getcwd()
|
||||
|
||||
def restore(self):
|
||||
os.chdir(self.__saved)
|
||||
|
||||
|
||||
class SysModulesSnapshot(object):
|
||||
def __init__(self, preserve=None):
|
||||
self.__preserve = preserve
|
||||
self.__saved = dict(sys.modules)
|
||||
|
||||
def restore(self):
|
||||
if self.__preserve:
|
||||
self.__saved.update(
|
||||
(k, m) for k, m in sys.modules.items() if self.__preserve(k))
|
||||
sys.modules.clear()
|
||||
sys.modules.update(self.__saved)
|
||||
|
||||
|
||||
class SysPathsSnapshot(object):
|
||||
def __init__(self):
|
||||
self.__saved = list(sys.path), list(sys.meta_path)
|
||||
|
||||
def restore(self):
|
||||
sys.path[:], sys.meta_path[:] = self.__saved
|
||||
|
||||
|
||||
class Testdir(object):
|
||||
"""Temporary test directory with tools to test/run pytest itself.
|
||||
|
||||
This is based on the ``tmpdir`` fixture but provides a number of
|
||||
methods which aid with testing pytest itself. Unless
|
||||
:py:meth:`chdir` is used all methods will use :py:attr:`tmpdir` as
|
||||
current working directory.
|
||||
This is based on the ``tmpdir`` fixture but provides a number of methods
|
||||
which aid with testing pytest itself. Unless :py:meth:`chdir` is used all
|
||||
methods will use :py:attr:`tmpdir` as their current working directory.
|
||||
|
||||
Attributes:
|
||||
|
||||
:tmpdir: The :py:class:`py.path.local` instance of the temporary
|
||||
directory.
|
||||
:tmpdir: The :py:class:`py.path.local` instance of the temporary directory.
|
||||
|
||||
:plugins: A list of plugins to use with :py:meth:`parseconfig` and
|
||||
:py:meth:`runpytest`. Initially this is an empty list but
|
||||
plugins can be added to the list. The type of items to add to
|
||||
the list depend on the method which uses them so refer to them
|
||||
for details.
|
||||
:py:meth:`runpytest`. Initially this is an empty list but plugins can
|
||||
be added to the list. The type of items to add to the list depends on
|
||||
the method using them so refer to them for details.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, request, tmpdir_factory):
|
||||
self.request = request
|
||||
self._mod_collections = WeakKeyDictionary()
|
||||
# XXX remove duplication with tmpdir plugin
|
||||
basetmp = tmpdir_factory.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.tmpdir = tmpdir_factory.mktemp(name, numbered=True)
|
||||
self.plugins = []
|
||||
self._savesyspath = (list(sys.path), list(sys.meta_path))
|
||||
self._savemodulekeys = set(sys.modules)
|
||||
self.chdir() # always chdir
|
||||
self._cwd_snapshot = CwdSnapshot()
|
||||
self._sys_path_snapshot = SysPathsSnapshot()
|
||||
self._sys_modules_snapshot = self.__take_sys_modules_snapshot()
|
||||
self.chdir()
|
||||
self.request.addfinalizer(self.finalize)
|
||||
method = self.request.config.getoption("--runpytest")
|
||||
if method == "inprocess":
|
||||
@@ -445,29 +467,22 @@ class Testdir:
|
||||
def finalize(self):
|
||||
"""Clean up global state artifacts.
|
||||
|
||||
Some methods modify the global interpreter state and this
|
||||
tries to clean this up. It does not remove the temporary
|
||||
directory however so it can be looked at after the test run
|
||||
has finished.
|
||||
Some methods modify the global interpreter state and this tries to
|
||||
clean this up. It does not remove the temporary directory however so
|
||||
it can be looked at after the test run has finished.
|
||||
|
||||
"""
|
||||
sys.path[:], sys.meta_path[:] = self._savesyspath
|
||||
if hasattr(self, '_olddir'):
|
||||
self._olddir.chdir()
|
||||
self.delete_loaded_modules()
|
||||
self._sys_modules_snapshot.restore()
|
||||
self._sys_path_snapshot.restore()
|
||||
self._cwd_snapshot.restore()
|
||||
|
||||
def delete_loaded_modules(self):
|
||||
"""Delete modules that have been loaded during a test.
|
||||
|
||||
This allows the interpreter to catch module changes in case
|
||||
the module is re-imported.
|
||||
"""
|
||||
for name in set(sys.modules).difference(self._savemodulekeys):
|
||||
# some zope modules used by twisted-related tests keeps internal
|
||||
# state and can't be deleted; we had some trouble in the past
|
||||
# with zope.interface for example
|
||||
if not name.startswith("zope"):
|
||||
del sys.modules[name]
|
||||
def __take_sys_modules_snapshot(self):
|
||||
# some zope modules used by twisted-related tests keep internal state
|
||||
# and can't be deleted; we had some trouble in the past with
|
||||
# `zope.interface` for example
|
||||
def preserve_module(name):
|
||||
return name.startswith("zope")
|
||||
return SysModulesSnapshot(preserve=preserve_module)
|
||||
|
||||
def make_hook_recorder(self, pluginmanager):
|
||||
"""Create a new :py:class:`HookRecorder` for a PluginManager."""
|
||||
@@ -482,33 +497,26 @@ class Testdir:
|
||||
This is done automatically upon instantiation.
|
||||
|
||||
"""
|
||||
old = self.tmpdir.chdir()
|
||||
if not hasattr(self, '_olddir'):
|
||||
self._olddir = old
|
||||
self.tmpdir.chdir()
|
||||
|
||||
def _makefile(self, ext, args, kwargs, encoding="utf-8"):
|
||||
def _makefile(self, ext, args, kwargs, encoding='utf-8'):
|
||||
items = list(kwargs.items())
|
||||
|
||||
def to_text(s):
|
||||
return s.decode(encoding) if isinstance(s, bytes) else six.text_type(s)
|
||||
|
||||
if args:
|
||||
source = py.builtin._totext("\n").join(
|
||||
map(py.builtin._totext, args)) + py.builtin._totext("\n")
|
||||
source = u"\n".join(to_text(x) for x in args)
|
||||
basename = self.request.function.__name__
|
||||
items.insert(0, (basename, source))
|
||||
|
||||
ret = None
|
||||
for name, value in items:
|
||||
p = self.tmpdir.join(name).new(ext=ext)
|
||||
for basename, value in items:
|
||||
p = self.tmpdir.join(basename).new(ext=ext)
|
||||
p.dirpath().ensure_dir()
|
||||
source = Source(value)
|
||||
|
||||
def my_totext(s, encoding="utf-8"):
|
||||
if py.builtin._isbytes(s):
|
||||
s = py.builtin._totext(s, encoding=encoding)
|
||||
return s
|
||||
|
||||
source_unicode = "\n".join([my_totext(line) for line in source.lines])
|
||||
source = py.builtin._totext(source_unicode)
|
||||
content = source.strip().encode(encoding) # + "\n"
|
||||
# content = content.rstrip() + "\n"
|
||||
p.write(content, "wb")
|
||||
source = u"\n".join(to_text(line) for line in source.lines)
|
||||
p.write(source.strip().encode(encoding), "wb")
|
||||
if ret is None:
|
||||
ret = p
|
||||
return ret
|
||||
@@ -516,17 +524,15 @@ class Testdir:
|
||||
def makefile(self, ext, *args, **kwargs):
|
||||
"""Create a new file in the testdir.
|
||||
|
||||
ext: The extension the file should use, including the dot.
|
||||
E.g. ".py".
|
||||
ext: The extension the file should use, including the dot, e.g. `.py`.
|
||||
|
||||
args: All args will be treated as strings and joined using
|
||||
newlines. The result will be written as contents to the
|
||||
file. The name of the file will be based on the test
|
||||
function requesting this fixture.
|
||||
args: All args will be treated as strings and joined using newlines.
|
||||
The result will be written as contents to the file. The name of the
|
||||
file will be based on the test function requesting this fixture.
|
||||
E.g. "testdir.makefile('.txt', 'line1', 'line2')"
|
||||
|
||||
kwargs: Each keyword is the name of a file, while the value of
|
||||
it will be written as contents of the file.
|
||||
kwargs: Each keyword is the name of a file, while the value of it will
|
||||
be written as contents of the file.
|
||||
E.g. "testdir.makefile('.ini', pytest='[pytest]\naddopts=-rs\n')"
|
||||
|
||||
"""
|
||||
@@ -556,14 +562,16 @@ class Testdir:
|
||||
def syspathinsert(self, path=None):
|
||||
"""Prepend a directory to sys.path, defaults to :py:attr:`tmpdir`.
|
||||
|
||||
This is undone automatically after the test.
|
||||
This is undone automatically when this object dies at the end of each
|
||||
test.
|
||||
|
||||
"""
|
||||
if path is None:
|
||||
path = self.tmpdir
|
||||
sys.path.insert(0, str(path))
|
||||
# a call to syspathinsert() usually means that the caller
|
||||
# wants to import some dynamically created files.
|
||||
# with python3 we thus invalidate import caches.
|
||||
# a call to syspathinsert() usually means that the caller wants to
|
||||
# import some dynamically created files, thus with python3 we
|
||||
# invalidate its import caches
|
||||
self._possibly_invalidate_import_caches()
|
||||
|
||||
def _possibly_invalidate_import_caches(self):
|
||||
@@ -583,8 +591,8 @@ class Testdir:
|
||||
def mkpydir(self, name):
|
||||
"""Create a new python package.
|
||||
|
||||
This creates a (sub)directory with an empty ``__init__.py``
|
||||
file so that is recognised as a python package.
|
||||
This creates a (sub)directory with an empty ``__init__.py`` file so it
|
||||
gets recognised as a python package.
|
||||
|
||||
"""
|
||||
p = self.mkdir(name)
|
||||
@@ -597,10 +605,10 @@ class Testdir:
|
||||
"""Return the collection node of a file.
|
||||
|
||||
:param config: :py:class:`_pytest.config.Config` instance, see
|
||||
:py:meth:`parseconfig` and :py:meth:`parseconfigure` to
|
||||
create the configuration.
|
||||
:py:meth:`parseconfig` and :py:meth:`parseconfigure` to create the
|
||||
configuration
|
||||
|
||||
:param arg: A :py:class:`py.path.local` instance of the file.
|
||||
:param arg: a :py:class:`py.path.local` instance of the file
|
||||
|
||||
"""
|
||||
session = Session(config)
|
||||
@@ -614,11 +622,10 @@ class Testdir:
|
||||
def getpathnode(self, path):
|
||||
"""Return the collection node of a file.
|
||||
|
||||
This is like :py:meth:`getnode` but uses
|
||||
:py:meth:`parseconfigure` to create the (configured) pytest
|
||||
Config instance.
|
||||
This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to
|
||||
create the (configured) pytest Config instance.
|
||||
|
||||
:param path: A :py:class:`py.path.local` instance of the file.
|
||||
:param path: a :py:class:`py.path.local` instance of the file
|
||||
|
||||
"""
|
||||
config = self.parseconfigure(path)
|
||||
@@ -632,8 +639,8 @@ class Testdir:
|
||||
def genitems(self, colitems):
|
||||
"""Generate all test items from a collection node.
|
||||
|
||||
This recurses into the collection node and returns a list of
|
||||
all the test items contained within.
|
||||
This recurses into the collection node and returns a list of all the
|
||||
test items contained within.
|
||||
|
||||
"""
|
||||
session = colitems[0].session
|
||||
@@ -645,10 +652,10 @@ class Testdir:
|
||||
def runitem(self, source):
|
||||
"""Run the "test_func" Item.
|
||||
|
||||
The calling test instance (the class which contains the test
|
||||
method) must provide a ``.getrunner()`` method which should
|
||||
return a runner which can run the test protocol for a single
|
||||
item, like e.g. :py:func:`_pytest.runner.runtestprotocol`.
|
||||
The calling test instance (class containing the test method) must
|
||||
provide a ``.getrunner()`` method which should return a runner which
|
||||
can run the test protocol for a single item, e.g.
|
||||
:py:func:`_pytest.runner.runtestprotocol`.
|
||||
|
||||
"""
|
||||
# used from runner functional tests
|
||||
@@ -662,14 +669,14 @@ class Testdir:
|
||||
"""Run a test module in process using ``pytest.main()``.
|
||||
|
||||
This run writes "source" into a temporary file and runs
|
||||
``pytest.main()`` on it, returning a :py:class:`HookRecorder`
|
||||
instance for the result.
|
||||
``pytest.main()`` on it, returning a :py:class:`HookRecorder` instance
|
||||
for the result.
|
||||
|
||||
:param source: The source code of the test module.
|
||||
:param source: the source code of the test module
|
||||
|
||||
:param cmdlineargs: Any extra command line arguments to use.
|
||||
:param cmdlineargs: any extra command line arguments to use
|
||||
|
||||
:return: :py:class:`HookRecorder` instance of the result.
|
||||
:return: :py:class:`HookRecorder` instance of the result
|
||||
|
||||
"""
|
||||
p = self.makepyfile(source)
|
||||
@@ -679,13 +686,9 @@ class Testdir:
|
||||
def inline_genitems(self, *args):
|
||||
"""Run ``pytest.main(['--collectonly'])`` in-process.
|
||||
|
||||
Returns a tuple of the collected items and a
|
||||
:py:class:`HookRecorder` instance.
|
||||
|
||||
This runs the :py:func:`pytest.main` function to run all of
|
||||
pytest inside the test process itself like
|
||||
:py:meth:`inline_run`. However the return value is a tuple of
|
||||
the collection items and a :py:class:`HookRecorder` instance.
|
||||
Runs the :py:func:`pytest.main` function to run all of pytest inside
|
||||
the test process itself like :py:meth:`inline_run`, but returns a
|
||||
tuple of the collected items and a :py:class:`HookRecorder` instance.
|
||||
|
||||
"""
|
||||
rec = self.inline_run("--collect-only", *args)
|
||||
@@ -695,60 +698,78 @@ class Testdir:
|
||||
def inline_run(self, *args, **kwargs):
|
||||
"""Run ``pytest.main()`` in-process, returning a HookRecorder.
|
||||
|
||||
This runs the :py:func:`pytest.main` function to run all of
|
||||
pytest inside the test process itself. This means it can
|
||||
return a :py:class:`HookRecorder` instance which gives more
|
||||
detailed results from then run then can be done by matching
|
||||
stdout/stderr from :py:meth:`runpytest`.
|
||||
Runs the :py:func:`pytest.main` function to run all of pytest inside
|
||||
the test process itself. This means it can return a
|
||||
:py:class:`HookRecorder` instance which gives more detailed results
|
||||
from that run than can be done by matching stdout/stderr from
|
||||
:py:meth:`runpytest`.
|
||||
|
||||
:param args: Any command line arguments to pass to
|
||||
:py:func:`pytest.main`.
|
||||
:param args: command line arguments to pass to :py:func:`pytest.main`
|
||||
|
||||
:param plugin: (keyword-only) Extra plugin instances the
|
||||
``pytest.main()`` instance should use.
|
||||
:param plugin: (keyword-only) extra plugin instances the
|
||||
``pytest.main()`` instance should use
|
||||
|
||||
:return: a :py:class:`HookRecorder` instance
|
||||
|
||||
:return: A :py:class:`HookRecorder` instance.
|
||||
"""
|
||||
# When running py.test inline any plugins active in the main
|
||||
# test process are already imported. So this disables the
|
||||
# warning which will trigger to say they can no longer be
|
||||
# re-written, which is fine as they are already re-written.
|
||||
orig_warn = AssertionRewritingHook._warn_already_imported
|
||||
finalizers = []
|
||||
try:
|
||||
# When running py.test inline any plugins active in the main test
|
||||
# process are already imported. So this disables the warning which
|
||||
# will trigger to say they can no longer be rewritten, which is
|
||||
# fine as they have already been rewritten.
|
||||
orig_warn = AssertionRewritingHook._warn_already_imported
|
||||
|
||||
def revert():
|
||||
AssertionRewritingHook._warn_already_imported = orig_warn
|
||||
def revert_warn_already_imported():
|
||||
AssertionRewritingHook._warn_already_imported = orig_warn
|
||||
finalizers.append(revert_warn_already_imported)
|
||||
AssertionRewritingHook._warn_already_imported = lambda *a: None
|
||||
|
||||
self.request.addfinalizer(revert)
|
||||
AssertionRewritingHook._warn_already_imported = lambda *a: None
|
||||
# Any sys.module or sys.path changes done while running py.test
|
||||
# inline should be reverted after the test run completes to avoid
|
||||
# clashing with later inline tests run within the same pytest test,
|
||||
# e.g. just because they use matching test module names.
|
||||
finalizers.append(self.__take_sys_modules_snapshot().restore)
|
||||
finalizers.append(SysPathsSnapshot().restore)
|
||||
|
||||
rec = []
|
||||
# Important note:
|
||||
# - our tests should not leave any other references/registrations
|
||||
# laying around other than possibly loaded test modules
|
||||
# referenced from sys.modules, as nothing will clean those up
|
||||
# automatically
|
||||
|
||||
class Collect:
|
||||
def pytest_configure(x, config):
|
||||
rec.append(self.make_hook_recorder(config.pluginmanager))
|
||||
rec = []
|
||||
|
||||
plugins = kwargs.get("plugins") or []
|
||||
plugins.append(Collect())
|
||||
ret = pytest.main(list(args), plugins=plugins)
|
||||
self.delete_loaded_modules()
|
||||
if len(rec) == 1:
|
||||
reprec = rec.pop()
|
||||
else:
|
||||
class reprec:
|
||||
pass
|
||||
reprec.ret = ret
|
||||
class Collect(object):
|
||||
def pytest_configure(x, config):
|
||||
rec.append(self.make_hook_recorder(config.pluginmanager))
|
||||
|
||||
# typically we reraise keyboard interrupts from the child run
|
||||
# because it's our user requesting interruption of the testing
|
||||
if ret == 2 and not kwargs.get("no_reraise_ctrlc"):
|
||||
calls = reprec.getcalls("pytest_keyboard_interrupt")
|
||||
if calls and calls[-1].excinfo.type == KeyboardInterrupt:
|
||||
raise KeyboardInterrupt()
|
||||
return reprec
|
||||
plugins = kwargs.get("plugins") or []
|
||||
plugins.append(Collect())
|
||||
ret = pytest.main(list(args), plugins=plugins)
|
||||
if len(rec) == 1:
|
||||
reprec = rec.pop()
|
||||
else:
|
||||
class reprec(object):
|
||||
pass
|
||||
reprec.ret = ret
|
||||
|
||||
# typically we reraise keyboard interrupts from the child run
|
||||
# because it's our user requesting interruption of the testing
|
||||
if ret == 2 and not kwargs.get("no_reraise_ctrlc"):
|
||||
calls = reprec.getcalls("pytest_keyboard_interrupt")
|
||||
if calls and calls[-1].excinfo.type == KeyboardInterrupt:
|
||||
raise KeyboardInterrupt()
|
||||
return reprec
|
||||
finally:
|
||||
for finalizer in finalizers:
|
||||
finalizer()
|
||||
|
||||
def runpytest_inprocess(self, *args, **kwargs):
|
||||
""" Return result of running pytest in-process, providing a similar
|
||||
interface to what self.runpytest() provides. """
|
||||
"""Return result of running pytest in-process, providing a similar
|
||||
interface to what self.runpytest() provides.
|
||||
|
||||
"""
|
||||
if kwargs.get("syspathinsert"):
|
||||
self.syspathinsert()
|
||||
now = time.time()
|
||||
@@ -759,13 +780,13 @@ class Testdir:
|
||||
reprec = self.inline_run(*args, **kwargs)
|
||||
except SystemExit as e:
|
||||
|
||||
class reprec:
|
||||
class reprec(object):
|
||||
ret = e.args[0]
|
||||
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
|
||||
class reprec:
|
||||
class reprec(object):
|
||||
ret = 3
|
||||
finally:
|
||||
out, err = capture.readouterr()
|
||||
@@ -780,7 +801,7 @@ class Testdir:
|
||||
return res
|
||||
|
||||
def runpytest(self, *args, **kwargs):
|
||||
""" Run pytest inline or in a subprocess, depending on the command line
|
||||
"""Run pytest inline or in a subprocess, depending on the command line
|
||||
option "--runpytest" and return a :py:class:`RunResult`.
|
||||
|
||||
"""
|
||||
@@ -801,13 +822,13 @@ class Testdir:
|
||||
def parseconfig(self, *args):
|
||||
"""Return a new pytest Config instance from given commandline args.
|
||||
|
||||
This invokes the pytest bootstrapping code in _pytest.config
|
||||
to create a new :py:class:`_pytest.core.PluginManager` and
|
||||
call the pytest_cmdline_parse hook to create new
|
||||
This invokes the pytest bootstrapping code in _pytest.config to create
|
||||
a new :py:class:`_pytest.core.PluginManager` and call the
|
||||
pytest_cmdline_parse hook to create a new
|
||||
:py:class:`_pytest.config.Config` instance.
|
||||
|
||||
If :py:attr:`plugins` has been populated they should be plugin
|
||||
modules which will be registered with the PluginManager.
|
||||
If :py:attr:`plugins` has been populated they should be plugin modules
|
||||
to be registered with the PluginManager.
|
||||
|
||||
"""
|
||||
args = self._ensure_basetemp(args)
|
||||
@@ -823,9 +844,8 @@ class Testdir:
|
||||
def parseconfigure(self, *args):
|
||||
"""Return a new pytest configured Config instance.
|
||||
|
||||
This returns a new :py:class:`_pytest.config.Config` instance
|
||||
like :py:meth:`parseconfig`, but also calls the
|
||||
pytest_configure hook.
|
||||
This returns a new :py:class:`_pytest.config.Config` instance like
|
||||
:py:meth:`parseconfig`, but also calls the pytest_configure hook.
|
||||
|
||||
"""
|
||||
config = self.parseconfig(*args)
|
||||
@@ -836,14 +856,14 @@ class Testdir:
|
||||
def getitem(self, source, funcname="test_func"):
|
||||
"""Return the test item for a test function.
|
||||
|
||||
This writes the source to a python file and runs pytest's
|
||||
collection on the resulting module, returning the test item
|
||||
for the requested function name.
|
||||
This writes the source to a python file and runs pytest's collection on
|
||||
the resulting module, returning the test item for the requested
|
||||
function name.
|
||||
|
||||
:param source: The module source.
|
||||
:param source: the module source
|
||||
|
||||
:param funcname: The name of the test function for which the
|
||||
Item must be returned.
|
||||
:param funcname: the name of the test function for which to return a
|
||||
test item
|
||||
|
||||
"""
|
||||
items = self.getitems(source)
|
||||
@@ -856,9 +876,8 @@ class Testdir:
|
||||
def getitems(self, source):
|
||||
"""Return all test items collected from the module.
|
||||
|
||||
This writes the source to a python file and runs pytest's
|
||||
collection on the resulting module, returning all test items
|
||||
contained within.
|
||||
This writes the source to a python file and runs pytest's collection on
|
||||
the resulting module, returning all test items contained within.
|
||||
|
||||
"""
|
||||
modcol = self.getmodulecol(source)
|
||||
@@ -867,17 +886,17 @@ class Testdir:
|
||||
def getmodulecol(self, source, configargs=(), withinit=False):
|
||||
"""Return the module collection node for ``source``.
|
||||
|
||||
This writes ``source`` to a file using :py:meth:`makepyfile`
|
||||
and then runs the pytest collection on it, returning the
|
||||
collection node for the test module.
|
||||
This writes ``source`` to a file using :py:meth:`makepyfile` and then
|
||||
runs the pytest collection on it, returning the collection node for the
|
||||
test module.
|
||||
|
||||
:param source: The source code of the module to collect.
|
||||
:param source: the source code of the module to collect
|
||||
|
||||
:param configargs: Any extra arguments to pass to
|
||||
:py:meth:`parseconfigure`.
|
||||
:param configargs: any extra arguments to pass to
|
||||
:py:meth:`parseconfigure`
|
||||
|
||||
:param withinit: Whether to also write a ``__init__.py`` file
|
||||
to the temporary directory to ensure it is a package.
|
||||
:param withinit: whether to also write an ``__init__.py`` file to the
|
||||
same directory to ensure it is a package
|
||||
|
||||
"""
|
||||
kw = {self.request.function.__name__: Source(source).strip()}
|
||||
@@ -892,13 +911,12 @@ class Testdir:
|
||||
def collect_by_name(self, modcol, name):
|
||||
"""Return the collection node for name from the module collection.
|
||||
|
||||
This will search a module collection node for a collection
|
||||
node matching the given name.
|
||||
This will search a module collection node for a collection node
|
||||
matching the given name.
|
||||
|
||||
:param modcol: A module collection node, see
|
||||
:py:meth:`getmodulecol`.
|
||||
:param modcol: a module collection node; see :py:meth:`getmodulecol`
|
||||
|
||||
:param name: The name of the node to return.
|
||||
:param name: the name of the node to return
|
||||
|
||||
"""
|
||||
if modcol not in self._mod_collections:
|
||||
@@ -910,8 +928,8 @@ class Testdir:
|
||||
def popen(self, cmdargs, stdout, stderr, **kw):
|
||||
"""Invoke subprocess.Popen.
|
||||
|
||||
This calls subprocess.Popen making sure the current working
|
||||
directory is the PYTHONPATH.
|
||||
This calls subprocess.Popen making sure the current working directory
|
||||
is in the PYTHONPATH.
|
||||
|
||||
You probably want to use :py:meth:`run` instead.
|
||||
|
||||
@@ -929,8 +947,7 @@ class Testdir:
|
||||
def run(self, *cmdargs):
|
||||
"""Run a command with arguments.
|
||||
|
||||
Run a process using subprocess.Popen saving the stdout and
|
||||
stderr.
|
||||
Run a process using subprocess.Popen saving the stdout and stderr.
|
||||
|
||||
Returns a :py:class:`RunResult`.
|
||||
|
||||
@@ -973,14 +990,15 @@ class Testdir:
|
||||
print("couldn't print to %s because of encoding" % (fp,))
|
||||
|
||||
def _getpytestargs(self):
|
||||
# we cannot use "(sys.executable,script)"
|
||||
# because on windows the script is e.g. a pytest.exe
|
||||
return (sys.executable, _pytest_fullpath,) # noqa
|
||||
# we cannot use `(sys.executable, script)` because on Windows the
|
||||
# script is e.g. `pytest.exe`
|
||||
return (sys.executable, PYTEST_FULLPATH) # noqa
|
||||
|
||||
def runpython(self, script):
|
||||
"""Run a python script using sys.executable as interpreter.
|
||||
|
||||
Returns a :py:class:`RunResult`.
|
||||
|
||||
"""
|
||||
return self.run(sys.executable, script)
|
||||
|
||||
@@ -991,25 +1009,18 @@ class Testdir:
|
||||
def runpytest_subprocess(self, *args, **kwargs):
|
||||
"""Run pytest as a subprocess with given arguments.
|
||||
|
||||
Any plugins added to the :py:attr:`plugins` list will added
|
||||
using the ``-p`` command line option. Addtionally
|
||||
``--basetemp`` is used put any temporary files and directories
|
||||
in a numbered directory prefixed with "runpytest-" so they do
|
||||
not conflict with the normal numberd pytest location for
|
||||
temporary files and directories.
|
||||
Any plugins added to the :py:attr:`plugins` list will added using the
|
||||
``-p`` command line option. Additionally ``--basetemp`` is used put
|
||||
any temporary files and directories in a numbered directory prefixed
|
||||
with "runpytest-" so they do not conflict with the normal numbered
|
||||
pytest location for temporary files and directories.
|
||||
|
||||
Returns a :py:class:`RunResult`.
|
||||
|
||||
"""
|
||||
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
|
||||
args = ('--basetemp=%s' % p,) + args
|
||||
plugins = [x for x in self.plugins if isinstance(x, str)]
|
||||
if plugins:
|
||||
args = ('-p', plugins[0]) + args
|
||||
@@ -1019,8 +1030,8 @@ class Testdir:
|
||||
def spawn_pytest(self, string, expect_timeout=10.0):
|
||||
"""Run pytest using pexpect.
|
||||
|
||||
This makes sure to use the right pytest and sets up the
|
||||
temporary directory locations.
|
||||
This makes sure to use the right pytest and sets up the temporary
|
||||
directory locations.
|
||||
|
||||
The pexpect child is returned.
|
||||
|
||||
@@ -1034,6 +1045,7 @@ class Testdir:
|
||||
"""Run a command using pexpect.
|
||||
|
||||
The pexpect child is returned.
|
||||
|
||||
"""
|
||||
pexpect = pytest.importorskip("pexpect", "3.0")
|
||||
if hasattr(sys, 'pypy_version_info') and '64' in platform.machine():
|
||||
@@ -1055,13 +1067,15 @@ def getdecoded(out):
|
||||
py.io.saferepr(out),)
|
||||
|
||||
|
||||
class LineComp:
|
||||
class LineComp(object):
|
||||
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.
|
||||
"""Assert that lines2 are contained (linearly) in lines1.
|
||||
|
||||
Return a list of extralines found.
|
||||
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
val = self.stringio.getvalue()
|
||||
@@ -1071,14 +1085,14 @@ class LineComp:
|
||||
return LineMatcher(lines1).fnmatch_lines(lines2)
|
||||
|
||||
|
||||
class LineMatcher:
|
||||
class LineMatcher(object):
|
||||
"""Flexible matching of text.
|
||||
|
||||
This is a convenience class to test large texts like the output of
|
||||
commands.
|
||||
|
||||
The constructor takes a list of lines without their trailing
|
||||
newlines, i.e. ``text.splitlines()``.
|
||||
The constructor takes a list of lines without their trailing newlines, i.e.
|
||||
``text.splitlines()``.
|
||||
|
||||
"""
|
||||
|
||||
@@ -1098,16 +1112,34 @@ class LineMatcher:
|
||||
return lines2
|
||||
|
||||
def fnmatch_lines_random(self, lines2):
|
||||
"""Check lines exist in the output using in any order.
|
||||
|
||||
Lines are checked using ``fnmatch.fnmatch``. The argument is a list of
|
||||
lines which have to occur in the output, in any order.
|
||||
|
||||
"""
|
||||
self._match_lines_random(lines2, fnmatch)
|
||||
|
||||
def re_match_lines_random(self, lines2):
|
||||
"""Check lines exist in the output using ``re.match``, in any order.
|
||||
|
||||
The argument is a list of lines which have to occur in the output, in
|
||||
any order.
|
||||
|
||||
"""
|
||||
self._match_lines_random(lines2, lambda name, pat: re.match(pat, name))
|
||||
|
||||
def _match_lines_random(self, lines2, match_func):
|
||||
"""Check lines exist in the output.
|
||||
|
||||
The argument is a list of lines which have to occur in the
|
||||
output, in any order. Each line can contain glob whildcards.
|
||||
The argument is a list of lines which have to occur in the output, in
|
||||
any order. Each line can contain glob whildcards.
|
||||
|
||||
"""
|
||||
lines2 = self._getlines(lines2)
|
||||
for line in lines2:
|
||||
for x in self.lines:
|
||||
if line == x or fnmatch(x, line):
|
||||
if line == x or match_func(x, line):
|
||||
self._log("matched: ", repr(line))
|
||||
break
|
||||
else:
|
||||
@@ -1118,6 +1150,7 @@ class LineMatcher:
|
||||
"""Return all lines following the given line in the text.
|
||||
|
||||
The given line can contain glob wildcards.
|
||||
|
||||
"""
|
||||
for i, line in enumerate(self.lines):
|
||||
if fnline == line or fnmatch(line, fnline):
|
||||
@@ -1132,12 +1165,36 @@ class LineMatcher:
|
||||
return '\n'.join(self._log_output)
|
||||
|
||||
def fnmatch_lines(self, lines2):
|
||||
"""Search the text for matching lines.
|
||||
"""Search captured text for matching lines using ``fnmatch.fnmatch``.
|
||||
|
||||
The argument is a list of lines which have to match and can
|
||||
use glob wildcards. If they do not match an pytest.fail() is
|
||||
called. The matches and non-matches are also printed on
|
||||
stdout.
|
||||
The argument is a list of lines which have to match and can use glob
|
||||
wildcards. If they do not match a pytest.fail() is called. The
|
||||
matches and non-matches are also printed on stdout.
|
||||
|
||||
"""
|
||||
self._match_lines(lines2, fnmatch, 'fnmatch')
|
||||
|
||||
def re_match_lines(self, lines2):
|
||||
"""Search captured text for matching lines using ``re.match``.
|
||||
|
||||
The argument is a list of lines which have to match using ``re.match``.
|
||||
If they do not match a pytest.fail() is called.
|
||||
|
||||
The matches and non-matches are also printed on stdout.
|
||||
|
||||
"""
|
||||
self._match_lines(lines2, lambda name, pat: re.match(pat, name), 're.match')
|
||||
|
||||
def _match_lines(self, lines2, match_func, match_nickname):
|
||||
"""Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``.
|
||||
|
||||
:param list[str] lines2: list of string patterns to match. The actual
|
||||
format depends on ``match_func``
|
||||
:param match_func: a callable ``match_func(line, pattern)`` where line
|
||||
is the captured line from stdout/stderr and pattern is the matching
|
||||
pattern
|
||||
:param str match_nickname: the nickname for the match function that
|
||||
will be logged to stdout when a match occurs
|
||||
|
||||
"""
|
||||
lines2 = self._getlines(lines2)
|
||||
@@ -1152,8 +1209,8 @@ class LineMatcher:
|
||||
if line == nextline:
|
||||
self._log("exact match:", repr(line))
|
||||
break
|
||||
elif fnmatch(nextline, line):
|
||||
self._log("fnmatch:", repr(line))
|
||||
elif match_func(nextline, line):
|
||||
self._log("%s:" % match_nickname, repr(line))
|
||||
self._log(" with:", repr(nextline))
|
||||
break
|
||||
else:
|
||||
|
||||
@@ -6,19 +6,23 @@ import inspect
|
||||
import sys
|
||||
import os
|
||||
import collections
|
||||
import warnings
|
||||
from textwrap import dedent
|
||||
from itertools import count
|
||||
|
||||
|
||||
import py
|
||||
import six
|
||||
from _pytest.mark import MarkerError
|
||||
from _pytest.config import hookimpl
|
||||
|
||||
import _pytest
|
||||
import _pytest._pluggy as pluggy
|
||||
import pluggy
|
||||
from _pytest import fixtures
|
||||
from _pytest import main
|
||||
from _pytest import nodes
|
||||
from _pytest import deprecated
|
||||
from _pytest.compat import (
|
||||
isclass, isfunction, is_generator, _ascii_escaped,
|
||||
isclass, isfunction, is_generator, ascii_escaped,
|
||||
REGEX_TYPE, STRING_TYPES, NoneType, NOTSET,
|
||||
get_real_func, getfslineno, safe_getattr,
|
||||
safe_str, getlocation, enum,
|
||||
@@ -26,9 +30,17 @@ from _pytest.compat import (
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.mark import transfer_markers
|
||||
|
||||
cutdir1 = py.path.local(pluggy.__file__.rstrip("oc"))
|
||||
cutdir2 = py.path.local(_pytest.__file__).dirpath()
|
||||
cutdir3 = py.path.local(py.__file__).dirpath()
|
||||
|
||||
# relative paths that we use to filter traceback entries from appearing to the user;
|
||||
# see filter_traceback
|
||||
# note: if we need to add more paths than what we have now we should probably use a list
|
||||
# for better maintenance
|
||||
_pluggy_dir = py.path.local(pluggy.__file__.rstrip("oc"))
|
||||
# pluggy is either a package or a single module depending on the version
|
||||
if _pluggy_dir.basename == '__init__.py':
|
||||
_pluggy_dir = _pluggy_dir.dirpath()
|
||||
_pytest_dir = py.path.local(_pytest.__file__).dirpath()
|
||||
_py_dir = py.path.local(py.__file__).dirpath()
|
||||
|
||||
|
||||
def filter_traceback(entry):
|
||||
@@ -43,10 +55,10 @@ def filter_traceback(entry):
|
||||
is_generated = '<' in raw_filename and '>' in raw_filename
|
||||
if is_generated:
|
||||
return False
|
||||
# entry.path might point to an inexisting file, in which case it will
|
||||
# alsso return a str object. see #1133
|
||||
# entry.path might point to an non-existing file, in which case it will
|
||||
# also return a str object. see #1133
|
||||
p = py.path.local(entry.path)
|
||||
return p != cutdir1 and not p.relto(cutdir2) and not p.relto(cutdir3)
|
||||
return not p.relto(_pluggy_dir) and not p.relto(_pytest_dir) and not p.relto(_py_dir)
|
||||
|
||||
|
||||
def pyobj_property(name):
|
||||
@@ -257,7 +269,7 @@ class PyobjMixin(PyobjContext):
|
||||
return fspath, lineno, modpath
|
||||
|
||||
|
||||
class PyCollector(PyobjMixin, main.Collector):
|
||||
class PyCollector(PyobjMixin, nodes.Collector):
|
||||
|
||||
def funcnamefilter(self, name):
|
||||
return self._matches_prefix_or_glob_option('python_functions', name)
|
||||
@@ -327,7 +339,7 @@ class PyCollector(PyobjMixin, main.Collector):
|
||||
if name in seen:
|
||||
continue
|
||||
seen[name] = True
|
||||
res = self.makeitem(name, obj)
|
||||
res = self._makeitem(name, obj)
|
||||
if res is None:
|
||||
continue
|
||||
if not isinstance(res, list):
|
||||
@@ -337,6 +349,10 @@ class PyCollector(PyobjMixin, main.Collector):
|
||||
return values
|
||||
|
||||
def makeitem(self, name, obj):
|
||||
warnings.warn(deprecated.COLLECTOR_MAKEITEM, stacklevel=2)
|
||||
self._makeitem(name, obj)
|
||||
|
||||
def _makeitem(self, name, obj):
|
||||
# assert self.ihook.fspath == self.fspath, self
|
||||
return self.ihook.pytest_pycollect_makeitem(
|
||||
collector=self, name=name, obj=obj)
|
||||
@@ -378,7 +394,7 @@ class PyCollector(PyobjMixin, main.Collector):
|
||||
)
|
||||
|
||||
|
||||
class Module(main.File, PyCollector):
|
||||
class Module(nodes.File, PyCollector):
|
||||
""" Collector for test classes and functions. """
|
||||
|
||||
def _getobj(self):
|
||||
@@ -555,7 +571,6 @@ class FunctionMixin(PyobjMixin):
|
||||
if ntraceback == traceback:
|
||||
ntraceback = ntraceback.cut(path=path)
|
||||
if ntraceback == traceback:
|
||||
# ntraceback = ntraceback.cut(excludepath=cutdir2)
|
||||
ntraceback = ntraceback.filter(filter_traceback)
|
||||
if not ntraceback:
|
||||
ntraceback = traceback
|
||||
@@ -613,7 +628,7 @@ class Generator(FunctionMixin, PyCollector):
|
||||
if not isinstance(obj, (tuple, list)):
|
||||
obj = (obj,)
|
||||
# explicit naming
|
||||
if isinstance(obj[0], py.builtin._basestring):
|
||||
if isinstance(obj[0], six.string_types):
|
||||
name = obj[0]
|
||||
obj = obj[1:]
|
||||
else:
|
||||
@@ -644,14 +659,14 @@ class CallSpec2(object):
|
||||
self._globalid_args = set()
|
||||
self._globalparam = NOTSET
|
||||
self._arg2scopenum = {} # used for sorting parametrized resources
|
||||
self.keywords = {}
|
||||
self.marks = []
|
||||
self.indices = {}
|
||||
|
||||
def copy(self, metafunc):
|
||||
cs = CallSpec2(self.metafunc)
|
||||
cs.funcargs.update(self.funcargs)
|
||||
cs.params.update(self.params)
|
||||
cs.keywords.update(self.keywords)
|
||||
cs.marks.extend(self.marks)
|
||||
cs.indices.update(self.indices)
|
||||
cs._arg2scopenum.update(self._arg2scopenum)
|
||||
cs._idlist = list(self._idlist)
|
||||
@@ -676,8 +691,8 @@ class CallSpec2(object):
|
||||
def id(self):
|
||||
return "-".join(map(str, filter(None, self._idlist)))
|
||||
|
||||
def setmulti(self, valtypes, argnames, valset, id, keywords, scopenum,
|
||||
param_index):
|
||||
def setmulti2(self, valtypes, argnames, valset, id, marks, scopenum,
|
||||
param_index):
|
||||
for arg, val in zip(argnames, valset):
|
||||
self._checkargnotcontained(arg)
|
||||
valtype_for_arg = valtypes[arg]
|
||||
@@ -685,7 +700,7 @@ class CallSpec2(object):
|
||||
self.indices[arg] = param_index
|
||||
self._arg2scopenum[arg] = scopenum
|
||||
self._idlist.append(id)
|
||||
self.keywords.update(keywords)
|
||||
self.marks.extend(marks)
|
||||
|
||||
def setall(self, funcargs, id, param):
|
||||
for x in funcargs:
|
||||
@@ -725,7 +740,7 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
||||
self.cls = cls
|
||||
|
||||
self._calls = []
|
||||
self._ids = py.builtin.set()
|
||||
self._ids = set()
|
||||
self._arg2fixturedefs = fixtureinfo.name2fixturedefs
|
||||
|
||||
def parametrize(self, argnames, argvalues, indirect=False, ids=None,
|
||||
@@ -768,30 +783,13 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
||||
to set a dynamic scope using test context or configuration.
|
||||
"""
|
||||
from _pytest.fixtures import scope2index
|
||||
from _pytest.mark import MARK_GEN, ParameterSet
|
||||
from _pytest.mark import ParameterSet
|
||||
from py.io import saferepr
|
||||
|
||||
if not isinstance(argnames, (tuple, list)):
|
||||
argnames = [x.strip() for x in argnames.split(",") if x.strip()]
|
||||
force_tuple = len(argnames) == 1
|
||||
else:
|
||||
force_tuple = False
|
||||
parameters = [
|
||||
ParameterSet.extract_from(x, legacy_force_tuple=force_tuple)
|
||||
for x in argvalues]
|
||||
argnames, parameters = ParameterSet._for_parametrize(
|
||||
argnames, argvalues, self.function, self.config)
|
||||
del argvalues
|
||||
|
||||
if not parameters:
|
||||
fs, lineno = getfslineno(self.function)
|
||||
reason = "got empty parameter set %r, function %s at %s:%d" % (
|
||||
argnames, self.function.__name__, fs, lineno)
|
||||
mark = MARK_GEN.skip(reason=reason)
|
||||
parameters.append(ParameterSet(
|
||||
values=(NOTSET,) * len(argnames),
|
||||
marks=[mark],
|
||||
id=None,
|
||||
))
|
||||
|
||||
if scope is None:
|
||||
scope = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect)
|
||||
|
||||
@@ -827,7 +825,7 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
||||
raise ValueError('%d tests specified with %d ids' % (
|
||||
len(parameters), len(ids)))
|
||||
for id_value in ids:
|
||||
if id_value is not None and not isinstance(id_value, py.builtin._basestring):
|
||||
if id_value is not None and not isinstance(id_value, six.string_types):
|
||||
msg = 'ids must be list of strings, found: %s (type: %s)'
|
||||
raise ValueError(msg % (saferepr(id_value), type(id_value).__name__))
|
||||
ids = idmaker(argnames, parameters, idfn, ids, self.config)
|
||||
@@ -841,15 +839,19 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
||||
'equal to the number of names ({1})'.format(
|
||||
param.values, argnames))
|
||||
newcallspec = callspec.copy(self)
|
||||
newcallspec.setmulti(valtypes, argnames, param.values, a_id,
|
||||
param.deprecated_arg_dict, scopenum, param_index)
|
||||
newcallspec.setmulti2(valtypes, argnames, param.values, a_id,
|
||||
param.marks, scopenum, param_index)
|
||||
newcalls.append(newcallspec)
|
||||
self._calls = newcalls
|
||||
|
||||
def addcall(self, funcargs=None, id=NOTSET, param=NOTSET):
|
||||
""" (deprecated, use parametrize) Add a new call to the underlying
|
||||
test function during the collection phase of a test run. Note that
|
||||
request.addcall() is called during the test collection phase prior and
|
||||
""" Add a new call to the underlying test function during the collection phase of a test run.
|
||||
|
||||
.. deprecated:: 3.3
|
||||
|
||||
Use :meth:`parametrize` instead.
|
||||
|
||||
Note that request.addcall() is called during the test collection phase prior and
|
||||
independently to actual test execution. You should only use addcall()
|
||||
if you need to specify multiple arguments of a test function.
|
||||
|
||||
@@ -862,6 +864,8 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
||||
:arg param: a parameter which will be exposed to a later fixture function
|
||||
invocation through the ``request.param`` attribute.
|
||||
"""
|
||||
if self.config:
|
||||
self.config.warn('C1', message=deprecated.METAFUNC_ADD_CALL, fslocation=None)
|
||||
assert funcargs is None or isinstance(funcargs, dict)
|
||||
if funcargs is not None:
|
||||
for name in funcargs:
|
||||
@@ -921,7 +925,7 @@ def _idval(val, argname, idx, idfn, config=None):
|
||||
msg += '\nUpdate your code as this will raise an error in pytest-4.0.'
|
||||
warnings.warn(msg, DeprecationWarning)
|
||||
if s:
|
||||
return _ascii_escaped(s)
|
||||
return ascii_escaped(s)
|
||||
|
||||
if config:
|
||||
hook_id = config.hook.pytest_make_parametrize_id(
|
||||
@@ -930,14 +934,14 @@ def _idval(val, argname, idx, idfn, config=None):
|
||||
return hook_id
|
||||
|
||||
if isinstance(val, STRING_TYPES):
|
||||
return _ascii_escaped(val)
|
||||
return ascii_escaped(val)
|
||||
elif isinstance(val, (float, int, bool, NoneType)):
|
||||
return str(val)
|
||||
elif isinstance(val, REGEX_TYPE):
|
||||
return _ascii_escaped(val.pattern)
|
||||
return ascii_escaped(val.pattern)
|
||||
elif enum is not None and isinstance(val, enum.Enum):
|
||||
return str(val)
|
||||
elif isclass(val) and hasattr(val, '__name__'):
|
||||
elif (isclass(val) or isfunction(val)) and hasattr(val, '__name__'):
|
||||
return val.__name__
|
||||
return str(argname) + str(idx)
|
||||
|
||||
@@ -950,7 +954,7 @@ def _idvalset(idx, parameterset, argnames, idfn, ids, config=None):
|
||||
for val, argname in zip(parameterset.values, argnames)]
|
||||
return "-".join(this_id)
|
||||
else:
|
||||
return _ascii_escaped(ids[idx])
|
||||
return ascii_escaped(ids[idx])
|
||||
|
||||
|
||||
def idmaker(argnames, parametersets, idfn=None, ids=None, config=None):
|
||||
@@ -1094,7 +1098,7 @@ def write_docstring(tw, doc):
|
||||
tw.write(INDENT + line + "\n")
|
||||
|
||||
|
||||
class Function(FunctionMixin, main.Item, fixtures.FuncargnamesCompatAttr):
|
||||
class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr):
|
||||
""" a Function Item is responsible for setting up and executing a
|
||||
Python test function.
|
||||
"""
|
||||
@@ -1112,7 +1116,13 @@ class Function(FunctionMixin, main.Item, fixtures.FuncargnamesCompatAttr):
|
||||
self.keywords.update(self.obj.__dict__)
|
||||
if callspec:
|
||||
self.callspec = callspec
|
||||
self.keywords.update(callspec.keywords)
|
||||
# this is total hostile and a mess
|
||||
# keywords are broken by design by now
|
||||
# this will be redeemed later
|
||||
for mark in callspec.marks:
|
||||
# feel free to cry, this was broken for years before
|
||||
# and keywords cant fix it per design
|
||||
self.keywords[mark.name] = mark
|
||||
if keywords:
|
||||
self.keywords.update(keywords)
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@ import math
|
||||
import sys
|
||||
|
||||
import py
|
||||
from six.moves import zip
|
||||
|
||||
from _pytest.compat import isclass, izip
|
||||
from _pytest.compat import isclass
|
||||
from _pytest.outcomes import fail
|
||||
import _pytest._code
|
||||
|
||||
@@ -145,13 +146,15 @@ class ApproxSequence(ApproxBase):
|
||||
return ApproxBase.__eq__(self, actual)
|
||||
|
||||
def _yield_comparisons(self, actual):
|
||||
return izip(actual, self.expected)
|
||||
return zip(actual, self.expected)
|
||||
|
||||
|
||||
class ApproxScalar(ApproxBase):
|
||||
"""
|
||||
Perform approximate comparisons for single numbers only.
|
||||
"""
|
||||
DEFAULT_ABSOLUTE_TOLERANCE = 1e-12
|
||||
DEFAULT_RELATIVE_TOLERANCE = 1e-6
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
@@ -222,7 +225,7 @@ class ApproxScalar(ApproxBase):
|
||||
|
||||
# Figure out what the absolute tolerance should be. ``self.abs`` is
|
||||
# either None or a value specified by the user.
|
||||
absolute_tolerance = set_default(self.abs, 1e-12)
|
||||
absolute_tolerance = set_default(self.abs, self.DEFAULT_ABSOLUTE_TOLERANCE)
|
||||
|
||||
if absolute_tolerance < 0:
|
||||
raise ValueError("absolute tolerance can't be negative: {}".format(absolute_tolerance))
|
||||
@@ -240,7 +243,7 @@ class ApproxScalar(ApproxBase):
|
||||
# we've made sure the user didn't ask for an absolute tolerance only,
|
||||
# because we don't want to raise errors about the relative tolerance if
|
||||
# we aren't even going to use it.
|
||||
relative_tolerance = set_default(self.rel, 1e-6) * abs(self.expected)
|
||||
relative_tolerance = set_default(self.rel, self.DEFAULT_RELATIVE_TOLERANCE) * abs(self.expected)
|
||||
|
||||
if relative_tolerance < 0:
|
||||
raise ValueError("relative tolerance can't be negative: {}".format(absolute_tolerance))
|
||||
@@ -251,6 +254,13 @@ class ApproxScalar(ApproxBase):
|
||||
return max(relative_tolerance, absolute_tolerance)
|
||||
|
||||
|
||||
class ApproxDecimal(ApproxScalar):
|
||||
from decimal import Decimal
|
||||
|
||||
DEFAULT_ABSOLUTE_TOLERANCE = Decimal('1e-12')
|
||||
DEFAULT_RELATIVE_TOLERANCE = Decimal('1e-6')
|
||||
|
||||
|
||||
def approx(expected, rel=None, abs=None, nan_ok=False):
|
||||
"""
|
||||
Assert that two numbers (or two sets of numbers) are equal to each other
|
||||
@@ -400,6 +410,7 @@ def approx(expected, rel=None, abs=None, nan_ok=False):
|
||||
|
||||
from collections import Mapping, Sequence
|
||||
from _pytest.compat import STRING_TYPES as String
|
||||
from decimal import Decimal
|
||||
|
||||
# Delegate the comparison to a class that knows how to deal with the type
|
||||
# of the expected value (e.g. int, float, list, dict, numpy.array, etc).
|
||||
@@ -421,6 +432,8 @@ def approx(expected, rel=None, abs=None, nan_ok=False):
|
||||
cls = ApproxMapping
|
||||
elif isinstance(expected, Sequence) and not isinstance(expected, String):
|
||||
cls = ApproxSequence
|
||||
elif isinstance(expected, Decimal):
|
||||
cls = ApproxDecimal
|
||||
else:
|
||||
cls = ApproxScalar
|
||||
|
||||
@@ -452,10 +465,13 @@ def raises(expected_exception, *args, **kwargs):
|
||||
Assert that a code block/function call raises ``expected_exception``
|
||||
and raise a failure exception otherwise.
|
||||
|
||||
:arg message: if specified, provides a custom failure message if the
|
||||
exception is not raised
|
||||
:arg match: if specified, asserts that the exception matches a text or regex
|
||||
|
||||
This helper produces a ``ExceptionInfo()`` object (see below).
|
||||
|
||||
If using Python 2.5 or above, you may use this function as a
|
||||
context manager::
|
||||
You may use this function as a context manager::
|
||||
|
||||
>>> with raises(ZeroDivisionError):
|
||||
... 1/0
|
||||
@@ -567,7 +583,6 @@ def raises(expected_exception, *args, **kwargs):
|
||||
message = kwargs.pop("message")
|
||||
if "match" in kwargs:
|
||||
match_expr = kwargs.pop("match")
|
||||
message += " matching '{0}'".format(match_expr)
|
||||
return RaisesContext(expected_exception, message, match_expr)
|
||||
elif isinstance(args[0], str):
|
||||
code, = args
|
||||
@@ -610,17 +625,10 @@ class RaisesContext(object):
|
||||
__tracebackhide__ = True
|
||||
if tp[0] is None:
|
||||
fail(self.message)
|
||||
if sys.version_info < (2, 7):
|
||||
# py26: on __exit__() exc_value often does not contain the
|
||||
# exception value.
|
||||
# http://bugs.python.org/issue7853
|
||||
if not isinstance(tp[1], BaseException):
|
||||
exc_type, value, traceback = tp
|
||||
tp = exc_type, exc_type(value), traceback
|
||||
self.excinfo.__init__(tp)
|
||||
suppress_exception = issubclass(self.excinfo.type, self.expected_exception)
|
||||
if sys.version_info[0] == 2 and suppress_exception:
|
||||
sys.exc_clear()
|
||||
if self.match_expr:
|
||||
if self.match_expr and suppress_exception:
|
||||
self.excinfo.match(self.match_expr)
|
||||
return suppress_exception
|
||||
|
||||
@@ -8,6 +8,8 @@ import py
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
import re
|
||||
|
||||
from _pytest.fixtures import yield_fixture
|
||||
from _pytest.outcomes import fail
|
||||
|
||||
@@ -98,10 +100,28 @@ def warns(expected_warning, *args, **kwargs):
|
||||
|
||||
>>> with warns(RuntimeWarning):
|
||||
... warnings.warn("my warning", RuntimeWarning)
|
||||
|
||||
In the context manager form you may use the keyword argument ``match`` to assert
|
||||
that the exception matches a text or regex::
|
||||
|
||||
>>> with warns(UserWarning, match='must be 0 or None'):
|
||||
... warnings.warn("value must be 0 or None", UserWarning)
|
||||
|
||||
>>> with warns(UserWarning, match=r'must be \d+$'):
|
||||
... warnings.warn("value must be 42", UserWarning)
|
||||
|
||||
>>> with warns(UserWarning, match=r'must be \d+$'):
|
||||
... warnings.warn("this is not here", UserWarning)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
Failed: DID NOT WARN. No warnings of type ...UserWarning... was emitted...
|
||||
|
||||
"""
|
||||
wcheck = WarningsChecker(expected_warning)
|
||||
match_expr = None
|
||||
if not args:
|
||||
return wcheck
|
||||
if "match" in kwargs:
|
||||
match_expr = kwargs.pop("match")
|
||||
return WarningsChecker(expected_warning, match_expr=match_expr)
|
||||
elif isinstance(args[0], str):
|
||||
code, = args
|
||||
assert isinstance(code, str)
|
||||
@@ -109,12 +129,12 @@ def warns(expected_warning, *args, **kwargs):
|
||||
loc = frame.f_locals.copy()
|
||||
loc.update(kwargs)
|
||||
|
||||
with wcheck:
|
||||
with WarningsChecker(expected_warning, match_expr=match_expr):
|
||||
code = _pytest._code.Source(code).compile()
|
||||
py.builtin.exec_(code, frame.f_globals, loc)
|
||||
else:
|
||||
func = args[0]
|
||||
with wcheck:
|
||||
with WarningsChecker(expected_warning, match_expr=match_expr):
|
||||
return func(*args[1:], **kwargs)
|
||||
|
||||
|
||||
@@ -174,7 +194,7 @@ class WarningsRecorder(warnings.catch_warnings):
|
||||
|
||||
|
||||
class WarningsChecker(WarningsRecorder):
|
||||
def __init__(self, expected_warning=None):
|
||||
def __init__(self, expected_warning=None, match_expr=None):
|
||||
super(WarningsChecker, self).__init__()
|
||||
|
||||
msg = ("exceptions must be old-style classes or "
|
||||
@@ -189,6 +209,7 @@ class WarningsChecker(WarningsRecorder):
|
||||
raise TypeError(msg % type(expected_warning))
|
||||
|
||||
self.expected_warning = expected_warning
|
||||
self.match_expr = match_expr
|
||||
|
||||
def __exit__(self, *exc_info):
|
||||
super(WarningsChecker, self).__exit__(*exc_info)
|
||||
@@ -203,3 +224,13 @@ class WarningsChecker(WarningsRecorder):
|
||||
"The list of emitted warnings is: {1}.".format(
|
||||
self.expected_warning,
|
||||
[each.message for each in self]))
|
||||
elif self.match_expr is not None:
|
||||
for r in self:
|
||||
if issubclass(r.category, self.expected_warning):
|
||||
if re.compile(self.match_expr).search(str(r.message)):
|
||||
break
|
||||
else:
|
||||
fail("DID NOT WARN. No warnings of type {0} matching"
|
||||
" ('{1}') was emitted. The list of emitted warnings"
|
||||
" is: {2}.".format(self.expected_warning, self.match_expr,
|
||||
[each.message for each in self]))
|
||||
|
||||
@@ -7,7 +7,6 @@ import sys
|
||||
from time import time
|
||||
|
||||
import py
|
||||
from _pytest.compat import _PY2
|
||||
from _pytest._code.code import TerminalRepr, ExceptionInfo
|
||||
from _pytest.outcomes import skip, Skipped, TEST_OUTCOME
|
||||
|
||||
@@ -61,6 +60,9 @@ def pytest_runtest_protocol(item, nextitem):
|
||||
nodeid=item.nodeid, location=item.location,
|
||||
)
|
||||
runtestprotocol(item, nextitem=nextitem)
|
||||
item.ihook.pytest_runtest_logfinish(
|
||||
nodeid=item.nodeid, location=item.location,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@@ -131,9 +133,8 @@ def _update_current_test_var(item, when):
|
||||
var_name = 'PYTEST_CURRENT_TEST'
|
||||
if when:
|
||||
value = '{0} ({1})'.format(item.nodeid, when)
|
||||
if _PY2:
|
||||
# python 2 doesn't like null bytes on environment variables (see #2644)
|
||||
value = value.replace('\x00', '(null)')
|
||||
# don't allow null bytes on environment variables (see #2644, #2957)
|
||||
value = value.replace('\x00', '(null)')
|
||||
os.environ[var_name] = value
|
||||
else:
|
||||
os.environ.pop(var_name)
|
||||
@@ -177,7 +178,7 @@ def call_runtest_hook(item, when, **kwds):
|
||||
return CallInfo(lambda: ihook(item=item, **kwds), when=when)
|
||||
|
||||
|
||||
class CallInfo:
|
||||
class CallInfo(object):
|
||||
""" Result/Exception info a function invocation. """
|
||||
#: None or ExceptionInfo object.
|
||||
excinfo = None
|
||||
@@ -431,7 +432,7 @@ class SetupState(object):
|
||||
is called at the end of teardown_all().
|
||||
"""
|
||||
assert colitem and not isinstance(colitem, tuple)
|
||||
assert py.builtin.callable(finalizer)
|
||||
assert callable(finalizer)
|
||||
# assert colitem in self.stack # some unit tests don't setup stack :/
|
||||
self._finalizers.setdefault(colitem, []).append(finalizer)
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ def _show_fixture_action(fixturedef, msg):
|
||||
config = fixturedef._fixturemanager.config
|
||||
capman = config.pluginmanager.getplugin('capturemanager')
|
||||
if capman:
|
||||
out, err = capman.suspendcapture()
|
||||
out, err = capman.suspend_global_capture()
|
||||
|
||||
tw = config.get_terminal_writer()
|
||||
tw.line()
|
||||
@@ -63,7 +63,7 @@ def _show_fixture_action(fixturedef, msg):
|
||||
tw.write('[{0}]'.format(fixturedef.cached_param))
|
||||
|
||||
if capman:
|
||||
capman.resumecapture()
|
||||
capman.resume_global_capture()
|
||||
sys.stdout.write(out)
|
||||
sys.stderr.write(err)
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import os
|
||||
import six
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import py
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.mark import MarkInfo, MarkDecorator
|
||||
from _pytest.outcomes import fail, skip, xfail, TEST_OUTCOME
|
||||
@@ -60,22 +60,31 @@ def pytest_configure(config):
|
||||
)
|
||||
|
||||
|
||||
class MarkEvaluator:
|
||||
class MarkEvaluator(object):
|
||||
def __init__(self, item, name):
|
||||
self.item = item
|
||||
self.name = name
|
||||
|
||||
@property
|
||||
def holder(self):
|
||||
return self.item.keywords.get(self.name)
|
||||
self._marks = None
|
||||
self._mark = None
|
||||
self._mark_name = name
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self.holder)
|
||||
self._marks = self._get_marks()
|
||||
return bool(self._marks)
|
||||
__nonzero__ = __bool__
|
||||
|
||||
def wasvalid(self):
|
||||
return not hasattr(self, 'exc')
|
||||
|
||||
def _get_marks(self):
|
||||
|
||||
keyword = self.item.keywords.get(self._mark_name)
|
||||
if isinstance(keyword, MarkDecorator):
|
||||
return [keyword.mark]
|
||||
elif isinstance(keyword, MarkInfo):
|
||||
return [x.combined for x in keyword]
|
||||
else:
|
||||
return []
|
||||
|
||||
def invalidraise(self, exc):
|
||||
raises = self.get('raises')
|
||||
if not raises:
|
||||
@@ -95,7 +104,7 @@ class MarkEvaluator:
|
||||
fail("Error evaluating %r expression\n"
|
||||
" %s\n"
|
||||
"%s"
|
||||
% (self.name, self.expr, "\n".join(msg)),
|
||||
% (self._mark_name, self.expr, "\n".join(msg)),
|
||||
pytrace=False)
|
||||
|
||||
def _getglobals(self):
|
||||
@@ -107,40 +116,45 @@ class MarkEvaluator:
|
||||
def _istrue(self):
|
||||
if hasattr(self, 'result'):
|
||||
return self.result
|
||||
if self.holder:
|
||||
if self.holder.args or 'condition' in self.holder.kwargs:
|
||||
self.result = False
|
||||
# "holder" might be a MarkInfo or a MarkDecorator; only
|
||||
# MarkInfo keeps track of all parameters it received in an
|
||||
# _arglist attribute
|
||||
marks = getattr(self.holder, '_marks', None) \
|
||||
or [self.holder.mark]
|
||||
for _, args, kwargs in marks:
|
||||
if 'condition' in kwargs:
|
||||
args = (kwargs['condition'],)
|
||||
for expr in args:
|
||||
self._marks = self._get_marks()
|
||||
|
||||
if self._marks:
|
||||
self.result = False
|
||||
for mark in self._marks:
|
||||
self._mark = mark
|
||||
if 'condition' in mark.kwargs:
|
||||
args = (mark.kwargs['condition'],)
|
||||
else:
|
||||
args = mark.args
|
||||
|
||||
for expr in args:
|
||||
self.expr = expr
|
||||
if isinstance(expr, six.string_types):
|
||||
d = self._getglobals()
|
||||
result = cached_eval(self.item.config, expr, d)
|
||||
else:
|
||||
if "reason" not in mark.kwargs:
|
||||
# XXX better be checked at collection time
|
||||
msg = "you need to specify reason=STRING " \
|
||||
"when using booleans as conditions."
|
||||
fail(msg)
|
||||
result = bool(expr)
|
||||
if result:
|
||||
self.result = True
|
||||
self.reason = mark.kwargs.get('reason', None)
|
||||
self.expr = expr
|
||||
if isinstance(expr, py.builtin._basestring):
|
||||
d = self._getglobals()
|
||||
result = cached_eval(self.item.config, expr, d)
|
||||
else:
|
||||
if "reason" not in kwargs:
|
||||
# XXX better be checked at collection time
|
||||
msg = "you need to specify reason=STRING " \
|
||||
"when using booleans as conditions."
|
||||
fail(msg)
|
||||
result = bool(expr)
|
||||
if result:
|
||||
self.result = True
|
||||
self.reason = kwargs.get('reason', None)
|
||||
self.expr = expr
|
||||
return self.result
|
||||
else:
|
||||
self.result = True
|
||||
return getattr(self, 'result', False)
|
||||
return self.result
|
||||
|
||||
if not args:
|
||||
self.result = True
|
||||
self.reason = mark.kwargs.get('reason', None)
|
||||
return self.result
|
||||
return False
|
||||
|
||||
def get(self, attr, default=None):
|
||||
return self.holder.kwargs.get(attr, default)
|
||||
if self._mark is None:
|
||||
return default
|
||||
return self._mark.kwargs.get(attr, default)
|
||||
|
||||
def getexplanation(self):
|
||||
expl = getattr(self, 'reason', None) or self.get('reason', None)
|
||||
@@ -155,17 +169,17 @@ class MarkEvaluator:
|
||||
@hookimpl(tryfirst=True)
|
||||
def pytest_runtest_setup(item):
|
||||
# Check if skip or skipif are specified as pytest marks
|
||||
|
||||
item._skipped_by_mark = False
|
||||
skipif_info = item.keywords.get('skipif')
|
||||
if isinstance(skipif_info, (MarkInfo, MarkDecorator)):
|
||||
eval_skipif = MarkEvaluator(item, 'skipif')
|
||||
if eval_skipif.istrue():
|
||||
item._evalskip = eval_skipif
|
||||
item._skipped_by_mark = True
|
||||
skip(eval_skipif.getexplanation())
|
||||
|
||||
skip_info = item.keywords.get('skip')
|
||||
if isinstance(skip_info, (MarkInfo, MarkDecorator)):
|
||||
item._evalskip = True
|
||||
item._skipped_by_mark = True
|
||||
if 'reason' in skip_info.kwargs:
|
||||
skip(skip_info.kwargs['reason'])
|
||||
elif skip_info.args:
|
||||
@@ -212,7 +226,6 @@ def pytest_runtest_makereport(item, call):
|
||||
outcome = yield
|
||||
rep = outcome.get_result()
|
||||
evalxfail = getattr(item, '_evalxfail', None)
|
||||
evalskip = getattr(item, '_evalskip', None)
|
||||
# unitttest special case, see setting of _unexpectedsuccess
|
||||
if hasattr(item, '_unexpectedsuccess') and rep.when == "call":
|
||||
from _pytest.compat import _is_unittest_unexpected_success_a_failure
|
||||
@@ -248,7 +261,7 @@ def pytest_runtest_makereport(item, call):
|
||||
else:
|
||||
rep.outcome = "passed"
|
||||
rep.wasxfail = explanation
|
||||
elif evalskip is not None and rep.skipped and type(rep.longrepr) is tuple:
|
||||
elif getattr(item, '_skipped_by_mark', False) and rep.skipped and type(rep.longrepr) is tuple:
|
||||
# skipped by mark.skipif; change the location of the failure
|
||||
# to point to the item definition, otherwise it will display
|
||||
# the location of where the skip exception was raised within pytest
|
||||
@@ -345,6 +358,13 @@ def folded_skips(skipped):
|
||||
for event in skipped:
|
||||
key = event.longrepr
|
||||
assert len(key) == 3, (event, key)
|
||||
keywords = getattr(event, 'keywords', {})
|
||||
# folding reports with global pytestmark variable
|
||||
# this is workaround, because for now we cannot identify the scope of a skip marker
|
||||
# TODO: revisit after marks scope would be fixed
|
||||
when = getattr(event, 'when', None)
|
||||
if when == 'setup' and 'skip' in keywords and 'pytestmark' not in keywords:
|
||||
key = (key[0], None, key[2], )
|
||||
d.setdefault(key, []).append(event)
|
||||
values = []
|
||||
for key, events in d.items():
|
||||
@@ -367,6 +387,11 @@ def show_skipped(terminalreporter, lines):
|
||||
for num, fspath, lineno, reason in fskips:
|
||||
if reason.startswith("Skipped: "):
|
||||
reason = reason[9:]
|
||||
lines.append(
|
||||
"SKIP [%d] %s:%d: %s" %
|
||||
(num, fspath, lineno + 1, reason))
|
||||
if lineno is not None:
|
||||
lines.append(
|
||||
"SKIP [%d] %s:%d: %s" %
|
||||
(num, fspath, lineno + 1, reason))
|
||||
else:
|
||||
lines.append(
|
||||
"SKIP [%d] %s: %s" %
|
||||
(num, fspath, reason))
|
||||
|
||||
@@ -5,16 +5,18 @@ This is a good source for looking at the various reporting hooks.
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import itertools
|
||||
from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, \
|
||||
EXIT_USAGEERROR, EXIT_NOTESTSCOLLECTED
|
||||
import pytest
|
||||
import py
|
||||
import platform
|
||||
import sys
|
||||
import time
|
||||
import platform
|
||||
|
||||
import pluggy
|
||||
import py
|
||||
import six
|
||||
|
||||
import pytest
|
||||
from _pytest import nodes
|
||||
import _pytest._pluggy as pluggy
|
||||
from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, \
|
||||
EXIT_USAGEERROR, EXIT_NOTESTSCOLLECTED
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
@@ -48,6 +50,10 @@ def pytest_addoption(parser):
|
||||
choices=['yes', 'no', 'auto'],
|
||||
help="color terminal output (yes/no/auto).")
|
||||
|
||||
parser.addini("console_output_style",
|
||||
help="console output: classic or with additional progress information (classic|progress).",
|
||||
default='progress')
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
config.option.verbose -= config.option.quiet
|
||||
@@ -88,7 +94,7 @@ def pytest_report_teststatus(report):
|
||||
return report.outcome, letter, report.outcome.upper()
|
||||
|
||||
|
||||
class WarningReport:
|
||||
class WarningReport(object):
|
||||
"""
|
||||
Simple structure to hold warnings information captured by ``pytest_logwarning``.
|
||||
"""
|
||||
@@ -123,7 +129,7 @@ class WarningReport:
|
||||
return None
|
||||
|
||||
|
||||
class TerminalReporter:
|
||||
class TerminalReporter(object):
|
||||
def __init__(self, config, file=None):
|
||||
import _pytest.config
|
||||
self.config = config
|
||||
@@ -132,17 +138,32 @@ class TerminalReporter:
|
||||
self.showfspath = self.verbosity >= 0
|
||||
self.showlongtestinfo = self.verbosity > 0
|
||||
self._numcollected = 0
|
||||
self._session = None
|
||||
|
||||
self.stats = {}
|
||||
self.startdir = py.path.local()
|
||||
if file is None:
|
||||
file = sys.stdout
|
||||
self._tw = self.writer = _pytest.config.create_terminal_writer(config,
|
||||
file)
|
||||
self._tw = _pytest.config.create_terminal_writer(config, file)
|
||||
# self.writer will be deprecated in pytest-3.4
|
||||
self.writer = self._tw
|
||||
self._screen_width = self._tw.fullwidth
|
||||
self.currentfspath = None
|
||||
self.reportchars = getreportopt(config)
|
||||
self.hasmarkup = self._tw.hasmarkup
|
||||
self.isatty = file.isatty()
|
||||
self._progress_nodeids_reported = set()
|
||||
self._show_progress_info = self._determine_show_progress_info()
|
||||
|
||||
def _determine_show_progress_info(self):
|
||||
"""Return True if we should display progress information based on the current config"""
|
||||
# do not show progress if we are not capturing output (#3038)
|
||||
if self.config.getoption('capture') == 'no':
|
||||
return False
|
||||
# do not show progress if we are showing fixture setup/teardown
|
||||
if self.config.getoption('setupshow'):
|
||||
return False
|
||||
return self.config.getini('console_output_style') == 'progress'
|
||||
|
||||
def hasopt(self, char):
|
||||
char = {'xfailed': 'x', 'skipped': 's'}.get(char, char)
|
||||
@@ -151,6 +172,8 @@ class TerminalReporter:
|
||||
def write_fspath_result(self, nodeid, res):
|
||||
fspath = self.config.rootdir.join(nodeid.split("::")[0])
|
||||
if fspath != self.currentfspath:
|
||||
if self.currentfspath is not None:
|
||||
self._write_progress_information_filling_space()
|
||||
self.currentfspath = fspath
|
||||
fspath = self.startdir.bestrelpath(fspath)
|
||||
self._tw.line()
|
||||
@@ -175,8 +198,8 @@ class TerminalReporter:
|
||||
self._tw.write(content, **markup)
|
||||
|
||||
def write_line(self, line, **markup):
|
||||
if not py.builtin._istext(line):
|
||||
line = py.builtin.text(line, errors="replace")
|
||||
if not isinstance(line, six.text_type):
|
||||
line = six.text_type(line, errors="replace")
|
||||
self.ensure_newline()
|
||||
self._tw.line(line, **markup)
|
||||
|
||||
@@ -191,7 +214,7 @@ class TerminalReporter:
|
||||
"""
|
||||
erase = markup.pop('erase', False)
|
||||
if erase:
|
||||
fill_count = self._tw.fullwidth - len(line)
|
||||
fill_count = self._tw.fullwidth - len(line) - 1
|
||||
fill = ' ' * fill_count
|
||||
else:
|
||||
fill = ''
|
||||
@@ -209,7 +232,7 @@ class TerminalReporter:
|
||||
self._tw.line(msg, **kw)
|
||||
|
||||
def pytest_internalerror(self, excrepr):
|
||||
for line in py.builtin.text(excrepr).split("\n"):
|
||||
for line in six.text_type(excrepr).split("\n"):
|
||||
self.write_line("INTERNALERROR> " + line)
|
||||
return 1
|
||||
|
||||
@@ -244,38 +267,76 @@ class TerminalReporter:
|
||||
rep = report
|
||||
res = self.config.hook.pytest_report_teststatus(report=rep)
|
||||
cat, letter, word = res
|
||||
if isinstance(word, tuple):
|
||||
word, markup = word
|
||||
else:
|
||||
markup = None
|
||||
self.stats.setdefault(cat, []).append(rep)
|
||||
self._tests_ran = True
|
||||
if not letter and not word:
|
||||
# probably passed setup/teardown
|
||||
return
|
||||
running_xdist = hasattr(rep, 'node')
|
||||
if self.verbosity <= 0:
|
||||
if not hasattr(rep, 'node') and self.showfspath:
|
||||
if not running_xdist and self.showfspath:
|
||||
self.write_fspath_result(rep.nodeid, letter)
|
||||
else:
|
||||
self._tw.write(letter)
|
||||
else:
|
||||
if isinstance(word, tuple):
|
||||
word, markup = word
|
||||
else:
|
||||
self._progress_nodeids_reported.add(rep.nodeid)
|
||||
if markup is None:
|
||||
if rep.passed:
|
||||
markup = {'green': True}
|
||||
elif rep.failed:
|
||||
markup = {'red': True}
|
||||
elif rep.skipped:
|
||||
markup = {'yellow': True}
|
||||
else:
|
||||
markup = {}
|
||||
line = self._locationline(rep.nodeid, *rep.location)
|
||||
if not hasattr(rep, 'node'):
|
||||
if not running_xdist:
|
||||
self.write_ensure_prefix(line, word, **markup)
|
||||
# self._tw.write(word, **markup)
|
||||
if self._show_progress_info:
|
||||
self._write_progress_information_filling_space()
|
||||
else:
|
||||
self.ensure_newline()
|
||||
if hasattr(rep, 'node'):
|
||||
self._tw.write("[%s] " % rep.node.gateway.id)
|
||||
self._tw.write("[%s]" % rep.node.gateway.id)
|
||||
if self._show_progress_info:
|
||||
self._tw.write(self._get_progress_information_message() + " ", cyan=True)
|
||||
else:
|
||||
self._tw.write(' ')
|
||||
self._tw.write(word, **markup)
|
||||
self._tw.write(" " + line)
|
||||
self.currentfspath = -2
|
||||
|
||||
def pytest_runtest_logfinish(self, nodeid):
|
||||
if self.verbosity <= 0 and self._show_progress_info:
|
||||
self._progress_nodeids_reported.add(nodeid)
|
||||
last_item = len(self._progress_nodeids_reported) == self._session.testscollected
|
||||
if last_item:
|
||||
self._write_progress_information_filling_space()
|
||||
else:
|
||||
past_edge = self._tw.chars_on_current_line + self._PROGRESS_LENGTH + 1 >= self._screen_width
|
||||
if past_edge:
|
||||
msg = self._get_progress_information_message()
|
||||
self._tw.write(msg + '\n', cyan=True)
|
||||
|
||||
_PROGRESS_LENGTH = len(' [100%]')
|
||||
|
||||
def _get_progress_information_message(self):
|
||||
if self.config.getoption('capture') == 'no':
|
||||
return ''
|
||||
collected = self._session.testscollected
|
||||
if collected:
|
||||
progress = len(self._progress_nodeids_reported) * 100 // collected
|
||||
return ' [{:3d}%]'.format(progress)
|
||||
return ' [100%]'
|
||||
|
||||
def _write_progress_information_filling_space(self):
|
||||
msg = self._get_progress_information_message()
|
||||
fill = ' ' * (self._tw.fullwidth - self._tw.chars_on_current_line - len(msg) - 1)
|
||||
self.write(fill + msg, cyan=True)
|
||||
|
||||
def pytest_collection(self):
|
||||
if not self.isatty and self.config.option.verbose >= 1:
|
||||
self.write("collecting ... ", bold=True)
|
||||
@@ -318,6 +379,7 @@ class TerminalReporter:
|
||||
|
||||
@pytest.hookimpl(trylast=True)
|
||||
def pytest_sessionstart(self, session):
|
||||
self._session = session
|
||||
self._sessionstarttime = time.time()
|
||||
if not self.showheader:
|
||||
return
|
||||
@@ -494,9 +556,9 @@ class TerminalReporter:
|
||||
grouped = itertools.groupby(all_warnings, key=lambda wr: wr.get_location(self.config))
|
||||
|
||||
self.write_sep("=", "warnings summary", yellow=True, bold=False)
|
||||
for location, warnings in grouped:
|
||||
for location, warning_records in grouped:
|
||||
self._tw.line(str(location) or '<undetermined location>')
|
||||
for w in warnings:
|
||||
for w in warning_records:
|
||||
lines = w.message.splitlines()
|
||||
indented = '\n'.join(' ' + x for x in lines)
|
||||
self._tw.line(indented)
|
||||
|
||||
@@ -8,7 +8,7 @@ import py
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
|
||||
|
||||
class TempdirFactory:
|
||||
class TempdirFactory(object):
|
||||
"""Factory for temporary directories under the common base temp directory.
|
||||
|
||||
The base directory can be configured using the ``--basetemp`` option.
|
||||
|
||||
@@ -9,7 +9,6 @@ import _pytest._code
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.outcomes import fail, skip, xfail
|
||||
from _pytest.python import transfer_markers, Class, Module, Function
|
||||
from _pytest.skipping import MarkEvaluator
|
||||
|
||||
|
||||
def pytest_pycollect_makeitem(collector, name, obj):
|
||||
@@ -134,8 +133,7 @@ class TestCaseFunction(Function):
|
||||
try:
|
||||
skip(reason)
|
||||
except skip.Exception:
|
||||
self._evalskip = MarkEvaluator(self, 'SkipTest')
|
||||
self._evalskip.result = True
|
||||
self._skipped_by_mark = True
|
||||
self._addexcinfo(sys.exc_info())
|
||||
|
||||
def addExpectedFailure(self, testcase, rawexcinfo, reason=""):
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
This directory vendors the `pluggy` module.
|
||||
|
||||
For a more detailed discussion for the reasons to vendoring this
|
||||
package, please see [this issue](https://github.com/pytest-dev/pytest/issues/944).
|
||||
|
||||
To update the current version, execute:
|
||||
|
||||
```
|
||||
$ pip install -U pluggy==<version> --no-compile --target=_pytest/vendored_packages
|
||||
```
|
||||
|
||||
And commit the modified files. The `pluggy-<version>.dist-info` directory
|
||||
created by `pip` should be added as well.
|
||||
@@ -1,11 +0,0 @@
|
||||
|
||||
Plugin registration and hook calling for Python
|
||||
===============================================
|
||||
|
||||
This is the plugin manager as used by pytest but stripped
|
||||
of pytest specific details.
|
||||
|
||||
During the 0.x series this plugin does not have much documentation
|
||||
except extensive docstrings in the pluggy.py module.
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pip
|
||||
@@ -1,22 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 holger krekel (rather uses bitbucket/hpk42)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
Metadata-Version: 2.0
|
||||
Name: pluggy
|
||||
Version: 0.4.0
|
||||
Summary: plugin and hook calling mechanisms for python
|
||||
Home-page: https://github.com/pytest-dev/pluggy
|
||||
Author: Holger Krekel
|
||||
Author-email: holger at merlinux.eu
|
||||
License: MIT license
|
||||
Platform: unix
|
||||
Platform: linux
|
||||
Platform: osx
|
||||
Platform: win32
|
||||
Classifier: Development Status :: 4 - Beta
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Operating System :: POSIX
|
||||
Classifier: Operating System :: Microsoft :: Windows
|
||||
Classifier: Operating System :: MacOS :: MacOS X
|
||||
Classifier: Topic :: Software Development :: Testing
|
||||
Classifier: Topic :: Software Development :: Libraries
|
||||
Classifier: Topic :: Utilities
|
||||
Classifier: Programming Language :: Python :: 2
|
||||
Classifier: Programming Language :: Python :: 2.6
|
||||
Classifier: Programming Language :: Python :: 2.7
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.3
|
||||
Classifier: Programming Language :: Python :: 3.4
|
||||
Classifier: Programming Language :: Python :: 3.5
|
||||
|
||||
|
||||
Plugin registration and hook calling for Python
|
||||
===============================================
|
||||
|
||||
This is the plugin manager as used by pytest but stripped
|
||||
of pytest specific details.
|
||||
|
||||
During the 0.x series this plugin does not have much documentation
|
||||
except extensive docstrings in the pluggy.py module.
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
pluggy.py,sha256=u0oG9cv-oLOkNvEBlwnnu8pp1AyxpoERgUO00S3rvpQ,31543
|
||||
pluggy-0.4.0.dist-info/DESCRIPTION.rst,sha256=ltvjkFd40LW_xShthp6RRVM6OB_uACYDFR3kTpKw7o4,307
|
||||
pluggy-0.4.0.dist-info/LICENSE.txt,sha256=ruwhUOyV1HgE9F35JVL9BCZ9vMSALx369I4xq9rhpkM,1134
|
||||
pluggy-0.4.0.dist-info/METADATA,sha256=pe2hbsqKFaLHC6wAQPpFPn0KlpcPfLBe_BnS4O70bfk,1364
|
||||
pluggy-0.4.0.dist-info/RECORD,,
|
||||
pluggy-0.4.0.dist-info/WHEEL,sha256=9Z5Xm-eel1bTS7e6ogYiKz0zmPEqDwIypurdHN1hR40,116
|
||||
pluggy-0.4.0.dist-info/metadata.json,sha256=T3go5L2qOa_-H-HpCZi3EoVKb8sZ3R-fOssbkWo2nvM,1119
|
||||
pluggy-0.4.0.dist-info/top_level.txt,sha256=xKSCRhai-v9MckvMuWqNz16c1tbsmOggoMSwTgcpYHE,7
|
||||
pluggy-0.4.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
@@ -1,6 +0,0 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.29.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py2-none-any
|
||||
Tag: py3-none-any
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: POSIX", "Operating System :: Microsoft :: Windows", "Operating System :: MacOS :: MacOS X", "Topic :: Software Development :: Testing", "Topic :: Software Development :: Libraries", "Topic :: Utilities", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5"], "extensions": {"python.details": {"contacts": [{"email": "holger at merlinux.eu", "name": "Holger Krekel", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst", "license": "LICENSE.txt"}, "project_urls": {"Home": "https://github.com/pytest-dev/pluggy"}}}, "generator": "bdist_wheel (0.29.0)", "license": "MIT license", "metadata_version": "2.0", "name": "pluggy", "platform": "unix", "summary": "plugin and hook calling mechanisms for python", "version": "0.4.0"}
|
||||
@@ -1 +0,0 @@
|
||||
pluggy
|
||||
@@ -1,802 +0,0 @@
|
||||
"""
|
||||
PluginManager, basic initialization and tracing.
|
||||
|
||||
pluggy is the cristallized core of plugin management as used
|
||||
by some 150 plugins for pytest.
|
||||
|
||||
Pluggy uses semantic versioning. Breaking changes are only foreseen for
|
||||
Major releases (incremented X in "X.Y.Z"). If you want to use pluggy in
|
||||
your project you should thus use a dependency restriction like
|
||||
"pluggy>=0.1.0,<1.0" to avoid surprises.
|
||||
|
||||
pluggy is concerned with hook specification, hook implementations and hook
|
||||
calling. For any given hook specification a hook call invokes up to N implementations.
|
||||
A hook implementation can influence its position and type of execution:
|
||||
if attributed "tryfirst" or "trylast" it will be tried to execute
|
||||
first or last. However, if attributed "hookwrapper" an implementation
|
||||
can wrap all calls to non-hookwrapper implementations. A hookwrapper
|
||||
can thus execute some code ahead and after the execution of other hooks.
|
||||
|
||||
Hook specification is done by way of a regular python function where
|
||||
both the function name and the names of all its arguments are significant.
|
||||
Each hook implementation function is verified against the original specification
|
||||
function, including the names of all its arguments. To allow for hook specifications
|
||||
to evolve over the livetime of a project, hook implementations can
|
||||
accept less arguments. One can thus add new arguments and semantics to
|
||||
a hook specification by adding another argument typically without breaking
|
||||
existing hook implementations.
|
||||
|
||||
The chosen approach is meant to let a hook designer think carefuly about
|
||||
which objects are needed by an extension writer. By contrast, subclass-based
|
||||
extension mechanisms often expose a lot more state and behaviour than needed,
|
||||
thus restricting future developments.
|
||||
|
||||
Pluggy currently consists of functionality for:
|
||||
|
||||
- a way to register new hook specifications. Without a hook
|
||||
specification no hook calling can be performed.
|
||||
|
||||
- a registry of plugins which contain hook implementation functions. It
|
||||
is possible to register plugins for which a hook specification is not yet
|
||||
known and validate all hooks when the system is in a more referentially
|
||||
consistent state. Setting an "optionalhook" attribution to a hook
|
||||
implementation will avoid PluginValidationError's if a specification
|
||||
is missing. This allows to have optional integration between plugins.
|
||||
|
||||
- a "hook" relay object from which you can launch 1:N calls to
|
||||
registered hook implementation functions
|
||||
|
||||
- a mechanism for ordering hook implementation functions
|
||||
|
||||
- mechanisms for two different type of 1:N calls: "firstresult" for when
|
||||
the call should stop when the first implementation returns a non-None result.
|
||||
And the other (default) way of guaranteeing that all hook implementations
|
||||
will be called and their non-None result collected.
|
||||
|
||||
- mechanisms for "historic" extension points such that all newly
|
||||
registered functions will receive all hook calls that happened
|
||||
before their registration.
|
||||
|
||||
- a mechanism for discovering plugin objects which are based on
|
||||
setuptools based entry points.
|
||||
|
||||
- a simple tracing mechanism, including tracing of plugin calls and
|
||||
their arguments.
|
||||
|
||||
"""
|
||||
import sys
|
||||
import inspect
|
||||
|
||||
__version__ = '0.4.0'
|
||||
|
||||
__all__ = ["PluginManager", "PluginValidationError", "HookCallError",
|
||||
"HookspecMarker", "HookimplMarker"]
|
||||
|
||||
_py3 = sys.version_info > (3, 0)
|
||||
|
||||
|
||||
class HookspecMarker:
|
||||
""" Decorator helper class for marking functions as hook specifications.
|
||||
|
||||
You can instantiate it with a project_name to get a decorator.
|
||||
Calling PluginManager.add_hookspecs later will discover all marked functions
|
||||
if the PluginManager uses the same project_name.
|
||||
"""
|
||||
|
||||
def __init__(self, project_name):
|
||||
self.project_name = project_name
|
||||
|
||||
def __call__(self, function=None, firstresult=False, historic=False):
|
||||
""" if passed a function, directly sets attributes on the function
|
||||
which will make it discoverable to add_hookspecs(). If passed no
|
||||
function, returns a decorator which can be applied to a function
|
||||
later using the attributes supplied.
|
||||
|
||||
If firstresult is True the 1:N hook call (N being the number of registered
|
||||
hook implementation functions) will stop at I<=N when the I'th function
|
||||
returns a non-None result.
|
||||
|
||||
If historic is True calls to a hook will be memorized and replayed
|
||||
on later registered plugins.
|
||||
|
||||
"""
|
||||
def setattr_hookspec_opts(func):
|
||||
if historic and firstresult:
|
||||
raise ValueError("cannot have a historic firstresult hook")
|
||||
setattr(func, self.project_name + "_spec",
|
||||
dict(firstresult=firstresult, historic=historic))
|
||||
return func
|
||||
|
||||
if function is not None:
|
||||
return setattr_hookspec_opts(function)
|
||||
else:
|
||||
return setattr_hookspec_opts
|
||||
|
||||
|
||||
class HookimplMarker:
|
||||
""" Decorator helper class for marking functions as hook implementations.
|
||||
|
||||
You can instantiate with a project_name to get a decorator.
|
||||
Calling PluginManager.register later will discover all marked functions
|
||||
if the PluginManager uses the same project_name.
|
||||
"""
|
||||
def __init__(self, project_name):
|
||||
self.project_name = project_name
|
||||
|
||||
def __call__(self, function=None, hookwrapper=False, optionalhook=False,
|
||||
tryfirst=False, trylast=False):
|
||||
|
||||
""" if passed a function, directly sets attributes on the function
|
||||
which will make it discoverable to register(). If passed no function,
|
||||
returns a decorator which can be applied to a function later using
|
||||
the attributes supplied.
|
||||
|
||||
If optionalhook is True a missing matching hook specification will not result
|
||||
in an error (by default it is an error if no matching spec is found).
|
||||
|
||||
If tryfirst is True this hook implementation will run as early as possible
|
||||
in the chain of N hook implementations for a specfication.
|
||||
|
||||
If trylast is True this hook implementation will run as late as possible
|
||||
in the chain of N hook implementations.
|
||||
|
||||
If hookwrapper is True the hook implementations needs to execute exactly
|
||||
one "yield". The code before the yield is run early before any non-hookwrapper
|
||||
function is run. The code after the yield is run after all non-hookwrapper
|
||||
function have run. The yield receives an ``_CallOutcome`` object representing
|
||||
the exception or result outcome of the inner calls (including other hookwrapper
|
||||
calls).
|
||||
|
||||
"""
|
||||
def setattr_hookimpl_opts(func):
|
||||
setattr(func, self.project_name + "_impl",
|
||||
dict(hookwrapper=hookwrapper, optionalhook=optionalhook,
|
||||
tryfirst=tryfirst, trylast=trylast))
|
||||
return func
|
||||
|
||||
if function is None:
|
||||
return setattr_hookimpl_opts
|
||||
else:
|
||||
return setattr_hookimpl_opts(function)
|
||||
|
||||
|
||||
def normalize_hookimpl_opts(opts):
|
||||
opts.setdefault("tryfirst", False)
|
||||
opts.setdefault("trylast", False)
|
||||
opts.setdefault("hookwrapper", False)
|
||||
opts.setdefault("optionalhook", False)
|
||||
|
||||
|
||||
class _TagTracer:
|
||||
def __init__(self):
|
||||
self._tag2proc = {}
|
||||
self.writer = None
|
||||
self.indent = 0
|
||||
|
||||
def get(self, name):
|
||||
return _TagTracerSub(self, (name,))
|
||||
|
||||
def format_message(self, tags, args):
|
||||
if isinstance(args[-1], dict):
|
||||
extra = args[-1]
|
||||
args = args[:-1]
|
||||
else:
|
||||
extra = {}
|
||||
|
||||
content = " ".join(map(str, args))
|
||||
indent = " " * self.indent
|
||||
|
||||
lines = [
|
||||
"%s%s [%s]\n" % (indent, content, ":".join(tags))
|
||||
]
|
||||
|
||||
for name, value in extra.items():
|
||||
lines.append("%s %s: %s\n" % (indent, name, value))
|
||||
return lines
|
||||
|
||||
def processmessage(self, tags, args):
|
||||
if self.writer is not None and args:
|
||||
lines = self.format_message(tags, args)
|
||||
self.writer(''.join(lines))
|
||||
try:
|
||||
self._tag2proc[tags](tags, args)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def setwriter(self, writer):
|
||||
self.writer = writer
|
||||
|
||||
def setprocessor(self, tags, processor):
|
||||
if isinstance(tags, str):
|
||||
tags = tuple(tags.split(":"))
|
||||
else:
|
||||
assert isinstance(tags, tuple)
|
||||
self._tag2proc[tags] = processor
|
||||
|
||||
|
||||
class _TagTracerSub:
|
||||
def __init__(self, root, tags):
|
||||
self.root = root
|
||||
self.tags = tags
|
||||
|
||||
def __call__(self, *args):
|
||||
self.root.processmessage(self.tags, args)
|
||||
|
||||
def setmyprocessor(self, processor):
|
||||
self.root.setprocessor(self.tags, processor)
|
||||
|
||||
def get(self, name):
|
||||
return self.__class__(self.root, self.tags + (name,))
|
||||
|
||||
|
||||
def _raise_wrapfail(wrap_controller, msg):
|
||||
co = wrap_controller.gi_code
|
||||
raise RuntimeError("wrap_controller at %r %s:%d %s" %
|
||||
(co.co_name, co.co_filename, co.co_firstlineno, msg))
|
||||
|
||||
|
||||
def _wrapped_call(wrap_controller, func):
|
||||
""" Wrap calling to a function with a generator which needs to yield
|
||||
exactly once. The yield point will trigger calling the wrapped function
|
||||
and return its _CallOutcome to the yield point. The generator then needs
|
||||
to finish (raise StopIteration) in order for the wrapped call to complete.
|
||||
"""
|
||||
try:
|
||||
next(wrap_controller) # first yield
|
||||
except StopIteration:
|
||||
_raise_wrapfail(wrap_controller, "did not yield")
|
||||
call_outcome = _CallOutcome(func)
|
||||
try:
|
||||
wrap_controller.send(call_outcome)
|
||||
_raise_wrapfail(wrap_controller, "has second yield")
|
||||
except StopIteration:
|
||||
pass
|
||||
return call_outcome.get_result()
|
||||
|
||||
|
||||
class _CallOutcome:
|
||||
""" Outcome of a function call, either an exception or a proper result.
|
||||
Calling the ``get_result`` method will return the result or reraise
|
||||
the exception raised when the function was called. """
|
||||
excinfo = None
|
||||
|
||||
def __init__(self, func):
|
||||
try:
|
||||
self.result = func()
|
||||
except BaseException:
|
||||
self.excinfo = sys.exc_info()
|
||||
|
||||
def force_result(self, result):
|
||||
self.result = result
|
||||
self.excinfo = None
|
||||
|
||||
def get_result(self):
|
||||
if self.excinfo is None:
|
||||
return self.result
|
||||
else:
|
||||
ex = self.excinfo
|
||||
if _py3:
|
||||
raise ex[1].with_traceback(ex[2])
|
||||
_reraise(*ex) # noqa
|
||||
|
||||
if not _py3:
|
||||
exec("""
|
||||
def _reraise(cls, val, tb):
|
||||
raise cls, val, tb
|
||||
""")
|
||||
|
||||
|
||||
class _TracedHookExecution:
|
||||
def __init__(self, pluginmanager, before, after):
|
||||
self.pluginmanager = pluginmanager
|
||||
self.before = before
|
||||
self.after = after
|
||||
self.oldcall = pluginmanager._inner_hookexec
|
||||
assert not isinstance(self.oldcall, _TracedHookExecution)
|
||||
self.pluginmanager._inner_hookexec = self
|
||||
|
||||
def __call__(self, hook, hook_impls, kwargs):
|
||||
self.before(hook.name, hook_impls, kwargs)
|
||||
outcome = _CallOutcome(lambda: self.oldcall(hook, hook_impls, kwargs))
|
||||
self.after(outcome, hook.name, hook_impls, kwargs)
|
||||
return outcome.get_result()
|
||||
|
||||
def undo(self):
|
||||
self.pluginmanager._inner_hookexec = self.oldcall
|
||||
|
||||
|
||||
class PluginManager(object):
|
||||
""" Core Pluginmanager class which manages registration
|
||||
of plugin objects and 1:N hook calling.
|
||||
|
||||
You can register new hooks by calling ``add_hookspec(module_or_class)``.
|
||||
You can register plugin objects (which contain hooks) by calling
|
||||
``register(plugin)``. The Pluginmanager is initialized with a
|
||||
prefix that is searched for in the names of the dict of registered
|
||||
plugin objects. An optional excludefunc allows to blacklist names which
|
||||
are not considered as hooks despite a matching prefix.
|
||||
|
||||
For debugging purposes you can call ``enable_tracing()``
|
||||
which will subsequently send debug information to the trace helper.
|
||||
"""
|
||||
|
||||
def __init__(self, project_name, implprefix=None):
|
||||
""" if implprefix is given implementation functions
|
||||
will be recognized if their name matches the implprefix. """
|
||||
self.project_name = project_name
|
||||
self._name2plugin = {}
|
||||
self._plugin2hookcallers = {}
|
||||
self._plugin_distinfo = []
|
||||
self.trace = _TagTracer().get("pluginmanage")
|
||||
self.hook = _HookRelay(self.trace.root.get("hook"))
|
||||
self._implprefix = implprefix
|
||||
self._inner_hookexec = lambda hook, methods, kwargs: \
|
||||
_MultiCall(methods, kwargs, hook.spec_opts).execute()
|
||||
|
||||
def _hookexec(self, hook, methods, kwargs):
|
||||
# called from all hookcaller instances.
|
||||
# enable_tracing will set its own wrapping function at self._inner_hookexec
|
||||
return self._inner_hookexec(hook, methods, kwargs)
|
||||
|
||||
def register(self, plugin, name=None):
|
||||
""" Register a plugin and return its canonical name or None if the name
|
||||
is blocked from registering. Raise a ValueError if the plugin is already
|
||||
registered. """
|
||||
plugin_name = name or self.get_canonical_name(plugin)
|
||||
|
||||
if plugin_name in self._name2plugin or plugin in self._plugin2hookcallers:
|
||||
if self._name2plugin.get(plugin_name, -1) is None:
|
||||
return # blocked plugin, return None to indicate no registration
|
||||
raise ValueError("Plugin already registered: %s=%s\n%s" %
|
||||
(plugin_name, plugin, self._name2plugin))
|
||||
|
||||
# XXX if an error happens we should make sure no state has been
|
||||
# changed at point of return
|
||||
self._name2plugin[plugin_name] = plugin
|
||||
|
||||
# register matching hook implementations of the plugin
|
||||
self._plugin2hookcallers[plugin] = hookcallers = []
|
||||
for name in dir(plugin):
|
||||
hookimpl_opts = self.parse_hookimpl_opts(plugin, name)
|
||||
if hookimpl_opts is not None:
|
||||
normalize_hookimpl_opts(hookimpl_opts)
|
||||
method = getattr(plugin, name)
|
||||
hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_opts)
|
||||
hook = getattr(self.hook, name, None)
|
||||
if hook is None:
|
||||
hook = _HookCaller(name, self._hookexec)
|
||||
setattr(self.hook, name, hook)
|
||||
elif hook.has_spec():
|
||||
self._verify_hook(hook, hookimpl)
|
||||
hook._maybe_apply_history(hookimpl)
|
||||
hook._add_hookimpl(hookimpl)
|
||||
hookcallers.append(hook)
|
||||
return plugin_name
|
||||
|
||||
def parse_hookimpl_opts(self, plugin, name):
|
||||
method = getattr(plugin, name)
|
||||
try:
|
||||
res = getattr(method, self.project_name + "_impl", None)
|
||||
except Exception:
|
||||
res = {}
|
||||
if res is not None and not isinstance(res, dict):
|
||||
# false positive
|
||||
res = None
|
||||
elif res is None and self._implprefix and name.startswith(self._implprefix):
|
||||
res = {}
|
||||
return res
|
||||
|
||||
def unregister(self, plugin=None, name=None):
|
||||
""" unregister a plugin object and all its contained hook implementations
|
||||
from internal data structures. """
|
||||
if name is None:
|
||||
assert plugin is not None, "one of name or plugin needs to be specified"
|
||||
name = self.get_name(plugin)
|
||||
|
||||
if plugin is None:
|
||||
plugin = self.get_plugin(name)
|
||||
|
||||
# if self._name2plugin[name] == None registration was blocked: ignore
|
||||
if self._name2plugin.get(name):
|
||||
del self._name2plugin[name]
|
||||
|
||||
for hookcaller in self._plugin2hookcallers.pop(plugin, []):
|
||||
hookcaller._remove_plugin(plugin)
|
||||
|
||||
return plugin
|
||||
|
||||
def set_blocked(self, name):
|
||||
""" block registrations of the given name, unregister if already registered. """
|
||||
self.unregister(name=name)
|
||||
self._name2plugin[name] = None
|
||||
|
||||
def is_blocked(self, name):
|
||||
""" return True if the name blogs registering plugins of that name. """
|
||||
return name in self._name2plugin and self._name2plugin[name] is None
|
||||
|
||||
def add_hookspecs(self, module_or_class):
|
||||
""" add new hook specifications defined in the given module_or_class.
|
||||
Functions are recognized if they have been decorated accordingly. """
|
||||
names = []
|
||||
for name in dir(module_or_class):
|
||||
spec_opts = self.parse_hookspec_opts(module_or_class, name)
|
||||
if spec_opts is not None:
|
||||
hc = getattr(self.hook, name, None)
|
||||
if hc is None:
|
||||
hc = _HookCaller(name, self._hookexec, module_or_class, spec_opts)
|
||||
setattr(self.hook, name, hc)
|
||||
else:
|
||||
# plugins registered this hook without knowing the spec
|
||||
hc.set_specification(module_or_class, spec_opts)
|
||||
for hookfunction in (hc._wrappers + hc._nonwrappers):
|
||||
self._verify_hook(hc, hookfunction)
|
||||
names.append(name)
|
||||
|
||||
if not names:
|
||||
raise ValueError("did not find any %r hooks in %r" %
|
||||
(self.project_name, module_or_class))
|
||||
|
||||
def parse_hookspec_opts(self, module_or_class, name):
|
||||
method = getattr(module_or_class, name)
|
||||
return getattr(method, self.project_name + "_spec", None)
|
||||
|
||||
def get_plugins(self):
|
||||
""" return the set of registered plugins. """
|
||||
return set(self._plugin2hookcallers)
|
||||
|
||||
def is_registered(self, plugin):
|
||||
""" Return True if the plugin is already registered. """
|
||||
return plugin in self._plugin2hookcallers
|
||||
|
||||
def get_canonical_name(self, plugin):
|
||||
""" Return canonical name for a plugin object. Note that a plugin
|
||||
may be registered under a different name which was specified
|
||||
by the caller of register(plugin, name). To obtain the name
|
||||
of an registered plugin use ``get_name(plugin)`` instead."""
|
||||
return getattr(plugin, "__name__", None) or str(id(plugin))
|
||||
|
||||
def get_plugin(self, name):
|
||||
""" Return a plugin or None for the given name. """
|
||||
return self._name2plugin.get(name)
|
||||
|
||||
def has_plugin(self, name):
|
||||
""" Return True if a plugin with the given name is registered. """
|
||||
return self.get_plugin(name) is not None
|
||||
|
||||
def get_name(self, plugin):
|
||||
""" Return name for registered plugin or None if not registered. """
|
||||
for name, val in self._name2plugin.items():
|
||||
if plugin == val:
|
||||
return name
|
||||
|
||||
def _verify_hook(self, hook, hookimpl):
|
||||
if hook.is_historic() and hookimpl.hookwrapper:
|
||||
raise PluginValidationError(
|
||||
"Plugin %r\nhook %r\nhistoric incompatible to hookwrapper" %
|
||||
(hookimpl.plugin_name, hook.name))
|
||||
|
||||
for arg in hookimpl.argnames:
|
||||
if arg not in hook.argnames:
|
||||
raise PluginValidationError(
|
||||
"Plugin %r\nhook %r\nargument %r not available\n"
|
||||
"plugin definition: %s\n"
|
||||
"available hookargs: %s" %
|
||||
(hookimpl.plugin_name, hook.name, arg,
|
||||
_formatdef(hookimpl.function), ", ".join(hook.argnames)))
|
||||
|
||||
def check_pending(self):
|
||||
""" Verify that all hooks which have not been verified against
|
||||
a hook specification are optional, otherwise raise PluginValidationError"""
|
||||
for name in self.hook.__dict__:
|
||||
if name[0] != "_":
|
||||
hook = getattr(self.hook, name)
|
||||
if not hook.has_spec():
|
||||
for hookimpl in (hook._wrappers + hook._nonwrappers):
|
||||
if not hookimpl.optionalhook:
|
||||
raise PluginValidationError(
|
||||
"unknown hook %r in plugin %r" %
|
||||
(name, hookimpl.plugin))
|
||||
|
||||
def load_setuptools_entrypoints(self, entrypoint_name):
|
||||
""" Load modules from querying the specified setuptools entrypoint name.
|
||||
Return the number of loaded plugins. """
|
||||
from pkg_resources import (iter_entry_points, DistributionNotFound,
|
||||
VersionConflict)
|
||||
for ep in iter_entry_points(entrypoint_name):
|
||||
# is the plugin registered or blocked?
|
||||
if self.get_plugin(ep.name) or self.is_blocked(ep.name):
|
||||
continue
|
||||
try:
|
||||
plugin = ep.load()
|
||||
except DistributionNotFound:
|
||||
continue
|
||||
except VersionConflict as e:
|
||||
raise PluginValidationError(
|
||||
"Plugin %r could not be loaded: %s!" % (ep.name, e))
|
||||
self.register(plugin, name=ep.name)
|
||||
self._plugin_distinfo.append((plugin, ep.dist))
|
||||
return len(self._plugin_distinfo)
|
||||
|
||||
def list_plugin_distinfo(self):
|
||||
""" return list of distinfo/plugin tuples for all setuptools registered
|
||||
plugins. """
|
||||
return list(self._plugin_distinfo)
|
||||
|
||||
def list_name_plugin(self):
|
||||
""" return list of name/plugin pairs. """
|
||||
return list(self._name2plugin.items())
|
||||
|
||||
def get_hookcallers(self, plugin):
|
||||
""" get all hook callers for the specified plugin. """
|
||||
return self._plugin2hookcallers.get(plugin)
|
||||
|
||||
def add_hookcall_monitoring(self, before, after):
|
||||
""" add before/after tracing functions for all hooks
|
||||
and return an undo function which, when called,
|
||||
will remove the added tracers.
|
||||
|
||||
``before(hook_name, hook_impls, kwargs)`` will be called ahead
|
||||
of all hook calls and receive a hookcaller instance, a list
|
||||
of HookImpl instances and the keyword arguments for the hook call.
|
||||
|
||||
``after(outcome, hook_name, hook_impls, kwargs)`` receives the
|
||||
same arguments as ``before`` but also a :py:class:`_CallOutcome <_pytest.vendored_packages.pluggy._CallOutcome>` object
|
||||
which represents the result of the overall hook call.
|
||||
"""
|
||||
return _TracedHookExecution(self, before, after).undo
|
||||
|
||||
def enable_tracing(self):
|
||||
""" enable tracing of hook calls and return an undo function. """
|
||||
hooktrace = self.hook._trace
|
||||
|
||||
def before(hook_name, methods, kwargs):
|
||||
hooktrace.root.indent += 1
|
||||
hooktrace(hook_name, kwargs)
|
||||
|
||||
def after(outcome, hook_name, methods, kwargs):
|
||||
if outcome.excinfo is None:
|
||||
hooktrace("finish", hook_name, "-->", outcome.result)
|
||||
hooktrace.root.indent -= 1
|
||||
|
||||
return self.add_hookcall_monitoring(before, after)
|
||||
|
||||
def subset_hook_caller(self, name, remove_plugins):
|
||||
""" Return a new _HookCaller instance for the named method
|
||||
which manages calls to all registered plugins except the
|
||||
ones from remove_plugins. """
|
||||
orig = getattr(self.hook, name)
|
||||
plugins_to_remove = [plug for plug in remove_plugins if hasattr(plug, name)]
|
||||
if plugins_to_remove:
|
||||
hc = _HookCaller(orig.name, orig._hookexec, orig._specmodule_or_class,
|
||||
orig.spec_opts)
|
||||
for hookimpl in (orig._wrappers + orig._nonwrappers):
|
||||
plugin = hookimpl.plugin
|
||||
if plugin not in plugins_to_remove:
|
||||
hc._add_hookimpl(hookimpl)
|
||||
# we also keep track of this hook caller so it
|
||||
# gets properly removed on plugin unregistration
|
||||
self._plugin2hookcallers.setdefault(plugin, []).append(hc)
|
||||
return hc
|
||||
return orig
|
||||
|
||||
|
||||
class _MultiCall:
|
||||
""" execute a call into multiple python functions/methods. """
|
||||
|
||||
# XXX note that the __multicall__ argument is supported only
|
||||
# for pytest compatibility reasons. It was never officially
|
||||
# supported there and is explicitely deprecated since 2.8
|
||||
# so we can remove it soon, allowing to avoid the below recursion
|
||||
# in execute() and simplify/speed up the execute loop.
|
||||
|
||||
def __init__(self, hook_impls, kwargs, specopts={}):
|
||||
self.hook_impls = hook_impls
|
||||
self.kwargs = kwargs
|
||||
self.kwargs["__multicall__"] = self
|
||||
self.specopts = specopts
|
||||
|
||||
def execute(self):
|
||||
all_kwargs = self.kwargs
|
||||
self.results = results = []
|
||||
firstresult = self.specopts.get("firstresult")
|
||||
|
||||
while self.hook_impls:
|
||||
hook_impl = self.hook_impls.pop()
|
||||
try:
|
||||
args = [all_kwargs[argname] for argname in hook_impl.argnames]
|
||||
except KeyError:
|
||||
for argname in hook_impl.argnames:
|
||||
if argname not in all_kwargs:
|
||||
raise HookCallError(
|
||||
"hook call must provide argument %r" % (argname,))
|
||||
if hook_impl.hookwrapper:
|
||||
return _wrapped_call(hook_impl.function(*args), self.execute)
|
||||
res = hook_impl.function(*args)
|
||||
if res is not None:
|
||||
if firstresult:
|
||||
return res
|
||||
results.append(res)
|
||||
|
||||
if not firstresult:
|
||||
return results
|
||||
|
||||
def __repr__(self):
|
||||
status = "%d meths" % (len(self.hook_impls),)
|
||||
if hasattr(self, "results"):
|
||||
status = ("%d results, " % len(self.results)) + status
|
||||
return "<_MultiCall %s, kwargs=%r>" % (status, self.kwargs)
|
||||
|
||||
|
||||
def varnames(func, startindex=None):
|
||||
""" return argument name tuple for a function, method, class or callable.
|
||||
|
||||
In case of a class, its "__init__" method is considered.
|
||||
For methods the "self" parameter is not included unless you are passing
|
||||
an unbound method with Python3 (which has no supports for unbound methods)
|
||||
"""
|
||||
cache = getattr(func, "__dict__", {})
|
||||
try:
|
||||
return cache["_varnames"]
|
||||
except KeyError:
|
||||
pass
|
||||
if inspect.isclass(func):
|
||||
try:
|
||||
func = func.__init__
|
||||
except AttributeError:
|
||||
return ()
|
||||
startindex = 1
|
||||
else:
|
||||
if not inspect.isfunction(func) and not inspect.ismethod(func):
|
||||
try:
|
||||
func = getattr(func, '__call__', func)
|
||||
except Exception:
|
||||
return ()
|
||||
if startindex is None:
|
||||
startindex = int(inspect.ismethod(func))
|
||||
|
||||
try:
|
||||
rawcode = func.__code__
|
||||
except AttributeError:
|
||||
return ()
|
||||
try:
|
||||
x = rawcode.co_varnames[startindex:rawcode.co_argcount]
|
||||
except AttributeError:
|
||||
x = ()
|
||||
else:
|
||||
defaults = func.__defaults__
|
||||
if defaults:
|
||||
x = x[:-len(defaults)]
|
||||
try:
|
||||
cache["_varnames"] = x
|
||||
except TypeError:
|
||||
pass
|
||||
return x
|
||||
|
||||
|
||||
class _HookRelay:
|
||||
""" hook holder object for performing 1:N hook calls where N is the number
|
||||
of registered plugins.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, trace):
|
||||
self._trace = trace
|
||||
|
||||
|
||||
class _HookCaller(object):
|
||||
def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None):
|
||||
self.name = name
|
||||
self._wrappers = []
|
||||
self._nonwrappers = []
|
||||
self._hookexec = hook_execute
|
||||
if specmodule_or_class is not None:
|
||||
assert spec_opts is not None
|
||||
self.set_specification(specmodule_or_class, spec_opts)
|
||||
|
||||
def has_spec(self):
|
||||
return hasattr(self, "_specmodule_or_class")
|
||||
|
||||
def set_specification(self, specmodule_or_class, spec_opts):
|
||||
assert not self.has_spec()
|
||||
self._specmodule_or_class = specmodule_or_class
|
||||
specfunc = getattr(specmodule_or_class, self.name)
|
||||
argnames = varnames(specfunc, startindex=inspect.isclass(specmodule_or_class))
|
||||
assert "self" not in argnames # sanity check
|
||||
self.argnames = ["__multicall__"] + list(argnames)
|
||||
self.spec_opts = spec_opts
|
||||
if spec_opts.get("historic"):
|
||||
self._call_history = []
|
||||
|
||||
def is_historic(self):
|
||||
return hasattr(self, "_call_history")
|
||||
|
||||
def _remove_plugin(self, plugin):
|
||||
def remove(wrappers):
|
||||
for i, method in enumerate(wrappers):
|
||||
if method.plugin == plugin:
|
||||
del wrappers[i]
|
||||
return True
|
||||
if remove(self._wrappers) is None:
|
||||
if remove(self._nonwrappers) is None:
|
||||
raise ValueError("plugin %r not found" % (plugin,))
|
||||
|
||||
def _add_hookimpl(self, hookimpl):
|
||||
if hookimpl.hookwrapper:
|
||||
methods = self._wrappers
|
||||
else:
|
||||
methods = self._nonwrappers
|
||||
|
||||
if hookimpl.trylast:
|
||||
methods.insert(0, hookimpl)
|
||||
elif hookimpl.tryfirst:
|
||||
methods.append(hookimpl)
|
||||
else:
|
||||
# find last non-tryfirst method
|
||||
i = len(methods) - 1
|
||||
while i >= 0 and methods[i].tryfirst:
|
||||
i -= 1
|
||||
methods.insert(i + 1, hookimpl)
|
||||
|
||||
def __repr__(self):
|
||||
return "<_HookCaller %r>" % (self.name,)
|
||||
|
||||
def __call__(self, **kwargs):
|
||||
assert not self.is_historic()
|
||||
return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
|
||||
|
||||
def call_historic(self, proc=None, kwargs=None):
|
||||
self._call_history.append((kwargs or {}, proc))
|
||||
# historizing hooks don't return results
|
||||
self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
|
||||
|
||||
def call_extra(self, methods, kwargs):
|
||||
""" Call the hook with some additional temporarily participating
|
||||
methods using the specified kwargs as call parameters. """
|
||||
old = list(self._nonwrappers), list(self._wrappers)
|
||||
for method in methods:
|
||||
opts = dict(hookwrapper=False, trylast=False, tryfirst=False)
|
||||
hookimpl = HookImpl(None, "<temp>", method, opts)
|
||||
self._add_hookimpl(hookimpl)
|
||||
try:
|
||||
return self(**kwargs)
|
||||
finally:
|
||||
self._nonwrappers, self._wrappers = old
|
||||
|
||||
def _maybe_apply_history(self, method):
|
||||
if self.is_historic():
|
||||
for kwargs, proc in self._call_history:
|
||||
res = self._hookexec(self, [method], kwargs)
|
||||
if res and proc is not None:
|
||||
proc(res[0])
|
||||
|
||||
|
||||
class HookImpl:
|
||||
def __init__(self, plugin, plugin_name, function, hook_impl_opts):
|
||||
self.function = function
|
||||
self.argnames = varnames(self.function)
|
||||
self.plugin = plugin
|
||||
self.opts = hook_impl_opts
|
||||
self.plugin_name = plugin_name
|
||||
self.__dict__.update(hook_impl_opts)
|
||||
|
||||
|
||||
class PluginValidationError(Exception):
|
||||
""" plugin failed validation. """
|
||||
|
||||
|
||||
class HookCallError(Exception):
|
||||
""" Hook was called wrongly. """
|
||||
|
||||
|
||||
if hasattr(inspect, 'signature'):
|
||||
def _formatdef(func):
|
||||
return "%s%s" % (
|
||||
func.__name__,
|
||||
str(inspect.signature(func))
|
||||
)
|
||||
else:
|
||||
def _formatdef(func):
|
||||
return "%s%s" % (
|
||||
func.__name__,
|
||||
inspect.formatargspec(*inspect.getargspec(func))
|
||||
)
|
||||
@@ -72,8 +72,10 @@ def catch_warnings_for_item(item):
|
||||
unicode_warning = False
|
||||
|
||||
if compat._PY2 and any(isinstance(m, compat.UNICODE_TYPES) for m in warn_msg.args):
|
||||
new_args = [compat.safe_str(m) for m in warn_msg.args]
|
||||
unicode_warning = warn_msg.args != new_args
|
||||
new_args = []
|
||||
for m in warn_msg.args:
|
||||
new_args.append(compat.ascii_escaped(m) if isinstance(m, compat.UNICODE_TYPES) else m)
|
||||
unicode_warning = list(warn_msg.args) != new_args
|
||||
warn_msg.args = new_args
|
||||
|
||||
msg = warnings.formatwarning(
|
||||
|
||||
@@ -10,9 +10,7 @@ environment:
|
||||
- TOXENV: "coveralls"
|
||||
# note: please use "tox --listenvs" to populate the build matrix below
|
||||
- TOXENV: "linting"
|
||||
- TOXENV: "py26"
|
||||
- TOXENV: "py27"
|
||||
- TOXENV: "py33"
|
||||
- TOXENV: "py34"
|
||||
- TOXENV: "py35"
|
||||
- TOXENV: "py36"
|
||||
@@ -21,10 +19,12 @@ environment:
|
||||
- TOXENV: "py27-xdist"
|
||||
- TOXENV: "py27-trial"
|
||||
- TOXENV: "py27-numpy"
|
||||
- TOXENV: "py27-pluggymaster"
|
||||
- TOXENV: "py36-pexpect"
|
||||
- TOXENV: "py36-xdist"
|
||||
- TOXENV: "py36-trial"
|
||||
- TOXENV: "py36-numpy"
|
||||
- TOXENV: "py36-pluggymaster"
|
||||
- TOXENV: "py27-nobyte"
|
||||
- TOXENV: "doctesting"
|
||||
- TOXENV: "py35-freeze"
|
||||
|
||||
32
changelog/README.rst
Normal file
32
changelog/README.rst
Normal file
@@ -0,0 +1,32 @@
|
||||
This directory contains "newsfragments" which are short files that contain a small **ReST**-formatted
|
||||
text that will be added to the next ``CHANGELOG``.
|
||||
|
||||
The ``CHANGELOG`` will be read by users, so this description should be aimed to pytest users
|
||||
instead of describing internal changes which are only relevant to the developers.
|
||||
|
||||
Make sure to use full sentences with correct case and punctuation, for example::
|
||||
|
||||
Fix issue with non-ascii messages from the ``warnings`` module.
|
||||
|
||||
Each file should be named like ``<ISSUE>.<TYPE>.rst``, where
|
||||
``<ISSUE>`` is an issue number, and ``<TYPE>`` is one of:
|
||||
|
||||
* ``feature``: new user facing features, like new command-line options and new behavior.
|
||||
* ``bugfix``: fixes a reported bug.
|
||||
* ``doc``: documentation improvement, like rewording an entire session or adding missing docs.
|
||||
* ``removal``: feature deprecation or removal.
|
||||
* ``vendor``: changes in packages vendored in pytest.
|
||||
* ``trivial``: fixing a small typo or internal change that might be noteworthy.
|
||||
|
||||
So for example: ``123.feature.rst``, ``456.bugfix.rst``.
|
||||
|
||||
If your PR fixes an issue, use that number here. If there is no issue,
|
||||
then after you submit the PR and get the PR number you can add a
|
||||
changelog using that instead.
|
||||
|
||||
If you are not sure what issue type to use, don't hesitate to ask in your PR.
|
||||
|
||||
Note that the ``towncrier`` tool will automatically
|
||||
reflow your text, so it will work best if you stick to a single paragraph, but multiple sentences and links are OK
|
||||
and encouraged. You can install ``towncrier`` and then run ``towncrier --draft``
|
||||
if you want to get a preview of how your change will look in the final release notes.
|
||||
@@ -13,8 +13,6 @@ PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
REGENDOC_ARGS := \
|
||||
--normalize "/={8,} (.*) ={8,}/======= \1 ========/" \
|
||||
--normalize "/_{8,} (.*) _{8,}/_______ \1 ________/" \
|
||||
--normalize "/in \d+.\d+ seconds/in 0.12 seconds/" \
|
||||
--normalize "@/tmp/pytest-of-.*/pytest-\d+@PYTEST_TMPDIR@" \
|
||||
--normalize "@pytest-(\d+)\\.[^ ,]+@pytest-\1.x.y@" \
|
||||
|
||||
@@ -6,6 +6,12 @@ Release announcements
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
release-3.4.2
|
||||
release-3.4.1
|
||||
release-3.4.0
|
||||
release-3.3.2
|
||||
release-3.3.1
|
||||
release-3.3.0
|
||||
release-3.2.5
|
||||
release-3.2.4
|
||||
release-3.2.3
|
||||
|
||||
@@ -62,7 +62,7 @@ holger krekel
|
||||
- fix issue655: work around different ways that cause python2/3
|
||||
to leak sys.exc_info into fixtures/tests causing failures in 3rd party code
|
||||
|
||||
- fix issue615: assertion re-writing did not correctly escape % signs
|
||||
- fix issue615: assertion rewriting did not correctly escape % signs
|
||||
when formatting boolean operations, which tripped over mixing
|
||||
booleans with modulo operators. Thanks to Tom Viner for the report,
|
||||
triaging and fix.
|
||||
|
||||
50
doc/en/announce/release-3.3.0.rst
Normal file
50
doc/en/announce/release-3.3.0.rst
Normal file
@@ -0,0 +1,50 @@
|
||||
pytest-3.3.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 3.3.0 release!
|
||||
|
||||
pytest is a mature Python testing tool with more than a 1600 tests
|
||||
against itself, passing on many different interpreters and platforms.
|
||||
|
||||
This release contains a number of bugs fixes and improvements, so users are encouraged
|
||||
to take a look at the CHANGELOG:
|
||||
|
||||
http://doc.pytest.org/en/latest/changelog.html
|
||||
|
||||
For complete documentation, please visit:
|
||||
|
||||
http://docs.pytest.org
|
||||
|
||||
As usual, you can upgrade from pypi via:
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* Ceridwen
|
||||
* Daniel Hahler
|
||||
* Dirk Thomas
|
||||
* Dmitry Malinovsky
|
||||
* Florian Bruhin
|
||||
* George Y. Kussumoto
|
||||
* Hugo
|
||||
* Jesús Espino
|
||||
* Joan Massich
|
||||
* Ofir
|
||||
* OfirOshir
|
||||
* Ronny Pfannschmidt
|
||||
* Samuel Dion-Girardeau
|
||||
* Srinivas Reddy Thatiparthy
|
||||
* Sviatoslav Abakumov
|
||||
* Tarcisio Fischer
|
||||
* Thomas Hisch
|
||||
* Tyler Goodlet
|
||||
* hugovk
|
||||
* je
|
||||
* prokaktus
|
||||
|
||||
|
||||
Happy testing,
|
||||
The Pytest Development Team
|
||||
25
doc/en/announce/release-3.3.1.rst
Normal file
25
doc/en/announce/release-3.3.1.rst
Normal file
@@ -0,0 +1,25 @@
|
||||
pytest-3.3.1
|
||||
=======================================
|
||||
|
||||
pytest 3.3.1 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at http://doc.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Bruno Oliveira
|
||||
* Daniel Hahler
|
||||
* Eugene Prikazchikov
|
||||
* Florian Bruhin
|
||||
* Roland Puntaier
|
||||
* Ronny Pfannschmidt
|
||||
* Sebastian Rahlf
|
||||
* Tom Viner
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
28
doc/en/announce/release-3.3.2.rst
Normal file
28
doc/en/announce/release-3.3.2.rst
Normal file
@@ -0,0 +1,28 @@
|
||||
pytest-3.3.2
|
||||
=======================================
|
||||
|
||||
pytest 3.3.2 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at http://doc.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Anthony Sottile
|
||||
* Antony Lee
|
||||
* Austin
|
||||
* Bruno Oliveira
|
||||
* Florian Bruhin
|
||||
* Floris Bruynooghe
|
||||
* Henk-Jaap Wagenaar
|
||||
* Jurko Gospodnetić
|
||||
* Ronny Pfannschmidt
|
||||
* Srinivas Reddy Thatiparthy
|
||||
* Thomas Hisch
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
52
doc/en/announce/release-3.4.0.rst
Normal file
52
doc/en/announce/release-3.4.0.rst
Normal file
@@ -0,0 +1,52 @@
|
||||
pytest-3.4.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 3.4.0 release!
|
||||
|
||||
pytest is a mature Python testing tool with more than a 1600 tests
|
||||
against itself, passing on many different interpreters and platforms.
|
||||
|
||||
This release contains a number of bugs fixes and improvements, so users are encouraged
|
||||
to take a look at the CHANGELOG:
|
||||
|
||||
http://doc.pytest.org/en/latest/changelog.html
|
||||
|
||||
For complete documentation, please visit:
|
||||
|
||||
http://docs.pytest.org
|
||||
|
||||
As usual, you can upgrade from pypi via:
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Aaron
|
||||
* Alan Velasco
|
||||
* Anders Hovmöller
|
||||
* Andrew Toolan
|
||||
* Anthony Sottile
|
||||
* Aron Coyle
|
||||
* Brian Maissy
|
||||
* Bruno Oliveira
|
||||
* Cyrus Maden
|
||||
* Florian Bruhin
|
||||
* Henk-Jaap Wagenaar
|
||||
* Ian Lesperance
|
||||
* Jon Dufresne
|
||||
* Jurko Gospodnetić
|
||||
* Kate
|
||||
* Kimberly
|
||||
* Per A. Brodtkorb
|
||||
* Pierre-Alexandre Fonta
|
||||
* Raphael Castaneda
|
||||
* Ronny Pfannschmidt
|
||||
* ST John
|
||||
* Segev Finer
|
||||
* Thomas Hisch
|
||||
* Tzu-ping Chung
|
||||
* feuillemorte
|
||||
|
||||
|
||||
Happy testing,
|
||||
The Pytest Development Team
|
||||
27
doc/en/announce/release-3.4.1.rst
Normal file
27
doc/en/announce/release-3.4.1.rst
Normal file
@@ -0,0 +1,27 @@
|
||||
pytest-3.4.1
|
||||
=======================================
|
||||
|
||||
pytest 3.4.1 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at http://doc.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Aaron
|
||||
* Alan Velasco
|
||||
* Andy Freeland
|
||||
* Brian Maissy
|
||||
* Bruno Oliveira
|
||||
* Florian Bruhin
|
||||
* Jason R. Coombs
|
||||
* Marcin Bachry
|
||||
* Pedro Algarvio
|
||||
* Ronny Pfannschmidt
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
28
doc/en/announce/release-3.4.2.rst
Normal file
28
doc/en/announce/release-3.4.2.rst
Normal file
@@ -0,0 +1,28 @@
|
||||
pytest-3.4.2
|
||||
=======================================
|
||||
|
||||
pytest 3.4.2 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at http://doc.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Allan Feldman
|
||||
* Bruno Oliveira
|
||||
* Florian Bruhin
|
||||
* Jason R. Coombs
|
||||
* Kyle Altendorf
|
||||
* Maik Figura
|
||||
* Ronny Pfannschmidt
|
||||
* codetriage-readme-bot
|
||||
* feuillemorte
|
||||
* joshm91
|
||||
* mike
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
@@ -25,15 +25,15 @@ to assert that your function returns a certain value. If this assertion fails
|
||||
you will see the return value of the function call::
|
||||
|
||||
$ pytest test_assert1.py
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 1 item
|
||||
|
||||
test_assert1.py F
|
||||
test_assert1.py F [100%]
|
||||
|
||||
======= FAILURES ========
|
||||
_______ test_function ________
|
||||
================================= FAILURES =================================
|
||||
______________________________ test_function _______________________________
|
||||
|
||||
def test_function():
|
||||
> assert f() == 4
|
||||
@@ -41,7 +41,7 @@ you will see the return value of the function call::
|
||||
E + where 3 = f()
|
||||
|
||||
test_assert1.py:5: AssertionError
|
||||
======= 1 failed in 0.12 seconds ========
|
||||
========================= 1 failed in 0.12 seconds =========================
|
||||
|
||||
``pytest`` has support for showing the values of the most common subexpressions
|
||||
including calls, attributes, comparisons, and binary and unary
|
||||
@@ -168,15 +168,15 @@ when it encounters comparisons. For example::
|
||||
if you run this module::
|
||||
|
||||
$ pytest test_assert2.py
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 1 item
|
||||
|
||||
test_assert2.py F
|
||||
test_assert2.py F [100%]
|
||||
|
||||
======= FAILURES ========
|
||||
_______ test_set_comparison ________
|
||||
================================= FAILURES =================================
|
||||
___________________________ test_set_comparison ____________________________
|
||||
|
||||
def test_set_comparison():
|
||||
set1 = set("1308")
|
||||
@@ -190,7 +190,7 @@ if you run this module::
|
||||
E Use -v to get the full diff
|
||||
|
||||
test_assert2.py:5: AssertionError
|
||||
======= 1 failed in 0.12 seconds ========
|
||||
========================= 1 failed in 0.12 seconds =========================
|
||||
|
||||
Special comparisons are done for a number of cases:
|
||||
|
||||
@@ -238,9 +238,9 @@ you can run the test module and get the custom output defined in
|
||||
the conftest file::
|
||||
|
||||
$ pytest -q test_foocompare.py
|
||||
F
|
||||
======= FAILURES ========
|
||||
_______ test_compare ________
|
||||
F [100%]
|
||||
================================= FAILURES =================================
|
||||
_______________________________ test_compare _______________________________
|
||||
|
||||
def test_compare():
|
||||
f1 = Foo(1)
|
||||
|
||||
@@ -10,3 +10,11 @@ With the pytest 3.0 release we introduced a clear communication scheme for when
|
||||
To communicate changes we are already issuing deprecation warnings, but they are not displayed by default. In pytest 3.0 we changed the default setting so that pytest deprecation warnings are displayed if not explicitly silenced (with ``--disable-pytest-warnings``).
|
||||
|
||||
We will only remove deprecated functionality in major releases (e.g. if we deprecate something in 3.0 we will remove it in 4.0), and keep it around for at least two minor releases (e.g. if we deprecate something in 3.9 and 4.0 is the next release, we will not remove it in 4.0 but in 5.0).
|
||||
|
||||
|
||||
Deprecation Roadmap
|
||||
-------------------
|
||||
|
||||
We track deprecation and removal of features using milestones and the `deprecation <https://github.com/pytest-dev/pytest/issues?q=label%3A%22type%3A+deprecation%22>`_ and `removal <https://github.com/pytest-dev/pytest/labels/type%3A%20removal>`_ labels on GitHub.
|
||||
|
||||
Following our deprecation policy, after starting issuing deprecation warnings we keep features for *at least* two minor versions before considering removal.
|
||||
|
||||
@@ -91,11 +91,23 @@ You can ask for available builtin or project-custom
|
||||
capsys
|
||||
Enable capturing of writes to sys.stdout/sys.stderr and make
|
||||
captured output available via ``capsys.readouterr()`` method calls
|
||||
which return a ``(out, err)`` tuple.
|
||||
which return a ``(out, err)`` tuple. ``out`` and ``err`` will be ``text``
|
||||
objects.
|
||||
capsysbinary
|
||||
Enable capturing of writes to sys.stdout/sys.stderr and make
|
||||
captured output available via ``capsys.readouterr()`` method calls
|
||||
which return a ``(out, err)`` tuple. ``out`` and ``err`` will be ``bytes``
|
||||
objects.
|
||||
capfd
|
||||
Enable capturing of writes to file descriptors 1 and 2 and make
|
||||
captured output available via ``capfd.readouterr()`` method calls
|
||||
which return a ``(out, err)`` tuple.
|
||||
which return a ``(out, err)`` tuple. ``out`` and ``err`` will be ``text``
|
||||
objects.
|
||||
capfdbinary
|
||||
Enable capturing of write to file descriptors 1 and 2 and make
|
||||
captured output available via ``capfdbinary.readouterr`` method calls
|
||||
which return a ``(out, err)`` tuple. ``out`` and ``err`` will be
|
||||
``bytes`` objects.
|
||||
doctest_namespace
|
||||
Inject names into the doctest namespace.
|
||||
pytestconfig
|
||||
@@ -104,6 +116,18 @@ You can ask for available builtin or project-custom
|
||||
Add extra xml properties to the tag for the calling test.
|
||||
The fixture is callable with ``(name, value)``, with value being automatically
|
||||
xml-encoded.
|
||||
record_xml_attribute
|
||||
Add extra xml attributes to the tag for the calling test.
|
||||
The fixture is callable with ``(name, value)``, with value being automatically
|
||||
xml-encoded
|
||||
caplog
|
||||
Access and control log capturing.
|
||||
|
||||
Captured logs are available through the following methods::
|
||||
|
||||
* caplog.text() -> string containing formatted log output
|
||||
* caplog.records() -> list of logging.LogRecord instances
|
||||
* caplog.record_tuples() -> list of (logger_name, level, message) tuples
|
||||
monkeypatch
|
||||
The returned ``monkeypatch`` fixture provides these
|
||||
helper methods to modify objects, dictionaries or os.environ::
|
||||
|
||||
@@ -46,9 +46,9 @@ First, let's create 50 test invocation of which only 2 fail::
|
||||
If you run this for the first time you will see two failures::
|
||||
|
||||
$ pytest -q
|
||||
.................F.......F........................
|
||||
======= FAILURES ========
|
||||
_______ test_num[17] ________
|
||||
.................F.......F........................ [100%]
|
||||
================================= FAILURES =================================
|
||||
_______________________________ test_num[17] _______________________________
|
||||
|
||||
i = 17
|
||||
|
||||
@@ -59,7 +59,7 @@ If you run this for the first time you will see two failures::
|
||||
E Failed: bad luck
|
||||
|
||||
test_50.py:6: Failed
|
||||
_______ test_num[25] ________
|
||||
_______________________________ test_num[25] _______________________________
|
||||
|
||||
i = 25
|
||||
|
||||
@@ -75,16 +75,16 @@ If you run this for the first time you will see two failures::
|
||||
If you then run it with ``--lf``::
|
||||
|
||||
$ pytest --lf
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 50 items
|
||||
run-last-failure: rerun previous 2 failures
|
||||
|
||||
test_50.py FF
|
||||
test_50.py FF [100%]
|
||||
|
||||
======= FAILURES ========
|
||||
_______ test_num[17] ________
|
||||
================================= FAILURES =================================
|
||||
_______________________________ test_num[17] _______________________________
|
||||
|
||||
i = 17
|
||||
|
||||
@@ -95,7 +95,7 @@ If you then run it with ``--lf``::
|
||||
E Failed: bad luck
|
||||
|
||||
test_50.py:6: Failed
|
||||
_______ test_num[25] ________
|
||||
_______________________________ test_num[25] _______________________________
|
||||
|
||||
i = 25
|
||||
|
||||
@@ -106,8 +106,8 @@ If you then run it with ``--lf``::
|
||||
E Failed: bad luck
|
||||
|
||||
test_50.py:6: Failed
|
||||
======= 48 tests deselected ========
|
||||
======= 2 failed, 48 deselected in 0.12 seconds ========
|
||||
=========================== 48 tests deselected ============================
|
||||
================= 2 failed, 48 deselected in 0.12 seconds ==================
|
||||
|
||||
You have run only the two failing test from the last run, while 48 tests have
|
||||
not been run ("deselected").
|
||||
@@ -117,16 +117,16 @@ previous failures will be executed first (as can be seen from the series
|
||||
of ``FF`` and dots)::
|
||||
|
||||
$ pytest --ff
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 50 items
|
||||
run-last-failure: rerun previous 2 failures first
|
||||
|
||||
test_50.py FF................................................
|
||||
test_50.py FF................................................ [100%]
|
||||
|
||||
======= FAILURES ========
|
||||
_______ test_num[17] ________
|
||||
================================= FAILURES =================================
|
||||
_______________________________ test_num[17] _______________________________
|
||||
|
||||
i = 17
|
||||
|
||||
@@ -137,7 +137,7 @@ of ``FF`` and dots)::
|
||||
E Failed: bad luck
|
||||
|
||||
test_50.py:6: Failed
|
||||
_______ test_num[25] ________
|
||||
_______________________________ test_num[25] _______________________________
|
||||
|
||||
i = 25
|
||||
|
||||
@@ -148,7 +148,7 @@ of ``FF`` and dots)::
|
||||
E Failed: bad luck
|
||||
|
||||
test_50.py:6: Failed
|
||||
======= 2 failed, 48 passed in 0.12 seconds ========
|
||||
=================== 2 failed, 48 passed in 0.12 seconds ====================
|
||||
|
||||
.. _`config.cache`:
|
||||
|
||||
@@ -182,9 +182,9 @@ If you run this command once, it will take a while because
|
||||
of the sleep::
|
||||
|
||||
$ pytest -q
|
||||
F
|
||||
======= FAILURES ========
|
||||
_______ test_function ________
|
||||
F [100%]
|
||||
================================= FAILURES =================================
|
||||
______________________________ test_function _______________________________
|
||||
|
||||
mydata = 42
|
||||
|
||||
@@ -199,9 +199,9 @@ If you run it a second time the value will be retrieved from
|
||||
the cache and this will be quick::
|
||||
|
||||
$ pytest -q
|
||||
F
|
||||
======= FAILURES ========
|
||||
_______ test_function ________
|
||||
F [100%]
|
||||
================================= FAILURES =================================
|
||||
______________________________ test_function _______________________________
|
||||
|
||||
mydata = 42
|
||||
|
||||
@@ -222,17 +222,17 @@ You can always peek at the content of the cache using the
|
||||
``--cache-show`` command line option::
|
||||
|
||||
$ py.test --cache-show
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
cachedir: $REGENDOC_TMPDIR/.cache
|
||||
cachedir: $REGENDOC_TMPDIR/.pytest_cache
|
||||
------------------------------- cache values -------------------------------
|
||||
cache/lastfailed contains:
|
||||
{'test_caching.py::test_function': True}
|
||||
example/value contains:
|
||||
42
|
||||
|
||||
======= no tests ran in 0.12 seconds ========
|
||||
======================= no tests ran in 0.12 seconds =======================
|
||||
|
||||
Clearing Cache content
|
||||
-------------------------------
|
||||
|
||||
@@ -63,15 +63,15 @@ and running this module will show you precisely the output
|
||||
of the failing function and hide the other one::
|
||||
|
||||
$ pytest
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 2 items
|
||||
|
||||
test_module.py .F
|
||||
test_module.py .F [100%]
|
||||
|
||||
======= FAILURES ========
|
||||
_______ test_func2 ________
|
||||
================================= FAILURES =================================
|
||||
________________________________ test_func2 ________________________________
|
||||
|
||||
def test_func2():
|
||||
> assert False
|
||||
@@ -80,26 +80,26 @@ of the failing function and hide the other one::
|
||||
test_module.py:9: AssertionError
|
||||
-------------------------- Captured stdout setup ---------------------------
|
||||
setting up <function test_func2 at 0xdeadbeef>
|
||||
======= 1 failed, 1 passed in 0.12 seconds ========
|
||||
==================== 1 failed, 1 passed in 0.12 seconds ====================
|
||||
|
||||
Accessing captured output from a test function
|
||||
---------------------------------------------------
|
||||
|
||||
The ``capsys`` and ``capfd`` fixtures allow to access stdout/stderr
|
||||
output created during test execution. Here is an example test function
|
||||
that performs some output related checks:
|
||||
The ``capsys``, ``capsysbinary``, ``capfd``, and ``capfdbinary`` fixtures
|
||||
allow access to stdout/stderr output created during test execution. Here is
|
||||
an example test function that performs some output related checks:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_myoutput(capsys): # or use "capfd" for fd-level
|
||||
print ("hello")
|
||||
print("hello")
|
||||
sys.stderr.write("world\n")
|
||||
out, err = capsys.readouterr()
|
||||
assert out == "hello\n"
|
||||
assert err == "world\n"
|
||||
print ("next")
|
||||
out, err = capsys.readouterr()
|
||||
assert out == "next\n"
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
assert captured.err == "world\n"
|
||||
print("next")
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == "next\n"
|
||||
|
||||
The ``readouterr()`` call snapshots the output so far -
|
||||
and capturing will be continued. After the test
|
||||
@@ -110,11 +110,30 @@ output streams and also interacts well with pytest's
|
||||
own per-test capturing.
|
||||
|
||||
If you want to capture on filedescriptor level you can use
|
||||
the ``capfd`` function argument which offers the exact
|
||||
the ``capfd`` fixture which offers the exact
|
||||
same interface but allows to also capture output from
|
||||
libraries or subprocesses that directly write to operating
|
||||
system level output streams (FD1 and FD2).
|
||||
|
||||
.. versionadded:: 3.3
|
||||
|
||||
The return value from ``readouterr`` changed to a ``namedtuple`` with two attributes, ``out`` and ``err``.
|
||||
|
||||
.. versionadded:: 3.3
|
||||
|
||||
If the code under test writes non-textual data, you can capture this using
|
||||
the ``capsysbinary`` fixture which instead returns ``bytes`` from
|
||||
the ``readouterr`` method. The ``capfsysbinary`` fixture is currently only
|
||||
available in python 3.
|
||||
|
||||
|
||||
.. versionadded:: 3.3
|
||||
|
||||
If the code under test writes non-textual data, you can capture this using
|
||||
the ``capfdbinary`` fixture which instead returns ``bytes`` from
|
||||
the ``readouterr`` method. The ``capfdbinary`` fixture operates on the
|
||||
filedescriptor level.
|
||||
|
||||
|
||||
.. versionadded:: 3.0
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ Full pytest documentation
|
||||
xunit_setup
|
||||
plugins
|
||||
writing_plugins
|
||||
logging
|
||||
|
||||
goodpractices
|
||||
pythonpath
|
||||
|
||||
@@ -152,11 +152,25 @@ above will show verbose output because ``-v`` overwrites ``-q``.
|
||||
Builtin configuration file options
|
||||
----------------------------------------------
|
||||
|
||||
Here is a list of builtin configuration options that may be written in a ``pytest.ini``, ``tox.ini`` or ``setup.cfg``
|
||||
file, usually located at the root of your repository. All options must be under a ``[pytest]`` section
|
||||
(``[tool:pytest]`` for ``setup.cfg`` files).
|
||||
|
||||
Configuration file options may be overwritten in the command-line by using ``-o/--override``, which can also be
|
||||
passed multiple times. The expected format is ``name=value``. For example::
|
||||
|
||||
pytest -o console_output_style=classic -o cache_dir=/tmp/mycache
|
||||
|
||||
|
||||
.. confval:: minversion
|
||||
|
||||
Specifies a minimal pytest version required for running tests.
|
||||
|
||||
minversion = 2.1 # will fail if we run with pytest-2.0
|
||||
.. code-block:: ini
|
||||
|
||||
# content of pytest.ini
|
||||
[pytest]
|
||||
minversion = 3.0 # will fail if we run with pytest-2.8
|
||||
|
||||
.. confval:: addopts
|
||||
|
||||
@@ -165,6 +179,7 @@ Builtin configuration file options
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
# content of pytest.ini
|
||||
[pytest]
|
||||
addopts = --maxfail=2 -rf # exit after 2 failures, report fail info
|
||||
|
||||
@@ -312,3 +327,47 @@ Builtin configuration file options
|
||||
relative to :ref:`rootdir <rootdir>`. Additionally path may contain environment
|
||||
variables, that will be expanded. For more information about cache plugin
|
||||
please refer to :ref:`cache_provider`.
|
||||
|
||||
|
||||
.. confval:: console_output_style
|
||||
|
||||
.. versionadded:: 3.3
|
||||
|
||||
Sets the console output style while running tests:
|
||||
|
||||
* ``classic``: classic pytest output.
|
||||
* ``progress``: like classic pytest output, but with a progress indicator.
|
||||
|
||||
The default is ``progress``, but you can fallback to ``classic`` if you prefer or
|
||||
the new mode is causing unexpected problems:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
# content of pytest.ini
|
||||
[pytest]
|
||||
console_output_style = classic
|
||||
|
||||
|
||||
.. confval:: empty_parameter_set_mark
|
||||
|
||||
.. versionadded:: 3.4
|
||||
|
||||
Allows to pick the action for empty parametersets in parameterization
|
||||
|
||||
* ``skip`` skips tests with a empty parameterset (default)
|
||||
* ``xfail`` marks tests with a empty parameterset as xfail(run=False)
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
# content of pytest.ini
|
||||
[pytest]
|
||||
empty_parameter_set_mark = xfail
|
||||
|
||||
.. note::
|
||||
|
||||
The default value of this option is planned to change to ``xfail`` in future releases
|
||||
as this is considered less error prone, see `#3155`_ for more details.
|
||||
|
||||
|
||||
|
||||
.. _`#3155`: https://github.com/pytest-dev/pytest/issues/3155
|
||||
|
||||
@@ -61,14 +61,14 @@ and another like this::
|
||||
then you can just invoke ``pytest`` without command line options::
|
||||
|
||||
$ pytest
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini
|
||||
collected 1 item
|
||||
|
||||
mymodule.py .
|
||||
mymodule.py . [100%]
|
||||
|
||||
======= 1 passed in 0.12 seconds ========
|
||||
========================= 1 passed in 0.12 seconds =========================
|
||||
|
||||
It is possible to use fixtures using the ``getfixture`` helper::
|
||||
|
||||
@@ -81,9 +81,9 @@ Also, :ref:`usefixtures` and :ref:`autouse` fixtures are supported
|
||||
when executing text doctest files.
|
||||
|
||||
The standard ``doctest`` module provides some setting flags to configure the
|
||||
strictness of doctest tests. In pytest You can enable those flags those flags
|
||||
using the configuration file. To make pytest ignore trailing whitespaces and
|
||||
ignore lengthy exception stack traces you can just write:
|
||||
strictness of doctest tests. In pytest, you can enable those flags using the
|
||||
configuration file. To make pytest ignore trailing whitespaces and ignore
|
||||
lengthy exception stack traces you can just write:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
|
||||
@@ -157,12 +157,14 @@ class TestRaises(object):
|
||||
|
||||
# thanks to Matthew Scott for this test
|
||||
def test_dynamic_compile_shows_nicely():
|
||||
import imp
|
||||
import sys
|
||||
src = 'def foo():\n assert 1 == 0\n'
|
||||
name = 'abc-123'
|
||||
module = py.std.imp.new_module(name)
|
||||
module = imp.new_module(name)
|
||||
code = _pytest._code.compile(src, name, 'exec')
|
||||
py.builtin.exec_(code, module.__dict__)
|
||||
py.std.sys.modules[name] = module
|
||||
sys.modules[name] = module
|
||||
module.foo()
|
||||
|
||||
|
||||
|
||||
@@ -30,32 +30,32 @@ You can "mark" a test function with custom metadata like this::
|
||||
You can then restrict a test run to only run tests marked with ``webtest``::
|
||||
|
||||
$ pytest -v -m webtest
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 4 items
|
||||
|
||||
test_server.py::test_send_http PASSED
|
||||
test_server.py::test_send_http PASSED [100%]
|
||||
|
||||
======= 3 tests deselected ========
|
||||
======= 1 passed, 3 deselected in 0.12 seconds ========
|
||||
============================ 3 tests deselected ============================
|
||||
================== 1 passed, 3 deselected in 0.12 seconds ==================
|
||||
|
||||
Or the inverse, running all tests except the webtest ones::
|
||||
|
||||
$ pytest -v -m "not webtest"
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 4 items
|
||||
|
||||
test_server.py::test_something_quick PASSED
|
||||
test_server.py::test_another PASSED
|
||||
test_server.py::TestClass::test_method PASSED
|
||||
test_server.py::test_something_quick PASSED [ 33%]
|
||||
test_server.py::test_another PASSED [ 66%]
|
||||
test_server.py::TestClass::test_method PASSED [100%]
|
||||
|
||||
======= 1 tests deselected ========
|
||||
======= 3 passed, 1 deselected in 0.12 seconds ========
|
||||
============================ 1 tests deselected ============================
|
||||
================== 3 passed, 1 deselected in 0.12 seconds ==================
|
||||
|
||||
Selecting tests based on their node ID
|
||||
--------------------------------------
|
||||
@@ -65,42 +65,42 @@ arguments to select only specified tests. This makes it easy to select
|
||||
tests based on their module, class, method, or function name::
|
||||
|
||||
$ pytest -v test_server.py::TestClass::test_method
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 1 item
|
||||
|
||||
test_server.py::TestClass::test_method PASSED
|
||||
test_server.py::TestClass::test_method PASSED [100%]
|
||||
|
||||
======= 1 passed in 0.12 seconds ========
|
||||
========================= 1 passed in 0.12 seconds =========================
|
||||
|
||||
You can also select on the class::
|
||||
|
||||
$ pytest -v test_server.py::TestClass
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 1 item
|
||||
|
||||
test_server.py::TestClass::test_method PASSED
|
||||
test_server.py::TestClass::test_method PASSED [100%]
|
||||
|
||||
======= 1 passed in 0.12 seconds ========
|
||||
========================= 1 passed in 0.12 seconds =========================
|
||||
|
||||
Or select multiple nodes::
|
||||
|
||||
$ pytest -v test_server.py::TestClass test_server.py::test_send_http
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 2 items
|
||||
|
||||
test_server.py::TestClass::test_method PASSED
|
||||
test_server.py::test_send_http PASSED
|
||||
test_server.py::TestClass::test_method PASSED [ 50%]
|
||||
test_server.py::test_send_http PASSED [100%]
|
||||
|
||||
======= 2 passed in 0.12 seconds ========
|
||||
========================= 2 passed in 0.12 seconds =========================
|
||||
|
||||
.. _node-id:
|
||||
|
||||
@@ -129,47 +129,47 @@ exact match on markers that ``-m`` provides. This makes it easy to
|
||||
select tests based on their names::
|
||||
|
||||
$ pytest -v -k http # running with the above defined example module
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 4 items
|
||||
|
||||
test_server.py::test_send_http PASSED
|
||||
test_server.py::test_send_http PASSED [100%]
|
||||
|
||||
======= 3 tests deselected ========
|
||||
======= 1 passed, 3 deselected in 0.12 seconds ========
|
||||
============================ 3 tests deselected ============================
|
||||
================== 1 passed, 3 deselected in 0.12 seconds ==================
|
||||
|
||||
And you can also run all tests except the ones that match the keyword::
|
||||
|
||||
$ pytest -k "not send_http" -v
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 4 items
|
||||
|
||||
test_server.py::test_something_quick PASSED
|
||||
test_server.py::test_another PASSED
|
||||
test_server.py::TestClass::test_method PASSED
|
||||
test_server.py::test_something_quick PASSED [ 33%]
|
||||
test_server.py::test_another PASSED [ 66%]
|
||||
test_server.py::TestClass::test_method PASSED [100%]
|
||||
|
||||
======= 1 tests deselected ========
|
||||
======= 3 passed, 1 deselected in 0.12 seconds ========
|
||||
============================ 1 tests deselected ============================
|
||||
================== 3 passed, 1 deselected in 0.12 seconds ==================
|
||||
|
||||
Or to select "http" and "quick" tests::
|
||||
|
||||
$ pytest -k "http or quick" -v
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 4 items
|
||||
|
||||
test_server.py::test_send_http PASSED
|
||||
test_server.py::test_something_quick PASSED
|
||||
test_server.py::test_send_http PASSED [ 50%]
|
||||
test_server.py::test_something_quick PASSED [100%]
|
||||
|
||||
======= 2 tests deselected ========
|
||||
======= 2 passed, 2 deselected in 0.12 seconds ========
|
||||
============================ 2 tests deselected ============================
|
||||
================== 2 passed, 2 deselected in 0.12 seconds ==================
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -354,26 +354,26 @@ and an example invocations specifying a different environment than what
|
||||
the test needs::
|
||||
|
||||
$ pytest -E stage2
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 1 item
|
||||
|
||||
test_someenv.py s
|
||||
test_someenv.py s [100%]
|
||||
|
||||
======= 1 skipped in 0.12 seconds ========
|
||||
======================== 1 skipped in 0.12 seconds =========================
|
||||
|
||||
and here is one that specifies exactly the environment needed::
|
||||
|
||||
$ pytest -E stage1
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 1 item
|
||||
|
||||
test_someenv.py .
|
||||
test_someenv.py . [100%]
|
||||
|
||||
======= 1 passed in 0.12 seconds ========
|
||||
========================= 1 passed in 0.12 seconds =========================
|
||||
|
||||
The ``--markers`` option always gives you a list of available markers::
|
||||
|
||||
@@ -530,29 +530,29 @@ Let's do a little test file to show how this looks like::
|
||||
then you will see two tests skipped and two executed tests as expected::
|
||||
|
||||
$ pytest -rs # this option reports skip reasons
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 4 items
|
||||
|
||||
test_plat.py s.s.
|
||||
======= short test summary info ========
|
||||
test_plat.py s.s. [100%]
|
||||
========================= short test summary info ==========================
|
||||
SKIP [2] $REGENDOC_TMPDIR/conftest.py:13: cannot run on platform linux
|
||||
|
||||
======= 2 passed, 2 skipped in 0.12 seconds ========
|
||||
=================== 2 passed, 2 skipped in 0.12 seconds ====================
|
||||
|
||||
Note that if you specify a platform via the marker-command line option like this::
|
||||
|
||||
$ pytest -m linux
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 4 items
|
||||
|
||||
test_plat.py .
|
||||
test_plat.py . [100%]
|
||||
|
||||
======= 3 tests deselected ========
|
||||
======= 1 passed, 3 deselected in 0.12 seconds ========
|
||||
============================ 3 tests deselected ============================
|
||||
================== 1 passed, 3 deselected in 0.12 seconds ==================
|
||||
|
||||
then the unmarked-tests will not be run. It is thus a way to restrict the run to the specific tests.
|
||||
|
||||
@@ -596,47 +596,47 @@ We want to dynamically define two markers and can do it in a
|
||||
We can now use the ``-m option`` to select one set::
|
||||
|
||||
$ pytest -m interface --tb=short
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 4 items
|
||||
|
||||
test_module.py FF
|
||||
test_module.py FF [100%]
|
||||
|
||||
======= FAILURES ========
|
||||
_______ test_interface_simple ________
|
||||
================================= FAILURES =================================
|
||||
__________________________ test_interface_simple ___________________________
|
||||
test_module.py:3: in test_interface_simple
|
||||
assert 0
|
||||
E assert 0
|
||||
_______ test_interface_complex ________
|
||||
__________________________ test_interface_complex __________________________
|
||||
test_module.py:6: in test_interface_complex
|
||||
assert 0
|
||||
E assert 0
|
||||
======= 2 tests deselected ========
|
||||
======= 2 failed, 2 deselected in 0.12 seconds ========
|
||||
============================ 2 tests deselected ============================
|
||||
================== 2 failed, 2 deselected in 0.12 seconds ==================
|
||||
|
||||
or to select both "event" and "interface" tests::
|
||||
|
||||
$ pytest -m "interface or event" --tb=short
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 4 items
|
||||
|
||||
test_module.py FFF
|
||||
test_module.py FFF [100%]
|
||||
|
||||
======= FAILURES ========
|
||||
_______ test_interface_simple ________
|
||||
================================= FAILURES =================================
|
||||
__________________________ test_interface_simple ___________________________
|
||||
test_module.py:3: in test_interface_simple
|
||||
assert 0
|
||||
E assert 0
|
||||
_______ test_interface_complex ________
|
||||
__________________________ test_interface_complex __________________________
|
||||
test_module.py:6: in test_interface_complex
|
||||
assert 0
|
||||
E assert 0
|
||||
_______ test_event_simple ________
|
||||
____________________________ test_event_simple _____________________________
|
||||
test_module.py:9: in test_event_simple
|
||||
assert 0
|
||||
E assert 0
|
||||
======= 1 tests deselected ========
|
||||
======= 3 failed, 1 deselected in 0.12 seconds ========
|
||||
============================ 1 tests deselected ============================
|
||||
================== 3 failed, 1 deselected in 0.12 seconds ==================
|
||||
|
||||
@@ -6,7 +6,7 @@ import py
|
||||
import pytest
|
||||
import _pytest._code
|
||||
|
||||
pythonlist = ['python2.6', 'python2.7', 'python3.4', 'python3.5']
|
||||
pythonlist = ['python2.7', 'python3.4', 'python3.5']
|
||||
@pytest.fixture(params=pythonlist)
|
||||
def python1(request, tmpdir):
|
||||
picklefile = tmpdir.join("data.pickle")
|
||||
|
||||
@@ -26,19 +26,19 @@ and if you installed `PyYAML`_ or a compatible YAML-parser you can
|
||||
now execute the test specification::
|
||||
|
||||
nonpython $ pytest test_simple.yml
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR/nonpython, inifile:
|
||||
collected 2 items
|
||||
|
||||
test_simple.yml F.
|
||||
test_simple.yml F. [100%]
|
||||
|
||||
======= FAILURES ========
|
||||
_______ usecase: hello ________
|
||||
================================= FAILURES =================================
|
||||
______________________________ usecase: hello ______________________________
|
||||
usecase execution failed
|
||||
spec failed: 'some': 'other'
|
||||
no further details known at this point.
|
||||
======= 1 failed, 1 passed in 0.12 seconds ========
|
||||
==================== 1 failed, 1 passed in 0.12 seconds ====================
|
||||
|
||||
.. regendoc:wipe
|
||||
|
||||
@@ -58,21 +58,21 @@ your own domain specific testing language this way.
|
||||
consulted when reporting in ``verbose`` mode::
|
||||
|
||||
nonpython $ pytest -v
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR/nonpython, inifile:
|
||||
collecting ... collected 2 items
|
||||
|
||||
test_simple.yml::hello FAILED
|
||||
test_simple.yml::ok PASSED
|
||||
test_simple.yml::hello FAILED [ 50%]
|
||||
test_simple.yml::ok PASSED [100%]
|
||||
|
||||
======= FAILURES ========
|
||||
_______ usecase: hello ________
|
||||
================================= FAILURES =================================
|
||||
______________________________ usecase: hello ______________________________
|
||||
usecase execution failed
|
||||
spec failed: 'some': 'other'
|
||||
no further details known at this point.
|
||||
======= 1 failed, 1 passed in 0.12 seconds ========
|
||||
==================== 1 failed, 1 passed in 0.12 seconds ====================
|
||||
|
||||
.. regendoc:wipe
|
||||
|
||||
@@ -80,7 +80,7 @@ While developing your custom test collection and execution it's also
|
||||
interesting to just look at the collection tree::
|
||||
|
||||
nonpython $ pytest --collect-only
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR/nonpython, inifile:
|
||||
collected 2 items
|
||||
@@ -88,4 +88,4 @@ interesting to just look at the collection tree::
|
||||
<YamlItem 'hello'>
|
||||
<YamlItem 'ok'>
|
||||
|
||||
======= no tests ran in 0.12 seconds ========
|
||||
======================= no tests ran in 0.12 seconds =======================
|
||||
|
||||
@@ -45,16 +45,16 @@ Now we add a test configuration like this::
|
||||
This means that we only run 2 tests if we do not pass ``--all``::
|
||||
|
||||
$ pytest -q test_compute.py
|
||||
..
|
||||
.. [100%]
|
||||
2 passed in 0.12 seconds
|
||||
|
||||
We run only two computations, so we see two dots.
|
||||
let's run the full monty::
|
||||
|
||||
$ pytest -q --all
|
||||
....F
|
||||
======= FAILURES ========
|
||||
_______ test_compute[4] ________
|
||||
....F [100%]
|
||||
================================= FAILURES =================================
|
||||
_____________________________ test_compute[4] ______________________________
|
||||
|
||||
param1 = 4
|
||||
|
||||
@@ -138,7 +138,7 @@ objects, they are still using the default pytest representation::
|
||||
|
||||
|
||||
$ pytest test_time.py --collect-only
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 8 items
|
||||
@@ -152,7 +152,7 @@ objects, they are still using the default pytest representation::
|
||||
<Function 'test_timedistance_v3[forward]'>
|
||||
<Function 'test_timedistance_v3[backward]'>
|
||||
|
||||
======= no tests ran in 0.12 seconds ========
|
||||
======================= no tests ran in 0.12 seconds =======================
|
||||
|
||||
In ``test_timedistance_v3``, we used ``pytest.param`` to specify the test IDs
|
||||
together with the actual data, instead of listing them separately.
|
||||
@@ -194,20 +194,20 @@ only have to work a bit to construct the correct arguments for pytest's
|
||||
this is a fully self-contained example which you can run with::
|
||||
|
||||
$ pytest test_scenarios.py
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 4 items
|
||||
|
||||
test_scenarios.py ....
|
||||
test_scenarios.py .... [100%]
|
||||
|
||||
======= 4 passed in 0.12 seconds ========
|
||||
========================= 4 passed in 0.12 seconds =========================
|
||||
|
||||
If you just collect tests you'll also nicely see 'advanced' and 'basic' as variants for the test function::
|
||||
|
||||
|
||||
$ pytest --collect-only test_scenarios.py
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 4 items
|
||||
@@ -219,7 +219,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia
|
||||
<Function 'test_demo1[advanced]'>
|
||||
<Function 'test_demo2[advanced]'>
|
||||
|
||||
======= no tests ran in 0.12 seconds ========
|
||||
======================= no tests ran in 0.12 seconds =======================
|
||||
|
||||
Note that we told ``metafunc.parametrize()`` that your scenario values
|
||||
should be considered class-scoped. With pytest-2.3 this leads to a
|
||||
@@ -272,7 +272,7 @@ creates a database object for the actual test invocations::
|
||||
Let's first see how it looks like at collection time::
|
||||
|
||||
$ pytest test_backends.py --collect-only
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 2 items
|
||||
@@ -280,14 +280,14 @@ Let's first see how it looks like at collection time::
|
||||
<Function 'test_db_initialized[d1]'>
|
||||
<Function 'test_db_initialized[d2]'>
|
||||
|
||||
======= no tests ran in 0.12 seconds ========
|
||||
======================= no tests ran in 0.12 seconds =======================
|
||||
|
||||
And then when we run the test::
|
||||
|
||||
$ pytest -q test_backends.py
|
||||
.F
|
||||
======= FAILURES ========
|
||||
_______ test_db_initialized[d2] ________
|
||||
.F [100%]
|
||||
================================= FAILURES =================================
|
||||
_________________________ test_db_initialized[d2] __________________________
|
||||
|
||||
db = <conftest.DB2 object at 0xdeadbeef>
|
||||
|
||||
@@ -333,14 +333,14 @@ will be passed to respective fixture function::
|
||||
The result of this test will be successful::
|
||||
|
||||
$ pytest test_indirect_list.py --collect-only
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 1 item
|
||||
<Module 'test_indirect_list.py'>
|
||||
<Function 'test_indirect[a-b]'>
|
||||
|
||||
======= no tests ran in 0.12 seconds ========
|
||||
======================= no tests ran in 0.12 seconds =======================
|
||||
|
||||
.. regendoc:wipe
|
||||
|
||||
@@ -381,9 +381,9 @@ Our test generator looks up a class-level definition which specifies which
|
||||
argument sets to use for each test function. Let's run it::
|
||||
|
||||
$ pytest -q
|
||||
F..
|
||||
======= FAILURES ========
|
||||
_______ TestClass.test_equals[1-2] ________
|
||||
F.. [100%]
|
||||
================================= FAILURES =================================
|
||||
________________________ TestClass.test_equals[1-2] ________________________
|
||||
|
||||
self = <test_parametrize.TestClass object at 0xdeadbeef>, a = 1, b = 2
|
||||
|
||||
@@ -411,10 +411,8 @@ is to be run with different sets of arguments for its three arguments:
|
||||
Running it results in some skips if we don't have all the python interpreters installed and otherwise runs all combinations (5 interpreters times 5 interpreters times 3 objects to serialize/deserialize)::
|
||||
|
||||
. $ pytest -rs -q multipython.py
|
||||
sssssssssssssss.........sss.........sss.........
|
||||
======= short test summary info ========
|
||||
SKIP [21] $REGENDOC_TMPDIR/CWD/multipython.py:24: 'python2.6' not found
|
||||
27 passed, 21 skipped in 0.12 seconds
|
||||
........................... [100%]
|
||||
27 passed in 0.12 seconds
|
||||
|
||||
Indirect parametrization of optional implementations/imports
|
||||
--------------------------------------------------------------------
|
||||
@@ -460,16 +458,16 @@ And finally a little test module::
|
||||
If you run this with reporting for skips enabled::
|
||||
|
||||
$ pytest -rs test_module.py
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 2 items
|
||||
|
||||
test_module.py .s
|
||||
======= short test summary info ========
|
||||
test_module.py .s [100%]
|
||||
========================= short test summary info ==========================
|
||||
SKIP [1] $REGENDOC_TMPDIR/conftest.py:11: could not import 'opt2'
|
||||
|
||||
======= 1 passed, 1 skipped in 0.12 seconds ========
|
||||
=================== 1 passed, 1 skipped in 0.12 seconds ====================
|
||||
|
||||
You'll see that we don't have a ``opt2`` module and thus the second test run
|
||||
of our ``test_func1`` was skipped. A few notes:
|
||||
|
||||
@@ -116,7 +116,7 @@ that match ``*_check``. For example, if we have::
|
||||
then the test collection looks like this::
|
||||
|
||||
$ pytest --collect-only
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini
|
||||
collected 2 items
|
||||
@@ -126,7 +126,7 @@ then the test collection looks like this::
|
||||
<Function 'simple_check'>
|
||||
<Function 'complex_check'>
|
||||
|
||||
======= no tests ran in 0.12 seconds ========
|
||||
======================= no tests ran in 0.12 seconds =======================
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -162,7 +162,7 @@ Finding out what is collected
|
||||
You can always peek at the collection tree without running tests like this::
|
||||
|
||||
. $ pytest --collect-only pythoncollection.py
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini
|
||||
collected 3 items
|
||||
@@ -173,7 +173,7 @@ You can always peek at the collection tree without running tests like this::
|
||||
<Function 'test_method'>
|
||||
<Function 'test_anothermethod'>
|
||||
|
||||
======= no tests ran in 0.12 seconds ========
|
||||
======================= no tests ran in 0.12 seconds =======================
|
||||
|
||||
.. _customizing-test-collection:
|
||||
|
||||
@@ -231,9 +231,9 @@ If you run with a Python 3 interpreter both the one test and the ``setup.py``
|
||||
file will be left out::
|
||||
|
||||
$ pytest --collect-only
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini
|
||||
collected 0 items
|
||||
|
||||
======= no tests ran in 0.12 seconds ========
|
||||
======================= no tests ran in 0.12 seconds =======================
|
||||
|
||||
@@ -10,15 +10,15 @@ not showing the nice colors here in the HTML that you
|
||||
get on the terminal - we are working on that)::
|
||||
|
||||
assertion $ pytest failure_demo.py
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR/assertion, inifile:
|
||||
collected 42 items
|
||||
|
||||
failure_demo.py FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
|
||||
failure_demo.py FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF [100%]
|
||||
|
||||
======= FAILURES ========
|
||||
_______ test_generative[0] ________
|
||||
================================= FAILURES =================================
|
||||
____________________________ test_generative[0] ____________________________
|
||||
|
||||
param1 = 3, param2 = 6
|
||||
|
||||
@@ -27,7 +27,7 @@ get on the terminal - we are working on that)::
|
||||
E assert (3 * 2) < 6
|
||||
|
||||
failure_demo.py:16: AssertionError
|
||||
_______ TestFailing.test_simple ________
|
||||
_________________________ TestFailing.test_simple __________________________
|
||||
|
||||
self = <failure_demo.TestFailing object at 0xdeadbeef>
|
||||
|
||||
@@ -43,7 +43,7 @@ get on the terminal - we are working on that)::
|
||||
E + and 43 = <function TestFailing.test_simple.<locals>.g at 0xdeadbeef>()
|
||||
|
||||
failure_demo.py:29: AssertionError
|
||||
_______ TestFailing.test_simple_multiline ________
|
||||
____________________ TestFailing.test_simple_multiline _____________________
|
||||
|
||||
self = <failure_demo.TestFailing object at 0xdeadbeef>
|
||||
|
||||
@@ -63,7 +63,7 @@ get on the terminal - we are working on that)::
|
||||
E assert 42 == 54
|
||||
|
||||
failure_demo.py:12: AssertionError
|
||||
_______ TestFailing.test_not ________
|
||||
___________________________ TestFailing.test_not ___________________________
|
||||
|
||||
self = <failure_demo.TestFailing object at 0xdeadbeef>
|
||||
|
||||
@@ -75,7 +75,7 @@ get on the terminal - we are working on that)::
|
||||
E + where 42 = <function TestFailing.test_not.<locals>.f at 0xdeadbeef>()
|
||||
|
||||
failure_demo.py:39: AssertionError
|
||||
_______ TestSpecialisedExplanations.test_eq_text ________
|
||||
_________________ TestSpecialisedExplanations.test_eq_text _________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
|
||||
@@ -86,7 +86,7 @@ get on the terminal - we are working on that)::
|
||||
E + eggs
|
||||
|
||||
failure_demo.py:43: AssertionError
|
||||
_______ TestSpecialisedExplanations.test_eq_similar_text ________
|
||||
_____________ TestSpecialisedExplanations.test_eq_similar_text _____________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
|
||||
@@ -99,7 +99,7 @@ get on the terminal - we are working on that)::
|
||||
E ? ^
|
||||
|
||||
failure_demo.py:46: AssertionError
|
||||
_______ TestSpecialisedExplanations.test_eq_multiline_text ________
|
||||
____________ TestSpecialisedExplanations.test_eq_multiline_text ____________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
|
||||
@@ -112,7 +112,7 @@ get on the terminal - we are working on that)::
|
||||
E bar
|
||||
|
||||
failure_demo.py:49: AssertionError
|
||||
_______ TestSpecialisedExplanations.test_eq_long_text ________
|
||||
______________ TestSpecialisedExplanations.test_eq_long_text _______________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
|
||||
@@ -129,7 +129,7 @@ get on the terminal - we are working on that)::
|
||||
E ? ^
|
||||
|
||||
failure_demo.py:54: AssertionError
|
||||
_______ TestSpecialisedExplanations.test_eq_long_text_multiline ________
|
||||
_________ TestSpecialisedExplanations.test_eq_long_text_multiline __________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
|
||||
@@ -149,7 +149,7 @@ get on the terminal - we are working on that)::
|
||||
E ...Full output truncated (7 lines hidden), use '-vv' to show
|
||||
|
||||
failure_demo.py:59: AssertionError
|
||||
_______ TestSpecialisedExplanations.test_eq_list ________
|
||||
_________________ TestSpecialisedExplanations.test_eq_list _________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
|
||||
@@ -160,7 +160,7 @@ get on the terminal - we are working on that)::
|
||||
E Use -v to get the full diff
|
||||
|
||||
failure_demo.py:62: AssertionError
|
||||
_______ TestSpecialisedExplanations.test_eq_list_long ________
|
||||
______________ TestSpecialisedExplanations.test_eq_list_long _______________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
|
||||
@@ -173,7 +173,7 @@ get on the terminal - we are working on that)::
|
||||
E Use -v to get the full diff
|
||||
|
||||
failure_demo.py:67: AssertionError
|
||||
_______ TestSpecialisedExplanations.test_eq_dict ________
|
||||
_________________ TestSpecialisedExplanations.test_eq_dict _________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
|
||||
@@ -191,7 +191,7 @@ get on the terminal - we are working on that)::
|
||||
E ...Full output truncated (2 lines hidden), use '-vv' to show
|
||||
|
||||
failure_demo.py:70: AssertionError
|
||||
_______ TestSpecialisedExplanations.test_eq_set ________
|
||||
_________________ TestSpecialisedExplanations.test_eq_set __________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
|
||||
@@ -209,7 +209,7 @@ get on the terminal - we are working on that)::
|
||||
E ...Full output truncated (2 lines hidden), use '-vv' to show
|
||||
|
||||
failure_demo.py:73: AssertionError
|
||||
_______ TestSpecialisedExplanations.test_eq_longer_list ________
|
||||
_____________ TestSpecialisedExplanations.test_eq_longer_list ______________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
|
||||
@@ -220,7 +220,7 @@ get on the terminal - we are working on that)::
|
||||
E Use -v to get the full diff
|
||||
|
||||
failure_demo.py:76: AssertionError
|
||||
_______ TestSpecialisedExplanations.test_in_list ________
|
||||
_________________ TestSpecialisedExplanations.test_in_list _________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
|
||||
@@ -229,7 +229,7 @@ get on the terminal - we are working on that)::
|
||||
E assert 1 in [0, 2, 3, 4, 5]
|
||||
|
||||
failure_demo.py:79: AssertionError
|
||||
_______ TestSpecialisedExplanations.test_not_in_text_multiline ________
|
||||
__________ TestSpecialisedExplanations.test_not_in_text_multiline __________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
|
||||
@@ -248,7 +248,7 @@ get on the terminal - we are working on that)::
|
||||
E ...Full output truncated (2 lines hidden), use '-vv' to show
|
||||
|
||||
failure_demo.py:83: AssertionError
|
||||
_______ TestSpecialisedExplanations.test_not_in_text_single ________
|
||||
___________ TestSpecialisedExplanations.test_not_in_text_single ____________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
|
||||
@@ -261,7 +261,7 @@ get on the terminal - we are working on that)::
|
||||
E ? +++
|
||||
|
||||
failure_demo.py:87: AssertionError
|
||||
_______ TestSpecialisedExplanations.test_not_in_text_single_long ________
|
||||
_________ TestSpecialisedExplanations.test_not_in_text_single_long _________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
|
||||
@@ -287,7 +287,7 @@ get on the terminal - we are working on that)::
|
||||
E ? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
failure_demo.py:95: AssertionError
|
||||
_______ test_attribute ________
|
||||
______________________________ test_attribute ______________________________
|
||||
|
||||
def test_attribute():
|
||||
class Foo(object):
|
||||
@@ -298,7 +298,7 @@ get on the terminal - we are working on that)::
|
||||
E + where 1 = <failure_demo.test_attribute.<locals>.Foo object at 0xdeadbeef>.b
|
||||
|
||||
failure_demo.py:102: AssertionError
|
||||
_______ test_attribute_instance ________
|
||||
_________________________ test_attribute_instance __________________________
|
||||
|
||||
def test_attribute_instance():
|
||||
class Foo(object):
|
||||
@@ -309,7 +309,7 @@ get on the terminal - we are working on that)::
|
||||
E + where <failure_demo.test_attribute_instance.<locals>.Foo object at 0xdeadbeef> = <class 'failure_demo.test_attribute_instance.<locals>.Foo'>()
|
||||
|
||||
failure_demo.py:108: AssertionError
|
||||
_______ test_attribute_failure ________
|
||||
__________________________ test_attribute_failure __________________________
|
||||
|
||||
def test_attribute_failure():
|
||||
class Foo(object):
|
||||
@@ -329,7 +329,7 @@ get on the terminal - we are working on that)::
|
||||
E Exception: Failed to get attrib
|
||||
|
||||
failure_demo.py:114: Exception
|
||||
_______ test_attribute_multiple ________
|
||||
_________________________ test_attribute_multiple __________________________
|
||||
|
||||
def test_attribute_multiple():
|
||||
class Foo(object):
|
||||
@@ -344,7 +344,7 @@ get on the terminal - we are working on that)::
|
||||
E + where <failure_demo.test_attribute_multiple.<locals>.Bar object at 0xdeadbeef> = <class 'failure_demo.test_attribute_multiple.<locals>.Bar'>()
|
||||
|
||||
failure_demo.py:125: AssertionError
|
||||
_______ TestRaises.test_raises ________
|
||||
__________________________ TestRaises.test_raises __________________________
|
||||
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef>
|
||||
|
||||
@@ -358,8 +358,8 @@ get on the terminal - we are working on that)::
|
||||
> int(s)
|
||||
E ValueError: invalid literal for int() with base 10: 'qwe'
|
||||
|
||||
<0-codegen $PYTHON_PREFIX/lib/python3.5/site-packages/_pytest/python_api.py:580>:1: ValueError
|
||||
_______ TestRaises.test_raises_doesnt ________
|
||||
<0-codegen $PYTHON_PREFIX/lib/python3.5/site-packages/_pytest/python_api.py:595>:1: ValueError
|
||||
______________________ TestRaises.test_raises_doesnt _______________________
|
||||
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef>
|
||||
|
||||
@@ -368,7 +368,7 @@ get on the terminal - we are working on that)::
|
||||
E Failed: DID NOT RAISE <class 'OSError'>
|
||||
|
||||
failure_demo.py:137: Failed
|
||||
_______ TestRaises.test_raise ________
|
||||
__________________________ TestRaises.test_raise ___________________________
|
||||
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef>
|
||||
|
||||
@@ -377,7 +377,7 @@ get on the terminal - we are working on that)::
|
||||
E ValueError: demo error
|
||||
|
||||
failure_demo.py:140: ValueError
|
||||
_______ TestRaises.test_tupleerror ________
|
||||
________________________ TestRaises.test_tupleerror ________________________
|
||||
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef>
|
||||
|
||||
@@ -399,7 +399,7 @@ get on the terminal - we are working on that)::
|
||||
failure_demo.py:148: TypeError
|
||||
--------------------------- Captured stdout call ---------------------------
|
||||
l is [1, 2, 3]
|
||||
_______ TestRaises.test_some_error ________
|
||||
________________________ TestRaises.test_some_error ________________________
|
||||
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef>
|
||||
|
||||
@@ -408,26 +408,28 @@ get on the terminal - we are working on that)::
|
||||
E NameError: name 'namenotexi' is not defined
|
||||
|
||||
failure_demo.py:151: NameError
|
||||
_______ test_dynamic_compile_shows_nicely ________
|
||||
____________________ test_dynamic_compile_shows_nicely _____________________
|
||||
|
||||
def test_dynamic_compile_shows_nicely():
|
||||
import imp
|
||||
import sys
|
||||
src = 'def foo():\n assert 1 == 0\n'
|
||||
name = 'abc-123'
|
||||
module = py.std.imp.new_module(name)
|
||||
module = imp.new_module(name)
|
||||
code = _pytest._code.compile(src, name, 'exec')
|
||||
py.builtin.exec_(code, module.__dict__)
|
||||
py.std.sys.modules[name] = module
|
||||
sys.modules[name] = module
|
||||
> module.foo()
|
||||
|
||||
failure_demo.py:166:
|
||||
failure_demo.py:168:
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
|
||||
def foo():
|
||||
> assert 1 == 0
|
||||
E AssertionError
|
||||
|
||||
<2-codegen 'abc-123' $REGENDOC_TMPDIR/assertion/failure_demo.py:163>:2: AssertionError
|
||||
_______ TestMoreErrors.test_complex_error ________
|
||||
<2-codegen 'abc-123' $REGENDOC_TMPDIR/assertion/failure_demo.py:165>:2: AssertionError
|
||||
____________________ TestMoreErrors.test_complex_error _____________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
|
||||
|
||||
@@ -438,7 +440,7 @@ get on the terminal - we are working on that)::
|
||||
return 43
|
||||
> somefunc(f(), g())
|
||||
|
||||
failure_demo.py:176:
|
||||
failure_demo.py:178:
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
failure_demo.py:9: in somefunc
|
||||
otherfunc(x,y)
|
||||
@@ -451,7 +453,7 @@ get on the terminal - we are working on that)::
|
||||
E assert 44 == 43
|
||||
|
||||
failure_demo.py:6: AssertionError
|
||||
_______ TestMoreErrors.test_z1_unpack_error ________
|
||||
___________________ TestMoreErrors.test_z1_unpack_error ____________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
|
||||
|
||||
@@ -460,8 +462,8 @@ get on the terminal - we are working on that)::
|
||||
> a,b = l
|
||||
E ValueError: not enough values to unpack (expected 2, got 0)
|
||||
|
||||
failure_demo.py:180: ValueError
|
||||
_______ TestMoreErrors.test_z2_type_error ________
|
||||
failure_demo.py:182: ValueError
|
||||
____________________ TestMoreErrors.test_z2_type_error _____________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
|
||||
|
||||
@@ -470,8 +472,8 @@ get on the terminal - we are working on that)::
|
||||
> a,b = l
|
||||
E TypeError: 'int' object is not iterable
|
||||
|
||||
failure_demo.py:184: TypeError
|
||||
_______ TestMoreErrors.test_startswith ________
|
||||
failure_demo.py:186: TypeError
|
||||
______________________ TestMoreErrors.test_startswith ______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
|
||||
|
||||
@@ -483,8 +485,8 @@ get on the terminal - we are working on that)::
|
||||
E + where False = <built-in method startswith of str object at 0xdeadbeef>('456')
|
||||
E + where <built-in method startswith of str object at 0xdeadbeef> = '123'.startswith
|
||||
|
||||
failure_demo.py:189: AssertionError
|
||||
_______ TestMoreErrors.test_startswith_nested ________
|
||||
failure_demo.py:191: AssertionError
|
||||
__________________ TestMoreErrors.test_startswith_nested ___________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
|
||||
|
||||
@@ -500,8 +502,8 @@ get on the terminal - we are working on that)::
|
||||
E + where '123' = <function TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef>()
|
||||
E + and '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef>()
|
||||
|
||||
failure_demo.py:196: AssertionError
|
||||
_______ TestMoreErrors.test_global_func ________
|
||||
failure_demo.py:198: AssertionError
|
||||
_____________________ TestMoreErrors.test_global_func ______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
|
||||
|
||||
@@ -511,8 +513,8 @@ get on the terminal - we are working on that)::
|
||||
E + where False = isinstance(43, float)
|
||||
E + where 43 = globf(42)
|
||||
|
||||
failure_demo.py:199: AssertionError
|
||||
_______ TestMoreErrors.test_instance ________
|
||||
failure_demo.py:201: AssertionError
|
||||
_______________________ TestMoreErrors.test_instance _______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
|
||||
|
||||
@@ -522,8 +524,8 @@ get on the terminal - we are working on that)::
|
||||
E assert 42 != 42
|
||||
E + where 42 = <failure_demo.TestMoreErrors object at 0xdeadbeef>.x
|
||||
|
||||
failure_demo.py:203: AssertionError
|
||||
_______ TestMoreErrors.test_compare ________
|
||||
failure_demo.py:205: AssertionError
|
||||
_______________________ TestMoreErrors.test_compare ________________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
|
||||
|
||||
@@ -532,8 +534,8 @@ get on the terminal - we are working on that)::
|
||||
E assert 11 < 5
|
||||
E + where 11 = globf(10)
|
||||
|
||||
failure_demo.py:206: AssertionError
|
||||
_______ TestMoreErrors.test_try_finally ________
|
||||
failure_demo.py:208: AssertionError
|
||||
_____________________ TestMoreErrors.test_try_finally ______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
|
||||
|
||||
@@ -543,8 +545,8 @@ get on the terminal - we are working on that)::
|
||||
> assert x == 0
|
||||
E assert 1 == 0
|
||||
|
||||
failure_demo.py:211: AssertionError
|
||||
_______ TestCustomAssertMsg.test_single_line ________
|
||||
failure_demo.py:213: AssertionError
|
||||
___________________ TestCustomAssertMsg.test_single_line ___________________
|
||||
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef>
|
||||
|
||||
@@ -557,8 +559,8 @@ get on the terminal - we are working on that)::
|
||||
E assert 1 == 2
|
||||
E + where 1 = <class 'failure_demo.TestCustomAssertMsg.test_single_line.<locals>.A'>.a
|
||||
|
||||
failure_demo.py:222: AssertionError
|
||||
_______ TestCustomAssertMsg.test_multiline ________
|
||||
failure_demo.py:224: AssertionError
|
||||
____________________ TestCustomAssertMsg.test_multiline ____________________
|
||||
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef>
|
||||
|
||||
@@ -574,8 +576,8 @@ get on the terminal - we are working on that)::
|
||||
E assert 1 == 2
|
||||
E + where 1 = <class 'failure_demo.TestCustomAssertMsg.test_multiline.<locals>.A'>.a
|
||||
|
||||
failure_demo.py:228: AssertionError
|
||||
_______ TestCustomAssertMsg.test_custom_repr ________
|
||||
failure_demo.py:230: AssertionError
|
||||
___________________ TestCustomAssertMsg.test_custom_repr ___________________
|
||||
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef>
|
||||
|
||||
@@ -594,5 +596,11 @@ get on the terminal - we are working on that)::
|
||||
E assert 1 == 2
|
||||
E + where 1 = This is JSON\n{\n 'foo': 'bar'\n}.a
|
||||
|
||||
failure_demo.py:238: AssertionError
|
||||
======= 42 failed in 0.12 seconds ========
|
||||
failure_demo.py:240: AssertionError
|
||||
============================= warnings summary =============================
|
||||
None
|
||||
Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0.
|
||||
Please use Metafunc.parametrize instead.
|
||||
|
||||
-- Docs: http://doc.pytest.org/en/latest/warnings.html
|
||||
================== 42 failed, 1 warnings in 0.12 seconds ===================
|
||||
|
||||
@@ -41,9 +41,9 @@ provide the ``cmdopt`` through a :ref:`fixture function <fixture function>`:
|
||||
Let's run this without supplying our new option::
|
||||
|
||||
$ pytest -q test_sample.py
|
||||
F
|
||||
======= FAILURES ========
|
||||
_______ test_answer ________
|
||||
F [100%]
|
||||
================================= FAILURES =================================
|
||||
_______________________________ test_answer ________________________________
|
||||
|
||||
cmdopt = 'type1'
|
||||
|
||||
@@ -63,9 +63,9 @@ Let's run this without supplying our new option::
|
||||
And now with supplying a command line option::
|
||||
|
||||
$ pytest -q --cmdopt=type2
|
||||
F
|
||||
======= FAILURES ========
|
||||
_______ test_answer ________
|
||||
F [100%]
|
||||
================================= FAILURES =================================
|
||||
_______________________________ test_answer ________________________________
|
||||
|
||||
cmdopt = 'type2'
|
||||
|
||||
@@ -112,12 +112,12 @@ of subprocesses close to your CPU. Running in an empty
|
||||
directory with the above conftest.py::
|
||||
|
||||
$ pytest
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 0 items
|
||||
|
||||
======= no tests ran in 0.12 seconds ========
|
||||
======================= no tests ran in 0.12 seconds =======================
|
||||
|
||||
.. _`excontrolskip`:
|
||||
|
||||
@@ -166,28 +166,28 @@ We can now write a test module like this:
|
||||
and when running it will see a skipped "slow" test::
|
||||
|
||||
$ pytest -rs # "-rs" means report details on the little 's'
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 2 items
|
||||
|
||||
test_module.py .s
|
||||
======= short test summary info ========
|
||||
test_module.py .s [100%]
|
||||
========================= short test summary info ==========================
|
||||
SKIP [1] test_module.py:8: need --runslow option to run
|
||||
|
||||
======= 1 passed, 1 skipped in 0.12 seconds ========
|
||||
=================== 1 passed, 1 skipped in 0.12 seconds ====================
|
||||
|
||||
Or run it including the ``slow`` marked test::
|
||||
|
||||
$ pytest --runslow
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 2 items
|
||||
|
||||
test_module.py ..
|
||||
test_module.py .. [100%]
|
||||
|
||||
======= 2 passed in 0.12 seconds ========
|
||||
========================= 2 passed in 0.12 seconds =========================
|
||||
|
||||
Writing well integrated assertion helpers
|
||||
--------------------------------------------------
|
||||
@@ -218,9 +218,9 @@ unless the ``--full-trace`` command line option is specified.
|
||||
Let's run our little function::
|
||||
|
||||
$ pytest -q test_checkconfig.py
|
||||
F
|
||||
======= FAILURES ========
|
||||
_______ test_something ________
|
||||
F [100%]
|
||||
================================= FAILURES =================================
|
||||
______________________________ test_something ______________________________
|
||||
|
||||
def test_something():
|
||||
> checkconfig(42)
|
||||
@@ -305,13 +305,13 @@ It's easy to present extra information in a ``pytest`` run:
|
||||
which will add the string to the test header accordingly::
|
||||
|
||||
$ pytest
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
project deps: mylib-1.1
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 0 items
|
||||
|
||||
======= no tests ran in 0.12 seconds ========
|
||||
======================= no tests ran in 0.12 seconds =======================
|
||||
|
||||
.. regendoc:wipe
|
||||
|
||||
@@ -330,25 +330,25 @@ display more information if applicable:
|
||||
which will add info only when run with "--v"::
|
||||
|
||||
$ pytest -v
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
info1: did you know that ...
|
||||
did you?
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 0 items
|
||||
|
||||
======= no tests ran in 0.12 seconds ========
|
||||
======================= no tests ran in 0.12 seconds =======================
|
||||
|
||||
and nothing when run plainly::
|
||||
|
||||
$ pytest
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 0 items
|
||||
|
||||
======= no tests ran in 0.12 seconds ========
|
||||
======================= no tests ran in 0.12 seconds =======================
|
||||
|
||||
profiling test duration
|
||||
--------------------------
|
||||
@@ -377,18 +377,18 @@ out which tests are the slowest. Let's make an artificial test suite:
|
||||
Now we can profile which test functions execute the slowest::
|
||||
|
||||
$ pytest --durations=3
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 3 items
|
||||
|
||||
test_some_are_slow.py ...
|
||||
test_some_are_slow.py ... [100%]
|
||||
|
||||
======= slowest 3 test durations ========
|
||||
========================= slowest 3 test durations =========================
|
||||
0.30s call test_some_are_slow.py::test_funcslow2
|
||||
0.20s call test_some_are_slow.py::test_funcslow1
|
||||
0.10s call test_some_are_slow.py::test_funcfast
|
||||
======= 3 passed in 0.12 seconds ========
|
||||
========================= 3 passed in 0.12 seconds =========================
|
||||
|
||||
incremental testing - test steps
|
||||
---------------------------------------------------
|
||||
@@ -443,18 +443,18 @@ tests in a class. Here is a test module example:
|
||||
If we run this::
|
||||
|
||||
$ pytest -rx
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 4 items
|
||||
|
||||
test_step.py .Fx.
|
||||
======= short test summary info ========
|
||||
test_step.py .Fx. [100%]
|
||||
========================= short test summary info ==========================
|
||||
XFAIL test_step.py::TestUserHandling::()::test_deletion
|
||||
reason: previous test failed (test_modification)
|
||||
|
||||
======= FAILURES ========
|
||||
_______ TestUserHandling.test_modification ________
|
||||
================================= FAILURES =================================
|
||||
____________________ TestUserHandling.test_modification ____________________
|
||||
|
||||
self = <test_step.TestUserHandling object at 0xdeadbeef>
|
||||
|
||||
@@ -463,7 +463,7 @@ If we run this::
|
||||
E assert 0
|
||||
|
||||
test_step.py:9: AssertionError
|
||||
======= 1 failed, 2 passed, 1 xfailed in 0.12 seconds ========
|
||||
============== 1 failed, 2 passed, 1 xfailed in 0.12 seconds ===============
|
||||
|
||||
We'll see that ``test_deletion`` was not executed because ``test_modification``
|
||||
failed. It is reported as an "expected failure".
|
||||
@@ -522,27 +522,27 @@ the ``db`` fixture:
|
||||
We can run this::
|
||||
|
||||
$ pytest
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 7 items
|
||||
|
||||
test_step.py .Fx.
|
||||
a/test_db.py F
|
||||
a/test_db2.py F
|
||||
b/test_error.py E
|
||||
test_step.py .Fx. [ 57%]
|
||||
a/test_db.py F [ 71%]
|
||||
a/test_db2.py F [ 85%]
|
||||
b/test_error.py E [100%]
|
||||
|
||||
======= ERRORS ========
|
||||
_______ ERROR at setup of test_root ________
|
||||
================================== ERRORS ==================================
|
||||
_______________________ ERROR at setup of test_root ________________________
|
||||
file $REGENDOC_TMPDIR/b/test_error.py, line 1
|
||||
def test_root(db): # no db here, will error out
|
||||
E fixture 'db' not found
|
||||
> available fixtures: cache, capfd, capsys, doctest_namespace, monkeypatch, pytestconfig, record_xml_property, recwarn, tmpdir, tmpdir_factory
|
||||
> available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_xml_attribute, record_xml_property, recwarn, tmpdir, tmpdir_factory
|
||||
> use 'pytest --fixtures [testpath]' for help on them.
|
||||
|
||||
$REGENDOC_TMPDIR/b/test_error.py:1
|
||||
======= FAILURES ========
|
||||
_______ TestUserHandling.test_modification ________
|
||||
================================= FAILURES =================================
|
||||
____________________ TestUserHandling.test_modification ____________________
|
||||
|
||||
self = <test_step.TestUserHandling object at 0xdeadbeef>
|
||||
|
||||
@@ -551,7 +551,7 @@ We can run this::
|
||||
E assert 0
|
||||
|
||||
test_step.py:9: AssertionError
|
||||
_______ test_a1 ________
|
||||
_________________________________ test_a1 __________________________________
|
||||
|
||||
db = <conftest.DB object at 0xdeadbeef>
|
||||
|
||||
@@ -561,7 +561,7 @@ We can run this::
|
||||
E assert 0
|
||||
|
||||
a/test_db.py:2: AssertionError
|
||||
_______ test_a2 ________
|
||||
_________________________________ test_a2 __________________________________
|
||||
|
||||
db = <conftest.DB object at 0xdeadbeef>
|
||||
|
||||
@@ -571,7 +571,7 @@ We can run this::
|
||||
E assert 0
|
||||
|
||||
a/test_db2.py:2: AssertionError
|
||||
======= 3 failed, 2 passed, 1 xfailed, 1 error in 0.12 seconds ========
|
||||
========== 3 failed, 2 passed, 1 xfailed, 1 error in 0.12 seconds ==========
|
||||
|
||||
The two test modules in the ``a`` directory see the same ``db`` fixture instance
|
||||
while the one test in the sister-directory ``b`` doesn't see it. We could of course
|
||||
@@ -630,15 +630,15 @@ if you then have failing tests:
|
||||
and run them::
|
||||
|
||||
$ pytest test_module.py
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 2 items
|
||||
|
||||
test_module.py FF
|
||||
test_module.py FF [100%]
|
||||
|
||||
======= FAILURES ========
|
||||
_______ test_fail1 ________
|
||||
================================= FAILURES =================================
|
||||
________________________________ test_fail1 ________________________________
|
||||
|
||||
tmpdir = local('PYTEST_TMPDIR/test_fail10')
|
||||
|
||||
@@ -647,14 +647,14 @@ and run them::
|
||||
E assert 0
|
||||
|
||||
test_module.py:2: AssertionError
|
||||
_______ test_fail2 ________
|
||||
________________________________ test_fail2 ________________________________
|
||||
|
||||
def test_fail2():
|
||||
> assert 0
|
||||
E assert 0
|
||||
|
||||
test_module.py:4: AssertionError
|
||||
======= 2 failed in 0.12 seconds ========
|
||||
========================= 2 failed in 0.12 seconds =========================
|
||||
|
||||
you will have a "failures" file which contains the failing test ids::
|
||||
|
||||
@@ -724,7 +724,7 @@ if you then have failing tests:
|
||||
and run it::
|
||||
|
||||
$ pytest -s test_module.py
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 3 items
|
||||
@@ -733,8 +733,8 @@ and run it::
|
||||
Fexecuting test failed test_module.py::test_call_fails
|
||||
F
|
||||
|
||||
======= ERRORS ========
|
||||
_______ ERROR at setup of test_setup_fails ________
|
||||
================================== ERRORS ==================================
|
||||
____________________ ERROR at setup of test_setup_fails ____________________
|
||||
|
||||
@pytest.fixture
|
||||
def other():
|
||||
@@ -742,8 +742,8 @@ and run it::
|
||||
E assert 0
|
||||
|
||||
test_module.py:6: AssertionError
|
||||
======= FAILURES ========
|
||||
_______ test_call_fails ________
|
||||
================================= FAILURES =================================
|
||||
_____________________________ test_call_fails ______________________________
|
||||
|
||||
something = None
|
||||
|
||||
@@ -752,14 +752,14 @@ and run it::
|
||||
E assert 0
|
||||
|
||||
test_module.py:12: AssertionError
|
||||
_______ test_fail2 ________
|
||||
________________________________ test_fail2 ________________________________
|
||||
|
||||
def test_fail2():
|
||||
> assert 0
|
||||
E assert 0
|
||||
|
||||
test_module.py:15: AssertionError
|
||||
======= 2 failed, 1 error in 0.12 seconds ========
|
||||
==================== 2 failed, 1 error in 0.12 seconds =====================
|
||||
|
||||
You'll see that the fixture finalizers could use the precise reporting
|
||||
information.
|
||||
@@ -826,15 +826,20 @@ Instead of freezing the pytest runner as a separate executable, you can make
|
||||
your frozen program work as the pytest runner by some clever
|
||||
argument handling during program startup. This allows you to
|
||||
have a single executable, which is usually more convenient.
|
||||
Please note that the mechanism for plugin discovery used by pytest
|
||||
(setupttools entry points) doesn't work with frozen executables so pytest
|
||||
can't find any third party plugins automatically. To include third party plugins
|
||||
like ``pytest-timeout`` they must be imported explicitly and passed on to pytest.main.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# contents of app_main.py
|
||||
import sys
|
||||
import pytest_timeout # Third party plugin
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == '--pytest':
|
||||
import pytest
|
||||
sys.exit(pytest.main(sys.argv[2:]))
|
||||
sys.exit(pytest.main(sys.argv[2:], plugins=[pytest_timeout]))
|
||||
else:
|
||||
# normal application execution: at this point argv can be parsed
|
||||
# by your argument-parsing library of choice as usual
|
||||
@@ -845,3 +850,4 @@ This allows you to execute tests using the frozen
|
||||
application with standard ``pytest`` command-line options::
|
||||
|
||||
./app_main --pytest --verbose --tb=long --junitxml=results.xml test-suite/
|
||||
|
||||
|
||||
@@ -69,15 +69,15 @@ will discover and call the :py:func:`@pytest.fixture <_pytest.python.fixture>`
|
||||
marked ``smtp`` fixture function. Running the test looks like this::
|
||||
|
||||
$ pytest test_smtpsimple.py
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 1 item
|
||||
|
||||
test_smtpsimple.py F
|
||||
test_smtpsimple.py F [100%]
|
||||
|
||||
======= FAILURES ========
|
||||
_______ test_ehlo ________
|
||||
================================= FAILURES =================================
|
||||
________________________________ test_ehlo _________________________________
|
||||
|
||||
smtp = <smtplib.SMTP object at 0xdeadbeef>
|
||||
|
||||
@@ -88,7 +88,7 @@ marked ``smtp`` fixture function. Running the test looks like this::
|
||||
E assert 0
|
||||
|
||||
test_smtpsimple.py:11: AssertionError
|
||||
======= 1 failed in 0.12 seconds ========
|
||||
========================= 1 failed in 0.12 seconds =========================
|
||||
|
||||
In the failure traceback we see that the test function was called with a
|
||||
``smtp`` argument, the ``smtplib.SMTP()`` instance created by the fixture
|
||||
@@ -205,15 +205,15 @@ We deliberately insert failing ``assert 0`` statements in order to
|
||||
inspect what is going on and can now run the tests::
|
||||
|
||||
$ pytest test_module.py
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 2 items
|
||||
|
||||
test_module.py FF
|
||||
test_module.py FF [100%]
|
||||
|
||||
======= FAILURES ========
|
||||
_______ test_ehlo ________
|
||||
================================= FAILURES =================================
|
||||
________________________________ test_ehlo _________________________________
|
||||
|
||||
smtp = <smtplib.SMTP object at 0xdeadbeef>
|
||||
|
||||
@@ -225,7 +225,7 @@ inspect what is going on and can now run the tests::
|
||||
E assert 0
|
||||
|
||||
test_module.py:6: AssertionError
|
||||
_______ test_noop ________
|
||||
________________________________ test_noop _________________________________
|
||||
|
||||
smtp = <smtplib.SMTP object at 0xdeadbeef>
|
||||
|
||||
@@ -236,7 +236,7 @@ inspect what is going on and can now run the tests::
|
||||
E assert 0
|
||||
|
||||
test_module.py:11: AssertionError
|
||||
======= 2 failed in 0.12 seconds ========
|
||||
========================= 2 failed in 0.12 seconds =========================
|
||||
|
||||
You see the two ``assert 0`` failing and more importantly you can also see
|
||||
that the same (module-scoped) ``smtp`` object was passed into the two
|
||||
@@ -369,7 +369,7 @@ ends, but ``addfinalizer`` has two key differences over ``yield``:
|
||||
Fixtures can introspect the requesting test context
|
||||
-------------------------------------------------------------
|
||||
|
||||
Fixture function can accept the :py:class:`request <FixtureRequest>` object
|
||||
Fixture functions can accept the :py:class:`request <FixtureRequest>` object
|
||||
to introspect the "requesting" test function, class or module context.
|
||||
Further extending the previous ``smtp`` fixture example, let's
|
||||
read an optional server URL from the test module which uses our fixture::
|
||||
@@ -408,9 +408,9 @@ server URL in its module namespace::
|
||||
Running it::
|
||||
|
||||
$ pytest -qq --tb=short test_anothersmtp.py
|
||||
F
|
||||
======= FAILURES ========
|
||||
_______ test_showhelo ________
|
||||
F [100%]
|
||||
================================= FAILURES =================================
|
||||
______________________________ test_showhelo _______________________________
|
||||
test_anothersmtp.py:5: in test_showhelo
|
||||
assert 0, smtp.helo()
|
||||
E AssertionError: (250, b'mail.python.org')
|
||||
@@ -457,9 +457,9 @@ a value via ``request.param``. No test function code needs to change.
|
||||
So let's just do another run::
|
||||
|
||||
$ pytest -q test_module.py
|
||||
FFFF
|
||||
======= FAILURES ========
|
||||
_______ test_ehlo[smtp.gmail.com] ________
|
||||
FFFF [100%]
|
||||
================================= FAILURES =================================
|
||||
________________________ test_ehlo[smtp.gmail.com] _________________________
|
||||
|
||||
smtp = <smtplib.SMTP object at 0xdeadbeef>
|
||||
|
||||
@@ -471,7 +471,7 @@ So let's just do another run::
|
||||
E assert 0
|
||||
|
||||
test_module.py:6: AssertionError
|
||||
_______ test_noop[smtp.gmail.com] ________
|
||||
________________________ test_noop[smtp.gmail.com] _________________________
|
||||
|
||||
smtp = <smtplib.SMTP object at 0xdeadbeef>
|
||||
|
||||
@@ -482,7 +482,7 @@ So let's just do another run::
|
||||
E assert 0
|
||||
|
||||
test_module.py:11: AssertionError
|
||||
_______ test_ehlo[mail.python.org] ________
|
||||
________________________ test_ehlo[mail.python.org] ________________________
|
||||
|
||||
smtp = <smtplib.SMTP object at 0xdeadbeef>
|
||||
|
||||
@@ -495,7 +495,7 @@ So let's just do another run::
|
||||
test_module.py:5: AssertionError
|
||||
-------------------------- Captured stdout setup ---------------------------
|
||||
finalizing <smtplib.SMTP object at 0xdeadbeef>
|
||||
_______ test_noop[mail.python.org] ________
|
||||
________________________ test_noop[mail.python.org] ________________________
|
||||
|
||||
smtp = <smtplib.SMTP object at 0xdeadbeef>
|
||||
|
||||
@@ -559,7 +559,7 @@ return ``None`` then pytest's auto-generated ID will be used.
|
||||
Running the above tests results in the following test IDs being used::
|
||||
|
||||
$ pytest --collect-only
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 10 items
|
||||
@@ -577,7 +577,7 @@ Running the above tests results in the following test IDs being used::
|
||||
<Function 'test_ehlo[mail.python.org]'>
|
||||
<Function 'test_noop[mail.python.org]'>
|
||||
|
||||
======= no tests ran in 0.12 seconds ========
|
||||
======================= no tests ran in 0.12 seconds =======================
|
||||
|
||||
.. _`interdependent fixtures`:
|
||||
|
||||
@@ -610,16 +610,16 @@ Here we declare an ``app`` fixture which receives the previously defined
|
||||
``smtp`` fixture and instantiates an ``App`` object with it. Let's run it::
|
||||
|
||||
$ pytest -v test_appsetup.py
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 2 items
|
||||
|
||||
test_appsetup.py::test_smtp_exists[smtp.gmail.com] PASSED
|
||||
test_appsetup.py::test_smtp_exists[mail.python.org] PASSED
|
||||
test_appsetup.py::test_smtp_exists[smtp.gmail.com] PASSED [ 50%]
|
||||
test_appsetup.py::test_smtp_exists[mail.python.org] PASSED [100%]
|
||||
|
||||
======= 2 passed in 0.12 seconds ========
|
||||
========================= 2 passed in 0.12 seconds =========================
|
||||
|
||||
Due to the parametrization of ``smtp`` the test will run twice with two
|
||||
different ``App`` instances and respective smtp servers. There is no
|
||||
@@ -679,9 +679,9 @@ to show the setup/teardown flow::
|
||||
Let's run the tests in verbose mode and with looking at the print-output::
|
||||
|
||||
$ pytest -v -s test_module.py
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 8 items
|
||||
|
||||
@@ -718,7 +718,7 @@ Let's run the tests in verbose mode and with looking at the print-output::
|
||||
TEARDOWN modarg mod2
|
||||
|
||||
|
||||
======= 8 passed in 0.12 seconds ========
|
||||
========================= 8 passed in 0.12 seconds =========================
|
||||
|
||||
You can see that the parametrized module-scoped ``modarg`` resource caused an
|
||||
ordering of test execution that lead to the fewest possible "active" resources.
|
||||
@@ -781,7 +781,7 @@ you specified a "cleandir" function argument to each of them. Let's run it
|
||||
to verify our fixture is activated and the tests pass::
|
||||
|
||||
$ pytest -q
|
||||
..
|
||||
.. [100%]
|
||||
2 passed in 0.12 seconds
|
||||
|
||||
You can specify multiple fixtures like this:
|
||||
@@ -862,7 +862,7 @@ class-level ``usefixtures`` decorator.
|
||||
If we run it, we get two passing tests::
|
||||
|
||||
$ pytest -q
|
||||
..
|
||||
.. [100%]
|
||||
2 passed in 0.12 seconds
|
||||
|
||||
Here is how autouse fixtures work in other scopes:
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
Installation and Getting Started
|
||||
===================================
|
||||
|
||||
**Pythons**: Python 2.6,2.7,3.3,3.4,3.5,3.6 Jython, PyPy-2.3
|
||||
**Pythons**: Python 2.7, 3.4, 3.5, 3.6, Jython, PyPy-2.3
|
||||
|
||||
**Platforms**: Unix/Posix and Windows
|
||||
|
||||
**PyPI package name**: `pytest <http://pypi.python.org/pypi/pytest>`_
|
||||
|
||||
**dependencies**: `py <http://pypi.python.org/pypi/py>`_,
|
||||
**Dependencies**: `py <http://pypi.python.org/pypi/py>`_,
|
||||
`colorama (Windows) <http://pypi.python.org/pypi/colorama>`_,
|
||||
`argparse (py26) <http://pypi.python.org/pypi/argparse>`_,
|
||||
`ordereddict (py26) <http://pypi.python.org/pypi/ordereddict>`_.
|
||||
|
||||
**documentation as PDF**: `download latest <https://media.readthedocs.org/pdf/pytest/latest/pytest.pdf>`_
|
||||
**Documentation as PDF**: `download latest <https://media.readthedocs.org/pdf/pytest/latest/pytest.pdf>`_
|
||||
|
||||
``pytest`` is a framework that makes building simple and scalable tests easy. Tests are expressive and readable—no boilerplate code required. Get started in minutes with a small unit test or complex functional test for your application or library.
|
||||
|
||||
.. _`getstarted`:
|
||||
.. _installation:
|
||||
.. _`installation`:
|
||||
|
||||
Installation
|
||||
Install ``pytest``
|
||||
----------------------------------------
|
||||
|
||||
Installation::
|
||||
1. Run the following command in your command line::
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
To check your installation has installed the correct version::
|
||||
2. Check that you installed the correct version::
|
||||
|
||||
$ pytest --version
|
||||
This is pytest version 3.x.y, imported from $PYTHON_PREFIX/lib/python3.5/site-packages/pytest.py
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
Our first test run
|
||||
Create your first test
|
||||
----------------------------------------------------------
|
||||
|
||||
Let's create a first test file with a simple test function::
|
||||
Create a simple test function with just four lines of code::
|
||||
|
||||
# content of test_sample.py
|
||||
def func(x):
|
||||
@@ -43,18 +43,18 @@ Let's create a first test file with a simple test function::
|
||||
def test_answer():
|
||||
assert func(3) == 5
|
||||
|
||||
That's it. You can execute the test function now::
|
||||
That’s it. You can now execute the test function::
|
||||
|
||||
$ pytest
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 1 item
|
||||
|
||||
test_sample.py F
|
||||
test_sample.py F [100%]
|
||||
|
||||
======= FAILURES ========
|
||||
_______ test_answer ________
|
||||
================================= FAILURES =================================
|
||||
_______________________________ test_answer ________________________________
|
||||
|
||||
def test_answer():
|
||||
> assert func(3) == 5
|
||||
@@ -62,32 +62,24 @@ That's it. You can execute the test function now::
|
||||
E + where 4 = func(3)
|
||||
|
||||
test_sample.py:5: AssertionError
|
||||
======= 1 failed in 0.12 seconds ========
|
||||
========================= 1 failed in 0.12 seconds =========================
|
||||
|
||||
We got a failure report because our little ``func(3)`` call did not return ``5``.
|
||||
This test returns a failure report because ``func(3)`` does not return ``5``.
|
||||
|
||||
.. note::
|
||||
|
||||
You can simply use the ``assert`` statement for asserting test
|
||||
expectations. pytest's :ref:`assert introspection` will intelligently
|
||||
report intermediate values of the assert expression freeing
|
||||
you from the need to learn the many names of `JUnit legacy methods`_.
|
||||
You can use the ``assert`` statement to verify test expectations. pytest’s `Advanced assertion introspection <http://docs.python.org/reference/simple_stmts.html#the-assert-statement>`_ will intelligently report intermediate values of the assert expression so you can avoid the many names `of JUnit legacy methods <http://docs.python.org/library/unittest.html#test-cases>`_.
|
||||
|
||||
.. _`JUnit legacy methods`: http://docs.python.org/library/unittest.html#test-cases
|
||||
|
||||
.. _`assert statement`: http://docs.python.org/reference/simple_stmts.html#the-assert-statement
|
||||
|
||||
Running multiple tests
|
||||
Run multiple tests
|
||||
----------------------------------------------------------
|
||||
|
||||
``pytest`` will run all files in the current directory and its subdirectories of the form test_*.py or \*_test.py. More generally, it follows :ref:`standard test discovery rules <test discovery>`.
|
||||
``pytest`` will run all files of the form test_*.py or \*_test.py in the current directory and its subdirectories. More generally, it follows :ref:`standard test discovery rules <test discovery>`.
|
||||
|
||||
|
||||
Asserting that a certain exception is raised
|
||||
Assert that a certain exception is raised
|
||||
--------------------------------------------------------------
|
||||
|
||||
If you want to assert that some code raises an exception you can
|
||||
use the ``raises`` helper::
|
||||
Use the ``raises`` helper to assert that some code raises an exception::
|
||||
|
||||
# content of test_sysexit.py
|
||||
import pytest
|
||||
@@ -98,18 +90,16 @@ use the ``raises`` helper::
|
||||
with pytest.raises(SystemExit):
|
||||
f()
|
||||
|
||||
Running it with, this time in "quiet" reporting mode::
|
||||
Execute the test function with “quiet” reporting mode::
|
||||
|
||||
$ pytest -q test_sysexit.py
|
||||
.
|
||||
. [100%]
|
||||
1 passed in 0.12 seconds
|
||||
|
||||
Grouping multiple tests in a class
|
||||
Group multiple tests in a class
|
||||
--------------------------------------------------------------
|
||||
|
||||
Once you start to have more than a few tests it often makes sense
|
||||
to group tests logically, in classes and modules. Let's write a class
|
||||
containing two tests::
|
||||
Once you develop multiple tests, you may want to group them into a class. pytest makes it easy to create a class containing more than one test::
|
||||
|
||||
# content of test_class.py
|
||||
class TestClass(object):
|
||||
@@ -121,14 +111,12 @@ containing two tests::
|
||||
x = "hello"
|
||||
assert hasattr(x, 'check')
|
||||
|
||||
The two tests are found because of the standard :ref:`test discovery`.
|
||||
There is no need to subclass anything. We can simply
|
||||
run the module by passing its filename::
|
||||
``pytest`` discovers all tests following its :ref:`Conventions for Python test discovery <test discovery>`, so it finds both ``test_`` prefixed functions. There is no need to subclass anything. We can simply run the module by passing its filename::
|
||||
|
||||
$ pytest -q test_class.py
|
||||
.F
|
||||
======= FAILURES ========
|
||||
_______ TestClass.test_two ________
|
||||
.F [100%]
|
||||
================================= FAILURES =================================
|
||||
____________________________ TestClass.test_two ____________________________
|
||||
|
||||
self = <test_class.TestClass object at 0xdeadbeef>
|
||||
|
||||
@@ -141,31 +129,24 @@ run the module by passing its filename::
|
||||
test_class.py:8: AssertionError
|
||||
1 failed, 1 passed in 0.12 seconds
|
||||
|
||||
The first test passed, the second failed. Again we can easily see
|
||||
the intermediate values used in the assertion, helping us to
|
||||
understand the reason for the failure.
|
||||
The first test passed and the second failed. You can easily see the intermediate values in the assertion to help you understand the reason for the failure.
|
||||
|
||||
Going functional: requesting a unique temporary directory
|
||||
Request a unique temporary directory for functional tests
|
||||
--------------------------------------------------------------
|
||||
|
||||
For functional tests one often needs to create some files
|
||||
and pass them to application objects. pytest provides
|
||||
:ref:`builtinfixtures` which allow to request arbitrary
|
||||
resources, for example a unique temporary directory::
|
||||
``pytest`` provides `Builtin fixtures/function arguments <https://docs.pytest.org/en/latest/builtin.html#builtinfixtures>`_ to request arbitrary resources, like a unique temporary directory::
|
||||
|
||||
# content of test_tmpdir.py
|
||||
def test_needsfiles(tmpdir):
|
||||
print (tmpdir)
|
||||
assert 0
|
||||
|
||||
We list the name ``tmpdir`` in the test function signature and
|
||||
``pytest`` will lookup and call a fixture factory to create the resource
|
||||
before performing the test function call. Let's just run it::
|
||||
List the name ``tmpdir`` in the test function signature and ``pytest`` will lookup and call a fixture factory to create the resource before performing the test function call. Before the test runs, ``pytest`` creates a unique-per-test-invocation temporary directory::
|
||||
|
||||
$ pytest -q test_tmpdir.py
|
||||
F
|
||||
======= FAILURES ========
|
||||
_______ test_needsfiles ________
|
||||
F [100%]
|
||||
================================= FAILURES =================================
|
||||
_____________________________ test_needsfiles ______________________________
|
||||
|
||||
tmpdir = local('PYTEST_TMPDIR/test_needsfiles0')
|
||||
|
||||
@@ -179,22 +160,21 @@ before performing the test function call. Let's just run it::
|
||||
PYTEST_TMPDIR/test_needsfiles0
|
||||
1 failed in 0.12 seconds
|
||||
|
||||
Before the test runs, a unique-per-test-invocation temporary directory
|
||||
was created. More info at :ref:`tmpdir handling`.
|
||||
More info on tmpdir handling is available at :ref:`Temporary directories and files <tmpdir handling>`.
|
||||
|
||||
You can find out what kind of builtin :ref:`fixtures` exist by typing::
|
||||
Find out what kind of builtin :ref:`pytest fixtures <fixtures>` exist with the command::
|
||||
|
||||
pytest --fixtures # shows builtin and custom fixtures
|
||||
|
||||
Where to go next
|
||||
Continue reading
|
||||
-------------------------------------
|
||||
|
||||
Here are a few suggestions where to go next:
|
||||
Check out additional pytest resources to help you customize tests for your unique workflow:
|
||||
|
||||
* :ref:`cmdline` for command line invocation examples
|
||||
* :ref:`good practices <goodpractices>` for virtualenv, test layout
|
||||
* :ref:`existingtestsuite` for working with pre-existing tests
|
||||
* :ref:`fixtures` for providing a functional baseline to your tests
|
||||
* :ref:`plugins` managing and writing plugins
|
||||
* ":ref:`cmdline`" for command line invocation examples
|
||||
* ":ref:`goodpractices`" for virtualenv and test layouts
|
||||
* ":ref:`existingtestsuite`" for working with pre-existing tests
|
||||
* ":ref:`fixtures`" for providing a functional baseline to your tests
|
||||
* ":ref:`plugins`" for managing and writing plugins
|
||||
|
||||
.. include:: links.inc
|
||||
|
||||
@@ -24,15 +24,15 @@ An example of a simple test:
|
||||
To execute it::
|
||||
|
||||
$ pytest
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 1 item
|
||||
|
||||
test_sample.py F
|
||||
test_sample.py F [100%]
|
||||
|
||||
======= FAILURES ========
|
||||
_______ test_answer ________
|
||||
================================= FAILURES =================================
|
||||
_______________________________ test_answer ________________________________
|
||||
|
||||
def test_answer():
|
||||
> assert inc(3) == 5
|
||||
@@ -40,7 +40,7 @@ To execute it::
|
||||
E + where 4 = inc(3)
|
||||
|
||||
test_sample.py:5: AssertionError
|
||||
======= 1 failed in 0.12 seconds ========
|
||||
========================= 1 failed in 0.12 seconds =========================
|
||||
|
||||
Due to ``pytest``'s detailed assertion introspection, only plain ``assert`` statements are used.
|
||||
See :ref:`Getting Started <getstarted>` for more examples.
|
||||
@@ -57,7 +57,7 @@ Features
|
||||
|
||||
- Can run :ref:`unittest <unittest>` (including trial) and :ref:`nose <noseintegration>` test suites out of the box;
|
||||
|
||||
- Python2.6+, Python3.3+, PyPy-2.3, Jython-2.5 (untested);
|
||||
- Python 2.7, Python 3.4+, PyPy 2.3, Jython 2.5 (untested);
|
||||
|
||||
- Rich plugin architecture, with over 315+ `external plugins <http://plugincompat.herokuapp.com>`_ and thriving community;
|
||||
|
||||
|
||||
259
doc/en/logging.rst
Normal file
259
doc/en/logging.rst
Normal file
@@ -0,0 +1,259 @@
|
||||
.. _logging:
|
||||
|
||||
Logging
|
||||
-------
|
||||
|
||||
.. versionadded:: 3.3
|
||||
.. versionchanged:: 3.4
|
||||
|
||||
pytest captures log messages of level ``WARNING`` or above automatically and displays them in their own section
|
||||
for each failed test in the same manner as captured stdout and stderr.
|
||||
|
||||
Running without options::
|
||||
|
||||
pytest
|
||||
|
||||
Shows failed tests like so::
|
||||
|
||||
----------------------- Captured stdlog call ----------------------
|
||||
test_reporting.py 26 WARNING text going to logger
|
||||
----------------------- Captured stdout call ----------------------
|
||||
text going to stdout
|
||||
----------------------- Captured stderr call ----------------------
|
||||
text going to stderr
|
||||
==================== 2 failed in 0.02 seconds =====================
|
||||
|
||||
By default each captured log message shows the module, line number, log level
|
||||
and message.
|
||||
|
||||
If desired the log and date format can be specified to
|
||||
anything that the logging module supports by passing specific formatting options::
|
||||
|
||||
pytest --log-format="%(asctime)s %(levelname)s %(message)s" \
|
||||
--log-date-format="%Y-%m-%d %H:%M:%S"
|
||||
|
||||
Shows failed tests like so::
|
||||
|
||||
----------------------- Captured stdlog call ----------------------
|
||||
2010-04-10 14:48:44 WARNING text going to logger
|
||||
----------------------- Captured stdout call ----------------------
|
||||
text going to stdout
|
||||
----------------------- Captured stderr call ----------------------
|
||||
text going to stderr
|
||||
==================== 2 failed in 0.02 seconds =====================
|
||||
|
||||
These options can also be customized through ``pytest.ini`` file:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
log_format = %(asctime)s %(levelname)s %(message)s
|
||||
log_date_format = %Y-%m-%d %H:%M:%S
|
||||
|
||||
Further it is possible to disable reporting logs on failed tests completely
|
||||
with::
|
||||
|
||||
pytest --no-print-logs
|
||||
|
||||
Or in the ``pytest.ini`` file:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
log_print = False
|
||||
|
||||
|
||||
Shows failed tests in the normal manner as no logs were captured::
|
||||
|
||||
----------------------- Captured stdout call ----------------------
|
||||
text going to stdout
|
||||
----------------------- Captured stderr call ----------------------
|
||||
text going to stderr
|
||||
==================== 2 failed in 0.02 seconds =====================
|
||||
|
||||
|
||||
caplog fixture
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
Inside tests it is possible to change the log level for the captured log
|
||||
messages. This is supported by the ``caplog`` fixture::
|
||||
|
||||
def test_foo(caplog):
|
||||
caplog.set_level(logging.INFO)
|
||||
pass
|
||||
|
||||
By default the level is set on the root logger,
|
||||
however as a convenience it is also possible to set the log level of any
|
||||
logger::
|
||||
|
||||
def test_foo(caplog):
|
||||
caplog.set_level(logging.CRITICAL, logger='root.baz')
|
||||
pass
|
||||
|
||||
The log levels set are restored automatically at the end of the test.
|
||||
|
||||
It is also possible to use a context manager to temporarily change the log
|
||||
level inside a ``with`` block::
|
||||
|
||||
def test_bar(caplog):
|
||||
with caplog.at_level(logging.INFO):
|
||||
pass
|
||||
|
||||
Again, by default the level of the root logger is affected but the level of any
|
||||
logger can be changed instead with::
|
||||
|
||||
def test_bar(caplog):
|
||||
with caplog.at_level(logging.CRITICAL, logger='root.baz'):
|
||||
pass
|
||||
|
||||
Lastly all the logs sent to the logger during the test run are made available on
|
||||
the fixture in the form of both the ``logging.LogRecord`` instances and the final log text.
|
||||
This is useful for when you want to assert on the contents of a message::
|
||||
|
||||
def test_baz(caplog):
|
||||
func_under_test()
|
||||
for record in caplog.records:
|
||||
assert record.levelname != 'CRITICAL'
|
||||
assert 'wally' not in caplog.text
|
||||
|
||||
For all the available attributes of the log records see the
|
||||
``logging.LogRecord`` class.
|
||||
|
||||
You can also resort to ``record_tuples`` if all you want to do is to ensure,
|
||||
that certain messages have been logged under a given logger name with a given
|
||||
severity and message::
|
||||
|
||||
def test_foo(caplog):
|
||||
logging.getLogger().info('boo %s', 'arg')
|
||||
|
||||
assert caplog.record_tuples == [
|
||||
('root', logging.INFO, 'boo arg'),
|
||||
]
|
||||
|
||||
You can call ``caplog.clear()`` to reset the captured log records in a test::
|
||||
|
||||
def test_something_with_clearing_records(caplog):
|
||||
some_method_that_creates_log_records()
|
||||
caplog.clear()
|
||||
your_test_method()
|
||||
assert ['Foo'] == [rec.message for rec in caplog.records]
|
||||
|
||||
|
||||
The ``caplop.records`` attribute contains records from the current stage only, so
|
||||
inside the ``setup`` phase it contains only setup logs, same with the ``call`` and
|
||||
``teardown`` phases.
|
||||
|
||||
To access logs from other stages, use the ``caplog.get_records(when)`` method. As an example,
|
||||
if you want to make sure that tests which use a certain fixture never log any warnings, you can inspect
|
||||
the records for the ``setup`` and ``call`` stages during teardown like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def window(caplog):
|
||||
window = create_window()
|
||||
yield window
|
||||
for when in ('setup', 'call'):
|
||||
messages = [x.message for x in caplog.get_records(when) if x.level == logging.WARNING]
|
||||
if messages:
|
||||
pytest.fail('warning messages encountered during testing: {}'.format(messages))
|
||||
|
||||
|
||||
caplog fixture API
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: _pytest.logging.LogCaptureFixture
|
||||
:members:
|
||||
|
||||
.. _live_logs:
|
||||
|
||||
Live Logs
|
||||
^^^^^^^^^
|
||||
|
||||
By setting the :confval:`log_cli` configuration option to ``true``, pytest will output
|
||||
logging records as they are emitted directly into the console.
|
||||
|
||||
You can specify the logging level for which log records with equal or higher
|
||||
level are printed to the console by passing ``--log-cli-level``. This setting
|
||||
accepts the logging level names as seen in python's documentation or an integer
|
||||
as the logging level num.
|
||||
|
||||
Additionally, you can also specify ``--log-cli-format`` and
|
||||
``--log-cli-date-format`` which mirror and default to ``--log-format`` and
|
||||
``--log-date-format`` if not provided, but are applied only to the console
|
||||
logging handler.
|
||||
|
||||
All of the CLI log options can also be set in the configuration INI file. The
|
||||
option names are:
|
||||
|
||||
* ``log_cli_level``
|
||||
* ``log_cli_format``
|
||||
* ``log_cli_date_format``
|
||||
|
||||
If you need to record the whole test suite logging calls to a file, you can pass
|
||||
``--log-file=/path/to/log/file``. This log file is opened in write mode which
|
||||
means that it will be overwritten at each run tests session.
|
||||
|
||||
You can also specify the logging level for the log file by passing
|
||||
``--log-file-level``. This setting accepts the logging level names as seen in
|
||||
python's documentation(ie, uppercased level names) or an integer as the logging
|
||||
level num.
|
||||
|
||||
Additionally, you can also specify ``--log-file-format`` and
|
||||
``--log-file-date-format`` which are equal to ``--log-format`` and
|
||||
``--log-date-format`` but are applied to the log file logging handler.
|
||||
|
||||
All of the log file options can also be set in the configuration INI file. The
|
||||
option names are:
|
||||
|
||||
* ``log_file``
|
||||
* ``log_file_level``
|
||||
* ``log_file_format``
|
||||
* ``log_file_date_format``
|
||||
|
||||
.. _log_release_notes:
|
||||
|
||||
Release notes
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
This feature was introduced as a drop-in replacement for the `pytest-catchlog
|
||||
<https://pypi.org/project/pytest-catchlog/>`_ plugin and they conflict
|
||||
with each other. The backward compatibility API with ``pytest-capturelog``
|
||||
has been dropped when this feature was introduced, so if for that reason you
|
||||
still need ``pytest-catchlog`` you can disable the internal feature by
|
||||
adding to your ``pytest.ini``:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
addopts=-p no:logging
|
||||
|
||||
|
||||
.. _log_changes_3_4:
|
||||
|
||||
Incompatible changes in pytest 3.4
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
This feature was introduced in ``3.3`` and some **incompatible changes** have been
|
||||
made in ``3.4`` after community feedback:
|
||||
|
||||
* Log levels are no longer changed unless explicitly requested by the :confval:`log_level` configuration
|
||||
or ``--log-level`` command-line options. This allows users to configure logger objects themselves.
|
||||
* :ref:`Live Logs <live_logs>` is now disabled by default and can be enabled setting the
|
||||
:confval:`log_cli` configuration option to ``true``. When enabled, the verbosity is increased so logging for each
|
||||
test is visible.
|
||||
* :ref:`Live Logs <live_logs>` are now sent to ``sys.stdout`` and no longer require the ``-s`` command-line option
|
||||
to work.
|
||||
|
||||
If you want to partially restore the logging behavior of version ``3.3``, you can add this options to your ``ini``
|
||||
file:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
log_cli=true
|
||||
log_level=NOTSET
|
||||
|
||||
More details about the discussion that lead to this changes can be read in
|
||||
issue `#3013 <https://github.com/pytest-dev/pytest/issues/3013>`_.
|
||||
@@ -53,15 +53,15 @@ tuples so that the ``test_eval`` function will run three times using
|
||||
them in turn::
|
||||
|
||||
$ pytest
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 3 items
|
||||
|
||||
test_expectation.py ..F
|
||||
test_expectation.py ..F [100%]
|
||||
|
||||
======= FAILURES ========
|
||||
_______ test_eval[6*9-42] ________
|
||||
================================= FAILURES =================================
|
||||
____________________________ test_eval[6*9-42] _____________________________
|
||||
|
||||
test_input = '6*9', expected = 42
|
||||
|
||||
@@ -76,7 +76,7 @@ them in turn::
|
||||
E + where 54 = eval('6*9')
|
||||
|
||||
test_expectation.py:8: AssertionError
|
||||
======= 1 failed, 2 passed in 0.12 seconds ========
|
||||
==================== 1 failed, 2 passed in 0.12 seconds ====================
|
||||
|
||||
As designed in this example, only one pair of input/output values fails
|
||||
the simple test function. And as usual with test function arguments,
|
||||
@@ -102,14 +102,14 @@ for example with the builtin ``mark.xfail``::
|
||||
Let's run this::
|
||||
|
||||
$ pytest
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 3 items
|
||||
|
||||
test_expectation.py ..x
|
||||
test_expectation.py ..x [100%]
|
||||
|
||||
======= 2 passed, 1 xfailed in 0.12 seconds ========
|
||||
=================== 2 passed, 1 xfailed in 0.12 seconds ====================
|
||||
|
||||
The one parameter set which caused a failure previously now
|
||||
shows up as an "xfailed (expected to fail)" test.
|
||||
@@ -123,8 +123,8 @@ To get all combinations of multiple parametrized arguments you can stack
|
||||
def test_foo(x, y):
|
||||
pass
|
||||
|
||||
This will run the test with the arguments set to ``x=0/y=2``, ``x=0/y=3``, ``x=1/y=2`` and
|
||||
``x=1/y=3``.
|
||||
This will run the test with the arguments set to ``x=0/y=2``, ``x=1/y=2``,
|
||||
``x=0/y=3``, and ``x=1/y=3`` exhausting parameters in the order of the decorators.
|
||||
|
||||
.. _`pytest_generate_tests`:
|
||||
|
||||
@@ -165,15 +165,15 @@ command line option and the parametrization of our test function::
|
||||
If we now pass two stringinput values, our test will run twice::
|
||||
|
||||
$ pytest -q --stringinput="hello" --stringinput="world" test_strings.py
|
||||
..
|
||||
.. [100%]
|
||||
2 passed in 0.12 seconds
|
||||
|
||||
Let's also run with a stringinput that will lead to a failing test::
|
||||
|
||||
$ pytest -q --stringinput="!" test_strings.py
|
||||
F
|
||||
======= FAILURES ========
|
||||
_______ test_valid_string[!] ________
|
||||
F [100%]
|
||||
================================= FAILURES =================================
|
||||
___________________________ test_valid_string[!] ___________________________
|
||||
|
||||
stringinput = '!'
|
||||
|
||||
@@ -193,9 +193,9 @@ If you don't specify a stringinput it will be skipped because
|
||||
list::
|
||||
|
||||
$ pytest -q -rs test_strings.py
|
||||
s
|
||||
======= short test summary info ========
|
||||
SKIP [1] test_strings.py:2: got empty parameter set ['stringinput'], function test_valid_string at $REGENDOC_TMPDIR/test_strings.py:1
|
||||
s [100%]
|
||||
========================= short test summary info ==========================
|
||||
SKIP [1] test_strings.py: got empty parameter set ['stringinput'], function test_valid_string at $REGENDOC_TMPDIR/test_strings.py:1
|
||||
1 skipped in 0.12 seconds
|
||||
|
||||
Note that when calling ``metafunc.parametrize`` multiple times with different parameter sets, all parameter names across
|
||||
|
||||
@@ -27,9 +27,6 @@ Here is a little annotated list for some popular plugins:
|
||||
for `twisted <http://twistedmatrix.com>`_ apps, starting a reactor and
|
||||
processing deferreds from test functions.
|
||||
|
||||
* `pytest-catchlog <http://pypi.python.org/pypi/pytest-catchlog>`_:
|
||||
to capture and assert about messages from the logging module
|
||||
|
||||
* `pytest-cov <http://pypi.python.org/pypi/pytest-cov>`_:
|
||||
coverage reporting, compatible with distributed testing
|
||||
|
||||
@@ -151,6 +148,7 @@ in the `pytest repository <https://github.com/pytest-dev/pytest>`_.
|
||||
_pytest.resultlog
|
||||
_pytest.runner
|
||||
_pytest.main
|
||||
_pytest.logging
|
||||
_pytest.skipping
|
||||
_pytest.terminal
|
||||
_pytest.tmpdir
|
||||
|
||||
@@ -37,7 +37,7 @@ Here are some examples of projects using ``pytest`` (please send notes via :ref:
|
||||
* `mwlib <http://pypi.python.org/pypi/mwlib>`_ mediawiki parser and utility library
|
||||
* `The Translate Toolkit <http://translate.sourceforge.net/wiki/toolkit/index>`_ for localization and conversion
|
||||
* `execnet <http://codespeak.net/execnet>`_ rapid multi-Python deployment
|
||||
* `pylib <http://py.rtfd.org>`_ cross-platform path, IO, dynamic code library
|
||||
* `pylib <https://py.readthedocs.io>`_ cross-platform path, IO, dynamic code library
|
||||
* `Pacha <http://pacha.cafepais.com/>`_ configuration management in five minutes
|
||||
* `bbfreeze <http://pypi.python.org/pypi/bbfreeze>`_ create standalone executables from Python scripts
|
||||
* `pdb++ <http://bitbucket.org/antocuni/pdb>`_ a fancier version of PDB
|
||||
@@ -58,7 +58,7 @@ Here are some examples of projects using ``pytest`` (please send notes via :ref:
|
||||
* `katcp <https://bitbucket.org/hodgestar/katcp>`_ Telescope communication protocol over Twisted
|
||||
* `kss plugin timer <http://pypi.python.org/pypi/kss.plugin.timer>`_
|
||||
* `pyudev <https://pyudev.readthedocs.io/en/latest/tests/plugins.html>`_ a pure Python binding to the Linux library libudev
|
||||
* `pytest-localserver <https://bitbucket.org/basti/pytest-localserver/>`_ a plugin for pytest that provides an httpserver and smtpserver
|
||||
* `pytest-localserver <https://bitbucket.org/pytest-dev/pytest-localserver/>`_ a plugin for pytest that provides an httpserver and smtpserver
|
||||
* `pytest-monkeyplus <http://pypi.python.org/pypi/pytest-monkeyplus/>`_ a plugin that extends monkeypatch
|
||||
|
||||
These projects help integrate ``pytest`` into other Python frameworks:
|
||||
|
||||
@@ -58,6 +58,16 @@ by calling the ``pytest.skip(reason)`` function:
|
||||
if not valid_config():
|
||||
pytest.skip("unsupported configuration")
|
||||
|
||||
It is also possible to skip the whole module using
|
||||
``pytest.skip(reason, allow_module_level=True)`` at the module level:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import pytest
|
||||
|
||||
if not pytest.config.getoption("--custom-flag"):
|
||||
pytest.skip("--custom-flag is missing, skipping tests", allow_module_level=True)
|
||||
|
||||
The imperative method is useful when it is not possible to evaluate the skip condition
|
||||
during import time.
|
||||
|
||||
@@ -68,11 +78,11 @@ during import time.
|
||||
|
||||
If you wish to skip something conditionally then you can use ``skipif`` instead.
|
||||
Here is an example of marking a test function to be skipped
|
||||
when run on a Python3.3 interpreter::
|
||||
when run on a Python3.6 interpreter::
|
||||
|
||||
import sys
|
||||
@pytest.mark.skipif(sys.version_info < (3,3),
|
||||
reason="requires python3.3")
|
||||
@pytest.mark.skipif(sys.version_info < (3,6),
|
||||
reason="requires python3.6")
|
||||
def test_function():
|
||||
...
|
||||
|
||||
@@ -264,8 +274,8 @@ You can change the default value of the ``strict`` parameter using the
|
||||
As with skipif_ you can also mark your expectation of a failure
|
||||
on a particular platform::
|
||||
|
||||
@pytest.mark.xfail(sys.version_info >= (3,3),
|
||||
reason="python3.3 api changes")
|
||||
@pytest.mark.xfail(sys.version_info >= (3,6),
|
||||
reason="python3.6 api changes")
|
||||
def test_function():
|
||||
...
|
||||
|
||||
@@ -321,13 +331,13 @@ Here is a simple test file with the several usages:
|
||||
Running it with the report-on-xfail option gives this output::
|
||||
|
||||
example $ pytest -rx xfail_demo.py
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR/example, inifile:
|
||||
collected 7 items
|
||||
|
||||
xfail_demo.py xxxxxxx
|
||||
======= short test summary info ========
|
||||
xfail_demo.py xxxxxxx [100%]
|
||||
========================= short test summary info ==========================
|
||||
XFAIL xfail_demo.py::test_hello
|
||||
XFAIL xfail_demo.py::test_hello2
|
||||
reason: [NOTRUN]
|
||||
@@ -341,7 +351,7 @@ Running it with the report-on-xfail option gives this output::
|
||||
reason: reason
|
||||
XFAIL xfail_demo.py::test_hello7
|
||||
|
||||
======= 7 xfailed in 0.12 seconds ========
|
||||
======================== 7 xfailed in 0.12 seconds =========================
|
||||
|
||||
.. _`skip/xfail with parametrize`:
|
||||
|
||||
|
||||
@@ -28,15 +28,15 @@ Running this would result in a passed test except for the last
|
||||
``assert 0`` line which we use to look at values::
|
||||
|
||||
$ pytest test_tmpdir.py
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 1 item
|
||||
|
||||
test_tmpdir.py F
|
||||
test_tmpdir.py F [100%]
|
||||
|
||||
======= FAILURES ========
|
||||
_______ test_create_file ________
|
||||
================================= FAILURES =================================
|
||||
_____________________________ test_create_file _____________________________
|
||||
|
||||
tmpdir = local('PYTEST_TMPDIR/test_create_file0')
|
||||
|
||||
@@ -49,7 +49,7 @@ Running this would result in a passed test except for the last
|
||||
E assert 0
|
||||
|
||||
test_tmpdir.py:7: AssertionError
|
||||
======= 1 failed in 0.12 seconds ========
|
||||
========================= 1 failed in 0.12 seconds =========================
|
||||
|
||||
The 'tmpdir_factory' fixture
|
||||
----------------------------
|
||||
@@ -106,6 +106,4 @@ When distributing tests on the local machine, ``pytest`` takes care to
|
||||
configure a basetemp directory for the sub processes such that all temporary
|
||||
data lands below a single per-test run basetemp directory.
|
||||
|
||||
.. _`py.path.local`: http://py.rtfd.org/en/latest/path.html
|
||||
|
||||
|
||||
.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
|
||||
|
||||
@@ -126,15 +126,15 @@ Due to the deliberately failing assert statements, we can take a look at
|
||||
the ``self.db`` values in the traceback::
|
||||
|
||||
$ pytest test_unittest_db.py
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 2 items
|
||||
|
||||
test_unittest_db.py FF
|
||||
test_unittest_db.py FF [100%]
|
||||
|
||||
======= FAILURES ========
|
||||
_______ MyTest.test_method1 ________
|
||||
================================= FAILURES =================================
|
||||
___________________________ MyTest.test_method1 ____________________________
|
||||
|
||||
self = <test_unittest_db.MyTest testMethod=test_method1>
|
||||
|
||||
@@ -145,7 +145,7 @@ the ``self.db`` values in the traceback::
|
||||
E assert 0
|
||||
|
||||
test_unittest_db.py:9: AssertionError
|
||||
_______ MyTest.test_method2 ________
|
||||
___________________________ MyTest.test_method2 ____________________________
|
||||
|
||||
self = <test_unittest_db.MyTest testMethod=test_method2>
|
||||
|
||||
@@ -155,7 +155,7 @@ the ``self.db`` values in the traceback::
|
||||
E assert 0
|
||||
|
||||
test_unittest_db.py:12: AssertionError
|
||||
======= 2 failed in 0.12 seconds ========
|
||||
========================= 2 failed in 0.12 seconds =========================
|
||||
|
||||
This default pytest traceback shows that the two test methods
|
||||
share the same ``self.db`` instance which was our intention
|
||||
@@ -203,7 +203,7 @@ on the class like in the previous example.
|
||||
Running this test module ...::
|
||||
|
||||
$ pytest -q test_unittest_cleandir.py
|
||||
.
|
||||
. [100%]
|
||||
1 passed in 0.12 seconds
|
||||
|
||||
... gives us one passed test because the ``initdir`` fixture function
|
||||
|
||||
@@ -189,7 +189,6 @@ in your code and pytest automatically disables its output capture for that test:
|
||||
for test output occurring after you exit the interactive PDB_ tracing session
|
||||
and continue with the regular test run.
|
||||
|
||||
|
||||
.. _durations:
|
||||
|
||||
Profiling test execution duration
|
||||
@@ -257,6 +256,66 @@ This will add an extra property ``example_key="1"`` to the generated
|
||||
Also please note that using this feature will break any schema verification.
|
||||
This might be a problem when used with some CI servers.
|
||||
|
||||
record_xml_attribute
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. versionadded:: 3.4
|
||||
|
||||
To add an additional xml attribute to a testcase element, you can use
|
||||
``record_xml_attribute`` fixture. This can also be used to override existing values:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_function(record_xml_attribute):
|
||||
record_xml_attribute("assertions", "REQ-1234")
|
||||
record_xml_attribute("classname", "custom_classname")
|
||||
print('hello world')
|
||||
assert True
|
||||
|
||||
Unlike ``record_xml_property``, this will not add a new child element.
|
||||
Instead, this will add an attribute ``assertions="REQ-1234"`` inside the generated
|
||||
``testcase`` tag and override the default ``classname`` with ``"classname=custom_classname"``:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<testcase classname="custom_classname" file="test_function.py" line="0" name="test_function" time="0.003" assertions="REQ-1234">
|
||||
<system-out>
|
||||
hello world
|
||||
</system-out>
|
||||
</testcase>
|
||||
|
||||
.. warning::
|
||||
|
||||
``record_xml_attribute`` is an experimental feature, and its interface might be replaced
|
||||
by something more powerful and general in future versions. The
|
||||
functionality per-se will be kept, however.
|
||||
|
||||
Using this over ``record_xml_property`` can help when using ci tools to parse the xml report.
|
||||
However, some parsers are quite strict about the elements and attributes that are allowed.
|
||||
Many tools use an xsd schema (like the example below) to validate incoming xml.
|
||||
Make sure you are using attribute names that are allowed by your parser.
|
||||
|
||||
Below is the Scheme used by Jenkins to validate the XML report:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<xs:element name="testcase">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element ref="skipped" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element ref="error" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element ref="failure" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element ref="system-out" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element ref="system-err" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||
<xs:attribute name="assertions" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="time" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="classname" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="status" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
|
||||
LogXML: add_global_property
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
@@ -390,4 +449,14 @@ hook was invoked::
|
||||
*** test run reporting finishing
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
Calling ``pytest.main()`` will result in importing your tests and any modules
|
||||
that they import. Due to the caching mechanism of python's import system,
|
||||
making subsequent calls to ``pytest.main()`` from the same process will not
|
||||
reflect changes to those files between the calls. For this reason, making
|
||||
multiple calls to ``pytest.main()`` from the same process (in order to re-run
|
||||
tests, for example) is not recommended.
|
||||
|
||||
|
||||
.. include:: links.inc
|
||||
|
||||
@@ -21,20 +21,20 @@ and displays them at the end of the session::
|
||||
Running pytest now produces this output::
|
||||
|
||||
$ pytest test_show_warnings.py
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 1 item
|
||||
|
||||
test_show_warnings.py .
|
||||
test_show_warnings.py . [100%]
|
||||
|
||||
======= warnings summary ========
|
||||
============================= warnings summary =============================
|
||||
test_show_warnings.py::test_one
|
||||
$REGENDOC_TMPDIR/test_show_warnings.py:4: UserWarning: api v1, should use functions from v2
|
||||
warnings.warn(UserWarning("api v1, should use functions from v2"))
|
||||
|
||||
-- Docs: http://doc.pytest.org/en/latest/warnings.html
|
||||
======= 1 passed, 1 warnings in 0.12 seconds ========
|
||||
=================== 1 passed, 1 warnings in 0.12 seconds ===================
|
||||
|
||||
Pytest by default catches all warnings except for ``DeprecationWarning`` and ``PendingDeprecationWarning``.
|
||||
|
||||
@@ -42,9 +42,9 @@ The ``-W`` flag can be passed to control which warnings will be displayed or eve
|
||||
them into errors::
|
||||
|
||||
$ pytest -q test_show_warnings.py -W error::UserWarning
|
||||
F
|
||||
======= FAILURES ========
|
||||
_______ test_one ________
|
||||
F [100%]
|
||||
================================= FAILURES =================================
|
||||
_________________________________ test_one _________________________________
|
||||
|
||||
def test_one():
|
||||
> assert api_v1() == 1
|
||||
@@ -112,6 +112,12 @@ decorator or to all tests in a module by setting the ``pytestmark`` variable:
|
||||
pytestmark = pytest.mark.filterwarnings('error')
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
Except for these features, pytest does not change the python warning filter; it only captures
|
||||
and displays the warnings which are issued with respect to the currently configured filter,
|
||||
including changes to the filter made by test functions or by the system under test.
|
||||
|
||||
.. note::
|
||||
|
||||
``DeprecationWarning`` and ``PendingDeprecationWarning`` are hidden by the standard library
|
||||
@@ -168,7 +174,20 @@ which works in a similar manner to :ref:`raises <assertraises>`::
|
||||
with pytest.warns(UserWarning):
|
||||
warnings.warn("my warning", UserWarning)
|
||||
|
||||
The test will fail if the warning in question is not raised.
|
||||
The test will fail if the warning in question is not raised. The keyword
|
||||
argument ``match`` to assert that the exception matches a text or regex::
|
||||
|
||||
>>> with warns(UserWarning, match='must be 0 or None'):
|
||||
... warnings.warn("value must be 0 or None", UserWarning)
|
||||
|
||||
>>> with warns(UserWarning, match=r'must be \d+$'):
|
||||
... warnings.warn("value must be 42", UserWarning)
|
||||
|
||||
>>> with warns(UserWarning, match=r'must be \d+$'):
|
||||
... warnings.warn("this is not here", UserWarning)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
Failed: DID NOT WARN. No warnings of type ...UserWarning... was emitted...
|
||||
|
||||
You can also call ``pytest.warns`` on a function or code string::
|
||||
|
||||
|
||||
@@ -184,16 +184,16 @@ statements and the detailed introspection of expressions upon
|
||||
assertion failures. This is provided by "assertion rewriting" which
|
||||
modifies the parsed AST before it gets compiled to bytecode. This is
|
||||
done via a :pep:`302` import hook which gets installed early on when
|
||||
``pytest`` starts up and will perform this re-writing when modules get
|
||||
``pytest`` starts up and will perform this rewriting when modules get
|
||||
imported. However since we do not want to test different bytecode
|
||||
then you will run in production this hook only re-writes test modules
|
||||
then you will run in production this hook only rewrites test modules
|
||||
themselves as well as any modules which are part of plugins. Any
|
||||
other imported module will not be re-written and normal assertion
|
||||
other imported module will not be rewritten and normal assertion
|
||||
behaviour will happen.
|
||||
|
||||
If you have assertion helpers in other modules where you would need
|
||||
assertion rewriting to be enabled you need to ask ``pytest``
|
||||
explicitly to re-write this module before it gets imported.
|
||||
explicitly to rewrite this module before it gets imported.
|
||||
|
||||
.. autofunction:: pytest.register_assert_rewrite
|
||||
|
||||
@@ -216,10 +216,10 @@ With the following typical ``setup.py`` extract:
|
||||
...
|
||||
)
|
||||
|
||||
In this case only ``pytest_foo/plugin.py`` will be re-written. If the
|
||||
In this case only ``pytest_foo/plugin.py`` will be rewritten. If the
|
||||
helper module also contains assert statements which need to be
|
||||
re-written it needs to be marked as such, before it gets imported.
|
||||
This is easiest by marking it for re-writing inside the
|
||||
rewritten it needs to be marked as such, before it gets imported.
|
||||
This is easiest by marking it for rewriting inside the
|
||||
``__init__.py`` module, which will always be imported first when a
|
||||
module inside a package is imported. This way ``plugin.py`` can still
|
||||
import ``helper.py`` normally. The contents of
|
||||
@@ -263,7 +263,7 @@ for assertion rewriting (see :func:`pytest.register_assert_rewrite`).
|
||||
However for this to have any effect the module must not be
|
||||
imported already; if it was already imported at the time the
|
||||
``pytest_plugins`` statement is processed, a warning will result and
|
||||
assertions inside the plugin will not be re-written. To fix this you
|
||||
assertions inside the plugin will not be rewritten. To fix this you
|
||||
can either call :func:`pytest.register_assert_rewrite` yourself before
|
||||
the module is imported, or you can arrange the code to delay the
|
||||
importing until after the plugin is registered.
|
||||
@@ -278,7 +278,7 @@ the plugin manager like this:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
plugin = config.pluginmanager.getplugin("name_of_plugin")
|
||||
plugin = config.pluginmanager.get_plugin("name_of_plugin")
|
||||
|
||||
If you want to look at the names of existing plugins, use
|
||||
the ``--trace-config`` option.
|
||||
@@ -452,7 +452,7 @@ hook wrappers and passes the same arguments as to the regular hooks.
|
||||
|
||||
At the yield point of the hook wrapper pytest will execute the next hook
|
||||
implementations and return their result to the yield point in the form of
|
||||
a :py:class:`CallOutcome <_pytest.vendored_packages.pluggy._CallOutcome>` instance which encapsulates a result or
|
||||
a :py:class:`Result <pluggy._Result>` instance which encapsulates a result or
|
||||
exception info. The yield point itself will thus typically not raise
|
||||
exceptions (unless there are bugs).
|
||||
|
||||
@@ -462,19 +462,24 @@ Here is an example definition of a hook wrapper::
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_pyfunc_call(pyfuncitem):
|
||||
# do whatever you want before the next hook executes
|
||||
do_something_before_next_hook_executes()
|
||||
|
||||
outcome = yield
|
||||
# outcome.excinfo may be None or a (cls, val, tb) tuple
|
||||
|
||||
res = outcome.get_result() # will raise if outcome was exception
|
||||
# postprocess result
|
||||
|
||||
post_process_result(res)
|
||||
|
||||
outcome.force_result(new_res) # to override the return value to the plugin system
|
||||
|
||||
Note that hook wrappers don't return results themselves, they merely
|
||||
perform tracing or other side effects around the actual hook implementations.
|
||||
If the result of the underlying hook is a mutable object, they may modify
|
||||
that result but it's probably better to avoid it.
|
||||
|
||||
For more information, consult the `pluggy documentation <http://pluggy.readthedocs.io/en/latest/#wrappers>`_.
|
||||
|
||||
|
||||
Hook function ordering / call example
|
||||
-------------------------------------
|
||||
@@ -517,7 +522,7 @@ Here is the order of execution:
|
||||
Plugin1).
|
||||
|
||||
4. Plugin3's pytest_collection_modifyitems then executing the code after the yield
|
||||
point. The yield receives a :py:class:`CallOutcome <_pytest.vendored_packages.pluggy._CallOutcome>` instance which encapsulates
|
||||
point. The yield receives a :py:class:`Result <pluggy._Result>` instance which encapsulates
|
||||
the result from calling the non-wrappers. Wrappers shall not modify the result.
|
||||
|
||||
It's possible to use ``tryfirst`` and ``trylast`` also in conjunction with
|
||||
@@ -583,11 +588,22 @@ pytest hook reference
|
||||
Initialization, command line and configuration hooks
|
||||
----------------------------------------------------
|
||||
|
||||
Bootstrapping hooks
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Bootstrapping hooks called for plugins registered early enough (internal and setuptools plugins).
|
||||
|
||||
.. autofunction:: pytest_load_initial_conftests
|
||||
.. autofunction:: pytest_cmdline_preparse
|
||||
.. autofunction:: pytest_cmdline_parse
|
||||
.. autofunction:: pytest_addoption
|
||||
.. autofunction:: pytest_cmdline_main
|
||||
|
||||
Initialization hooks
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Initialization hooks called for plugins and ``conftest.py`` files.
|
||||
|
||||
.. autofunction:: pytest_addoption
|
||||
.. autofunction:: pytest_configure
|
||||
.. autofunction:: pytest_unconfigure
|
||||
|
||||
@@ -596,7 +612,10 @@ Generic "runtest" hooks
|
||||
|
||||
All runtest related hooks receive a :py:class:`pytest.Item <_pytest.main.Item>` object.
|
||||
|
||||
.. autofunction:: pytest_runtestloop
|
||||
.. autofunction:: pytest_runtest_protocol
|
||||
.. autofunction:: pytest_runtest_logstart
|
||||
.. autofunction:: pytest_runtest_logfinish
|
||||
.. autofunction:: pytest_runtest_setup
|
||||
.. autofunction:: pytest_runtest_call
|
||||
.. autofunction:: pytest_runtest_teardown
|
||||
@@ -616,6 +635,7 @@ Collection hooks
|
||||
|
||||
``pytest`` calls the following hooks for collecting files and directories:
|
||||
|
||||
.. autofunction:: pytest_collection
|
||||
.. autofunction:: pytest_ignore_collect
|
||||
.. autofunction:: pytest_collect_directory
|
||||
.. autofunction:: pytest_collect_file
|
||||
@@ -680,14 +700,22 @@ Reference of objects involved in hooks
|
||||
.. autoclass:: _pytest.config.Parser()
|
||||
:members:
|
||||
|
||||
.. autoclass:: _pytest.main.Node()
|
||||
.. autoclass:: _pytest.nodes.Node()
|
||||
:members:
|
||||
|
||||
.. autoclass:: _pytest.main.Collector()
|
||||
.. autoclass:: _pytest.nodes.Collector()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: _pytest.main.Item()
|
||||
.. autoclass:: _pytest.nodes.FSCollector()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: _pytest.main.Session()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: _pytest.nodes.Item()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
@@ -714,7 +742,7 @@ Reference of objects involved in hooks
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
.. autoclass:: _pytest.vendored_packages.pluggy._CallOutcome()
|
||||
.. autoclass:: pluggy._Result
|
||||
:members:
|
||||
|
||||
.. autofunction:: _pytest.config.get_plugin_manager()
|
||||
@@ -724,7 +752,7 @@ Reference of objects involved in hooks
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: _pytest.vendored_packages.pluggy.PluginManager()
|
||||
.. autoclass:: pluggy.PluginManager()
|
||||
:members:
|
||||
|
||||
.. currentmodule:: _pytest.pytester
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import json
|
||||
import py
|
||||
import textwrap
|
||||
|
||||
issues_url = "https://api.github.com/repos/pytest-dev/pytest/issues"
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ pytest: unit and functional testing with Python.
|
||||
# else we are imported
|
||||
|
||||
from _pytest.config import (
|
||||
main, UsageError, _preloadplugins, cmdline,
|
||||
main, UsageError, cmdline,
|
||||
hookspec, hookimpl
|
||||
)
|
||||
from _pytest.fixtures import fixture, yield_fixture
|
||||
@@ -18,7 +18,8 @@ from _pytest.debugging import pytestPDB as __pytestPDB
|
||||
from _pytest.recwarn import warns, deprecated_call
|
||||
from _pytest.outcomes import fail, skip, importorskip, exit, xfail
|
||||
from _pytest.mark import MARK_GEN as mark, param
|
||||
from _pytest.main import Item, Collector, File, Session
|
||||
from _pytest.main import Session
|
||||
from _pytest.nodes import Item, Collector, File
|
||||
from _pytest.fixtures import fillfixtures as _fillfuncargs
|
||||
from _pytest.python import (
|
||||
Module, Class, Instance, Function, Generator,
|
||||
@@ -74,5 +75,4 @@ if __name__ == '__main__':
|
||||
else:
|
||||
|
||||
from _pytest.compat import _setup_collect_fakemodule
|
||||
_preloadplugins() # to populate pytest.* namespace so help(pytest) works
|
||||
_setup_collect_fakemodule()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user