Compare commits
387 Commits
8.1.0.dev0
...
8.3.0.dev0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9eecdc7dae | ||
|
|
79ca819e59 | ||
|
|
835765c9d3 | ||
|
|
7e7503c0b0 | ||
|
|
882c4da2f3 | ||
|
|
2e8fb9f140 | ||
|
|
acf2971f46 | ||
|
|
3c77aec1da | ||
|
|
d217d68cde | ||
|
|
530be28575 | ||
|
|
d208c1d4a5 | ||
|
|
80ca255d42 | ||
|
|
1a332802ff | ||
|
|
93c2cdf6d6 | ||
|
|
4eb8b6d525 | ||
|
|
fafab1dbfd | ||
|
|
5cffef7f07 | ||
|
|
a830a3e98d | ||
|
|
1d322ffb48 | ||
|
|
63d985c6d3 | ||
|
|
3617ef1c79 | ||
|
|
58136c5376 | ||
|
|
042625957a | ||
|
|
6fb474a3eb | ||
|
|
ff806b239e | ||
|
|
ad95d59d61 | ||
|
|
48b6d18834 | ||
|
|
58844247f7 | ||
|
|
b56b294f54 | ||
|
|
6e248818b9 | ||
|
|
0644e467fb | ||
|
|
5f6e825a26 | ||
|
|
089116bdff | ||
|
|
1f001cd105 | ||
|
|
99890636bf | ||
|
|
17fc20af78 | ||
|
|
1ca59dfd6b | ||
|
|
e06c337bd8 | ||
|
|
a02cc0ad1b | ||
|
|
74f05648d5 | ||
|
|
f75dd87eb7 | ||
|
|
5acc3f86ac | ||
|
|
4e3dd21506 | ||
|
|
2e65f4e3ac | ||
|
|
cc588d1a1a | ||
|
|
b63bea9523 | ||
|
|
a10ddad774 | ||
|
|
1188ea2608 | ||
|
|
aa5324560b | ||
|
|
381593ccf0 | ||
|
|
9d6b5f8772 | ||
|
|
44895289c7 | ||
|
|
1125296b53 | ||
|
|
bd9b62161a | ||
|
|
e64efd8653 | ||
|
|
2dd58e827e | ||
|
|
12e061e2e8 | ||
|
|
5d345f6d7f | ||
|
|
2cba2237cd | ||
|
|
e7b048e92d | ||
|
|
4a8cefbf64 | ||
|
|
909759de2d | ||
|
|
85ff5d1b54 | ||
|
|
cfc4cf074e | ||
|
|
aeae91e27d | ||
|
|
3eb16b34be | ||
|
|
c650e3a94f | ||
|
|
470edc5884 | ||
|
|
07a08028de | ||
|
|
42d3960fea | ||
|
|
e7bf216516 | ||
|
|
532782a228 | ||
|
|
bf8b088bbe | ||
|
|
027464975f | ||
|
|
6be5c9d88c | ||
|
|
2607fe8b47 | ||
|
|
2e69f31444 | ||
|
|
70c11582aa | ||
|
|
c203f1615c | ||
|
|
bec0e9caf8 | ||
|
|
2e5da5d2fb | ||
|
|
c0532dda18 | ||
|
|
14437788f0 | ||
|
|
7eaaf370bb | ||
|
|
6c9e107681 | ||
|
|
122b43439c | ||
|
|
a29ea1bc32 | ||
|
|
0a442a9599 | ||
|
|
520ff29c07 | ||
|
|
b777b05c0e | ||
|
|
b90da34a86 | ||
|
|
0dc0360351 | ||
|
|
140c777590 | ||
|
|
774f0c44e6 | ||
|
|
006058f1f9 | ||
|
|
437eb86edd | ||
|
|
9033d4d3ff | ||
|
|
2ccc73be9a | ||
|
|
ff551b7685 | ||
|
|
f5de111357 | ||
|
|
71671f60b5 | ||
|
|
a9d1f55a0f | ||
|
|
83887c066b | ||
|
|
4a8bd5f2e2 | ||
|
|
51595d9000 | ||
|
|
1a5e0eb71d | ||
|
|
221097517b | ||
|
|
dacee1f11d | ||
|
|
303cd0d48a | ||
|
|
86945f9a1f | ||
|
|
03e54712dd | ||
|
|
18c0e5075f | ||
|
|
6ee02a3e6c | ||
|
|
0951cbc6e4 | ||
|
|
37b3401091 | ||
|
|
13558b9f53 | ||
|
|
23bdc643c9 | ||
|
|
71849cc05c | ||
|
|
e410705561 | ||
|
|
00043f7f10 | ||
|
|
f4e10251a4 | ||
|
|
43492f5707 | ||
|
|
82fe28dae4 | ||
|
|
5e2ee7175c | ||
|
|
89ee4493cc | ||
|
|
8248946a55 | ||
|
|
434282e17f | ||
|
|
d6134bc21e | ||
|
|
aac720abc9 | ||
|
|
111c0d910e | ||
|
|
199d4e2b73 | ||
|
|
5746b8e696 | ||
|
|
c85fce39b6 | ||
|
|
067daf9f7d | ||
|
|
5867426455 | ||
|
|
4dea18308b | ||
|
|
7524e60d8f | ||
|
|
dcf01fd39a | ||
|
|
300ceb435e | ||
|
|
887e251abb | ||
|
|
6ed005161d | ||
|
|
31026a2df2 | ||
|
|
f20e32c982 | ||
|
|
1612d4e393 | ||
|
|
5e0d11746c | ||
|
|
b6bf58abe8 | ||
|
|
ff4c3b2873 | ||
|
|
4db5e53709 | ||
|
|
c967d508d6 | ||
|
|
bd45ccd2ca | ||
|
|
c83c1c4bda | ||
|
|
686f9e0720 | ||
|
|
98b008ff6c | ||
|
|
73edefdd64 | ||
|
|
ffd727e9d6 | ||
|
|
b6eb985d55 | ||
|
|
affc652f1c | ||
|
|
7460b1aa31 | ||
|
|
84bd31de64 | ||
|
|
a2a9aa6cde | ||
|
|
c09f74619b | ||
|
|
b510adf9ef | ||
|
|
93cd7ba857 | ||
|
|
8d9b95dcdb | ||
|
|
ad651ddfce | ||
|
|
00d9640abc | ||
|
|
010ce2ab0f | ||
|
|
1640f2e454 | ||
|
|
59e9a58cc9 | ||
|
|
691d8fcafb | ||
|
|
c5c729e27a | ||
|
|
9bea1be215 | ||
|
|
2007cf6296 | ||
|
|
79def57cc6 | ||
|
|
8a410d0ba6 | ||
|
|
a37cff3ca8 | ||
|
|
95f21f96cc | ||
|
|
998fee1679 | ||
|
|
40011b838b | ||
|
|
b37696703b | ||
|
|
abf6a60567 | ||
|
|
52da8dd66c | ||
|
|
cefb3e2277 | ||
|
|
5f241f388b | ||
|
|
aebeb33137 | ||
|
|
9f13d41d7b | ||
|
|
984478109f | ||
|
|
22b541e4eb | ||
|
|
fbe18fc7a9 | ||
|
|
9b838f491f | ||
|
|
6ef0cf150a | ||
|
|
0475b1c925 | ||
|
|
dcf9da92be | ||
|
|
8e56795b77 | ||
|
|
718cd40015 | ||
|
|
288201b8f5 | ||
|
|
c6313a06fb | ||
|
|
5d7a5a9343 | ||
|
|
cbb5c82c39 | ||
|
|
acafd003aa | ||
|
|
46e6fb12c7 | ||
|
|
fe7907d78c | ||
|
|
5bb1363435 | ||
|
|
ca84327f15 | ||
|
|
3b798e5422 | ||
|
|
526b971221 | ||
|
|
23dfb52974 | ||
|
|
7da04c963f | ||
|
|
b922270ce7 | ||
|
|
1180348303 | ||
|
|
7690a0ddf1 | ||
|
|
29a5f94428 | ||
|
|
ea57c40c43 | ||
|
|
a182e10b06 | ||
|
|
d1ee6d154f | ||
|
|
d02618d173 | ||
|
|
9454fc38d3 | ||
|
|
101328aba5 | ||
|
|
42785cca44 | ||
|
|
6c0b6c2f92 | ||
|
|
9cd14b4ffb | ||
|
|
4c894f20a1 | ||
|
|
6e5008f19f | ||
|
|
b9d02c5b53 | ||
|
|
d1095426c1 | ||
|
|
757778f5f6 | ||
|
|
404d31a942 | ||
|
|
0d91539614 | ||
|
|
835e8032f4 | ||
|
|
c76ea72331 | ||
|
|
dab1583ce1 | ||
|
|
411c2cbf9d | ||
|
|
c3583dee0b | ||
|
|
a1d99e4bdd | ||
|
|
aaa9ca7327 | ||
|
|
29094983d7 | ||
|
|
b8e56557c6 | ||
|
|
304ccb4ad7 | ||
|
|
90634a6060 | ||
|
|
ccbae95ad4 | ||
|
|
cd9b241047 | ||
|
|
5c67cb2cf5 | ||
|
|
2fc7926bae | ||
|
|
79de84941a | ||
|
|
0d1f4c63fa | ||
|
|
2d4e27daf2 | ||
|
|
2e50788b6d | ||
|
|
3101c026b9 | ||
|
|
e193a263c7 | ||
|
|
4eb246d4e1 | ||
|
|
41ff3584d7 | ||
|
|
b62d4b3527 | ||
|
|
e7bab63537 | ||
|
|
52fba25ff9 | ||
|
|
fcb818b73c | ||
|
|
7eef4619d5 | ||
|
|
e28f35c296 | ||
|
|
b28bb01c4e | ||
|
|
3ba4095400 | ||
|
|
99e8129ba3 | ||
|
|
20b18f0f9a | ||
|
|
cb5f738858 | ||
|
|
5be64c31cb | ||
|
|
233ab89f13 | ||
|
|
8967c527ff | ||
|
|
180a16a344 | ||
|
|
514376fe29 | ||
|
|
bdfc5c80d8 | ||
|
|
9ef905e7a0 | ||
|
|
4588653b24 | ||
|
|
046f64751b | ||
|
|
4546d5445a | ||
|
|
de161f8791 | ||
|
|
3be2a9d655 | ||
|
|
e885013c6b | ||
|
|
8b54596639 | ||
|
|
899a6cf2ce | ||
|
|
407d984142 | ||
|
|
21bec6cfbe | ||
|
|
cb57bf50b1 | ||
|
|
c0dfc45186 | ||
|
|
5855f65d0f | ||
|
|
14d3707818 | ||
|
|
8853a57532 | ||
|
|
878af85aef | ||
|
|
e7b43b2121 | ||
|
|
d0f427aec4 | ||
|
|
c6da0d20d2 | ||
|
|
a164ed6400 | ||
|
|
eb9013d42c | ||
|
|
ac2cd72e5f | ||
|
|
bca4bb0738 | ||
|
|
2a77f8d88b | ||
|
|
d972957303 | ||
|
|
44bf7a2ec0 | ||
|
|
5ab8972bb5 | ||
|
|
21440521fa | ||
|
|
79ae968131 | ||
|
|
c3fc717ff7 | ||
|
|
d71ef04f11 | ||
|
|
111ad26f71 | ||
|
|
2178ee86d7 | ||
|
|
a6dd90a414 | ||
|
|
676f38d04a | ||
|
|
212c55218b | ||
|
|
34fafe4c6b | ||
|
|
eefc9d47fc | ||
|
|
7fd561e4ba | ||
|
|
da9749cd97 | ||
|
|
ca5bbd0a9f | ||
|
|
5cd0535395 | ||
|
|
0f5aa5a7d2 | ||
|
|
9ea2e0a79f | ||
|
|
0f5ecd83c4 | ||
|
|
6b9bba2edb | ||
|
|
6e9f566d79 | ||
|
|
a6708b9254 | ||
|
|
e895c9d38c | ||
|
|
c973ccb622 | ||
|
|
dd1447cfe5 | ||
|
|
9ad8b9fc36 | ||
|
|
348e6de102 | ||
|
|
9af6d46371 | ||
|
|
e1074f9c3d | ||
|
|
6e74601466 | ||
|
|
3acbdc2f79 | ||
|
|
707642ad35 | ||
|
|
2413d1b214 | ||
|
|
2bb0eca347 | ||
|
|
1c9d6834fd | ||
|
|
c6ed86453f | ||
|
|
e403bbf1a9 | ||
|
|
a7c2549321 | ||
|
|
d65bcd9a3b | ||
|
|
06dbd3c21c | ||
|
|
1b78de4e21 | ||
|
|
82fda31e99 | ||
|
|
5645fa45fb | ||
|
|
5bd5b80afd | ||
|
|
bd58c09500 | ||
|
|
996e45d66a | ||
|
|
c7d85c5dc6 | ||
|
|
b1c430820f | ||
|
|
35a3863b15 | ||
|
|
368fa2c03e | ||
|
|
372c17e228 | ||
|
|
c4a356eaee | ||
|
|
2270cab1c2 | ||
|
|
956d0e5e9d | ||
|
|
7bc8385924 | ||
|
|
9b7e10a3c0 | ||
|
|
913d93debb | ||
|
|
97dfc3429e | ||
|
|
992d0f082f | ||
|
|
b968f63ca5 | ||
|
|
c8792bd080 | ||
|
|
3234c79ee5 | ||
|
|
851b72f289 | ||
|
|
1d7349d18c | ||
|
|
5747a6c06e | ||
|
|
13eacdad8a | ||
|
|
a616adf3ae | ||
|
|
685e52ec30 | ||
|
|
7b4ab8134e | ||
|
|
c2a4a8d518 | ||
|
|
ac96256272 | ||
|
|
d38193646d | ||
|
|
cd07177906 | ||
|
|
6321b74fae | ||
|
|
5aa289e478 | ||
|
|
12b9bd5801 | ||
|
|
f017df443a | ||
|
|
1ba07450e4 | ||
|
|
215f4d1fab | ||
|
|
6c89f9261c | ||
|
|
a98f02d423 | ||
|
|
effc2b0529 | ||
|
|
2c5c97b6d1 | ||
|
|
cb5a42c836 | ||
|
|
0f18a7fe5e | ||
|
|
0591569b4b | ||
|
|
477959ef7d | ||
|
|
4147c92b21 | ||
|
|
10fbb2325f | ||
|
|
1f8b39ed32 | ||
|
|
f4e7b0d6e0 | ||
|
|
f13724e2e3 |
@@ -23,6 +23,11 @@ afc607cfd81458d4e4f3b1f3cf8cc931b933907e
|
||||
5f95dce95602921a70bfbc7d8de2f7712c5e4505
|
||||
# ran pyupgrade-docs again
|
||||
75d0b899bbb56d6849e9d69d83a9426ed3f43f8b
|
||||
|
||||
# move argument parser to own file
|
||||
c9df77cbd6a365dcb73c39618e4842711817e871
|
||||
# Replace reorder-python-imports by isort due to black incompatibility (#11896)
|
||||
8b54596639f41dfac070030ef20394b9001fe63c
|
||||
# Run blacken-docs with black's 2024's style
|
||||
4546d5445aaefe6a03957db028c263521dfb5c4b
|
||||
# Migration to ruff / ruff format
|
||||
4588653b2497ed25976b7aaff225b889fb476756
|
||||
25
.github/workflows/deploy.yml
vendored
25
.github/workflows/deploy.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Build and Check Package
|
||||
uses: hynek/build-and-inspect-python-package@v1.5.4
|
||||
uses: hynek/build-and-inspect-python-package@v2.4.0
|
||||
|
||||
deploy:
|
||||
if: github.repository == 'pytest-dev/pytest'
|
||||
@@ -41,13 +41,13 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download Package
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: Packages
|
||||
path: dist
|
||||
|
||||
- name: Publish package to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.11
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.14
|
||||
|
||||
- name: Push tag
|
||||
run: |
|
||||
@@ -72,6 +72,12 @@ jobs:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download Package
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: Packages
|
||||
path: dist
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -82,9 +88,14 @@ jobs:
|
||||
python -m pip install --upgrade pip
|
||||
pip install --upgrade tox
|
||||
|
||||
- name: Publish GitHub release notes
|
||||
env:
|
||||
GH_RELEASE_NOTES_TOKEN: ${{ github.token }}
|
||||
- name: Generate release notes
|
||||
run: |
|
||||
sudo apt-get install pandoc
|
||||
tox -e publish-gh-release-notes
|
||||
tox -e generate-gh-release-notes -- ${{ github.event.inputs.version }} scripts/latest-release-notes.md
|
||||
|
||||
- name: Publish GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
body_path: scripts/latest-release-notes.md
|
||||
files: dist/*
|
||||
tag_name: ${{ github.event.inputs.version }}
|
||||
|
||||
4
.github/workflows/stale.yml
vendored
4
.github/workflows/stale.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
days-before-issue-close: 7
|
||||
only-labels: "status: needs information"
|
||||
stale-issue-label: "stale"
|
||||
stale-issue-message: "This issue is stale because it has been open for 14 days with no activity."
|
||||
close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale."
|
||||
stale-issue-message: "This issue is stale because it has the `status: needs information` label and requested follow-up information was not provided for 14 days."
|
||||
close-issue-message: "This issue was closed because it has the `status: needs information` label and follow-up information has not been provided for 7 days since being marked as stale."
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
|
||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Build and Check Package
|
||||
uses: hynek/build-and-inspect-python-package@v1.5.4
|
||||
uses: hynek/build-and-inspect-python-package@v2.4.0
|
||||
|
||||
build:
|
||||
needs: [package]
|
||||
@@ -173,7 +173,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download Package
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: Packages
|
||||
path: dist
|
||||
@@ -205,7 +205,7 @@ jobs:
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
if: "matrix.use_coverage"
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
|
||||
4
.github/workflows/update-plugin-list.yml
vendored
4
.github/workflows/update-plugin-list.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
python-version: "3.11"
|
||||
cache: pip
|
||||
- name: requests-cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pytest-plugin-list/
|
||||
key: plugins-http-cache-${{ github.run_id }} # Can use time based key as well
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
run: python scripts/update-plugin-list.py
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38
|
||||
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83
|
||||
with:
|
||||
commit-message: '[automated] Update plugin list'
|
||||
author: 'pytest bot <pytestbot@users.noreply.github.com>'
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -51,6 +51,7 @@ coverage.xml
|
||||
.settings
|
||||
.vscode
|
||||
__pycache__/
|
||||
.python-version
|
||||
|
||||
# generated by pip
|
||||
pip-wheel-metadata/
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.12.1
|
||||
hooks:
|
||||
- id: black
|
||||
args: [--safe, --quiet]
|
||||
- repo: https://github.com/asottile/blacken-docs
|
||||
rev: 1.16.0
|
||||
hooks:
|
||||
- id: blacken-docs
|
||||
additional_dependencies: [black==23.7.0]
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: "v0.4.1"
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: ["--fix"]
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
@@ -20,58 +16,47 @@ repos:
|
||||
- id: debug-statements
|
||||
exclude: _pytest/(debugging|hookspec).py
|
||||
language_version: python3
|
||||
- repo: https://github.com/PyCQA/autoflake
|
||||
rev: v2.2.1
|
||||
- repo: https://github.com/adamchainz/blacken-docs
|
||||
rev: 1.16.0
|
||||
hooks:
|
||||
- id: autoflake
|
||||
name: autoflake
|
||||
args: ["--in-place", "--remove-unused-variables", "--remove-all-unused-imports"]
|
||||
language: python
|
||||
files: \.py$
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.1.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
language_version: python3
|
||||
additional_dependencies:
|
||||
- flake8-typing-imports==1.12.0
|
||||
- flake8-docstrings==1.5.0
|
||||
- repo: https://github.com/asottile/reorder-python-imports
|
||||
rev: v3.12.0
|
||||
hooks:
|
||||
- id: reorder-python-imports
|
||||
args: ['--application-directories=.:src', --py38-plus]
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.15.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py38-plus]
|
||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||
rev: v2.5.0
|
||||
hooks:
|
||||
- id: setup-cfg-fmt
|
||||
args: ["--max-py-version=3.12", "--include-version-classifiers"]
|
||||
- id: blacken-docs
|
||||
additional_dependencies: [black==24.1.1]
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: v1.10.0
|
||||
hooks:
|
||||
- id: python-use-type-annotations
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.8.0
|
||||
rev: v1.9.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
files: ^(src/|testing/)
|
||||
files: ^(src/|testing/|scripts/)
|
||||
args: []
|
||||
additional_dependencies:
|
||||
- iniconfig>=1.1.0
|
||||
- attrs>=19.2.0
|
||||
- pluggy>=1.5.0
|
||||
- packaging
|
||||
- tomli
|
||||
- types-pkg_resources
|
||||
- types-tabulate
|
||||
# for mypy running on python>=3.11 since exceptiongroup is only a dependency
|
||||
# on <3.11
|
||||
- exceptiongroup>=1.0.0rc8
|
||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||
rev: "1.8.0"
|
||||
hooks:
|
||||
- id: pyproject-fmt
|
||||
# https://pyproject-fmt.readthedocs.io/en/latest/#calculating-max-supported-python-version
|
||||
additional_dependencies: ["tox>=4.9"]
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: pylint
|
||||
name: pylint
|
||||
entry: pylint
|
||||
language: system
|
||||
types: [python]
|
||||
args: ["-rn", "-sn", "--fail-on=I"]
|
||||
stages: [manual]
|
||||
- id: rst
|
||||
name: rst
|
||||
entry: rst-lint --encoding utf-8
|
||||
|
||||
17
AUTHORS
17
AUTHORS
@@ -54,7 +54,9 @@ Aviral Verma
|
||||
Aviv Palivoda
|
||||
Babak Keyvani
|
||||
Barney Gale
|
||||
Ben Brown
|
||||
Ben Gartner
|
||||
Ben Leith
|
||||
Ben Webb
|
||||
Benjamin Peterson
|
||||
Benjamin Schubert
|
||||
@@ -92,12 +94,14 @@ Christopher Dignam
|
||||
Christopher Gilling
|
||||
Claire Cecil
|
||||
Claudio Madotto
|
||||
Clément M.T. Robert
|
||||
CrazyMerlyn
|
||||
Cristian Vera
|
||||
Cyrus Maden
|
||||
Damian Skrzypczak
|
||||
Daniel Grana
|
||||
Daniel Hahler
|
||||
Daniel Miller
|
||||
Daniel Nuri
|
||||
Daniel Sánchez Castelló
|
||||
Daniel Valenzuela Zenteno
|
||||
@@ -115,6 +119,7 @@ Daw-Ran Liou
|
||||
Debi Mishra
|
||||
Denis Kirisov
|
||||
Denivy Braiam Rück
|
||||
Dheeraj C K
|
||||
Dhiren Serai
|
||||
Diego Russo
|
||||
Dmitry Dygalo
|
||||
@@ -125,6 +130,8 @@ Edison Gustavo Muenz
|
||||
Edoardo Batini
|
||||
Edson Tadeu M. Manoel
|
||||
Eduardo Schettino
|
||||
Edward Haigh
|
||||
Eero Vaher
|
||||
Eli Boyarski
|
||||
Elizaveta Shashkova
|
||||
Éloi Rivard
|
||||
@@ -132,13 +139,16 @@ Endre Galaczi
|
||||
Eric Hunsberger
|
||||
Eric Liu
|
||||
Eric Siegerman
|
||||
Eric Yuan
|
||||
Erik Aronesty
|
||||
Erik Hasse
|
||||
Erik M. Bray
|
||||
Evan Kepner
|
||||
Evgeny Seliverstov
|
||||
Fabian Sturm
|
||||
Fabien Zarifian
|
||||
Fabio Zadrozny
|
||||
faph
|
||||
Felix Hofstätter
|
||||
Felix Nieuwenhuizen
|
||||
Feng Ma
|
||||
@@ -228,6 +238,7 @@ Kyle Altendorf
|
||||
Lawrence Mitchell
|
||||
Lee Kamentsky
|
||||
Lev Maximov
|
||||
Levon Saldamli
|
||||
Lewis Cowles
|
||||
Llandy Riveron Del Risco
|
||||
Loic Esteve
|
||||
@@ -242,6 +253,7 @@ Marc Mueller
|
||||
Marc Schlaich
|
||||
Marcelo Duarte Trevisani
|
||||
Marcin Bachry
|
||||
Marc Bresson
|
||||
Marco Gorelli
|
||||
Mark Abramowitz
|
||||
Mark Dickinson
|
||||
@@ -275,6 +287,7 @@ Mike Hoyle (hoylemd)
|
||||
Mike Lundy
|
||||
Milan Lesnek
|
||||
Miro Hrončok
|
||||
mrbean-bremen
|
||||
Nathaniel Compton
|
||||
Nathaniel Waisbrot
|
||||
Ned Batchelder
|
||||
@@ -309,6 +322,7 @@ Pierre Sassoulas
|
||||
Pieter Mulder
|
||||
Piotr Banaszkiewicz
|
||||
Piotr Helm
|
||||
Poulami Sau
|
||||
Prakhar Gurunani
|
||||
Prashant Anand
|
||||
Prashant Sharma
|
||||
@@ -336,6 +350,7 @@ Ronny Pfannschmidt
|
||||
Ross Lawley
|
||||
Ruaridh Williamson
|
||||
Russel Winder
|
||||
Russell Martin
|
||||
Ryan Puddephatt
|
||||
Ryan Wooden
|
||||
Sadra Barikbin
|
||||
@@ -410,6 +425,7 @@ Vivaan Verma
|
||||
Vlad Dragos
|
||||
Vlad Radziuk
|
||||
Vladyslav Rachek
|
||||
Volodymyr Kochetkov
|
||||
Volodymyr Piskun
|
||||
Wei Lin
|
||||
Wil Cooley
|
||||
@@ -420,6 +436,7 @@ Xixi Zhao
|
||||
Xuan Luong
|
||||
Xuecong Liao
|
||||
Yannick Péroux
|
||||
Yao Xiao
|
||||
Yoav Caspi
|
||||
Yuliang Shao
|
||||
Yusuke Kadowaki
|
||||
|
||||
@@ -297,12 +297,12 @@ Here is a simple overview, with pytest-specific bits:
|
||||
When committing, ``pre-commit`` will re-format the files if necessary.
|
||||
|
||||
#. If instead of using ``tox`` you prefer to run the tests directly, then we suggest to create a virtual environment and use
|
||||
an editable install with the ``testing`` extra::
|
||||
an editable install with the ``dev`` extra::
|
||||
|
||||
$ python3 -m venv .venv
|
||||
$ source .venv/bin/activate # Linux
|
||||
$ .venv/Scripts/activate.bat # Windows
|
||||
$ pip install -e ".[testing]"
|
||||
$ pip install -e ".[dev]"
|
||||
|
||||
Afterwards, you can edit the files and run pytest normally::
|
||||
|
||||
|
||||
@@ -27,9 +27,6 @@
|
||||
:target: https://results.pre-commit.ci/latest/github/pytest-dev/pytest/main
|
||||
:alt: pre-commit.ci status
|
||||
|
||||
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
||||
:target: https://github.com/psf/black
|
||||
|
||||
.. image:: https://www.codetriage.com/pytest-dev/pytest/badges/users.svg
|
||||
:target: https://www.codetriage.com/pytest-dev/pytest
|
||||
|
||||
@@ -97,12 +94,12 @@ Features
|
||||
- `Modular fixtures <https://docs.pytest.org/en/stable/explanation/fixtures.html>`_ for
|
||||
managing small or parametrized long-lived test resources
|
||||
|
||||
- Can run `unittest <https://docs.pytest.org/en/stable/how-to/unittest.html>`_ (or trial),
|
||||
`nose <https://docs.pytest.org/en/stable/how-to/nose.html>`_ test suites out of the box
|
||||
- Can run `unittest <https://docs.pytest.org/en/stable/how-to/unittest.html>`_ (or trial)
|
||||
test suites out of the box
|
||||
|
||||
- Python 3.8+ or PyPy3
|
||||
|
||||
- Rich plugin architecture, with over 850+ `external plugins <https://docs.pytest.org/en/latest/reference/plugin_list.html>`_ and thriving community
|
||||
- Rich plugin architecture, with over 1300+ `external plugins <https://docs.pytest.org/en/latest/reference/plugin_list.html>`_ and thriving community
|
||||
|
||||
|
||||
Documentation
|
||||
|
||||
@@ -23,9 +23,9 @@ members of the `contributors team`_ interested in receiving funding.
|
||||
|
||||
The current list of contributors receiving funding are:
|
||||
|
||||
* `@asottile`_
|
||||
* `@nicoddemus`_
|
||||
* `@The-Compiler`_
|
||||
* `@RonnyPfannschmidt`_
|
||||
|
||||
Contributors interested in receiving a part of the funds just need to submit a PR adding their
|
||||
name to the list. Contributors that want to stop receiving the funds should also submit a PR
|
||||
@@ -55,6 +55,6 @@ funds. Just drop a line to one of the `@pytest-dev/tidelift-admins`_ or use the
|
||||
.. _`@pytest-dev/tidelift-admins`: https://github.com/orgs/pytest-dev/teams/tidelift-admins/members
|
||||
.. _`agreement`: https://tidelift.com/docs/lifting/agreement
|
||||
|
||||
.. _`@asottile`: https://github.com/asottile
|
||||
.. _`@nicoddemus`: https://github.com/nicoddemus
|
||||
.. _`@The-Compiler`: https://github.com/The-Compiler
|
||||
.. _`@RonnyPfannschmidt`: https://github.com/RonnyPfannschmidt
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import sys
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import cProfile
|
||||
import pytest # NOQA
|
||||
import pstats
|
||||
|
||||
import pytest # noqa: F401
|
||||
|
||||
script = sys.argv[1:] if len(sys.argv) > 1 else ["empty.py"]
|
||||
cProfile.run("pytest.cmdline.main(%r)" % script, "prof")
|
||||
p = pstats.Stats("prof")
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# FastFilesCompleter 0.7383 1.0760
|
||||
import timeit
|
||||
|
||||
|
||||
imports = [
|
||||
"from argcomplete.completers import FilesCompleter as completer",
|
||||
"from _pytest._argcomplete import FastFilesCompleter as completer",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import pytest
|
||||
|
||||
|
||||
SKIP = True
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from unittest import TestCase # noqa: F401
|
||||
|
||||
|
||||
for i in range(15000):
|
||||
exec(
|
||||
f"""
|
||||
|
||||
@@ -5,11 +5,10 @@
|
||||
<div id="searchbox" style="display: none" role="search">
|
||||
<div class="searchformwrapper">
|
||||
<form class="search" action="{{ pathto('search') }}" method="get">
|
||||
<input type="text" name="q" aria-labelledby="searchlabel"
|
||||
placeholder="Search"/>
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<input type="submit" value="{{ _('Go') }}" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">$('#searchbox').show(0);</script>
|
||||
<script>document.getElementById('searchbox').style.display = "block"</script>
|
||||
{%- endif %}
|
||||
|
||||
@@ -44,7 +44,7 @@ Partner projects, sign up here! (by 22 March)
|
||||
What does it mean to "adopt pytest"?
|
||||
-----------------------------------------
|
||||
|
||||
There can be many different definitions of "success". Pytest can run many nose_ and unittest_ tests by default, so using pytest as your testrunner may be possible from day 1. Job done, right?
|
||||
There can be many different definitions of "success". Pytest can run many unittest_ tests by default, so using pytest as your testrunner may be possible from day 1. Job done, right?
|
||||
|
||||
Progressive success might look like:
|
||||
|
||||
@@ -62,7 +62,6 @@ Progressive success might look like:
|
||||
|
||||
It may be after the month is up, the partner project decides that pytest is not right for it. That's okay - hopefully the pytest team will also learn something about its weaknesses or deficiencies.
|
||||
|
||||
.. _nose: nose.html
|
||||
.. _unittest: unittest.html
|
||||
.. _assert: assert.html
|
||||
.. _pycmd: https://bitbucket.org/hpk42/pycmd/overview
|
||||
|
||||
@@ -6,6 +6,14 @@ Release announcements
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
release-8.2.0
|
||||
release-8.1.2
|
||||
release-8.1.1
|
||||
release-8.1.0
|
||||
release-8.0.2
|
||||
release-8.0.1
|
||||
release-8.0.0
|
||||
release-8.0.0rc2
|
||||
release-8.0.0rc1
|
||||
release-7.4.4
|
||||
release-7.4.3
|
||||
|
||||
26
doc/en/announce/release-8.0.0.rst
Normal file
26
doc/en/announce/release-8.0.0.rst
Normal file
@@ -0,0 +1,26 @@
|
||||
pytest-8.0.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 8.0.0 release!
|
||||
|
||||
This release contains new features, improvements, bug fixes, and breaking changes, so users
|
||||
are encouraged to take a look at the CHANGELOG carefully:
|
||||
|
||||
https://docs.pytest.org/en/stable/changelog.html
|
||||
|
||||
For complete documentation, please visit:
|
||||
|
||||
https://docs.pytest.org/en/stable/
|
||||
|
||||
As usual, you can upgrade from PyPI via:
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Bruno Oliveira
|
||||
* Ran Benita
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
32
doc/en/announce/release-8.0.0rc2.rst
Normal file
32
doc/en/announce/release-8.0.0rc2.rst
Normal file
@@ -0,0 +1,32 @@
|
||||
pytest-8.0.0rc2
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 8.0.0rc2 prerelease!
|
||||
|
||||
This is a prerelease, not intended for production use, but to test the upcoming features and improvements
|
||||
in order to catch any major problems before the final version is released to the major public.
|
||||
|
||||
We appreciate your help testing this out before the final release, making sure to report any
|
||||
regressions to our issue tracker:
|
||||
|
||||
https://github.com/pytest-dev/pytest/issues
|
||||
|
||||
When doing so, please include the string ``[prerelease]`` in the title.
|
||||
|
||||
You can upgrade from PyPI via:
|
||||
|
||||
pip install pytest==8.0.0rc2
|
||||
|
||||
Users are encouraged to take a look at the CHANGELOG carefully:
|
||||
|
||||
https://docs.pytest.org/en/release-8.0.0rc2/changelog.html
|
||||
|
||||
Thanks to all the contributors to this release:
|
||||
|
||||
* Ben Brown
|
||||
* Bruno Oliveira
|
||||
* Ran Benita
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
21
doc/en/announce/release-8.0.1.rst
Normal file
21
doc/en/announce/release-8.0.1.rst
Normal file
@@ -0,0 +1,21 @@
|
||||
pytest-8.0.1
|
||||
=======================================
|
||||
|
||||
pytest 8.0.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/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Bruno Oliveira
|
||||
* Clément Robert
|
||||
* Pierre Sassoulas
|
||||
* Ran Benita
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
18
doc/en/announce/release-8.0.2.rst
Normal file
18
doc/en/announce/release-8.0.2.rst
Normal file
@@ -0,0 +1,18 @@
|
||||
pytest-8.0.2
|
||||
=======================================
|
||||
|
||||
pytest 8.0.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/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Ran Benita
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
54
doc/en/announce/release-8.1.0.rst
Normal file
54
doc/en/announce/release-8.1.0.rst
Normal file
@@ -0,0 +1,54 @@
|
||||
pytest-8.1.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 8.1.0 release!
|
||||
|
||||
This release contains new features, improvements, and bug fixes,
|
||||
the full list of changes is available in the changelog:
|
||||
|
||||
https://docs.pytest.org/en/stable/changelog.html
|
||||
|
||||
For complete documentation, please visit:
|
||||
|
||||
https://docs.pytest.org/en/stable/
|
||||
|
||||
As usual, you can upgrade from PyPI via:
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Ben Brown
|
||||
* Ben Leith
|
||||
* Bruno Oliveira
|
||||
* Clément Robert
|
||||
* Dave Hall
|
||||
* Dương Quốc Khánh
|
||||
* Eero Vaher
|
||||
* Eric Larson
|
||||
* Fabian Sturm
|
||||
* Faisal Fawad
|
||||
* Florian Bruhin
|
||||
* Franck Charras
|
||||
* Joachim B Haga
|
||||
* John Litborn
|
||||
* Loïc Estève
|
||||
* Marc Bresson
|
||||
* Patrick Lannigan
|
||||
* Pierre Sassoulas
|
||||
* Ran Benita
|
||||
* Reagan Lee
|
||||
* Ronny Pfannschmidt
|
||||
* Russell Martin
|
||||
* clee2000
|
||||
* donghui
|
||||
* faph
|
||||
* jakkdl
|
||||
* mrbean-bremen
|
||||
* robotherapist
|
||||
* whysage
|
||||
* woutdenolf
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
18
doc/en/announce/release-8.1.1.rst
Normal file
18
doc/en/announce/release-8.1.1.rst
Normal file
@@ -0,0 +1,18 @@
|
||||
pytest-8.1.1
|
||||
=======================================
|
||||
|
||||
pytest 8.1.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/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Ran Benita
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
18
doc/en/announce/release-8.1.2.rst
Normal file
18
doc/en/announce/release-8.1.2.rst
Normal file
@@ -0,0 +1,18 @@
|
||||
pytest-8.1.2
|
||||
=======================================
|
||||
|
||||
pytest 8.1.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/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Bruno Oliveira
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
43
doc/en/announce/release-8.2.0.rst
Normal file
43
doc/en/announce/release-8.2.0.rst
Normal file
@@ -0,0 +1,43 @@
|
||||
pytest-8.2.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 8.2.0 release!
|
||||
|
||||
This release contains new features, improvements, and bug fixes,
|
||||
the full list of changes is available in the changelog:
|
||||
|
||||
https://docs.pytest.org/en/stable/changelog.html
|
||||
|
||||
For complete documentation, please visit:
|
||||
|
||||
https://docs.pytest.org/en/stable/
|
||||
|
||||
As usual, you can upgrade from PyPI via:
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Bruno Oliveira
|
||||
* Daniel Miller
|
||||
* Florian Bruhin
|
||||
* HolyMagician03-UMich
|
||||
* John Litborn
|
||||
* Levon Saldamli
|
||||
* Linghao Zhang
|
||||
* Manuel López-Ibáñez
|
||||
* Pierre Sassoulas
|
||||
* Ran Benita
|
||||
* Ronny Pfannschmidt
|
||||
* Sebastian Meyer
|
||||
* Shekhar verma
|
||||
* Tamir Duberstein
|
||||
* Tobias Stoeckmann
|
||||
* dj
|
||||
* jakkdl
|
||||
* poulami-sau
|
||||
* tserg
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
@@ -22,7 +22,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
cachedir: .pytest_cache
|
||||
rootdir: /home/sweet/project
|
||||
collected 0 items
|
||||
cache -- .../_pytest/cacheprovider.py:526
|
||||
cache -- .../_pytest/cacheprovider.py:542
|
||||
Return a cache object that can persist state between testing sessions.
|
||||
|
||||
cache.get(key, default)
|
||||
@@ -33,7 +33,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
|
||||
Values can be any object handled by the json stdlib module.
|
||||
|
||||
capsysbinary -- .../_pytest/capture.py:1008
|
||||
capsysbinary -- .../_pytest/capture.py:1003
|
||||
Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||
|
||||
The captured output is made available via ``capsysbinary.readouterr()``
|
||||
@@ -43,7 +43,6 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_output(capsysbinary):
|
||||
@@ -51,7 +50,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
captured = capsysbinary.readouterr()
|
||||
assert captured.out == b"hello\n"
|
||||
|
||||
capfd -- .../_pytest/capture.py:1036
|
||||
capfd -- .../_pytest/capture.py:1030
|
||||
Enable text capturing of writes to file descriptors ``1`` and ``2``.
|
||||
|
||||
The captured output is made available via ``capfd.readouterr()`` method
|
||||
@@ -61,7 +60,6 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_system_echo(capfd):
|
||||
@@ -69,7 +67,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
captured = capfd.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
|
||||
capfdbinary -- .../_pytest/capture.py:1064
|
||||
capfdbinary -- .../_pytest/capture.py:1057
|
||||
Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
|
||||
|
||||
The captured output is made available via ``capfd.readouterr()`` method
|
||||
@@ -79,7 +77,6 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_system_echo(capfdbinary):
|
||||
@@ -87,7 +84,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
captured = capfdbinary.readouterr()
|
||||
assert captured.out == b"hello\n"
|
||||
|
||||
capsys -- .../_pytest/capture.py:980
|
||||
capsys -- .../_pytest/capture.py:976
|
||||
Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||
|
||||
The captured output is made available via ``capsys.readouterr()`` method
|
||||
@@ -97,7 +94,6 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_output(capsys):
|
||||
@@ -105,7 +101,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
|
||||
doctest_namespace [session scope] -- .../_pytest/doctest.py:743
|
||||
doctest_namespace [session scope] -- .../_pytest/doctest.py:738
|
||||
Fixture that returns a :py:class:`dict` that will be injected into the
|
||||
namespace of doctests.
|
||||
|
||||
@@ -119,7 +115,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
|
||||
For more details: :ref:`doctest_namespace`.
|
||||
|
||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1365
|
||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1335
|
||||
Session-scoped fixture that returns the session's :class:`pytest.Config`
|
||||
object.
|
||||
|
||||
@@ -129,7 +125,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
if pytestconfig.getoption("verbose") > 0:
|
||||
...
|
||||
|
||||
record_property -- .../_pytest/junitxml.py:282
|
||||
record_property -- .../_pytest/junitxml.py:284
|
||||
Add extra properties to the calling test.
|
||||
|
||||
User properties become part of the test report and are available to the
|
||||
@@ -143,13 +139,13 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
def test_function(record_property):
|
||||
record_property("example_key", 1)
|
||||
|
||||
record_xml_attribute -- .../_pytest/junitxml.py:305
|
||||
record_xml_attribute -- .../_pytest/junitxml.py:307
|
||||
Add extra xml attributes to the tag for the calling test.
|
||||
|
||||
The fixture is callable with ``name, value``. The value is
|
||||
automatically XML-encoded.
|
||||
|
||||
record_testsuite_property [session scope] -- .../_pytest/junitxml.py:343
|
||||
record_testsuite_property [session scope] -- .../_pytest/junitxml.py:345
|
||||
Record a new ``<property>`` tag as child of the root ``<testsuite>``.
|
||||
|
||||
This is suitable to writing global information regarding the entire test
|
||||
@@ -174,18 +170,18 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
`pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See
|
||||
:issue:`7767` for details.
|
||||
|
||||
tmpdir_factory [session scope] -- .../_pytest/legacypath.py:300
|
||||
tmpdir_factory [session scope] -- .../_pytest/legacypath.py:303
|
||||
Return a :class:`pytest.TempdirFactory` instance for the test session.
|
||||
|
||||
tmpdir -- .../_pytest/legacypath.py:307
|
||||
tmpdir -- .../_pytest/legacypath.py:310
|
||||
Return a temporary directory path object which is unique to each test
|
||||
function invocation, created as a sub directory of the base temporary
|
||||
directory.
|
||||
|
||||
By default, a new base temporary directory is created each test session,
|
||||
and old bases are removed after 3 sessions, to aid in debugging. If
|
||||
``--basetemp`` is used then it is cleared each session. See :ref:`base
|
||||
temporary directory`.
|
||||
``--basetemp`` is used then it is cleared each session. See
|
||||
:ref:`temporary directory location and retention`.
|
||||
|
||||
The returned object is a `legacy_path`_ object.
|
||||
|
||||
@@ -196,7 +192,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
|
||||
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html
|
||||
|
||||
caplog -- .../_pytest/logging.py:593
|
||||
caplog -- .../_pytest/logging.py:602
|
||||
Access and control log capturing.
|
||||
|
||||
Captured logs are available through the following properties/methods::
|
||||
@@ -207,7 +203,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
* caplog.record_tuples -> list of (logger_name, level, message) tuples
|
||||
* caplog.clear() -> clear captured records and formatted log output string
|
||||
|
||||
monkeypatch -- .../_pytest/monkeypatch.py:30
|
||||
monkeypatch -- .../_pytest/monkeypatch.py:33
|
||||
A convenient fixture for monkey-patching.
|
||||
|
||||
The fixture provides these methods to modify objects, dictionaries, or
|
||||
@@ -231,16 +227,16 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
To undo modifications done by the fixture in a contained scope,
|
||||
use :meth:`context() <pytest.MonkeyPatch.context>`.
|
||||
|
||||
recwarn -- .../_pytest/recwarn.py:30
|
||||
recwarn -- .../_pytest/recwarn.py:32
|
||||
Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions.
|
||||
|
||||
See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information
|
||||
on warning categories.
|
||||
|
||||
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:239
|
||||
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:242
|
||||
Return a :class:`pytest.TempPathFactory` instance for the test session.
|
||||
|
||||
tmp_path -- .../_pytest/tmpdir.py:254
|
||||
tmp_path -- .../_pytest/tmpdir.py:257
|
||||
Return a temporary directory path object which is unique to each test
|
||||
function invocation, created as a sub directory of the base temporary
|
||||
directory.
|
||||
@@ -249,8 +245,8 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
and old bases are removed after 3 sessions, to aid in debugging.
|
||||
This behavior can be configured with :confval:`tmp_path_retention_count` and
|
||||
:confval:`tmp_path_retention_policy`.
|
||||
If ``--basetemp`` is used then it is cleared each session. See :ref:`base
|
||||
temporary directory`.
|
||||
If ``--basetemp`` is used then it is cleared each session. See
|
||||
:ref:`temporary directory location and retention`.
|
||||
|
||||
The returned object is a :class:`pathlib.Path` object.
|
||||
|
||||
|
||||
@@ -28,6 +28,301 @@ with advance notice in the **Deprecations** section of releases.
|
||||
|
||||
.. towncrier release notes start
|
||||
|
||||
pytest 8.2.0 (2024-04-27)
|
||||
=========================
|
||||
|
||||
Deprecations
|
||||
------------
|
||||
|
||||
- `#12069 <https://github.com/pytest-dev/pytest/issues/12069>`_: A deprecation warning is now raised when implementations of one of the following hooks request a deprecated ``py.path.local`` parameter instead of the ``pathlib.Path`` parameter which replaced it:
|
||||
|
||||
- :hook:`pytest_ignore_collect` - the ``path`` parameter - use ``collection_path`` instead.
|
||||
- :hook:`pytest_collect_file` - the ``path`` parameter - use ``file_path`` instead.
|
||||
- :hook:`pytest_pycollect_makemodule` - the ``path`` parameter - use ``module_path`` instead.
|
||||
- :hook:`pytest_report_header` - the ``startdir`` parameter - use ``start_path`` instead.
|
||||
- :hook:`pytest_report_collectionfinish` - the ``startdir`` parameter - use ``start_path`` instead.
|
||||
|
||||
The replacement parameters are available since pytest 7.0.0.
|
||||
The old parameters will be removed in pytest 9.0.0.
|
||||
|
||||
See :ref:`legacy-path-hooks-deprecated` for more details.
|
||||
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#11871 <https://github.com/pytest-dev/pytest/issues/11871>`_: Added support for reading command line arguments from a file using the prefix character ``@``, like e.g.: ``pytest @tests.txt``. The file must have one argument per line.
|
||||
|
||||
See :ref:`Read arguments from file <args-from-file>` for details.
|
||||
|
||||
|
||||
|
||||
Improvements
|
||||
------------
|
||||
|
||||
- `#11523 <https://github.com/pytest-dev/pytest/issues/11523>`_: :func:`pytest.importorskip` will now issue a warning if the module could be found, but raised :class:`ImportError` instead of :class:`ModuleNotFoundError`.
|
||||
|
||||
The warning can be suppressed by passing ``exc_type=ImportError`` to :func:`pytest.importorskip`.
|
||||
|
||||
See :ref:`import-or-skip-import-error` for details.
|
||||
|
||||
|
||||
- `#11728 <https://github.com/pytest-dev/pytest/issues/11728>`_: For ``unittest``-based tests, exceptions during class cleanup (as raised by functions registered with :meth:`TestCase.addClassCleanup <unittest.TestCase.addClassCleanup>`) are now reported instead of silently failing.
|
||||
|
||||
|
||||
- `#11777 <https://github.com/pytest-dev/pytest/issues/11777>`_: Text is no longer truncated in the ``short test summary info`` section when ``-vv`` is given.
|
||||
|
||||
|
||||
- `#12112 <https://github.com/pytest-dev/pytest/issues/12112>`_: Improved namespace packages detection when :confval:`consider_namespace_packages` is enabled, covering more situations (like editable installs).
|
||||
|
||||
|
||||
- `#9502 <https://github.com/pytest-dev/pytest/issues/9502>`_: Added :envvar:`PYTEST_VERSION` environment variable which is defined at the start of the pytest session and undefined afterwards. It contains the value of ``pytest.__version__``, and among other things can be used to easily check if code is running from within a pytest run.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#12065 <https://github.com/pytest-dev/pytest/issues/12065>`_: Fixed a regression in pytest 8.0.0 where test classes containing ``setup_method`` and tests using ``@staticmethod`` or ``@classmethod`` would crash with ``AttributeError: 'NoneType' object has no attribute 'setup_method'``.
|
||||
|
||||
Now the :attr:`request.instance <pytest.FixtureRequest.instance>` attribute of tests using ``@staticmethod`` and ``@classmethod`` is no longer ``None``, but a fresh instance of the class, like in non-static methods.
|
||||
Previously it was ``None``, and all fixtures of such tests would share a single ``self``.
|
||||
|
||||
|
||||
- `#12135 <https://github.com/pytest-dev/pytest/issues/12135>`_: Fixed issue where fixtures adding their finalizer multiple times to fixtures they request would cause unreliable and non-intuitive teardown ordering in some instances.
|
||||
|
||||
|
||||
- `#12194 <https://github.com/pytest-dev/pytest/issues/12194>`_: Fixed a bug with ``--importmode=importlib`` and ``--doctest-modules`` where child modules did not appear as attributes in parent modules.
|
||||
|
||||
|
||||
- `#1489 <https://github.com/pytest-dev/pytest/issues/1489>`_: Fixed some instances where teardown of higher-scoped fixtures was not happening in the reverse order they were initialized in.
|
||||
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#12069 <https://github.com/pytest-dev/pytest/issues/12069>`_: ``pluggy>=1.5.0`` is now required.
|
||||
|
||||
|
||||
- `#12167 <https://github.com/pytest-dev/pytest/issues/12167>`_: :ref:`cache <cache>`: create supporting files (``CACHEDIR.TAG``, ``.gitignore``, etc.) in a temporary directory to provide atomic semantics.
|
||||
|
||||
|
||||
pytest 8.1.2 (2024-04-26)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#12114 <https://github.com/pytest-dev/pytest/issues/12114>`_: Fixed error in :func:`pytest.approx` when used with `numpy` arrays and comparing with other types.
|
||||
|
||||
|
||||
pytest 8.1.1 (2024-03-08)
|
||||
=========================
|
||||
|
||||
.. note::
|
||||
|
||||
This release is not a usual bug fix release -- it contains features and improvements, being a follow up
|
||||
to ``8.1.0``, which has been yanked from PyPI.
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#11475 <https://github.com/pytest-dev/pytest/issues/11475>`_: Added the new :confval:`consider_namespace_packages` configuration option, defaulting to ``False``.
|
||||
|
||||
If set to ``True``, pytest will attempt to identify modules that are part of `namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages>`__ when importing modules.
|
||||
|
||||
|
||||
- `#11653 <https://github.com/pytest-dev/pytest/issues/11653>`_: Added the new :confval:`verbosity_test_cases` configuration option for fine-grained control of test execution verbosity.
|
||||
See :ref:`Fine-grained verbosity <pytest.fine_grained_verbosity>` for more details.
|
||||
|
||||
|
||||
|
||||
Improvements
|
||||
------------
|
||||
|
||||
- `#10865 <https://github.com/pytest-dev/pytest/issues/10865>`_: :func:`pytest.warns` now validates that :func:`warnings.warn` was called with a `str` or a `Warning`.
|
||||
Currently in Python it is possible to use other types, however this causes an exception when :func:`warnings.filterwarnings` is used to filter those warnings (see `CPython #103577 <https://github.com/python/cpython/issues/103577>`__ for a discussion).
|
||||
While this can be considered a bug in CPython, we decided to put guards in pytest as the error message produced without this check in place is confusing.
|
||||
|
||||
|
||||
- `#11311 <https://github.com/pytest-dev/pytest/issues/11311>`_: When using ``--override-ini`` for paths in invocations without a configuration file defined, the current working directory is used
|
||||
as the relative directory.
|
||||
|
||||
Previoulsy this would raise an :class:`AssertionError`.
|
||||
|
||||
|
||||
- `#11475 <https://github.com/pytest-dev/pytest/issues/11475>`_: :ref:`--import-mode=importlib <import-mode-importlib>` now tries to import modules using the standard import mechanism (but still without changing :py:data:`sys.path`), falling back to importing modules directly only if that fails.
|
||||
|
||||
This means that installed packages will be imported under their canonical name if possible first, for example ``app.core.models``, instead of having the module name always be derived from their path (for example ``.env310.lib.site_packages.app.core.models``).
|
||||
|
||||
|
||||
- `#11801 <https://github.com/pytest-dev/pytest/issues/11801>`_: Added the :func:`iter_parents() <_pytest.nodes.Node.iter_parents>` helper method on nodes.
|
||||
It is similar to :func:`listchain <_pytest.nodes.Node.listchain>`, but goes from bottom to top, and returns an iterator, not a list.
|
||||
|
||||
|
||||
- `#11850 <https://github.com/pytest-dev/pytest/issues/11850>`_: Added support for :data:`sys.last_exc` for post-mortem debugging on Python>=3.12.
|
||||
|
||||
|
||||
- `#11962 <https://github.com/pytest-dev/pytest/issues/11962>`_: In case no other suitable candidates for configuration file are found, a ``pyproject.toml`` (even without a ``[tool.pytest.ini_options]`` table) will be considered as the configuration file and define the ``rootdir``.
|
||||
|
||||
|
||||
- `#11978 <https://github.com/pytest-dev/pytest/issues/11978>`_: Add ``--log-file-mode`` option to the logging plugin, enabling appending to log-files. This option accepts either ``"w"`` or ``"a"`` and defaults to ``"w"``.
|
||||
|
||||
Previously, the mode was hard-coded to be ``"w"`` which truncates the file before logging.
|
||||
|
||||
|
||||
- `#12047 <https://github.com/pytest-dev/pytest/issues/12047>`_: When multiple finalizers of a fixture raise an exception, now all exceptions are reported as an exception group.
|
||||
Previously, only the first exception was reported.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#11475 <https://github.com/pytest-dev/pytest/issues/11475>`_: Fixed regression where ``--importmode=importlib`` would import non-test modules more than once.
|
||||
|
||||
|
||||
- `#11904 <https://github.com/pytest-dev/pytest/issues/11904>`_: Fixed a regression in pytest 8.0.0 that would cause test collection to fail due to permission errors when using ``--pyargs``.
|
||||
|
||||
This change improves the collection tree for tests specified using ``--pyargs``, see :pull:`12043` for a comparison with pytest 8.0 and <8.
|
||||
|
||||
|
||||
- `#12011 <https://github.com/pytest-dev/pytest/issues/12011>`_: Fixed a regression in 8.0.1 whereby ``setup_module`` xunit-style fixtures are not executed when ``--doctest-modules`` is passed.
|
||||
|
||||
|
||||
- `#12014 <https://github.com/pytest-dev/pytest/issues/12014>`_: Fix the ``stacklevel`` used when warning about marks used on fixtures.
|
||||
|
||||
|
||||
- `#12039 <https://github.com/pytest-dev/pytest/issues/12039>`_: Fixed a regression in ``8.0.2`` where tests created using :fixture:`tmp_path` have been collected multiple times in CI under Windows.
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#11790 <https://github.com/pytest-dev/pytest/issues/11790>`_: Documented the retention of temporary directories created using the ``tmp_path`` fixture in more detail.
|
||||
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#11785 <https://github.com/pytest-dev/pytest/issues/11785>`_: Some changes were made to private functions which may affect plugins which access them:
|
||||
|
||||
- ``FixtureManager._getautousenames()`` now takes a ``Node`` itself instead of the nodeid.
|
||||
- ``FixtureManager.getfixturedefs()`` now takes the ``Node`` itself instead of the nodeid.
|
||||
- The ``_pytest.nodes.iterparentnodeids()`` function is removed without replacement.
|
||||
Prefer to traverse the node hierarchy itself instead.
|
||||
If you really need to, copy the function from the previous pytest release.
|
||||
|
||||
|
||||
- `#12069 <https://github.com/pytest-dev/pytest/issues/12069>`_: Delayed the deprecation of the following features to ``9.0.0``:
|
||||
|
||||
* :ref:`node-ctor-fspath-deprecation`.
|
||||
* :ref:`legacy-path-hooks-deprecated`.
|
||||
|
||||
It was discovered after ``8.1.0`` was released that the warnings about the impeding removal were not being displayed, so the team decided to revert the removal.
|
||||
|
||||
This is the reason for ``8.1.0`` being yanked.
|
||||
|
||||
|
||||
pytest 8.1.0 (YANKED)
|
||||
=====================
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
This release has been **yanked**: it broke some plugins without the proper warning period, due to
|
||||
some warnings not showing up as expected.
|
||||
|
||||
See `#12069 <https://github.com/pytest-dev/pytest/issues/12069>`__.
|
||||
|
||||
|
||||
pytest 8.0.2 (2024-02-24)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#11895 <https://github.com/pytest-dev/pytest/issues/11895>`_: Fix collection on Windows where initial paths contain the short version of a path (for example ``c:\PROGRA~1\tests``).
|
||||
|
||||
|
||||
- `#11953 <https://github.com/pytest-dev/pytest/issues/11953>`_: Fix an ``IndexError`` crash raising from ``getstatementrange_ast``.
|
||||
|
||||
|
||||
- `#12021 <https://github.com/pytest-dev/pytest/issues/12021>`_: Reverted a fix to `--maxfail` handling in pytest 8.0.0 because it caused a regression in pytest-xdist whereby session fixture teardowns may get executed multiple times when the max-fails is reached.
|
||||
|
||||
|
||||
pytest 8.0.1 (2024-02-16)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#11875 <https://github.com/pytest-dev/pytest/issues/11875>`_: Correctly handle errors from :func:`getpass.getuser` in Python 3.13.
|
||||
|
||||
|
||||
- `#11879 <https://github.com/pytest-dev/pytest/issues/11879>`_: Fix an edge case where ``ExceptionInfo._stringify_exception`` could crash :func:`pytest.raises`.
|
||||
|
||||
|
||||
- `#11906 <https://github.com/pytest-dev/pytest/issues/11906>`_: Fix regression with :func:`pytest.warns` using custom warning subclasses which have more than one parameter in their `__init__`.
|
||||
|
||||
|
||||
- `#11907 <https://github.com/pytest-dev/pytest/issues/11907>`_: Fix a regression in pytest 8.0.0 whereby calling :func:`pytest.skip` and similar control-flow exceptions within a :func:`pytest.warns()` block would get suppressed instead of propagating.
|
||||
|
||||
|
||||
- `#11929 <https://github.com/pytest-dev/pytest/issues/11929>`_: Fix a regression in pytest 8.0.0 whereby autouse fixtures defined in a module get ignored by the doctests in the module.
|
||||
|
||||
|
||||
- `#11937 <https://github.com/pytest-dev/pytest/issues/11937>`_: Fix a regression in pytest 8.0.0 whereby items would be collected in reverse order in some circumstances.
|
||||
|
||||
|
||||
pytest 8.0.0 (2024-01-27)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#11842 <https://github.com/pytest-dev/pytest/issues/11842>`_: Properly escape the ``reason`` of a :ref:`skip <pytest.mark.skip ref>` mark when writing JUnit XML files.
|
||||
|
||||
|
||||
- `#11861 <https://github.com/pytest-dev/pytest/issues/11861>`_: Avoid microsecond exceeds ``1_000_000`` when using ``log-date-format`` with ``%f`` specifier, which might cause the test suite to crash.
|
||||
|
||||
|
||||
pytest 8.0.0rc2 (2024-01-17)
|
||||
============================
|
||||
|
||||
|
||||
Improvements
|
||||
------------
|
||||
|
||||
- `#11233 <https://github.com/pytest-dev/pytest/issues/11233>`_: Improvements to ``-r`` for xfailures and xpasses:
|
||||
|
||||
* Report tracebacks for xfailures when ``-rx`` is set.
|
||||
* Report captured output for xpasses when ``-rX`` is set.
|
||||
* For xpasses, add ``-`` in summary between test name and reason, to match how xfail is displayed.
|
||||
|
||||
- `#11825 <https://github.com/pytest-dev/pytest/issues/11825>`_: The :hook:`pytest_plugin_registered` hook has a new ``plugin_name`` parameter containing the name by which ``plugin`` is registered.
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#11706 <https://github.com/pytest-dev/pytest/issues/11706>`_: Fix reporting of teardown errors in higher-scoped fixtures when using `--maxfail` or `--stepwise`.
|
||||
|
||||
NOTE: This change was reverted in pytest 8.0.2 to fix a `regression <https://github.com/pytest-dev/pytest-xdist/issues/1024>`_ it caused in pytest-xdist.
|
||||
|
||||
|
||||
- `#11758 <https://github.com/pytest-dev/pytest/issues/11758>`_: Fixed ``IndexError: string index out of range`` crash in ``if highlighted[-1] == "\n" and source[-1] != "\n"``.
|
||||
This bug was introduced in pytest 8.0.0rc1.
|
||||
|
||||
|
||||
- `#9765 <https://github.com/pytest-dev/pytest/issues/9765>`_, `#11816 <https://github.com/pytest-dev/pytest/issues/11816>`_: Fixed a frustrating bug that afflicted some users with the only error being ``assert mod not in mods``. The issue was caused by the fact that ``str(Path(mod))`` and ``mod.__file__`` don't necessarily produce the same string, and was being erroneously used interchangably in some places in the code.
|
||||
|
||||
This fix also broke the internal API of ``PytestPluginManager.consider_conftest`` by introducing a new parameter -- we mention this in case it is being used by external code, even if marked as *private*.
|
||||
|
||||
|
||||
pytest 8.0.0rc1 (2023-12-30)
|
||||
============================
|
||||
|
||||
@@ -223,11 +518,15 @@ These are breaking changes where deprecation was not possible.
|
||||
therefore fail on the newly-re-emitted warnings.
|
||||
|
||||
|
||||
- The internal ``FixtureManager.getfixtureclosure`` method has changed. Plugins which use this method or
|
||||
which subclass ``FixtureManager`` and overwrite that method will need to adapt to the change.
|
||||
|
||||
|
||||
|
||||
Deprecations
|
||||
------------
|
||||
|
||||
- `#10465 <https://github.com/pytest-dev/pytest/issues/10465>`_: Test functions returning a value other than ``None`` will now issue a :class:`pytest.PytestWarning` instead of :class:`pytest.PytestRemovedIn8Warning`, meaning this will stay a warning instead of becoming an error in the future.
|
||||
- `#10465 <https://github.com/pytest-dev/pytest/issues/10465>`_: Test functions returning a value other than ``None`` will now issue a :class:`pytest.PytestWarning` instead of ``pytest.PytestRemovedIn8Warning``, meaning this will stay a warning instead of becoming an error in the future.
|
||||
|
||||
|
||||
- `#3664 <https://github.com/pytest-dev/pytest/issues/3664>`_: Applying a mark to a fixture function now issues a warning: marks in fixtures never had any effect, but it is a common user error to apply a mark to a fixture (for example ``usefixtures``) and expect it to work.
|
||||
@@ -1257,7 +1556,7 @@ Deprecations
|
||||
See :ref:`the deprecation note <diamond-inheritance-deprecated>` for full details.
|
||||
|
||||
|
||||
- `#8592 <https://github.com/pytest-dev/pytest/issues/8592>`_: :hook:`pytest_cmdline_preparse` has been officially deprecated. It will be removed in a future release. Use :hook:`pytest_load_initial_conftests` instead.
|
||||
- `#8592 <https://github.com/pytest-dev/pytest/issues/8592>`_: ``pytest_cmdline_preparse`` has been officially deprecated. It will be removed in a future release. Use :hook:`pytest_load_initial_conftests` instead.
|
||||
|
||||
See :ref:`the deprecation note <cmdline-preparse-deprecated>` for full details.
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from _pytest import __version__ as version
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import sphinx.application
|
||||
|
||||
@@ -394,7 +395,7 @@ epub_copyright = "2013, holger krekel et alii"
|
||||
# The format is a list of tuples containing the path and title.
|
||||
# epub_pre_files = []
|
||||
|
||||
# HTML files shat should be inserted after the pages created by sphinx.
|
||||
# HTML files that should be inserted after the pages created by sphinx.
|
||||
# The format is a list of tuples containing the path and title.
|
||||
# epub_post_files = []
|
||||
|
||||
@@ -441,9 +442,10 @@ intersphinx_mapping = {
|
||||
|
||||
def configure_logging(app: "sphinx.application.Sphinx") -> None:
|
||||
"""Configure Sphinx's WarningHandler to handle (expected) missing include."""
|
||||
import sphinx.util.logging
|
||||
import logging
|
||||
|
||||
import sphinx.util.logging
|
||||
|
||||
class WarnLogFilter(logging.Filter):
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
"""Ignore warnings about missing include with "only" directive.
|
||||
|
||||
@@ -44,7 +44,6 @@ How-to guides
|
||||
|
||||
how-to/existingtestsuite
|
||||
how-to/unittest
|
||||
how-to/nose
|
||||
how-to/xunit_setup
|
||||
|
||||
how-to/bash-completion
|
||||
|
||||
@@ -19,129 +19,39 @@ Below is a complete list of all pytest features which are considered deprecated.
|
||||
:class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
|
||||
|
||||
|
||||
.. _nose-deprecation:
|
||||
.. _import-or-skip-import-error:
|
||||
|
||||
Support for tests written for nose
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
``pytest.importorskip`` default behavior regarding :class:`ImportError`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.2
|
||||
.. deprecated:: 8.2
|
||||
|
||||
Support for running tests written for `nose <https://nose.readthedocs.io/en/latest/>`__ is now deprecated.
|
||||
Traditionally :func:`pytest.importorskip` will capture :class:`ImportError`, with the original intent being to skip
|
||||
tests where a dependent module is not installed, for example testing with different dependencies.
|
||||
|
||||
``nose`` has been in maintenance mode-only for years, and maintaining the plugin is not trivial as it spills
|
||||
over the code base (see :issue:`9886` for more details).
|
||||
However some packages might be installed in the system, but are not importable due to
|
||||
some other issue, for example, a compilation error or a broken installation. In those cases :func:`pytest.importorskip`
|
||||
would still silently skip the test, but more often than not users would like to see the unexpected
|
||||
error so the underlying issue can be fixed.
|
||||
|
||||
setup/teardown
|
||||
^^^^^^^^^^^^^^
|
||||
In ``8.2`` the ``exc_type`` parameter has been added, giving users the ability of passing :class:`ModuleNotFoundError`
|
||||
to skip tests only if the module cannot really be found, and not because of some other error.
|
||||
|
||||
One thing that might catch users by surprise is that plain ``setup`` and ``teardown`` methods are not pytest native,
|
||||
they are in fact part of the ``nose`` support.
|
||||
Catching only :class:`ModuleNotFoundError` by default (and letting other errors propagate) would be the best solution,
|
||||
however for backward compatibility, pytest will keep the existing behavior but raise an warning if:
|
||||
|
||||
1. The captured exception is of type :class:`ImportError`, and:
|
||||
2. The user does not pass ``exc_type`` explicitly.
|
||||
|
||||
.. code-block:: python
|
||||
If the import attempt raises :class:`ModuleNotFoundError` (the usual case), then the module is skipped and no
|
||||
warning is emitted.
|
||||
|
||||
class Test:
|
||||
def setup(self):
|
||||
self.resource = make_resource()
|
||||
This way, the usual cases will keep working the same way, while unexpected errors will now issue a warning, with
|
||||
users being able to supress the warning by passing ``exc_type=ImportError`` explicitly.
|
||||
|
||||
def teardown(self):
|
||||
self.resource.close()
|
||||
|
||||
def test_foo(self):
|
||||
...
|
||||
|
||||
def test_bar(self):
|
||||
...
|
||||
|
||||
|
||||
|
||||
Native pytest support uses ``setup_method`` and ``teardown_method`` (see :ref:`xunit-method-setup`), so the above should be changed to:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Test:
|
||||
def setup_method(self):
|
||||
self.resource = make_resource()
|
||||
|
||||
def teardown_method(self):
|
||||
self.resource.close()
|
||||
|
||||
def test_foo(self):
|
||||
...
|
||||
|
||||
def test_bar(self):
|
||||
...
|
||||
|
||||
|
||||
This is easy to do in an entire code base by doing a simple find/replace.
|
||||
|
||||
@with_setup
|
||||
^^^^^^^^^^^
|
||||
|
||||
Code using `@with_setup <with-setup-nose>`_ such as this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from nose.tools import with_setup
|
||||
|
||||
|
||||
def setup_some_resource():
|
||||
...
|
||||
|
||||
|
||||
def teardown_some_resource():
|
||||
...
|
||||
|
||||
|
||||
@with_setup(setup_some_resource, teardown_some_resource)
|
||||
def test_foo():
|
||||
...
|
||||
|
||||
Will also need to be ported to a supported pytest style. One way to do it is using a fixture:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def setup_some_resource():
|
||||
...
|
||||
|
||||
|
||||
def teardown_some_resource():
|
||||
...
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def some_resource():
|
||||
setup_some_resource()
|
||||
yield
|
||||
teardown_some_resource()
|
||||
|
||||
|
||||
def test_foo(some_resource):
|
||||
...
|
||||
|
||||
|
||||
.. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup
|
||||
|
||||
.. _instance-collector-deprecation:
|
||||
|
||||
The ``pytest.Instance`` collector
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. versionremoved:: 7.0
|
||||
|
||||
The ``pytest.Instance`` collector type has been removed.
|
||||
|
||||
Previously, Python test methods were collected as :class:`~pytest.Class` -> ``Instance`` -> :class:`~pytest.Function`.
|
||||
Now :class:`~pytest.Class` collects the test methods directly.
|
||||
|
||||
Most plugins which reference ``Instance`` do so in order to ignore or skip it,
|
||||
using a check such as ``if isinstance(node, Instance): return``.
|
||||
Such plugins should simply remove consideration of ``Instance`` on pytest>=7.
|
||||
However, to keep such uses working, a dummy type has been instanted in ``pytest.Instance`` and ``_pytest.python.Instance``,
|
||||
and importing it emits a deprecation warning. This will be removed in pytest 8.
|
||||
In ``9.0``, the warning will turn into an error, and in ``9.1`` :func:`pytest.importorskip` will only capture
|
||||
:class:`ModuleNotFoundError` by default and no warnings will be issued anymore -- but users can still capture
|
||||
:class:`ImportError` by passing it to ``exc_type``.
|
||||
|
||||
|
||||
.. _node-ctor-fspath-deprecation:
|
||||
@@ -183,7 +93,6 @@ both ``fspath`` (``py.path.local``) and ``path`` (``pathlib.Path``) attributes,
|
||||
no matter what argument was used in the constructor. We expect to deprecate the
|
||||
``fspath`` attribute in a future release.
|
||||
|
||||
.. _legacy-path-hooks-deprecated:
|
||||
|
||||
Configuring hook specs/impls using markers
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -197,13 +106,11 @@ have been available since years and should be used instead.
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
def pytest_runtest_call():
|
||||
...
|
||||
def pytest_runtest_call(): ...
|
||||
|
||||
|
||||
# or
|
||||
def pytest_runtest_call():
|
||||
...
|
||||
def pytest_runtest_call(): ...
|
||||
|
||||
|
||||
pytest_runtest_call.tryfirst = True
|
||||
@@ -213,8 +120,7 @@ should be changed to:
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_runtest_call():
|
||||
...
|
||||
def pytest_runtest_call(): ...
|
||||
|
||||
Changed ``hookimpl`` attributes:
|
||||
|
||||
@@ -229,6 +135,8 @@ Changed ``hookwrapper`` attributes:
|
||||
* ``historic``
|
||||
|
||||
|
||||
.. _legacy-path-hooks-deprecated:
|
||||
|
||||
``py.path.local`` arguments for hooks replaced with ``pathlib.Path``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -273,62 +181,6 @@ Directly constructing the following classes is now deprecated:
|
||||
|
||||
These constructors have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 8.
|
||||
|
||||
.. _cmdline-preparse-deprecated:
|
||||
|
||||
Passing ``msg=`` to ``pytest.skip``, ``pytest.fail`` or ``pytest.exit``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.0
|
||||
|
||||
Passing the keyword argument ``msg`` to :func:`pytest.skip`, :func:`pytest.fail` or :func:`pytest.exit`
|
||||
is now deprecated and ``reason`` should be used instead. This change is to bring consistency between these
|
||||
functions and the ``@pytest.mark.skip`` and ``@pytest.mark.xfail`` markers which already accept a ``reason`` argument.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_fail_example():
|
||||
# old
|
||||
pytest.fail(msg="foo")
|
||||
# new
|
||||
pytest.fail(reason="bar")
|
||||
|
||||
|
||||
def test_skip_example():
|
||||
# old
|
||||
pytest.skip(msg="foo")
|
||||
# new
|
||||
pytest.skip(reason="bar")
|
||||
|
||||
|
||||
def test_exit_example():
|
||||
# old
|
||||
pytest.exit(msg="foo")
|
||||
# new
|
||||
pytest.exit(reason="bar")
|
||||
|
||||
|
||||
Implementing the ``pytest_cmdline_preparse`` hook
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.0
|
||||
|
||||
Implementing the :hook:`pytest_cmdline_preparse` hook has been officially deprecated.
|
||||
Implement the :hook:`pytest_load_initial_conftests` hook instead.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def pytest_cmdline_preparse(config: Config, args: List[str]) -> None:
|
||||
...
|
||||
|
||||
|
||||
# becomes:
|
||||
|
||||
|
||||
def pytest_load_initial_conftests(
|
||||
early_config: Config, parser: Parser, args: List[str]
|
||||
) -> None:
|
||||
...
|
||||
|
||||
.. _diamond-inheritance-deprecated:
|
||||
|
||||
Diamond inheritance between :class:`pytest.Collector` and :class:`pytest.Item`
|
||||
@@ -381,7 +233,7 @@ conflicts (such as :class:`pytest.File` now taking ``path`` instead of
|
||||
deprecation warning is now raised.
|
||||
|
||||
Applying a mark to a fixture function
|
||||
-------------------------------------
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.4
|
||||
|
||||
@@ -391,38 +243,13 @@ Applying a mark to a fixture function never had any effect, but it is a common u
|
||||
|
||||
@pytest.mark.usefixtures("clean_database")
|
||||
@pytest.fixture
|
||||
def user() -> User:
|
||||
...
|
||||
def user() -> User: ...
|
||||
|
||||
Users expected in this case that the ``usefixtures`` mark would have its intended effect of using the ``clean_database`` fixture when ``user`` was invoked, when in fact it has no effect at all.
|
||||
|
||||
Now pytest will issue a warning when it encounters this problem, and will raise an error in the future versions.
|
||||
|
||||
|
||||
Backward compatibilities in ``Parser.addoption``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 2.4
|
||||
|
||||
Several behaviors of :meth:`Parser.addoption <pytest.Parser.addoption>` are now
|
||||
scheduled for removal in pytest 8 (deprecated since pytest 2.4.0):
|
||||
|
||||
- ``parser.addoption(..., help=".. %default ..")`` - use ``%(default)s`` instead.
|
||||
- ``parser.addoption(..., type="int/string/float/complex")`` - use ``type=int`` etc. instead.
|
||||
|
||||
|
||||
Using ``pytest.warns(None)``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.0
|
||||
|
||||
:func:`pytest.warns(None) <pytest.warns>` is now deprecated because it was frequently misused.
|
||||
Its correct usage was checking that the code emits at least one warning of any type - like ``pytest.warns()``
|
||||
or ``pytest.warns(Warning)``.
|
||||
|
||||
See :ref:`warns use cases` for examples.
|
||||
|
||||
|
||||
Returning non-None value in test functions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -463,19 +290,6 @@ The proper fix is to change the `return` to an `assert`:
|
||||
assert foo(a, b) == result
|
||||
|
||||
|
||||
The ``--strict`` command-line option
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 6.2
|
||||
|
||||
The ``--strict`` command-line option has been deprecated in favor of ``--strict-markers``, which
|
||||
better conveys what the option does.
|
||||
|
||||
We have plans to maybe in the future to reintroduce ``--strict`` and make it an encompassing
|
||||
flag for all strictness related options (``--strict-markers`` and ``--strict-config``
|
||||
at the moment, more might be introduced in the future).
|
||||
|
||||
|
||||
The ``yield_fixture`` function/decorator
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -494,6 +308,227 @@ an appropriate period of deprecation has passed.
|
||||
|
||||
Some breaking changes which could not be deprecated are also listed.
|
||||
|
||||
.. _nose-deprecation:
|
||||
|
||||
Support for tests written for nose
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.2
|
||||
.. versionremoved:: 8.0
|
||||
|
||||
Support for running tests written for `nose <https://nose.readthedocs.io/en/latest/>`__ is now deprecated.
|
||||
|
||||
``nose`` has been in maintenance mode-only for years, and maintaining the plugin is not trivial as it spills
|
||||
over the code base (see :issue:`9886` for more details).
|
||||
|
||||
setup/teardown
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
One thing that might catch users by surprise is that plain ``setup`` and ``teardown`` methods are not pytest native,
|
||||
they are in fact part of the ``nose`` support.
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Test:
|
||||
def setup(self):
|
||||
self.resource = make_resource()
|
||||
|
||||
def teardown(self):
|
||||
self.resource.close()
|
||||
|
||||
def test_foo(self): ...
|
||||
|
||||
def test_bar(self): ...
|
||||
|
||||
|
||||
|
||||
Native pytest support uses ``setup_method`` and ``teardown_method`` (see :ref:`xunit-method-setup`), so the above should be changed to:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Test:
|
||||
def setup_method(self):
|
||||
self.resource = make_resource()
|
||||
|
||||
def teardown_method(self):
|
||||
self.resource.close()
|
||||
|
||||
def test_foo(self): ...
|
||||
|
||||
def test_bar(self): ...
|
||||
|
||||
|
||||
This is easy to do in an entire code base by doing a simple find/replace.
|
||||
|
||||
@with_setup
|
||||
^^^^^^^^^^^
|
||||
|
||||
Code using `@with_setup <with-setup-nose>`_ such as this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from nose.tools import with_setup
|
||||
|
||||
|
||||
def setup_some_resource(): ...
|
||||
|
||||
|
||||
def teardown_some_resource(): ...
|
||||
|
||||
|
||||
@with_setup(setup_some_resource, teardown_some_resource)
|
||||
def test_foo(): ...
|
||||
|
||||
Will also need to be ported to a supported pytest style. One way to do it is using a fixture:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def setup_some_resource(): ...
|
||||
|
||||
|
||||
def teardown_some_resource(): ...
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def some_resource():
|
||||
setup_some_resource()
|
||||
yield
|
||||
teardown_some_resource()
|
||||
|
||||
|
||||
def test_foo(some_resource): ...
|
||||
|
||||
|
||||
.. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup
|
||||
|
||||
|
||||
The ``compat_co_firstlineno`` attribute
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Nose inspects this attribute on function objects to allow overriding the function's inferred line number.
|
||||
Pytest no longer respects this attribute.
|
||||
|
||||
|
||||
|
||||
Passing ``msg=`` to ``pytest.skip``, ``pytest.fail`` or ``pytest.exit``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.0
|
||||
.. versionremoved:: 8.0
|
||||
|
||||
Passing the keyword argument ``msg`` to :func:`pytest.skip`, :func:`pytest.fail` or :func:`pytest.exit`
|
||||
is now deprecated and ``reason`` should be used instead. This change is to bring consistency between these
|
||||
functions and the ``@pytest.mark.skip`` and ``@pytest.mark.xfail`` markers which already accept a ``reason`` argument.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_fail_example():
|
||||
# old
|
||||
pytest.fail(msg="foo")
|
||||
# new
|
||||
pytest.fail(reason="bar")
|
||||
|
||||
|
||||
def test_skip_example():
|
||||
# old
|
||||
pytest.skip(msg="foo")
|
||||
# new
|
||||
pytest.skip(reason="bar")
|
||||
|
||||
|
||||
def test_exit_example():
|
||||
# old
|
||||
pytest.exit(msg="foo")
|
||||
# new
|
||||
pytest.exit(reason="bar")
|
||||
|
||||
|
||||
.. _instance-collector-deprecation:
|
||||
|
||||
The ``pytest.Instance`` collector
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. versionremoved:: 7.0
|
||||
|
||||
The ``pytest.Instance`` collector type has been removed.
|
||||
|
||||
Previously, Python test methods were collected as :class:`~pytest.Class` -> ``Instance`` -> :class:`~pytest.Function`.
|
||||
Now :class:`~pytest.Class` collects the test methods directly.
|
||||
|
||||
Most plugins which reference ``Instance`` do so in order to ignore or skip it,
|
||||
using a check such as ``if isinstance(node, Instance): return``.
|
||||
Such plugins should simply remove consideration of ``Instance`` on pytest>=7.
|
||||
However, to keep such uses working, a dummy type has been instanted in ``pytest.Instance`` and ``_pytest.python.Instance``,
|
||||
and importing it emits a deprecation warning. This was removed in pytest 8.
|
||||
|
||||
|
||||
Using ``pytest.warns(None)``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.0
|
||||
.. versionremoved:: 8.0
|
||||
|
||||
:func:`pytest.warns(None) <pytest.warns>` is now deprecated because it was frequently misused.
|
||||
Its correct usage was checking that the code emits at least one warning of any type - like ``pytest.warns()``
|
||||
or ``pytest.warns(Warning)``.
|
||||
|
||||
See :ref:`warns use cases` for examples.
|
||||
|
||||
|
||||
Backward compatibilities in ``Parser.addoption``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 2.4
|
||||
.. versionremoved:: 8.0
|
||||
|
||||
Several behaviors of :meth:`Parser.addoption <pytest.Parser.addoption>` are now
|
||||
removed in pytest 8 (deprecated since pytest 2.4.0):
|
||||
|
||||
- ``parser.addoption(..., help=".. %default ..")`` - use ``%(default)s`` instead.
|
||||
- ``parser.addoption(..., type="int/string/float/complex")`` - use ``type=int`` etc. instead.
|
||||
|
||||
|
||||
The ``--strict`` command-line option
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 6.2
|
||||
.. versionremoved:: 8.0
|
||||
|
||||
The ``--strict`` command-line option has been deprecated in favor of ``--strict-markers``, which
|
||||
better conveys what the option does.
|
||||
|
||||
We have plans to maybe in the future to reintroduce ``--strict`` and make it an encompassing
|
||||
flag for all strictness related options (``--strict-markers`` and ``--strict-config``
|
||||
at the moment, more might be introduced in the future).
|
||||
|
||||
|
||||
.. _cmdline-preparse-deprecated:
|
||||
|
||||
Implementing the ``pytest_cmdline_preparse`` hook
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.0
|
||||
.. versionremoved:: 8.0
|
||||
|
||||
Implementing the ``pytest_cmdline_preparse`` hook has been officially deprecated.
|
||||
Implement the :hook:`pytest_load_initial_conftests` hook instead.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def pytest_cmdline_preparse(config: Config, args: List[str]) -> None: ...
|
||||
|
||||
|
||||
# becomes:
|
||||
|
||||
|
||||
def pytest_load_initial_conftests(
|
||||
early_config: Config, parser: Parser, args: List[str]
|
||||
) -> None: ...
|
||||
|
||||
|
||||
Collection changes in pytest 8
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -907,8 +942,7 @@ Applying marks to values of a ``pytest.mark.parametrize`` call is now deprecated
|
||||
(50, 500),
|
||||
],
|
||||
)
|
||||
def test_foo(a, b):
|
||||
...
|
||||
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.
|
||||
@@ -931,8 +965,7 @@ To update the code, use ``pytest.param``:
|
||||
(50, 500),
|
||||
],
|
||||
)
|
||||
def test_foo(a, b):
|
||||
...
|
||||
def test_foo(a, b): ...
|
||||
|
||||
|
||||
.. _pytest_funcarg__ prefix deprecated:
|
||||
@@ -1083,15 +1116,13 @@ This is just a matter of renaming the fixture as the API is the same:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_foo(record_xml_property):
|
||||
...
|
||||
def test_foo(record_xml_property): ...
|
||||
|
||||
Change to:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_foo(record_property):
|
||||
...
|
||||
def test_foo(record_property): ...
|
||||
|
||||
|
||||
.. _passing command-line string to pytest.main deprecated:
|
||||
@@ -1253,8 +1284,7 @@ Example of usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MySymbol:
|
||||
...
|
||||
class MySymbol: ...
|
||||
|
||||
|
||||
def pytest_namespace():
|
||||
|
||||
@@ -172,7 +172,7 @@ class TestRaises:
|
||||
raise ValueError("demo error")
|
||||
|
||||
def test_tupleerror(self):
|
||||
a, b = [1] # NOQA
|
||||
a, b = [1] # noqa: F841
|
||||
|
||||
def test_reinterpret_fails_with_print_for_the_fun_of_it(self):
|
||||
items = [1, 2, 3]
|
||||
@@ -180,7 +180,7 @@ class TestRaises:
|
||||
a, b = items.pop()
|
||||
|
||||
def test_some_error(self):
|
||||
if namenotexi: # NOQA
|
||||
if namenotexi: # noqa: F821
|
||||
pass
|
||||
|
||||
def func1(self):
|
||||
|
||||
@@ -2,6 +2,7 @@ import os.path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
mydir = os.path.dirname(__file__)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import os.path
|
||||
import shutil
|
||||
|
||||
|
||||
failure_demo = os.path.join(os.path.dirname(__file__), "failure_demo.py")
|
||||
pytest_plugins = ("pytester",)
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ For basic examples, see
|
||||
- :ref:`Fixtures <fixtures>` for basic fixture/setup examples
|
||||
- :ref:`parametrize` for basic test function parametrization
|
||||
- :ref:`unittest` for basic unittest integration
|
||||
- :ref:`noseintegration` for basic nosetests integration
|
||||
|
||||
The following examples aim at various use cases you might encounter.
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Module containing a parametrized tests testing cross-python serialization
|
||||
via the pickle module."""
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import textwrap
|
||||
@@ -32,14 +33,12 @@ class Python:
|
||||
dumpfile = self.picklefile.with_name("dump.py")
|
||||
dumpfile.write_text(
|
||||
textwrap.dedent(
|
||||
r"""
|
||||
rf"""
|
||||
import pickle
|
||||
f = open({!r}, 'wb')
|
||||
s = pickle.dump({!r}, f, protocol=2)
|
||||
f = open({str(self.picklefile)!r}, 'wb')
|
||||
s = pickle.dump({obj!r}, f, protocol=2)
|
||||
f.close()
|
||||
""".format(
|
||||
str(self.picklefile), obj
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
subprocess.run((self.pythonpath, str(dumpfile)), check=True)
|
||||
@@ -48,17 +47,15 @@ class Python:
|
||||
loadfile = self.picklefile.with_name("load.py")
|
||||
loadfile.write_text(
|
||||
textwrap.dedent(
|
||||
r"""
|
||||
rf"""
|
||||
import pickle
|
||||
f = open({!r}, 'rb')
|
||||
f = open({str(self.picklefile)!r}, 'rb')
|
||||
obj = pickle.load(f)
|
||||
f.close()
|
||||
res = eval({!r})
|
||||
res = eval({expression!r})
|
||||
if not res:
|
||||
raise SystemExit(1)
|
||||
""".format(
|
||||
str(self.picklefile), expression
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
print(loadfile)
|
||||
|
||||
@@ -162,7 +162,7 @@ objects, they are still using the default pytest representation:
|
||||
rootdir: /home/sweet/project
|
||||
collected 8 items
|
||||
|
||||
<Dir parametrize.rst-189>
|
||||
<Dir parametrize.rst-198>
|
||||
<Module test_time.py>
|
||||
<Function test_timedistance_v0[a0-b0-expected0]>
|
||||
<Function test_timedistance_v0[a1-b1-expected1]>
|
||||
@@ -239,7 +239,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia
|
||||
rootdir: /home/sweet/project
|
||||
collected 4 items
|
||||
|
||||
<Dir parametrize.rst-189>
|
||||
<Dir parametrize.rst-198>
|
||||
<Module test_scenarios.py>
|
||||
<Class TestSampleWithScenarios>
|
||||
<Function test_demo1[basic]>
|
||||
@@ -318,7 +318,7 @@ Let's first see how it looks like at collection time:
|
||||
rootdir: /home/sweet/project
|
||||
collected 2 items
|
||||
|
||||
<Dir parametrize.rst-189>
|
||||
<Dir parametrize.rst-198>
|
||||
<Module test_backends.py>
|
||||
<Function test_db_initialized[d1]>
|
||||
<Function test_db_initialized[d2]>
|
||||
@@ -505,8 +505,8 @@ Running it results in some skips if we don't have all the python interpreters in
|
||||
. $ pytest -rs -q multipython.py
|
||||
ssssssssssss...ssssssssssss [100%]
|
||||
========================= short test summary info ==========================
|
||||
SKIPPED [12] multipython.py:68: 'python3.9' not found
|
||||
SKIPPED [12] multipython.py:68: 'python3.11' not found
|
||||
SKIPPED [12] multipython.py:65: 'python3.9' not found
|
||||
SKIPPED [12] multipython.py:65: 'python3.11' not found
|
||||
3 passed, 24 skipped in 0.12s
|
||||
|
||||
Parametrization of optional implementations/imports
|
||||
|
||||
@@ -152,7 +152,7 @@ The test collection would look like this:
|
||||
configfile: pytest.ini
|
||||
collected 2 items
|
||||
|
||||
<Dir pythoncollection.rst-190>
|
||||
<Dir pythoncollection.rst-199>
|
||||
<Module check_myapp.py>
|
||||
<Class CheckMyApp>
|
||||
<Function simple_check>
|
||||
@@ -215,7 +215,7 @@ You can always peek at the collection tree without running tests like this:
|
||||
configfile: pytest.ini
|
||||
collected 3 items
|
||||
|
||||
<Dir pythoncollection.rst-190>
|
||||
<Dir pythoncollection.rst-199>
|
||||
<Dir CWD>
|
||||
<Module pythoncollection.py>
|
||||
<Function test_function>
|
||||
|
||||
@@ -445,7 +445,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef0020>
|
||||
|
||||
def test_tupleerror(self):
|
||||
> a, b = [1] # NOQA
|
||||
> a, b = [1] # noqa: F841
|
||||
E ValueError: not enough values to unpack (expected 2, got 1)
|
||||
|
||||
failure_demo.py:175: ValueError
|
||||
@@ -467,7 +467,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef0022>
|
||||
|
||||
def test_some_error(self):
|
||||
> if namenotexi: # NOQA
|
||||
> if namenotexi: # noqa: F821
|
||||
E NameError: name 'namenotexi' is not defined
|
||||
|
||||
failure_demo.py:183: NameError
|
||||
|
||||
@@ -660,6 +660,31 @@ If we run this:
|
||||
E assert 0
|
||||
|
||||
test_step.py:11: AssertionError
|
||||
================================ XFAILURES =================================
|
||||
______________________ TestUserHandling.test_deletion ______________________
|
||||
|
||||
item = <Function test_deletion>
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
if "incremental" in item.keywords:
|
||||
# retrieve the class name of the test
|
||||
cls_name = str(item.cls)
|
||||
# check if a previous test has failed for this class
|
||||
if cls_name in _test_failed_incremental:
|
||||
# retrieve the index of the test (if parametrize is used in combination with incremental)
|
||||
parametrize_index = (
|
||||
tuple(item.callspec.indices.values())
|
||||
if hasattr(item, "callspec")
|
||||
else ()
|
||||
)
|
||||
# retrieve the name of the first test function to fail for this class name and index
|
||||
test_name = _test_failed_incremental[cls_name].get(parametrize_index, None)
|
||||
# if name found, test has failed for the combination of class name & test name
|
||||
if test_name is not None:
|
||||
> pytest.xfail(f"previous test failed ({test_name})")
|
||||
E _pytest.outcomes.XFailed: previous test failed (test_modification)
|
||||
|
||||
conftest.py:47: XFailed
|
||||
========================= short test summary info ==========================
|
||||
XFAIL test_step.py::TestUserHandling::test_deletion - reason: previous test failed (test_modification)
|
||||
================== 1 failed, 2 passed, 1 xfailed in 0.12s ==================
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import pytest
|
||||
|
||||
|
||||
xfail = pytest.mark.xfail
|
||||
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ style of setup/teardown functions:
|
||||
In addition, pytest continues to support :ref:`xunitsetup`. You can mix
|
||||
both styles, moving incrementally from classic to new style, as you
|
||||
prefer. You can also start out from existing :ref:`unittest.TestCase
|
||||
style <unittest.TestCase>` or :ref:`nose based <nosestyle>` projects.
|
||||
style <unittest.TestCase>`.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -52,10 +52,9 @@ Plugins
|
||||
|
||||
Rerunning any failed tests can mitigate the negative effects of flaky tests by giving them additional chances to pass, so that the overall build does not fail. Several pytest plugins support this:
|
||||
|
||||
* `flaky <https://github.com/box/flaky>`_
|
||||
* `pytest-flakefinder <https://github.com/dropbox/pytest-flakefinder>`_ - `blog post <https://blogs.dropbox.com/tech/2016/03/open-sourcing-pytest-tools/>`_
|
||||
* `pytest-rerunfailures <https://github.com/pytest-dev/pytest-rerunfailures>`_
|
||||
* `pytest-replay <https://github.com/ESSS/pytest-replay>`_: This plugin helps to reproduce locally crashes or flaky tests observed during CI runs.
|
||||
* `pytest-flakefinder <https://github.com/dropbox/pytest-flakefinder>`_ - `blog post <https://blogs.dropbox.com/tech/2016/03/open-sourcing-pytest-tools/>`_
|
||||
|
||||
Plugins to deliberately randomize tests can help expose tests with state problems:
|
||||
|
||||
@@ -106,7 +105,7 @@ This is a limited list, please submit an issue or pull request to expand it!
|
||||
* Gao, Zebao, Yalan Liang, Myra B. Cohen, Atif M. Memon, and Zhen Wang. "Making system user interactive tests repeatable: When and what should we control?." In *Software Engineering (ICSE), 2015 IEEE/ACM 37th IEEE International Conference on*, vol. 1, pp. 55-65. IEEE, 2015. `PDF <http://www.cs.umd.edu/~atif/pubs/gao-icse15.pdf>`__
|
||||
* Palomba, Fabio, and Andy Zaidman. "Does refactoring of test smells induce fixing flaky tests?." In *Software Maintenance and Evolution (ICSME), 2017 IEEE International Conference on*, pp. 1-12. IEEE, 2017. `PDF in Google Drive <https://drive.google.com/file/d/10HdcCQiuQVgW3yYUJD-TSTq1NbYEprl0/view>`__
|
||||
* Bell, Jonathan, Owolabi Legunsen, Michael Hilton, Lamyaa Eloussi, Tifany Yung, and Darko Marinov. "DeFlaker: Automatically detecting flaky tests." In *Proceedings of the 2018 International Conference on Software Engineering*. 2018. `PDF <https://www.jonbell.net/icse18-deflaker.pdf>`__
|
||||
|
||||
* Dutta, Saikat and Shi, August and Choudhary, Rutvik and Zhang, Zhekun and Jain, Aryaman and Misailovic, Sasa. "Detecting flaky tests in probabilistic and machine learning applications." In *Proceedings of the 29th ACM SIGSOFT International Symposium on Software Testing and Analysis (ISSTA)*, pp. 211-224. ACM, 2020. `PDF <https://www.cs.cornell.edu/~saikatd/papers/flash-issta20.pdf>`__
|
||||
|
||||
Resources
|
||||
^^^^^^^^^
|
||||
|
||||
@@ -60,8 +60,10 @@ Within Python modules, ``pytest`` also discovers tests using the standard
|
||||
:ref:`unittest.TestCase <unittest.TestCase>` subclassing technique.
|
||||
|
||||
|
||||
Choosing a test layout / import rules
|
||||
-------------------------------------
|
||||
.. _`test layout`:
|
||||
|
||||
Choosing a test layout
|
||||
----------------------
|
||||
|
||||
``pytest`` supports two common test layouts:
|
||||
|
||||
|
||||
@@ -10,19 +10,27 @@ Import modes
|
||||
|
||||
pytest as a testing framework needs to import test modules and ``conftest.py`` files for execution.
|
||||
|
||||
Importing files in Python (at least until recently) is a non-trivial processes, often requiring
|
||||
changing :data:`sys.path`. Some aspects of the
|
||||
Importing files in Python is a non-trivial processes, so aspects of the
|
||||
import process can be controlled through the ``--import-mode`` command-line flag, which can assume
|
||||
these values:
|
||||
|
||||
* ``prepend`` (default): the directory path containing each module will be inserted into the *beginning*
|
||||
of :py:data:`sys.path` if not already there, and then imported with the :func:`importlib.import_module <importlib.import_module>` function.
|
||||
.. _`import-mode-prepend`:
|
||||
|
||||
This requires test module names to be unique when the test directory tree is not arranged in
|
||||
packages, because the modules will put in :py:data:`sys.modules` after importing.
|
||||
* ``prepend`` (default): the directory path containing each module will be inserted into the *beginning*
|
||||
of :py:data:`sys.path` if not already there, and then imported with
|
||||
the :func:`importlib.import_module <importlib.import_module>` function.
|
||||
|
||||
It is highly recommended to arrange your test modules as packages by adding ``__init__.py`` files to your directories
|
||||
containing tests. This will make the tests part of a proper Python package, allowing pytest to resolve their full
|
||||
name (for example ``tests.core.test_core`` for ``test_core.py`` inside the ``tests.core`` package).
|
||||
|
||||
If the test directory tree is not arranged as packages, then each test file needs to have a unique name
|
||||
compared to the other test files, otherwise pytest will raise an error if it finds two tests with the same name.
|
||||
|
||||
This is the classic mechanism, dating back from the time Python 2 was still supported.
|
||||
|
||||
.. _`import-mode-append`:
|
||||
|
||||
* ``append``: the directory containing each module is appended to the end of :py:data:`sys.path` if not already
|
||||
there, and imported with :func:`importlib.import_module <importlib.import_module>`.
|
||||
|
||||
@@ -38,32 +46,78 @@ these values:
|
||||
the tests will run against the installed version
|
||||
of ``pkg_under_test`` when ``--import-mode=append`` is used whereas
|
||||
with ``prepend`` they would pick up the local version. This kind of confusion is why
|
||||
we advocate for using :ref:`src <src-layout>` layouts.
|
||||
we advocate for using :ref:`src-layouts <src-layout>`.
|
||||
|
||||
Same as ``prepend``, requires test module names to be unique when the test directory tree is
|
||||
not arranged in packages, because the modules will put in :py:data:`sys.modules` after importing.
|
||||
|
||||
* ``importlib``: new in pytest-6.0, this mode uses more fine control mechanisms provided by :mod:`importlib` to import test modules. This gives full control over the import process, and doesn't require changing :py:data:`sys.path`.
|
||||
.. _`import-mode-importlib`:
|
||||
|
||||
For this reason this doesn't require test module names to be unique.
|
||||
* ``importlib``: this mode uses more fine control mechanisms provided by :mod:`importlib` to import test modules, without changing :py:data:`sys.path`.
|
||||
|
||||
One drawback however is that test modules are non-importable by each other. Also, utility
|
||||
modules in the tests directories are not automatically importable because the tests directory is no longer
|
||||
added to :py:data:`sys.path`.
|
||||
Advantages of this mode:
|
||||
|
||||
Initially we intended to make ``importlib`` the default in future releases, however it is clear now that
|
||||
it has its own set of drawbacks so the default will remain ``prepend`` for the foreseeable future.
|
||||
* pytest will not change :py:data:`sys.path` at all.
|
||||
* Test module names do not need to be unique -- pytest will generate a unique name automatically based on the ``rootdir``.
|
||||
|
||||
Disadvantages:
|
||||
|
||||
* Test modules can't import each other.
|
||||
* Testing utility modules in the tests directories (for example a ``tests.helpers`` module containing test-related functions/classes)
|
||||
are not importable. The recommendation in this case it to place testing utility modules together with the application/library
|
||||
code, for example ``app.testing.helpers``.
|
||||
|
||||
Important: by "test utility modules" we mean functions/classes which are imported by
|
||||
other tests directly; this does not include fixtures, which should be placed in ``conftest.py`` files, along
|
||||
with the test modules, and are discovered automatically by pytest.
|
||||
|
||||
It works like this:
|
||||
|
||||
1. Given a certain module path, for example ``tests/core/test_models.py``, derives a canonical name
|
||||
like ``tests.core.test_models`` and tries to import it.
|
||||
|
||||
For non-test modules this will work if they are accessible via :py:data:`sys.path`, so
|
||||
for example ``.env/lib/site-packages/app/core.py`` will be importable as ``app.core``.
|
||||
This is happens when plugins import non-test modules (for example doctesting).
|
||||
|
||||
If this step succeeds, the module is returned.
|
||||
|
||||
For test modules, unless they are reachable from :py:data:`sys.path`, this step will fail.
|
||||
|
||||
2. If the previous step fails, we import the module directly using ``importlib`` facilities, which lets us import it without
|
||||
changing :py:data:`sys.path`.
|
||||
|
||||
Because Python requires the module to also be available in :py:data:`sys.modules`, pytest derives a unique name for it based
|
||||
on its relative location from the ``rootdir``, and adds the module to :py:data:`sys.modules`.
|
||||
|
||||
For example, ``tests/core/test_models.py`` will end up being imported as the module ``tests.core.test_models``.
|
||||
|
||||
.. versionadded:: 6.0
|
||||
|
||||
.. note::
|
||||
|
||||
Initially we intended to make ``importlib`` the default in future releases, however it is clear now that
|
||||
it has its own set of drawbacks so the default will remain ``prepend`` for the foreseeable future.
|
||||
|
||||
.. note::
|
||||
|
||||
By default, pytest will not attempt to resolve namespace packages automatically, but that can
|
||||
be changed via the :confval:`consider_namespace_packages` configuration variable.
|
||||
|
||||
.. seealso::
|
||||
|
||||
The :confval:`pythonpath` configuration variable.
|
||||
|
||||
The :confval:`consider_namespace_packages` configuration variable.
|
||||
|
||||
:ref:`test layout`.
|
||||
|
||||
|
||||
``prepend`` and ``append`` import modes scenarios
|
||||
-------------------------------------------------
|
||||
|
||||
Here's a list of scenarios when using ``prepend`` or ``append`` import modes where pytest needs to
|
||||
change ``sys.path`` in order to import test modules or ``conftest.py`` files, and the issues users
|
||||
change :py:data:`sys.path` in order to import test modules or ``conftest.py`` files, and the issues users
|
||||
might encounter because of that.
|
||||
|
||||
Test modules / ``conftest.py`` files inside packages
|
||||
@@ -92,7 +146,7 @@ pytest will find ``foo/bar/tests/test_foo.py`` and realize it is part of a packa
|
||||
there's an ``__init__.py`` file in the same folder. It will then search upwards until it can find the
|
||||
last folder which still contains an ``__init__.py`` file in order to find the package *root* (in
|
||||
this case ``foo/``). To load the module, it will insert ``root/`` to the front of
|
||||
``sys.path`` (if not there already) in order to load
|
||||
:py:data:`sys.path` (if not there already) in order to load
|
||||
``test_foo.py`` as the *module* ``foo.bar.tests.test_foo``.
|
||||
|
||||
The same logic applies to the ``conftest.py`` file: it will be imported as ``foo.conftest`` module.
|
||||
@@ -122,8 +176,8 @@ When executing:
|
||||
|
||||
pytest will find ``foo/bar/tests/test_foo.py`` and realize it is NOT part of a package given that
|
||||
there's no ``__init__.py`` file in the same folder. It will then add ``root/foo/bar/tests`` to
|
||||
``sys.path`` in order to import ``test_foo.py`` as the *module* ``test_foo``. The same is done
|
||||
with the ``conftest.py`` file by adding ``root/foo`` to ``sys.path`` to import it as ``conftest``.
|
||||
:py:data:`sys.path` in order to import ``test_foo.py`` as the *module* ``test_foo``. The same is done
|
||||
with the ``conftest.py`` file by adding ``root/foo`` to :py:data:`sys.path` to import it as ``conftest``.
|
||||
|
||||
For this reason this layout cannot have test modules with the same name, as they all will be
|
||||
imported in the global import namespace.
|
||||
@@ -136,7 +190,7 @@ Invoking ``pytest`` versus ``python -m pytest``
|
||||
-----------------------------------------------
|
||||
|
||||
Running pytest with ``pytest [...]`` instead of ``python -m pytest [...]`` yields nearly
|
||||
equivalent behaviour, except that the latter will add the current directory to ``sys.path``, which
|
||||
equivalent behaviour, except that the latter will add the current directory to :py:data:`sys.path`, which
|
||||
is standard ``python`` behavior.
|
||||
|
||||
See also :ref:`invoke-python`.
|
||||
|
||||
@@ -99,8 +99,7 @@ sets. pytest-2.3 introduces a decorator for use on the factory itself:
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.fixture(params=["mysql", "pg"])
|
||||
def db(request):
|
||||
... # use request.param
|
||||
def db(request): ... # use request.param
|
||||
|
||||
Here the factory will be invoked twice (with the respective "mysql"
|
||||
and "pg" values set as ``request.param`` attributes) and all of
|
||||
@@ -141,8 +140,7 @@ argument:
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.fixture()
|
||||
def db(request):
|
||||
...
|
||||
def db(request): ...
|
||||
|
||||
The name under which the funcarg resource can be requested is ``db``.
|
||||
|
||||
@@ -151,8 +149,7 @@ aka:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def pytest_funcarg__db(request):
|
||||
...
|
||||
def pytest_funcarg__db(request): ...
|
||||
|
||||
|
||||
But it is then not possible to define scoping and parametrization.
|
||||
|
||||
@@ -22,7 +22,7 @@ Install ``pytest``
|
||||
.. code-block:: bash
|
||||
|
||||
$ pytest --version
|
||||
pytest 8.0.0rc1
|
||||
pytest 8.2.0
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
|
||||
@@ -227,8 +227,7 @@ to use strings:
|
||||
|
||||
|
||||
@pytest.mark.skipif("sys.version_info >= (3,3)")
|
||||
def test_function():
|
||||
...
|
||||
def test_function(): ...
|
||||
|
||||
During test function setup the skipif condition is evaluated by calling
|
||||
``eval('sys.version_info >= (3,0)', namespace)``. The namespace contains
|
||||
@@ -262,8 +261,7 @@ configuration value which you might have added:
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.skipif("not config.getvalue('db')")
|
||||
def test_function():
|
||||
...
|
||||
def test_function(): ...
|
||||
|
||||
The equivalent with "boolean conditions" is:
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ method to test for exceptions returned as part of an :class:`ExceptionGroup`:
|
||||
.. code-block:: python
|
||||
|
||||
def test_exception_in_group():
|
||||
with pytest.raises(RuntimeError) as excinfo:
|
||||
with pytest.raises(ExceptionGroup) as excinfo:
|
||||
raise ExceptionGroup(
|
||||
"Group message",
|
||||
[
|
||||
@@ -176,7 +176,7 @@ exception at a specific level; exceptions contained directly in the top
|
||||
.. code-block:: python
|
||||
|
||||
def test_exception_in_group_at_given_depth():
|
||||
with pytest.raises(RuntimeError) as excinfo:
|
||||
with pytest.raises(ExceptionGroup) as excinfo:
|
||||
raise ExceptionGroup(
|
||||
"Group message",
|
||||
[
|
||||
|
||||
@@ -224,6 +224,7 @@ place the objects you want to appear in the doctest namespace:
|
||||
.. code-block:: python
|
||||
|
||||
# content of conftest.py
|
||||
import pytest
|
||||
import numpy
|
||||
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ How to use pytest with an existing test suite
|
||||
==============================================
|
||||
|
||||
Pytest can be used with most existing test suites, but its
|
||||
behavior differs from other test runners such as :ref:`nose <noseintegration>` or
|
||||
Python's default unittest framework.
|
||||
behavior differs from other test runners such as Python's
|
||||
default unittest framework.
|
||||
|
||||
Before using this section you will want to :ref:`install pytest <getstarted>`.
|
||||
|
||||
|
||||
@@ -494,7 +494,7 @@ Fixtures are created when first requested by a test, and are destroyed based on
|
||||
* ``function``: the default scope, the fixture is destroyed at the end of the test.
|
||||
* ``class``: the fixture is destroyed during teardown of the last test in the class.
|
||||
* ``module``: the fixture is destroyed during teardown of the last test in the module.
|
||||
* ``package``: the fixture is destroyed during teardown of the last test in the package.
|
||||
* ``package``: the fixture is destroyed during teardown of the last test in the package where the fixture is defined, including sub-packages and sub-directories within it.
|
||||
* ``session``: the fixture is destroyed at the end of the test session.
|
||||
|
||||
.. note::
|
||||
@@ -1418,7 +1418,7 @@ Running the above tests results in the following test IDs being used:
|
||||
rootdir: /home/sweet/project
|
||||
collected 12 items
|
||||
|
||||
<Dir fixtures.rst-208>
|
||||
<Dir fixtures.rst-217>
|
||||
<Module test_anothersmtp.py>
|
||||
<Function test_showhelo[smtp.gmail.com]>
|
||||
<Function test_showhelo[mail.python.org]>
|
||||
@@ -1721,8 +1721,7 @@ You can specify multiple fixtures like this:
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.usefixtures("cleandir", "anotherfixture")
|
||||
def test():
|
||||
...
|
||||
def test(): ...
|
||||
|
||||
and you may specify fixture usage at the test module level using :globalvar:`pytestmark`:
|
||||
|
||||
@@ -1750,8 +1749,7 @@ into an ini-file:
|
||||
|
||||
@pytest.mark.usefixtures("my_other_fixture")
|
||||
@pytest.fixture
|
||||
def my_fixture_that_sadly_wont_use_my_other_fixture():
|
||||
...
|
||||
def my_fixture_that_sadly_wont_use_my_other_fixture(): ...
|
||||
|
||||
This generates a deprecation warning, and will become an error in Pytest 8.
|
||||
|
||||
|
||||
@@ -52,7 +52,6 @@ pytest and other test systems
|
||||
|
||||
existingtestsuite
|
||||
unittest
|
||||
nose
|
||||
xunit_setup
|
||||
|
||||
pytest development environment
|
||||
|
||||
@@ -206,8 +206,9 @@ option names are:
|
||||
* ``log_cli_date_format``
|
||||
|
||||
If you need to record the whole test suite logging calls to a file, you can pass
|
||||
``--log-file=/path/to/log/file``. This log file is opened in write mode which
|
||||
``--log-file=/path/to/log/file``. This log file is opened in write mode by default which
|
||||
means that it will be overwritten at each run tests session.
|
||||
If you'd like the file opened in append mode instead, then you can pass ``--log-file-mode=a``.
|
||||
Note that relative paths for the log-file location, whether passed on the CLI or declared in a
|
||||
config file, are always resolved relative to the current working directory.
|
||||
|
||||
@@ -223,12 +224,13 @@ All of the log file options can also be set in the configuration INI file. The
|
||||
option names are:
|
||||
|
||||
* ``log_file``
|
||||
* ``log_file_mode``
|
||||
* ``log_file_level``
|
||||
* ``log_file_format``
|
||||
* ``log_file_date_format``
|
||||
|
||||
You can call ``set_log_path()`` to customize the log_file path dynamically. This functionality
|
||||
is considered **experimental**.
|
||||
is considered **experimental**. Note that ``set_log_path()`` respects the ``log_file_mode`` option.
|
||||
|
||||
.. _log_colors:
|
||||
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
.. _`noseintegration`:
|
||||
|
||||
How to run tests written for nose
|
||||
=======================================
|
||||
|
||||
``pytest`` has basic support for running tests written for nose_.
|
||||
|
||||
.. warning::
|
||||
This functionality has been deprecated and is likely to be removed in ``pytest 8.x``.
|
||||
|
||||
.. _nosestyle:
|
||||
|
||||
Usage
|
||||
-------------
|
||||
|
||||
After :ref:`installation` type:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python setup.py develop # make sure tests can import our package
|
||||
pytest # instead of 'nosetests'
|
||||
|
||||
and you should be able to run your nose style tests and
|
||||
make use of pytest's capabilities.
|
||||
|
||||
Supported nose Idioms
|
||||
----------------------
|
||||
|
||||
* ``setup()`` and ``teardown()`` at module/class/method level: any function or method called ``setup`` will be called during the setup phase for each test, same for ``teardown``.
|
||||
* ``SkipTest`` exceptions and markers
|
||||
* setup/teardown decorators
|
||||
* ``__test__`` attribute on modules/classes/functions
|
||||
* general usage of nose utilities
|
||||
|
||||
Unsupported idioms / known issues
|
||||
----------------------------------
|
||||
|
||||
- unittest-style ``setUp, tearDown, setUpClass, tearDownClass``
|
||||
are recognized only on ``unittest.TestCase`` classes but not
|
||||
on plain classes. ``nose`` supports these methods also on plain
|
||||
classes but pytest deliberately does not. As nose and pytest already
|
||||
both support ``setup_class, teardown_class, setup_method, teardown_method``
|
||||
it doesn't seem useful to duplicate the unittest-API like nose does.
|
||||
If you however rather think pytest should support the unittest-spelling on
|
||||
plain classes please post to :issue:`377`.
|
||||
|
||||
- nose imports test modules with the same import path (e.g.
|
||||
``tests.test_mode``) but different file system paths
|
||||
(e.g. ``tests/test_mode.py`` and ``other/tests/test_mode.py``)
|
||||
by extending sys.path/import semantics. pytest does not do that. Note that
|
||||
`nose2 choose to avoid this sys.path/import hackery <https://nose2.readthedocs.io/en/latest/differences.html#test-discovery-and-loading>`_.
|
||||
|
||||
If you place a conftest.py file in the root directory of your project
|
||||
(as determined by pytest) pytest will run tests "nose style" against
|
||||
the code below that directory by adding it to your ``sys.path`` instead of
|
||||
running against your installed code.
|
||||
|
||||
You may find yourself wanting to do this if you ran ``python setup.py install``
|
||||
to set up your project, as opposed to ``python setup.py develop`` or any of
|
||||
the package manager equivalents. Installing with develop in a
|
||||
virtual environment like tox is recommended over this pattern.
|
||||
|
||||
- nose-style doctests are not collected and executed correctly,
|
||||
also doctest fixtures don't work.
|
||||
|
||||
- no nose-configuration is recognized.
|
||||
|
||||
- ``yield``-based methods are
|
||||
fundamentally incompatible with pytest because they don't support fixtures
|
||||
properly since collection and test execution are separated.
|
||||
|
||||
Here is a table comparing the default supported naming conventions for both
|
||||
nose and pytest.
|
||||
|
||||
========= ========================== ======= =====
|
||||
what default naming convention pytest nose
|
||||
========= ========================== ======= =====
|
||||
module ``test*.py`` ✅
|
||||
module ``test_*.py`` ✅ ✅
|
||||
module ``*_test.py`` ✅
|
||||
module ``*_tests.py``
|
||||
class ``*(unittest.TestCase)`` ✅ ✅
|
||||
method ``test_*`` ✅ ✅
|
||||
class ``Test*`` ✅
|
||||
method ``test_*`` ✅
|
||||
function ``test_*`` ✅
|
||||
========= ========================== ======= =====
|
||||
|
||||
|
||||
Migrating from nose to pytest
|
||||
------------------------------
|
||||
|
||||
`nose2pytest <https://github.com/pytest-dev/nose2pytest>`_ is a Python script
|
||||
and pytest plugin to help convert Nose-based tests into pytest-based tests.
|
||||
Specifically, the script transforms ``nose.tools.assert_*`` function calls into
|
||||
raw assert statements, while preserving format of original arguments
|
||||
as much as possible.
|
||||
|
||||
.. _nose: https://nose.readthedocs.io/en/latest/
|
||||
@@ -294,9 +294,47 @@ Now if we increase verbosity even more:
|
||||
|
||||
test_verbosity_example.py:19: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_verbosity_example.py::test_words_fail - AssertionError: asser...
|
||||
FAILED test_verbosity_example.py::test_numbers_fail - AssertionError: ass...
|
||||
FAILED test_verbosity_example.py::test_long_text_fail - AssertionError: a...
|
||||
FAILED test_verbosity_example.py::test_words_fail - AssertionError: assert ['banana', 'apple', 'grapes', 'melon', 'kiwi'] == ['banana', 'apple', 'orange', 'melon', 'kiwi']
|
||||
|
||||
At index 2 diff: 'grapes' != 'orange'
|
||||
|
||||
Full diff:
|
||||
[
|
||||
'banana',
|
||||
'apple',
|
||||
- 'orange',
|
||||
? ^ ^^
|
||||
+ 'grapes',
|
||||
? ^ ^ +
|
||||
'melon',
|
||||
'kiwi',
|
||||
]
|
||||
FAILED test_verbosity_example.py::test_numbers_fail - AssertionError: assert {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4} == {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}
|
||||
|
||||
Common items:
|
||||
{'0': 0}
|
||||
Left contains 4 more items:
|
||||
{'1': 1, '2': 2, '3': 3, '4': 4}
|
||||
Right contains 4 more items:
|
||||
{'10': 10, '20': 20, '30': 30, '40': 40}
|
||||
|
||||
Full diff:
|
||||
{
|
||||
'0': 0,
|
||||
- '10': 10,
|
||||
? - -
|
||||
+ '1': 1,
|
||||
- '20': 20,
|
||||
? - -
|
||||
+ '2': 2,
|
||||
- '30': 30,
|
||||
? - -
|
||||
+ '3': 3,
|
||||
- '40': 40,
|
||||
? - -
|
||||
+ '4': 4,
|
||||
}
|
||||
FAILED test_verbosity_example.py::test_long_text_fail - AssertionError: assert 'hello world' in 'Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet '
|
||||
======================= 3 failed, 1 passed in 0.12s ========================
|
||||
|
||||
Notice now that:
|
||||
@@ -325,7 +363,9 @@ This is done by setting a verbosity level in the configuration file for the spec
|
||||
``pytest --no-header`` with a value of ``2`` would have the same output as the previous example, but each test inside
|
||||
the file is shown by a single character in the output.
|
||||
|
||||
(Note: currently this is the only option available, but more might be added in the future).
|
||||
:confval:`verbosity_test_cases`: Controls how verbose the test execution output should be when pytest is executed.
|
||||
Running ``pytest --no-header`` with a value of ``2`` would have the same output as the first verbosity example, but each
|
||||
test inside the file gets its own line in the output.
|
||||
|
||||
.. _`pytest.detailed_failed_tests_usage`:
|
||||
|
||||
@@ -404,10 +444,19 @@ Example:
|
||||
E assert 0
|
||||
|
||||
test_example.py:14: AssertionError
|
||||
================================ XFAILURES =================================
|
||||
________________________________ test_xfail ________________________________
|
||||
|
||||
def test_xfail():
|
||||
> pytest.xfail("xfailing this test")
|
||||
E _pytest.outcomes.XFailed: xfailing this test
|
||||
|
||||
test_example.py:26: XFailed
|
||||
================================= XPASSES ==================================
|
||||
========================= short test summary info ==========================
|
||||
SKIPPED [1] test_example.py:22: skipping this test
|
||||
XFAIL test_example.py::test_xfail - reason: xfailing this test
|
||||
XPASS test_example.py::test_xpass always xfail
|
||||
XPASS test_example.py::test_xpass - always xfail
|
||||
ERROR test_example.py::test_error - assert 0
|
||||
FAILED test_example.py::test_fail - assert 0
|
||||
== 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s ===
|
||||
|
||||
@@ -47,8 +47,7 @@ which may be passed an optional ``reason``:
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.skip(reason="no way of currently testing this")
|
||||
def test_the_unknown():
|
||||
...
|
||||
def test_the_unknown(): ...
|
||||
|
||||
|
||||
Alternatively, it is also possible to skip imperatively during test execution or setup
|
||||
@@ -93,8 +92,7 @@ when run on an interpreter earlier than Python3.10:
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires python3.10 or higher")
|
||||
def test_function():
|
||||
...
|
||||
def test_function(): ...
|
||||
|
||||
If the condition evaluates to ``True`` during collection, the test function will be skipped,
|
||||
with the specified reason appearing in the summary when using ``-rs``.
|
||||
@@ -112,8 +110,7 @@ You can share ``skipif`` markers between modules. Consider this test module:
|
||||
|
||||
|
||||
@minversion
|
||||
def test_function():
|
||||
...
|
||||
def test_function(): ...
|
||||
|
||||
You can import the marker and reuse it in another test module:
|
||||
|
||||
@@ -124,8 +121,7 @@ You can import the marker and reuse it in another test module:
|
||||
|
||||
|
||||
@minversion
|
||||
def test_anotherfunction():
|
||||
...
|
||||
def test_anotherfunction(): ...
|
||||
|
||||
For larger test suites it's usually a good idea to have one file
|
||||
where you define the markers which you then consistently apply
|
||||
@@ -232,8 +228,7 @@ expect a test to fail:
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_function():
|
||||
...
|
||||
def test_function(): ...
|
||||
|
||||
This test will run but no traceback will be reported when it fails. Instead, terminal
|
||||
reporting will list it in the "expected to fail" (``XFAIL``) or "unexpectedly
|
||||
@@ -275,8 +270,7 @@ that condition as the first parameter:
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.xfail(sys.platform == "win32", reason="bug in a 3rd party library")
|
||||
def test_function():
|
||||
...
|
||||
def test_function(): ...
|
||||
|
||||
Note that you have to pass a reason as well (see the parameter description at
|
||||
:ref:`pytest.mark.xfail ref`).
|
||||
@@ -289,8 +283,7 @@ You can specify the motive of an expected failure with the ``reason`` parameter:
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.xfail(reason="known parser issue")
|
||||
def test_function():
|
||||
...
|
||||
def test_function(): ...
|
||||
|
||||
|
||||
``raises`` parameter
|
||||
@@ -302,8 +295,7 @@ a single exception, or a tuple of exceptions, in the ``raises`` argument.
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.xfail(raises=RuntimeError)
|
||||
def test_function():
|
||||
...
|
||||
def test_function(): ...
|
||||
|
||||
Then the test will be reported as a regular failure if it fails with an
|
||||
exception not mentioned in ``raises``.
|
||||
@@ -317,8 +309,7 @@ even executed, use the ``run`` parameter as ``False``:
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.xfail(run=False)
|
||||
def test_function():
|
||||
...
|
||||
def test_function(): ...
|
||||
|
||||
This is specially useful for xfailing tests that are crashing the interpreter and should be
|
||||
investigated later.
|
||||
@@ -334,8 +325,7 @@ You can change this by setting the ``strict`` keyword-only parameter to ``True``
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.xfail(strict=True)
|
||||
def test_function():
|
||||
...
|
||||
def test_function(): ...
|
||||
|
||||
|
||||
This will make ``XPASS`` ("unexpectedly passing") results from this test to fail the test suite.
|
||||
|
||||
@@ -8,9 +8,8 @@ How to use temporary directories and files in tests
|
||||
The ``tmp_path`` fixture
|
||||
------------------------
|
||||
|
||||
You can use the ``tmp_path`` fixture which will
|
||||
provide a temporary directory unique to the test invocation,
|
||||
created in the `base temporary directory`_.
|
||||
You can use the ``tmp_path`` fixture which will provide a temporary directory
|
||||
unique to each test function.
|
||||
|
||||
``tmp_path`` is a :class:`pathlib.Path` object. Here is an example test usage:
|
||||
|
||||
@@ -62,6 +61,11 @@ Running this would result in a passed test except for the last
|
||||
FAILED test_tmp_path.py::test_create_file - assert 0
|
||||
============================ 1 failed in 0.12s =============================
|
||||
|
||||
By default, ``pytest`` retains the temporary directory for the last 3 ``pytest``
|
||||
invocations. Concurrent invocations of the same test function are supported by
|
||||
configuring the base temporary directory to be unique for each concurrent
|
||||
run. See `temporary directory location and retention`_ for details.
|
||||
|
||||
.. _`tmp_path_factory example`:
|
||||
|
||||
The ``tmp_path_factory`` fixture
|
||||
@@ -100,7 +104,7 @@ See :ref:`tmp_path_factory API <tmp_path_factory factory api>` for details.
|
||||
.. _tmpdir:
|
||||
|
||||
The ``tmpdir`` and ``tmpdir_factory`` fixtures
|
||||
---------------------------------------------------
|
||||
----------------------------------------------
|
||||
|
||||
The ``tmpdir`` and ``tmpdir_factory`` fixtures are similar to ``tmp_path``
|
||||
and ``tmp_path_factory``, but use/return legacy `py.path.local`_ objects
|
||||
@@ -124,10 +128,10 @@ See :fixture:`tmpdir <tmpdir>` :fixture:`tmpdir_factory <tmpdir_factory>`
|
||||
API for details.
|
||||
|
||||
|
||||
.. _`base temporary directory`:
|
||||
.. _`temporary directory location and retention`:
|
||||
|
||||
The default base temporary directory
|
||||
-----------------------------------------------
|
||||
Temporary directory location and retention
|
||||
------------------------------------------
|
||||
|
||||
Temporary directories are by default created as sub-directories of
|
||||
the system temporary directory. The base name will be ``pytest-NUM`` where
|
||||
@@ -152,7 +156,7 @@ You can override the default temporary directory setting like this:
|
||||
for that purpose only.
|
||||
|
||||
When distributing tests on the local machine using ``pytest-xdist``, care is taken to
|
||||
automatically configure a basetemp directory for the sub processes such that all temporary
|
||||
data lands below a single per-test run basetemp directory.
|
||||
automatically configure a `basetemp` directory for the sub processes such that all temporary
|
||||
data lands below a single per-test run temporary directory.
|
||||
|
||||
.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
|
||||
|
||||
@@ -17,7 +17,8 @@ in the current directory and its subdirectories. More generally, pytest follows
|
||||
Specifying which tests to run
|
||||
------------------------------
|
||||
|
||||
Pytest supports several ways to run and select tests from the command-line.
|
||||
Pytest supports several ways to run and select tests from the command-line or from a file
|
||||
(see below for :ref:`reading arguments from file <args-from-file>`).
|
||||
|
||||
**Run tests in a module**
|
||||
|
||||
@@ -91,6 +92,28 @@ For more information see :ref:`marks <mark>`.
|
||||
|
||||
This will import ``pkg.testing`` and use its filesystem location to find and run tests from.
|
||||
|
||||
.. _args-from-file:
|
||||
|
||||
**Read arguments from file**
|
||||
|
||||
.. versionadded:: 8.2
|
||||
|
||||
All of the above can be read from a file using the ``@`` prefix:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pytest @tests_to_run.txt
|
||||
|
||||
where ``tests_to_run.txt`` contains an entry per line, e.g.:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
tests/test_file.py
|
||||
tests/test_mod.py::test_func[x1,y2]
|
||||
tests/test_mod.py::TestClass
|
||||
-m slow
|
||||
|
||||
This file can also be generated using ``pytest --collect-only -q`` and modified as needed.
|
||||
|
||||
Getting help on version, option names, environment variables
|
||||
--------------------------------------------------------------
|
||||
|
||||
@@ -100,7 +100,7 @@ object, the wrapper may modify that result, but it's probably better to avoid it
|
||||
|
||||
If the hook implementation failed with an exception, the wrapper can handle that
|
||||
exception using a ``try-catch-finally`` around the ``yield``, by propagating it,
|
||||
supressing it, or raising a different exception entirely.
|
||||
suppressing it, or raising a different exception entirely.
|
||||
|
||||
For more information, consult the
|
||||
:ref:`pluggy documentation about hook wrappers <pluggy:hookwrappers>`.
|
||||
|
||||
@@ -46,24 +46,18 @@ Plugin discovery order at tool startup
|
||||
|
||||
5. by loading all plugins specified through the :envvar:`PYTEST_PLUGINS` environment variable.
|
||||
|
||||
6. by loading all :file:`conftest.py` files as inferred by the command line
|
||||
invocation:
|
||||
6. by loading all "initial ":file:`conftest.py` files:
|
||||
|
||||
- if no test paths are specified, use the current dir as a test path
|
||||
- if exists, load ``conftest.py`` and ``test*/conftest.py`` relative
|
||||
to the directory part of the first test path. After the ``conftest.py``
|
||||
file is loaded, load all plugins specified in its
|
||||
:globalvar:`pytest_plugins` variable if present.
|
||||
|
||||
Note that pytest does not find ``conftest.py`` files in deeper nested
|
||||
sub directories at tool startup. It is usually a good idea to keep
|
||||
your ``conftest.py`` file in the top level test or project root directory.
|
||||
|
||||
7. by recursively loading all plugins specified by the
|
||||
:globalvar:`pytest_plugins` variable in ``conftest.py`` files.
|
||||
- determine the test paths: specified on the command line, otherwise in
|
||||
:confval:`testpaths` if defined and running from the rootdir, otherwise the
|
||||
current dir
|
||||
- for each test path, load ``conftest.py`` and ``test*/conftest.py`` relative
|
||||
to the directory part of the test path, if exist. Before a ``conftest.py``
|
||||
file is loaded, load ``conftest.py`` files in all of its parent directories.
|
||||
After a ``conftest.py`` file is loaded, recursively load all plugins specified
|
||||
in its :globalvar:`pytest_plugins` variable if present.
|
||||
|
||||
|
||||
.. _`pytest/plugin`: http://bitbucket.org/pytest-dev/pytest/src/tip/pytest/plugin/
|
||||
.. _`conftest.py plugins`:
|
||||
.. _`localplugin`:
|
||||
.. _`local conftest plugins`:
|
||||
@@ -108,9 +102,9 @@ Here is how you might run it::
|
||||
See also: :ref:`pythonpath`.
|
||||
|
||||
.. note::
|
||||
Some hooks should be implemented only in plugins or conftest.py files situated at the
|
||||
tests root directory due to how pytest discovers plugins during startup,
|
||||
see the documentation of each hook for details.
|
||||
Some hooks cannot be implemented in conftest.py files which are not
|
||||
:ref:`initial <pluginorder>` due to how pytest discovers plugins during
|
||||
startup. See the documentation of each hook for details.
|
||||
|
||||
Writing your own plugin
|
||||
-----------------------
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
:orphan:
|
||||
|
||||
.. sidebar:: Next Open Trainings
|
||||
.. sidebar:: Next Open Trainings and Events
|
||||
|
||||
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, **March 5th to 7th 2024** (3 day in-depth training), **Leipzig, Germany / Remote**
|
||||
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_ (3 day in-depth training):
|
||||
* **June 11th to 13th 2024**, Remote
|
||||
* **March 4th to 6th 2025**, Leipzig, Germany / Remote
|
||||
- `pytest development sprint <https://github.com/pytest-dev/pytest/discussions/11655>`_, **June 17th -- 22nd 2024**
|
||||
- pytest tips and tricks for a better testsuite, `Europython 2024 <https://ep2024.europython.eu/>`_, **July 8th -- 14th 2024** (3h), Prague
|
||||
|
||||
Also see :doc:`previous talks and blogposts <talks>`.
|
||||
|
||||
@@ -74,17 +78,17 @@ Features
|
||||
|
||||
- :ref:`Modular fixtures <fixture>` for managing small or parametrized long-lived test resources
|
||||
|
||||
- Can run :ref:`unittest <unittest>` (including trial) and :ref:`nose <noseintegration>` test suites out of the box
|
||||
- Can run :ref:`unittest <unittest>` (including trial) test suites out of the box
|
||||
|
||||
- Python 3.8+ or PyPy 3
|
||||
|
||||
- Rich plugin architecture, with over 800+ :ref:`external plugins <plugin-list>` and thriving community
|
||||
- Rich plugin architecture, with over 1300+ :ref:`external plugins <plugin-list>` and thriving community
|
||||
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
* :ref:`Get started <get-started>` - install pytest and grasp its basics just twenty minutes
|
||||
* :ref:`Get started <get-started>` - install pytest and grasp its basics in just twenty minutes
|
||||
* :ref:`How-to guides <how-to>` - step-by-step guides, covering a vast range of use-cases and needs
|
||||
* :ref:`Reference guides <reference>` - includes the complete pytest API reference, lists of plugins and more
|
||||
* :ref:`Explanation <explanation>` - background, discussion of key topics, answers to higher-level questions
|
||||
|
||||
@@ -177,13 +177,20 @@ Files will only be matched for configuration if:
|
||||
* ``tox.ini``: contains a ``[pytest]`` section.
|
||||
* ``setup.cfg``: contains a ``[tool:pytest]`` section.
|
||||
|
||||
Finally, a ``pyproject.toml`` file will be considered the ``configfile`` if no other match was found, in this case
|
||||
even if it does not contain a ``[tool.pytest.ini_options]`` table (this was added in ``8.1``).
|
||||
|
||||
The files are considered in the order above. Options from multiple ``configfiles`` candidates
|
||||
are never merged - the first match wins.
|
||||
|
||||
The configuration file also determines the value of the ``rootpath``.
|
||||
|
||||
The :class:`Config <pytest.Config>` object (accessible via hooks or through the :fixture:`pytestconfig` fixture)
|
||||
will subsequently carry these attributes:
|
||||
|
||||
- :attr:`config.rootpath <pytest.Config.rootpath>`: the determined root directory, guaranteed to exist.
|
||||
- :attr:`config.rootpath <pytest.Config.rootpath>`: the determined root directory, guaranteed to exist. It is used as
|
||||
a reference directory for constructing test addresses ("nodeids") and can be used also by plugins for storing
|
||||
per-testrun information.
|
||||
|
||||
- :attr:`config.inipath <pytest.Config.inipath>`: the determined ``configfile``, may be ``None``
|
||||
(it is named ``inipath`` for historical reasons).
|
||||
@@ -193,9 +200,7 @@ will subsequently carry these attributes:
|
||||
versions of the older ``config.rootdir`` and ``config.inifile``, which have type
|
||||
``py.path.local``, and still exist for backward compatibility.
|
||||
|
||||
The ``rootdir`` is used as a reference directory for constructing test
|
||||
addresses ("nodeids") and can be used also by plugins for storing
|
||||
per-testrun information.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -136,7 +136,7 @@ pytest.freeze_includes
|
||||
Marks
|
||||
-----
|
||||
|
||||
Marks can be used apply meta data to *test functions* (but not fixtures), which can then be accessed by
|
||||
Marks can be used to apply metadata to *test functions* (but not fixtures), which can then be accessed by
|
||||
fixtures or plugins.
|
||||
|
||||
|
||||
@@ -164,8 +164,7 @@ Add warning filters to marked test items.
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:.*usage will be deprecated.*:DeprecationWarning")
|
||||
def test_foo():
|
||||
...
|
||||
def test_foo(): ...
|
||||
|
||||
|
||||
.. _`pytest.mark.parametrize ref`:
|
||||
@@ -276,8 +275,7 @@ For example:
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.timeout(10, "slow", method="thread")
|
||||
def test_function():
|
||||
...
|
||||
def test_function(): ...
|
||||
|
||||
Will create and attach a :class:`Mark <pytest.Mark>` object to the collected
|
||||
:class:`Item <pytest.Item>`, which can then be accessed by fixtures or hooks with
|
||||
@@ -294,8 +292,7 @@ Example for using multiple custom markers:
|
||||
|
||||
@pytest.mark.timeout(10, "slow", method="thread")
|
||||
@pytest.mark.slow
|
||||
def test_function():
|
||||
...
|
||||
def test_function(): ...
|
||||
|
||||
When :meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>` or :meth:`Node.iter_markers_with_node <_pytest.nodes.Node.iter_markers_with_node>` is used with multiple markers, the marker closest to the function will be iterated over first. The above example will result in ``@pytest.mark.slow`` followed by ``@pytest.mark.timeout(...)``.
|
||||
|
||||
@@ -643,8 +640,6 @@ Bootstrapping hooks called for plugins registered early enough (internal and set
|
||||
|
||||
.. hook:: pytest_load_initial_conftests
|
||||
.. autofunction:: pytest_load_initial_conftests
|
||||
.. hook:: pytest_cmdline_preparse
|
||||
.. autofunction:: pytest_cmdline_preparse
|
||||
.. hook:: pytest_cmdline_parse
|
||||
.. autofunction:: pytest_cmdline_parse
|
||||
.. hook:: pytest_cmdline_main
|
||||
@@ -1122,6 +1117,11 @@ When set (regardless of value), pytest acknowledges that is running in a CI proc
|
||||
This contains a command-line (parsed by the py:mod:`shlex` module) that will be **prepended** to the command line given
|
||||
by the user, see :ref:`adding default options` for more information.
|
||||
|
||||
.. envvar:: PYTEST_VERSION
|
||||
|
||||
This environment variable is defined at the start of the pytest session and is undefined afterwards.
|
||||
It contains the value of ``pytest.__version__``, and among other things can be used to easily check if a code is running from within a pytest run.
|
||||
|
||||
.. envvar:: PYTEST_CURRENT_TEST
|
||||
|
||||
This is not meant to be set by users, but is set by pytest internally with the name of the current test so other
|
||||
@@ -1209,9 +1209,6 @@ Custom warnings generated in some situations such as improper usage or deprecate
|
||||
.. autoclass:: pytest.PytestReturnNotNoneWarning
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: pytest.PytestRemovedIn8Warning
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: pytest.PytestRemovedIn9Warning
|
||||
:show-inheritance:
|
||||
|
||||
@@ -1282,6 +1279,18 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
variables, that will be expanded. For more information about cache plugin
|
||||
please refer to :ref:`cache_provider`.
|
||||
|
||||
.. confval:: consider_namespace_packages
|
||||
|
||||
Controls if pytest should attempt to identify `namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages>`__
|
||||
when collecting Python modules. Default is ``False``.
|
||||
|
||||
Set to ``True`` if the package you are testing is part of a namespace package.
|
||||
|
||||
Only `native namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#native-namespace-packages>`__
|
||||
are supported, with no plans to support `legacy namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#legacy-namespace-packages>`__.
|
||||
|
||||
.. versionadded:: 8.1
|
||||
|
||||
.. confval:: console_output_style
|
||||
|
||||
Sets the console output style while running tests:
|
||||
@@ -1873,6 +1882,19 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
"auto" can be used to explicitly use the global verbosity level.
|
||||
|
||||
|
||||
.. confval:: verbosity_test_cases
|
||||
|
||||
Set a verbosity level specifically for test case execution related output, overriding the application wide level.
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
verbosity_test_cases = 2
|
||||
|
||||
Defaults to application wide verbosity level (via the ``-v`` command-line option). A special value of
|
||||
"auto" can be used to explicitly use the global verbosity level.
|
||||
|
||||
|
||||
.. confval:: xfail_strict
|
||||
|
||||
If set to ``True``, tests marked with ``@pytest.mark.xfail`` that actually succeed will by default fail the
|
||||
@@ -2036,7 +2058,7 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
failure
|
||||
--doctest-glob=pat Doctests file matching pattern, default: test*.txt
|
||||
--doctest-ignore-import-errors
|
||||
Ignore doctest ImportErrors
|
||||
Ignore doctest collection errors
|
||||
--doctest-continue-on-failure
|
||||
For a given doctest, continue to run after the first
|
||||
failure
|
||||
@@ -2085,6 +2107,8 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
--log-cli-date-format=LOG_CLI_DATE_FORMAT
|
||||
Log date format used by the logging module
|
||||
--log-file=LOG_FILE Path to a file when logging will be written to
|
||||
--log-file-mode={w,a}
|
||||
Log file open mode
|
||||
--log-file-level=LOG_FILE_LEVEL
|
||||
Log file logging level
|
||||
--log-file-format=LOG_FILE_FORMAT
|
||||
@@ -2100,7 +2124,7 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
|
||||
[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:
|
||||
|
||||
markers (linelist): Markers for test functions
|
||||
markers (linelist): Register new markers for test functions
|
||||
empty_parameter_set_mark (string):
|
||||
Default marker for empty parametersets
|
||||
norecursedirs (args): Directory patterns to avoid for recursion
|
||||
@@ -2110,6 +2134,9 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
Each line specifies a pattern for
|
||||
warnings.filterwarnings. Processed after
|
||||
-W/--pythonwarnings.
|
||||
consider_namespace_packages (bool):
|
||||
Consider namespace packages when resolving module
|
||||
names during import
|
||||
usefixtures (args): List of default fixtures to be used with this
|
||||
project
|
||||
python_files (args): Glob-style file patterns for Python test module
|
||||
@@ -2128,6 +2155,11 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
progress information ("progress" (percentage) |
|
||||
"count" | "progress-even-when-capture-no" (forces
|
||||
progress even when capture=no)
|
||||
verbosity_test_cases (string):
|
||||
Specify a verbosity level for test case execution,
|
||||
overriding the main level. Higher levels will
|
||||
provide more detailed information about each test
|
||||
case executed.
|
||||
xfail_strict (bool): Default for the strict parameter of xfail markers
|
||||
when not given explicitly (default: False)
|
||||
tmp_path_retention_count (string):
|
||||
@@ -2175,6 +2207,8 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
log_cli_date_format (string):
|
||||
Default value for --log-cli-date-format
|
||||
log_file (string): Default value for --log-file
|
||||
log_file_mode (string):
|
||||
Default value for --log-file-mode
|
||||
log_file_level (string):
|
||||
Default value for --log-file-level
|
||||
log_file_format (string):
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
pallets-sphinx-themes
|
||||
pluggy>=1.2.0
|
||||
pluggy>=1.5.0
|
||||
pygments-pytest>=2.3.0
|
||||
sphinx-removed-in>=0.2.0
|
||||
sphinx>=5,<8
|
||||
sphinx>=7
|
||||
sphinxcontrib-trio
|
||||
sphinxcontrib-svg2pdfconverter
|
||||
# Pin packaging because it no longer handles 'latest' version, which
|
||||
|
||||
@@ -3,6 +3,7 @@ from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
issues_url = "https://api.github.com/repos/pytest-dev/pytest/issues"
|
||||
|
||||
|
||||
|
||||
309
pyproject.toml
309
pyproject.toml
@@ -1,13 +1,291 @@
|
||||
[build-system]
|
||||
requires = [
|
||||
"setuptools>=45.0",
|
||||
"setuptools-scm[toml]>=6.2.3",
|
||||
[project]
|
||||
name = "pytest"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
readme = "README.rst"
|
||||
keywords = [
|
||||
"test",
|
||||
"unittest",
|
||||
]
|
||||
license = {text = "MIT"}
|
||||
authors = [
|
||||
{name = "Holger Krekel"},
|
||||
{name = "Bruno Oliveira"},
|
||||
{name = "Ronny Pfannschmidt"},
|
||||
{name = "Floris Bruynooghe"},
|
||||
{name = "Brianna Laugher"},
|
||||
{name = "Florian Bruhin"},
|
||||
{name = "Others (See AUTHORS)"},
|
||||
]
|
||||
requires-python = ">=3.8"
|
||||
classifiers = [
|
||||
"Development Status :: 6 - Mature",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: MacOS",
|
||||
"Operating System :: Microsoft :: Windows",
|
||||
"Operating System :: POSIX",
|
||||
"Operating System :: Unix",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Software Development :: Libraries",
|
||||
"Topic :: Software Development :: Testing",
|
||||
"Topic :: Utilities",
|
||||
]
|
||||
dynamic = [
|
||||
"version",
|
||||
]
|
||||
dependencies = [
|
||||
'colorama; sys_platform == "win32"',
|
||||
'exceptiongroup>=1.0.0rc8; python_version < "3.11"',
|
||||
"iniconfig",
|
||||
"packaging",
|
||||
"pluggy<2.0,>=1.5",
|
||||
'tomli>=1; python_version < "3.11"',
|
||||
]
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"argcomplete",
|
||||
"attrs>=19.2",
|
||||
"hypothesis>=3.56",
|
||||
"mock",
|
||||
"pygments>=2.7.2",
|
||||
"requests",
|
||||
"setuptools",
|
||||
"xmlschema",
|
||||
]
|
||||
[project.urls]
|
||||
Changelog = "https://docs.pytest.org/en/stable/changelog.html"
|
||||
Homepage = "https://docs.pytest.org/en/latest/"
|
||||
Source = "https://github.com/pytest-dev/pytest"
|
||||
Tracker = "https://github.com/pytest-dev/pytest/issues"
|
||||
Twitter = "https://twitter.com/pytestdotorg"
|
||||
[project.scripts]
|
||||
"py.test" = "pytest:console_main"
|
||||
pytest = "pytest:console_main"
|
||||
|
||||
[build-system]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = [
|
||||
"setuptools>=61",
|
||||
"setuptools-scm[toml]>=6.2.3",
|
||||
]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
"_pytest" = ["py.typed"]
|
||||
"pytest" = ["py.typed"]
|
||||
|
||||
[tool.setuptools_scm]
|
||||
write_to = "src/_pytest/_version.py"
|
||||
|
||||
[tool.black]
|
||||
target-version = ['py38']
|
||||
|
||||
[tool.ruff]
|
||||
src = ["src"]
|
||||
line-length = 88
|
||||
|
||||
[tool.ruff.format]
|
||||
docstring-code-format = true
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"B", # bugbear
|
||||
"D", # pydocstyle
|
||||
"E", # pycodestyle
|
||||
"F", # pyflakes
|
||||
"I", # isort
|
||||
"PYI", # flake8-pyi
|
||||
"UP", # pyupgrade
|
||||
"RUF", # ruff
|
||||
"W", # pycodestyle
|
||||
"PIE", # flake8-pie
|
||||
"PGH004", # pygrep-hooks - Use specific rule codes when using noqa
|
||||
"PLE", # pylint error
|
||||
"PLW", # pylint warning
|
||||
"PLR1714", # Consider merging multiple comparisons
|
||||
]
|
||||
ignore = [
|
||||
# bugbear ignore
|
||||
"B004", # Using `hasattr(x, "__call__")` to test if x is callable is unreliable.
|
||||
"B007", # Loop control variable `i` not used within loop body
|
||||
"B009", # Do not call `getattr` with a constant attribute value
|
||||
"B010", # [*] Do not call `setattr` with a constant attribute value.
|
||||
"B011", # Do not `assert False` (`python -O` removes these calls)
|
||||
"B028", # No explicit `stacklevel` keyword argument found
|
||||
# pycodestyle ignore
|
||||
# pytest can do weird low-level things, and we usually know
|
||||
# what we're doing when we use type(..) is ...
|
||||
"E721", # Do not compare types, use `isinstance()`
|
||||
# pydocstyle ignore
|
||||
"D100", # Missing docstring in public module
|
||||
"D101", # Missing docstring in public class
|
||||
"D102", # Missing docstring in public method
|
||||
"D103", # Missing docstring in public function
|
||||
"D104", # Missing docstring in public package
|
||||
"D105", # Missing docstring in magic method
|
||||
"D106", # Missing docstring in public nested class
|
||||
"D107", # Missing docstring in `__init__`
|
||||
"D209", # [*] Multi-line docstring closing quotes should be on a separate line
|
||||
"D205", # 1 blank line required between summary line and description
|
||||
"D400", # First line should end with a period
|
||||
"D401", # First line of docstring should be in imperative mood
|
||||
"D402", # First line should not be the function's signature
|
||||
"D404", # First word of the docstring should not be "This"
|
||||
"D415", # First line should end with a period, question mark, or exclamation point
|
||||
# ruff ignore
|
||||
"RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`
|
||||
# pylint ignore
|
||||
"PLW0603", # Using the global statement
|
||||
"PLW0120", # remove the else and dedent its contents
|
||||
"PLW2901", # for loop variable overwritten by assignment target
|
||||
"PLR5501", # Use `elif` instead of `else` then `if`
|
||||
]
|
||||
|
||||
[tool.ruff.lint.pycodestyle]
|
||||
# In order to be able to format for 88 char in ruff format
|
||||
max-line-length = 120
|
||||
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "pep257"
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
force-single-line = true
|
||||
combine-as-imports = true
|
||||
force-sort-within-sections = true
|
||||
order-by-type = false
|
||||
known-local-folder = ["pytest", "_pytest"]
|
||||
lines-after-imports = 2
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"src/_pytest/_py/**/*.py" = ["B", "PYI"]
|
||||
"src/_pytest/_version.py" = ["I001"]
|
||||
"testing/python/approx.py" = ["B015"]
|
||||
|
||||
[tool.pylint.main]
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length = 120
|
||||
disable= [
|
||||
"abstract-method",
|
||||
"arguments-differ",
|
||||
"arguments-renamed",
|
||||
"assigning-non-slot",
|
||||
"attribute-defined-outside-init",
|
||||
"bad-classmethod-argument",
|
||||
"bad-mcs-method-argument",
|
||||
"broad-exception-caught",
|
||||
"broad-exception-raised",
|
||||
"cell-var-from-loop",
|
||||
"comparison-of-constants",
|
||||
"comparison-with-callable",
|
||||
"comparison-with-itself",
|
||||
"condition-evals-to-constant",
|
||||
"consider-using-dict-items",
|
||||
"consider-using-enumerate",
|
||||
"consider-using-from-import",
|
||||
"consider-using-f-string",
|
||||
"consider-using-in",
|
||||
"consider-using-sys-exit",
|
||||
"consider-using-ternary",
|
||||
"consider-using-with",
|
||||
"cyclic-import",
|
||||
"disallowed-name",
|
||||
"duplicate-code",
|
||||
"eval-used",
|
||||
"exec-used",
|
||||
"expression-not-assigned",
|
||||
"fixme",
|
||||
"global-statement",
|
||||
"implicit-str-concat",
|
||||
"import-error",
|
||||
"import-outside-toplevel",
|
||||
"inconsistent-return-statements",
|
||||
"invalid-bool-returned",
|
||||
"invalid-name",
|
||||
"invalid-repr-returned",
|
||||
"invalid-str-returned",
|
||||
"keyword-arg-before-vararg",
|
||||
"line-too-long",
|
||||
"method-hidden",
|
||||
"misplaced-bare-raise",
|
||||
"missing-docstring",
|
||||
"missing-timeout",
|
||||
"multiple-statements",
|
||||
"no-else-break",
|
||||
"no-else-continue",
|
||||
"no-else-raise",
|
||||
"no-else-return",
|
||||
"no-member",
|
||||
"no-name-in-module",
|
||||
"no-self-argument",
|
||||
"not-an-iterable",
|
||||
"not-callable",
|
||||
"pointless-exception-statement",
|
||||
"pointless-statement",
|
||||
"pointless-string-statement",
|
||||
"protected-access",
|
||||
"raise-missing-from",
|
||||
"redefined-argument-from-local",
|
||||
"redefined-builtin",
|
||||
"redefined-outer-name",
|
||||
"reimported",
|
||||
"simplifiable-condition",
|
||||
"simplifiable-if-expression",
|
||||
"singleton-comparison",
|
||||
"superfluous-parens",
|
||||
"super-init-not-called",
|
||||
"too-few-public-methods",
|
||||
"too-many-ancestors",
|
||||
"too-many-arguments",
|
||||
"too-many-branches",
|
||||
"too-many-function-args",
|
||||
"too-many-instance-attributes",
|
||||
"too-many-lines",
|
||||
"too-many-locals",
|
||||
"too-many-nested-blocks",
|
||||
"too-many-public-methods",
|
||||
"too-many-return-statements",
|
||||
"too-many-statements",
|
||||
"try-except-raise",
|
||||
"typevar-name-incorrect-variance",
|
||||
"unbalanced-tuple-unpacking",
|
||||
"undefined-loop-variable",
|
||||
"undefined-variable",
|
||||
"unexpected-keyword-arg",
|
||||
"unidiomatic-typecheck",
|
||||
"unnecessary-comprehension",
|
||||
"unnecessary-dunder-call",
|
||||
"unnecessary-lambda",
|
||||
"unnecessary-lambda-assignment",
|
||||
"unpacking-non-sequence",
|
||||
"unspecified-encoding",
|
||||
"unsubscriptable-object",
|
||||
"unused-argument",
|
||||
"unused-import",
|
||||
"unused-variable",
|
||||
"used-before-assignment",
|
||||
"use-dict-literal",
|
||||
"use-implicit-booleaness-not-comparison",
|
||||
"use-implicit-booleaness-not-len",
|
||||
"useless-else-on-loop",
|
||||
"useless-import-alias",
|
||||
"useless-return",
|
||||
"use-maxsplit-arg",
|
||||
"using-constant-test",
|
||||
"wrong-import-order",
|
||||
]
|
||||
|
||||
[tool.check-wheel-contents]
|
||||
# check-wheel-contents is executed by the build-and-inspect-python-package action.
|
||||
# W009: Wheel contains multiple toplevel library entries
|
||||
ignore = "W009"
|
||||
|
||||
[tool.pyproject-fmt]
|
||||
indent = 4
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
minversion = "2.0"
|
||||
addopts = "-rfEX -p pytester --strict-markers"
|
||||
@@ -67,7 +345,6 @@ markers = [
|
||||
"uses_pexpect",
|
||||
]
|
||||
|
||||
|
||||
[tool.towncrier]
|
||||
package = "pytest"
|
||||
package_dir = "src"
|
||||
@@ -116,10 +393,18 @@ template = "changelog/_template.rst"
|
||||
name = "Trivial/Internal Changes"
|
||||
showcontent = true
|
||||
|
||||
[tool.black]
|
||||
target-version = ['py38']
|
||||
|
||||
# check-wheel-contents is executed by the build-and-inspect-python-package action.
|
||||
[tool.check-wheel-contents]
|
||||
# W009: Wheel contains multiple toplevel library entries
|
||||
ignore = "W009"
|
||||
[tool.mypy]
|
||||
files = ["src", "testing", "scripts"]
|
||||
mypy_path = ["src"]
|
||||
check_untyped_defs = true
|
||||
disallow_any_generics = true
|
||||
disallow_untyped_defs = true
|
||||
ignore_missing_imports = true
|
||||
show_error_codes = true
|
||||
strict_equality = true
|
||||
warn_redundant_casts = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
warn_unused_configs = true
|
||||
no_implicit_reexport = true
|
||||
warn_unused_ignores = true
|
||||
|
||||
1
scripts/.gitignore
vendored
Normal file
1
scripts/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
latest-release-notes.md
|
||||
67
scripts/generate-gh-release-notes.py
Normal file
67
scripts/generate-gh-release-notes.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# mypy: disallow-untyped-defs
|
||||
"""
|
||||
Script used to generate a Markdown file containing only the changelog entries of a specific pytest release, which
|
||||
is then published as a GitHub Release during deploy (see workflows/deploy.yml).
|
||||
|
||||
The script requires ``pandoc`` to be previously installed in the system -- we need to convert from RST (the format of
|
||||
our CHANGELOG) into Markdown (which is required by GitHub Releases).
|
||||
|
||||
Requires Python3.6+.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
from typing import Sequence
|
||||
|
||||
import pypandoc
|
||||
|
||||
|
||||
def extract_changelog_entries_for(version: str) -> str:
|
||||
p = Path(__file__).parent.parent / "doc/en/changelog.rst"
|
||||
changelog_lines = p.read_text(encoding="UTF-8").splitlines()
|
||||
|
||||
title_regex = re.compile(r"pytest (\d\.\d+\.\d+\w*) \(\d{4}-\d{2}-\d{2}\)")
|
||||
consuming_version = False
|
||||
version_lines = []
|
||||
for line in changelog_lines:
|
||||
m = title_regex.match(line)
|
||||
if m:
|
||||
# Found the version we want: start to consume lines until we find the next version title.
|
||||
if m.group(1) == version:
|
||||
consuming_version = True
|
||||
# Found a new version title while parsing the version we want: break out.
|
||||
elif consuming_version:
|
||||
break
|
||||
if consuming_version:
|
||||
version_lines.append(line)
|
||||
|
||||
return "\n".join(version_lines)
|
||||
|
||||
|
||||
def convert_rst_to_md(text: str) -> str:
|
||||
result = pypandoc.convert_text(
|
||||
text, "md", format="rst", extra_args=["--wrap=preserve"]
|
||||
)
|
||||
assert isinstance(result, str), repr(result)
|
||||
return result
|
||||
|
||||
|
||||
def main(argv: Sequence[str]) -> int:
|
||||
if len(argv) != 3:
|
||||
print("Usage: generate-gh-release-notes VERSION FILE")
|
||||
return 2
|
||||
|
||||
version, filename = argv[1:3]
|
||||
print(f"Generating GitHub release notes for version {version}")
|
||||
rst_body = extract_changelog_entries_for(version)
|
||||
md_body = convert_rst_to_md(rst_body)
|
||||
Path(filename).write_text(md_body, encoding="UTF-8")
|
||||
print()
|
||||
print(f"Done: {filename}")
|
||||
print()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
@@ -1,3 +1,4 @@
|
||||
# mypy: disallow-untyped-defs
|
||||
"""
|
||||
This script is part of the pytest release process which is triggered manually in the Actions
|
||||
tab of the repository.
|
||||
@@ -12,9 +13,10 @@ After that, it will create a release using the `release` tox environment, and pu
|
||||
**Token**: currently the token from the GitHub Actions is used, pushed with
|
||||
`pytest bot <pytestbot@gmail.com>` commit author.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
from pathlib import Path
|
||||
import re
|
||||
from subprocess import check_call
|
||||
from subprocess import check_output
|
||||
from subprocess import run
|
||||
@@ -78,7 +80,7 @@ def prepare_release_pr(
|
||||
)
|
||||
except InvalidFeatureRelease as e:
|
||||
print(f"{Fore.RED}{e}")
|
||||
raise SystemExit(1)
|
||||
raise SystemExit(1) from None
|
||||
|
||||
print(f"Version: {Fore.CYAN}{version}")
|
||||
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
"""
|
||||
Script used to publish GitHub release notes extracted from CHANGELOG.rst.
|
||||
|
||||
This script is meant to be executed after a successful deployment in GitHub actions.
|
||||
|
||||
Uses the following environment variables:
|
||||
|
||||
* GIT_TAG: the name of the tag of the current commit.
|
||||
* GH_RELEASE_NOTES_TOKEN: a personal access token with 'repo' permissions.
|
||||
|
||||
Create one at:
|
||||
|
||||
https://github.com/settings/tokens
|
||||
|
||||
This token should be set in a secret in the repository, which is exposed as an
|
||||
environment variable in the main.yml workflow file.
|
||||
|
||||
The script also requires ``pandoc`` to be previously installed in the system.
|
||||
|
||||
Requires Python3.6+.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import github3
|
||||
import pypandoc
|
||||
|
||||
|
||||
def publish_github_release(slug, token, tag_name, body):
|
||||
github = github3.login(token=token)
|
||||
owner, repo = slug.split("/")
|
||||
repo = github.repository(owner, repo)
|
||||
return repo.create_release(tag_name=tag_name, body=body)
|
||||
|
||||
|
||||
def parse_changelog(tag_name):
|
||||
p = Path(__file__).parent.parent / "doc/en/changelog.rst"
|
||||
changelog_lines = p.read_text(encoding="UTF-8").splitlines()
|
||||
|
||||
title_regex = re.compile(r"pytest (\d\.\d+\.\d+) \(\d{4}-\d{2}-\d{2}\)")
|
||||
consuming_version = False
|
||||
version_lines = []
|
||||
for line in changelog_lines:
|
||||
m = title_regex.match(line)
|
||||
if m:
|
||||
# found the version we want: start to consume lines until we find the next version title
|
||||
if m.group(1) == tag_name:
|
||||
consuming_version = True
|
||||
# found a new version title while parsing the version we want: break out
|
||||
elif consuming_version:
|
||||
break
|
||||
if consuming_version:
|
||||
version_lines.append(line)
|
||||
|
||||
return "\n".join(version_lines)
|
||||
|
||||
|
||||
def convert_rst_to_md(text):
|
||||
return pypandoc.convert_text(
|
||||
text, "md", format="rst", extra_args=["--wrap=preserve"]
|
||||
)
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) > 1:
|
||||
tag_name = argv[1]
|
||||
else:
|
||||
tag_name = os.environ.get("GITHUB_REF")
|
||||
if not tag_name:
|
||||
print("tag_name not given and $GITHUB_REF not set", file=sys.stderr)
|
||||
return 1
|
||||
if tag_name.startswith("refs/tags/"):
|
||||
tag_name = tag_name[len("refs/tags/") :]
|
||||
|
||||
token = os.environ.get("GH_RELEASE_NOTES_TOKEN")
|
||||
if not token:
|
||||
print("GH_RELEASE_NOTES_TOKEN not set", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
slug = os.environ.get("GITHUB_REPOSITORY")
|
||||
if not slug:
|
||||
print("GITHUB_REPOSITORY not set", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
rst_body = parse_changelog(tag_name)
|
||||
md_body = convert_rst_to_md(rst_body)
|
||||
if not publish_github_release(slug, token, tag_name, md_body):
|
||||
print("Could not publish release notes:", file=sys.stderr)
|
||||
print(md_body, file=sys.stderr)
|
||||
return 5
|
||||
|
||||
print()
|
||||
print(f"Release notes for {tag_name} published successfully:")
|
||||
print(f"https://github.com/{slug}/releases/tag/{tag_name}")
|
||||
print()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
@@ -1,4 +1,6 @@
|
||||
# mypy: disallow-untyped-defs
|
||||
"""Invoke development tasks."""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -10,15 +12,15 @@ from colorama import Fore
|
||||
from colorama import init
|
||||
|
||||
|
||||
def announce(version, template_name, doc_version):
|
||||
def announce(version: str, template_name: str, doc_version: str) -> None:
|
||||
"""Generates a new release announcement entry in the docs."""
|
||||
# Get our list of authors
|
||||
stdout = check_output(["git", "describe", "--abbrev=0", "--tags"])
|
||||
stdout = stdout.decode("utf-8")
|
||||
stdout = check_output(["git", "describe", "--abbrev=0", "--tags"], encoding="UTF-8")
|
||||
last_version = stdout.strip()
|
||||
|
||||
stdout = check_output(["git", "log", f"{last_version}..HEAD", "--format=%aN"])
|
||||
stdout = stdout.decode("utf-8")
|
||||
stdout = check_output(
|
||||
["git", "log", f"{last_version}..HEAD", "--format=%aN"], encoding="UTF-8"
|
||||
)
|
||||
|
||||
contributors = {
|
||||
name
|
||||
@@ -61,7 +63,7 @@ def announce(version, template_name, doc_version):
|
||||
check_call(["git", "add", str(target)])
|
||||
|
||||
|
||||
def regen(version):
|
||||
def regen(version: str) -> None:
|
||||
"""Call regendoc tool to update examples and pytest output in the docs."""
|
||||
print(f"{Fore.CYAN}[generate.regen] {Fore.RESET}Updating docs")
|
||||
check_call(
|
||||
@@ -70,7 +72,7 @@ def regen(version):
|
||||
)
|
||||
|
||||
|
||||
def fix_formatting():
|
||||
def fix_formatting() -> None:
|
||||
"""Runs pre-commit in all files to ensure they are formatted correctly"""
|
||||
print(
|
||||
f"{Fore.CYAN}[generate.fix linting] {Fore.RESET}Fixing formatting using pre-commit"
|
||||
@@ -78,13 +80,15 @@ def fix_formatting():
|
||||
call(["pre-commit", "run", "--all-files"])
|
||||
|
||||
|
||||
def check_links():
|
||||
def check_links() -> None:
|
||||
"""Runs sphinx-build to check links"""
|
||||
print(f"{Fore.CYAN}[generate.check_links] {Fore.RESET}Checking links")
|
||||
check_call(["tox", "-e", "docs-checklinks"])
|
||||
|
||||
|
||||
def pre_release(version, template_name, doc_version, *, skip_check_links):
|
||||
def pre_release(
|
||||
version: str, template_name: str, doc_version: str, *, skip_check_links: bool
|
||||
) -> None:
|
||||
"""Generates new docs, release announcements and creates a local tag."""
|
||||
announce(version, template_name, doc_version)
|
||||
regen(version)
|
||||
@@ -102,12 +106,12 @@ def pre_release(version, template_name, doc_version, *, skip_check_links):
|
||||
print("Please push your branch and open a PR.")
|
||||
|
||||
|
||||
def changelog(version, write_out=False):
|
||||
def changelog(version: str, write_out: bool = False) -> None:
|
||||
addopts = [] if write_out else ["--draft"]
|
||||
check_call(["towncrier", "--yes", "--version", version] + addopts)
|
||||
check_call(["towncrier", "--yes", "--version", version, *addopts])
|
||||
|
||||
|
||||
def main():
|
||||
def main() -> None:
|
||||
init(autoreset=True)
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("version", help="Release version")
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import sys
|
||||
# mypy: disallow-untyped-defs
|
||||
from subprocess import call
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
def main() -> int:
|
||||
"""
|
||||
Platform agnostic wrapper script for towncrier.
|
||||
Fixes the issue (#7251) where windows users are unable to natively run tox -e docs to build pytest docs.
|
||||
Platform-agnostic wrapper script for towncrier.
|
||||
Fixes the issue (#7251) where Windows users are unable to natively run tox -e docs to build pytest docs.
|
||||
"""
|
||||
with open(
|
||||
"doc/en/_changelog_towncrier_draft.rst", "w", encoding="utf-8"
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
# mypy: disallow-untyped-defs
|
||||
import datetime
|
||||
import pathlib
|
||||
import re
|
||||
from textwrap import dedent
|
||||
from textwrap import indent
|
||||
from typing import Any
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import TypedDict
|
||||
|
||||
import packaging.version
|
||||
import platformdirs
|
||||
import tabulate
|
||||
import wcwidth
|
||||
from requests_cache import CachedResponse
|
||||
from requests_cache import CachedSession
|
||||
from requests_cache import OriginalResponse
|
||||
from requests_cache import SQLiteCache
|
||||
import tabulate
|
||||
from tqdm import tqdm
|
||||
import wcwidth
|
||||
|
||||
|
||||
FILE_HEAD = r"""
|
||||
@@ -24,7 +29,7 @@ Pytest Plugin List
|
||||
==================
|
||||
|
||||
Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
|
||||
It includes PyPI projects whose names begin with "pytest-" and a handful of manually selected projects.
|
||||
It includes PyPI projects whose names begin with ``pytest-`` or ``pytest_`` and a handful of manually selected projects.
|
||||
Packages classified as inactive are excluded.
|
||||
|
||||
For detailed insights into how this list is generated,
|
||||
@@ -56,6 +61,7 @@ DEVELOPMENT_STATUS_CLASSIFIERS = (
|
||||
)
|
||||
ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins
|
||||
"logassert",
|
||||
"logot",
|
||||
"nuts",
|
||||
"flask_fixture",
|
||||
}
|
||||
@@ -81,7 +87,6 @@ def project_response_with_refresh(
|
||||
|
||||
force refresh in case of last serial mismatch
|
||||
"""
|
||||
|
||||
response = session.get(f"https://pypi.org/pypi/{name}/json")
|
||||
if int(response.headers.get("X-PyPI-Last-Serial", -1)) != last_serial:
|
||||
response = session.get(f"https://pypi.org/pypi/{name}/json", refresh=True)
|
||||
@@ -105,11 +110,24 @@ def pytest_plugin_projects_from_pypi(session: CachedSession) -> dict[str, int]:
|
||||
return {
|
||||
name: p["_last-serial"]
|
||||
for p in response.json()["projects"]
|
||||
if (name := p["name"]).startswith("pytest-") or name in ADDITIONAL_PROJECTS
|
||||
if (
|
||||
(name := p["name"]).startswith(("pytest-", "pytest_"))
|
||||
or name in ADDITIONAL_PROJECTS
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def iter_plugins():
|
||||
class PluginInfo(TypedDict):
|
||||
"""Relevant information about a plugin to generate the summary."""
|
||||
|
||||
name: str
|
||||
summary: str
|
||||
last_release: str
|
||||
status: str
|
||||
requires: str
|
||||
|
||||
|
||||
def iter_plugins() -> Iterator[PluginInfo]:
|
||||
session = get_session()
|
||||
name_2_serial = pytest_plugin_projects_from_pypi(session)
|
||||
|
||||
@@ -136,7 +154,7 @@ def iter_plugins():
|
||||
requires = requirement
|
||||
break
|
||||
|
||||
def version_sort_key(version_string):
|
||||
def version_sort_key(version_string: str) -> Any:
|
||||
"""
|
||||
Return the sort key for the given version string
|
||||
returned by the API.
|
||||
@@ -162,20 +180,19 @@ def iter_plugins():
|
||||
yield {
|
||||
"name": name,
|
||||
"summary": summary.strip(),
|
||||
"last release": last_release,
|
||||
"last_release": last_release,
|
||||
"status": status,
|
||||
"requires": requires,
|
||||
}
|
||||
|
||||
|
||||
def plugin_definitions(plugins):
|
||||
def plugin_definitions(plugins: Iterable[PluginInfo]) -> Iterator[str]:
|
||||
"""Return RST for the plugin list that fits better on a vertical page."""
|
||||
|
||||
for plugin in plugins:
|
||||
yield dedent(
|
||||
f"""
|
||||
{plugin['name']}
|
||||
*last release*: {plugin["last release"]},
|
||||
*last release*: {plugin["last_release"]},
|
||||
*status*: {plugin["status"]},
|
||||
*requires*: {plugin["requires"]}
|
||||
|
||||
@@ -184,7 +201,7 @@ def plugin_definitions(plugins):
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
def main() -> None:
|
||||
plugins = [*iter_plugins()]
|
||||
|
||||
reference_dir = pathlib.Path("doc", "en", "reference")
|
||||
@@ -195,7 +212,7 @@ def main():
|
||||
f.write(f"This list contains {len(plugins)} plugins.\n\n")
|
||||
f.write(".. only:: not latex\n\n")
|
||||
|
||||
wcwidth # reference library that must exist for tabulate to work
|
||||
_ = wcwidth # reference library that must exist for tabulate to work
|
||||
plugin_table = tabulate.tabulate(plugins, headers="keys", tablefmt="rst")
|
||||
f.write(indent(plugin_table, " "))
|
||||
f.write("\n\n")
|
||||
|
||||
105
setup.cfg
105
setup.cfg
@@ -1,105 +0,0 @@
|
||||
[metadata]
|
||||
name = pytest
|
||||
description = pytest: simple powerful testing with Python
|
||||
long_description = file: README.rst
|
||||
long_description_content_type = text/x-rst
|
||||
url = https://docs.pytest.org/en/latest/
|
||||
author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others
|
||||
license = MIT
|
||||
license_files = LICENSE
|
||||
platforms = unix, linux, osx, cygwin, win32
|
||||
classifiers =
|
||||
Development Status :: 6 - Mature
|
||||
Intended Audience :: Developers
|
||||
License :: OSI Approved :: MIT License
|
||||
Operating System :: MacOS :: MacOS X
|
||||
Operating System :: Microsoft :: Windows
|
||||
Operating System :: POSIX
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3 :: Only
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: 3.10
|
||||
Programming Language :: Python :: 3.11
|
||||
Programming Language :: Python :: 3.12
|
||||
Topic :: Software Development :: Libraries
|
||||
Topic :: Software Development :: Testing
|
||||
Topic :: Utilities
|
||||
keywords = test, unittest
|
||||
project_urls =
|
||||
Changelog=https://docs.pytest.org/en/stable/changelog.html
|
||||
Twitter=https://twitter.com/pytestdotorg
|
||||
Source=https://github.com/pytest-dev/pytest
|
||||
Tracker=https://github.com/pytest-dev/pytest/issues
|
||||
|
||||
[options]
|
||||
packages =
|
||||
_pytest
|
||||
_pytest._code
|
||||
_pytest._io
|
||||
_pytest._py
|
||||
_pytest.assertion
|
||||
_pytest.config
|
||||
_pytest.mark
|
||||
pytest
|
||||
py_modules = py
|
||||
install_requires =
|
||||
iniconfig
|
||||
packaging
|
||||
pluggy>=1.3.0,<2.0
|
||||
colorama;sys_platform=="win32"
|
||||
exceptiongroup>=1.0.0rc8;python_version<"3.11"
|
||||
tomli>=1.0.0;python_version<"3.11"
|
||||
python_requires = >=3.8
|
||||
package_dir =
|
||||
=src
|
||||
setup_requires =
|
||||
setuptools
|
||||
setuptools-scm>=6.0
|
||||
zip_safe = no
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
pytest=pytest:console_main
|
||||
py.test=pytest:console_main
|
||||
|
||||
[options.extras_require]
|
||||
testing =
|
||||
argcomplete
|
||||
attrs>=19.2.0
|
||||
hypothesis>=3.56
|
||||
mock
|
||||
nose
|
||||
pygments>=2.7.2
|
||||
requests
|
||||
setuptools
|
||||
xmlschema
|
||||
|
||||
[options.package_data]
|
||||
_pytest = py.typed
|
||||
pytest = py.typed
|
||||
|
||||
[build_sphinx]
|
||||
source_dir = doc/en/
|
||||
build_dir = doc/build
|
||||
all_files = 1
|
||||
|
||||
[check-manifest]
|
||||
ignore =
|
||||
src/_pytest/_version.py
|
||||
|
||||
[devpi:upload]
|
||||
formats = sdist.tgz,bdist_wheel
|
||||
|
||||
[mypy]
|
||||
mypy_path = src
|
||||
check_untyped_defs = True
|
||||
disallow_any_generics = True
|
||||
ignore_missing_imports = True
|
||||
show_error_codes = True
|
||||
strict_equality = True
|
||||
warn_redundant_casts = True
|
||||
warn_return_any = True
|
||||
warn_unreachable = True
|
||||
warn_unused_configs = True
|
||||
no_implicit_reexport = True
|
||||
@@ -1,9 +1,10 @@
|
||||
__all__ = ["__version__", "version_tuple"]
|
||||
|
||||
try:
|
||||
from ._version import version as __version__, version_tuple
|
||||
from ._version import version as __version__
|
||||
from ._version import version_tuple
|
||||
except ImportError: # pragma: no cover
|
||||
# broken installation, we don't even try
|
||||
# unknown only works because we do poor mans version compare
|
||||
__version__ = "unknown"
|
||||
version_tuple = (0, 0, "unknown") # type:ignore[assignment]
|
||||
version_tuple = (0, 0, "unknown")
|
||||
|
||||
@@ -61,10 +61,11 @@ If things do not work right away:
|
||||
which should throw a KeyError: 'COMPLINE' (which is properly set by the
|
||||
global argcomplete script).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from glob import glob
|
||||
import os
|
||||
import sys
|
||||
from glob import glob
|
||||
from typing import Any
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Python inspection/code generation API."""
|
||||
|
||||
from .code import Code
|
||||
from .code import ExceptionInfo
|
||||
from .code import filter_traceback
|
||||
@@ -9,6 +10,7 @@ from .code import TracebackEntry
|
||||
from .source import getrawcode
|
||||
from .source import Source
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Code",
|
||||
"ExceptionInfo",
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import ast
|
||||
import dataclasses
|
||||
import inspect
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
from inspect import CO_VARARGS
|
||||
from inspect import CO_VARKEYWORDS
|
||||
from io import StringIO
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
from traceback import format_exception_only
|
||||
from types import CodeType
|
||||
from types import FrameType
|
||||
@@ -50,7 +51,8 @@ from _pytest.deprecated import check_ispytest
|
||||
from _pytest.pathlib import absolutepath
|
||||
from _pytest.pathlib import bestrelpath
|
||||
|
||||
if sys.version_info[:2] < (3, 11):
|
||||
|
||||
if sys.version_info < (3, 11):
|
||||
from exceptiongroup import BaseExceptionGroup
|
||||
|
||||
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
|
||||
@@ -277,9 +279,9 @@ class TracebackEntry:
|
||||
|
||||
Mostly for internal use.
|
||||
"""
|
||||
tbh: Union[
|
||||
bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]
|
||||
] = False
|
||||
tbh: Union[bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]] = (
|
||||
False
|
||||
)
|
||||
for maybe_ns_dct in (self.frame.f_locals, self.frame.f_globals):
|
||||
# in normal cases, f_locals and f_globals are dictionaries
|
||||
# however via `exec(...)` / `eval(...)` they can be other types
|
||||
@@ -376,12 +378,10 @@ class Traceback(List[TracebackEntry]):
|
||||
return self
|
||||
|
||||
@overload
|
||||
def __getitem__(self, key: "SupportsIndex") -> TracebackEntry:
|
||||
...
|
||||
def __getitem__(self, key: "SupportsIndex") -> TracebackEntry: ...
|
||||
|
||||
@overload
|
||||
def __getitem__(self, key: slice) -> "Traceback":
|
||||
...
|
||||
def __getitem__(self, key: slice) -> "Traceback": ...
|
||||
|
||||
def __getitem__(
|
||||
self, key: Union["SupportsIndex", slice]
|
||||
@@ -486,9 +486,10 @@ class ExceptionInfo(Generic[E]):
|
||||
|
||||
.. versionadded:: 7.4
|
||||
"""
|
||||
assert (
|
||||
exception.__traceback__
|
||||
), "Exceptions passed to ExcInfo.from_exception(...) must have a non-None __traceback__."
|
||||
assert exception.__traceback__, (
|
||||
"Exceptions passed to ExcInfo.from_exception(...)"
|
||||
" must have a non-None __traceback__."
|
||||
)
|
||||
exc_info = (type(exception), exception, exception.__traceback__)
|
||||
return cls.from_exc_info(exc_info, exprinfo)
|
||||
|
||||
@@ -587,9 +588,7 @@ class ExceptionInfo(Generic[E]):
|
||||
def __repr__(self) -> str:
|
||||
if self._excinfo is None:
|
||||
return "<ExceptionInfo for raises contextmanager>"
|
||||
return "<{} {} tblen={}>".format(
|
||||
self.__class__.__name__, saferepr(self._excinfo[1]), len(self.traceback)
|
||||
)
|
||||
return f"<{self.__class__.__name__} {saferepr(self._excinfo[1])} tblen={len(self.traceback)}>"
|
||||
|
||||
def exconly(self, tryshort: bool = False) -> str:
|
||||
"""Return the exception as a string.
|
||||
@@ -698,10 +697,21 @@ class ExceptionInfo(Generic[E]):
|
||||
return fmt.repr_excinfo(self)
|
||||
|
||||
def _stringify_exception(self, exc: BaseException) -> str:
|
||||
try:
|
||||
notes = getattr(exc, "__notes__", [])
|
||||
except KeyError:
|
||||
# Workaround for https://github.com/python/cpython/issues/98778 on
|
||||
# Python <= 3.9, and some 3.10 and 3.11 patch versions.
|
||||
HTTPError = getattr(sys.modules.get("urllib.error", None), "HTTPError", ())
|
||||
if sys.version_info < (3, 12) and isinstance(exc, HTTPError):
|
||||
notes = []
|
||||
else:
|
||||
raise
|
||||
|
||||
return "\n".join(
|
||||
[
|
||||
str(exc),
|
||||
*getattr(exc, "__notes__", []),
|
||||
*notes,
|
||||
]
|
||||
)
|
||||
|
||||
@@ -776,6 +786,8 @@ class ExceptionInfo(Generic[E]):
|
||||
If `None`, will search for a matching exception at any nesting depth.
|
||||
If >= 1, will only match an exception if it's at the specified depth (depth = 1 being
|
||||
the exceptions contained within the topmost exception group).
|
||||
|
||||
.. versionadded:: 8.0
|
||||
"""
|
||||
msg = "Captured exception is not an instance of `BaseExceptionGroup`"
|
||||
assert isinstance(self.value, BaseExceptionGroup), msg
|
||||
@@ -1006,13 +1018,8 @@ class FormattedExcinfo:
|
||||
extraline: Optional[str] = (
|
||||
"!!! Recursion error detected, but an error occurred locating the origin of recursion.\n"
|
||||
" The following exception happened when comparing locals in the stack frame:\n"
|
||||
" {exc_type}: {exc_msg}\n"
|
||||
" Displaying first and last {max_frames} stack frames out of {total}."
|
||||
).format(
|
||||
exc_type=type(e).__name__,
|
||||
exc_msg=str(e),
|
||||
max_frames=max_frames,
|
||||
total=len(traceback),
|
||||
f" {type(e).__name__}: {e!s}\n"
|
||||
f" Displaying first and last {max_frames} stack frames out of {len(traceback)}."
|
||||
)
|
||||
# Type ignored because adding two instances of a List subtype
|
||||
# currently incorrectly has type List instead of the subtype.
|
||||
@@ -1044,13 +1051,13 @@ class FormattedExcinfo:
|
||||
# full support for exception groups added to ExceptionInfo.
|
||||
# See https://github.com/pytest-dev/pytest/issues/9159
|
||||
if isinstance(e, BaseExceptionGroup):
|
||||
reprtraceback: Union[
|
||||
ReprTracebackNative, ReprTraceback
|
||||
] = ReprTracebackNative(
|
||||
traceback.format_exception(
|
||||
type(excinfo_.value),
|
||||
excinfo_.value,
|
||||
excinfo_.traceback[0]._rawentry,
|
||||
reprtraceback: Union[ReprTracebackNative, ReprTraceback] = (
|
||||
ReprTracebackNative(
|
||||
traceback.format_exception(
|
||||
type(excinfo_.value),
|
||||
excinfo_.value,
|
||||
excinfo_.traceback[0]._rawentry,
|
||||
)
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -1219,7 +1226,6 @@ class ReprEntry(TerminalRepr):
|
||||
the "E" prefix) using syntax highlighting, taking care to not highlighting the ">"
|
||||
character, as doing so might break line continuations.
|
||||
"""
|
||||
|
||||
if not self.lines:
|
||||
return
|
||||
|
||||
@@ -1342,7 +1348,7 @@ def getfslineno(obj: object) -> Tuple[Union[str, Path], int]:
|
||||
# in 6ec13a2b9. It ("place_as") appears to be something very custom.
|
||||
obj = get_real_func(obj)
|
||||
if hasattr(obj, "place_as"):
|
||||
obj = obj.place_as # type: ignore[attr-defined]
|
||||
obj = obj.place_as
|
||||
|
||||
try:
|
||||
code = Code.from_function(obj)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import ast
|
||||
from bisect import bisect_right
|
||||
import inspect
|
||||
import textwrap
|
||||
import tokenize
|
||||
import types
|
||||
import warnings
|
||||
from bisect import bisect_right
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
@@ -12,6 +12,7 @@ from typing import Optional
|
||||
from typing import overload
|
||||
from typing import Tuple
|
||||
from typing import Union
|
||||
import warnings
|
||||
|
||||
|
||||
class Source:
|
||||
@@ -46,12 +47,10 @@ class Source:
|
||||
__hash__ = None # type: ignore
|
||||
|
||||
@overload
|
||||
def __getitem__(self, key: int) -> str:
|
||||
...
|
||||
def __getitem__(self, key: int) -> str: ...
|
||||
|
||||
@overload
|
||||
def __getitem__(self, key: slice) -> "Source":
|
||||
...
|
||||
def __getitem__(self, key: slice) -> "Source": ...
|
||||
|
||||
def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]:
|
||||
if isinstance(key, int):
|
||||
@@ -196,7 +195,9 @@ def getstatementrange_ast(
|
||||
# by using the BlockFinder helper used which inspect.getsource() uses itself.
|
||||
block_finder = inspect.BlockFinder()
|
||||
# If we start with an indented line, put blockfinder to "started" mode.
|
||||
block_finder.started = source.lines[start][0].isspace()
|
||||
block_finder.started = (
|
||||
bool(source.lines[start]) and source.lines[start][0].isspace()
|
||||
)
|
||||
it = ((x + "\n") for x in source.lines[start:end])
|
||||
try:
|
||||
for tok in tokenize.generate_tokens(lambda: next(it)):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# mypy: allow-untyped-defs
|
||||
# This module was imported from the cpython standard library
|
||||
# (https://github.com/python/cpython/) at commit
|
||||
# c5140945c723ae6c4b7ee81ff720ac8ea4b52cfd (python3.12).
|
||||
@@ -14,9 +15,9 @@
|
||||
# useful, thank small children who sleep at night.
|
||||
import collections as _collections
|
||||
import dataclasses as _dataclasses
|
||||
from io import StringIO as _StringIO
|
||||
import re
|
||||
import types as _types
|
||||
from io import StringIO as _StringIO
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
|
||||
@@ -19,8 +19,8 @@ def _format_repr_exception(exc: BaseException, obj: object) -> str:
|
||||
raise
|
||||
except BaseException as exc:
|
||||
exc_info = f"unpresentable exception ({_try_repr_or_str(exc)})"
|
||||
return "<[{} raised in repr()] {} object at 0x{:x}>".format(
|
||||
exc_info, type(obj).__name__, id(obj)
|
||||
return (
|
||||
f"<[{exc_info} raised in repr()] {type(obj).__name__} object at 0x{id(obj):x}>"
|
||||
)
|
||||
|
||||
|
||||
@@ -108,7 +108,6 @@ def saferepr(
|
||||
This function is a wrapper around the Repr/reprlib functionality of the
|
||||
stdlib.
|
||||
"""
|
||||
|
||||
return SafeRepr(maxsize, use_ascii).repr(obj)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Helper functions for writing to terminals and files."""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
@@ -8,6 +9,7 @@ from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import TextIO
|
||||
|
||||
from ..compat import assert_never
|
||||
from .wcwidth import wcswidth
|
||||
|
||||
|
||||
@@ -183,9 +185,7 @@ class TerminalWriter:
|
||||
"""
|
||||
if indents and len(indents) != len(lines):
|
||||
raise ValueError(
|
||||
"indents size ({}) should have same size as lines ({})".format(
|
||||
len(indents), len(lines)
|
||||
)
|
||||
f"indents size ({len(indents)}) should have same size as lines ({len(lines)})"
|
||||
)
|
||||
if not indents:
|
||||
indents = [""] * len(lines)
|
||||
@@ -200,8 +200,9 @@ class TerminalWriter:
|
||||
"""Highlight the given source if we have markup support."""
|
||||
from _pytest.config.exceptions import UsageError
|
||||
|
||||
if not self.hasmarkup or not self.code_highlight:
|
||||
if not source or not self.hasmarkup or not self.code_highlight:
|
||||
return source
|
||||
|
||||
try:
|
||||
from pygments.formatters.terminal import TerminalFormatter
|
||||
|
||||
@@ -209,6 +210,8 @@ class TerminalWriter:
|
||||
from pygments.lexers.python import PythonLexer as Lexer
|
||||
elif lexer == "diff":
|
||||
from pygments.lexers.diff import DiffLexer as Lexer
|
||||
else:
|
||||
assert_never(lexer)
|
||||
from pygments import highlight
|
||||
import pygments.util
|
||||
except ImportError:
|
||||
@@ -232,17 +235,17 @@ class TerminalWriter:
|
||||
# which may lead to the previous color being propagated to the
|
||||
# start of the expression, so reset first.
|
||||
return "\x1b[0m" + highlighted
|
||||
except pygments.util.ClassNotFound:
|
||||
except pygments.util.ClassNotFound as e:
|
||||
raise UsageError(
|
||||
"PYTEST_THEME environment variable had an invalid value: '{}'. "
|
||||
"Only valid pygment styles are allowed.".format(
|
||||
os.getenv("PYTEST_THEME")
|
||||
)
|
||||
)
|
||||
except pygments.util.OptionError:
|
||||
) from e
|
||||
except pygments.util.OptionError as e:
|
||||
raise UsageError(
|
||||
"PYTEST_THEME_MODE environment variable had an invalid value: '{}'. "
|
||||
"The only allowed values are 'dark' and 'light'.".format(
|
||||
os.getenv("PYTEST_THEME_MODE")
|
||||
)
|
||||
)
|
||||
) from e
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import unicodedata
|
||||
from functools import lru_cache
|
||||
import unicodedata
|
||||
|
||||
|
||||
@lru_cache(100)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""create errno-specific classes for IO or os calls."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import errno
|
||||
@@ -8,6 +9,7 @@ from typing import Callable
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""local path implementation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import atexit
|
||||
from contextlib import contextmanager
|
||||
import fnmatch
|
||||
import importlib.util
|
||||
import io
|
||||
import os
|
||||
import posixpath
|
||||
import sys
|
||||
import uuid
|
||||
import warnings
|
||||
from contextlib import contextmanager
|
||||
from os.path import abspath
|
||||
from os.path import dirname
|
||||
from os.path import exists
|
||||
@@ -19,18 +17,23 @@ from os.path import isdir
|
||||
from os.path import isfile
|
||||
from os.path import islink
|
||||
from os.path import normpath
|
||||
import posixpath
|
||||
from stat import S_ISDIR
|
||||
from stat import S_ISLNK
|
||||
from stat import S_ISREG
|
||||
import sys
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Literal
|
||||
from typing import overload
|
||||
from typing import TYPE_CHECKING
|
||||
import uuid
|
||||
import warnings
|
||||
|
||||
from . import error
|
||||
|
||||
|
||||
# Moved from local.py.
|
||||
iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt")
|
||||
|
||||
@@ -203,12 +206,10 @@ class Stat:
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
...
|
||||
def size(self) -> int: ...
|
||||
|
||||
@property
|
||||
def mtime(self) -> float:
|
||||
...
|
||||
def mtime(self) -> float: ...
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
return getattr(self._osstatresult, "st_" + name)
|
||||
@@ -223,7 +224,7 @@ class Stat:
|
||||
raise NotImplementedError("XXX win32")
|
||||
import pwd
|
||||
|
||||
entry = error.checked_call(pwd.getpwuid, self.uid) # type:ignore[attr-defined]
|
||||
entry = error.checked_call(pwd.getpwuid, self.uid) # type:ignore[attr-defined,unused-ignore]
|
||||
return entry[0]
|
||||
|
||||
@property
|
||||
@@ -233,7 +234,7 @@ class Stat:
|
||||
raise NotImplementedError("XXX win32")
|
||||
import grp
|
||||
|
||||
entry = error.checked_call(grp.getgrgid, self.gid) # type:ignore[attr-defined]
|
||||
entry = error.checked_call(grp.getgrgid, self.gid) # type:ignore[attr-defined,unused-ignore]
|
||||
return entry[0]
|
||||
|
||||
def isdir(self):
|
||||
@@ -251,7 +252,7 @@ def getuserid(user):
|
||||
import pwd
|
||||
|
||||
if not isinstance(user, int):
|
||||
user = pwd.getpwnam(user)[2] # type:ignore[attr-defined]
|
||||
user = pwd.getpwnam(user)[2] # type:ignore[attr-defined,unused-ignore]
|
||||
return user
|
||||
|
||||
|
||||
@@ -259,7 +260,7 @@ def getgroupid(group):
|
||||
import grp
|
||||
|
||||
if not isinstance(group, int):
|
||||
group = grp.getgrnam(group)[2] # type:ignore[attr-defined]
|
||||
group = grp.getgrnam(group)[2] # type:ignore[attr-defined,unused-ignore]
|
||||
return group
|
||||
|
||||
|
||||
@@ -316,7 +317,7 @@ class LocalPath:
|
||||
def readlink(self) -> str:
|
||||
"""Return value of a symbolic link."""
|
||||
# https://github.com/python/mypy/issues/12278
|
||||
return error.checked_call(os.readlink, self.strpath) # type: ignore[arg-type,return-value]
|
||||
return error.checked_call(os.readlink, self.strpath) # type: ignore[arg-type,return-value,unused-ignore]
|
||||
|
||||
def mklinkto(self, oldname):
|
||||
"""Posix style hard link to another name."""
|
||||
@@ -450,7 +451,7 @@ class LocalPath:
|
||||
|
||||
def ensure_dir(self, *args):
|
||||
"""Ensure the path joined with args is a directory."""
|
||||
return self.ensure(*args, **{"dir": True})
|
||||
return self.ensure(*args, dir=True)
|
||||
|
||||
def bestrelpath(self, dest):
|
||||
"""Return a string which is a relative path from self
|
||||
@@ -675,7 +676,7 @@ class LocalPath:
|
||||
else:
|
||||
kw.setdefault("dirname", dirname)
|
||||
kw.setdefault("sep", self.sep)
|
||||
obj.strpath = normpath("%(dirname)s%(sep)s%(basename)s" % kw)
|
||||
obj.strpath = normpath("{dirname}{sep}{basename}".format(**kw))
|
||||
return obj
|
||||
|
||||
def _getbyspec(self, spec: str) -> list[str]:
|
||||
@@ -755,12 +756,11 @@ class LocalPath:
|
||||
if ensure:
|
||||
self.dirpath().ensure(dir=1)
|
||||
if encoding:
|
||||
# Using type ignore here because of this error:
|
||||
# error: Argument 1 has incompatible type overloaded function;
|
||||
# expected "Callable[[str, Any, Any], TextIOWrapper]" [arg-type]
|
||||
# Which seems incorrect, given io.open supports the given argument types.
|
||||
return error.checked_call(
|
||||
io.open, self.strpath, mode, encoding=encoding # type:ignore[arg-type]
|
||||
io.open,
|
||||
self.strpath,
|
||||
mode,
|
||||
encoding=encoding,
|
||||
)
|
||||
return error.checked_call(open, self.strpath, mode)
|
||||
|
||||
@@ -779,11 +779,11 @@ class LocalPath:
|
||||
|
||||
valid checkers::
|
||||
|
||||
file=1 # is a file
|
||||
file=0 # is not a file (may not even exist)
|
||||
dir=1 # is a dir
|
||||
link=1 # is a link
|
||||
exists=1 # exists
|
||||
file = 1 # is a file
|
||||
file = 0 # is not a file (may not even exist)
|
||||
dir = 1 # is a dir
|
||||
link = 1 # is a link
|
||||
exists = 1 # exists
|
||||
|
||||
You can specify multiple checker definitions, for example::
|
||||
|
||||
@@ -961,12 +961,10 @@ class LocalPath:
|
||||
return p
|
||||
|
||||
@overload
|
||||
def stat(self, raising: Literal[True] = ...) -> Stat:
|
||||
...
|
||||
def stat(self, raising: Literal[True] = ...) -> Stat: ...
|
||||
|
||||
@overload
|
||||
def stat(self, raising: Literal[False]) -> Stat | None:
|
||||
...
|
||||
def stat(self, raising: Literal[False]) -> Stat | None: ...
|
||||
|
||||
def stat(self, raising: bool = True) -> Stat | None:
|
||||
"""Return an os.stat() tuple."""
|
||||
@@ -1100,9 +1098,7 @@ class LocalPath:
|
||||
modname = self.purebasename
|
||||
spec = importlib.util.spec_from_file_location(modname, str(self))
|
||||
if spec is None or spec.loader is None:
|
||||
raise ImportError(
|
||||
f"Can't find module {modname} at location {str(self)}"
|
||||
)
|
||||
raise ImportError(f"Can't find module {modname} at location {self!s}")
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
@@ -1167,7 +1163,8 @@ class LocalPath:
|
||||
where the 'self' path points to executable.
|
||||
The process is directly invoked and not through a system shell.
|
||||
"""
|
||||
from subprocess import Popen, PIPE
|
||||
from subprocess import PIPE
|
||||
from subprocess import Popen
|
||||
|
||||
popen_opts.pop("stdout", None)
|
||||
popen_opts.pop("stderr", None)
|
||||
@@ -1273,12 +1270,7 @@ class LocalPath:
|
||||
|
||||
if rootdir is None:
|
||||
rootdir = cls.get_temproot()
|
||||
# Using type ignore here because of this error:
|
||||
# error: Argument 1 has incompatible type overloaded function; expected "Callable[[str], str]" [arg-type]
|
||||
# Which seems incorrect, given tempfile.mkdtemp supports the given argument types.
|
||||
path = error.checked_call(
|
||||
tempfile.mkdtemp, dir=str(rootdir) # type:ignore[arg-type]
|
||||
)
|
||||
path = error.checked_call(tempfile.mkdtemp, dir=str(rootdir))
|
||||
return cls(path)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Support for presenting detailed information in failing assertions."""
|
||||
|
||||
import sys
|
||||
from typing import Any
|
||||
from typing import Generator
|
||||
@@ -15,6 +17,7 @@ from _pytest.config import hookimpl
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.nodes import Item
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.main import Session
|
||||
|
||||
@@ -128,7 +131,6 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
|
||||
reporting via the pytest_assertrepr_compare hook. This sets up this custom
|
||||
comparison for the test.
|
||||
"""
|
||||
|
||||
ihook = item.ihook
|
||||
|
||||
def callbinrepr(op, left: object, right: object) -> Optional[str]:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Rewrite assertion AST to produce nice error messages."""
|
||||
|
||||
import ast
|
||||
from collections import defaultdict
|
||||
import errno
|
||||
import functools
|
||||
import importlib.abc
|
||||
@@ -9,13 +11,12 @@ import io
|
||||
import itertools
|
||||
import marshal
|
||||
import os
|
||||
from pathlib import Path
|
||||
from pathlib import PurePath
|
||||
import struct
|
||||
import sys
|
||||
import tokenize
|
||||
import types
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from pathlib import PurePath
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import IO
|
||||
@@ -33,15 +34,17 @@ from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
|
||||
from _pytest._io.saferepr import saferepr
|
||||
from _pytest._version import version
|
||||
from _pytest.assertion import util
|
||||
from _pytest.assertion.util import ( # noqa: F401
|
||||
format_explanation as _format_explanation,
|
||||
)
|
||||
from _pytest.config import Config
|
||||
from _pytest.main import Session
|
||||
from _pytest.pathlib import absolutepath
|
||||
from _pytest.pathlib import fnmatch_ex
|
||||
from _pytest.stash import StashKey
|
||||
|
||||
|
||||
# fmt: off
|
||||
from _pytest.assertion.util import format_explanation as _format_explanation # noqa:F401, isort:skip
|
||||
# fmt:on
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.assertion import AssertionState
|
||||
|
||||
@@ -286,15 +289,13 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
|
||||
else:
|
||||
from importlib.abc import TraversableResources
|
||||
|
||||
def get_resource_reader(self, name: str) -> TraversableResources: # type: ignore
|
||||
def get_resource_reader(self, name: str) -> TraversableResources:
|
||||
if sys.version_info < (3, 11):
|
||||
from importlib.readers import FileReader
|
||||
else:
|
||||
from importlib.resources.readers import FileReader
|
||||
|
||||
return FileReader( # type:ignore[no-any-return]
|
||||
types.SimpleNamespace(path=self._rewritten_names[name])
|
||||
)
|
||||
return FileReader(types.SimpleNamespace(path=self._rewritten_names[name]))
|
||||
|
||||
|
||||
def _write_pyc_fp(
|
||||
@@ -669,9 +670,9 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
self.enable_assertion_pass_hook = False
|
||||
self.source = source
|
||||
self.scope: tuple[ast.AST, ...] = ()
|
||||
self.variables_overwrite: defaultdict[
|
||||
tuple[ast.AST, ...], Dict[str, str]
|
||||
] = defaultdict(dict)
|
||||
self.variables_overwrite: defaultdict[tuple[ast.AST, ...], Dict[str, str]] = (
|
||||
defaultdict(dict)
|
||||
)
|
||||
|
||||
def run(self, mod: ast.Module) -> None:
|
||||
"""Find all assert statements in *mod* and rewrite them."""
|
||||
@@ -858,9 +859,10 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
the expression is false.
|
||||
"""
|
||||
if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1:
|
||||
from _pytest.warning_types import PytestAssertRewriteWarning
|
||||
import warnings
|
||||
|
||||
from _pytest.warning_types import PytestAssertRewriteWarning
|
||||
|
||||
# TODO: This assert should not be needed.
|
||||
assert self.module_path is not None
|
||||
warnings.warn_explicit(
|
||||
@@ -921,7 +923,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
# If any hooks implement assert_pass hook
|
||||
hook_impl_test = ast.If(
|
||||
self.helper("_check_if_assertion_pass_impl"),
|
||||
self.expl_stmts + [hook_call_pass],
|
||||
[*self.expl_stmts, hook_call_pass],
|
||||
[],
|
||||
)
|
||||
statements_pass = [hook_impl_test]
|
||||
@@ -971,7 +973,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
# name if it's a local variable or _should_repr_global_name()
|
||||
# thinks it's acceptable.
|
||||
locs = ast.Call(self.builtin("locals"), [], [])
|
||||
target_id = name.target.id # type: ignore[attr-defined]
|
||||
target_id = name.target.id
|
||||
inlocs = ast.Compare(ast.Constant(target_id), [ast.In()], [locs])
|
||||
dorepr = self.helper("_should_repr_global_name", name)
|
||||
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
|
||||
@@ -1002,7 +1004,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
if i:
|
||||
fail_inner: List[ast.stmt] = []
|
||||
# cond is set in a prior loop iteration below
|
||||
self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa
|
||||
self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa: F821
|
||||
self.expl_stmts = fail_inner
|
||||
# Check if the left operand is a ast.NamedExpr and the value has already been visited
|
||||
if (
|
||||
@@ -1016,9 +1018,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
]
|
||||
):
|
||||
pytest_temp = self.variable()
|
||||
self.variables_overwrite[self.scope][
|
||||
v.left.target.id
|
||||
] = v.left # type:ignore[assignment]
|
||||
self.variables_overwrite[self.scope][v.left.target.id] = v.left # type:ignore[assignment]
|
||||
v.left.target.id = pytest_temp
|
||||
self.push_format_context()
|
||||
res, expl = self.visit(v)
|
||||
@@ -1062,9 +1062,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get(
|
||||
self.scope, {}
|
||||
):
|
||||
arg = self.variables_overwrite[self.scope][
|
||||
arg.id
|
||||
] # type:ignore[assignment]
|
||||
arg = self.variables_overwrite[self.scope][arg.id] # type:ignore[assignment]
|
||||
res, expl = self.visit(arg)
|
||||
arg_expls.append(expl)
|
||||
new_args.append(res)
|
||||
@@ -1072,9 +1070,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
if isinstance(
|
||||
keyword.value, ast.Name
|
||||
) and keyword.value.id in self.variables_overwrite.get(self.scope, {}):
|
||||
keyword.value = self.variables_overwrite[self.scope][
|
||||
keyword.value.id
|
||||
] # type:ignore[assignment]
|
||||
keyword.value = self.variables_overwrite[self.scope][keyword.value.id] # type:ignore[assignment]
|
||||
res, expl = self.visit(keyword.value)
|
||||
new_kwargs.append(ast.keyword(keyword.arg, res))
|
||||
if keyword.arg:
|
||||
@@ -1111,13 +1107,9 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
if isinstance(
|
||||
comp.left, ast.Name
|
||||
) and comp.left.id in self.variables_overwrite.get(self.scope, {}):
|
||||
comp.left = self.variables_overwrite[self.scope][
|
||||
comp.left.id
|
||||
] # type:ignore[assignment]
|
||||
comp.left = self.variables_overwrite[self.scope][comp.left.id] # type:ignore[assignment]
|
||||
if isinstance(comp.left, ast.NamedExpr):
|
||||
self.variables_overwrite[self.scope][
|
||||
comp.left.target.id
|
||||
] = comp.left # type:ignore[assignment]
|
||||
self.variables_overwrite[self.scope][comp.left.target.id] = comp.left # type:ignore[assignment]
|
||||
left_res, left_expl = self.visit(comp.left)
|
||||
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
|
||||
left_expl = f"({left_expl})"
|
||||
@@ -1135,9 +1127,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
and next_operand.target.id == left_res.id
|
||||
):
|
||||
next_operand.target.id = self.variable()
|
||||
self.variables_overwrite[self.scope][
|
||||
left_res.id
|
||||
] = next_operand # type:ignore[assignment]
|
||||
self.variables_overwrite[self.scope][left_res.id] = next_operand # type:ignore[assignment]
|
||||
next_res, next_expl = self.visit(next_operand)
|
||||
if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
|
||||
next_expl = f"({next_expl})"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Current default behaviour is to truncate assertion explanations at
|
||||
terminal lines, unless running with an assertions verbosity level of at least 2 or running on CI.
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
@@ -91,7 +92,8 @@ def _truncate_explanation(
|
||||
else:
|
||||
# Add proper ellipsis when we were able to fit a full line exactly
|
||||
truncated_explanation[-1] = "..."
|
||||
return truncated_explanation + [
|
||||
return [
|
||||
*truncated_explanation,
|
||||
"",
|
||||
f"...Full output truncated ({truncated_line_count} line"
|
||||
f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Utilities for assertion debugging."""
|
||||
|
||||
import collections.abc
|
||||
import os
|
||||
import pprint
|
||||
@@ -14,13 +16,14 @@ from typing import Protocol
|
||||
from typing import Sequence
|
||||
from unicodedata import normalize
|
||||
|
||||
import _pytest._code
|
||||
from _pytest import outcomes
|
||||
import _pytest._code
|
||||
from _pytest._io.pprint import PrettyPrinter
|
||||
from _pytest._io.saferepr import saferepr
|
||||
from _pytest._io.saferepr import saferepr_unlimited
|
||||
from _pytest.config import Config
|
||||
|
||||
|
||||
# The _reprcompare attribute on the util module is used by the new assertion
|
||||
# interpretation code and assertion rewriter to detect this plugin was
|
||||
# loaded and in turn call the hooks defined here as part of the
|
||||
@@ -220,10 +223,9 @@ def assertrepr_compare(
|
||||
except outcomes.Exit:
|
||||
raise
|
||||
except Exception:
|
||||
repr_crash = _pytest._code.ExceptionInfo.from_current()._getreprcrash()
|
||||
explanation = [
|
||||
"(pytest_assertion plugin: representation of details failed: {}.".format(
|
||||
_pytest._code.ExceptionInfo.from_current()._getreprcrash()
|
||||
),
|
||||
f"(pytest_assertion plugin: representation of details failed: {repr_crash}.",
|
||||
" Probably an object has a faulty __repr__.)",
|
||||
]
|
||||
|
||||
@@ -231,8 +233,8 @@ def assertrepr_compare(
|
||||
return None
|
||||
|
||||
if explanation[0] != "":
|
||||
explanation = [""] + explanation
|
||||
return [summary] + explanation
|
||||
explanation = ["", *explanation]
|
||||
return [summary, *explanation]
|
||||
|
||||
|
||||
def _compare_eq_any(
|
||||
@@ -301,8 +303,8 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
|
||||
if i > 42:
|
||||
i -= 10 # Provide some context
|
||||
explanation += [
|
||||
"Skipping {} identical trailing "
|
||||
"characters in diff, use -v to show".format(i)
|
||||
f"Skipping {i} identical trailing "
|
||||
"characters in diff, use -v to show"
|
||||
]
|
||||
left = left[:-i]
|
||||
right = right[:-i]
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Implementation of the cache provider."""
|
||||
|
||||
# This plugin was not named "cache" to avoid conflicts with the external
|
||||
# pytest-cache version.
|
||||
import dataclasses
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
from typing import Dict
|
||||
from typing import final
|
||||
from typing import Generator
|
||||
@@ -31,6 +34,7 @@ from _pytest.nodes import Directory
|
||||
from _pytest.nodes import File
|
||||
from _pytest.reports import TestReport
|
||||
|
||||
|
||||
README_CONTENT = """\
|
||||
# pytest cache directory #
|
||||
|
||||
@@ -111,6 +115,7 @@ class Cache:
|
||||
"""
|
||||
check_ispytest(_ispytest)
|
||||
import warnings
|
||||
|
||||
from _pytest.warning_types import PytestCacheWarning
|
||||
|
||||
warnings.warn(
|
||||
@@ -119,6 +124,10 @@ class Cache:
|
||||
stacklevel=3,
|
||||
)
|
||||
|
||||
def _mkdir(self, path: Path) -> None:
|
||||
self._ensure_cache_dir_and_supporting_files()
|
||||
path.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
def mkdir(self, name: str) -> Path:
|
||||
"""Return a directory path object with the given name.
|
||||
|
||||
@@ -137,7 +146,7 @@ class Cache:
|
||||
if len(path.parts) > 1:
|
||||
raise ValueError("name is not allowed to contain path separators")
|
||||
res = self._cachedir.joinpath(self._CACHE_PREFIX_DIRS, path)
|
||||
res.mkdir(exist_ok=True, parents=True)
|
||||
self._mkdir(res)
|
||||
return res
|
||||
|
||||
def _getvaluepath(self, key: str) -> Path:
|
||||
@@ -174,19 +183,13 @@ class Cache:
|
||||
"""
|
||||
path = self._getvaluepath(key)
|
||||
try:
|
||||
if path.parent.is_dir():
|
||||
cache_dir_exists_already = True
|
||||
else:
|
||||
cache_dir_exists_already = self._cachedir.exists()
|
||||
path.parent.mkdir(exist_ok=True, parents=True)
|
||||
self._mkdir(path.parent)
|
||||
except OSError as exc:
|
||||
self.warn(
|
||||
f"could not create cache path {path}: {exc}",
|
||||
_ispytest=True,
|
||||
)
|
||||
return
|
||||
if not cache_dir_exists_already:
|
||||
self._ensure_supporting_files()
|
||||
data = json.dumps(value, ensure_ascii=False, indent=2)
|
||||
try:
|
||||
f = path.open("w", encoding="UTF-8")
|
||||
@@ -199,17 +202,32 @@ class Cache:
|
||||
with f:
|
||||
f.write(data)
|
||||
|
||||
def _ensure_supporting_files(self) -> None:
|
||||
"""Create supporting files in the cache dir that are not really part of the cache."""
|
||||
readme_path = self._cachedir / "README.md"
|
||||
readme_path.write_text(README_CONTENT, encoding="UTF-8")
|
||||
def _ensure_cache_dir_and_supporting_files(self) -> None:
|
||||
"""Create the cache dir and its supporting files."""
|
||||
if self._cachedir.is_dir():
|
||||
return
|
||||
|
||||
gitignore_path = self._cachedir.joinpath(".gitignore")
|
||||
msg = "# Created by pytest automatically.\n*\n"
|
||||
gitignore_path.write_text(msg, encoding="UTF-8")
|
||||
self._cachedir.parent.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory(
|
||||
prefix="pytest-cache-files-",
|
||||
dir=self._cachedir.parent,
|
||||
) as newpath:
|
||||
path = Path(newpath)
|
||||
with open(path.joinpath("README.md"), "xt", encoding="UTF-8") as f:
|
||||
f.write(README_CONTENT)
|
||||
with open(path.joinpath(".gitignore"), "xt", encoding="UTF-8") as f:
|
||||
f.write("# Created by pytest automatically.\n*\n")
|
||||
with open(path.joinpath("CACHEDIR.TAG"), "xb") as f:
|
||||
f.write(CACHEDIR_TAG_CONTENT)
|
||||
|
||||
cachedir_tag_path = self._cachedir.joinpath("CACHEDIR.TAG")
|
||||
cachedir_tag_path.write_bytes(CACHEDIR_TAG_CONTENT)
|
||||
path.rename(self._cachedir)
|
||||
# Create a directory in place of the one we just moved so that `TemporaryDirectory`'s
|
||||
# cleanup doesn't complain.
|
||||
#
|
||||
# TODO: pass ignore_cleanup_errors=True when we no longer support python < 3.10. See
|
||||
# https://github.com/python/cpython/issues/74168. Note that passing delete=False would
|
||||
# do the wrong thing in case of errors and isn't supported until python 3.12.
|
||||
path.mkdir()
|
||||
|
||||
|
||||
class LFPluginCollWrapper:
|
||||
@@ -366,15 +384,13 @@ class LFPlugin:
|
||||
|
||||
noun = "failure" if self._previously_failed_count == 1 else "failures"
|
||||
suffix = " first" if self.config.getoption("failedfirst") else ""
|
||||
self._report_status = "rerun previous {count} {noun}{suffix}".format(
|
||||
count=self._previously_failed_count, suffix=suffix, noun=noun
|
||||
self._report_status = (
|
||||
f"rerun previous {self._previously_failed_count} {noun}{suffix}"
|
||||
)
|
||||
|
||||
if self._skipped_files > 0:
|
||||
files_noun = "file" if self._skipped_files == 1 else "files"
|
||||
self._report_status += " (skipped {files} {files_noun})".format(
|
||||
files=self._skipped_files, files_noun=files_noun
|
||||
)
|
||||
self._report_status += f" (skipped {self._skipped_files} {files_noun})"
|
||||
else:
|
||||
self._report_status = "no previously failed tests, "
|
||||
if self.config.getoption("last_failed_no_failures") == "none":
|
||||
@@ -431,7 +447,7 @@ class NFPlugin:
|
||||
return res
|
||||
|
||||
def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]:
|
||||
return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True) # type: ignore[no-any-return]
|
||||
return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True)
|
||||
|
||||
def pytest_sessionfinish(self) -> None:
|
||||
config = self.config
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Per-test stdout/stderr capturing mechanism."""
|
||||
|
||||
import abc
|
||||
import collections
|
||||
import contextlib
|
||||
import io
|
||||
from io import UnsupportedOperation
|
||||
import os
|
||||
import sys
|
||||
from io import UnsupportedOperation
|
||||
from tempfile import TemporaryFile
|
||||
from types import TracebackType
|
||||
from typing import Any
|
||||
@@ -38,6 +40,7 @@ from _pytest.nodes import File
|
||||
from _pytest.nodes import Item
|
||||
from _pytest.reports import CollectReport
|
||||
|
||||
|
||||
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
|
||||
|
||||
|
||||
@@ -103,17 +106,16 @@ def _windowsconsoleio_workaround(stream: TextIO) -> None:
|
||||
return
|
||||
|
||||
# Bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666).
|
||||
if not hasattr(stream, "buffer"): # type: ignore[unreachable]
|
||||
if not hasattr(stream, "buffer"): # type: ignore[unreachable,unused-ignore]
|
||||
return
|
||||
|
||||
buffered = hasattr(stream.buffer, "raw")
|
||||
raw_stdout = stream.buffer.raw if buffered else stream.buffer # type: ignore[attr-defined]
|
||||
raw_stdout = stream.buffer.raw if hasattr(stream.buffer, "raw") else stream.buffer
|
||||
|
||||
if not isinstance(raw_stdout, io._WindowsConsoleIO): # type: ignore[attr-defined]
|
||||
if not isinstance(raw_stdout, io._WindowsConsoleIO): # type: ignore[attr-defined,unused-ignore]
|
||||
return
|
||||
|
||||
def _reopen_stdio(f, mode):
|
||||
if not buffered and mode[0] == "w":
|
||||
if not hasattr(stream.buffer, "raw") and mode[0] == "w":
|
||||
buffering = 0
|
||||
else:
|
||||
buffering = -1
|
||||
@@ -480,12 +482,9 @@ class FDCaptureBase(CaptureBase[AnyStr]):
|
||||
self._state = "initialized"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<{} {} oldfd={} _state={!r} tmpfile={!r}>".format(
|
||||
self.__class__.__name__,
|
||||
self.targetfd,
|
||||
self.targetfd_save,
|
||||
self._state,
|
||||
self.tmpfile,
|
||||
return (
|
||||
f"<{self.__class__.__name__} {self.targetfd} oldfd={self.targetfd_save} "
|
||||
f"_state={self._state!r} tmpfile={self.tmpfile!r}>"
|
||||
)
|
||||
|
||||
def _assert_state(self, op: str, states: Tuple[str, ...]) -> None:
|
||||
@@ -596,7 +595,8 @@ if sys.version_info >= (3, 11) or TYPE_CHECKING:
|
||||
else:
|
||||
|
||||
class CaptureResult(
|
||||
collections.namedtuple("CaptureResult", ["out", "err"]), Generic[AnyStr]
|
||||
collections.namedtuple("CaptureResult", ["out", "err"]), # noqa: PYI024
|
||||
Generic[AnyStr],
|
||||
):
|
||||
"""The result of :method:`caplog.readouterr() <pytest.CaptureFixture.readouterr>`."""
|
||||
|
||||
@@ -618,12 +618,9 @@ class MultiCapture(Generic[AnyStr]):
|
||||
self.err: Optional[CaptureBase[AnyStr]] = err
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<MultiCapture out={!r} err={!r} in_={!r} _state={!r} _in_suspended={!r}>".format(
|
||||
self.out,
|
||||
self.err,
|
||||
self.in_,
|
||||
self._state,
|
||||
self._in_suspended,
|
||||
return (
|
||||
f"<MultiCapture out={self.out!r} err={self.err!r} in_={self.in_!r} "
|
||||
f"_state={self._state!r} _in_suspended={self._in_suspended!r}>"
|
||||
)
|
||||
|
||||
def start_capturing(self) -> None:
|
||||
@@ -732,8 +729,9 @@ class CaptureManager:
|
||||
self._capture_fixture: Optional[CaptureFixture[Any]] = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<CaptureManager _method={!r} _global_capturing={!r} _capture_fixture={!r}>".format(
|
||||
self._method, self._global_capturing, self._capture_fixture
|
||||
return (
|
||||
f"<CaptureManager _method={self._method!r} _global_capturing={self._global_capturing!r} "
|
||||
f"_capture_fixture={self._capture_fixture!r}>"
|
||||
)
|
||||
|
||||
def is_capturing(self) -> Union[str, bool]:
|
||||
@@ -789,9 +787,7 @@ class CaptureManager:
|
||||
current_fixture = self._capture_fixture.request.fixturename
|
||||
requested_fixture = capture_fixture.request.fixturename
|
||||
capture_fixture.request.raiseerror(
|
||||
"cannot use {} and {} at the same time".format(
|
||||
requested_fixture, current_fixture
|
||||
)
|
||||
f"cannot use {requested_fixture} and {current_fixture} at the same time"
|
||||
)
|
||||
self._capture_fixture = capture_fixture
|
||||
|
||||
@@ -987,7 +983,6 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
|
||||
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_output(capsys):
|
||||
@@ -1015,7 +1010,6 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None,
|
||||
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_output(capsysbinary):
|
||||
@@ -1043,7 +1037,6 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
|
||||
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_system_echo(capfd):
|
||||
@@ -1071,7 +1064,6 @@ def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, N
|
||||
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_system_echo(capfdbinary):
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Python version compatibility code."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import enum
|
||||
import functools
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
from inspect import Parameter
|
||||
from inspect import signature
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Final
|
||||
from typing import NoReturn
|
||||
from typing import TypeVar
|
||||
|
||||
import py
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_S = TypeVar("_S")
|
||||
|
||||
#: constant to prepare valuing pylib path replacements/lazy proxies later on
|
||||
# intended for removal in pytest 8.0 or 9.0
|
||||
|
||||
@@ -41,7 +39,7 @@ def legacy_path(path: str | os.PathLike[str]) -> LEGACY_PATH:
|
||||
# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
|
||||
class NotSetType(enum.Enum):
|
||||
token = 0
|
||||
NOTSET: Final = NotSetType.token # noqa: E305
|
||||
NOTSET: Final = NotSetType.token
|
||||
# fmt: on
|
||||
|
||||
|
||||
@@ -105,7 +103,6 @@ def getfuncargnames(
|
||||
function: Callable[..., object],
|
||||
*,
|
||||
name: str = "",
|
||||
is_method: bool = False,
|
||||
cls: type | None = None,
|
||||
) -> tuple[str, ...]:
|
||||
"""Return the names of a function's mandatory arguments.
|
||||
@@ -116,9 +113,8 @@ def getfuncargnames(
|
||||
* Aren't bound with functools.partial.
|
||||
* Aren't replaced with mocks.
|
||||
|
||||
The is_method and cls arguments indicate that the function should
|
||||
be treated as a bound method even though it's not unless, only in
|
||||
the case of cls, the function is a static method.
|
||||
The cls arguments indicate that the function should be treated as a bound
|
||||
method even though it's not unless the function is a static method.
|
||||
|
||||
The name parameter should be the original name in which the function was collected.
|
||||
"""
|
||||
@@ -156,7 +152,7 @@ def getfuncargnames(
|
||||
# If this function should be treated as a bound method even though
|
||||
# it's passed as an unbound method or function, remove the first
|
||||
# parameter name.
|
||||
if is_method or (
|
||||
if (
|
||||
# Not using `getattr` because we don't want to resolve the staticmethod.
|
||||
# Not using `cls.__dict__` because we want to check the entire MRO.
|
||||
cls
|
||||
@@ -191,25 +187,13 @@ _non_printable_ascii_translate_table.update(
|
||||
)
|
||||
|
||||
|
||||
def _translate_non_printable(s: str) -> str:
|
||||
return s.translate(_non_printable_ascii_translate_table)
|
||||
|
||||
|
||||
STRING_TYPES = bytes, str
|
||||
|
||||
|
||||
def _bytes_to_ascii(val: bytes) -> str:
|
||||
return val.decode("ascii", "backslashreplace")
|
||||
|
||||
|
||||
def ascii_escaped(val: bytes | str) -> str:
|
||||
r"""If val is pure ASCII, return it as an str, otherwise, escape
|
||||
bytes objects into a sequence of escaped bytes:
|
||||
|
||||
b'\xc3\xb4\xc5\xd6' -> r'\xc3\xb4\xc5\xd6'
|
||||
|
||||
and escapes unicode objects into a sequence of escaped unicode
|
||||
ids, e.g.:
|
||||
and escapes strings into a sequence of escaped unicode ids, e.g.:
|
||||
|
||||
r'4\nV\U00043efa\x0eMXWB\x1e\u3028\u15fd\xcd\U0007d944'
|
||||
|
||||
@@ -220,10 +204,10 @@ def ascii_escaped(val: bytes | str) -> str:
|
||||
a UTF-8 string.
|
||||
"""
|
||||
if isinstance(val, bytes):
|
||||
ret = _bytes_to_ascii(val)
|
||||
ret = val.decode("ascii", "backslashreplace")
|
||||
else:
|
||||
ret = val.encode("unicode_escape").decode("ascii")
|
||||
return _translate_non_printable(ret)
|
||||
return ret.translate(_non_printable_ascii_translate_table)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
@@ -258,9 +242,7 @@ def get_real_func(obj):
|
||||
from _pytest._io.saferepr import saferepr
|
||||
|
||||
raise ValueError(
|
||||
("could not find real function of {start}\nstopped at {current}").format(
|
||||
start=saferepr(start_obj), current=saferepr(obj)
|
||||
)
|
||||
f"could not find real function of {saferepr(start_obj)}\nstopped at {saferepr(obj)}"
|
||||
)
|
||||
if isinstance(obj, functools.partial):
|
||||
obj = obj.func
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Command line options, ini-file and conftest.py processing."""
|
||||
|
||||
import argparse
|
||||
import collections.abc
|
||||
import copy
|
||||
import dataclasses
|
||||
import enum
|
||||
from functools import lru_cache
|
||||
import glob
|
||||
import importlib.metadata
|
||||
import inspect
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shlex
|
||||
import sys
|
||||
import types
|
||||
import warnings
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
import types
|
||||
from types import FunctionType
|
||||
from types import TracebackType
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
@@ -37,6 +37,7 @@ from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
import warnings
|
||||
|
||||
import pluggy
|
||||
from pluggy import HookimplMarker
|
||||
@@ -45,16 +46,17 @@ from pluggy import HookspecMarker
|
||||
from pluggy import HookspecOpts
|
||||
from pluggy import PluginManager
|
||||
|
||||
import _pytest._code
|
||||
import _pytest.deprecated
|
||||
import _pytest.hookspec
|
||||
from .compat import PathAwareHookProxy
|
||||
from .exceptions import PrintHelp as PrintHelp
|
||||
from .exceptions import UsageError as UsageError
|
||||
from .findpaths import determine_setup
|
||||
from _pytest import __version__
|
||||
import _pytest._code
|
||||
from _pytest._code import ExceptionInfo
|
||||
from _pytest._code import filter_traceback
|
||||
from _pytest._io import TerminalWriter
|
||||
import _pytest.deprecated
|
||||
import _pytest.hookspec
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.outcomes import Skipped
|
||||
from _pytest.pathlib import absolutepath
|
||||
@@ -67,10 +69,12 @@ from _pytest.stash import Stash
|
||||
from _pytest.warning_types import PytestConfigWarning
|
||||
from _pytest.warning_types import warn_explicit_for
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .argparsing import Argument
|
||||
from .argparsing import Parser
|
||||
from _pytest._code.code import _TracebackStyle
|
||||
from _pytest.terminal import TerminalReporter
|
||||
from .argparsing import Argument, Parser
|
||||
|
||||
|
||||
_PluggyPlugin = object
|
||||
@@ -114,16 +118,14 @@ class ConftestImportFailure(Exception):
|
||||
def __init__(
|
||||
self,
|
||||
path: Path,
|
||||
excinfo: Tuple[Type[Exception], Exception, TracebackType],
|
||||
*,
|
||||
cause: Exception,
|
||||
) -> None:
|
||||
super().__init__(path, excinfo)
|
||||
self.path = path
|
||||
self.excinfo = excinfo
|
||||
self.cause = cause
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "{}: {} (from {})".format(
|
||||
self.excinfo[0].__name__, self.excinfo[1], self.path
|
||||
)
|
||||
return f"{type(self.cause).__name__}: {self.cause} (from {self.path})"
|
||||
|
||||
|
||||
def filter_traceback_for_conftest_import_failure(
|
||||
@@ -150,11 +152,13 @@ def main(
|
||||
|
||||
:returns: An exit code.
|
||||
"""
|
||||
old_pytest_version = os.environ.get("PYTEST_VERSION")
|
||||
try:
|
||||
os.environ["PYTEST_VERSION"] = __version__
|
||||
try:
|
||||
config = _prepareconfig(args, plugins)
|
||||
except ConftestImportFailure as e:
|
||||
exc_info = ExceptionInfo.from_exc_info(e.excinfo)
|
||||
exc_info = ExceptionInfo.from_exception(e.cause)
|
||||
tw = TerminalWriter(sys.stderr)
|
||||
tw.line(f"ImportError while loading conftest '{e.path}'.", red=True)
|
||||
exc_info.traceback = exc_info.traceback.filter(
|
||||
@@ -185,6 +189,11 @@ def main(
|
||||
for msg in e.args:
|
||||
tw.line(f"ERROR: {msg}\n", red=True)
|
||||
return ExitCode.USAGE_ERROR
|
||||
finally:
|
||||
if old_pytest_version is None:
|
||||
os.environ.pop("PYTEST_VERSION", None)
|
||||
else:
|
||||
os.environ["PYTEST_VERSION"] = old_pytest_version
|
||||
|
||||
|
||||
def console_main() -> int:
|
||||
@@ -240,7 +249,8 @@ essential_plugins = (
|
||||
"helpconfig", # Provides -p.
|
||||
)
|
||||
|
||||
default_plugins = essential_plugins + (
|
||||
default_plugins = (
|
||||
*essential_plugins,
|
||||
"python",
|
||||
"terminal",
|
||||
"debugging",
|
||||
@@ -252,7 +262,6 @@ default_plugins = essential_plugins + (
|
||||
"monkeypatch",
|
||||
"recwarn",
|
||||
"pastebin",
|
||||
"nose",
|
||||
"assertion",
|
||||
"junitxml",
|
||||
"doctest",
|
||||
@@ -496,15 +505,19 @@ class PytestPluginManager(PluginManager):
|
||||
)
|
||||
)
|
||||
return None
|
||||
ret: Optional[str] = super().register(plugin, name)
|
||||
if ret:
|
||||
plugin_name = super().register(plugin, name)
|
||||
if plugin_name is not None:
|
||||
self.hook.pytest_plugin_registered.call_historic(
|
||||
kwargs=dict(plugin=plugin, manager=self)
|
||||
kwargs=dict(
|
||||
plugin=plugin,
|
||||
plugin_name=plugin_name,
|
||||
manager=self,
|
||||
)
|
||||
)
|
||||
|
||||
if isinstance(plugin, types.ModuleType):
|
||||
self.consider_module(plugin)
|
||||
return ret
|
||||
return plugin_name
|
||||
|
||||
def getplugin(self, name: str):
|
||||
# Support deprecated naming because plugins (xdist e.g.) use it.
|
||||
@@ -543,7 +556,10 @@ class PytestPluginManager(PluginManager):
|
||||
noconftest: bool,
|
||||
rootpath: Path,
|
||||
confcutdir: Optional[Path],
|
||||
invocation_dir: Path,
|
||||
importmode: Union[ImportMode, str],
|
||||
*,
|
||||
consider_namespace_packages: bool,
|
||||
) -> None:
|
||||
"""Load initial conftest files given a preparsed "namespace".
|
||||
|
||||
@@ -552,8 +568,9 @@ class PytestPluginManager(PluginManager):
|
||||
All builtin and 3rd party plugins will have been loaded, however, so
|
||||
common options will not confuse our logic here.
|
||||
"""
|
||||
current = Path.cwd()
|
||||
self._confcutdir = absolutepath(current / confcutdir) if confcutdir else None
|
||||
self._confcutdir = (
|
||||
absolutepath(invocation_dir / confcutdir) if confcutdir else None
|
||||
)
|
||||
self._noconftest = noconftest
|
||||
self._using_pyargs = pyargs
|
||||
foundanchor = False
|
||||
@@ -563,37 +580,73 @@ class PytestPluginManager(PluginManager):
|
||||
i = path.find("::")
|
||||
if i != -1:
|
||||
path = path[:i]
|
||||
anchor = absolutepath(current / path)
|
||||
anchor = absolutepath(invocation_dir / path)
|
||||
|
||||
# Ensure we do not break if what appears to be an anchor
|
||||
# is in fact a very long option (#10169, #11394).
|
||||
if safe_exists(anchor):
|
||||
self._try_load_conftest(anchor, importmode, rootpath)
|
||||
self._try_load_conftest(
|
||||
anchor,
|
||||
importmode,
|
||||
rootpath,
|
||||
consider_namespace_packages=consider_namespace_packages,
|
||||
)
|
||||
foundanchor = True
|
||||
if not foundanchor:
|
||||
self._try_load_conftest(current, importmode, rootpath)
|
||||
self._try_load_conftest(
|
||||
invocation_dir,
|
||||
importmode,
|
||||
rootpath,
|
||||
consider_namespace_packages=consider_namespace_packages,
|
||||
)
|
||||
|
||||
def _is_in_confcutdir(self, path: Path) -> bool:
|
||||
"""Whether a path is within the confcutdir.
|
||||
|
||||
When false, should not load conftest.
|
||||
"""
|
||||
"""Whether to consider the given path to load conftests from."""
|
||||
if self._confcutdir is None:
|
||||
return True
|
||||
# The semantics here are literally:
|
||||
# Do not load a conftest if it is found upwards from confcut dir.
|
||||
# But this is *not* the same as:
|
||||
# Load only conftests from confcutdir or below.
|
||||
# At first glance they might seem the same thing, however we do support use cases where
|
||||
# we want to load conftests that are not found in confcutdir or below, but are found
|
||||
# in completely different directory hierarchies like packages installed
|
||||
# in out-of-source trees.
|
||||
# (see #9767 for a regression where the logic was inverted).
|
||||
return path not in self._confcutdir.parents
|
||||
|
||||
def _try_load_conftest(
|
||||
self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path
|
||||
self,
|
||||
anchor: Path,
|
||||
importmode: Union[str, ImportMode],
|
||||
rootpath: Path,
|
||||
*,
|
||||
consider_namespace_packages: bool,
|
||||
) -> None:
|
||||
self._loadconftestmodules(anchor, importmode, rootpath)
|
||||
self._loadconftestmodules(
|
||||
anchor,
|
||||
importmode,
|
||||
rootpath,
|
||||
consider_namespace_packages=consider_namespace_packages,
|
||||
)
|
||||
# let's also consider test* subdirs
|
||||
if anchor.is_dir():
|
||||
for x in anchor.glob("test*"):
|
||||
if x.is_dir():
|
||||
self._loadconftestmodules(x, importmode, rootpath)
|
||||
self._loadconftestmodules(
|
||||
x,
|
||||
importmode,
|
||||
rootpath,
|
||||
consider_namespace_packages=consider_namespace_packages,
|
||||
)
|
||||
|
||||
def _loadconftestmodules(
|
||||
self, path: Path, importmode: Union[str, ImportMode], rootpath: Path
|
||||
self,
|
||||
path: Path,
|
||||
importmode: Union[str, ImportMode],
|
||||
rootpath: Path,
|
||||
*,
|
||||
consider_namespace_packages: bool,
|
||||
) -> None:
|
||||
if self._noconftest:
|
||||
return
|
||||
@@ -605,15 +658,17 @@ class PytestPluginManager(PluginManager):
|
||||
if directory in self._dirpath2confmods:
|
||||
return
|
||||
|
||||
# XXX these days we may rather want to use config.rootpath
|
||||
# and allow users to opt into looking into the rootdir parent
|
||||
# directories instead of requiring to specify confcutdir.
|
||||
clist = []
|
||||
for parent in reversed((directory, *directory.parents)):
|
||||
if self._is_in_confcutdir(parent):
|
||||
conftestpath = parent / "conftest.py"
|
||||
if conftestpath.is_file():
|
||||
mod = self._importconftest(conftestpath, importmode, rootpath)
|
||||
mod = self._importconftest(
|
||||
conftestpath,
|
||||
importmode,
|
||||
rootpath,
|
||||
consider_namespace_packages=consider_namespace_packages,
|
||||
)
|
||||
clist.append(mod)
|
||||
self._dirpath2confmods[directory] = clist
|
||||
|
||||
@@ -635,22 +690,39 @@ class PytestPluginManager(PluginManager):
|
||||
raise KeyError(name)
|
||||
|
||||
def _importconftest(
|
||||
self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path
|
||||
self,
|
||||
conftestpath: Path,
|
||||
importmode: Union[str, ImportMode],
|
||||
rootpath: Path,
|
||||
*,
|
||||
consider_namespace_packages: bool,
|
||||
) -> types.ModuleType:
|
||||
existing = self.get_plugin(str(conftestpath))
|
||||
conftestpath_plugin_name = str(conftestpath)
|
||||
existing = self.get_plugin(conftestpath_plugin_name)
|
||||
if existing is not None:
|
||||
return cast(types.ModuleType, existing)
|
||||
|
||||
# conftest.py files there are not in a Python package all have module
|
||||
# name "conftest", and thus conflict with each other. Clear the existing
|
||||
# before loading the new one, otherwise the existing one will be
|
||||
# returned from the module cache.
|
||||
pkgpath = resolve_package_path(conftestpath)
|
||||
if pkgpath is None:
|
||||
_ensure_removed_sysmodule(conftestpath.stem)
|
||||
try:
|
||||
del sys.modules[conftestpath.stem]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
mod = import_path(conftestpath, mode=importmode, root=rootpath)
|
||||
mod = import_path(
|
||||
conftestpath,
|
||||
mode=importmode,
|
||||
root=rootpath,
|
||||
consider_namespace_packages=consider_namespace_packages,
|
||||
)
|
||||
except Exception as e:
|
||||
assert e.__traceback__ is not None
|
||||
exc_info = (type(e), e, e.__traceback__)
|
||||
raise ConftestImportFailure(conftestpath, exc_info) from e
|
||||
raise ConftestImportFailure(conftestpath, cause=e) from e
|
||||
|
||||
self._check_non_top_pytest_plugins(mod, conftestpath)
|
||||
|
||||
@@ -659,10 +731,15 @@ class PytestPluginManager(PluginManager):
|
||||
if dirpath in self._dirpath2confmods:
|
||||
for path, mods in self._dirpath2confmods.items():
|
||||
if dirpath in path.parents or path == dirpath:
|
||||
assert mod not in mods
|
||||
if mod in mods:
|
||||
raise AssertionError(
|
||||
f"While trying to load conftest path {conftestpath!s}, "
|
||||
f"found that the module {mod} is already loaded with path {mod.__file__}. "
|
||||
"This is not supposed to happen. Please report this issue to pytest."
|
||||
)
|
||||
mods.append(mod)
|
||||
self.trace(f"loading conftestmodule {mod!r}")
|
||||
self.consider_conftest(mod)
|
||||
self.consider_conftest(mod, registration_name=conftestpath_plugin_name)
|
||||
return mod
|
||||
|
||||
def _check_non_top_pytest_plugins(
|
||||
@@ -733,18 +810,17 @@ class PytestPluginManager(PluginManager):
|
||||
self.set_blocked("pytest_" + name)
|
||||
else:
|
||||
name = arg
|
||||
# Unblock the plugin. None indicates that it has been blocked.
|
||||
# There is no interface with pluggy for this.
|
||||
if self._name2plugin.get(name, -1) is None:
|
||||
del self._name2plugin[name]
|
||||
# Unblock the plugin.
|
||||
self.unblock(name)
|
||||
if not name.startswith("pytest_"):
|
||||
if self._name2plugin.get("pytest_" + name, -1) is None:
|
||||
del self._name2plugin["pytest_" + name]
|
||||
self.unblock("pytest_" + name)
|
||||
self.import_plugin(arg, consider_entry_points=True)
|
||||
|
||||
def consider_conftest(self, conftestmodule: types.ModuleType) -> None:
|
||||
def consider_conftest(
|
||||
self, conftestmodule: types.ModuleType, registration_name: str
|
||||
) -> None:
|
||||
""":meta private:"""
|
||||
self.register(conftestmodule, name=conftestmodule.__file__)
|
||||
self.register(conftestmodule, name=registration_name)
|
||||
|
||||
def consider_env(self) -> None:
|
||||
""":meta private:"""
|
||||
@@ -800,7 +876,7 @@ class PytestPluginManager(PluginManager):
|
||||
|
||||
|
||||
def _get_plugin_specs_as_list(
|
||||
specs: Union[None, types.ModuleType, str, Sequence[str]]
|
||||
specs: Union[None, types.ModuleType, str, Sequence[str]],
|
||||
) -> List[str]:
|
||||
"""Parse a plugins specification into a list of plugin names."""
|
||||
# None means empty.
|
||||
@@ -821,13 +897,6 @@ def _get_plugin_specs_as_list(
|
||||
)
|
||||
|
||||
|
||||
def _ensure_removed_sysmodule(modname: str) -> None:
|
||||
try:
|
||||
del sys.modules[modname]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
class Notset:
|
||||
def __repr__(self):
|
||||
return "<NOTSET>"
|
||||
@@ -968,7 +1037,8 @@ class Config:
|
||||
*,
|
||||
invocation_params: Optional[InvocationParams] = None,
|
||||
) -> None:
|
||||
from .argparsing import Parser, FILE_OR_DIR
|
||||
from .argparsing import FILE_OR_DIR
|
||||
from .argparsing import Parser
|
||||
|
||||
if invocation_params is None:
|
||||
invocation_params = self.InvocationParams(
|
||||
@@ -1163,7 +1233,11 @@ class Config:
|
||||
noconftest=early_config.known_args_namespace.noconftest,
|
||||
rootpath=early_config.rootpath,
|
||||
confcutdir=early_config.known_args_namespace.confcutdir,
|
||||
invocation_dir=early_config.invocation_params.dir,
|
||||
importmode=early_config.known_args_namespace.importmode,
|
||||
consider_namespace_packages=early_config.getini(
|
||||
"consider_namespace_packages"
|
||||
),
|
||||
)
|
||||
|
||||
def _initini(self, args: Sequence[str]) -> None:
|
||||
@@ -1171,8 +1245,8 @@ class Config:
|
||||
args, namespace=copy.copy(self.option)
|
||||
)
|
||||
rootpath, inipath, inicfg = determine_setup(
|
||||
ns.inifilename,
|
||||
ns.file_or_dir + unknown_args,
|
||||
inifile=ns.inifilename,
|
||||
args=ns.file_or_dir + unknown_args,
|
||||
rootdir_cmd_arg=ns.rootdir or None,
|
||||
invocation_dir=self.invocation_params.dir,
|
||||
)
|
||||
@@ -1256,6 +1330,8 @@ class Config:
|
||||
"""Decide the args (initial paths/nodeids) to use given the relevant inputs.
|
||||
|
||||
:param warn: Whether can issue warnings.
|
||||
|
||||
:returns: The args and the args source. Guaranteed to be non-empty.
|
||||
"""
|
||||
if args:
|
||||
source = Config.ArgsSource.ARGS
|
||||
@@ -1319,11 +1395,6 @@ class Config:
|
||||
self._validate_plugins()
|
||||
self._warn_about_skipped_plugins()
|
||||
|
||||
if self.known_args_namespace.strict:
|
||||
self.issue_config_time_warning(
|
||||
_pytest.deprecated.STRICT_OPTION, stacklevel=2
|
||||
)
|
||||
|
||||
if self.known_args_namespace.confcutdir is None:
|
||||
if self.inipath is not None:
|
||||
confcutdir = str(self.inipath.parent)
|
||||
@@ -1369,12 +1440,7 @@ class Config:
|
||||
|
||||
if Version(minver) > Version(pytest.__version__):
|
||||
raise pytest.UsageError(
|
||||
"%s: 'minversion' requires pytest-%s, actual pytest-%s'"
|
||||
% (
|
||||
self.inipath,
|
||||
minver,
|
||||
pytest.__version__,
|
||||
)
|
||||
f"{self.inipath}: 'minversion' requires pytest-{minver}, actual pytest-{pytest.__version__}'"
|
||||
)
|
||||
|
||||
def _validate_config_options(self) -> None:
|
||||
@@ -1387,8 +1453,9 @@ class Config:
|
||||
return
|
||||
|
||||
# Imported lazily to improve start-up time.
|
||||
from packaging.requirements import InvalidRequirement
|
||||
from packaging.requirements import Requirement
|
||||
from packaging.version import Version
|
||||
from packaging.requirements import InvalidRequirement, Requirement
|
||||
|
||||
plugin_info = self.pluginmanager.list_plugin_distinfo()
|
||||
plugin_dist_info = {dist.project_name: dist.version for _, dist in plugin_info}
|
||||
@@ -1432,8 +1499,6 @@ class Config:
|
||||
kwargs=dict(pluginmanager=self.pluginmanager)
|
||||
)
|
||||
self._preparse(args, addopts=addopts)
|
||||
# XXX deprecated hook:
|
||||
self.hook.pytest_cmdline_preparse(config=self, args=args)
|
||||
self._parser.after_preparse = True # type: ignore
|
||||
try:
|
||||
args = self._parser.parse_setoption(
|
||||
@@ -1562,9 +1627,11 @@ class Config:
|
||||
# in this case, we already have a list ready to use.
|
||||
#
|
||||
if type == "paths":
|
||||
# TODO: This assert is probably not valid in all cases.
|
||||
assert self.inipath is not None
|
||||
dp = self.inipath.parent
|
||||
dp = (
|
||||
self.inipath.parent
|
||||
if self.inipath is not None
|
||||
else self.invocation_params.dir
|
||||
)
|
||||
input_values = shlex.split(value) if isinstance(value, str) else value
|
||||
return [dp / x for x in input_values]
|
||||
elif type == "args":
|
||||
@@ -1610,9 +1677,7 @@ class Config:
|
||||
key, user_ini_value = ini_config.split("=", 1)
|
||||
except ValueError as e:
|
||||
raise UsageError(
|
||||
"-o/--override-ini expects option=value style (got: {!r}).".format(
|
||||
ini_config
|
||||
)
|
||||
f"-o/--override-ini expects option=value style (got: {ini_config!r})."
|
||||
) from e
|
||||
else:
|
||||
if key == name:
|
||||
@@ -1653,6 +1718,8 @@ class Config:
|
||||
|
||||
#: Verbosity type for failed assertions (see :confval:`verbosity_assertions`).
|
||||
VERBOSITY_ASSERTIONS: Final = "assertions"
|
||||
#: Verbosity type for test case execution (see :confval:`verbosity_test_cases`).
|
||||
VERBOSITY_TEST_CASES: Final = "test_cases"
|
||||
_VERBOSITY_INI_DEFAULT: Final = "auto"
|
||||
|
||||
def get_verbosity(self, verbosity_type: Optional[str] = None) -> int:
|
||||
@@ -1670,7 +1737,6 @@ class Config:
|
||||
can be used to explicitly use the global verbosity level.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
# content of pytest.ini
|
||||
@@ -1850,13 +1916,13 @@ def parse_warning_filter(
|
||||
try:
|
||||
action: "warnings._ActionKind" = warnings._getaction(action_) # type: ignore[attr-defined]
|
||||
except warnings._OptionError as e:
|
||||
raise UsageError(error_template.format(error=str(e)))
|
||||
raise UsageError(error_template.format(error=str(e))) from None
|
||||
try:
|
||||
category: Type[Warning] = _resolve_warning_category(category_)
|
||||
except Exception:
|
||||
exc_info = ExceptionInfo.from_current()
|
||||
exception_text = exc_info.getrepr(style="native")
|
||||
raise UsageError(error_template.format(error=exception_text))
|
||||
raise UsageError(error_template.format(error=exception_text)) from None
|
||||
if message and escape:
|
||||
message = re.escape(message)
|
||||
if module and escape:
|
||||
@@ -1869,7 +1935,7 @@ def parse_warning_filter(
|
||||
except ValueError as e:
|
||||
raise UsageError(
|
||||
error_template.format(error=f"invalid lineno {lineno_!r}: {e}")
|
||||
)
|
||||
) from None
|
||||
else:
|
||||
lineno = 0
|
||||
return action, message, category, module, lineno
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import argparse
|
||||
from gettext import gettext
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
from gettext import gettext
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
@@ -19,11 +19,9 @@ from typing import Union
|
||||
|
||||
import _pytest._io
|
||||
from _pytest.config.exceptions import UsageError
|
||||
from _pytest.deprecated import ARGUMENT_PERCENT_DEFAULT
|
||||
from _pytest.deprecated import ARGUMENT_TYPE_STR
|
||||
from _pytest.deprecated import ARGUMENT_TYPE_STR_CHOICE
|
||||
from _pytest.deprecated import check_ispytest
|
||||
|
||||
|
||||
FILE_OR_DIR = "file_or_dir"
|
||||
|
||||
|
||||
@@ -124,7 +122,7 @@ class Parser:
|
||||
from _pytest._argcomplete import filescompleter
|
||||
|
||||
optparser = MyOptionParser(self, self.extra_info, prog=self.prog)
|
||||
groups = self._groups + [self._anonymous]
|
||||
groups = [*self._groups, self._anonymous]
|
||||
for group in groups:
|
||||
if group.options:
|
||||
desc = group.description or group.name
|
||||
@@ -200,9 +198,16 @@ class Parser:
|
||||
* ``paths``: a list of :class:`pathlib.Path`, separated as in a shell
|
||||
* ``pathlist``: a list of ``py.path``, separated as in a shell
|
||||
|
||||
For ``paths`` and ``pathlist`` types, they are considered relative to the ini-file.
|
||||
In case the execution is happening without an ini-file defined,
|
||||
they will be considered relative to the current working directory (for example with ``--override-ini``).
|
||||
|
||||
.. versionadded:: 7.0
|
||||
The ``paths`` variable type.
|
||||
|
||||
.. versionadded:: 8.1
|
||||
Use the current working directory to resolve ``paths`` and ``pathlist`` in the absence of an ini-file.
|
||||
|
||||
Defaults to ``string`` if ``None`` or not passed.
|
||||
:param default:
|
||||
Default value if no ini-file option exists but is queried.
|
||||
@@ -219,7 +224,7 @@ class Parser:
|
||||
|
||||
|
||||
def get_ini_default_for_type(
|
||||
type: Optional[Literal["string", "paths", "pathlist", "args", "linelist", "bool"]]
|
||||
type: Optional[Literal["string", "paths", "pathlist", "args", "linelist", "bool"]],
|
||||
) -> Any:
|
||||
"""
|
||||
Used by addini to get the default value for a given ini-option type, when
|
||||
@@ -259,39 +264,15 @@ class Argument:
|
||||
https://docs.python.org/3/library/optparse.html#optparse-standard-option-types
|
||||
"""
|
||||
|
||||
_typ_map = {"int": int, "string": str, "float": float, "complex": complex}
|
||||
|
||||
def __init__(self, *names: str, **attrs: Any) -> None:
|
||||
"""Store params in private vars for use in add_argument."""
|
||||
self._attrs = attrs
|
||||
self._short_opts: List[str] = []
|
||||
self._long_opts: List[str] = []
|
||||
if "%default" in (attrs.get("help") or ""):
|
||||
warnings.warn(ARGUMENT_PERCENT_DEFAULT, stacklevel=3)
|
||||
try:
|
||||
typ = attrs["type"]
|
||||
self.type = attrs["type"]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
# This might raise a keyerror as well, don't want to catch that.
|
||||
if isinstance(typ, str):
|
||||
if typ == "choice":
|
||||
warnings.warn(
|
||||
ARGUMENT_TYPE_STR_CHOICE.format(typ=typ, names=names),
|
||||
stacklevel=4,
|
||||
)
|
||||
# argparse expects a type here take it from
|
||||
# the type of the first element
|
||||
attrs["type"] = type(attrs["choices"][0])
|
||||
else:
|
||||
warnings.warn(
|
||||
ARGUMENT_TYPE_STR.format(typ=typ, names=names), stacklevel=4
|
||||
)
|
||||
attrs["type"] = Argument._typ_map[typ]
|
||||
# Used in test_parseopt -> test_parse_defaultgetter.
|
||||
self.type = attrs["type"]
|
||||
else:
|
||||
self.type = typ
|
||||
try:
|
||||
# Attribute existence is tested in Config._processopt.
|
||||
self.default = attrs["default"]
|
||||
@@ -322,11 +303,6 @@ class Argument:
|
||||
self._attrs[attr] = getattr(self, attr)
|
||||
except AttributeError:
|
||||
pass
|
||||
if self._attrs.get("help"):
|
||||
a = self._attrs["help"]
|
||||
a = a.replace("%default", "%(default)s")
|
||||
# a = a.replace('%prog', '%(prog)s')
|
||||
self._attrs["help"] = a
|
||||
return self._attrs
|
||||
|
||||
def _set_opt_strings(self, opts: Sequence[str]) -> None:
|
||||
@@ -439,6 +415,7 @@ class MyOptionParser(argparse.ArgumentParser):
|
||||
add_help=False,
|
||||
formatter_class=DropShorterLongHelpFormatter,
|
||||
allow_abbrev=False,
|
||||
fromfile_prefix_chars="@",
|
||||
)
|
||||
# extra_info is a dict of (param -> value) to display if there's
|
||||
# an usage error to provide more contextual information to the user.
|
||||
@@ -449,8 +426,7 @@ class MyOptionParser(argparse.ArgumentParser):
|
||||
msg = f"{self.prog}: error: {message}"
|
||||
|
||||
if hasattr(self._parser, "_config_source_hint"):
|
||||
# Type ignored because the attribute is set dynamically.
|
||||
msg = f"{msg} ({self._parser._config_source_hint})" # type: ignore
|
||||
msg = f"{msg} ({self._parser._config_source_hint})"
|
||||
|
||||
raise UsageError(self.format_usage() + msg)
|
||||
|
||||
@@ -472,7 +448,7 @@ class MyOptionParser(argparse.ArgumentParser):
|
||||
getattr(parsed, FILE_OR_DIR).extend(unrecognized)
|
||||
return parsed
|
||||
|
||||
if sys.version_info[:2] < (3, 9): # pragma: no cover
|
||||
if sys.version_info < (3, 9): # pragma: no cover
|
||||
# Backport of https://github.com/python/cpython/pull/14316 so we can
|
||||
# disable long --argument abbreviations without breaking short flags.
|
||||
def _parse_optional(
|
||||
@@ -480,7 +456,7 @@ class MyOptionParser(argparse.ArgumentParser):
|
||||
) -> Optional[Tuple[Optional[argparse.Action], str, Optional[str]]]:
|
||||
if not arg_string:
|
||||
return None
|
||||
if not arg_string[0] in self.prefix_chars:
|
||||
if arg_string[0] not in self.prefix_chars:
|
||||
return None
|
||||
if arg_string in self._option_string_actions:
|
||||
action = self._option_string_actions[arg_string]
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Mapping
|
||||
import warnings
|
||||
|
||||
import pluggy
|
||||
|
||||
@@ -11,6 +12,7 @@ from ..compat import LEGACY_PATH
|
||||
from ..compat import legacy_path
|
||||
from ..deprecated import HOOK_LEGACY_PATH_ARG
|
||||
|
||||
|
||||
# hookname: (Path, LEGACY_PATH)
|
||||
imply_paths_hooks: Mapping[str, tuple[str, str]] = {
|
||||
"pytest_ignore_collect": ("collection_path", "path"),
|
||||
@@ -53,7 +55,7 @@ class PathAwareHookProxy:
|
||||
path_var, fspath_var = imply_paths_hooks[key]
|
||||
|
||||
@functools.wraps(hook)
|
||||
def fixed_hook(**kw):
|
||||
def fixed_hook(**kw: Any) -> Any:
|
||||
path_value: Path | None = kw.pop(path_var, None)
|
||||
fspath_value: LEGACY_PATH | None = kw.pop(fspath_var, None)
|
||||
if fspath_value is not None:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
@@ -37,7 +37,6 @@ def load_config_dict_from_file(
|
||||
|
||||
Return None if the file does not contain valid pytest configuration.
|
||||
"""
|
||||
|
||||
# Configuration from ini files are obtained from the [pytest] section, if present.
|
||||
if filepath.suffix == ".ini":
|
||||
iniconfig = _parse_ini_config(filepath)
|
||||
@@ -87,6 +86,7 @@ def load_config_dict_from_file(
|
||||
|
||||
|
||||
def locate_config(
|
||||
invocation_dir: Path,
|
||||
args: Iterable[Path],
|
||||
) -> Tuple[Optional[Path], Optional[Path], Dict[str, Union[str, List[str]]]]:
|
||||
"""Search in the list of arguments for a valid ini-file for pytest,
|
||||
@@ -100,20 +100,28 @@ def locate_config(
|
||||
]
|
||||
args = [x for x in args if not str(x).startswith("-")]
|
||||
if not args:
|
||||
args = [Path.cwd()]
|
||||
args = [invocation_dir]
|
||||
found_pyproject_toml: Optional[Path] = None
|
||||
for arg in args:
|
||||
argpath = absolutepath(arg)
|
||||
for base in (argpath, *argpath.parents):
|
||||
for config_name in config_names:
|
||||
p = base / config_name
|
||||
if p.is_file():
|
||||
if p.name == "pyproject.toml" and found_pyproject_toml is None:
|
||||
found_pyproject_toml = p
|
||||
ini_config = load_config_dict_from_file(p)
|
||||
if ini_config is not None:
|
||||
return base, p, ini_config
|
||||
if found_pyproject_toml is not None:
|
||||
return found_pyproject_toml.parent, found_pyproject_toml, {}
|
||||
return None, None, {}
|
||||
|
||||
|
||||
def get_common_ancestor(paths: Iterable[Path]) -> Path:
|
||||
def get_common_ancestor(
|
||||
invocation_dir: Path,
|
||||
paths: Iterable[Path],
|
||||
) -> Path:
|
||||
common_ancestor: Optional[Path] = None
|
||||
for path in paths:
|
||||
if not path.exists():
|
||||
@@ -130,7 +138,7 @@ def get_common_ancestor(paths: Iterable[Path]) -> Path:
|
||||
if shared is not None:
|
||||
common_ancestor = shared
|
||||
if common_ancestor is None:
|
||||
common_ancestor = Path.cwd()
|
||||
common_ancestor = invocation_dir
|
||||
elif common_ancestor.is_file():
|
||||
common_ancestor = common_ancestor.parent
|
||||
return common_ancestor
|
||||
@@ -162,10 +170,11 @@ CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supporte
|
||||
|
||||
|
||||
def determine_setup(
|
||||
*,
|
||||
inifile: Optional[str],
|
||||
args: Sequence[str],
|
||||
rootdir_cmd_arg: Optional[str] = None,
|
||||
invocation_dir: Optional[Path] = None,
|
||||
rootdir_cmd_arg: Optional[str],
|
||||
invocation_dir: Path,
|
||||
) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]:
|
||||
"""Determine the rootdir, inifile and ini configuration values from the
|
||||
command line arguments.
|
||||
@@ -177,8 +186,7 @@ def determine_setup(
|
||||
:param rootdir_cmd_arg:
|
||||
The `--rootdir` command line argument, if given.
|
||||
:param invocation_dir:
|
||||
The working directory when pytest was invoked, if known.
|
||||
If not known, the current working directory is used.
|
||||
The working directory when pytest was invoked.
|
||||
"""
|
||||
rootdir = None
|
||||
dirs = get_dirs_from_args(args)
|
||||
@@ -189,8 +197,8 @@ def determine_setup(
|
||||
if rootdir_cmd_arg is None:
|
||||
rootdir = inipath_.parent
|
||||
else:
|
||||
ancestor = get_common_ancestor(dirs)
|
||||
rootdir, inipath, inicfg = locate_config([ancestor])
|
||||
ancestor = get_common_ancestor(invocation_dir, dirs)
|
||||
rootdir, inipath, inicfg = locate_config(invocation_dir, [ancestor])
|
||||
if rootdir is None and rootdir_cmd_arg is None:
|
||||
for possible_rootdir in (ancestor, *ancestor.parents):
|
||||
if (possible_rootdir / "setup.py").is_file():
|
||||
@@ -198,22 +206,18 @@ def determine_setup(
|
||||
break
|
||||
else:
|
||||
if dirs != [ancestor]:
|
||||
rootdir, inipath, inicfg = locate_config(dirs)
|
||||
rootdir, inipath, inicfg = locate_config(invocation_dir, dirs)
|
||||
if rootdir is None:
|
||||
if invocation_dir is not None:
|
||||
cwd = invocation_dir
|
||||
else:
|
||||
cwd = Path.cwd()
|
||||
rootdir = get_common_ancestor([cwd, ancestor])
|
||||
rootdir = get_common_ancestor(
|
||||
invocation_dir, [invocation_dir, ancestor]
|
||||
)
|
||||
if is_fs_root(rootdir):
|
||||
rootdir = ancestor
|
||||
if rootdir_cmd_arg:
|
||||
rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg))
|
||||
if not rootdir.is_dir():
|
||||
raise UsageError(
|
||||
"Directory '{}' not found. Check your '--rootdir' option.".format(
|
||||
rootdir
|
||||
)
|
||||
f"Directory '{rootdir}' not found. Check your '--rootdir' option."
|
||||
)
|
||||
assert rootdir is not None
|
||||
return rootdir, inipath, inicfg or {}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Interactive debugging with PDB, the Python Debugger."""
|
||||
|
||||
import argparse
|
||||
import functools
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Generator
|
||||
@@ -13,6 +14,7 @@ from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
import unittest
|
||||
|
||||
from _pytest import outcomes
|
||||
from _pytest._code import ExceptionInfo
|
||||
@@ -25,6 +27,7 @@ from _pytest.config.exceptions import UsageError
|
||||
from _pytest.nodes import Node
|
||||
from _pytest.reports import BaseReport
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.capture import CaptureManager
|
||||
from _pytest.runner import CallInfo
|
||||
@@ -152,9 +155,7 @@ class pytestPDB:
|
||||
def _get_pdb_wrapper_class(cls, pdb_cls, capman: Optional["CaptureManager"]):
|
||||
import _pytest.config
|
||||
|
||||
# Type ignored because mypy doesn't support "dynamic"
|
||||
# inheritance like this.
|
||||
class PytestPdbWrapper(pdb_cls): # type: ignore[valid-type,misc]
|
||||
class PytestPdbWrapper(pdb_cls):
|
||||
_pytest_capman = capman
|
||||
_continued = False
|
||||
|
||||
@@ -263,8 +264,7 @@ class pytestPDB:
|
||||
elif capturing:
|
||||
tw.sep(
|
||||
">",
|
||||
"PDB %s (IO-capturing turned off for %s)"
|
||||
% (method, capturing),
|
||||
f"PDB {method} (IO-capturing turned off for {capturing})",
|
||||
)
|
||||
else:
|
||||
tw.sep(">", f"PDB {method}")
|
||||
@@ -377,7 +377,8 @@ def _postmortem_traceback(excinfo: ExceptionInfo[BaseException]) -> types.Traceb
|
||||
elif isinstance(excinfo.value, ConftestImportFailure):
|
||||
# A config.ConftestImportFailure is not useful for post_mortem.
|
||||
# Use the underlying exception instead:
|
||||
return excinfo.value.excinfo[2]
|
||||
assert excinfo.value.cause.__traceback__ is not None
|
||||
return excinfo.value.cause.__traceback__
|
||||
else:
|
||||
assert excinfo._excinfo is not None
|
||||
return excinfo._excinfo[2]
|
||||
|
||||
@@ -8,13 +8,14 @@ All constants defined in this module should be either instances of
|
||||
:class:`PytestWarning`, or :class:`UnformattedWarning`
|
||||
in case of warnings which need to format their messages.
|
||||
"""
|
||||
|
||||
from warnings import warn
|
||||
|
||||
from _pytest.warning_types import PytestDeprecationWarning
|
||||
from _pytest.warning_types import PytestRemovedIn8Warning
|
||||
from _pytest.warning_types import PytestRemovedIn9Warning
|
||||
from _pytest.warning_types import UnformattedWarning
|
||||
|
||||
|
||||
# set of plugins which have been integrated into the core; we use this list to ignore
|
||||
# them during registration to avoid conflicts
|
||||
DEPRECATED_EXTERNAL_PLUGINS = {
|
||||
@@ -23,21 +24,6 @@ DEPRECATED_EXTERNAL_PLUGINS = {
|
||||
"pytest_faulthandler",
|
||||
}
|
||||
|
||||
NOSE_SUPPORT = UnformattedWarning(
|
||||
PytestRemovedIn8Warning,
|
||||
"Support for nose tests is deprecated and will be removed in a future release.\n"
|
||||
"{nodeid} is using nose method: `{method}` ({stage})\n"
|
||||
"See docs: https://docs.pytest.org/en/stable/deprecations.html#support-for-tests-written-for-nose",
|
||||
)
|
||||
|
||||
NOSE_SUPPORT_METHOD = UnformattedWarning(
|
||||
PytestRemovedIn8Warning,
|
||||
"Support for nose tests is deprecated and will be removed in a future release.\n"
|
||||
"{nodeid} is using nose-specific method: `{method}(self)`\n"
|
||||
"To remove this warning, rename it to `{method}_method(self)`\n"
|
||||
"See docs: https://docs.pytest.org/en/stable/deprecations.html#support-for-tests-written-for-nose",
|
||||
)
|
||||
|
||||
|
||||
# This can be* removed pytest 8, but it's harmless and common, so no rush to remove.
|
||||
# * If you're in the future: "could have been".
|
||||
@@ -46,74 +32,25 @@ YIELD_FIXTURE = PytestDeprecationWarning(
|
||||
"Use @pytest.fixture instead; they are the same."
|
||||
)
|
||||
|
||||
WARNING_CMDLINE_PREPARSE_HOOK = PytestRemovedIn8Warning(
|
||||
"The pytest_cmdline_preparse hook is deprecated and will be removed in a future release. \n"
|
||||
"Please use pytest_load_initial_conftests hook instead."
|
||||
)
|
||||
|
||||
FSCOLLECTOR_GETHOOKPROXY_ISINITPATH = PytestRemovedIn8Warning(
|
||||
"The gethookproxy() and isinitpath() methods of FSCollector and Package are deprecated; "
|
||||
"use self.session.gethookproxy() and self.session.isinitpath() instead. "
|
||||
)
|
||||
|
||||
STRICT_OPTION = PytestRemovedIn8Warning(
|
||||
"The --strict option is deprecated, use --strict-markers instead."
|
||||
)
|
||||
|
||||
# This deprecation is never really meant to be removed.
|
||||
PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.")
|
||||
|
||||
ARGUMENT_PERCENT_DEFAULT = PytestRemovedIn8Warning(
|
||||
'pytest now uses argparse. "%default" should be changed to "%(default)s"',
|
||||
)
|
||||
|
||||
ARGUMENT_TYPE_STR_CHOICE = UnformattedWarning(
|
||||
PytestRemovedIn8Warning,
|
||||
"`type` argument to addoption() is the string {typ!r}."
|
||||
" For choices this is optional and can be omitted, "
|
||||
" but when supplied should be a type (for example `str` or `int`)."
|
||||
" (options: {names})",
|
||||
)
|
||||
|
||||
ARGUMENT_TYPE_STR = UnformattedWarning(
|
||||
PytestRemovedIn8Warning,
|
||||
"`type` argument to addoption() is the string {typ!r}, "
|
||||
" but when supplied should be a type (for example `str` or `int`)."
|
||||
" (options: {names})",
|
||||
)
|
||||
|
||||
|
||||
HOOK_LEGACY_PATH_ARG = UnformattedWarning(
|
||||
PytestRemovedIn8Warning,
|
||||
PytestRemovedIn9Warning,
|
||||
"The ({pylib_path_arg}: py.path.local) argument is deprecated, please use ({pathlib_path_arg}: pathlib.Path)\n"
|
||||
"see https://docs.pytest.org/en/latest/deprecations.html"
|
||||
"#py-path-local-arguments-for-hooks-replaced-with-pathlib-path",
|
||||
)
|
||||
|
||||
NODE_CTOR_FSPATH_ARG = UnformattedWarning(
|
||||
PytestRemovedIn8Warning,
|
||||
PytestRemovedIn9Warning,
|
||||
"The (fspath: py.path.local) argument to {node_type_name} is deprecated. "
|
||||
"Please use the (path: pathlib.Path) argument instead.\n"
|
||||
"See https://docs.pytest.org/en/latest/deprecations.html"
|
||||
"#fspath-argument-for-node-constructors-replaced-with-pathlib-path",
|
||||
)
|
||||
|
||||
WARNS_NONE_ARG = PytestRemovedIn8Warning(
|
||||
"Passing None has been deprecated.\n"
|
||||
"See https://docs.pytest.org/en/latest/how-to/capture-warnings.html"
|
||||
"#additional-use-cases-of-warnings-in-tests"
|
||||
" for alternatives in common use cases."
|
||||
)
|
||||
|
||||
KEYWORD_MSG_ARG = UnformattedWarning(
|
||||
PytestRemovedIn8Warning,
|
||||
"pytest.{func}(msg=...) is now deprecated, use pytest.{func}(reason=...) instead",
|
||||
)
|
||||
|
||||
INSTANCE_COLLECTOR = PytestRemovedIn8Warning(
|
||||
"The pytest.Instance collector type is deprecated and is no longer used. "
|
||||
"See https://docs.pytest.org/en/latest/deprecations.html#the-pytest-instance-collector",
|
||||
)
|
||||
HOOK_LEGACY_MARKING = UnformattedWarning(
|
||||
PytestDeprecationWarning,
|
||||
"The hook{type} {fullname} uses old-style configuration options (marks or attributes).\n"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user