Compare commits
539 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b322004047 | ||
|
|
2d795dc07b | ||
|
|
2d6b846978 | ||
|
|
703d0f50d8 | ||
|
|
56e6482405 | ||
|
|
589176e9fe | ||
|
|
3734a27733 | ||
|
|
e1a21e46b0 | ||
|
|
551400e8d6 | ||
|
|
b7b729298c | ||
|
|
21ca38b932 | ||
|
|
565f4cb4ad | ||
|
|
af6548a4e7 | ||
|
|
2fb2962df4 | ||
|
|
f838c7b7eb | ||
|
|
25b53c4196 | ||
|
|
fc27171d57 | ||
|
|
d18e426b24 | ||
|
|
e83fa48dd1 | ||
|
|
c53d52c3f2 | ||
|
|
3886c6d735 | ||
|
|
80936b6762 | ||
|
|
5ca08e9b8a | ||
|
|
ba2c49e71e | ||
|
|
da615a4fbe | ||
|
|
32562881d4 | ||
|
|
2a0bbfe63f | ||
|
|
9aed656ec7 | ||
|
|
a600e7a2a4 | ||
|
|
57a95b3a2c | ||
|
|
f5e430fd8f | ||
|
|
38a4c7e56c | ||
|
|
40f02d72b0 | ||
|
|
554f600fb4 | ||
|
|
e3a3c90d94 | ||
|
|
f7327759e8 | ||
|
|
ee1950af77 | ||
|
|
3d0f3baa2b | ||
|
|
8da758b93a | ||
|
|
59e5d1bfbf | ||
|
|
b9e2cd0a81 | ||
|
|
a84fcbf5b2 | ||
|
|
59c1bfada7 | ||
|
|
3267f64724 | ||
|
|
c9fd1bdbd6 | ||
|
|
93aa988e01 | ||
|
|
7996724f23 | ||
|
|
90ee8a7599 | ||
|
|
378a75ddf6 | ||
|
|
e1b3a68462 | ||
|
|
fb7dbc9fa3 | ||
|
|
a0ea300e96 | ||
|
|
09b289e286 | ||
|
|
694dbe5bd4 | ||
|
|
4f8fff9cab | ||
|
|
ac7ebfa22e | ||
|
|
db92cea14c | ||
|
|
bce1d40fb0 | ||
|
|
dc86fb6758 | ||
|
|
2df4f63149 | ||
|
|
e3cf4fc258 | ||
|
|
978b315861 | ||
|
|
580edc13e7 | ||
|
|
1df593f978 | ||
|
|
b7f2e3d4f5 | ||
|
|
f011bc642c | ||
|
|
b1d7a187f2 | ||
|
|
678d65f051 | ||
|
|
fcd3fad03d | ||
|
|
9e8540f25f | ||
|
|
19bb2c6235 | ||
|
|
bc8e52c3c2 | ||
|
|
333bb0883a | ||
|
|
dce2621710 | ||
|
|
acec0b688f | ||
|
|
197b7c3bce | ||
|
|
37d074efc8 | ||
|
|
a5a8d53dfe | ||
|
|
9fd71d6fe0 | ||
|
|
b11bfa106c | ||
|
|
bd7e33277b | ||
|
|
ddc8edffbc | ||
|
|
bdd22fdd52 | ||
|
|
194b52145b | ||
|
|
15e1dd0f87 | ||
|
|
23c43a37e0 | ||
|
|
92767fec51 | ||
|
|
5fc80d8bc3 | ||
|
|
2f60548e08 | ||
|
|
f10ab021e2 | ||
|
|
ff7b5dbbde | ||
|
|
4b53bbc0a9 | ||
|
|
769ffc32bf | ||
|
|
5819536f00 | ||
|
|
952cab2d85 | ||
|
|
d636fcd557 | ||
|
|
f77d606d4e | ||
|
|
16c683dff9 | ||
|
|
2e48c32dea | ||
|
|
2451716746 | ||
|
|
6a7df7f031 | ||
|
|
ac3a42bafd | ||
|
|
be23aeb989 | ||
|
|
bfd0d18371 | ||
|
|
9928c7794b | ||
|
|
706ea86bba | ||
|
|
1d5a0ef284 | ||
|
|
c8b4a1a471 | ||
|
|
68fe0eb8f3 | ||
|
|
de854c6ee1 | ||
|
|
04126feea7 | ||
|
|
eeebcd77dd | ||
|
|
478a244f5e | ||
|
|
47ccd58fb4 | ||
|
|
2277817176 | ||
|
|
1baeefc2fd | ||
|
|
260f848c05 | ||
|
|
7c0d1cad40 | ||
|
|
b26e60c2da | ||
|
|
2be06ba67e | ||
|
|
8e991a622c | ||
|
|
b099fcfa33 | ||
|
|
4c9b850e13 | ||
|
|
81a9df6ed1 | ||
|
|
58ef95ed4d | ||
|
|
435ad221f9 | ||
|
|
d1b50526fa | ||
|
|
4d633a29be | ||
|
|
8a1633c3b4 | ||
|
|
82f5986424 | ||
|
|
fb16d3e27a | ||
|
|
2b13a9b95d | ||
|
|
1b30514783 | ||
|
|
af2b0e1174 | ||
|
|
781a730bea | ||
|
|
7c09d88b72 | ||
|
|
4021770688 | ||
|
|
f95c7f5803 | ||
|
|
24dcc76495 | ||
|
|
442f7a7706 | ||
|
|
d18c75baa3 | ||
|
|
bc976dca3b | ||
|
|
7b8968ff80 | ||
|
|
0c68e7a2c9 | ||
|
|
369284752e | ||
|
|
e872532d0c | ||
|
|
9785ee438d | ||
|
|
959e6b4f44 | ||
|
|
5945c3fe88 | ||
|
|
7155b2277c | ||
|
|
a7a1686433 | ||
|
|
371939fb86 | ||
|
|
7fc9d4c976 | ||
|
|
2b5adc88a7 | ||
|
|
02aa8adae1 | ||
|
|
67e69a7e49 | ||
|
|
b09762df27 | ||
|
|
9631b3c166 | ||
|
|
4b70ba2c21 | ||
|
|
de3353aac1 | ||
|
|
ef73a56032 | ||
|
|
d839686c7b | ||
|
|
d89b5057ca | ||
|
|
83137c89e9 | ||
|
|
dbae5a7ff8 | ||
|
|
07b7b6fa7d | ||
|
|
56a5dbe252 | ||
|
|
8a4d5227e2 | ||
|
|
e6ea9edffe | ||
|
|
b7ad4c2bed | ||
|
|
d79179a239 | ||
|
|
c9f9664336 | ||
|
|
aa4d80cad9 | ||
|
|
4209ad6fca | ||
|
|
3ea74310d7 | ||
|
|
c3e53a072d | ||
|
|
fa877665ad | ||
|
|
3b582858f3 | ||
|
|
78baa7b575 | ||
|
|
7484e346f9 | ||
|
|
d25123eb01 | ||
|
|
d33da078a8 | ||
|
|
d59adc61f9 | ||
|
|
7cc513b2af | ||
|
|
0ee007ca33 | ||
|
|
1ebca37689 | ||
|
|
b59d32a5c7 | ||
|
|
3e4e6297ce | ||
|
|
0b2b40e35d | ||
|
|
f9dd58000a | ||
|
|
449290406c | ||
|
|
12824e6279 | ||
|
|
a62d9a40e7 | ||
|
|
30cb598e9c | ||
|
|
114de91ab7 | ||
|
|
7a7c634e33 | ||
|
|
b4ace46c42 | ||
|
|
a8fc056aad | ||
|
|
9e262038c8 | ||
|
|
ef437ea448 | ||
|
|
cdc7e13067 | ||
|
|
39d9f7cff5 | ||
|
|
632800add5 | ||
|
|
bc494661ad | ||
|
|
5a4c1b628b | ||
|
|
4316fe8a92 | ||
|
|
aa0328782f | ||
|
|
9c7f1d9b32 | ||
|
|
dab90ef726 | ||
|
|
61f2a26675 | ||
|
|
b0d45267c5 | ||
|
|
1480aa31a7 | ||
|
|
75714ee707 | ||
|
|
abffd16ce6 | ||
|
|
fb289667e3 | ||
|
|
8ec4d03c91 | ||
|
|
c55bf23cbe | ||
|
|
99d162e44a | ||
|
|
8bd612b367 | ||
|
|
5256542ea4 | ||
|
|
50f81db817 | ||
|
|
2681b0aed7 | ||
|
|
b10ab0211c | ||
|
|
09a0e45492 | ||
|
|
a9c5d31806 | ||
|
|
c9eeafade5 | ||
|
|
39b25ddcf3 | ||
|
|
4038d6c773 | ||
|
|
a435faad5c | ||
|
|
3645ba3072 | ||
|
|
70739296e1 | ||
|
|
8301993e5e | ||
|
|
1dc265e34a | ||
|
|
e7a833635d | ||
|
|
fed5356941 | ||
|
|
5b81bd862c | ||
|
|
2902c7263c | ||
|
|
442dccef65 | ||
|
|
e830432592 | ||
|
|
ae788997f2 | ||
|
|
88b800355a | ||
|
|
864338de71 | ||
|
|
eb5e651d7e | ||
|
|
55bffb7c15 | ||
|
|
be18f85a6e | ||
|
|
28b8f3ca3a | ||
|
|
d91459fc75 | ||
|
|
4de8e680e3 | ||
|
|
ef283efc42 | ||
|
|
b5b6e051ed | ||
|
|
10b1b79f4e | ||
|
|
6d7e06e6be | ||
|
|
78eddcb5b1 | ||
|
|
757873edb3 | ||
|
|
64ab68ff0a | ||
|
|
66330444a3 | ||
|
|
97f1645993 | ||
|
|
3dbc61dd80 | ||
|
|
b42938421e | ||
|
|
87fecce77b | ||
|
|
05d953d9e4 | ||
|
|
d478e2bbca | ||
|
|
595d62bc3e | ||
|
|
8c41236c66 | ||
|
|
99b90f45d0 | ||
|
|
d848a20563 | ||
|
|
06a7fef00f | ||
|
|
55e5817570 | ||
|
|
3f4b8d3aec | ||
|
|
a3f482ceba | ||
|
|
3ccf2a5e61 | ||
|
|
8e1d59a8dd | ||
|
|
7eaf98af4b | ||
|
|
d282424589 | ||
|
|
ddaa5d88ac | ||
|
|
4ff90b1fcf | ||
|
|
2d29c3e7d1 | ||
|
|
abd5fc80e8 | ||
|
|
80d4dd6f0b | ||
|
|
e440b43258 | ||
|
|
e25d46aae6 | ||
|
|
7c87874277 | ||
|
|
1cf9e68dbc | ||
|
|
b2e6f66438 | ||
|
|
b01e379428 | ||
|
|
1586653102 | ||
|
|
18ac7e0b79 | ||
|
|
c854daa234 | ||
|
|
12c5a6af64 | ||
|
|
30922ee694 | ||
|
|
35ba053f00 | ||
|
|
d0cb16010b | ||
|
|
ad0f4f0ac0 | ||
|
|
9c716e4d74 | ||
|
|
94ac0f7e6b | ||
|
|
440881d63a | ||
|
|
aa318e9adf | ||
|
|
20b66e60c0 | ||
|
|
d017b69f38 | ||
|
|
ae5d16be10 | ||
|
|
cbad319736 | ||
|
|
a9eab07739 | ||
|
|
c2980eb80f | ||
|
|
bf5c76359c | ||
|
|
40758e86ca | ||
|
|
d678d380cb | ||
|
|
3f8f395210 | ||
|
|
7c52a37d46 | ||
|
|
198b1dcffd | ||
|
|
e2934c3f8c | ||
|
|
6b7e1a246c | ||
|
|
817c094ce6 | ||
|
|
df1f43ee28 | ||
|
|
778d4364fa | ||
|
|
039d582b52 | ||
|
|
9b8039cf09 | ||
|
|
b687f20d25 | ||
|
|
510be29db8 | ||
|
|
fe343a79f8 | ||
|
|
1e3bc1814d | ||
|
|
57512aa997 | ||
|
|
2e8f7ef31b | ||
|
|
c51173d426 | ||
|
|
2da331ea9c | ||
|
|
94d8c071b6 | ||
|
|
0d55fb3797 | ||
|
|
f3967333a1 | ||
|
|
a76bc64c54 | ||
|
|
dd5c2b22bd | ||
|
|
a29d6194f5 | ||
|
|
6f2943c7b3 | ||
|
|
5e15c86cc6 | ||
|
|
498884a2a0 | ||
|
|
09bdbffbde | ||
|
|
09ab5fd7e9 | ||
|
|
040a61e22c | ||
|
|
2c32dad343 | ||
|
|
8a8f9bd751 | ||
|
|
192d3adda3 | ||
|
|
5865520c51 | ||
|
|
e2fa78c99f | ||
|
|
fe895a40b6 | ||
|
|
934f38995a | ||
|
|
5dcd24fecb | ||
|
|
e5a362d0f5 | ||
|
|
f8654e6656 | ||
|
|
0d4f479aa8 | ||
|
|
79d00ab35a | ||
|
|
79ae86cc3f | ||
|
|
c051a9e7b9 | ||
|
|
cdd6f86e43 | ||
|
|
83451b548f | ||
|
|
38538c6c6d | ||
|
|
55ebd9f803 | ||
|
|
8ca8d25202 | ||
|
|
a8d67f5e7b | ||
|
|
03bc8aba4e | ||
|
|
6b13379f37 | ||
|
|
9dcdea5de7 | ||
|
|
863bab5326 | ||
|
|
8521503246 | ||
|
|
0b6258ab5b | ||
|
|
b63cb18776 | ||
|
|
00097df5cd | ||
|
|
10e243d206 | ||
|
|
e7444bbd5e | ||
|
|
eb7a57f965 | ||
|
|
252eae5bc8 | ||
|
|
9c7b3c57d7 | ||
|
|
1350c601dc | ||
|
|
ef112fd7dd | ||
|
|
8fa57c8384 | ||
|
|
ad02f6f879 | ||
|
|
bd6ba3f3e1 | ||
|
|
e17f5fad14 | ||
|
|
72d06e6dec | ||
|
|
aca1723d45 | ||
|
|
85df6bbe26 | ||
|
|
ba49581510 | ||
|
|
0bf9628e62 | ||
|
|
d878d9d4d5 | ||
|
|
93b74d28d2 | ||
|
|
8c4dfca0c1 | ||
|
|
b8787b8732 | ||
|
|
ef294fc727 | ||
|
|
0f78ef8e02 | ||
|
|
1cd4bafda7 | ||
|
|
8f5fd537d8 | ||
|
|
9869a3d9e1 | ||
|
|
2f00b041e4 | ||
|
|
62db3f7abc | ||
|
|
7ee27fedee | ||
|
|
7b1e3d1c9a | ||
|
|
a52f791461 | ||
|
|
fb99b5c66e | ||
|
|
ddfa41b5a7 | ||
|
|
6a26ac4125 | ||
|
|
cdaa9c06e1 | ||
|
|
0dc82e8501 | ||
|
|
f9bed82c4d | ||
|
|
2406076611 | ||
|
|
44eb1f580c | ||
|
|
3392be37e1 | ||
|
|
3d2680b31b | ||
|
|
0b603156b9 | ||
|
|
0c247be769 | ||
|
|
3e6f0f34ff | ||
|
|
4fb9cc3bf0 | ||
|
|
930a158a6a | ||
|
|
d347a30656 | ||
|
|
32b62f770f | ||
|
|
aaae43e0ba | ||
|
|
1a75a3c08e | ||
|
|
09e9a01df3 | ||
|
|
4b974b051d | ||
|
|
5c445b05e7 | ||
|
|
4e0dbe92dd | ||
|
|
956389fa8c | ||
|
|
61f985f3c7 | ||
|
|
2f0d0fb349 | ||
|
|
e98176cf50 | ||
|
|
38fc208205 | ||
|
|
89f92a459a | ||
|
|
40d0031cce | ||
|
|
d0811c1f3d | ||
|
|
ac41f36a02 | ||
|
|
7a626921c0 | ||
|
|
59bc6efbf2 | ||
|
|
a7292a0544 | ||
|
|
1971033051 | ||
|
|
d4d04e7f25 | ||
|
|
4f0eec2022 | ||
|
|
cc7f294cfe | ||
|
|
e1298faef9 | ||
|
|
e211d6fe2a | ||
|
|
4a42deee7e | ||
|
|
2cce026766 | ||
|
|
e16cb2fdd0 | ||
|
|
dcb94d8f31 | ||
|
|
19f66cb824 | ||
|
|
36944157f8 | ||
|
|
2cfee583db | ||
|
|
1667cf3350 | ||
|
|
a4f5b8a4d6 | ||
|
|
5f4cd536f9 | ||
|
|
83813bf515 | ||
|
|
118cb3d3be | ||
|
|
3789bb53a7 | ||
|
|
9298f7e4a9 | ||
|
|
ab6406b42e | ||
|
|
8ba0b7bc2a | ||
|
|
f2659f77be | ||
|
|
b9c136b809 | ||
|
|
fd1691a2b3 | ||
|
|
21d189eb52 | ||
|
|
e9d9f71374 | ||
|
|
a136111dcc | ||
|
|
2d488f7615 | ||
|
|
1356d20e90 | ||
|
|
1e28cb855d | ||
|
|
29db2da9a7 | ||
|
|
5b295ec68e | ||
|
|
fa645a7003 | ||
|
|
13baab746d | ||
|
|
9f1ade9acf | ||
|
|
2d2c67d7c0 | ||
|
|
e8c8559efa | ||
|
|
91a96ec3d6 | ||
|
|
12f74a28fa | ||
|
|
0fa35960ba | ||
|
|
c627ac4e59 | ||
|
|
0375c1b728 | ||
|
|
4848bbdf9a | ||
|
|
8656c1a61d | ||
|
|
a5224f7490 | ||
|
|
466bbbf8e8 | ||
|
|
a9608d54e0 | ||
|
|
ed57b8e08a | ||
|
|
75493f78bf | ||
|
|
c487cf9dd5 | ||
|
|
afbaee7649 | ||
|
|
853889e5db | ||
|
|
ecd1e43afb | ||
|
|
226f0c48bf | ||
|
|
9b74bf1e0c | ||
|
|
fa51a26743 | ||
|
|
59067ad33d | ||
|
|
cbb2f9541b | ||
|
|
b29ae03cb6 | ||
|
|
0711060422 | ||
|
|
1ef29ab548 | ||
|
|
e13ad22364 | ||
|
|
30f2729684 | ||
|
|
3812985ed4 | ||
|
|
c6ed69a666 | ||
|
|
3a0f436c1a | ||
|
|
a326fa22c6 | ||
|
|
623b3982b0 | ||
|
|
5a7de2c2cb | ||
|
|
24d4882d82 | ||
|
|
ac5929eef3 | ||
|
|
e24b6b0388 | ||
|
|
985ac09048 | ||
|
|
1c4a672a52 | ||
|
|
59f95b7f59 | ||
|
|
41b7b109e9 | ||
|
|
c9ec724886 | ||
|
|
16ff9f591e | ||
|
|
da091b832d | ||
|
|
8feeb09398 | ||
|
|
b6b0bc03f3 | ||
|
|
a1219ab8fc | ||
|
|
47ae1fb36b | ||
|
|
bc7282576f | ||
|
|
d7d5cf4136 | ||
|
|
a3c8246b60 | ||
|
|
15ffe63204 | ||
|
|
c99c7d0f95 | ||
|
|
886b8d27c6 | ||
|
|
a4408eb9c1 | ||
|
|
b0ebcfb785 | ||
|
|
a02310a140 | ||
|
|
2fa0518e89 | ||
|
|
df0c652333 | ||
|
|
5820c5c5a6 | ||
|
|
ed012c808a | ||
|
|
6b75a7733b | ||
|
|
2d449e95e4 | ||
|
|
dac16cd9e5 | ||
|
|
2c941b5d13 | ||
|
|
98c899c9b0 | ||
|
|
f1224a0e85 | ||
|
|
ccb3ef3b33 | ||
|
|
c0b1a39192 | ||
|
|
51f9cd0e02 | ||
|
|
426a4cdca9 | ||
|
|
59369651db | ||
|
|
f7b1de70c0 | ||
|
|
04f27d4eb4 |
@@ -24,5 +24,6 @@ exclude_lines =
|
||||
\#\s*pragma: no cover
|
||||
^\s*raise NotImplementedError\b
|
||||
^\s*return NotImplemented\b
|
||||
^\s*assert False(,|$)
|
||||
|
||||
^\s*if TYPE_CHECKING:
|
||||
|
||||
28
.gitblameignore
Normal file
28
.gitblameignore
Normal file
@@ -0,0 +1,28 @@
|
||||
# List of revisions that can be ignored with git-blame(1).
|
||||
#
|
||||
# See `blame.ignoreRevsFile` in git-config(1) to enable it by default, or
|
||||
# use it with `--ignore-revs-file` manually with git-blame.
|
||||
#
|
||||
# To "install" it:
|
||||
#
|
||||
# git config --local blame.ignoreRevsFile .gitblameignore
|
||||
|
||||
# run black
|
||||
703e4b11ba76171eccd3f13e723c47b810ded7ef
|
||||
# switched to src layout
|
||||
eaa882f3d5340956beb176aa1753e07e3f3f2190
|
||||
# pre-commit run pyupgrade --all-files
|
||||
a91fe1feddbded535a4322ab854429e3a3961fb4
|
||||
# move node base classes from main to nodes
|
||||
afc607cfd81458d4e4f3b1f3cf8cc931b933907e
|
||||
# [?] split most fixture related code into own plugin
|
||||
8c49561470708761f7321504f5e8343811be87ac
|
||||
# run pyupgrade
|
||||
9aacb4635e81edd6ecf281d4f6c0cfc8e94ab301
|
||||
# run blacken-docs
|
||||
5f95dce95602921a70bfbc7d8de2f7712c5e4505
|
||||
# ran pyupgrade-docs again
|
||||
75d0b899bbb56d6849e9d69d83a9426ed3f43f8b
|
||||
|
||||
# move argument parser to own file
|
||||
c9df77cbd6a365dcb73c39618e4842711817e871
|
||||
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -3,10 +3,9 @@ Thanks for submitting a PR, your contribution is really appreciated!
|
||||
|
||||
Here is a quick checklist that should be present in PRs.
|
||||
|
||||
- [ ] Target the `master` branch for bug fixes, documentation updates and trivial changes.
|
||||
- [ ] Target the `features` branch for new features, improvements, and removals/deprecations.
|
||||
- [ ] Include documentation when adding new features.
|
||||
- [ ] Include new tests or update existing tests when applicable.
|
||||
- [X] Allow maintainers to push and squash when merging my commits. Please uncheck this if you prefer to squash the commits yourself.
|
||||
|
||||
Unless your change is trivial or a small documentation fix (e.g., a typo or reword of a small section) please:
|
||||
|
||||
|
||||
66
.github/workflows/main.yml
vendored
66
.github/workflows/main.yml
vendored
@@ -10,12 +10,14 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- "[0-9]+.[0-9]+.x"
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- "[0-9]+.[0-9]+.x"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -37,12 +39,15 @@ jobs:
|
||||
"ubuntu-py37-pluggy",
|
||||
"ubuntu-py37-freeze",
|
||||
"ubuntu-py38",
|
||||
"ubuntu-py39",
|
||||
"ubuntu-pypy3",
|
||||
|
||||
"macos-py37",
|
||||
"macos-py38",
|
||||
|
||||
"linting",
|
||||
"docs",
|
||||
"doctesting",
|
||||
]
|
||||
|
||||
include:
|
||||
@@ -50,6 +55,7 @@ jobs:
|
||||
python: "3.5"
|
||||
os: windows-latest
|
||||
tox_env: "py35-xdist"
|
||||
use_coverage: true
|
||||
- name: "windows-py36"
|
||||
python: "3.6"
|
||||
os: windows-latest
|
||||
@@ -57,7 +63,7 @@ jobs:
|
||||
- name: "windows-py37"
|
||||
python: "3.7"
|
||||
os: windows-latest
|
||||
tox_env: "py37-twisted-numpy"
|
||||
tox_env: "py37-numpy"
|
||||
- name: "windows-py37-pluggy"
|
||||
python: "3.7"
|
||||
os: windows-latest
|
||||
@@ -65,7 +71,8 @@ jobs:
|
||||
- name: "windows-py38"
|
||||
python: "3.8"
|
||||
os: windows-latest
|
||||
tox_env: "py38"
|
||||
tox_env: "py38-unittestextras"
|
||||
use_coverage: true
|
||||
|
||||
- name: "ubuntu-py35"
|
||||
python: "3.5"
|
||||
@@ -78,7 +85,8 @@ jobs:
|
||||
- name: "ubuntu-py37"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py37-lsof-numpy-oldattrs-pexpect-twisted"
|
||||
tox_env: "py37-lsof-numpy-oldattrs-pexpect"
|
||||
use_coverage: true
|
||||
- name: "ubuntu-py37-pluggy"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
@@ -87,18 +95,18 @@ jobs:
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py37-freeze"
|
||||
# coverage does not apply for freeze test, skip it
|
||||
skip_coverage: true
|
||||
- name: "ubuntu-py38"
|
||||
python: "3.8"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py38-xdist"
|
||||
- name: "ubuntu-py39"
|
||||
python: "3.8"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py39-xdist"
|
||||
- name: "ubuntu-pypy3"
|
||||
python: "pypy3"
|
||||
os: ubuntu-latest
|
||||
tox_env: "pypy3-xdist"
|
||||
# coverage too slow with pypy3, skip it
|
||||
skip_coverage: true
|
||||
|
||||
- name: "macos-py37"
|
||||
python: "3.7"
|
||||
@@ -108,11 +116,21 @@ jobs:
|
||||
python: "3.8"
|
||||
os: macos-latest
|
||||
tox_env: "py38-xdist"
|
||||
use_coverage: true
|
||||
|
||||
- name: "linting"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
tox_env: "linting,docs,doctesting"
|
||||
tox_env: "linting"
|
||||
- name: "docs"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
tox_env: "docs"
|
||||
- name: "doctesting"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
tox_env: "doctesting"
|
||||
use_coverage: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
@@ -120,17 +138,23 @@ jobs:
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
- name: install python3.9
|
||||
if: matrix.tox_env == 'py39-xdist'
|
||||
run: |
|
||||
sudo add-apt-repository ppa:deadsnakes/nightly
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends python3.9-dev python3.9-distutils
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install tox coverage
|
||||
|
||||
- name: Test without coverage
|
||||
if: "matrix.skip_coverage"
|
||||
if: "! matrix.use_coverage"
|
||||
run: "tox -e ${{ matrix.tox_env }}"
|
||||
|
||||
- name: Test with coverage
|
||||
if: "! matrix.skip_coverage"
|
||||
if: "matrix.use_coverage"
|
||||
env:
|
||||
_PYTEST_TOX_COVERAGE_RUN: "coverage run -m"
|
||||
COVERAGE_PROCESS_START: ".coveragerc"
|
||||
@@ -138,25 +162,15 @@ jobs:
|
||||
run: "tox -e ${{ matrix.tox_env }}"
|
||||
|
||||
- name: Prepare coverage token
|
||||
if: success() && !matrix.skip_coverage && ( github.repository == 'pytest-dev/pytest' || github.event_name == 'pull_request' )
|
||||
if: (matrix.use_coverage && ( github.repository == 'pytest-dev/pytest' || github.event_name == 'pull_request' ))
|
||||
run: |
|
||||
python scripts/append_codecov_token.py
|
||||
|
||||
- name: Combine coverage
|
||||
if: success() && !matrix.skip_coverage
|
||||
run: |
|
||||
python -m coverage combine
|
||||
python -m coverage xml
|
||||
|
||||
- name: Codecov upload
|
||||
if: success() && !matrix.skip_coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
token: ${{ secrets.codecov }}
|
||||
file: ./coverage.xml
|
||||
flags: ${{ runner.os }}
|
||||
fail_ci_if_error: false
|
||||
name: ${{ matrix.name }}
|
||||
- name: Report coverage
|
||||
if: (matrix.use_coverage)
|
||||
env:
|
||||
CODECOV_NAME: ${{ matrix.name }}
|
||||
run: bash scripts/report-coverage.sh -F GHA,${{ runner.os }}
|
||||
|
||||
deploy:
|
||||
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && github.repository == 'pytest-dev/pytest'
|
||||
|
||||
28
.github/workflows/release-on-comment.yml
vendored
Normal file
28
.github/workflows/release-on-comment.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# part of our release process, see `release-on-comment.py`
|
||||
name: release on comment
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
issue_comment:
|
||||
types: [created, edited]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: (github.event.comment && startsWith(github.event.comment.body, '@pytestbot please')) || (github.event.issue && !github.event.comment && startsWith(github.event.issue.body, '@pytestbot please'))
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: "3.8"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install --upgrade setuptools tox
|
||||
- name: Prepare release
|
||||
run: |
|
||||
tox -e release-on-comment -- $GITHUB_EVENT_PATH ${{ secrets.chatops }}
|
||||
@@ -47,14 +47,14 @@ repos:
|
||||
- id: rst
|
||||
name: rst
|
||||
entry: rst-lint --encoding utf-8
|
||||
files: ^(HOWTORELEASE.rst|README.rst|TIDELIFT.rst)$
|
||||
files: ^(RELEASING.rst|README.rst|TIDELIFT.rst)$
|
||||
language: python
|
||||
additional_dependencies: [pygments, restructuredtext_lint]
|
||||
- id: changelogs-rst
|
||||
name: changelog filenames
|
||||
language: fail
|
||||
entry: 'changelog files must be named ####.(feature|bugfix|doc|deprecation|removal|vendor|trivial).rst'
|
||||
exclude: changelog/(\d+\.(feature|improvement|bugfix|doc|deprecation|removal|vendor|trivial).rst|README.rst|_template.rst)
|
||||
entry: 'changelog files must be named ####.(breaking|bugfix|deprecation|doc|feature|improvement|trivial|vendor).rst'
|
||||
exclude: changelog/(\d+\.(breaking|bugfix|deprecation|doc|feature|improvement|trivial|vendor).rst|README.rst|_template.rst)
|
||||
files: ^changelog/
|
||||
- id: py-deprecated
|
||||
name: py library is deprecated
|
||||
@@ -64,7 +64,7 @@ repos:
|
||||
_code\.|
|
||||
builtin\.|
|
||||
code\.|
|
||||
io\.(BytesIO|saferepr)|
|
||||
io\.(BytesIO|saferepr|TerminalWriter)|
|
||||
path\.local\.sysfind|
|
||||
process\.|
|
||||
std\.
|
||||
|
||||
64
.travis.yml
64
.travis.yml
@@ -1,10 +1,6 @@
|
||||
language: python
|
||||
dist: xenial
|
||||
stages:
|
||||
- baseline
|
||||
- test
|
||||
|
||||
python: '3.7'
|
||||
dist: trusty
|
||||
python: '3.5.1'
|
||||
cache: false
|
||||
|
||||
env:
|
||||
@@ -20,55 +16,11 @@ install:
|
||||
|
||||
jobs:
|
||||
include:
|
||||
# OSX tests - first (in test stage), since they are the slower ones.
|
||||
# Coverage for:
|
||||
# - osx
|
||||
# - verbose=1
|
||||
- os: osx
|
||||
osx_image: xcode10.1
|
||||
language: generic
|
||||
env: TOXENV=py37-xdist PYTEST_COVERAGE=1 PYTEST_ADDOPTS=-v
|
||||
before_install:
|
||||
- which python3
|
||||
- python3 -V
|
||||
- ln -sfn "$(which python3)" /usr/local/bin/python
|
||||
- python -V
|
||||
- test $(python -c 'import sys; print("%d%d" % sys.version_info[0:2])') = 37
|
||||
|
||||
# Full run of latest supported version, without xdist.
|
||||
# Coverage for:
|
||||
# - pytester's LsofFdLeakChecker
|
||||
# - TestArgComplete (linux only)
|
||||
# - numpy
|
||||
# - old attrs
|
||||
# - verbose=0
|
||||
# - test_sys_breakpoint_interception (via pexpect).
|
||||
- env: TOXENV=py37-lsof-numpy-oldattrs-pexpect-twisted PYTEST_COVERAGE=1 PYTEST_ADDOPTS=
|
||||
python: '3.7'
|
||||
|
||||
# Coverage tracking is slow with pypy, skip it.
|
||||
- env: TOXENV=pypy3-xdist
|
||||
python: 'pypy3'
|
||||
|
||||
# Coverage for Python 3.5.{0,1} specific code, mostly typing related.
|
||||
- env: TOXENV=py35 PYTEST_COVERAGE=1 PYTEST_ADDOPTS="-k test_raises_cyclic_reference"
|
||||
python: '3.5.1'
|
||||
dist: trusty
|
||||
|
||||
# Specialized factors for py37.
|
||||
- env: TOXENV=py37-pluggymaster-xdist
|
||||
- env: TOXENV=py37-freeze
|
||||
|
||||
- env: TOXENV=py38-xdist
|
||||
python: '3.8'
|
||||
|
||||
- stage: baseline
|
||||
env: TOXENV=py36-xdist
|
||||
python: '3.6'
|
||||
- env: TOXENV=linting,docs,doctesting PYTEST_COVERAGE=1
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/pre-commit
|
||||
before_install:
|
||||
# Work around https://github.com/jaraco/zipp/issues/40.
|
||||
- python -m pip install -U 'setuptools>=34.4.0' virtualenv==16.7.9
|
||||
|
||||
before_script:
|
||||
- |
|
||||
@@ -89,7 +41,7 @@ script: tox
|
||||
after_success:
|
||||
- |
|
||||
if [[ "$PYTEST_COVERAGE" = 1 ]]; then
|
||||
env CODECOV_NAME="$TOXENV-$TRAVIS_OS_NAME" scripts/report-coverage.sh
|
||||
env CODECOV_NAME="$TOXENV-$TRAVIS_OS_NAME" scripts/report-coverage.sh -F Travis
|
||||
fi
|
||||
|
||||
notifications:
|
||||
@@ -105,6 +57,4 @@ notifications:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- features
|
||||
- 4.6-maintenance
|
||||
- /^\d+(\.\d+)+$/
|
||||
- /^\d+\.\d+\.x$/
|
||||
|
||||
17
AUTHORS
17
AUTHORS
@@ -52,21 +52,21 @@ Carl Friedrich Bolz
|
||||
Carlos Jenkins
|
||||
Ceridwen
|
||||
Charles Cloud
|
||||
Charles Machalow
|
||||
Charnjit SiNGH (CCSJ)
|
||||
Chris Lamb
|
||||
Chris NeJame
|
||||
Christian Boelsen
|
||||
Christian Fetzer
|
||||
Christian Neumüller
|
||||
Christian Theunert
|
||||
Christian Tismer
|
||||
Christopher Gilling
|
||||
Christoph Buelter
|
||||
Christopher Dignam
|
||||
Christopher Gilling
|
||||
Claudio Madotto
|
||||
CrazyMerlyn
|
||||
Cyrus Maden
|
||||
Damian Skrzypczak
|
||||
Dhiren Serai
|
||||
Daniel Grana
|
||||
Daniel Hahler
|
||||
Daniel Nuri
|
||||
@@ -81,6 +81,7 @@ David Szotten
|
||||
David Vierra
|
||||
Daw-Ran Liou
|
||||
Denis Kirisov
|
||||
Dhiren Serai
|
||||
Diego Russo
|
||||
Dmitry Dygalo
|
||||
Dmitry Pribysh
|
||||
@@ -113,6 +114,7 @@ Guido Wesdorp
|
||||
Guoqiang Zhang
|
||||
Harald Armin Massa
|
||||
Henk-Jaap Wagenaar
|
||||
Holger Kohr
|
||||
Hugo van Kemenade
|
||||
Hui Wang (coldnight)
|
||||
Ian Bicking
|
||||
@@ -121,6 +123,7 @@ Ilya Konstantinov
|
||||
Ionuț Turturică
|
||||
Iwan Briquemont
|
||||
Jaap Broekhuizen
|
||||
Jakub Mitoraj
|
||||
Jan Balster
|
||||
Janne Vanhala
|
||||
Jason R. Coombs
|
||||
@@ -142,6 +145,7 @@ Joshua Bronson
|
||||
Jurko Gospodnetić
|
||||
Justyna Janczyszyn
|
||||
Kale Kundert
|
||||
Karl O. Pinc
|
||||
Katarzyna Jachim
|
||||
Katerina Koukiou
|
||||
Kevin Cox
|
||||
@@ -191,6 +195,7 @@ Mihai Capotă
|
||||
Mike Hoyle (hoylemd)
|
||||
Mike Lundy
|
||||
Miro Hrončok
|
||||
Nathaniel Compton
|
||||
Nathaniel Waisbrot
|
||||
Ned Batchelder
|
||||
Neven Mundar
|
||||
@@ -207,8 +212,10 @@ Omer Hadari
|
||||
Ondřej Súkup
|
||||
Oscar Benjamin
|
||||
Patrick Hayes
|
||||
Pauli Virtanen
|
||||
Paweł Adamczak
|
||||
Pedro Algarvio
|
||||
Philipp Loose
|
||||
Pieter Mulder
|
||||
Piotr Banaszkiewicz
|
||||
Pulkit Goyal
|
||||
@@ -241,6 +248,7 @@ Simon Gomizelj
|
||||
Skylar Downes
|
||||
Srinivas Reddy Thatiparthy
|
||||
Stefan Farmbauer
|
||||
Stefan Scherfke
|
||||
Stefan Zimmermann
|
||||
Stefano Taschini
|
||||
Steffen Allner
|
||||
@@ -257,7 +265,9 @@ Tim Hoffmann
|
||||
Tim Strazny
|
||||
Tom Dalton
|
||||
Tom Viner
|
||||
Tomáš Gavenčiak
|
||||
Tomer Keren
|
||||
Tor Colvin
|
||||
Trevor Bekolay
|
||||
Tyler Goodlet
|
||||
Tzu-ping Chung
|
||||
@@ -268,6 +278,7 @@ Vidar T. Fauske
|
||||
Virgil Dupras
|
||||
Vitaly Lashmanov
|
||||
Vlad Dragos
|
||||
Vladyslav Rachek
|
||||
Volodymyr Piskun
|
||||
Wei Lin
|
||||
Wil Cooley
|
||||
|
||||
@@ -71,7 +71,6 @@ contacted individually:
|
||||
- Brianna Laugher ([@pfctdayelise](https://github.com/pfctdayelise)): brianna@laugher.id.au
|
||||
- Bruno Oliveira ([@nicoddemus](https://github.com/nicoddemus)): nicoddemus@gmail.com
|
||||
- Florian Bruhin ([@the-compiler](https://github.com/the-compiler)): pytest@the-compiler.org
|
||||
- Ronny Pfannschmidt ([@RonnyPfannschmidt](https://github.com/RonnyPfannschmidt)): ich@ronnypfannschmidt.de
|
||||
|
||||
## Attribution
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ without using a local copy. This can be convenient for small fixes.
|
||||
|
||||
$ tox -e docs
|
||||
|
||||
The built documentation should be available in the ``doc/en/_build/``.
|
||||
The built documentation should be available in ``doc/en/_build/html``.
|
||||
|
||||
Where 'en' refers to the documentation language.
|
||||
|
||||
@@ -166,8 +166,6 @@ Short version
|
||||
|
||||
#. Fork the repository.
|
||||
#. Enable and install `pre-commit <https://pre-commit.com>`_ to ensure style-guides and code checks are followed.
|
||||
#. Target ``master`` for bugfixes and doc changes.
|
||||
#. Target ``features`` for new features or functionality changes.
|
||||
#. Follow **PEP-8** for naming and `black <https://github.com/psf/black>`_ for formatting.
|
||||
#. Tests are run using ``tox``::
|
||||
|
||||
@@ -204,15 +202,11 @@ Here is a simple overview, with pytest-specific bits:
|
||||
|
||||
$ git clone git@github.com:YOUR_GITHUB_USERNAME/pytest.git
|
||||
$ cd pytest
|
||||
# now, to fix a bug create your own branch off "master":
|
||||
# now, create your own branch off "master":
|
||||
|
||||
$ git checkout -b your-bugfix-branch-name master
|
||||
|
||||
# or to instead add a feature create your own branch off "features":
|
||||
|
||||
$ git checkout -b your-feature-branch-name features
|
||||
|
||||
Given we have "major.minor.micro" version numbers, bugfixes will usually
|
||||
Given we have "major.minor.micro" version numbers, bug fixes will usually
|
||||
be released in micro releases whereas features will be released in
|
||||
minor releases and incompatible changes in major releases.
|
||||
|
||||
@@ -294,8 +288,7 @@ Here is a simple overview, with pytest-specific bits:
|
||||
compare: your-branch-name
|
||||
|
||||
base-fork: pytest-dev/pytest
|
||||
base: master # if it's a bugfix
|
||||
base: features # if it's a feature
|
||||
base: master
|
||||
|
||||
|
||||
Writing Tests
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
Release Procedure
|
||||
-----------------
|
||||
|
||||
Our current policy for releasing is to aim for a bugfix every few weeks and a minor release every 2-3 months. The idea
|
||||
is to get fixes and new features out instead of trying to cram a ton of features into a release and by consequence
|
||||
taking a lot of time to make a new one.
|
||||
|
||||
.. important::
|
||||
|
||||
pytest releases must be prepared on **Linux** because the docs and examples expect
|
||||
to be executed in that platform.
|
||||
|
||||
#. Create a branch ``release-X.Y.Z`` with the version for the release.
|
||||
|
||||
* **maintenance releases**: from ``4.6-maintenance``;
|
||||
|
||||
* **patch releases**: from the latest ``master``;
|
||||
|
||||
* **minor releases**: from the latest ``features``; then merge with the latest ``master``;
|
||||
|
||||
Ensure your are in a clean work tree.
|
||||
|
||||
#. Using ``tox``, generate docs, changelog, announcements::
|
||||
|
||||
$ tox -e release -- <VERSION>
|
||||
|
||||
This will generate a commit with all the changes ready for pushing.
|
||||
|
||||
#. Open a PR for this branch targeting ``master`` (or ``4.6-maintenance`` for
|
||||
maintenance releases).
|
||||
|
||||
#. After all tests pass and the PR has been approved, publish to PyPI by pushing the tag::
|
||||
|
||||
git tag <VERSION>
|
||||
git push git@github.com:pytest-dev/pytest.git <VERSION>
|
||||
|
||||
Wait for the deploy to complete, then make sure it is `available on PyPI <https://pypi.org/project/pytest>`_.
|
||||
|
||||
#. Merge the PR.
|
||||
|
||||
#. If this is a maintenance release, cherry-pick the CHANGELOG / announce
|
||||
files to the ``master`` branch::
|
||||
|
||||
git fetch --all --prune
|
||||
git checkout origin/master -b cherry-pick-maintenance-release
|
||||
git cherry-pick --no-commit -m1 origin/4.6-maintenance
|
||||
git checkout origin/master -- changelog
|
||||
git commit # no arguments
|
||||
|
||||
#. Send an email announcement with the contents from::
|
||||
|
||||
doc/en/announce/release-<VERSION>.rst
|
||||
|
||||
To the following mailing lists:
|
||||
|
||||
* pytest-dev@python.org (all releases)
|
||||
* python-announce-list@python.org (all releases)
|
||||
* testing-in-python@lists.idyll.org (only major/minor releases)
|
||||
|
||||
And announce it on `Twitter <https://twitter.com/>`_ with the ``#pytest`` hashtag.
|
||||
@@ -31,6 +31,10 @@
|
||||
.. image:: https://www.codetriage.com/pytest-dev/pytest/badges/users.svg
|
||||
:target: https://www.codetriage.com/pytest-dev/pytest
|
||||
|
||||
.. image:: https://readthedocs.org/projects/pytest/badge/?version=latest
|
||||
:target: https://pytest.readthedocs.io/en/latest/?badge=latest
|
||||
:alt: Documentation Status
|
||||
|
||||
The ``pytest`` framework makes it easy to write small tests, yet
|
||||
scales to support complex functional testing for applications and libraries.
|
||||
|
||||
|
||||
82
RELEASING.rst
Normal file
82
RELEASING.rst
Normal file
@@ -0,0 +1,82 @@
|
||||
Release Procedure
|
||||
-----------------
|
||||
|
||||
Our current policy for releasing is to aim for a bug-fix release every few weeks and a minor release every 2-3 months. The idea
|
||||
is to get fixes and new features out instead of trying to cram a ton of features into a release and by consequence
|
||||
taking a lot of time to make a new one.
|
||||
|
||||
Preparing: Automatic Method
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
We have developed an automated workflow for releases, that uses GitHub workflows and is triggered
|
||||
by opening an issue or issuing a comment one.
|
||||
|
||||
The comment must be in the form::
|
||||
|
||||
@pytestbot please prepare release from BRANCH
|
||||
|
||||
Where ``BRANCH`` is ``master`` or one of the maintenance branches.
|
||||
|
||||
After that, the workflow should publish a PR and notify that it has done so as a comment
|
||||
in the original issue.
|
||||
|
||||
Preparing: Manual Method
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. important::
|
||||
|
||||
pytest releases must be prepared on **Linux** because the docs and examples expect
|
||||
to be executed on that platform.
|
||||
|
||||
To release a version ``MAJOR.MINOR.PATCH``, follow these steps:
|
||||
|
||||
#. For major and minor releases, create a new branch ``MAJOR.MINOR.x`` from the
|
||||
latest ``master`` and push it to the ``pytest-dev/pytest`` repo.
|
||||
|
||||
#. Create a branch ``release-MAJOR.MINOR.PATCH`` from the ``MAJOR.MINOR.x`` branch.
|
||||
|
||||
Ensure your are updated and in a clean working tree.
|
||||
|
||||
#. Using ``tox``, generate docs, changelog, announcements::
|
||||
|
||||
$ tox -e release -- MAJOR.MINOR.PATCH
|
||||
|
||||
This will generate a commit with all the changes ready for pushing.
|
||||
|
||||
#. Open a PR for the ``release-MAJOR.MINOR.PATCH`` branch targeting ``MAJOR.MINOR.x``.
|
||||
|
||||
|
||||
Releasing
|
||||
~~~~~~~~~
|
||||
|
||||
Both automatic and manual processes described above follow the same steps from this point onward.
|
||||
|
||||
#. After all tests pass and the PR has been approved, tag the release commit
|
||||
in the ``MAJOR.MINOR.x`` branch and push it. This will publish to PyPI::
|
||||
|
||||
git tag MAJOR.MINOR.PATCH
|
||||
git push git@github.com:pytest-dev/pytest.git MAJOR.MINOR.PATCH
|
||||
|
||||
Wait for the deploy to complete, then make sure it is `available on PyPI <https://pypi.org/project/pytest>`_.
|
||||
|
||||
#. Merge the PR.
|
||||
|
||||
#. Cherry-pick the CHANGELOG / announce files to the ``master`` branch::
|
||||
|
||||
git fetch --all --prune
|
||||
git checkout origin/master -b cherry-pick-release
|
||||
git cherry-pick --no-commit -m1 origin/MAJOR.MINOR.x
|
||||
git checkout origin/master -- changelog
|
||||
git commit # no arguments
|
||||
|
||||
#. Send an email announcement with the contents from::
|
||||
|
||||
doc/en/announce/release-<VERSION>.rst
|
||||
|
||||
To the following mailing lists:
|
||||
|
||||
* pytest-dev@python.org (all releases)
|
||||
* python-announce-list@python.org (all releases)
|
||||
* testing-in-python@lists.idyll.org (only major/minor releases)
|
||||
|
||||
And announce it on `Twitter <https://twitter.com/>`_ with the ``#pytest`` hashtag.
|
||||
@@ -1,80 +0,0 @@
|
||||
trigger:
|
||||
- master
|
||||
- features
|
||||
|
||||
variables:
|
||||
PYTEST_ADDOPTS: "--junitxml=build/test-results/$(tox.env).xml -vv"
|
||||
PYTEST_COVERAGE: '0'
|
||||
|
||||
jobs:
|
||||
|
||||
- job: 'Test'
|
||||
pool:
|
||||
vmImage: "vs2017-win2016"
|
||||
strategy:
|
||||
matrix:
|
||||
# -- pypy3 disabled for now: #5279 --
|
||||
# pypy3:
|
||||
# python.version: 'pypy3'
|
||||
# tox.env: 'pypy3'
|
||||
py35-xdist:
|
||||
python.version: '3.5'
|
||||
tox.env: 'py35-xdist'
|
||||
# Coverage for:
|
||||
# - test_supports_breakpoint_module_global
|
||||
PYTEST_COVERAGE: '1'
|
||||
py36-xdist:
|
||||
python.version: '3.6'
|
||||
tox.env: 'py36-xdist'
|
||||
py37:
|
||||
python.version: '3.7'
|
||||
tox.env: 'py37-twisted-numpy'
|
||||
# Coverage for:
|
||||
# - _py36_windowsconsoleio_workaround (with py36+)
|
||||
# - test_request_garbage (no xdist)
|
||||
PYTEST_COVERAGE: '1'
|
||||
py37-linting/docs/doctesting:
|
||||
python.version: '3.7'
|
||||
tox.env: 'linting,docs,doctesting'
|
||||
py37-pluggymaster-xdist:
|
||||
python.version: '3.7'
|
||||
tox.env: 'py37-pluggymaster-xdist'
|
||||
py38-xdist:
|
||||
python.version: '3.8'
|
||||
tox.env: 'py38-xdist'
|
||||
maxParallel: 10
|
||||
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '$(python.version)'
|
||||
architecture: 'x64'
|
||||
|
||||
- script: python -m pip install --upgrade pip && python -m pip install tox
|
||||
displayName: 'Install tox'
|
||||
|
||||
- bash: |
|
||||
if [[ "$PYTEST_COVERAGE" == "1" ]]; then
|
||||
export _PYTEST_TOX_COVERAGE_RUN="coverage run -m"
|
||||
export _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess
|
||||
export COVERAGE_FILE="$PWD/.coverage"
|
||||
export COVERAGE_PROCESS_START="$PWD/.coveragerc"
|
||||
fi
|
||||
python -m tox -e $(tox.env)
|
||||
displayName: 'Run tests'
|
||||
|
||||
- task: PublishTestResults@2
|
||||
inputs:
|
||||
testResultsFiles: 'build/test-results/$(tox.env).xml'
|
||||
testRunTitle: '$(tox.env)'
|
||||
condition: succeededOrFailed()
|
||||
|
||||
- bash: |
|
||||
if [[ "$PYTEST_COVERAGE" == 1 ]]; then
|
||||
scripts/report-coverage.sh
|
||||
fi
|
||||
env:
|
||||
CODECOV_NAME: $(tox.env)
|
||||
CODECOV_TOKEN: $(CODECOV_TOKEN)
|
||||
displayName: Report and upload coverage
|
||||
condition: eq(variables['PYTEST_COVERAGE'], '1')
|
||||
@@ -15,10 +15,10 @@ Each file should be named like ``<ISSUE>.<TYPE>.rst``, where
|
||||
|
||||
* ``feature``: new user facing features, like new command-line options and new behavior.
|
||||
* ``improvement``: improvement of existing functionality, usually without requiring user intervention (for example, new fields being written in ``--junitxml``, improved colors in terminal, etc).
|
||||
* ``bugfix``: fixes a reported bug.
|
||||
* ``bugfix``: fixes a bug.
|
||||
* ``doc``: documentation improvement, like rewording an entire session or adding missing docs.
|
||||
* ``deprecation``: feature deprecation.
|
||||
* ``removal``: feature removal.
|
||||
* ``breaking``: a change which may break existing suites, such as feature removal or behavior change.
|
||||
* ``vendor``: changes in packages vendored in pytest.
|
||||
* ``trivial``: fixing a small typo or internal change that might be noteworthy.
|
||||
|
||||
@@ -34,4 +34,4 @@ If you are not sure what issue type to use, don't hesitate to ask in your PR.
|
||||
other than ``features`` it is usually better to stick to a single paragraph to keep it concise.
|
||||
|
||||
You can also run ``tox -e docs`` to build the documentation
|
||||
with the draft changelog (``doc/en/_build/changelog.html``) if you want to get a preview of how your change will look in the final release notes.
|
||||
with the draft changelog (``doc/en/_build/html/changelog.html``) if you want to get a preview of how your change will look in the final release notes.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# reference: https://docs.codecov.io/docs/codecovyml-reference
|
||||
coverage:
|
||||
status:
|
||||
project: true
|
||||
patch: true
|
||||
changes: true
|
||||
|
||||
comment: off
|
||||
project: false
|
||||
comment: false
|
||||
|
||||
150
doc/en/Makefile
150
doc/en/Makefile
@@ -1,16 +1,24 @@
|
||||
# Makefile for Sphinx documentation
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
# You can set these variables from the command line, and also
|
||||
# from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
|
||||
REGENDOC_ARGS := \
|
||||
--normalize "/[ \t]+\n/\n/" \
|
||||
@@ -25,130 +33,8 @@ REGENDOC_ARGS := \
|
||||
--normalize "@hypothesis-(\d+)\\.[.\d,]+@hypothesis-\1.x.y@" \
|
||||
--normalize "@Python (\d+)\\.[^ ,]+@Python \1.x.y@"
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
|
||||
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " showtarget to show the pytest.org target directory"
|
||||
@echo " install to install docs to pytest.org/SITETARGET"
|
||||
@echo " install-ldf to install the doc pdf to pytest.org/SITETARGET"
|
||||
@echo " regen to regenerate pytest examples using the installed pytest"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
|
||||
clean:
|
||||
-rm -rf $(BUILDDIR)/*
|
||||
|
||||
regen: REGENDOC_FILES:=*.rst */*.rst
|
||||
regen:
|
||||
PYTHONDONTWRITEBYTECODE=1 PYTEST_ADDOPTS="-pno:hypothesis -Wignore::pytest.PytestUnknownMarkWarning" COLUMNS=76 regendoc --update ${REGENDOC_FILES} ${REGENDOC_ARGS}
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pytest.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pytest.qhc"
|
||||
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/pytest"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pytest"
|
||||
@echo "# devhelp"
|
||||
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
make -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
||||
|
||||
texinfo:
|
||||
mkdir -p $(BUILDDIR)/texinfo
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
info:
|
||||
mkdir -p $(BUILDDIR)/texinfo
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
.PHONY: regen
|
||||
|
||||
@@ -21,3 +21,7 @@
|
||||
<hr>
|
||||
{{ toc }}
|
||||
{%- endif %}
|
||||
|
||||
<hr>
|
||||
<a href="{{ pathto('genindex') }}">Index</a>
|
||||
<hr>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{% extends "!layout.html" %}
|
||||
{% block header %}
|
||||
{{super()}}
|
||||
{% endblock %}
|
||||
{% block footer %}
|
||||
{{ super() }}
|
||||
<script type="text/javascript">
|
||||
|
||||
var _gaq = _gaq || [];
|
||||
_gaq.push(['_setAccount', 'UA-7597274-13']);
|
||||
_gaq.push(['_trackPageview']);
|
||||
|
||||
(function() {
|
||||
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
|
||||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
||||
})();
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -6,6 +6,12 @@ Release announcements
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
release-5.4.3
|
||||
release-5.4.2
|
||||
release-5.4.1
|
||||
release-5.4.0
|
||||
release-5.3.5
|
||||
release-5.3.4
|
||||
release-5.3.3
|
||||
release-5.3.2
|
||||
release-5.3.1
|
||||
|
||||
20
doc/en/announce/release-5.3.4.rst
Normal file
20
doc/en/announce/release-5.3.4.rst
Normal file
@@ -0,0 +1,20 @@
|
||||
pytest-5.3.4
|
||||
=======================================
|
||||
|
||||
pytest 5.3.4 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Bruno Oliveira
|
||||
* Daniel Hahler
|
||||
* Ran Benita
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
19
doc/en/announce/release-5.3.5.rst
Normal file
19
doc/en/announce/release-5.3.5.rst
Normal file
@@ -0,0 +1,19 @@
|
||||
pytest-5.3.5
|
||||
=======================================
|
||||
|
||||
pytest 5.3.5 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Daniel Hahler
|
||||
* Ran Benita
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
59
doc/en/announce/release-5.4.0.rst
Normal file
59
doc/en/announce/release-5.4.0.rst
Normal file
@@ -0,0 +1,59 @@
|
||||
pytest-5.4.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 5.4.0 release!
|
||||
|
||||
pytest is a mature Python testing tool with more than a 2000 tests
|
||||
against itself, passing on many different interpreters and platforms.
|
||||
|
||||
This release contains a number of bug fixes and improvements, so users are encouraged
|
||||
to take a look at the CHANGELOG:
|
||||
|
||||
https://docs.pytest.org/en/latest/changelog.html
|
||||
|
||||
For complete documentation, please visit:
|
||||
|
||||
https://docs.pytest.org/en/latest/
|
||||
|
||||
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
|
||||
* Christoph Buelter
|
||||
* Christoph Bülter
|
||||
* Daniel Arndt
|
||||
* Daniel Hahler
|
||||
* Holger Kohr
|
||||
* Hugo
|
||||
* Hugo van Kemenade
|
||||
* Jakub Mitoraj
|
||||
* Kyle Altendorf
|
||||
* Minuddin Ahmed Rana
|
||||
* Nathaniel Compton
|
||||
* ParetoLife
|
||||
* Pauli Virtanen
|
||||
* Philipp Loose
|
||||
* Ran Benita
|
||||
* Ronny Pfannschmidt
|
||||
* Stefan Scherfke
|
||||
* Stefano Mazzucco
|
||||
* TWood67
|
||||
* Tobias Schmidt
|
||||
* Tomáš Gavenčiak
|
||||
* Vinay Calastry
|
||||
* Vladyslav Rachek
|
||||
* Zac Hatfield-Dodds
|
||||
* captainCapitalism
|
||||
* cmachalo
|
||||
* gftea
|
||||
* kpinc
|
||||
* rebecca-palmer
|
||||
* sdementen
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
18
doc/en/announce/release-5.4.1.rst
Normal file
18
doc/en/announce/release-5.4.1.rst
Normal file
@@ -0,0 +1,18 @@
|
||||
pytest-5.4.1
|
||||
=======================================
|
||||
|
||||
pytest 5.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 https://docs.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Bruno Oliveira
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
22
doc/en/announce/release-5.4.2.rst
Normal file
22
doc/en/announce/release-5.4.2.rst
Normal file
@@ -0,0 +1,22 @@
|
||||
pytest-5.4.2
|
||||
=======================================
|
||||
|
||||
pytest 5.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 https://docs.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* Daniel Hahler
|
||||
* Ran Benita
|
||||
* Ronny Pfannschmidt
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
21
doc/en/announce/release-5.4.3.rst
Normal file
21
doc/en/announce/release-5.4.3.rst
Normal file
@@ -0,0 +1,21 @@
|
||||
pytest-5.4.3
|
||||
=======================================
|
||||
|
||||
pytest 5.4.3 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* Ran Benita
|
||||
* Tor Colvin
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
@@ -47,6 +47,8 @@ you will see the return value of the function call:
|
||||
E + where 3 = f()
|
||||
|
||||
test_assert1.py:6: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_assert1.py::test_function - assert 3 == 4
|
||||
============================ 1 failed in 0.12s =============================
|
||||
|
||||
``pytest`` has support for showing the values of the most common subexpressions
|
||||
@@ -208,6 +210,8 @@ if you run this module:
|
||||
E Use -v to get the full diff
|
||||
|
||||
test_assert2.py:6: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_assert2.py::test_set_comparison - AssertionError: assert {'0'...
|
||||
============================ 1 failed in 0.12s =============================
|
||||
|
||||
Special comparisons are done for a number of cases:
|
||||
@@ -279,6 +283,8 @@ the conftest file:
|
||||
E vals: 1 != 2
|
||||
|
||||
test_foocompare.py:12: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_foocompare.py::test_compare - assert Comparing Foo instances:
|
||||
1 failed in 0.12s
|
||||
|
||||
.. _assert-details:
|
||||
|
||||
@@ -3,6 +3,61 @@
|
||||
Backwards Compatibility Policy
|
||||
==============================
|
||||
|
||||
.. versionadded: 6.0
|
||||
|
||||
pytest is actively evolving and is a project that has been decades in the making,
|
||||
we keep learning about new and better structures to express different details about testing.
|
||||
|
||||
While we implement those modifications we try to ensure an easy transition and don't want to impose unnecessary churn on our users and community/plugin authors.
|
||||
|
||||
As of now, pytest considers multipe types of backward compatibility transitions:
|
||||
|
||||
a) trivial: APIs which trivially translate to the new mechanism,
|
||||
and do not cause problematic changes.
|
||||
|
||||
We try to support those indefinitely while encouraging users to switch to newer/better mechanisms through documentation.
|
||||
|
||||
b) transitional: the old and new API don't conflict
|
||||
and we can help users transition by using warnings, while supporting both for a prolonged time.
|
||||
|
||||
We will only start the removal of deprecated functionality in major releases (e.g. if we deprecate something in 3.0 we will start to 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 start to remove it in 5.0, not in 4.0).
|
||||
|
||||
When the deprecation expires (e.g. 4.0 is released), we won't remove the deprecated functionality immediately, but will use the standard warning filters to turn them into **errors** by default. This approach makes it explicit that removal is imminent, and still gives you time to turn the deprecated feature into a warning instead of an error so it can be dealt with in your own time. In the next minor release (e.g. 4.1), the feature will be effectively removed.
|
||||
|
||||
|
||||
c) true breakage: should only to be considered when normal transition is unreasonably unsustainable and would offset important development/features by years.
|
||||
In addition, they should be limited to APIs where the number of actual users is very small (for example only impacting some plugins), and can be coordinated with the community in advance.
|
||||
|
||||
Examples for such upcoming changes:
|
||||
|
||||
* removal of ``pytest_runtest_protocol/nextitem`` - `#895`_
|
||||
* rearranging of the node tree to include ``FunctionDefinition``
|
||||
* rearranging of ``SetupState`` `#895`_
|
||||
|
||||
True breakages must be announced first in an issue containing:
|
||||
|
||||
* Detailed description of the change
|
||||
* Rationale
|
||||
* Expected impact on users and plugin authors (example in `#895`_)
|
||||
|
||||
After there's no hard *-1* on the issue it should be followed up by an initial proof-of-concept Pull Request.
|
||||
|
||||
This POC serves as both a coordination point to assess impact and potential inspriation to come up with a transitional solution after all.
|
||||
|
||||
After a reasonable amount of time the PR can be merged to base a new major release.
|
||||
|
||||
For the PR to mature from POC to acceptance, it must contain:
|
||||
* Setup of deprecation errors/warnings that help users fix and port their code. If it is possible to introduce a deprecation period under the current series, before the true breakage, it should be introduced in a separate PR and be part of the current release stream.
|
||||
* Detailed description of the rationale and examples on how to port code in ``doc/en/deprecations.rst``.
|
||||
|
||||
|
||||
History
|
||||
=========
|
||||
|
||||
|
||||
Focus primary on smooth transition - stance (pre 6.0)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Keeping backwards compatibility has a very high priority in the pytest project. Although we have deprecated functionality over the years, most of it is still supported. All deprecations in pytest were done because simpler or more efficient ways of accomplishing the same tasks have emerged, making the old way of doing things unnecessary.
|
||||
|
||||
With the pytest 3.0 release we introduced a clear communication scheme for when we will actually remove the old busted joint and politely ask you to use the new hotness instead, while giving you enough time to adjust your tests or raise concerns if there are valid reasons to keep deprecated functionality around.
|
||||
@@ -20,3 +75,6 @@ Deprecation Roadmap
|
||||
Features currently deprecated and removed in previous releases can be found in :ref:`deprecations`.
|
||||
|
||||
We track future 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.
|
||||
|
||||
|
||||
.. _`#895`: https://github.com/pytest-dev/pytest/issues/895
|
||||
|
||||
@@ -137,9 +137,11 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
tmpdir_factory [session scope]
|
||||
Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session.
|
||||
|
||||
|
||||
tmp_path_factory [session scope]
|
||||
Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session.
|
||||
|
||||
|
||||
tmpdir
|
||||
Return a temporary directory path object
|
||||
which is unique to each test function invocation,
|
||||
|
||||
@@ -75,6 +75,9 @@ If you run this for the first time you will see two failures:
|
||||
E Failed: bad luck
|
||||
|
||||
test_50.py:7: Failed
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_50.py::test_num[17] - Failed: bad luck
|
||||
FAILED test_50.py::test_num[25] - Failed: bad luck
|
||||
2 failed, 48 passed in 0.12s
|
||||
|
||||
If you then run it with ``--lf``:
|
||||
@@ -86,7 +89,7 @@ If you then run it with ``--lf``:
|
||||
platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y
|
||||
cachedir: $PYTHON_PREFIX/.pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR
|
||||
collected 50 items / 48 deselected / 2 selected
|
||||
collected 2 items
|
||||
run-last-failure: rerun previous 2 failures
|
||||
|
||||
test_50.py FF [100%]
|
||||
@@ -114,7 +117,10 @@ If you then run it with ``--lf``:
|
||||
E Failed: bad luck
|
||||
|
||||
test_50.py:7: Failed
|
||||
===================== 2 failed, 48 deselected in 0.12s =====================
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_50.py::test_num[17] - Failed: bad luck
|
||||
FAILED test_50.py::test_num[25] - Failed: bad luck
|
||||
============================ 2 failed in 0.12s =============================
|
||||
|
||||
You have run only the two failing tests from the last run, while the 48 passing
|
||||
tests have not been run ("deselected").
|
||||
@@ -158,6 +164,9 @@ of ``FF`` and dots):
|
||||
E Failed: bad luck
|
||||
|
||||
test_50.py:7: Failed
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_50.py::test_num[17] - Failed: bad luck
|
||||
FAILED test_50.py::test_num[25] - Failed: bad luck
|
||||
======================= 2 failed, 48 passed in 0.12s =======================
|
||||
|
||||
.. _`config.cache`:
|
||||
@@ -230,6 +239,8 @@ If you run this command for the first time, you can see the print statement:
|
||||
test_caching.py:20: AssertionError
|
||||
-------------------------- Captured stdout setup ---------------------------
|
||||
running expensive computation...
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_caching.py::test_function - assert 42 == 23
|
||||
1 failed in 0.12s
|
||||
|
||||
If you run it a second time, the value will be retrieved from
|
||||
@@ -249,9 +260,11 @@ the cache and nothing will be printed:
|
||||
E assert 42 == 23
|
||||
|
||||
test_caching.py:20: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_caching.py::test_function - assert 42 == 23
|
||||
1 failed in 0.12s
|
||||
|
||||
See the :ref:`cache-api` for more details.
|
||||
See the :fixture:`config.cache fixture <config.cache>` for more details.
|
||||
|
||||
|
||||
Inspecting Cache content
|
||||
|
||||
@@ -21,27 +21,36 @@ file descriptors. This allows to capture output from simple
|
||||
print statements as well as output from a subprocess started by
|
||||
a test.
|
||||
|
||||
.. _capture-method:
|
||||
|
||||
Setting capturing methods or disabling capturing
|
||||
-------------------------------------------------
|
||||
|
||||
There are two ways in which ``pytest`` can perform capturing:
|
||||
There are three ways in which ``pytest`` can perform capturing:
|
||||
|
||||
* file descriptor (FD) level capturing (default): All writes going to the
|
||||
* ``fd`` (file descriptor) level capturing (default): All writes going to the
|
||||
operating system file descriptors 1 and 2 will be captured.
|
||||
|
||||
* ``sys`` level capturing: Only writes to Python files ``sys.stdout``
|
||||
and ``sys.stderr`` will be captured. No capturing of writes to
|
||||
filedescriptors is performed.
|
||||
|
||||
* ``tee-sys`` capturing: Python writes to ``sys.stdout`` and ``sys.stderr``
|
||||
will be captured, however the writes will also be passed-through to
|
||||
the actual ``sys.stdout`` and ``sys.stderr``. This allows output to be
|
||||
'live printed' and captured for plugin use, such as junitxml (new in pytest 5.4).
|
||||
|
||||
.. _`disable capturing`:
|
||||
|
||||
You can influence output capturing mechanisms from the command line:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pytest -s # disable all capturing
|
||||
pytest --capture=sys # replace sys.stdout/stderr with in-mem files
|
||||
pytest --capture=fd # also point filedescriptors 1 and 2 to temp file
|
||||
pytest -s # disable all capturing
|
||||
pytest --capture=sys # replace sys.stdout/stderr with in-mem files
|
||||
pytest --capture=fd # also point filedescriptors 1 and 2 to temp file
|
||||
pytest --capture=tee-sys # combines 'sys' and '-s', capturing sys.stdout/stderr
|
||||
# and passing it along to the actual sys.stdout/stderr
|
||||
|
||||
.. _printdebugging:
|
||||
|
||||
@@ -91,6 +100,8 @@ of the failing function and hide the other one:
|
||||
test_module.py:12: AssertionError
|
||||
-------------------------- Captured stdout setup ---------------------------
|
||||
setting up <function test_func2 at 0xdeadbeef>
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_module.py::test_func2 - assert False
|
||||
======================= 1 failed, 1 passed in 0.12s ========================
|
||||
|
||||
Accessing captured output from a test function
|
||||
@@ -159,5 +170,3 @@ as a context manager, disabling capture inside the ``with`` block:
|
||||
with capsys.disabled():
|
||||
print("output not captured, going directly to sys.stdout")
|
||||
print("this output is also captured")
|
||||
|
||||
.. include:: links.inc
|
||||
|
||||
@@ -28,6 +28,315 @@ with advance notice in the **Deprecations** section of releases.
|
||||
|
||||
.. towncrier release notes start
|
||||
|
||||
pytest 5.4.3 (2020-06-02)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#6428 <https://github.com/pytest-dev/pytest/issues/6428>`_: Paths appearing in error messages are now correct in case the current working directory has
|
||||
changed since the start of the session.
|
||||
|
||||
|
||||
- `#6755 <https://github.com/pytest-dev/pytest/issues/6755>`_: Support deleting paths longer than 260 characters on windows created inside tmpdir.
|
||||
|
||||
|
||||
- `#6956 <https://github.com/pytest-dev/pytest/issues/6956>`_: Prevent pytest from printing ConftestImportFailure traceback to stdout.
|
||||
|
||||
|
||||
- `#7150 <https://github.com/pytest-dev/pytest/issues/7150>`_: Prevent hiding the underlying exception when ``ConfTestImportFailure`` is raised.
|
||||
|
||||
|
||||
- `#7215 <https://github.com/pytest-dev/pytest/issues/7215>`_: Fix regression where running with ``--pdb`` would call the ``tearDown`` methods of ``unittest.TestCase``
|
||||
subclasses for skipped tests.
|
||||
|
||||
|
||||
pytest 5.4.2 (2020-05-08)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#6871 <https://github.com/pytest-dev/pytest/issues/6871>`_: Fix crash with captured output when using the :fixture:`capsysbinary fixture <capsysbinary>`.
|
||||
|
||||
|
||||
- `#6924 <https://github.com/pytest-dev/pytest/issues/6924>`_: Ensure a ``unittest.IsolatedAsyncioTestCase`` is actually awaited.
|
||||
|
||||
|
||||
- `#6925 <https://github.com/pytest-dev/pytest/issues/6925>`_: Fix TerminalRepr instances to be hashable again.
|
||||
|
||||
|
||||
- `#6947 <https://github.com/pytest-dev/pytest/issues/6947>`_: Fix regression where functions registered with ``TestCase.addCleanup`` were not being called on test failures.
|
||||
|
||||
|
||||
- `#6951 <https://github.com/pytest-dev/pytest/issues/6951>`_: Allow users to still set the deprecated ``TerminalReporter.writer`` attribute.
|
||||
|
||||
|
||||
- `#6992 <https://github.com/pytest-dev/pytest/issues/6992>`_: Revert "tmpdir: clean up indirection via config for factories" #6767 as it breaks pytest-xdist.
|
||||
|
||||
|
||||
- `#7110 <https://github.com/pytest-dev/pytest/issues/7110>`_: Fixed regression: ``asyncbase.TestCase`` tests are executed correctly again.
|
||||
|
||||
|
||||
- `#7143 <https://github.com/pytest-dev/pytest/issues/7143>`_: Fix ``File.from_constructor`` so it forwards extra keyword arguments to the constructor.
|
||||
|
||||
|
||||
- `#7145 <https://github.com/pytest-dev/pytest/issues/7145>`_: Classes with broken ``__getattribute__`` methods are displayed correctly during failures.
|
||||
|
||||
|
||||
- `#7180 <https://github.com/pytest-dev/pytest/issues/7180>`_: Fix ``_is_setup_py`` for files encoded differently than locale.
|
||||
|
||||
|
||||
pytest 5.4.1 (2020-03-13)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#6909 <https://github.com/pytest-dev/pytest/issues/6909>`_: Revert the change introduced by `#6330 <https://github.com/pytest-dev/pytest/pull/6330>`_, which required all arguments to ``@pytest.mark.parametrize`` to be explicitly defined in the function signature.
|
||||
|
||||
The intention of the original change was to remove what was expected to be an unintended/surprising behavior, but it turns out many people relied on it, so the restriction has been reverted.
|
||||
|
||||
|
||||
- `#6910 <https://github.com/pytest-dev/pytest/issues/6910>`_: Fix crash when plugins return an unknown stats while using the ``--reportlog`` option.
|
||||
|
||||
|
||||
pytest 5.4.0 (2020-03-12)
|
||||
=========================
|
||||
|
||||
Breaking Changes
|
||||
----------------
|
||||
|
||||
- `#6316 <https://github.com/pytest-dev/pytest/issues/6316>`_: Matching of ``-k EXPRESSION`` to test names is now case-insensitive.
|
||||
|
||||
|
||||
- `#6443 <https://github.com/pytest-dev/pytest/issues/6443>`_: Plugins specified with ``-p`` are now loaded after internal plugins, which results in their hooks being called *before* the internal ones.
|
||||
|
||||
This makes the ``-p`` behavior consistent with ``PYTEST_PLUGINS``.
|
||||
|
||||
|
||||
- `#6637 <https://github.com/pytest-dev/pytest/issues/6637>`_: Removed the long-deprecated ``pytest_itemstart`` hook.
|
||||
|
||||
This hook has been marked as deprecated and not been even called by pytest for over 10 years now.
|
||||
|
||||
|
||||
- `#6673 <https://github.com/pytest-dev/pytest/issues/6673>`_: Reversed / fix meaning of "+/-" in error diffs. "-" means that sth. expected is missing in the result and "+" means that there are unexpected extras in the result.
|
||||
|
||||
|
||||
- `#6737 <https://github.com/pytest-dev/pytest/issues/6737>`_: The ``cached_result`` attribute of ``FixtureDef`` is now set to ``None`` when
|
||||
the result is unavailable, instead of being deleted.
|
||||
|
||||
If your plugin performs checks like ``hasattr(fixturedef, 'cached_result')``,
|
||||
for example in a ``pytest_fixture_post_finalizer`` hook implementation, replace
|
||||
it with ``fixturedef.cached_result is not None``. If you ``del`` the attribute,
|
||||
set it to ``None`` instead.
|
||||
|
||||
|
||||
|
||||
Deprecations
|
||||
------------
|
||||
|
||||
- `#3238 <https://github.com/pytest-dev/pytest/issues/3238>`_: Option ``--no-print-logs`` is deprecated and meant to be removed in a future release. If you use ``--no-print-logs``, please try out ``--show-capture`` and
|
||||
provide feedback.
|
||||
|
||||
``--show-capture`` command-line option was added in ``pytest 3.5.0`` and allows to specify how to
|
||||
display captured output when tests fail: ``no``, ``stdout``, ``stderr``, ``log`` or ``all`` (the default).
|
||||
|
||||
|
||||
- `#571 <https://github.com/pytest-dev/pytest/issues/571>`_: Deprecate the unused/broken `pytest_collect_directory` hook.
|
||||
It was misaligned since the removal of the ``Directory`` collector in 2010
|
||||
and incorrect/unusable as soon as collection was split from test execution.
|
||||
|
||||
|
||||
- `#5975 <https://github.com/pytest-dev/pytest/issues/5975>`_: Deprecate using direct constructors for ``Nodes``.
|
||||
|
||||
Instead they are new constructed via ``Node.from_parent``.
|
||||
|
||||
This transitional mechanism enables us to detangle the very intensely
|
||||
entangled ``Node`` relationships by enforcing more controlled creation/configruation patterns.
|
||||
|
||||
As part of that session/config are already disallowed parameters and as we work on the details we might need disallow a few more as well.
|
||||
|
||||
Subclasses are expected to use `super().from_parent` if they intend to expand the creation of `Nodes`.
|
||||
|
||||
|
||||
- `#6779 <https://github.com/pytest-dev/pytest/issues/6779>`_: The ``TerminalReporter.writer`` attribute has been deprecated and should no longer be used. This
|
||||
was inadvertently exposed as part of the public API of that plugin and ties it too much
|
||||
with ``py.io.TerminalWriter``.
|
||||
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#4597 <https://github.com/pytest-dev/pytest/issues/4597>`_: New :ref:`--capture=tee-sys <capture-method>` option to allow both live printing and capturing of test output.
|
||||
|
||||
|
||||
- `#5712 <https://github.com/pytest-dev/pytest/issues/5712>`_: Now all arguments to ``@pytest.mark.parametrize`` need to be explicitly declared in the function signature or via ``indirect``.
|
||||
Previously it was possible to omit an argument if a fixture with the same name existed, which was just an accident of implementation and was not meant to be a part of the API.
|
||||
|
||||
|
||||
- `#6454 <https://github.com/pytest-dev/pytest/issues/6454>`_: Changed default for `-r` to `fE`, which displays failures and errors in the :ref:`short test summary <pytest.detailed_failed_tests_usage>`. `-rN` can be used to disable it (the old behavior).
|
||||
|
||||
|
||||
- `#6469 <https://github.com/pytest-dev/pytest/issues/6469>`_: New options have been added to the :confval:`junit_logging` option: ``log``, ``out-err``, and ``all``.
|
||||
|
||||
|
||||
- `#6834 <https://github.com/pytest-dev/pytest/issues/6834>`_: Excess warning summaries are now collapsed per file to ensure readable display of warning summaries.
|
||||
|
||||
|
||||
|
||||
Improvements
|
||||
------------
|
||||
|
||||
- `#1857 <https://github.com/pytest-dev/pytest/issues/1857>`_: ``pytest.mark.parametrize`` accepts integers for ``ids`` again, converting it to strings.
|
||||
|
||||
|
||||
- `#449 <https://github.com/pytest-dev/pytest/issues/449>`_: Use "yellow" main color with any XPASSED tests.
|
||||
|
||||
|
||||
- `#4639 <https://github.com/pytest-dev/pytest/issues/4639>`_: Revert "A warning is now issued when assertions are made for ``None``".
|
||||
|
||||
The warning proved to be less useful than initially expected and had quite a
|
||||
few false positive cases.
|
||||
|
||||
|
||||
- `#5686 <https://github.com/pytest-dev/pytest/issues/5686>`_: ``tmpdir_factory.mktemp`` now fails when given absolute and non-normalized paths.
|
||||
|
||||
|
||||
- `#5984 <https://github.com/pytest-dev/pytest/issues/5984>`_: The ``pytest_warning_captured`` hook now receives a ``location`` parameter with the code location that generated the warning.
|
||||
|
||||
|
||||
- `#6213 <https://github.com/pytest-dev/pytest/issues/6213>`_: pytester: the ``testdir`` fixture respects environment settings from the ``monkeypatch`` fixture for inner runs.
|
||||
|
||||
|
||||
- `#6247 <https://github.com/pytest-dev/pytest/issues/6247>`_: ``--fulltrace`` is honored with collection errors.
|
||||
|
||||
|
||||
- `#6384 <https://github.com/pytest-dev/pytest/issues/6384>`_: Make `--showlocals` work also with `--tb=short`.
|
||||
|
||||
|
||||
- `#6653 <https://github.com/pytest-dev/pytest/issues/6653>`_: Add support for matching lines consecutively with :attr:`LineMatcher <_pytest.pytester.LineMatcher>`'s :func:`~_pytest.pytester.LineMatcher.fnmatch_lines` and :func:`~_pytest.pytester.LineMatcher.re_match_lines`.
|
||||
|
||||
|
||||
- `#6658 <https://github.com/pytest-dev/pytest/issues/6658>`_: Code is now highlighted in tracebacks when ``pygments`` is installed.
|
||||
|
||||
Users are encouraged to install ``pygments`` into their environment and provide feedback, because
|
||||
the plan is to make ``pygments`` a regular dependency in the future.
|
||||
|
||||
|
||||
- `#6795 <https://github.com/pytest-dev/pytest/issues/6795>`_: Import usage error message with invalid `-o` option.
|
||||
|
||||
|
||||
- `#759 <https://github.com/pytest-dev/pytest/issues/759>`_: ``pytest.mark.parametrize`` supports iterators and generators for ``ids``.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#310 <https://github.com/pytest-dev/pytest/issues/310>`_: Add support for calling `pytest.xfail()` and `pytest.importorskip()` with doctests.
|
||||
|
||||
|
||||
- `#3823 <https://github.com/pytest-dev/pytest/issues/3823>`_: ``--trace`` now works with unittests.
|
||||
|
||||
|
||||
- `#4445 <https://github.com/pytest-dev/pytest/issues/4445>`_: Fixed some warning reports produced by pytest to point to the correct location of the warning in the user's code.
|
||||
|
||||
|
||||
- `#5301 <https://github.com/pytest-dev/pytest/issues/5301>`_: Fix ``--last-failed`` to collect new tests from files with known failures.
|
||||
|
||||
|
||||
- `#5928 <https://github.com/pytest-dev/pytest/issues/5928>`_: Report ``PytestUnknownMarkWarning`` at the level of the user's code, not ``pytest``'s.
|
||||
|
||||
|
||||
- `#5991 <https://github.com/pytest-dev/pytest/issues/5991>`_: Fix interaction with ``--pdb`` and unittests: do not use unittest's ``TestCase.debug()``.
|
||||
|
||||
|
||||
- `#6334 <https://github.com/pytest-dev/pytest/issues/6334>`_: Fix summary entries appearing twice when ``f/F`` and ``s/S`` report chars were used at the same time in the ``-r`` command-line option (for example ``-rFf``).
|
||||
|
||||
The upper case variants were never documented and the preferred form should be the lower case.
|
||||
|
||||
|
||||
- `#6409 <https://github.com/pytest-dev/pytest/issues/6409>`_: Fallback to green (instead of yellow) for non-last items without previous passes with colored terminal progress indicator.
|
||||
|
||||
|
||||
- `#6454 <https://github.com/pytest-dev/pytest/issues/6454>`_: `--disable-warnings` is honored with `-ra` and `-rA`.
|
||||
|
||||
|
||||
- `#6497 <https://github.com/pytest-dev/pytest/issues/6497>`_: Fix bug in the comparison of request key with cached key in fixture.
|
||||
|
||||
A construct ``if key == cached_key:`` can fail either because ``==`` is explicitly disallowed, or for, e.g., NumPy arrays, where the result of ``a == b`` cannot generally be converted to `bool`.
|
||||
The implemented fix replaces `==` with ``is``.
|
||||
|
||||
|
||||
- `#6557 <https://github.com/pytest-dev/pytest/issues/6557>`_: Make capture output streams ``.write()`` method return the same return value from original streams.
|
||||
|
||||
|
||||
- `#6566 <https://github.com/pytest-dev/pytest/issues/6566>`_: Fix ``EncodedFile.writelines`` to call the underlying buffer's ``writelines`` method.
|
||||
|
||||
|
||||
- `#6575 <https://github.com/pytest-dev/pytest/issues/6575>`_: Fix internal crash when ``faulthandler`` starts initialized
|
||||
(for example with ``PYTHONFAULTHANDLER=1`` environment variable set) and ``faulthandler_timeout`` defined
|
||||
in the configuration file.
|
||||
|
||||
|
||||
- `#6597 <https://github.com/pytest-dev/pytest/issues/6597>`_: Fix node ids which contain a parametrized empty-string variable.
|
||||
|
||||
|
||||
- `#6646 <https://github.com/pytest-dev/pytest/issues/6646>`_: Assertion rewriting hooks are (re)stored for the current item, which fixes them being still used after e.g. pytester's :func:`testdir.runpytest <_pytest.pytester.Testdir.runpytest>` etc.
|
||||
|
||||
|
||||
- `#6660 <https://github.com/pytest-dev/pytest/issues/6660>`_: :func:`pytest.exit() <_pytest.outcomes.exit>` is handled when emitted from the :func:`pytest_sessionfinish <_pytest.hookspec.pytest_sessionfinish>` hook. This includes quitting from a debugger.
|
||||
|
||||
|
||||
- `#6752 <https://github.com/pytest-dev/pytest/issues/6752>`_: When :py:func:`pytest.raises` is used as a function (as opposed to a context manager),
|
||||
a `match` keyword argument is now passed through to the tested function. Previously
|
||||
it was swallowed and ignored (regression in pytest 5.1.0).
|
||||
|
||||
|
||||
- `#6801 <https://github.com/pytest-dev/pytest/issues/6801>`_: Do not display empty lines inbetween traceback for unexpected exceptions with doctests.
|
||||
|
||||
|
||||
- `#6802 <https://github.com/pytest-dev/pytest/issues/6802>`_: The :fixture:`testdir fixture <testdir>` works within doctests now.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#6696 <https://github.com/pytest-dev/pytest/issues/6696>`_: Add list of fixtures to start of fixture chapter.
|
||||
|
||||
|
||||
- `#6742 <https://github.com/pytest-dev/pytest/issues/6742>`_: Expand first sentence on fixtures into a paragraph.
|
||||
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#6404 <https://github.com/pytest-dev/pytest/issues/6404>`_: Remove usage of ``parser`` module, deprecated in Python 3.9.
|
||||
|
||||
|
||||
pytest 5.3.5 (2020-01-29)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#6517 <https://github.com/pytest-dev/pytest/issues/6517>`_: Fix regression in pytest 5.3.4 causing an INTERNALERROR due to a wrong assertion.
|
||||
|
||||
|
||||
pytest 5.3.4 (2020-01-20)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#6496 <https://github.com/pytest-dev/pytest/issues/6496>`_: Revert `#6436 <https://github.com/pytest-dev/pytest/issues/6436>`__: unfortunately this change has caused a number of regressions in many suites,
|
||||
so the team decided to revert this change and make a new release while we continue to look for a solution.
|
||||
|
||||
|
||||
pytest 5.3.3 (2020-01-16)
|
||||
=========================
|
||||
|
||||
@@ -161,7 +470,7 @@ Features
|
||||
rather than implicitly.
|
||||
|
||||
|
||||
- `#5914 <https://github.com/pytest-dev/pytest/issues/5914>`_: :ref:`testdir` learned two new functions, :py:func:`~_pytest.pytester.LineMatcher.no_fnmatch_line` and
|
||||
- `#5914 <https://github.com/pytest-dev/pytest/issues/5914>`_: :fixture:`testdir` learned two new functions, :py:func:`~_pytest.pytester.LineMatcher.no_fnmatch_line` and
|
||||
:py:func:`~_pytest.pytester.LineMatcher.no_re_match_line`.
|
||||
|
||||
The functions are used to ensure the captured text *does not* match the given
|
||||
@@ -2347,7 +2656,7 @@ Deprecations and Removals
|
||||
- `#4036 <https://github.com/pytest-dev/pytest/issues/4036>`_: The ``item`` parameter of ``pytest_warning_captured`` hook is now documented as deprecated. We realized only after
|
||||
the ``3.8`` release that this parameter is incompatible with ``pytest-xdist``.
|
||||
|
||||
Our policy is to not deprecate features during bugfix releases, but in this case we believe it makes sense as we are
|
||||
Our policy is to not deprecate features during bug-fix releases, but in this case we believe it makes sense as we are
|
||||
only documenting it as deprecated, without issuing warnings which might potentially break test suites. This will get
|
||||
the word out that hook implementers should not use this parameter at all.
|
||||
|
||||
@@ -5370,7 +5679,7 @@ time or change existing behaviors in order to make them less surprising/more use
|
||||
Thanks Daniel Grunwald for the report and Bruno Oliveira for the PR.
|
||||
|
||||
- (experimental) adapt more SEMVER style versioning and change meaning of
|
||||
master branch in git repo: "master" branch now keeps the bugfixes, changes
|
||||
master branch in git repo: "master" branch now keeps the bug fixes, changes
|
||||
aimed for micro releases. "features" branch will only be released
|
||||
with minor or major pytest releases.
|
||||
|
||||
|
||||
@@ -83,7 +83,6 @@ copyright = "2015–2020, holger krekel and pytest-dev team"
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = [
|
||||
"links.inc",
|
||||
"_build",
|
||||
"naming20.rst",
|
||||
"test/*",
|
||||
@@ -162,7 +161,7 @@ html_logo = "img/pytest1.png"
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
html_favicon = "img/pytest1favi.ico"
|
||||
html_favicon = "img/favicon.png"
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
@@ -208,7 +207,7 @@ html_sidebars = {
|
||||
html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
html_use_index = False
|
||||
html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
# html_split_index = False
|
||||
@@ -373,10 +372,18 @@ def configure_logging(app: "sphinx.application.Sphinx") -> None:
|
||||
def setup(app: "sphinx.application.Sphinx") -> None:
|
||||
# from sphinx.ext.autodoc import cut_lines
|
||||
# app.connect('autodoc-process-docstring', cut_lines(4, what=['module']))
|
||||
app.add_crossref_type(
|
||||
"fixture",
|
||||
"fixture",
|
||||
objname="built-in fixture",
|
||||
indextemplate="pair: %s; fixture",
|
||||
)
|
||||
|
||||
app.add_object_type(
|
||||
"confval",
|
||||
"confval",
|
||||
objname="configuration value",
|
||||
indextemplate="pair: %s; configuration value",
|
||||
)
|
||||
|
||||
configure_logging(app)
|
||||
|
||||
@@ -19,6 +19,30 @@ Below is a complete list of all pytest features which are considered deprecated.
|
||||
:class:`_pytest.warning_types.PytestWarning` or subclasses, which can be filtered using
|
||||
:ref:`standard warning filters <warnings>`.
|
||||
|
||||
|
||||
``--no-print-logs`` command-line option
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 5.4
|
||||
|
||||
|
||||
Option ``--no-print-logs`` is deprecated and meant to be removed in a future release. If you use ``--no-print-logs``, please try out ``--show-capture`` and
|
||||
provide feedback.
|
||||
|
||||
``--show-capture`` command-line option was added in ``pytest 3.5.0` and allows to specify how to
|
||||
display captured output when tests fail: ``no``, ``stdout``, ``stderr``, ``log`` or ``all`` (the default).
|
||||
|
||||
|
||||
|
||||
Node Construction changed to ``Node.from_parent``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 5.4
|
||||
|
||||
The construction of nodes new should use the named constructor ``from_parent``.
|
||||
This limitation in api surface intends to enable better/simpler refactoring of the collection tree.
|
||||
|
||||
|
||||
``junit_family`` default value change to "xunit2"
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -71,6 +95,18 @@ The plan is remove the ``--result-log`` option in pytest 6.0 if ``pytest-reportl
|
||||
to all users and is deemed stable. The ``pytest-reportlog`` plugin might even be merged into the core
|
||||
at some point, depending on the plans for the plugins and number of users using it.
|
||||
|
||||
TerminalReporter.writer
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 5.4
|
||||
|
||||
The ``TerminalReporter.writer`` attribute has been deprecated and should no longer be used. This
|
||||
was inadvertently exposed as part of the public API of that plugin and ties it too much
|
||||
with ``py.io.TerminalWriter``.
|
||||
|
||||
Plugins that used ``TerminalReporter.writer`` directly should instead use ``TerminalReporter``
|
||||
methods that provide the same functionality.
|
||||
|
||||
|
||||
Removed Features
|
||||
----------------
|
||||
|
||||
@@ -19,7 +19,7 @@ Branches
|
||||
|
||||
We have two long term branches:
|
||||
|
||||
* ``master``: contains the code for the next bugfix release.
|
||||
* ``master``: contains the code for the next bug-fix release.
|
||||
* ``features``: contains the code with new features for the next minor release.
|
||||
|
||||
The official repository usually does not contain topic branches, developers and contributors should create topic
|
||||
@@ -57,4 +57,4 @@ Issues created at those events should have other relevant labels added as well.
|
||||
Those labels should be removed after they are no longer relevant.
|
||||
|
||||
|
||||
.. include:: ../../HOWTORELEASE.rst
|
||||
.. include:: ../../RELEASING.rst
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def setup(request):
|
||||
setup = CostlySetup()
|
||||
yield setup
|
||||
setup.finalize()
|
||||
|
||||
|
||||
class CostlySetup:
|
||||
def __init__(self):
|
||||
import time
|
||||
|
||||
print("performing costly setup")
|
||||
time.sleep(5)
|
||||
self.timecostly = 1
|
||||
|
||||
def finalize(self):
|
||||
del self.timecostly
|
||||
@@ -1 +0,0 @@
|
||||
#
|
||||
@@ -1,2 +0,0 @@
|
||||
def test_quick(setup):
|
||||
pass
|
||||
@@ -1 +0,0 @@
|
||||
#
|
||||
@@ -1,6 +0,0 @@
|
||||
def test_something(setup):
|
||||
assert setup.timecostly == 1
|
||||
|
||||
|
||||
def test_something_more(setup):
|
||||
assert setup.timecostly == 1
|
||||
@@ -148,6 +148,10 @@ which implements a substring match on the test names instead of the
|
||||
exact match on markers that ``-m`` provides. This makes it easy to
|
||||
select tests based on their names:
|
||||
|
||||
.. versionadded: 5.4
|
||||
|
||||
The expression matching is now case-insensitive.
|
||||
|
||||
.. code-block:: pytest
|
||||
|
||||
$ pytest -v -k http # running with the above defined example module
|
||||
@@ -711,6 +715,9 @@ We can now use the ``-m option`` to select one set:
|
||||
test_module.py:8: in test_interface_complex
|
||||
assert 0
|
||||
E assert 0
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_module.py::test_interface_simple - assert 0
|
||||
FAILED test_module.py::test_interface_complex - assert 0
|
||||
===================== 2 failed, 2 deselected in 0.12s ======================
|
||||
|
||||
or to select both "event" and "interface" tests:
|
||||
@@ -739,4 +746,8 @@ or to select both "event" and "interface" tests:
|
||||
test_module.py:12: in test_event_simple
|
||||
assert 0
|
||||
E assert 0
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_module.py::test_interface_simple - assert 0
|
||||
FAILED test_module.py::test_interface_complex - assert 0
|
||||
FAILED test_module.py::test_event_simple - assert 0
|
||||
===================== 3 failed, 1 deselected in 0.12s ======================
|
||||
|
||||
@@ -41,6 +41,8 @@ now execute the test specification:
|
||||
usecase execution failed
|
||||
spec failed: 'some': 'other'
|
||||
no further details known at this point.
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_simple.yaml::hello
|
||||
======================= 1 failed, 1 passed in 0.12s ========================
|
||||
|
||||
.. regendoc:wipe
|
||||
@@ -77,6 +79,8 @@ consulted when reporting in ``verbose`` mode:
|
||||
usecase execution failed
|
||||
spec failed: 'some': 'other'
|
||||
no further details known at this point.
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_simple.yaml::hello
|
||||
======================= 1 failed, 1 passed in 0.12s ========================
|
||||
|
||||
.. regendoc:wipe
|
||||
|
||||
@@ -4,7 +4,7 @@ import pytest
|
||||
|
||||
def pytest_collect_file(parent, path):
|
||||
if path.ext == ".yaml" and path.basename.startswith("test"):
|
||||
return YamlFile(path, parent)
|
||||
return YamlFile.from_parent(parent, fspath=path)
|
||||
|
||||
|
||||
class YamlFile(pytest.File):
|
||||
@@ -13,7 +13,7 @@ class YamlFile(pytest.File):
|
||||
|
||||
raw = yaml.safe_load(self.fspath.open())
|
||||
for name, spec in sorted(raw.items()):
|
||||
yield YamlItem(name, self, spec)
|
||||
yield YamlItem.from_parent(self, name=name, spec=spec)
|
||||
|
||||
|
||||
class YamlItem(pytest.Item):
|
||||
|
||||
@@ -73,6 +73,8 @@ let's run the full monty:
|
||||
E assert 4 < 4
|
||||
|
||||
test_compute.py:4: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_compute.py::test_compute[4] - assert 4 < 4
|
||||
1 failed, 4 passed in 0.12s
|
||||
|
||||
As expected when running the full range of ``param1`` values
|
||||
@@ -343,6 +345,8 @@ And then when we run the test:
|
||||
E Failed: deliberately failing for demo purposes
|
||||
|
||||
test_backends.py:8: Failed
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_backends.py::test_db_initialized[d2] - Failed: deliberately f...
|
||||
1 failed, 1 passed in 0.12s
|
||||
|
||||
The first invocation with ``db == "DB1"`` passed while the second with ``db == "DB2"`` failed. Our ``db`` fixture function has instantiated each of the DB values during the setup phase while the ``pytest_generate_tests`` generated two according calls to the ``test_db_initialized`` during the collection phase.
|
||||
@@ -454,6 +458,8 @@ argument sets to use for each test function. Let's run it:
|
||||
E assert 1 == 2
|
||||
|
||||
test_parametrize.py:21: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_parametrize.py::TestClass::test_equals[1-2] - assert 1 == 2
|
||||
1 failed, 2 passed in 0.12s
|
||||
|
||||
Indirect parametrization with multiple fixtures
|
||||
@@ -475,11 +481,10 @@ Running it results in some skips if we don't have all the python interpreters in
|
||||
.. code-block:: pytest
|
||||
|
||||
. $ pytest -rs -q multipython.py
|
||||
ssssssssssssssssssssssss... [100%]
|
||||
ssssssssssss......sss...... [100%]
|
||||
========================= short test summary info ==========================
|
||||
SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:29: 'python3.5' not found
|
||||
SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:29: 'python3.6' not found
|
||||
3 passed, 24 skipped in 0.12s
|
||||
SKIPPED [15] $REGENDOC_TMPDIR/CWD/multipython.py:29: 'python3.5' not found
|
||||
12 passed, 15 skipped in 0.12s
|
||||
|
||||
Indirect parametrization of optional implementations/imports
|
||||
--------------------------------------------------------------------
|
||||
@@ -604,13 +609,13 @@ Then run ``pytest`` with verbose mode and with only the ``basic`` marker:
|
||||
platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python
|
||||
cachedir: $PYTHON_PREFIX/.pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR
|
||||
collecting ... collected 17 items / 14 deselected / 3 selected
|
||||
collecting ... collected 14 items / 11 deselected / 3 selected
|
||||
|
||||
test_pytest_param_example.py::test_eval[1+7-8] PASSED [ 33%]
|
||||
test_pytest_param_example.py::test_eval[basic_2+4] PASSED [ 66%]
|
||||
test_pytest_param_example.py::test_eval[basic_6*9] XFAIL [100%]
|
||||
|
||||
=============== 2 passed, 14 deselected, 1 xfailed in 0.12s ================
|
||||
=============== 2 passed, 11 deselected, 1 xfailed in 0.12s ================
|
||||
|
||||
As the result:
|
||||
|
||||
|
||||
@@ -81,8 +81,8 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
def test_eq_text(self):
|
||||
> assert "spam" == "eggs"
|
||||
E AssertionError: assert 'spam' == 'eggs'
|
||||
E - spam
|
||||
E + eggs
|
||||
E - eggs
|
||||
E + spam
|
||||
|
||||
failure_demo.py:45: AssertionError
|
||||
_____________ TestSpecialisedExplanations.test_eq_similar_text _____________
|
||||
@@ -92,9 +92,9 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
def test_eq_similar_text(self):
|
||||
> assert "foo 1 bar" == "foo 2 bar"
|
||||
E AssertionError: assert 'foo 1 bar' == 'foo 2 bar'
|
||||
E - foo 1 bar
|
||||
E - foo 2 bar
|
||||
E ? ^
|
||||
E + foo 2 bar
|
||||
E + foo 1 bar
|
||||
E ? ^
|
||||
|
||||
failure_demo.py:48: AssertionError
|
||||
@@ -106,8 +106,8 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
> assert "foo\nspam\nbar" == "foo\neggs\nbar"
|
||||
E AssertionError: assert 'foo\nspam\nbar' == 'foo\neggs\nbar'
|
||||
E foo
|
||||
E - spam
|
||||
E + eggs
|
||||
E - eggs
|
||||
E + spam
|
||||
E bar
|
||||
|
||||
failure_demo.py:51: AssertionError
|
||||
@@ -122,9 +122,9 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
E AssertionError: assert '111111111111...2222222222222' == '111111111111...2222222222222'
|
||||
E Skipping 90 identical leading characters in diff, use -v to show
|
||||
E Skipping 91 identical trailing characters in diff, use -v to show
|
||||
E - 1111111111a222222222
|
||||
E - 1111111111b222222222
|
||||
E ? ^
|
||||
E + 1111111111b222222222
|
||||
E + 1111111111a222222222
|
||||
E ? ^
|
||||
|
||||
failure_demo.py:56: AssertionError
|
||||
@@ -650,4 +650,49 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
E + where 1 = This is JSON\n{\n 'foo': 'bar'\n}.a
|
||||
|
||||
failure_demo.py:282: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED failure_demo.py::test_generative[3-6] - assert (3 * 2) < 6
|
||||
FAILED failure_demo.py::TestFailing::test_simple - assert 42 == 43
|
||||
FAILED failure_demo.py::TestFailing::test_simple_multiline - assert 42 == 54
|
||||
FAILED failure_demo.py::TestFailing::test_not - assert not 42
|
||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_text - Asser...
|
||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_similar_text
|
||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_multiline_text
|
||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_long_text - ...
|
||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_long_text_multiline
|
||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_list - asser...
|
||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_list_long - ...
|
||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_dict - Asser...
|
||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_set - Assert...
|
||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_longer_list
|
||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_in_list - asser...
|
||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_not_in_text_multiline
|
||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_not_in_text_single
|
||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_not_in_text_single_long
|
||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_not_in_text_single_long_term
|
||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_dataclass - ...
|
||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_attrs - Asse...
|
||||
FAILED failure_demo.py::test_attribute - assert 1 == 2
|
||||
FAILED failure_demo.py::test_attribute_instance - AssertionError: assert ...
|
||||
FAILED failure_demo.py::test_attribute_failure - Exception: Failed to get...
|
||||
FAILED failure_demo.py::test_attribute_multiple - AssertionError: assert ...
|
||||
FAILED failure_demo.py::TestRaises::test_raises - ValueError: invalid lit...
|
||||
FAILED failure_demo.py::TestRaises::test_raises_doesnt - Failed: DID NOT ...
|
||||
FAILED failure_demo.py::TestRaises::test_raise - ValueError: demo error
|
||||
FAILED failure_demo.py::TestRaises::test_tupleerror - ValueError: not eno...
|
||||
FAILED failure_demo.py::TestRaises::test_reinterpret_fails_with_print_for_the_fun_of_it
|
||||
FAILED failure_demo.py::TestRaises::test_some_error - NameError: name 'na...
|
||||
FAILED failure_demo.py::test_dynamic_compile_shows_nicely - AssertionError
|
||||
FAILED failure_demo.py::TestMoreErrors::test_complex_error - assert 44 == 43
|
||||
FAILED failure_demo.py::TestMoreErrors::test_z1_unpack_error - ValueError...
|
||||
FAILED failure_demo.py::TestMoreErrors::test_z2_type_error - TypeError: c...
|
||||
FAILED failure_demo.py::TestMoreErrors::test_startswith - AssertionError:...
|
||||
FAILED failure_demo.py::TestMoreErrors::test_startswith_nested - Assertio...
|
||||
FAILED failure_demo.py::TestMoreErrors::test_global_func - assert False
|
||||
FAILED failure_demo.py::TestMoreErrors::test_instance - assert 42 != 42
|
||||
FAILED failure_demo.py::TestMoreErrors::test_compare - assert 11 < 5
|
||||
FAILED failure_demo.py::TestMoreErrors::test_try_finally - assert 1 == 0
|
||||
FAILED failure_demo.py::TestCustomAssertMsg::test_single_line - Assertion...
|
||||
FAILED failure_demo.py::TestCustomAssertMsg::test_multiline - AssertionEr...
|
||||
FAILED failure_demo.py::TestCustomAssertMsg::test_custom_repr - Assertion...
|
||||
============================ 44 failed in 0.12s ============================
|
||||
|
||||
@@ -65,6 +65,8 @@ Let's run this without supplying our new option:
|
||||
test_sample.py:6: AssertionError
|
||||
--------------------------- Captured stdout call ---------------------------
|
||||
first
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_sample.py::test_answer - assert 0
|
||||
1 failed in 0.12s
|
||||
|
||||
And now with supplying a command line option:
|
||||
@@ -89,6 +91,8 @@ And now with supplying a command line option:
|
||||
test_sample.py:6: AssertionError
|
||||
--------------------------- Captured stdout call ---------------------------
|
||||
second
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_sample.py::test_answer - assert 0
|
||||
1 failed in 0.12s
|
||||
|
||||
You can see that the command line option arrived in our test. This
|
||||
@@ -261,6 +265,8 @@ Let's run our little function:
|
||||
E Failed: not configured: 42
|
||||
|
||||
test_checkconfig.py:11: Failed
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_checkconfig.py::test_something - Failed: not configured: 42
|
||||
1 failed in 0.12s
|
||||
|
||||
If you only want to hide certain exceptions, you can set ``__tracebackhide__``
|
||||
@@ -442,8 +448,8 @@ Now we can profile which test functions execute the slowest:
|
||||
|
||||
========================= slowest 3 test durations =========================
|
||||
0.30s call test_some_are_slow.py::test_funcslow2
|
||||
0.21s call test_some_are_slow.py::test_funcslow1
|
||||
0.11s call test_some_are_slow.py::test_funcfast
|
||||
0.20s call test_some_are_slow.py::test_funcslow1
|
||||
0.10s call test_some_are_slow.py::test_funcfast
|
||||
============================ 3 passed in 0.12s =============================
|
||||
|
||||
incremental testing - test steps
|
||||
@@ -461,21 +467,52 @@ an ``incremental`` marker which is to be used on classes:
|
||||
|
||||
# content of conftest.py
|
||||
|
||||
from typing import Dict, Tuple
|
||||
import pytest
|
||||
|
||||
# store history of failures per test class name and per index in parametrize (if parametrize used)
|
||||
_test_failed_incremental: Dict[str, Dict[Tuple[int, ...], str]] = {}
|
||||
|
||||
|
||||
def pytest_runtest_makereport(item, call):
|
||||
if "incremental" in item.keywords:
|
||||
# incremental marker is used
|
||||
if call.excinfo is not None:
|
||||
parent = item.parent
|
||||
parent._previousfailed = item
|
||||
# the test has failed
|
||||
# retrieve the class name of the test
|
||||
cls_name = str(item.cls)
|
||||
# retrieve the index of the test (if parametrize is used in combination with incremental)
|
||||
parametrize_index = (
|
||||
tuple(item.callspec.indices.values())
|
||||
if hasattr(item, "callspec")
|
||||
else ()
|
||||
)
|
||||
# retrieve the name of the test function
|
||||
test_name = item.originalname or item.name
|
||||
# store in _test_failed_incremental the original name of the failed test
|
||||
_test_failed_incremental.setdefault(cls_name, {}).setdefault(
|
||||
parametrize_index, test_name
|
||||
)
|
||||
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
if "incremental" in item.keywords:
|
||||
previousfailed = getattr(item.parent, "_previousfailed", None)
|
||||
if previousfailed is not None:
|
||||
pytest.xfail("previous test failed ({})".format(previousfailed.name))
|
||||
# retrieve the class name of the test
|
||||
cls_name = str(item.cls)
|
||||
# check if a previous test has failed for this class
|
||||
if cls_name in _test_failed_incremental:
|
||||
# retrieve the index of the test (if parametrize is used in combination with incremental)
|
||||
parametrize_index = (
|
||||
tuple(item.callspec.indices.values())
|
||||
if hasattr(item, "callspec")
|
||||
else ()
|
||||
)
|
||||
# retrieve the name of the first test function to fail for this class name and index
|
||||
test_name = _test_failed_incremental[cls_name].get(parametrize_index, None)
|
||||
# if name found, test has failed for the combination of class name & test name
|
||||
if test_name is not None:
|
||||
pytest.xfail("previous test failed ({})".format(test_name))
|
||||
|
||||
|
||||
These two hook implementations work together to abort incremental-marked
|
||||
tests in a class. Here is a test module example:
|
||||
@@ -641,6 +678,11 @@ We can run this:
|
||||
E assert 0
|
||||
|
||||
a/test_db2.py:2: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_step.py::TestUserHandling::test_modification - assert 0
|
||||
FAILED a/test_db.py::test_a1 - AssertionError: <conftest.DB object at 0x7...
|
||||
FAILED a/test_db2.py::test_a2 - AssertionError: <conftest.DB object at 0x...
|
||||
ERROR b/test_error.py::test_root
|
||||
============= 3 failed, 2 passed, 1 xfailed, 1 error in 0.12s ==============
|
||||
|
||||
The two test modules in the ``a`` directory see the same ``db`` fixture instance
|
||||
@@ -730,6 +772,9 @@ and run them:
|
||||
E assert 0
|
||||
|
||||
test_module.py:6: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_module.py::test_fail1 - assert 0
|
||||
FAILED test_module.py::test_fail2 - assert 0
|
||||
============================ 2 failed in 0.12s =============================
|
||||
|
||||
you will have a "failures" file which contains the failing test ids:
|
||||
@@ -845,6 +890,10 @@ and run it:
|
||||
E assert 0
|
||||
|
||||
test_module.py:19: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_module.py::test_call_fails - assert 0
|
||||
FAILED test_module.py::test_fail2 - assert 0
|
||||
ERROR test_module.py::test_setup_fails - assert 0
|
||||
======================== 2 failed, 1 error in 0.12s ========================
|
||||
|
||||
You'll see that the fixture finalizers could use the precise reporting
|
||||
|
||||
@@ -32,5 +32,3 @@ reinstall every time you want to run your tests, and is less brittle than
|
||||
mucking about with sys.path to point your tests at local code.
|
||||
|
||||
Also consider using :ref:`tox <use tox>`.
|
||||
|
||||
.. include:: links.inc
|
||||
|
||||
@@ -153,4 +153,6 @@ As of mid-2013, there shouldn't be a problem anymore when you
|
||||
use the standard setuptools (note that distribute has been merged
|
||||
back into setuptools which is now shipped directly with virtualenv).
|
||||
|
||||
.. include:: links.inc
|
||||
.. _nose: https://nose.readthedocs.io/en/latest/
|
||||
.. _pylib: https://py.readthedocs.io/en/latest/
|
||||
.. _`xUnit style setup`: xunit_setup.html
|
||||
|
||||
@@ -10,13 +10,19 @@ pytest fixtures: explicit, modular, scalable
|
||||
|
||||
|
||||
.. _`xUnit`: https://en.wikipedia.org/wiki/XUnit
|
||||
.. _`purpose of test fixtures`: https://en.wikipedia.org/wiki/Test_fixture#Software
|
||||
.. _`Software test fixtures`: https://en.wikipedia.org/wiki/Test_fixture#Software
|
||||
.. _`Dependency injection`: https://en.wikipedia.org/wiki/Dependency_injection
|
||||
|
||||
The `purpose of test fixtures`_ is to provide a fixed baseline
|
||||
upon which tests can reliably and repeatedly execute. pytest fixtures
|
||||
offer dramatic improvements over the classic xUnit style of setup/teardown
|
||||
functions:
|
||||
`Software test fixtures`_ initialize test functions. They provide a
|
||||
fixed baseline so that tests execute reliably and produce consistent,
|
||||
repeatable, results. Initialization may setup services, state, or
|
||||
other operating environments. These are accessed by test functions
|
||||
through arguments; for each fixture used by a test function there is
|
||||
typically a parameter (named after the fixture) in the test function's
|
||||
definition.
|
||||
|
||||
pytest fixtures offer dramatic improvements over the classic xUnit
|
||||
style of setup/teardown functions:
|
||||
|
||||
* fixtures have explicit names and are activated by declaring their use
|
||||
from test functions, modules, classes or whole projects.
|
||||
@@ -34,6 +40,74 @@ both styles, moving incrementally from classic to new style, as you
|
||||
prefer. You can also start out from existing :ref:`unittest.TestCase
|
||||
style <unittest.TestCase>` or :ref:`nose based <nosestyle>` projects.
|
||||
|
||||
:ref:`Fixtures <fixtures-api>` are defined using the
|
||||
:ref:`@pytest.fixture <pytest.fixture-api>` decorator, :ref:`described
|
||||
below <funcargs>`. Pytest has useful built-in fixtures, listed here
|
||||
for reference:
|
||||
|
||||
:fixture:`capfd`
|
||||
Capture, as text, output to file descriptors ``1`` and ``2``.
|
||||
|
||||
:fixture:`capfdbinary`
|
||||
Capture, as bytes, output to file descriptors ``1`` and ``2``.
|
||||
|
||||
:fixture:`caplog`
|
||||
Control logging and access log entries.
|
||||
|
||||
:fixture:`capsys`
|
||||
Capture, as text, output to ``sys.stdout`` and ``sys.stderr``.
|
||||
|
||||
:fixture:`capsysbinary`
|
||||
Capture, as bytes, output to ``sys.stdout`` and ``sys.stderr``.
|
||||
|
||||
:fixture:`cache`
|
||||
Store and retrieve values across pytest runs.
|
||||
|
||||
:fixture:`doctest_namespace`
|
||||
Provide a dict injected into the docstests namespace.
|
||||
|
||||
:fixture:`monkeypatch`
|
||||
Temporarily modify classes, functions, dictionaries,
|
||||
``os.environ``, and other objects.
|
||||
|
||||
:fixture:`pytestconfig`
|
||||
Access to configuration values, pluginmanager and plugin hooks.
|
||||
|
||||
:fixture:`record_property`
|
||||
Add extra properties to the test.
|
||||
|
||||
:fixture:`record_testsuite_property`
|
||||
Add extra properties to the test suite.
|
||||
|
||||
:fixture:`recwarn`
|
||||
Record warnings emitted by test functions.
|
||||
|
||||
:fixture:`request`
|
||||
Provide information on the executing test function.
|
||||
|
||||
:fixture:`testdir`
|
||||
Provide a temporary test directory to aid in running, and
|
||||
testing, pytest plugins.
|
||||
|
||||
:fixture:`tmp_path`
|
||||
Provide a :class:`pathlib.Path` object to a temporary directory
|
||||
which is unique to each test function.
|
||||
|
||||
:fixture:`tmp_path_factory`
|
||||
Make session-scoped temporary directories and return
|
||||
:class:`pathlib.Path` objects.
|
||||
|
||||
:fixture:`tmpdir`
|
||||
Provide a :class:`py.path.local` object to a temporary
|
||||
directory which is unique to each test function;
|
||||
replaced by :fixture:`tmp_path`.
|
||||
|
||||
.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
|
||||
|
||||
:fixture:`tmpdir_factory`
|
||||
Make session-scoped temporary directories and return
|
||||
:class:`py.path.local` objects;
|
||||
replaced by :fixture:`tmp_path_factory`.
|
||||
|
||||
.. _`funcargs`:
|
||||
.. _`funcarg mechanism`:
|
||||
@@ -96,6 +170,8 @@ marked ``smtp_connection`` fixture function. Running the test looks like this:
|
||||
E assert 0
|
||||
|
||||
test_smtpsimple.py:14: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_smtpsimple.py::test_ehlo - assert 0
|
||||
============================ 1 failed in 0.12s =============================
|
||||
|
||||
In the failure traceback we see that the test function was called with a
|
||||
@@ -258,6 +334,9 @@ inspect what is going on and can now run the tests:
|
||||
E assert 0
|
||||
|
||||
test_module.py:13: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_module.py::test_ehlo - assert 0
|
||||
FAILED test_module.py::test_noop - assert 0
|
||||
============================ 2 failed in 0.12s =============================
|
||||
|
||||
You see the two ``assert 0`` failing and more importantly you can also see
|
||||
@@ -320,7 +399,7 @@ containers for different environments. See the example below.
|
||||
.. code-block:: python
|
||||
|
||||
def determine_scope(fixture_name, config):
|
||||
if config.getoption("--keep-containers"):
|
||||
if config.getoption("--keep-containers", None):
|
||||
return "session"
|
||||
return "function"
|
||||
|
||||
@@ -391,6 +470,9 @@ Let's execute it:
|
||||
$ pytest -s -q --tb=no
|
||||
FFteardown smtp
|
||||
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_module.py::test_ehlo - assert 0
|
||||
FAILED test_module.py::test_noop - assert 0
|
||||
2 failed in 0.12s
|
||||
|
||||
We see that the ``smtp_connection`` instance is finalized after the two
|
||||
@@ -545,6 +627,9 @@ again, nothing much has changed:
|
||||
$ pytest -s -q --tb=no
|
||||
FFfinalizing <smtplib.SMTP object at 0xdeadbeef> (smtp.gmail.com)
|
||||
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_module.py::test_ehlo - assert 0
|
||||
FAILED test_module.py::test_noop - assert 0
|
||||
2 failed in 0.12s
|
||||
|
||||
Let's quickly create another test module that actually sets the
|
||||
@@ -574,6 +659,8 @@ Running it:
|
||||
E assert 0
|
||||
------------------------- Captured stdout teardown -------------------------
|
||||
finalizing <smtplib.SMTP object at 0xdeadbeef> (mail.python.org)
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_anothersmtp.py::test_showhelo - AssertionError: (250, b'mail....
|
||||
|
||||
voila! The ``smtp_connection`` fixture function picked up our mail server name
|
||||
from the module namespace.
|
||||
@@ -722,6 +809,11 @@ So let's just do another run:
|
||||
test_module.py:13: AssertionError
|
||||
------------------------- Captured stdout teardown -------------------------
|
||||
finalizing <smtplib.SMTP object at 0xdeadbeef>
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_module.py::test_ehlo[smtp.gmail.com] - assert 0
|
||||
FAILED test_module.py::test_noop[smtp.gmail.com] - assert 0
|
||||
FAILED test_module.py::test_ehlo[mail.python.org] - AssertionError: asser...
|
||||
FAILED test_module.py::test_noop[mail.python.org] - assert 0
|
||||
4 failed in 0.12s
|
||||
|
||||
We see that our two test functions each ran twice, against the different
|
||||
@@ -849,7 +941,7 @@ Running this test will *skip* the invocation of ``data_set`` with value ``2``:
|
||||
Modularity: using fixtures from a fixture function
|
||||
----------------------------------------------------------
|
||||
|
||||
You can not only use fixtures in test functions but fixture functions
|
||||
In addition to using fixtures in test functions, fixture functions
|
||||
can use other fixtures themselves. This contributes to a modular design
|
||||
of your fixtures and allows re-use of framework-specific fixtures across
|
||||
many projects. As a simple example, we can extend the previous example
|
||||
@@ -1042,11 +1134,13 @@ file:
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@pytest.fixture
|
||||
def cleandir():
|
||||
old_cwd = os.getcwd()
|
||||
newpath = tempfile.mkdtemp()
|
||||
os.chdir(newpath)
|
||||
yield
|
||||
os.chdir(old_cwd)
|
||||
shutil.rmtree(newpath)
|
||||
|
||||
and declare its use in a test module via a ``usefixtures`` marker:
|
||||
|
||||
@@ -28,7 +28,7 @@ Install ``pytest``
|
||||
.. code-block:: bash
|
||||
|
||||
$ pytest --version
|
||||
This is pytest version 5.x.y, imported from $PYTHON_PREFIX/lib/python3.7/site-packages/pytest/__init__.py
|
||||
This is pytest version 5.x.y, imported from $PYTHON_PREFIX/lib/python3.8/site-packages/pytest/__init__.py
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
@@ -69,6 +69,8 @@ That’s it. You can now execute the test function:
|
||||
E + where 4 = func(3)
|
||||
|
||||
test_sample.py:6: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_sample.py::test_answer - assert 4 == 5
|
||||
============================ 1 failed in 0.12s =============================
|
||||
|
||||
This test returns a failure report because ``func(3)`` does not return ``5``.
|
||||
@@ -127,7 +129,7 @@ Once you develop multiple tests, you may want to group them into a class. pytest
|
||||
x = "hello"
|
||||
assert hasattr(x, "check")
|
||||
|
||||
``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`` 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, but make sure to prefix your class with ``Test`` otherwise the class will be skipped. We can simply run the module by passing its filename:
|
||||
|
||||
.. code-block:: pytest
|
||||
|
||||
@@ -145,6 +147,8 @@ Once you develop multiple tests, you may want to group them into a class. pytest
|
||||
E + where False = hasattr('hello', 'check')
|
||||
|
||||
test_class.py:8: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_class.py::TestClass::test_two - AssertionError: assert False
|
||||
1 failed, 1 passed in 0.12s
|
||||
|
||||
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.
|
||||
@@ -180,6 +184,8 @@ List the name ``tmpdir`` in the test function signature and ``pytest`` will look
|
||||
test_tmpdir.py:3: AssertionError
|
||||
--------------------------- Captured stdout call ---------------------------
|
||||
PYTEST_TMPDIR/test_needsfiles0
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_tmpdir.py::test_needsfiles - assert 0
|
||||
1 failed in 0.12s
|
||||
|
||||
More info on tmpdir handling is available at :ref:`Temporary directories and files <tmpdir handling>`.
|
||||
@@ -203,5 +209,3 @@ Check out additional pytest resources to help you customize tests for your uniqu
|
||||
* ":ref:`fixtures`" for providing a functional baseline to your tests
|
||||
* ":ref:`plugins`" for managing and writing plugins
|
||||
* ":ref:`goodpractices`" for virtualenv and test layouts
|
||||
|
||||
.. include:: links.inc
|
||||
|
||||
@@ -232,5 +232,4 @@ options. It will run tests against the installed package and not
|
||||
against your source code checkout, helping to detect packaging
|
||||
glitches.
|
||||
|
||||
|
||||
.. include:: links.inc
|
||||
.. _`venv`: https://docs.python.org/3/library/venv.html
|
||||
|
||||
BIN
doc/en/img/favicon.png
Normal file
BIN
doc/en/img/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.7 KiB |
@@ -44,6 +44,8 @@ To execute it:
|
||||
E + where 4 = inc(3)
|
||||
|
||||
test_sample.py:6: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_sample.py::test_answer - assert 4 == 5
|
||||
============================ 1 failed in 0.12s =============================
|
||||
|
||||
Due to ``pytest``'s detailed assertion introspection, only plain ``assert`` statements are used.
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
|
||||
.. _`skipping plugin`: plugin/skipping.html
|
||||
.. _`funcargs mechanism`: funcargs.html
|
||||
.. _`doctest.py`: http://docs.python.org/library/doctest.html
|
||||
.. _`xUnit style setup`: xunit_setup.html
|
||||
.. _`pytest_nose`: plugin/nose.html
|
||||
.. _`reStructured Text`: http://docutils.sourceforge.net
|
||||
.. _`Python debugger`: http://docs.python.org/lib/module-pdb.html
|
||||
.. _nose: https://nose.readthedocs.io/en/latest/
|
||||
.. _pytest: https://pypi.org/project/pytest/
|
||||
.. _mercurial: http://mercurial.selenic.com/wiki/
|
||||
.. _`setuptools`: https://pypi.org/project/setuptools/
|
||||
.. _`easy_install`:
|
||||
.. _`distribute docs`:
|
||||
.. _`distribute`: https://pypi.org/project/distribute/
|
||||
.. _`pip`: https://pypi.org/project/pip/
|
||||
.. _`venv`: https://docs.python.org/3/library/venv.html
|
||||
.. _`virtualenv`: https://pypi.org/project/virtualenv/
|
||||
.. _hudson: http://hudson-ci.org/
|
||||
.. _jenkins: http://jenkins-ci.org/
|
||||
.. _tox: http://testrun.org/tox
|
||||
.. _pylib: https://py.readthedocs.io/en/latest/
|
||||
@@ -3,8 +3,6 @@
|
||||
Running tests written for nose
|
||||
=======================================
|
||||
|
||||
.. include:: links.inc
|
||||
|
||||
``pytest`` has basic support for running tests written for nose_.
|
||||
|
||||
.. _nosestyle:
|
||||
@@ -72,3 +70,5 @@ Unsupported idioms / known issues
|
||||
There are no plans to fix this currently because ``yield``-tests
|
||||
are deprecated in pytest 3.0, with ``pytest.mark.parametrize``
|
||||
being the recommended alternative.
|
||||
|
||||
.. _nose: https://nose.readthedocs.io/en/latest/
|
||||
|
||||
@@ -75,6 +75,8 @@ them in turn:
|
||||
E + where 54 = eval('6*9')
|
||||
|
||||
test_expectation.py:6: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_expectation.py::test_eval[6*9-42] - AssertionError: assert 54...
|
||||
======================= 1 failed, 2 passed in 0.12s ========================
|
||||
|
||||
.. note::
|
||||
@@ -225,6 +227,8 @@ Let's also run with a stringinput that will lead to a failing test:
|
||||
E + where <built-in method isalpha of str object at 0xdeadbeef> = '!'.isalpha
|
||||
|
||||
test_strings.py:4: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_strings.py::test_valid_string[!] - AssertionError: assert False
|
||||
1 failed in 0.12s
|
||||
|
||||
As expected our test function fails.
|
||||
|
||||
@@ -41,8 +41,7 @@ Here is a little annotated list for some popular plugins:
|
||||
* `pytest-instafail <https://pypi.org/project/pytest-instafail/>`_:
|
||||
to report failures while the test run is happening.
|
||||
|
||||
* `pytest-bdd <https://pypi.org/project/pytest-bdd/>`_ and
|
||||
`pytest-konira <https://pypi.org/project/pytest-konira/>`_
|
||||
* `pytest-bdd <https://pypi.org/project/pytest-bdd/>`_:
|
||||
to write tests using behaviour-driven testing.
|
||||
|
||||
* `pytest-timeout <https://pypi.org/project/pytest-timeout/>`_:
|
||||
|
||||
@@ -72,7 +72,7 @@ Some organisations using pytest
|
||||
-----------------------------------
|
||||
|
||||
* `Square Kilometre Array, Cape Town <http://ska.ac.za/>`_
|
||||
* `Some Mozilla QA people <http://www.theautomatedtester.co.uk/blog/2011/pytest_and_xdist_plugin.html>`_ use pytest to distribute their Selenium tests
|
||||
* `Some Mozilla QA people <https://www.theautomatedtester.co.uk/blog/2011/pytest_and_xdist_plugin/>`_ use pytest to distribute their Selenium tests
|
||||
* `Shootq <http://web.shootq.com/>`_
|
||||
* `Stups department of Heinrich Heine University Duesseldorf <http://www.stups.uni-duesseldorf.de/projects.php>`_
|
||||
* cellzome
|
||||
|
||||
@@ -29,9 +29,9 @@ Maintenance of 4.6.X versions
|
||||
-----------------------------
|
||||
|
||||
Until January 2020, the pytest core team ported many bug-fixes from the main release into the
|
||||
``4.6-maintenance`` branch, with several 4.6.X releases being made along the year.
|
||||
``4.6.x`` branch, with several 4.6.X releases being made along the year.
|
||||
|
||||
From now on, the core team will **no longer actively backport patches**, but the ``4.6-maintenance``
|
||||
From now on, the core team will **no longer actively backport patches**, but the ``4.6.x``
|
||||
branch will continue to exist so the community itself can contribute patches.
|
||||
|
||||
The core team will be happy to accept those patches, and make new 4.6.X releases **until mid-2020**
|
||||
@@ -74,7 +74,7 @@ Please follow these instructions:
|
||||
|
||||
#. ``git fetch --all --prune``
|
||||
|
||||
#. ``git checkout origin/4.6-maintenance -b backport-XXXX`` # use the PR number here
|
||||
#. ``git checkout origin/4.6.x -b backport-XXXX`` # use the PR number here
|
||||
|
||||
#. Locate the merge commit on the PR, in the *merged* message, for example:
|
||||
|
||||
@@ -82,14 +82,14 @@ Please follow these instructions:
|
||||
|
||||
#. ``git cherry-pick -m1 REVISION`` # use the revision you found above (``0f8b462``).
|
||||
|
||||
#. Open a PR targeting ``4.6-maintenance``:
|
||||
#. Open a PR targeting ``4.6.x``:
|
||||
|
||||
* Prefix the message with ``[4.6]`` so it is an obvious backport
|
||||
* Delete the PR body, it usually contains a duplicate commit message.
|
||||
|
||||
**Providing new PRs to 4.6**
|
||||
|
||||
Fresh pull requests to ``4.6-maintenance`` will be accepted provided that
|
||||
Fresh pull requests to ``4.6.x`` will be accepted provided that
|
||||
the equivalent code in the active branches does not contain that bug (for example, a bug is specific
|
||||
to Python 2 only).
|
||||
|
||||
|
||||
@@ -77,6 +77,8 @@ This is also discussed in details in :ref:`test discovery`.
|
||||
Invoking ``pytest`` versus ``python -m pytest``
|
||||
-----------------------------------------------
|
||||
|
||||
Running pytest with ``python -m pytest [...]`` instead of ``pytest [...]`` yields nearly
|
||||
equivalent behaviour, except that the former call will add the current directory to ``sys.path``.
|
||||
Running pytest with ``pytest [...]`` instead of ``python -m pytest [...]`` yields nearly
|
||||
equivalent behaviour, except that the latter will add the current directory to ``sys.path``, which
|
||||
is standard ``python`` behavior.
|
||||
|
||||
See also :ref:`cmdline`.
|
||||
|
||||
@@ -126,7 +126,7 @@ Add warning filters to marked test items.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.warnings("ignore:.*usage will be deprecated.*:DeprecationWarning")
|
||||
@pytest.mark.filterwarnings("ignore:.*usage will be deprecated.*:DeprecationWarning")
|
||||
def test_foo():
|
||||
...
|
||||
|
||||
@@ -243,6 +243,8 @@ Will create and attach a :class:`Mark <_pytest.mark.structures.Mark>` object to
|
||||
mark.kwargs == {"method": "thread"}
|
||||
|
||||
|
||||
.. _`fixtures-api`:
|
||||
|
||||
Fixtures
|
||||
--------
|
||||
|
||||
@@ -273,6 +275,8 @@ Example of a fixture requiring another fixture:
|
||||
For more details, consult the full :ref:`fixtures docs <fixture>`.
|
||||
|
||||
|
||||
.. _`pytest.fixture-api`:
|
||||
|
||||
@pytest.fixture
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -280,7 +284,7 @@ For more details, consult the full :ref:`fixtures docs <fixture>`.
|
||||
:decorator:
|
||||
|
||||
|
||||
.. _`cache-api`:
|
||||
.. fixture:: config.cache
|
||||
|
||||
config.cache
|
||||
~~~~~~~~~~~~
|
||||
@@ -301,6 +305,8 @@ Under the hood, the cache plugin uses the simple
|
||||
.. automethod:: Cache.makedir
|
||||
|
||||
|
||||
.. fixture:: capsys
|
||||
|
||||
capsys
|
||||
~~~~~~
|
||||
|
||||
@@ -326,6 +332,8 @@ capsys
|
||||
:members:
|
||||
|
||||
|
||||
.. fixture:: capsysbinary
|
||||
|
||||
capsysbinary
|
||||
~~~~~~~~~~~~
|
||||
|
||||
@@ -346,6 +354,8 @@ capsysbinary
|
||||
assert captured.out == b"hello\n"
|
||||
|
||||
|
||||
.. fixture:: capfd
|
||||
|
||||
capfd
|
||||
~~~~~~
|
||||
|
||||
@@ -366,6 +376,8 @@ capfd
|
||||
assert captured.out == "hello\n"
|
||||
|
||||
|
||||
.. fixture:: capfdbinary
|
||||
|
||||
capfdbinary
|
||||
~~~~~~~~~~~~
|
||||
|
||||
@@ -386,6 +398,8 @@ capfdbinary
|
||||
assert captured.out == b"hello\n"
|
||||
|
||||
|
||||
.. fixture:: doctest_namespace
|
||||
|
||||
doctest_namespace
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -404,6 +418,8 @@ doctest_namespace
|
||||
For more details: :ref:`doctest_namespace`.
|
||||
|
||||
|
||||
.. fixture:: request
|
||||
|
||||
request
|
||||
~~~~~~~
|
||||
|
||||
@@ -415,12 +431,16 @@ The ``request`` fixture is a special fixture providing information of the reques
|
||||
:members:
|
||||
|
||||
|
||||
.. fixture:: pytestconfig
|
||||
|
||||
pytestconfig
|
||||
~~~~~~~~~~~~
|
||||
|
||||
.. autofunction:: _pytest.fixtures.pytestconfig()
|
||||
|
||||
|
||||
.. fixture:: record_property
|
||||
|
||||
record_property
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -429,6 +449,8 @@ record_property
|
||||
.. autofunction:: _pytest.junitxml.record_property()
|
||||
|
||||
|
||||
.. fixture:: record_testsuite_property
|
||||
|
||||
record_testsuite_property
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -436,6 +458,9 @@ record_testsuite_property
|
||||
|
||||
.. autofunction:: _pytest.junitxml.record_testsuite_property()
|
||||
|
||||
|
||||
.. fixture:: caplog
|
||||
|
||||
caplog
|
||||
~~~~~~
|
||||
|
||||
@@ -450,6 +475,8 @@ caplog
|
||||
:members:
|
||||
|
||||
|
||||
.. fixture:: monkeypatch
|
||||
|
||||
monkeypatch
|
||||
~~~~~~~~~~~
|
||||
|
||||
@@ -465,7 +492,8 @@ monkeypatch
|
||||
.. autoclass:: _pytest.monkeypatch.MonkeyPatch
|
||||
:members:
|
||||
|
||||
.. _testdir:
|
||||
|
||||
.. fixture:: testdir
|
||||
|
||||
testdir
|
||||
~~~~~~~
|
||||
@@ -493,6 +521,8 @@ To use it, include in your top-most ``conftest.py`` file:
|
||||
:members:
|
||||
|
||||
|
||||
.. fixture:: recwarn
|
||||
|
||||
recwarn
|
||||
~~~~~~~
|
||||
|
||||
@@ -516,6 +546,8 @@ Each recorded warning is an instance of :class:`warnings.WarningMessage`.
|
||||
differently; see :ref:`ensuring_function_triggers`.
|
||||
|
||||
|
||||
.. fixture:: tmp_path
|
||||
|
||||
tmp_path
|
||||
~~~~~~~~
|
||||
|
||||
@@ -527,6 +559,8 @@ tmp_path
|
||||
:no-auto-options:
|
||||
|
||||
|
||||
.. fixture:: tmp_path_factory
|
||||
|
||||
tmp_path_factory
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -542,6 +576,8 @@ tmp_path_factory
|
||||
.. automethod:: TempPathFactory.getbasetemp
|
||||
|
||||
|
||||
.. fixture:: tmpdir
|
||||
|
||||
tmpdir
|
||||
~~~~~~
|
||||
|
||||
@@ -553,6 +589,8 @@ tmpdir
|
||||
:no-auto-options:
|
||||
|
||||
|
||||
.. fixture:: tmpdir_factory
|
||||
|
||||
tmpdir_factory
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
@@ -738,7 +776,7 @@ ExceptionInfo
|
||||
pytest.ExitCode
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: _pytest.main.ExitCode
|
||||
.. autoclass:: _pytest.config.ExitCode
|
||||
:members:
|
||||
|
||||
|
||||
@@ -901,8 +939,8 @@ Can be either a ``str`` or ``Sequence[str]``.
|
||||
pytest_plugins = ("myapp.testsupport.tools", "myapp.testsupport.regression")
|
||||
|
||||
|
||||
pytest_mark
|
||||
~~~~~~~~~~~
|
||||
pytestmark
|
||||
~~~~~~~~~~
|
||||
|
||||
**Tutorial**: :ref:`scoped-marking`
|
||||
|
||||
@@ -1164,9 +1202,17 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
.. confval:: junit_logging
|
||||
|
||||
.. versionadded:: 3.5
|
||||
.. versionchanged:: 5.4
|
||||
``log``, ``all``, ``out-err`` options added.
|
||||
|
||||
Configures if stdout/stderr should be written to the JUnit XML file. Valid values are
|
||||
``system-out``, ``system-err``, and ``no`` (the default).
|
||||
Configures if captured output should be written to the JUnit XML file. Valid values are:
|
||||
|
||||
* ``log``: write only ``logging`` captured output.
|
||||
* ``system-out``: write captured ``stdout`` contents.
|
||||
* ``system-err``: write captured ``stderr`` contents.
|
||||
* ``out-err``: write both captured ``stdout`` and ``stderr`` contents.
|
||||
* ``all``: write captured ``logging``, ``stdout`` and ``stderr`` contents.
|
||||
* ``no`` (the default): no captured output is written.
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
|
||||
@@ -234,11 +234,11 @@ expect a test to fail:
|
||||
def test_function():
|
||||
...
|
||||
|
||||
This test will be run but no traceback will be reported
|
||||
when it fails. Instead terminal reporting will list it in the
|
||||
"expected to fail" (``XFAIL``) or "unexpectedly passing" (``XPASS``) sections.
|
||||
This test will run but no traceback will be reported when it fails. Instead, terminal
|
||||
reporting will list it in the "expected to fail" (``XFAIL``) or "unexpectedly
|
||||
passing" (``XPASS``) sections.
|
||||
|
||||
Alternatively, you can also mark a test as ``XFAIL`` from within a test or setup function
|
||||
Alternatively, you can also mark a test as ``XFAIL`` from within the test or its setup function
|
||||
imperatively:
|
||||
|
||||
.. code-block:: python
|
||||
@@ -247,8 +247,19 @@ imperatively:
|
||||
if not valid_config():
|
||||
pytest.xfail("failing configuration (but should work)")
|
||||
|
||||
This will unconditionally make ``test_function`` ``XFAIL``. Note that no other code is executed
|
||||
after ``pytest.xfail`` call, differently from the marker. That's because it is implemented
|
||||
.. code-block:: python
|
||||
|
||||
def test_function2():
|
||||
import slow_module
|
||||
|
||||
if slow_module.slow_function():
|
||||
pytest.xfail("slow_module taking too long")
|
||||
|
||||
These two examples illustrate situations where you don't want to check for a condition
|
||||
at the module level, which is when a condition would otherwise be evaluated for marks.
|
||||
|
||||
This will make ``test_function`` ``XFAIL``. Note that no other code is executed after
|
||||
the ``pytest.xfail`` call, differently from the marker. That's because it is implemented
|
||||
internally by raising a known exception.
|
||||
|
||||
**Reference**: :ref:`pytest.mark.xfail ref`
|
||||
@@ -261,8 +272,8 @@ internally by raising a known exception.
|
||||
|
||||
|
||||
|
||||
Both ``XFAIL`` and ``XPASS`` don't fail the test suite, unless the ``strict`` keyword-only
|
||||
parameter is passed as ``True``:
|
||||
Both ``XFAIL`` and ``XPASS`` don't fail the test suite by default.
|
||||
You can change this by setting the ``strict`` keyword-only parameter to ``True``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ Talks and blog postings
|
||||
<https://www.youtube.com/watch?v=P-AhpukDIik>`_
|
||||
|
||||
- `3-part blog series about pytest from @pydanny alias Daniel Greenfeld (January
|
||||
2014) <http://pydanny.com/pytest-no-boilerplate-testing.html>`_
|
||||
2014) <https://daniel.roygreenfeld.com/pytest-no-boilerplate-testing.html>`_
|
||||
|
||||
- `pytest: helps you write better Django apps, Andreas Pelme, DjangoCon
|
||||
Europe 2014 <https://www.youtube.com/watch?v=aaArYVh6XSM>`_.
|
||||
|
||||
@@ -64,6 +64,8 @@ Running this would result in a passed test except for the last
|
||||
E assert 0
|
||||
|
||||
test_tmp_path.py:13: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_tmp_path.py::test_create_file - assert 0
|
||||
============================ 1 failed in 0.12s =============================
|
||||
|
||||
.. _`tmp_path_factory example`:
|
||||
@@ -133,6 +135,8 @@ Running this would result in a passed test except for the last
|
||||
E assert 0
|
||||
|
||||
test_tmpdir.py:9: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_tmpdir.py::test_create_file - assert 0
|
||||
============================ 1 failed in 0.12s =============================
|
||||
|
||||
.. _`tmpdir factory example`:
|
||||
|
||||
@@ -166,6 +166,9 @@ the ``self.db`` values in the traceback:
|
||||
E assert 0
|
||||
|
||||
test_unittest_db.py:13: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_unittest_db.py::MyTest::test_method1 - AssertionError: <conft...
|
||||
FAILED test_unittest_db.py::MyTest::test_method2 - AssertionError: <conft...
|
||||
============================ 2 failed in 0.12s =============================
|
||||
|
||||
This default pytest traceback shows that the two test methods
|
||||
@@ -238,17 +241,6 @@ was executed ahead of the ``test_method``.
|
||||
|
||||
.. _pdb-unittest-note:
|
||||
|
||||
.. note::
|
||||
|
||||
Running tests from ``unittest.TestCase`` subclasses with ``--pdb`` will
|
||||
disable tearDown and cleanup methods for the case that an Exception
|
||||
occurs. This allows proper post mortem debugging for all applications
|
||||
which have significant logic in their tearDown machinery. However,
|
||||
supporting this feature has the following side effect: If people
|
||||
overwrite ``unittest.TestCase`` ``__call__`` or ``run``, they need to
|
||||
to overwrite ``debug`` in the same way (this is also true for standard
|
||||
unittest).
|
||||
|
||||
.. note::
|
||||
|
||||
Due to architectural differences between the two frameworks, setup and
|
||||
|
||||
@@ -33,7 +33,7 @@ Running ``pytest`` can result in six different exit codes:
|
||||
:Exit code 4: pytest command line usage error
|
||||
:Exit code 5: No tests were collected
|
||||
|
||||
They are represented by the :class:`_pytest.main.ExitCode` enum. The exit codes being a part of the public API can be imported and accessed directly using:
|
||||
They are represented by the :class:`_pytest.config.ExitCode` enum. The exit codes being a part of the public API can be imported and accessed directly using:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -94,8 +94,8 @@ Pytest supports several ways to run and select tests from the command-line.
|
||||
|
||||
pytest -k "MyClass and not method"
|
||||
|
||||
This will run tests which contain names that match the given *string expression*, which can
|
||||
include Python operators that use filenames, class names and function names as variables.
|
||||
This will run tests which contain names that match the given *string expression* (case-insensitive),
|
||||
which can include Python operators that use filenames, class names and function names as variables.
|
||||
The example above will run ``TestMyClass.test_something`` but not ``TestMyClass.test_method_simple``.
|
||||
|
||||
.. _nodeids:
|
||||
@@ -169,11 +169,11 @@ option you make sure a trace is shown.
|
||||
Detailed summary report
|
||||
-----------------------
|
||||
|
||||
|
||||
|
||||
The ``-r`` flag can be used to display a "short test summary info" at the end of the test session,
|
||||
making it easy in large test suites to get a clear picture of all failures, skips, xfails, etc.
|
||||
|
||||
It defaults to ``fE`` to list failures and errors.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
@@ -261,8 +261,12 @@ Here is the full list of available characters that can be used:
|
||||
- ``X`` - xpassed
|
||||
- ``p`` - passed
|
||||
- ``P`` - passed with output
|
||||
|
||||
Special characters for (de)selection of groups:
|
||||
|
||||
- ``a`` - all except ``pP``
|
||||
- ``A`` - all
|
||||
- ``N`` - none, this can be used to display nothing (since ``fE`` is the default)
|
||||
|
||||
More than one character can be used, so for example to only see failed and skipped tests, you can execute:
|
||||
|
||||
@@ -817,6 +821,9 @@ hook was invoked:
|
||||
E assert 0
|
||||
|
||||
test_example.py:14: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_example.py::test_fail - assert 0
|
||||
ERROR test_example.py::test_error - assert 0
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -827,5 +834,4 @@ hook was invoked:
|
||||
multiple calls to ``pytest.main()`` from the same process (in order to re-run
|
||||
tests, for example) is not recommended.
|
||||
|
||||
|
||||
.. include:: links.inc
|
||||
.. _jenkins: http://jenkins-ci.org/
|
||||
|
||||
@@ -64,6 +64,8 @@ them into errors:
|
||||
E UserWarning: api v1, should use functions from v2
|
||||
|
||||
test_show_warnings.py:5: UserWarning
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_show_warnings.py::test_one - UserWarning: api v1, should use ...
|
||||
1 failed in 0.12s
|
||||
|
||||
The same option can be set in the ``pytest.ini`` file using the ``filterwarnings`` ini option.
|
||||
|
||||
@@ -179,11 +179,12 @@ 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 rewriting when modules get
|
||||
imported. However since we do not want to test different bytecode
|
||||
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 rewritten and normal assertion
|
||||
behaviour will happen.
|
||||
imported. However, since we do not want to test different bytecode
|
||||
from what you will run in production, this hook only rewrites test modules
|
||||
themselves (as defined by the :confval:`python_files` configuration option),
|
||||
and any modules which are part of plugins.
|
||||
Any 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``
|
||||
@@ -441,8 +442,13 @@ additionally it is possible to copy examples for an example folder before runnin
|
||||
$REGENDOC_TMPDIR/test_example.py:4: PytestExperimentalApiWarning: testdir.copy_example is an experimental api that may change over time
|
||||
testdir.copy_example("test_example.py")
|
||||
|
||||
test_example.py::test_plugin
|
||||
$PYTHON_PREFIX/lib/python3.8/site-packages/_pytest/compat.py:333: PytestDeprecationWarning: The TerminalReporter.writer attribute is deprecated, use TerminalReporter._tw instead at your own risk.
|
||||
See https://docs.pytest.org/en/latest/deprecations.html#terminalreporter-writer for more information.
|
||||
return getattr(object, name, default)
|
||||
|
||||
-- Docs: https://docs.pytest.org/en/latest/warnings.html
|
||||
======================= 2 passed, 1 warning in 0.12s =======================
|
||||
====================== 2 passed, 2 warnings in 0.12s =======================
|
||||
|
||||
For more information about the result object that ``runpytest()`` returns, and
|
||||
the methods that it provides please check out the :py:class:`RunResult
|
||||
|
||||
@@ -16,8 +16,8 @@ title_format = "pytest {version} ({project_date})"
|
||||
template = "changelog/_template.rst"
|
||||
|
||||
[[tool.towncrier.type]]
|
||||
directory = "removal"
|
||||
name = "Removals"
|
||||
directory = "breaking"
|
||||
name = "Breaking Changes"
|
||||
showcontent = true
|
||||
|
||||
[[tool.towncrier.type]]
|
||||
@@ -54,3 +54,6 @@ template = "changelog/_template.rst"
|
||||
directory = "trivial"
|
||||
name = "Trivial/Internal Changes"
|
||||
showcontent = true
|
||||
|
||||
[tool.black]
|
||||
target-version = ['py35']
|
||||
|
||||
@@ -61,7 +61,9 @@ def parse_changelog(tag_name):
|
||||
|
||||
|
||||
def convert_rst_to_md(text):
|
||||
return pypandoc.convert_text(text, "md", format="rst")
|
||||
return pypandoc.convert_text(
|
||||
text, "md", format="rst", extra_args=["--wrap=preserve"]
|
||||
)
|
||||
|
||||
|
||||
def main(argv):
|
||||
|
||||
215
scripts/release-on-comment.py
Normal file
215
scripts/release-on-comment.py
Normal file
@@ -0,0 +1,215 @@
|
||||
"""
|
||||
This script is part of the pytest release process which is triggered by comments
|
||||
in issues.
|
||||
|
||||
This script is started by the `prepare_release.yml` workflow, which is triggered by two comment
|
||||
related events:
|
||||
|
||||
* https://help.github.com/en/actions/reference/events-that-trigger-workflows#issue-comment-event-issue_comment
|
||||
* https://help.github.com/en/actions/reference/events-that-trigger-workflows#issues-event-issues
|
||||
|
||||
This script receives the payload and a secrets on the command line.
|
||||
|
||||
The payload must contain a comment with a phrase matching this regular expression:
|
||||
|
||||
@pytestbot please prepare release from <branch name>
|
||||
|
||||
Then the appropriate version will be obtained based on the given branch name:
|
||||
|
||||
* a feature or bug fix release from master (based if there are features in the current changelog
|
||||
folder)
|
||||
* a bug fix from a maintenance branch
|
||||
|
||||
After that, it will create a release using the `release` tox environment, and push a new PR.
|
||||
|
||||
**Secret**: currently the secret is defined in the @pytestbot account, which the core maintainers
|
||||
have access to. There we created a new secret named `chatops` with write access to the repository.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from subprocess import check_call
|
||||
from subprocess import check_output
|
||||
from textwrap import dedent
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
|
||||
from colorama import Fore
|
||||
from colorama import init
|
||||
from github3.repos import Repository
|
||||
|
||||
|
||||
class InvalidFeatureRelease(Exception):
|
||||
pass
|
||||
|
||||
|
||||
SLUG = "pytest-dev/pytest"
|
||||
|
||||
PR_BODY = """\
|
||||
Created automatically from {comment_url}.
|
||||
|
||||
Once all builds pass and it has been **approved** by one or more maintainers, the build
|
||||
can be released by pushing a tag `{version}` to this repository.
|
||||
"""
|
||||
|
||||
|
||||
def login(token: str) -> Repository:
|
||||
import github3
|
||||
|
||||
github = github3.login(token=token)
|
||||
owner, repo = SLUG.split("/")
|
||||
return github.repository(owner, repo)
|
||||
|
||||
|
||||
def get_comment_data(payload: Dict) -> str:
|
||||
if "comment" in payload:
|
||||
return payload["comment"]
|
||||
else:
|
||||
return payload["issue"]
|
||||
|
||||
|
||||
def validate_and_get_issue_comment_payload(
|
||||
issue_payload_path: Optional[Path],
|
||||
) -> Tuple[str, str]:
|
||||
payload = json.loads(issue_payload_path.read_text(encoding="UTF-8"))
|
||||
body = get_comment_data(payload)["body"]
|
||||
m = re.match(r"@pytestbot please prepare release from ([\w\-_\.]+)", body)
|
||||
if m:
|
||||
base_branch = m.group(1)
|
||||
else:
|
||||
base_branch = None
|
||||
return payload, base_branch
|
||||
|
||||
|
||||
def print_and_exit(msg) -> None:
|
||||
print(msg)
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
def trigger_release(payload_path: Path, token: str) -> None:
|
||||
payload, base_branch = validate_and_get_issue_comment_payload(payload_path)
|
||||
if base_branch is None:
|
||||
url = get_comment_data(payload)["html_url"]
|
||||
print_and_exit(
|
||||
f"Comment {Fore.CYAN}{url}{Fore.RESET} did not match the trigger command."
|
||||
)
|
||||
print()
|
||||
print(f"Precessing release for branch {Fore.CYAN}{base_branch}")
|
||||
|
||||
repo = login(token)
|
||||
|
||||
issue_number = payload["issue"]["number"]
|
||||
issue = repo.issue(issue_number)
|
||||
|
||||
check_call(["git", "checkout", f"origin/{base_branch}"])
|
||||
print("DEBUG:", check_output(["git", "rev-parse", "HEAD"]))
|
||||
|
||||
try:
|
||||
version = find_next_version(base_branch)
|
||||
except InvalidFeatureRelease as e:
|
||||
issue.create_comment(str(e))
|
||||
print_and_exit(f"{Fore.RED}{e}")
|
||||
|
||||
try:
|
||||
print(f"Version: {Fore.CYAN}{version}")
|
||||
|
||||
release_branch = f"release-{version}"
|
||||
|
||||
check_call(["git", "config", "user.name", "pytest bot"])
|
||||
check_call(["git", "config", "user.email", "pytestbot@gmail.com"])
|
||||
|
||||
check_call(["git", "checkout", "-b", release_branch, f"origin/{base_branch}"])
|
||||
|
||||
print(f"Branch {Fore.CYAN}{release_branch}{Fore.RESET} created.")
|
||||
|
||||
check_call(
|
||||
[sys.executable, "scripts/release.py", version, "--skip-check-links"]
|
||||
)
|
||||
|
||||
oauth_url = f"https://{token}:x-oauth-basic@github.com/{SLUG}.git"
|
||||
check_call(["git", "push", oauth_url, f"HEAD:{release_branch}", "--force"])
|
||||
print(f"Branch {Fore.CYAN}{release_branch}{Fore.RESET} pushed.")
|
||||
|
||||
body = PR_BODY.format(
|
||||
comment_url=get_comment_data(payload)["html_url"], version=version
|
||||
)
|
||||
pr = repo.create_pull(
|
||||
f"Prepare release {version}",
|
||||
base=base_branch,
|
||||
head=release_branch,
|
||||
body=body,
|
||||
)
|
||||
print(f"Pull request {Fore.CYAN}{pr.url}{Fore.RESET} created.")
|
||||
|
||||
comment = issue.create_comment(
|
||||
f"As requested, opened a PR for release `{version}`: #{pr.number}."
|
||||
)
|
||||
print(f"Notified in original comment {Fore.CYAN}{comment.url}{Fore.RESET}.")
|
||||
|
||||
print(f"{Fore.GREEN}Success.")
|
||||
except Exception as e:
|
||||
link = f"https://github.com/{SLUG}/actions/runs/{os.environ['GITHUB_RUN_ID']}"
|
||||
issue.create_comment(
|
||||
dedent(
|
||||
f"""
|
||||
Sorry, the request to prepare release `{version}` from {base_branch} failed with:
|
||||
|
||||
```
|
||||
{e}
|
||||
```
|
||||
|
||||
See: {link}.
|
||||
"""
|
||||
)
|
||||
)
|
||||
print_and_exit(f"{Fore.RED}{e}")
|
||||
|
||||
|
||||
def find_next_version(base_branch: str) -> str:
|
||||
output = check_output(["git", "tag"], encoding="UTF-8")
|
||||
valid_versions = []
|
||||
for v in output.splitlines():
|
||||
m = re.match(r"\d.\d.\d+$", v.strip())
|
||||
if m:
|
||||
valid_versions.append(tuple(int(x) for x in v.split(".")))
|
||||
|
||||
valid_versions.sort()
|
||||
last_version = valid_versions[-1]
|
||||
|
||||
changelog = Path("changelog")
|
||||
|
||||
features = list(changelog.glob("*.feature.rst"))
|
||||
breaking = list(changelog.glob("*.breaking.rst"))
|
||||
is_feature_release = features or breaking
|
||||
|
||||
if is_feature_release and base_branch != "master":
|
||||
msg = dedent(
|
||||
f"""
|
||||
Found features or breaking changes in `{base_branch}`, and feature releases can only be
|
||||
created from `master`.":
|
||||
"""
|
||||
)
|
||||
msg += "\n".join(f"* `{x.name}`" for x in sorted(features + breaking))
|
||||
raise InvalidFeatureRelease(msg)
|
||||
|
||||
if is_feature_release:
|
||||
return f"{last_version[0]}.{last_version[1] + 1}.0"
|
||||
else:
|
||||
return f"{last_version[0]}.{last_version[1]}.{last_version[2] + 1}"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
init(autoreset=True)
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("payload")
|
||||
parser.add_argument("token")
|
||||
options = parser.parse_args()
|
||||
trigger_release(Path(options.payload), options.token)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -6,7 +6,7 @@ The pytest team is proud to announce the {version} release!
|
||||
pytest is a mature Python testing tool with more than a 2000 tests
|
||||
against itself, passing on many different interpreters and platforms.
|
||||
|
||||
This release contains a number of bugs fixes and improvements, so users are encouraged
|
||||
This release contains a number of bug fixes and improvements, so users are encouraged
|
||||
to take a look at the CHANGELOG:
|
||||
|
||||
https://docs.pytest.org/en/latest/changelog.html
|
||||
@@ -15,7 +15,7 @@ For complete documentation, please visit:
|
||||
|
||||
https://docs.pytest.org/en/latest/
|
||||
|
||||
As usual, you can upgrade from pypi via:
|
||||
As usual, you can upgrade from PyPI via:
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
@@ -24,4 +24,4 @@ Thanks to all who contributed to this release, among them:
|
||||
{contributors}
|
||||
|
||||
Happy testing,
|
||||
The Pytest Development Team
|
||||
The pytest Development Team
|
||||
|
||||
@@ -100,7 +100,7 @@ def pre_release(version, *, skip_check_links):
|
||||
print()
|
||||
print(f"{Fore.CYAN}[generate.pre_release] {Fore.GREEN}All done!")
|
||||
print()
|
||||
print(f"Please push your branch and open a PR.")
|
||||
print("Please push your branch and open a PR.")
|
||||
|
||||
|
||||
def changelog(version, write_out=False):
|
||||
|
||||
@@ -15,4 +15,4 @@ python -m coverage xml
|
||||
python -m coverage report -m
|
||||
# Set --connect-timeout to work around https://github.com/curl/curl/issues/4461
|
||||
curl -S -L --connect-timeout 5 --retry 6 -s https://codecov.io/bash -o codecov-upload.sh
|
||||
bash codecov-upload.sh -Z -X fix -f coverage.xml
|
||||
bash codecov-upload.sh -Z -X fix -f coverage.xml "$@"
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
@echo off
|
||||
rem Source: https://github.com/appveyor/ci/blob/master/scripts/appveyor-retry.cmd
|
||||
rem initiate the retry number
|
||||
set retryNumber=0
|
||||
set maxRetries=3
|
||||
|
||||
:RUN
|
||||
%*
|
||||
set LastErrorLevel=%ERRORLEVEL%
|
||||
IF %LastErrorLevel% == 0 GOTO :EOF
|
||||
set /a retryNumber=%retryNumber%+1
|
||||
IF %reTryNumber% == %maxRetries% (GOTO :FAILED)
|
||||
|
||||
:RETRY
|
||||
set /a retryNumberDisp=%retryNumber%+1
|
||||
@echo Command "%*" failed with exit code %LastErrorLevel%. Retrying %retryNumberDisp% of %maxRetries%
|
||||
GOTO :RUN
|
||||
|
||||
: FAILED
|
||||
@echo Sorry, we tried running command for %maxRetries% times and all attempts were unsuccessful!
|
||||
EXIT /B %LastErrorLevel%
|
||||
@@ -10,7 +10,6 @@ project_urls =
|
||||
author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others
|
||||
|
||||
license = MIT license
|
||||
license_file = LICENSE
|
||||
keywords = test, unittest
|
||||
classifiers =
|
||||
Development Status :: 6 - Mature
|
||||
@@ -27,6 +26,7 @@ classifiers =
|
||||
Programming Language :: Python :: 3.6
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
platforms = unix, linux, osx, cygwin, win32
|
||||
|
||||
[options]
|
||||
@@ -66,6 +66,7 @@ formats = sdist.tgz,bdist_wheel
|
||||
mypy_path = src
|
||||
ignore_missing_imports = True
|
||||
no_implicit_optional = True
|
||||
show_error_codes = True
|
||||
strict_equality = True
|
||||
warn_redundant_casts = True
|
||||
warn_return_any = True
|
||||
|
||||
@@ -53,19 +53,22 @@ If things do not work right away:
|
||||
which should throw a KeyError: 'COMPLINE' (which is properly set by the
|
||||
global argcomplete script).
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from glob import glob
|
||||
from typing import Any
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class FastFilesCompleter:
|
||||
"Fast file completer class"
|
||||
|
||||
def __init__(self, directories=True):
|
||||
def __init__(self, directories: bool = True) -> None:
|
||||
self.directories = directories
|
||||
|
||||
def __call__(self, prefix, **kwargs):
|
||||
def __call__(self, prefix: str, **kwargs: Any) -> List[str]:
|
||||
"""only called on non option completions"""
|
||||
if os.path.sep in prefix[1:]:
|
||||
prefix_dir = len(os.path.dirname(prefix) + os.path.sep)
|
||||
@@ -94,13 +97,13 @@ if os.environ.get("_ARGCOMPLETE"):
|
||||
sys.exit(-1)
|
||||
filescompleter = FastFilesCompleter() # type: Optional[FastFilesCompleter]
|
||||
|
||||
def try_argcomplete(parser):
|
||||
def try_argcomplete(parser: argparse.ArgumentParser) -> None:
|
||||
argcomplete.autocomplete(parser, always_complete_options=False)
|
||||
|
||||
|
||||
else:
|
||||
|
||||
def try_argcomplete(parser):
|
||||
def try_argcomplete(parser: argparse.ArgumentParser) -> None:
|
||||
pass
|
||||
|
||||
filescompleter = None
|
||||
|
||||
@@ -29,8 +29,10 @@ import pluggy
|
||||
import py
|
||||
|
||||
import _pytest
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest._io.saferepr import safeformat
|
||||
from _pytest._io.saferepr import saferepr
|
||||
from _pytest.compat import ATTRS_EQ_FIELD
|
||||
from _pytest.compat import overload
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
|
||||
@@ -41,7 +43,7 @@ if TYPE_CHECKING:
|
||||
|
||||
from _pytest._code import Source
|
||||
|
||||
_TracebackStyle = Literal["long", "short", "no", "native"]
|
||||
_TracebackStyle = Literal["long", "short", "line", "no", "native"]
|
||||
|
||||
|
||||
class Code:
|
||||
@@ -67,20 +69,22 @@ class Code:
|
||||
return not self == other
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
""" return a path object pointing to source code (note that it
|
||||
might not point to an actually existing file). """
|
||||
def path(self) -> Union[py.path.local, str]:
|
||||
""" return a path object pointing to source code (or a str in case
|
||||
of OSError / non-existing file).
|
||||
"""
|
||||
if not self.raw.co_filename:
|
||||
return ""
|
||||
try:
|
||||
p = py.path.local(self.raw.co_filename)
|
||||
# maybe don't try this checking
|
||||
if not p.check():
|
||||
raise OSError("py.path check failed.")
|
||||
return p
|
||||
except OSError:
|
||||
# XXX maybe try harder like the weird logic
|
||||
# in the standard lib [linecache.updatecache] does?
|
||||
p = self.raw.co_filename
|
||||
|
||||
return p
|
||||
return self.raw.co_filename
|
||||
|
||||
@property
|
||||
def fullsource(self) -> Optional["Source"]:
|
||||
@@ -335,7 +339,7 @@ class Traceback(List[TracebackEntry]):
|
||||
(path is None or codepath == path)
|
||||
and (
|
||||
excludepath is None
|
||||
or not hasattr(codepath, "relto")
|
||||
or not isinstance(codepath, py.path.local)
|
||||
or not codepath.relto(excludepath)
|
||||
)
|
||||
and (lineno is None or x.lineno == lineno)
|
||||
@@ -630,17 +634,18 @@ class ExceptionInfo(Generic[_E]):
|
||||
)
|
||||
return fmt.repr_excinfo(self)
|
||||
|
||||
def match(self, regexp: "Union[str, Pattern]") -> bool:
|
||||
def match(self, regexp: "Union[str, Pattern]") -> "Literal[True]":
|
||||
"""
|
||||
Check whether the regular expression 'regexp' is found in the string
|
||||
representation of the exception using ``re.search``. If it matches
|
||||
then True is returned (so that it is possible to write
|
||||
``assert excinfo.match()``). If it doesn't match an AssertionError is
|
||||
raised.
|
||||
Check whether the regular expression `regexp` matches the string
|
||||
representation of the exception using :func:`python:re.search`.
|
||||
If it matches `True` is returned.
|
||||
If it doesn't match an `AssertionError` is raised.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
if not re.search(regexp, str(self.value)):
|
||||
assert 0, "Pattern {!r} not found in {!r}".format(regexp, str(self.value))
|
||||
assert re.search(
|
||||
regexp, str(self.value)
|
||||
), "Pattern {!r} does not match {!r}".format(regexp, str(self.value))
|
||||
# Return True to allow for "assert excinfo.match()".
|
||||
return True
|
||||
|
||||
|
||||
@@ -785,11 +790,9 @@ class FormattedExcinfo:
|
||||
else:
|
||||
message = excinfo and excinfo.typename or ""
|
||||
path = self._makepath(entry.path)
|
||||
filelocrepr = ReprFileLocation(path, entry.lineno + 1, message)
|
||||
localsrepr = None
|
||||
if not short:
|
||||
localsrepr = self.repr_locals(entry.locals)
|
||||
return ReprEntry(lines, reprargs, localsrepr, filelocrepr, style)
|
||||
reprfileloc = ReprFileLocation(path, entry.lineno + 1, message)
|
||||
localsrepr = self.repr_locals(entry.locals)
|
||||
return ReprEntry(lines, reprargs, localsrepr, reprfileloc, style)
|
||||
if excinfo:
|
||||
lines.extend(self.get_exconly(excinfo, indent=4))
|
||||
return ReprEntry(lines, None, None, None, style)
|
||||
@@ -909,50 +912,53 @@ class FormattedExcinfo:
|
||||
return ExceptionChainRepr(repr_chain)
|
||||
|
||||
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
class TerminalRepr:
|
||||
def __str__(self) -> str:
|
||||
# FYI this is called from pytest-xdist's serialization of exception
|
||||
# information.
|
||||
io = StringIO()
|
||||
tw = py.io.TerminalWriter(file=io)
|
||||
tw = TerminalWriter(file=io)
|
||||
self.toterminal(tw)
|
||||
return io.getvalue().strip()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<{} instance at {:0x}>".format(self.__class__, id(self))
|
||||
|
||||
def toterminal(self, tw) -> None:
|
||||
def toterminal(self, tw: TerminalWriter) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
class ExceptionRepr(TerminalRepr):
|
||||
def __init__(self) -> None:
|
||||
def __attrs_post_init__(self):
|
||||
self.sections = [] # type: List[Tuple[str, str, str]]
|
||||
|
||||
def addsection(self, name: str, content: str, sep: str = "-") -> None:
|
||||
self.sections.append((name, content, sep))
|
||||
|
||||
def toterminal(self, tw) -> None:
|
||||
def toterminal(self, tw: TerminalWriter) -> None:
|
||||
for name, content, sep in self.sections:
|
||||
tw.sep(sep, name)
|
||||
tw.line(content)
|
||||
|
||||
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
class ExceptionChainRepr(ExceptionRepr):
|
||||
def __init__(
|
||||
self,
|
||||
chain: Sequence[
|
||||
chain = attr.ib(
|
||||
type=Sequence[
|
||||
Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]
|
||||
],
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.chain = chain
|
||||
]
|
||||
)
|
||||
|
||||
def __attrs_post_init__(self):
|
||||
super().__attrs_post_init__()
|
||||
# reprcrash and reprtraceback of the outermost (the newest) exception
|
||||
# in the chain
|
||||
self.reprtraceback = chain[-1][0]
|
||||
self.reprcrash = chain[-1][1]
|
||||
self.reprtraceback = self.chain[-1][0]
|
||||
self.reprcrash = self.chain[-1][1]
|
||||
|
||||
def toterminal(self, tw) -> None:
|
||||
def toterminal(self, tw: TerminalWriter) -> None:
|
||||
for element in self.chain:
|
||||
element[0].toterminal(tw)
|
||||
if element[2] is not None:
|
||||
@@ -961,33 +967,25 @@ class ExceptionChainRepr(ExceptionRepr):
|
||||
super().toterminal(tw)
|
||||
|
||||
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
class ReprExceptionInfo(ExceptionRepr):
|
||||
def __init__(
|
||||
self, reprtraceback: "ReprTraceback", reprcrash: "ReprFileLocation"
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.reprtraceback = reprtraceback
|
||||
self.reprcrash = reprcrash
|
||||
reprtraceback = attr.ib(type="ReprTraceback")
|
||||
reprcrash = attr.ib(type="ReprFileLocation")
|
||||
|
||||
def toterminal(self, tw) -> None:
|
||||
def toterminal(self, tw: TerminalWriter) -> None:
|
||||
self.reprtraceback.toterminal(tw)
|
||||
super().toterminal(tw)
|
||||
|
||||
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
class ReprTraceback(TerminalRepr):
|
||||
reprentries = attr.ib(type=Sequence[Union["ReprEntry", "ReprEntryNative"]])
|
||||
extraline = attr.ib(type=Optional[str])
|
||||
style = attr.ib(type="_TracebackStyle")
|
||||
|
||||
entrysep = "_ "
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]],
|
||||
extraline: Optional[str],
|
||||
style: "_TracebackStyle",
|
||||
) -> None:
|
||||
self.reprentries = reprentries
|
||||
self.extraline = extraline
|
||||
self.style = style
|
||||
|
||||
def toterminal(self, tw) -> None:
|
||||
def toterminal(self, tw: TerminalWriter) -> None:
|
||||
# the entries might have different styles
|
||||
for i, entry in enumerate(self.reprentries):
|
||||
if entry.style == "long":
|
||||
@@ -1013,44 +1011,75 @@ class ReprTracebackNative(ReprTraceback):
|
||||
self.extraline = None
|
||||
|
||||
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
class ReprEntryNative(TerminalRepr):
|
||||
lines = attr.ib(type=Sequence[str])
|
||||
style = "native" # type: _TracebackStyle
|
||||
|
||||
def __init__(self, tblines: Sequence[str]) -> None:
|
||||
self.lines = tblines
|
||||
|
||||
def toterminal(self, tw) -> None:
|
||||
def toterminal(self, tw: TerminalWriter) -> None:
|
||||
tw.write("".join(self.lines))
|
||||
|
||||
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
class ReprEntry(TerminalRepr):
|
||||
def __init__(
|
||||
self,
|
||||
lines: Sequence[str],
|
||||
reprfuncargs: Optional["ReprFuncArgs"],
|
||||
reprlocals: Optional["ReprLocals"],
|
||||
filelocrepr: Optional["ReprFileLocation"],
|
||||
style: "_TracebackStyle",
|
||||
) -> None:
|
||||
self.lines = lines
|
||||
self.reprfuncargs = reprfuncargs
|
||||
self.reprlocals = reprlocals
|
||||
self.reprfileloc = filelocrepr
|
||||
self.style = style
|
||||
lines = attr.ib(type=Sequence[str])
|
||||
reprfuncargs = attr.ib(type=Optional["ReprFuncArgs"])
|
||||
reprlocals = attr.ib(type=Optional["ReprLocals"])
|
||||
reprfileloc = attr.ib(type=Optional["ReprFileLocation"])
|
||||
style = attr.ib(type="_TracebackStyle")
|
||||
|
||||
def toterminal(self, tw) -> None:
|
||||
def _write_entry_lines(self, tw: TerminalWriter) -> None:
|
||||
"""Writes the source code portions of a list of traceback entries with syntax highlighting.
|
||||
|
||||
Usually entries are lines like these:
|
||||
|
||||
" x = 1"
|
||||
"> assert x == 2"
|
||||
"E assert 1 == 2"
|
||||
|
||||
This function takes care of rendering the "source" portions of it (the lines without
|
||||
the "E" prefix) using syntax highlighting, taking care to not highlighting the ">"
|
||||
character, as doing so might break line continuations.
|
||||
"""
|
||||
|
||||
indent_size = 4
|
||||
|
||||
def is_fail(line):
|
||||
return line.startswith("{} ".format(FormattedExcinfo.fail_marker))
|
||||
|
||||
if not self.lines:
|
||||
return
|
||||
|
||||
# separate indents and source lines that are not failures: we want to
|
||||
# highlight the code but not the indentation, which may contain markers
|
||||
# such as "> assert 0"
|
||||
indents = []
|
||||
source_lines = []
|
||||
for line in self.lines:
|
||||
if not is_fail(line):
|
||||
indents.append(line[:indent_size])
|
||||
source_lines.append(line[indent_size:])
|
||||
|
||||
tw._write_source(source_lines, indents)
|
||||
|
||||
# failure lines are always completely red and bold
|
||||
for line in (x for x in self.lines if is_fail(x)):
|
||||
tw.line(line, bold=True, red=True)
|
||||
|
||||
def toterminal(self, tw: TerminalWriter) -> None:
|
||||
if self.style == "short":
|
||||
assert self.reprfileloc is not None
|
||||
self.reprfileloc.toterminal(tw)
|
||||
for line in self.lines:
|
||||
red = line.startswith("E ")
|
||||
tw.line(line, bold=True, red=red)
|
||||
self._write_entry_lines(tw)
|
||||
if self.reprlocals:
|
||||
self.reprlocals.toterminal(tw, indent=" " * 8)
|
||||
return
|
||||
|
||||
if self.reprfuncargs:
|
||||
self.reprfuncargs.toterminal(tw)
|
||||
for line in self.lines:
|
||||
red = line.startswith("E ")
|
||||
tw.line(line, bold=True, red=red)
|
||||
|
||||
self._write_entry_lines(tw)
|
||||
|
||||
if self.reprlocals:
|
||||
tw.line("")
|
||||
self.reprlocals.toterminal(tw)
|
||||
@@ -1065,13 +1094,13 @@ class ReprEntry(TerminalRepr):
|
||||
)
|
||||
|
||||
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
class ReprFileLocation(TerminalRepr):
|
||||
def __init__(self, path, lineno: int, message: str) -> None:
|
||||
self.path = str(path)
|
||||
self.lineno = lineno
|
||||
self.message = message
|
||||
path = attr.ib(type=str, converter=str)
|
||||
lineno = attr.ib(type=int)
|
||||
message = attr.ib(type=str)
|
||||
|
||||
def toterminal(self, tw) -> None:
|
||||
def toterminal(self, tw: TerminalWriter) -> None:
|
||||
# filename and lineno output for each entry,
|
||||
# using an output format that most editors understand
|
||||
msg = self.message
|
||||
@@ -1082,20 +1111,20 @@ class ReprFileLocation(TerminalRepr):
|
||||
tw.line(":{}: {}".format(self.lineno, msg))
|
||||
|
||||
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
class ReprLocals(TerminalRepr):
|
||||
def __init__(self, lines: Sequence[str]) -> None:
|
||||
self.lines = lines
|
||||
lines = attr.ib(type=Sequence[str])
|
||||
|
||||
def toterminal(self, tw) -> None:
|
||||
def toterminal(self, tw: TerminalWriter, indent="") -> None:
|
||||
for line in self.lines:
|
||||
tw.line(line)
|
||||
tw.line(indent + line)
|
||||
|
||||
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
class ReprFuncArgs(TerminalRepr):
|
||||
def __init__(self, args: Sequence[Tuple[str, object]]) -> None:
|
||||
self.args = args
|
||||
args = attr.ib(type=Sequence[Tuple[str, object]])
|
||||
|
||||
def toterminal(self, tw) -> None:
|
||||
def toterminal(self, tw: TerminalWriter) -> None:
|
||||
if self.args:
|
||||
linesofar = ""
|
||||
for name, value in self.args:
|
||||
|
||||
@@ -5,9 +5,10 @@ import sys
|
||||
import textwrap
|
||||
import tokenize
|
||||
import warnings
|
||||
from ast import PyCF_ONLY_AST as _AST_FLAG
|
||||
from bisect import bisect_right
|
||||
from types import CodeType
|
||||
from types import FrameType
|
||||
from typing import Any
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
@@ -17,7 +18,12 @@ from typing import Union
|
||||
|
||||
import py
|
||||
|
||||
from _pytest.compat import get_real_func
|
||||
from _pytest.compat import overload
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Literal
|
||||
|
||||
|
||||
class Source:
|
||||
@@ -121,7 +127,7 @@ class Source:
|
||||
start, end = self.getstatementrange(lineno)
|
||||
return self[start:end]
|
||||
|
||||
def getstatementrange(self, lineno: int):
|
||||
def getstatementrange(self, lineno: int) -> Tuple[int, int]:
|
||||
""" return (start, end) tuple which spans the minimal
|
||||
statement region which containing the given lineno.
|
||||
"""
|
||||
@@ -140,18 +146,13 @@ class Source:
|
||||
""" return True if source is parseable, heuristically
|
||||
deindenting it by default.
|
||||
"""
|
||||
from parser import suite as syntax_checker
|
||||
|
||||
if deindent:
|
||||
source = str(self.deindent())
|
||||
else:
|
||||
source = str(self)
|
||||
try:
|
||||
# compile(source+'\n', "x", "exec")
|
||||
syntax_checker(source + "\n")
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except Exception:
|
||||
ast.parse(source)
|
||||
except (SyntaxError, ValueError, TypeError):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
@@ -159,14 +160,36 @@ class Source:
|
||||
def __str__(self) -> str:
|
||||
return "\n".join(self.lines)
|
||||
|
||||
@overload
|
||||
def compile(
|
||||
self,
|
||||
filename=None,
|
||||
mode="exec",
|
||||
filename: Optional[str] = ...,
|
||||
mode: str = ...,
|
||||
flag: "Literal[0]" = ...,
|
||||
dont_inherit: int = ...,
|
||||
_genframe: Optional[FrameType] = ...,
|
||||
) -> CodeType:
|
||||
raise NotImplementedError()
|
||||
|
||||
@overload # noqa: F811
|
||||
def compile( # noqa: F811
|
||||
self,
|
||||
filename: Optional[str] = ...,
|
||||
mode: str = ...,
|
||||
flag: int = ...,
|
||||
dont_inherit: int = ...,
|
||||
_genframe: Optional[FrameType] = ...,
|
||||
) -> Union[CodeType, ast.AST]:
|
||||
raise NotImplementedError()
|
||||
|
||||
def compile( # noqa: F811
|
||||
self,
|
||||
filename: Optional[str] = None,
|
||||
mode: str = "exec",
|
||||
flag: int = 0,
|
||||
dont_inherit: int = 0,
|
||||
_genframe: Optional[FrameType] = None,
|
||||
):
|
||||
) -> Union[CodeType, ast.AST]:
|
||||
""" return compiled code object. if filename is None
|
||||
invent an artificial filename which displays
|
||||
the source/line position of the caller frame.
|
||||
@@ -196,8 +219,10 @@ class Source:
|
||||
newex.text = ex.text
|
||||
raise newex
|
||||
else:
|
||||
if flag & _AST_FLAG:
|
||||
if flag & ast.PyCF_ONLY_AST:
|
||||
assert isinstance(co, ast.AST)
|
||||
return co
|
||||
assert isinstance(co, CodeType)
|
||||
lines = [(x + "\n") for x in self.lines]
|
||||
# Type ignored because linecache.cache is private.
|
||||
linecache.cache[filename] = (1, None, lines, filename) # type: ignore
|
||||
@@ -209,7 +234,35 @@ class Source:
|
||||
#
|
||||
|
||||
|
||||
def compile_(source, filename=None, mode="exec", flags: int = 0, dont_inherit: int = 0):
|
||||
@overload
|
||||
def compile_(
|
||||
source: Union[str, bytes, ast.mod, ast.AST],
|
||||
filename: Optional[str] = ...,
|
||||
mode: str = ...,
|
||||
flags: "Literal[0]" = ...,
|
||||
dont_inherit: int = ...,
|
||||
) -> CodeType:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
@overload # noqa: F811
|
||||
def compile_( # noqa: F811
|
||||
source: Union[str, bytes, ast.mod, ast.AST],
|
||||
filename: Optional[str] = ...,
|
||||
mode: str = ...,
|
||||
flags: int = ...,
|
||||
dont_inherit: int = ...,
|
||||
) -> Union[CodeType, ast.AST]:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def compile_( # noqa: F811
|
||||
source: Union[str, bytes, ast.mod, ast.AST],
|
||||
filename: Optional[str] = None,
|
||||
mode: str = "exec",
|
||||
flags: int = 0,
|
||||
dont_inherit: int = 0,
|
||||
) -> Union[CodeType, ast.AST]:
|
||||
""" compile the given source to a raw code object,
|
||||
and maintain an internal cache which allows later
|
||||
retrieval of the source code for the code object
|
||||
@@ -217,14 +270,16 @@ def compile_(source, filename=None, mode="exec", flags: int = 0, dont_inherit: i
|
||||
"""
|
||||
if isinstance(source, ast.AST):
|
||||
# XXX should Source support having AST?
|
||||
return compile(source, filename, mode, flags, dont_inherit)
|
||||
assert filename is not None
|
||||
co = compile(source, filename, mode, flags, dont_inherit)
|
||||
assert isinstance(co, (CodeType, ast.AST))
|
||||
return co
|
||||
_genframe = sys._getframe(1) # the caller
|
||||
s = Source(source)
|
||||
co = s.compile(filename, mode, flags, _genframe=_genframe)
|
||||
return co
|
||||
return s.compile(filename, mode, flags, _genframe=_genframe)
|
||||
|
||||
|
||||
def getfslineno(obj):
|
||||
def getfslineno(obj: Any) -> Tuple[Union[str, py.path.local], int]:
|
||||
""" Return source location (path, lineno) for the given object.
|
||||
If the source cannot be determined return ("", -1).
|
||||
|
||||
@@ -232,6 +287,13 @@ def getfslineno(obj):
|
||||
"""
|
||||
from .code import Code
|
||||
|
||||
# xxx let decorators etc specify a sane ordering
|
||||
# NOTE: this used to be done in _pytest.compat.getfslineno, initially added
|
||||
# in 6ec13a2b9. It ("place_as") appears to be something very custom.
|
||||
obj = get_real_func(obj)
|
||||
if hasattr(obj, "place_as"):
|
||||
obj = obj.place_as
|
||||
|
||||
try:
|
||||
code = Code(obj)
|
||||
except TypeError:
|
||||
@@ -240,18 +302,16 @@ def getfslineno(obj):
|
||||
except TypeError:
|
||||
return "", -1
|
||||
|
||||
fspath = fn and py.path.local(fn) or None
|
||||
fspath = fn and py.path.local(fn) or ""
|
||||
lineno = -1
|
||||
if fspath:
|
||||
try:
|
||||
_, lineno = findsource(obj)
|
||||
except IOError:
|
||||
pass
|
||||
return fspath, lineno
|
||||
else:
|
||||
fspath = code.path
|
||||
lineno = code.firstlineno
|
||||
assert isinstance(lineno, int)
|
||||
return fspath, lineno
|
||||
return code.path, code.firstlineno
|
||||
|
||||
|
||||
#
|
||||
@@ -321,7 +381,7 @@ def getstatementrange_ast(
|
||||
# don't produce duplicate warnings when compiling source to find ast
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
astnode = compile(content, "source", "exec", _AST_FLAG)
|
||||
astnode = ast.parse(content, "source", "exec")
|
||||
|
||||
start, end = get_statement_startend2(lineno, astnode)
|
||||
# we need to correct the end:
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
from typing import List
|
||||
from typing import Sequence
|
||||
|
||||
from py.io import TerminalWriter as BaseTerminalWriter # noqa: F401
|
||||
|
||||
|
||||
class TerminalWriter(BaseTerminalWriter):
|
||||
def _write_source(self, lines: List[str], indents: Sequence[str] = ()) -> None:
|
||||
"""Write lines of source code possibly highlighted.
|
||||
|
||||
Keeping this private for now because the API is clunky. We should discuss how
|
||||
to evolve the terminal writer so we can have more precise color support, for example
|
||||
being able to write part of a line in one color and the rest in another, and so on.
|
||||
"""
|
||||
if indents and len(indents) != len(lines):
|
||||
raise ValueError(
|
||||
"indents size ({}) should have same size as lines ({})".format(
|
||||
len(indents), len(lines)
|
||||
)
|
||||
)
|
||||
if not indents:
|
||||
indents = [""] * len(lines)
|
||||
source = "\n".join(lines)
|
||||
new_lines = self._highlight(source).splitlines()
|
||||
for indent, new_line in zip(indents, new_lines):
|
||||
self.line(indent + new_line)
|
||||
|
||||
def _highlight(self, source):
|
||||
"""Highlight the given source code according to the "code_highlight" option"""
|
||||
if not self.hasmarkup:
|
||||
return source
|
||||
try:
|
||||
from pygments.formatters.terminal import TerminalFormatter
|
||||
from pygments.lexers.python import PythonLexer
|
||||
from pygments import highlight
|
||||
except ImportError:
|
||||
return source
|
||||
else:
|
||||
return highlight(source, PythonLexer(), TerminalFormatter(bg="dark"))
|
||||
|
||||
@@ -20,7 +20,7 @@ def _format_repr_exception(exc: BaseException, obj: Any) -> str:
|
||||
except BaseException as exc:
|
||||
exc_info = "unpresentable exception ({})".format(_try_repr_or_str(exc))
|
||||
return "<[{} raised in repr()] {} object at 0x{:x}>".format(
|
||||
exc_info, obj.__class__.__name__, id(obj)
|
||||
exc_info, type(obj).__name__, id(obj)
|
||||
)
|
||||
|
||||
|
||||
@@ -80,3 +80,24 @@ def saferepr(obj: Any, maxsize: int = 240) -> str:
|
||||
around the Repr/reprlib functionality of the standard 2.6 lib.
|
||||
"""
|
||||
return SafeRepr(maxsize).repr(obj)
|
||||
|
||||
|
||||
class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter):
|
||||
"""PrettyPrinter that always dispatches (regardless of width)."""
|
||||
|
||||
def _format(self, object, stream, indent, allowance, context, level):
|
||||
p = self._dispatch.get(type(object).__repr__, None)
|
||||
|
||||
objid = id(object)
|
||||
if objid in context or p is None:
|
||||
return super()._format(object, stream, indent, allowance, context, level)
|
||||
|
||||
context[objid] = 1
|
||||
p(self, object, stream, indent, allowance, context, level + 1)
|
||||
del context[objid]
|
||||
|
||||
|
||||
def _pformat_dispatch(object, indent=1, width=80, depth=None, *, compact=False):
|
||||
return AlwaysDispatchingPrettyPrinter(
|
||||
indent=indent, width=width, depth=depth, compact=compact
|
||||
).pformat(object)
|
||||
|
||||
@@ -2,11 +2,20 @@
|
||||
support for presenting detailed information in failing assertions.
|
||||
"""
|
||||
import sys
|
||||
from typing import Any
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
from _pytest.assertion import rewrite
|
||||
from _pytest.assertion import truncate
|
||||
from _pytest.assertion import util
|
||||
from _pytest.assertion.rewrite import assertstate_key
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import hookimpl
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.main import Session
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
@@ -77,13 +86,13 @@ class AssertionState:
|
||||
|
||||
def install_importhook(config):
|
||||
"""Try to install the rewrite hook, raise SystemError if it fails."""
|
||||
config._assertstate = AssertionState(config, "rewrite")
|
||||
config._assertstate.hook = hook = rewrite.AssertionRewritingHook(config)
|
||||
config._store[assertstate_key] = AssertionState(config, "rewrite")
|
||||
config._store[assertstate_key].hook = hook = rewrite.AssertionRewritingHook(config)
|
||||
sys.meta_path.insert(0, hook)
|
||||
config._assertstate.trace("installed rewrite import hook")
|
||||
config._store[assertstate_key].trace("installed rewrite import hook")
|
||||
|
||||
def undo():
|
||||
hook = config._assertstate.hook
|
||||
hook = config._store[assertstate_key].hook
|
||||
if hook is not None and hook in sys.meta_path:
|
||||
sys.meta_path.remove(hook)
|
||||
|
||||
@@ -91,17 +100,18 @@ def install_importhook(config):
|
||||
return hook
|
||||
|
||||
|
||||
def pytest_collection(session):
|
||||
def pytest_collection(session: "Session") -> None:
|
||||
# this hook is only called when test modules are collected
|
||||
# so for example not in the master process of pytest-xdist
|
||||
# (which does not collect test modules)
|
||||
assertstate = getattr(session.config, "_assertstate", None)
|
||||
assertstate = session.config._store.get(assertstate_key, None)
|
||||
if assertstate:
|
||||
if assertstate.hook is not None:
|
||||
assertstate.hook.set_session(session)
|
||||
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
@hookimpl(tryfirst=True, hookwrapper=True)
|
||||
def pytest_runtest_protocol(item):
|
||||
"""Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks
|
||||
|
||||
The newinterpret and rewrite modules will use util._reprcompare if
|
||||
@@ -139,6 +149,7 @@ def pytest_runtest_setup(item):
|
||||
return res
|
||||
return None
|
||||
|
||||
saved_assert_hooks = util._reprcompare, util._assertion_pass
|
||||
util._reprcompare = callbinrepr
|
||||
|
||||
if item.ihook.pytest_assertion_pass.get_hookimpls():
|
||||
@@ -150,18 +161,19 @@ def pytest_runtest_setup(item):
|
||||
|
||||
util._assertion_pass = call_assertion_pass_hook
|
||||
|
||||
yield
|
||||
|
||||
def pytest_runtest_teardown(item):
|
||||
util._reprcompare = None
|
||||
util._assertion_pass = None
|
||||
util._reprcompare, util._assertion_pass = saved_assert_hooks
|
||||
|
||||
|
||||
def pytest_sessionfinish(session):
|
||||
assertstate = getattr(session.config, "_assertstate", None)
|
||||
assertstate = session.config._store.get(assertstate_key, None)
|
||||
if assertstate:
|
||||
if assertstate.hook is not None:
|
||||
assertstate.hook.set_session(None)
|
||||
|
||||
|
||||
def pytest_assertrepr_compare(config, op, left, right):
|
||||
def pytest_assertrepr_compare(
|
||||
config: Config, op: str, left: Any, right: Any
|
||||
) -> Optional[List[str]]:
|
||||
return util.assertrepr_compare(config=config, op=op, left=left, right=right)
|
||||
|
||||
@@ -26,9 +26,18 @@ from _pytest.assertion.util import ( # noqa: F401
|
||||
format_explanation as _format_explanation,
|
||||
)
|
||||
from _pytest.compat import fspath
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.pathlib import fnmatch_ex
|
||||
from _pytest.pathlib import Path
|
||||
from _pytest.pathlib import PurePath
|
||||
from _pytest.store import StoreKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.assertion import AssertionState # noqa: F401
|
||||
|
||||
|
||||
assertstate_key = StoreKey["AssertionState"]()
|
||||
|
||||
|
||||
# pytest caches rewritten pycs in pycache dirs
|
||||
PYTEST_TAG = "{}-pytest-{}".format(sys.implementation.cache_tag, version)
|
||||
@@ -36,7 +45,7 @@ PYC_EXT = ".py" + (__debug__ and "c" or "o")
|
||||
PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
|
||||
|
||||
|
||||
class AssertionRewritingHook(importlib.abc.MetaPathFinder):
|
||||
class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader):
|
||||
"""PEP302/PEP451 import hook which rewrites asserts."""
|
||||
|
||||
def __init__(self, config):
|
||||
@@ -65,7 +74,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder):
|
||||
def find_spec(self, name, path=None, target=None):
|
||||
if self._writing_pyc:
|
||||
return None
|
||||
state = self.config._assertstate
|
||||
state = self.config._store[assertstate_key]
|
||||
if self._early_rewrite_bailout(name, state):
|
||||
return None
|
||||
state.trace("find_module called for: %s" % name)
|
||||
@@ -104,7 +113,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder):
|
||||
|
||||
def exec_module(self, module):
|
||||
fn = Path(module.__spec__.origin)
|
||||
state = self.config._assertstate
|
||||
state = self.config._store[assertstate_key]
|
||||
|
||||
self._rewritten_names.add(module.__name__)
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from typing import Tuple
|
||||
|
||||
import _pytest._code
|
||||
from _pytest import outcomes
|
||||
from _pytest._io.saferepr import _pformat_dispatch
|
||||
from _pytest._io.saferepr import safeformat
|
||||
from _pytest._io.saferepr import saferepr
|
||||
from _pytest.compat import ATTRS_EQ_FIELD
|
||||
@@ -28,27 +29,6 @@ _reprcompare = None # type: Optional[Callable[[str, object, object], Optional[s
|
||||
_assertion_pass = None # type: Optional[Callable[[int, str, str], None]]
|
||||
|
||||
|
||||
class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter):
|
||||
"""PrettyPrinter that always dispatches (regardless of width)."""
|
||||
|
||||
def _format(self, object, stream, indent, allowance, context, level):
|
||||
p = self._dispatch.get(type(object).__repr__, None)
|
||||
|
||||
objid = id(object)
|
||||
if objid in context or p is None:
|
||||
return super()._format(object, stream, indent, allowance, context, level)
|
||||
|
||||
context[objid] = 1
|
||||
p(self, object, stream, indent, allowance, context, level + 1)
|
||||
del context[objid]
|
||||
|
||||
|
||||
def _pformat_dispatch(object, indent=1, width=80, depth=None, *, compact=False):
|
||||
return AlwaysDispatchingPrettyPrinter(
|
||||
indent=1, width=80, depth=None, compact=False
|
||||
).pformat(object)
|
||||
|
||||
|
||||
def format_explanation(explanation: str) -> str:
|
||||
"""This formats an explanation
|
||||
|
||||
@@ -195,9 +175,10 @@ def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[
|
||||
raise
|
||||
except Exception:
|
||||
explanation = [
|
||||
"(pytest_assertion plugin: representation of details failed. "
|
||||
"Probably an object has a faulty __repr__.)",
|
||||
str(_pytest._code.ExceptionInfo.from_current()),
|
||||
"(pytest_assertion plugin: representation of details failed: {}.".format(
|
||||
_pytest._code.ExceptionInfo.from_current()._getreprcrash()
|
||||
),
|
||||
" Probably an object has a faulty __repr__.)",
|
||||
]
|
||||
|
||||
if not explanation:
|
||||
@@ -245,9 +226,11 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
|
||||
left = repr(str(left))
|
||||
right = repr(str(right))
|
||||
explanation += ["Strings contain only whitespace, escaping them using repr()"]
|
||||
# "right" is the expected base against which we compare "left",
|
||||
# see https://github.com/pytest-dev/pytest/issues/3333
|
||||
explanation += [
|
||||
line.strip("\n")
|
||||
for line in ndiff(left.splitlines(keepends), right.splitlines(keepends))
|
||||
for line in ndiff(right.splitlines(keepends), left.splitlines(keepends))
|
||||
]
|
||||
return explanation
|
||||
|
||||
@@ -258,8 +241,8 @@ def _compare_eq_verbose(left: Any, right: Any) -> List[str]:
|
||||
right_lines = repr(right).splitlines(keepends)
|
||||
|
||||
explanation = [] # type: List[str]
|
||||
explanation += ["-" + line for line in left_lines]
|
||||
explanation += ["+" + line for line in right_lines]
|
||||
explanation += ["+" + line for line in left_lines]
|
||||
explanation += ["-" + line for line in right_lines]
|
||||
|
||||
return explanation
|
||||
|
||||
@@ -299,8 +282,10 @@ def _compare_eq_iterable(
|
||||
_surrounding_parens_on_own_lines(right_formatting)
|
||||
|
||||
explanation = ["Full diff:"]
|
||||
# "right" is the expected base against which we compare "left",
|
||||
# see https://github.com/pytest-dev/pytest/issues/3333
|
||||
explanation.extend(
|
||||
line.rstrip() for line in difflib.ndiff(left_formatting, right_formatting)
|
||||
line.rstrip() for line in difflib.ndiff(right_formatting, left_formatting)
|
||||
)
|
||||
return explanation
|
||||
|
||||
@@ -335,8 +320,9 @@ def _compare_eq_sequence(
|
||||
break
|
||||
|
||||
if comparing_bytes:
|
||||
# when comparing bytes, it doesn't help to show the "sides contain one or more items"
|
||||
# longer explanation, so skip it
|
||||
# when comparing bytes, it doesn't help to show the "sides contain one or more
|
||||
# items" longer explanation, so skip it
|
||||
|
||||
return explanation
|
||||
|
||||
len_diff = len_left - len_right
|
||||
@@ -463,7 +449,7 @@ def _notin_text(term: str, text: str, verbose: int = 0) -> List[str]:
|
||||
head = text[:index]
|
||||
tail = text[index + len(term) :]
|
||||
correct_text = head + tail
|
||||
diff = _diff_text(correct_text, text, verbose)
|
||||
diff = _diff_text(text, correct_text, verbose)
|
||||
newdiff = ["%s is contained here:" % saferepr(term, maxsize=42)]
|
||||
for line in diff:
|
||||
if line.startswith("Skipping"):
|
||||
|
||||
@@ -7,7 +7,11 @@ ignores the external pytest-cache
|
||||
import json
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
from typing import Dict
|
||||
from typing import Generator
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
|
||||
import attr
|
||||
import py
|
||||
@@ -16,9 +20,12 @@ import pytest
|
||||
from .pathlib import Path
|
||||
from .pathlib import resolve_from_str
|
||||
from .pathlib import rm_rf
|
||||
from .reports import CollectReport
|
||||
from _pytest import nodes
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest.config import Config
|
||||
from _pytest.main import Session
|
||||
from _pytest.python import Module
|
||||
|
||||
README_CONTENT = """\
|
||||
# pytest cache directory #
|
||||
@@ -70,10 +77,10 @@ class Cache:
|
||||
return resolve_from_str(config.getini("cache_dir"), config.rootdir)
|
||||
|
||||
def warn(self, fmt, **args):
|
||||
from _pytest.warnings import _issue_warning_captured
|
||||
import warnings
|
||||
from _pytest.warning_types import PytestCacheWarning
|
||||
|
||||
_issue_warning_captured(
|
||||
warnings.warn(
|
||||
PytestCacheWarning(fmt.format(**args) if args else fmt),
|
||||
self._config.hook,
|
||||
stacklevel=3,
|
||||
@@ -160,42 +167,88 @@ class Cache:
|
||||
cachedir_tag_path.write_bytes(CACHEDIR_TAG_CONTENT)
|
||||
|
||||
|
||||
class LFPluginCollWrapper:
|
||||
def __init__(self, lfplugin: "LFPlugin"):
|
||||
self.lfplugin = lfplugin
|
||||
self._collected_at_least_one_failure = False
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_make_collect_report(self, collector) -> Generator:
|
||||
if isinstance(collector, Session):
|
||||
out = yield
|
||||
res = out.get_result() # type: CollectReport
|
||||
|
||||
# Sort any lf-paths to the beginning.
|
||||
lf_paths = self.lfplugin._last_failed_paths
|
||||
res.result = sorted(
|
||||
res.result, key=lambda x: 0 if Path(str(x.fspath)) in lf_paths else 1,
|
||||
)
|
||||
out.force_result(res)
|
||||
return
|
||||
|
||||
elif isinstance(collector, Module):
|
||||
if Path(str(collector.fspath)) in self.lfplugin._last_failed_paths:
|
||||
out = yield
|
||||
res = out.get_result()
|
||||
|
||||
filtered_result = [
|
||||
x for x in res.result if x.nodeid in self.lfplugin.lastfailed
|
||||
]
|
||||
if filtered_result:
|
||||
res.result = filtered_result
|
||||
out.force_result(res)
|
||||
|
||||
if not self._collected_at_least_one_failure:
|
||||
self.lfplugin.config.pluginmanager.register(
|
||||
LFPluginCollSkipfiles(self.lfplugin), "lfplugin-collskip"
|
||||
)
|
||||
self._collected_at_least_one_failure = True
|
||||
return res
|
||||
yield
|
||||
|
||||
|
||||
class LFPluginCollSkipfiles:
|
||||
def __init__(self, lfplugin: "LFPlugin"):
|
||||
self.lfplugin = lfplugin
|
||||
|
||||
@pytest.hookimpl
|
||||
def pytest_make_collect_report(self, collector) -> Optional[CollectReport]:
|
||||
if isinstance(collector, Module):
|
||||
if Path(str(collector.fspath)) not in self.lfplugin._last_failed_paths:
|
||||
self.lfplugin._skipped_files += 1
|
||||
|
||||
return CollectReport(
|
||||
collector.nodeid, "passed", longrepr=None, result=[]
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class LFPlugin:
|
||||
""" Plugin which implements the --lf (run last-failing) option """
|
||||
|
||||
def __init__(self, config):
|
||||
def __init__(self, config: Config) -> None:
|
||||
self.config = config
|
||||
active_keys = "lf", "failedfirst"
|
||||
self.active = any(config.getoption(key) for key in active_keys)
|
||||
self.lastfailed = config.cache.get("cache/lastfailed", {})
|
||||
assert config.cache
|
||||
self.lastfailed = config.cache.get(
|
||||
"cache/lastfailed", {}
|
||||
) # type: Dict[str, bool]
|
||||
self._previously_failed_count = None
|
||||
self._report_status = None
|
||||
self._skipped_files = 0 # count skipped files during collection due to --lf
|
||||
|
||||
def last_failed_paths(self):
|
||||
"""Returns a set with all Paths()s of the previously failed nodeids (cached).
|
||||
"""
|
||||
try:
|
||||
return self._last_failed_paths
|
||||
except AttributeError:
|
||||
rootpath = Path(self.config.rootdir)
|
||||
result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed}
|
||||
result = {x for x in result if x.exists()}
|
||||
self._last_failed_paths = result
|
||||
return result
|
||||
if config.getoption("lf"):
|
||||
self._last_failed_paths = self.get_last_failed_paths()
|
||||
config.pluginmanager.register(
|
||||
LFPluginCollWrapper(self), "lfplugin-collwrapper"
|
||||
)
|
||||
|
||||
def pytest_ignore_collect(self, path):
|
||||
"""
|
||||
Ignore this file path if we are in --lf mode and it is not in the list of
|
||||
previously failed files.
|
||||
"""
|
||||
if self.active and self.config.getoption("lf") and path.isfile():
|
||||
last_failed_paths = self.last_failed_paths()
|
||||
if last_failed_paths:
|
||||
skip_it = Path(path) not in self.last_failed_paths()
|
||||
if skip_it:
|
||||
self._skipped_files += 1
|
||||
return skip_it
|
||||
def get_last_failed_paths(self) -> Set[Path]:
|
||||
"""Returns a set with all Paths()s of the previously failed nodeids."""
|
||||
rootpath = Path(str(self.config.rootdir))
|
||||
result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed}
|
||||
return {x for x in result if x.exists()}
|
||||
|
||||
def pytest_report_collectionfinish(self):
|
||||
if self.active and self.config.getoption("verbose") >= 0:
|
||||
@@ -258,7 +311,7 @@ class LFPlugin:
|
||||
self._report_status = "no previously failed tests, "
|
||||
if self.config.getoption("last_failed_no_failures") == "none":
|
||||
self._report_status += "deselecting all items."
|
||||
config.hook.pytest_deselected(items=items)
|
||||
config.hook.pytest_deselected(items=items[:])
|
||||
items[:] = []
|
||||
else:
|
||||
self._report_status += "not deselecting items."
|
||||
@@ -379,7 +432,7 @@ def pytest_cmdline_main(config):
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_configure(config):
|
||||
def pytest_configure(config: Config) -> None:
|
||||
config.cache = Cache.for_config(config)
|
||||
config.pluginmanager.register(LFPlugin(config), "lfplugin")
|
||||
config.pluginmanager.register(NFPlugin(config), "nfplugin")
|
||||
@@ -418,7 +471,7 @@ def pytest_report_header(config):
|
||||
def cacheshow(config, session):
|
||||
from pprint import pformat
|
||||
|
||||
tw = py.io.TerminalWriter()
|
||||
tw = TerminalWriter()
|
||||
tw.line("cachedir: " + str(config.cache._cachedir))
|
||||
if not config.cache._cachedir.is_dir():
|
||||
tw.line("cache is empty")
|
||||
|
||||
@@ -9,11 +9,23 @@ import os
|
||||
import sys
|
||||
from io import UnsupportedOperation
|
||||
from tempfile import TemporaryFile
|
||||
from typing import BinaryIO
|
||||
from typing import Generator
|
||||
from typing import Iterable
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from _pytest.compat import CaptureAndPassthroughIO
|
||||
from _pytest.compat import CaptureIO
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.config import Config
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Literal
|
||||
|
||||
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
|
||||
|
||||
patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}
|
||||
|
||||
|
||||
@@ -24,8 +36,8 @@ def pytest_addoption(parser):
|
||||
action="store",
|
||||
default="fd" if hasattr(os, "dup") else "sys",
|
||||
metavar="method",
|
||||
choices=["fd", "sys", "no"],
|
||||
help="per-test capturing method: one of fd|sys|no.",
|
||||
choices=["fd", "sys", "no", "tee-sys"],
|
||||
help="per-test capturing method: one of fd|sys|no|tee-sys.",
|
||||
)
|
||||
group._addoption(
|
||||
"-s",
|
||||
@@ -37,7 +49,7 @@ def pytest_addoption(parser):
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_load_initial_conftests(early_config, parser, args):
|
||||
def pytest_load_initial_conftests(early_config: Config):
|
||||
ns = early_config.known_args_namespace
|
||||
if ns.capture == "fd":
|
||||
_py36_windowsconsoleio_workaround(sys.stdout)
|
||||
@@ -60,6 +72,18 @@ def pytest_load_initial_conftests(early_config, parser, args):
|
||||
sys.stderr.write(err)
|
||||
|
||||
|
||||
def _get_multicapture(method: "_CaptureMethod") -> "MultiCapture":
|
||||
if method == "fd":
|
||||
return MultiCapture(out=True, err=True, Capture=FDCapture)
|
||||
elif method == "sys":
|
||||
return MultiCapture(out=True, err=True, Capture=SysCapture)
|
||||
elif method == "no":
|
||||
return MultiCapture(out=False, err=False, in_=False)
|
||||
elif method == "tee-sys":
|
||||
return MultiCapture(out=True, err=True, in_=False, Capture=TeeSysCapture)
|
||||
raise ValueError("unknown capturing method: {!r}".format(method))
|
||||
|
||||
|
||||
class CaptureManager:
|
||||
"""
|
||||
Capture plugin, manages that the appropriate capture method is enabled/disabled during collection and each
|
||||
@@ -73,33 +97,21 @@ class CaptureManager:
|
||||
case special handling is needed to ensure the fixtures take precedence over the global capture.
|
||||
"""
|
||||
|
||||
def __init__(self, method):
|
||||
def __init__(self, method: "_CaptureMethod") -> None:
|
||||
self._method = method
|
||||
self._global_capturing = None
|
||||
self._current_item = None
|
||||
self._capture_fixture = None # type: Optional[CaptureFixture]
|
||||
|
||||
def __repr__(self):
|
||||
return "<CaptureManager _method={!r} _global_capturing={!r} _current_item={!r}>".format(
|
||||
self._method, self._global_capturing, self._current_item
|
||||
return "<CaptureManager _method={!r} _global_capturing={!r} _capture_fixture={!r}>".format(
|
||||
self._method, self._global_capturing, self._capture_fixture
|
||||
)
|
||||
|
||||
def _getcapture(self, method):
|
||||
if method == "fd":
|
||||
return MultiCapture(out=True, err=True, Capture=FDCapture)
|
||||
elif method == "sys":
|
||||
return MultiCapture(out=True, err=True, Capture=SysCapture)
|
||||
elif method == "no":
|
||||
return MultiCapture(out=False, err=False, in_=False)
|
||||
raise ValueError("unknown capturing method: %r" % method) # pragma: no cover
|
||||
|
||||
def is_capturing(self):
|
||||
if self.is_globally_capturing():
|
||||
return "global"
|
||||
capture_fixture = getattr(self._current_item, "_capture_fixture", None)
|
||||
if capture_fixture is not None:
|
||||
return (
|
||||
"fixture %s" % self._current_item._capture_fixture.request.fixturename
|
||||
)
|
||||
if self._capture_fixture:
|
||||
return "fixture %s" % self._capture_fixture.request.fixturename
|
||||
return False
|
||||
|
||||
# Global capturing control
|
||||
@@ -109,7 +121,7 @@ class CaptureManager:
|
||||
|
||||
def start_global_capturing(self):
|
||||
assert self._global_capturing is None
|
||||
self._global_capturing = self._getcapture(self._method)
|
||||
self._global_capturing = _get_multicapture(self._method)
|
||||
self._global_capturing.start_capturing()
|
||||
|
||||
def stop_global_capturing(self):
|
||||
@@ -131,41 +143,66 @@ class CaptureManager:
|
||||
|
||||
def suspend(self, in_=False):
|
||||
# Need to undo local capsys-et-al if it exists before disabling global capture.
|
||||
self.suspend_fixture(self._current_item)
|
||||
self.suspend_fixture()
|
||||
self.suspend_global_capture(in_)
|
||||
|
||||
def resume(self):
|
||||
self.resume_global_capture()
|
||||
self.resume_fixture(self._current_item)
|
||||
self.resume_fixture()
|
||||
|
||||
def read_global_capture(self):
|
||||
return self._global_capturing.readouterr()
|
||||
|
||||
# Fixture Control (it's just forwarding, think about removing this later)
|
||||
|
||||
def activate_fixture(self, item):
|
||||
@contextlib.contextmanager
|
||||
def _capturing_for_request(
|
||||
self, request: FixtureRequest
|
||||
) -> Generator["CaptureFixture", None, None]:
|
||||
"""
|
||||
Context manager that creates a ``CaptureFixture`` instance for the
|
||||
given ``request``, ensuring there is only a single one being requested
|
||||
at the same time.
|
||||
|
||||
This is used as a helper with ``capsys``, ``capfd`` etc.
|
||||
"""
|
||||
if self._capture_fixture:
|
||||
other_name = next(
|
||||
k
|
||||
for k, v in map_fixname_class.items()
|
||||
if v is self._capture_fixture.captureclass
|
||||
)
|
||||
raise request.raiseerror(
|
||||
"cannot use {} and {} at the same time".format(
|
||||
request.fixturename, other_name
|
||||
)
|
||||
)
|
||||
capture_class = map_fixname_class[request.fixturename]
|
||||
self._capture_fixture = CaptureFixture(capture_class, request)
|
||||
self.activate_fixture()
|
||||
yield self._capture_fixture
|
||||
self._capture_fixture.close()
|
||||
self._capture_fixture = None
|
||||
|
||||
def activate_fixture(self):
|
||||
"""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()
|
||||
if self._capture_fixture:
|
||||
self._capture_fixture._start()
|
||||
|
||||
def deactivate_fixture(self, item):
|
||||
def deactivate_fixture(self):
|
||||
"""Deactivates the ``capsys`` or ``capfd`` fixture of this item, if any."""
|
||||
fixture = getattr(item, "_capture_fixture", None)
|
||||
if fixture is not None:
|
||||
fixture.close()
|
||||
if self._capture_fixture:
|
||||
self._capture_fixture.close()
|
||||
|
||||
def suspend_fixture(self, item):
|
||||
fixture = getattr(item, "_capture_fixture", None)
|
||||
if fixture is not None:
|
||||
fixture._suspend()
|
||||
def suspend_fixture(self):
|
||||
if self._capture_fixture:
|
||||
self._capture_fixture._suspend()
|
||||
|
||||
def resume_fixture(self, item):
|
||||
fixture = getattr(item, "_capture_fixture", None)
|
||||
if fixture is not None:
|
||||
fixture._resume()
|
||||
def resume_fixture(self):
|
||||
if self._capture_fixture:
|
||||
self._capture_fixture._resume()
|
||||
|
||||
# Helper context managers
|
||||
|
||||
@@ -181,11 +218,11 @@ class CaptureManager:
|
||||
@contextlib.contextmanager
|
||||
def item_capture(self, when, item):
|
||||
self.resume_global_capture()
|
||||
self.activate_fixture(item)
|
||||
self.activate_fixture()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.deactivate_fixture(item)
|
||||
self.deactivate_fixture()
|
||||
self.suspend_global_capture(in_=False)
|
||||
|
||||
out, err = self.read_global_capture()
|
||||
@@ -209,12 +246,6 @@ class CaptureManager:
|
||||
else:
|
||||
yield
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_protocol(self, item):
|
||||
self._current_item = item
|
||||
yield
|
||||
self._current_item = None
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_setup(self, item):
|
||||
with self.item_capture("setup", item):
|
||||
@@ -239,18 +270,6 @@ class CaptureManager:
|
||||
self.stop_global_capturing()
|
||||
|
||||
|
||||
capture_fixtures = {"capfd", "capfdbinary", "capsys", "capsysbinary"}
|
||||
|
||||
|
||||
def _ensure_only_one_capture_fixture(request: FixtureRequest, name):
|
||||
fixtures = sorted(set(request.fixturenames) & capture_fixtures - {name})
|
||||
if fixtures:
|
||||
arg = fixtures[0] if len(fixtures) == 1 else fixtures
|
||||
raise request.raiseerror(
|
||||
"cannot use {} and {} at the same time".format(arg, name)
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def capsys(request):
|
||||
"""Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||
@@ -259,8 +278,8 @@ def capsys(request):
|
||||
calls, which return a ``(out, err)`` namedtuple.
|
||||
``out`` and ``err`` will be ``text`` objects.
|
||||
"""
|
||||
_ensure_only_one_capture_fixture(request, "capsys")
|
||||
with _install_capture_fixture_on_item(request, SysCapture) as fixture:
|
||||
capman = request.config.pluginmanager.getplugin("capturemanager")
|
||||
with capman._capturing_for_request(request) as fixture:
|
||||
yield fixture
|
||||
|
||||
|
||||
@@ -272,8 +291,8 @@ def capsysbinary(request):
|
||||
method calls, which return a ``(out, err)`` namedtuple.
|
||||
``out`` and ``err`` will be ``bytes`` objects.
|
||||
"""
|
||||
_ensure_only_one_capture_fixture(request, "capsysbinary")
|
||||
with _install_capture_fixture_on_item(request, SysCaptureBinary) as fixture:
|
||||
capman = request.config.pluginmanager.getplugin("capturemanager")
|
||||
with capman._capturing_for_request(request) as fixture:
|
||||
yield fixture
|
||||
|
||||
|
||||
@@ -285,12 +304,12 @@ def capfd(request):
|
||||
calls, which return a ``(out, err)`` namedtuple.
|
||||
``out`` and ``err`` will be ``text`` objects.
|
||||
"""
|
||||
_ensure_only_one_capture_fixture(request, "capfd")
|
||||
if not hasattr(os, "dup"):
|
||||
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:
|
||||
capman = request.config.pluginmanager.getplugin("capturemanager")
|
||||
with capman._capturing_for_request(request) as fixture:
|
||||
yield fixture
|
||||
|
||||
|
||||
@@ -302,35 +321,15 @@ def capfdbinary(request):
|
||||
calls, which return a ``(out, err)`` namedtuple.
|
||||
``out`` and ``err`` will be ``byte`` 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:
|
||||
capman = request.config.pluginmanager.getplugin("capturemanager")
|
||||
with capman._capturing_for_request(request) 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 returned by :py:func:`capsys`, :py:func:`capsysbinary`, :py:func:`capfd` and :py:func:`capfdbinary`
|
||||
@@ -413,30 +412,27 @@ def safe_text_dupfile(f, mode, default_encoding="UTF8"):
|
||||
class EncodedFile:
|
||||
errors = "strict" # possibly needed by py3 code (issue555)
|
||||
|
||||
def __init__(self, buffer, encoding):
|
||||
def __init__(self, buffer: BinaryIO, encoding: str) -> None:
|
||||
self.buffer = buffer
|
||||
self.encoding = encoding
|
||||
|
||||
def write(self, obj):
|
||||
if isinstance(obj, str):
|
||||
obj = obj.encode(self.encoding, "replace")
|
||||
else:
|
||||
def write(self, s: str) -> int:
|
||||
if not isinstance(s, str):
|
||||
raise TypeError(
|
||||
"write() argument must be str, not {}".format(type(obj).__name__)
|
||||
"write() argument must be str, not {}".format(type(s).__name__)
|
||||
)
|
||||
self.buffer.write(obj)
|
||||
return self.buffer.write(s.encode(self.encoding, "replace"))
|
||||
|
||||
def writelines(self, linelist):
|
||||
data = "".join(linelist)
|
||||
self.write(data)
|
||||
def writelines(self, lines: Iterable[str]) -> None:
|
||||
self.buffer.writelines(x.encode(self.encoding, "replace") for x in lines)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
"""Ensure that file.name is a string."""
|
||||
return repr(self.buffer)
|
||||
|
||||
@property
|
||||
def mode(self):
|
||||
def mode(self) -> str:
|
||||
return self.buffer.mode.replace("b", "")
|
||||
|
||||
def __getattr__(self, name):
|
||||
@@ -449,6 +445,7 @@ CaptureResult = collections.namedtuple("CaptureResult", ["out", "err"])
|
||||
class MultiCapture:
|
||||
out = err = in_ = None
|
||||
_state = None
|
||||
_in_suspended = False
|
||||
|
||||
def __init__(self, out=True, err=True, in_=True, Capture=None):
|
||||
if in_:
|
||||
@@ -460,11 +457,7 @@ class MultiCapture:
|
||||
|
||||
def __repr__(self):
|
||||
return "<MultiCapture out={!r} err={!r} in_={!r} _state={!r} _in_suspended={!r}>".format(
|
||||
self.out,
|
||||
self.err,
|
||||
self.in_,
|
||||
self._state,
|
||||
getattr(self, "_in_suspended", "<UNSET>"),
|
||||
self.out, self.err, self.in_, self._state, self._in_suspended,
|
||||
)
|
||||
|
||||
def start_capturing(self):
|
||||
@@ -501,9 +494,9 @@ class MultiCapture:
|
||||
self.out.resume()
|
||||
if self.err:
|
||||
self.err.resume()
|
||||
if hasattr(self, "_in_suspended"):
|
||||
if self._in_suspended:
|
||||
self.in_.resume()
|
||||
del self._in_suspended
|
||||
self._in_suspended = False
|
||||
|
||||
def stop_capturing(self):
|
||||
""" stop capturing and reset capturing streams """
|
||||
@@ -517,12 +510,16 @@ class MultiCapture:
|
||||
if self.in_:
|
||||
self.in_.done()
|
||||
|
||||
def readouterr(self):
|
||||
""" return snapshot unicode value of stdout/stderr capturings. """
|
||||
return CaptureResult(
|
||||
self.out.snap() if self.out is not None else "",
|
||||
self.err.snap() if self.err is not None else "",
|
||||
)
|
||||
def readouterr(self) -> CaptureResult:
|
||||
if self.out:
|
||||
out = self.out.snap()
|
||||
else:
|
||||
out = ""
|
||||
if self.err:
|
||||
err = self.err.snap()
|
||||
else:
|
||||
err = ""
|
||||
return CaptureResult(out, err)
|
||||
|
||||
|
||||
class NoCapture:
|
||||
@@ -566,8 +563,12 @@ class FDCaptureBinary:
|
||||
self.tmpfile_fd = tmpfile.fileno()
|
||||
|
||||
def __repr__(self):
|
||||
return "<FDCapture {} oldfd={} _state={!r}>".format(
|
||||
self.targetfd, getattr(self, "targetfd_save", None), self._state
|
||||
return "<{} {} oldfd={} _state={!r} tmpfile={}>".format(
|
||||
self.__class__.__name__,
|
||||
self.targetfd,
|
||||
getattr(self, "targetfd_save", "<UNSET>"),
|
||||
self._state,
|
||||
hasattr(self, "tmpfile") and repr(self.tmpfile) or "<UNSET>",
|
||||
)
|
||||
|
||||
def _start(self):
|
||||
@@ -609,8 +610,6 @@ class FDCaptureBinary:
|
||||
|
||||
def writeorg(self, data):
|
||||
""" write to original file descriptor. """
|
||||
if isinstance(data, str):
|
||||
data = data.encode("utf8") # XXX use encoding of original stream
|
||||
os.write(self.targetfd_save, data)
|
||||
|
||||
|
||||
@@ -630,10 +629,15 @@ class FDCapture(FDCaptureBinary):
|
||||
res = str(res, enc, "replace")
|
||||
return res
|
||||
|
||||
def writeorg(self, data):
|
||||
""" write to original file descriptor. """
|
||||
data = data.encode("utf-8") # XXX use encoding of original stream
|
||||
os.write(self.targetfd_save, data)
|
||||
|
||||
class SysCapture:
|
||||
|
||||
EMPTY_BUFFER = str()
|
||||
class SysCaptureBinary:
|
||||
|
||||
EMPTY_BUFFER = b""
|
||||
_state = None
|
||||
|
||||
def __init__(self, fd, tmpfile=None):
|
||||
@@ -648,8 +652,12 @@ class SysCapture:
|
||||
self.tmpfile = tmpfile
|
||||
|
||||
def __repr__(self):
|
||||
return "<SysCapture {} _old={!r}, tmpfile={!r} _state={!r}>".format(
|
||||
self.name, self._old, self.tmpfile, self._state
|
||||
return "<{} {} _old={} _state={!r} tmpfile={!r}>".format(
|
||||
self.__class__.__name__,
|
||||
self.name,
|
||||
hasattr(self, "_old") and repr(self._old) or "<UNSET>",
|
||||
self._state,
|
||||
self.tmpfile,
|
||||
)
|
||||
|
||||
def start(self):
|
||||
@@ -657,7 +665,7 @@ class SysCapture:
|
||||
self._state = "started"
|
||||
|
||||
def snap(self):
|
||||
res = self.tmpfile.getvalue()
|
||||
res = self.tmpfile.buffer.getvalue()
|
||||
self.tmpfile.seek(0)
|
||||
self.tmpfile.truncate()
|
||||
return res
|
||||
@@ -676,20 +684,45 @@ class SysCapture:
|
||||
setattr(sys, self.name, self.tmpfile)
|
||||
self._state = "resumed"
|
||||
|
||||
def writeorg(self, data):
|
||||
self._old.flush()
|
||||
self._old.buffer.write(data)
|
||||
self._old.buffer.flush()
|
||||
|
||||
|
||||
class SysCapture(SysCaptureBinary):
|
||||
EMPTY_BUFFER = str() # type: ignore[assignment] # noqa: F821
|
||||
|
||||
def snap(self):
|
||||
res = self.tmpfile.getvalue()
|
||||
self.tmpfile.seek(0)
|
||||
self.tmpfile.truncate()
|
||||
return res
|
||||
|
||||
def writeorg(self, data):
|
||||
self._old.write(data)
|
||||
self._old.flush()
|
||||
|
||||
|
||||
class SysCaptureBinary(SysCapture):
|
||||
# Ignore type because it doesn't match the type in the superclass (str).
|
||||
EMPTY_BUFFER = b"" # type: ignore
|
||||
class TeeSysCapture(SysCapture):
|
||||
def __init__(self, fd, tmpfile=None):
|
||||
name = patchsysdict[fd]
|
||||
self._old = getattr(sys, name)
|
||||
self.name = name
|
||||
if tmpfile is None:
|
||||
if name == "stdin":
|
||||
tmpfile = DontReadFromInput()
|
||||
else:
|
||||
tmpfile = CaptureAndPassthroughIO(self._old)
|
||||
self.tmpfile = tmpfile
|
||||
|
||||
def snap(self):
|
||||
res = self.tmpfile.buffer.getvalue()
|
||||
self.tmpfile.seek(0)
|
||||
self.tmpfile.truncate()
|
||||
return res
|
||||
|
||||
map_fixname_class = {
|
||||
"capfd": FDCapture,
|
||||
"capfdbinary": FDCaptureBinary,
|
||||
"capsys": SysCapture,
|
||||
"capsysbinary": SysCaptureBinary,
|
||||
}
|
||||
|
||||
|
||||
class DontReadFromInput:
|
||||
|
||||
@@ -13,6 +13,7 @@ from inspect import signature
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Generic
|
||||
from typing import IO
|
||||
from typing import Optional
|
||||
from typing import overload
|
||||
from typing import Tuple
|
||||
@@ -22,7 +23,6 @@ from typing import Union
|
||||
import attr
|
||||
import py
|
||||
|
||||
import _pytest
|
||||
from _pytest._io.saferepr import saferepr
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.outcomes import TEST_OUTCOME
|
||||
@@ -93,12 +93,21 @@ def iscoroutinefunction(func: object) -> bool:
|
||||
return inspect.iscoroutinefunction(func) or getattr(func, "_is_coroutine", False)
|
||||
|
||||
|
||||
def is_async_function(func: object) -> bool:
|
||||
"""Return True if the given function seems to be an async function or async generator"""
|
||||
return iscoroutinefunction(func) or (
|
||||
sys.version_info >= (3, 6) and inspect.isasyncgenfunction(func)
|
||||
)
|
||||
|
||||
|
||||
def getlocation(function, curdir=None) -> str:
|
||||
function = get_real_func(function)
|
||||
fn = py.path.local(inspect.getfile(function))
|
||||
lineno = function.__code__.co_firstlineno
|
||||
if curdir is not None and fn.relto(curdir):
|
||||
fn = fn.relto(curdir)
|
||||
if curdir is not None:
|
||||
relfn = fn.relto(curdir)
|
||||
if relfn:
|
||||
return "%s:%d" % (relfn, lineno + 1)
|
||||
return "%s:%d" % (fn, lineno + 1)
|
||||
|
||||
|
||||
@@ -141,12 +150,12 @@ def getfuncargnames(
|
||||
the case of cls, the function is a static method.
|
||||
|
||||
The name parameter should be the original name in which the function was collected.
|
||||
|
||||
@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.
|
||||
"""
|
||||
# TODO(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
|
||||
@@ -225,7 +234,7 @@ def _bytes_to_ascii(val: bytes) -> str:
|
||||
return val.decode("ascii", "backslashreplace")
|
||||
|
||||
|
||||
def ascii_escaped(val: Union[bytes, str]):
|
||||
def ascii_escaped(val: Union[bytes, str]) -> str:
|
||||
"""If val is pure ascii, returns it as a str(). Otherwise, escapes
|
||||
bytes objects into a sequence of escaped bytes:
|
||||
|
||||
@@ -305,16 +314,6 @@ def get_real_method(obj, holder):
|
||||
return obj
|
||||
|
||||
|
||||
def getfslineno(obj):
|
||||
# xxx let decorators etc specify a sane ordering
|
||||
obj = get_real_func(obj)
|
||||
if hasattr(obj, "place_as"):
|
||||
obj = obj.place_as
|
||||
fslineno = _pytest._code.getfslineno(obj)
|
||||
assert isinstance(fslineno[1], int), obj
|
||||
return fslineno
|
||||
|
||||
|
||||
def getimfunc(func):
|
||||
try:
|
||||
return func.__func__
|
||||
@@ -377,6 +376,16 @@ class CaptureIO(io.TextIOWrapper):
|
||||
return self.buffer.getvalue().decode("UTF-8")
|
||||
|
||||
|
||||
class CaptureAndPassthroughIO(CaptureIO):
|
||||
def __init__(self, other: IO) -> None:
|
||||
self._other = other
|
||||
super().__init__()
|
||||
|
||||
def write(self, s) -> int:
|
||||
super().write(s)
|
||||
return self._other.write(s)
|
||||
|
||||
|
||||
if sys.version_info < (3, 5, 2):
|
||||
|
||||
def overload(f): # noqa: F811
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
""" command line options, ini-file and conftest.py processing. """
|
||||
import argparse
|
||||
import copy
|
||||
import enum
|
||||
import inspect
|
||||
import os
|
||||
import shlex
|
||||
@@ -27,7 +28,6 @@ from pluggy import HookspecMarker
|
||||
from pluggy import PluginManager
|
||||
|
||||
import _pytest._code
|
||||
import _pytest.assertion
|
||||
import _pytest.deprecated
|
||||
import _pytest.hookspec # the extension point definitions
|
||||
from .exceptions import PrintHelp
|
||||
@@ -36,21 +36,55 @@ from .findpaths import determine_setup
|
||||
from .findpaths import exists
|
||||
from _pytest._code import ExceptionInfo
|
||||
from _pytest._code import filter_traceback
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest.compat import importlib_metadata
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.outcomes import Skipped
|
||||
from _pytest.pathlib import Path
|
||||
from _pytest.store import Store
|
||||
from _pytest.warning_types import PytestConfigWarning
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Type
|
||||
|
||||
from .argparsing import Argument
|
||||
|
||||
|
||||
_PluggyPlugin = object
|
||||
"""A type to represent plugin objects.
|
||||
Plugins can be any namespace, so we can't narrow it down much, but we use an
|
||||
alias to make the intent clear.
|
||||
Ideally this type would be provided by pluggy itself."""
|
||||
|
||||
|
||||
hookimpl = HookimplMarker("pytest")
|
||||
hookspec = HookspecMarker("pytest")
|
||||
|
||||
|
||||
class ExitCode(enum.IntEnum):
|
||||
"""
|
||||
.. versionadded:: 5.0
|
||||
|
||||
Encodes the valid exit codes by pytest.
|
||||
|
||||
Currently users and plugins may supply other exit codes as well.
|
||||
"""
|
||||
|
||||
#: tests passed
|
||||
OK = 0
|
||||
#: tests failed
|
||||
TESTS_FAILED = 1
|
||||
#: pytest was interrupted
|
||||
INTERRUPTED = 2
|
||||
#: an internal error got in the way
|
||||
INTERNAL_ERROR = 3
|
||||
#: pytest was misused
|
||||
USAGE_ERROR = 4
|
||||
#: pytest couldn't find tests
|
||||
NO_TESTS_COLLECTED = 5
|
||||
|
||||
|
||||
class ConftestImportFailure(Exception):
|
||||
def __init__(self, path, excinfo):
|
||||
Exception.__init__(self, path, excinfo)
|
||||
@@ -58,7 +92,7 @@ class ConftestImportFailure(Exception):
|
||||
self.excinfo = excinfo # type: Tuple[Type[Exception], Exception, TracebackType]
|
||||
|
||||
|
||||
def main(args=None, plugins=None) -> "Union[int, _pytest.main.ExitCode]":
|
||||
def main(args=None, plugins=None) -> Union[int, ExitCode]:
|
||||
""" return exit code, after performing an in-process test run.
|
||||
|
||||
:arg args: list of command line arguments.
|
||||
@@ -66,14 +100,12 @@ def main(args=None, plugins=None) -> "Union[int, _pytest.main.ExitCode]":
|
||||
:arg plugins: list of plugin objects to be auto-registered during
|
||||
initialization.
|
||||
"""
|
||||
from _pytest.main import ExitCode
|
||||
|
||||
try:
|
||||
try:
|
||||
config = _prepareconfig(args, plugins)
|
||||
except ConftestImportFailure as e:
|
||||
exc_info = ExceptionInfo(e.excinfo)
|
||||
tw = py.io.TerminalWriter(sys.stderr)
|
||||
tw = TerminalWriter(sys.stderr)
|
||||
tw.line(
|
||||
"ImportError while loading conftest '{e.path}'.".format(e=e), red=True
|
||||
)
|
||||
@@ -99,7 +131,7 @@ def main(args=None, plugins=None) -> "Union[int, _pytest.main.ExitCode]":
|
||||
finally:
|
||||
config._ensure_unconfigure()
|
||||
except UsageError as e:
|
||||
tw = py.io.TerminalWriter(sys.stderr)
|
||||
tw = TerminalWriter(sys.stderr)
|
||||
for msg in e.args:
|
||||
tw.line("ERROR: {}\n".format(msg), red=True)
|
||||
return ExitCode.USAGE_ERROR
|
||||
@@ -183,7 +215,7 @@ def get_config(args=None, plugins=None):
|
||||
|
||||
if args is not None:
|
||||
# Handle any "-p no:plugin" args.
|
||||
pluginmanager.consider_preparse(args)
|
||||
pluginmanager.consider_preparse(args, exclude_only=True)
|
||||
|
||||
for spec in default_plugins:
|
||||
pluginmanager.import_plugin(spec)
|
||||
@@ -202,13 +234,15 @@ def get_plugin_manager():
|
||||
return get_config().pluginmanager
|
||||
|
||||
|
||||
def _prepareconfig(args=None, plugins=None):
|
||||
def _prepareconfig(
|
||||
args: Optional[Union[py.path.local, List[str]]] = None, plugins=None
|
||||
):
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
elif isinstance(args, py.path.local):
|
||||
args = [str(args)]
|
||||
elif not isinstance(args, (tuple, list)):
|
||||
msg = "`args` parameter expected to be a list or tuple of strings, got: {!r} (type: {})"
|
||||
elif not isinstance(args, list):
|
||||
msg = "`args` parameter expected to be a list of strings, got: {!r} (type: {})"
|
||||
raise TypeError(msg.format(args, type(args)))
|
||||
|
||||
config = get_config(args, plugins)
|
||||
@@ -252,6 +286,8 @@ class PytestPluginManager(PluginManager):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
import _pytest.assertion
|
||||
|
||||
super().__init__("pytest")
|
||||
# The objects are module objects, only used generically.
|
||||
self._conftest_plugins = set() # type: Set[object]
|
||||
@@ -480,7 +516,7 @@ class PytestPluginManager(PluginManager):
|
||||
if path and path.relto(dirpath) or path == dirpath:
|
||||
assert mod not in mods
|
||||
mods.append(mod)
|
||||
self.trace("loaded conftestmodule %r" % (mod))
|
||||
self.trace("loading conftestmodule {!r}".format(mod))
|
||||
self.consider_conftest(mod)
|
||||
return mod
|
||||
|
||||
@@ -489,7 +525,7 @@ class PytestPluginManager(PluginManager):
|
||||
#
|
||||
#
|
||||
|
||||
def consider_preparse(self, args):
|
||||
def consider_preparse(self, args, *, exclude_only=False):
|
||||
i = 0
|
||||
n = len(args)
|
||||
while i < n:
|
||||
@@ -506,6 +542,8 @@ class PytestPluginManager(PluginManager):
|
||||
parg = opt[2:]
|
||||
else:
|
||||
continue
|
||||
if exclude_only and not parg.startswith("no:"):
|
||||
continue
|
||||
self.consider_pluginarg(parg)
|
||||
|
||||
def consider_pluginarg(self, arg):
|
||||
@@ -574,13 +612,9 @@ class PytestPluginManager(PluginManager):
|
||||
try:
|
||||
__import__(importspec)
|
||||
except ImportError as e:
|
||||
new_exc_message = 'Error importing plugin "{}": {}'.format(
|
||||
modname, str(e.args[0])
|
||||
)
|
||||
new_exc = ImportError(new_exc_message)
|
||||
tb = sys.exc_info()[2]
|
||||
|
||||
raise new_exc.with_traceback(tb)
|
||||
raise ImportError(
|
||||
'Error importing plugin "{}": {}'.format(modname, str(e.args[0]))
|
||||
).with_traceback(e.__traceback__)
|
||||
|
||||
except Skipped as e:
|
||||
from _pytest.warnings import _issue_warning_captured
|
||||
@@ -588,7 +622,7 @@ class PytestPluginManager(PluginManager):
|
||||
_issue_warning_captured(
|
||||
PytestConfigWarning("skipped plugin {!r}: {}".format(modname, e.msg)),
|
||||
self.hook,
|
||||
stacklevel=1,
|
||||
stacklevel=2,
|
||||
)
|
||||
else:
|
||||
mod = sys.modules[importspec]
|
||||
@@ -731,7 +765,7 @@ class Config:
|
||||
plugins = attr.ib()
|
||||
dir = attr.ib(type=Path)
|
||||
|
||||
def __init__(self, pluginmanager, *, invocation_params=None):
|
||||
def __init__(self, pluginmanager, *, invocation_params=None) -> None:
|
||||
from .argparsing import Parser, FILE_OR_DIR
|
||||
|
||||
if invocation_params is None:
|
||||
@@ -754,12 +788,20 @@ class Config:
|
||||
self._override_ini = () # type: Sequence[str]
|
||||
self._opt2dest = {} # type: Dict[str, str]
|
||||
self._cleanup = [] # type: List[Callable[[], None]]
|
||||
# A place where plugins can store information on the config for their
|
||||
# own use. Currently only intended for internal plugins.
|
||||
self._store = Store()
|
||||
self.pluginmanager.register(self, "pytestconfig")
|
||||
self._configured = False
|
||||
self.hook.pytest_addoption.call_historic(
|
||||
kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager)
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.cacheprovider import Cache
|
||||
|
||||
self.cache = None # type: Optional[Cache]
|
||||
|
||||
@property
|
||||
def invocation_dir(self):
|
||||
"""Backward compatibility"""
|
||||
@@ -844,11 +886,11 @@ class Config:
|
||||
config.pluginmanager.consider_pluginarg(x)
|
||||
return config
|
||||
|
||||
def _processopt(self, opt):
|
||||
def _processopt(self, opt: "Argument") -> None:
|
||||
for name in opt._short_opts + opt._long_opts:
|
||||
self._opt2dest[name] = opt.dest
|
||||
|
||||
if hasattr(opt, "default") and opt.dest:
|
||||
if hasattr(opt, "default"):
|
||||
if not hasattr(self.option, opt.dest):
|
||||
setattr(self.option, opt.dest, opt.default)
|
||||
|
||||
@@ -856,7 +898,7 @@ class Config:
|
||||
def pytest_load_initial_conftests(self, early_config):
|
||||
self.pluginmanager._set_initial_conftests(early_config.known_args_namespace)
|
||||
|
||||
def _initini(self, args) -> None:
|
||||
def _initini(self, args: Sequence[str]) -> None:
|
||||
ns, unknown_args = self._parser.parse_known_and_unknown_args(
|
||||
args, namespace=copy.copy(self.option)
|
||||
)
|
||||
@@ -873,7 +915,7 @@ class Config:
|
||||
self._parser.addini("minversion", "minimally required pytest version")
|
||||
self._override_ini = ns.override_ini or ()
|
||||
|
||||
def _consider_importhook(self, args):
|
||||
def _consider_importhook(self, args: Sequence[str]) -> None:
|
||||
"""Install the PEP 302 import hook if using assertion rewriting.
|
||||
|
||||
Needs to parse the --assert=<mode> option from the commandline
|
||||
@@ -883,6 +925,8 @@ class Config:
|
||||
ns, unknown_args = self._parser.parse_known_and_unknown_args(args)
|
||||
mode = getattr(ns, "assertmode", "plain")
|
||||
if mode == "rewrite":
|
||||
import _pytest.assertion
|
||||
|
||||
try:
|
||||
hook = _pytest.assertion.install_importhook(self)
|
||||
except SystemError:
|
||||
@@ -913,19 +957,19 @@ class Config:
|
||||
for name in _iter_rewritable_modules(package_files):
|
||||
hook.mark_rewrite(name)
|
||||
|
||||
def _validate_args(self, args, via):
|
||||
def _validate_args(self, args: List[str], via: str) -> List[str]:
|
||||
"""Validate known args."""
|
||||
self._parser._config_source_hint = via
|
||||
self._parser._config_source_hint = via # type: ignore
|
||||
try:
|
||||
self._parser.parse_known_and_unknown_args(
|
||||
args, namespace=copy.copy(self.option)
|
||||
)
|
||||
finally:
|
||||
del self._parser._config_source_hint
|
||||
del self._parser._config_source_hint # type: ignore
|
||||
|
||||
return args
|
||||
|
||||
def _preparse(self, args, addopts=True):
|
||||
def _preparse(self, args: List[str], addopts: bool = True) -> None:
|
||||
if addopts:
|
||||
env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
|
||||
if len(env_addopts):
|
||||
@@ -941,7 +985,7 @@ class Config:
|
||||
|
||||
self._checkversion()
|
||||
self._consider_importhook(args)
|
||||
self.pluginmanager.consider_preparse(args)
|
||||
self.pluginmanager.consider_preparse(args, exclude_only=False)
|
||||
if not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
|
||||
# Don't autoload from setuptools entry point. Only explicitly specified
|
||||
# plugins are going to be loaded.
|
||||
@@ -989,7 +1033,7 @@ class Config:
|
||||
)
|
||||
)
|
||||
|
||||
def parse(self, args, addopts=True):
|
||||
def parse(self, args: List[str], addopts: bool = True) -> None:
|
||||
# parse given cmdline arguments into this config object.
|
||||
assert not hasattr(
|
||||
self, "args"
|
||||
@@ -1000,7 +1044,7 @@ class Config:
|
||||
self._preparse(args, addopts=addopts)
|
||||
# XXX deprecated hook:
|
||||
self.hook.pytest_cmdline_preparse(config=self, args=args)
|
||||
self._parser.after_preparse = True
|
||||
self._parser.after_preparse = True # type: ignore
|
||||
try:
|
||||
args = self._parser.parse_setoption(
|
||||
args, self.option, namespace=self.option
|
||||
@@ -1087,7 +1131,11 @@ class Config:
|
||||
try:
|
||||
key, user_ini_value = ini_config.split("=", 1)
|
||||
except ValueError:
|
||||
raise UsageError("-o/--override-ini expects option=value style.")
|
||||
raise UsageError(
|
||||
"-o/--override-ini expects option=value style (got: {!r}).".format(
|
||||
ini_config
|
||||
)
|
||||
)
|
||||
else:
|
||||
if key == name:
|
||||
value = user_ini_value
|
||||
@@ -1153,34 +1201,12 @@ def _warn_about_missing_assertion(mode):
|
||||
)
|
||||
|
||||
|
||||
def setns(obj, dic):
|
||||
import pytest
|
||||
|
||||
for name, value in dic.items():
|
||||
if isinstance(value, dict):
|
||||
mod = getattr(obj, name, None)
|
||||
if mod is None:
|
||||
modname = "pytest.%s" % name
|
||||
mod = types.ModuleType(modname)
|
||||
sys.modules[modname] = mod
|
||||
mod.__all__ = []
|
||||
setattr(obj, name, mod)
|
||||
obj.__all__.append(name)
|
||||
setns(mod, value)
|
||||
else:
|
||||
setattr(obj, name, value)
|
||||
obj.__all__.append(name)
|
||||
# if obj != pytest:
|
||||
# pytest.__all__.append(name)
|
||||
setattr(pytest, name, value)
|
||||
|
||||
|
||||
def create_terminal_writer(config, *args, **kwargs):
|
||||
def create_terminal_writer(config: Config, *args, **kwargs) -> TerminalWriter:
|
||||
"""Create a TerminalWriter instance configured according to the options
|
||||
in the config object. Every code which requires a TerminalWriter object
|
||||
and has access to a config object should use this function.
|
||||
"""
|
||||
tw = py.io.TerminalWriter(*args, **kwargs)
|
||||
tw = TerminalWriter(*args, **kwargs)
|
||||
if config.option.color == "yes":
|
||||
tw.hasmarkup = True
|
||||
if config.option.color == "no":
|
||||
|
||||
@@ -3,15 +3,25 @@ import sys
|
||||
import warnings
|
||||
from gettext import gettext
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
from typing import Union
|
||||
|
||||
import py
|
||||
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.config.exceptions import UsageError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import NoReturn
|
||||
from typing_extensions import Literal # noqa: F401
|
||||
|
||||
FILE_OR_DIR = "file_or_dir"
|
||||
|
||||
|
||||
@@ -22,9 +32,13 @@ class Parser:
|
||||
there's an error processing the command line arguments.
|
||||
"""
|
||||
|
||||
prog = None
|
||||
prog = None # type: Optional[str]
|
||||
|
||||
def __init__(self, usage=None, processopt=None):
|
||||
def __init__(
|
||||
self,
|
||||
usage: Optional[str] = None,
|
||||
processopt: Optional[Callable[["Argument"], None]] = None,
|
||||
) -> None:
|
||||
self._anonymous = OptionGroup("custom options", parser=self)
|
||||
self._groups = [] # type: List[OptionGroup]
|
||||
self._processopt = processopt
|
||||
@@ -33,12 +47,14 @@ class Parser:
|
||||
self._ininames = [] # type: List[str]
|
||||
self.extra_info = {} # type: Dict[str, Any]
|
||||
|
||||
def processoption(self, option):
|
||||
def processoption(self, option: "Argument") -> None:
|
||||
if self._processopt:
|
||||
if option.dest:
|
||||
self._processopt(option)
|
||||
|
||||
def getgroup(self, name, description="", after=None):
|
||||
def getgroup(
|
||||
self, name: str, description: str = "", after: Optional[str] = None
|
||||
) -> "OptionGroup":
|
||||
""" get (or create) a named option Group.
|
||||
|
||||
:name: name of the option group.
|
||||
@@ -61,13 +77,13 @@ class Parser:
|
||||
self._groups.insert(i + 1, group)
|
||||
return group
|
||||
|
||||
def addoption(self, *opts, **attrs):
|
||||
def addoption(self, *opts: str, **attrs: Any) -> None:
|
||||
""" register a command line option.
|
||||
|
||||
:opts: option names, can be short or long options.
|
||||
:attrs: same attributes which the ``add_option()`` function of the
|
||||
:attrs: same attributes which the ``add_argument()`` function of the
|
||||
`argparse library
|
||||
<http://docs.python.org/2/library/argparse.html>`_
|
||||
<https://docs.python.org/library/argparse.html>`_
|
||||
accepts.
|
||||
|
||||
After command line parsing options are available on the pytest config
|
||||
@@ -77,13 +93,17 @@ class Parser:
|
||||
"""
|
||||
self._anonymous.addoption(*opts, **attrs)
|
||||
|
||||
def parse(self, args, namespace=None):
|
||||
def parse(
|
||||
self,
|
||||
args: Sequence[Union[str, py.path.local]],
|
||||
namespace: Optional[argparse.Namespace] = None,
|
||||
) -> argparse.Namespace:
|
||||
from _pytest._argcomplete import try_argcomplete
|
||||
|
||||
self.optparser = self._getparser()
|
||||
try_argcomplete(self.optparser)
|
||||
args = [str(x) if isinstance(x, py.path.local) else x for x in args]
|
||||
return self.optparser.parse_args(args, namespace=namespace)
|
||||
strargs = [str(x) if isinstance(x, py.path.local) else x for x in args]
|
||||
return self.optparser.parse_args(strargs, namespace=namespace)
|
||||
|
||||
def _getparser(self) -> "MyOptionParser":
|
||||
from _pytest._argcomplete import filescompleter
|
||||
@@ -98,36 +118,52 @@ class Parser:
|
||||
n = option.names()
|
||||
a = option.attrs()
|
||||
arggroup.add_argument(*n, **a)
|
||||
file_or_dir_arg = optparser.add_argument(FILE_OR_DIR, nargs="*")
|
||||
# bash like autocompletion for dirs (appending '/')
|
||||
# Type ignored because typeshed doesn't know about argcomplete.
|
||||
optparser.add_argument( # type: ignore
|
||||
FILE_OR_DIR, nargs="*"
|
||||
).completer = filescompleter
|
||||
file_or_dir_arg.completer = filescompleter # type: ignore
|
||||
return optparser
|
||||
|
||||
def parse_setoption(self, args, option, namespace=None):
|
||||
def parse_setoption(
|
||||
self,
|
||||
args: Sequence[Union[str, py.path.local]],
|
||||
option: argparse.Namespace,
|
||||
namespace: Optional[argparse.Namespace] = None,
|
||||
) -> List[str]:
|
||||
parsedoption = self.parse(args, namespace=namespace)
|
||||
for name, value in parsedoption.__dict__.items():
|
||||
setattr(option, name, value)
|
||||
return getattr(parsedoption, FILE_OR_DIR)
|
||||
return cast(List[str], getattr(parsedoption, FILE_OR_DIR))
|
||||
|
||||
def parse_known_args(self, args, namespace=None) -> argparse.Namespace:
|
||||
def parse_known_args(
|
||||
self,
|
||||
args: Sequence[Union[str, py.path.local]],
|
||||
namespace: Optional[argparse.Namespace] = None,
|
||||
) -> argparse.Namespace:
|
||||
"""parses and returns a namespace object with known arguments at this
|
||||
point.
|
||||
"""
|
||||
return self.parse_known_and_unknown_args(args, namespace=namespace)[0]
|
||||
|
||||
def parse_known_and_unknown_args(
|
||||
self, args, namespace=None
|
||||
self,
|
||||
args: Sequence[Union[str, py.path.local]],
|
||||
namespace: Optional[argparse.Namespace] = None,
|
||||
) -> Tuple[argparse.Namespace, List[str]]:
|
||||
"""parses and returns a namespace object with known arguments, and
|
||||
the remaining arguments unknown at this point.
|
||||
"""
|
||||
optparser = self._getparser()
|
||||
args = [str(x) if isinstance(x, py.path.local) else x for x in args]
|
||||
return optparser.parse_known_args(args, namespace=namespace)
|
||||
strargs = [str(x) if isinstance(x, py.path.local) else x for x in args]
|
||||
return optparser.parse_known_args(strargs, namespace=namespace)
|
||||
|
||||
def addini(self, name, help, type=None, default=None):
|
||||
def addini(
|
||||
self,
|
||||
name: str,
|
||||
help: str,
|
||||
type: Optional["Literal['pathlist', 'args', 'linelist', 'bool']"] = None,
|
||||
default=None,
|
||||
) -> None:
|
||||
""" register an ini-file option.
|
||||
|
||||
:name: name of the ini-variable
|
||||
@@ -149,11 +185,11 @@ class ArgumentError(Exception):
|
||||
inconsistent arguments.
|
||||
"""
|
||||
|
||||
def __init__(self, msg, option):
|
||||
def __init__(self, msg: str, option: Union["Argument", str]) -> None:
|
||||
self.msg = msg
|
||||
self.option_id = str(option)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
if self.option_id:
|
||||
return "option {}: {}".format(self.option_id, self.msg)
|
||||
else:
|
||||
@@ -170,12 +206,11 @@ class Argument:
|
||||
|
||||
_typ_map = {"int": int, "string": str, "float": float, "complex": complex}
|
||||
|
||||
def __init__(self, *names, **attrs):
|
||||
def __init__(self, *names: str, **attrs: Any) -> None:
|
||||
"""store parms in private vars for use in add_argument"""
|
||||
self._attrs = attrs
|
||||
self._short_opts = [] # type: List[str]
|
||||
self._long_opts = [] # type: List[str]
|
||||
self.dest = attrs.get("dest")
|
||||
if "%default" in (attrs.get("help") or ""):
|
||||
warnings.warn(
|
||||
'pytest now uses argparse. "%default" should be'
|
||||
@@ -221,23 +256,25 @@ class Argument:
|
||||
except KeyError:
|
||||
pass
|
||||
self._set_opt_strings(names)
|
||||
if not self.dest:
|
||||
if self._long_opts:
|
||||
self.dest = self._long_opts[0][2:].replace("-", "_")
|
||||
else:
|
||||
try:
|
||||
self.dest = self._short_opts[0][1:]
|
||||
except IndexError:
|
||||
raise ArgumentError("need a long or short option", self)
|
||||
dest = attrs.get("dest") # type: Optional[str]
|
||||
if dest:
|
||||
self.dest = dest
|
||||
elif self._long_opts:
|
||||
self.dest = self._long_opts[0][2:].replace("-", "_")
|
||||
else:
|
||||
try:
|
||||
self.dest = self._short_opts[0][1:]
|
||||
except IndexError:
|
||||
self.dest = "???" # Needed for the error repr.
|
||||
raise ArgumentError("need a long or short option", self)
|
||||
|
||||
def names(self):
|
||||
def names(self) -> List[str]:
|
||||
return self._short_opts + self._long_opts
|
||||
|
||||
def attrs(self):
|
||||
def attrs(self) -> Mapping[str, Any]:
|
||||
# update any attributes set by processopt
|
||||
attrs = "default dest help".split()
|
||||
if self.dest:
|
||||
attrs.append(self.dest)
|
||||
attrs.append(self.dest)
|
||||
for attr in attrs:
|
||||
try:
|
||||
self._attrs[attr] = getattr(self, attr)
|
||||
@@ -250,7 +287,7 @@ class Argument:
|
||||
self._attrs["help"] = a
|
||||
return self._attrs
|
||||
|
||||
def _set_opt_strings(self, opts):
|
||||
def _set_opt_strings(self, opts: Sequence[str]) -> None:
|
||||
"""directly from optparse
|
||||
|
||||
might not be necessary as this is passed to argparse later on"""
|
||||
@@ -293,13 +330,15 @@ class Argument:
|
||||
|
||||
|
||||
class OptionGroup:
|
||||
def __init__(self, name, description="", parser=None):
|
||||
def __init__(
|
||||
self, name: str, description: str = "", parser: Optional[Parser] = None
|
||||
) -> None:
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.options = [] # type: List[Argument]
|
||||
self.parser = parser
|
||||
|
||||
def addoption(self, *optnames, **attrs):
|
||||
def addoption(self, *optnames: str, **attrs: Any) -> None:
|
||||
""" add an option to this group.
|
||||
|
||||
if a shortened version of a long option is specified it will
|
||||
@@ -315,11 +354,11 @@ class OptionGroup:
|
||||
option = Argument(*optnames, **attrs)
|
||||
self._addoption_instance(option, shortupper=False)
|
||||
|
||||
def _addoption(self, *optnames, **attrs):
|
||||
def _addoption(self, *optnames: str, **attrs: Any) -> None:
|
||||
option = Argument(*optnames, **attrs)
|
||||
self._addoption_instance(option, shortupper=True)
|
||||
|
||||
def _addoption_instance(self, option, shortupper=False):
|
||||
def _addoption_instance(self, option: "Argument", shortupper: bool = False) -> None:
|
||||
if not shortupper:
|
||||
for opt in option._short_opts:
|
||||
if opt[0] == "-" and opt[1].islower():
|
||||
@@ -330,9 +369,12 @@ class OptionGroup:
|
||||
|
||||
|
||||
class MyOptionParser(argparse.ArgumentParser):
|
||||
def __init__(self, parser, extra_info=None, prog=None):
|
||||
if not extra_info:
|
||||
extra_info = {}
|
||||
def __init__(
|
||||
self,
|
||||
parser: Parser,
|
||||
extra_info: Optional[Dict[str, Any]] = None,
|
||||
prog: Optional[str] = None,
|
||||
) -> None:
|
||||
self._parser = parser
|
||||
argparse.ArgumentParser.__init__(
|
||||
self,
|
||||
@@ -344,34 +386,42 @@ class MyOptionParser(argparse.ArgumentParser):
|
||||
)
|
||||
# extra_info is a dict of (param -> value) to display if there's
|
||||
# an usage error to provide more contextual information to the user
|
||||
self.extra_info = extra_info
|
||||
self.extra_info = extra_info if extra_info else {}
|
||||
|
||||
def error(self, message):
|
||||
def error(self, message: str) -> "NoReturn":
|
||||
"""Transform argparse error message into UsageError."""
|
||||
msg = "{}: error: {}".format(self.prog, message)
|
||||
|
||||
if hasattr(self._parser, "_config_source_hint"):
|
||||
msg = "{} ({})".format(msg, self._parser._config_source_hint)
|
||||
# Type ignored because the attribute is set dynamically.
|
||||
msg = "{} ({})".format(msg, self._parser._config_source_hint) # type: ignore
|
||||
|
||||
raise UsageError(self.format_usage() + msg)
|
||||
|
||||
def parse_args(self, args=None, namespace=None):
|
||||
# Type ignored because typeshed has a very complex type in the superclass.
|
||||
def parse_args( # type: ignore
|
||||
self,
|
||||
args: Optional[Sequence[str]] = None,
|
||||
namespace: Optional[argparse.Namespace] = None,
|
||||
) -> argparse.Namespace:
|
||||
"""allow splitting of positional arguments"""
|
||||
args, argv = self.parse_known_args(args, namespace)
|
||||
if argv:
|
||||
for arg in argv:
|
||||
parsed, unrecognized = self.parse_known_args(args, namespace)
|
||||
if unrecognized:
|
||||
for arg in unrecognized:
|
||||
if arg and arg[0] == "-":
|
||||
lines = ["unrecognized arguments: %s" % (" ".join(argv))]
|
||||
lines = ["unrecognized arguments: %s" % (" ".join(unrecognized))]
|
||||
for k, v in sorted(self.extra_info.items()):
|
||||
lines.append(" {}: {}".format(k, v))
|
||||
self.error("\n".join(lines))
|
||||
getattr(args, FILE_OR_DIR).extend(argv)
|
||||
return args
|
||||
getattr(parsed, FILE_OR_DIR).extend(unrecognized)
|
||||
return parsed
|
||||
|
||||
if sys.version_info[:2] < (3, 9): # pragma: no cover
|
||||
# Backport of https://github.com/python/cpython/pull/14316 so we can
|
||||
# disable long --argument abbreviations without breaking short flags.
|
||||
def _parse_optional(self, arg_string):
|
||||
def _parse_optional(
|
||||
self, arg_string: str
|
||||
) -> Optional[Tuple[Optional[argparse.Action], str, Optional[str]]]:
|
||||
if not arg_string:
|
||||
return None
|
||||
if not arg_string[0] in self.prefix_chars:
|
||||
@@ -409,49 +459,45 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter):
|
||||
"""shorten help for long options that differ only in extra hyphens
|
||||
|
||||
- collapse **long** options that are the same except for extra hyphens
|
||||
- special action attribute map_long_option allows suppressing additional
|
||||
long options
|
||||
- shortcut if there are only two options and one of them is a short one
|
||||
- cache result on action object as this is called at least 2 times
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Use more accurate terminal width via pylib."""
|
||||
if "width" not in kwargs:
|
||||
kwargs["width"] = py.io.get_terminal_width()
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _format_action_invocation(self, action):
|
||||
def _format_action_invocation(self, action: argparse.Action) -> str:
|
||||
orgstr = argparse.HelpFormatter._format_action_invocation(self, action)
|
||||
if orgstr and orgstr[0] != "-": # only optional arguments
|
||||
return orgstr
|
||||
res = getattr(action, "_formatted_action_invocation", None)
|
||||
res = getattr(
|
||||
action, "_formatted_action_invocation", None
|
||||
) # type: Optional[str]
|
||||
if res:
|
||||
return res
|
||||
options = orgstr.split(", ")
|
||||
if len(options) == 2 and (len(options[0]) == 2 or len(options[1]) == 2):
|
||||
# a shortcut for '-h, --help' or '--abc', '-a'
|
||||
action._formatted_action_invocation = orgstr
|
||||
action._formatted_action_invocation = orgstr # type: ignore
|
||||
return orgstr
|
||||
return_list = []
|
||||
option_map = getattr(action, "map_long_option", {})
|
||||
if option_map is None:
|
||||
option_map = {}
|
||||
short_long = {} # type: Dict[str, str]
|
||||
for option in options:
|
||||
if len(option) == 2 or option[2] == " ":
|
||||
continue
|
||||
if not option.startswith("--"):
|
||||
raise ArgumentError(
|
||||
'long optional argument without "--": [%s]' % (option), self
|
||||
'long optional argument without "--": [%s]' % (option), option
|
||||
)
|
||||
xxoption = option[2:]
|
||||
if xxoption.split()[0] not in option_map:
|
||||
shortened = xxoption.replace("-", "")
|
||||
if shortened not in short_long or len(short_long[shortened]) < len(
|
||||
xxoption
|
||||
):
|
||||
short_long[shortened] = xxoption
|
||||
shortened = xxoption.replace("-", "")
|
||||
if shortened not in short_long or len(short_long[shortened]) < len(
|
||||
xxoption
|
||||
):
|
||||
short_long[shortened] = xxoption
|
||||
# now short_long has been filled out to the longest with dashes
|
||||
# **and** we keep the right option ordering from add_argument
|
||||
for option in options:
|
||||
@@ -459,5 +505,6 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter):
|
||||
return_list.append(option)
|
||||
if option[2:] == short_long.get(option.replace("-", "")):
|
||||
return_list.append(option.replace(" ", "=", 1))
|
||||
action._formatted_action_invocation = ", ".join(return_list)
|
||||
return action._formatted_action_invocation
|
||||
formatted_action_invocation = ", ".join(return_list)
|
||||
action._formatted_action_invocation = formatted_action_invocation # type: ignore
|
||||
return formatted_action_invocation
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import os
|
||||
from typing import Any
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
|
||||
import py
|
||||
|
||||
@@ -60,7 +63,7 @@ def getcfg(args, config=None):
|
||||
return None, None, None
|
||||
|
||||
|
||||
def get_common_ancestor(paths):
|
||||
def get_common_ancestor(paths: Iterable[py.path.local]) -> py.path.local:
|
||||
common_ancestor = None
|
||||
for path in paths:
|
||||
if not path.exists():
|
||||
@@ -113,7 +116,7 @@ def determine_setup(
|
||||
args: List[str],
|
||||
rootdir_cmd_arg: Optional[str] = None,
|
||||
config: Optional["Config"] = None,
|
||||
):
|
||||
) -> Tuple[py.path.local, Optional[str], Any]:
|
||||
dirs = get_dirs_from_args(args)
|
||||
if inifile:
|
||||
iniconfig = py.iniconfig.IniConfig(inifile)
|
||||
@@ -121,7 +124,9 @@ def determine_setup(
|
||||
sections = ["tool:pytest", "pytest"] if is_cfg_file else ["pytest"]
|
||||
for section in sections:
|
||||
try:
|
||||
inicfg = iniconfig[section]
|
||||
inicfg = iniconfig[
|
||||
section
|
||||
] # type: Optional[py.iniconfig._SectionWrapper]
|
||||
if is_cfg_file and section == "pytest" and config is not None:
|
||||
fail(
|
||||
CFG_PYTEST_SECTION.format(filename=str(inifile)), pytrace=False
|
||||
|
||||
@@ -4,6 +4,7 @@ import functools
|
||||
import sys
|
||||
|
||||
from _pytest import outcomes
|
||||
from _pytest.config import ConftestImportFailure
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.config.exceptions import UsageError
|
||||
|
||||
@@ -272,11 +273,15 @@ class PdbInvoke:
|
||||
class PdbTrace:
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_pyfunc_call(self, pyfuncitem):
|
||||
_test_pytest_function(pyfuncitem)
|
||||
wrap_pytest_function_for_tracing(pyfuncitem)
|
||||
yield
|
||||
|
||||
|
||||
def _test_pytest_function(pyfuncitem):
|
||||
def wrap_pytest_function_for_tracing(pyfuncitem):
|
||||
"""Changes the python function object of the given Function item by a wrapper which actually
|
||||
enters pdb before calling the python function itself, effectively leaving the user
|
||||
in the pdb prompt in the first statement of the function.
|
||||
"""
|
||||
_pdb = pytestPDB._init_pdb("runcall")
|
||||
testfunction = pyfuncitem.obj
|
||||
|
||||
@@ -291,6 +296,13 @@ def _test_pytest_function(pyfuncitem):
|
||||
pyfuncitem.obj = wrapper
|
||||
|
||||
|
||||
def maybe_wrap_pytest_function_for_tracing(pyfuncitem):
|
||||
"""Wrap the given pytestfunct item for tracing support if --trace was given in
|
||||
the command line"""
|
||||
if pyfuncitem.config.getvalue("trace"):
|
||||
wrap_pytest_function_for_tracing(pyfuncitem)
|
||||
|
||||
|
||||
def _enter_pdb(node, excinfo, rep):
|
||||
# XXX we re-use the TerminalReporter's terminalwriter
|
||||
# because this seems to avoid some encoding related troubles
|
||||
@@ -327,6 +339,10 @@ def _postmortem_traceback(excinfo):
|
||||
# A doctest.UnexpectedException is not useful for post_mortem.
|
||||
# Use the underlying exception instead:
|
||||
return excinfo.value.exc_info[2]
|
||||
elif isinstance(excinfo.value, ConftestImportFailure):
|
||||
# A config.ConftestImportFailure is not useful for post_mortem.
|
||||
# Use the underlying exception instead:
|
||||
return excinfo.value.excinfo[2]
|
||||
else:
|
||||
return excinfo._excinfo[2]
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ All constants defined in this module should be either PytestWarning instances or
|
||||
in case of warnings which need to format their messages.
|
||||
"""
|
||||
from _pytest.warning_types import PytestDeprecationWarning
|
||||
from _pytest.warning_types import UnformattedWarning
|
||||
|
||||
# set of plugins which have been integrated into the core; we use this list to ignore
|
||||
# them during registration to avoid conflicts
|
||||
@@ -18,13 +19,11 @@ DEPRECATED_EXTERNAL_PLUGINS = {
|
||||
"pytest_faulthandler",
|
||||
}
|
||||
|
||||
|
||||
FUNCARGNAMES = PytestDeprecationWarning(
|
||||
"The `funcargnames` attribute was an alias for `fixturenames`, "
|
||||
"since pytest 2.3 - use the newer attribute instead."
|
||||
)
|
||||
|
||||
|
||||
RESULT_LOG = PytestDeprecationWarning(
|
||||
"--result-log is deprecated, please try the new pytest-reportlog plugin.\n"
|
||||
"See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information."
|
||||
@@ -35,8 +34,29 @@ FIXTURE_POSITIONAL_ARGUMENTS = PytestDeprecationWarning(
|
||||
"as a keyword argument instead."
|
||||
)
|
||||
|
||||
NODE_USE_FROM_PARENT = UnformattedWarning(
|
||||
PytestDeprecationWarning,
|
||||
"direct construction of {name} has been deprecated, please use {name}.from_parent",
|
||||
)
|
||||
|
||||
JUNIT_XML_DEFAULT_FAMILY = PytestDeprecationWarning(
|
||||
"The 'junit_family' default value will change to 'xunit2' in pytest 6.0.\n"
|
||||
"Add 'junit_family=xunit1' to your pytest.ini file to keep the current format "
|
||||
"in future versions of pytest and silence this warning."
|
||||
)
|
||||
|
||||
NO_PRINT_LOGS = PytestDeprecationWarning(
|
||||
"--no-print-logs is deprecated and scheduled for removal in pytest 6.0.\n"
|
||||
"Please use --show-capture instead."
|
||||
)
|
||||
|
||||
COLLECT_DIRECTORY_HOOK = PytestDeprecationWarning(
|
||||
"The pytest_collect_directory hook is not working.\n"
|
||||
"Please use collect_ignore in conftests or pytest_collection_modifyitems."
|
||||
)
|
||||
|
||||
|
||||
TERMINALWRITER_WRITER = PytestDeprecationWarning(
|
||||
"The TerminalReporter.writer attribute is deprecated, use TerminalReporter._tw instead at your own risk.\n"
|
||||
"See https://docs.pytest.org/en/latest/deprecations.html#terminalreporter-writer for more information."
|
||||
)
|
||||
|
||||
@@ -13,15 +13,18 @@ from typing import Sequence
|
||||
from typing import Tuple
|
||||
from typing import Union
|
||||
|
||||
import py.path
|
||||
|
||||
import pytest
|
||||
from _pytest import outcomes
|
||||
from _pytest._code.code import ExceptionInfo
|
||||
from _pytest._code.code import ReprFileLocation
|
||||
from _pytest._code.code import TerminalRepr
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest.compat import safe_getattr
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
from _pytest.outcomes import Skipped
|
||||
from _pytest.outcomes import OutcomeException
|
||||
from _pytest.python_api import approx
|
||||
from _pytest.warning_types import PytestWarning
|
||||
|
||||
@@ -105,20 +108,20 @@ def pytest_unconfigure():
|
||||
RUNNER_CLASS = None
|
||||
|
||||
|
||||
def pytest_collect_file(path, parent):
|
||||
def pytest_collect_file(path: py.path.local, parent):
|
||||
config = parent.config
|
||||
if path.ext == ".py":
|
||||
if config.option.doctestmodules and not _is_setup_py(config, path, parent):
|
||||
return DoctestModule(path, parent)
|
||||
if config.option.doctestmodules and not _is_setup_py(path):
|
||||
return DoctestModule.from_parent(parent, fspath=path)
|
||||
elif _is_doctest(config, path, parent):
|
||||
return DoctestTextfile(path, parent)
|
||||
return DoctestTextfile.from_parent(parent, fspath=path)
|
||||
|
||||
|
||||
def _is_setup_py(config, path, parent):
|
||||
def _is_setup_py(path: py.path.local) -> bool:
|
||||
if path.basename != "setup.py":
|
||||
return False
|
||||
contents = path.read()
|
||||
return "setuptools" in contents or "distutils" in contents
|
||||
contents = path.read_binary()
|
||||
return b"setuptools" in contents or b"distutils" in contents
|
||||
|
||||
|
||||
def _is_doctest(config, path, parent):
|
||||
@@ -137,7 +140,7 @@ class ReprFailDoctest(TerminalRepr):
|
||||
):
|
||||
self.reprlocation_lines = reprlocation_lines
|
||||
|
||||
def toterminal(self, tw) -> None:
|
||||
def toterminal(self, tw: TerminalWriter) -> None:
|
||||
for reprlocation, lines in self.reprlocation_lines:
|
||||
for line in lines:
|
||||
tw.line(line)
|
||||
@@ -175,7 +178,7 @@ def _init_runner_class() -> "Type[doctest.DocTestRunner]":
|
||||
raise failure
|
||||
|
||||
def report_unexpected_exception(self, out, test, example, exc_info):
|
||||
if isinstance(exc_info[1], Skipped):
|
||||
if isinstance(exc_info[1], OutcomeException):
|
||||
raise exc_info[1]
|
||||
if isinstance(exc_info[1], bdb.BdbQuit):
|
||||
outcomes.exit("Quitting debugger")
|
||||
@@ -216,6 +219,16 @@ class DoctestItem(pytest.Item):
|
||||
self.obj = None
|
||||
self.fixture_request = None
|
||||
|
||||
@classmethod
|
||||
def from_parent( # type: ignore
|
||||
cls, parent: "Union[DoctestTextfile, DoctestModule]", *, name, runner, dtest
|
||||
):
|
||||
# incompatible signature due to to imposed limits on sublcass
|
||||
"""
|
||||
the public named constructor
|
||||
"""
|
||||
return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
|
||||
|
||||
def setup(self):
|
||||
if self.dtest is not None:
|
||||
self.fixture_request = _setup_fixtures(self)
|
||||
@@ -226,7 +239,7 @@ class DoctestItem(pytest.Item):
|
||||
globs[name] = value
|
||||
self.dtest.globs.update(globs)
|
||||
|
||||
def runtest(self):
|
||||
def runtest(self) -> None:
|
||||
_check_all_skipped(self.dtest)
|
||||
self._disable_output_capturing_for_darwin()
|
||||
failures = [] # type: List[doctest.DocTestFailure]
|
||||
@@ -300,13 +313,16 @@ class DoctestItem(pytest.Item):
|
||||
else:
|
||||
inner_excinfo = ExceptionInfo(failure.exc_info)
|
||||
lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)]
|
||||
lines += traceback.format_exception(*failure.exc_info)
|
||||
lines += [
|
||||
x.strip("\n")
|
||||
for x in traceback.format_exception(*failure.exc_info)
|
||||
]
|
||||
reprlocation_lines.append((reprlocation, lines))
|
||||
return ReprFailDoctest(reprlocation_lines)
|
||||
else:
|
||||
return super().repr_failure(excinfo)
|
||||
|
||||
def reportinfo(self) -> Tuple[str, int, str]:
|
||||
def reportinfo(self) -> Tuple[py.path.local, int, str]:
|
||||
return self.fspath, self.dtest.lineno, "[doctest] %s" % self.name
|
||||
|
||||
|
||||
@@ -371,7 +387,9 @@ class DoctestTextfile(pytest.Module):
|
||||
parser = doctest.DocTestParser()
|
||||
test = parser.get_doctest(text, globs, name, filename, 0)
|
||||
if test.examples:
|
||||
yield DoctestItem(test.name, self, runner, test)
|
||||
yield DoctestItem.from_parent(
|
||||
self, name=test.name, runner=runner, dtest=test
|
||||
)
|
||||
|
||||
|
||||
def _check_all_skipped(test):
|
||||
@@ -480,7 +498,9 @@ class DoctestModule(pytest.Module):
|
||||
|
||||
for test in finder.find(module, module.__name__):
|
||||
if test.examples: # skip empty doctests
|
||||
yield DoctestItem(test.name, self, runner, test)
|
||||
yield DoctestItem.from_parent(
|
||||
self, name=test.name, runner=runner, dtest=test
|
||||
)
|
||||
|
||||
|
||||
def _setup_fixtures(doctest_item):
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
from typing import TextIO
|
||||
|
||||
import pytest
|
||||
from _pytest.store import StoreKey
|
||||
|
||||
|
||||
fault_handler_stderr_key = StoreKey[TextIO]()
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
@@ -17,70 +22,92 @@ def pytest_addoption(parser):
|
||||
def pytest_configure(config):
|
||||
import faulthandler
|
||||
|
||||
# avoid trying to dup sys.stderr if faulthandler is already enabled
|
||||
if faulthandler.is_enabled():
|
||||
return
|
||||
if not faulthandler.is_enabled():
|
||||
# faulthhandler is not enabled, so install plugin that does the actual work
|
||||
# of enabling faulthandler before each test executes.
|
||||
config.pluginmanager.register(FaultHandlerHooks(), "faulthandler-hooks")
|
||||
else:
|
||||
from _pytest.warnings import _issue_warning_captured
|
||||
|
||||
stderr_fd_copy = os.dup(_get_stderr_fileno())
|
||||
config.fault_handler_stderr = os.fdopen(stderr_fd_copy, "w")
|
||||
faulthandler.enable(file=config.fault_handler_stderr)
|
||||
# Do not handle dumping to stderr if faulthandler is already enabled, so warn
|
||||
# users that the option is being ignored.
|
||||
timeout = FaultHandlerHooks.get_timeout_config_value(config)
|
||||
if timeout > 0:
|
||||
_issue_warning_captured(
|
||||
pytest.PytestConfigWarning(
|
||||
"faulthandler module enabled before pytest configuration step, "
|
||||
"'faulthandler_timeout' option ignored"
|
||||
),
|
||||
config.hook,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
|
||||
def _get_stderr_fileno():
|
||||
try:
|
||||
return sys.stderr.fileno()
|
||||
except (AttributeError, io.UnsupportedOperation):
|
||||
# python-xdist monkeypatches sys.stderr with an object that is not an actual file.
|
||||
# https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors
|
||||
# This is potentially dangerous, but the best we can do.
|
||||
return sys.__stderr__.fileno()
|
||||
class FaultHandlerHooks:
|
||||
"""Implements hooks that will actually install fault handler before tests execute,
|
||||
as well as correctly handle pdb and internal errors."""
|
||||
|
||||
def pytest_configure(self, config):
|
||||
import faulthandler
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
import faulthandler
|
||||
stderr_fd_copy = os.dup(self._get_stderr_fileno())
|
||||
config._store[fault_handler_stderr_key] = open(stderr_fd_copy, "w")
|
||||
faulthandler.enable(file=config._store[fault_handler_stderr_key])
|
||||
|
||||
faulthandler.disable()
|
||||
# close our dup file installed during pytest_configure
|
||||
f = getattr(config, "fault_handler_stderr", None)
|
||||
if f is not None:
|
||||
def pytest_unconfigure(self, config):
|
||||
import faulthandler
|
||||
|
||||
faulthandler.disable()
|
||||
# close our dup file installed during pytest_configure
|
||||
# re-enable the faulthandler, attaching it to the default sys.stderr
|
||||
# so we can see crashes after pytest has finished, usually during
|
||||
# garbage collection during interpreter shutdown
|
||||
config.fault_handler_stderr.close()
|
||||
del config.fault_handler_stderr
|
||||
faulthandler.enable(file=_get_stderr_fileno())
|
||||
config._store[fault_handler_stderr_key].close()
|
||||
del config._store[fault_handler_stderr_key]
|
||||
faulthandler.enable(file=self._get_stderr_fileno())
|
||||
|
||||
@staticmethod
|
||||
def _get_stderr_fileno():
|
||||
try:
|
||||
return sys.stderr.fileno()
|
||||
except (AttributeError, io.UnsupportedOperation):
|
||||
# pytest-xdist monkeypatches sys.stderr with an object that is not an actual file.
|
||||
# https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors
|
||||
# This is potentially dangerous, but the best we can do.
|
||||
return sys.__stderr__.fileno()
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_protocol(item):
|
||||
timeout = float(item.config.getini("faulthandler_timeout") or 0.0)
|
||||
if timeout > 0:
|
||||
@staticmethod
|
||||
def get_timeout_config_value(config):
|
||||
return float(config.getini("faulthandler_timeout") or 0.0)
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_protocol(self, item):
|
||||
timeout = self.get_timeout_config_value(item.config)
|
||||
stderr = item.config._store[fault_handler_stderr_key]
|
||||
if timeout > 0 and stderr is not None:
|
||||
import faulthandler
|
||||
|
||||
faulthandler.dump_traceback_later(timeout, file=stderr)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
faulthandler.cancel_dump_traceback_later()
|
||||
else:
|
||||
yield
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_enter_pdb(self):
|
||||
"""Cancel any traceback dumping due to timeout before entering pdb.
|
||||
"""
|
||||
import faulthandler
|
||||
|
||||
stderr = item.config.fault_handler_stderr
|
||||
faulthandler.dump_traceback_later(timeout, file=stderr)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
faulthandler.cancel_dump_traceback_later()
|
||||
else:
|
||||
yield
|
||||
faulthandler.cancel_dump_traceback_later()
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_exception_interact(self):
|
||||
"""Cancel any traceback dumping due to an interactive exception being
|
||||
raised.
|
||||
"""
|
||||
import faulthandler
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_enter_pdb():
|
||||
"""Cancel any traceback dumping due to timeout before entering pdb.
|
||||
"""
|
||||
import faulthandler
|
||||
|
||||
faulthandler.cancel_dump_traceback_later()
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_exception_interact():
|
||||
"""Cancel any traceback dumping due to an interactive exception being
|
||||
raised.
|
||||
"""
|
||||
import faulthandler
|
||||
|
||||
faulthandler.cancel_dump_traceback_later()
|
||||
faulthandler.cancel_dump_traceback_later()
|
||||
|
||||
@@ -16,11 +16,12 @@ import py
|
||||
import _pytest
|
||||
from _pytest._code.code import FormattedExcinfo
|
||||
from _pytest._code.code import TerminalRepr
|
||||
from _pytest._code.source import getfslineno
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest.compat import _format_args
|
||||
from _pytest.compat import _PytestWrapper
|
||||
from _pytest.compat import get_real_func
|
||||
from _pytest.compat import get_real_method
|
||||
from _pytest.compat import getfslineno
|
||||
from _pytest.compat import getfuncargnames
|
||||
from _pytest.compat import getimfunc
|
||||
from _pytest.compat import getlocation
|
||||
@@ -30,6 +31,7 @@ from _pytest.compat import safe_getattr
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS
|
||||
from _pytest.deprecated import FUNCARGNAMES
|
||||
from _pytest.mark import ParameterSet
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.outcomes import TEST_OUTCOME
|
||||
|
||||
@@ -37,6 +39,7 @@ if TYPE_CHECKING:
|
||||
from typing import Type
|
||||
|
||||
from _pytest import nodes
|
||||
from _pytest.main import Session
|
||||
|
||||
|
||||
@attr.s(frozen=True)
|
||||
@@ -45,7 +48,7 @@ class PseudoFixtureDef:
|
||||
scope = attr.ib()
|
||||
|
||||
|
||||
def pytest_sessionstart(session):
|
||||
def pytest_sessionstart(session: "Session"):
|
||||
import _pytest.python
|
||||
import _pytest.nodes
|
||||
|
||||
@@ -351,7 +354,7 @@ class FixtureRequest:
|
||||
self.fixturename = None
|
||||
#: Scope string, one of "function", "class", "module", "session"
|
||||
self.scope = "function"
|
||||
self._fixture_defs = {} # argname -> FixtureDef
|
||||
self._fixture_defs = {} # type: Dict[str, FixtureDef]
|
||||
fixtureinfo = pyfuncitem._fixtureinfo
|
||||
self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy()
|
||||
self._arg2index = {}
|
||||
@@ -424,9 +427,10 @@ class FixtureRequest:
|
||||
return self._pyfuncitem.getparent(_pytest.python.Module).obj
|
||||
|
||||
@scopeproperty()
|
||||
def fspath(self):
|
||||
def fspath(self) -> py.path.local:
|
||||
""" the file system path of the test module which collected this test. """
|
||||
return self._pyfuncitem.fspath
|
||||
# TODO: Remove ignore once _pyfuncitem is properly typed.
|
||||
return self._pyfuncitem.fspath # type: ignore
|
||||
|
||||
@property
|
||||
def keywords(self):
|
||||
@@ -511,13 +515,11 @@ class FixtureRequest:
|
||||
values.append(fixturedef)
|
||||
current = current._parent_request
|
||||
|
||||
def _compute_fixture_value(self, fixturedef):
|
||||
def _compute_fixture_value(self, fixturedef: "FixtureDef") -> None:
|
||||
"""
|
||||
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)
|
||||
@@ -545,11 +547,13 @@ class FixtureRequest:
|
||||
if has_params:
|
||||
frame = inspect.stack()[3]
|
||||
frameinfo = inspect.getframeinfo(frame[0])
|
||||
source_path = frameinfo.filename
|
||||
source_path = py.path.local(frameinfo.filename)
|
||||
source_lineno = frameinfo.lineno
|
||||
source_path = py.path.local(source_path)
|
||||
if source_path.relto(funcitem.config.rootdir):
|
||||
source_path = source_path.relto(funcitem.config.rootdir)
|
||||
rel_source_path = source_path.relto(funcitem.config.rootdir)
|
||||
if rel_source_path:
|
||||
source_path_str = rel_source_path
|
||||
else:
|
||||
source_path_str = str(source_path)
|
||||
msg = (
|
||||
"The requested fixture has no parameter defined for test:\n"
|
||||
" {}\n\n"
|
||||
@@ -558,7 +562,7 @@ class FixtureRequest:
|
||||
funcitem.nodeid,
|
||||
fixturedef.argname,
|
||||
getlocation(fixturedef.func, funcitem.config.rootdir),
|
||||
source_path,
|
||||
source_path_str,
|
||||
source_lineno,
|
||||
)
|
||||
)
|
||||
@@ -751,7 +755,7 @@ class FixtureLookupErrorRepr(TerminalRepr):
|
||||
self.firstlineno = firstlineno
|
||||
self.argname = argname
|
||||
|
||||
def toterminal(self, tw) -> None:
|
||||
def toterminal(self, tw: TerminalWriter) -> None:
|
||||
# tw.line("FixtureLookupError: %s" %(self.argname), red=True)
|
||||
for tbline in self.tblines:
|
||||
tw.line(tbline.rstrip())
|
||||
@@ -852,25 +856,26 @@ class FixtureDef:
|
||||
self.argnames = getfuncargnames(func, name=argname, is_method=unittest)
|
||||
self.unittest = unittest
|
||||
self.ids = ids
|
||||
self.cached_result = None
|
||||
self._finalizers = []
|
||||
|
||||
def addfinalizer(self, finalizer):
|
||||
self._finalizers.append(finalizer)
|
||||
|
||||
def finish(self, request):
|
||||
exceptions = []
|
||||
exc = None
|
||||
try:
|
||||
while self._finalizers:
|
||||
try:
|
||||
func = self._finalizers.pop()
|
||||
func()
|
||||
except: # noqa
|
||||
exceptions.append(sys.exc_info())
|
||||
if exceptions:
|
||||
_, val, tb = exceptions[0]
|
||||
# Ensure to not keep frame references through traceback.
|
||||
del exceptions
|
||||
raise val.with_traceback(tb)
|
||||
except BaseException as e:
|
||||
# XXX Only first exception will be seen by user,
|
||||
# ideally all should be reported.
|
||||
if exc is None:
|
||||
exc = e
|
||||
if exc:
|
||||
raise exc
|
||||
finally:
|
||||
hook = self._fixturemanager.session.gethookproxy(request.node.fspath)
|
||||
hook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
|
||||
@@ -878,21 +883,23 @@ class FixtureDef:
|
||||
# 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.cached_result = None
|
||||
self._finalizers = []
|
||||
|
||||
def execute(self, request):
|
||||
for argname in self._dependee_fixture_argnames(request):
|
||||
# get required arguments and register our own finish()
|
||||
# with their finalization
|
||||
for argname in self.argnames:
|
||||
fixturedef = request._get_active_fixturedef(argname)
|
||||
if argname != "request":
|
||||
fixturedef.addfinalizer(functools.partial(self.finish, request=request))
|
||||
|
||||
my_cache_key = self.cache_key(request)
|
||||
cached_result = getattr(self, "cached_result", None)
|
||||
if cached_result is not None:
|
||||
result, cache_key, err = cached_result
|
||||
if my_cache_key == cache_key:
|
||||
if self.cached_result is not None:
|
||||
result, cache_key, err = self.cached_result
|
||||
# note: comparison with `==` can fail (or be expensive) for e.g.
|
||||
# numpy arrays (#6497)
|
||||
if my_cache_key is cache_key:
|
||||
if err is not None:
|
||||
_, val, tb = err
|
||||
raise val.with_traceback(tb)
|
||||
@@ -901,66 +908,11 @@ class FixtureDef:
|
||||
# we have a previous but differently parametrized fixture instance
|
||||
# so we need to tear it down before creating a new one
|
||||
self.finish(request)
|
||||
assert not hasattr(self, "cached_result")
|
||||
assert self.cached_result is None
|
||||
|
||||
hook = self._fixturemanager.session.gethookproxy(request.node.fspath)
|
||||
return hook.pytest_fixture_setup(fixturedef=self, request=request)
|
||||
|
||||
def _dependee_fixture_argnames(self, request):
|
||||
"""A list of argnames for fixtures that this fixture depends on.
|
||||
|
||||
Given a request, this looks at the currently known list of fixture argnames, and
|
||||
attempts to determine what slice of the list contains fixtures that it can know
|
||||
should execute before it. This information is necessary so that this fixture can
|
||||
know what fixtures to register its finalizer with to make sure that if they
|
||||
would be torn down, they would tear down this fixture before themselves. It's
|
||||
crucial for fixtures to be torn down in the inverse order that they were set up
|
||||
in so that they don't try to clean up something that another fixture is still
|
||||
depending on.
|
||||
|
||||
When autouse fixtures are involved, it can be tricky to figure out when fixtures
|
||||
should be torn down. To solve this, this method leverages the ``fixturenames``
|
||||
list provided by the ``request`` object, as this list is at least somewhat
|
||||
sorted (in terms of the order fixtures are set up in) by the time this method is
|
||||
reached. It's sorted enough that the starting point of fixtures that depend on
|
||||
this one can be found using the ``self._parent_request`` stack.
|
||||
|
||||
If a request in the ``self._parent_request`` stack has a ``:class:FixtureDef``
|
||||
associated with it, then that fixture is dependent on this one, so any fixture
|
||||
names that appear in the list of fixture argnames that come after it can also be
|
||||
ruled out. The argnames of all fixtures associated with a request in the
|
||||
``self._parent_request`` stack are found, and the lowest index argname is
|
||||
considered the earliest point in the list of fixture argnames where everything
|
||||
from that point onward can be considered to execute after this fixture.
|
||||
Everything before this point can be considered fixtures that this fixture
|
||||
depends on, and so this fixture should register its finalizer with all of them
|
||||
to ensure that if any of them are to be torn down, they will tear this fixture
|
||||
down first.
|
||||
|
||||
This is the first part of the list of fixture argnames that is returned. The last
|
||||
part of the list is everything in ``self.argnames`` as those are explicit
|
||||
dependees of this fixture, so this fixture should definitely register its
|
||||
finalizer with them.
|
||||
"""
|
||||
all_fix_names = request.fixturenames
|
||||
try:
|
||||
current_fix_index = all_fix_names.index(self.argname)
|
||||
except ValueError:
|
||||
current_fix_index = len(request.fixturenames)
|
||||
parent_fixture_indexes = set()
|
||||
|
||||
parent_request = request._parent_request
|
||||
while hasattr(parent_request, "_parent_request"):
|
||||
if hasattr(parent_request, "_fixturedef"):
|
||||
parent_fix_name = parent_request._fixturedef.argname
|
||||
if parent_fix_name in all_fix_names:
|
||||
parent_fixture_indexes.add(all_fix_names.index(parent_fix_name))
|
||||
parent_request = parent_request._parent_request
|
||||
|
||||
stack_slice_index = min([current_fix_index, *parent_fixture_indexes])
|
||||
active_fixture_argnames = all_fix_names[:stack_slice_index]
|
||||
return {*active_fixture_argnames, *self.argnames}
|
||||
|
||||
def cache_key(self, request):
|
||||
return request.param_index if not hasattr(request, "param") else request.param
|
||||
|
||||
@@ -1001,6 +953,7 @@ def pytest_fixture_setup(fixturedef, request):
|
||||
kwargs = {}
|
||||
for argname in fixturedef.argnames:
|
||||
fixdef = request._get_active_fixturedef(argname)
|
||||
assert fixdef.cached_result is not None
|
||||
result, arg_cache_key, exc = fixdef.cached_result
|
||||
request._check_scope(argname, request.scope, fixdef.scope)
|
||||
kwargs[argname] = result
|
||||
@@ -1189,7 +1142,7 @@ def fixture(
|
||||
params=params,
|
||||
autouse=autouse,
|
||||
ids=ids,
|
||||
name=name
|
||||
name=name,
|
||||
)
|
||||
scope = arguments.get("scope")
|
||||
params = arguments.get("params")
|
||||
@@ -1227,7 +1180,7 @@ def yield_fixture(
|
||||
params=params,
|
||||
autouse=autouse,
|
||||
ids=ids,
|
||||
name=name
|
||||
name=name,
|
||||
)
|
||||
|
||||
|
||||
@@ -1297,7 +1250,6 @@ class FixtureManager:
|
||||
self.config = session.config
|
||||
self._arg2fixturedefs = {}
|
||||
self._holderobjseen = set()
|
||||
self._arg2finish = {}
|
||||
self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))]
|
||||
session.config.pluginmanager.register(self, "funcmanage")
|
||||
|
||||
@@ -1310,8 +1262,6 @@ class FixtureManager:
|
||||
This things are done later as well when dealing with parametrization
|
||||
so this could be improved
|
||||
"""
|
||||
from _pytest.mark import ParameterSet
|
||||
|
||||
parametrize_argnames = []
|
||||
for marker in node.iter_markers(name="parametrize"):
|
||||
if not marker.kwargs.get("indirect", False):
|
||||
|
||||
@@ -40,8 +40,9 @@ def pytest_addoption(parser):
|
||||
group = parser.getgroup("debugconfig")
|
||||
group.addoption(
|
||||
"--version",
|
||||
"-V",
|
||||
action="store_true",
|
||||
help="display pytest lib version and import information.",
|
||||
help="display pytest version and information about plugins.",
|
||||
)
|
||||
group._addoption(
|
||||
"-h",
|
||||
@@ -66,7 +67,7 @@ def pytest_addoption(parser):
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="trace considerations of conftest.py files.",
|
||||
),
|
||||
)
|
||||
group.addoption(
|
||||
"--debug",
|
||||
action="store_true",
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
""" hook specifications for pytest plugins, invoked from main.py and builtin plugins. """
|
||||
from typing import Any
|
||||
from typing import Optional
|
||||
|
||||
from pluggy import HookspecMarker
|
||||
|
||||
from .deprecated import COLLECT_DIRECTORY_HOOK
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.main import Session
|
||||
|
||||
|
||||
hookspec = HookspecMarker("pytest")
|
||||
|
||||
@@ -158,7 +167,7 @@ def pytest_load_initial_conftests(early_config, parser, args):
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_collection(session):
|
||||
def pytest_collection(session: "Session") -> Optional[Any]:
|
||||
"""Perform the collection protocol for the given session.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult`.
|
||||
@@ -197,7 +206,7 @@ def pytest_ignore_collect(path, config):
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
@hookspec(firstresult=True, warn_on_impl=COLLECT_DIRECTORY_HOOK)
|
||||
def pytest_collect_directory(path, parent):
|
||||
""" called before traversing a directory for collection files.
|
||||
|
||||
@@ -307,10 +316,6 @@ def pytest_runtestloop(session):
|
||||
"""
|
||||
|
||||
|
||||
def pytest_itemstart(item, node):
|
||||
"""(**Deprecated**) use pytest_runtest_logstart. """
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_runtest_protocol(item, nextitem):
|
||||
""" implements the runtest_setup/call/teardown protocol for
|
||||
@@ -419,9 +424,9 @@ def pytest_fixture_setup(fixturedef, request):
|
||||
|
||||
|
||||
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."""
|
||||
"""Called after fixture teardown, but before the cache is cleared, so
|
||||
the fixture result ``fixturedef.cached_result`` is still available (not
|
||||
``None``)."""
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -562,7 +567,7 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config):
|
||||
|
||||
|
||||
@hookspec(historic=True)
|
||||
def pytest_warning_captured(warning_message, when, item):
|
||||
def pytest_warning_captured(warning_message, when, item, location):
|
||||
"""
|
||||
Process a warning captured by the internal pytest warnings plugin.
|
||||
|
||||
@@ -582,6 +587,10 @@ def pytest_warning_captured(warning_message, when, item):
|
||||
in a future release.
|
||||
|
||||
The item being executed if ``when`` is ``"runtest"``, otherwise ``None``.
|
||||
|
||||
:param tuple location:
|
||||
Holds information about the execution context of the captured warning (filename, linenumber, function).
|
||||
``function`` evaluates to <module> when the execution context is at the module level.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -22,9 +22,13 @@ import pytest
|
||||
from _pytest import deprecated
|
||||
from _pytest import nodes
|
||||
from _pytest.config import filename_arg
|
||||
from _pytest.store import StoreKey
|
||||
from _pytest.warnings import _issue_warning_captured
|
||||
|
||||
|
||||
xml_key = StoreKey["LogXML"]()
|
||||
|
||||
|
||||
class Junit(py.xml.Namespace):
|
||||
pass
|
||||
|
||||
@@ -167,51 +171,28 @@ class _NodeReporter:
|
||||
content_out = report.capstdout
|
||||
content_log = report.caplog
|
||||
content_err = report.capstderr
|
||||
if self.xml.logging == "no":
|
||||
return
|
||||
content_all = ""
|
||||
if self.xml.logging in ["log", "all"]:
|
||||
content_all = self._prepare_content(content_log, " Captured Log ")
|
||||
if self.xml.logging in ["system-out", "out-err", "all"]:
|
||||
content_all += self._prepare_content(content_out, " Captured Out ")
|
||||
self._write_content(report, content_all, "system-out")
|
||||
content_all = ""
|
||||
if self.xml.logging in ["system-err", "out-err", "all"]:
|
||||
content_all += self._prepare_content(content_err, " Captured Err ")
|
||||
self._write_content(report, content_all, "system-err")
|
||||
content_all = ""
|
||||
if content_all:
|
||||
self._write_content(report, content_all, "system-out")
|
||||
|
||||
if content_log or content_out:
|
||||
if content_log and self.xml.logging == "system-out":
|
||||
if content_out:
|
||||
# syncing stdout and the log-output is not done yet. It's
|
||||
# probably not worth the effort. Therefore, first the captured
|
||||
# stdout is shown and then the captured logs.
|
||||
content = "\n".join(
|
||||
[
|
||||
" Captured Stdout ".center(80, "-"),
|
||||
content_out,
|
||||
"",
|
||||
" Captured Log ".center(80, "-"),
|
||||
content_log,
|
||||
]
|
||||
)
|
||||
else:
|
||||
content = content_log
|
||||
else:
|
||||
content = content_out
|
||||
def _prepare_content(self, content, header):
|
||||
return "\n".join([header.center(80, "-"), content, ""])
|
||||
|
||||
if content:
|
||||
tag = getattr(Junit, "system-out")
|
||||
self.append(tag(bin_xml_escape(content)))
|
||||
|
||||
if content_log or content_err:
|
||||
if content_log and self.xml.logging == "system-err":
|
||||
if content_err:
|
||||
content = "\n".join(
|
||||
[
|
||||
" Captured Stderr ".center(80, "-"),
|
||||
content_err,
|
||||
"",
|
||||
" Captured Log ".center(80, "-"),
|
||||
content_log,
|
||||
]
|
||||
)
|
||||
else:
|
||||
content = content_log
|
||||
else:
|
||||
content = content_err
|
||||
|
||||
if content:
|
||||
tag = getattr(Junit, "system-err")
|
||||
self.append(tag(bin_xml_escape(content)))
|
||||
def _write_content(self, report, content, jheader):
|
||||
tag = getattr(Junit, jheader)
|
||||
self.append(tag(bin_xml_escape(content)))
|
||||
|
||||
def append_pass(self, report):
|
||||
self.add_stats("passed")
|
||||
@@ -283,7 +264,7 @@ def _warn_incompatibility_with_xunit2(request, fixture_name):
|
||||
"""Emits a PytestWarning about the given fixture being incompatible with newer xunit revisions"""
|
||||
from _pytest.warning_types import PytestWarning
|
||||
|
||||
xml = getattr(request.config, "_xml", None)
|
||||
xml = request.config._store.get(xml_key, None)
|
||||
if xml is not None and xml.family not in ("xunit1", "legacy"):
|
||||
request.node.warn(
|
||||
PytestWarning(
|
||||
@@ -335,7 +316,7 @@ def record_xml_attribute(request):
|
||||
|
||||
attr_func = add_attr_noop
|
||||
|
||||
xml = getattr(request.config, "_xml", None)
|
||||
xml = request.config._store.get(xml_key, None)
|
||||
if xml is not None:
|
||||
node_reporter = xml.node_reporter(request.node.nodeid)
|
||||
attr_func = node_reporter.add_attribute
|
||||
@@ -376,7 +357,7 @@ def record_testsuite_property(request):
|
||||
__tracebackhide__ = True
|
||||
_check_record_param_type("name", name)
|
||||
|
||||
xml = getattr(request.config, "_xml", None)
|
||||
xml = request.config._store.get(xml_key, None)
|
||||
if xml is not None:
|
||||
record_func = xml.add_global_property # noqa
|
||||
return record_func
|
||||
@@ -408,9 +389,9 @@ def pytest_addoption(parser):
|
||||
parser.addini(
|
||||
"junit_logging",
|
||||
"Write captured log messages to JUnit report: "
|
||||
"one of no|system-out|system-err",
|
||||
"one of no|log|system-out|system-err|out-err|all",
|
||||
default="no",
|
||||
) # choices=['no', 'stdout', 'stderr'])
|
||||
)
|
||||
parser.addini(
|
||||
"junit_log_passing_tests",
|
||||
"Capture log information for passing tests to JUnit report: ",
|
||||
@@ -435,7 +416,7 @@ def pytest_configure(config):
|
||||
if not junit_family:
|
||||
_issue_warning_captured(deprecated.JUNIT_XML_DEFAULT_FAMILY, config.hook, 2)
|
||||
junit_family = "xunit1"
|
||||
config._xml = LogXML(
|
||||
config._store[xml_key] = LogXML(
|
||||
xmlpath,
|
||||
config.option.junitprefix,
|
||||
config.getini("junit_suite_name"),
|
||||
@@ -444,13 +425,13 @@ def pytest_configure(config):
|
||||
junit_family,
|
||||
config.getini("junit_log_passing_tests"),
|
||||
)
|
||||
config.pluginmanager.register(config._xml)
|
||||
config.pluginmanager.register(config._store[xml_key])
|
||||
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
xml = getattr(config, "_xml", None)
|
||||
xml = config._store.get(xml_key, None)
|
||||
if xml:
|
||||
del config._xml
|
||||
del config._store[xml_key]
|
||||
config.pluginmanager.unregister(xml)
|
||||
|
||||
|
||||
|
||||
@@ -5,12 +5,16 @@ from contextlib import contextmanager
|
||||
from io import StringIO
|
||||
from typing import AbstractSet
|
||||
from typing import Dict
|
||||
from typing import Generator
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from _pytest import nodes
|
||||
from _pytest.compat import nullcontext
|
||||
from _pytest.config import _strtobool
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import create_terminal_writer
|
||||
from _pytest.pathlib import Path
|
||||
|
||||
@@ -192,7 +196,12 @@ def pytest_addoption(parser):
|
||||
"--log-level",
|
||||
dest="log_level",
|
||||
default=None,
|
||||
help="logging level used by the logging module",
|
||||
metavar="LEVEL",
|
||||
help=(
|
||||
"level of messages to catch/display.\n"
|
||||
"Not set by default, so it depends on the root/parent log handler's"
|
||||
' effective level, where it is "WARNING" by default.'
|
||||
),
|
||||
)
|
||||
add_option_ini(
|
||||
"--log-format",
|
||||
@@ -325,13 +334,13 @@ class LogCaptureFixture:
|
||||
logger.setLevel(level)
|
||||
|
||||
@property
|
||||
def handler(self):
|
||||
def handler(self) -> LogCaptureHandler:
|
||||
"""
|
||||
:rtype: LogCaptureHandler
|
||||
"""
|
||||
return self._item.catch_log_handler
|
||||
return self._item.catch_log_handler # type: ignore[no-any-return] # noqa: F723
|
||||
|
||||
def get_records(self, when):
|
||||
def get_records(self, when: str) -> List[logging.LogRecord]:
|
||||
"""
|
||||
Get the logging records for one of the possible test phases.
|
||||
|
||||
@@ -345,7 +354,7 @@ class LogCaptureFixture:
|
||||
"""
|
||||
handler = self._item.catch_log_handlers.get(when)
|
||||
if handler:
|
||||
return handler.records
|
||||
return handler.records # type: ignore[no-any-return] # noqa: F723
|
||||
else:
|
||||
return []
|
||||
|
||||
@@ -441,9 +450,7 @@ def caplog(request):
|
||||
result._finalize()
|
||||
|
||||
|
||||
def get_actual_log_level(config, *setting_names):
|
||||
"""Return the actual logging level."""
|
||||
|
||||
def get_log_level_for_setting(config: Config, *setting_names: str) -> Optional[int]:
|
||||
for setting_name in setting_names:
|
||||
log_level = config.getoption(setting_name)
|
||||
if log_level is None:
|
||||
@@ -451,7 +458,7 @@ def get_actual_log_level(config, *setting_names):
|
||||
if log_level:
|
||||
break
|
||||
else:
|
||||
return
|
||||
return None
|
||||
|
||||
if isinstance(log_level, str):
|
||||
log_level = log_level.upper()
|
||||
@@ -476,7 +483,7 @@ class LoggingPlugin:
|
||||
"""Attaches to the logging module and captures log messages for each test.
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
def __init__(self, config: Config) -> None:
|
||||
"""Creates a new plugin to capture log messages.
|
||||
|
||||
The formatter can be safely shared across all handlers so
|
||||
@@ -485,14 +492,20 @@ class LoggingPlugin:
|
||||
self._config = config
|
||||
|
||||
self.print_logs = get_option_ini(config, "log_print")
|
||||
if not self.print_logs:
|
||||
from _pytest.warnings import _issue_warning_captured
|
||||
from _pytest.deprecated import NO_PRINT_LOGS
|
||||
|
||||
_issue_warning_captured(NO_PRINT_LOGS, self._config.hook, stacklevel=2)
|
||||
|
||||
self.formatter = self._create_formatter(
|
||||
get_option_ini(config, "log_format"),
|
||||
get_option_ini(config, "log_date_format"),
|
||||
get_option_ini(config, "log_auto_indent"),
|
||||
)
|
||||
self.log_level = get_actual_log_level(config, "log_level")
|
||||
self.log_level = get_log_level_for_setting(config, "log_level")
|
||||
|
||||
self.log_file_level = get_actual_log_level(config, "log_file_level")
|
||||
self.log_file_level = get_log_level_for_setting(config, "log_file_level")
|
||||
self.log_file_format = get_option_ini(config, "log_file_format", "log_format")
|
||||
self.log_file_date_format = get_option_ini(
|
||||
config, "log_file_date_format", "log_date_format"
|
||||
@@ -505,7 +518,7 @@ class LoggingPlugin:
|
||||
if log_file:
|
||||
self.log_file_handler = logging.FileHandler(
|
||||
log_file, mode="w", encoding="UTF-8"
|
||||
)
|
||||
) # type: Optional[logging.FileHandler]
|
||||
self.log_file_handler.setFormatter(self.log_file_formatter)
|
||||
else:
|
||||
self.log_file_handler = None
|
||||
@@ -555,7 +568,7 @@ class LoggingPlugin:
|
||||
get_option_ini(config, "log_auto_indent"),
|
||||
)
|
||||
|
||||
log_cli_level = get_actual_log_level(config, "log_cli_level", "log_level")
|
||||
log_cli_level = get_log_level_for_setting(config, "log_cli_level", "log_level")
|
||||
self.log_cli_handler = log_cli_handler
|
||||
self.live_logs_context = lambda: catching_logs(
|
||||
log_cli_handler, formatter=log_cli_formatter, level=log_cli_level
|
||||
@@ -591,7 +604,7 @@ class LoggingPlugin:
|
||||
) is not None or self._config.getini("log_cli")
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||
def pytest_collection(self):
|
||||
def pytest_collection(self) -> Generator[None, None, None]:
|
||||
with self.live_logs_context():
|
||||
if self.log_cli_handler:
|
||||
self.log_cli_handler.set_when("collection")
|
||||
@@ -612,7 +625,9 @@ class LoggingPlugin:
|
||||
yield
|
||||
|
||||
@contextmanager
|
||||
def _runtest_for_main(self, item, when):
|
||||
def _runtest_for_main(
|
||||
self, item: nodes.Item, when: str
|
||||
) -> Generator[None, None, None]:
|
||||
"""Implements the internals of pytest_runtest_xxx() hook."""
|
||||
with catching_logs(
|
||||
LogCaptureHandler(), formatter=self.formatter, level=self.log_level
|
||||
@@ -625,15 +640,15 @@ class LoggingPlugin:
|
||||
return
|
||||
|
||||
if not hasattr(item, "catch_log_handlers"):
|
||||
item.catch_log_handlers = {}
|
||||
item.catch_log_handlers[when] = log_handler
|
||||
item.catch_log_handler = log_handler
|
||||
item.catch_log_handlers = {} # type: ignore[attr-defined] # noqa: F821
|
||||
item.catch_log_handlers[when] = log_handler # type: ignore[attr-defined] # noqa: F821
|
||||
item.catch_log_handler = log_handler # type: ignore[attr-defined] # noqa: F821
|
||||
try:
|
||||
yield # run test
|
||||
finally:
|
||||
if when == "teardown":
|
||||
del item.catch_log_handler
|
||||
del item.catch_log_handlers
|
||||
del item.catch_log_handler # type: ignore[attr-defined] # noqa: F821
|
||||
del item.catch_log_handlers # type: ignore[attr-defined] # noqa: F821
|
||||
|
||||
if self.print_logs:
|
||||
# Add a captured log section to the report.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user