Compare commits
475 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
771e860011 | ||
|
|
c94b2b227e | ||
|
|
cd72e23f3f | ||
|
|
b9990f77ff | ||
|
|
6b126997e1 | ||
|
|
02ae7d8531 | ||
|
|
db106d6085 | ||
|
|
b0eed7b56c | ||
|
|
51d358a7a9 | ||
|
|
c54cdd05b8 | ||
|
|
dde147641e | ||
|
|
2abd005cc9 | ||
|
|
366b88388b | ||
|
|
0fc1801d0d | ||
|
|
11ff14be1f | ||
|
|
dc0b4efffa | ||
|
|
864d7fef30 | ||
|
|
18035211f5 | ||
|
|
61080da89d | ||
|
|
ac5704290f | ||
|
|
cdd0e18ca8 | ||
|
|
e4871f7722 | ||
|
|
517ee588c6 | ||
|
|
1b51497936 | ||
|
|
a83ee197c6 | ||
|
|
12f94b81c0 | ||
|
|
e6b01b45f1 | ||
|
|
56dd7bc551 | ||
|
|
8a768b0db0 | ||
|
|
8dca8f3c9f | ||
|
|
cc335d44a0 | ||
|
|
4f4c91caf5 | ||
|
|
ba5a295544 | ||
|
|
93bdbf7572 | ||
|
|
ab8907f6f5 | ||
|
|
215a2ed3de | ||
|
|
f7285b6ab2 | ||
|
|
ee0a306ee4 | ||
|
|
7931b5b489 | ||
|
|
c5bbf8ac73 | ||
|
|
253c5786af | ||
|
|
c4550bc922 | ||
|
|
2c00f8aad1 | ||
|
|
456715a5c1 | ||
|
|
f858177495 | ||
|
|
54b8ad4554 | ||
|
|
d72154acda | ||
|
|
f55ded20a9 | ||
|
|
17655b54f2 | ||
|
|
12edc4e7b8 | ||
|
|
2868c31495 | ||
|
|
39a13d7064 | ||
|
|
e4e4fd1e52 | ||
|
|
e8c220b9bd | ||
|
|
9646a1cd7a | ||
|
|
9087ac4010 | ||
|
|
093e19a7d9 | ||
|
|
9e867ce864 | ||
|
|
8abf30ad71 | ||
|
|
ea25eb1ecc | ||
|
|
58b6e8616c | ||
|
|
f129ba617f | ||
|
|
99d957bd3d | ||
|
|
661013c3e9 | ||
|
|
141c51f0cb | ||
|
|
d65c7658d5 | ||
|
|
7855284ef7 | ||
|
|
5b0f88712b | ||
|
|
2e42d937dc | ||
|
|
27d932e882 | ||
|
|
40091ec2c7 | ||
|
|
76fb9970c8 | ||
|
|
d32f2c5c14 | ||
|
|
49defa2890 | ||
|
|
fe2dae4885 | ||
|
|
ced62f30ba | ||
|
|
bbd1cbb0b3 | ||
|
|
d4dfd526c1 | ||
|
|
d4351ac5a2 | ||
|
|
766d2daa06 | ||
|
|
836c9f82f1 | ||
|
|
46d6a3fc27 | ||
|
|
1dfa303b1e | ||
|
|
6258248865 | ||
|
|
4808145846 | ||
|
|
a0666354dd | ||
|
|
ce55dcf64c | ||
|
|
d7be039f1b | ||
|
|
7e1fac5f91 | ||
|
|
486ded3fca | ||
|
|
0be84cd68b | ||
|
|
323c846ce6 | ||
|
|
3bd9f981a2 | ||
|
|
7ded937e19 | ||
|
|
6d0667f1db | ||
|
|
7c380b19f3 | ||
|
|
5322f422e3 | ||
|
|
c6c326f076 | ||
|
|
d6832a8b56 | ||
|
|
3bfaa8ab84 | ||
|
|
9fb305b17b | ||
|
|
e3bf9cede4 | ||
|
|
4a49715614 | ||
|
|
86c7dcff68 | ||
|
|
7268462b33 | ||
|
|
448830e656 | ||
|
|
3683d92c53 | ||
|
|
d3d8d53e41 | ||
|
|
7a271a91b0 | ||
|
|
47f5c29002 | ||
|
|
27d2683a02 | ||
|
|
792f365c14 | ||
|
|
91b2797498 | ||
|
|
c27c8f41a8 | ||
|
|
ee54fb9a6b | ||
|
|
10ddc466bf | ||
|
|
24c83d725a | ||
|
|
6bf4692c7d | ||
|
|
81426c3d19 | ||
|
|
ed42ada373 | ||
|
|
e2667106a2 | ||
|
|
29d5849519 | ||
|
|
eabf15b626 | ||
|
|
2dc619cbf4 | ||
|
|
307fa7a42a | ||
|
|
ef97121d42 | ||
|
|
d46b6b2bc3 | ||
|
|
2cb3534679 | ||
|
|
8e11fe5304 | ||
|
|
36dc671843 | ||
|
|
dbaa9464ba | ||
|
|
933de16fe4 | ||
|
|
e8348a1d12 | ||
|
|
0f5263cdc3 | ||
|
|
4736b2bdfb | ||
|
|
8ecdd4e9ff | ||
|
|
b3940666a7 | ||
|
|
e20987ce82 | ||
|
|
584051aa90 | ||
|
|
16e2737da3 | ||
|
|
36c2a101cb | ||
|
|
ebd597b2fd | ||
|
|
94829c391b | ||
|
|
b82d6f7a0b | ||
|
|
4a436b5470 | ||
|
|
ad6f63edda | ||
|
|
3036914097 | ||
|
|
2831cb9ab5 | ||
|
|
00716177b4 | ||
|
|
85cc9b8f12 | ||
|
|
fed4f73a61 | ||
|
|
d76fa59b35 | ||
|
|
2532dc1dbb | ||
|
|
642cd86dd1 | ||
|
|
b3a5b0ebe1 | ||
|
|
8b4a29357e | ||
|
|
ab3637d486 | ||
|
|
66a690928c | ||
|
|
8e00280fc1 | ||
|
|
d053cdfbbb | ||
|
|
2e39fd89d1 | ||
|
|
b48e23d54c | ||
|
|
c9a85b0e78 | ||
|
|
bf265a424d | ||
|
|
5436e42990 | ||
|
|
67f40e18a7 | ||
|
|
52ff1eaf37 | ||
|
|
602e74c2a7 | ||
|
|
be511c1a05 | ||
|
|
f36f9d2698 | ||
|
|
d1322570dd | ||
|
|
4c9015c3b1 | ||
|
|
c14a23d4e4 | ||
|
|
b8fc3e569a | ||
|
|
e0f6fce9e9 | ||
|
|
d93de6cc67 | ||
|
|
aeb92accb2 | ||
|
|
943bbdd8ce | ||
|
|
9a3836a0cf | ||
|
|
4b164d947d | ||
|
|
11a07211b6 | ||
|
|
28a3f0fcb9 | ||
|
|
b8c30aab2b | ||
|
|
b8958168f5 | ||
|
|
aaaae0b232 | ||
|
|
c55d641963 | ||
|
|
4ec85f934c | ||
|
|
8393fdd51d | ||
|
|
4071c8a4a8 | ||
|
|
e8c10d4a98 | ||
|
|
a86035625c | ||
|
|
4f631440be | ||
|
|
3901569f26 | ||
|
|
689b856cb7 | ||
|
|
65545d8fb2 | ||
|
|
1caf6d5907 | ||
|
|
55871c68a4 | ||
|
|
fc11b81005 | ||
|
|
a6fb4c8268 | ||
|
|
48dcc67274 | ||
|
|
ccaec8d360 | ||
|
|
66609665f2 | ||
|
|
4b36f9aa64 | ||
|
|
5ecbb0acba | ||
|
|
20902deb75 | ||
|
|
ed5556bdac | ||
|
|
ee64f1fb9f | ||
|
|
8e0e862c84 | ||
|
|
42422a7f62 | ||
|
|
f3a173b736 | ||
|
|
5c38a5160d | ||
|
|
dcf9eb0104 | ||
|
|
dd225e1b9d | ||
|
|
900cef6397 | ||
|
|
0d095fc978 | ||
|
|
dcd635ba0c | ||
|
|
33f0338eeb | ||
|
|
d5e5433553 | ||
|
|
d2906950ce | ||
|
|
fe7050ba00 | ||
|
|
a1208f5631 | ||
|
|
870a93c37b | ||
|
|
96b2ae6654 | ||
|
|
b098292352 | ||
|
|
212937eb3e | ||
|
|
70c7273640 | ||
|
|
e5ab62b1b6 | ||
|
|
b8b9e8d41c | ||
|
|
e712adc226 | ||
|
|
f9ac60807c | ||
|
|
3f03625a5d | ||
|
|
29d3faed66 | ||
|
|
c5dec6056f | ||
|
|
642847c079 | ||
|
|
f102ccc8f0 | ||
|
|
20f93ae8fa | ||
|
|
1101a20408 | ||
|
|
df435fa8bd | ||
|
|
a5269b26e0 | ||
|
|
d3673c7429 | ||
|
|
5010e02eda | ||
|
|
25fe3706a4 | ||
|
|
1a323fbd3c | ||
|
|
9d971d33be | ||
|
|
bc009a8582 | ||
|
|
5e7d427df1 | ||
|
|
dd59ed3b18 | ||
|
|
20d0f0e56b | ||
|
|
d24a7e6c5a | ||
|
|
4dc73bda45 | ||
|
|
732cc2687d | ||
|
|
5d2d64c190 | ||
|
|
7a6d16c1eb | ||
|
|
c2179c3127 | ||
|
|
d8d7f73e1c | ||
|
|
3c23b5b010 | ||
|
|
783019a8e6 | ||
|
|
d2fc7ca6e0 | ||
|
|
2d06927a06 | ||
|
|
44d29d887e | ||
|
|
32c5a113e2 | ||
|
|
ba5630e0f8 | ||
|
|
808df48ee8 | ||
|
|
a089a9577e | ||
|
|
6be2136f20 | ||
|
|
f9ab81a493 | ||
|
|
1636522563 | ||
|
|
b1fbb2ab92 | ||
|
|
e85edf5212 | ||
|
|
b03bad5dbb | ||
|
|
19ec300b2a | ||
|
|
11442f2ad7 | ||
|
|
97748b6605 | ||
|
|
2b762337bd | ||
|
|
9899b8f1fb | ||
|
|
4474beeb82 | ||
|
|
5d8467bedc | ||
|
|
eca3e781b6 | ||
|
|
0d04aa7c59 | ||
|
|
c61ff31ffa | ||
|
|
e03a19f88d | ||
|
|
fcc5b6d604 | ||
|
|
42afce27b3 | ||
|
|
56d0b5a7e2 | ||
|
|
ec57cbf82d | ||
|
|
3f6a46c2a4 | ||
|
|
c30184709d | ||
|
|
4ba3cb25b0 | ||
|
|
650c458df9 | ||
|
|
58aa4f91f5 | ||
|
|
9b382ed16c | ||
|
|
f02dbaf97f | ||
|
|
41f6ea13ce | ||
|
|
f6eb39df33 | ||
|
|
7a5e11bbcf | ||
|
|
7122fa5613 | ||
|
|
7aff81739e | ||
|
|
27772f67c0 | ||
|
|
10b3b2dc68 | ||
|
|
c2841542af | ||
|
|
1f28096587 | ||
|
|
e86b01e831 | ||
|
|
83802d1494 | ||
|
|
d1fa8ae08e | ||
|
|
29dac03314 | ||
|
|
e7eb7e799b | ||
|
|
18cc74b8d0 | ||
|
|
7f48f552c1 | ||
|
|
1e2e65f0fa | ||
|
|
28c9cc7321 | ||
|
|
ccb90b5c46 | ||
|
|
048342817b | ||
|
|
d1a3aa7b2b | ||
|
|
e967d4587a | ||
|
|
a79dc12f1e | ||
|
|
37d2469266 | ||
|
|
1df6d28080 | ||
|
|
03eaad376b | ||
|
|
739f9a4a4b | ||
|
|
93224f8cf9 | ||
|
|
bb57186dd4 | ||
|
|
2803eb9fbb | ||
|
|
913c07e414 | ||
|
|
4a9f468aac | ||
|
|
05155e4db0 | ||
|
|
f53eff93db | ||
|
|
bceaede198 | ||
|
|
86a14d007d | ||
|
|
a4dd6ee3ce | ||
|
|
130cf7e0db | ||
|
|
cbb41f1ae2 | ||
|
|
fa78da3c03 | ||
|
|
ae8f3695b5 | ||
|
|
87ddb2dbd5 | ||
|
|
9aa6b0903b | ||
|
|
da6830f19b | ||
|
|
32ee0b9c88 | ||
|
|
49800ea134 | ||
|
|
feb8240410 | ||
|
|
b7dd9154c3 | ||
|
|
482bd5efd2 | ||
|
|
bf074b37a3 | ||
|
|
495a55725b | ||
|
|
53c9124fc9 | ||
|
|
8fe55b1d18 | ||
|
|
a0ce9a4441 | ||
|
|
2cf2dc3d95 | ||
|
|
7537e94ddf | ||
|
|
ab40696007 | ||
|
|
2c90b3db9e | ||
|
|
826adafe2e | ||
|
|
3dd2933dbd | ||
|
|
d12f46caef | ||
|
|
b55351274e | ||
|
|
c00d934b21 | ||
|
|
6b526cbe6a | ||
|
|
e0539e6ede | ||
|
|
5eb85efa14 | ||
|
|
9ee8d72fd2 | ||
|
|
8c4ca383ca | ||
|
|
f2a427da25 | ||
|
|
e0466d0ad8 | ||
|
|
418a66a09f | ||
|
|
5e2bd17d18 | ||
|
|
ec6fca4aa7 | ||
|
|
1f20626618 | ||
|
|
69b34f7658 | ||
|
|
531b76a513 | ||
|
|
f63c683faa | ||
|
|
410d5762c0 | ||
|
|
ddb308455a | ||
|
|
f42b5019ec | ||
|
|
adc9ed85bc | ||
|
|
4592def14d | ||
|
|
2e0a7cf78d | ||
|
|
5a52acaa92 | ||
|
|
6d497f2c77 | ||
|
|
b7560a8808 | ||
|
|
d3ca739c00 | ||
|
|
3db76ccf3d | ||
|
|
438f7a1254 | ||
|
|
47bf58d69e | ||
|
|
5ef51262f7 | ||
|
|
a054aa4797 | ||
|
|
f1cfd10c94 | ||
|
|
d3f72ca202 | ||
|
|
022c58bf64 | ||
|
|
b42518acd5 | ||
|
|
284a2d110f | ||
|
|
9ae0a3cd85 | ||
|
|
615c671434 | ||
|
|
29bfa5efa4 | ||
|
|
016f8f1536 | ||
|
|
e9417be9df | ||
|
|
c304998ed7 | ||
|
|
415a62e373 | ||
|
|
8ce3aeadbf | ||
|
|
b818314045 | ||
|
|
56d414177a | ||
|
|
0fffa6ba2f | ||
|
|
60499d221e | ||
|
|
9965ed84da | ||
|
|
7e13593452 | ||
|
|
208dd3aad1 | ||
|
|
19a01c9849 | ||
|
|
78ac7d99f5 | ||
|
|
0c8dbdcd92 | ||
|
|
8e4501ee29 | ||
|
|
0100f61b62 | ||
|
|
1a9d913ee1 | ||
|
|
51e32cf7cc | ||
|
|
3fcc4cdbd5 | ||
|
|
ffd47ceefc | ||
|
|
10f21b423a | ||
|
|
eec7081b8d | ||
|
|
b01704cce1 | ||
|
|
15ede8aab8 | ||
|
|
f7dc9b9fef | ||
|
|
dc13f0b469 | ||
|
|
a13c6a84df | ||
|
|
dfa713163a | ||
|
|
90c00dfd54 | ||
|
|
f3b9b21996 | ||
|
|
885b8a3b4c | ||
|
|
4675912d89 | ||
|
|
495b44198f | ||
|
|
8d8e68cf90 | ||
|
|
f3b0caf299 | ||
|
|
75d29acc06 | ||
|
|
cbbb36fc9b | ||
|
|
d53e449296 | ||
|
|
01df368d93 | ||
|
|
2256f2f04d | ||
|
|
95881c870e | ||
|
|
019e33ee3f | ||
|
|
19fa01b91d | ||
|
|
96aad2983b | ||
|
|
c18a5b5179 | ||
|
|
ed4b94a180 | ||
|
|
29c5ac71bc | ||
|
|
84a9f7a263 | ||
|
|
11e591e442 | ||
|
|
64f00683f2 | ||
|
|
84a033fd97 | ||
|
|
0183d46275 | ||
|
|
9bd4b0a05e | ||
|
|
f0e852b4db | ||
|
|
ade01b1f5b | ||
|
|
3035b2724d | ||
|
|
8c96eea583 | ||
|
|
338953a25d | ||
|
|
f1bd46266b | ||
|
|
77cad3c436 | ||
|
|
3ca70692de | ||
|
|
417516c378 | ||
|
|
f730291e67 | ||
|
|
d76fb8345c | ||
|
|
a605ad4d11 | ||
|
|
4b94760c8e | ||
|
|
23295e1e98 | ||
|
|
8f4685e024 | ||
|
|
80eef29681 | ||
|
|
2f2d5861bb | ||
|
|
2a917a582e | ||
|
|
325319dc3b | ||
|
|
dda5e5ea32 | ||
|
|
5e260c4d34 | ||
|
|
5fefc48f33 | ||
|
|
93f783228c | ||
|
|
044d2b8e6e | ||
|
|
539a22c750 | ||
|
|
a6cdd0d9da | ||
|
|
c64a8c9c7f | ||
|
|
5f97711377 | ||
|
|
126896f69d |
11
.coveragerc
11
.coveragerc
@@ -1,4 +1,9 @@
|
||||
[run]
|
||||
omit =
|
||||
# standlonetemplate is read dynamically and tested by test_genscript
|
||||
*standalonetemplate.py
|
||||
source = pytest,_pytest,testing/
|
||||
parallel = 1
|
||||
branch = 1
|
||||
|
||||
[paths]
|
||||
source = src/
|
||||
.tox/*/lib/python*/site-packages/
|
||||
.tox\*\Lib\site-packages\
|
||||
|
||||
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -1 +1 @@
|
||||
CHANGELOG merge=union
|
||||
*.bat text eol=crlf
|
||||
|
||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -3,7 +3,7 @@ Thanks for submitting a PR, your contribution is really appreciated!
|
||||
Here's a quick checklist that should be present in PRs (you can delete this text from the final description, this is
|
||||
just a guideline):
|
||||
|
||||
- [ ] Create a new changelog file in the `changelog` folder, with a name like `<ISSUE NUMBER>.<TYPE>.rst`. See [changelog/README.rst](/changelog/README.rst) for details.
|
||||
- [ ] Create a new changelog file in the `changelog` folder, with a name like `<ISSUE NUMBER>.<TYPE>.rst`. See [changelog/README.rst](https://github.com/pytest-dev/pytest/blob/master/changelog/README.rst) for details.
|
||||
- [ ] Target the `master` branch for bug fixes, documentation updates and trivial changes.
|
||||
- [ ] Target the `features` branch for new features and removals/deprecations.
|
||||
- [ ] Include documentation when adding new features.
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -24,6 +24,7 @@ src/_pytest/_version.py
|
||||
.eggs/
|
||||
|
||||
doc/*/_build
|
||||
doc/*/.doctrees
|
||||
build/
|
||||
dist/
|
||||
*.egg-info
|
||||
@@ -35,6 +36,11 @@ env/
|
||||
.cache
|
||||
.pytest_cache
|
||||
.coverage
|
||||
.coverage.*
|
||||
coverage.xml
|
||||
.ropeproject
|
||||
.idea
|
||||
.hypothesis
|
||||
.pydevproject
|
||||
.project
|
||||
.settings
|
||||
|
||||
@@ -7,26 +7,29 @@ repos:
|
||||
args: [--safe, --quiet]
|
||||
language_version: python3
|
||||
- repo: https://github.com/asottile/blacken-docs
|
||||
rev: v0.2.0
|
||||
rev: v0.3.0
|
||||
hooks:
|
||||
- id: blacken-docs
|
||||
additional_dependencies: [black==18.6b4]
|
||||
additional_dependencies: [black==18.9b0]
|
||||
language_version: python3
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v1.3.0
|
||||
rev: v2.0.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: debug-statements
|
||||
exclude: _pytest/debugging.py
|
||||
language_version: python3
|
||||
- id: flake8
|
||||
language_version: python3
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v1.2.0
|
||||
rev: v1.8.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
- id: pyupgrade
|
||||
args: [--keep-percent-format]
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: v1.0.0
|
||||
rev: v1.1.0
|
||||
hooks:
|
||||
- id: rst-backticks
|
||||
- repo: local
|
||||
@@ -38,7 +41,8 @@ repos:
|
||||
language: python
|
||||
additional_dependencies: [pygments, restructuredtext_lint]
|
||||
- id: changelogs-rst
|
||||
name: changelog files must end in .rst
|
||||
entry: ./scripts/fail
|
||||
language: script
|
||||
files: 'changelog/.*(?<!\.rst)$'
|
||||
name: changelog filenames
|
||||
language: fail
|
||||
entry: 'changelog files must be named ####.(feature|bugfix|doc|deprecation|removal|vendor|trivial).rst'
|
||||
exclude: changelog/(\d+\.(feature|bugfix|doc|deprecation|removal|vendor|trivial).rst|README.rst|_template.rst)
|
||||
files: ^changelog/
|
||||
|
||||
56
.travis.yml
56
.travis.yml
@@ -2,7 +2,8 @@ sudo: false
|
||||
language: python
|
||||
stages:
|
||||
- baseline
|
||||
- test
|
||||
- name: test
|
||||
if: repo = pytest-dev/pytest AND tag IS NOT present
|
||||
- name: deploy
|
||||
if: repo = pytest-dev/pytest AND tag IS present
|
||||
python:
|
||||
@@ -11,31 +12,24 @@ install:
|
||||
- pip install --upgrade --pre tox
|
||||
env:
|
||||
matrix:
|
||||
# coveralls is not listed in tox's envlist, but should run in travis
|
||||
- TOXENV=coveralls
|
||||
# note: please use "tox --listenvs" to populate the build matrix below
|
||||
# please remove the linting env in all cases
|
||||
- TOXENV=py27-pexpect
|
||||
- TOXENV=py27-xdist
|
||||
- TOXENV=py27-trial
|
||||
- TOXENV=py27-numpy
|
||||
- TOXENV=py27-pluggymaster
|
||||
- TOXENV=py36-pexpect
|
||||
- TOXENV=py36-xdist
|
||||
- TOXENV=py36-trial
|
||||
- TOXENV=py36-numpy
|
||||
- TOXENV=py36-pluggymaster
|
||||
# Specialized factors for py27.
|
||||
- TOXENV=py27-pexpect,py27-trial,py27-numpy
|
||||
- TOXENV=py27-nobyte
|
||||
- TOXENV=doctesting
|
||||
- TOXENV=docs
|
||||
- TOXENV=py27-xdist
|
||||
- TOXENV=py27-pluggymaster PYTEST_NO_COVERAGE=1
|
||||
# Specialized factors for py36.
|
||||
- TOXENV=py36-pexpect,py36-trial,py36-numpy
|
||||
- TOXENV=py36-xdist
|
||||
- TOXENV=py36-pluggymaster PYTEST_NO_COVERAGE=1
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- env: TOXENV=pypy
|
||||
# Coverage tracking is slow with pypy, skip it.
|
||||
- env: TOXENV=pypy PYTEST_NO_COVERAGE=1
|
||||
python: 'pypy-5.4'
|
||||
- env: TOXENV=py35
|
||||
python: '3.5'
|
||||
- env: TOXENV=py36-freeze
|
||||
- env: TOXENV=py36-freeze PYTEST_NO_COVERAGE=1
|
||||
python: '3.6'
|
||||
- env: TOXENV=py37
|
||||
python: '3.7'
|
||||
@@ -61,11 +55,11 @@ jobs:
|
||||
env: TOXENV=py27
|
||||
- env: TOXENV=py34
|
||||
- env: TOXENV=py36
|
||||
- env: TOXENV=linting
|
||||
- env: TOXENV=linting,docs,doctesting PYTEST_NO_COVERAGE=1
|
||||
|
||||
- stage: deploy
|
||||
python: '3.6'
|
||||
env:
|
||||
env: PYTEST_NO_COVERAGE=1
|
||||
install: pip install -U setuptools setuptools_scm
|
||||
script: skip
|
||||
deploy:
|
||||
@@ -79,8 +73,28 @@ jobs:
|
||||
tags: true
|
||||
repo: pytest-dev/pytest
|
||||
|
||||
before_script:
|
||||
- |
|
||||
if [[ "$PYTEST_NO_COVERAGE" != 1 ]]; then
|
||||
export COVERAGE_FILE="$PWD/.coverage"
|
||||
export COVERAGE_PROCESS_START="$PWD/.coveragerc"
|
||||
export _PYTEST_TOX_COVERAGE_RUN="coverage run -m"
|
||||
export _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess
|
||||
fi
|
||||
|
||||
script: tox --recreate
|
||||
|
||||
after_success:
|
||||
- |
|
||||
if [[ "$PYTEST_NO_COVERAGE" != 1 ]]; then
|
||||
set -e
|
||||
pip install coverage
|
||||
coverage combine
|
||||
coverage xml --ignore-errors
|
||||
coverage report -m --ignore-errors
|
||||
bash <(curl -s https://codecov.io/bash) -Z -X gcov -X coveragepy -X search -X xcode -X gcovout -X fix -f coverage.xml -F "${TOXENV//-/,},linux"
|
||||
fi
|
||||
|
||||
notifications:
|
||||
irc:
|
||||
channels:
|
||||
|
||||
14
AUTHORS
14
AUTHORS
@@ -10,9 +10,11 @@ Ahn Ki-Wook
|
||||
Alan Velasco
|
||||
Alexander Johnson
|
||||
Alexei Kozlenok
|
||||
Allan Feldman
|
||||
Anatoly Bubenkoff
|
||||
Anders Hovmöller
|
||||
Andras Tim
|
||||
Andrea Cimatoribus
|
||||
Andreas Zeidler
|
||||
Andrzej Ostrowski
|
||||
Andy Freeland
|
||||
@@ -46,7 +48,9 @@ Christian Boelsen
|
||||
Christian Theunert
|
||||
Christian Tismer
|
||||
Christopher Gilling
|
||||
CrazyMerlyn
|
||||
Cyrus Maden
|
||||
Dhiren Serai
|
||||
Daniel Grana
|
||||
Daniel Hahler
|
||||
Daniel Nuri
|
||||
@@ -71,6 +75,7 @@ Endre Galaczi
|
||||
Eric Hunsberger
|
||||
Eric Siegerman
|
||||
Erik M. Bray
|
||||
Fabio Zadrozny
|
||||
Feng Ma
|
||||
Florian Bruhin
|
||||
Floris Bruynooghe
|
||||
@@ -90,6 +95,7 @@ Hui Wang (coldnight)
|
||||
Ian Bicking
|
||||
Ian Lesperance
|
||||
Ionuț Turturică
|
||||
Iwan Briquemont
|
||||
Jaap Broekhuizen
|
||||
Jan Balster
|
||||
Janne Vanhala
|
||||
@@ -115,6 +121,7 @@ Katerina Koukiou
|
||||
Kevin Cox
|
||||
Kodi B. Arfer
|
||||
Kostis Anagnostopoulos
|
||||
Kyle Altendorf
|
||||
Lawrence Mitchell
|
||||
Lee Kamentsky
|
||||
Lev Maximov
|
||||
@@ -175,6 +182,7 @@ Raphael Pierzina
|
||||
Raquel Alegre
|
||||
Ravi Chandra
|
||||
Roberto Polli
|
||||
Roland Puntaier
|
||||
Romain Dorgueil
|
||||
Roman Bolshakov
|
||||
Ronny Pfannschmidt
|
||||
@@ -203,6 +211,7 @@ Thomas Hisch
|
||||
Tim Strazny
|
||||
Tom Dalton
|
||||
Tom Viner
|
||||
Tomer Keren
|
||||
Trevor Bekolay
|
||||
Tyler Goodlet
|
||||
Tzu-ping Chung
|
||||
@@ -210,13 +219,14 @@ Vasily Kuznetsov
|
||||
Victor Maryama
|
||||
Victor Uriarte
|
||||
Vidar T. Fauske
|
||||
Virgil Dupras
|
||||
Vitaly Lashmanov
|
||||
Vlad Dragos
|
||||
Wil Cooley
|
||||
William Lee
|
||||
Wim Glenn
|
||||
Wouter van Ackooy
|
||||
Xuan Luong
|
||||
Xuecong Liao
|
||||
Zac Hatfield-Dodds
|
||||
Zoltán Máté
|
||||
Roland Puntaier
|
||||
Allan Feldman
|
||||
|
||||
373
CHANGELOG.rst
373
CHANGELOG.rst
@@ -18,6 +18,368 @@ with advance notice in the **Deprecations** section of releases.
|
||||
|
||||
.. towncrier release notes start
|
||||
|
||||
pytest 3.9.2 (2018-10-22)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#2909 <https://github.com/pytest-dev/pytest/issues/2909>`_: Improve error message when a recursive dependency between fixtures is detected.
|
||||
|
||||
|
||||
- `#3340 <https://github.com/pytest-dev/pytest/issues/3340>`_: Fix logging messages not shown in hooks ``pytest_sessionstart()`` and ``pytest_sessionfinish()``.
|
||||
|
||||
|
||||
- `#3533 <https://github.com/pytest-dev/pytest/issues/3533>`_: Fix unescaped XML raw objects in JUnit report for skipped tests
|
||||
|
||||
|
||||
- `#3691 <https://github.com/pytest-dev/pytest/issues/3691>`_: Python 2: safely format warning message about passing unicode strings to ``warnings.warn``, which may cause
|
||||
surprising ``MemoryError`` exception when monkey patching ``warnings.warn`` itself.
|
||||
|
||||
|
||||
- `#4026 <https://github.com/pytest-dev/pytest/issues/4026>`_: Improve error message when it is not possible to determine a function's signature.
|
||||
|
||||
|
||||
- `#4177 <https://github.com/pytest-dev/pytest/issues/4177>`_: Pin ``setuptools>=40.0`` to support ``py_modules`` in ``setup.cfg``
|
||||
|
||||
|
||||
- `#4179 <https://github.com/pytest-dev/pytest/issues/4179>`_: Restore the tmpdir behaviour of symlinking the current test run.
|
||||
|
||||
|
||||
- `#4192 <https://github.com/pytest-dev/pytest/issues/4192>`_: Fix filename reported by ``warnings.warn`` when using ``recwarn`` under python2.
|
||||
|
||||
|
||||
pytest 3.9.1 (2018-10-16)
|
||||
=========================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#4159 <https://github.com/pytest-dev/pytest/issues/4159>`_: For test-suites containing test classes, the information about the subclassed
|
||||
module is now output only if a higher verbosity level is specified (at least
|
||||
"-vv").
|
||||
|
||||
|
||||
pytest 3.9.0 (2018-10-15 - not published due to a release automation bug)
|
||||
=========================================================================
|
||||
|
||||
Deprecations
|
||||
------------
|
||||
|
||||
- `#3616 <https://github.com/pytest-dev/pytest/issues/3616>`_: The following accesses have been documented as deprecated for years, but are now actually emitting deprecation warnings.
|
||||
|
||||
* Access of ``Module``, ``Function``, ``Class``, ``Instance``, ``File`` and ``Item`` through ``Node`` instances. Now
|
||||
users will this warning::
|
||||
|
||||
usage of Function.Module is deprecated, please use pytest.Module instead
|
||||
|
||||
Users should just ``import pytest`` and access those objects using the ``pytest`` module.
|
||||
|
||||
* ``request.cached_setup``, this was the precursor of the setup/teardown mechanism available to fixtures. You can
|
||||
consult `funcarg comparison section in the docs <https://docs.pytest.org/en/latest/funcarg_compare.html>`_.
|
||||
|
||||
* Using objects named ``"Class"`` as a way to customize the type of nodes that are collected in ``Collector``
|
||||
subclasses has been deprecated. Users instead should use ``pytest_collect_make_item`` to customize node types during
|
||||
collection.
|
||||
|
||||
This issue should affect only advanced plugins who create new collection types, so if you see this warning
|
||||
message please contact the authors so they can change the code.
|
||||
|
||||
* The warning that produces the message below has changed to ``RemovedInPytest4Warning``::
|
||||
|
||||
getfuncargvalue is deprecated, use getfixturevalue
|
||||
|
||||
|
||||
- `#3988 <https://github.com/pytest-dev/pytest/issues/3988>`_: Add a Deprecation warning for pytest.ensuretemp as it was deprecated since a while.
|
||||
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#2293 <https://github.com/pytest-dev/pytest/issues/2293>`_: Improve usage errors messages by hiding internal details which can be distracting and noisy.
|
||||
|
||||
This has the side effect that some error conditions that previously raised generic errors (such as
|
||||
``ValueError`` for unregistered marks) are now raising ``Failed`` exceptions.
|
||||
|
||||
|
||||
- `#3332 <https://github.com/pytest-dev/pytest/issues/3332>`_: Improve the error displayed when a ``conftest.py`` file could not be imported.
|
||||
|
||||
In order to implement this, a new ``chain`` parameter was added to ``ExceptionInfo.getrepr``
|
||||
to show or hide chained tracebacks in Python 3 (defaults to ``True``).
|
||||
|
||||
|
||||
- `#3849 <https://github.com/pytest-dev/pytest/issues/3849>`_: Add ``empty_parameter_set_mark=fail_at_collect`` ini option for raising an exception when parametrize collects an empty set.
|
||||
|
||||
|
||||
- `#3964 <https://github.com/pytest-dev/pytest/issues/3964>`_: Log messages generated in the collection phase are shown when
|
||||
live-logging is enabled and/or when they are logged to a file.
|
||||
|
||||
|
||||
- `#3985 <https://github.com/pytest-dev/pytest/issues/3985>`_: Introduce ``tmp_path`` as a fixture providing a Path object.
|
||||
|
||||
|
||||
- `#4013 <https://github.com/pytest-dev/pytest/issues/4013>`_: Deprecation warnings are now shown even if you customize the warnings filters yourself. In the previous version
|
||||
any customization would override pytest's filters and deprecation warnings would fall back to being hidden by default.
|
||||
|
||||
|
||||
- `#4073 <https://github.com/pytest-dev/pytest/issues/4073>`_: Allow specification of timeout for ``Testdir.runpytest_subprocess()`` and ``Testdir.run()``.
|
||||
|
||||
|
||||
- `#4098 <https://github.com/pytest-dev/pytest/issues/4098>`_: Add returncode argument to pytest.exit() to exit pytest with a specific return code.
|
||||
|
||||
|
||||
- `#4102 <https://github.com/pytest-dev/pytest/issues/4102>`_: Reimplement ``pytest.deprecated_call`` using ``pytest.warns`` so it supports the ``match='...'`` keyword argument.
|
||||
|
||||
This has the side effect that ``pytest.deprecated_call`` now raises ``pytest.fail.Exception`` instead
|
||||
of ``AssertionError``.
|
||||
|
||||
|
||||
- `#4149 <https://github.com/pytest-dev/pytest/issues/4149>`_: Require setuptools>=30.3 and move most of the metadata to ``setup.cfg``.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#2535 <https://github.com/pytest-dev/pytest/issues/2535>`_: Improve error message when test functions of ``unittest.TestCase`` subclasses use a parametrized fixture.
|
||||
|
||||
|
||||
- `#3057 <https://github.com/pytest-dev/pytest/issues/3057>`_: ``request.fixturenames`` now correctly returns the name of fixtures created by ``request.getfixturevalue()``.
|
||||
|
||||
|
||||
- `#3946 <https://github.com/pytest-dev/pytest/issues/3946>`_: Warning filters passed as command line options using ``-W`` now take precedence over filters defined in ``ini``
|
||||
configuration files.
|
||||
|
||||
|
||||
- `#4066 <https://github.com/pytest-dev/pytest/issues/4066>`_: Fix source reindenting by using ``textwrap.dedent`` directly.
|
||||
|
||||
|
||||
- `#4102 <https://github.com/pytest-dev/pytest/issues/4102>`_: ``pytest.warn`` will capture previously-warned warnings in Python 2. Previously they were never raised.
|
||||
|
||||
|
||||
- `#4108 <https://github.com/pytest-dev/pytest/issues/4108>`_: Resolve symbolic links for args.
|
||||
|
||||
This fixes running ``pytest tests/test_foo.py::test_bar``, where ``tests``
|
||||
is a symlink to ``project/app/tests``:
|
||||
previously ``project/app/conftest.py`` would be ignored for fixtures then.
|
||||
|
||||
|
||||
- `#4132 <https://github.com/pytest-dev/pytest/issues/4132>`_: Fix duplicate printing of internal errors when using ``--pdb``.
|
||||
|
||||
|
||||
- `#4135 <https://github.com/pytest-dev/pytest/issues/4135>`_: pathlib based tmpdir cleanup now correctly handles symlinks in the folder.
|
||||
|
||||
|
||||
- `#4152 <https://github.com/pytest-dev/pytest/issues/4152>`_: Display the filename when encountering ``SyntaxWarning``.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#3713 <https://github.com/pytest-dev/pytest/issues/3713>`_: Update usefixtures documentation to clarify that it can't be used with fixture functions.
|
||||
|
||||
|
||||
- `#4058 <https://github.com/pytest-dev/pytest/issues/4058>`_: Update fixture documentation to specify that a fixture can be invoked twice in the scope it's defined for.
|
||||
|
||||
|
||||
- `#4064 <https://github.com/pytest-dev/pytest/issues/4064>`_: According to unittest.rst, setUpModule and tearDownModule were not implemented, but it turns out they are. So updated the documentation for unittest.
|
||||
|
||||
|
||||
- `#4151 <https://github.com/pytest-dev/pytest/issues/4151>`_: Add tempir testing example to CONTRIBUTING.rst guide
|
||||
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#2293 <https://github.com/pytest-dev/pytest/issues/2293>`_: The internal ``MarkerError`` exception has been removed.
|
||||
|
||||
|
||||
- `#3988 <https://github.com/pytest-dev/pytest/issues/3988>`_: Port the implementation of tmpdir to pathlib.
|
||||
|
||||
|
||||
- `#4063 <https://github.com/pytest-dev/pytest/issues/4063>`_: Exclude 0.00 second entries from ``--duration`` output unless ``-vv`` is passed on the command-line.
|
||||
|
||||
|
||||
- `#4093 <https://github.com/pytest-dev/pytest/issues/4093>`_: Fixed formatting of string literals in internal tests.
|
||||
|
||||
|
||||
pytest 3.8.2 (2018-10-02)
|
||||
=========================
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
In a future release ``item`` will always be ``None`` and will emit a proper warning when a hook implementation
|
||||
makes use of it.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#3539 <https://github.com/pytest-dev/pytest/issues/3539>`_: Fix reload on assertion rewritten modules.
|
||||
|
||||
|
||||
- `#4034 <https://github.com/pytest-dev/pytest/issues/4034>`_: The ``.user_properties`` attribute of ``TestReport`` objects is a list
|
||||
of (name, value) tuples, but could sometimes be instantiated as a tuple
|
||||
of tuples. It is now always a list.
|
||||
|
||||
|
||||
- `#4039 <https://github.com/pytest-dev/pytest/issues/4039>`_: No longer issue warnings about using ``pytest_plugins`` in non-top-level directories when using ``--pyargs``: the
|
||||
current ``--pyargs`` mechanism is not reliable and might give false negatives.
|
||||
|
||||
|
||||
- `#4040 <https://github.com/pytest-dev/pytest/issues/4040>`_: Exclude empty reports for passed tests when ``-rP`` option is used.
|
||||
|
||||
|
||||
- `#4051 <https://github.com/pytest-dev/pytest/issues/4051>`_: Improve error message when an invalid Python expression is passed to the ``-m`` option.
|
||||
|
||||
|
||||
- `#4056 <https://github.com/pytest-dev/pytest/issues/4056>`_: ``MonkeyPatch.setenv`` and ``MonkeyPatch.delenv`` issue a warning if the environment variable name is not ``str`` on Python 2.
|
||||
|
||||
In Python 2, adding ``unicode`` keys to ``os.environ`` causes problems with ``subprocess`` (and possible other modules),
|
||||
making this a subtle bug specially susceptible when used with ``from __future__ import unicode_literals``.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#3928 <https://github.com/pytest-dev/pytest/issues/3928>`_: Add possible values for fixture scope to docs.
|
||||
|
||||
|
||||
pytest 3.8.1 (2018-09-22)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#3286 <https://github.com/pytest-dev/pytest/issues/3286>`_: ``.pytest_cache`` directory is now automatically ignored by Git. Users who would like to contribute a solution for other SCMs please consult/comment on this issue.
|
||||
|
||||
|
||||
- `#3749 <https://github.com/pytest-dev/pytest/issues/3749>`_: Fix the following error during collection of tests inside packages::
|
||||
|
||||
TypeError: object of type 'Package' has no len()
|
||||
|
||||
|
||||
- `#3941 <https://github.com/pytest-dev/pytest/issues/3941>`_: Fix bug where indirect parametrization would consider the scope of all fixtures used by the test function to determine the parametrization scope, and not only the scope of the fixtures being parametrized.
|
||||
|
||||
|
||||
- `#3973 <https://github.com/pytest-dev/pytest/issues/3973>`_: Fix crash of the assertion rewriter if a test changed the current working directory without restoring it afterwards.
|
||||
|
||||
|
||||
- `#3998 <https://github.com/pytest-dev/pytest/issues/3998>`_: Fix issue that prevented some caplog properties (for example ``record_tuples``) from being available when entering the debugger with ``--pdb``.
|
||||
|
||||
|
||||
- `#3999 <https://github.com/pytest-dev/pytest/issues/3999>`_: Fix ``UnicodeDecodeError`` in python2.x when a class returns a non-ascii binary ``__repr__`` in an assertion which also contains non-ascii text.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#3996 <https://github.com/pytest-dev/pytest/issues/3996>`_: New `Deprecations and Removals <https://docs.pytest.org/en/latest/deprecations.html>`_ page shows all currently
|
||||
deprecated features, the rationale to do so, and alternatives to update your code. It also list features removed
|
||||
from pytest in past major releases to help those with ancient pytest versions to upgrade.
|
||||
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#3955 <https://github.com/pytest-dev/pytest/issues/3955>`_: Improve pre-commit detection for changelog filenames
|
||||
|
||||
|
||||
- `#3975 <https://github.com/pytest-dev/pytest/issues/3975>`_: Remove legacy code around im_func as that was python2 only
|
||||
|
||||
|
||||
pytest 3.8.0 (2018-09-05)
|
||||
=========================
|
||||
|
||||
Deprecations and Removals
|
||||
-------------------------
|
||||
|
||||
- `#2452 <https://github.com/pytest-dev/pytest/issues/2452>`_: ``Config.warn`` and ``Node.warn`` have been
|
||||
deprecated, see `<https://docs.pytest.org/en/latest/deprecations.html#config-warn-and-node-warn>`_ for rationale and
|
||||
examples.
|
||||
|
||||
- `#3936 <https://github.com/pytest-dev/pytest/issues/3936>`_: ``@pytest.mark.filterwarnings`` second parameter is no longer regex-escaped,
|
||||
making it possible to actually use regular expressions to check the warning message.
|
||||
|
||||
**Note**: regex-escaping the match string was an implementation oversight that might break test suites which depend
|
||||
on the old behavior.
|
||||
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#2452 <https://github.com/pytest-dev/pytest/issues/2452>`_: Internal pytest warnings are now issued using the standard ``warnings`` module, making it possible to use
|
||||
the standard warnings filters to manage those warnings. This introduces ``PytestWarning``,
|
||||
``PytestDeprecationWarning`` and ``RemovedInPytest4Warning`` warning types as part of the public API.
|
||||
|
||||
Consult `the documentation <https://docs.pytest.org/en/latest/warnings.html#internal-pytest-warnings>`_ for more info.
|
||||
|
||||
|
||||
- `#2908 <https://github.com/pytest-dev/pytest/issues/2908>`_: ``DeprecationWarning`` and ``PendingDeprecationWarning`` are now shown by default if no other warning filter is
|
||||
configured. This makes pytest more compliant with
|
||||
`PEP-0506 <https://www.python.org/dev/peps/pep-0565/#recommended-filter-settings-for-test-runners>`_. See
|
||||
`the docs <https://docs.pytest.org/en/latest/warnings.html#deprecationwarning-and-pendingdeprecationwarning>`_ for
|
||||
more info.
|
||||
|
||||
|
||||
- `#3251 <https://github.com/pytest-dev/pytest/issues/3251>`_: Warnings are now captured and displayed during test collection.
|
||||
|
||||
|
||||
- `#3784 <https://github.com/pytest-dev/pytest/issues/3784>`_: ``PYTEST_DISABLE_PLUGIN_AUTOLOAD`` environment variable disables plugin auto-loading when set.
|
||||
|
||||
|
||||
- `#3829 <https://github.com/pytest-dev/pytest/issues/3829>`_: Added the ``count`` option to ``console_output_style`` to enable displaying the progress as a count instead of a percentage.
|
||||
|
||||
|
||||
- `#3837 <https://github.com/pytest-dev/pytest/issues/3837>`_: Added support for 'xfailed' and 'xpassed' outcomes to the ``pytester.RunResult.assert_outcomes`` signature.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#3911 <https://github.com/pytest-dev/pytest/issues/3911>`_: Terminal writer now takes into account unicode character width when writing out progress.
|
||||
|
||||
|
||||
- `#3913 <https://github.com/pytest-dev/pytest/issues/3913>`_: Pytest now returns with correct exit code (EXIT_USAGEERROR, 4) when called with unknown arguments.
|
||||
|
||||
|
||||
- `#3918 <https://github.com/pytest-dev/pytest/issues/3918>`_: Improve performance of assertion rewriting.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#3566 <https://github.com/pytest-dev/pytest/issues/3566>`_: Added a blurb in usage.rst for the usage of -r flag which is used to show an extra test summary info.
|
||||
|
||||
|
||||
- `#3907 <https://github.com/pytest-dev/pytest/issues/3907>`_: Corrected type of the exceptions collection passed to ``xfail``: ``raises`` argument accepts a ``tuple`` instead of ``list``.
|
||||
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#3853 <https://github.com/pytest-dev/pytest/issues/3853>`_: Removed ``"run all (no recorded failures)"`` message printed with ``--failed-first`` and ``--last-failed`` when there are no failed tests.
|
||||
|
||||
|
||||
pytest 3.7.4 (2018-08-29)
|
||||
=========================
|
||||
|
||||
@@ -166,15 +528,10 @@ pytest 3.7.0 (2018-07-30)
|
||||
Deprecations and Removals
|
||||
-------------------------
|
||||
|
||||
- `#2639 <https://github.com/pytest-dev/pytest/issues/2639>`_: ``pytest_namespace`` has been deprecated.
|
||||
|
||||
See the documentation for ``pytest_namespace`` hook for suggestions on how to deal
|
||||
with this in plugins which use this functionality.
|
||||
- `#2639 <https://github.com/pytest-dev/pytest/issues/2639>`_: ``pytest_namespace`` has been `deprecated <https://docs.pytest.org/en/latest/deprecations.html#pytest-namespace>`_.
|
||||
|
||||
|
||||
- `#3661 <https://github.com/pytest-dev/pytest/issues/3661>`_: Calling a fixture function directly, as opposed to request them in a test function, now issues a ``RemovedInPytest4Warning``. It will be changed into an error in pytest ``4.0``.
|
||||
|
||||
This is a great source of confusion to new users, which will often call the fixture functions and request them from test functions interchangeably, which breaks the fixture resolution model.
|
||||
- `#3661 <https://github.com/pytest-dev/pytest/issues/3661>`_: Calling a fixture function directly, as opposed to request them in a test function, now issues a ``RemovedInPytest4Warning``. See `the documentation for rationale and examples <https://docs.pytest.org/en/latest/deprecations.html#calling-fixtures-directly>`_.
|
||||
|
||||
|
||||
|
||||
@@ -536,7 +893,7 @@ Deprecations and Removals
|
||||
<https://github.com/pytest-dev/pytest/issues/2770>`_)
|
||||
|
||||
- Defining ``pytest_plugins`` is now deprecated in non-top-level conftest.py
|
||||
files, because they "leak" to the entire directory tree. (`#3084
|
||||
files, because they "leak" to the entire directory tree. `See the docs <https://docs.pytest.org/en/latest/deprecations.html#pytest-plugins-in-non-top-level-conftest-files>`_ for the rationale behind this decision (`#3084
|
||||
<https://github.com/pytest-dev/pytest/issues/3084>`_)
|
||||
|
||||
|
||||
|
||||
@@ -280,6 +280,47 @@ Here is a simple overview, with pytest-specific bits:
|
||||
base: features # if it's a feature
|
||||
|
||||
|
||||
Writing Tests
|
||||
----------------------------
|
||||
|
||||
Writing tests for plugins or for pytest itself is often done using the `testdir fixture <https://docs.pytest.org/en/latest/reference.html#testdir>`_, as a "black-box" test.
|
||||
|
||||
For example, to ensure a simple test passes you can write:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_true_assertion(testdir):
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
def test_foo():
|
||||
assert True
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
result.assert_outcomes(failed=0, passed=1)
|
||||
|
||||
|
||||
Alternatively, it is possible to make checks based on the actual output of the termal using
|
||||
*glob-like* expressions:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_true_assertion(testdir):
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
def test_foo():
|
||||
assert False
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines(["*assert False*", "*1 failed*"])
|
||||
|
||||
When choosing a file where to write a new test, take a look at the existing files and see if there's
|
||||
one file which looks like a good fit. For example, a regression test about a bug in the ``--lf`` option
|
||||
should go into ``test_cacheprovider.py``, given that this option is implemented in ``cacheprovider.py``.
|
||||
If in doubt, go ahead and open a PR with your best guess and we can discuss this over the code.
|
||||
|
||||
|
||||
Joining the Development Team
|
||||
----------------------------
|
||||
|
||||
|
||||
@@ -28,10 +28,13 @@ taking a lot of time to make a new one.
|
||||
|
||||
#. 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 into ``master``.
|
||||
|
||||
#. Send an email announcement with the contents from::
|
||||
|
||||
doc/en/announce/release-<VERSION>.rst
|
||||
|
||||
@@ -15,8 +15,9 @@
|
||||
.. image:: https://img.shields.io/pypi/pyversions/pytest.svg
|
||||
:target: https://pypi.org/project/pytest/
|
||||
|
||||
.. image:: https://img.shields.io/coveralls/pytest-dev/pytest/master.svg
|
||||
:target: https://coveralls.io/r/pytest-dev/pytest
|
||||
.. image:: https://codecov.io/gh/pytest-dev/pytest/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/pytest-dev/pytest
|
||||
:alt: Code coverage Status
|
||||
|
||||
.. image:: https://travis-ci.org/pytest-dev/pytest.svg?branch=master
|
||||
:target: https://travis-ci.org/pytest-dev/pytest
|
||||
@@ -25,7 +26,7 @@
|
||||
:target: https://ci.appveyor.com/project/pytestbot/pytest
|
||||
|
||||
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
||||
:target: https://github.com/ambv/black
|
||||
:target: https://github.com/ambv/black
|
||||
|
||||
.. image:: https://www.codetriage.com/pytest-dev/pytest/badges/users.svg
|
||||
:target: https://www.codetriage.com/pytest-dev/pytest
|
||||
|
||||
53
appveyor.yml
53
appveyor.yml
@@ -1,35 +1,29 @@
|
||||
environment:
|
||||
COVERALLS_REPO_TOKEN:
|
||||
secure: 2NJ5Ct55cHJ9WEg3xbSqCuv0rdgzzb6pnzOIG5OkMbTndw3wOBrXntWFoQrXiMFi
|
||||
# this is pytest's token in coveralls.io, encrypted
|
||||
# using pytestbot account as detailed here:
|
||||
# https://www.appveyor.com/docs/build-configuration#secure-variables
|
||||
|
||||
matrix:
|
||||
# coveralls is not in the default env list
|
||||
- TOXENV: "coveralls"
|
||||
# note: please use "tox --listenvs" to populate the build matrix below
|
||||
- TOXENV: "linting"
|
||||
- TOXENV: "py27"
|
||||
- TOXENV: "py34"
|
||||
- TOXENV: "py35"
|
||||
- TOXENV: "py36"
|
||||
- TOXENV: "py37"
|
||||
# - TOXENV: "pypy" reenable when we are able to provide a scandir wheel or build scandir
|
||||
- TOXENV: "py27-pexpect"
|
||||
- TOXENV: "py27-xdist"
|
||||
- TOXENV: "py27-trial"
|
||||
- TOXENV: "py27-numpy"
|
||||
PYTEST_NO_COVERAGE: "1"
|
||||
- TOXENV: "linting,docs,doctesting"
|
||||
- TOXENV: "py36"
|
||||
- TOXENV: "py35"
|
||||
- TOXENV: "py34"
|
||||
- TOXENV: "pypy"
|
||||
PYTEST_NO_COVERAGE: "1"
|
||||
# Specialized factors for py27.
|
||||
- TOXENV: "py27-trial,py27-numpy,py27-nobyte"
|
||||
- TOXENV: "py27-pluggymaster"
|
||||
- TOXENV: "py36-pexpect"
|
||||
- TOXENV: "py36-xdist"
|
||||
- TOXENV: "py36-trial"
|
||||
- TOXENV: "py36-numpy"
|
||||
PYTEST_NO_COVERAGE: "1"
|
||||
- TOXENV: "py27-xdist"
|
||||
# Specialized factors for py36.
|
||||
- TOXENV: "py36-trial,py36-numpy"
|
||||
- TOXENV: "py36-pluggymaster"
|
||||
- TOXENV: "py27-nobyte"
|
||||
- TOXENV: "doctesting"
|
||||
PYTEST_NO_COVERAGE: "1"
|
||||
- TOXENV: "py36-freeze"
|
||||
- TOXENV: "docs"
|
||||
PYTEST_NO_COVERAGE: "1"
|
||||
- TOXENV: "py36-xdist"
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
|
||||
install:
|
||||
- echo Installed Pythons
|
||||
@@ -37,12 +31,19 @@ install:
|
||||
|
||||
- if "%TOXENV%" == "pypy" call scripts\install-pypy.bat
|
||||
|
||||
- C:\Python36\python -m pip install --upgrade pip
|
||||
- C:\Python36\python -m pip install --upgrade --pre tox
|
||||
|
||||
build: false # Not a C# project, build stuff at the test step instead.
|
||||
|
||||
before_test:
|
||||
- call scripts\prepare-coverage.bat
|
||||
|
||||
test_script:
|
||||
- call scripts\call-tox.bat
|
||||
- C:\Python36\python -m tox
|
||||
|
||||
on_success:
|
||||
- call scripts\upload-coverage.bat
|
||||
|
||||
cache:
|
||||
- '%LOCALAPPDATA%\pip\cache'
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
# 10000 iterations, just for relative comparison
|
||||
# 2.7.5 3.3.2
|
||||
# FilesCompleter 75.1109 69.2116
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ Each file should be named like ``<ISSUE>.<TYPE>.rst``, where
|
||||
* ``feature``: new user facing features, like new command-line options and new behavior.
|
||||
* ``bugfix``: fixes a reported bug.
|
||||
* ``doc``: documentation improvement, like rewording an entire session or adding missing docs.
|
||||
* ``removal``: feature deprecation or removal.
|
||||
* ``deprecation``: feature deprecation.
|
||||
* ``removal``: feature removal.
|
||||
* ``vendor``: changes in packages vendored in pytest.
|
||||
* ``trivial``: fixing a small typo or internal change that might be noteworthy.
|
||||
|
||||
|
||||
@@ -6,6 +6,12 @@ Release announcements
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
release-3.9.2
|
||||
release-3.9.1
|
||||
release-3.9.0
|
||||
release-3.8.2
|
||||
release-3.8.1
|
||||
release-3.8.0
|
||||
release-3.7.4
|
||||
release-3.7.3
|
||||
release-3.7.2
|
||||
|
||||
@@ -20,8 +20,7 @@ Thanks to all who contributed to this release, among them:
|
||||
* Ondřej Súkup
|
||||
* Ronny Pfannschmidt
|
||||
* T.E.A de Souza
|
||||
* Victor
|
||||
* victor
|
||||
* Victor Maryama
|
||||
|
||||
|
||||
Happy testing,
|
||||
|
||||
@@ -22,10 +22,9 @@ Thanks to all who contributed to this release, among them:
|
||||
* Ronny Pfannschmidt
|
||||
* Sankt Petersbug
|
||||
* Tyler Richard
|
||||
* Victor
|
||||
* Victor Maryama
|
||||
* Vlad Shcherbina
|
||||
* turturica
|
||||
* victor
|
||||
* wim glenn
|
||||
|
||||
|
||||
|
||||
38
doc/en/announce/release-3.8.0.rst
Normal file
38
doc/en/announce/release-3.8.0.rst
Normal file
@@ -0,0 +1,38 @@
|
||||
pytest-3.8.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 3.8.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 bugs 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
|
||||
* CrazyMerlyn
|
||||
* Daniel Hahler
|
||||
* Fabio Zadrozny
|
||||
* Jeffrey Rackauckas
|
||||
* Ronny Pfannschmidt
|
||||
* Virgil Dupras
|
||||
* dhirensr
|
||||
* hoefling
|
||||
* wim glenn
|
||||
|
||||
|
||||
Happy testing,
|
||||
The Pytest Development Team
|
||||
25
doc/en/announce/release-3.8.1.rst
Normal file
25
doc/en/announce/release-3.8.1.rst
Normal file
@@ -0,0 +1,25 @@
|
||||
pytest-3.8.1
|
||||
=======================================
|
||||
|
||||
pytest 3.8.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:
|
||||
|
||||
* Ankit Goel
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* Daniel Hahler
|
||||
* Maximilian Albert
|
||||
* Ronny Pfannschmidt
|
||||
* William Jamir Silva
|
||||
* wim glenn
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
28
doc/en/announce/release-3.8.2.rst
Normal file
28
doc/en/announce/release-3.8.2.rst
Normal file
@@ -0,0 +1,28 @@
|
||||
pytest-3.8.2
|
||||
=======================================
|
||||
|
||||
pytest 3.8.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:
|
||||
|
||||
* Ankit Goel
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* Daniel Hahler
|
||||
* Denis Otkidach
|
||||
* Harry Percival
|
||||
* Jeffrey Rackauckas
|
||||
* Jose Carlos Menezes
|
||||
* Ronny Pfannschmidt
|
||||
* Zac-HD
|
||||
* iwanb
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
43
doc/en/announce/release-3.9.0.rst
Normal file
43
doc/en/announce/release-3.9.0.rst
Normal file
@@ -0,0 +1,43 @@
|
||||
pytest-3.9.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 3.9.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 bugs 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:
|
||||
|
||||
* Andrea Cimatoribus
|
||||
* Ankit Goel
|
||||
* Anthony Sottile
|
||||
* Ben Eyal
|
||||
* Bruno Oliveira
|
||||
* Daniel Hahler
|
||||
* Jeffrey Rackauckas
|
||||
* Jose Carlos Menezes
|
||||
* Kyle Altendorf
|
||||
* Niklas JQ
|
||||
* Palash Chatterjee
|
||||
* Ronny Pfannschmidt
|
||||
* Thomas Hess
|
||||
* Thomas Hisch
|
||||
* Tomer Keren
|
||||
* Victor Maryama
|
||||
|
||||
|
||||
Happy testing,
|
||||
The Pytest Development Team
|
||||
20
doc/en/announce/release-3.9.1.rst
Normal file
20
doc/en/announce/release-3.9.1.rst
Normal file
@@ -0,0 +1,20 @@
|
||||
pytest-3.9.1
|
||||
=======================================
|
||||
|
||||
pytest 3.9.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
|
||||
* Ronny Pfannschmidt
|
||||
* Thomas Hisch
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
23
doc/en/announce/release-3.9.2.rst
Normal file
23
doc/en/announce/release-3.9.2.rst
Normal file
@@ -0,0 +1,23 @@
|
||||
pytest-3.9.2
|
||||
=======================================
|
||||
|
||||
pytest 3.9.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:
|
||||
|
||||
* Ankit Goel
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* Ronny Pfannschmidt
|
||||
* Vincent Barbaresi
|
||||
* ykantor
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
@@ -264,8 +264,12 @@ Advanced assertion introspection
|
||||
Reporting details about a failing assertion is achieved by rewriting assert
|
||||
statements before they are run. Rewritten assert statements put introspection
|
||||
information into the assertion failure message. ``pytest`` only rewrites test
|
||||
modules directly discovered by its test collection process, so asserts in
|
||||
supporting modules which are not themselves test modules will not be rewritten.
|
||||
modules directly discovered by its test collection process, so **asserts in
|
||||
supporting modules which are not themselves test modules will not be rewritten**.
|
||||
|
||||
You can manually enable assertion rewriting for an imported module by calling
|
||||
`register_assert_rewrite <https://docs.pytest.org/en/latest/writing_plugins.html#assertion-rewriting>`_
|
||||
before you import it (a good place to do that is in ``conftest.py``).
|
||||
|
||||
.. note::
|
||||
|
||||
|
||||
@@ -7,14 +7,16 @@ Keeping backwards compatibility has a very high priority in the pytest project.
|
||||
|
||||
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.
|
||||
|
||||
To communicate changes we are already issuing deprecation warnings, but they are not displayed by default. In pytest 3.0 we changed the default setting so that pytest deprecation warnings are displayed if not explicitly silenced (with ``--disable-pytest-warnings``).
|
||||
To communicate changes we issue deprecation warnings using a custom warning hierarchy (see :ref:`internal-warnings`). These warnings may be suppressed using the standard means: ``-W`` command-line flag or ``filterwarnings`` ini options (see :ref:`warnings`), but we suggest to use these sparingly and temporarily, and heed the warnings when possible.
|
||||
|
||||
We will only remove deprecated functionality in major releases (e.g. if we deprecate something in 3.0 we will remove it in 4.0), and keep it around for at least two minor releases (e.g. if we deprecate something in 3.9 and 4.0 is the next release, we will not remove it in 4.0 but in 5.0).
|
||||
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.
|
||||
|
||||
|
||||
Deprecation Roadmap
|
||||
-------------------
|
||||
|
||||
We track deprecation and removal of features using milestones and the `deprecation <https://github.com/pytest-dev/pytest/issues?q=label%3A%22type%3A+deprecation%22>`_ and `removal <https://github.com/pytest-dev/pytest/labels/type%3A%20removal>`_ labels on GitHub.
|
||||
Features currently deprecated and removed in previous releases can be found in :ref:`deprecations`.
|
||||
|
||||
Following our deprecation policy, after starting issuing deprecation warnings we keep features for *at least* two minor versions before considering removal.
|
||||
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.
|
||||
|
||||
@@ -104,7 +104,9 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
See http://docs.python.org/library/warnings.html for information
|
||||
on warning categories.
|
||||
tmpdir_factory
|
||||
Return a TempdirFactory instance for the test session.
|
||||
Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session.
|
||||
tmp_path_factory
|
||||
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,
|
||||
@@ -113,6 +115,16 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
path object.
|
||||
|
||||
.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
|
||||
tmp_path
|
||||
Return a temporary directory path object
|
||||
which is unique to each test function invocation,
|
||||
created as a sub directory of the base temporary
|
||||
directory. The returned object is a :class:`pathlib.Path`
|
||||
object.
|
||||
|
||||
.. note::
|
||||
|
||||
in python < 3.6 this is a pathlib2.Path
|
||||
|
||||
no tests ran in 0.12 seconds
|
||||
|
||||
|
||||
@@ -329,7 +329,7 @@ texinfo_documents = [
|
||||
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {"python": ("http://docs.python.org/3", None)}
|
||||
intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}
|
||||
|
||||
|
||||
def setup(app):
|
||||
|
||||
@@ -39,6 +39,7 @@ Full pytest documentation
|
||||
bash-completion
|
||||
|
||||
backwards-compatibility
|
||||
deprecations
|
||||
historical-notes
|
||||
license
|
||||
contributing
|
||||
|
||||
@@ -32,7 +32,7 @@ Here's a summary what ``pytest`` uses ``rootdir`` for:
|
||||
class name, function name and parametrization (if any).
|
||||
|
||||
* Is used by plugins as a stable location to store project/test run specific information;
|
||||
for example, the internal :ref:`cache <cache>` plugin creates a ``.cache`` subdirectory
|
||||
for example, the internal :ref:`cache <cache>` plugin creates a ``.pytest_cache`` subdirectory
|
||||
in ``rootdir`` to store its cross-test run state.
|
||||
|
||||
Important to emphasize that ``rootdir`` is **NOT** used to modify ``sys.path``/``PYTHONPATH`` or
|
||||
|
||||
386
doc/en/deprecations.rst
Normal file
386
doc/en/deprecations.rst
Normal file
@@ -0,0 +1,386 @@
|
||||
.. _deprecations:
|
||||
|
||||
Deprecations and Removals
|
||||
=========================
|
||||
|
||||
This page lists all pytest features that are currently deprecated or have been removed in past major releases.
|
||||
The objective is to give users a clear rationale why a certain feature has been removed, and what alternatives
|
||||
should be used instead.
|
||||
|
||||
Deprecated Features
|
||||
-------------------
|
||||
|
||||
Below is a complete list of all pytest features which are considered deprecated. Using those features will issue
|
||||
:class:`_pytest.warning_types.PytestWarning` or subclasses, which can be filtered using
|
||||
:ref:`standard warning filters <warnings>`.
|
||||
|
||||
Internal classes accessed through ``Node``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.9
|
||||
|
||||
Access of ``Module``, ``Function``, ``Class``, ``Instance``, ``File`` and ``Item`` through ``Node`` instances now issue
|
||||
this warning::
|
||||
|
||||
usage of Function.Module is deprecated, please use pytest.Module instead
|
||||
|
||||
Users should just ``import pytest`` and access those objects using the ``pytest`` module.
|
||||
|
||||
This has been documented as deprecated for years, but only now we are actually emitting deprecation warnings.
|
||||
|
||||
``cached_setup``
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.9
|
||||
|
||||
``request.cached_setup`` was the precursor of the setup/teardown mechanism available to fixtures.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.fixture
|
||||
def db_session():
|
||||
return request.cached_setup(
|
||||
setup=Session.create, teardown=lambda session: session.close(), scope="module"
|
||||
)
|
||||
|
||||
This should be updated to make use of standard fixture mechanisms:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def db_session():
|
||||
session = Session.create()
|
||||
yield session
|
||||
session.close()
|
||||
|
||||
|
||||
You can consult `funcarg comparison section in the docs <https://docs.pytest.org/en/latest/funcarg_compare.html>`_ for
|
||||
more information.
|
||||
|
||||
This has been documented as deprecated for years, but only now we are actually emitting deprecation warnings.
|
||||
|
||||
|
||||
Using ``Class`` in custom Collectors
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.9
|
||||
|
||||
Using objects named ``"Class"`` as a way to customize the type of nodes that are collected in ``Collector``
|
||||
subclasses has been deprecated. Users instead should use ``pytest_pycollect_makeitem`` to customize node types during
|
||||
collection.
|
||||
|
||||
This issue should affect only advanced plugins who create new collection types, so if you see this warning
|
||||
message please contact the authors so they can change the code.
|
||||
|
||||
|
||||
``Config.warn`` and ``Node.warn``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.8
|
||||
|
||||
Those methods were part of the internal pytest warnings system, but since ``3.8`` pytest is using the builtin warning
|
||||
system for its own warnings, so those two functions are now deprecated.
|
||||
|
||||
``Config.warn`` should be replaced by calls to the standard ``warnings.warn``, example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
config.warn("C1", "some warning")
|
||||
|
||||
Becomes:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
warnings.warn(pytest.PytestWarning("some warning"))
|
||||
|
||||
``Node.warn`` now supports two signatures:
|
||||
|
||||
* ``node.warn(PytestWarning("some message"))``: is now the **recommended** way to call this function.
|
||||
The warning instance must be a PytestWarning or subclass.
|
||||
|
||||
* ``node.warn("CI", "some message")``: this code/message form is now **deprecated** and should be converted to the warning instance form above.
|
||||
|
||||
|
||||
``pytest_namespace``
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.7
|
||||
|
||||
This hook is deprecated because it greatly complicates the pytest internals regarding configuration and initialization, making some
|
||||
bug fixes and refactorings impossible.
|
||||
|
||||
Example of usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MySymbol:
|
||||
...
|
||||
|
||||
|
||||
def pytest_namespace():
|
||||
return {"my_symbol": MySymbol()}
|
||||
|
||||
|
||||
Plugin authors relying on this hook should instead require that users now import the plugin modules directly (with an appropriate public API).
|
||||
|
||||
As a stopgap measure, plugin authors may still inject their names into pytest's namespace, usually during ``pytest_configure``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def pytest_configure():
|
||||
pytest.my_symbol = MySymbol()
|
||||
|
||||
|
||||
|
||||
Calling fixtures directly
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.7
|
||||
|
||||
Calling a fixture function directly, as opposed to request them in a test function, is deprecated.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.fixture
|
||||
def cell():
|
||||
return ...
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def full_cell():
|
||||
cell = cell()
|
||||
cell.make_full()
|
||||
return cell
|
||||
|
||||
This is a great source of confusion to new users, which will often call the fixture functions and request them from test functions interchangeably, which breaks the fixture resolution model.
|
||||
|
||||
In those cases just request the function directly in the dependent fixture:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.fixture
|
||||
def cell():
|
||||
return ...
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def full_cell(cell):
|
||||
cell.make_full()
|
||||
return cell
|
||||
|
||||
``Node.get_marker``
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.6
|
||||
|
||||
As part of a large :ref:`marker-revamp`, :meth:`_pytest.nodes.Node.get_marker` is deprecated. See
|
||||
:ref:`the documentation <update marker code>` on tips on how to update your code.
|
||||
|
||||
|
||||
record_xml_property
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.5
|
||||
|
||||
The ``record_xml_property`` fixture is now deprecated in favor of the more generic ``record_property``, which
|
||||
can be used by other consumers (for example ``pytest-html``) to obtain custom information about the test run.
|
||||
|
||||
This is just a matter of renaming the fixture as the API is the same:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_foo(record_xml_property):
|
||||
...
|
||||
|
||||
Change to:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_foo(record_property):
|
||||
...
|
||||
|
||||
pytest_plugins in non-top-level conftest files
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.5
|
||||
|
||||
Defining ``pytest_plugins`` is now deprecated in non-top-level conftest.py
|
||||
files because they will activate referenced plugins *globally*, which is surprising because for all other pytest
|
||||
features ``conftest.py`` files are only *active* for tests at or below it.
|
||||
|
||||
Metafunc.addcall
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.3
|
||||
|
||||
:meth:`_pytest.python.Metafunc.addcall` was a precursor to the current parametrized mechanism. Users should use
|
||||
:meth:`_pytest.python.Metafunc.parametrize` instead.
|
||||
|
||||
marks in ``pytest.mark.parametrize``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.2
|
||||
|
||||
Applying marks to values of a ``pytest.mark.parametrize`` call is now deprecated. For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"a, b", [(3, 9), pytest.mark.xfail(reason="flaky")(6, 36), (10, 100)]
|
||||
)
|
||||
def test_foo(a, b):
|
||||
...
|
||||
|
||||
This code applies the ``pytest.mark.xfail(reason="flaky")`` mark to the ``(6, 36)`` value of the above parametrization
|
||||
call.
|
||||
|
||||
This was considered hard to read and understand, and also its implementation presented problems to the code preventing
|
||||
further internal improvements in the marks architecture.
|
||||
|
||||
To update the code, use ``pytest.param``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"a, b",
|
||||
[(3, 9), pytest.param((6, 36), marks=pytest.mark.xfail(reason="flaky")), (10, 100)],
|
||||
)
|
||||
def test_foo(a, b):
|
||||
...
|
||||
|
||||
|
||||
|
||||
Passing command-line string to ``pytest.main()``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.0
|
||||
|
||||
Passing a command-line string to ``pytest.main()`` is deprecated:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
pytest.main("-v -s")
|
||||
|
||||
Pass a list instead:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
pytest.main(["-v", "-s"])
|
||||
|
||||
|
||||
By passing a string, users expect that pytest will interpret that command-line using the shell rules they are working
|
||||
on (for example ``bash`` or ``Powershell``), but this is very hard/impossible to do in a portable way.
|
||||
|
||||
|
||||
``yield`` tests
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.0
|
||||
|
||||
pytest supports ``yield``-style tests, where a test function actually ``yield`` functions and values
|
||||
that are then turned into proper test methods. Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def check(x, y):
|
||||
assert x ** x == y
|
||||
|
||||
|
||||
def test_squared():
|
||||
yield check, 2, 4
|
||||
yield check, 3, 9
|
||||
|
||||
This would result into two actual test functions being generated.
|
||||
|
||||
This form of test function doesn't support fixtures properly, and users should switch to ``pytest.mark.parametrize``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.parametrize("x, y", [(2, 4), (3, 9)])
|
||||
def test_squared(x, y):
|
||||
assert x ** x == y
|
||||
|
||||
|
||||
``pytest_funcarg__`` prefix
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.0
|
||||
|
||||
In very early pytest versions fixtures could be defined using the ``pytest_funcarg__`` prefix:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def pytest_funcarg__data():
|
||||
return SomeData()
|
||||
|
||||
Switch over to the ``@pytest.fixture`` decorator:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.fixture
|
||||
def data():
|
||||
return SomeData()
|
||||
|
||||
[pytest] section in setup.cfg files
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.0
|
||||
|
||||
``[pytest]`` sections in ``setup.cfg`` files should now be named ``[tool:pytest]``
|
||||
to avoid conflicts with other distutils commands.
|
||||
|
||||
Result log (``--result-log``)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.0
|
||||
|
||||
The ``--resultlog`` command line option has been deprecated: it is little used
|
||||
and there are more modern and better alternatives, for example `pytest-tap <https://tappy.readthedocs.io/en/latest/>`_.
|
||||
|
||||
Removed Features
|
||||
----------------
|
||||
|
||||
As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after
|
||||
an appropriate period of deprecation has passed.
|
||||
|
||||
|
||||
Reinterpretation mode (``--assert=reinterp``)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
*Removed in version 3.0.*
|
||||
|
||||
Reinterpretation mode has now been removed and only plain and rewrite
|
||||
mode are available, consequently the ``--assert=reinterp`` option is
|
||||
no longer available. This also means files imported from plugins or
|
||||
``conftest.py`` will not benefit from improved assertions by
|
||||
default, you should use ``pytest.register_assert_rewrite()`` to
|
||||
explicitly turn on assertion rewriting for those files.
|
||||
|
||||
Removed command-line options
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
*Removed in version 3.0.*
|
||||
|
||||
The following deprecated commandline options were removed:
|
||||
|
||||
* ``--genscript``: no longer supported;
|
||||
* ``--no-assert``: use ``--assert=plain`` instead;
|
||||
* ``--nomagic``: use ``--assert=plain`` instead;
|
||||
* ``--report``: use ``-r`` instead;
|
||||
|
||||
py.test-X* entry points
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
*Removed in version 3.0.*
|
||||
|
||||
Removed all ``py.test-X*`` entry points. The versioned, suffixed entry points
|
||||
were never documented and a leftover from a pre-virtualenv era. These entry
|
||||
points also created broken entry points in wheels, so removing them also
|
||||
removes a source of confusion for users.
|
||||
@@ -247,7 +247,7 @@ class TestCustomAssertMsg(object):
|
||||
b = 2
|
||||
assert (
|
||||
A.a == b
|
||||
), "A.a appears not to be b\n" "or does not appear to be b\none of those"
|
||||
), "A.a appears not to be b\nor does not appear to be b\none of those"
|
||||
|
||||
def test_custom_repr(self):
|
||||
class JSON(object):
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
hello = "world"
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import py
|
||||
|
||||
failure_demo = py.path.local(__file__).dirpath("failure_demo.py")
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
|
||||
@@ -582,7 +582,7 @@ get on the terminal - we are working on that)::
|
||||
b = 2
|
||||
> assert (
|
||||
A.a == b
|
||||
), "A.a appears not to be b\n" "or does not appear to be b\none of those"
|
||||
), "A.a appears not to be b\nor does not appear to be b\none of those"
|
||||
E AssertionError: A.a appears not to be b
|
||||
E or does not appear to be b
|
||||
E one of those
|
||||
@@ -613,9 +613,9 @@ get on the terminal - we are working on that)::
|
||||
|
||||
failure_demo.py:261: AssertionError
|
||||
============================= warnings summary =============================
|
||||
<undetermined location>
|
||||
Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0.
|
||||
Please use Metafunc.parametrize instead.
|
||||
$REGENDOC_TMPDIR/assertion/failure_demo.py:24: RemovedInPytest4Warning: Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0.
|
||||
Please use Metafunc.parametrize instead.
|
||||
metafunc.addcall(funcargs=dict(param1=3, param2=6))
|
||||
|
||||
-- Docs: https://docs.pytest.org/en/latest/warnings.html
|
||||
================== 42 failed, 1 warnings in 0.12 seconds ===================
|
||||
|
||||
@@ -574,7 +574,7 @@ We can run this::
|
||||
file $REGENDOC_TMPDIR/b/test_error.py, line 1
|
||||
def test_root(db): # no db here, will error out
|
||||
E fixture 'db' not found
|
||||
> available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_xml_attribute, record_xml_property, recwarn, tmpdir, tmpdir_factory
|
||||
> available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_xml_attribute, record_xml_property, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory
|
||||
> use 'pytest --fixtures [testpath]' for help on them.
|
||||
|
||||
$REGENDOC_TMPDIR/b/test_error.py:1
|
||||
@@ -852,6 +852,8 @@ In that order.
|
||||
can be changed between releases (even bug fixes) so it shouldn't be relied on for scripting
|
||||
or automation.
|
||||
|
||||
.. _freezing-pytest:
|
||||
|
||||
Freezing pytest
|
||||
---------------
|
||||
|
||||
|
||||
@@ -171,6 +171,7 @@ to cause the decorated ``smtp_connection`` fixture function to only be invoked
|
||||
once per test *module* (the default is to invoke once per test *function*).
|
||||
Multiple test functions in a test module will thus
|
||||
each receive the same ``smtp_connection`` fixture instance, thus saving time.
|
||||
Possible values for ``scope`` are: ``function``, ``class``, ``module``, ``package`` or ``session``.
|
||||
|
||||
The next example puts the fixture function into a separate ``conftest.py`` file
|
||||
so that tests from multiple test modules in the directory can
|
||||
@@ -258,6 +259,11 @@ instance, you can simply declare it:
|
||||
|
||||
Finally, the ``class`` scope will invoke the fixture once per test *class*.
|
||||
|
||||
.. note::
|
||||
|
||||
Pytest will only cache one instance of a fixture at a time.
|
||||
This means that when using a parametrized fixture, pytest may invoke a fixture more than once in the given scope.
|
||||
|
||||
|
||||
``package`` scope (experimental)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Installation and Getting Started
|
||||
===================================
|
||||
|
||||
**Pythons**: Python 2.7, 3.4, 3.5, 3.6, Jython, PyPy-2.3
|
||||
**Pythons**: Python 2.7, 3.4, 3.5, 3.6, 3.7, Jython, PyPy-2.3
|
||||
|
||||
**Platforms**: Unix/Posix and Windows
|
||||
|
||||
|
||||
@@ -175,3 +175,13 @@ Previous to version 2.4 to set a break point in code one needed to use ``pytest.
|
||||
This is no longer needed and one can use the native ``import pdb;pdb.set_trace()`` call directly.
|
||||
|
||||
For more details see :ref:`breakpoints`.
|
||||
|
||||
"compat" properties
|
||||
-------------------
|
||||
|
||||
.. deprecated:: 3.9
|
||||
|
||||
Access of ``Module``, ``Function``, ``Class``, ``Instance``, ``File`` and ``Item`` through ``Node`` instances have long
|
||||
been documented as deprecated, but started to emit warnings from pytest ``3.9`` and onward.
|
||||
|
||||
Users should just ``import pytest`` and access those objects using the ``pytest`` module.
|
||||
|
||||
@@ -52,6 +52,8 @@ should add ``--strict`` to ``addopts``:
|
||||
serial
|
||||
|
||||
|
||||
.. _marker-revamp:
|
||||
|
||||
Marker revamp and iteration
|
||||
---------------------------
|
||||
|
||||
|
||||
@@ -69,17 +69,15 @@ You may also discover more plugins through a `pytest- pypi.python.org search`_.
|
||||
Requiring/Loading plugins in a test module or conftest file
|
||||
-----------------------------------------------------------
|
||||
|
||||
You can require plugins in a test module or a conftest file like this::
|
||||
You can require plugins in a test module or a conftest file like this:
|
||||
|
||||
pytest_plugins = "myapp.testsupport.myplugin",
|
||||
.. code-block:: python
|
||||
|
||||
pytest_plugins = ("myapp.testsupport.myplugin",)
|
||||
|
||||
When the test module or conftest plugin is loaded the specified plugins
|
||||
will be loaded as well.
|
||||
|
||||
pytest_plugins = "myapp.testsupport.myplugin"
|
||||
|
||||
which will import the specified module as a ``pytest`` plugin.
|
||||
|
||||
.. note::
|
||||
Requiring plugins using a ``pytest_plugins`` variable in non-root
|
||||
``conftest.py`` files is deprecated. See
|
||||
|
||||
@@ -84,6 +84,12 @@ pytest.warns
|
||||
.. autofunction:: pytest.warns(expected_warning: Exception, [match])
|
||||
:with:
|
||||
|
||||
pytest.freeze_includes
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Tutorial**: :ref:`freezing-pytest`.
|
||||
|
||||
.. autofunction:: pytest.freeze_includes
|
||||
|
||||
.. _`marks ref`:
|
||||
|
||||
@@ -172,7 +178,7 @@ Mark a test function as using the given fixture names.
|
||||
|
||||
.. warning::
|
||||
|
||||
This mark can be used with *test functions* only, having no affect when applied
|
||||
This mark has no effect when applied
|
||||
to a **fixture** function.
|
||||
|
||||
.. py:function:: pytest.mark.usefixtures(*names)
|
||||
@@ -611,6 +617,8 @@ Session related reporting hooks:
|
||||
.. autofunction:: pytest_terminal_summary
|
||||
.. autofunction:: pytest_fixture_setup
|
||||
.. autofunction:: pytest_fixture_post_finalizer
|
||||
.. autofunction:: pytest_logwarning
|
||||
.. autofunction:: pytest_warning_captured
|
||||
|
||||
And here is the central hook for reporting about
|
||||
test execution:
|
||||
@@ -866,6 +874,11 @@ Contains comma-separated list of modules that should be loaded as plugins:
|
||||
|
||||
export PYTEST_PLUGINS=mymodule.plugin,xdist
|
||||
|
||||
PYTEST_DISABLE_PLUGIN_AUTOLOAD
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When set, disables plugin auto-loading through setuptools entrypoints. Only explicitly specified plugins will be
|
||||
loaded.
|
||||
|
||||
PYTEST_CURRENT_TEST
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
@@ -935,6 +948,7 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
|
||||
* ``classic``: classic pytest output.
|
||||
* ``progress``: like classic pytest output, but with a progress indicator.
|
||||
* ``count``: like progress, but shows progress as the number of tests completed instead of a percent.
|
||||
|
||||
The default is ``progress``, but you can fallback to ``classic`` if you prefer or
|
||||
the new mode is causing unexpected problems:
|
||||
@@ -968,6 +982,7 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
|
||||
* ``skip`` skips tests with an empty parameterset (default)
|
||||
* ``xfail`` marks tests with an empty parameterset as xfail(run=False)
|
||||
* ``fail_at_collect`` raises an exception if parametrize collects an empty parameter set
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
@@ -1248,15 +1263,25 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
|
||||
One or more Glob-style file patterns determining which python files
|
||||
are considered as test modules. Search for multiple glob patterns by
|
||||
adding a space between patterns::
|
||||
adding a space between patterns:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
python_files = test_*.py check_*.py example_*.py
|
||||
|
||||
By default, pytest will consider any file matching with ``test_*.py``
|
||||
and ``*_test.py`` globs as a test module.
|
||||
Or one per line:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
python_files =
|
||||
test_*.py
|
||||
check_*.py
|
||||
example_*.py
|
||||
|
||||
By default, files matching ``test_*.py`` and ``*_test.py`` will be considered
|
||||
test modules.
|
||||
|
||||
|
||||
.. confval:: python_functions
|
||||
|
||||
@@ -58,18 +58,20 @@ by calling the ``pytest.skip(reason)`` function:
|
||||
if not valid_config():
|
||||
pytest.skip("unsupported configuration")
|
||||
|
||||
The imperative method is useful when it is not possible to evaluate the skip condition
|
||||
during import time.
|
||||
|
||||
It is also possible to skip the whole module using
|
||||
``pytest.skip(reason, allow_module_level=True)`` at the module level:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
if not pytest.config.getoption("--custom-flag"):
|
||||
pytest.skip("--custom-flag is missing, skipping tests", allow_module_level=True)
|
||||
if not sys.platform.startswith("win"):
|
||||
pytest.skip("skipping windows-only tests", allow_module_level=True)
|
||||
|
||||
The imperative method is useful when it is not possible to evaluate the skip condition
|
||||
during import time.
|
||||
|
||||
**Reference**: :ref:`pytest.mark.skip ref`
|
||||
|
||||
@@ -277,7 +279,7 @@ on a particular platform::
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you want to be more specific as to why the test is failing, you can specify
|
||||
a single exception, or a list of exceptions, in the ``raises`` argument.
|
||||
a single exception, or a tuple of exceptions, in the ``raises`` argument.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ Talks and Tutorials
|
||||
Books
|
||||
---------------------------------------------
|
||||
|
||||
- `pytest Quick Start Guide, by Bruno Oliveira (2018)
|
||||
<https://www.packtpub.com/web-development/pytest-quick-start-guide>`_.
|
||||
|
||||
- `Python Testing with pytest, by Brian Okken (2017)
|
||||
<https://pragprog.com/book/bopytest/python-testing-with-pytest>`_.
|
||||
|
||||
|
||||
@@ -5,6 +5,76 @@
|
||||
Temporary directories and files
|
||||
================================================
|
||||
|
||||
The ``tmp_path`` fixture
|
||||
------------------------
|
||||
|
||||
.. versionadded:: 3.9
|
||||
|
||||
|
||||
You can use the ``tmp_path`` fixture which will
|
||||
provide a temporary directory unique to the test invocation,
|
||||
created in the `base temporary directory`_.
|
||||
|
||||
``tmp_path`` is a ``pathlib/pathlib2.Path`` object. Here is an example test usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# content of test_tmp_path.py
|
||||
import os
|
||||
|
||||
CONTENT = u"content"
|
||||
|
||||
|
||||
def test_create_file(tmp_path):
|
||||
d = tmp_path / "sub"
|
||||
d.mkdir()
|
||||
p = d / "hello.txt"
|
||||
p.write_text(CONTENT)
|
||||
assert p.read_text() == CONTENT
|
||||
assert len(list(tmp_path.iterdir())) == 1
|
||||
assert 0
|
||||
|
||||
Running this would result in a passed test except for the last
|
||||
``assert 0`` line which we use to look at values::
|
||||
|
||||
$ pytest test_tmp_path.py
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 1 item
|
||||
|
||||
test_tmp_path.py F [100%]
|
||||
|
||||
================================= FAILURES =================================
|
||||
_____________________________ test_create_file _____________________________
|
||||
|
||||
tmp_path = PosixPath('PYTEST_TMPDIR/test_create_file0')
|
||||
|
||||
def test_create_file(tmp_path):
|
||||
d = tmp_path / "sub"
|
||||
d.mkdir()
|
||||
p = d / "hello.txt"
|
||||
p.write_text(CONTENT)
|
||||
assert p.read_text() == CONTENT
|
||||
assert len(list(tmp_path.iterdir())) == 1
|
||||
> assert 0
|
||||
E assert 0
|
||||
|
||||
test_tmp_path.py:13: AssertionError
|
||||
========================= 1 failed in 0.12 seconds =========================
|
||||
|
||||
The ``tmp_path_factory`` fixture
|
||||
--------------------------------
|
||||
|
||||
.. versionadded:: 3.9
|
||||
|
||||
|
||||
The ``tmp_path_factory`` is a session-scoped fixture which can be used
|
||||
to create arbitrary temporary directories from any other fixture or test.
|
||||
|
||||
It is intended to replace ``tmpdir_factory``, and returns :class:`pathlib.Path` instances.
|
||||
|
||||
|
||||
The 'tmpdir' fixture
|
||||
--------------------
|
||||
|
||||
|
||||
@@ -22,16 +22,15 @@ Almost all ``unittest`` features are supported:
|
||||
|
||||
* ``@unittest.skip`` style decorators;
|
||||
* ``setUp/tearDown``;
|
||||
* ``setUpClass/tearDownClass()``;
|
||||
* ``setUpClass/tearDownClass``;
|
||||
* ``setUpModule/tearDownModule``;
|
||||
|
||||
.. _`load_tests protocol`: https://docs.python.org/3/library/unittest.html#load-tests-protocol
|
||||
.. _`setUpModule/tearDownModule`: https://docs.python.org/3/library/unittest.html#setupmodule-and-teardownmodule
|
||||
.. _`subtests`: https://docs.python.org/3/library/unittest.html#distinguishing-test-iterations-using-subtests
|
||||
|
||||
Up to this point pytest does not have support for the following features:
|
||||
|
||||
* `load_tests protocol`_;
|
||||
* `setUpModule/tearDownModule`_;
|
||||
* `subtests`_;
|
||||
|
||||
Benefits out of the box
|
||||
|
||||
@@ -140,6 +140,49 @@ will be shown (because KeyboardInterrupt is caught by pytest). By using this
|
||||
option you make sure a trace is shown.
|
||||
|
||||
|
||||
.. _`pytest.detailed_failed_tests_usage`:
|
||||
|
||||
Detailed summary report
|
||||
-----------------------
|
||||
|
||||
.. versionadded:: 2.9
|
||||
|
||||
The ``-r`` flag can be used to display test results summary 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.
|
||||
|
||||
Example::
|
||||
|
||||
$ pytest -ra
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 0 items
|
||||
|
||||
======================= no tests ran in 0.12 seconds =======================
|
||||
|
||||
The ``-r`` options accepts a number of characters after it, with ``a`` used above meaning "all except passes".
|
||||
|
||||
Here is the full list of available characters that can be used:
|
||||
|
||||
- ``f`` - failed
|
||||
- ``E`` - error
|
||||
- ``s`` - skipped
|
||||
- ``x`` - xfailed
|
||||
- ``X`` - xpassed
|
||||
- ``p`` - passed
|
||||
- ``P`` - passed with output
|
||||
- ``a`` - all except ``pP``
|
||||
|
||||
More than one character can be used, so for example to only see failed and skipped tests, you can execute::
|
||||
|
||||
$ pytest -rfs
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 0 items
|
||||
|
||||
======================= no tests ran in 0.12 seconds =======================
|
||||
|
||||
.. _pdb-option:
|
||||
|
||||
Dropping to PDB_ (Python Debugger) on failures
|
||||
@@ -212,7 +255,7 @@ Pytest supports the use of ``breakpoint()`` with the following behaviours:
|
||||
|
||||
- When ``breakpoint()`` is called and ``PYTHONBREAKPOINT`` is set to the default value, pytest will use the custom internal PDB trace UI instead of the system default ``Pdb``.
|
||||
- When tests are complete, the system will default back to the system ``Pdb`` trace UI.
|
||||
- If ``--pdb`` is called on execution of pytest, the custom internal Pdb trace UI is used on ``bothbreakpoint()`` and failed tests/unhandled exceptions.
|
||||
- If ``--pdb`` is called on execution of pytest, the custom internal Pdb trace UI is used on both ``breakpoint()`` and failed tests/unhandled exceptions.
|
||||
- If ``--pdbcls`` is used, the custom class debugger will be executed when a test fails (as expected within existing behaviour), but also when ``breakpoint()`` is called from within a test, the custom class debugger will be instantiated.
|
||||
|
||||
.. _durations:
|
||||
@@ -226,6 +269,7 @@ To get a list of the slowest 10 test durations::
|
||||
|
||||
pytest --durations=10
|
||||
|
||||
By default, pytest will not show test durations that are too small (<0.01s) unless ``-vv`` is passed on the command-line.
|
||||
|
||||
Creating JUnitXML format files
|
||||
----------------------------------------------------
|
||||
|
||||
@@ -29,15 +29,12 @@ Running pytest now produces this output::
|
||||
test_show_warnings.py . [100%]
|
||||
|
||||
============================= warnings summary =============================
|
||||
test_show_warnings.py::test_one
|
||||
$REGENDOC_TMPDIR/test_show_warnings.py:4: UserWarning: api v1, should use functions from v2
|
||||
warnings.warn(UserWarning("api v1, should use functions from v2"))
|
||||
$REGENDOC_TMPDIR/test_show_warnings.py:4: UserWarning: api v1, should use functions from v2
|
||||
warnings.warn(UserWarning("api v1, should use functions from v2"))
|
||||
|
||||
-- Docs: https://docs.pytest.org/en/latest/warnings.html
|
||||
=================== 1 passed, 1 warnings in 0.12 seconds ===================
|
||||
|
||||
Pytest by default catches all warnings except for ``DeprecationWarning`` and ``PendingDeprecationWarning``.
|
||||
|
||||
The ``-W`` flag can be passed to control which warnings will be displayed or even turn
|
||||
them into errors::
|
||||
|
||||
@@ -78,7 +75,6 @@ Both ``-W`` command-line option and ``filterwarnings`` ini option are based on P
|
||||
`-W option`_ and `warnings.simplefilter`_, so please refer to those sections in the Python
|
||||
documentation for other examples and advanced usage.
|
||||
|
||||
|
||||
.. _`filterwarnings`:
|
||||
|
||||
``@pytest.mark.filterwarnings``
|
||||
@@ -117,24 +113,6 @@ decorator or to all tests in a module by setting the ``pytestmark`` variable:
|
||||
pytestmark = pytest.mark.filterwarnings("error")
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
Except for these features, pytest does not change the python warning filter; it only captures
|
||||
and displays the warnings which are issued with respect to the currently configured filter,
|
||||
including changes to the filter made by test functions or by the system under test.
|
||||
|
||||
.. note::
|
||||
|
||||
``DeprecationWarning`` and ``PendingDeprecationWarning`` are hidden by the standard library
|
||||
by default so you have to explicitly configure them to be displayed in your ``pytest.ini``:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
filterwarnings =
|
||||
once::DeprecationWarning
|
||||
once::PendingDeprecationWarning
|
||||
|
||||
|
||||
*Credits go to Florian Schulze for the reference implementation in the* `pytest-warnings`_
|
||||
*plugin.*
|
||||
@@ -143,18 +121,102 @@ decorator or to all tests in a module by setting the ``pytestmark`` variable:
|
||||
.. _warnings.simplefilter: https://docs.python.org/3/library/warnings.html#warnings.simplefilter
|
||||
.. _`pytest-warnings`: https://github.com/fschulze/pytest-warnings
|
||||
|
||||
Disabling warnings summary
|
||||
--------------------------
|
||||
|
||||
Disabling warning capture
|
||||
-------------------------
|
||||
Although not recommended, you can use the ``--disable-warnings`` command-line option to suppress the
|
||||
warning summary entirely from the test run output.
|
||||
|
||||
This feature is enabled by default but can be disabled entirely in your ``pytest.ini`` file with:
|
||||
Disabling warning capture entirely
|
||||
----------------------------------
|
||||
|
||||
This plugin is enabled by default but can be disabled entirely in your ``pytest.ini`` file with:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
addopts = -p no:warnings
|
||||
|
||||
Or passing ``-p no:warnings`` in the command-line.
|
||||
Or passing ``-p no:warnings`` in the command-line. This might be useful if your test suites handles warnings
|
||||
using an external system.
|
||||
|
||||
|
||||
.. _`deprecation-warnings`:
|
||||
|
||||
DeprecationWarning and PendingDeprecationWarning
|
||||
------------------------------------------------
|
||||
|
||||
.. versionadded:: 3.8
|
||||
.. versionchanged:: 3.9
|
||||
|
||||
By default pytest will display ``DeprecationWarning`` and ``PendingDeprecationWarning`` warnings from
|
||||
user code and third-party libraries, as recommended by `PEP-0506 <https://www.python.org/dev/peps/pep-0565>`_.
|
||||
This helps users keep their code modern and avoid breakages when deprecated warnings are effectively removed.
|
||||
|
||||
Sometimes it is useful to hide some specific deprecation warnings that happen in code that you have no control over
|
||||
(such as third-party libraries), in which case you might use the warning filters options (ini or marks) to ignore
|
||||
those warnings.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
filterwarnings =
|
||||
ignore:.*U.*mode is deprecated:DeprecationWarning
|
||||
|
||||
|
||||
This will ignore all warnings of type ``DeprecationWarning`` where the start of the message matches
|
||||
the regular expression ``".*U.*mode is deprecated"``.
|
||||
|
||||
.. note::
|
||||
If warnings are configured at the interpreter level, using
|
||||
the `PYTHONWARNINGS <https://docs.python.org/3/using/cmdline.html#envvar-PYTHONWARNINGS>`_ environment variable or the
|
||||
``-W`` command-line option, pytest will not configure any filters by default.
|
||||
|
||||
Also pytest doesn't follow ``PEP-0506`` suggestion of resetting all warning filters because
|
||||
it might break test suites that configure warning filters themselves
|
||||
by calling ``warnings.simplefilter`` (see issue `#2430 <https://github.com/pytest-dev/pytest/issues/2430>`_
|
||||
for an example of that).
|
||||
|
||||
|
||||
.. _`ensuring a function triggers a deprecation warning`:
|
||||
|
||||
.. _ensuring_function_triggers:
|
||||
|
||||
Ensuring code triggers a deprecation warning
|
||||
--------------------------------------------
|
||||
|
||||
You can also call a global helper for checking
|
||||
that a certain function call triggers a ``DeprecationWarning`` or
|
||||
``PendingDeprecationWarning``::
|
||||
|
||||
import pytest
|
||||
|
||||
def test_global():
|
||||
pytest.deprecated_call(myfunction, 17)
|
||||
|
||||
By default, ``DeprecationWarning`` and ``PendingDeprecationWarning`` will not be
|
||||
caught when using ``pytest.warns`` or ``recwarn`` because default Python warnings filters hide
|
||||
them. If you wish to record them in your own code, use the
|
||||
command ``warnings.simplefilter('always')``::
|
||||
|
||||
import warnings
|
||||
import pytest
|
||||
|
||||
def test_deprecation(recwarn):
|
||||
warnings.simplefilter('always')
|
||||
warnings.warn("deprecated", DeprecationWarning)
|
||||
assert len(recwarn) == 1
|
||||
assert recwarn.pop(DeprecationWarning)
|
||||
|
||||
You can also use it as a contextmanager::
|
||||
|
||||
def test_global():
|
||||
with pytest.deprecated_call():
|
||||
myobject.deprecated_method()
|
||||
|
||||
|
||||
|
||||
.. _`asserting warnings`:
|
||||
|
||||
@@ -261,38 +323,51 @@ warnings, or index into it to get a particular recorded warning.
|
||||
|
||||
Full API: :class:`WarningsRecorder`.
|
||||
|
||||
.. _`ensuring a function triggers a deprecation warning`:
|
||||
|
||||
.. _ensuring_function_triggers:
|
||||
.. _internal-warnings:
|
||||
|
||||
Ensuring a function triggers a deprecation warning
|
||||
-------------------------------------------------------
|
||||
Internal pytest warnings
|
||||
------------------------
|
||||
|
||||
You can also call a global helper for checking
|
||||
that a certain function call triggers a ``DeprecationWarning`` or
|
||||
``PendingDeprecationWarning``::
|
||||
.. versionadded:: 3.8
|
||||
|
||||
import pytest
|
||||
pytest may generate its own warnings in some situations, such as improper usage or deprecated features.
|
||||
|
||||
def test_global():
|
||||
pytest.deprecated_call(myfunction, 17)
|
||||
For example, pytest will emit a warning if it encounters a class that matches :confval:`python_classes` but also
|
||||
defines an ``__init__`` constructor, as this prevents the class from being instantiated:
|
||||
|
||||
By default, ``DeprecationWarning`` and ``PendingDeprecationWarning`` will not be
|
||||
caught when using ``pytest.warns`` or ``recwarn`` because default Python warnings filters hide
|
||||
them. If you wish to record them in your own code, use the
|
||||
command ``warnings.simplefilter('always')``::
|
||||
.. code-block:: python
|
||||
|
||||
import warnings
|
||||
import pytest
|
||||
# content of test_pytest_warnings.py
|
||||
class Test:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def test_deprecation(recwarn):
|
||||
warnings.simplefilter('always')
|
||||
warnings.warn("deprecated", DeprecationWarning)
|
||||
assert len(recwarn) == 1
|
||||
assert recwarn.pop(DeprecationWarning)
|
||||
def test_foo(self):
|
||||
assert 1 == 1
|
||||
|
||||
You can also use it as a contextmanager::
|
||||
::
|
||||
|
||||
def test_global():
|
||||
with pytest.deprecated_call():
|
||||
myobject.deprecated_method()
|
||||
$ pytest test_pytest_warnings.py -q
|
||||
|
||||
============================= warnings summary =============================
|
||||
$REGENDOC_TMPDIR/test_pytest_warnings.py:1: PytestWarning: cannot collect test class 'Test' because it has a __init__ constructor
|
||||
class Test:
|
||||
|
||||
-- Docs: https://docs.pytest.org/en/latest/warnings.html
|
||||
1 warnings in 0.12 seconds
|
||||
|
||||
These warnings might be filtered using the same builtin mechanisms used to filter other types of warnings.
|
||||
|
||||
Please read our :ref:`backwards-compatibility` to learn how we proceed about deprecating and eventually removing
|
||||
features.
|
||||
|
||||
The following warning types ares used by pytest and are part of the public API:
|
||||
|
||||
.. autoclass:: pytest.PytestWarning
|
||||
|
||||
.. autoclass:: pytest.PytestDeprecationWarning
|
||||
|
||||
.. autoclass:: pytest.RemovedInPytest4Warning
|
||||
|
||||
.. autoclass:: pytest.PytestExperimentalApiWarning
|
||||
|
||||
@@ -418,12 +418,23 @@ additionally it is possible to copy examples for a example folder before running
|
||||
test_example.py .. [100%]
|
||||
|
||||
============================= warnings summary =============================
|
||||
test_example.py::test_plugin
|
||||
$REGENDOC_TMPDIR/test_example.py:4: PytestExerimentalApiWarning: testdir.copy_example is an experimental api that may change over time
|
||||
testdir.copy_example("test_example.py")
|
||||
$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")
|
||||
$PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:329: RemovedInPytest4Warning: usage of Session.Class is deprecated, please use pytest.Class instead
|
||||
return getattr(object, name, default)
|
||||
$PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:329: RemovedInPytest4Warning: usage of Session.File is deprecated, please use pytest.File instead
|
||||
return getattr(object, name, default)
|
||||
$PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:329: RemovedInPytest4Warning: usage of Session.Function is deprecated, please use pytest.Function instead
|
||||
return getattr(object, name, default)
|
||||
$PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:329: RemovedInPytest4Warning: usage of Session.Instance is deprecated, please use pytest.Instance instead
|
||||
return getattr(object, name, default)
|
||||
$PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:329: RemovedInPytest4Warning: usage of Session.Item is deprecated, please use pytest.Item instead
|
||||
return getattr(object, name, default)
|
||||
$PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:329: RemovedInPytest4Warning: usage of Session.Module is deprecated, please use pytest.Module instead
|
||||
return getattr(object, name, default)
|
||||
|
||||
-- Docs: https://docs.pytest.org/en/latest/warnings.html
|
||||
=================== 2 passed, 1 warnings in 0.12 seconds ===================
|
||||
=================== 2 passed, 7 warnings in 0.12 seconds ===================
|
||||
|
||||
For more information about the result object that ``runpytest()`` returns, and
|
||||
the methods that it provides please check out the :py:class:`RunResult
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[build-system]
|
||||
requires = [
|
||||
"setuptools",
|
||||
# sync with setup.py until we discard non-pep-517/518
|
||||
"setuptools>=40.0",
|
||||
"setuptools-scm",
|
||||
"wheel",
|
||||
]
|
||||
@@ -15,7 +16,12 @@ template = "changelog/_template.rst"
|
||||
|
||||
[[tool.towncrier.type]]
|
||||
directory = "removal"
|
||||
name = "Deprecations and Removals"
|
||||
name = "Removals"
|
||||
showcontent = true
|
||||
|
||||
[[tool.towncrier.type]]
|
||||
directory = "deprecation"
|
||||
name = "Deprecations"
|
||||
showcontent = true
|
||||
|
||||
[[tool.towncrier.type]]
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
REM skip "coveralls" run in PRs or forks
|
||||
if "%TOXENV%" == "coveralls" (
|
||||
if not defined COVERALLS_REPO_TOKEN (
|
||||
echo skipping coveralls run because COVERALLS_REPO_TOKEN is not defined
|
||||
exit /b 0
|
||||
)
|
||||
)
|
||||
C:\Python36\python -m tox
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
"""Used by .pre-commit-config.yaml"""
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(" ".join(sys.argv[1:]))
|
||||
sys.exit(1)
|
||||
10
scripts/prepare-coverage.bat
Normal file
10
scripts/prepare-coverage.bat
Normal file
@@ -0,0 +1,10 @@
|
||||
REM scripts called by AppVeyor to setup the environment variables to enable coverage
|
||||
if not defined PYTEST_NO_COVERAGE (
|
||||
set "COVERAGE_FILE=%CD%\.coverage"
|
||||
set "COVERAGE_PROCESS_START=%CD%\.coveragerc"
|
||||
set "_PYTEST_TOX_COVERAGE_RUN=coverage run -m"
|
||||
set "_PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess"
|
||||
echo Coverage setup completed
|
||||
) else (
|
||||
echo Skipping coverage setup, PYTEST_NO_COVERAGE is set
|
||||
)
|
||||
11
scripts/upload-coverage.bat
Normal file
11
scripts/upload-coverage.bat
Normal file
@@ -0,0 +1,11 @@
|
||||
REM script called by AppVeyor to combine and upload coverage information to codecov
|
||||
if not defined PYTEST_NO_COVERAGE (
|
||||
echo Prepare to upload coverage information
|
||||
C:\Python36\Scripts\pip install codecov
|
||||
C:\Python36\Scripts\coverage combine
|
||||
C:\Python36\Scripts\coverage xml --ignore-errors
|
||||
C:\Python36\Scripts\coverage report -m --ignore-errors
|
||||
C:\Python36\Scripts\codecov --required -X gcov pycov search -f coverage.xml --flags %TOXENV:-= % windows
|
||||
) else (
|
||||
echo Skipping coverage upload, PYTEST_NO_COVERAGE is set
|
||||
)
|
||||
54
setup.cfg
54
setup.cfg
@@ -1,3 +1,55 @@
|
||||
[metadata]
|
||||
|
||||
name = pytest
|
||||
description = pytest: simple powerful testing with Python
|
||||
long_description = file: README.rst
|
||||
url = https://docs.pytest.org/en/latest/
|
||||
project_urls =
|
||||
Source=https://github.com/pytest-dev/pytest
|
||||
Tracker=https://github.com/pytest-dev/pytest/issues
|
||||
|
||||
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
|
||||
Intended Audience :: Developers
|
||||
License :: OSI Approved :: MIT License
|
||||
Operating System :: POSIX
|
||||
Operating System :: Microsoft :: Windows
|
||||
Operating System :: MacOS :: MacOS X
|
||||
Topic :: Software Development :: Testing
|
||||
Topic :: Software Development :: Libraries
|
||||
Topic :: Utilities
|
||||
Programming Language :: Python :: 2
|
||||
Programming Language :: Python :: 2.7
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3.4
|
||||
Programming Language :: Python :: 3.5
|
||||
Programming Language :: Python :: 3.6
|
||||
Programming Language :: Python :: 3.7
|
||||
platforms = unix, linux, osx, cygwin, win32
|
||||
|
||||
[options]
|
||||
zip_safe = no
|
||||
packages =
|
||||
_pytest
|
||||
_pytest.assertion
|
||||
_pytest._code
|
||||
_pytest.mark
|
||||
_pytest.config
|
||||
|
||||
py_modules = pytest
|
||||
python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*
|
||||
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
pytest=pytest:main
|
||||
py.test=pytest:main
|
||||
|
||||
[build_sphinx]
|
||||
source-dir = doc/en/
|
||||
build-dir = doc/build
|
||||
@@ -13,8 +65,6 @@ universal = 1
|
||||
ignore =
|
||||
_pytest/_version.py
|
||||
|
||||
[metadata]
|
||||
license_file = LICENSE
|
||||
|
||||
[devpi:upload]
|
||||
formats = sdist.tgz,bdist_wheel
|
||||
|
||||
130
setup.py
130
setup.py
@@ -1,126 +1,34 @@
|
||||
import os
|
||||
import sys
|
||||
import setuptools
|
||||
import pkg_resources
|
||||
from setuptools import setup
|
||||
|
||||
classifiers = [
|
||||
"Development Status :: 6 - Mature",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: POSIX",
|
||||
"Operating System :: Microsoft :: Windows",
|
||||
"Operating System :: MacOS :: MacOS X",
|
||||
"Topic :: Software Development :: Testing",
|
||||
"Topic :: Software Development :: Libraries",
|
||||
"Topic :: Utilities",
|
||||
] + [
|
||||
("Programming Language :: Python :: %s" % x)
|
||||
for x in "2 2.7 3 3.4 3.5 3.6 3.7".split()
|
||||
|
||||
# TODO: if py gets upgrade to >=1.6,
|
||||
# remove _width_of_current_line in terminal.py
|
||||
INSTALL_REQUIRES = [
|
||||
"py>=1.5.0",
|
||||
"six>=1.10.0",
|
||||
"setuptools",
|
||||
"attrs>=17.4.0",
|
||||
"more-itertools>=4.0.0",
|
||||
"atomicwrites>=1.0",
|
||||
'funcsigs;python_version<"3.0"',
|
||||
'pathlib2>=2.2.0;python_version<"3.6"',
|
||||
'colorama;sys_platform=="win32"',
|
||||
]
|
||||
|
||||
with open("README.rst") as fd:
|
||||
long_description = fd.read()
|
||||
|
||||
|
||||
def get_environment_marker_support_level():
|
||||
"""
|
||||
Tests how well setuptools supports PEP-426 environment marker.
|
||||
|
||||
The first known release to support it is 0.7 (and the earliest on PyPI seems to be 0.7.2
|
||||
so we're using that), see: https://setuptools.readthedocs.io/en/latest/history.html#id350
|
||||
|
||||
The support is later enhanced to allow direct conditional inclusions inside install_requires,
|
||||
which is now recommended by setuptools. It first appeared in 36.2.0, went broken with 36.2.1, and
|
||||
again worked since 36.2.2, so we're using that. See:
|
||||
https://setuptools.readthedocs.io/en/latest/history.html#v36-2-2
|
||||
https://github.com/pypa/setuptools/issues/1099
|
||||
|
||||
References:
|
||||
|
||||
* https://wheel.readthedocs.io/en/latest/index.html#defining-conditional-dependencies
|
||||
* https://www.python.org/dev/peps/pep-0426/#environment-markers
|
||||
* https://setuptools.readthedocs.io/en/latest/setuptools.html#declaring-platform-specific-dependencies
|
||||
"""
|
||||
try:
|
||||
version = pkg_resources.parse_version(setuptools.__version__)
|
||||
if version >= pkg_resources.parse_version("36.2.2"):
|
||||
return 2
|
||||
if version >= pkg_resources.parse_version("0.7.2"):
|
||||
return 1
|
||||
except Exception as exc:
|
||||
sys.stderr.write("Could not test setuptool's version: %s\n" % exc)
|
||||
|
||||
# as of testing on 2018-05-26 fedora was on version 37* and debian was on version 33+
|
||||
# we should consider erroring on those
|
||||
return 0
|
||||
# if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy;
|
||||
# used by tox.ini to test with pluggy master
|
||||
if "_PYTEST_SETUP_SKIP_PLUGGY_DEP" not in os.environ:
|
||||
INSTALL_REQUIRES.append("pluggy>=0.7")
|
||||
|
||||
|
||||
def main():
|
||||
extras_require = {}
|
||||
install_requires = [
|
||||
"py>=1.5.0",
|
||||
"six>=1.10.0",
|
||||
"setuptools",
|
||||
"attrs>=17.4.0",
|
||||
"more-itertools>=4.0.0",
|
||||
"atomicwrites>=1.0",
|
||||
]
|
||||
# if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy;
|
||||
# used by tox.ini to test with pluggy master
|
||||
if "_PYTEST_SETUP_SKIP_PLUGGY_DEP" not in os.environ:
|
||||
install_requires.append("pluggy>=0.7")
|
||||
environment_marker_support_level = get_environment_marker_support_level()
|
||||
if environment_marker_support_level >= 2:
|
||||
install_requires.append('funcsigs;python_version<"3.0"')
|
||||
install_requires.append('pathlib2>=2.2.0;python_version<"3.6"')
|
||||
install_requires.append('colorama;sys_platform=="win32"')
|
||||
elif environment_marker_support_level == 1:
|
||||
extras_require[':python_version<"3.0"'] = ["funcsigs"]
|
||||
extras_require[':python_version<"3.6"'] = ["pathlib2>=2.2.0"]
|
||||
extras_require[':sys_platform=="win32"'] = ["colorama"]
|
||||
else:
|
||||
if sys.platform == "win32":
|
||||
install_requires.append("colorama")
|
||||
if sys.version_info < (3, 0):
|
||||
install_requires.append("funcsigs")
|
||||
if sys.version_info < (3, 6):
|
||||
install_requires.append("pathlib2>=2.2.0")
|
||||
|
||||
setup(
|
||||
name="pytest",
|
||||
description="pytest: simple powerful testing with Python",
|
||||
long_description=long_description,
|
||||
use_scm_version={"write_to": "src/_pytest/_version.py"},
|
||||
url="https://docs.pytest.org/en/latest/",
|
||||
project_urls={
|
||||
"Source": "https://github.com/pytest-dev/pytest",
|
||||
"Tracker": "https://github.com/pytest-dev/pytest/issues",
|
||||
},
|
||||
license="MIT license",
|
||||
platforms=["unix", "linux", "osx", "cygwin", "win32"],
|
||||
author=(
|
||||
"Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, "
|
||||
"Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others"
|
||||
),
|
||||
entry_points={"console_scripts": ["pytest=pytest:main", "py.test=pytest:main"]},
|
||||
classifiers=classifiers,
|
||||
keywords="test unittest",
|
||||
# the following should be enabled for release
|
||||
setup_requires=["setuptools-scm"],
|
||||
setup_requires=["setuptools-scm", "setuptools>=40.0"],
|
||||
package_dir={"": "src"},
|
||||
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*",
|
||||
install_requires=install_requires,
|
||||
extras_require=extras_require,
|
||||
packages=[
|
||||
"_pytest",
|
||||
"_pytest.assertion",
|
||||
"_pytest._code",
|
||||
"_pytest.mark",
|
||||
"_pytest.config",
|
||||
],
|
||||
py_modules=["pytest"],
|
||||
zip_safe=False,
|
||||
install_requires=INSTALL_REQUIRES,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
"""allow bash-completion for argparse with argcomplete if installed
|
||||
needs argcomplete>=0.5.6 for python 3.2/3.3 (older versions fail
|
||||
to find the magic string, so _ARGCOMPLETE env. var is never set, and
|
||||
|
||||
@@ -4,6 +4,7 @@ from .code import Code # noqa
|
||||
from .code import ExceptionInfo # noqa
|
||||
from .code import Frame # noqa
|
||||
from .code import Traceback # noqa
|
||||
from .code import filter_traceback # noqa
|
||||
from .code import getrawcode # noqa
|
||||
from .source import Source # noqa
|
||||
from .source import compile_ as compile # noqa
|
||||
|
||||
@@ -6,8 +6,10 @@ import traceback
|
||||
from inspect import CO_VARARGS, CO_VARKEYWORDS
|
||||
|
||||
import attr
|
||||
import pluggy
|
||||
import re
|
||||
from weakref import ref
|
||||
import _pytest
|
||||
from _pytest.compat import _PY2, _PY3, PY35, safe_str
|
||||
from six import text_type
|
||||
import py
|
||||
@@ -451,13 +453,35 @@ class ExceptionInfo(object):
|
||||
tbfilter=True,
|
||||
funcargs=False,
|
||||
truncate_locals=True,
|
||||
chain=True,
|
||||
):
|
||||
""" return str()able representation of this exception info.
|
||||
showlocals: show locals per traceback entry
|
||||
style: long|short|no|native traceback style
|
||||
tbfilter: hide entries (where __tracebackhide__ is true)
|
||||
"""
|
||||
Return str()able representation of this exception info.
|
||||
|
||||
in case of style==native, tbfilter and showlocals is ignored.
|
||||
:param bool showlocals:
|
||||
Show locals per traceback entry.
|
||||
Ignored if ``style=="native"``.
|
||||
|
||||
:param str style: long|short|no|native traceback style
|
||||
|
||||
:param bool abspath:
|
||||
If paths should be changed to absolute or left unchanged.
|
||||
|
||||
:param bool tbfilter:
|
||||
Hide entries that contain a local variable ``__tracebackhide__==True``.
|
||||
Ignored if ``style=="native"``.
|
||||
|
||||
:param bool funcargs:
|
||||
Show fixtures ("funcargs" for legacy purposes) per traceback entry.
|
||||
|
||||
:param bool truncate_locals:
|
||||
With ``showlocals==True``, make sure locals can be safely represented as strings.
|
||||
|
||||
:param bool chain: if chained exceptions in Python 3 should be shown.
|
||||
|
||||
.. versionchanged:: 3.9
|
||||
|
||||
Added the ``chain`` parameter.
|
||||
"""
|
||||
if style == "native":
|
||||
return ReprExceptionInfo(
|
||||
@@ -476,6 +500,7 @@ class ExceptionInfo(object):
|
||||
tbfilter=tbfilter,
|
||||
funcargs=funcargs,
|
||||
truncate_locals=truncate_locals,
|
||||
chain=chain,
|
||||
)
|
||||
return fmt.repr_excinfo(self)
|
||||
|
||||
@@ -516,6 +541,7 @@ class FormattedExcinfo(object):
|
||||
tbfilter = attr.ib(default=True)
|
||||
funcargs = attr.ib(default=False)
|
||||
truncate_locals = attr.ib(default=True)
|
||||
chain = attr.ib(default=True)
|
||||
astcache = attr.ib(default=attr.Factory(dict), init=False, repr=False)
|
||||
|
||||
def _getindent(self, source):
|
||||
@@ -735,7 +761,7 @@ class FormattedExcinfo(object):
|
||||
reprcrash = None
|
||||
|
||||
repr_chain += [(reprtraceback, reprcrash, descr)]
|
||||
if e.__cause__ is not None:
|
||||
if e.__cause__ is not None and self.chain:
|
||||
e = e.__cause__
|
||||
excinfo = (
|
||||
ExceptionInfo((type(e), e, e.__traceback__))
|
||||
@@ -743,7 +769,11 @@ class FormattedExcinfo(object):
|
||||
else None
|
||||
)
|
||||
descr = "The above exception was the direct cause of the following exception:"
|
||||
elif e.__context__ is not None and not e.__suppress_context__:
|
||||
elif (
|
||||
e.__context__ is not None
|
||||
and not e.__suppress_context__
|
||||
and self.chain
|
||||
):
|
||||
e = e.__context__
|
||||
excinfo = (
|
||||
ExceptionInfo((type(e), e, e.__traceback__))
|
||||
@@ -979,3 +1009,36 @@ else:
|
||||
return "maximum recursion depth exceeded" in str(excinfo.value)
|
||||
except UnicodeError:
|
||||
return False
|
||||
|
||||
|
||||
# relative paths that we use to filter traceback entries from appearing to the user;
|
||||
# see filter_traceback
|
||||
# note: if we need to add more paths than what we have now we should probably use a list
|
||||
# for better maintenance
|
||||
|
||||
_PLUGGY_DIR = py.path.local(pluggy.__file__.rstrip("oc"))
|
||||
# pluggy is either a package or a single module depending on the version
|
||||
if _PLUGGY_DIR.basename == "__init__.py":
|
||||
_PLUGGY_DIR = _PLUGGY_DIR.dirpath()
|
||||
_PYTEST_DIR = py.path.local(_pytest.__file__).dirpath()
|
||||
_PY_DIR = py.path.local(py.__file__).dirpath()
|
||||
|
||||
|
||||
def filter_traceback(entry):
|
||||
"""Return True if a TracebackEntry instance should be removed from tracebacks:
|
||||
* dynamically generated code (no code to show up for it);
|
||||
* internal traceback from pytest or its internal libraries, py and pluggy.
|
||||
"""
|
||||
# entry.path might sometimes return a str object when the entry
|
||||
# points to dynamically generated code
|
||||
# see https://bitbucket.org/pytest-dev/py/issues/71
|
||||
raw_filename = entry.frame.code.raw.co_filename
|
||||
is_generated = "<" in raw_filename and ">" in raw_filename
|
||||
if is_generated:
|
||||
return False
|
||||
# entry.path might point to a non-existing file, in which case it will
|
||||
# also return a str object. see #1133
|
||||
p = py.path.local(entry.path)
|
||||
return (
|
||||
not p.relto(_PLUGGY_DIR) and not p.relto(_PYTEST_DIR) and not p.relto(_PY_DIR)
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ import linecache
|
||||
import sys
|
||||
import six
|
||||
import inspect
|
||||
import textwrap
|
||||
import tokenize
|
||||
import py
|
||||
|
||||
@@ -23,7 +24,6 @@ class Source(object):
|
||||
def __init__(self, *parts, **kwargs):
|
||||
self.lines = lines = []
|
||||
de = kwargs.get("deindent", True)
|
||||
rstrip = kwargs.get("rstrip", True)
|
||||
for part in parts:
|
||||
if not part:
|
||||
partlines = []
|
||||
@@ -33,11 +33,6 @@ class Source(object):
|
||||
partlines = [x.rstrip("\n") for x in part]
|
||||
elif isinstance(part, six.string_types):
|
||||
partlines = part.split("\n")
|
||||
if rstrip:
|
||||
while partlines:
|
||||
if partlines[-1].strip():
|
||||
break
|
||||
partlines.pop()
|
||||
else:
|
||||
partlines = getsource(part, deindent=de).lines
|
||||
if de:
|
||||
@@ -115,17 +110,10 @@ class Source(object):
|
||||
ast, start, end = getstatementrange_ast(lineno, self)
|
||||
return start, end
|
||||
|
||||
def deindent(self, offset=None):
|
||||
""" return a new source object deindented by offset.
|
||||
If offset is None then guess an indentation offset from
|
||||
the first non-blank line. Subsequent lines which have a
|
||||
lower indentation offset will be copied verbatim as
|
||||
they are assumed to be part of multilines.
|
||||
"""
|
||||
# XXX maybe use the tokenizer to properly handle multiline
|
||||
# strings etc.pp?
|
||||
def deindent(self):
|
||||
"""return a new source object deindented."""
|
||||
newsource = Source()
|
||||
newsource.lines[:] = deindent(self.lines, offset)
|
||||
newsource.lines[:] = deindent(self.lines)
|
||||
return newsource
|
||||
|
||||
def isparseable(self, deindent=True):
|
||||
@@ -268,47 +256,8 @@ def getsource(obj, **kwargs):
|
||||
return Source(strsrc, **kwargs)
|
||||
|
||||
|
||||
def deindent(lines, offset=None):
|
||||
if offset is None:
|
||||
for line in lines:
|
||||
line = line.expandtabs()
|
||||
s = line.lstrip()
|
||||
if s:
|
||||
offset = len(line) - len(s)
|
||||
break
|
||||
else:
|
||||
offset = 0
|
||||
if offset == 0:
|
||||
return list(lines)
|
||||
newlines = []
|
||||
|
||||
def readline_generator(lines):
|
||||
for line in lines:
|
||||
yield line + "\n"
|
||||
|
||||
it = readline_generator(lines)
|
||||
|
||||
try:
|
||||
for _, _, (sline, _), (eline, _), _ in tokenize.generate_tokens(
|
||||
lambda: next(it)
|
||||
):
|
||||
if sline > len(lines):
|
||||
break # End of input reached
|
||||
if sline > len(newlines):
|
||||
line = lines[sline - 1].expandtabs()
|
||||
if line.lstrip() and line[:offset].isspace():
|
||||
line = line[offset:] # Deindent
|
||||
newlines.append(line)
|
||||
|
||||
for i in range(sline, eline):
|
||||
# Don't deindent continuing lines of
|
||||
# multiline tokens (i.e. multiline strings)
|
||||
newlines.append(lines[i])
|
||||
except (IndentationError, tokenize.TokenError):
|
||||
pass
|
||||
# Add any lines we didn't see. E.g. if an exception was raised.
|
||||
newlines.extend(lines[len(newlines) :])
|
||||
return newlines
|
||||
def deindent(lines):
|
||||
return textwrap.dedent("\n".join(lines)).splitlines()
|
||||
|
||||
|
||||
def get_statement_startend2(lineno, node):
|
||||
|
||||
@@ -8,6 +8,7 @@ import marshal
|
||||
import os
|
||||
import re
|
||||
import six
|
||||
import string
|
||||
import struct
|
||||
import sys
|
||||
import types
|
||||
@@ -16,7 +17,9 @@ import atomicwrites
|
||||
import py
|
||||
|
||||
from _pytest.assertion import util
|
||||
|
||||
from _pytest.pathlib import PurePath
|
||||
from _pytest.compat import spec_from_file_location
|
||||
from _pytest.pathlib import fnmatch_ex
|
||||
|
||||
# pytest caches rewritten pycs in __pycache__.
|
||||
if hasattr(imp, "get_tag"):
|
||||
@@ -45,14 +48,6 @@ else:
|
||||
return ast.Call(a, b, c, None, None)
|
||||
|
||||
|
||||
if sys.version_info >= (3, 4):
|
||||
from importlib.util import spec_from_file_location
|
||||
else:
|
||||
|
||||
def spec_from_file_location(*_, **__):
|
||||
return None
|
||||
|
||||
|
||||
class AssertionRewritingHook(object):
|
||||
"""PEP302 Import hook which rewrites asserts."""
|
||||
|
||||
@@ -67,14 +62,24 @@ class AssertionRewritingHook(object):
|
||||
# flag to guard against trying to rewrite a pyc file while we are already writing another pyc file,
|
||||
# which might result in infinite recursion (#3506)
|
||||
self._writing_pyc = False
|
||||
self._basenames_to_check_rewrite = {"conftest"}
|
||||
self._marked_for_rewrite_cache = {}
|
||||
self._session_paths_checked = False
|
||||
|
||||
def set_session(self, session):
|
||||
self.session = session
|
||||
self._session_paths_checked = False
|
||||
|
||||
def _imp_find_module(self, name, path=None):
|
||||
"""Indirection so we can mock calls to find_module originated from the hook during testing"""
|
||||
return imp.find_module(name, path)
|
||||
|
||||
def find_module(self, name, path=None):
|
||||
if self._writing_pyc:
|
||||
return None
|
||||
state = self.config._assertstate
|
||||
if self._early_rewrite_bailout(name, state):
|
||||
return None
|
||||
state.trace("find_module called for: %s" % name)
|
||||
names = name.rsplit(".", 1)
|
||||
lastname = names[-1]
|
||||
@@ -87,7 +92,7 @@ class AssertionRewritingHook(object):
|
||||
pth = path[0]
|
||||
if pth is None:
|
||||
try:
|
||||
fd, fn, desc = imp.find_module(lastname, path)
|
||||
fd, fn, desc = self._imp_find_module(lastname, path)
|
||||
except ImportError:
|
||||
return None
|
||||
if fd is not None:
|
||||
@@ -166,6 +171,44 @@ class AssertionRewritingHook(object):
|
||||
self.modules[name] = co, pyc
|
||||
return self
|
||||
|
||||
def _early_rewrite_bailout(self, name, state):
|
||||
"""
|
||||
This is a fast way to get out of rewriting modules. Profiling has
|
||||
shown that the call to imp.find_module (inside of the find_module
|
||||
from this class) is a major slowdown, so, this method tries to
|
||||
filter what we're sure won't be rewritten before getting to it.
|
||||
"""
|
||||
if self.session is not None and not self._session_paths_checked:
|
||||
self._session_paths_checked = True
|
||||
for path in self.session._initialpaths:
|
||||
# Make something as c:/projects/my_project/path.py ->
|
||||
# ['c:', 'projects', 'my_project', 'path.py']
|
||||
parts = str(path).split(os.path.sep)
|
||||
# add 'path' to basenames to be checked.
|
||||
self._basenames_to_check_rewrite.add(os.path.splitext(parts[-1])[0])
|
||||
|
||||
# Note: conftest already by default in _basenames_to_check_rewrite.
|
||||
parts = name.split(".")
|
||||
if parts[-1] in self._basenames_to_check_rewrite:
|
||||
return False
|
||||
|
||||
# For matching the name it must be as if it was a filename.
|
||||
path = PurePath(os.path.sep.join(parts) + ".py")
|
||||
|
||||
for pat in self.fnpats:
|
||||
# if the pattern contains subdirectories ("tests/**.py" for example) we can't bail out based
|
||||
# on the name alone because we need to match against the full path
|
||||
if os.path.dirname(pat):
|
||||
return False
|
||||
if fnmatch_ex(pat, path):
|
||||
return False
|
||||
|
||||
if self._is_marked_for_rewrite(name, state):
|
||||
return False
|
||||
|
||||
state.trace("early skip of rewriting module: %s" % (name,))
|
||||
return True
|
||||
|
||||
def _should_rewrite(self, name, fn_pypath, state):
|
||||
# always rewrite conftest files
|
||||
fn = str(fn_pypath)
|
||||
@@ -185,12 +228,20 @@ class AssertionRewritingHook(object):
|
||||
state.trace("matched test file %r" % (fn,))
|
||||
return True
|
||||
|
||||
for marked in self._must_rewrite:
|
||||
if name == marked or name.startswith(marked + "."):
|
||||
state.trace("matched marked file %r (from %r)" % (name, marked))
|
||||
return True
|
||||
return self._is_marked_for_rewrite(name, state)
|
||||
|
||||
return False
|
||||
def _is_marked_for_rewrite(self, name, state):
|
||||
try:
|
||||
return self._marked_for_rewrite_cache[name]
|
||||
except KeyError:
|
||||
for marked in self._must_rewrite:
|
||||
if name == marked or name.startswith(marked + "."):
|
||||
state.trace("matched marked file %r (from %r)" % (name, marked))
|
||||
self._marked_for_rewrite_cache[name] = True
|
||||
return True
|
||||
|
||||
self._marked_for_rewrite_cache[name] = False
|
||||
return False
|
||||
|
||||
def mark_rewrite(self, *names):
|
||||
"""Mark import names as needing to be rewritten.
|
||||
@@ -207,24 +258,29 @@ class AssertionRewritingHook(object):
|
||||
):
|
||||
self._warn_already_imported(name)
|
||||
self._must_rewrite.update(names)
|
||||
self._marked_for_rewrite_cache.clear()
|
||||
|
||||
def _warn_already_imported(self, name):
|
||||
self.config.warn(
|
||||
"P1", "Module already imported so cannot be rewritten: %s" % name
|
||||
from _pytest.warning_types import PytestWarning
|
||||
from _pytest.warnings import _issue_config_warning
|
||||
|
||||
_issue_config_warning(
|
||||
PytestWarning("Module already imported so cannot be rewritten: %s" % name),
|
||||
self.config,
|
||||
)
|
||||
|
||||
def load_module(self, name):
|
||||
# If there is an existing module object named 'fullname' in
|
||||
# sys.modules, the loader must use that existing module. (Otherwise,
|
||||
# the reload() builtin will not work correctly.)
|
||||
if name in sys.modules:
|
||||
return sys.modules[name]
|
||||
|
||||
co, pyc = self.modules.pop(name)
|
||||
# I wish I could just call imp.load_compiled here, but __file__ has to
|
||||
# be set properly. In Python 3.2+, this all would be handled correctly
|
||||
# by load_compiled.
|
||||
mod = sys.modules[name] = imp.new_module(name)
|
||||
if name in sys.modules:
|
||||
# If there is an existing module object named 'fullname' in
|
||||
# sys.modules, the loader must use that existing module. (Otherwise,
|
||||
# the reload() builtin will not work correctly.)
|
||||
mod = sys.modules[name]
|
||||
else:
|
||||
# I wish I could just call imp.load_compiled here, but __file__ has to
|
||||
# be set properly. In Python 3.2+, this all would be handled correctly
|
||||
# by load_compiled.
|
||||
mod = sys.modules[name] = imp.new_module(name)
|
||||
try:
|
||||
mod.__file__ = co.co_filename
|
||||
# Normally, this attribute is 3.2+.
|
||||
@@ -241,7 +297,7 @@ class AssertionRewritingHook(object):
|
||||
|
||||
def is_package(self, name):
|
||||
try:
|
||||
fd, fn, desc = imp.find_module(name)
|
||||
fd, fn, desc = self._imp_find_module(name)
|
||||
except ImportError:
|
||||
return False
|
||||
if fd is not None:
|
||||
@@ -343,7 +399,7 @@ def _rewrite_test(config, fn):
|
||||
finally:
|
||||
del state._indecode
|
||||
try:
|
||||
tree = ast.parse(source)
|
||||
tree = ast.parse(source, filename=fn.strpath)
|
||||
except SyntaxError:
|
||||
# Let this pop up again in the real import.
|
||||
state.trace("failed to parse: %r" % (fn,))
|
||||
@@ -412,10 +468,14 @@ def _saferepr(obj):
|
||||
|
||||
"""
|
||||
r = py.io.saferepr(obj)
|
||||
if isinstance(r, six.text_type):
|
||||
return r.replace(u"\n", u"\\n")
|
||||
else:
|
||||
return r.replace(b"\n", b"\\n")
|
||||
# only occurs in python2.x, repr must return text in python3+
|
||||
if isinstance(r, bytes):
|
||||
# Represent unprintable bytes as `\x##`
|
||||
r = u"".join(
|
||||
u"\\x{:x}".format(ord(c)) if c not in string.printable else c.decode()
|
||||
for c in r
|
||||
)
|
||||
return r.replace(u"\n", u"\\n")
|
||||
|
||||
|
||||
from _pytest.assertion.util import format_explanation as _format_explanation # noqa
|
||||
@@ -746,13 +806,17 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
the expression is false.
|
||||
|
||||
"""
|
||||
if isinstance(assert_.test, ast.Tuple) and self.config is not None:
|
||||
fslocation = (self.module_path, assert_.lineno)
|
||||
self.config.warn(
|
||||
"R1",
|
||||
"assertion is always true, perhaps " "remove parentheses?",
|
||||
fslocation=fslocation,
|
||||
if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1:
|
||||
from _pytest.warning_types import PytestWarning
|
||||
import warnings
|
||||
|
||||
warnings.warn_explicit(
|
||||
PytestWarning("assertion is always true, perhaps remove parentheses?"),
|
||||
category=None,
|
||||
filename=str(self.module_path),
|
||||
lineno=assert_.lineno,
|
||||
)
|
||||
|
||||
self.statements = []
|
||||
self.variables = []
|
||||
self.variable_counter = itertools.count()
|
||||
|
||||
@@ -199,7 +199,7 @@ def _diff_text(left, right, verbose=False):
|
||||
if i > 42:
|
||||
i -= 10 # Provide some context
|
||||
explanation = [
|
||||
u("Skipping %s identical leading " "characters in diff, use -v to show")
|
||||
u("Skipping %s identical leading characters in diff, use -v to show")
|
||||
% i
|
||||
]
|
||||
left = left[i:]
|
||||
|
||||
@@ -13,10 +13,9 @@ import attr
|
||||
|
||||
import pytest
|
||||
import json
|
||||
import shutil
|
||||
|
||||
from . import paths
|
||||
from .compat import _PY2 as PY2, Path
|
||||
from .compat import _PY2 as PY2
|
||||
from .pathlib import Path, resolve_from_str, rmtree
|
||||
|
||||
README_CONTENT = u"""\
|
||||
# pytest cache directory #
|
||||
@@ -33,22 +32,27 @@ See [the docs](https://docs.pytest.org/en/latest/cache.html) for more informatio
|
||||
@attr.s
|
||||
class Cache(object):
|
||||
_cachedir = attr.ib(repr=False)
|
||||
_warn = attr.ib(repr=False)
|
||||
_config = attr.ib(repr=False)
|
||||
|
||||
@classmethod
|
||||
def for_config(cls, config):
|
||||
cachedir = cls.cache_dir_from_config(config)
|
||||
if config.getoption("cacheclear") and cachedir.exists():
|
||||
shutil.rmtree(str(cachedir))
|
||||
rmtree(cachedir, force=True)
|
||||
cachedir.mkdir()
|
||||
return cls(cachedir, config.warn)
|
||||
return cls(cachedir, config)
|
||||
|
||||
@staticmethod
|
||||
def cache_dir_from_config(config):
|
||||
return paths.resolve_from_str(config.getini("cache_dir"), config.rootdir)
|
||||
return resolve_from_str(config.getini("cache_dir"), config.rootdir)
|
||||
|
||||
def warn(self, fmt, **args):
|
||||
self._warn(code="I9", message=fmt.format(**args) if args else fmt)
|
||||
from _pytest.warnings import _issue_config_warning
|
||||
from _pytest.warning_types import PytestWarning
|
||||
|
||||
_issue_config_warning(
|
||||
PytestWarning(fmt.format(**args) if args else fmt), self._config
|
||||
)
|
||||
|
||||
def makedir(self, name):
|
||||
""" return a directory path object with the given name. If the
|
||||
@@ -110,15 +114,18 @@ class Cache(object):
|
||||
else:
|
||||
with f:
|
||||
json.dump(value, f, indent=2, sort_keys=True)
|
||||
self._ensure_readme()
|
||||
|
||||
def _ensure_readme(self):
|
||||
self._ensure_supporting_files()
|
||||
|
||||
def _ensure_supporting_files(self):
|
||||
"""Create supporting files in the cache dir that are not really part of the cache."""
|
||||
if self._cachedir.is_dir():
|
||||
readme_path = self._cachedir / "README.md"
|
||||
if not readme_path.is_file():
|
||||
readme_path.write_text(README_CONTENT)
|
||||
|
||||
msg = u"# created by pytest automatically, do not change\n*"
|
||||
self._cachedir.joinpath(".gitignore").write_text(msg, encoding="UTF-8")
|
||||
|
||||
|
||||
class LFPlugin(object):
|
||||
""" Plugin which implements the --lf (run last-failing) option """
|
||||
@@ -134,15 +141,12 @@ class LFPlugin(object):
|
||||
def pytest_report_collectionfinish(self):
|
||||
if self.active and self.config.getoption("verbose") >= 0:
|
||||
if not self._previously_failed_count:
|
||||
mode = "run {} (no recorded failures)".format(
|
||||
self._no_failures_behavior
|
||||
)
|
||||
else:
|
||||
noun = "failure" if self._previously_failed_count == 1 else "failures"
|
||||
suffix = " first" if self.config.getoption("failedfirst") else ""
|
||||
mode = "rerun previous {count} {noun}{suffix}".format(
|
||||
count=self._previously_failed_count, suffix=suffix, noun=noun
|
||||
)
|
||||
return None
|
||||
noun = "failure" if self._previously_failed_count == 1 else "failures"
|
||||
suffix = " first" if self.config.getoption("failedfirst") else ""
|
||||
mode = "rerun previous {count} {noun}{suffix}".format(
|
||||
count=self._previously_failed_count, suffix=suffix, noun=noun
|
||||
)
|
||||
return "run-last-failure: %s" % mode
|
||||
|
||||
def pytest_runtest_logreport(self, report):
|
||||
@@ -339,7 +343,7 @@ def cacheshow(config, session):
|
||||
key = valpath.relative_to(vdir)
|
||||
val = config.cache.get(key, dummy)
|
||||
if val is dummy:
|
||||
tw.line("%s contains unreadable content, " "will be ignored" % key)
|
||||
tw.line("%s contains unreadable content, will be ignored" % key)
|
||||
else:
|
||||
tw.line("%s contains:" % key)
|
||||
for line in pformat(val).splitlines():
|
||||
|
||||
@@ -654,7 +654,7 @@ class DontReadFromInput(six.Iterator):
|
||||
return self
|
||||
|
||||
def fileno(self):
|
||||
raise UnsupportedOperation("redirected stdin is pseudofile, " "has no fileno()")
|
||||
raise UnsupportedOperation("redirected stdin is pseudofile, has no fileno()")
|
||||
|
||||
def isatty(self):
|
||||
return False
|
||||
|
||||
@@ -13,7 +13,7 @@ from contextlib import contextmanager
|
||||
import py
|
||||
|
||||
import _pytest
|
||||
from _pytest.outcomes import TEST_OUTCOME
|
||||
from _pytest.outcomes import TEST_OUTCOME, fail
|
||||
from six import text_type
|
||||
import six
|
||||
|
||||
@@ -23,8 +23,6 @@ except ImportError: # pragma: no cover
|
||||
# Only available in Python 3.4+ or as a backport
|
||||
enum = None
|
||||
|
||||
__all__ = ["Path"]
|
||||
|
||||
_PY3 = sys.version_info > (3, 0)
|
||||
_PY2 = not _PY3
|
||||
|
||||
@@ -41,11 +39,6 @@ PY35 = sys.version_info[:2] >= (3, 5)
|
||||
PY36 = sys.version_info[:2] >= (3, 6)
|
||||
MODULE_NOT_FOUND_ERROR = "ModuleNotFoundError" if PY36 else "ImportError"
|
||||
|
||||
if PY36:
|
||||
from pathlib import Path
|
||||
else:
|
||||
from pathlib2 import Path
|
||||
|
||||
|
||||
if _PY3:
|
||||
from collections.abc import MutableMapping as MappingMixin
|
||||
@@ -56,6 +49,14 @@ else:
|
||||
from collections import Mapping, Sequence # noqa
|
||||
|
||||
|
||||
if sys.version_info >= (3, 4):
|
||||
from importlib.util import spec_from_file_location
|
||||
else:
|
||||
|
||||
def spec_from_file_location(*_, **__):
|
||||
return None
|
||||
|
||||
|
||||
def _format_args(func):
|
||||
return str(signature(func))
|
||||
|
||||
@@ -130,9 +131,17 @@ def getfuncargnames(function, is_method=False, cls=None):
|
||||
# ordered mapping of parameter names to Parameter instances. This
|
||||
# creates a tuple of the names of the parameters that don't have
|
||||
# defaults.
|
||||
try:
|
||||
parameters = signature(function).parameters
|
||||
except (ValueError, TypeError) as e:
|
||||
fail(
|
||||
"Could not determine arguments of {!r}: {}".format(function, e),
|
||||
pytrace=False,
|
||||
)
|
||||
|
||||
arg_names = tuple(
|
||||
p.name
|
||||
for p in signature(function).parameters.values()
|
||||
for p in parameters.values()
|
||||
if (
|
||||
p.kind is Parameter.POSITIONAL_OR_KEYWORD
|
||||
or p.kind is Parameter.KEYWORD_ONLY
|
||||
@@ -267,7 +276,7 @@ def get_real_func(obj):
|
||||
obj = new_obj
|
||||
else:
|
||||
raise ValueError(
|
||||
("could not find real function of {start}" "\nstopped at {current}").format(
|
||||
("could not find real function of {start}\nstopped at {current}").format(
|
||||
start=py.io.saferepr(start_obj), current=py.io.saferepr(obj)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -3,7 +3,6 @@ from __future__ import absolute_import, division, print_function
|
||||
import argparse
|
||||
import inspect
|
||||
import shlex
|
||||
import traceback
|
||||
import types
|
||||
import warnings
|
||||
import copy
|
||||
@@ -19,6 +18,7 @@ import _pytest._code
|
||||
import _pytest.hookspec # the extension point definitions
|
||||
import _pytest.assertion
|
||||
from pluggy import PluginManager, HookimplMarker, HookspecMarker
|
||||
from _pytest._code import ExceptionInfo, filter_traceback
|
||||
from _pytest.compat import safe_str
|
||||
from .exceptions import UsageError, PrintHelp
|
||||
from .findpaths import determine_setup, exists
|
||||
@@ -26,9 +26,6 @@ from .findpaths import determine_setup, exists
|
||||
hookimpl = HookimplMarker("pytest")
|
||||
hookspec = HookspecMarker("pytest")
|
||||
|
||||
# pytest startup
|
||||
#
|
||||
|
||||
|
||||
class ConftestImportFailure(Exception):
|
||||
def __init__(self, path, excinfo):
|
||||
@@ -36,12 +33,6 @@ class ConftestImportFailure(Exception):
|
||||
self.path = path
|
||||
self.excinfo = excinfo
|
||||
|
||||
def __str__(self):
|
||||
etype, evalue, etb = self.excinfo
|
||||
formatted = traceback.format_tb(etb)
|
||||
# The level of the tracebacks we want to print is hand crafted :(
|
||||
return repr(evalue) + "\n" + "".join(formatted[2:])
|
||||
|
||||
|
||||
def main(args=None, plugins=None):
|
||||
""" return exit code, after performing an in-process test run.
|
||||
@@ -51,14 +42,26 @@ def main(args=None, plugins=None):
|
||||
:arg plugins: list of plugin objects to be auto-registered during
|
||||
initialization.
|
||||
"""
|
||||
from _pytest.main import EXIT_USAGEERROR
|
||||
|
||||
try:
|
||||
try:
|
||||
config = _prepareconfig(args, plugins)
|
||||
except ConftestImportFailure as e:
|
||||
exc_info = ExceptionInfo(e.excinfo)
|
||||
tw = py.io.TerminalWriter(sys.stderr)
|
||||
for line in traceback.format_exception(*e.excinfo):
|
||||
tw.line(
|
||||
"ImportError while loading conftest '{e.path}'.".format(e=e), red=True
|
||||
)
|
||||
exc_info.traceback = exc_info.traceback.filter(filter_traceback)
|
||||
exc_repr = (
|
||||
exc_info.getrepr(style="short", chain=False)
|
||||
if exc_info.traceback
|
||||
else exc_info.exconly()
|
||||
)
|
||||
formatted_tb = safe_str(exc_repr)
|
||||
for line in formatted_tb.splitlines():
|
||||
tw.line(line.rstrip(), red=True)
|
||||
tw.line("ERROR: could not load %s\n" % (e.path,), red=True)
|
||||
return 4
|
||||
else:
|
||||
try:
|
||||
@@ -69,7 +72,7 @@ def main(args=None, plugins=None):
|
||||
tw = py.io.TerminalWriter(sys.stderr)
|
||||
for msg in e.args:
|
||||
tw.line("ERROR: {}\n".format(msg), red=True)
|
||||
return 4
|
||||
return EXIT_USAGEERROR
|
||||
|
||||
|
||||
class cmdline(object): # compatibility namespace
|
||||
@@ -176,7 +179,9 @@ def _prepareconfig(args=None, plugins=None):
|
||||
else:
|
||||
pluginmanager.register(plugin)
|
||||
if warning:
|
||||
config.warn("C1", warning)
|
||||
from _pytest.warnings import _issue_config_warning
|
||||
|
||||
_issue_config_warning(warning, config=config)
|
||||
return pluginmanager.hook.pytest_cmdline_parse(
|
||||
pluginmanager=pluginmanager, args=args
|
||||
)
|
||||
@@ -347,6 +352,7 @@ class PytestPluginManager(PluginManager):
|
||||
else None
|
||||
)
|
||||
self._noconftest = namespace.noconftest
|
||||
self._using_pyargs = namespace.pyargs
|
||||
testpaths = namespace.file_or_dir
|
||||
foundanchor = False
|
||||
for path in testpaths:
|
||||
@@ -373,25 +379,27 @@ class PytestPluginManager(PluginManager):
|
||||
def _getconftestmodules(self, path):
|
||||
if self._noconftest:
|
||||
return []
|
||||
try:
|
||||
return self._path2confmods[path]
|
||||
except KeyError:
|
||||
if path.isfile():
|
||||
clist = self._getconftestmodules(path.dirpath())
|
||||
else:
|
||||
# XXX these days we may rather want to use config.rootdir
|
||||
# and allow users to opt into looking into the rootdir parent
|
||||
# directories instead of requiring to specify confcutdir
|
||||
clist = []
|
||||
for parent in path.parts():
|
||||
if self._confcutdir and self._confcutdir.relto(parent):
|
||||
continue
|
||||
conftestpath = parent.join("conftest.py")
|
||||
if conftestpath.isfile():
|
||||
mod = self._importconftest(conftestpath)
|
||||
clist.append(mod)
|
||||
|
||||
self._path2confmods[path] = clist
|
||||
if path.isfile():
|
||||
directory = path.dirpath()
|
||||
else:
|
||||
directory = path
|
||||
try:
|
||||
return self._path2confmods[directory]
|
||||
except KeyError:
|
||||
# XXX these days we may rather want to use config.rootdir
|
||||
# and allow users to opt into looking into the rootdir parent
|
||||
# directories instead of requiring to specify confcutdir
|
||||
clist = []
|
||||
for parent in directory.parts():
|
||||
if self._confcutdir and self._confcutdir.relto(parent):
|
||||
continue
|
||||
conftestpath = parent.join("conftest.py")
|
||||
if conftestpath.isfile():
|
||||
mod = self._importconftest(conftestpath)
|
||||
clist.append(mod)
|
||||
|
||||
self._path2confmods[directory] = clist
|
||||
return clist
|
||||
|
||||
def _rget_with_confmod(self, name, path):
|
||||
@@ -412,12 +420,21 @@ class PytestPluginManager(PluginManager):
|
||||
_ensure_removed_sysmodule(conftestpath.purebasename)
|
||||
try:
|
||||
mod = conftestpath.pyimport()
|
||||
if hasattr(mod, "pytest_plugins") and self._configured:
|
||||
if (
|
||||
hasattr(mod, "pytest_plugins")
|
||||
and self._configured
|
||||
and not self._using_pyargs
|
||||
):
|
||||
from _pytest.deprecated import (
|
||||
PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST
|
||||
)
|
||||
|
||||
warnings.warn(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST)
|
||||
warnings.warn_explicit(
|
||||
PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST,
|
||||
category=None,
|
||||
filename=str(conftestpath),
|
||||
lineno=0,
|
||||
)
|
||||
except Exception:
|
||||
raise ConftestImportFailure(conftestpath, sys.exc_info())
|
||||
|
||||
@@ -602,7 +619,29 @@ class Config(object):
|
||||
fin()
|
||||
|
||||
def warn(self, code, message, fslocation=None, nodeid=None):
|
||||
""" generate a warning for this test session. """
|
||||
"""
|
||||
.. deprecated:: 3.8
|
||||
|
||||
Use :py:func:`warnings.warn` or :py:func:`warnings.warn_explicit` directly instead.
|
||||
|
||||
Generate a warning for this test session.
|
||||
"""
|
||||
from _pytest.warning_types import RemovedInPytest4Warning
|
||||
|
||||
if isinstance(fslocation, (tuple, list)) and len(fslocation) > 2:
|
||||
filename, lineno = fslocation[:2]
|
||||
else:
|
||||
filename = "unknown file"
|
||||
lineno = 0
|
||||
msg = "config.warn has been deprecated, use warnings.warn instead"
|
||||
if nodeid:
|
||||
msg = "{}: {}".format(nodeid, msg)
|
||||
warnings.warn_explicit(
|
||||
RemovedInPytest4Warning(msg),
|
||||
category=None,
|
||||
filename=filename,
|
||||
lineno=lineno,
|
||||
)
|
||||
self.hook.pytest_logwarning.call_historic(
|
||||
kwargs=dict(
|
||||
code=code, message=message, fslocation=fslocation, nodeid=nodeid
|
||||
@@ -667,8 +706,8 @@ class Config(object):
|
||||
r = determine_setup(
|
||||
ns.inifilename,
|
||||
ns.file_or_dir + unknown_args,
|
||||
warnfunc=self.warn,
|
||||
rootdir_cmd_arg=ns.rootdir or None,
|
||||
config=self,
|
||||
)
|
||||
self.rootdir, self.inifile, self.inicfg = r
|
||||
self._parser.extra_info["rootdir"] = self.rootdir
|
||||
@@ -706,6 +745,10 @@ class Config(object):
|
||||
|
||||
self.pluginmanager.rewrite_hook = hook
|
||||
|
||||
if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
|
||||
# We don't autoload from setuptools entry points, no need to continue.
|
||||
return
|
||||
|
||||
# 'RECORD' available for plugins installed normally (pip install)
|
||||
# 'SOURCES.txt' available for plugins installed in dev mode (pip install -e)
|
||||
# for installed plugins 'SOURCES.txt' returns an empty list, and vice-versa
|
||||
@@ -731,7 +774,10 @@ class Config(object):
|
||||
self._checkversion()
|
||||
self._consider_importhook(args)
|
||||
self.pluginmanager.consider_preparse(args)
|
||||
self.pluginmanager.load_setuptools_entrypoints("pytest11")
|
||||
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.
|
||||
self.pluginmanager.load_setuptools_entrypoints("pytest11")
|
||||
self.pluginmanager.consider_env()
|
||||
self.known_args_namespace = ns = self._parser.parse_known_args(
|
||||
args, namespace=copy.copy(self.option)
|
||||
|
||||
@@ -2,8 +2,13 @@ import six
|
||||
import warnings
|
||||
import argparse
|
||||
|
||||
from gettext import gettext as _
|
||||
import sys as _sys
|
||||
|
||||
import py
|
||||
|
||||
from ..main import EXIT_USAGEERROR
|
||||
|
||||
FILE_OR_DIR = "file_or_dir"
|
||||
|
||||
|
||||
@@ -329,6 +334,16 @@ class MyOptionParser(argparse.ArgumentParser):
|
||||
# an usage error to provide more contextual information to the user
|
||||
self.extra_info = extra_info
|
||||
|
||||
def error(self, message):
|
||||
"""error(message: string)
|
||||
|
||||
Prints a usage message incorporating the message to stderr and
|
||||
exits.
|
||||
Overrides the method in parent class to change exit code"""
|
||||
self.print_usage(_sys.stderr)
|
||||
args = {"prog": self.prog, "message": message}
|
||||
self.exit(EXIT_USAGEERROR, _("%(prog)s: error: %(message)s\n") % args)
|
||||
|
||||
def parse_args(self, args=None, namespace=None):
|
||||
"""allow splitting of positional arguments"""
|
||||
args, argv = self.parse_known_args(args, namespace)
|
||||
|
||||
@@ -10,15 +10,12 @@ def exists(path, ignore=EnvironmentError):
|
||||
return False
|
||||
|
||||
|
||||
def getcfg(args, warnfunc=None):
|
||||
def getcfg(args, config=None):
|
||||
"""
|
||||
Search the list of arguments for a valid ini-file for pytest,
|
||||
and return a tuple of (rootdir, inifile, cfg-dict).
|
||||
|
||||
note: warnfunc is an optional function used to warn
|
||||
about ini-files that use deprecated features.
|
||||
This parameter should be removed when pytest
|
||||
adopts standard deprecation warnings (#1804).
|
||||
note: config is optional and used only to issue warnings explicitly (#2891).
|
||||
"""
|
||||
from _pytest.deprecated import CFG_PYTEST_SECTION
|
||||
|
||||
@@ -34,9 +31,15 @@ def getcfg(args, warnfunc=None):
|
||||
if exists(p):
|
||||
iniconfig = py.iniconfig.IniConfig(p)
|
||||
if "pytest" in iniconfig.sections:
|
||||
if inibasename == "setup.cfg" and warnfunc:
|
||||
warnfunc(
|
||||
"C1", CFG_PYTEST_SECTION.format(filename=inibasename)
|
||||
if inibasename == "setup.cfg" and config is not None:
|
||||
from _pytest.warnings import _issue_config_warning
|
||||
from _pytest.warning_types import RemovedInPytest4Warning
|
||||
|
||||
_issue_config_warning(
|
||||
RemovedInPytest4Warning(
|
||||
CFG_PYTEST_SECTION.format(filename=inibasename)
|
||||
),
|
||||
config=config,
|
||||
)
|
||||
return base, p, iniconfig["pytest"]
|
||||
if (
|
||||
@@ -95,33 +98,37 @@ def get_dirs_from_args(args):
|
||||
return [get_dir_from_path(path) for path in possible_paths if path.exists()]
|
||||
|
||||
|
||||
def determine_setup(inifile, args, warnfunc=None, rootdir_cmd_arg=None):
|
||||
def determine_setup(inifile, args, rootdir_cmd_arg=None, config=None):
|
||||
dirs = get_dirs_from_args(args)
|
||||
if inifile:
|
||||
iniconfig = py.iniconfig.IniConfig(inifile)
|
||||
is_cfg_file = str(inifile).endswith(".cfg")
|
||||
# TODO: [pytest] section in *.cfg files is depricated. Need refactoring.
|
||||
sections = ["tool:pytest", "pytest"] if is_cfg_file else ["pytest"]
|
||||
for section in sections:
|
||||
try:
|
||||
inicfg = iniconfig[section]
|
||||
if is_cfg_file and section == "pytest" and warnfunc:
|
||||
if is_cfg_file and section == "pytest" and config is not None:
|
||||
from _pytest.deprecated import CFG_PYTEST_SECTION
|
||||
from _pytest.warnings import _issue_config_warning
|
||||
|
||||
warnfunc("C1", CFG_PYTEST_SECTION.format(filename=str(inifile)))
|
||||
# TODO: [pytest] section in *.cfg files is deprecated. Need refactoring once
|
||||
# the deprecation expires.
|
||||
_issue_config_warning(
|
||||
CFG_PYTEST_SECTION.format(filename=str(inifile)), config
|
||||
)
|
||||
break
|
||||
except KeyError:
|
||||
inicfg = None
|
||||
rootdir = get_common_ancestor(dirs)
|
||||
else:
|
||||
ancestor = get_common_ancestor(dirs)
|
||||
rootdir, inifile, inicfg = getcfg([ancestor], warnfunc=warnfunc)
|
||||
rootdir, inifile, inicfg = getcfg([ancestor], config=config)
|
||||
if rootdir is None:
|
||||
for rootdir in ancestor.parts(reverse=True):
|
||||
if rootdir.join("setup.py").exists():
|
||||
break
|
||||
else:
|
||||
rootdir, inifile, inicfg = getcfg(dirs, warnfunc=warnfunc)
|
||||
rootdir, inifile, inicfg = getcfg(dirs, config=config)
|
||||
if rootdir is None:
|
||||
rootdir = get_common_ancestor([py.path.local(), ancestor])
|
||||
is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/"
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
""" interactive debugging with PDB, the Python Debugger. """
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import os
|
||||
import pdb
|
||||
import sys
|
||||
import os
|
||||
from doctest import UnexpectedException
|
||||
|
||||
from _pytest import outcomes
|
||||
from _pytest.config import hookimpl
|
||||
|
||||
try:
|
||||
@@ -109,9 +111,6 @@ class PdbInvoke(object):
|
||||
_enter_pdb(node, call.excinfo, report)
|
||||
|
||||
def pytest_internalerror(self, excrepr, excinfo):
|
||||
for line in str(excrepr).split("\n"):
|
||||
sys.stderr.write("INTERNALERROR> %s\n" % line)
|
||||
sys.stderr.flush()
|
||||
tb = _postmortem_traceback(excinfo)
|
||||
post_mortem(tb)
|
||||
|
||||
@@ -164,8 +163,9 @@ def _enter_pdb(node, excinfo, rep):
|
||||
rep.toterminal(tw)
|
||||
tw.sep(">", "entering PDB")
|
||||
tb = _postmortem_traceback(excinfo)
|
||||
post_mortem(tb)
|
||||
rep._pdbshown = True
|
||||
if post_mortem(tb):
|
||||
outcomes.exit("Quitting debugger")
|
||||
return rep
|
||||
|
||||
|
||||
@@ -196,3 +196,4 @@ def post_mortem(t):
|
||||
p = Pdb()
|
||||
p.reset()
|
||||
p.interaction(None, t)
|
||||
return p.quitting
|
||||
|
||||
@@ -4,37 +4,67 @@ that is planned to be removed in the next pytest release.
|
||||
|
||||
Keeping it in a central location makes it easy to track what is deprecated and should
|
||||
be removed when the time comes.
|
||||
|
||||
All constants defined in this module should be either PytestWarning instances or UnformattedWarning
|
||||
in case of warnings which need to format their messages.
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
|
||||
class RemovedInPytest4Warning(DeprecationWarning):
|
||||
"""warning class for features removed in pytest 4.0"""
|
||||
from _pytest.warning_types import UnformattedWarning, RemovedInPytest4Warning
|
||||
|
||||
|
||||
MAIN_STR_ARGS = "passing a string to pytest.main() is deprecated, " "pass a list of arguments instead."
|
||||
MAIN_STR_ARGS = RemovedInPytest4Warning(
|
||||
"passing a string to pytest.main() is deprecated, "
|
||||
"pass a list of arguments instead."
|
||||
)
|
||||
|
||||
YIELD_TESTS = "yield tests are deprecated, and scheduled to be removed in pytest 4.0"
|
||||
YIELD_TESTS = RemovedInPytest4Warning(
|
||||
"yield tests are deprecated, and scheduled to be removed in pytest 4.0"
|
||||
)
|
||||
|
||||
FUNCARG_PREFIX = (
|
||||
CACHED_SETUP = RemovedInPytest4Warning(
|
||||
"cached_setup is deprecated and will be removed in a future release. "
|
||||
"Use standard fixture functions instead."
|
||||
)
|
||||
|
||||
COMPAT_PROPERTY = UnformattedWarning(
|
||||
RemovedInPytest4Warning,
|
||||
"usage of {owner}.{name} is deprecated, please use pytest.{name} instead",
|
||||
)
|
||||
|
||||
CUSTOM_CLASS = UnformattedWarning(
|
||||
RemovedInPytest4Warning,
|
||||
'use of special named "{name}" objects in collectors of type "{type_name}" to '
|
||||
"customize the created nodes is deprecated. "
|
||||
"Use pytest_pycollect_makeitem(...) to create custom "
|
||||
"collection nodes instead.",
|
||||
)
|
||||
|
||||
FUNCARG_PREFIX = UnformattedWarning(
|
||||
RemovedInPytest4Warning,
|
||||
'{name}: declaring fixtures using "pytest_funcarg__" prefix is deprecated '
|
||||
"and scheduled to be removed in pytest 4.0. "
|
||||
"Please remove the prefix and use the @pytest.fixture decorator instead."
|
||||
"Please remove the prefix and use the @pytest.fixture decorator instead.",
|
||||
)
|
||||
|
||||
FIXTURE_FUNCTION_CALL = (
|
||||
"Fixture {name} called directly. Fixtures are not meant to be called directly, "
|
||||
FIXTURE_FUNCTION_CALL = UnformattedWarning(
|
||||
RemovedInPytest4Warning,
|
||||
'Fixture "{name}" called directly. Fixtures are not meant to be called directly, '
|
||||
"are created automatically when test functions request them as parameters. "
|
||||
"See https://docs.pytest.org/en/latest/fixture.html for more information."
|
||||
"See https://docs.pytest.org/en/latest/fixture.html for more information.",
|
||||
)
|
||||
|
||||
CFG_PYTEST_SECTION = (
|
||||
"[pytest] section in {filename} files is deprecated, use [tool:pytest] instead."
|
||||
CFG_PYTEST_SECTION = UnformattedWarning(
|
||||
RemovedInPytest4Warning,
|
||||
"[pytest] section in {filename} files is deprecated, use [tool:pytest] instead.",
|
||||
)
|
||||
|
||||
GETFUNCARGVALUE = "use of getfuncargvalue is deprecated, use getfixturevalue"
|
||||
GETFUNCARGVALUE = RemovedInPytest4Warning(
|
||||
"getfuncargvalue is deprecated, use getfixturevalue"
|
||||
)
|
||||
|
||||
RESULT_LOG = (
|
||||
RESULT_LOG = RemovedInPytest4Warning(
|
||||
"--result-log is deprecated and scheduled for removal in pytest 4.0.\n"
|
||||
"See https://docs.pytest.org/en/latest/usage.html#creating-resultlog-format-files for more information."
|
||||
)
|
||||
@@ -51,17 +81,21 @@ MARK_PARAMETERSET_UNPACKING = RemovedInPytest4Warning(
|
||||
"For more details, see: https://docs.pytest.org/en/latest/parametrize.html"
|
||||
)
|
||||
|
||||
RECORD_XML_PROPERTY = (
|
||||
NODE_WARN = RemovedInPytest4Warning(
|
||||
"Node.warn(code, message) form has been deprecated, use Node.warn(warning_instance) instead."
|
||||
)
|
||||
|
||||
RECORD_XML_PROPERTY = RemovedInPytest4Warning(
|
||||
'Fixture renamed from "record_xml_property" to "record_property" as user '
|
||||
"properties are now available to all reporters.\n"
|
||||
'"record_xml_property" is now deprecated.'
|
||||
)
|
||||
|
||||
COLLECTOR_MAKEITEM = RemovedInPytest4Warning(
|
||||
"pycollector makeitem was removed " "as it is an accidentially leaked internal api"
|
||||
"pycollector makeitem was removed as it is an accidentially leaked internal api"
|
||||
)
|
||||
|
||||
METAFUNC_ADD_CALL = (
|
||||
METAFUNC_ADD_CALL = RemovedInPytest4Warning(
|
||||
"Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0.\n"
|
||||
"Please use Metafunc.parametrize instead."
|
||||
)
|
||||
@@ -75,3 +109,8 @@ PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST = RemovedInPytest4Warning(
|
||||
PYTEST_NAMESPACE = RemovedInPytest4Warning(
|
||||
"pytest_namespace is deprecated and will be removed soon"
|
||||
)
|
||||
|
||||
PYTEST_ENSURETEMP = RemovedInPytest4Warning(
|
||||
"pytest/tmpdir_factory.ensuretemp is deprecated, \n"
|
||||
"please use the tmp_path fixture or tmp_path_factory.mktemp"
|
||||
)
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
class PytestExerimentalApiWarning(FutureWarning):
|
||||
"warning category used to denote experiments in pytest"
|
||||
|
||||
@classmethod
|
||||
def simple(cls, apiname):
|
||||
return cls(
|
||||
"{apiname} is an experimental api that may change over time".format(
|
||||
apiname=apiname
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
PYTESTER_COPY_EXAMPLE = PytestExerimentalApiWarning.simple("testdir.copy_example")
|
||||
@@ -32,7 +32,7 @@ from _pytest.compat import (
|
||||
get_real_method,
|
||||
_PytestWrapper,
|
||||
)
|
||||
from _pytest.deprecated import FIXTURE_FUNCTION_CALL, RemovedInPytest4Warning
|
||||
from _pytest.deprecated import FIXTURE_FUNCTION_CALL
|
||||
from _pytest.outcomes import fail, TEST_OUTCOME
|
||||
|
||||
FIXTURE_MSG = 'fixtures cannot have "pytest_funcarg__" prefix and be decorated with @pytest.fixture:\n{}'
|
||||
@@ -359,8 +359,10 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
|
||||
@property
|
||||
def fixturenames(self):
|
||||
# backward incompatible note: now a readonly property
|
||||
return list(self._pyfuncitem._fixtureinfo.names_closure)
|
||||
"""names of all active fixtures in this request"""
|
||||
result = list(self._pyfuncitem._fixtureinfo.names_closure)
|
||||
result.extend(set(self._fixture_defs).difference(result))
|
||||
return result
|
||||
|
||||
@property
|
||||
def node(self):
|
||||
@@ -479,6 +481,9 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
or ``session`` indicating the caching lifecycle of the resource.
|
||||
:arg extrakey: added to internal caching key of (funcargname, scope).
|
||||
"""
|
||||
from _pytest.deprecated import CACHED_SETUP
|
||||
|
||||
warnings.warn(CACHED_SETUP, stacklevel=2)
|
||||
if not hasattr(self.config, "_setupcache"):
|
||||
self.config._setupcache = {} # XXX weakref?
|
||||
cachekey = (self.fixturename, self._getscopeitem(scope), extrakey)
|
||||
@@ -512,7 +517,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
""" Deprecated, use getfixturevalue. """
|
||||
from _pytest import deprecated
|
||||
|
||||
warnings.warn(deprecated.GETFUNCARGVALUE, DeprecationWarning, stacklevel=2)
|
||||
warnings.warn(deprecated.GETFUNCARGVALUE, stacklevel=2)
|
||||
return self.getfixturevalue(argname)
|
||||
|
||||
def _get_active_fixturedef(self, argname):
|
||||
@@ -562,7 +567,20 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
except (AttributeError, ValueError):
|
||||
param = NOTSET
|
||||
param_index = 0
|
||||
if fixturedef.params is not None:
|
||||
has_params = fixturedef.params is not None
|
||||
fixtures_not_supported = getattr(funcitem, "nofuncargs", False)
|
||||
if has_params and fixtures_not_supported:
|
||||
msg = (
|
||||
"{name} does not support fixtures, maybe unittest.TestCase subclass?\n"
|
||||
"Node id: {nodeid}\n"
|
||||
"Function type: {typename}"
|
||||
).format(
|
||||
name=funcitem.name,
|
||||
nodeid=funcitem.nodeid,
|
||||
typename=type(funcitem).__name__,
|
||||
)
|
||||
fail(msg, pytrace=False)
|
||||
if has_params:
|
||||
frame = inspect.stack()[3]
|
||||
frameinfo = inspect.getframeinfo(frame[0])
|
||||
source_path = frameinfo.filename
|
||||
@@ -571,16 +589,18 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
if source_path.relto(funcitem.config.rootdir):
|
||||
source_path = source_path.relto(funcitem.config.rootdir)
|
||||
msg = (
|
||||
"The requested fixture has no parameter defined for the "
|
||||
"current test.\n\nRequested fixture '{}' defined in:\n{}"
|
||||
"The requested fixture has no parameter defined for test:\n"
|
||||
" {}\n\n"
|
||||
"Requested fixture '{}' defined in:\n{}"
|
||||
"\n\nRequested here:\n{}:{}".format(
|
||||
funcitem.nodeid,
|
||||
fixturedef.argname,
|
||||
getlocation(fixturedef.func, funcitem.config.rootdir),
|
||||
source_path,
|
||||
source_lineno,
|
||||
)
|
||||
)
|
||||
fail(msg)
|
||||
fail(msg, pytrace=False)
|
||||
else:
|
||||
# indices might not be set if old-style metafunc.addcall() was used
|
||||
param_index = funcitem.callspec.indices.get(argname, 0)
|
||||
@@ -698,10 +718,11 @@ def scope2index(scope, descr, where=None):
|
||||
try:
|
||||
return scopes.index(scope)
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
"{} {}has an unsupported scope value '{}'".format(
|
||||
fail(
|
||||
"{} {}got an unexpected scope value '{}'".format(
|
||||
descr, "from {} ".format(where) if where else "", scope
|
||||
)
|
||||
),
|
||||
pytrace=False,
|
||||
)
|
||||
|
||||
|
||||
@@ -741,14 +762,19 @@ class FixtureLookupError(LookupError):
|
||||
|
||||
if msg is None:
|
||||
fm = self.request._fixturemanager
|
||||
available = []
|
||||
available = set()
|
||||
parentid = self.request._pyfuncitem.parent.nodeid
|
||||
for name, fixturedefs in fm._arg2fixturedefs.items():
|
||||
faclist = list(fm._matchfactories(fixturedefs, parentid))
|
||||
if faclist and name not in available:
|
||||
available.append(name)
|
||||
msg = "fixture %r not found" % (self.argname,)
|
||||
msg += "\n available fixtures: %s" % (", ".join(sorted(available)),)
|
||||
if faclist:
|
||||
available.add(name)
|
||||
if self.argname in available:
|
||||
msg = " recursive dependency involving fixture '{}' detected".format(
|
||||
self.argname
|
||||
)
|
||||
else:
|
||||
msg = "fixture '{}' not found".format(self.argname)
|
||||
msg += "\n available fixtures: {}".format(", ".join(sorted(available)))
|
||||
msg += "\n use 'pytest --fixtures [testpath]' for help on them."
|
||||
|
||||
return FixtureLookupErrorRepr(fspath, lineno, tblines, msg, self.argname)
|
||||
@@ -834,7 +860,9 @@ class FixtureDef(object):
|
||||
self.argname = argname
|
||||
self.scope = scope
|
||||
self.scopenum = scope2index(
|
||||
scope or "function", descr="fixture {}".format(func.__name__), where=baseid
|
||||
scope or "function",
|
||||
descr="Fixture '{}'".format(func.__name__),
|
||||
where=baseid,
|
||||
)
|
||||
self.params = params
|
||||
self.argnames = getfuncargnames(func, is_method=unittest)
|
||||
@@ -896,7 +924,7 @@ class FixtureDef(object):
|
||||
return hook.pytest_fixture_setup(fixturedef=self, request=request)
|
||||
|
||||
def __repr__(self):
|
||||
return "<FixtureDef name=%r scope=%r baseid=%r >" % (
|
||||
return "<FixtureDef name=%r scope=%r baseid=%r>" % (
|
||||
self.argname,
|
||||
self.scope,
|
||||
self.baseid,
|
||||
@@ -956,8 +984,9 @@ def wrap_function_to_warning_if_called_directly(function, fixture_marker):
|
||||
used as an argument in a test function.
|
||||
"""
|
||||
is_yield_function = is_generator(function)
|
||||
msg = FIXTURE_FUNCTION_CALL.format(name=fixture_marker.name or function.__name__)
|
||||
warning = RemovedInPytest4Warning(msg)
|
||||
warning = FIXTURE_FUNCTION_CALL.format(
|
||||
name=fixture_marker.name or function.__name__
|
||||
)
|
||||
|
||||
if is_yield_function:
|
||||
|
||||
@@ -996,7 +1025,7 @@ class FixtureFunctionMarker(object):
|
||||
|
||||
def __call__(self, function):
|
||||
if isclass(function):
|
||||
raise ValueError("class fixtures not supported (may be in the future)")
|
||||
raise ValueError("class fixtures not supported (maybe in the future)")
|
||||
|
||||
if getattr(function, "_pytestfixturefunction", False):
|
||||
raise ValueError(
|
||||
@@ -1151,7 +1180,7 @@ class FixtureManager(object):
|
||||
def pytest_plugin_registered(self, plugin):
|
||||
nodeid = None
|
||||
try:
|
||||
p = py.path.local(plugin.__file__)
|
||||
p = py.path.local(plugin.__file__).realpath()
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
@@ -1257,6 +1286,8 @@ class FixtureManager(object):
|
||||
items[:] = reorder_items(items)
|
||||
|
||||
def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False):
|
||||
from _pytest import deprecated
|
||||
|
||||
if nodeid is not NOTSET:
|
||||
holderobj = node_or_obj
|
||||
else:
|
||||
@@ -1279,10 +1310,13 @@ class FixtureManager(object):
|
||||
if not callable(obj):
|
||||
continue
|
||||
marker = defaultfuncargprefixmarker
|
||||
from _pytest import deprecated
|
||||
|
||||
self.config.warn(
|
||||
"C1", deprecated.FUNCARG_PREFIX.format(name=name), nodeid=nodeid
|
||||
filename, lineno = getfslineno(obj)
|
||||
warnings.warn_explicit(
|
||||
deprecated.FUNCARG_PREFIX.format(name=name),
|
||||
category=None,
|
||||
filename=str(filename),
|
||||
lineno=lineno + 1,
|
||||
)
|
||||
name = name[len(self._argprefix) :]
|
||||
elif not isinstance(marker, FixtureFunctionMarker):
|
||||
@@ -1342,8 +1376,7 @@ class FixtureManager(object):
|
||||
fixturedefs = self._arg2fixturedefs[argname]
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return tuple(self._matchfactories(fixturedefs, nodeid))
|
||||
return tuple(self._matchfactories(fixturedefs, nodeid))
|
||||
|
||||
def _matchfactories(self, fixturedefs, nodeid):
|
||||
for fixturedef in fixturedefs:
|
||||
|
||||
@@ -139,7 +139,7 @@ def showhelp(config):
|
||||
tw.line()
|
||||
tw.line()
|
||||
tw.line(
|
||||
"[pytest] ini-options in the first " "pytest.ini|tox.ini|setup.cfg file found:"
|
||||
"[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg file found:"
|
||||
)
|
||||
tw.line()
|
||||
|
||||
@@ -156,6 +156,7 @@ def showhelp(config):
|
||||
vars = [
|
||||
("PYTEST_ADDOPTS", "extra command line options"),
|
||||
("PYTEST_PLUGINS", "comma-separated plugins to load during startup"),
|
||||
("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "set to disable plugin auto-loading"),
|
||||
("PYTEST_DEBUG", "set to enable debug tracing of pytest's internals"),
|
||||
]
|
||||
for name, help in vars:
|
||||
|
||||
@@ -526,7 +526,17 @@ def pytest_terminal_summary(terminalreporter, exitstatus):
|
||||
|
||||
@hookspec(historic=True)
|
||||
def pytest_logwarning(message, code, nodeid, fslocation):
|
||||
""" process a warning specified by a message, a code string,
|
||||
"""
|
||||
.. deprecated:: 3.8
|
||||
|
||||
This hook is will stop working in a future release.
|
||||
|
||||
pytest no longer triggers this hook, but the
|
||||
terminal writer still implements it to display warnings issued by
|
||||
:meth:`_pytest.config.Config.warn` and :meth:`_pytest.nodes.Node.warn`. Calling those functions will be
|
||||
an error in future releases.
|
||||
|
||||
process a warning specified by a message, a code string,
|
||||
a nodeid and fslocation (both of which may be None
|
||||
if the warning is not tied to a particular node/location).
|
||||
|
||||
@@ -535,6 +545,30 @@ def pytest_logwarning(message, code, nodeid, fslocation):
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(historic=True)
|
||||
def pytest_warning_captured(warning_message, when, item):
|
||||
"""
|
||||
Process a warning captured by the internal pytest warnings plugin.
|
||||
|
||||
:param warnings.WarningMessage warning_message:
|
||||
The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains
|
||||
the same attributes as the parameters of :py:func:`warnings.showwarning`.
|
||||
|
||||
:param str when:
|
||||
Indicates when the warning was captured. Possible values:
|
||||
|
||||
* ``"config"``: during pytest configuration/initialization stage.
|
||||
* ``"collect"``: during test collection.
|
||||
* ``"runtest"``: during test execution.
|
||||
|
||||
:param pytest.Item|None item:
|
||||
**DEPRECATED**: This parameter is incompatible with ``pytest-xdist``, and will always receive ``None``
|
||||
in a future release.
|
||||
|
||||
The item being executed if ``when`` is ``"runtest"``, otherwise ``None``.
|
||||
"""
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# doctest hooks
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@@ -38,7 +38,7 @@ class Junit(py.xml.Namespace):
|
||||
# this dynamically instead of hardcoding it. The spec range of valid
|
||||
# chars is: Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD]
|
||||
# | [#x10000-#x10FFFF]
|
||||
_legal_chars = (0x09, 0x0A, 0x0d)
|
||||
_legal_chars = (0x09, 0x0A, 0x0D)
|
||||
_legal_ranges = ((0x20, 0x7E), (0x80, 0xD7FF), (0xE000, 0xFFFD), (0x10000, 0x10FFFF))
|
||||
_legal_xml_re = [
|
||||
unicode("%s-%s") % (unichr(low), unichr(high))
|
||||
@@ -221,12 +221,14 @@ class _NodeReporter(object):
|
||||
else:
|
||||
filename, lineno, skipreason = report.longrepr
|
||||
if skipreason.startswith("Skipped: "):
|
||||
skipreason = bin_xml_escape(skipreason[9:])
|
||||
skipreason = skipreason[9:]
|
||||
details = "%s:%s: %s" % (filename, lineno, skipreason)
|
||||
|
||||
self.append(
|
||||
Junit.skipped(
|
||||
"%s:%s: %s" % (filename, lineno, skipreason),
|
||||
bin_xml_escape(details),
|
||||
type="pytest.skip",
|
||||
message=skipreason,
|
||||
message=bin_xml_escape(skipreason),
|
||||
)
|
||||
)
|
||||
self.write_captured_output(report)
|
||||
@@ -258,12 +260,11 @@ def record_property(request):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def record_xml_property(record_property):
|
||||
def record_xml_property(record_property, request):
|
||||
"""(Deprecated) use record_property."""
|
||||
import warnings
|
||||
from _pytest import deprecated
|
||||
|
||||
warnings.warn(deprecated.RECORD_XML_PROPERTY, DeprecationWarning, stacklevel=2)
|
||||
request.node.warn(deprecated.RECORD_XML_PROPERTY)
|
||||
|
||||
return record_property
|
||||
|
||||
@@ -274,9 +275,9 @@ def record_xml_attribute(request):
|
||||
The fixture is callable with ``(name, value)``, with value being
|
||||
automatically xml-encoded
|
||||
"""
|
||||
request.node.warn(
|
||||
code="C3", message="record_xml_attribute is an experimental feature"
|
||||
)
|
||||
from _pytest.warning_types import PytestWarning
|
||||
|
||||
request.node.warn(PytestWarning("record_xml_attribute is an experimental feature"))
|
||||
xml = getattr(request.config, "_xml", None)
|
||||
if xml is not None:
|
||||
node_reporter = xml.node_reporter(request.node.nodeid)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import logging
|
||||
from contextlib import closing, contextmanager
|
||||
from contextlib import contextmanager
|
||||
import re
|
||||
import six
|
||||
|
||||
@@ -213,7 +213,8 @@ class LogCaptureFixture(object):
|
||||
def __init__(self, item):
|
||||
"""Creates a new funcarg."""
|
||||
self._item = item
|
||||
self._initial_log_levels = {} # type: Dict[str, int] # dict of log name -> log level
|
||||
# dict of log name -> log level
|
||||
self._initial_log_levels = {} # type: Dict[str, int]
|
||||
|
||||
def _finalize(self):
|
||||
"""Finalizes the fixture.
|
||||
@@ -278,7 +279,7 @@ class LogCaptureFixture(object):
|
||||
Unlike 'records', which contains the format string and parameters for interpolation, log messages in this list
|
||||
are all interpolated.
|
||||
Unlike 'text', which contains the output from the handler, log messages in this list are unadorned with
|
||||
levels, timestamps, etc, making exact comparisions more reliable.
|
||||
levels, timestamps, etc, making exact comparisons more reliable.
|
||||
|
||||
Note that traceback or stack info (from :func:`logging.exception` or the `exc_info` or `stack_info` arguments
|
||||
to the logging functions) is not included, as this is added by the formatter in the handler.
|
||||
@@ -414,7 +415,6 @@ class LoggingPlugin(object):
|
||||
else:
|
||||
self.log_file_handler = None
|
||||
|
||||
# initialized during pytest_runtestloop
|
||||
self.log_cli_handler = None
|
||||
|
||||
def _log_cli_enabled(self):
|
||||
@@ -425,6 +425,22 @@ class LoggingPlugin(object):
|
||||
"--log-cli-level"
|
||||
) is not None or self._config.getini("log_cli")
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||
def pytest_collection(self):
|
||||
# This has to be called before the first log message is logged,
|
||||
# so we can access the terminal reporter plugin.
|
||||
self._setup_cli_logging()
|
||||
|
||||
with self.live_logs_context():
|
||||
if self.log_cli_handler:
|
||||
self.log_cli_handler.set_when("collection")
|
||||
|
||||
if self.log_file_handler is not None:
|
||||
with catching_logs(self.log_file_handler, level=self.log_file_level):
|
||||
yield
|
||||
else:
|
||||
yield
|
||||
|
||||
@contextmanager
|
||||
def _runtest_for(self, item, when):
|
||||
"""Implements the internals of pytest_runtest_xxx() hook."""
|
||||
@@ -445,8 +461,8 @@ class LoggingPlugin(object):
|
||||
try:
|
||||
yield # run test
|
||||
finally:
|
||||
del item.catch_log_handler
|
||||
if when == "teardown":
|
||||
del item.catch_log_handler
|
||||
del item.catch_log_handlers
|
||||
|
||||
if self.print_logs:
|
||||
@@ -481,25 +497,41 @@ class LoggingPlugin(object):
|
||||
with self._runtest_for(None, "finish"):
|
||||
yield
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||
def pytest_sessionfinish(self):
|
||||
with self.live_logs_context():
|
||||
if self.log_cli_handler:
|
||||
self.log_cli_handler.set_when("sessionfinish")
|
||||
if self.log_file_handler is not None:
|
||||
with catching_logs(self.log_file_handler, level=self.log_file_level):
|
||||
yield
|
||||
else:
|
||||
yield
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||
def pytest_sessionstart(self):
|
||||
self._setup_cli_logging()
|
||||
with self.live_logs_context():
|
||||
if self.log_cli_handler:
|
||||
self.log_cli_handler.set_when("sessionstart")
|
||||
if self.log_file_handler is not None:
|
||||
with catching_logs(self.log_file_handler, level=self.log_file_level):
|
||||
yield
|
||||
else:
|
||||
yield
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtestloop(self, session):
|
||||
"""Runs all collected test items."""
|
||||
self._setup_cli_logging()
|
||||
with self.live_logs_context:
|
||||
with self.live_logs_context():
|
||||
if self.log_file_handler is not None:
|
||||
with closing(self.log_file_handler):
|
||||
with catching_logs(
|
||||
self.log_file_handler, level=self.log_file_level
|
||||
):
|
||||
yield # run all the tests
|
||||
with catching_logs(self.log_file_handler, level=self.log_file_level):
|
||||
yield # run all the tests
|
||||
else:
|
||||
yield # run all the tests
|
||||
|
||||
def _setup_cli_logging(self):
|
||||
"""Sets up the handler and logger for the Live Logs feature, if enabled.
|
||||
|
||||
This must be done right before starting the loop so we can access the terminal reporter plugin.
|
||||
"""
|
||||
"""Sets up the handler and logger for the Live Logs feature, if enabled."""
|
||||
terminal_reporter = self._config.pluginmanager.get_plugin("terminalreporter")
|
||||
if self._log_cli_enabled() and terminal_reporter is not None:
|
||||
capture_manager = self._config.pluginmanager.get_plugin("capturemanager")
|
||||
@@ -529,11 +561,14 @@ class LoggingPlugin(object):
|
||||
self._config, "log_cli_level", "log_level"
|
||||
)
|
||||
self.log_cli_handler = log_cli_handler
|
||||
self.live_logs_context = catching_logs(
|
||||
self.live_logs_context = lambda: catching_logs(
|
||||
log_cli_handler, formatter=log_cli_formatter, level=log_cli_level
|
||||
)
|
||||
else:
|
||||
self.live_logs_context = dummy_context_manager()
|
||||
self.live_logs_context = lambda: dummy_context_manager()
|
||||
# Note that the lambda for the live_logs_context is needed because
|
||||
# live_logs_context can otherwise not be entered multiple times due
|
||||
# to limitations of contextlib.contextmanager
|
||||
|
||||
|
||||
class _LiveLoggingStreamHandler(logging.StreamHandler):
|
||||
|
||||
@@ -156,7 +156,10 @@ def pytest_addoption(parser):
|
||||
dest="basetemp",
|
||||
default=None,
|
||||
metavar="dir",
|
||||
help="base temporary directory for this test run.",
|
||||
help=(
|
||||
"base temporary directory for this test run."
|
||||
"(warning: this directory is removed if it exists)"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -182,10 +185,13 @@ def wrap_session(config, doit):
|
||||
session.exitstatus = EXIT_TESTSFAILED
|
||||
except KeyboardInterrupt:
|
||||
excinfo = _pytest._code.ExceptionInfo()
|
||||
if initstate < 2 and isinstance(excinfo.value, exit.Exception):
|
||||
exitstatus = EXIT_INTERRUPTED
|
||||
if initstate <= 2 and isinstance(excinfo.value, exit.Exception):
|
||||
sys.stderr.write("{}: {}\n".format(excinfo.typename, excinfo.value.msg))
|
||||
if excinfo.value.returncode is not None:
|
||||
exitstatus = excinfo.value.returncode
|
||||
config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
|
||||
session.exitstatus = EXIT_INTERRUPTED
|
||||
session.exitstatus = exitstatus
|
||||
except: # noqa
|
||||
excinfo = _pytest._code.ExceptionInfo()
|
||||
config.notify_exception(excinfo, config.option)
|
||||
@@ -383,6 +389,7 @@ class Session(nodes.FSCollector):
|
||||
self.trace = config.trace.root.get("collection")
|
||||
self._norecursepatterns = config.getini("norecursedirs")
|
||||
self.startdir = py.path.local()
|
||||
self._initialpaths = frozenset()
|
||||
# Keep track of any collected nodes in here, so we don't duplicate fixtures
|
||||
self._node_cache = {}
|
||||
|
||||
@@ -441,13 +448,14 @@ class Session(nodes.FSCollector):
|
||||
self.trace("perform_collect", self, args)
|
||||
self.trace.root.indent += 1
|
||||
self._notfound = []
|
||||
self._initialpaths = set()
|
||||
initialpaths = []
|
||||
self._initialparts = []
|
||||
self.items = items = []
|
||||
for arg in args:
|
||||
parts = self._parsearg(arg)
|
||||
self._initialparts.append(parts)
|
||||
self._initialpaths.add(parts[0])
|
||||
initialpaths.append(parts[0])
|
||||
self._initialpaths = frozenset(initialpaths)
|
||||
rep = collect_one_node(self)
|
||||
self.ihook.pytest_collectreport(report=rep)
|
||||
self.trace.root.indent -= 1
|
||||
@@ -485,7 +493,7 @@ class Session(nodes.FSCollector):
|
||||
from _pytest.python import Package
|
||||
|
||||
names = self._parsearg(arg)
|
||||
argpath = names.pop(0)
|
||||
argpath = names.pop(0).realpath()
|
||||
paths = []
|
||||
|
||||
root = self
|
||||
@@ -502,13 +510,14 @@ class Session(nodes.FSCollector):
|
||||
pkginit = parent.join("__init__.py")
|
||||
if pkginit.isfile():
|
||||
if pkginit in self._node_cache:
|
||||
root = self._node_cache[pkginit]
|
||||
root = self._node_cache[pkginit][0]
|
||||
else:
|
||||
col = root._collectfile(pkginit)
|
||||
if col:
|
||||
if isinstance(col[0], Package):
|
||||
root = col[0]
|
||||
self._node_cache[root.fspath] = root
|
||||
# always store a list in the cache, matchnodes expects it
|
||||
self._node_cache[root.fspath] = [root]
|
||||
|
||||
# If it's a directory argument, recurse and look for any Subpackages.
|
||||
# Let the Package collector deal with subnodes, don't collect here.
|
||||
@@ -528,8 +537,8 @@ class Session(nodes.FSCollector):
|
||||
if (type(x), x.fspath) in self._node_cache:
|
||||
yield self._node_cache[(type(x), x.fspath)]
|
||||
else:
|
||||
yield x
|
||||
self._node_cache[(type(x), x.fspath)] = x
|
||||
yield x
|
||||
else:
|
||||
assert argpath.check(file=1)
|
||||
|
||||
@@ -561,10 +570,7 @@ class Session(nodes.FSCollector):
|
||||
return True
|
||||
|
||||
def _tryconvertpyarg(self, x):
|
||||
"""Convert a dotted module name to path.
|
||||
|
||||
"""
|
||||
|
||||
"""Convert a dotted module name to path."""
|
||||
try:
|
||||
with _patched_find_module():
|
||||
loader = pkgutil.find_loader(x)
|
||||
@@ -596,8 +602,7 @@ class Session(nodes.FSCollector):
|
||||
raise UsageError(
|
||||
"file or package not found: " + arg + " (missing __init__.py?)"
|
||||
)
|
||||
else:
|
||||
raise UsageError("file not found: " + arg)
|
||||
raise UsageError("file not found: " + arg)
|
||||
parts[0] = path
|
||||
return parts
|
||||
|
||||
|
||||
@@ -24,11 +24,6 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
class MarkerError(Exception):
|
||||
|
||||
"""Error in use of a pytest marker/attribute."""
|
||||
|
||||
|
||||
def param(*values, **kw):
|
||||
"""Specify a parameter in `pytest.mark.parametrize`_ calls or
|
||||
:ref:`parametrized fixtures <fixture-parametrize-marks>`.
|
||||
@@ -163,9 +158,9 @@ def pytest_configure(config):
|
||||
|
||||
empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION)
|
||||
|
||||
if empty_parameterset not in ("skip", "xfail", None, ""):
|
||||
if empty_parameterset not in ("skip", "xfail", "fail_at_collect", None, ""):
|
||||
raise UsageError(
|
||||
"{!s} must be one of skip and xfail,"
|
||||
"{!s} must be one of skip, xfail or fail_at_collect"
|
||||
" but it is {!r}".format(EMPTY_PARAMETERSET_OPTION, empty_parameterset)
|
||||
)
|
||||
|
||||
|
||||
@@ -90,7 +90,10 @@ class MarkEvaluator(object):
|
||||
else:
|
||||
if "reason" not in mark.kwargs:
|
||||
# XXX better be checked at collection time
|
||||
msg = "you need to specify reason=STRING " "when using booleans as conditions."
|
||||
msg = (
|
||||
"you need to specify reason=STRING "
|
||||
"when using booleans as conditions."
|
||||
)
|
||||
fail(msg)
|
||||
result = bool(expr)
|
||||
if result:
|
||||
|
||||
@@ -66,7 +66,10 @@ python_keywords_allowed_list = ["or", "and", "not"]
|
||||
|
||||
def matchmark(colitem, markexpr):
|
||||
"""Tries to match on any marker names, attached to the given colitem."""
|
||||
return eval(markexpr, {}, MarkMapping.from_item(colitem))
|
||||
try:
|
||||
return eval(markexpr, {}, MarkMapping.from_item(colitem))
|
||||
except SyntaxError as e:
|
||||
raise SyntaxError(str(e) + "\nMarker expression must be valid Python!")
|
||||
|
||||
|
||||
def matchkeyword(colitem, keywordexpr):
|
||||
|
||||
@@ -6,6 +6,7 @@ from operator import attrgetter
|
||||
|
||||
import attr
|
||||
|
||||
from _pytest.outcomes import fail
|
||||
from ..deprecated import MARK_PARAMETERSET_UNPACKING, MARK_INFO_ATTRIBUTE
|
||||
from ..compat import NOTSET, getfslineno, MappingMixin
|
||||
from six.moves import map
|
||||
@@ -32,11 +33,19 @@ def istestfunc(func):
|
||||
|
||||
|
||||
def get_empty_parameterset_mark(config, argnames, func):
|
||||
from ..nodes import Collector
|
||||
|
||||
requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION)
|
||||
if requested_mark in ("", None, "skip"):
|
||||
mark = MARK_GEN.skip
|
||||
elif requested_mark == "xfail":
|
||||
mark = MARK_GEN.xfail(run=False)
|
||||
elif requested_mark == "fail_at_collect":
|
||||
f_name = func.__name__
|
||||
_, lineno = getfslineno(func)
|
||||
raise Collector.CollectError(
|
||||
"Empty parameter set in '%s' at line %d" % (f_name, lineno)
|
||||
)
|
||||
else:
|
||||
raise LookupError(requested_mark)
|
||||
fs, lineno = getfslineno(func)
|
||||
@@ -65,7 +74,7 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
|
||||
return cls(values, marks, id_)
|
||||
|
||||
@classmethod
|
||||
def extract_from(cls, parameterset, legacy_force_tuple=False):
|
||||
def extract_from(cls, parameterset, belonging_definition, legacy_force_tuple=False):
|
||||
"""
|
||||
:param parameterset:
|
||||
a legacy style parameterset that may or may not be a tuple,
|
||||
@@ -75,6 +84,7 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
|
||||
enforce tuple wrapping so single argument tuple values
|
||||
don't get decomposed and break tests
|
||||
|
||||
:param belonging_definition: the item that we will be extracting the parameters from.
|
||||
"""
|
||||
|
||||
if isinstance(parameterset, cls):
|
||||
@@ -93,20 +103,24 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
|
||||
if legacy_force_tuple:
|
||||
argval = (argval,)
|
||||
|
||||
if newmarks:
|
||||
warnings.warn(MARK_PARAMETERSET_UNPACKING)
|
||||
if newmarks and belonging_definition is not None:
|
||||
belonging_definition.warn(MARK_PARAMETERSET_UNPACKING)
|
||||
|
||||
return cls(argval, marks=newmarks, id=None)
|
||||
|
||||
@classmethod
|
||||
def _for_parametrize(cls, argnames, argvalues, func, config):
|
||||
def _for_parametrize(cls, argnames, argvalues, func, config, function_definition):
|
||||
if not isinstance(argnames, (tuple, list)):
|
||||
argnames = [x.strip() for x in argnames.split(",") if x.strip()]
|
||||
force_tuple = len(argnames) == 1
|
||||
else:
|
||||
force_tuple = False
|
||||
parameters = [
|
||||
ParameterSet.extract_from(x, legacy_force_tuple=force_tuple)
|
||||
ParameterSet.extract_from(
|
||||
x,
|
||||
legacy_force_tuple=force_tuple,
|
||||
belonging_definition=function_definition,
|
||||
)
|
||||
for x in argvalues
|
||||
]
|
||||
del argvalues
|
||||
@@ -302,7 +316,7 @@ def _marked(func, mark):
|
||||
return any(mark == info.combined for info in func_mark)
|
||||
|
||||
|
||||
@attr.s
|
||||
@attr.s(repr=False)
|
||||
class MarkInfo(object):
|
||||
""" Marking object created by :class:`MarkDecorator` instances. """
|
||||
|
||||
@@ -380,7 +394,7 @@ class MarkGenerator(object):
|
||||
x = marker.split("(", 1)[0]
|
||||
values.add(x)
|
||||
if name not in self._markers:
|
||||
raise AttributeError("%r not a registered marker" % (name,))
|
||||
fail("{!r} not a registered marker".format(name), pytrace=False)
|
||||
|
||||
|
||||
MARK_GEN = MarkGenerator()
|
||||
|
||||
@@ -4,9 +4,12 @@ from __future__ import absolute_import, division, print_function
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import warnings
|
||||
from contextlib import contextmanager
|
||||
|
||||
import six
|
||||
|
||||
import pytest
|
||||
from _pytest.fixtures import fixture
|
||||
|
||||
RE_IMPORT_ERROR_NAME = re.compile("^No module named (.*)$")
|
||||
@@ -209,22 +212,41 @@ class MonkeyPatch(object):
|
||||
self._setitem.append((dic, name, dic.get(name, notset)))
|
||||
del dic[name]
|
||||
|
||||
def _warn_if_env_name_is_not_str(self, name):
|
||||
"""On Python 2, warn if the given environment variable name is not a native str (#4056)"""
|
||||
if six.PY2 and not isinstance(name, str):
|
||||
warnings.warn(
|
||||
pytest.PytestWarning(
|
||||
"Environment variable name {!r} should be str".format(name)
|
||||
)
|
||||
)
|
||||
|
||||
def setenv(self, name, value, prepend=None):
|
||||
""" Set environment variable ``name`` to ``value``. If ``prepend``
|
||||
is a character, read the current environment variable value
|
||||
and prepend the ``value`` adjoined with the ``prepend`` character."""
|
||||
value = str(value)
|
||||
if not isinstance(value, str):
|
||||
warnings.warn(
|
||||
pytest.PytestWarning(
|
||||
"Environment variable value {!r} should be str, converted to str implicitly".format(
|
||||
value
|
||||
)
|
||||
)
|
||||
)
|
||||
value = str(value)
|
||||
if prepend and name in os.environ:
|
||||
value = value + prepend + os.environ[name]
|
||||
self._warn_if_env_name_is_not_str(name)
|
||||
self.setitem(os.environ, name, value)
|
||||
|
||||
def delenv(self, name, raising=True):
|
||||
""" Delete ``name`` from the environment. Raise KeyError it does not
|
||||
exist.
|
||||
""" Delete ``name`` from the environment. Raise KeyError if it does
|
||||
not exist.
|
||||
|
||||
If ``raising`` is set to False, no exception will be raised if the
|
||||
environment variable is missing.
|
||||
"""
|
||||
self._warn_if_env_name_is_not_str(name)
|
||||
self.delitem(os.environ, name, raising=raising)
|
||||
|
||||
def syspath_prepend(self, path):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import os
|
||||
import warnings
|
||||
|
||||
import six
|
||||
import py
|
||||
@@ -7,6 +8,8 @@ import attr
|
||||
|
||||
import _pytest
|
||||
import _pytest._code
|
||||
from _pytest.compat import getfslineno
|
||||
from _pytest.outcomes import fail
|
||||
|
||||
from _pytest.mark.structures import NodeKeywords, MarkInfo
|
||||
|
||||
@@ -59,11 +62,11 @@ class _CompatProperty(object):
|
||||
if obj is None:
|
||||
return self
|
||||
|
||||
# TODO: reenable in the features branch
|
||||
# warnings.warn(
|
||||
# "usage of {owner!r}.{name} is deprecated, please use pytest.{name} instead".format(
|
||||
# name=self.name, owner=type(owner).__name__),
|
||||
# PendingDeprecationWarning, stacklevel=2)
|
||||
from _pytest.deprecated import COMPAT_PROPERTY
|
||||
|
||||
warnings.warn(
|
||||
COMPAT_PROPERTY.format(name=self.name, owner=owner.__name__), stacklevel=2
|
||||
)
|
||||
return getattr(__import__("pytest"), self.name)
|
||||
|
||||
|
||||
@@ -124,29 +127,107 @@ class Node(object):
|
||||
if isinstance(maybe_compatprop, _CompatProperty):
|
||||
return getattr(__import__("pytest"), name)
|
||||
else:
|
||||
from _pytest.deprecated import CUSTOM_CLASS
|
||||
|
||||
cls = getattr(self, name)
|
||||
# TODO: reenable in the features branch
|
||||
# warnings.warn("use of node.%s is deprecated, "
|
||||
# "use pytest_pycollect_makeitem(...) to create custom "
|
||||
# "collection nodes" % name, category=DeprecationWarning)
|
||||
self.warn(CUSTOM_CLASS.format(name=name, type_name=type(self).__name__))
|
||||
return cls
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s %r>" % (self.__class__.__name__, getattr(self, "name", None))
|
||||
|
||||
def warn(self, code, message):
|
||||
""" generate a warning with the given code and message for this
|
||||
item. """
|
||||
def warn(self, _code_or_warning=None, message=None, code=None):
|
||||
"""Issue a warning for this item.
|
||||
|
||||
Warnings will be displayed after the test session, unless explicitly suppressed.
|
||||
|
||||
This can be called in two forms:
|
||||
|
||||
**Warning instance**
|
||||
|
||||
This was introduced in pytest 3.8 and uses the standard warning mechanism to issue warnings.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
node.warn(PytestWarning("some message"))
|
||||
|
||||
The warning instance must be a subclass of :class:`pytest.PytestWarning`.
|
||||
|
||||
**code/message (deprecated)**
|
||||
|
||||
This form was used in pytest prior to 3.8 and is considered deprecated. Using this form will emit another
|
||||
warning about the deprecation:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
node.warn("CI", "some message")
|
||||
|
||||
:param Union[Warning,str] _code_or_warning:
|
||||
warning instance or warning code (legacy). This parameter receives an underscore for backward
|
||||
compatibility with the legacy code/message form, and will be replaced for something
|
||||
more usual when the legacy form is removed.
|
||||
|
||||
:param Union[str,None] message: message to display when called in the legacy form.
|
||||
:param str code: code for the warning, in legacy form when using keyword arguments.
|
||||
:return:
|
||||
"""
|
||||
if message is None:
|
||||
if _code_or_warning is None:
|
||||
raise ValueError("code_or_warning must be given")
|
||||
self._std_warn(_code_or_warning)
|
||||
else:
|
||||
if _code_or_warning and code:
|
||||
raise ValueError(
|
||||
"code_or_warning and code cannot both be passed to this function"
|
||||
)
|
||||
code = _code_or_warning or code
|
||||
self._legacy_warn(code, message)
|
||||
|
||||
def _legacy_warn(self, code, message):
|
||||
"""
|
||||
.. deprecated:: 3.8
|
||||
|
||||
Use :meth:`Node.std_warn <_pytest.nodes.Node.std_warn>` instead.
|
||||
|
||||
Generate a warning with the given code and message for this item.
|
||||
"""
|
||||
from _pytest.deprecated import NODE_WARN
|
||||
|
||||
self._std_warn(NODE_WARN)
|
||||
|
||||
assert isinstance(code, str)
|
||||
fslocation = getattr(self, "location", None)
|
||||
if fslocation is None:
|
||||
fslocation = getattr(self, "fspath", None)
|
||||
fslocation = get_fslocation_from_item(self)
|
||||
self.ihook.pytest_logwarning.call_historic(
|
||||
kwargs=dict(
|
||||
code=code, message=message, nodeid=self.nodeid, fslocation=fslocation
|
||||
)
|
||||
)
|
||||
|
||||
def _std_warn(self, warning):
|
||||
"""Issue a warning for this item.
|
||||
|
||||
Warnings will be displayed after the test session, unless explicitly suppressed
|
||||
|
||||
:param Warning warning: the warning instance to issue. Must be a subclass of PytestWarning.
|
||||
|
||||
:raise ValueError: if ``warning`` instance is not a subclass of PytestWarning.
|
||||
"""
|
||||
from _pytest.warning_types import PytestWarning
|
||||
|
||||
if not isinstance(warning, PytestWarning):
|
||||
raise ValueError(
|
||||
"warning must be an instance of PytestWarning or subclass, got {!r}".format(
|
||||
warning
|
||||
)
|
||||
)
|
||||
path, lineno = get_fslocation_from_item(self)
|
||||
warnings.warn_explicit(
|
||||
warning,
|
||||
category=None,
|
||||
filename=str(path),
|
||||
lineno=lineno + 1 if lineno is not None else None,
|
||||
)
|
||||
|
||||
# methods for ordering nodes
|
||||
@property
|
||||
def nodeid(self):
|
||||
@@ -266,6 +347,9 @@ class Node(object):
|
||||
pass
|
||||
|
||||
def _repr_failure_py(self, excinfo, style=None):
|
||||
if excinfo.errisinstance(fail.Exception):
|
||||
if not excinfo.value.pytrace:
|
||||
return six.text_type(excinfo.value)
|
||||
fm = self.session._fixturemanager
|
||||
if excinfo.errisinstance(fm.FixtureLookupError):
|
||||
return excinfo.value.formatrepr()
|
||||
@@ -310,6 +394,24 @@ class Node(object):
|
||||
repr_failure = _repr_failure_py
|
||||
|
||||
|
||||
def get_fslocation_from_item(item):
|
||||
"""Tries to extract the actual location from an item, depending on available attributes:
|
||||
|
||||
* "fslocation": a pair (path, lineno)
|
||||
* "obj": a Python object that the item wraps.
|
||||
* "fspath": just a path
|
||||
|
||||
:rtype: a tuple of (str|LocalPath, int) with filename and line number.
|
||||
"""
|
||||
result = getattr(item, "location", None)
|
||||
if result is not None:
|
||||
return result[:2]
|
||||
obj = getattr(item, "obj", None)
|
||||
if obj is not None:
|
||||
return getfslineno(obj)
|
||||
return getattr(item, "fspath", "unknown location"), -1
|
||||
|
||||
|
||||
class Collector(Node):
|
||||
""" Collector instances create children through collect()
|
||||
and thus iteratively build a tree.
|
||||
|
||||
@@ -49,31 +49,43 @@ class Failed(OutcomeException):
|
||||
class Exit(KeyboardInterrupt):
|
||||
""" raised for immediate program exits (no tracebacks/summaries)"""
|
||||
|
||||
def __init__(self, msg="unknown reason"):
|
||||
def __init__(self, msg="unknown reason", returncode=None):
|
||||
self.msg = msg
|
||||
self.returncode = returncode
|
||||
KeyboardInterrupt.__init__(self, msg)
|
||||
|
||||
|
||||
# exposed helper methods
|
||||
|
||||
|
||||
def exit(msg):
|
||||
""" exit testing process as if KeyboardInterrupt was triggered. """
|
||||
def exit(msg, returncode=None):
|
||||
"""
|
||||
Exit testing process as if KeyboardInterrupt was triggered.
|
||||
|
||||
:param str msg: message to display upon exit.
|
||||
:param int returncode: return code to be used when exiting pytest.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
raise Exit(msg)
|
||||
raise Exit(msg, returncode)
|
||||
|
||||
|
||||
exit.Exception = Exit
|
||||
|
||||
|
||||
def skip(msg="", **kwargs):
|
||||
""" skip an executing test with the given message. Note: it's usually
|
||||
better to use the pytest.mark.skipif marker to declare a test to be
|
||||
skipped under certain conditions like mismatching platforms or
|
||||
dependencies. See the pytest_skipping plugin for details.
|
||||
"""
|
||||
Skip an executing test with the given message.
|
||||
|
||||
This function should be called only during testing (setup, call or teardown) or
|
||||
during collection by using the ``allow_module_level`` flag.
|
||||
|
||||
:kwarg bool allow_module_level: allows this function to be called at
|
||||
module level, skipping the rest of the module. Default to False.
|
||||
|
||||
.. note::
|
||||
It is better to use the :ref:`pytest.mark.skipif ref` marker when possible to declare a test to be
|
||||
skipped under certain conditions like mismatching platforms or
|
||||
dependencies.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
allow_module_level = kwargs.pop("allow_module_level", False)
|
||||
@@ -87,10 +99,12 @@ skip.Exception = Skipped
|
||||
|
||||
|
||||
def fail(msg="", pytrace=True):
|
||||
""" explicitly fail a currently-executing test with the given Message.
|
||||
"""
|
||||
Explicitly fail an executing test with the given message.
|
||||
|
||||
:arg pytrace: if false the msg represents the full failure information
|
||||
and no python traceback will be reported.
|
||||
:param str msg: the message to show the user as reason for the failure.
|
||||
:param bool pytrace: if false the msg represents the full failure information and no
|
||||
python traceback will be reported.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
raise Failed(msg=msg, pytrace=pytrace)
|
||||
@@ -104,7 +118,15 @@ class XFailed(fail.Exception):
|
||||
|
||||
|
||||
def xfail(reason=""):
|
||||
""" xfail an executing test or setup functions with the given reason."""
|
||||
"""
|
||||
Imperatively xfail an executing test or setup functions with the given reason.
|
||||
|
||||
This function should be called only during testing (setup, call or teardown).
|
||||
|
||||
.. note::
|
||||
It is better to use the :ref:`pytest.mark.xfail ref` marker when possible to declare a test to be
|
||||
xfailed under certain conditions like known bugs or missing features.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
raise XFailed(reason)
|
||||
|
||||
|
||||
304
src/_pytest/pathlib.py
Normal file
304
src/_pytest/pathlib.py
Normal file
@@ -0,0 +1,304 @@
|
||||
|
||||
import os
|
||||
import errno
|
||||
import atexit
|
||||
import operator
|
||||
import six
|
||||
import sys
|
||||
from functools import reduce
|
||||
import uuid
|
||||
from six.moves import map
|
||||
import itertools
|
||||
import shutil
|
||||
from os.path import expanduser, expandvars, isabs, sep
|
||||
from posixpath import sep as posix_sep
|
||||
import fnmatch
|
||||
import stat
|
||||
|
||||
from .compat import PY36
|
||||
|
||||
|
||||
if PY36:
|
||||
from pathlib import Path, PurePath
|
||||
else:
|
||||
from pathlib2 import Path, PurePath
|
||||
|
||||
__all__ = ["Path", "PurePath"]
|
||||
|
||||
|
||||
LOCK_TIMEOUT = 60 * 60 * 3
|
||||
|
||||
get_lock_path = operator.methodcaller("joinpath", ".lock")
|
||||
|
||||
|
||||
def ensure_reset_dir(path):
|
||||
"""
|
||||
ensures the given path is a empty directory
|
||||
"""
|
||||
if path.exists():
|
||||
rmtree(path, force=True)
|
||||
path.mkdir()
|
||||
|
||||
|
||||
def _shutil_rmtree_remove_writable(func, fspath, _):
|
||||
"Clear the readonly bit and reattempt the removal"
|
||||
os.chmod(fspath, stat.S_IWRITE)
|
||||
func(fspath)
|
||||
|
||||
|
||||
def rmtree(path, force=False):
|
||||
if force:
|
||||
# ignore_errors leaves dead folders around
|
||||
# python needs a rm -rf as a followup
|
||||
# the trick with _shutil_rmtree_remove_writable is unreliable
|
||||
shutil.rmtree(str(path), ignore_errors=True)
|
||||
else:
|
||||
shutil.rmtree(str(path))
|
||||
|
||||
|
||||
def find_prefixed(root, prefix):
|
||||
"""finds all elements in root that begin with the prefix, case insensitive"""
|
||||
l_prefix = prefix.lower()
|
||||
for x in root.iterdir():
|
||||
if x.name.lower().startswith(l_prefix):
|
||||
yield x
|
||||
|
||||
|
||||
def extract_suffixes(iter, prefix):
|
||||
"""
|
||||
:param iter: iterator over path names
|
||||
:param prefix: expected prefix of the path names
|
||||
:returns: the parts of the paths following the prefix
|
||||
"""
|
||||
p_len = len(prefix)
|
||||
for p in iter:
|
||||
yield p.name[p_len:]
|
||||
|
||||
|
||||
def find_suffixes(root, prefix):
|
||||
"""combines find_prefixes and extract_suffixes
|
||||
"""
|
||||
return extract_suffixes(find_prefixed(root, prefix), prefix)
|
||||
|
||||
|
||||
def parse_num(maybe_num):
|
||||
"""parses number path suffixes, returns -1 on error"""
|
||||
try:
|
||||
return int(maybe_num)
|
||||
except ValueError:
|
||||
return -1
|
||||
|
||||
|
||||
if six.PY2:
|
||||
|
||||
def _max(iterable, default):
|
||||
"""needed due to python2.7 lacking the default argument for max"""
|
||||
return reduce(max, iterable, default)
|
||||
|
||||
|
||||
else:
|
||||
_max = max
|
||||
|
||||
|
||||
def _force_symlink(root, target, link_to):
|
||||
"""helper to create the current symlink
|
||||
|
||||
its full of race conditions that are reasonably ok to ignore
|
||||
for the contex of best effort linking to the latest testrun
|
||||
|
||||
the presumption being thatin case of much parallelism
|
||||
the inaccuracy is going to be acceptable
|
||||
"""
|
||||
current_symlink = root.joinpath(target)
|
||||
try:
|
||||
current_symlink.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
current_symlink.symlink_to(link_to)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def make_numbered_dir(root, prefix):
|
||||
"""create a directory with a increased number as suffix for the given prefix"""
|
||||
for i in range(10):
|
||||
# try up to 10 times to create the folder
|
||||
max_existing = _max(map(parse_num, find_suffixes(root, prefix)), default=-1)
|
||||
new_number = max_existing + 1
|
||||
new_path = root.joinpath("{}{}".format(prefix, new_number))
|
||||
try:
|
||||
new_path.mkdir()
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
_force_symlink(root, prefix + "current", new_path)
|
||||
return new_path
|
||||
else:
|
||||
raise EnvironmentError(
|
||||
"could not create numbered dir with prefix "
|
||||
"{prefix} in {root} after 10 tries".format(prefix=prefix, root=root)
|
||||
)
|
||||
|
||||
|
||||
def create_cleanup_lock(p):
|
||||
"""crates a lock to prevent premature folder cleanup"""
|
||||
lock_path = get_lock_path(p)
|
||||
try:
|
||||
fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644)
|
||||
except OSError as e:
|
||||
if e.errno == errno.EEXIST:
|
||||
six.raise_from(
|
||||
EnvironmentError("cannot create lockfile in {path}".format(path=p)), e
|
||||
)
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
pid = os.getpid()
|
||||
spid = str(pid)
|
||||
if not isinstance(spid, six.binary_type):
|
||||
spid = spid.encode("ascii")
|
||||
os.write(fd, spid)
|
||||
os.close(fd)
|
||||
if not lock_path.is_file():
|
||||
raise EnvironmentError("lock path got renamed after sucessfull creation")
|
||||
return lock_path
|
||||
|
||||
|
||||
def register_cleanup_lock_removal(lock_path, register=atexit.register):
|
||||
"""registers a cleanup function for removing a lock, by default on atexit"""
|
||||
pid = os.getpid()
|
||||
|
||||
def cleanup_on_exit(lock_path=lock_path, original_pid=pid):
|
||||
current_pid = os.getpid()
|
||||
if current_pid != original_pid:
|
||||
# fork
|
||||
return
|
||||
try:
|
||||
lock_path.unlink()
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
|
||||
return register(cleanup_on_exit)
|
||||
|
||||
|
||||
def delete_a_numbered_dir(path):
|
||||
"""removes a numbered directory"""
|
||||
create_cleanup_lock(path)
|
||||
parent = path.parent
|
||||
|
||||
garbage = parent.joinpath("garbage-{}".format(uuid.uuid4()))
|
||||
path.rename(garbage)
|
||||
rmtree(garbage, force=True)
|
||||
|
||||
|
||||
def ensure_deletable(path, consider_lock_dead_if_created_before):
|
||||
"""checks if a lock exists and breaks it if its considered dead"""
|
||||
if path.is_symlink():
|
||||
return False
|
||||
lock = get_lock_path(path)
|
||||
if not lock.exists():
|
||||
return True
|
||||
try:
|
||||
lock_time = lock.stat().st_mtime
|
||||
except Exception:
|
||||
return False
|
||||
else:
|
||||
if lock_time < consider_lock_dead_if_created_before:
|
||||
lock.unlink()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def try_cleanup(path, consider_lock_dead_if_created_before):
|
||||
"""tries to cleanup a folder if we can ensure its deletable"""
|
||||
if ensure_deletable(path, consider_lock_dead_if_created_before):
|
||||
delete_a_numbered_dir(path)
|
||||
|
||||
|
||||
def cleanup_candidates(root, prefix, keep):
|
||||
"""lists candidates for numbered directories to be removed - follows py.path"""
|
||||
max_existing = _max(map(parse_num, find_suffixes(root, prefix)), default=-1)
|
||||
max_delete = max_existing - keep
|
||||
paths = find_prefixed(root, prefix)
|
||||
paths, paths2 = itertools.tee(paths)
|
||||
numbers = map(parse_num, extract_suffixes(paths2, prefix))
|
||||
for path, number in zip(paths, numbers):
|
||||
if number <= max_delete:
|
||||
yield path
|
||||
|
||||
|
||||
def cleanup_numbered_dir(root, prefix, keep, consider_lock_dead_if_created_before):
|
||||
"""cleanup for lock driven numbered directories"""
|
||||
for path in cleanup_candidates(root, prefix, keep):
|
||||
try_cleanup(path, consider_lock_dead_if_created_before)
|
||||
for path in root.glob("garbage-*"):
|
||||
try_cleanup(path, consider_lock_dead_if_created_before)
|
||||
|
||||
|
||||
def make_numbered_dir_with_cleanup(root, prefix, keep, lock_timeout):
|
||||
"""creates a numbered dir with a cleanup lock and removes old ones"""
|
||||
e = None
|
||||
for i in range(10):
|
||||
try:
|
||||
p = make_numbered_dir(root, prefix)
|
||||
lock_path = create_cleanup_lock(p)
|
||||
register_cleanup_lock_removal(lock_path)
|
||||
except Exception as e:
|
||||
pass
|
||||
else:
|
||||
consider_lock_dead_if_created_before = p.stat().st_mtime - lock_timeout
|
||||
cleanup_numbered_dir(
|
||||
root=root,
|
||||
prefix=prefix,
|
||||
keep=keep,
|
||||
consider_lock_dead_if_created_before=consider_lock_dead_if_created_before,
|
||||
)
|
||||
return p
|
||||
assert e is not None
|
||||
raise e
|
||||
|
||||
|
||||
def resolve_from_str(input, root):
|
||||
assert not isinstance(input, Path), "would break on py2"
|
||||
root = Path(root)
|
||||
input = expanduser(input)
|
||||
input = expandvars(input)
|
||||
if isabs(input):
|
||||
return Path(input)
|
||||
else:
|
||||
return root.joinpath(input)
|
||||
|
||||
|
||||
def fnmatch_ex(pattern, path):
|
||||
"""FNMatcher port from py.path.common which works with PurePath() instances.
|
||||
|
||||
The difference between this algorithm and PurePath.match() is that the latter matches "**" glob expressions
|
||||
for each part of the path, while this algorithm uses the whole path instead.
|
||||
|
||||
For example:
|
||||
"tests/foo/bar/doc/test_foo.py" matches pattern "tests/**/doc/test*.py" with this algorithm, but not with
|
||||
PurePath.match().
|
||||
|
||||
This algorithm was ported to keep backward-compatibility with existing settings which assume paths match according
|
||||
this logic.
|
||||
|
||||
References:
|
||||
* https://bugs.python.org/issue29249
|
||||
* https://bugs.python.org/issue34731
|
||||
"""
|
||||
path = PurePath(path)
|
||||
iswin32 = sys.platform.startswith("win")
|
||||
|
||||
if iswin32 and sep not in pattern and posix_sep in pattern:
|
||||
# Running on Windows, the pattern has no Windows path separators,
|
||||
# and the pattern has one or more Posix path separators. Replace
|
||||
# the Posix path separators with the Windows path separator.
|
||||
pattern = pattern.replace(posix_sep, sep)
|
||||
|
||||
if sep not in pattern:
|
||||
name = path.name
|
||||
else:
|
||||
name = six.text_type(path)
|
||||
return fnmatch.fnmatch(name, pattern)
|
||||
@@ -1,13 +0,0 @@
|
||||
from .compat import Path
|
||||
from os.path import expanduser, expandvars, isabs
|
||||
|
||||
|
||||
def resolve_from_str(input, root):
|
||||
assert not isinstance(input, Path), "would break on py2"
|
||||
root = Path(root)
|
||||
input = expanduser(input)
|
||||
input = expandvars(input)
|
||||
if isabs(input):
|
||||
return Path(input)
|
||||
else:
|
||||
return root.joinpath(input)
|
||||
@@ -17,12 +17,13 @@ from weakref import WeakKeyDictionary
|
||||
|
||||
from _pytest.capture import MultiCapture, SysCapture
|
||||
from _pytest._code import Source
|
||||
from _pytest.main import Session, EXIT_INTERRUPTED, EXIT_OK
|
||||
from _pytest.assertion.rewrite import AssertionRewritingHook
|
||||
from _pytest.pathlib import Path
|
||||
from _pytest.compat import safe_str
|
||||
|
||||
import py
|
||||
import pytest
|
||||
from _pytest.main import Session, EXIT_OK
|
||||
from _pytest.assertion.rewrite import AssertionRewritingHook
|
||||
from _pytest.compat import Path
|
||||
from _pytest.compat import safe_str
|
||||
|
||||
IGNORE_PAM = [ # filenames added when obtaining details about the current user
|
||||
u"/var/lib/sss/mc/passwd"
|
||||
@@ -61,6 +62,11 @@ def pytest_configure(config):
|
||||
config.pluginmanager.register(checker)
|
||||
|
||||
|
||||
def raise_on_kwargs(kwargs):
|
||||
if kwargs:
|
||||
raise TypeError("Unexpected arguments: {}".format(", ".join(sorted(kwargs))))
|
||||
|
||||
|
||||
class LsofFdLeakChecker(object):
|
||||
def get_open_files(self):
|
||||
out = self._exec_lsof()
|
||||
@@ -126,7 +132,7 @@ class LsofFdLeakChecker(object):
|
||||
error.append(error[0])
|
||||
error.append("*** function %s:%s: %s " % item.location)
|
||||
error.append("See issue #2366")
|
||||
item.warn("", "\n".join(error))
|
||||
item.warn(pytest.PytestWarning("\n".join(error)))
|
||||
|
||||
|
||||
# XXX copied from execnet's conftest.py - needs to be merged
|
||||
@@ -407,7 +413,9 @@ class RunResult(object):
|
||||
return d
|
||||
raise ValueError("Pytest terminal report not found")
|
||||
|
||||
def assert_outcomes(self, passed=0, skipped=0, failed=0, error=0):
|
||||
def assert_outcomes(
|
||||
self, passed=0, skipped=0, failed=0, error=0, xpassed=0, xfailed=0
|
||||
):
|
||||
"""Assert that the specified outcomes appear with the respective
|
||||
numbers (0 means it didn't occur) in the text output from a test run.
|
||||
|
||||
@@ -418,10 +426,18 @@ class RunResult(object):
|
||||
"skipped": d.get("skipped", 0),
|
||||
"failed": d.get("failed", 0),
|
||||
"error": d.get("error", 0),
|
||||
"xpassed": d.get("xpassed", 0),
|
||||
"xfailed": d.get("xfailed", 0),
|
||||
}
|
||||
assert obtained == dict(
|
||||
passed=passed, skipped=skipped, failed=failed, error=error
|
||||
)
|
||||
expected = {
|
||||
"passed": passed,
|
||||
"skipped": skipped,
|
||||
"failed": failed,
|
||||
"error": error,
|
||||
"xpassed": xpassed,
|
||||
"xfailed": xfailed,
|
||||
}
|
||||
assert obtained == expected
|
||||
|
||||
|
||||
class CwdSnapshot(object):
|
||||
@@ -472,11 +488,16 @@ class Testdir(object):
|
||||
|
||||
"""
|
||||
|
||||
class TimeoutExpired(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self, request, tmpdir_factory):
|
||||
self.request = request
|
||||
self._mod_collections = WeakKeyDictionary()
|
||||
name = request.function.__name__
|
||||
self.tmpdir = tmpdir_factory.mktemp(name, numbered=True)
|
||||
self.test_tmproot = tmpdir_factory.mktemp("tmp-" + name, numbered=True)
|
||||
os.environ["PYTEST_DEBUG_TEMPROOT"] = str(self.test_tmproot)
|
||||
self.plugins = []
|
||||
self._cwd_snapshot = CwdSnapshot()
|
||||
self._sys_path_snapshot = SysPathsSnapshot()
|
||||
@@ -503,6 +524,7 @@ class Testdir(object):
|
||||
self._sys_modules_snapshot.restore()
|
||||
self._sys_path_snapshot.restore()
|
||||
self._cwd_snapshot.restore()
|
||||
os.environ.pop("PYTEST_DEBUG_TEMPROOT", None)
|
||||
|
||||
def __take_sys_modules_snapshot(self):
|
||||
# some zope modules used by twisted-related tests keep internal state
|
||||
@@ -515,7 +537,6 @@ class Testdir(object):
|
||||
|
||||
def make_hook_recorder(self, pluginmanager):
|
||||
"""Create a new :py:class:`HookRecorder` for a PluginManager."""
|
||||
assert not hasattr(pluginmanager, "reprec")
|
||||
pluginmanager.reprec = reprec = HookRecorder(pluginmanager)
|
||||
self.request.addfinalizer(reprec.finish_recording)
|
||||
return reprec
|
||||
@@ -633,10 +654,10 @@ class Testdir(object):
|
||||
return p
|
||||
|
||||
def copy_example(self, name=None):
|
||||
from . import experiments
|
||||
import warnings
|
||||
from _pytest.warning_types import PYTESTER_COPY_EXAMPLE
|
||||
|
||||
warnings.warn(experiments.PYTESTER_COPY_EXAMPLE, stacklevel=2)
|
||||
warnings.warn(PYTESTER_COPY_EXAMPLE, stacklevel=2)
|
||||
example_dir = self.request.config.getini("pytester_example_dir")
|
||||
if example_dir is None:
|
||||
raise ValueError("pytester_example_dir is unset, can't copy examples")
|
||||
@@ -836,7 +857,7 @@ class Testdir(object):
|
||||
|
||||
# typically we reraise keyboard interrupts from the child run
|
||||
# because it's our user requesting interruption of the testing
|
||||
if ret == 2 and not kwargs.get("no_reraise_ctrlc"):
|
||||
if ret == EXIT_INTERRUPTED and not kwargs.get("no_reraise_ctrlc"):
|
||||
calls = reprec.getcalls("pytest_keyboard_interrupt")
|
||||
if calls and calls[-1].excinfo.type == KeyboardInterrupt:
|
||||
raise KeyboardInterrupt()
|
||||
@@ -1030,14 +1051,23 @@ class Testdir(object):
|
||||
|
||||
return popen
|
||||
|
||||
def run(self, *cmdargs):
|
||||
def run(self, *cmdargs, **kwargs):
|
||||
"""Run a command with arguments.
|
||||
|
||||
Run a process using subprocess.Popen saving the stdout and stderr.
|
||||
|
||||
:param args: the sequence of arguments to pass to `subprocess.Popen()`
|
||||
:param timeout: the period in seconds after which to timeout and raise
|
||||
:py:class:`Testdir.TimeoutExpired`
|
||||
|
||||
Returns a :py:class:`RunResult`.
|
||||
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
|
||||
timeout = kwargs.pop("timeout", None)
|
||||
raise_on_kwargs(kwargs)
|
||||
|
||||
cmdargs = [
|
||||
str(arg) if isinstance(arg, py.path.local) else arg for arg in cmdargs
|
||||
]
|
||||
@@ -1052,7 +1082,40 @@ class Testdir(object):
|
||||
popen = self.popen(
|
||||
cmdargs, stdout=f1, stderr=f2, close_fds=(sys.platform != "win32")
|
||||
)
|
||||
ret = popen.wait()
|
||||
|
||||
def handle_timeout():
|
||||
__tracebackhide__ = True
|
||||
|
||||
timeout_message = (
|
||||
"{seconds} second timeout expired running:"
|
||||
" {command}".format(seconds=timeout, command=cmdargs)
|
||||
)
|
||||
|
||||
popen.kill()
|
||||
popen.wait()
|
||||
raise self.TimeoutExpired(timeout_message)
|
||||
|
||||
if timeout is None:
|
||||
ret = popen.wait()
|
||||
elif six.PY3:
|
||||
try:
|
||||
ret = popen.wait(timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
handle_timeout()
|
||||
else:
|
||||
end = time.time() + timeout
|
||||
|
||||
resolution = min(0.1, timeout / 10)
|
||||
|
||||
while True:
|
||||
ret = popen.poll()
|
||||
if ret is not None:
|
||||
break
|
||||
|
||||
if time.time() > end:
|
||||
handle_timeout()
|
||||
|
||||
time.sleep(resolution)
|
||||
finally:
|
||||
f1.close()
|
||||
f2.close()
|
||||
@@ -1099,9 +1162,15 @@ class Testdir(object):
|
||||
with "runpytest-" so they do not conflict with the normal numbered
|
||||
pytest location for temporary files and directories.
|
||||
|
||||
:param args: the sequence of arguments to pass to the pytest subprocess
|
||||
:param timeout: the period in seconds after which to timeout and raise
|
||||
:py:class:`Testdir.TimeoutExpired`
|
||||
|
||||
Returns a :py:class:`RunResult`.
|
||||
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
|
||||
p = py.path.local.make_numbered_dir(
|
||||
prefix="runpytest-", keep=None, rootdir=self.tmpdir
|
||||
)
|
||||
@@ -1110,7 +1179,7 @@ class Testdir(object):
|
||||
if plugins:
|
||||
args = ("-p", plugins[0]) + args
|
||||
args = self._getpytestargs() + args
|
||||
return self.run(*args)
|
||||
return self.run(*args, timeout=kwargs.get("timeout"))
|
||||
|
||||
def spawn_pytest(self, string, expect_timeout=10.0):
|
||||
"""Run pytest using pexpect.
|
||||
@@ -1258,6 +1327,7 @@ class LineMatcher(object):
|
||||
matches and non-matches are also printed on stdout.
|
||||
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
self._match_lines(lines2, fnmatch, "fnmatch")
|
||||
|
||||
def re_match_lines(self, lines2):
|
||||
@@ -1269,6 +1339,7 @@ class LineMatcher(object):
|
||||
The matches and non-matches are also printed on stdout.
|
||||
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
self._match_lines(lines2, lambda name, pat: re.match(pat, name), "re.match")
|
||||
|
||||
def _match_lines(self, lines2, match_func, match_nickname):
|
||||
|
||||
@@ -13,11 +13,10 @@ from textwrap import dedent
|
||||
import py
|
||||
import six
|
||||
from _pytest.main import FSHookProxy
|
||||
from _pytest.mark import MarkerError
|
||||
from _pytest.config import hookimpl
|
||||
|
||||
import _pytest
|
||||
import pluggy
|
||||
from _pytest._code import filter_traceback
|
||||
from _pytest import fixtures
|
||||
from _pytest import nodes
|
||||
from _pytest import deprecated
|
||||
@@ -37,6 +36,7 @@ from _pytest.compat import (
|
||||
getlocation,
|
||||
enum,
|
||||
get_default_arg_names,
|
||||
getimfunc,
|
||||
)
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.mark.structures import (
|
||||
@@ -44,38 +44,7 @@ from _pytest.mark.structures import (
|
||||
get_unpacked_marks,
|
||||
normalize_mark_list,
|
||||
)
|
||||
|
||||
|
||||
# relative paths that we use to filter traceback entries from appearing to the user;
|
||||
# see filter_traceback
|
||||
# note: if we need to add more paths than what we have now we should probably use a list
|
||||
# for better maintenance
|
||||
_pluggy_dir = py.path.local(pluggy.__file__.rstrip("oc"))
|
||||
# pluggy is either a package or a single module depending on the version
|
||||
if _pluggy_dir.basename == "__init__.py":
|
||||
_pluggy_dir = _pluggy_dir.dirpath()
|
||||
_pytest_dir = py.path.local(_pytest.__file__).dirpath()
|
||||
_py_dir = py.path.local(py.__file__).dirpath()
|
||||
|
||||
|
||||
def filter_traceback(entry):
|
||||
"""Return True if a TracebackEntry instance should be removed from tracebacks:
|
||||
* dynamically generated code (no code to show up for it);
|
||||
* internal traceback from pytest or its internal libraries, py and pluggy.
|
||||
"""
|
||||
# entry.path might sometimes return a str object when the entry
|
||||
# points to dynamically generated code
|
||||
# see https://bitbucket.org/pytest-dev/py/issues/71
|
||||
raw_filename = entry.frame.code.raw.co_filename
|
||||
is_generated = "<" in raw_filename and ">" in raw_filename
|
||||
if is_generated:
|
||||
return False
|
||||
# entry.path might point to a non-existing file, in which case it will
|
||||
# also return a str object. see #1133
|
||||
p = py.path.local(entry.path)
|
||||
return (
|
||||
not p.relto(_pluggy_dir) and not p.relto(_pytest_dir) and not p.relto(_py_dir)
|
||||
)
|
||||
from _pytest.warning_types import RemovedInPytest4Warning, PytestWarning
|
||||
|
||||
|
||||
def pyobj_property(name):
|
||||
@@ -130,7 +99,7 @@ def pytest_addoption(parser):
|
||||
"python_functions",
|
||||
type="args",
|
||||
default=["test"],
|
||||
help="prefixes or glob names for Python test function and " "method discovery",
|
||||
help="prefixes or glob names for Python test function and method discovery",
|
||||
)
|
||||
|
||||
group.addoption(
|
||||
@@ -158,8 +127,8 @@ def pytest_generate_tests(metafunc):
|
||||
alt_spellings = ["parameterize", "parametrise", "parameterise"]
|
||||
for attr in alt_spellings:
|
||||
if hasattr(metafunc.function, attr):
|
||||
msg = "{0} has '{1}', spelling should be 'parametrize'"
|
||||
raise MarkerError(msg.format(metafunc.function.__name__, attr))
|
||||
msg = "{0} has '{1}' mark, spelling should be 'parametrize'"
|
||||
fail(msg.format(metafunc.function.__name__, attr), pytrace=False)
|
||||
for marker in metafunc.definition.iter_markers(name="parametrize"):
|
||||
metafunc.parametrize(*marker.args, **marker.kwargs)
|
||||
|
||||
@@ -239,9 +208,14 @@ def pytest_pycollect_makeitem(collector, name, obj):
|
||||
# or a funtools.wrapped.
|
||||
# We musn't if it's been wrapped with mock.patch (python 2 only)
|
||||
if not (isfunction(obj) or isfunction(get_real_func(obj))):
|
||||
collector.warn(
|
||||
code="C2",
|
||||
message="cannot collect %r because it is not a function." % name,
|
||||
filename, lineno = getfslineno(obj)
|
||||
warnings.warn_explicit(
|
||||
message=PytestWarning(
|
||||
"cannot collect %r because it is not a function." % name
|
||||
),
|
||||
category=None,
|
||||
filename=str(filename),
|
||||
lineno=lineno + 1,
|
||||
)
|
||||
elif getattr(obj, "__test__", True):
|
||||
if is_generator(obj):
|
||||
@@ -349,11 +323,6 @@ class PyCollector(PyobjMixin, nodes.Collector):
|
||||
if isinstance(obj, staticmethod):
|
||||
# static methods need to be unwrapped
|
||||
obj = safe_getattr(obj, "__func__", False)
|
||||
if obj is False:
|
||||
# Python 2.6 wraps in a different way that we won't try to handle
|
||||
msg = "cannot collect static method %r because it is not a function"
|
||||
self.warn(code="C2", message=msg % name)
|
||||
return False
|
||||
return (
|
||||
safe_getattr(obj, "__call__", False)
|
||||
and fixtures.getfixturemarker(obj) is None
|
||||
@@ -662,16 +631,18 @@ class Class(PyCollector):
|
||||
return []
|
||||
if hasinit(self.obj):
|
||||
self.warn(
|
||||
"C1",
|
||||
"cannot collect test class %r because it has a "
|
||||
"__init__ constructor" % self.obj.__name__,
|
||||
PytestWarning(
|
||||
"cannot collect test class %r because it has a "
|
||||
"__init__ constructor" % self.obj.__name__
|
||||
)
|
||||
)
|
||||
return []
|
||||
elif hasnew(self.obj):
|
||||
self.warn(
|
||||
"C1",
|
||||
"cannot collect test class %r because it has a "
|
||||
"__new__ constructor" % self.obj.__name__,
|
||||
PytestWarning(
|
||||
"cannot collect test class %r because it has a "
|
||||
"__new__ constructor" % self.obj.__name__
|
||||
)
|
||||
)
|
||||
return []
|
||||
return [self._getcustomclass("Instance")(name="()", parent=self)]
|
||||
@@ -679,14 +650,12 @@ class Class(PyCollector):
|
||||
def setup(self):
|
||||
setup_class = _get_xunit_func(self.obj, "setup_class")
|
||||
if setup_class is not None:
|
||||
setup_class = getattr(setup_class, "im_func", setup_class)
|
||||
setup_class = getattr(setup_class, "__func__", setup_class)
|
||||
setup_class = getimfunc(setup_class)
|
||||
setup_class(self.obj)
|
||||
|
||||
fin_class = getattr(self.obj, "teardown_class", None)
|
||||
if fin_class is not None:
|
||||
fin_class = getattr(fin_class, "im_func", fin_class)
|
||||
fin_class = getattr(fin_class, "__func__", fin_class)
|
||||
fin_class = getimfunc(fin_class)
|
||||
self.addfinalizer(lambda: fin_class(self.obj))
|
||||
|
||||
|
||||
@@ -759,12 +728,6 @@ class FunctionMixin(PyobjMixin):
|
||||
for entry in excinfo.traceback[1:-1]:
|
||||
entry.set_repr_style("short")
|
||||
|
||||
def _repr_failure_py(self, excinfo, style="long"):
|
||||
if excinfo.errisinstance(fail.Exception):
|
||||
if not excinfo.value.pytrace:
|
||||
return six.text_type(excinfo.value)
|
||||
return super(FunctionMixin, self)._repr_failure_py(excinfo, style=style)
|
||||
|
||||
def repr_failure(self, excinfo, outerr=None):
|
||||
assert outerr is None, "XXX outerr usage is deprecated"
|
||||
style = self.config.option.tbstyle
|
||||
@@ -798,8 +761,11 @@ class Generator(FunctionMixin, PyCollector):
|
||||
"%r generated tests with non-unique name %r" % (self, name)
|
||||
)
|
||||
seen[name] = True
|
||||
values.append(self.Function(name, self, args=args, callobj=call))
|
||||
self.warn("C1", deprecated.YIELD_TESTS)
|
||||
with warnings.catch_warnings():
|
||||
# ignore our own deprecation warning
|
||||
function_class = self.Function
|
||||
values.append(function_class(name, self, args=args, callobj=call))
|
||||
self.warn(deprecated.YIELD_TESTS)
|
||||
return values
|
||||
|
||||
def getcallargs(self, obj):
|
||||
@@ -966,7 +932,11 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
||||
from _pytest.mark import ParameterSet
|
||||
|
||||
argnames, parameters = ParameterSet._for_parametrize(
|
||||
argnames, argvalues, self.function, self.config
|
||||
argnames,
|
||||
argvalues,
|
||||
self.function,
|
||||
self.config,
|
||||
function_definition=self.definition,
|
||||
)
|
||||
del argvalues
|
||||
|
||||
@@ -977,9 +947,11 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
||||
|
||||
arg_values_types = self._resolve_arg_value_types(argnames, indirect)
|
||||
|
||||
ids = self._resolve_arg_ids(argnames, ids, parameters)
|
||||
ids = self._resolve_arg_ids(argnames, ids, parameters, item=self.definition)
|
||||
|
||||
scopenum = scope2index(scope, descr="call to {}".format(self.parametrize))
|
||||
scopenum = scope2index(
|
||||
scope, descr="parametrize() call in {}".format(self.function.__name__)
|
||||
)
|
||||
|
||||
# create the new calls: if we are parametrize() multiple times (by applying the decorator
|
||||
# more than once) then we accumulate those calls generating the cartesian product
|
||||
@@ -1000,13 +972,14 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
||||
newcalls.append(newcallspec)
|
||||
self._calls = newcalls
|
||||
|
||||
def _resolve_arg_ids(self, argnames, ids, parameters):
|
||||
def _resolve_arg_ids(self, argnames, ids, parameters, item):
|
||||
"""Resolves the actual ids for the given argnames, based on the ``ids`` parameter given
|
||||
to ``parametrize``.
|
||||
|
||||
:param List[str] argnames: list of argument names passed to ``parametrize()``.
|
||||
:param ids: the ids parameter of the parametrized call (see docs).
|
||||
:param List[ParameterSet] parameters: the list of parameter values, same size as ``argnames``.
|
||||
:param Item item: the item that generated this parametrized call.
|
||||
:rtype: List[str]
|
||||
:return: the list of ids for each argname given
|
||||
"""
|
||||
@@ -1017,17 +990,18 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
||||
idfn = ids
|
||||
ids = None
|
||||
if ids:
|
||||
func_name = self.function.__name__
|
||||
if len(ids) != len(parameters):
|
||||
raise ValueError(
|
||||
"%d tests specified with %d ids" % (len(parameters), len(ids))
|
||||
)
|
||||
msg = "In {}: {} parameter sets specified, with different number of ids: {}"
|
||||
fail(msg.format(func_name, len(parameters), len(ids)), pytrace=False)
|
||||
for id_value in ids:
|
||||
if id_value is not None and not isinstance(id_value, six.string_types):
|
||||
msg = "ids must be list of strings, found: %s (type: %s)"
|
||||
raise ValueError(
|
||||
msg % (saferepr(id_value), type(id_value).__name__)
|
||||
msg = "In {}: ids must be list of strings, found: {} (type: {!r})"
|
||||
fail(
|
||||
msg.format(func_name, saferepr(id_value), type(id_value)),
|
||||
pytrace=False,
|
||||
)
|
||||
ids = idmaker(argnames, parameters, idfn, ids, self.config)
|
||||
ids = idmaker(argnames, parameters, idfn, ids, self.config, item=item)
|
||||
return ids
|
||||
|
||||
def _resolve_arg_value_types(self, argnames, indirect):
|
||||
@@ -1050,9 +1024,11 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
||||
valtypes = dict.fromkeys(argnames, "funcargs")
|
||||
for arg in indirect:
|
||||
if arg not in argnames:
|
||||
raise ValueError(
|
||||
"indirect given to %r: fixture %r doesn't exist"
|
||||
% (self.function, arg)
|
||||
fail(
|
||||
"In {}: indirect fixture '{}' doesn't exist".format(
|
||||
self.function.__name__, arg
|
||||
),
|
||||
pytrace=False,
|
||||
)
|
||||
valtypes[arg] = "params"
|
||||
return valtypes
|
||||
@@ -1066,19 +1042,25 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
||||
:raise ValueError: if validation fails.
|
||||
"""
|
||||
default_arg_names = set(get_default_arg_names(self.function))
|
||||
func_name = self.function.__name__
|
||||
for arg in argnames:
|
||||
if arg not in self.fixturenames:
|
||||
if arg in default_arg_names:
|
||||
raise ValueError(
|
||||
"%r already takes an argument %r with a default value"
|
||||
% (self.function, arg)
|
||||
fail(
|
||||
"In {}: function already takes an argument '{}' with a default value".format(
|
||||
func_name, arg
|
||||
),
|
||||
pytrace=False,
|
||||
)
|
||||
else:
|
||||
if isinstance(indirect, (tuple, list)):
|
||||
name = "fixture" if arg in indirect else "argument"
|
||||
else:
|
||||
name = "fixture" if indirect else "argument"
|
||||
raise ValueError("%r uses no %s %r" % (self.function, name, arg))
|
||||
fail(
|
||||
"In {}: function uses no {} '{}'".format(func_name, name, arg),
|
||||
pytrace=False,
|
||||
)
|
||||
|
||||
def addcall(self, funcargs=None, id=NOTSET, param=NOTSET):
|
||||
""" Add a new call to the underlying test function during the collection phase of a test run.
|
||||
@@ -1100,10 +1082,8 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
||||
:arg param: a parameter which will be exposed to a later fixture function
|
||||
invocation through the ``request.param`` attribute.
|
||||
"""
|
||||
if self.config:
|
||||
self.config.warn(
|
||||
"C1", message=deprecated.METAFUNC_ADD_CALL, fslocation=None
|
||||
)
|
||||
warnings.warn(deprecated.METAFUNC_ADD_CALL, stacklevel=2)
|
||||
|
||||
assert funcargs is None or isinstance(funcargs, dict)
|
||||
if funcargs is not None:
|
||||
for name in funcargs:
|
||||
@@ -1137,13 +1117,18 @@ def _find_parametrized_scope(argnames, arg2fixturedefs, indirect):
|
||||
"""
|
||||
from _pytest.fixtures import scopes
|
||||
|
||||
indirect_as_list = isinstance(indirect, (list, tuple))
|
||||
all_arguments_are_fixtures = (
|
||||
indirect is True or indirect_as_list and len(indirect) == argnames
|
||||
)
|
||||
if isinstance(indirect, (list, tuple)):
|
||||
all_arguments_are_fixtures = len(indirect) == len(argnames)
|
||||
else:
|
||||
all_arguments_are_fixtures = bool(indirect)
|
||||
|
||||
if all_arguments_are_fixtures:
|
||||
fixturedefs = arg2fixturedefs or {}
|
||||
used_scopes = [fixturedef[0].scope for name, fixturedef in fixturedefs.items()]
|
||||
used_scopes = [
|
||||
fixturedef[0].scope
|
||||
for name, fixturedef in fixturedefs.items()
|
||||
if name in argnames
|
||||
]
|
||||
if used_scopes:
|
||||
# Takes the most narrow scope from used fixtures
|
||||
for scope in reversed(scopes):
|
||||
@@ -1153,21 +1138,20 @@ def _find_parametrized_scope(argnames, arg2fixturedefs, indirect):
|
||||
return "function"
|
||||
|
||||
|
||||
def _idval(val, argname, idx, idfn, config=None):
|
||||
def _idval(val, argname, idx, idfn, item, config):
|
||||
if idfn:
|
||||
s = None
|
||||
try:
|
||||
s = idfn(val)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
# See issue https://github.com/pytest-dev/pytest/issues/2169
|
||||
import warnings
|
||||
|
||||
msg = (
|
||||
"Raised while trying to determine id of parameter %s at position %d."
|
||||
% (argname, idx)
|
||||
"While trying to determine id of parameter {} at position "
|
||||
"{} the following exception was raised:\n".format(argname, idx)
|
||||
)
|
||||
msg += "\nUpdate your code as this will raise an error in pytest-4.0."
|
||||
warnings.warn(msg, DeprecationWarning)
|
||||
msg += " {}: {}\n".format(type(e).__name__, e)
|
||||
msg += "This warning will be an error error in pytest-4.0."
|
||||
item.warn(RemovedInPytest4Warning(msg))
|
||||
if s:
|
||||
return ascii_escaped(s)
|
||||
|
||||
@@ -1191,12 +1175,12 @@ def _idval(val, argname, idx, idfn, config=None):
|
||||
return str(argname) + str(idx)
|
||||
|
||||
|
||||
def _idvalset(idx, parameterset, argnames, idfn, ids, config=None):
|
||||
def _idvalset(idx, parameterset, argnames, idfn, ids, item, config):
|
||||
if parameterset.id is not None:
|
||||
return parameterset.id
|
||||
if ids is None or (idx >= len(ids) or ids[idx] is None):
|
||||
this_id = [
|
||||
_idval(val, argname, idx, idfn, config)
|
||||
_idval(val, argname, idx, idfn, item=item, config=config)
|
||||
for val, argname in zip(parameterset.values, argnames)
|
||||
]
|
||||
return "-".join(this_id)
|
||||
@@ -1204,9 +1188,9 @@ def _idvalset(idx, parameterset, argnames, idfn, ids, config=None):
|
||||
return ascii_escaped(ids[idx])
|
||||
|
||||
|
||||
def idmaker(argnames, parametersets, idfn=None, ids=None, config=None):
|
||||
def idmaker(argnames, parametersets, idfn=None, ids=None, config=None, item=None):
|
||||
ids = [
|
||||
_idvalset(valindex, parameterset, argnames, idfn, ids, config)
|
||||
_idvalset(valindex, parameterset, argnames, idfn, ids, config=config, item=item)
|
||||
for valindex, parameterset in enumerate(parametersets)
|
||||
]
|
||||
if len(set(ids)) != len(ids):
|
||||
@@ -1429,7 +1413,7 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr):
|
||||
@property
|
||||
def function(self):
|
||||
"underlying python 'function' object"
|
||||
return getattr(self.obj, "im_func", self.obj)
|
||||
return getimfunc(self.obj)
|
||||
|
||||
def _getobj(self):
|
||||
name = self.name
|
||||
|
||||
@@ -43,45 +43,10 @@ def deprecated_call(func=None, *args, **kwargs):
|
||||
in which case it will ensure calling ``func(*args, **kwargs)`` produces one of the warnings
|
||||
types above.
|
||||
"""
|
||||
if not func:
|
||||
return _DeprecatedCallContext()
|
||||
else:
|
||||
__tracebackhide__ = True
|
||||
with _DeprecatedCallContext():
|
||||
return func(*args, **kwargs)
|
||||
|
||||
|
||||
class _DeprecatedCallContext(object):
|
||||
"""Implements the logic to capture deprecation warnings as a context manager."""
|
||||
|
||||
def __enter__(self):
|
||||
self._captured_categories = []
|
||||
self._old_warn = warnings.warn
|
||||
self._old_warn_explicit = warnings.warn_explicit
|
||||
warnings.warn_explicit = self._warn_explicit
|
||||
warnings.warn = self._warn
|
||||
|
||||
def _warn_explicit(self, message, category, *args, **kwargs):
|
||||
self._captured_categories.append(category)
|
||||
|
||||
def _warn(self, message, category=None, *args, **kwargs):
|
||||
if isinstance(message, Warning):
|
||||
self._captured_categories.append(message.__class__)
|
||||
else:
|
||||
self._captured_categories.append(category)
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
warnings.warn_explicit = self._old_warn_explicit
|
||||
warnings.warn = self._old_warn
|
||||
|
||||
if exc_type is None:
|
||||
deprecation_categories = (DeprecationWarning, PendingDeprecationWarning)
|
||||
if not any(
|
||||
issubclass(c, deprecation_categories) for c in self._captured_categories
|
||||
):
|
||||
__tracebackhide__ = True
|
||||
msg = "Did not produce DeprecationWarning or PendingDeprecationWarning"
|
||||
raise AssertionError(msg)
|
||||
__tracebackhide__ = True
|
||||
if func is not None:
|
||||
args = (func,) + args
|
||||
return warns((DeprecationWarning, PendingDeprecationWarning), *args, **kwargs)
|
||||
|
||||
|
||||
def warns(expected_warning, *args, **kwargs):
|
||||
@@ -116,6 +81,7 @@ def warns(expected_warning, *args, **kwargs):
|
||||
Failed: DID NOT WARN. No warnings of type ...UserWarning... was emitted...
|
||||
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
match_expr = None
|
||||
if not args:
|
||||
if "match" in kwargs:
|
||||
@@ -183,12 +149,38 @@ class WarningsRecorder(warnings.catch_warnings):
|
||||
raise RuntimeError("Cannot enter %r twice" % self)
|
||||
self._list = super(WarningsRecorder, self).__enter__()
|
||||
warnings.simplefilter("always")
|
||||
# python3 keeps track of a "filter version", when the filters are
|
||||
# updated previously seen warnings can be re-warned. python2 has no
|
||||
# concept of this so we must reset the warnings registry manually.
|
||||
# trivial patching of `warnings.warn` seems to be enough somehow?
|
||||
if six.PY2:
|
||||
|
||||
def warn(*args, **kwargs):
|
||||
kwargs.setdefault("stacklevel", 1)
|
||||
kwargs["stacklevel"] += 1
|
||||
|
||||
# emulate resetting the warn registry
|
||||
f_globals = sys._getframe(kwargs["stacklevel"] - 1).f_globals
|
||||
if "__warningregistry__" in f_globals:
|
||||
orig = f_globals["__warningregistry__"]
|
||||
f_globals["__warningregistry__"] = None
|
||||
try:
|
||||
return self._saved_warn(*args, **kwargs)
|
||||
finally:
|
||||
f_globals["__warningregistry__"] = orig
|
||||
else:
|
||||
return self._saved_warn(*args, **kwargs)
|
||||
|
||||
warnings.warn, self._saved_warn = warn, warnings.warn
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc_info):
|
||||
if not self._entered:
|
||||
__tracebackhide__ = True
|
||||
raise RuntimeError("Cannot exit %r without entering first" % self)
|
||||
# see above where `self._saved_warn` is assigned
|
||||
if six.PY2:
|
||||
warnings.warn = self._saved_warn
|
||||
super(WarningsRecorder, self).__exit__(*exc_info)
|
||||
|
||||
|
||||
@@ -196,7 +188,7 @@ class WarningsChecker(WarningsRecorder):
|
||||
def __init__(self, expected_warning=None, match_expr=None):
|
||||
super(WarningsChecker, self).__init__()
|
||||
|
||||
msg = "exceptions must be old-style classes or " "derived from Warning, not %s"
|
||||
msg = "exceptions must be old-style classes or derived from Warning, not %s"
|
||||
if isinstance(expected_warning, tuple):
|
||||
for exc in expected_warning:
|
||||
if not inspect.isclass(exc):
|
||||
@@ -212,6 +204,8 @@ class WarningsChecker(WarningsRecorder):
|
||||
def __exit__(self, *exc_info):
|
||||
super(WarningsChecker, self).__exit__(*exc_info)
|
||||
|
||||
__tracebackhide__ = True
|
||||
|
||||
# only check if we're not currently handling an exception
|
||||
if all(a is None for a in exc_info):
|
||||
if self.expected_warning is not None:
|
||||
|
||||
@@ -110,7 +110,7 @@ class TestReport(BaseReport):
|
||||
when,
|
||||
sections=(),
|
||||
duration=0,
|
||||
user_properties=(),
|
||||
user_properties=None,
|
||||
**extra
|
||||
):
|
||||
#: normalized collection node id
|
||||
@@ -136,7 +136,7 @@ class TestReport(BaseReport):
|
||||
|
||||
#: user properties is a list of tuples (name, value) that holds user
|
||||
#: defined properties of the test
|
||||
self.user_properties = user_properties
|
||||
self.user_properties = list(user_properties or [])
|
||||
|
||||
#: list of pairs ``(str, str)`` of extra information which needs to
|
||||
#: marshallable. Used by pytest to add captured text
|
||||
|
||||
@@ -31,8 +31,9 @@ def pytest_configure(config):
|
||||
config.pluginmanager.register(config._resultlog)
|
||||
|
||||
from _pytest.deprecated import RESULT_LOG
|
||||
from _pytest.warnings import _issue_config_warning
|
||||
|
||||
config.warn("C1", RESULT_LOG)
|
||||
_issue_config_warning(RESULT_LOG, config)
|
||||
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
|
||||
@@ -30,6 +30,7 @@ def pytest_addoption(parser):
|
||||
|
||||
def pytest_terminal_summary(terminalreporter):
|
||||
durations = terminalreporter.config.option.durations
|
||||
verbose = terminalreporter.config.getvalue("verbose")
|
||||
if durations is None:
|
||||
return
|
||||
tr = terminalreporter
|
||||
@@ -49,6 +50,10 @@ def pytest_terminal_summary(terminalreporter):
|
||||
dlist = dlist[:durations]
|
||||
|
||||
for rep in dlist:
|
||||
if verbose < 2 and rep.duration < 0.005:
|
||||
tr.write_line("")
|
||||
tr.write_line("(0.00 durations hidden. Use -vv to show these durations.)")
|
||||
break
|
||||
nodeid = rep.nodeid.replace("::()::", "::")
|
||||
tr.write_line("%02.2fs %-8s %s" % (rep.duration, rep.when, nodeid))
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import platform
|
||||
import sys
|
||||
import time
|
||||
|
||||
import attr
|
||||
import pluggy
|
||||
import py
|
||||
import six
|
||||
@@ -184,23 +185,23 @@ def pytest_report_teststatus(report):
|
||||
return report.outcome, letter, report.outcome.upper()
|
||||
|
||||
|
||||
@attr.s
|
||||
class WarningReport(object):
|
||||
"""
|
||||
Simple structure to hold warnings information captured by ``pytest_logwarning``.
|
||||
Simple structure to hold warnings information captured by ``pytest_logwarning`` and ``pytest_warning_captured``.
|
||||
|
||||
:ivar str message: user friendly message about the warning
|
||||
:ivar str|None nodeid: node id that generated the warning (see ``get_location``).
|
||||
:ivar tuple|py.path.local fslocation:
|
||||
file system location of the source of the warning (see ``get_location``).
|
||||
|
||||
:ivar bool legacy: if this warning report was generated from the deprecated ``pytest_logwarning`` hook.
|
||||
"""
|
||||
|
||||
def __init__(self, code, message, nodeid=None, fslocation=None):
|
||||
"""
|
||||
:param code: unused
|
||||
:param str message: user friendly message about the warning
|
||||
:param str|None nodeid: node id that generated the warning (see ``get_location``).
|
||||
:param tuple|py.path.local fslocation:
|
||||
file system location of the source of the warning (see ``get_location``).
|
||||
"""
|
||||
self.code = code
|
||||
self.message = message
|
||||
self.nodeid = nodeid
|
||||
self.fslocation = fslocation
|
||||
message = attr.ib()
|
||||
nodeid = attr.ib(default=None)
|
||||
fslocation = attr.ib(default=None)
|
||||
legacy = attr.ib(default=False)
|
||||
|
||||
def get_location(self, config):
|
||||
"""
|
||||
@@ -213,6 +214,8 @@ class WarningReport(object):
|
||||
if isinstance(self.fslocation, tuple) and len(self.fslocation) >= 2:
|
||||
filename, linenum = self.fslocation[:2]
|
||||
relpath = py.path.local(filename).relto(config.invocation_dir)
|
||||
if not relpath:
|
||||
relpath = str(filename)
|
||||
return "%s:%s" % (relpath, linenum)
|
||||
else:
|
||||
return str(self.fslocation)
|
||||
@@ -254,7 +257,7 @@ class TerminalReporter(object):
|
||||
# do not show progress if we are showing fixture setup/teardown
|
||||
if self.config.getoption("setupshow"):
|
||||
return False
|
||||
return self.config.getini("console_output_style") == "progress"
|
||||
return self.config.getini("console_output_style") in ("progress", "count")
|
||||
|
||||
def hasopt(self, char):
|
||||
char = {"xfailed": "x", "skipped": "s"}.get(char, char)
|
||||
@@ -327,13 +330,27 @@ class TerminalReporter(object):
|
||||
self.write_line("INTERNALERROR> " + line)
|
||||
return 1
|
||||
|
||||
def pytest_logwarning(self, code, fslocation, message, nodeid):
|
||||
def pytest_logwarning(self, fslocation, message, nodeid):
|
||||
warnings = self.stats.setdefault("warnings", [])
|
||||
warning = WarningReport(
|
||||
code=code, fslocation=fslocation, message=message, nodeid=nodeid
|
||||
fslocation=fslocation, message=message, nodeid=nodeid, legacy=True
|
||||
)
|
||||
warnings.append(warning)
|
||||
|
||||
def pytest_warning_captured(self, warning_message, item):
|
||||
# from _pytest.nodes import get_fslocation_from_item
|
||||
from _pytest.warnings import warning_record_to_str
|
||||
|
||||
warnings = self.stats.setdefault("warnings", [])
|
||||
fslocation = warning_message.filename, warning_message.lineno
|
||||
message = warning_record_to_str(warning_message)
|
||||
|
||||
nodeid = item.nodeid if item is not None else ""
|
||||
warning_report = WarningReport(
|
||||
fslocation=fslocation, message=message, nodeid=nodeid
|
||||
)
|
||||
warnings.append(warning_report)
|
||||
|
||||
def pytest_plugin_registered(self, plugin):
|
||||
if self.config.option.traceconfig:
|
||||
msg = "PLUGIN registered: %s" % (plugin,)
|
||||
@@ -404,6 +421,12 @@ class TerminalReporter(object):
|
||||
self.currentfspath = -2
|
||||
|
||||
def pytest_runtest_logfinish(self, nodeid):
|
||||
if self.config.getini("console_output_style") == "count":
|
||||
num_tests = self._session.testscollected
|
||||
progress_length = len(" [{}/{}]".format(str(num_tests), str(num_tests)))
|
||||
else:
|
||||
progress_length = len(" [100%]")
|
||||
|
||||
if self.verbosity <= 0 and self._show_progress_info:
|
||||
self._progress_nodeids_reported.add(nodeid)
|
||||
last_item = (
|
||||
@@ -412,31 +435,43 @@ class TerminalReporter(object):
|
||||
if last_item:
|
||||
self._write_progress_information_filling_space()
|
||||
else:
|
||||
past_edge = (
|
||||
self._tw.chars_on_current_line + self._PROGRESS_LENGTH + 1
|
||||
>= self._screen_width
|
||||
)
|
||||
w = self._width_of_current_line
|
||||
past_edge = w + progress_length + 1 >= self._screen_width
|
||||
if past_edge:
|
||||
msg = self._get_progress_information_message()
|
||||
self._tw.write(msg + "\n", cyan=True)
|
||||
|
||||
_PROGRESS_LENGTH = len(" [100%]")
|
||||
|
||||
def _get_progress_information_message(self):
|
||||
if self.config.getoption("capture") == "no":
|
||||
return ""
|
||||
collected = self._session.testscollected
|
||||
if collected:
|
||||
progress = len(self._progress_nodeids_reported) * 100 // collected
|
||||
return " [{:3d}%]".format(progress)
|
||||
return " [100%]"
|
||||
if self.config.getini("console_output_style") == "count":
|
||||
if collected:
|
||||
progress = self._progress_nodeids_reported
|
||||
counter_format = "{{:{}d}}".format(len(str(collected)))
|
||||
format_string = " [{}/{{}}]".format(counter_format)
|
||||
return format_string.format(len(progress), collected)
|
||||
return " [ {} / {} ]".format(collected, collected)
|
||||
else:
|
||||
if collected:
|
||||
progress = len(self._progress_nodeids_reported) * 100 // collected
|
||||
return " [{:3d}%]".format(progress)
|
||||
return " [100%]"
|
||||
|
||||
def _write_progress_information_filling_space(self):
|
||||
msg = self._get_progress_information_message()
|
||||
fill = " " * (
|
||||
self._tw.fullwidth - self._tw.chars_on_current_line - len(msg) - 1
|
||||
)
|
||||
self.write(fill + msg, cyan=True)
|
||||
w = self._width_of_current_line
|
||||
fill = self._tw.fullwidth - w - 1
|
||||
self.write(msg.rjust(fill), cyan=True)
|
||||
|
||||
@property
|
||||
def _width_of_current_line(self):
|
||||
"""Return the width of current line, using the superior implementation of py-1.6 when available"""
|
||||
try:
|
||||
return self._tw.width_of_current_line
|
||||
except AttributeError:
|
||||
# py < 1.6.0
|
||||
return self._tw.chars_on_current_line
|
||||
|
||||
def pytest_collection(self):
|
||||
if not self.isatty and self.config.option.verbose >= 1:
|
||||
@@ -641,7 +676,9 @@ class TerminalReporter(object):
|
||||
|
||||
if fspath:
|
||||
res = mkrel(nodeid).replace("::()", "") # parens-normalization
|
||||
if nodeid.split("::")[0] != fspath.replace("\\", nodes.SEP):
|
||||
if self.verbosity >= 2 and nodeid.split("::")[0] != fspath.replace(
|
||||
"\\", nodes.SEP
|
||||
):
|
||||
res += " <- " + self.startdir.bestrelpath(fspath)
|
||||
else:
|
||||
res = "[location]"
|
||||
@@ -685,11 +722,20 @@ class TerminalReporter(object):
|
||||
|
||||
self.write_sep("=", "warnings summary", yellow=True, bold=False)
|
||||
for location, warning_records in grouped:
|
||||
self._tw.line(str(location) if location else "<undetermined location>")
|
||||
# legacy warnings show their location explicitly, while standard warnings look better without
|
||||
# it because the location is already formatted into the message
|
||||
warning_records = list(warning_records)
|
||||
is_legacy = warning_records[0].legacy
|
||||
if location and is_legacy:
|
||||
self._tw.line(str(location))
|
||||
for w in warning_records:
|
||||
lines = w.message.splitlines()
|
||||
indented = "\n".join(" " + x for x in lines)
|
||||
self._tw.line(indented)
|
||||
if is_legacy:
|
||||
lines = w.message.splitlines()
|
||||
indented = "\n".join(" " + x for x in lines)
|
||||
message = indented.rstrip()
|
||||
else:
|
||||
message = w.message.rstrip()
|
||||
self._tw.line(message)
|
||||
self._tw.line()
|
||||
self._tw.line("-- Docs: https://docs.pytest.org/en/latest/warnings.html")
|
||||
|
||||
@@ -701,9 +747,10 @@ class TerminalReporter(object):
|
||||
return
|
||||
self.write_sep("=", "PASSES")
|
||||
for rep in reports:
|
||||
msg = self._getfailureheadline(rep)
|
||||
self.write_sep("_", msg)
|
||||
self._outrep_summary(rep)
|
||||
if rep.sections:
|
||||
msg = self._getfailureheadline(rep)
|
||||
self.write_sep("_", msg)
|
||||
self._outrep_summary(rep)
|
||||
|
||||
def print_teardown_sections(self, rep):
|
||||
showcapture = self.config.option.showcapture
|
||||
@@ -790,9 +837,7 @@ def repr_pythonversion(v=None):
|
||||
|
||||
|
||||
def build_summary_stats_line(stats):
|
||||
keys = (
|
||||
"failed passed skipped deselected " "xfailed xpassed warnings error"
|
||||
).split()
|
||||
keys = ("failed passed skipped deselected xfailed xpassed warnings error").split()
|
||||
unknown_key_seen = False
|
||||
for key in stats.keys():
|
||||
if key not in keys:
|
||||
|
||||
@@ -1,22 +1,86 @@
|
||||
""" support for providing temporary directories to test functions. """
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
import pytest
|
||||
import py
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
import attr
|
||||
import tempfile
|
||||
import warnings
|
||||
|
||||
from .pathlib import (
|
||||
Path,
|
||||
make_numbered_dir,
|
||||
make_numbered_dir_with_cleanup,
|
||||
ensure_reset_dir,
|
||||
LOCK_TIMEOUT,
|
||||
)
|
||||
|
||||
|
||||
class TempdirFactory(object):
|
||||
@attr.s
|
||||
class TempPathFactory(object):
|
||||
"""Factory for temporary directories under the common base temp directory.
|
||||
|
||||
The base directory can be configured using the ``--basetemp`` option.
|
||||
The base directory can be configured using the ``--basetemp`` option."""
|
||||
|
||||
_given_basetemp = attr.ib()
|
||||
_trace = attr.ib()
|
||||
_basetemp = attr.ib(default=None)
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config):
|
||||
"""
|
||||
:param config: a pytest configuration
|
||||
"""
|
||||
return cls(
|
||||
given_basetemp=config.option.basetemp, trace=config.trace.get("tmpdir")
|
||||
)
|
||||
|
||||
def mktemp(self, basename, numbered=True):
|
||||
"""makes a temporary directory managed by the factory"""
|
||||
if not numbered:
|
||||
p = self.getbasetemp().joinpath(basename)
|
||||
p.mkdir()
|
||||
else:
|
||||
p = make_numbered_dir(root=self.getbasetemp(), prefix=basename)
|
||||
self._trace("mktemp", p)
|
||||
return p
|
||||
|
||||
def getbasetemp(self):
|
||||
""" return base temporary directory. """
|
||||
if self._basetemp is None:
|
||||
if self._given_basetemp is not None:
|
||||
basetemp = Path(self._given_basetemp)
|
||||
ensure_reset_dir(basetemp)
|
||||
else:
|
||||
from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT")
|
||||
temproot = Path(from_env or tempfile.gettempdir())
|
||||
user = get_user() or "unknown"
|
||||
# use a sub-directory in the temproot to speed-up
|
||||
# make_numbered_dir() call
|
||||
rootdir = temproot.joinpath("pytest-of-{}".format(user))
|
||||
rootdir.mkdir(exist_ok=True)
|
||||
basetemp = make_numbered_dir_with_cleanup(
|
||||
prefix="pytest-", root=rootdir, keep=3, lock_timeout=LOCK_TIMEOUT
|
||||
)
|
||||
assert basetemp is not None
|
||||
self._basetemp = t = basetemp
|
||||
self._trace("new basetemp", t)
|
||||
return t
|
||||
else:
|
||||
return self._basetemp
|
||||
|
||||
|
||||
@attr.s
|
||||
class TempdirFactory(object):
|
||||
"""
|
||||
backward comptibility wrapper that implements
|
||||
:class:``py.path.local`` for :class:``TempPathFactory``
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.trace = config.trace.get("tmpdir")
|
||||
_tmppath_factory = attr.ib()
|
||||
|
||||
def ensuretemp(self, string, dir=1):
|
||||
""" (deprecated) return temporary directory path with
|
||||
@@ -26,6 +90,9 @@ class TempdirFactory(object):
|
||||
and is guaranteed to be empty.
|
||||
"""
|
||||
# py.log._apiwarn(">1.1", "use tmpdir function argument")
|
||||
from .deprecated import PYTEST_ENSURETEMP
|
||||
|
||||
warnings.warn(PYTEST_ENSURETEMP, stacklevel=2)
|
||||
return self.getbasetemp().ensure(string, dir=dir)
|
||||
|
||||
def mktemp(self, basename, numbered=True):
|
||||
@@ -33,46 +100,11 @@ class TempdirFactory(object):
|
||||
If ``numbered``, ensure the directory is unique by adding a number
|
||||
prefix greater than any existing one.
|
||||
"""
|
||||
basetemp = self.getbasetemp()
|
||||
if not numbered:
|
||||
p = basetemp.mkdir(basename)
|
||||
else:
|
||||
p = py.path.local.make_numbered_dir(
|
||||
prefix=basename, keep=0, rootdir=basetemp, lock_timeout=None
|
||||
)
|
||||
self.trace("mktemp", p)
|
||||
return p
|
||||
return py.path.local(self._tmppath_factory.mktemp(basename, numbered).resolve())
|
||||
|
||||
def getbasetemp(self):
|
||||
""" return base temporary directory. """
|
||||
try:
|
||||
return self._basetemp
|
||||
except AttributeError:
|
||||
basetemp = self.config.option.basetemp
|
||||
if basetemp:
|
||||
basetemp = py.path.local(basetemp)
|
||||
if basetemp.check():
|
||||
basetemp.remove()
|
||||
basetemp.mkdir()
|
||||
else:
|
||||
temproot = py.path.local.get_temproot()
|
||||
user = get_user()
|
||||
if user:
|
||||
# use a sub-directory in the temproot to speed-up
|
||||
# make_numbered_dir() call
|
||||
rootdir = temproot.join("pytest-of-%s" % user)
|
||||
else:
|
||||
rootdir = temproot
|
||||
rootdir.ensure(dir=1)
|
||||
basetemp = py.path.local.make_numbered_dir(
|
||||
prefix="pytest-", rootdir=rootdir
|
||||
)
|
||||
self._basetemp = t = basetemp.realpath()
|
||||
self.trace("new basetemp", t)
|
||||
return t
|
||||
|
||||
def finish(self):
|
||||
self.trace("finish")
|
||||
"""backward compat wrapper for ``_tmppath_factory.getbasetemp``"""
|
||||
return py.path.local(self._tmppath_factory.getbasetemp().resolve())
|
||||
|
||||
|
||||
def get_user():
|
||||
@@ -87,10 +119,6 @@ def get_user():
|
||||
return None
|
||||
|
||||
|
||||
# backward compatibility
|
||||
TempdirHandler = TempdirFactory
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
"""Create a TempdirFactory and attach it to the config object.
|
||||
|
||||
@@ -99,19 +127,36 @@ def pytest_configure(config):
|
||||
to the tmpdir_factory session fixture.
|
||||
"""
|
||||
mp = MonkeyPatch()
|
||||
t = TempdirFactory(config)
|
||||
config._cleanup.extend([mp.undo, t.finish])
|
||||
tmppath_handler = TempPathFactory.from_config(config)
|
||||
t = TempdirFactory(tmppath_handler)
|
||||
config._cleanup.append(mp.undo)
|
||||
mp.setattr(config, "_tmp_path_factory", tmppath_handler, raising=False)
|
||||
mp.setattr(config, "_tmpdirhandler", t, raising=False)
|
||||
mp.setattr(pytest, "ensuretemp", t.ensuretemp, raising=False)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def tmpdir_factory(request):
|
||||
"""Return a TempdirFactory instance for the test session.
|
||||
"""Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session.
|
||||
"""
|
||||
return request.config._tmpdirhandler
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def tmp_path_factory(request):
|
||||
"""Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session.
|
||||
"""
|
||||
return request.config._tmp_path_factory
|
||||
|
||||
|
||||
def _mk_tmp(request, factory):
|
||||
name = request.node.name
|
||||
name = re.sub(r"[\W]", "_", name)
|
||||
MAXVAL = 30
|
||||
name = name[:MAXVAL]
|
||||
return factory.mktemp(name, numbered=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tmpdir(request, tmpdir_factory):
|
||||
"""Return a temporary directory path object
|
||||
@@ -122,10 +167,20 @@ def tmpdir(request, tmpdir_factory):
|
||||
|
||||
.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
|
||||
"""
|
||||
name = request.node.name
|
||||
name = re.sub(r"[\W]", "_", name)
|
||||
MAXVAL = 30
|
||||
if len(name) > MAXVAL:
|
||||
name = name[:MAXVAL]
|
||||
x = tmpdir_factory.mktemp(name, numbered=True)
|
||||
return x
|
||||
return _mk_tmp(request, tmpdir_factory)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tmp_path(request, tmp_path_factory):
|
||||
"""Return a temporary directory path object
|
||||
which is unique to each test function invocation,
|
||||
created as a sub directory of the base temporary
|
||||
directory. The returned object is a :class:`pathlib.Path`
|
||||
object.
|
||||
|
||||
.. note::
|
||||
|
||||
in python < 3.6 this is a pathlib2.Path
|
||||
"""
|
||||
|
||||
return _mk_tmp(request, tmp_path_factory)
|
||||
|
||||
@@ -9,6 +9,7 @@ import _pytest._code
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.outcomes import fail, skip, xfail
|
||||
from _pytest.python import transfer_markers, Class, Module, Function
|
||||
from _pytest.compat import getimfunc
|
||||
|
||||
|
||||
def pytest_pycollect_makeitem(collector, name, obj):
|
||||
@@ -53,7 +54,7 @@ class UnitTestCase(Class):
|
||||
x = getattr(self.obj, name)
|
||||
if not getattr(x, "__test__", True):
|
||||
continue
|
||||
funcobj = getattr(x, "im_func", x)
|
||||
funcobj = getimfunc(x)
|
||||
transfer_markers(funcobj, cls, module)
|
||||
yield TestCaseFunction(name, parent=self, callobj=funcobj)
|
||||
foundsomething = True
|
||||
|
||||
60
src/_pytest/warning_types.py
Normal file
60
src/_pytest/warning_types.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import attr
|
||||
|
||||
|
||||
class PytestWarning(UserWarning):
|
||||
"""
|
||||
Bases: :class:`UserWarning`.
|
||||
|
||||
Base class for all warnings emitted by pytest.
|
||||
"""
|
||||
|
||||
|
||||
class PytestDeprecationWarning(PytestWarning, DeprecationWarning):
|
||||
"""
|
||||
Bases: :class:`pytest.PytestWarning`, :class:`DeprecationWarning`.
|
||||
|
||||
Warning class for features that will be removed in a future version.
|
||||
"""
|
||||
|
||||
|
||||
class RemovedInPytest4Warning(PytestDeprecationWarning):
|
||||
"""
|
||||
Bases: :class:`pytest.PytestDeprecationWarning`.
|
||||
|
||||
Warning class for features scheduled to be removed in pytest 4.0.
|
||||
"""
|
||||
|
||||
|
||||
class PytestExperimentalApiWarning(PytestWarning, FutureWarning):
|
||||
"""
|
||||
Bases: :class:`pytest.PytestWarning`, :class:`FutureWarning`.
|
||||
|
||||
Warning category used to denote experiments in pytest. Use sparingly as the API might change or even be
|
||||
removed completely in future version
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def simple(cls, apiname):
|
||||
return cls(
|
||||
"{apiname} is an experimental api that may change over time".format(
|
||||
apiname=apiname
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@attr.s
|
||||
class UnformattedWarning(object):
|
||||
"""Used to hold warnings that need to format their message at runtime, as opposed to a direct message.
|
||||
|
||||
Using this class avoids to keep all the warning types and messages in this module, avoiding misuse.
|
||||
"""
|
||||
|
||||
category = attr.ib()
|
||||
template = attr.ib()
|
||||
|
||||
def format(self, **kwargs):
|
||||
"""Returns an instance of the warning category, formatted with given kwargs"""
|
||||
return self.category(self.template.format(**kwargs))
|
||||
|
||||
|
||||
PYTESTER_COPY_EXAMPLE = PytestExperimentalApiWarning.simple("testdir.copy_example")
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import sys
|
||||
import warnings
|
||||
from contextlib import contextmanager
|
||||
|
||||
@@ -58,62 +59,114 @@ def pytest_configure(config):
|
||||
|
||||
|
||||
@contextmanager
|
||||
def catch_warnings_for_item(item):
|
||||
def catch_warnings_for_item(config, ihook, when, item):
|
||||
"""
|
||||
catches the warnings generated during setup/call/teardown execution
|
||||
of the given item and after it is done posts them as warnings to this
|
||||
item.
|
||||
"""
|
||||
args = item.config.getoption("pythonwarnings") or []
|
||||
inifilters = item.config.getini("filterwarnings")
|
||||
with warnings.catch_warnings(record=True) as log:
|
||||
for arg in args:
|
||||
warnings._setoption(arg)
|
||||
Context manager that catches warnings generated in the contained execution block.
|
||||
|
||||
``item`` can be None if we are not in the context of an item execution.
|
||||
|
||||
Each warning captured triggers the ``pytest_warning_captured`` hook.
|
||||
"""
|
||||
cmdline_filters = config.getoption("pythonwarnings") or []
|
||||
inifilters = config.getini("filterwarnings")
|
||||
with warnings.catch_warnings(record=True) as log:
|
||||
|
||||
if not sys.warnoptions:
|
||||
# if user is not explicitly configuring warning filters, show deprecation warnings by default (#2908)
|
||||
warnings.filterwarnings("always", category=DeprecationWarning)
|
||||
warnings.filterwarnings("always", category=PendingDeprecationWarning)
|
||||
|
||||
# filters should have this precedence: mark, cmdline options, ini
|
||||
# filters should be applied in the inverse order of precedence
|
||||
for arg in inifilters:
|
||||
_setoption(warnings, arg)
|
||||
|
||||
for mark in item.iter_markers(name="filterwarnings"):
|
||||
for arg in mark.args:
|
||||
warnings._setoption(arg)
|
||||
for arg in cmdline_filters:
|
||||
warnings._setoption(arg)
|
||||
|
||||
if item is not None:
|
||||
for mark in item.iter_markers(name="filterwarnings"):
|
||||
for arg in mark.args:
|
||||
_setoption(warnings, arg)
|
||||
|
||||
yield
|
||||
|
||||
for warning in log:
|
||||
warn_msg = warning.message
|
||||
unicode_warning = False
|
||||
|
||||
if compat._PY2 and any(
|
||||
isinstance(m, compat.UNICODE_TYPES) for m in warn_msg.args
|
||||
):
|
||||
new_args = []
|
||||
for m in warn_msg.args:
|
||||
new_args.append(
|
||||
compat.ascii_escaped(m)
|
||||
if isinstance(m, compat.UNICODE_TYPES)
|
||||
else m
|
||||
)
|
||||
unicode_warning = list(warn_msg.args) != new_args
|
||||
warn_msg.args = new_args
|
||||
|
||||
msg = warnings.formatwarning(
|
||||
warn_msg,
|
||||
warning.category,
|
||||
warning.filename,
|
||||
warning.lineno,
|
||||
warning.line,
|
||||
for warning_message in log:
|
||||
ihook.pytest_warning_captured.call_historic(
|
||||
kwargs=dict(warning_message=warning_message, when=when, item=item)
|
||||
)
|
||||
item.warn("unused", msg)
|
||||
|
||||
if unicode_warning:
|
||||
warnings.warn(
|
||||
"Warning is using unicode non convertible to ascii, "
|
||||
"converting to a safe representation:\n %s" % msg,
|
||||
UnicodeWarning,
|
||||
)
|
||||
|
||||
def warning_record_to_str(warning_message):
|
||||
"""Convert a warnings.WarningMessage to a string, taking in account a lot of unicode shenaningans in Python 2.
|
||||
|
||||
When Python 2 support is dropped this function can be greatly simplified.
|
||||
"""
|
||||
warn_msg = warning_message.message
|
||||
unicode_warning = False
|
||||
if compat._PY2 and any(isinstance(m, compat.UNICODE_TYPES) for m in warn_msg.args):
|
||||
new_args = []
|
||||
for m in warn_msg.args:
|
||||
new_args.append(
|
||||
compat.ascii_escaped(m) if isinstance(m, compat.UNICODE_TYPES) else m
|
||||
)
|
||||
unicode_warning = list(warn_msg.args) != new_args
|
||||
warn_msg.args = new_args
|
||||
|
||||
msg = warnings.formatwarning(
|
||||
warn_msg,
|
||||
warning_message.category,
|
||||
warning_message.filename,
|
||||
warning_message.lineno,
|
||||
warning_message.line,
|
||||
)
|
||||
if unicode_warning:
|
||||
warnings.warn(
|
||||
"Warning is using unicode non convertible to ascii, "
|
||||
"converting to a safe representation:\n {!r}".format(compat.safe_str(msg)),
|
||||
UnicodeWarning,
|
||||
)
|
||||
return msg
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||
def pytest_runtest_protocol(item):
|
||||
with catch_warnings_for_item(
|
||||
config=item.config, ihook=item.ihook, when="runtest", item=item
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||
def pytest_collection(session):
|
||||
config = session.config
|
||||
with catch_warnings_for_item(
|
||||
config=config, ihook=config.hook, when="collect", item=None
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_protocol(item):
|
||||
with catch_warnings_for_item(item):
|
||||
def pytest_terminal_summary(terminalreporter):
|
||||
config = terminalreporter.config
|
||||
with catch_warnings_for_item(
|
||||
config=config, ihook=config.hook, when="config", item=None
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
def _issue_config_warning(warning, config):
|
||||
"""
|
||||
This function should be used instead of calling ``warnings.warn`` directly when we are in the "configure" stage:
|
||||
at this point the actual options might not have been set, so we manually trigger the pytest_warning_captured
|
||||
hook so we can display this warnings in the terminal. This is a hack until we can sort out #2891.
|
||||
|
||||
:param warning: the warning instance.
|
||||
:param config:
|
||||
"""
|
||||
with warnings.catch_warnings(record=True) as records:
|
||||
warnings.simplefilter("always", type(warning))
|
||||
warnings.warn(warning, stacklevel=2)
|
||||
config.hook.pytest_warning_captured.call_historic(
|
||||
kwargs=dict(warning_message=records[0], when="config", item=None)
|
||||
)
|
||||
|
||||
@@ -19,45 +19,54 @@ from _pytest.main import Session
|
||||
from _pytest.nodes import Item, Collector, File
|
||||
from _pytest.fixtures import fillfixtures as _fillfuncargs
|
||||
from _pytest.python import Package, Module, Class, Instance, Function, Generator
|
||||
|
||||
from _pytest.python_api import approx, raises
|
||||
from _pytest.warning_types import (
|
||||
PytestWarning,
|
||||
PytestDeprecationWarning,
|
||||
RemovedInPytest4Warning,
|
||||
PytestExperimentalApiWarning,
|
||||
)
|
||||
|
||||
set_trace = __pytestPDB.set_trace
|
||||
|
||||
__all__ = [
|
||||
"main",
|
||||
"UsageError",
|
||||
"cmdline",
|
||||
"hookspec",
|
||||
"hookimpl",
|
||||
"__version__",
|
||||
"register_assert_rewrite",
|
||||
"freeze_includes",
|
||||
"set_trace",
|
||||
"warns",
|
||||
"deprecated_call",
|
||||
"fixture",
|
||||
"yield_fixture",
|
||||
"fail",
|
||||
"skip",
|
||||
"xfail",
|
||||
"importorskip",
|
||||
"exit",
|
||||
"mark",
|
||||
"param",
|
||||
"approx",
|
||||
"_fillfuncargs",
|
||||
"Item",
|
||||
"File",
|
||||
"Collector",
|
||||
"Package",
|
||||
"Session",
|
||||
"Module",
|
||||
"approx",
|
||||
"Class",
|
||||
"Instance",
|
||||
"cmdline",
|
||||
"Collector",
|
||||
"deprecated_call",
|
||||
"exit",
|
||||
"fail",
|
||||
"File",
|
||||
"fixture",
|
||||
"freeze_includes",
|
||||
"Function",
|
||||
"Generator",
|
||||
"hookimpl",
|
||||
"hookspec",
|
||||
"importorskip",
|
||||
"Instance",
|
||||
"Item",
|
||||
"main",
|
||||
"mark",
|
||||
"Module",
|
||||
"Package",
|
||||
"param",
|
||||
"PytestDeprecationWarning",
|
||||
"PytestExperimentalApiWarning",
|
||||
"PytestWarning",
|
||||
"raises",
|
||||
"register_assert_rewrite",
|
||||
"RemovedInPytest4Warning",
|
||||
"Session",
|
||||
"set_trace",
|
||||
"skip",
|
||||
"UsageError",
|
||||
"warns",
|
||||
"xfail",
|
||||
"yield_fixture",
|
||||
]
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -12,6 +12,13 @@ import pytest
|
||||
from _pytest.main import EXIT_NOTESTSCOLLECTED, EXIT_USAGEERROR
|
||||
|
||||
|
||||
def prepend_pythonpath(*dirs):
|
||||
cur = os.getenv("PYTHONPATH")
|
||||
if cur:
|
||||
dirs += (cur,)
|
||||
return os.pathsep.join(str(p) for p in dirs)
|
||||
|
||||
|
||||
class TestGeneralUsage(object):
|
||||
def test_config_error(self, testdir):
|
||||
testdir.copy_example("conftest_usageerror/conftest.py")
|
||||
@@ -133,9 +140,16 @@ class TestGeneralUsage(object):
|
||||
assert result.ret
|
||||
result.stderr.fnmatch_lines(["*ERROR: not found:*{}".format(p2.basename)])
|
||||
|
||||
def test_issue486_better_reporting_on_conftest_load_failure(self, testdir):
|
||||
def test_better_reporting_on_conftest_load_failure(self, testdir, request):
|
||||
"""Show a user-friendly traceback on conftest import failures (#486, #3332)"""
|
||||
testdir.makepyfile("")
|
||||
testdir.makeconftest("import qwerty")
|
||||
testdir.makeconftest(
|
||||
"""
|
||||
def foo():
|
||||
import qwerty
|
||||
foo()
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest("--help")
|
||||
result.stdout.fnmatch_lines(
|
||||
"""
|
||||
@@ -144,10 +158,23 @@ class TestGeneralUsage(object):
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
dirname = request.node.name + "0"
|
||||
exc_name = (
|
||||
"ModuleNotFoundError" if sys.version_info >= (3, 6) else "ImportError"
|
||||
)
|
||||
result.stderr.fnmatch_lines(
|
||||
"""
|
||||
*ERROR*could not load*conftest.py*
|
||||
"""
|
||||
[
|
||||
"ImportError while loading conftest '*{sep}{dirname}{sep}conftest.py'.".format(
|
||||
dirname=dirname, sep=os.sep
|
||||
),
|
||||
"conftest.py:3: in <module>",
|
||||
" foo()",
|
||||
"conftest.py:2: in foo",
|
||||
" import qwerty",
|
||||
"E {}: No module named {q}qwerty{q}".format(
|
||||
exc_name, q="'" if six.PY3 else ""
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
def test_early_skip(self, testdir):
|
||||
@@ -570,14 +597,8 @@ class TestInvocationVariants(object):
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines(["*1 passed*"])
|
||||
|
||||
def join_pythonpath(what):
|
||||
cur = os.environ.get("PYTHONPATH")
|
||||
if cur:
|
||||
return str(what) + os.pathsep + cur
|
||||
return what
|
||||
|
||||
empty_package = testdir.mkpydir("empty_package")
|
||||
monkeypatch.setenv("PYTHONPATH", join_pythonpath(empty_package))
|
||||
monkeypatch.setenv("PYTHONPATH", str(empty_package), prepend=os.pathsep)
|
||||
# the path which is not a package raises a warning on pypy;
|
||||
# no idea why only pypy and not normal python warn about it here
|
||||
with warnings.catch_warnings():
|
||||
@@ -586,7 +607,7 @@ class TestInvocationVariants(object):
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines(["*2 passed*"])
|
||||
|
||||
monkeypatch.setenv("PYTHONPATH", join_pythonpath(testdir))
|
||||
monkeypatch.setenv("PYTHONPATH", str(testdir), prepend=os.pathsep)
|
||||
result = testdir.runpytest("--pyargs", "tpkg.test_missing", syspathinsert=True)
|
||||
assert result.ret != 0
|
||||
result.stderr.fnmatch_lines(["*not*found*test_missing*"])
|
||||
@@ -608,7 +629,7 @@ class TestInvocationVariants(object):
|
||||
lib = ns.mkdir(dirname)
|
||||
lib.ensure("__init__.py")
|
||||
lib.join("test_{}.py".format(dirname)).write(
|
||||
"def test_{}(): pass\n" "def test_other():pass".format(dirname)
|
||||
"def test_{}(): pass\ndef test_other():pass".format(dirname)
|
||||
)
|
||||
|
||||
# The structure of the test directory is now:
|
||||
@@ -626,18 +647,13 @@ class TestInvocationVariants(object):
|
||||
# ├── __init__.py
|
||||
# └── test_world.py
|
||||
|
||||
def join_pythonpath(*dirs):
|
||||
cur = os.environ.get("PYTHONPATH")
|
||||
if cur:
|
||||
dirs += (cur,)
|
||||
return os.pathsep.join(str(p) for p in dirs)
|
||||
|
||||
monkeypatch.setenv("PYTHONPATH", join_pythonpath(*search_path))
|
||||
# NOTE: the different/reversed ordering is intentional here.
|
||||
monkeypatch.setenv("PYTHONPATH", prepend_pythonpath(*search_path))
|
||||
for p in search_path:
|
||||
monkeypatch.syspath_prepend(p)
|
||||
|
||||
# mixed module and filenames:
|
||||
os.chdir("world")
|
||||
monkeypatch.chdir("world")
|
||||
result = testdir.runpytest("--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world")
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines(
|
||||
@@ -688,8 +704,6 @@ class TestInvocationVariants(object):
|
||||
pytest.skip(six.text_type(e.args[0]))
|
||||
monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False)
|
||||
|
||||
search_path = ["lib", os.path.join("local", "lib")]
|
||||
|
||||
dirname = "lib"
|
||||
d = testdir.mkdir(dirname)
|
||||
foo = d.mkdir("foo")
|
||||
@@ -697,10 +711,10 @@ class TestInvocationVariants(object):
|
||||
lib = foo.mkdir("bar")
|
||||
lib.ensure("__init__.py")
|
||||
lib.join("test_bar.py").write(
|
||||
"def test_bar(): pass\n" "def test_other(a_fixture):pass"
|
||||
"def test_bar(): pass\ndef test_other(a_fixture):pass"
|
||||
)
|
||||
lib.join("conftest.py").write(
|
||||
"import pytest\n" "@pytest.fixture\n" "def a_fixture():pass"
|
||||
"import pytest\n@pytest.fixture\ndef a_fixture():pass"
|
||||
)
|
||||
|
||||
d_local = testdir.mkdir("local")
|
||||
@@ -722,27 +736,33 @@ class TestInvocationVariants(object):
|
||||
# ├── conftest.py
|
||||
# └── test_bar.py
|
||||
|
||||
def join_pythonpath(*dirs):
|
||||
cur = os.getenv("PYTHONPATH")
|
||||
if cur:
|
||||
dirs += (cur,)
|
||||
return os.pathsep.join(str(p) for p in dirs)
|
||||
|
||||
monkeypatch.setenv("PYTHONPATH", join_pythonpath(*search_path))
|
||||
# NOTE: the different/reversed ordering is intentional here.
|
||||
search_path = ["lib", os.path.join("local", "lib")]
|
||||
monkeypatch.setenv("PYTHONPATH", prepend_pythonpath(*search_path))
|
||||
for p in search_path:
|
||||
monkeypatch.syspath_prepend(p)
|
||||
|
||||
# module picked up in symlink-ed directory:
|
||||
# It picks up local/lib/foo/bar (symlink) via sys.path.
|
||||
result = testdir.runpytest("--pyargs", "-v", "foo.bar")
|
||||
testdir.chdir()
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*lib/foo/bar/test_bar.py::test_bar*PASSED*",
|
||||
"*lib/foo/bar/test_bar.py::test_other*PASSED*",
|
||||
"*2 passed*",
|
||||
]
|
||||
)
|
||||
if hasattr(py.path.local, "mksymlinkto"):
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"lib/foo/bar/test_bar.py::test_bar PASSED*",
|
||||
"lib/foo/bar/test_bar.py::test_other PASSED*",
|
||||
"*2 passed*",
|
||||
]
|
||||
)
|
||||
else:
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*lib/foo/bar/test_bar.py::test_bar PASSED*",
|
||||
"*lib/foo/bar/test_bar.py::test_other PASSED*",
|
||||
"*2 passed*",
|
||||
]
|
||||
)
|
||||
|
||||
def test_cmdline_python_package_not_exists(self, testdir):
|
||||
result = testdir.runpytest("--pyargs", "tpkgwhatv")
|
||||
@@ -816,7 +836,10 @@ class TestDurations(object):
|
||||
result = testdir.runpytest("--durations=10")
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines_random(
|
||||
["*durations*", "*call*test_3*", "*call*test_2*", "*call*test_1*"]
|
||||
["*durations*", "*call*test_3*", "*call*test_2*"]
|
||||
)
|
||||
result.stdout.fnmatch_lines(
|
||||
["(0.00 durations hidden. Use -vv to show these durations.)"]
|
||||
)
|
||||
|
||||
def test_calls_show_2(self, testdir):
|
||||
@@ -830,6 +853,18 @@ class TestDurations(object):
|
||||
testdir.makepyfile(self.source)
|
||||
result = testdir.runpytest("--durations=0")
|
||||
assert result.ret == 0
|
||||
for x in "23":
|
||||
for y in ("call",): # 'setup', 'call', 'teardown':
|
||||
for line in result.stdout.lines:
|
||||
if ("test_%s" % x) in line and y in line:
|
||||
break
|
||||
else:
|
||||
raise AssertionError("not found {} {}".format(x, y))
|
||||
|
||||
def test_calls_showall_verbose(self, testdir):
|
||||
testdir.makepyfile(self.source)
|
||||
result = testdir.runpytest("--durations=0", "-vv")
|
||||
assert result.ret == 0
|
||||
for x in "123":
|
||||
for y in ("call",): # 'setup', 'call', 'teardown':
|
||||
for line in result.stdout.lines:
|
||||
@@ -840,9 +875,9 @@ class TestDurations(object):
|
||||
|
||||
def test_with_deselected(self, testdir):
|
||||
testdir.makepyfile(self.source)
|
||||
result = testdir.runpytest("--durations=2", "-k test_1")
|
||||
result = testdir.runpytest("--durations=2", "-k test_2")
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines(["*durations*", "*call*test_1*"])
|
||||
result.stdout.fnmatch_lines(["*durations*", "*call*test_2*"])
|
||||
|
||||
def test_with_failing_collection(self, testdir):
|
||||
testdir.makepyfile(self.source)
|
||||
@@ -862,13 +897,15 @@ class TestDurations(object):
|
||||
|
||||
class TestDurationWithFixture(object):
|
||||
source = """
|
||||
import pytest
|
||||
import time
|
||||
frag = 0.001
|
||||
def setup_function(func):
|
||||
time.sleep(frag * 3)
|
||||
def test_1():
|
||||
time.sleep(frag*2)
|
||||
def test_2():
|
||||
frag = 0.01
|
||||
|
||||
@pytest.fixture
|
||||
def setup_fixt():
|
||||
time.sleep(frag)
|
||||
|
||||
def test_1(setup_fixt):
|
||||
time.sleep(frag)
|
||||
"""
|
||||
|
||||
@@ -1061,3 +1098,8 @@ def test_fixture_mock_integration(testdir):
|
||||
p = testdir.copy_example("acceptance/fixture_mock_integration.py")
|
||||
result = testdir.runpytest(p)
|
||||
result.stdout.fnmatch_lines("*1 passed*")
|
||||
|
||||
|
||||
def test_usage_error_code(testdir):
|
||||
result = testdir.runpytest("-unknown-option-")
|
||||
assert result.ret == EXIT_USAGEERROR
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user