Compare commits
297 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bf3bcb2bf1 | |||
| ecb23106d8 | |||
| 8174a30164 | |||
| 0142bb6687 | |||
| 52cf700f1b | |||
| 6b3ece5bdb | |||
| 4059000834 | |||
| 7d5207a736 | |||
| 3e65a461c7 | |||
| fc3b5b2610 | |||
| 9335a0b445 | |||
| 0ded3297a9 | |||
| fc9cbbd4c4 | |||
| 1a17539065 | |||
| 32de8e2893 | |||
| 5d4a342a7a | |||
| 1790f17228 | |||
| ee8baa2676 | |||
| ae38b076da | |||
| 85c5bd26b6 | |||
| e1204e1e23 | |||
| 313b61471f | |||
| 1daa8129c6 | |||
| b5ff089d2c | |||
| fda8024622 | |||
| 4b823a42ce | |||
| 6c9b277ce4 | |||
| 3de43e5102 | |||
| c76ae74bd7 | |||
| 24534cdd29 | |||
| 99c78aa93a | |||
| 3a6bdcd76b | |||
| 4a1bba25b9 | |||
| 9e1add75f7 | |||
| 4da9026766 | |||
| 7c231baa64 | |||
| fc538c5766 | |||
| fbfd4b5005 | |||
| ec752537ea | |||
| dd667336ce | |||
| 29d16d2939 | |||
| 5313d50e18 | |||
| 4f3f36c396 | |||
| af124c7f21 | |||
| 4e6d53fef5 | |||
| 751d726d21 | |||
| 9e491f430e | |||
| 63f258f432 | |||
| baaa67dfb9 | |||
| 519f351b4f | |||
| 5d53447a73 | |||
| 1716d3c9bf | |||
| ac699e7b25 | |||
| a5f37199a9 | |||
| 9fa82598a9 | |||
| ba32a3bd87 | |||
| c8641f879f | |||
| 6041511fb4 | |||
| 739408b958 | |||
| 1636322995 | |||
| b8edacb8f1 | |||
| 612489e2bd | |||
| 383774db10 | |||
| 3b5b3cf50e | |||
| 23e343af60 | |||
| f9a995b56d | |||
| d9d78a8aef | |||
| 76d15231f5 | |||
| 4cc05e7bee | |||
| 2d57d5c32f | |||
| faeb16146b | |||
| b241c0b479 | |||
| 78403237cf | |||
| f84fea0888 | |||
| 271bdf6c23 | |||
| fd56968f2b | |||
| 5b75b0d03f | |||
| aac5d5d08b | |||
| b1460f3261 | |||
| a88ae8289c | |||
| 62320e4ff7 | |||
| 6514041a35 | |||
| 7d548c38e2 | |||
| 07eeeb8dfc | |||
| be774667c2 | |||
| 762bb61562 | |||
| 725de3a0d3 | |||
| fcada1ea47 | |||
| 6f7f89f3c4 | |||
| 0a20452f78 | |||
| cc23ec91d0 | |||
| a15f544962 | |||
| 3823ce60dd | |||
| e03f82c359 | |||
| 158f41fdf8 | |||
| fd6a4507ac | |||
| 15156757b6 | |||
| 0860f4e916 | |||
| 11965d1c27 | |||
| 14be71b234 | |||
| 2d6206b89a | |||
| 41f57ef95d | |||
| 4eca6063c8 | |||
| 819f5abd73 | |||
| adf891ab1d | |||
| f08184ba20 | |||
| 28783e5d23 | |||
| 7834b3b07f | |||
| ece756fcb4 | |||
| d380771065 | |||
| b893d2a0fe | |||
| e3b1799766 | |||
| 5d1385320f | |||
| eff54aece1 | |||
| bc1fc3f0fc | |||
| 784ffa0fad | |||
| 90412827c3 | |||
| 424c3eebde | |||
| 9c2247ec1b | |||
| 61f7c27ec0 | |||
| 1b81d636e2 | |||
| 1b196fbeaf | |||
| 22524046cf | |||
| 6dcd652d4a | |||
| be9faa68d8 | |||
| e4e13dd913 | |||
| 5a399030c1 | |||
| 19c8ef63a4 | |||
| 22951bba67 | |||
| ec8e23951d | |||
| bf47357511 | |||
| 2d2dc4a2a8 | |||
| 3683722bcb | |||
| 31d0b51039 | |||
| 2d2f69dab5 | |||
| 2a39ed3461 | |||
| f1c7585184 | |||
| a3b39069bc | |||
| 172c832cbd | |||
| 839b90db45 | |||
| 549cc512f7 | |||
| 2369bed1db | |||
| 54864f0c9b | |||
| ba969d2ae7 | |||
| 407b330fe1 | |||
| 431ec6d34e | |||
| eada68b2b3 | |||
| ab069247cd | |||
| 7af1e4e4ed | |||
| 0ae04ae629 | |||
| 723035be7f | |||
| 6e478b0947 | |||
| a869141b3d | |||
| 5e98aefc92 | |||
| 5f47e423b2 | |||
| 5a61ec3d4a | |||
| b3b44ea814 | |||
| 1d48b3021d | |||
| d5dda84ef3 | |||
| 517e02e59e | |||
| 4e259590c9 | |||
| 97a2761d72 | |||
| 88c9e92258 | |||
| 72ad32411f | |||
| cb9e8be301 | |||
| d72da480c4 | |||
| 07e7deb4a7 | |||
| 572b5657d7 | |||
| 44afed9b13 | |||
| 13ea4780b8 | |||
| 135600fca3 | |||
| c237297b3d | |||
| 9ccae9a8e3 | |||
| 77152d26e7 | |||
| da626e7186 | |||
| 051f8f1f0f | |||
| 31ad577325 | |||
| 835cac8d8b | |||
| 464f29901f | |||
| aa72496d24 | |||
| e9f3a01392 | |||
| 00c94ab01b | |||
| 9048621002 | |||
| 27165cf8db | |||
| 7a829cb57d | |||
| 5e1c3d2477 | |||
| 59e7d2bbc9 | |||
| af99040123 | |||
| a2b7db7655 | |||
| 9c93c96b14 | |||
| 4a46ee8bc9 | |||
| 5dbfb8e108 | |||
| 86a1beba07 | |||
| ca40380e99 | |||
| 05eee78aaa | |||
| 02893139f9 | |||
| 8c53dbf9d7 | |||
| 54b8b40f83 | |||
| 54911acf8d | |||
| c746d2b016 | |||
| a3693ce503 | |||
| af4143729f | |||
| bd7919e03d | |||
| 7d4b40337b | |||
| 6a714d7b70 | |||
| 5a23eeff7a | |||
| 310b67b227 | |||
| 4d4ed42c34 | |||
| 096b942ec4 | |||
| 61cfaacec6 | |||
| 95c62eb527 | |||
| 03b19945fb | |||
| b2ac31cc9f | |||
| 1a96f16401 | |||
| 7421f3bb94 | |||
| 5e0583f4b9 | |||
| 8efb4bb9c1 | |||
| 3ad4344656 | |||
| 6bf7f55555 | |||
| 61f70a5a75 | |||
| 326ae0cd88 | |||
| 10220d3f31 | |||
| a98b00cd09 | |||
| 215ea7fd03 | |||
| b31db4809b | |||
| f6adebb990 | |||
| b90e7b84d0 | |||
| 19807ab79a | |||
| 3e52124185 | |||
| 1eca228bd5 | |||
| cab02e67d7 | |||
| 64dbc7a0a1 | |||
| 60d992677d | |||
| 0079decf29 | |||
| 3a58fc2d44 | |||
| 39b6bb551c | |||
| 9fbd67dd4b | |||
| eca93db05b | |||
| fb701b538c | |||
| 314e623304 | |||
| 62e75c7d55 | |||
| fd30759d94 | |||
| eb984a717a | |||
| 54f0fb3c63 | |||
| 49a4ed14cf | |||
| f513d33d5a | |||
| 857e34ef85 | |||
| 99dfc19fe6 | |||
| 56544c11b5 | |||
| 7710e18b4c | |||
| 791b51d0fa | |||
| bc4e70e048 | |||
| b817aa457c | |||
| 66b28912ac | |||
| cca029d55e | |||
| d5466b3917 | |||
| 4fce29f15d | |||
| 69e3973d86 | |||
| c842893b02 | |||
| 506b10d295 | |||
| 05061493cb | |||
| f97f3dc3a3 | |||
| 3c31b0132f | |||
| 593178d909 | |||
| 54d5a63d14 | |||
| b55e264a67 | |||
| 13d6114c0a | |||
| b635e16d30 | |||
| a092b3ab36 | |||
| a006dabf6e | |||
| aa7e9de91d | |||
| 6aec32163d | |||
| 2f33ea87c8 | |||
| 1ada62e237 | |||
| 50b232b0cb | |||
| 496196b15c | |||
| 0314b50c52 | |||
| 8e2de91bf8 | |||
| 692ab1160b | |||
| 549839bac5 | |||
| 646a46e5f4 | |||
| f07017f91b | |||
| a17d3b0c44 | |||
| bbec1ce67f | |||
| 5a040aef97 | |||
| c1d2168df6 | |||
| bbe7cbae4a | |||
| deae8f47f6 | |||
| 10f55f79af | |||
| a6d244343f | |||
| 2b552c2240 | |||
| 54d7b9a08e | |||
| 6afc02abca | |||
| e75e2d66a0 | |||
| 66db0b7522 | |||
| 3a68c08426 | |||
| 9e1804a6ee |
@@ -9,3 +9,9 @@ updates:
|
|||||||
allow:
|
allow:
|
||||||
- dependency-type: direct
|
- dependency-type: direct
|
||||||
- dependency-type: indirect
|
- dependency-type: indirect
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
time: "03:00"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: true
|
persist-credentials: true
|
||||||
|
|||||||
@@ -23,30 +23,34 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Build and Check Package
|
||||||
|
uses: hynek/build-and-inspect-python-package@v1.5
|
||||||
|
|
||||||
|
- name: Download Package
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: Packages
|
||||||
|
path: dist
|
||||||
|
|
||||||
|
- name: Publish package to PyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
|
with:
|
||||||
|
password: ${{ secrets.pypi_token }}
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.7"
|
python-version: "3.7"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install tox
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install --upgrade build tox
|
pip install --upgrade tox
|
||||||
|
|
||||||
- name: Build package
|
|
||||||
run: |
|
|
||||||
python -m build
|
|
||||||
|
|
||||||
- name: Publish package to PyPI
|
|
||||||
uses: pypa/gh-action-pypi-publish@master
|
|
||||||
with:
|
|
||||||
user: __token__
|
|
||||||
password: ${{ secrets.pypi_token }}
|
|
||||||
|
|
||||||
- name: Publish GitHub release notes
|
- name: Publish GitHub release notes
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.8"
|
python-version: "3.8"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
name: close needs-information issues
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "30 1 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
close-issues:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v8
|
||||||
|
with:
|
||||||
|
debug-only: false
|
||||||
|
days-before-issue-stale: 14
|
||||||
|
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."
|
||||||
|
days-before-pr-stale: -1
|
||||||
|
days-before-pr-close: -1
|
||||||
@@ -18,6 +18,11 @@ on:
|
|||||||
env:
|
env:
|
||||||
PYTEST_ADDOPTS: "--color=yes"
|
PYTEST_ADDOPTS: "--color=yes"
|
||||||
|
|
||||||
|
# Cancel running jobs for the same workflow and branch.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
# Set permissions at the job level.
|
# Set permissions at the job level.
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
@@ -38,6 +43,7 @@ jobs:
|
|||||||
"windows-py39",
|
"windows-py39",
|
||||||
"windows-py310",
|
"windows-py310",
|
||||||
"windows-py311",
|
"windows-py311",
|
||||||
|
"windows-py312",
|
||||||
|
|
||||||
"ubuntu-py37",
|
"ubuntu-py37",
|
||||||
"ubuntu-py37-pluggy",
|
"ubuntu-py37-pluggy",
|
||||||
@@ -46,12 +52,13 @@ jobs:
|
|||||||
"ubuntu-py39",
|
"ubuntu-py39",
|
||||||
"ubuntu-py310",
|
"ubuntu-py310",
|
||||||
"ubuntu-py311",
|
"ubuntu-py311",
|
||||||
|
"ubuntu-py312",
|
||||||
"ubuntu-pypy3",
|
"ubuntu-pypy3",
|
||||||
|
|
||||||
"macos-py37",
|
"macos-py37",
|
||||||
"macos-py38",
|
|
||||||
"macos-py39",
|
"macos-py39",
|
||||||
"macos-py310",
|
"macos-py310",
|
||||||
|
"macos-py312",
|
||||||
|
|
||||||
"docs",
|
"docs",
|
||||||
"doctesting",
|
"doctesting",
|
||||||
@@ -81,9 +88,13 @@ jobs:
|
|||||||
os: windows-latest
|
os: windows-latest
|
||||||
tox_env: "py310-xdist"
|
tox_env: "py310-xdist"
|
||||||
- name: "windows-py311"
|
- name: "windows-py311"
|
||||||
python: "3.11-dev"
|
python: "3.11"
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
tox_env: "py311"
|
tox_env: "py311"
|
||||||
|
- name: "windows-py312"
|
||||||
|
python: "3.12-dev"
|
||||||
|
os: windows-latest
|
||||||
|
tox_env: "py312"
|
||||||
|
|
||||||
- name: "ubuntu-py37"
|
- name: "ubuntu-py37"
|
||||||
python: "3.7"
|
python: "3.7"
|
||||||
@@ -111,10 +122,15 @@ jobs:
|
|||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
tox_env: "py310-xdist"
|
tox_env: "py310-xdist"
|
||||||
- name: "ubuntu-py311"
|
- name: "ubuntu-py311"
|
||||||
python: "3.11-dev"
|
python: "3.11"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
tox_env: "py311"
|
tox_env: "py311"
|
||||||
use_coverage: true
|
use_coverage: true
|
||||||
|
- name: "ubuntu-py312"
|
||||||
|
python: "3.12-dev"
|
||||||
|
os: ubuntu-latest
|
||||||
|
tox_env: "py312"
|
||||||
|
use_coverage: true
|
||||||
- name: "ubuntu-pypy3"
|
- name: "ubuntu-pypy3"
|
||||||
python: "pypy-3.7"
|
python: "pypy-3.7"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
@@ -124,19 +140,19 @@ jobs:
|
|||||||
python: "3.7"
|
python: "3.7"
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
tox_env: "py37-xdist"
|
tox_env: "py37-xdist"
|
||||||
- name: "macos-py38"
|
|
||||||
python: "3.8"
|
|
||||||
os: macos-latest
|
|
||||||
tox_env: "py38-xdist"
|
|
||||||
use_coverage: true
|
|
||||||
- name: "macos-py39"
|
- name: "macos-py39"
|
||||||
python: "3.9"
|
python: "3.9"
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
tox_env: "py39-xdist"
|
tox_env: "py39-xdist"
|
||||||
|
use_coverage: true
|
||||||
- name: "macos-py310"
|
- name: "macos-py310"
|
||||||
python: "3.10"
|
python: "3.10"
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
tox_env: "py310-xdist"
|
tox_env: "py310-xdist"
|
||||||
|
- name: "macos-py312"
|
||||||
|
python: "3.12-dev"
|
||||||
|
os: macos-latest
|
||||||
|
tox_env: "py312-xdist"
|
||||||
|
|
||||||
- name: "plugins"
|
- name: "plugins"
|
||||||
python: "3.9"
|
python: "3.9"
|
||||||
@@ -163,6 +179,7 @@ jobs:
|
|||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python }}
|
python-version: ${{ matrix.python }}
|
||||||
|
check-latest: ${{ endsWith(matrix.python, '-dev') }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -189,3 +206,10 @@ jobs:
|
|||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
files: ./coverage.xml
|
files: ./coverage.xml
|
||||||
verbose: true
|
verbose: true
|
||||||
|
|
||||||
|
check-package:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Build and Check Package
|
||||||
|
uses: hynek/build-and-inspect-python-package@v1.5
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ on:
|
|||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
createPullRequest:
|
update-plugin-list:
|
||||||
if: github.repository_owner == 'pytest-dev'
|
if: github.repository_owner == 'pytest-dev'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
@@ -20,12 +20,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: 3.8
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
run: python scripts/update-plugin-list.py
|
run: python scripts/update-plugin-list.py
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@2455e1596942c2902952003bbb574afbbe2ab2e6
|
uses: peter-evans/create-pull-request@5b4a9f6a9e2af26e5f02351490b90d01eb8ec1e5
|
||||||
with:
|
with:
|
||||||
commit-message: '[automated] Update plugin list'
|
commit-message: '[automated] Update plugin list'
|
||||||
author: 'pytest bot <pytestbot@users.noreply.github.com>'
|
author: 'pytest bot <pytestbot@users.noreply.github.com>'
|
||||||
|
|||||||
+13
-15
@@ -1,18 +1,16 @@
|
|||||||
default_language_version:
|
|
||||||
python: "3.10"
|
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 22.10.0
|
rev: 23.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
args: [--safe, --quiet]
|
args: [--safe, --quiet]
|
||||||
- repo: https://github.com/asottile/blacken-docs
|
- repo: https://github.com/asottile/blacken-docs
|
||||||
rev: v1.12.1
|
rev: 1.13.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: blacken-docs
|
- id: blacken-docs
|
||||||
additional_dependencies: [black==20.8b1]
|
additional_dependencies: [black==23.1.0]
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.3.0
|
rev: v4.4.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
@@ -23,7 +21,7 @@ repos:
|
|||||||
exclude: _pytest/(debugging|hookspec).py
|
exclude: _pytest/(debugging|hookspec).py
|
||||||
language_version: python3
|
language_version: python3
|
||||||
- repo: https://github.com/PyCQA/autoflake
|
- repo: https://github.com/PyCQA/autoflake
|
||||||
rev: v1.7.6
|
rev: v2.1.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: autoflake
|
- id: autoflake
|
||||||
name: autoflake
|
name: autoflake
|
||||||
@@ -31,34 +29,34 @@ repos:
|
|||||||
language: python
|
language: python
|
||||||
files: \.py$
|
files: \.py$
|
||||||
- repo: https://github.com/PyCQA/flake8
|
- repo: https://github.com/PyCQA/flake8
|
||||||
rev: 5.0.4
|
rev: 6.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
language_version: python3
|
language_version: python3
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- flake8-typing-imports==1.12.0
|
- flake8-typing-imports==1.12.0
|
||||||
- flake8-docstrings==1.5.0
|
- flake8-docstrings==1.5.0
|
||||||
- repo: https://github.com/asottile/reorder_python_imports
|
- repo: https://github.com/asottile/reorder-python-imports
|
||||||
rev: v3.8.5
|
rev: v3.9.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: reorder-python-imports
|
- id: reorder-python-imports
|
||||||
args: ['--application-directories=.:src', --py37-plus]
|
args: ['--application-directories=.:src', --py37-plus]
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v3.1.0
|
rev: v3.4.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args: [--py37-plus]
|
args: [--py37-plus]
|
||||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||||
rev: v2.1.0
|
rev: v2.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: setup-cfg-fmt
|
- id: setup-cfg-fmt
|
||||||
args: ["--max-py-version=3.10", "--include-version-classifiers"]
|
args: ["--max-py-version=3.12", "--include-version-classifiers"]
|
||||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||||
rev: v1.9.0
|
rev: v1.10.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: python-use-type-annotations
|
- id: python-use-type-annotations
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: v0.982
|
rev: v1.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
files: ^(src/|testing/)
|
files: ^(src/|testing/)
|
||||||
|
|||||||
+6
-3
@@ -2,9 +2,12 @@ version: 2
|
|||||||
|
|
||||||
python:
|
python:
|
||||||
install:
|
install:
|
||||||
- requirements: doc/en/requirements.txt
|
# Install pytest first, then doc/en/requirements.txt.
|
||||||
- method: pip
|
# This order is important to honor any pins in doc/en/requirements.txt
|
||||||
path: .
|
# when the pinned library is also a dependency of pytest.
|
||||||
|
- method: pip
|
||||||
|
path: .
|
||||||
|
- requirements: doc/en/requirements.txt
|
||||||
|
|
||||||
build:
|
build:
|
||||||
os: ubuntu-20.04
|
os: ubuntu-20.04
|
||||||
|
|||||||
@@ -8,10 +8,14 @@ Abdeali JK
|
|||||||
Abdelrahman Elbehery
|
Abdelrahman Elbehery
|
||||||
Abhijeet Kasurde
|
Abhijeet Kasurde
|
||||||
Adam Johnson
|
Adam Johnson
|
||||||
|
Adam Stewart
|
||||||
Adam Uhlir
|
Adam Uhlir
|
||||||
Ahn Ki-Wook
|
Ahn Ki-Wook
|
||||||
Akiomi Kamakura
|
Akiomi Kamakura
|
||||||
Alan Velasco
|
Alan Velasco
|
||||||
|
Alessio Izzo
|
||||||
|
Alex Jones
|
||||||
|
Alex Lambson
|
||||||
Alexander Johnson
|
Alexander Johnson
|
||||||
Alexander King
|
Alexander King
|
||||||
Alexei Kozlenok
|
Alexei Kozlenok
|
||||||
@@ -43,6 +47,7 @@ Ariel Pillemer
|
|||||||
Armin Rigo
|
Armin Rigo
|
||||||
Aron Coyle
|
Aron Coyle
|
||||||
Aron Curzon
|
Aron Curzon
|
||||||
|
Ashish Kurmi
|
||||||
Aviral Verma
|
Aviral Verma
|
||||||
Aviv Palivoda
|
Aviv Palivoda
|
||||||
Babak Keyvani
|
Babak Keyvani
|
||||||
@@ -53,10 +58,12 @@ Benjamin Peterson
|
|||||||
Bernard Pratz
|
Bernard Pratz
|
||||||
Bob Ippolito
|
Bob Ippolito
|
||||||
Brian Dorsey
|
Brian Dorsey
|
||||||
|
Brian Larsen
|
||||||
Brian Maissy
|
Brian Maissy
|
||||||
Brian Okken
|
Brian Okken
|
||||||
Brianna Laugher
|
Brianna Laugher
|
||||||
Bruno Oliveira
|
Bruno Oliveira
|
||||||
|
Cal Jacobson
|
||||||
Cal Leeming
|
Cal Leeming
|
||||||
Carl Friedrich Bolz
|
Carl Friedrich Bolz
|
||||||
Carlos Jenkins
|
Carlos Jenkins
|
||||||
@@ -65,6 +72,7 @@ Charles Cloud
|
|||||||
Charles Machalow
|
Charles Machalow
|
||||||
Charnjit SiNGH (CCSJ)
|
Charnjit SiNGH (CCSJ)
|
||||||
Cheuk Ting Ho
|
Cheuk Ting Ho
|
||||||
|
Chris Mahoney
|
||||||
Chris Lamb
|
Chris Lamb
|
||||||
Chris NeJame
|
Chris NeJame
|
||||||
Chris Rose
|
Chris Rose
|
||||||
@@ -88,6 +96,7 @@ Daniel Grana
|
|||||||
Daniel Hahler
|
Daniel Hahler
|
||||||
Daniel Nuri
|
Daniel Nuri
|
||||||
Daniel Sánchez Castelló
|
Daniel Sánchez Castelló
|
||||||
|
Daniel Valenzuela Zenteno
|
||||||
Daniel Wandschneider
|
Daniel Wandschneider
|
||||||
Daniele Procida
|
Daniele Procida
|
||||||
Danielle Jenkins
|
Danielle Jenkins
|
||||||
@@ -122,8 +131,10 @@ Eric Siegerman
|
|||||||
Erik Aronesty
|
Erik Aronesty
|
||||||
Erik M. Bray
|
Erik M. Bray
|
||||||
Evan Kepner
|
Evan Kepner
|
||||||
|
Evgeny Seliverstov
|
||||||
Fabien Zarifian
|
Fabien Zarifian
|
||||||
Fabio Zadrozny
|
Fabio Zadrozny
|
||||||
|
Felix Hofstätter
|
||||||
Felix Nieuwenhuizen
|
Felix Nieuwenhuizen
|
||||||
Feng Ma
|
Feng Ma
|
||||||
Florian Bruhin
|
Florian Bruhin
|
||||||
@@ -158,6 +169,7 @@ Ionuț Turturică
|
|||||||
Itxaso Aizpurua
|
Itxaso Aizpurua
|
||||||
Iwan Briquemont
|
Iwan Briquemont
|
||||||
Jaap Broekhuizen
|
Jaap Broekhuizen
|
||||||
|
Jake VanderPlas
|
||||||
Jakob van Santen
|
Jakob van Santen
|
||||||
Jakub Mitoraj
|
Jakub Mitoraj
|
||||||
James Bourbeau
|
James Bourbeau
|
||||||
@@ -182,10 +194,11 @@ Joseph Hunkeler
|
|||||||
Josh Karpel
|
Josh Karpel
|
||||||
Joshua Bronson
|
Joshua Bronson
|
||||||
Jurko Gospodnetić
|
Jurko Gospodnetić
|
||||||
Justyna Janczyszyn
|
|
||||||
Justice Ndou
|
Justice Ndou
|
||||||
|
Justyna Janczyszyn
|
||||||
Kale Kundert
|
Kale Kundert
|
||||||
Kamran Ahmad
|
Kamran Ahmad
|
||||||
|
Kenny Y
|
||||||
Karl O. Pinc
|
Karl O. Pinc
|
||||||
Karthikeyan Singaravelan
|
Karthikeyan Singaravelan
|
||||||
Katarzyna Jachim
|
Katarzyna Jachim
|
||||||
@@ -222,6 +235,7 @@ Marcin Bachry
|
|||||||
Marco Gorelli
|
Marco Gorelli
|
||||||
Mark Abramowitz
|
Mark Abramowitz
|
||||||
Mark Dickinson
|
Mark Dickinson
|
||||||
|
Marko Pacak
|
||||||
Markus Unterwaditzer
|
Markus Unterwaditzer
|
||||||
Martijn Faassen
|
Martijn Faassen
|
||||||
Martin Altmayer
|
Martin Altmayer
|
||||||
@@ -235,7 +249,6 @@ Matthias Hafner
|
|||||||
Maxim Filipenko
|
Maxim Filipenko
|
||||||
Maximilian Cosmo Sitter
|
Maximilian Cosmo Sitter
|
||||||
mbyt
|
mbyt
|
||||||
Mickey Pashov
|
|
||||||
Michael Aquilina
|
Michael Aquilina
|
||||||
Michael Birtwell
|
Michael Birtwell
|
||||||
Michael Droettboom
|
Michael Droettboom
|
||||||
@@ -244,6 +257,7 @@ Michael Krebs
|
|||||||
Michael Seifert
|
Michael Seifert
|
||||||
Michal Wajszczuk
|
Michal Wajszczuk
|
||||||
Michał Zięba
|
Michał Zięba
|
||||||
|
Mickey Pashov
|
||||||
Mihai Capotă
|
Mihai Capotă
|
||||||
Mike Hoyle (hoylemd)
|
Mike Hoyle (hoylemd)
|
||||||
Mike Lundy
|
Mike Lundy
|
||||||
@@ -258,9 +272,9 @@ Niclas Olofsson
|
|||||||
Nicolas Delaby
|
Nicolas Delaby
|
||||||
Nikolay Kondratyev
|
Nikolay Kondratyev
|
||||||
Nipunn Koorapati
|
Nipunn Koorapati
|
||||||
Olga Matoula
|
|
||||||
Oleg Pidsadnyi
|
Oleg Pidsadnyi
|
||||||
Oleg Sushchenko
|
Oleg Sushchenko
|
||||||
|
Olga Matoula
|
||||||
Oliver Bestwalter
|
Oliver Bestwalter
|
||||||
Omar Kohl
|
Omar Kohl
|
||||||
Omer Hadari
|
Omer Hadari
|
||||||
@@ -276,6 +290,7 @@ Paweł Adamczak
|
|||||||
Pedro Algarvio
|
Pedro Algarvio
|
||||||
Petter Strandmark
|
Petter Strandmark
|
||||||
Philipp Loose
|
Philipp Loose
|
||||||
|
Pierre Sassoulas
|
||||||
Pieter Mulder
|
Pieter Mulder
|
||||||
Piotr Banaszkiewicz
|
Piotr Banaszkiewicz
|
||||||
Piotr Helm
|
Piotr Helm
|
||||||
@@ -285,15 +300,18 @@ Prashant Sharma
|
|||||||
Pulkit Goyal
|
Pulkit Goyal
|
||||||
Punyashloka Biswal
|
Punyashloka Biswal
|
||||||
Quentin Pradet
|
Quentin Pradet
|
||||||
|
q0w
|
||||||
Ralf Schmitt
|
Ralf Schmitt
|
||||||
Ram Rachum
|
|
||||||
Ralph Giles
|
Ralph Giles
|
||||||
|
Ram Rachum
|
||||||
Ran Benita
|
Ran Benita
|
||||||
Raphael Castaneda
|
Raphael Castaneda
|
||||||
Raphael Pierzina
|
Raphael Pierzina
|
||||||
|
Rafal Semik
|
||||||
Raquel Alegre
|
Raquel Alegre
|
||||||
Ravi Chandra
|
Ravi Chandra
|
||||||
Robert Holt
|
Robert Holt
|
||||||
|
Roberto Aldera
|
||||||
Roberto Polli
|
Roberto Polli
|
||||||
Roland Puntaier
|
Roland Puntaier
|
||||||
Romain Dorgueil
|
Romain Dorgueil
|
||||||
@@ -310,6 +328,7 @@ Samuel Searles-Bryant
|
|||||||
Samuele Pedroni
|
Samuele Pedroni
|
||||||
Sanket Duthade
|
Sanket Duthade
|
||||||
Sankt Petersbug
|
Sankt Petersbug
|
||||||
|
Saravanan Padmanaban
|
||||||
Segev Finer
|
Segev Finer
|
||||||
Serhii Mozghovyi
|
Serhii Mozghovyi
|
||||||
Seth Junot
|
Seth Junot
|
||||||
@@ -323,6 +342,7 @@ Srinivas Reddy Thatiparthy
|
|||||||
Stefan Farmbauer
|
Stefan Farmbauer
|
||||||
Stefan Scherfke
|
Stefan Scherfke
|
||||||
Stefan Zimmermann
|
Stefan Zimmermann
|
||||||
|
Stefanie Molin
|
||||||
Stefano Taschini
|
Stefano Taschini
|
||||||
Steffen Allner
|
Steffen Allner
|
||||||
Stephan Obermann
|
Stephan Obermann
|
||||||
@@ -341,6 +361,7 @@ Thomas Grainger
|
|||||||
Thomas Hisch
|
Thomas Hisch
|
||||||
Tim Hoffmann
|
Tim Hoffmann
|
||||||
Tim Strazny
|
Tim Strazny
|
||||||
|
TJ Bruno
|
||||||
Tobias Diez
|
Tobias Diez
|
||||||
Tom Dalton
|
Tom Dalton
|
||||||
Tom Viner
|
Tom Viner
|
||||||
@@ -371,7 +392,10 @@ Wouter van Ackooy
|
|||||||
Xixi Zhao
|
Xixi Zhao
|
||||||
Xuan Luong
|
Xuan Luong
|
||||||
Xuecong Liao
|
Xuecong Liao
|
||||||
|
Yannick Péroux
|
||||||
Yoav Caspi
|
Yoav Caspi
|
||||||
|
Yuliang Shao
|
||||||
|
Yusuke Kadowaki
|
||||||
Yuval Shimon
|
Yuval Shimon
|
||||||
Zac Hatfield-Dodds
|
Zac Hatfield-Dodds
|
||||||
Zachary Kneupper
|
Zachary Kneupper
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Terminal Reporting: Fixed bug when running in ``--tb=line`` mode where ``pytest.fail(pytrace=False)`` tests report ``None``.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Update test log report annotation to named tuple and fixed inconsistency in docs for :hook:`pytest_report_teststatus` hook.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Added :func:`ExceptionInfo.from_exception() <pytest.ExceptionInfo.from_exception>`, a simpler way to create an :class:`~pytest.ExceptionInfo` from an exception.
|
||||||
|
This can replace :func:`ExceptionInfo.from_exc_info() <pytest.ExceptionInfo.from_exc_info()>` for most uses.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
When an exception traceback to be displayed is completely filtered out (by mechanisms such as ``__tracebackhide__``, internal frames, and similar), now only the exception string and the following message are shown:
|
||||||
|
|
||||||
|
"All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames.".
|
||||||
|
|
||||||
|
Previously, the last frame of the traceback was shown, even though it was hidden.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Improved verbose output (``-vv``) of ``skip`` and ``xfail`` reasons by performing text wrapping while leaving a clear margin for progress output.
|
||||||
|
|
||||||
|
Added :func:`TerminalReporter.wrap_write() <pytest.TerminalReporter.wrap_write>` as a helper for that.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Added handling of ``%f`` directive to print microseconds in log format options, such as ``log-date-format``.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Added underlying exception to cache provider path creation and write warning messages.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Added warning when :confval:`testpaths` is set, but paths are not found by glob. In this case, pytest will fall back to searching from the current directory.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Enhanced the CLI flag for ``-c`` to now include ``--config-file`` to make it clear that this flag applies to the usage of a custom config file.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
When `--confcutdir` is not specified, and there is no config file present, the conftest cutoff directory (`--confcutdir`) is now set to the :ref:`rootdir`.
|
||||||
|
Previously in such cases, `conftest.py` files would be probed all the way to the root directory of the filesystem.
|
||||||
|
If you are badly affected by this change, consider adding an empty config file to your desired cutoff directory, or explicitly set `--confcutdir`.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Fixed the ``--last-failed`` whole-file skipping functionality ("skipped N files") for :ref:`non-python test files <non-python tests>`.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
The :confval:`norecursedir` check is now performed in a :hook:`pytest_ignore_collect` implementation, so plugins can affect it.
|
||||||
|
|
||||||
|
If after updating to this version you see that your `norecursedir` setting is not being respected,
|
||||||
|
it means that a conftest or a plugin you use has a bad `pytest_ignore_collect` implementation.
|
||||||
|
Most likely, your hook returns `False` for paths it does not want to ignore,
|
||||||
|
which ends the processing and doesn't allow other plugins, including pytest itself, to ignore the path.
|
||||||
|
The fix is to return `None` instead of `False` for paths your hook doesn't want to ignore.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Fixed traceback entries hidden with ``__tracebackhide__ = True`` still being shown for chained exceptions (parts after "... the above exception ..." message).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
:func:`_pytest.logging.LogCaptureFixture.set_level` and :func:`_pytest.logging.LogCaptureFixture.at_level`
|
||||||
|
will temporarily enable the requested ``level`` if ``level`` was disabled globally via
|
||||||
|
``logging.disable(LEVEL)``.
|
||||||
@@ -6,6 +6,11 @@ Release announcements
|
|||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
|
||||||
|
release-7.3.2
|
||||||
|
release-7.3.1
|
||||||
|
release-7.3.0
|
||||||
|
release-7.2.2
|
||||||
|
release-7.2.1
|
||||||
release-7.2.0
|
release-7.2.0
|
||||||
release-7.1.3
|
release-7.1.3
|
||||||
release-7.1.2
|
release-7.1.2
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
pytest-7.2.1
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 7.2.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:
|
||||||
|
|
||||||
|
* Anthony Sottile
|
||||||
|
* Bruno Oliveira
|
||||||
|
* Daniel Valenzuela
|
||||||
|
* Kadino
|
||||||
|
* Prerak Patel
|
||||||
|
* Ronny Pfannschmidt
|
||||||
|
* Santiago Castro
|
||||||
|
* s-padmanaban
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
pytest-7.2.2
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 7.2.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
|
||||||
|
* Garvit Shubham
|
||||||
|
* Mahesh Vashishtha
|
||||||
|
* Ramsey
|
||||||
|
* Ronny Pfannschmidt
|
||||||
|
* Teejay
|
||||||
|
* q0w
|
||||||
|
* vin01
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
pytest-7.3.0
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
The pytest team is proud to announce the 7.3.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:
|
||||||
|
|
||||||
|
* Aaron Berdy
|
||||||
|
* Adam Turner
|
||||||
|
* Albert Villanova del Moral
|
||||||
|
* Alessio Izzo
|
||||||
|
* Alex Hadley
|
||||||
|
* Alice Purcell
|
||||||
|
* Anthony Sottile
|
||||||
|
* Anton Yakutovich
|
||||||
|
* Ashish Kurmi
|
||||||
|
* Babak Keyvani
|
||||||
|
* Billy
|
||||||
|
* Brandon Chinn
|
||||||
|
* Bruno Oliveira
|
||||||
|
* Cal Jacobson
|
||||||
|
* Chanvin Xiao
|
||||||
|
* Cheuk Ting Ho
|
||||||
|
* Chris Wheeler
|
||||||
|
* Daniel Garcia Moreno
|
||||||
|
* Daniel Scheffler
|
||||||
|
* Daniel Valenzuela
|
||||||
|
* EmptyRabbit
|
||||||
|
* Ezio Melotti
|
||||||
|
* Felix Hofstätter
|
||||||
|
* Florian Best
|
||||||
|
* Florian Bruhin
|
||||||
|
* Fredrik Berndtsson
|
||||||
|
* Gabriel Landau
|
||||||
|
* Garvit Shubham
|
||||||
|
* Gergely Kalmár
|
||||||
|
* HTRafal
|
||||||
|
* Hugo van Kemenade
|
||||||
|
* Ilya Konstantinov
|
||||||
|
* Itxaso Aizpurua
|
||||||
|
* James Gerity
|
||||||
|
* Jay
|
||||||
|
* John Litborn
|
||||||
|
* Jon Parise
|
||||||
|
* Jouke Witteveen
|
||||||
|
* Kadino
|
||||||
|
* Kevin C
|
||||||
|
* Kian Eliasi
|
||||||
|
* Klaus Rettinghaus
|
||||||
|
* Kodi Arfer
|
||||||
|
* Mahesh Vashishtha
|
||||||
|
* Manuel Jacob
|
||||||
|
* Marko Pacak
|
||||||
|
* MatthewFlamm
|
||||||
|
* Miro Hrončok
|
||||||
|
* Nate Meyvis
|
||||||
|
* Neil Girdhar
|
||||||
|
* Nhieuvu1802
|
||||||
|
* Nipunn Koorapati
|
||||||
|
* Ofek Lev
|
||||||
|
* Paul Kehrer
|
||||||
|
* Paul Müller
|
||||||
|
* Paul Reece
|
||||||
|
* Pax
|
||||||
|
* Pete Baughman
|
||||||
|
* Peyman Salehi
|
||||||
|
* Philipp A
|
||||||
|
* Pierre Sassoulas
|
||||||
|
* Prerak Patel
|
||||||
|
* Ramsey
|
||||||
|
* Ran Benita
|
||||||
|
* Robert O'Shea
|
||||||
|
* Ronny Pfannschmidt
|
||||||
|
* Rowin
|
||||||
|
* Ruth Comer
|
||||||
|
* Samuel Colvin
|
||||||
|
* Samuel Gaist
|
||||||
|
* Sandro Tosi
|
||||||
|
* Santiago Castro
|
||||||
|
* Shantanu
|
||||||
|
* Simon K
|
||||||
|
* Stefanie Molin
|
||||||
|
* Stephen Rosen
|
||||||
|
* Sviatoslav Sydorenko
|
||||||
|
* Tatiana Ovary
|
||||||
|
* Teejay
|
||||||
|
* Thierry Moisan
|
||||||
|
* Thomas Grainger
|
||||||
|
* Tim Hoffmann
|
||||||
|
* Tobias Diez
|
||||||
|
* Tony Narlock
|
||||||
|
* Vivaan Verma
|
||||||
|
* Wolfremium
|
||||||
|
* Yannick PÉROUX
|
||||||
|
* Yusuke Kadowaki
|
||||||
|
* Zac Hatfield-Dodds
|
||||||
|
* Zach OBrien
|
||||||
|
* aizpurua23a
|
||||||
|
* bitzge
|
||||||
|
* bluthej
|
||||||
|
* gresm
|
||||||
|
* holesch
|
||||||
|
* itxasos23
|
||||||
|
* johnkangw
|
||||||
|
* q0w
|
||||||
|
* rdb
|
||||||
|
* s-padmanaban
|
||||||
|
* skhomuti
|
||||||
|
* sommersoft
|
||||||
|
* vin01
|
||||||
|
* wim glenn
|
||||||
|
* wodny
|
||||||
|
* zx.qiu
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
pytest-7.3.1
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 7.3.1 has just been released to PyPI.
|
||||||
|
|
||||||
|
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||||
|
|
||||||
|
pip install --upgrade pytest
|
||||||
|
|
||||||
|
The full changelog is available at 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
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
pytest-7.3.2
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 7.3.2 has just been released to PyPI.
|
||||||
|
|
||||||
|
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||||
|
|
||||||
|
pip install --upgrade pytest
|
||||||
|
|
||||||
|
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||||
|
|
||||||
|
Thanks to all of the contributors to this release:
|
||||||
|
|
||||||
|
* Adam J. Stewart
|
||||||
|
* Alessio Izzo
|
||||||
|
* Bruno Oliveira
|
||||||
|
* Ran Benita
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
||||||
@@ -92,3 +92,5 @@ pytest version min. Python version
|
|||||||
5.0 - 6.1 3.5+
|
5.0 - 6.1 3.5+
|
||||||
3.3 - 4.6 2.7, 3.4+
|
3.3 - 4.6 2.7, 3.4+
|
||||||
============== ===================
|
============== ===================
|
||||||
|
|
||||||
|
`Status of Python Versions <https://devguide.python.org/versions/>`__.
|
||||||
|
|||||||
+31
-29
@@ -33,25 +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.
|
Values can be any object handled by the json stdlib module.
|
||||||
|
|
||||||
capsys -- .../_pytest/capture.py:905
|
capsysbinary -- .../_pytest/capture.py:1001
|
||||||
Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
|
||||||
|
|
||||||
The captured output is made available via ``capsys.readouterr()`` method
|
|
||||||
calls, which return a ``(out, err)`` namedtuple.
|
|
||||||
``out`` and ``err`` will be ``text`` objects.
|
|
||||||
|
|
||||||
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
def test_output(capsys):
|
|
||||||
print("hello")
|
|
||||||
captured = capsys.readouterr()
|
|
||||||
assert captured.out == "hello\n"
|
|
||||||
|
|
||||||
capsysbinary -- .../_pytest/capture.py:933
|
|
||||||
Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||||
|
|
||||||
The captured output is made available via ``capsysbinary.readouterr()``
|
The captured output is made available via ``capsysbinary.readouterr()``
|
||||||
@@ -69,7 +51,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
|||||||
captured = capsysbinary.readouterr()
|
captured = capsysbinary.readouterr()
|
||||||
assert captured.out == b"hello\n"
|
assert captured.out == b"hello\n"
|
||||||
|
|
||||||
capfd -- .../_pytest/capture.py:961
|
capfd -- .../_pytest/capture.py:1029
|
||||||
Enable text capturing of writes to file descriptors ``1`` and ``2``.
|
Enable text capturing of writes to file descriptors ``1`` and ``2``.
|
||||||
|
|
||||||
The captured output is made available via ``capfd.readouterr()`` method
|
The captured output is made available via ``capfd.readouterr()`` method
|
||||||
@@ -87,7 +69,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
|||||||
captured = capfd.readouterr()
|
captured = capfd.readouterr()
|
||||||
assert captured.out == "hello\n"
|
assert captured.out == "hello\n"
|
||||||
|
|
||||||
capfdbinary -- .../_pytest/capture.py:989
|
capfdbinary -- .../_pytest/capture.py:1057
|
||||||
Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
|
Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
|
||||||
|
|
||||||
The captured output is made available via ``capfd.readouterr()`` method
|
The captured output is made available via ``capfd.readouterr()`` method
|
||||||
@@ -105,7 +87,25 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
|||||||
captured = capfdbinary.readouterr()
|
captured = capfdbinary.readouterr()
|
||||||
assert captured.out == b"hello\n"
|
assert captured.out == b"hello\n"
|
||||||
|
|
||||||
doctest_namespace [session scope] -- .../_pytest/doctest.py:738
|
capsys -- .../_pytest/capture.py:973
|
||||||
|
Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||||
|
|
||||||
|
The captured output is made available via ``capsys.readouterr()`` method
|
||||||
|
calls, which return a ``(out, err)`` namedtuple.
|
||||||
|
``out`` and ``err`` will be ``text`` objects.
|
||||||
|
|
||||||
|
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def test_output(capsys):
|
||||||
|
print("hello")
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert captured.out == "hello\n"
|
||||||
|
|
||||||
|
doctest_namespace [session scope] -- .../_pytest/doctest.py:737
|
||||||
Fixture that returns a :py:class:`dict` that will be injected into the
|
Fixture that returns a :py:class:`dict` that will be injected into the
|
||||||
namespace of doctests.
|
namespace of doctests.
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
|||||||
|
|
||||||
For more details: :ref:`doctest_namespace`.
|
For more details: :ref:`doctest_namespace`.
|
||||||
|
|
||||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1351
|
pytestconfig [session scope] -- .../_pytest/fixtures.py:1360
|
||||||
Session-scoped fixture that returns the session's :class:`pytest.Config`
|
Session-scoped fixture that returns the session's :class:`pytest.Config`
|
||||||
object.
|
object.
|
||||||
|
|
||||||
@@ -196,7 +196,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
|
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html
|
||||||
|
|
||||||
caplog -- .../_pytest/logging.py:491
|
caplog -- .../_pytest/logging.py:498
|
||||||
Access and control log capturing.
|
Access and control log capturing.
|
||||||
|
|
||||||
Captured logs are available through the following properties/methods::
|
Captured logs are available through the following properties/methods::
|
||||||
@@ -207,7 +207,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.record_tuples -> list of (logger_name, level, message) tuples
|
||||||
* caplog.clear() -> clear captured records and formatted log output string
|
* caplog.clear() -> clear captured records and formatted log output string
|
||||||
|
|
||||||
monkeypatch -- .../_pytest/monkeypatch.py:29
|
monkeypatch -- .../_pytest/monkeypatch.py:30
|
||||||
A convenient fixture for monkey-patching.
|
A convenient fixture for monkey-patching.
|
||||||
|
|
||||||
The fixture provides these methods to modify objects, dictionaries, or
|
The fixture provides these methods to modify objects, dictionaries, or
|
||||||
@@ -237,17 +237,19 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
|||||||
See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information
|
See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information
|
||||||
on warning categories.
|
on warning categories.
|
||||||
|
|
||||||
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:188
|
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:245
|
||||||
Return a :class:`pytest.TempPathFactory` instance for the test session.
|
Return a :class:`pytest.TempPathFactory` instance for the test session.
|
||||||
|
|
||||||
tmp_path -- .../_pytest/tmpdir.py:203
|
tmp_path -- .../_pytest/tmpdir.py:260
|
||||||
Return a temporary directory path object which is unique to each test
|
Return a temporary directory path object which is unique to each test
|
||||||
function invocation, created as a sub directory of the base temporary
|
function invocation, created as a sub directory of the base temporary
|
||||||
directory.
|
directory.
|
||||||
|
|
||||||
By default, a new base temporary directory is created each test session,
|
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
|
and old bases are removed after 3 sessions, to aid in debugging.
|
||||||
``--basetemp`` is used then it is cleared each session. See :ref:`base
|
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`.
|
temporary directory`.
|
||||||
|
|
||||||
The returned object is a :class:`pathlib.Path` object.
|
The returned object is a :class:`pathlib.Path` object.
|
||||||
|
|||||||
+187
-12
@@ -28,6 +28,186 @@ with advance notice in the **Deprecations** section of releases.
|
|||||||
|
|
||||||
.. towncrier release notes start
|
.. towncrier release notes start
|
||||||
|
|
||||||
|
pytest 7.3.2 (2023-06-10)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#10169 <https://github.com/pytest-dev/pytest/issues/10169>`_: Fix bug where very long option names could cause pytest to break with ``OSError: [Errno 36] File name too long`` on some systems.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10894 <https://github.com/pytest-dev/pytest/issues/10894>`_: Support for Python 3.12 (beta at the time of writing).
|
||||||
|
|
||||||
|
|
||||||
|
- `#10987 <https://github.com/pytest-dev/pytest/issues/10987>`_: :confval:`testpaths` is now honored to load root ``conftests``.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10999 <https://github.com/pytest-dev/pytest/issues/10999>`_: The `monkeypatch` `setitem`/`delitem` type annotations now allow `TypedDict` arguments.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11028 <https://github.com/pytest-dev/pytest/issues/11028>`_: Fixed bug in assertion rewriting where a variable assigned with the walrus operator could not be used later in a function call.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11054 <https://github.com/pytest-dev/pytest/issues/11054>`_: Fixed ``--last-failed``'s "(skipped N files)" functionality for files inside of packages (directories with `__init__.py` files).
|
||||||
|
|
||||||
|
|
||||||
|
pytest 7.3.1 (2023-04-14)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Improvements
|
||||||
|
------------
|
||||||
|
|
||||||
|
- `#10875 <https://github.com/pytest-dev/pytest/issues/10875>`_: Python 3.12 support: fixed ``RuntimeError: TestResult has no addDuration method`` when running ``unittest`` tests.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10890 <https://github.com/pytest-dev/pytest/issues/10890>`_: Python 3.12 support: fixed ``shutil.rmtree(onerror=...)`` deprecation warning when using :fixture:`tmp_path`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#10896 <https://github.com/pytest-dev/pytest/issues/10896>`_: Fixed performance regression related to :fixture:`tmp_path` and the new :confval:`tmp_path_retention_policy` option.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10903 <https://github.com/pytest-dev/pytest/issues/10903>`_: Fix crash ``INTERNALERROR IndexError: list index out of range`` which happens when displaying an exception where all entries are hidden.
|
||||||
|
This reverts the change "Correctly handle ``__tracebackhide__`` for chained exceptions." introduced in version 7.3.0.
|
||||||
|
|
||||||
|
|
||||||
|
pytest 7.3.0 (2023-04-08)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
|
||||||
|
- `#10525 <https://github.com/pytest-dev/pytest/issues/10525>`_: Test methods decorated with ``@classmethod`` can now be discovered as tests, following the same rules as normal methods. This fills the gap that static methods were discoverable as tests but not class methods.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10755 <https://github.com/pytest-dev/pytest/issues/10755>`_: :confval:`console_output_style` now supports ``progress-even-when-capture-no`` to force the use of the progress output even when capture is disabled. This is useful in large test suites where capture may have significant performance impact.
|
||||||
|
|
||||||
|
|
||||||
|
- `#7431 <https://github.com/pytest-dev/pytest/issues/7431>`_: ``--log-disable`` CLI option added to disable individual loggers.
|
||||||
|
|
||||||
|
|
||||||
|
- `#8141 <https://github.com/pytest-dev/pytest/issues/8141>`_: Added :confval:`tmp_path_retention_count` and :confval:`tmp_path_retention_policy` configuration options to control how directories created by the :fixture:`tmp_path` fixture are kept.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Improvements
|
||||||
|
------------
|
||||||
|
|
||||||
|
- `#10226 <https://github.com/pytest-dev/pytest/issues/10226>`_: If multiple errors are raised in teardown, we now re-raise an ``ExceptionGroup`` of them instead of discarding all but the last.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10658 <https://github.com/pytest-dev/pytest/issues/10658>`_: Allow ``-p`` arguments to include spaces (eg: ``-p no:logging`` instead of
|
||||||
|
``-pno:logging``). Mostly useful in the ``addopts`` section of the configuration
|
||||||
|
file.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10710 <https://github.com/pytest-dev/pytest/issues/10710>`_: Added ``start`` and ``stop`` timestamps to ``TestReport`` objects.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10727 <https://github.com/pytest-dev/pytest/issues/10727>`_: Split the report header for ``rootdir``, ``config file`` and ``testpaths`` so each has its own line.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10840 <https://github.com/pytest-dev/pytest/issues/10840>`_: pytest should no longer crash on AST with pathological position attributes, for example testing AST produced by `Hylang <https://github.com/hylang/hy>__`.
|
||||||
|
|
||||||
|
|
||||||
|
- `#6267 <https://github.com/pytest-dev/pytest/issues/6267>`_: The full output of a test is no longer truncated if the truncation message would be longer than
|
||||||
|
the hidden text. The line number shown has also been fixed.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#10743 <https://github.com/pytest-dev/pytest/issues/10743>`_: The assertion rewriting mechanism now works correctly when assertion expressions contain the walrus operator.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10765 <https://github.com/pytest-dev/pytest/issues/10765>`_: Fixed :fixture:`tmp_path` fixture always raising :class:`OSError` on ``emscripten`` platform due to missing :func:`os.getuid`.
|
||||||
|
|
||||||
|
|
||||||
|
- `#1904 <https://github.com/pytest-dev/pytest/issues/1904>`_: Correctly handle ``__tracebackhide__`` for chained exceptions.
|
||||||
|
NOTE: This change was reverted in version 7.3.1.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Improved Documentation
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
- `#10782 <https://github.com/pytest-dev/pytest/issues/10782>`_: Fixed the minimal example in :ref:`goodpractices`: ``pip install -e .`` requires a ``version`` entry in ``pyproject.toml`` to run successfully.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Trivial/Internal Changes
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
- `#10669 <https://github.com/pytest-dev/pytest/issues/10669>`_: pytest no longer directly depends on the `attrs <https://www.attrs.org/en/stable/>`__ package. While
|
||||||
|
we at pytest all love the package dearly and would like to thank the ``attrs`` team for many years of cooperation and support,
|
||||||
|
it makes sense for ``pytest`` to have as little external dependencies as possible, as this helps downstream projects.
|
||||||
|
With that in mind, we have replaced the pytest's limited internal usage to use the standard library's ``dataclasses`` instead.
|
||||||
|
|
||||||
|
Nice diffs for ``attrs`` classes are still supported though.
|
||||||
|
|
||||||
|
|
||||||
|
pytest 7.2.2 (2023-03-03)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#10533 <https://github.com/pytest-dev/pytest/issues/10533>`_: Fixed :func:`pytest.approx` handling of dictionaries containing one or more values of `0.0`.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10592 <https://github.com/pytest-dev/pytest/issues/10592>`_: Fixed crash if `--cache-show` and `--help` are passed at the same time.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10597 <https://github.com/pytest-dev/pytest/issues/10597>`_: Fixed bug where a fixture method named ``teardown`` would be called as part of ``nose`` teardown stage.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10626 <https://github.com/pytest-dev/pytest/issues/10626>`_: Fixed crash if ``--fixtures`` and ``--help`` are passed at the same time.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10660 <https://github.com/pytest-dev/pytest/issues/10660>`_: Fixed :py:func:`pytest.raises` to return a 'ContextManager' so that type-checkers could narrow
|
||||||
|
:code:`pytest.raises(...) if ... else nullcontext()` down to 'ContextManager' rather than 'object'.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Improved Documentation
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
- `#10690 <https://github.com/pytest-dev/pytest/issues/10690>`_: Added `CI` and `BUILD_NUMBER` environment variables to the documentation.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10721 <https://github.com/pytest-dev/pytest/issues/10721>`_: Fixed entry-points declaration in the documentation example using Hatch.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10753 <https://github.com/pytest-dev/pytest/issues/10753>`_: Changed wording of the module level skip to be very explicit
|
||||||
|
about not collecting tests and not executing the rest of the module.
|
||||||
|
|
||||||
|
|
||||||
|
pytest 7.2.1 (2023-01-13)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#10452 <https://github.com/pytest-dev/pytest/issues/10452>`_: Fix 'importlib.abc.TraversableResources' deprecation warning in Python 3.12.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10457 <https://github.com/pytest-dev/pytest/issues/10457>`_: If a test is skipped from inside a fixture, the test summary now shows the test location instead of the fixture location.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10506 <https://github.com/pytest-dev/pytest/issues/10506>`_: Fix bug where sometimes pytest would use the file system root directory as :ref:`rootdir <rootdir>` on Windows.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10607 <https://github.com/pytest-dev/pytest/issues/10607>`_: Fix a race condition when creating junitxml reports, which could occur when multiple instances of pytest execute in parallel.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10641 <https://github.com/pytest-dev/pytest/issues/10641>`_: Fix a race condition when creating or updating the stepwise plugin's cache, which could occur when multiple xdist worker nodes try to simultaneously update the stepwise plugin's cache.
|
||||||
|
|
||||||
|
|
||||||
pytest 7.2.0 (2022-10-23)
|
pytest 7.2.0 (2022-10-23)
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
@@ -57,6 +237,7 @@ Deprecations
|
|||||||
|
|
||||||
.. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup
|
.. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup
|
||||||
|
|
||||||
|
- `#7337 <https://github.com/pytest-dev/pytest/issues/7337>`_: A deprecation warning is now emitted if a test function returns something other than `None`. This prevents a common mistake among beginners that expect that returning a `bool` (for example `return foo(a, b) == result`) would cause a test to pass or fail, instead of using `assert`. The plan is to make returning non-`None` from tests an error in the future.
|
||||||
|
|
||||||
|
|
||||||
Features
|
Features
|
||||||
@@ -79,10 +260,7 @@ Improvements
|
|||||||
- `#10381 <https://github.com/pytest-dev/pytest/issues/10381>`_: The ``--no-showlocals`` flag has been added. This can be passed directly to tests to override ``--showlocals`` declared through ``addopts``.
|
- `#10381 <https://github.com/pytest-dev/pytest/issues/10381>`_: The ``--no-showlocals`` flag has been added. This can be passed directly to tests to override ``--showlocals`` declared through ``addopts``.
|
||||||
|
|
||||||
|
|
||||||
- `#3426 <https://github.com/pytest-dev/pytest/issues/3426>`_: Assertion failures with strings in NFC and NFD forms that normalize to the same string now have a dedicated error message detailing the issue, and their utf-8 representation is expresed instead.
|
- `#3426 <https://github.com/pytest-dev/pytest/issues/3426>`_: Assertion failures with strings in NFC and NFD forms that normalize to the same string now have a dedicated error message detailing the issue, and their utf-8 representation is expressed instead.
|
||||||
|
|
||||||
|
|
||||||
- `#7337 <https://github.com/pytest-dev/pytest/issues/7337>`_: A warning is now emitted if a test function returns something other than `None`. This prevents a common mistake among beginners that expect that returning a `bool` (for example `return foo(a, b) == result`) would cause a test to pass or fail, instead of using `assert`.
|
|
||||||
|
|
||||||
|
|
||||||
- `#8508 <https://github.com/pytest-dev/pytest/issues/8508>`_: Introduce multiline display for warning matching via :py:func:`pytest.warns` and
|
- `#8508 <https://github.com/pytest-dev/pytest/issues/8508>`_: Introduce multiline display for warning matching via :py:func:`pytest.warns` and
|
||||||
@@ -95,7 +273,7 @@ Improvements
|
|||||||
|
|
||||||
- `#9741 <https://github.com/pytest-dev/pytest/issues/9741>`_: On Python 3.11, use the standard library's :mod:`tomllib` to parse TOML.
|
- `#9741 <https://github.com/pytest-dev/pytest/issues/9741>`_: On Python 3.11, use the standard library's :mod:`tomllib` to parse TOML.
|
||||||
|
|
||||||
:mod:`tomli`` is no longer a dependency on Python 3.11.
|
:mod:`tomli` is no longer a dependency on Python 3.11.
|
||||||
|
|
||||||
|
|
||||||
- `#9742 <https://github.com/pytest-dev/pytest/issues/9742>`_: Display assertion message without escaped newline characters with ``-vv``.
|
- `#9742 <https://github.com/pytest-dev/pytest/issues/9742>`_: Display assertion message without escaped newline characters with ``-vv``.
|
||||||
@@ -110,7 +288,7 @@ Improvements
|
|||||||
- `#9883 <https://github.com/pytest-dev/pytest/issues/9883>`_: Normalize the help description of all command-line options.
|
- `#9883 <https://github.com/pytest-dev/pytest/issues/9883>`_: Normalize the help description of all command-line options.
|
||||||
|
|
||||||
|
|
||||||
- `#9920 <https://github.com/pytest-dev/pytest/issues/9920>`_: Display full crash messages in ``short test summary info``, when runng in a CI environment.
|
- `#9920 <https://github.com/pytest-dev/pytest/issues/9920>`_: Display full crash messages in ``short test summary info``, when running in a CI environment.
|
||||||
|
|
||||||
|
|
||||||
- `#9987 <https://github.com/pytest-dev/pytest/issues/9987>`_: Added support for hidden configuration file by allowing ``.pytest.ini`` as an alternative to ``pytest.ini``.
|
- `#9987 <https://github.com/pytest-dev/pytest/issues/9987>`_: Added support for hidden configuration file by allowing ``.pytest.ini`` as an alternative to ``pytest.ini``.
|
||||||
@@ -156,9 +334,6 @@ Improved Documentation
|
|||||||
Trivial/Internal Changes
|
Trivial/Internal Changes
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
- `#10196 <https://github.com/pytest-dev/pytest/issues/10196>`_: :class:`~pytest.PytestReturnNotNoneWarning` is now a subclass of :class:`~pytest.PytestRemovedIn8Warning`: the plan is to make returning non-``None`` from tests an error in the future.
|
|
||||||
|
|
||||||
|
|
||||||
- `#10313 <https://github.com/pytest-dev/pytest/issues/10313>`_: Made ``_pytest.doctest.DoctestItem`` export ``pytest.DoctestItem`` for
|
- `#10313 <https://github.com/pytest-dev/pytest/issues/10313>`_: Made ``_pytest.doctest.DoctestItem`` export ``pytest.DoctestItem`` for
|
||||||
type check and runtime purposes. Made `_pytest.doctest` use internal APIs
|
type check and runtime purposes. Made `_pytest.doctest` use internal APIs
|
||||||
to avoid circular imports.
|
to avoid circular imports.
|
||||||
@@ -173,7 +348,7 @@ Trivial/Internal Changes
|
|||||||
- `#9984 <https://github.com/pytest-dev/pytest/issues/9984>`_: Improve the error message when we attempt to access a fixture that has been
|
- `#9984 <https://github.com/pytest-dev/pytest/issues/9984>`_: Improve the error message when we attempt to access a fixture that has been
|
||||||
torn down.
|
torn down.
|
||||||
Add an additional sentence to the docstring explaining when it's not a good
|
Add an additional sentence to the docstring explaining when it's not a good
|
||||||
idea to call getfixturevalue.
|
idea to call ``getfixturevalue``.
|
||||||
|
|
||||||
|
|
||||||
pytest 7.1.3 (2022-08-31)
|
pytest 7.1.3 (2022-08-31)
|
||||||
@@ -416,7 +591,7 @@ Breaking Changes
|
|||||||
- `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: The :ref:`Node.reportinfo() <non-python tests>` function first return value type has been expanded from `py.path.local | str` to `os.PathLike[str] | str`.
|
- `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: The :ref:`Node.reportinfo() <non-python tests>` function first return value type has been expanded from `py.path.local | str` to `os.PathLike[str] | str`.
|
||||||
|
|
||||||
Most plugins which refer to `reportinfo()` only define it as part of a custom :class:`pytest.Item` implementation.
|
Most plugins which refer to `reportinfo()` only define it as part of a custom :class:`pytest.Item` implementation.
|
||||||
Since `py.path.local` is a `os.PathLike[str]`, these plugins are unaffacted.
|
Since `py.path.local` is an `os.PathLike[str]`, these plugins are unaffacted.
|
||||||
|
|
||||||
Plugins and users which call `reportinfo()`, use the first return value and interact with it as a `py.path.local`, would need to adjust by calling `py.path.local(fspath)`.
|
Plugins and users which call `reportinfo()`, use the first return value and interact with it as a `py.path.local`, would need to adjust by calling `py.path.local(fspath)`.
|
||||||
Although preferably, avoid the legacy `py.path.local` and use `pathlib.Path`, or use `item.location` or `item.path`, instead.
|
Although preferably, avoid the legacy `py.path.local` and use `pathlib.Path`, or use `item.location` or `item.path`, instead.
|
||||||
@@ -3916,7 +4091,7 @@ Removals
|
|||||||
See our :ref:`docs <calling fixtures directly deprecated>` on information on how to update your code.
|
See our :ref:`docs <calling fixtures directly deprecated>` on information on how to update your code.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than a existence check.
|
- :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than an existence check.
|
||||||
|
|
||||||
Use ``Node.get_closest_marker(name)`` as a replacement.
|
Use ``Node.get_closest_marker(name)`` as a replacement.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -341,7 +341,7 @@ epub_copyright = "2013, holger krekel et alii"
|
|||||||
# The scheme of the identifier. Typical schemes are ISBN or URL.
|
# The scheme of the identifier. Typical schemes are ISBN or URL.
|
||||||
# epub_scheme = ''
|
# epub_scheme = ''
|
||||||
|
|
||||||
# The unique identifier of the text. This can be a ISBN number
|
# The unique identifier of the text. This can be an ISBN number
|
||||||
# or the project homepage.
|
# or the project homepage.
|
||||||
# epub_identifier = ''
|
# epub_identifier = ''
|
||||||
|
|
||||||
|
|||||||
@@ -1052,7 +1052,7 @@ that are then turned into proper test methods. Example:
|
|||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def check(x, y):
|
def check(x, y):
|
||||||
assert x ** x == y
|
assert x**x == y
|
||||||
|
|
||||||
|
|
||||||
def test_squared():
|
def test_squared():
|
||||||
@@ -1067,7 +1067,7 @@ This form of test function doesn't support fixtures properly, and users should s
|
|||||||
|
|
||||||
@pytest.mark.parametrize("x, y", [(2, 4), (3, 9)])
|
@pytest.mark.parametrize("x, y", [(2, 4), (3, 9)])
|
||||||
def test_squared(x, y):
|
def test_squared(x, y):
|
||||||
assert x ** x == y
|
assert x**x == y
|
||||||
|
|
||||||
.. _internal classes accessed through node deprecated:
|
.. _internal classes accessed through node deprecated:
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ def b(a, order):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def c(a, b, order):
|
def c(b, order):
|
||||||
order.append("c")
|
order.append("c")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class YamlItem(pytest.Item):
|
|||||||
" no further details known at this point.",
|
" no further details known at this point.",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
return super().repr_failure(excinfo)
|
||||||
|
|
||||||
def reportinfo(self):
|
def reportinfo(self):
|
||||||
return self.path, 0, f"usecase: {self.name}"
|
return self.path, 0, f"usecase: {self.name}"
|
||||||
|
|||||||
@@ -504,9 +504,9 @@ Running it results in some skips if we don't have all the python interpreters in
|
|||||||
. $ pytest -rs -q multipython.py
|
. $ pytest -rs -q multipython.py
|
||||||
sssssssssssssssssssssssssss [100%]
|
sssssssssssssssssssssssssss [100%]
|
||||||
========================= short test summary info ==========================
|
========================= short test summary info ==========================
|
||||||
SKIPPED [9] multipython.py:29: 'python3.5' not found
|
SKIPPED [9] multipython.py:69: 'python3.5' not found
|
||||||
SKIPPED [9] multipython.py:29: 'python3.6' not found
|
SKIPPED [9] multipython.py:69: 'python3.6' not found
|
||||||
SKIPPED [9] multipython.py:29: 'python3.7' not found
|
SKIPPED [9] multipython.py:69: 'python3.7' not found
|
||||||
27 skipped in 0.12s
|
27 skipped in 0.12s
|
||||||
|
|
||||||
Indirect parametrization of optional implementations/imports
|
Indirect parametrization of optional implementations/imports
|
||||||
@@ -574,7 +574,7 @@ If you run this with reporting for skips enabled:
|
|||||||
test_module.py .s [100%]
|
test_module.py .s [100%]
|
||||||
|
|
||||||
========================= short test summary info ==========================
|
========================= short test summary info ==========================
|
||||||
SKIPPED [1] conftest.py:12: could not import 'opt2': No module named 'opt2'
|
SKIPPED [1] test_module.py:3: could not import 'opt2': No module named 'opt2'
|
||||||
======================= 1 passed, 1 skipped in 0.12s =======================
|
======================= 1 passed, 1 skipped in 0.12s =======================
|
||||||
|
|
||||||
You'll see that we don't have an ``opt2`` module and thus the second test run
|
You'll see that we don't have an ``opt2`` module and thus the second test run
|
||||||
|
|||||||
@@ -148,7 +148,8 @@ The test collection would look like this:
|
|||||||
$ pytest --collect-only
|
$ pytest --collect-only
|
||||||
=========================== test session starts ============================
|
=========================== test session starts ============================
|
||||||
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
|
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
|
||||||
rootdir: /home/sweet/project, configfile: pytest.ini
|
rootdir: /home/sweet/project
|
||||||
|
configfile: pytest.ini
|
||||||
collected 2 items
|
collected 2 items
|
||||||
|
|
||||||
<Module check_myapp.py>
|
<Module check_myapp.py>
|
||||||
@@ -209,7 +210,8 @@ You can always peek at the collection tree without running tests like this:
|
|||||||
. $ pytest --collect-only pythoncollection.py
|
. $ pytest --collect-only pythoncollection.py
|
||||||
=========================== test session starts ============================
|
=========================== test session starts ============================
|
||||||
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
|
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
|
||||||
rootdir: /home/sweet/project, configfile: pytest.ini
|
rootdir: /home/sweet/project
|
||||||
|
configfile: pytest.ini
|
||||||
collected 3 items
|
collected 3 items
|
||||||
|
|
||||||
<Module CWD/pythoncollection.py>
|
<Module CWD/pythoncollection.py>
|
||||||
@@ -290,7 +292,8 @@ file will be left out:
|
|||||||
$ pytest --collect-only
|
$ pytest --collect-only
|
||||||
=========================== test session starts ============================
|
=========================== test session starts ============================
|
||||||
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
|
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
|
||||||
rootdir: /home/sweet/project, configfile: pytest.ini
|
rootdir: /home/sweet/project
|
||||||
|
configfile: pytest.ini
|
||||||
collected 0 items
|
collected 0 items
|
||||||
|
|
||||||
======================= no tests collected in 0.12s ========================
|
======================= no tests collected in 0.12s ========================
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
|||||||
E 1
|
E 1
|
||||||
E 1...
|
E 1...
|
||||||
E
|
E
|
||||||
E ...Full output truncated (7 lines hidden), use '-vv' to show
|
E ...Full output truncated (6 lines hidden), use '-vv' to show
|
||||||
|
|
||||||
failure_demo.py:60: AssertionError
|
failure_demo.py:60: AssertionError
|
||||||
_________________ TestSpecialisedExplanations.test_eq_list _________________
|
_________________ TestSpecialisedExplanations.test_eq_list _________________
|
||||||
@@ -184,9 +184,8 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
|||||||
E Left contains 1 more item:
|
E Left contains 1 more item:
|
||||||
E {'c': 0}
|
E {'c': 0}
|
||||||
E Right contains 1 more item:
|
E Right contains 1 more item:
|
||||||
E {'d': 0}...
|
E {'d': 0}
|
||||||
E
|
E Use -v to get more diff
|
||||||
E ...Full output truncated (2 lines hidden), use '-vv' to show
|
|
||||||
|
|
||||||
failure_demo.py:71: AssertionError
|
failure_demo.py:71: AssertionError
|
||||||
_________________ TestSpecialisedExplanations.test_eq_set __________________
|
_________________ TestSpecialisedExplanations.test_eq_set __________________
|
||||||
@@ -195,16 +194,15 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
|||||||
|
|
||||||
def test_eq_set(self):
|
def test_eq_set(self):
|
||||||
> assert {0, 10, 11, 12} == {0, 20, 21}
|
> assert {0, 10, 11, 12} == {0, 20, 21}
|
||||||
E AssertionError: assert {0, 10, 11, 12} == {0, 20, 21}
|
E assert {0, 10, 11, 12} == {0, 20, 21}
|
||||||
E Extra items in the left set:
|
E Extra items in the left set:
|
||||||
E 10
|
E 10
|
||||||
E 11
|
E 11
|
||||||
E 12
|
E 12
|
||||||
E Extra items in the right set:
|
E Extra items in the right set:
|
||||||
E 20
|
E 20
|
||||||
E 21...
|
E 21
|
||||||
E
|
E Use -v to get more diff
|
||||||
E ...Full output truncated (2 lines hidden), use '-vv' to show
|
|
||||||
|
|
||||||
failure_demo.py:74: AssertionError
|
failure_demo.py:74: AssertionError
|
||||||
_____________ TestSpecialisedExplanations.test_eq_longer_list ______________
|
_____________ TestSpecialisedExplanations.test_eq_longer_list ______________
|
||||||
@@ -241,9 +239,8 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
|||||||
E which
|
E which
|
||||||
E includes foo
|
E includes foo
|
||||||
E ? +++
|
E ? +++
|
||||||
E and a...
|
E and a
|
||||||
E
|
E tail
|
||||||
E ...Full output truncated (2 lines hidden), use '-vv' to show
|
|
||||||
|
|
||||||
failure_demo.py:84: AssertionError
|
failure_demo.py:84: AssertionError
|
||||||
___________ TestSpecialisedExplanations.test_not_in_text_single ____________
|
___________ TestSpecialisedExplanations.test_not_in_text_single ____________
|
||||||
@@ -307,9 +304,9 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
|||||||
E ['b']
|
E ['b']
|
||||||
E
|
E
|
||||||
E Drill down into differing attribute b:
|
E Drill down into differing attribute b:
|
||||||
E b: 'b' != 'c'...
|
E b: 'b' != 'c'
|
||||||
E
|
E - c
|
||||||
E ...Full output truncated (3 lines hidden), use '-vv' to show
|
E + b
|
||||||
|
|
||||||
failure_demo.py:108: AssertionError
|
failure_demo.py:108: AssertionError
|
||||||
________________ TestSpecialisedExplanations.test_eq_attrs _________________
|
________________ TestSpecialisedExplanations.test_eq_attrs _________________
|
||||||
@@ -334,9 +331,9 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
|||||||
E ['b']
|
E ['b']
|
||||||
E
|
E
|
||||||
E Drill down into differing attribute b:
|
E Drill down into differing attribute b:
|
||||||
E b: 'b' != 'c'...
|
E b: 'b' != 'c'
|
||||||
E
|
E - c
|
||||||
E ...Full output truncated (3 lines hidden), use '-vv' to show
|
E + b
|
||||||
|
|
||||||
failure_demo.py:120: AssertionError
|
failure_demo.py:120: AssertionError
|
||||||
______________________________ test_attribute ______________________________
|
______________________________ test_attribute ______________________________
|
||||||
@@ -673,7 +670,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
|||||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_list - asser...
|
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_list - asser...
|
||||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_list_long - ...
|
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_list_long - ...
|
||||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_dict - Asser...
|
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_dict - Asser...
|
||||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_set - Assert...
|
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_set - assert...
|
||||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_longer_list
|
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_longer_list
|
||||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_in_list - asser...
|
FAILED failure_demo.py::TestSpecialisedExplanations::test_in_list - asser...
|
||||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_not_in_text_multiline
|
FAILED failure_demo.py::TestSpecialisedExplanations::test_not_in_text_multiline
|
||||||
|
|||||||
+14
-12
@@ -691,7 +691,7 @@ Here is an example for making a ``db`` fixture available in a directory:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="package")
|
||||||
def db():
|
def db():
|
||||||
return DB()
|
return DB()
|
||||||
|
|
||||||
@@ -892,8 +892,11 @@ here is a little example implemented via a local plugin:
|
|||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
# content of conftest.py
|
# content of conftest.py
|
||||||
|
from typing import Dict
|
||||||
import pytest
|
import pytest
|
||||||
|
from pytest import StashKey, CollectReport
|
||||||
|
|
||||||
|
phase_report_key = StashKey[Dict[str, CollectReport]]()
|
||||||
|
|
||||||
|
|
||||||
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
||||||
@@ -902,10 +905,9 @@ here is a little example implemented via a local plugin:
|
|||||||
outcome = yield
|
outcome = yield
|
||||||
rep = outcome.get_result()
|
rep = outcome.get_result()
|
||||||
|
|
||||||
# set a report attribute for each phase of a call, which can
|
# store test results for each phase of a call, which can
|
||||||
# be "setup", "call", "teardown"
|
# be "setup", "call", "teardown"
|
||||||
|
item.stash.setdefault(phase_report_key, {})[rep.when] = rep
|
||||||
setattr(item, "rep_" + rep.when, rep)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -913,11 +915,11 @@ here is a little example implemented via a local plugin:
|
|||||||
yield
|
yield
|
||||||
# request.node is an "item" because we use the default
|
# request.node is an "item" because we use the default
|
||||||
# "function" scope
|
# "function" scope
|
||||||
if request.node.rep_setup.failed:
|
report = request.node.stash[phase_report_key]
|
||||||
print("setting up a test failed!", request.node.nodeid)
|
if report["setup"].failed:
|
||||||
elif request.node.rep_setup.passed:
|
print("setting up a test failed or skipped", request.node.nodeid)
|
||||||
if request.node.rep_call.failed:
|
elif ("call" not in report) or report["call"].failed:
|
||||||
print("executing test failed", request.node.nodeid)
|
print("executing test failed or skipped", request.node.nodeid)
|
||||||
|
|
||||||
|
|
||||||
if you then have failing tests:
|
if you then have failing tests:
|
||||||
@@ -955,8 +957,8 @@ and run it:
|
|||||||
rootdir: /home/sweet/project
|
rootdir: /home/sweet/project
|
||||||
collected 3 items
|
collected 3 items
|
||||||
|
|
||||||
test_module.py Esetting up a test failed! test_module.py::test_setup_fails
|
test_module.py Esetting up a test failed or skipped test_module.py::test_setup_fails
|
||||||
Fexecuting test failed test_module.py::test_call_fails
|
Fexecuting test failed or skipped test_module.py::test_call_fails
|
||||||
F
|
F
|
||||||
|
|
||||||
================================== ERRORS ==================================
|
================================== ERRORS ==================================
|
||||||
|
|||||||
@@ -24,8 +24,9 @@ The first few lines should look like this:
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "PACKAGENAME"
|
name = "PACKAGENAME"
|
||||||
|
version = "PACKAGEVERSION"
|
||||||
|
|
||||||
where ``PACKAGENAME`` is the name of your package.
|
where ``PACKAGENAME`` and ``PACKAGEVERSION`` are the name and version of your package respectively.
|
||||||
|
|
||||||
You can then install your package in "editable" mode by running from the same directory:
|
You can then install your package in "editable" mode by running from the same directory:
|
||||||
|
|
||||||
@@ -50,8 +51,8 @@ Conventions for Python test discovery
|
|||||||
* In those directories, search for ``test_*.py`` or ``*_test.py`` files, imported by their `test package name`_.
|
* In those directories, search for ``test_*.py`` or ``*_test.py`` files, imported by their `test package name`_.
|
||||||
* From those files, collect test items:
|
* From those files, collect test items:
|
||||||
|
|
||||||
* ``test`` prefixed test functions or methods outside of class
|
* ``test`` prefixed test functions or methods outside of class.
|
||||||
* ``test`` prefixed test functions or methods inside ``Test`` prefixed test classes (without an ``__init__`` method)
|
* ``test`` prefixed test functions or methods inside ``Test`` prefixed test classes (without an ``__init__`` method). Methods decorated with ``@staticmethod`` and ``@classmethods`` are also considered.
|
||||||
|
|
||||||
For examples of how to customize your test discovery :doc:`/example/pythoncollection`.
|
For examples of how to customize your test discovery :doc:`/example/pythoncollection`.
|
||||||
|
|
||||||
@@ -270,8 +271,8 @@ tox
|
|||||||
|
|
||||||
Once you are done with your work and want to make sure that your actual
|
Once you are done with your work and want to make sure that your actual
|
||||||
package passes all tests you may want to look into :doc:`tox <tox:index>`, the
|
package passes all tests you may want to look into :doc:`tox <tox:index>`, the
|
||||||
virtualenv test automation tool and its :doc:`pytest support <tox:example/pytest>`.
|
virtualenv test automation tool.
|
||||||
tox helps you to setup virtualenv environments with pre-defined
|
``tox`` helps you to setup virtualenv environments with pre-defined
|
||||||
dependencies and then executing a pre-configured test command with
|
dependencies and then executing a pre-configured test command with
|
||||||
options. It will run tests against the installed package and not
|
options. It will run tests against the installed package and not
|
||||||
against your source code checkout, helping to detect packaging
|
against your source code checkout, helping to detect packaging
|
||||||
@@ -293,3 +294,20 @@ See also `pypa/setuptools#1684 <https://github.com/pypa/setuptools/issues/1684>`
|
|||||||
|
|
||||||
setuptools intends to
|
setuptools intends to
|
||||||
`remove the test command <https://github.com/pypa/setuptools/issues/931>`_.
|
`remove the test command <https://github.com/pypa/setuptools/issues/931>`_.
|
||||||
|
|
||||||
|
Checking with flake8-pytest-style
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
In order to ensure that pytest is being used correctly in your project,
|
||||||
|
it can be helpful to use the `flake8-pytest-style <https://github.com/m-burst/flake8-pytest-style>`_ flake8 plugin.
|
||||||
|
|
||||||
|
flake8-pytest-style checks for common mistakes and coding style violations in pytest code,
|
||||||
|
such as incorrect use of fixtures, test function names, and markers.
|
||||||
|
By using this plugin, you can catch these errors early in the development process
|
||||||
|
and ensure that your pytest code is consistent and easy to maintain.
|
||||||
|
|
||||||
|
A list of the lints detected by flake8-pytest-style can be found on its `PyPI page <https://pypi.org/project/flake8-pytest-style/>`_.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
flake8-pytest-style is not an official pytest project. Some of the rules enforce certain style choices, such as using `@pytest.fixture()` over `@pytest.fixture`, but you can configure the plugin to fit your preferred style.
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import process can be controlled through the ``--import-mode`` command-line flag
|
|||||||
these values:
|
these values:
|
||||||
|
|
||||||
* ``prepend`` (default): the directory path containing each module will be inserted into the *beginning*
|
* ``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:`__import__ <__import__>` builtin.
|
of :py:data:`sys.path` if not already there, and then imported with the :func:`importlib.import_module <importlib.import_module>` function.
|
||||||
|
|
||||||
This requires test module names to be unique when the test directory tree is not arranged in
|
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.
|
packages, because the modules will put in :py:data:`sys.modules` after importing.
|
||||||
@@ -24,7 +24,7 @@ these values:
|
|||||||
This is the classic mechanism, dating back from the time Python 2 was still supported.
|
This is the classic mechanism, dating back from the time Python 2 was still supported.
|
||||||
|
|
||||||
* ``append``: the directory containing each module is appended to the end of :py:data:`sys.path` if not already
|
* ``append``: the directory containing each module is appended to the end of :py:data:`sys.path` if not already
|
||||||
there, and imported with ``__import__``.
|
there, and imported with :func:`importlib.import_module <importlib.import_module>`.
|
||||||
|
|
||||||
This better allows to run test modules against installed versions of a package even if the
|
This better allows to run test modules against installed versions of a package even if the
|
||||||
package under test has the same import root. For example:
|
package under test has the same import root. For example:
|
||||||
@@ -43,7 +43,7 @@ these values:
|
|||||||
Same as ``prepend``, requires test module names to be unique when the test directory tree is
|
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.
|
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 :mod:`importlib` to import test modules. This gives full control over the import process, and doesn't require changing :py:data:`sys.path`.
|
* ``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`.
|
||||||
|
|
||||||
For this reason this doesn't require test module names to be unique.
|
For this reason this doesn't require test module names to be unique.
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Install ``pytest``
|
|||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ pytest --version
|
$ pytest --version
|
||||||
pytest 7.2.0
|
pytest 7.3.2
|
||||||
|
|
||||||
.. _`simpletest`:
|
.. _`simpletest`:
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,18 @@ When a warning matches more than one option in the list, the action for the last
|
|||||||
is performed.
|
is performed.
|
||||||
|
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The ``-W`` flag and the ``filterwarnings`` ini option use warning filters that are
|
||||||
|
similar in structure, but each configuration option interprets its filter
|
||||||
|
differently. For example, *message* in ``filterwarnings`` is a string containing a
|
||||||
|
regular expression that the start of the warning message must match,
|
||||||
|
case-insensitively, while *message* in ``-W`` is a literal string that the start of
|
||||||
|
the warning message must contain (case-insensitively), ignoring any whitespace at
|
||||||
|
the start or end of message. Consult the `warning filter`_ documentation for more
|
||||||
|
details.
|
||||||
|
|
||||||
|
|
||||||
.. _`filterwarnings`:
|
.. _`filterwarnings`:
|
||||||
|
|
||||||
``@pytest.mark.filterwarnings``
|
``@pytest.mark.filterwarnings``
|
||||||
@@ -270,20 +282,34 @@ which works in a similar manner to :ref:`raises <assertraises>` (except that
|
|||||||
warnings.warn("my warning", UserWarning)
|
warnings.warn("my warning", UserWarning)
|
||||||
|
|
||||||
The test will fail if the warning in question is not raised. Use the keyword
|
The test will fail if the warning in question is not raised. Use the keyword
|
||||||
argument ``match`` to assert that the warning matches a text or regex::
|
argument ``match`` to assert that the warning matches a text or regex.
|
||||||
|
To match a literal string that may contain regular expression metacharacters like ``(`` or ``.``, the pattern can
|
||||||
|
first be escaped with ``re.escape``.
|
||||||
|
|
||||||
>>> with warns(UserWarning, match='must be 0 or None'):
|
Some examples:
|
||||||
|
|
||||||
|
.. code-block:: pycon
|
||||||
|
|
||||||
|
|
||||||
|
>>> with warns(UserWarning, match="must be 0 or None"):
|
||||||
... warnings.warn("value must be 0 or None", UserWarning)
|
... warnings.warn("value must be 0 or None", UserWarning)
|
||||||
|
...
|
||||||
|
|
||||||
>>> with warns(UserWarning, match=r'must be \d+$'):
|
>>> with warns(UserWarning, match=r"must be \d+$"):
|
||||||
... warnings.warn("value must be 42", UserWarning)
|
... warnings.warn("value must be 42", UserWarning)
|
||||||
|
...
|
||||||
|
|
||||||
>>> with warns(UserWarning, match=r'must be \d+$'):
|
>>> with warns(UserWarning, match=r"must be \d+$"):
|
||||||
... warnings.warn("this is not here", UserWarning)
|
... warnings.warn("this is not here", UserWarning)
|
||||||
|
...
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted...
|
Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted...
|
||||||
|
|
||||||
|
>>> with warns(UserWarning, match=re.escape("issue with foo() func")):
|
||||||
|
... warnings.warn("issue with foo() func")
|
||||||
|
...
|
||||||
|
|
||||||
You can also call :func:`pytest.warns` on a function or code string:
|
You can also call :func:`pytest.warns` on a function or code string:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|||||||
@@ -1237,7 +1237,6 @@ If the data created by the factory requires managing, the fixture can take care
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def make_customer_record():
|
def make_customer_record():
|
||||||
|
|
||||||
created_records = []
|
created_records = []
|
||||||
|
|
||||||
def _make_customer_record(name):
|
def _make_customer_record(name):
|
||||||
|
|||||||
@@ -55,6 +55,13 @@ These options can also be customized through ``pytest.ini`` file:
|
|||||||
log_format = %(asctime)s %(levelname)s %(message)s
|
log_format = %(asctime)s %(levelname)s %(message)s
|
||||||
log_date_format = %Y-%m-%d %H:%M:%S
|
log_date_format = %Y-%m-%d %H:%M:%S
|
||||||
|
|
||||||
|
Specific loggers can be disabled via ``--log-disable={logger_name}``.
|
||||||
|
This argument can be passed multiple times:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pytest --log-disable=main --log-disable=testing
|
||||||
|
|
||||||
Further it is possible to disable reporting of captured content (stdout,
|
Further it is possible to disable reporting of captured content (stdout,
|
||||||
stderr and logs) on failed tests completely with:
|
stderr and logs) on failed tests completely with:
|
||||||
|
|
||||||
|
|||||||
@@ -135,10 +135,10 @@ This can be done in our test file by defining a class to represent ``r``.
|
|||||||
# this is the previous code block example
|
# this is the previous code block example
|
||||||
import app
|
import app
|
||||||
|
|
||||||
|
|
||||||
# custom class to be the mock return value
|
# custom class to be the mock return value
|
||||||
# will override the requests.Response returned from requests.get
|
# will override the requests.Response returned from requests.get
|
||||||
class MockResponse:
|
class MockResponse:
|
||||||
|
|
||||||
# mock json() method always returns a specific testing dictionary
|
# mock json() method always returns a specific testing dictionary
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def json():
|
def json():
|
||||||
@@ -146,7 +146,6 @@ This can be done in our test file by defining a class to represent ``r``.
|
|||||||
|
|
||||||
|
|
||||||
def test_get_json(monkeypatch):
|
def test_get_json(monkeypatch):
|
||||||
|
|
||||||
# Any arguments may be passed and mock_get() will always return our
|
# Any arguments may be passed and mock_get() will always return our
|
||||||
# mocked object, which only has the .json() method.
|
# mocked object, which only has the .json() method.
|
||||||
def mock_get(*args, **kwargs):
|
def mock_get(*args, **kwargs):
|
||||||
@@ -181,6 +180,7 @@ This mock can be shared across tests using a ``fixture``:
|
|||||||
# app.py that includes the get_json() function
|
# app.py that includes the get_json() function
|
||||||
import app
|
import app
|
||||||
|
|
||||||
|
|
||||||
# custom class to be the mock return value of requests.get()
|
# custom class to be the mock return value of requests.get()
|
||||||
class MockResponse:
|
class MockResponse:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -358,7 +358,6 @@ For testing purposes we can patch the ``DEFAULT_CONFIG`` dictionary to specific
|
|||||||
|
|
||||||
|
|
||||||
def test_connection(monkeypatch):
|
def test_connection(monkeypatch):
|
||||||
|
|
||||||
# Patch the values of DEFAULT_CONFIG to specific
|
# Patch the values of DEFAULT_CONFIG to specific
|
||||||
# testing values only for this test.
|
# testing values only for this test.
|
||||||
monkeypatch.setitem(app.DEFAULT_CONFIG, "user", "test_user")
|
monkeypatch.setitem(app.DEFAULT_CONFIG, "user", "test_user")
|
||||||
@@ -383,7 +382,6 @@ You can use the :py:meth:`monkeypatch.delitem <MonkeyPatch.delitem>` to remove v
|
|||||||
|
|
||||||
|
|
||||||
def test_missing_user(monkeypatch):
|
def test_missing_user(monkeypatch):
|
||||||
|
|
||||||
# patch the DEFAULT_CONFIG t be missing the 'user' key
|
# patch the DEFAULT_CONFIG t be missing the 'user' key
|
||||||
monkeypatch.delitem(app.DEFAULT_CONFIG, "user", raising=False)
|
monkeypatch.delitem(app.DEFAULT_CONFIG, "user", raising=False)
|
||||||
|
|
||||||
@@ -404,6 +402,7 @@ separate fixtures for each potential mock and reference them in the needed tests
|
|||||||
# app.py with the connection string function
|
# app.py with the connection string function
|
||||||
import app
|
import app
|
||||||
|
|
||||||
|
|
||||||
# all of the mocks are moved into separated fixtures
|
# all of the mocks are moved into separated fixtures
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_test_user(monkeypatch):
|
def mock_test_user(monkeypatch):
|
||||||
@@ -425,7 +424,6 @@ separate fixtures for each potential mock and reference them in the needed tests
|
|||||||
|
|
||||||
# tests reference only the fixture mocks that are needed
|
# tests reference only the fixture mocks that are needed
|
||||||
def test_connection(mock_test_user, mock_test_database):
|
def test_connection(mock_test_user, mock_test_database):
|
||||||
|
|
||||||
expected = "User Id=test_user; Location=test_db;"
|
expected = "User Id=test_user; Location=test_db;"
|
||||||
|
|
||||||
result = app.create_connection_string()
|
result = app.create_connection_string()
|
||||||
@@ -433,7 +431,6 @@ separate fixtures for each potential mock and reference them in the needed tests
|
|||||||
|
|
||||||
|
|
||||||
def test_missing_user(mock_missing_default_user):
|
def test_missing_user(mock_missing_default_user):
|
||||||
|
|
||||||
with pytest.raises(KeyError):
|
with pytest.raises(KeyError):
|
||||||
_ = app.create_connection_string()
|
_ = app.create_connection_string()
|
||||||
|
|
||||||
|
|||||||
@@ -167,9 +167,9 @@ Now we can increase pytest's verbosity:
|
|||||||
E Right contains 4 more items:
|
E Right contains 4 more items:
|
||||||
E {'10': 10, '20': 20, '30': 30, '40': 40}
|
E {'10': 10, '20': 20, '30': 30, '40': 40}
|
||||||
E Full diff:
|
E Full diff:
|
||||||
E - {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}...
|
E - {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}
|
||||||
E
|
E ? - - - - - - - -
|
||||||
E ...Full output truncated (3 lines hidden), use '-vv' to show
|
E + {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4}
|
||||||
|
|
||||||
test_verbosity_example.py:14: AssertionError
|
test_verbosity_example.py:14: AssertionError
|
||||||
___________________________ test_long_text_fail ____________________________
|
___________________________ test_long_text_fail ____________________________
|
||||||
|
|||||||
@@ -131,10 +131,12 @@ The default base temporary directory
|
|||||||
|
|
||||||
Temporary directories are by default created as sub-directories of
|
Temporary directories are by default created as sub-directories of
|
||||||
the system temporary directory. The base name will be ``pytest-NUM`` where
|
the system temporary directory. The base name will be ``pytest-NUM`` where
|
||||||
``NUM`` will be incremented with each test run. Moreover, entries older
|
``NUM`` will be incremented with each test run.
|
||||||
than 3 temporary directories will be removed.
|
By default, entries older than 3 temporary directories will be removed.
|
||||||
|
This behavior can be configured with :confval:`tmp_path_retention_count` and
|
||||||
|
:confval:`tmp_path_retention_policy`.
|
||||||
|
|
||||||
The number of entries currently cannot be changed, but using the ``--basetemp``
|
Using the ``--basetemp``
|
||||||
option will remove the directory before every run, effectively meaning the temporary directories
|
option will remove the directory before every run, effectively meaning the temporary directories
|
||||||
of only the most recent run will be kept.
|
of only the most recent run will be kept.
|
||||||
|
|
||||||
|
|||||||
@@ -35,11 +35,12 @@ Pytest supports several ways to run and select tests from the command-line.
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
pytest -k "MyClass and not method"
|
pytest -k 'MyClass and not method'
|
||||||
|
|
||||||
This will run tests which contain names that match the given *string expression* (case-insensitive),
|
This will run tests which contain names that match the given *string expression* (case-insensitive),
|
||||||
which can include Python operators that use filenames, class names and function names as variables.
|
which can include Python operators that use filenames, class names and function names as variables.
|
||||||
The example above will run ``TestMyClass.test_something`` but not ``TestMyClass.test_method_simple``.
|
The example above will run ``TestMyClass.test_something`` but not ``TestMyClass.test_method_simple``.
|
||||||
|
Use ``""`` instead of ``''`` in expression when running this on Windows
|
||||||
|
|
||||||
.. _nodeids:
|
.. _nodeids:
|
||||||
|
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ and use pytest_addoption as follows:
|
|||||||
|
|
||||||
# contents of hooks.py
|
# contents of hooks.py
|
||||||
|
|
||||||
|
|
||||||
# Use firstresult=True because we only want one plugin to define this
|
# Use firstresult=True because we only want one plugin to define this
|
||||||
# default value
|
# default value
|
||||||
@hookspec(firstresult=True)
|
@hookspec(firstresult=True)
|
||||||
|
|||||||
@@ -167,13 +167,8 @@ it in your ``pyproject.toml`` file.
|
|||||||
"Framework :: Pytest",
|
"Framework :: Pytest",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools]
|
[project.entry-points.pytest11]
|
||||||
packages = ["myproject"]
|
myproject = "myproject.pluginmodule"
|
||||||
|
|
||||||
[project.entry_points]
|
|
||||||
pytest11 = [
|
|
||||||
"myproject = myproject.pluginmodule",
|
|
||||||
]
|
|
||||||
|
|
||||||
If a package is installed this way, ``pytest`` will load
|
If a package is installed this way, ``pytest`` will load
|
||||||
``myproject.pluginmodule`` as a plugin which can define
|
``myproject.pluginmodule`` as a plugin which can define
|
||||||
@@ -454,7 +449,8 @@ in our ``pytest.ini`` to tell pytest where to look for example files.
|
|||||||
$ pytest
|
$ pytest
|
||||||
=========================== test session starts ============================
|
=========================== test session starts ============================
|
||||||
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
|
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
|
||||||
rootdir: /home/sweet/project, configfile: pytest.ini
|
rootdir: /home/sweet/project
|
||||||
|
configfile: pytest.ini
|
||||||
collected 2 items
|
collected 2 items
|
||||||
|
|
||||||
test_example.py .. [100%]
|
test_example.py .. [100%]
|
||||||
|
|||||||
+1
-2
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
.. sidebar:: Next Open Trainings
|
.. sidebar:: Next Open Trainings
|
||||||
|
|
||||||
- Professionelles Testen für Python mit pytest, part of `enterPy <https://www.enterpy.de/>`__ (German), `October 28th <https://www.enterpy.de/veranstaltung-15409-se-0-professionelles-testen-fuer-python-mit-pytest.html>`__ (sold out) and `November 4th <https://www.enterpy.de/veranstaltung-15557-se-0-professionelles-testen-fuer-python-mit-pytest-zusatztermin.html>`__, online
|
- `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/Remote
|
||||||
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, March 7th to 9th 2023 (3 day in-depth training), Remote and Leipzig, Germany
|
|
||||||
|
|
||||||
Also see :doc:`previous talks and blogposts <talks>`.
|
Also see :doc:`previous talks and blogposts <talks>`.
|
||||||
|
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ For example:
|
|||||||
|
|
||||||
.. literalinclude:: /example/fixtures/test_fixtures_order_dependencies.py
|
.. literalinclude:: /example/fixtures/test_fixtures_order_dependencies.py
|
||||||
|
|
||||||
If we map out what depends on what, we get something that look like this:
|
If we map out what depends on what, we get something that looks like this:
|
||||||
|
|
||||||
.. image:: /example/fixtures/test_fixtures_order_dependencies.*
|
.. image:: /example/fixtures/test_fixtures_order_dependencies.*
|
||||||
:align: center
|
:align: center
|
||||||
|
|||||||
+2654
-1492
File diff suppressed because it is too large
Load Diff
@@ -956,6 +956,12 @@ TestReport
|
|||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
:inherited-members:
|
:inherited-members:
|
||||||
|
|
||||||
|
TestShortLogReport
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. autoclass:: pytest.TestShortLogReport()
|
||||||
|
:members:
|
||||||
|
|
||||||
_Result
|
_Result
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
@@ -1047,6 +1053,14 @@ Environment Variables
|
|||||||
|
|
||||||
Environment variables that can be used to change pytest's behavior.
|
Environment variables that can be used to change pytest's behavior.
|
||||||
|
|
||||||
|
.. envvar:: CI
|
||||||
|
|
||||||
|
When set (regardless of value), pytest acknowledges that is running in a CI process. Alternative to ``BUILD_NUMBER`` variable.
|
||||||
|
|
||||||
|
.. envvar:: BUILD_NUMBER
|
||||||
|
|
||||||
|
When set (regardless of value), pytest acknowledges that is running in a CI process. Alternative to CI variable.
|
||||||
|
|
||||||
.. envvar:: PYTEST_ADDOPTS
|
.. envvar:: PYTEST_ADDOPTS
|
||||||
|
|
||||||
This contains a command-line (parsed by the py:mod:`shlex` module) that will be **prepended** to the command line given
|
This contains a command-line (parsed by the py:mod:`shlex` module) that will be **prepended** to the command line given
|
||||||
@@ -1212,6 +1226,7 @@ passed multiple times. The expected format is ``name=value``. For example::
|
|||||||
|
|
||||||
* ``classic``: classic pytest output.
|
* ``classic``: classic pytest output.
|
||||||
* ``progress``: like classic pytest output, but with a progress indicator.
|
* ``progress``: like classic pytest output, but with a progress indicator.
|
||||||
|
* ``progress-even-when-capture-no``: allows the use of the progress indicator even when ``capture=no``.
|
||||||
* ``count``: like progress, but shows progress as the number of tests completed instead of a percent.
|
* ``count``: like progress, but shows progress as the number of tests completed instead of a percent.
|
||||||
|
|
||||||
The default is ``progress``, but you can fallback to ``classic`` if you prefer or
|
The default is ``progress``, but you can fallback to ``classic`` if you prefer or
|
||||||
@@ -1704,13 +1719,12 @@ passed multiple times. The expected format is ``name=value``. For example::
|
|||||||
|
|
||||||
.. confval:: testpaths
|
.. confval:: testpaths
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Sets list of directories that should be searched for tests when
|
Sets list of directories that should be searched for tests when
|
||||||
no specific directories, files or test ids are given in the command line when
|
no specific directories, files or test ids are given in the command line when
|
||||||
executing pytest from the :ref:`rootdir <rootdir>` directory.
|
executing pytest from the :ref:`rootdir <rootdir>` directory.
|
||||||
File system paths may use shell-style wildcards, including the recursive
|
File system paths may use shell-style wildcards, including the recursive
|
||||||
``**`` pattern.
|
``**`` pattern.
|
||||||
|
|
||||||
Useful when all project tests are in a known location to speed up
|
Useful when all project tests are in a known location to speed up
|
||||||
test collection and to avoid picking up undesired tests by accident.
|
test collection and to avoid picking up undesired tests by accident.
|
||||||
|
|
||||||
@@ -1719,8 +1733,51 @@ passed multiple times. The expected format is ``name=value``. For example::
|
|||||||
[pytest]
|
[pytest]
|
||||||
testpaths = testing doc
|
testpaths = testing doc
|
||||||
|
|
||||||
This tells pytest to only look for tests in ``testing`` and ``doc``
|
This configuration means that executing:
|
||||||
directories when executing from the root directory.
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
pytest
|
||||||
|
|
||||||
|
has the same practical effects as executing:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
pytest testing doc
|
||||||
|
|
||||||
|
|
||||||
|
.. confval:: tmp_path_retention_count
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
How many sessions should we keep the `tmp_path` directories,
|
||||||
|
according to `tmp_path_retention_policy`.
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[pytest]
|
||||||
|
tmp_path_retention_count = 3
|
||||||
|
|
||||||
|
Default: ``3``
|
||||||
|
|
||||||
|
|
||||||
|
.. confval:: tmp_path_retention_policy
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Controls which directories created by the `tmp_path` fixture are kept around,
|
||||||
|
based on test outcome.
|
||||||
|
|
||||||
|
* `all`: retains directories for all tests, regardless of the outcome.
|
||||||
|
* `failed`: retains directories only for tests with outcome `error` or `failed`.
|
||||||
|
* `none`: directories are always removed after each test ends, regardless of the outcome.
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[pytest]
|
||||||
|
tmp_path_retention_policy = "all"
|
||||||
|
|
||||||
|
Default: ``all``
|
||||||
|
|
||||||
|
|
||||||
.. confval:: usefixtures
|
.. confval:: usefixtures
|
||||||
@@ -1861,7 +1918,8 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
|||||||
--strict-markers Markers not registered in the `markers` section of
|
--strict-markers Markers not registered in the `markers` section of
|
||||||
the configuration file raise errors
|
the configuration file raise errors
|
||||||
--strict (Deprecated) alias to --strict-markers
|
--strict (Deprecated) alias to --strict-markers
|
||||||
-c file Load configuration from `file` instead of trying to
|
-c, --config-file FILE
|
||||||
|
Load configuration from `FILE` instead of trying to
|
||||||
locate one of the implicit configuration files
|
locate one of the implicit configuration files
|
||||||
--continue-on-collection-errors
|
--continue-on-collection-errors
|
||||||
Force test execution even if collection errors occur
|
Force test execution even if collection errors occur
|
||||||
@@ -1952,8 +2010,11 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
|||||||
--log-auto-indent=LOG_AUTO_INDENT
|
--log-auto-indent=LOG_AUTO_INDENT
|
||||||
Auto-indent multiline messages passed to the logging
|
Auto-indent multiline messages passed to the logging
|
||||||
module. Accepts true|on, false|off or an integer.
|
module. Accepts true|on, false|off or an integer.
|
||||||
|
--log-disable=LOGGER_DISABLE
|
||||||
|
Disable a logger by name. Can be passed multiple
|
||||||
|
times.
|
||||||
|
|
||||||
[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg file found:
|
[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:
|
||||||
|
|
||||||
markers (linelist): Markers for test functions
|
markers (linelist): Markers for test functions
|
||||||
empty_parameter_set_mark (string):
|
empty_parameter_set_mark (string):
|
||||||
@@ -1981,9 +2042,18 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
|||||||
console_output_style (string):
|
console_output_style (string):
|
||||||
Console output: "classic", or with additional
|
Console output: "classic", or with additional
|
||||||
progress information ("progress" (percentage) |
|
progress information ("progress" (percentage) |
|
||||||
"count")
|
"count" | "progress-even-when-capture-no" (forces
|
||||||
|
progress even when capture=no)
|
||||||
xfail_strict (bool): Default for the strict parameter of xfail markers
|
xfail_strict (bool): Default for the strict parameter of xfail markers
|
||||||
when not given explicitly (default: False)
|
when not given explicitly (default: False)
|
||||||
|
tmp_path_retention_count (string):
|
||||||
|
How many sessions should we keep the `tmp_path`
|
||||||
|
directories, according to
|
||||||
|
`tmp_path_retention_policy`.
|
||||||
|
tmp_path_retention_policy (string):
|
||||||
|
Controls which directories created by the `tmp_path`
|
||||||
|
fixture are kept around, based on test outcome.
|
||||||
|
(all/failed/none)
|
||||||
enable_assertion_pass_hook (bool):
|
enable_assertion_pass_hook (bool):
|
||||||
Enables the pytest_assertion_pass hook. Make sure to
|
Enables the pytest_assertion_pass hook. Make sure to
|
||||||
delete any previously generated pyc cache files.
|
delete any previously generated pyc cache files.
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
pallets-sphinx-themes
|
pallets-sphinx-themes
|
||||||
pluggy>=1.0
|
pluggy>=1.0
|
||||||
pygments-pytest>=2.2.0
|
pygments-pytest>=2.3.0
|
||||||
sphinx-removed-in>=0.2.0
|
sphinx-removed-in>=0.2.0
|
||||||
sphinx>=5,<6
|
sphinx>=5,<6
|
||||||
sphinxcontrib-trio
|
sphinxcontrib-trio
|
||||||
sphinxcontrib-svg2pdfconverter
|
sphinxcontrib-svg2pdfconverter
|
||||||
|
# Pin packaging because it no longer handles 'latest' version, which
|
||||||
|
# is the version that is assigned to the docs.
|
||||||
|
# See https://github.com/pytest-dev/pytest/pull/10578#issuecomment-1348249045.
|
||||||
|
packaging <22
|
||||||
|
|||||||
@@ -114,3 +114,8 @@ template = "changelog/_template.rst"
|
|||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
target-version = ['py37']
|
target-version = ['py37']
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ Plugin List
|
|||||||
===========
|
===========
|
||||||
|
|
||||||
PyPI projects that match "pytest-\*" are considered plugins and are listed
|
PyPI projects that match "pytest-\*" are considered plugins and are listed
|
||||||
automatically. Packages classified as inactive are excluded.
|
automatically together with a manually-maintained list in `the source
|
||||||
|
code <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
|
||||||
|
Packages classified as inactive are excluded.
|
||||||
|
|
||||||
.. The following conditional uses a different format for this list when
|
.. The following conditional uses a different format for this list when
|
||||||
creating a PDF, because otherwise the table gets far too wide for the
|
creating a PDF, because otherwise the table gets far too wide for the
|
||||||
@@ -33,6 +35,9 @@ DEVELOPMENT_STATUS_CLASSIFIERS = (
|
|||||||
"Development Status :: 6 - Mature",
|
"Development Status :: 6 - Mature",
|
||||||
"Development Status :: 7 - Inactive",
|
"Development Status :: 7 - Inactive",
|
||||||
)
|
)
|
||||||
|
ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins
|
||||||
|
"logassert",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def escape_rst(text: str) -> str:
|
def escape_rst(text: str) -> str:
|
||||||
@@ -52,18 +57,18 @@ def iter_plugins():
|
|||||||
regex = r">([\d\w-]*)</a>"
|
regex = r">([\d\w-]*)</a>"
|
||||||
response = requests.get("https://pypi.org/simple")
|
response = requests.get("https://pypi.org/simple")
|
||||||
|
|
||||||
matches = list(
|
match_names = (match.groups()[0] for match in re.finditer(regex, response.text))
|
||||||
match
|
plugin_names = [
|
||||||
for match in re.finditer(regex, response.text)
|
name
|
||||||
if match.groups()[0].startswith("pytest-")
|
for name in match_names
|
||||||
)
|
if name.startswith("pytest-") or name in ADDITIONAL_PROJECTS
|
||||||
|
]
|
||||||
|
|
||||||
for match in tqdm(matches, smoothing=0):
|
for name in tqdm(plugin_names, smoothing=0):
|
||||||
name = match.groups()[0]
|
|
||||||
response = requests.get(f"https://pypi.org/pypi/{name}/json")
|
response = requests.get(f"https://pypi.org/pypi/{name}/json")
|
||||||
if response.status_code == 404:
|
if response.status_code == 404:
|
||||||
# Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple but
|
# Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple
|
||||||
# return 404 on the JSON API. Skip.
|
# but return 404 on the JSON API. Skip.
|
||||||
continue
|
continue
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
info = response.json()["info"]
|
info = response.json()["info"]
|
||||||
@@ -78,11 +83,23 @@ def iter_plugins():
|
|||||||
requires = "N/A"
|
requires = "N/A"
|
||||||
if info["requires_dist"]:
|
if info["requires_dist"]:
|
||||||
for requirement in info["requires_dist"]:
|
for requirement in info["requires_dist"]:
|
||||||
if requirement == "pytest" or "pytest " in requirement:
|
if re.match(r"pytest(?![-.\w])", requirement):
|
||||||
requires = requirement
|
requires = requirement
|
||||||
break
|
break
|
||||||
|
|
||||||
|
def version_sort_key(version_string):
|
||||||
|
"""
|
||||||
|
Return the sort key for the given version string
|
||||||
|
returned by the API.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return packaging.version.parse(version_string)
|
||||||
|
except packaging.version.InvalidVersion:
|
||||||
|
# Use a hard-coded pre-release version.
|
||||||
|
return packaging.version.Version("0.0.0alpha")
|
||||||
|
|
||||||
releases = response.json()["releases"]
|
releases = response.json()["releases"]
|
||||||
for release in sorted(releases, key=packaging.version.parse, reverse=True):
|
for release in sorted(releases, key=version_sort_key, reverse=True):
|
||||||
if releases[release]:
|
if releases[release]:
|
||||||
release_date = datetime.date.fromisoformat(
|
release_date = datetime.date.fromisoformat(
|
||||||
releases[release][-1]["upload_time_iso_8601"].split("T")[0]
|
releases[release][-1]["upload_time_iso_8601"].split("T")[0]
|
||||||
@@ -90,7 +107,9 @@ def iter_plugins():
|
|||||||
last_release = release_date.strftime("%b %d, %Y")
|
last_release = release_date.strftime("%b %d, %Y")
|
||||||
break
|
break
|
||||||
name = f':pypi:`{info["name"]}`'
|
name = f':pypi:`{info["name"]}`'
|
||||||
summary = escape_rst(info["summary"].replace("\n", ""))
|
summary = ""
|
||||||
|
if info["summary"]:
|
||||||
|
summary = escape_rst(info["summary"].replace("\n", ""))
|
||||||
yield {
|
yield {
|
||||||
"name": name,
|
"name": name,
|
||||||
"summary": summary.strip(),
|
"summary": summary.strip(),
|
||||||
@@ -122,7 +141,7 @@ def main():
|
|||||||
reference_dir = pathlib.Path("doc", "en", "reference")
|
reference_dir = pathlib.Path("doc", "en", "reference")
|
||||||
|
|
||||||
plugin_list = reference_dir / "plugin_list.rst"
|
plugin_list = reference_dir / "plugin_list.rst"
|
||||||
with plugin_list.open("w") as f:
|
with plugin_list.open("w", encoding="UTF-8") as f:
|
||||||
f.write(FILE_HEAD)
|
f.write(FILE_HEAD)
|
||||||
f.write(f"This list contains {len(plugins)} plugins.\n\n")
|
f.write(f"This list contains {len(plugins)} plugins.\n\n")
|
||||||
f.write(".. only:: not latex\n\n")
|
f.write(".. only:: not latex\n\n")
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ long_description_content_type = text/x-rst
|
|||||||
url = https://docs.pytest.org/en/latest/
|
url = https://docs.pytest.org/en/latest/
|
||||||
author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others
|
author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others
|
||||||
license = MIT
|
license = MIT
|
||||||
license_file = LICENSE
|
license_files = LICENSE
|
||||||
platforms = unix, linux, osx, cygwin, win32
|
platforms = unix, linux, osx, cygwin, win32
|
||||||
classifiers =
|
classifiers =
|
||||||
Development Status :: 6 - Mature
|
Development Status :: 6 - Mature
|
||||||
@@ -21,6 +21,8 @@ classifiers =
|
|||||||
Programming Language :: Python :: 3.8
|
Programming Language :: Python :: 3.8
|
||||||
Programming Language :: Python :: 3.9
|
Programming Language :: Python :: 3.9
|
||||||
Programming Language :: Python :: 3.10
|
Programming Language :: Python :: 3.10
|
||||||
|
Programming Language :: Python :: 3.11
|
||||||
|
Programming Language :: Python :: 3.12
|
||||||
Topic :: Software Development :: Libraries
|
Topic :: Software Development :: Libraries
|
||||||
Topic :: Software Development :: Testing
|
Topic :: Software Development :: Testing
|
||||||
Topic :: Utilities
|
Topic :: Utilities
|
||||||
@@ -43,7 +45,6 @@ packages =
|
|||||||
pytest
|
pytest
|
||||||
py_modules = py
|
py_modules = py
|
||||||
install_requires =
|
install_requires =
|
||||||
attrs>=19.2.0
|
|
||||||
iniconfig
|
iniconfig
|
||||||
packaging
|
packaging
|
||||||
pluggy>=0.12,<2.0
|
pluggy>=0.12,<2.0
|
||||||
@@ -67,11 +68,13 @@ console_scripts =
|
|||||||
[options.extras_require]
|
[options.extras_require]
|
||||||
testing =
|
testing =
|
||||||
argcomplete
|
argcomplete
|
||||||
|
attrs>=19.2.0
|
||||||
hypothesis>=3.56
|
hypothesis>=3.56
|
||||||
mock
|
mock
|
||||||
nose
|
nose
|
||||||
pygments>=2.7.2
|
pygments>=2.7.2
|
||||||
requests
|
requests
|
||||||
|
setuptools
|
||||||
xmlschema
|
xmlschema
|
||||||
|
|
||||||
[options.package_data]
|
[options.package_data]
|
||||||
@@ -95,7 +98,6 @@ mypy_path = src
|
|||||||
check_untyped_defs = True
|
check_untyped_defs = True
|
||||||
disallow_any_generics = True
|
disallow_any_generics = True
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
no_implicit_optional = True
|
|
||||||
show_error_codes = True
|
show_error_codes = True
|
||||||
strict_equality = True
|
strict_equality = True
|
||||||
warn_redundant_casts = True
|
warn_redundant_casts = True
|
||||||
|
|||||||
@@ -78,15 +78,15 @@ class FastFilesCompleter:
|
|||||||
|
|
||||||
def __call__(self, prefix: str, **kwargs: Any) -> List[str]:
|
def __call__(self, prefix: str, **kwargs: Any) -> List[str]:
|
||||||
# Only called on non option completions.
|
# Only called on non option completions.
|
||||||
if os.path.sep in prefix[1:]:
|
if os.sep in prefix[1:]:
|
||||||
prefix_dir = len(os.path.dirname(prefix) + os.path.sep)
|
prefix_dir = len(os.path.dirname(prefix) + os.sep)
|
||||||
else:
|
else:
|
||||||
prefix_dir = 0
|
prefix_dir = 0
|
||||||
completion = []
|
completion = []
|
||||||
globbed = []
|
globbed = []
|
||||||
if "*" not in prefix and "?" not in prefix:
|
if "*" not in prefix and "?" not in prefix:
|
||||||
# We are on unix, otherwise no bash.
|
# We are on unix, otherwise no bash.
|
||||||
if not prefix or prefix[-1] == os.path.sep:
|
if not prefix or prefix[-1] == os.sep:
|
||||||
globbed.extend(glob(prefix + ".*"))
|
globbed.extend(glob(prefix + ".*"))
|
||||||
prefix += "*"
|
prefix += "*"
|
||||||
globbed.extend(glob(prefix))
|
globbed.extend(glob(prefix))
|
||||||
|
|||||||
+156
-111
@@ -1,4 +1,5 @@
|
|||||||
import ast
|
import ast
|
||||||
|
import dataclasses
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -30,9 +31,7 @@ from typing import Type
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
from typing import Union
|
from typing import Union
|
||||||
from weakref import ref
|
|
||||||
|
|
||||||
import attr
|
|
||||||
import pluggy
|
import pluggy
|
||||||
|
|
||||||
import _pytest
|
import _pytest
|
||||||
@@ -50,9 +49,9 @@ from _pytest.pathlib import absolutepath
|
|||||||
from _pytest.pathlib import bestrelpath
|
from _pytest.pathlib import bestrelpath
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from typing_extensions import Final
|
||||||
from typing_extensions import Literal
|
from typing_extensions import Literal
|
||||||
from typing_extensions import SupportsIndex
|
from typing_extensions import SupportsIndex
|
||||||
from weakref import ReferenceType
|
|
||||||
|
|
||||||
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
|
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
|
||||||
|
|
||||||
@@ -194,25 +193,25 @@ class Frame:
|
|||||||
class TracebackEntry:
|
class TracebackEntry:
|
||||||
"""A single entry in a Traceback."""
|
"""A single entry in a Traceback."""
|
||||||
|
|
||||||
__slots__ = ("_rawentry", "_excinfo", "_repr_style")
|
__slots__ = ("_rawentry", "_repr_style")
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
rawentry: TracebackType,
|
rawentry: TracebackType,
|
||||||
excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,
|
repr_style: Optional['Literal["short", "long"]'] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._rawentry = rawentry
|
self._rawentry: "Final" = rawentry
|
||||||
self._excinfo = excinfo
|
self._repr_style: "Final" = repr_style
|
||||||
self._repr_style: Optional['Literal["short", "long"]'] = None
|
|
||||||
|
def with_repr_style(
|
||||||
|
self, repr_style: Optional['Literal["short", "long"]']
|
||||||
|
) -> "TracebackEntry":
|
||||||
|
return TracebackEntry(self._rawentry, repr_style)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def lineno(self) -> int:
|
def lineno(self) -> int:
|
||||||
return self._rawentry.tb_lineno - 1
|
return self._rawentry.tb_lineno - 1
|
||||||
|
|
||||||
def set_repr_style(self, mode: "Literal['short', 'long']") -> None:
|
|
||||||
assert mode in ("short", "long")
|
|
||||||
self._repr_style = mode
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def frame(self) -> Frame:
|
def frame(self) -> Frame:
|
||||||
return Frame(self._rawentry.tb_frame)
|
return Frame(self._rawentry.tb_frame)
|
||||||
@@ -272,7 +271,7 @@ class TracebackEntry:
|
|||||||
|
|
||||||
source = property(getsource)
|
source = property(getsource)
|
||||||
|
|
||||||
def ishidden(self) -> bool:
|
def ishidden(self, excinfo: Optional["ExceptionInfo[BaseException]"]) -> bool:
|
||||||
"""Return True if the current frame has a var __tracebackhide__
|
"""Return True if the current frame has a var __tracebackhide__
|
||||||
resolving to True.
|
resolving to True.
|
||||||
|
|
||||||
@@ -296,7 +295,7 @@ class TracebackEntry:
|
|||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
if tbh and callable(tbh):
|
if tbh and callable(tbh):
|
||||||
return tbh(None if self._excinfo is None else self._excinfo())
|
return tbh(excinfo)
|
||||||
return tbh
|
return tbh
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@@ -329,16 +328,14 @@ class Traceback(List[TracebackEntry]):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
tb: Union[TracebackType, Iterable[TracebackEntry]],
|
tb: Union[TracebackType, Iterable[TracebackEntry]],
|
||||||
excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize from given python traceback object and ExceptionInfo."""
|
"""Initialize from given python traceback object and ExceptionInfo."""
|
||||||
self._excinfo = excinfo
|
|
||||||
if isinstance(tb, TracebackType):
|
if isinstance(tb, TracebackType):
|
||||||
|
|
||||||
def f(cur: TracebackType) -> Iterable[TracebackEntry]:
|
def f(cur: TracebackType) -> Iterable[TracebackEntry]:
|
||||||
cur_: Optional[TracebackType] = cur
|
cur_: Optional[TracebackType] = cur
|
||||||
while cur_ is not None:
|
while cur_ is not None:
|
||||||
yield TracebackEntry(cur_, excinfo=excinfo)
|
yield TracebackEntry(cur_)
|
||||||
cur_ = cur_.tb_next
|
cur_ = cur_.tb_next
|
||||||
|
|
||||||
super().__init__(f(tb))
|
super().__init__(f(tb))
|
||||||
@@ -378,7 +375,7 @@ class Traceback(List[TracebackEntry]):
|
|||||||
continue
|
continue
|
||||||
if firstlineno is not None and x.frame.code.firstlineno != firstlineno:
|
if firstlineno is not None and x.frame.code.firstlineno != firstlineno:
|
||||||
continue
|
continue
|
||||||
return Traceback(x._rawentry, self._excinfo)
|
return Traceback(x._rawentry)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
@@ -398,26 +395,27 @@ class Traceback(List[TracebackEntry]):
|
|||||||
return super().__getitem__(key)
|
return super().__getitem__(key)
|
||||||
|
|
||||||
def filter(
|
def filter(
|
||||||
self, fn: Callable[[TracebackEntry], bool] = lambda x: not x.ishidden()
|
self,
|
||||||
|
# TODO(py38): change to positional only.
|
||||||
|
_excinfo_or_fn: Union[
|
||||||
|
"ExceptionInfo[BaseException]",
|
||||||
|
Callable[[TracebackEntry], bool],
|
||||||
|
],
|
||||||
) -> "Traceback":
|
) -> "Traceback":
|
||||||
"""Return a Traceback instance with certain items removed
|
"""Return a Traceback instance with certain items removed.
|
||||||
|
|
||||||
fn is a function that gets a single argument, a TracebackEntry
|
If the filter is an `ExceptionInfo`, removes all the ``TracebackEntry``s
|
||||||
instance, and should return True when the item should be added
|
which are hidden (see ishidden() above).
|
||||||
to the Traceback, False when not.
|
|
||||||
|
|
||||||
By default this removes all the TracebackEntries which are hidden
|
Otherwise, the filter is a function that gets a single argument, a
|
||||||
(see ishidden() above).
|
``TracebackEntry`` instance, and should return True when the item should
|
||||||
|
be added to the ``Traceback``, False when not.
|
||||||
"""
|
"""
|
||||||
return Traceback(filter(fn, self), self._excinfo)
|
if isinstance(_excinfo_or_fn, ExceptionInfo):
|
||||||
|
fn = lambda x: not x.ishidden(_excinfo_or_fn) # noqa: E731
|
||||||
def getcrashentry(self) -> TracebackEntry:
|
else:
|
||||||
"""Return last non-hidden traceback entry that lead to the exception of a traceback."""
|
fn = _excinfo_or_fn
|
||||||
for i in range(-1, -len(self) - 1, -1):
|
return Traceback(filter(fn, self))
|
||||||
entry = self[i]
|
|
||||||
if not entry.ishidden():
|
|
||||||
return entry
|
|
||||||
return self[-1]
|
|
||||||
|
|
||||||
def recursionindex(self) -> Optional[int]:
|
def recursionindex(self) -> Optional[int]:
|
||||||
"""Return the index of the frame/TracebackEntry where recursion originates if
|
"""Return the index of the frame/TracebackEntry where recursion originates if
|
||||||
@@ -445,7 +443,7 @@ E = TypeVar("E", bound=BaseException, covariant=True)
|
|||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@attr.s(repr=False, init=False, auto_attribs=True)
|
@dataclasses.dataclass
|
||||||
class ExceptionInfo(Generic[E]):
|
class ExceptionInfo(Generic[E]):
|
||||||
"""Wraps sys.exc_info() objects and offers help for navigating the traceback."""
|
"""Wraps sys.exc_info() objects and offers help for navigating the traceback."""
|
||||||
|
|
||||||
@@ -469,22 +467,41 @@ class ExceptionInfo(Generic[E]):
|
|||||||
self._traceback = traceback
|
self._traceback = traceback
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_exc_info(
|
def from_exception(
|
||||||
cls,
|
cls,
|
||||||
exc_info: Tuple[Type[E], E, TracebackType],
|
# Ignoring error: "Cannot use a covariant type variable as a parameter".
|
||||||
|
# This is OK to ignore because this class is (conceptually) readonly.
|
||||||
|
# See https://github.com/python/mypy/issues/7049.
|
||||||
|
exception: E, # type: ignore[misc]
|
||||||
exprinfo: Optional[str] = None,
|
exprinfo: Optional[str] = None,
|
||||||
) -> "ExceptionInfo[E]":
|
) -> "ExceptionInfo[E]":
|
||||||
"""Return an ExceptionInfo for an existing exc_info tuple.
|
"""Return an ExceptionInfo for an existing exception.
|
||||||
|
|
||||||
.. warning::
|
The exception must have a non-``None`` ``__traceback__`` attribute,
|
||||||
|
otherwise this function fails with an assertion error. This means that
|
||||||
Experimental API
|
the exception must have been raised, or added a traceback with the
|
||||||
|
:py:meth:`~BaseException.with_traceback()` method.
|
||||||
|
|
||||||
:param exprinfo:
|
:param exprinfo:
|
||||||
A text string helping to determine if we should strip
|
A text string helping to determine if we should strip
|
||||||
``AssertionError`` from the output. Defaults to the exception
|
``AssertionError`` from the output. Defaults to the exception
|
||||||
message/``__str__()``.
|
message/``__str__()``.
|
||||||
|
|
||||||
|
.. versionadded:: 7.4
|
||||||
"""
|
"""
|
||||||
|
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)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_exc_info(
|
||||||
|
cls,
|
||||||
|
exc_info: Tuple[Type[E], E, TracebackType],
|
||||||
|
exprinfo: Optional[str] = None,
|
||||||
|
) -> "ExceptionInfo[E]":
|
||||||
|
"""Like :func:`from_exception`, but using old-style exc_info tuple."""
|
||||||
_striptext = ""
|
_striptext = ""
|
||||||
if exprinfo is None and isinstance(exc_info[1], AssertionError):
|
if exprinfo is None and isinstance(exc_info[1], AssertionError):
|
||||||
exprinfo = getattr(exc_info[1], "msg", None)
|
exprinfo = getattr(exc_info[1], "msg", None)
|
||||||
@@ -563,7 +580,7 @@ class ExceptionInfo(Generic[E]):
|
|||||||
def traceback(self) -> Traceback:
|
def traceback(self) -> Traceback:
|
||||||
"""The traceback."""
|
"""The traceback."""
|
||||||
if self._traceback is None:
|
if self._traceback is None:
|
||||||
self._traceback = Traceback(self.tb, excinfo=ref(self))
|
self._traceback = Traceback(self.tb)
|
||||||
return self._traceback
|
return self._traceback
|
||||||
|
|
||||||
@traceback.setter
|
@traceback.setter
|
||||||
@@ -602,18 +619,25 @@ class ExceptionInfo(Generic[E]):
|
|||||||
"""
|
"""
|
||||||
return isinstance(self.value, exc)
|
return isinstance(self.value, exc)
|
||||||
|
|
||||||
def _getreprcrash(self) -> "ReprFileLocation":
|
def _getreprcrash(self) -> Optional["ReprFileLocation"]:
|
||||||
exconly = self.exconly(tryshort=True)
|
# Find last non-hidden traceback entry that led to the exception of the
|
||||||
entry = self.traceback.getcrashentry()
|
# traceback, or None if all hidden.
|
||||||
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
|
for i in range(-1, -len(self.traceback) - 1, -1):
|
||||||
return ReprFileLocation(path, lineno + 1, exconly)
|
entry = self.traceback[i]
|
||||||
|
if not entry.ishidden(self):
|
||||||
|
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
|
||||||
|
exconly = self.exconly(tryshort=True)
|
||||||
|
return ReprFileLocation(path, lineno + 1, exconly)
|
||||||
|
return None
|
||||||
|
|
||||||
def getrepr(
|
def getrepr(
|
||||||
self,
|
self,
|
||||||
showlocals: bool = False,
|
showlocals: bool = False,
|
||||||
style: "_TracebackStyle" = "long",
|
style: "_TracebackStyle" = "long",
|
||||||
abspath: bool = False,
|
abspath: bool = False,
|
||||||
tbfilter: bool = True,
|
tbfilter: Union[
|
||||||
|
bool, Callable[["ExceptionInfo[BaseException]"], Traceback]
|
||||||
|
] = True,
|
||||||
funcargs: bool = False,
|
funcargs: bool = False,
|
||||||
truncate_locals: bool = True,
|
truncate_locals: bool = True,
|
||||||
chain: bool = True,
|
chain: bool = True,
|
||||||
@@ -625,14 +649,20 @@ class ExceptionInfo(Generic[E]):
|
|||||||
Ignored if ``style=="native"``.
|
Ignored if ``style=="native"``.
|
||||||
|
|
||||||
:param str style:
|
:param str style:
|
||||||
long|short|no|native|value traceback style.
|
long|short|line|no|native|value traceback style.
|
||||||
|
|
||||||
:param bool abspath:
|
:param bool abspath:
|
||||||
If paths should be changed to absolute or left unchanged.
|
If paths should be changed to absolute or left unchanged.
|
||||||
|
|
||||||
:param bool tbfilter:
|
:param tbfilter:
|
||||||
Hide entries that contain a local variable ``__tracebackhide__==True``.
|
A filter for traceback entries.
|
||||||
Ignored if ``style=="native"``.
|
|
||||||
|
* If false, don't hide any entries.
|
||||||
|
* If true, hide internal entries and entries that contain a local
|
||||||
|
variable ``__tracebackhide__ = True``.
|
||||||
|
* If a callable, delegates the filtering to the callable.
|
||||||
|
|
||||||
|
Ignored if ``style`` is ``"native"``.
|
||||||
|
|
||||||
:param bool funcargs:
|
:param bool funcargs:
|
||||||
Show fixtures ("funcargs" for legacy purposes) per traceback entry.
|
Show fixtures ("funcargs" for legacy purposes) per traceback entry.
|
||||||
@@ -649,12 +679,14 @@ class ExceptionInfo(Generic[E]):
|
|||||||
"""
|
"""
|
||||||
if style == "native":
|
if style == "native":
|
||||||
return ReprExceptionInfo(
|
return ReprExceptionInfo(
|
||||||
ReprTracebackNative(
|
reprtraceback=ReprTracebackNative(
|
||||||
traceback.format_exception(
|
traceback.format_exception(
|
||||||
self.type, self.value, self.traceback[0]._rawentry
|
self.type,
|
||||||
|
self.value,
|
||||||
|
self.traceback[0]._rawentry if self.traceback else None,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
self._getreprcrash(),
|
reprcrash=self._getreprcrash(),
|
||||||
)
|
)
|
||||||
|
|
||||||
fmt = FormattedExcinfo(
|
fmt = FormattedExcinfo(
|
||||||
@@ -684,7 +716,7 @@ class ExceptionInfo(Generic[E]):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@attr.s(auto_attribs=True)
|
@dataclasses.dataclass
|
||||||
class FormattedExcinfo:
|
class FormattedExcinfo:
|
||||||
"""Presenting information about failing Functions and Generators."""
|
"""Presenting information about failing Functions and Generators."""
|
||||||
|
|
||||||
@@ -695,12 +727,12 @@ class FormattedExcinfo:
|
|||||||
showlocals: bool = False
|
showlocals: bool = False
|
||||||
style: "_TracebackStyle" = "long"
|
style: "_TracebackStyle" = "long"
|
||||||
abspath: bool = True
|
abspath: bool = True
|
||||||
tbfilter: bool = True
|
tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True
|
||||||
funcargs: bool = False
|
funcargs: bool = False
|
||||||
truncate_locals: bool = True
|
truncate_locals: bool = True
|
||||||
chain: bool = True
|
chain: bool = True
|
||||||
astcache: Dict[Union[str, Path], ast.AST] = attr.ib(
|
astcache: Dict[Union[str, Path], ast.AST] = dataclasses.field(
|
||||||
factory=dict, init=False, repr=False
|
default_factory=dict, init=False, repr=False
|
||||||
)
|
)
|
||||||
|
|
||||||
def _getindent(self, source: "Source") -> int:
|
def _getindent(self, source: "Source") -> int:
|
||||||
@@ -741,11 +773,13 @@ class FormattedExcinfo:
|
|||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
"""Return formatted and marked up source lines."""
|
"""Return formatted and marked up source lines."""
|
||||||
lines = []
|
lines = []
|
||||||
if source is None or line_index >= len(source.lines):
|
if source is not None and line_index < 0:
|
||||||
|
line_index += len(source)
|
||||||
|
if source is None or line_index >= len(source.lines) or line_index < 0:
|
||||||
|
# `line_index` could still be outside `range(len(source.lines))` if
|
||||||
|
# we're processing AST with pathological position attributes.
|
||||||
source = Source("???")
|
source = Source("???")
|
||||||
line_index = 0
|
line_index = 0
|
||||||
if line_index < 0:
|
|
||||||
line_index += len(source)
|
|
||||||
space_prefix = " "
|
space_prefix = " "
|
||||||
if short:
|
if short:
|
||||||
lines.append(space_prefix + source.lines[line_index].strip())
|
lines.append(space_prefix + source.lines[line_index].strip())
|
||||||
@@ -805,12 +839,16 @@ class FormattedExcinfo:
|
|||||||
|
|
||||||
def repr_traceback_entry(
|
def repr_traceback_entry(
|
||||||
self,
|
self,
|
||||||
entry: TracebackEntry,
|
entry: Optional[TracebackEntry],
|
||||||
excinfo: Optional[ExceptionInfo[BaseException]] = None,
|
excinfo: Optional[ExceptionInfo[BaseException]] = None,
|
||||||
) -> "ReprEntry":
|
) -> "ReprEntry":
|
||||||
lines: List[str] = []
|
lines: List[str] = []
|
||||||
style = entry._repr_style if entry._repr_style is not None else self.style
|
style = (
|
||||||
if style in ("short", "long"):
|
entry._repr_style
|
||||||
|
if entry is not None and entry._repr_style is not None
|
||||||
|
else self.style
|
||||||
|
)
|
||||||
|
if style in ("short", "long") and entry is not None:
|
||||||
source = self._getentrysource(entry)
|
source = self._getentrysource(entry)
|
||||||
if source is None:
|
if source is None:
|
||||||
source = Source("???")
|
source = Source("???")
|
||||||
@@ -851,25 +889,31 @@ class FormattedExcinfo:
|
|||||||
|
|
||||||
def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback":
|
def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback":
|
||||||
traceback = excinfo.traceback
|
traceback = excinfo.traceback
|
||||||
if self.tbfilter:
|
if callable(self.tbfilter):
|
||||||
traceback = traceback.filter()
|
traceback = self.tbfilter(excinfo)
|
||||||
|
elif self.tbfilter:
|
||||||
|
traceback = traceback.filter(excinfo)
|
||||||
|
|
||||||
if isinstance(excinfo.value, RecursionError):
|
if isinstance(excinfo.value, RecursionError):
|
||||||
traceback, extraline = self._truncate_recursive_traceback(traceback)
|
traceback, extraline = self._truncate_recursive_traceback(traceback)
|
||||||
else:
|
else:
|
||||||
extraline = None
|
extraline = None
|
||||||
|
|
||||||
|
if not traceback:
|
||||||
|
if extraline is None:
|
||||||
|
extraline = "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames."
|
||||||
|
entries = [self.repr_traceback_entry(None, excinfo)]
|
||||||
|
return ReprTraceback(entries, extraline, style=self.style)
|
||||||
|
|
||||||
last = traceback[-1]
|
last = traceback[-1]
|
||||||
entries = []
|
|
||||||
if self.style == "value":
|
if self.style == "value":
|
||||||
reprentry = self.repr_traceback_entry(last, excinfo)
|
entries = [self.repr_traceback_entry(last, excinfo)]
|
||||||
entries.append(reprentry)
|
|
||||||
return ReprTraceback(entries, None, style=self.style)
|
return ReprTraceback(entries, None, style=self.style)
|
||||||
|
|
||||||
for index, entry in enumerate(traceback):
|
entries = [
|
||||||
einfo = (last == entry) and excinfo or None
|
self.repr_traceback_entry(entry, excinfo if last == entry else None)
|
||||||
reprentry = self.repr_traceback_entry(entry, einfo)
|
for entry in traceback
|
||||||
entries.append(reprentry)
|
]
|
||||||
return ReprTraceback(entries, extraline, style=self.style)
|
return ReprTraceback(entries, extraline, style=self.style)
|
||||||
|
|
||||||
def _truncate_recursive_traceback(
|
def _truncate_recursive_traceback(
|
||||||
@@ -926,6 +970,7 @@ class FormattedExcinfo:
|
|||||||
seen: Set[int] = set()
|
seen: Set[int] = set()
|
||||||
while e is not None and id(e) not in seen:
|
while e is not None and id(e) not in seen:
|
||||||
seen.add(id(e))
|
seen.add(id(e))
|
||||||
|
|
||||||
if excinfo_:
|
if excinfo_:
|
||||||
# Fall back to native traceback as a temporary workaround until
|
# Fall back to native traceback as a temporary workaround until
|
||||||
# full support for exception groups added to ExceptionInfo.
|
# full support for exception groups added to ExceptionInfo.
|
||||||
@@ -942,9 +987,7 @@ class FormattedExcinfo:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
reprtraceback = self.repr_traceback(excinfo_)
|
reprtraceback = self.repr_traceback(excinfo_)
|
||||||
reprcrash: Optional[ReprFileLocation] = (
|
reprcrash = excinfo_._getreprcrash()
|
||||||
excinfo_._getreprcrash() if self.style != "value" else None
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
# Fallback to native repr if the exception doesn't have a traceback:
|
# Fallback to native repr if the exception doesn't have a traceback:
|
||||||
# ExceptionInfo objects require a full traceback to work.
|
# ExceptionInfo objects require a full traceback to work.
|
||||||
@@ -952,25 +995,17 @@ class FormattedExcinfo:
|
|||||||
traceback.format_exception(type(e), e, None)
|
traceback.format_exception(type(e), e, None)
|
||||||
)
|
)
|
||||||
reprcrash = None
|
reprcrash = None
|
||||||
|
|
||||||
repr_chain += [(reprtraceback, reprcrash, descr)]
|
repr_chain += [(reprtraceback, reprcrash, descr)]
|
||||||
|
|
||||||
if e.__cause__ is not None and self.chain:
|
if e.__cause__ is not None and self.chain:
|
||||||
e = e.__cause__
|
e = e.__cause__
|
||||||
excinfo_ = (
|
excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
|
||||||
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
|
|
||||||
if e.__traceback__
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
descr = "The above exception was the direct cause of the following exception:"
|
descr = "The above exception was the direct cause of the following exception:"
|
||||||
elif (
|
elif (
|
||||||
e.__context__ is not None and not e.__suppress_context__ and self.chain
|
e.__context__ is not None and not e.__suppress_context__ and self.chain
|
||||||
):
|
):
|
||||||
e = e.__context__
|
e = e.__context__
|
||||||
excinfo_ = (
|
excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
|
||||||
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
|
|
||||||
if e.__traceback__
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
descr = "During handling of the above exception, another exception occurred:"
|
descr = "During handling of the above exception, another exception occurred:"
|
||||||
else:
|
else:
|
||||||
e = None
|
e = None
|
||||||
@@ -978,7 +1013,7 @@ class FormattedExcinfo:
|
|||||||
return ExceptionChainRepr(repr_chain)
|
return ExceptionChainRepr(repr_chain)
|
||||||
|
|
||||||
|
|
||||||
@attr.s(eq=False, auto_attribs=True)
|
@dataclasses.dataclass(eq=False)
|
||||||
class TerminalRepr:
|
class TerminalRepr:
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
# FYI this is called from pytest-xdist's serialization of exception
|
# FYI this is called from pytest-xdist's serialization of exception
|
||||||
@@ -996,14 +1031,14 @@ class TerminalRepr:
|
|||||||
|
|
||||||
|
|
||||||
# This class is abstract -- only subclasses are instantiated.
|
# This class is abstract -- only subclasses are instantiated.
|
||||||
@attr.s(eq=False)
|
@dataclasses.dataclass(eq=False)
|
||||||
class ExceptionRepr(TerminalRepr):
|
class ExceptionRepr(TerminalRepr):
|
||||||
# Provided by subclasses.
|
# Provided by subclasses.
|
||||||
reprcrash: Optional["ReprFileLocation"]
|
|
||||||
reprtraceback: "ReprTraceback"
|
reprtraceback: "ReprTraceback"
|
||||||
|
reprcrash: Optional["ReprFileLocation"]
|
||||||
def __attrs_post_init__(self) -> None:
|
sections: List[Tuple[str, str, str]] = dataclasses.field(
|
||||||
self.sections: List[Tuple[str, str, str]] = []
|
init=False, default_factory=list
|
||||||
|
)
|
||||||
|
|
||||||
def addsection(self, name: str, content: str, sep: str = "-") -> None:
|
def addsection(self, name: str, content: str, sep: str = "-") -> None:
|
||||||
self.sections.append((name, content, sep))
|
self.sections.append((name, content, sep))
|
||||||
@@ -1014,16 +1049,23 @@ class ExceptionRepr(TerminalRepr):
|
|||||||
tw.line(content)
|
tw.line(content)
|
||||||
|
|
||||||
|
|
||||||
@attr.s(eq=False, auto_attribs=True)
|
@dataclasses.dataclass(eq=False)
|
||||||
class ExceptionChainRepr(ExceptionRepr):
|
class ExceptionChainRepr(ExceptionRepr):
|
||||||
chain: Sequence[Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]]
|
chain: Sequence[Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]]
|
||||||
|
|
||||||
def __attrs_post_init__(self) -> None:
|
def __init__(
|
||||||
super().__attrs_post_init__()
|
self,
|
||||||
|
chain: Sequence[
|
||||||
|
Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]
|
||||||
|
],
|
||||||
|
) -> None:
|
||||||
# reprcrash and reprtraceback of the outermost (the newest) exception
|
# reprcrash and reprtraceback of the outermost (the newest) exception
|
||||||
# in the chain.
|
# in the chain.
|
||||||
self.reprtraceback = self.chain[-1][0]
|
super().__init__(
|
||||||
self.reprcrash = self.chain[-1][1]
|
reprtraceback=chain[-1][0],
|
||||||
|
reprcrash=chain[-1][1],
|
||||||
|
)
|
||||||
|
self.chain = chain
|
||||||
|
|
||||||
def toterminal(self, tw: TerminalWriter) -> None:
|
def toterminal(self, tw: TerminalWriter) -> None:
|
||||||
for element in self.chain:
|
for element in self.chain:
|
||||||
@@ -1034,17 +1076,17 @@ class ExceptionChainRepr(ExceptionRepr):
|
|||||||
super().toterminal(tw)
|
super().toterminal(tw)
|
||||||
|
|
||||||
|
|
||||||
@attr.s(eq=False, auto_attribs=True)
|
@dataclasses.dataclass(eq=False)
|
||||||
class ReprExceptionInfo(ExceptionRepr):
|
class ReprExceptionInfo(ExceptionRepr):
|
||||||
reprtraceback: "ReprTraceback"
|
reprtraceback: "ReprTraceback"
|
||||||
reprcrash: "ReprFileLocation"
|
reprcrash: Optional["ReprFileLocation"]
|
||||||
|
|
||||||
def toterminal(self, tw: TerminalWriter) -> None:
|
def toterminal(self, tw: TerminalWriter) -> None:
|
||||||
self.reprtraceback.toterminal(tw)
|
self.reprtraceback.toterminal(tw)
|
||||||
super().toterminal(tw)
|
super().toterminal(tw)
|
||||||
|
|
||||||
|
|
||||||
@attr.s(eq=False, auto_attribs=True)
|
@dataclasses.dataclass(eq=False)
|
||||||
class ReprTraceback(TerminalRepr):
|
class ReprTraceback(TerminalRepr):
|
||||||
reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]]
|
reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]]
|
||||||
extraline: Optional[str]
|
extraline: Optional[str]
|
||||||
@@ -1073,12 +1115,12 @@ class ReprTraceback(TerminalRepr):
|
|||||||
|
|
||||||
class ReprTracebackNative(ReprTraceback):
|
class ReprTracebackNative(ReprTraceback):
|
||||||
def __init__(self, tblines: Sequence[str]) -> None:
|
def __init__(self, tblines: Sequence[str]) -> None:
|
||||||
self.style = "native"
|
|
||||||
self.reprentries = [ReprEntryNative(tblines)]
|
self.reprentries = [ReprEntryNative(tblines)]
|
||||||
self.extraline = None
|
self.extraline = None
|
||||||
|
self.style = "native"
|
||||||
|
|
||||||
|
|
||||||
@attr.s(eq=False, auto_attribs=True)
|
@dataclasses.dataclass(eq=False)
|
||||||
class ReprEntryNative(TerminalRepr):
|
class ReprEntryNative(TerminalRepr):
|
||||||
lines: Sequence[str]
|
lines: Sequence[str]
|
||||||
|
|
||||||
@@ -1088,7 +1130,7 @@ class ReprEntryNative(TerminalRepr):
|
|||||||
tw.write("".join(self.lines))
|
tw.write("".join(self.lines))
|
||||||
|
|
||||||
|
|
||||||
@attr.s(eq=False, auto_attribs=True)
|
@dataclasses.dataclass(eq=False)
|
||||||
class ReprEntry(TerminalRepr):
|
class ReprEntry(TerminalRepr):
|
||||||
lines: Sequence[str]
|
lines: Sequence[str]
|
||||||
reprfuncargs: Optional["ReprFuncArgs"]
|
reprfuncargs: Optional["ReprFuncArgs"]
|
||||||
@@ -1142,8 +1184,8 @@ class ReprEntry(TerminalRepr):
|
|||||||
|
|
||||||
def toterminal(self, tw: TerminalWriter) -> None:
|
def toterminal(self, tw: TerminalWriter) -> None:
|
||||||
if self.style == "short":
|
if self.style == "short":
|
||||||
assert self.reprfileloc is not None
|
if self.reprfileloc:
|
||||||
self.reprfileloc.toterminal(tw)
|
self.reprfileloc.toterminal(tw)
|
||||||
self._write_entry_lines(tw)
|
self._write_entry_lines(tw)
|
||||||
if self.reprlocals:
|
if self.reprlocals:
|
||||||
self.reprlocals.toterminal(tw, indent=" " * 8)
|
self.reprlocals.toterminal(tw, indent=" " * 8)
|
||||||
@@ -1168,12 +1210,15 @@ class ReprEntry(TerminalRepr):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@attr.s(eq=False, auto_attribs=True)
|
@dataclasses.dataclass(eq=False)
|
||||||
class ReprFileLocation(TerminalRepr):
|
class ReprFileLocation(TerminalRepr):
|
||||||
path: str = attr.ib(converter=str)
|
path: str
|
||||||
lineno: int
|
lineno: int
|
||||||
message: str
|
message: str
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
self.path = str(self.path)
|
||||||
|
|
||||||
def toterminal(self, tw: TerminalWriter) -> None:
|
def toterminal(self, tw: TerminalWriter) -> None:
|
||||||
# Filename and lineno output for each entry, using an output format
|
# Filename and lineno output for each entry, using an output format
|
||||||
# that most editors understand.
|
# that most editors understand.
|
||||||
@@ -1185,7 +1230,7 @@ class ReprFileLocation(TerminalRepr):
|
|||||||
tw.line(f":{self.lineno}: {msg}")
|
tw.line(f":{self.lineno}: {msg}")
|
||||||
|
|
||||||
|
|
||||||
@attr.s(eq=False, auto_attribs=True)
|
@dataclasses.dataclass(eq=False)
|
||||||
class ReprLocals(TerminalRepr):
|
class ReprLocals(TerminalRepr):
|
||||||
lines: Sequence[str]
|
lines: Sequence[str]
|
||||||
|
|
||||||
@@ -1194,7 +1239,7 @@ class ReprLocals(TerminalRepr):
|
|||||||
tw.line(indent + line)
|
tw.line(indent + line)
|
||||||
|
|
||||||
|
|
||||||
@attr.s(eq=False, auto_attribs=True)
|
@dataclasses.dataclass(eq=False)
|
||||||
class ReprFuncArgs(TerminalRepr):
|
class ReprFuncArgs(TerminalRepr):
|
||||||
args: Sequence[Tuple[str, object]]
|
args: Sequence[Tuple[str, object]]
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from stat import S_ISLNK
|
|||||||
from stat import S_ISREG
|
from stat import S_ISREG
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
from typing import cast
|
||||||
from typing import overload
|
from typing import overload
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@@ -146,7 +147,7 @@ class Visitor:
|
|||||||
self.fil = fil
|
self.fil = fil
|
||||||
self.ignore = ignore
|
self.ignore = ignore
|
||||||
self.breadthfirst = bf
|
self.breadthfirst = bf
|
||||||
self.optsort = sort and sorted or (lambda x: x)
|
self.optsort = cast(Callable[[Any], Any], sorted) if sort else (lambda x: x)
|
||||||
|
|
||||||
def gen(self, path):
|
def gen(self, path):
|
||||||
try:
|
try:
|
||||||
@@ -224,7 +225,7 @@ class Stat:
|
|||||||
raise NotImplementedError("XXX win32")
|
raise NotImplementedError("XXX win32")
|
||||||
import pwd
|
import pwd
|
||||||
|
|
||||||
entry = error.checked_call(pwd.getpwuid, self.uid)
|
entry = error.checked_call(pwd.getpwuid, self.uid) # type:ignore[attr-defined]
|
||||||
return entry[0]
|
return entry[0]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -234,7 +235,7 @@ class Stat:
|
|||||||
raise NotImplementedError("XXX win32")
|
raise NotImplementedError("XXX win32")
|
||||||
import grp
|
import grp
|
||||||
|
|
||||||
entry = error.checked_call(grp.getgrgid, self.gid)
|
entry = error.checked_call(grp.getgrgid, self.gid) # type:ignore[attr-defined]
|
||||||
return entry[0]
|
return entry[0]
|
||||||
|
|
||||||
def isdir(self):
|
def isdir(self):
|
||||||
@@ -252,7 +253,7 @@ def getuserid(user):
|
|||||||
import pwd
|
import pwd
|
||||||
|
|
||||||
if not isinstance(user, int):
|
if not isinstance(user, int):
|
||||||
user = pwd.getpwnam(user)[2]
|
user = pwd.getpwnam(user)[2] # type:ignore[attr-defined]
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
@@ -260,7 +261,7 @@ def getgroupid(group):
|
|||||||
import grp
|
import grp
|
||||||
|
|
||||||
if not isinstance(group, int):
|
if not isinstance(group, int):
|
||||||
group = grp.getgrnam(group)[2]
|
group = grp.getgrnam(group)[2] # type:ignore[attr-defined]
|
||||||
return group
|
return group
|
||||||
|
|
||||||
|
|
||||||
@@ -795,7 +796,7 @@ class LocalPath:
|
|||||||
kw = {"exists": 1}
|
kw = {"exists": 1}
|
||||||
return Checkers(self)._evaluate(kw)
|
return Checkers(self)._evaluate(kw)
|
||||||
|
|
||||||
_patternchars = set("*?[" + os.path.sep)
|
_patternchars = set("*?[" + os.sep)
|
||||||
|
|
||||||
def listdir(self, fil=None, sort=None):
|
def listdir(self, fil=None, sort=None):
|
||||||
"""List directory contents, possibly filter by the given fil func
|
"""List directory contents, possibly filter by the given fil func
|
||||||
@@ -952,7 +953,7 @@ class LocalPath:
|
|||||||
else:
|
else:
|
||||||
p.dirpath()._ensuredirs()
|
p.dirpath()._ensuredirs()
|
||||||
if not p.check(file=1):
|
if not p.check(file=1):
|
||||||
p.open("w").close()
|
p.open("wb").close()
|
||||||
return p
|
return p
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
@@ -1127,7 +1128,7 @@ class LocalPath:
|
|||||||
modfile = modfile[:-1]
|
modfile = modfile[:-1]
|
||||||
elif modfile.endswith("$py.class"):
|
elif modfile.endswith("$py.class"):
|
||||||
modfile = modfile[:-9] + ".py"
|
modfile = modfile[:-9] + ".py"
|
||||||
if modfile.endswith(os.path.sep + "__init__.py"):
|
if modfile.endswith(os.sep + "__init__.py"):
|
||||||
if self.basename != "__init__.py":
|
if self.basename != "__init__.py":
|
||||||
modfile = modfile[:-12]
|
modfile = modfile[:-12]
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -44,10 +44,20 @@ from _pytest.stash import StashKey
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from _pytest.assertion import AssertionState
|
from _pytest.assertion import AssertionState
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 8):
|
||||||
|
namedExpr = ast.NamedExpr
|
||||||
|
astNameConstant = ast.Constant
|
||||||
|
astStr = ast.Constant
|
||||||
|
astNum = ast.Constant
|
||||||
|
else:
|
||||||
|
namedExpr = ast.Expr
|
||||||
|
astNameConstant = ast.NameConstant
|
||||||
|
astStr = ast.Str
|
||||||
|
astNum = ast.Num
|
||||||
|
|
||||||
|
|
||||||
assertstate_key = StashKey["AssertionState"]()
|
assertstate_key = StashKey["AssertionState"]()
|
||||||
|
|
||||||
|
|
||||||
# pytest caches rewritten pycs in pycache dirs
|
# pytest caches rewritten pycs in pycache dirs
|
||||||
PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}"
|
PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}"
|
||||||
PYC_EXT = ".py" + (__debug__ and "c" or "o")
|
PYC_EXT = ".py" + (__debug__ and "c" or "o")
|
||||||
@@ -180,7 +190,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
|
|||||||
for initial_path in self.session._initialpaths:
|
for initial_path in self.session._initialpaths:
|
||||||
# Make something as c:/projects/my_project/path.py ->
|
# Make something as c:/projects/my_project/path.py ->
|
||||||
# ['c:', 'projects', 'my_project', 'path.py']
|
# ['c:', 'projects', 'my_project', 'path.py']
|
||||||
parts = str(initial_path).split(os.path.sep)
|
parts = str(initial_path).split(os.sep)
|
||||||
# add 'path' to basenames to be checked.
|
# add 'path' to basenames to be checked.
|
||||||
self._basenames_to_check_rewrite.add(os.path.splitext(parts[-1])[0])
|
self._basenames_to_check_rewrite.add(os.path.splitext(parts[-1])[0])
|
||||||
|
|
||||||
@@ -274,8 +284,12 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
|
|||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
if sys.version_info >= (3, 10):
|
if sys.version_info >= (3, 10):
|
||||||
|
if sys.version_info >= (3, 12):
|
||||||
|
from importlib.resources.abc import TraversableResources
|
||||||
|
else:
|
||||||
|
from importlib.abc import TraversableResources
|
||||||
|
|
||||||
def get_resource_reader(self, name: str) -> importlib.abc.TraversableResources: # type: ignore
|
def get_resource_reader(self, name: str) -> TraversableResources: # type: ignore
|
||||||
if sys.version_info < (3, 11):
|
if sys.version_info < (3, 11):
|
||||||
from importlib.readers import FileReader
|
from importlib.readers import FileReader
|
||||||
else:
|
else:
|
||||||
@@ -631,8 +645,12 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||||||
.push_format_context() and .pop_format_context() which allows
|
.push_format_context() and .pop_format_context() which allows
|
||||||
to build another %-formatted string while already building one.
|
to build another %-formatted string while already building one.
|
||||||
|
|
||||||
This state is reset on every new assert statement visited and used
|
:variables_overwrite: A dict filled with references to variables
|
||||||
by the other visitors.
|
that change value within an assert. This happens when a variable is
|
||||||
|
reassigned with the walrus operator
|
||||||
|
|
||||||
|
This state, except the variables_overwrite, is reset on every new assert
|
||||||
|
statement visited and used by the other visitors.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -648,6 +666,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||||||
else:
|
else:
|
||||||
self.enable_assertion_pass_hook = False
|
self.enable_assertion_pass_hook = False
|
||||||
self.source = source
|
self.source = source
|
||||||
|
self.variables_overwrite: Dict[str, str] = {}
|
||||||
|
|
||||||
def run(self, mod: ast.Module) -> None:
|
def run(self, mod: ast.Module) -> None:
|
||||||
"""Find all assert statements in *mod* and rewrite them."""
|
"""Find all assert statements in *mod* and rewrite them."""
|
||||||
@@ -662,14 +681,17 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||||||
if doc is not None and self.is_rewrite_disabled(doc):
|
if doc is not None and self.is_rewrite_disabled(doc):
|
||||||
return
|
return
|
||||||
pos = 0
|
pos = 0
|
||||||
lineno = 1
|
item = None
|
||||||
for item in mod.body:
|
for item in mod.body:
|
||||||
if (
|
if (
|
||||||
expect_docstring
|
expect_docstring
|
||||||
and isinstance(item, ast.Expr)
|
and isinstance(item, ast.Expr)
|
||||||
and isinstance(item.value, ast.Str)
|
and isinstance(item.value, astStr)
|
||||||
):
|
):
|
||||||
doc = item.value.s
|
if sys.version_info >= (3, 8):
|
||||||
|
doc = item.value.value
|
||||||
|
else:
|
||||||
|
doc = item.value.s
|
||||||
if self.is_rewrite_disabled(doc):
|
if self.is_rewrite_disabled(doc):
|
||||||
return
|
return
|
||||||
expect_docstring = False
|
expect_docstring = False
|
||||||
@@ -801,7 +823,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||||||
current = self.stack.pop()
|
current = self.stack.pop()
|
||||||
if self.stack:
|
if self.stack:
|
||||||
self.explanation_specifiers = self.stack[-1]
|
self.explanation_specifiers = self.stack[-1]
|
||||||
keys = [ast.Str(key) for key in current.keys()]
|
keys = [astStr(key) for key in current.keys()]
|
||||||
format_dict = ast.Dict(keys, list(current.values()))
|
format_dict = ast.Dict(keys, list(current.values()))
|
||||||
form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
|
form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
|
||||||
name = "@py_format" + str(next(self.variable_counter))
|
name = "@py_format" + str(next(self.variable_counter))
|
||||||
@@ -855,16 +877,16 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||||||
negation = ast.UnaryOp(ast.Not(), top_condition)
|
negation = ast.UnaryOp(ast.Not(), top_condition)
|
||||||
|
|
||||||
if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
|
if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
|
||||||
msg = self.pop_format_context(ast.Str(explanation))
|
msg = self.pop_format_context(astStr(explanation))
|
||||||
|
|
||||||
# Failed
|
# Failed
|
||||||
if assert_.msg:
|
if assert_.msg:
|
||||||
assertmsg = self.helper("_format_assertmsg", assert_.msg)
|
assertmsg = self.helper("_format_assertmsg", assert_.msg)
|
||||||
gluestr = "\n>assert "
|
gluestr = "\n>assert "
|
||||||
else:
|
else:
|
||||||
assertmsg = ast.Str("")
|
assertmsg = astStr("")
|
||||||
gluestr = "assert "
|
gluestr = "assert "
|
||||||
err_explanation = ast.BinOp(ast.Str(gluestr), ast.Add(), msg)
|
err_explanation = ast.BinOp(astStr(gluestr), ast.Add(), msg)
|
||||||
err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation)
|
err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation)
|
||||||
err_name = ast.Name("AssertionError", ast.Load())
|
err_name = ast.Name("AssertionError", ast.Load())
|
||||||
fmt = self.helper("_format_explanation", err_msg)
|
fmt = self.helper("_format_explanation", err_msg)
|
||||||
@@ -880,8 +902,8 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||||||
hook_call_pass = ast.Expr(
|
hook_call_pass = ast.Expr(
|
||||||
self.helper(
|
self.helper(
|
||||||
"_call_assertion_pass",
|
"_call_assertion_pass",
|
||||||
ast.Num(assert_.lineno),
|
astNum(assert_.lineno),
|
||||||
ast.Str(orig),
|
astStr(orig),
|
||||||
fmt_pass,
|
fmt_pass,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -900,7 +922,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||||||
variables = [
|
variables = [
|
||||||
ast.Name(name, ast.Store()) for name in self.format_variables
|
ast.Name(name, ast.Store()) for name in self.format_variables
|
||||||
]
|
]
|
||||||
clear_format = ast.Assign(variables, ast.NameConstant(None))
|
clear_format = ast.Assign(variables, astNameConstant(None))
|
||||||
self.statements.append(clear_format)
|
self.statements.append(clear_format)
|
||||||
|
|
||||||
else: # Original assertion rewriting
|
else: # Original assertion rewriting
|
||||||
@@ -911,9 +933,9 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||||||
assertmsg = self.helper("_format_assertmsg", assert_.msg)
|
assertmsg = self.helper("_format_assertmsg", assert_.msg)
|
||||||
explanation = "\n>assert " + explanation
|
explanation = "\n>assert " + explanation
|
||||||
else:
|
else:
|
||||||
assertmsg = ast.Str("")
|
assertmsg = astStr("")
|
||||||
explanation = "assert " + explanation
|
explanation = "assert " + explanation
|
||||||
template = ast.BinOp(assertmsg, ast.Add(), ast.Str(explanation))
|
template = ast.BinOp(assertmsg, ast.Add(), astStr(explanation))
|
||||||
msg = self.pop_format_context(template)
|
msg = self.pop_format_context(template)
|
||||||
fmt = self.helper("_format_explanation", msg)
|
fmt = self.helper("_format_explanation", msg)
|
||||||
err_name = ast.Name("AssertionError", ast.Load())
|
err_name = ast.Name("AssertionError", ast.Load())
|
||||||
@@ -925,7 +947,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||||||
# Clear temporary variables by setting them to None.
|
# Clear temporary variables by setting them to None.
|
||||||
if self.variables:
|
if self.variables:
|
||||||
variables = [ast.Name(name, ast.Store()) for name in self.variables]
|
variables = [ast.Name(name, ast.Store()) for name in self.variables]
|
||||||
clear = ast.Assign(variables, ast.NameConstant(None))
|
clear = ast.Assign(variables, astNameConstant(None))
|
||||||
self.statements.append(clear)
|
self.statements.append(clear)
|
||||||
# Fix locations (line numbers/column offsets).
|
# Fix locations (line numbers/column offsets).
|
||||||
for stmt in self.statements:
|
for stmt in self.statements:
|
||||||
@@ -933,14 +955,26 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||||||
ast.copy_location(node, assert_)
|
ast.copy_location(node, assert_)
|
||||||
return self.statements
|
return self.statements
|
||||||
|
|
||||||
|
def visit_NamedExpr(self, name: namedExpr) -> Tuple[namedExpr, str]:
|
||||||
|
# This method handles the 'walrus operator' repr of the target
|
||||||
|
# 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]
|
||||||
|
inlocs = ast.Compare(astStr(target_id), [ast.In()], [locs])
|
||||||
|
dorepr = self.helper("_should_repr_global_name", name)
|
||||||
|
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
|
||||||
|
expr = ast.IfExp(test, self.display(name), astStr(target_id))
|
||||||
|
return name, self.explanation_param(expr)
|
||||||
|
|
||||||
def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]:
|
def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]:
|
||||||
# Display the repr of the name if it's a local variable or
|
# Display the repr of the name if it's a local variable or
|
||||||
# _should_repr_global_name() thinks it's acceptable.
|
# _should_repr_global_name() thinks it's acceptable.
|
||||||
locs = ast.Call(self.builtin("locals"), [], [])
|
locs = ast.Call(self.builtin("locals"), [], [])
|
||||||
inlocs = ast.Compare(ast.Str(name.id), [ast.In()], [locs])
|
inlocs = ast.Compare(astStr(name.id), [ast.In()], [locs])
|
||||||
dorepr = self.helper("_should_repr_global_name", name)
|
dorepr = self.helper("_should_repr_global_name", name)
|
||||||
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
|
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
|
||||||
expr = ast.IfExp(test, self.display(name), ast.Str(name.id))
|
expr = ast.IfExp(test, self.display(name), astStr(name.id))
|
||||||
return name, self.explanation_param(expr)
|
return name, self.explanation_param(expr)
|
||||||
|
|
||||||
def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
|
def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
|
||||||
@@ -959,10 +993,26 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||||||
# cond is set in a prior loop iteration below
|
# 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
|
||||||
self.expl_stmts = fail_inner
|
self.expl_stmts = fail_inner
|
||||||
|
# Check if the left operand is a namedExpr and the value has already been visited
|
||||||
|
if (
|
||||||
|
isinstance(v, ast.Compare)
|
||||||
|
and isinstance(v.left, namedExpr)
|
||||||
|
and v.left.target.id
|
||||||
|
in [
|
||||||
|
ast_expr.id
|
||||||
|
for ast_expr in boolop.values[:i]
|
||||||
|
if hasattr(ast_expr, "id")
|
||||||
|
]
|
||||||
|
):
|
||||||
|
pytest_temp = self.variable()
|
||||||
|
self.variables_overwrite[
|
||||||
|
v.left.target.id
|
||||||
|
] = v.left # type:ignore[assignment]
|
||||||
|
v.left.target.id = pytest_temp
|
||||||
self.push_format_context()
|
self.push_format_context()
|
||||||
res, expl = self.visit(v)
|
res, expl = self.visit(v)
|
||||||
body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
|
body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
|
||||||
expl_format = self.pop_format_context(ast.Str(expl))
|
expl_format = self.pop_format_context(astStr(expl))
|
||||||
call = ast.Call(app, [expl_format], [])
|
call = ast.Call(app, [expl_format], [])
|
||||||
self.expl_stmts.append(ast.Expr(call))
|
self.expl_stmts.append(ast.Expr(call))
|
||||||
if i < levels:
|
if i < levels:
|
||||||
@@ -974,7 +1024,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||||||
self.statements = body = inner
|
self.statements = body = inner
|
||||||
self.statements = save
|
self.statements = save
|
||||||
self.expl_stmts = fail_save
|
self.expl_stmts = fail_save
|
||||||
expl_template = self.helper("_format_boolop", expl_list, ast.Num(is_or))
|
expl_template = self.helper("_format_boolop", expl_list, astNum(is_or))
|
||||||
expl = self.pop_format_context(expl_template)
|
expl = self.pop_format_context(expl_template)
|
||||||
return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
|
return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
|
||||||
|
|
||||||
@@ -998,10 +1048,19 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||||||
new_args = []
|
new_args = []
|
||||||
new_kwargs = []
|
new_kwargs = []
|
||||||
for arg in call.args:
|
for arg in call.args:
|
||||||
|
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite:
|
||||||
|
arg = self.variables_overwrite[arg.id] # type:ignore[assignment]
|
||||||
res, expl = self.visit(arg)
|
res, expl = self.visit(arg)
|
||||||
arg_expls.append(expl)
|
arg_expls.append(expl)
|
||||||
new_args.append(res)
|
new_args.append(res)
|
||||||
for keyword in call.keywords:
|
for keyword in call.keywords:
|
||||||
|
if (
|
||||||
|
isinstance(keyword.value, ast.Name)
|
||||||
|
and keyword.value.id in self.variables_overwrite
|
||||||
|
):
|
||||||
|
keyword.value = self.variables_overwrite[
|
||||||
|
keyword.value.id
|
||||||
|
] # type:ignore[assignment]
|
||||||
res, expl = self.visit(keyword.value)
|
res, expl = self.visit(keyword.value)
|
||||||
new_kwargs.append(ast.keyword(keyword.arg, res))
|
new_kwargs.append(ast.keyword(keyword.arg, res))
|
||||||
if keyword.arg:
|
if keyword.arg:
|
||||||
@@ -1034,6 +1093,15 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||||||
|
|
||||||
def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]:
|
def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]:
|
||||||
self.push_format_context()
|
self.push_format_context()
|
||||||
|
# We first check if we have overwritten a variable in the previous assert
|
||||||
|
if isinstance(comp.left, ast.Name) and comp.left.id in self.variables_overwrite:
|
||||||
|
comp.left = self.variables_overwrite[
|
||||||
|
comp.left.id
|
||||||
|
] # type:ignore[assignment]
|
||||||
|
if isinstance(comp.left, namedExpr):
|
||||||
|
self.variables_overwrite[
|
||||||
|
comp.left.target.id
|
||||||
|
] = comp.left # type:ignore[assignment]
|
||||||
left_res, left_expl = self.visit(comp.left)
|
left_res, left_expl = self.visit(comp.left)
|
||||||
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
|
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
|
||||||
left_expl = f"({left_expl})"
|
left_expl = f"({left_expl})"
|
||||||
@@ -1045,14 +1113,23 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||||||
syms = []
|
syms = []
|
||||||
results = [left_res]
|
results = [left_res]
|
||||||
for i, op, next_operand in it:
|
for i, op, next_operand in it:
|
||||||
|
if (
|
||||||
|
isinstance(next_operand, namedExpr)
|
||||||
|
and isinstance(left_res, ast.Name)
|
||||||
|
and next_operand.target.id == left_res.id
|
||||||
|
):
|
||||||
|
next_operand.target.id = self.variable()
|
||||||
|
self.variables_overwrite[
|
||||||
|
left_res.id
|
||||||
|
] = next_operand # type:ignore[assignment]
|
||||||
next_res, next_expl = self.visit(next_operand)
|
next_res, next_expl = self.visit(next_operand)
|
||||||
if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
|
if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
|
||||||
next_expl = f"({next_expl})"
|
next_expl = f"({next_expl})"
|
||||||
results.append(next_res)
|
results.append(next_res)
|
||||||
sym = BINOP_MAP[op.__class__]
|
sym = BINOP_MAP[op.__class__]
|
||||||
syms.append(ast.Str(sym))
|
syms.append(astStr(sym))
|
||||||
expl = f"{left_expl} {sym} {next_expl}"
|
expl = f"{left_expl} {sym} {next_expl}"
|
||||||
expls.append(ast.Str(expl))
|
expls.append(astStr(expl))
|
||||||
res_expr = ast.Compare(left_res, [op], [next_res])
|
res_expr = ast.Compare(left_res, [op], [next_res])
|
||||||
self.statements.append(ast.Assign([store_names[i]], res_expr))
|
self.statements.append(ast.Assign([store_names[i]], res_expr))
|
||||||
left_res, left_expl = next_res, next_expl
|
left_res, left_expl = next_res, next_expl
|
||||||
@@ -1068,6 +1145,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||||||
res: ast.expr = ast.BoolOp(ast.And(), load_names)
|
res: ast.expr = ast.BoolOp(ast.And(), load_names)
|
||||||
else:
|
else:
|
||||||
res = load_names[0]
|
res = load_names[0]
|
||||||
|
|
||||||
return res, self.explanation_param(self.pop_format_context(expl_call))
|
return res, self.explanation_param(self.pop_format_context(expl_call))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ def _truncate_explanation(
|
|||||||
"""Truncate given list of strings that makes up the assertion explanation.
|
"""Truncate given list of strings that makes up the assertion explanation.
|
||||||
|
|
||||||
Truncates to either 8 lines, or 640 characters - whichever the input reaches
|
Truncates to either 8 lines, or 640 characters - whichever the input reaches
|
||||||
first. The remaining lines will be replaced by a usage message.
|
first, taking the truncation explanation into account. The remaining lines
|
||||||
|
will be replaced by a usage message.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if max_lines is None:
|
if max_lines is None:
|
||||||
max_lines = DEFAULT_MAX_LINES
|
max_lines = DEFAULT_MAX_LINES
|
||||||
if max_chars is None:
|
if max_chars is None:
|
||||||
@@ -48,35 +48,56 @@ def _truncate_explanation(
|
|||||||
|
|
||||||
# Check if truncation required
|
# Check if truncation required
|
||||||
input_char_count = len("".join(input_lines))
|
input_char_count = len("".join(input_lines))
|
||||||
if len(input_lines) <= max_lines and input_char_count <= max_chars:
|
# The length of the truncation explanation depends on the number of lines
|
||||||
|
# removed but is at least 68 characters:
|
||||||
|
# The real value is
|
||||||
|
# 64 (for the base message:
|
||||||
|
# '...\n...Full output truncated (1 line hidden), use '-vv' to show")'
|
||||||
|
# )
|
||||||
|
# + 1 (for plural)
|
||||||
|
# + int(math.log10(len(input_lines) - max_lines)) (number of hidden line, at least 1)
|
||||||
|
# + 3 for the '...' added to the truncated line
|
||||||
|
# But if there's more than 100 lines it's very likely that we're going to
|
||||||
|
# truncate, so we don't need the exact value using log10.
|
||||||
|
tolerable_max_chars = (
|
||||||
|
max_chars + 70 # 64 + 1 (for plural) + 2 (for '99') + 3 for '...'
|
||||||
|
)
|
||||||
|
# The truncation explanation add two lines to the output
|
||||||
|
tolerable_max_lines = max_lines + 2
|
||||||
|
if (
|
||||||
|
len(input_lines) <= tolerable_max_lines
|
||||||
|
and input_char_count <= tolerable_max_chars
|
||||||
|
):
|
||||||
return input_lines
|
return input_lines
|
||||||
|
# Truncate first to max_lines, and then truncate to max_chars if necessary
|
||||||
# Truncate first to max_lines, and then truncate to max_chars if max_chars
|
|
||||||
# is exceeded.
|
|
||||||
truncated_explanation = input_lines[:max_lines]
|
truncated_explanation = input_lines[:max_lines]
|
||||||
truncated_explanation = _truncate_by_char_count(truncated_explanation, max_chars)
|
truncated_char = True
|
||||||
|
# We reevaluate the need to truncate chars following removal of some lines
|
||||||
# Add ellipsis to final line
|
if len("".join(truncated_explanation)) > tolerable_max_chars:
|
||||||
truncated_explanation[-1] = truncated_explanation[-1] + "..."
|
truncated_explanation = _truncate_by_char_count(
|
||||||
|
truncated_explanation, max_chars
|
||||||
# Append useful message to explanation
|
)
|
||||||
truncated_line_count = len(input_lines) - len(truncated_explanation)
|
|
||||||
truncated_line_count += 1 # Account for the part-truncated final line
|
|
||||||
msg = "...Full output truncated"
|
|
||||||
if truncated_line_count == 1:
|
|
||||||
msg += f" ({truncated_line_count} line hidden)"
|
|
||||||
else:
|
else:
|
||||||
msg += f" ({truncated_line_count} lines hidden)"
|
truncated_char = False
|
||||||
msg += f", {USAGE_MSG}"
|
|
||||||
truncated_explanation.extend(["", str(msg)])
|
truncated_line_count = len(input_lines) - len(truncated_explanation)
|
||||||
return truncated_explanation
|
if truncated_explanation[-1]:
|
||||||
|
# Add ellipsis and take into account part-truncated final line
|
||||||
|
truncated_explanation[-1] = truncated_explanation[-1] + "..."
|
||||||
|
if truncated_char:
|
||||||
|
# It's possible that we did not remove any char from this line
|
||||||
|
truncated_line_count += 1
|
||||||
|
else:
|
||||||
|
# Add proper ellipsis when we were able to fit a full line exactly
|
||||||
|
truncated_explanation[-1] = "..."
|
||||||
|
return truncated_explanation + [
|
||||||
|
"",
|
||||||
|
f"...Full output truncated ({truncated_line_count} line"
|
||||||
|
f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def _truncate_by_char_count(input_lines: List[str], max_chars: int) -> List[str]:
|
def _truncate_by_char_count(input_lines: List[str], max_chars: int) -> List[str]:
|
||||||
# Check if truncation required
|
|
||||||
if len("".join(input_lines)) <= max_chars:
|
|
||||||
return input_lines
|
|
||||||
|
|
||||||
# Find point at which input length exceeds total allowed length
|
# Find point at which input length exceeds total allowed length
|
||||||
iterated_char_count = 0
|
iterated_char_count = 0
|
||||||
for iterated_index, input_line in enumerate(input_lines):
|
for iterated_index, input_line in enumerate(input_lines):
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Implementation of the cache provider."""
|
"""Implementation of the cache provider."""
|
||||||
# This plugin was not named "cache" to avoid conflicts with the external
|
# This plugin was not named "cache" to avoid conflicts with the external
|
||||||
# pytest-cache version.
|
# pytest-cache version.
|
||||||
|
import dataclasses
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -12,8 +13,6 @@ from typing import Optional
|
|||||||
from typing import Set
|
from typing import Set
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import attr
|
|
||||||
|
|
||||||
from .pathlib import resolve_from_str
|
from .pathlib import resolve_from_str
|
||||||
from .pathlib import rm_rf
|
from .pathlib import rm_rf
|
||||||
from .reports import CollectReport
|
from .reports import CollectReport
|
||||||
@@ -28,11 +27,10 @@ from _pytest.deprecated import check_ispytest
|
|||||||
from _pytest.fixtures import fixture
|
from _pytest.fixtures import fixture
|
||||||
from _pytest.fixtures import FixtureRequest
|
from _pytest.fixtures import FixtureRequest
|
||||||
from _pytest.main import Session
|
from _pytest.main import Session
|
||||||
from _pytest.python import Module
|
from _pytest.nodes import File
|
||||||
from _pytest.python import Package
|
from _pytest.python import Package
|
||||||
from _pytest.reports import TestReport
|
from _pytest.reports import TestReport
|
||||||
|
|
||||||
|
|
||||||
README_CONTENT = """\
|
README_CONTENT = """\
|
||||||
# pytest cache directory #
|
# pytest cache directory #
|
||||||
|
|
||||||
@@ -53,10 +51,12 @@ Signature: 8a477f597d28d172789f06886806bc55
|
|||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@attr.s(init=False, auto_attribs=True)
|
@dataclasses.dataclass
|
||||||
class Cache:
|
class Cache:
|
||||||
_cachedir: Path = attr.ib(repr=False)
|
"""Instance of the `cache` fixture."""
|
||||||
_config: Config = attr.ib(repr=False)
|
|
||||||
|
_cachedir: Path = dataclasses.field(repr=False)
|
||||||
|
_config: Config = dataclasses.field(repr=False)
|
||||||
|
|
||||||
# Sub-directory under cache-dir for directories created by `mkdir()`.
|
# Sub-directory under cache-dir for directories created by `mkdir()`.
|
||||||
_CACHE_PREFIX_DIRS = "d"
|
_CACHE_PREFIX_DIRS = "d"
|
||||||
@@ -179,16 +179,22 @@ class Cache:
|
|||||||
else:
|
else:
|
||||||
cache_dir_exists_already = self._cachedir.exists()
|
cache_dir_exists_already = self._cachedir.exists()
|
||||||
path.parent.mkdir(exist_ok=True, parents=True)
|
path.parent.mkdir(exist_ok=True, parents=True)
|
||||||
except OSError:
|
except OSError as exc:
|
||||||
self.warn("could not create cache path {path}", path=path, _ispytest=True)
|
self.warn(
|
||||||
|
f"could not create cache path {path}: {exc}",
|
||||||
|
_ispytest=True,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
if not cache_dir_exists_already:
|
if not cache_dir_exists_already:
|
||||||
self._ensure_supporting_files()
|
self._ensure_supporting_files()
|
||||||
data = json.dumps(value, ensure_ascii=False, indent=2)
|
data = json.dumps(value, ensure_ascii=False, indent=2)
|
||||||
try:
|
try:
|
||||||
f = path.open("w", encoding="UTF-8")
|
f = path.open("w", encoding="UTF-8")
|
||||||
except OSError:
|
except OSError as exc:
|
||||||
self.warn("cache could not write path {path}", path=path, _ispytest=True)
|
self.warn(
|
||||||
|
f"cache could not write path {path}: {exc}",
|
||||||
|
_ispytest=True,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
with f:
|
with f:
|
||||||
f.write(data)
|
f.write(data)
|
||||||
@@ -213,22 +219,30 @@ class LFPluginCollWrapper:
|
|||||||
|
|
||||||
@hookimpl(hookwrapper=True)
|
@hookimpl(hookwrapper=True)
|
||||||
def pytest_make_collect_report(self, collector: nodes.Collector):
|
def pytest_make_collect_report(self, collector: nodes.Collector):
|
||||||
if isinstance(collector, Session):
|
if isinstance(collector, (Session, Package)):
|
||||||
out = yield
|
out = yield
|
||||||
res: CollectReport = out.get_result()
|
res: CollectReport = out.get_result()
|
||||||
|
|
||||||
# Sort any lf-paths to the beginning.
|
# Sort any lf-paths to the beginning.
|
||||||
lf_paths = self.lfplugin._last_failed_paths
|
lf_paths = self.lfplugin._last_failed_paths
|
||||||
|
|
||||||
|
# Use stable sort to priorize last failed.
|
||||||
|
def sort_key(node: Union[nodes.Item, nodes.Collector]) -> bool:
|
||||||
|
# Package.path is the __init__.py file, we need the directory.
|
||||||
|
if isinstance(node, Package):
|
||||||
|
path = node.path.parent
|
||||||
|
else:
|
||||||
|
path = node.path
|
||||||
|
return path in lf_paths
|
||||||
|
|
||||||
res.result = sorted(
|
res.result = sorted(
|
||||||
res.result,
|
res.result,
|
||||||
# use stable sort to priorize last failed
|
key=sort_key,
|
||||||
key=lambda x: x.path in lf_paths,
|
|
||||||
reverse=True,
|
reverse=True,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
elif isinstance(collector, Module):
|
elif isinstance(collector, File):
|
||||||
if collector.path in self.lfplugin._last_failed_paths:
|
if collector.path in self.lfplugin._last_failed_paths:
|
||||||
out = yield
|
out = yield
|
||||||
res = out.get_result()
|
res = out.get_result()
|
||||||
@@ -266,10 +280,9 @@ class LFPluginCollSkipfiles:
|
|||||||
def pytest_make_collect_report(
|
def pytest_make_collect_report(
|
||||||
self, collector: nodes.Collector
|
self, collector: nodes.Collector
|
||||||
) -> Optional[CollectReport]:
|
) -> Optional[CollectReport]:
|
||||||
# Packages are Modules, but _last_failed_paths only contains
|
# Packages are Files, but we only want to skip test-bearing Files,
|
||||||
# test-bearing paths and doesn't try to include the paths of their
|
# so don't filter Packages.
|
||||||
# packages, so don't filter them.
|
if isinstance(collector, File) and not isinstance(collector, Package):
|
||||||
if isinstance(collector, Module) and not isinstance(collector, Package):
|
|
||||||
if collector.path not in self.lfplugin._last_failed_paths:
|
if collector.path not in self.lfplugin._last_failed_paths:
|
||||||
self.lfplugin._skipped_files += 1
|
self.lfplugin._skipped_files += 1
|
||||||
|
|
||||||
@@ -299,9 +312,14 @@ class LFPlugin:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_last_failed_paths(self) -> Set[Path]:
|
def get_last_failed_paths(self) -> Set[Path]:
|
||||||
"""Return a set with all Paths()s of the previously failed nodeids."""
|
"""Return a set with all Paths of the previously failed nodeids and
|
||||||
|
their parents."""
|
||||||
rootpath = self.config.rootpath
|
rootpath = self.config.rootpath
|
||||||
result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed}
|
result = set()
|
||||||
|
for nodeid in self.lastfailed:
|
||||||
|
path = rootpath / nodeid.split("::")[0]
|
||||||
|
result.add(path)
|
||||||
|
result.update(path.parents)
|
||||||
return {x for x in result if x.exists()}
|
return {x for x in result if x.exists()}
|
||||||
|
|
||||||
def pytest_report_collectionfinish(self) -> Optional[str]:
|
def pytest_report_collectionfinish(self) -> Optional[str]:
|
||||||
@@ -492,7 +510,7 @@ def pytest_addoption(parser: Parser) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
|
def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
|
||||||
if config.option.cacheshow:
|
if config.option.cacheshow and not config.option.help:
|
||||||
from _pytest.main import wrap_session
|
from _pytest.main import wrap_session
|
||||||
|
|
||||||
return wrap_session(config, cacheshow)
|
return wrap_session(config, cacheshow)
|
||||||
|
|||||||
+196
-128
@@ -1,19 +1,26 @@
|
|||||||
"""Per-test stdout/stderr capturing mechanism."""
|
"""Per-test stdout/stderr capturing mechanism."""
|
||||||
|
import abc
|
||||||
|
import collections
|
||||||
import contextlib
|
import contextlib
|
||||||
import functools
|
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from io import UnsupportedOperation
|
from io import UnsupportedOperation
|
||||||
from tempfile import TemporaryFile
|
from tempfile import TemporaryFile
|
||||||
|
from types import TracebackType
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import AnyStr
|
from typing import AnyStr
|
||||||
|
from typing import BinaryIO
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import Generic
|
from typing import Generic
|
||||||
|
from typing import Iterable
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
|
from typing import List
|
||||||
|
from typing import NamedTuple
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import TextIO
|
from typing import TextIO
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
from typing import Type
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
@@ -29,6 +36,7 @@ from _pytest.nodes import File
|
|||||||
from _pytest.nodes import Item
|
from _pytest.nodes import Item
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from typing_extensions import Final
|
||||||
from typing_extensions import Literal
|
from typing_extensions import Literal
|
||||||
|
|
||||||
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
|
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
|
||||||
@@ -185,19 +193,27 @@ class TeeCaptureIO(CaptureIO):
|
|||||||
return self._other.write(s)
|
return self._other.write(s)
|
||||||
|
|
||||||
|
|
||||||
class DontReadFromInput:
|
class DontReadFromInput(TextIO):
|
||||||
encoding = None
|
@property
|
||||||
|
def encoding(self) -> str:
|
||||||
|
return sys.__stdin__.encoding
|
||||||
|
|
||||||
def read(self, *args):
|
def read(self, size: int = -1) -> str:
|
||||||
raise OSError(
|
raise OSError(
|
||||||
"pytest: reading from stdin while output is captured! Consider using `-s`."
|
"pytest: reading from stdin while output is captured! Consider using `-s`."
|
||||||
)
|
)
|
||||||
|
|
||||||
readline = read
|
readline = read
|
||||||
readlines = read
|
|
||||||
__next__ = read
|
|
||||||
|
|
||||||
def __iter__(self):
|
def __next__(self) -> str:
|
||||||
|
return self.readline()
|
||||||
|
|
||||||
|
def readlines(self, hint: Optional[int] = -1) -> List[str]:
|
||||||
|
raise OSError(
|
||||||
|
"pytest: reading from stdin while output is captured! Consider using `-s`."
|
||||||
|
)
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[str]:
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def fileno(self) -> int:
|
def fileno(self) -> int:
|
||||||
@@ -215,7 +231,7 @@ class DontReadFromInput:
|
|||||||
def readable(self) -> bool:
|
def readable(self) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def seek(self, offset: int) -> int:
|
def seek(self, offset: int, whence: int = 0) -> int:
|
||||||
raise UnsupportedOperation("redirected stdin is pseudofile, has no seek(int)")
|
raise UnsupportedOperation("redirected stdin is pseudofile, has no seek(int)")
|
||||||
|
|
||||||
def seekable(self) -> bool:
|
def seekable(self) -> bool:
|
||||||
@@ -224,41 +240,104 @@ class DontReadFromInput:
|
|||||||
def tell(self) -> int:
|
def tell(self) -> int:
|
||||||
raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
|
raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
|
||||||
|
|
||||||
def truncate(self, size: int) -> None:
|
def truncate(self, size: Optional[int] = None) -> int:
|
||||||
raise UnsupportedOperation("cannont truncate stdin")
|
raise UnsupportedOperation("cannot truncate stdin")
|
||||||
|
|
||||||
def write(self, *args) -> None:
|
def write(self, data: str) -> int:
|
||||||
raise UnsupportedOperation("cannot write to stdin")
|
raise UnsupportedOperation("cannot write to stdin")
|
||||||
|
|
||||||
def writelines(self, *args) -> None:
|
def writelines(self, lines: Iterable[str]) -> None:
|
||||||
raise UnsupportedOperation("Cannot write to stdin")
|
raise UnsupportedOperation("Cannot write to stdin")
|
||||||
|
|
||||||
def writable(self) -> bool:
|
def writable(self) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
def __enter__(self) -> "DontReadFromInput":
|
||||||
def buffer(self):
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def __exit__(
|
||||||
|
self,
|
||||||
|
type: Optional[Type[BaseException]],
|
||||||
|
value: Optional[BaseException],
|
||||||
|
traceback: Optional[TracebackType],
|
||||||
|
) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def buffer(self) -> BinaryIO:
|
||||||
|
# The str/bytes doesn't actually matter in this type, so OK to fake.
|
||||||
|
return self # type: ignore[return-value]
|
||||||
|
|
||||||
|
|
||||||
# Capture classes.
|
# Capture classes.
|
||||||
|
|
||||||
|
|
||||||
|
class CaptureBase(abc.ABC, Generic[AnyStr]):
|
||||||
|
EMPTY_BUFFER: AnyStr
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def __init__(self, fd: int) -> None:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def start(self) -> None:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def done(self) -> None:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def suspend(self) -> None:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def resume(self) -> None:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def writeorg(self, data: AnyStr) -> None:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def snap(self) -> AnyStr:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}
|
patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}
|
||||||
|
|
||||||
|
|
||||||
class NoCapture:
|
class NoCapture(CaptureBase[str]):
|
||||||
EMPTY_BUFFER = None
|
EMPTY_BUFFER = ""
|
||||||
__init__ = start = done = suspend = resume = lambda *args: None
|
|
||||||
|
def __init__(self, fd: int) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def done(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def suspend(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def resume(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def snap(self) -> str:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def writeorg(self, data: str) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SysCaptureBinary:
|
class SysCaptureBase(CaptureBase[AnyStr]):
|
||||||
|
def __init__(
|
||||||
EMPTY_BUFFER = b""
|
self, fd: int, tmpfile: Optional[TextIO] = None, *, tee: bool = False
|
||||||
|
) -> None:
|
||||||
def __init__(self, fd: int, tmpfile=None, *, tee: bool = False) -> None:
|
|
||||||
name = patchsysdict[fd]
|
name = patchsysdict[fd]
|
||||||
self._old = getattr(sys, name)
|
self._old: TextIO = getattr(sys, name)
|
||||||
self.name = name
|
self.name = name
|
||||||
if tmpfile is None:
|
if tmpfile is None:
|
||||||
if name == "stdin":
|
if name == "stdin":
|
||||||
@@ -298,14 +377,6 @@ class SysCaptureBinary:
|
|||||||
setattr(sys, self.name, self.tmpfile)
|
setattr(sys, self.name, self.tmpfile)
|
||||||
self._state = "started"
|
self._state = "started"
|
||||||
|
|
||||||
def snap(self):
|
|
||||||
self._assert_state("snap", ("started", "suspended"))
|
|
||||||
self.tmpfile.seek(0)
|
|
||||||
res = self.tmpfile.buffer.read()
|
|
||||||
self.tmpfile.seek(0)
|
|
||||||
self.tmpfile.truncate()
|
|
||||||
return res
|
|
||||||
|
|
||||||
def done(self) -> None:
|
def done(self) -> None:
|
||||||
self._assert_state("done", ("initialized", "started", "suspended", "done"))
|
self._assert_state("done", ("initialized", "started", "suspended", "done"))
|
||||||
if self._state == "done":
|
if self._state == "done":
|
||||||
@@ -327,36 +398,43 @@ class SysCaptureBinary:
|
|||||||
setattr(sys, self.name, self.tmpfile)
|
setattr(sys, self.name, self.tmpfile)
|
||||||
self._state = "started"
|
self._state = "started"
|
||||||
|
|
||||||
def writeorg(self, data) -> None:
|
|
||||||
|
class SysCaptureBinary(SysCaptureBase[bytes]):
|
||||||
|
EMPTY_BUFFER = b""
|
||||||
|
|
||||||
|
def snap(self) -> bytes:
|
||||||
|
self._assert_state("snap", ("started", "suspended"))
|
||||||
|
self.tmpfile.seek(0)
|
||||||
|
res = self.tmpfile.buffer.read()
|
||||||
|
self.tmpfile.seek(0)
|
||||||
|
self.tmpfile.truncate()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def writeorg(self, data: bytes) -> None:
|
||||||
self._assert_state("writeorg", ("started", "suspended"))
|
self._assert_state("writeorg", ("started", "suspended"))
|
||||||
self._old.flush()
|
self._old.flush()
|
||||||
self._old.buffer.write(data)
|
self._old.buffer.write(data)
|
||||||
self._old.buffer.flush()
|
self._old.buffer.flush()
|
||||||
|
|
||||||
|
|
||||||
class SysCapture(SysCaptureBinary):
|
class SysCapture(SysCaptureBase[str]):
|
||||||
EMPTY_BUFFER = "" # type: ignore[assignment]
|
EMPTY_BUFFER = ""
|
||||||
|
|
||||||
def snap(self):
|
def snap(self) -> str:
|
||||||
|
self._assert_state("snap", ("started", "suspended"))
|
||||||
|
assert isinstance(self.tmpfile, CaptureIO)
|
||||||
res = self.tmpfile.getvalue()
|
res = self.tmpfile.getvalue()
|
||||||
self.tmpfile.seek(0)
|
self.tmpfile.seek(0)
|
||||||
self.tmpfile.truncate()
|
self.tmpfile.truncate()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def writeorg(self, data):
|
def writeorg(self, data: str) -> None:
|
||||||
self._assert_state("writeorg", ("started", "suspended"))
|
self._assert_state("writeorg", ("started", "suspended"))
|
||||||
self._old.write(data)
|
self._old.write(data)
|
||||||
self._old.flush()
|
self._old.flush()
|
||||||
|
|
||||||
|
|
||||||
class FDCaptureBinary:
|
class FDCaptureBase(CaptureBase[AnyStr]):
|
||||||
"""Capture IO to/from a given OS-level file descriptor.
|
|
||||||
|
|
||||||
snap() produces `bytes`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
EMPTY_BUFFER = b""
|
|
||||||
|
|
||||||
def __init__(self, targetfd: int) -> None:
|
def __init__(self, targetfd: int) -> None:
|
||||||
self.targetfd = targetfd
|
self.targetfd = targetfd
|
||||||
|
|
||||||
@@ -382,7 +460,7 @@ class FDCaptureBinary:
|
|||||||
|
|
||||||
if targetfd == 0:
|
if targetfd == 0:
|
||||||
self.tmpfile = open(os.devnull, encoding="utf-8")
|
self.tmpfile = open(os.devnull, encoding="utf-8")
|
||||||
self.syscapture = SysCapture(targetfd)
|
self.syscapture: CaptureBase[str] = SysCapture(targetfd)
|
||||||
else:
|
else:
|
||||||
self.tmpfile = EncodedFile(
|
self.tmpfile = EncodedFile(
|
||||||
TemporaryFile(buffering=0),
|
TemporaryFile(buffering=0),
|
||||||
@@ -394,7 +472,7 @@ class FDCaptureBinary:
|
|||||||
if targetfd in patchsysdict:
|
if targetfd in patchsysdict:
|
||||||
self.syscapture = SysCapture(targetfd, self.tmpfile)
|
self.syscapture = SysCapture(targetfd, self.tmpfile)
|
||||||
else:
|
else:
|
||||||
self.syscapture = NoCapture()
|
self.syscapture = NoCapture(targetfd)
|
||||||
|
|
||||||
self._state = "initialized"
|
self._state = "initialized"
|
||||||
|
|
||||||
@@ -421,14 +499,6 @@ class FDCaptureBinary:
|
|||||||
self.syscapture.start()
|
self.syscapture.start()
|
||||||
self._state = "started"
|
self._state = "started"
|
||||||
|
|
||||||
def snap(self):
|
|
||||||
self._assert_state("snap", ("started", "suspended"))
|
|
||||||
self.tmpfile.seek(0)
|
|
||||||
res = self.tmpfile.buffer.read()
|
|
||||||
self.tmpfile.seek(0)
|
|
||||||
self.tmpfile.truncate()
|
|
||||||
return res
|
|
||||||
|
|
||||||
def done(self) -> None:
|
def done(self) -> None:
|
||||||
"""Stop capturing, restore streams, return original capture file,
|
"""Stop capturing, restore streams, return original capture file,
|
||||||
seeked to position zero."""
|
seeked to position zero."""
|
||||||
@@ -461,22 +531,38 @@ class FDCaptureBinary:
|
|||||||
os.dup2(self.tmpfile.fileno(), self.targetfd)
|
os.dup2(self.tmpfile.fileno(), self.targetfd)
|
||||||
self._state = "started"
|
self._state = "started"
|
||||||
|
|
||||||
def writeorg(self, data):
|
|
||||||
|
class FDCaptureBinary(FDCaptureBase[bytes]):
|
||||||
|
"""Capture IO to/from a given OS-level file descriptor.
|
||||||
|
|
||||||
|
snap() produces `bytes`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
EMPTY_BUFFER = b""
|
||||||
|
|
||||||
|
def snap(self) -> bytes:
|
||||||
|
self._assert_state("snap", ("started", "suspended"))
|
||||||
|
self.tmpfile.seek(0)
|
||||||
|
res = self.tmpfile.buffer.read()
|
||||||
|
self.tmpfile.seek(0)
|
||||||
|
self.tmpfile.truncate()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def writeorg(self, data: bytes) -> None:
|
||||||
"""Write to original file descriptor."""
|
"""Write to original file descriptor."""
|
||||||
self._assert_state("writeorg", ("started", "suspended"))
|
self._assert_state("writeorg", ("started", "suspended"))
|
||||||
os.write(self.targetfd_save, data)
|
os.write(self.targetfd_save, data)
|
||||||
|
|
||||||
|
|
||||||
class FDCapture(FDCaptureBinary):
|
class FDCapture(FDCaptureBase[str]):
|
||||||
"""Capture IO to/from a given OS-level file descriptor.
|
"""Capture IO to/from a given OS-level file descriptor.
|
||||||
|
|
||||||
snap() produces text.
|
snap() produces text.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Ignore type because it doesn't match the type in the superclass (bytes).
|
EMPTY_BUFFER = ""
|
||||||
EMPTY_BUFFER = "" # type: ignore
|
|
||||||
|
|
||||||
def snap(self):
|
def snap(self) -> str:
|
||||||
self._assert_state("snap", ("started", "suspended"))
|
self._assert_state("snap", ("started", "suspended"))
|
||||||
self.tmpfile.seek(0)
|
self.tmpfile.seek(0)
|
||||||
res = self.tmpfile.read()
|
res = self.tmpfile.read()
|
||||||
@@ -484,77 +570,49 @@ class FDCapture(FDCaptureBinary):
|
|||||||
self.tmpfile.truncate()
|
self.tmpfile.truncate()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def writeorg(self, data):
|
def writeorg(self, data: str) -> None:
|
||||||
"""Write to original file descriptor."""
|
"""Write to original file descriptor."""
|
||||||
super().writeorg(data.encode("utf-8")) # XXX use encoding of original stream
|
self._assert_state("writeorg", ("started", "suspended"))
|
||||||
|
# XXX use encoding of original stream
|
||||||
|
os.write(self.targetfd_save, data.encode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
# MultiCapture
|
# MultiCapture
|
||||||
|
|
||||||
|
|
||||||
# This class was a namedtuple, but due to mypy limitation[0] it could not be
|
# Generic NamedTuple only supported since Python 3.11.
|
||||||
# made generic, so was replaced by a regular class which tries to emulate the
|
if sys.version_info >= (3, 11) or TYPE_CHECKING:
|
||||||
# pertinent parts of a namedtuple. If the mypy limitation is ever lifted, can
|
|
||||||
# make it a namedtuple again.
|
|
||||||
# [0]: https://github.com/python/mypy/issues/685
|
|
||||||
@final
|
|
||||||
@functools.total_ordering
|
|
||||||
class CaptureResult(Generic[AnyStr]):
|
|
||||||
"""The result of :method:`CaptureFixture.readouterr`."""
|
|
||||||
|
|
||||||
__slots__ = ("out", "err")
|
@final
|
||||||
|
class CaptureResult(NamedTuple, Generic[AnyStr]):
|
||||||
|
"""The result of :method:`CaptureFixture.readouterr`."""
|
||||||
|
|
||||||
def __init__(self, out: AnyStr, err: AnyStr) -> None:
|
out: AnyStr
|
||||||
self.out: AnyStr = out
|
err: AnyStr
|
||||||
self.err: AnyStr = err
|
|
||||||
|
|
||||||
def __len__(self) -> int:
|
else:
|
||||||
return 2
|
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[AnyStr]:
|
class CaptureResult(
|
||||||
return iter((self.out, self.err))
|
collections.namedtuple("CaptureResult", ["out", "err"]), Generic[AnyStr]
|
||||||
|
):
|
||||||
|
"""The result of :method:`CaptureFixture.readouterr`."""
|
||||||
|
|
||||||
def __getitem__(self, item: int) -> AnyStr:
|
__slots__ = ()
|
||||||
return tuple(self)[item]
|
|
||||||
|
|
||||||
def _replace(
|
|
||||||
self, *, out: Optional[AnyStr] = None, err: Optional[AnyStr] = None
|
|
||||||
) -> "CaptureResult[AnyStr]":
|
|
||||||
return CaptureResult(
|
|
||||||
out=self.out if out is None else out, err=self.err if err is None else err
|
|
||||||
)
|
|
||||||
|
|
||||||
def count(self, value: AnyStr) -> int:
|
|
||||||
return tuple(self).count(value)
|
|
||||||
|
|
||||||
def index(self, value) -> int:
|
|
||||||
return tuple(self).index(value)
|
|
||||||
|
|
||||||
def __eq__(self, other: object) -> bool:
|
|
||||||
if not isinstance(other, (CaptureResult, tuple)):
|
|
||||||
return NotImplemented
|
|
||||||
return tuple(self) == tuple(other)
|
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
|
||||||
return hash(tuple(self))
|
|
||||||
|
|
||||||
def __lt__(self, other: object) -> bool:
|
|
||||||
if not isinstance(other, (CaptureResult, tuple)):
|
|
||||||
return NotImplemented
|
|
||||||
return tuple(self) < tuple(other)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"CaptureResult(out={self.out!r}, err={self.err!r})"
|
|
||||||
|
|
||||||
|
|
||||||
class MultiCapture(Generic[AnyStr]):
|
class MultiCapture(Generic[AnyStr]):
|
||||||
_state = None
|
_state = None
|
||||||
_in_suspended = False
|
_in_suspended = False
|
||||||
|
|
||||||
def __init__(self, in_, out, err) -> None:
|
def __init__(
|
||||||
self.in_ = in_
|
self,
|
||||||
self.out = out
|
in_: Optional[CaptureBase[AnyStr]],
|
||||||
self.err = err
|
out: Optional[CaptureBase[AnyStr]],
|
||||||
|
err: Optional[CaptureBase[AnyStr]],
|
||||||
|
) -> None:
|
||||||
|
self.in_: Optional[CaptureBase[AnyStr]] = in_
|
||||||
|
self.out: Optional[CaptureBase[AnyStr]] = out
|
||||||
|
self.err: Optional[CaptureBase[AnyStr]] = err
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return "<MultiCapture out={!r} err={!r} in_={!r} _state={!r} _in_suspended={!r}>".format(
|
return "<MultiCapture out={!r} err={!r} in_={!r} _state={!r} _in_suspended={!r}>".format(
|
||||||
@@ -578,8 +636,10 @@ class MultiCapture(Generic[AnyStr]):
|
|||||||
"""Pop current snapshot out/err capture and flush to orig streams."""
|
"""Pop current snapshot out/err capture and flush to orig streams."""
|
||||||
out, err = self.readouterr()
|
out, err = self.readouterr()
|
||||||
if out:
|
if out:
|
||||||
|
assert self.out is not None
|
||||||
self.out.writeorg(out)
|
self.out.writeorg(out)
|
||||||
if err:
|
if err:
|
||||||
|
assert self.err is not None
|
||||||
self.err.writeorg(err)
|
self.err.writeorg(err)
|
||||||
return out, err
|
return out, err
|
||||||
|
|
||||||
@@ -600,6 +660,7 @@ class MultiCapture(Generic[AnyStr]):
|
|||||||
if self.err:
|
if self.err:
|
||||||
self.err.resume()
|
self.err.resume()
|
||||||
if self._in_suspended:
|
if self._in_suspended:
|
||||||
|
assert self.in_ is not None
|
||||||
self.in_.resume()
|
self.in_.resume()
|
||||||
self._in_suspended = False
|
self._in_suspended = False
|
||||||
|
|
||||||
@@ -622,7 +683,8 @@ class MultiCapture(Generic[AnyStr]):
|
|||||||
def readouterr(self) -> CaptureResult[AnyStr]:
|
def readouterr(self) -> CaptureResult[AnyStr]:
|
||||||
out = self.out.snap() if self.out else ""
|
out = self.out.snap() if self.out else ""
|
||||||
err = self.err.snap() if self.err else ""
|
err = self.err.snap() if self.err else ""
|
||||||
return CaptureResult(out, err)
|
# TODO: This type error is real, need to fix.
|
||||||
|
return CaptureResult(out, err) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
||||||
def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]:
|
def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]:
|
||||||
@@ -662,7 +724,7 @@ class CaptureManager:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, method: "_CaptureMethod") -> None:
|
def __init__(self, method: "_CaptureMethod") -> None:
|
||||||
self._method = method
|
self._method: Final = method
|
||||||
self._global_capturing: Optional[MultiCapture[str]] = None
|
self._global_capturing: Optional[MultiCapture[str]] = None
|
||||||
self._capture_fixture: Optional[CaptureFixture[Any]] = None
|
self._capture_fixture: Optional[CaptureFixture[Any]] = None
|
||||||
|
|
||||||
@@ -831,14 +893,18 @@ class CaptureFixture(Generic[AnyStr]):
|
|||||||
:fixture:`capfd` and :fixture:`capfdbinary` fixtures."""
|
:fixture:`capfd` and :fixture:`capfdbinary` fixtures."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, captureclass, request: SubRequest, *, _ispytest: bool = False
|
self,
|
||||||
|
captureclass: Type[CaptureBase[AnyStr]],
|
||||||
|
request: SubRequest,
|
||||||
|
*,
|
||||||
|
_ispytest: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
check_ispytest(_ispytest)
|
check_ispytest(_ispytest)
|
||||||
self.captureclass = captureclass
|
self.captureclass: Type[CaptureBase[AnyStr]] = captureclass
|
||||||
self.request = request
|
self.request = request
|
||||||
self._capture: Optional[MultiCapture[AnyStr]] = None
|
self._capture: Optional[MultiCapture[AnyStr]] = None
|
||||||
self._captured_out = self.captureclass.EMPTY_BUFFER
|
self._captured_out: AnyStr = self.captureclass.EMPTY_BUFFER
|
||||||
self._captured_err = self.captureclass.EMPTY_BUFFER
|
self._captured_err: AnyStr = self.captureclass.EMPTY_BUFFER
|
||||||
|
|
||||||
def _start(self) -> None:
|
def _start(self) -> None:
|
||||||
if self._capture is None:
|
if self._capture is None:
|
||||||
@@ -893,7 +959,9 @@ class CaptureFixture(Generic[AnyStr]):
|
|||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def disabled(self) -> Generator[None, None, None]:
|
def disabled(self) -> Generator[None, None, None]:
|
||||||
"""Temporarily disable capturing while inside the ``with`` block."""
|
"""Temporarily disable capturing while inside the ``with`` block."""
|
||||||
capmanager = self.request.config.pluginmanager.getplugin("capturemanager")
|
capmanager: CaptureManager = self.request.config.pluginmanager.getplugin(
|
||||||
|
"capturemanager"
|
||||||
|
)
|
||||||
with capmanager.global_and_fixture_disabled():
|
with capmanager.global_and_fixture_disabled():
|
||||||
yield
|
yield
|
||||||
|
|
||||||
@@ -920,8 +988,8 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
|
|||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert captured.out == "hello\n"
|
assert captured.out == "hello\n"
|
||||||
"""
|
"""
|
||||||
capman = request.config.pluginmanager.getplugin("capturemanager")
|
capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
|
||||||
capture_fixture = CaptureFixture[str](SysCapture, request, _ispytest=True)
|
capture_fixture = CaptureFixture(SysCapture, request, _ispytest=True)
|
||||||
capman.set_fixture(capture_fixture)
|
capman.set_fixture(capture_fixture)
|
||||||
capture_fixture._start()
|
capture_fixture._start()
|
||||||
yield capture_fixture
|
yield capture_fixture
|
||||||
@@ -948,8 +1016,8 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None,
|
|||||||
captured = capsysbinary.readouterr()
|
captured = capsysbinary.readouterr()
|
||||||
assert captured.out == b"hello\n"
|
assert captured.out == b"hello\n"
|
||||||
"""
|
"""
|
||||||
capman = request.config.pluginmanager.getplugin("capturemanager")
|
capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
|
||||||
capture_fixture = CaptureFixture[bytes](SysCaptureBinary, request, _ispytest=True)
|
capture_fixture = CaptureFixture(SysCaptureBinary, request, _ispytest=True)
|
||||||
capman.set_fixture(capture_fixture)
|
capman.set_fixture(capture_fixture)
|
||||||
capture_fixture._start()
|
capture_fixture._start()
|
||||||
yield capture_fixture
|
yield capture_fixture
|
||||||
@@ -976,8 +1044,8 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
|
|||||||
captured = capfd.readouterr()
|
captured = capfd.readouterr()
|
||||||
assert captured.out == "hello\n"
|
assert captured.out == "hello\n"
|
||||||
"""
|
"""
|
||||||
capman = request.config.pluginmanager.getplugin("capturemanager")
|
capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
|
||||||
capture_fixture = CaptureFixture[str](FDCapture, request, _ispytest=True)
|
capture_fixture = CaptureFixture(FDCapture, request, _ispytest=True)
|
||||||
capman.set_fixture(capture_fixture)
|
capman.set_fixture(capture_fixture)
|
||||||
capture_fixture._start()
|
capture_fixture._start()
|
||||||
yield capture_fixture
|
yield capture_fixture
|
||||||
@@ -1005,8 +1073,8 @@ def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, N
|
|||||||
assert captured.out == b"hello\n"
|
assert captured.out == b"hello\n"
|
||||||
|
|
||||||
"""
|
"""
|
||||||
capman = request.config.pluginmanager.getplugin("capturemanager")
|
capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
|
||||||
capture_fixture = CaptureFixture[bytes](FDCaptureBinary, request, _ispytest=True)
|
capture_fixture = CaptureFixture(FDCaptureBinary, request, _ispytest=True)
|
||||||
capman.set_fixture(capture_fixture)
|
capman.set_fixture(capture_fixture)
|
||||||
capture_fixture._start()
|
capture_fixture._start()
|
||||||
yield capture_fixture
|
yield capture_fixture
|
||||||
|
|||||||
+27
-18
@@ -1,4 +1,7 @@
|
|||||||
"""Python version compatibility code."""
|
"""Python version compatibility code."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
import enum
|
import enum
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
@@ -11,13 +14,8 @@ from typing import Any
|
|||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Generic
|
from typing import Generic
|
||||||
from typing import NoReturn
|
from typing import NoReturn
|
||||||
from typing import Optional
|
|
||||||
from typing import Tuple
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
import attr
|
|
||||||
|
|
||||||
import py
|
import py
|
||||||
|
|
||||||
@@ -47,7 +45,7 @@ LEGACY_PATH = py.path. local
|
|||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
|
|
||||||
def legacy_path(path: Union[str, "os.PathLike[str]"]) -> LEGACY_PATH:
|
def legacy_path(path: str | os.PathLike[str]) -> LEGACY_PATH:
|
||||||
"""Internal wrapper to prepare lazy proxies for legacy_path instances"""
|
"""Internal wrapper to prepare lazy proxies for legacy_path instances"""
|
||||||
return LEGACY_PATH(path)
|
return LEGACY_PATH(path)
|
||||||
|
|
||||||
@@ -57,7 +55,7 @@ def legacy_path(path: Union[str, "os.PathLike[str]"]) -> LEGACY_PATH:
|
|||||||
# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
|
# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
|
||||||
class NotSetType(enum.Enum):
|
class NotSetType(enum.Enum):
|
||||||
token = 0
|
token = 0
|
||||||
NOTSET: "Final" = NotSetType.token # noqa: E305
|
NOTSET: Final = NotSetType.token # noqa: E305
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
if sys.version_info >= (3, 8):
|
if sys.version_info >= (3, 8):
|
||||||
@@ -95,7 +93,7 @@ def is_async_function(func: object) -> bool:
|
|||||||
return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
|
return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
|
||||||
|
|
||||||
|
|
||||||
def getlocation(function, curdir: Optional[str] = None) -> str:
|
def getlocation(function, curdir: str | None = None) -> str:
|
||||||
function = get_real_func(function)
|
function = get_real_func(function)
|
||||||
fn = Path(inspect.getfile(function))
|
fn = Path(inspect.getfile(function))
|
||||||
lineno = function.__code__.co_firstlineno
|
lineno = function.__code__.co_firstlineno
|
||||||
@@ -133,8 +131,8 @@ def getfuncargnames(
|
|||||||
*,
|
*,
|
||||||
name: str = "",
|
name: str = "",
|
||||||
is_method: bool = False,
|
is_method: bool = False,
|
||||||
cls: Optional[type] = None,
|
cls: type | None = None,
|
||||||
) -> Tuple[str, ...]:
|
) -> tuple[str, ...]:
|
||||||
"""Return the names of a function's mandatory arguments.
|
"""Return the names of a function's mandatory arguments.
|
||||||
|
|
||||||
Should return the names of all function arguments that:
|
Should return the names of all function arguments that:
|
||||||
@@ -198,7 +196,7 @@ def getfuncargnames(
|
|||||||
return arg_names
|
return arg_names
|
||||||
|
|
||||||
|
|
||||||
def get_default_arg_names(function: Callable[..., Any]) -> Tuple[str, ...]:
|
def get_default_arg_names(function: Callable[..., Any]) -> tuple[str, ...]:
|
||||||
# Note: this code intentionally mirrors the code at the beginning of
|
# Note: this code intentionally mirrors the code at the beginning of
|
||||||
# getfuncargnames, to get the arguments which were excluded from its result
|
# getfuncargnames, to get the arguments which were excluded from its result
|
||||||
# because they had default values.
|
# because they had default values.
|
||||||
@@ -229,7 +227,7 @@ def _bytes_to_ascii(val: bytes) -> str:
|
|||||||
return val.decode("ascii", "backslashreplace")
|
return val.decode("ascii", "backslashreplace")
|
||||||
|
|
||||||
|
|
||||||
def ascii_escaped(val: Union[bytes, str]) -> str:
|
def ascii_escaped(val: bytes | str) -> str:
|
||||||
r"""If val is pure ASCII, return it as an str, otherwise, escape
|
r"""If val is pure ASCII, return it as an str, otherwise, escape
|
||||||
bytes objects into a sequence of escaped bytes:
|
bytes objects into a sequence of escaped bytes:
|
||||||
|
|
||||||
@@ -253,7 +251,7 @@ def ascii_escaped(val: Union[bytes, str]) -> str:
|
|||||||
return _translate_non_printable(ret)
|
return _translate_non_printable(ret)
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@dataclasses.dataclass
|
||||||
class _PytestWrapper:
|
class _PytestWrapper:
|
||||||
"""Dummy wrapper around a function object for internal use only.
|
"""Dummy wrapper around a function object for internal use only.
|
||||||
|
|
||||||
@@ -262,7 +260,7 @@ class _PytestWrapper:
|
|||||||
decorator to issue warnings when the fixture function is called directly.
|
decorator to issue warnings when the fixture function is called directly.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
obj = attr.ib()
|
obj: Any
|
||||||
|
|
||||||
|
|
||||||
def get_real_func(obj):
|
def get_real_func(obj):
|
||||||
@@ -356,7 +354,6 @@ else:
|
|||||||
if sys.version_info >= (3, 8):
|
if sys.version_info >= (3, 8):
|
||||||
from functools import cached_property as cached_property
|
from functools import cached_property as cached_property
|
||||||
else:
|
else:
|
||||||
from typing import Type
|
|
||||||
|
|
||||||
class cached_property(Generic[_S, _T]):
|
class cached_property(Generic[_S, _T]):
|
||||||
__slots__ = ("func", "__doc__")
|
__slots__ = ("func", "__doc__")
|
||||||
@@ -367,12 +364,12 @@ else:
|
|||||||
|
|
||||||
@overload
|
@overload
|
||||||
def __get__(
|
def __get__(
|
||||||
self, instance: None, owner: Optional[Type[_S]] = ...
|
self, instance: None, owner: type[_S] | None = ...
|
||||||
) -> "cached_property[_S, _T]":
|
) -> cached_property[_S, _T]:
|
||||||
...
|
...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def __get__(self, instance: _S, owner: Optional[Type[_S]] = ...) -> _T:
|
def __get__(self, instance: _S, owner: type[_S] | None = ...) -> _T:
|
||||||
...
|
...
|
||||||
|
|
||||||
def __get__(self, instance, owner=None):
|
def __get__(self, instance, owner=None):
|
||||||
@@ -382,6 +379,18 @@ else:
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_id() -> int | None:
|
||||||
|
"""Return the current user id, or None if we cannot get it reliably on the current platform."""
|
||||||
|
# win32 does not have a getuid() function.
|
||||||
|
# On Emscripten, getuid() is a stub that always returns 0.
|
||||||
|
if sys.platform in ("win32", "emscripten"):
|
||||||
|
return None
|
||||||
|
# getuid shouldn't fail, but cpython defines such a case.
|
||||||
|
# Let's hope for the best.
|
||||||
|
uid = os.getuid()
|
||||||
|
return uid if uid != -1 else None
|
||||||
|
|
||||||
|
|
||||||
# Perform exhaustiveness checking.
|
# Perform exhaustiveness checking.
|
||||||
#
|
#
|
||||||
# Consider this example:
|
# Consider this example:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import collections.abc
|
import collections.abc
|
||||||
import copy
|
import copy
|
||||||
|
import dataclasses
|
||||||
import enum
|
import enum
|
||||||
import glob
|
import glob
|
||||||
import inspect
|
import inspect
|
||||||
@@ -34,7 +35,6 @@ from typing import Type
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import attr
|
|
||||||
from pluggy import HookimplMarker
|
from pluggy import HookimplMarker
|
||||||
from pluggy import HookspecMarker
|
from pluggy import HookspecMarker
|
||||||
from pluggy import PluginManager
|
from pluggy import PluginManager
|
||||||
@@ -49,7 +49,7 @@ from _pytest._code import ExceptionInfo
|
|||||||
from _pytest._code import filter_traceback
|
from _pytest._code import filter_traceback
|
||||||
from _pytest._io import TerminalWriter
|
from _pytest._io import TerminalWriter
|
||||||
from _pytest.compat import final
|
from _pytest.compat import final
|
||||||
from _pytest.compat import importlib_metadata
|
from _pytest.compat import importlib_metadata # type: ignore[attr-defined]
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
from _pytest.outcomes import Skipped
|
from _pytest.outcomes import Skipped
|
||||||
from _pytest.pathlib import absolutepath
|
from _pytest.pathlib import absolutepath
|
||||||
@@ -62,7 +62,6 @@ from _pytest.warning_types import PytestConfigWarning
|
|||||||
from _pytest.warning_types import warn_explicit_for
|
from _pytest.warning_types import warn_explicit_for
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
from _pytest._code.code import _TracebackStyle
|
from _pytest._code.code import _TracebackStyle
|
||||||
from _pytest.terminal import TerminalReporter
|
from _pytest.terminal import TerminalReporter
|
||||||
from .argparsing import Argument
|
from .argparsing import Argument
|
||||||
@@ -527,7 +526,10 @@ class PytestPluginManager(PluginManager):
|
|||||||
# Internal API for local conftest plugin handling.
|
# Internal API for local conftest plugin handling.
|
||||||
#
|
#
|
||||||
def _set_initial_conftests(
|
def _set_initial_conftests(
|
||||||
self, namespace: argparse.Namespace, rootpath: Path
|
self,
|
||||||
|
namespace: argparse.Namespace,
|
||||||
|
rootpath: Path,
|
||||||
|
testpaths_ini: Sequence[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Load initial conftest files given a preparsed "namespace".
|
"""Load initial conftest files given a preparsed "namespace".
|
||||||
|
|
||||||
@@ -544,7 +546,7 @@ class PytestPluginManager(PluginManager):
|
|||||||
)
|
)
|
||||||
self._noconftest = namespace.noconftest
|
self._noconftest = namespace.noconftest
|
||||||
self._using_pyargs = namespace.pyargs
|
self._using_pyargs = namespace.pyargs
|
||||||
testpaths = namespace.file_or_dir
|
testpaths = namespace.file_or_dir + testpaths_ini
|
||||||
foundanchor = False
|
foundanchor = False
|
||||||
for testpath in testpaths:
|
for testpath in testpaths:
|
||||||
path = str(testpath)
|
path = str(testpath)
|
||||||
@@ -553,7 +555,14 @@ class PytestPluginManager(PluginManager):
|
|||||||
if i != -1:
|
if i != -1:
|
||||||
path = path[:i]
|
path = path[:i]
|
||||||
anchor = absolutepath(current / path)
|
anchor = absolutepath(current / path)
|
||||||
if anchor.exists(): # we found some file object
|
|
||||||
|
# Ensure we do not break if what appears to be an anchor
|
||||||
|
# is in fact a very long option (#10169).
|
||||||
|
try:
|
||||||
|
anchor_exists = anchor.exists()
|
||||||
|
except OSError: # pragma: no cover
|
||||||
|
anchor_exists = False
|
||||||
|
if anchor_exists:
|
||||||
self._try_load_conftest(anchor, namespace.importmode, rootpath)
|
self._try_load_conftest(anchor, namespace.importmode, rootpath)
|
||||||
foundanchor = True
|
foundanchor = True
|
||||||
if not foundanchor:
|
if not foundanchor:
|
||||||
@@ -697,6 +706,7 @@ class PytestPluginManager(PluginManager):
|
|||||||
parg = opt[2:]
|
parg = opt[2:]
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
parg = parg.strip()
|
||||||
if exclude_only and not parg.startswith("no:"):
|
if exclude_only and not parg.startswith("no:"):
|
||||||
continue
|
continue
|
||||||
self.consider_pluginarg(parg)
|
self.consider_pluginarg(parg)
|
||||||
@@ -886,10 +896,6 @@ def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]:
|
|||||||
yield from _iter_rewritable_modules(new_package_files)
|
yield from _iter_rewritable_modules(new_package_files)
|
||||||
|
|
||||||
|
|
||||||
def _args_converter(args: Iterable[str]) -> Tuple[str, ...]:
|
|
||||||
return tuple(args)
|
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
class Config:
|
class Config:
|
||||||
"""Access to configuration values, pluginmanager and plugin hooks.
|
"""Access to configuration values, pluginmanager and plugin hooks.
|
||||||
@@ -903,7 +909,7 @@ class Config:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@attr.s(frozen=True, auto_attribs=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class InvocationParams:
|
class InvocationParams:
|
||||||
"""Holds parameters passed during :func:`pytest.main`.
|
"""Holds parameters passed during :func:`pytest.main`.
|
||||||
|
|
||||||
@@ -919,13 +925,24 @@ class Config:
|
|||||||
Plugins accessing ``InvocationParams`` must be aware of that.
|
Plugins accessing ``InvocationParams`` must be aware of that.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
args: Tuple[str, ...] = attr.ib(converter=_args_converter)
|
args: Tuple[str, ...]
|
||||||
"""The command-line arguments as passed to :func:`pytest.main`."""
|
"""The command-line arguments as passed to :func:`pytest.main`."""
|
||||||
plugins: Optional[Sequence[Union[str, _PluggyPlugin]]]
|
plugins: Optional[Sequence[Union[str, _PluggyPlugin]]]
|
||||||
"""Extra plugins, might be `None`."""
|
"""Extra plugins, might be `None`."""
|
||||||
dir: Path
|
dir: Path
|
||||||
"""The directory from which :func:`pytest.main` was invoked."""
|
"""The directory from which :func:`pytest.main` was invoked."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
args: Iterable[str],
|
||||||
|
plugins: Optional[Sequence[Union[str, _PluggyPlugin]]],
|
||||||
|
dir: Path,
|
||||||
|
) -> None:
|
||||||
|
object.__setattr__(self, "args", tuple(args))
|
||||||
|
object.__setattr__(self, "plugins", plugins)
|
||||||
|
object.__setattr__(self, "dir", dir)
|
||||||
|
|
||||||
class ArgsSource(enum.Enum):
|
class ArgsSource(enum.Enum):
|
||||||
"""Indicates the source of the test arguments.
|
"""Indicates the source of the test arguments.
|
||||||
|
|
||||||
@@ -998,6 +1015,8 @@ class Config:
|
|||||||
self.hook.pytest_addoption.call_historic(
|
self.hook.pytest_addoption.call_historic(
|
||||||
kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager)
|
kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager)
|
||||||
)
|
)
|
||||||
|
self.args_source = Config.ArgsSource.ARGS
|
||||||
|
self.args: List[str] = []
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from _pytest.cacheprovider import Cache
|
from _pytest.cacheprovider import Cache
|
||||||
@@ -1057,7 +1076,6 @@ class Config:
|
|||||||
try:
|
try:
|
||||||
self.parse(args)
|
self.parse(args)
|
||||||
except UsageError:
|
except UsageError:
|
||||||
|
|
||||||
# Handle --version and --help here in a minimal fashion.
|
# Handle --version and --help here in a minimal fashion.
|
||||||
# This gets done via helpconfig normally, but its
|
# This gets done via helpconfig normally, but its
|
||||||
# pytest_cmdline_main is not called in case of errors.
|
# pytest_cmdline_main is not called in case of errors.
|
||||||
@@ -1123,7 +1141,9 @@ class Config:
|
|||||||
@hookimpl(trylast=True)
|
@hookimpl(trylast=True)
|
||||||
def pytest_load_initial_conftests(self, early_config: "Config") -> None:
|
def pytest_load_initial_conftests(self, early_config: "Config") -> None:
|
||||||
self.pluginmanager._set_initial_conftests(
|
self.pluginmanager._set_initial_conftests(
|
||||||
early_config.known_args_namespace, rootpath=early_config.rootpath
|
early_config.known_args_namespace,
|
||||||
|
rootpath=early_config.rootpath,
|
||||||
|
testpaths_ini=self.getini("testpaths"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _initini(self, args: Sequence[str]) -> None:
|
def _initini(self, args: Sequence[str]) -> None:
|
||||||
@@ -1241,8 +1261,11 @@ class Config:
|
|||||||
_pytest.deprecated.STRICT_OPTION, stacklevel=2
|
_pytest.deprecated.STRICT_OPTION, stacklevel=2
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.known_args_namespace.confcutdir is None and self.inipath is not None:
|
if self.known_args_namespace.confcutdir is None:
|
||||||
confcutdir = str(self.inipath.parent)
|
if self.inipath is not None:
|
||||||
|
confcutdir = str(self.inipath.parent)
|
||||||
|
else:
|
||||||
|
confcutdir = str(self.rootpath)
|
||||||
self.known_args_namespace.confcutdir = confcutdir
|
self.known_args_namespace.confcutdir = confcutdir
|
||||||
try:
|
try:
|
||||||
self.hook.pytest_load_initial_conftests(
|
self.hook.pytest_load_initial_conftests(
|
||||||
@@ -1337,8 +1360,8 @@ class Config:
|
|||||||
|
|
||||||
def parse(self, args: List[str], addopts: bool = True) -> None:
|
def parse(self, args: List[str], addopts: bool = True) -> None:
|
||||||
# Parse given cmdline arguments into this config object.
|
# Parse given cmdline arguments into this config object.
|
||||||
assert not hasattr(
|
assert (
|
||||||
self, "args"
|
self.args == []
|
||||||
), "can only parse cmdline args at most once per Config object"
|
), "can only parse cmdline args at most once per Config object"
|
||||||
self.hook.pytest_addhooks.call_historic(
|
self.hook.pytest_addhooks.call_historic(
|
||||||
kwargs=dict(pluginmanager=self.pluginmanager)
|
kwargs=dict(pluginmanager=self.pluginmanager)
|
||||||
@@ -1362,6 +1385,15 @@ class Config:
|
|||||||
args = []
|
args = []
|
||||||
for path in testpaths:
|
for path in testpaths:
|
||||||
args.extend(sorted(glob.iglob(path, recursive=True)))
|
args.extend(sorted(glob.iglob(path, recursive=True)))
|
||||||
|
if testpaths and not args:
|
||||||
|
warning_text = (
|
||||||
|
"No files were found in testpaths; "
|
||||||
|
"consider removing or adjusting your testpaths configuration. "
|
||||||
|
"Searching recursively from the current directory instead."
|
||||||
|
)
|
||||||
|
self.issue_config_time_warning(
|
||||||
|
PytestConfigWarning(warning_text), stacklevel=3
|
||||||
|
)
|
||||||
if not args:
|
if not args:
|
||||||
source = Config.ArgsSource.INCOVATION_DIR
|
source = Config.ArgsSource.INCOVATION_DIR
|
||||||
args = [str(self.invocation_params.dir)]
|
args = [str(self.invocation_params.dir)]
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ class PathAwareHookProxy:
|
|||||||
|
|
||||||
@_wraps(hook)
|
@_wraps(hook)
|
||||||
def fixed_hook(**kw):
|
def fixed_hook(**kw):
|
||||||
|
|
||||||
path_value: Optional[Path] = kw.pop(path_var, None)
|
path_value: Optional[Path] = kw.pop(path_var, None)
|
||||||
fspath_value: Optional[LEGACY_PATH] = kw.pop(fspath_var, None)
|
fspath_value: Optional[LEGACY_PATH] = kw.pop(fspath_var, None)
|
||||||
if fspath_value is not None:
|
if fspath_value is not None:
|
||||||
|
|||||||
@@ -203,8 +203,7 @@ def determine_setup(
|
|||||||
else:
|
else:
|
||||||
cwd = Path.cwd()
|
cwd = Path.cwd()
|
||||||
rootdir = get_common_ancestor([cwd, ancestor])
|
rootdir = get_common_ancestor([cwd, ancestor])
|
||||||
is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/"
|
if is_fs_root(rootdir):
|
||||||
if is_fs_root:
|
|
||||||
rootdir = ancestor
|
rootdir = ancestor
|
||||||
if rootdir_cmd_arg:
|
if rootdir_cmd_arg:
|
||||||
rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg))
|
rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg))
|
||||||
@@ -216,3 +215,11 @@ def determine_setup(
|
|||||||
)
|
)
|
||||||
assert rootdir is not None
|
assert rootdir is not None
|
||||||
return rootdir, inipath, inicfg or {}
|
return rootdir, inipath, inicfg or {}
|
||||||
|
|
||||||
|
|
||||||
|
def is_fs_root(p: Path) -> bool:
|
||||||
|
r"""
|
||||||
|
Return True if the given path is pointing to the root of the
|
||||||
|
file system ("/" on Unix and "C:\\" on Windows for example).
|
||||||
|
"""
|
||||||
|
return os.path.splitdrive(str(p))[1] == os.sep
|
||||||
|
|||||||
@@ -531,7 +531,6 @@ class DoctestModule(Module):
|
|||||||
if _is_mocked(obj):
|
if _is_mocked(obj):
|
||||||
return
|
return
|
||||||
with _patch_unwrap_mock_aware():
|
with _patch_unwrap_mock_aware():
|
||||||
|
|
||||||
# Type ignored because this is a private function.
|
# Type ignored because this is a private function.
|
||||||
super()._find( # type:ignore[misc]
|
super()._find( # type:ignore[misc]
|
||||||
tests, obj, name, module, source_lines, globs, seen
|
tests, obj, name, module, source_lines, globs, seen
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import io
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import TextIO
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
@@ -11,7 +10,7 @@ from _pytest.nodes import Item
|
|||||||
from _pytest.stash import StashKey
|
from _pytest.stash import StashKey
|
||||||
|
|
||||||
|
|
||||||
fault_handler_stderr_key = StashKey[TextIO]()
|
fault_handler_stderr_fd_key = StashKey[int]()
|
||||||
fault_handler_originally_enabled_key = StashKey[bool]()
|
fault_handler_originally_enabled_key = StashKey[bool]()
|
||||||
|
|
||||||
|
|
||||||
@@ -26,10 +25,9 @@ def pytest_addoption(parser: Parser) -> None:
|
|||||||
def pytest_configure(config: Config) -> None:
|
def pytest_configure(config: Config) -> None:
|
||||||
import faulthandler
|
import faulthandler
|
||||||
|
|
||||||
stderr_fd_copy = os.dup(get_stderr_fileno())
|
config.stash[fault_handler_stderr_fd_key] = os.dup(get_stderr_fileno())
|
||||||
config.stash[fault_handler_stderr_key] = open(stderr_fd_copy, "w")
|
|
||||||
config.stash[fault_handler_originally_enabled_key] = faulthandler.is_enabled()
|
config.stash[fault_handler_originally_enabled_key] = faulthandler.is_enabled()
|
||||||
faulthandler.enable(file=config.stash[fault_handler_stderr_key])
|
faulthandler.enable(file=config.stash[fault_handler_stderr_fd_key])
|
||||||
|
|
||||||
|
|
||||||
def pytest_unconfigure(config: Config) -> None:
|
def pytest_unconfigure(config: Config) -> None:
|
||||||
@@ -37,9 +35,9 @@ def pytest_unconfigure(config: Config) -> None:
|
|||||||
|
|
||||||
faulthandler.disable()
|
faulthandler.disable()
|
||||||
# Close the dup file installed during pytest_configure.
|
# Close the dup file installed during pytest_configure.
|
||||||
if fault_handler_stderr_key in config.stash:
|
if fault_handler_stderr_fd_key in config.stash:
|
||||||
config.stash[fault_handler_stderr_key].close()
|
os.close(config.stash[fault_handler_stderr_fd_key])
|
||||||
del config.stash[fault_handler_stderr_key]
|
del config.stash[fault_handler_stderr_fd_key]
|
||||||
if config.stash.get(fault_handler_originally_enabled_key, False):
|
if config.stash.get(fault_handler_originally_enabled_key, False):
|
||||||
# Re-enable the faulthandler if it was originally enabled.
|
# Re-enable the faulthandler if it was originally enabled.
|
||||||
faulthandler.enable(file=get_stderr_fileno())
|
faulthandler.enable(file=get_stderr_fileno())
|
||||||
@@ -67,10 +65,10 @@ def get_timeout_config_value(config: Config) -> float:
|
|||||||
@pytest.hookimpl(hookwrapper=True, trylast=True)
|
@pytest.hookimpl(hookwrapper=True, trylast=True)
|
||||||
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
|
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
|
||||||
timeout = get_timeout_config_value(item.config)
|
timeout = get_timeout_config_value(item.config)
|
||||||
stderr = item.config.stash[fault_handler_stderr_key]
|
if timeout > 0:
|
||||||
if timeout > 0 and stderr is not None:
|
|
||||||
import faulthandler
|
import faulthandler
|
||||||
|
|
||||||
|
stderr = item.config.stash[fault_handler_stderr_fd_key]
|
||||||
faulthandler.dump_traceback_later(timeout, file=stderr)
|
faulthandler.dump_traceback_later(timeout, file=stderr)
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
|
|||||||
+89
-48
@@ -1,3 +1,4 @@
|
|||||||
|
import dataclasses
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
@@ -28,8 +29,6 @@ from typing import TYPE_CHECKING
|
|||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import attr
|
|
||||||
|
|
||||||
import _pytest
|
import _pytest
|
||||||
from _pytest import nodes
|
from _pytest import nodes
|
||||||
from _pytest._code import getfslineno
|
from _pytest._code import getfslineno
|
||||||
@@ -47,6 +46,7 @@ from _pytest.compat import getimfunc
|
|||||||
from _pytest.compat import getlocation
|
from _pytest.compat import getlocation
|
||||||
from _pytest.compat import is_generator
|
from _pytest.compat import is_generator
|
||||||
from _pytest.compat import NOTSET
|
from _pytest.compat import NOTSET
|
||||||
|
from _pytest.compat import NotSetType
|
||||||
from _pytest.compat import overload
|
from _pytest.compat import overload
|
||||||
from _pytest.compat import safe_getattr
|
from _pytest.compat import safe_getattr
|
||||||
from _pytest.config import _PluggyPlugin
|
from _pytest.config import _PluggyPlugin
|
||||||
@@ -58,6 +58,7 @@ from _pytest.mark import Mark
|
|||||||
from _pytest.mark import ParameterSet
|
from _pytest.mark import ParameterSet
|
||||||
from _pytest.mark.structures import MarkDecorator
|
from _pytest.mark.structures import MarkDecorator
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
|
from _pytest.outcomes import skip
|
||||||
from _pytest.outcomes import TEST_OUTCOME
|
from _pytest.outcomes import TEST_OUTCOME
|
||||||
from _pytest.pathlib import absolutepath
|
from _pytest.pathlib import absolutepath
|
||||||
from _pytest.pathlib import bestrelpath
|
from _pytest.pathlib import bestrelpath
|
||||||
@@ -102,7 +103,7 @@ _FixtureCachedResult = Union[
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@attr.s(frozen=True, auto_attribs=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class PseudoFixtureDef(Generic[FixtureValue]):
|
class PseudoFixtureDef(Generic[FixtureValue]):
|
||||||
cached_result: "_FixtureCachedResult[FixtureValue]"
|
cached_result: "_FixtureCachedResult[FixtureValue]"
|
||||||
_scope: Scope
|
_scope: Scope
|
||||||
@@ -112,16 +113,18 @@ def pytest_sessionstart(session: "Session") -> None:
|
|||||||
session._fixturemanager = FixtureManager(session)
|
session._fixturemanager = FixtureManager(session)
|
||||||
|
|
||||||
|
|
||||||
def get_scope_package(node, fixturedef: "FixtureDef[object]"):
|
def get_scope_package(
|
||||||
import pytest
|
node: nodes.Item,
|
||||||
|
fixturedef: "FixtureDef[object]",
|
||||||
|
) -> Optional[Union[nodes.Item, nodes.Collector]]:
|
||||||
|
from _pytest.python import Package
|
||||||
|
|
||||||
cls = pytest.Package
|
current: Optional[Union[nodes.Item, nodes.Collector]] = node
|
||||||
current = node
|
|
||||||
fixture_package_name = "{}/{}".format(fixturedef.baseid, "__init__.py")
|
fixture_package_name = "{}/{}".format(fixturedef.baseid, "__init__.py")
|
||||||
while current and (
|
while current and (
|
||||||
type(current) is not cls or fixture_package_name != current.nodeid
|
not isinstance(current, Package) or fixture_package_name != current.nodeid
|
||||||
):
|
):
|
||||||
current = current.parent
|
current = current.parent # type: ignore[assignment]
|
||||||
if current is None:
|
if current is None:
|
||||||
return node.session
|
return node.session
|
||||||
return current
|
return current
|
||||||
@@ -349,8 +352,10 @@ def get_direct_param_fixture_func(request: "FixtureRequest") -> Any:
|
|||||||
return request.param
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
@attr.s(slots=True, auto_attribs=True)
|
@dataclasses.dataclass
|
||||||
class FuncFixtureInfo:
|
class FuncFixtureInfo:
|
||||||
|
__slots__ = ("argnames", "initialnames", "names_closure", "name2fixturedefs")
|
||||||
|
|
||||||
# Original function argument names.
|
# Original function argument names.
|
||||||
argnames: Tuple[str, ...]
|
argnames: Tuple[str, ...]
|
||||||
# Argnames that function immediately requires. These include argnames +
|
# Argnames that function immediately requires. These include argnames +
|
||||||
@@ -432,7 +437,23 @@ class FixtureRequest:
|
|||||||
@property
|
@property
|
||||||
def node(self):
|
def node(self):
|
||||||
"""Underlying collection node (depends on current request scope)."""
|
"""Underlying collection node (depends on current request scope)."""
|
||||||
return self._getscopeitem(self._scope)
|
scope = self._scope
|
||||||
|
if scope is Scope.Function:
|
||||||
|
# This might also be a non-function Item despite its attribute name.
|
||||||
|
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
|
||||||
|
elif scope is Scope.Package:
|
||||||
|
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
|
||||||
|
# but on FixtureRequest (a subclass).
|
||||||
|
node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
|
||||||
|
else:
|
||||||
|
node = get_scope_node(self._pyfuncitem, scope)
|
||||||
|
if node is None and scope is Scope.Class:
|
||||||
|
# Fallback to function item itself.
|
||||||
|
node = self._pyfuncitem
|
||||||
|
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
|
||||||
|
scope, self._pyfuncitem
|
||||||
|
)
|
||||||
|
return node
|
||||||
|
|
||||||
def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
|
def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
|
||||||
fixturedefs = self._arg2fixturedefs.get(argname, None)
|
fixturedefs = self._arg2fixturedefs.get(argname, None)
|
||||||
@@ -516,11 +537,7 @@ class FixtureRequest:
|
|||||||
"""Add finalizer/teardown function to be called without arguments after
|
"""Add finalizer/teardown function to be called without arguments after
|
||||||
the last test within the requesting test context finished execution."""
|
the last test within the requesting test context finished execution."""
|
||||||
# XXX usually this method is shadowed by fixturedef specific ones.
|
# XXX usually this method is shadowed by fixturedef specific ones.
|
||||||
self._addfinalizer(finalizer, scope=self.scope)
|
self.node.addfinalizer(finalizer)
|
||||||
|
|
||||||
def _addfinalizer(self, finalizer: Callable[[], object], scope) -> None:
|
|
||||||
node = self._getscopeitem(scope)
|
|
||||||
node.addfinalizer(finalizer)
|
|
||||||
|
|
||||||
def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
|
def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
|
||||||
"""Apply a marker to a single test function invocation.
|
"""Apply a marker to a single test function invocation.
|
||||||
@@ -715,28 +732,6 @@ class FixtureRequest:
|
|||||||
lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args))
|
lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args))
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
def _getscopeitem(
|
|
||||||
self, scope: Union[Scope, "_ScopeName"]
|
|
||||||
) -> Union[nodes.Item, nodes.Collector]:
|
|
||||||
if isinstance(scope, str):
|
|
||||||
scope = Scope(scope)
|
|
||||||
if scope is Scope.Function:
|
|
||||||
# This might also be a non-function Item despite its attribute name.
|
|
||||||
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
|
|
||||||
elif scope is Scope.Package:
|
|
||||||
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
|
|
||||||
# but on FixtureRequest (a subclass).
|
|
||||||
node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
|
|
||||||
else:
|
|
||||||
node = get_scope_node(self._pyfuncitem, scope)
|
|
||||||
if node is None and scope is Scope.Class:
|
|
||||||
# Fallback to function item itself.
|
|
||||||
node = self._pyfuncitem
|
|
||||||
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
|
|
||||||
scope, self._pyfuncitem
|
|
||||||
)
|
|
||||||
return node
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return "<FixtureRequest for %r>" % (self.node)
|
return "<FixtureRequest for %r>" % (self.node)
|
||||||
|
|
||||||
@@ -1129,6 +1124,10 @@ def pytest_fixture_setup(
|
|||||||
except TEST_OUTCOME:
|
except TEST_OUTCOME:
|
||||||
exc_info = sys.exc_info()
|
exc_info = sys.exc_info()
|
||||||
assert exc_info[0] is not None
|
assert exc_info[0] is not None
|
||||||
|
if isinstance(
|
||||||
|
exc_info[1], skip.Exception
|
||||||
|
) and not fixturefunc.__name__.startswith("xunit_setup"):
|
||||||
|
exc_info[1]._use_item_location = True # type: ignore[attr-defined]
|
||||||
fixturedef.cached_result = (None, my_cache_key, exc_info)
|
fixturedef.cached_result = (None, my_cache_key, exc_info)
|
||||||
raise
|
raise
|
||||||
fixturedef.cached_result = (result, my_cache_key, None)
|
fixturedef.cached_result = (result, my_cache_key, None)
|
||||||
@@ -1176,19 +1175,21 @@ def wrap_function_to_error_out_if_called_directly(
|
|||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@attr.s(frozen=True, auto_attribs=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class FixtureFunctionMarker:
|
class FixtureFunctionMarker:
|
||||||
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]"
|
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]"
|
||||||
params: Optional[Tuple[object, ...]] = attr.ib(converter=_params_converter)
|
params: Optional[Tuple[object, ...]]
|
||||||
autouse: bool = False
|
autouse: bool = False
|
||||||
ids: Optional[
|
ids: Optional[
|
||||||
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
|
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
|
||||||
] = attr.ib(
|
] = None
|
||||||
default=None,
|
|
||||||
converter=_ensure_immutable_ids,
|
|
||||||
)
|
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
|
|
||||||
|
_ispytest: dataclasses.InitVar[bool] = False
|
||||||
|
|
||||||
|
def __post_init__(self, _ispytest: bool) -> None:
|
||||||
|
check_ispytest(_ispytest)
|
||||||
|
|
||||||
def __call__(self, function: FixtureFunction) -> FixtureFunction:
|
def __call__(self, function: FixtureFunction) -> FixtureFunction:
|
||||||
if inspect.isclass(function):
|
if inspect.isclass(function):
|
||||||
raise ValueError("class fixtures not supported (maybe in the future)")
|
raise ValueError("class fixtures not supported (maybe in the future)")
|
||||||
@@ -1308,10 +1309,11 @@ def fixture( # noqa: F811
|
|||||||
"""
|
"""
|
||||||
fixture_marker = FixtureFunctionMarker(
|
fixture_marker = FixtureFunctionMarker(
|
||||||
scope=scope,
|
scope=scope,
|
||||||
params=params,
|
params=tuple(params) if params is not None else None,
|
||||||
autouse=autouse,
|
autouse=autouse,
|
||||||
ids=ids,
|
ids=None if ids is None else ids if callable(ids) else tuple(ids),
|
||||||
name=name,
|
name=name,
|
||||||
|
_ispytest=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Direct decoration.
|
# Direct decoration.
|
||||||
@@ -1584,13 +1586,52 @@ class FixtureManager:
|
|||||||
# Separate parametrized setups.
|
# Separate parametrized setups.
|
||||||
items[:] = reorder_items(items)
|
items[:] = reorder_items(items)
|
||||||
|
|
||||||
|
@overload
|
||||||
def parsefactories(
|
def parsefactories(
|
||||||
self, node_or_obj, nodeid=NOTSET, unittest: bool = False
|
self,
|
||||||
|
node_or_obj: nodes.Node,
|
||||||
|
*,
|
||||||
|
unittest: bool = ...,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def parsefactories( # noqa: F811
|
||||||
|
self,
|
||||||
|
node_or_obj: object,
|
||||||
|
nodeid: Optional[str],
|
||||||
|
*,
|
||||||
|
unittest: bool = ...,
|
||||||
|
) -> None:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def parsefactories( # noqa: F811
|
||||||
|
self,
|
||||||
|
node_or_obj: Union[nodes.Node, object],
|
||||||
|
nodeid: Union[str, NotSetType, None] = NOTSET,
|
||||||
|
*,
|
||||||
|
unittest: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""Collect fixtures from a collection node or object.
|
||||||
|
|
||||||
|
Found fixtures are parsed into `FixtureDef`s and saved.
|
||||||
|
|
||||||
|
If `node_or_object` is a collection node (with an underlying Python
|
||||||
|
object), the node's object is traversed and the node's nodeid is used to
|
||||||
|
determine the fixtures' visibilty. `nodeid` must not be specified in
|
||||||
|
this case.
|
||||||
|
|
||||||
|
If `node_or_object` is an object (e.g. a plugin), the object is
|
||||||
|
traversed and the given `nodeid` is used to determine the fixtures'
|
||||||
|
visibility. `nodeid` must be specified in this case; None and "" mean
|
||||||
|
total visibility.
|
||||||
|
"""
|
||||||
if nodeid is not NOTSET:
|
if nodeid is not NOTSET:
|
||||||
holderobj = node_or_obj
|
holderobj = node_or_obj
|
||||||
else:
|
else:
|
||||||
holderobj = node_or_obj.obj
|
assert isinstance(node_or_obj, nodes.Node)
|
||||||
|
holderobj = cast(object, node_or_obj.obj) # type: ignore[attr-defined]
|
||||||
|
assert isinstance(node_or_obj.nodeid, str)
|
||||||
nodeid = node_or_obj.nodeid
|
nodeid = node_or_obj.nodeid
|
||||||
if holderobj in self._holderobjseen:
|
if holderobj in self._holderobjseen:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -164,7 +164,8 @@ def showhelp(config: Config) -> None:
|
|||||||
tw.write(config._parser.optparser.format_help())
|
tw.write(config._parser.optparser.format_help())
|
||||||
tw.line()
|
tw.line()
|
||||||
tw.line(
|
tw.line(
|
||||||
"[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg file found:"
|
"[pytest] ini-options in the first "
|
||||||
|
"pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:"
|
||||||
)
|
)
|
||||||
tw.line()
|
tw.line()
|
||||||
|
|
||||||
|
|||||||
+15
-8
@@ -21,7 +21,7 @@ if TYPE_CHECKING:
|
|||||||
from typing_extensions import Literal
|
from typing_extensions import Literal
|
||||||
|
|
||||||
from _pytest._code.code import ExceptionRepr
|
from _pytest._code.code import ExceptionRepr
|
||||||
from _pytest.code import ExceptionInfo
|
from _pytest._code.code import ExceptionInfo
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config import ExitCode
|
from _pytest.config import ExitCode
|
||||||
from _pytest.config import PytestPluginManager
|
from _pytest.config import PytestPluginManager
|
||||||
@@ -41,6 +41,7 @@ if TYPE_CHECKING:
|
|||||||
from _pytest.reports import TestReport
|
from _pytest.reports import TestReport
|
||||||
from _pytest.runner import CallInfo
|
from _pytest.runner import CallInfo
|
||||||
from _pytest.terminal import TerminalReporter
|
from _pytest.terminal import TerminalReporter
|
||||||
|
from _pytest.terminal import TestShortLogReport
|
||||||
from _pytest.compat import LEGACY_PATH
|
from _pytest.compat import LEGACY_PATH
|
||||||
|
|
||||||
|
|
||||||
@@ -505,7 +506,9 @@ def pytest_runtest_logstart(
|
|||||||
See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
|
See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
|
||||||
|
|
||||||
:param nodeid: Full node ID of the item.
|
:param nodeid: Full node ID of the item.
|
||||||
:param location: A tuple of ``(filename, lineno, testname)``.
|
:param location: A tuple of ``(filename, lineno, testname)``
|
||||||
|
where ``filename`` is a file path relative to ``config.rootpath``
|
||||||
|
and ``lineno`` is 0-based.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@@ -517,7 +520,9 @@ def pytest_runtest_logfinish(
|
|||||||
See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
|
See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
|
||||||
|
|
||||||
:param nodeid: Full node ID of the item.
|
:param nodeid: Full node ID of the item.
|
||||||
:param location: A tuple of ``(filename, lineno, testname)``.
|
:param location: A tuple of ``(filename, lineno, testname)``
|
||||||
|
where ``filename`` is a file path relative to ``config.rootpath``
|
||||||
|
and ``lineno`` is 0-based.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@@ -738,7 +743,7 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No
|
|||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def pytest_report_header(
|
def pytest_report_header( # type:ignore[empty-body]
|
||||||
config: "Config", start_path: Path, startdir: "LEGACY_PATH"
|
config: "Config", start_path: Path, startdir: "LEGACY_PATH"
|
||||||
) -> Union[str, List[str]]:
|
) -> Union[str, List[str]]:
|
||||||
"""Return a string or list of strings to be displayed as header info for terminal reporting.
|
"""Return a string or list of strings to be displayed as header info for terminal reporting.
|
||||||
@@ -767,7 +772,7 @@ def pytest_report_header(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def pytest_report_collectionfinish(
|
def pytest_report_collectionfinish( # type:ignore[empty-body]
|
||||||
config: "Config",
|
config: "Config",
|
||||||
start_path: Path,
|
start_path: Path,
|
||||||
startdir: "LEGACY_PATH",
|
startdir: "LEGACY_PATH",
|
||||||
@@ -800,9 +805,9 @@ def pytest_report_collectionfinish(
|
|||||||
|
|
||||||
|
|
||||||
@hookspec(firstresult=True)
|
@hookspec(firstresult=True)
|
||||||
def pytest_report_teststatus(
|
def pytest_report_teststatus( # type:ignore[empty-body]
|
||||||
report: Union["CollectReport", "TestReport"], config: "Config"
|
report: Union["CollectReport", "TestReport"], config: "Config"
|
||||||
) -> Tuple[str, str, Union[str, Mapping[str, bool]]]:
|
) -> "TestShortLogReport | Tuple[str, str, Union[str, Tuple[str, Mapping[str, bool]]]]":
|
||||||
"""Return result-category, shortletter and verbose word for status
|
"""Return result-category, shortletter and verbose word for status
|
||||||
reporting.
|
reporting.
|
||||||
|
|
||||||
@@ -880,7 +885,9 @@ def pytest_warning_recorded(
|
|||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def pytest_markeval_namespace(config: "Config") -> Dict[str, Any]:
|
def pytest_markeval_namespace( # type:ignore[empty-body]
|
||||||
|
config: "Config",
|
||||||
|
) -> Dict[str, Any]:
|
||||||
"""Called when constructing the globals dictionary used for
|
"""Called when constructing the globals dictionary used for
|
||||||
evaluating string conditions in xfail/skipif markers.
|
evaluating string conditions in xfail/skipif markers.
|
||||||
|
|
||||||
|
|||||||
@@ -645,8 +645,8 @@ class LogXML:
|
|||||||
|
|
||||||
def pytest_sessionfinish(self) -> None:
|
def pytest_sessionfinish(self) -> None:
|
||||||
dirname = os.path.dirname(os.path.abspath(self.logfile))
|
dirname = os.path.dirname(os.path.abspath(self.logfile))
|
||||||
if not os.path.isdir(dirname):
|
# exist_ok avoids filesystem race conditions between checking path existence and requesting creation
|
||||||
os.makedirs(dirname)
|
os.makedirs(dirname, exist_ok=True)
|
||||||
|
|
||||||
with open(self.logfile, "w", encoding="utf-8") as logfile:
|
with open(self.logfile, "w", encoding="utf-8") as logfile:
|
||||||
suite_stop_time = timing.time()
|
suite_stop_time = timing.time()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Add backward compatibility support for the legacy py path type."""
|
"""Add backward compatibility support for the legacy py path type."""
|
||||||
|
import dataclasses
|
||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -7,7 +8,6 @@ from typing import Optional
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import attr
|
|
||||||
from iniconfig import SectionWrapper
|
from iniconfig import SectionWrapper
|
||||||
|
|
||||||
from _pytest.cacheprovider import Cache
|
from _pytest.cacheprovider import Cache
|
||||||
@@ -268,7 +268,7 @@ class LegacyTestdirPlugin:
|
|||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@attr.s(init=False, auto_attribs=True)
|
@dataclasses.dataclass
|
||||||
class TempdirFactory:
|
class TempdirFactory:
|
||||||
"""Backward compatibility wrapper that implements :class:`py.path.local`
|
"""Backward compatibility wrapper that implements :class:`py.path.local`
|
||||||
for :class:`TempPathFactory`.
|
for :class:`TempPathFactory`.
|
||||||
|
|||||||
+90
-4
@@ -5,7 +5,11 @@ import os
|
|||||||
import re
|
import re
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from contextlib import nullcontext
|
from contextlib import nullcontext
|
||||||
|
from datetime import datetime
|
||||||
|
from datetime import timedelta
|
||||||
|
from datetime import timezone
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
from logging import LogRecord
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import AbstractSet
|
from typing import AbstractSet
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
@@ -53,7 +57,25 @@ def _remove_ansi_escape_sequences(text: str) -> str:
|
|||||||
return _ANSI_ESCAPE_SEQ.sub("", text)
|
return _ANSI_ESCAPE_SEQ.sub("", text)
|
||||||
|
|
||||||
|
|
||||||
class ColoredLevelFormatter(logging.Formatter):
|
class DatetimeFormatter(logging.Formatter):
|
||||||
|
"""A logging formatter which formats record with
|
||||||
|
:func:`datetime.datetime.strftime` formatter instead of
|
||||||
|
:func:`time.strftime` in case of microseconds in format string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def formatTime(self, record: LogRecord, datefmt=None) -> str:
|
||||||
|
if datefmt and "%f" in datefmt:
|
||||||
|
ct = self.converter(record.created)
|
||||||
|
tz = timezone(timedelta(seconds=ct.tm_gmtoff), ct.tm_zone)
|
||||||
|
# Construct `datetime.datetime` object from `struct_time`
|
||||||
|
# and msecs information from `record`
|
||||||
|
dt = datetime(*ct[0:6], microsecond=round(record.msecs * 1000), tzinfo=tz)
|
||||||
|
return dt.strftime(datefmt)
|
||||||
|
# Use `logging.Formatter` for non-microsecond formats
|
||||||
|
return super().formatTime(record, datefmt)
|
||||||
|
|
||||||
|
|
||||||
|
class ColoredLevelFormatter(DatetimeFormatter):
|
||||||
"""A logging formatter which colorizes the %(levelname)..s part of the
|
"""A logging formatter which colorizes the %(levelname)..s part of the
|
||||||
log format passed to __init__."""
|
log format passed to __init__."""
|
||||||
|
|
||||||
@@ -297,6 +319,13 @@ def pytest_addoption(parser: Parser) -> None:
|
|||||||
default=None,
|
default=None,
|
||||||
help="Auto-indent multiline messages passed to the logging module. Accepts true|on, false|off or an integer.",
|
help="Auto-indent multiline messages passed to the logging module. Accepts true|on, false|off or an integer.",
|
||||||
)
|
)
|
||||||
|
group.addoption(
|
||||||
|
"--log-disable",
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
|
dest="logger_disable",
|
||||||
|
help="Disable a logger by name. Can be passed multiple times.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
_HandlerType = TypeVar("_HandlerType", bound=logging.Handler)
|
_HandlerType = TypeVar("_HandlerType", bound=logging.Handler)
|
||||||
@@ -369,11 +398,12 @@ class LogCaptureFixture:
|
|||||||
self._initial_handler_level: Optional[int] = None
|
self._initial_handler_level: Optional[int] = None
|
||||||
# Dict of log name -> log level.
|
# Dict of log name -> log level.
|
||||||
self._initial_logger_levels: Dict[Optional[str], int] = {}
|
self._initial_logger_levels: Dict[Optional[str], int] = {}
|
||||||
|
self._initial_disabled_logging_level: Optional[int] = None
|
||||||
|
|
||||||
def _finalize(self) -> None:
|
def _finalize(self) -> None:
|
||||||
"""Finalize the fixture.
|
"""Finalize the fixture.
|
||||||
|
|
||||||
This restores the log levels changed by :meth:`set_level`.
|
This restores the log levels and the disabled logging levels changed by :meth:`set_level`.
|
||||||
"""
|
"""
|
||||||
# Restore log levels.
|
# Restore log levels.
|
||||||
if self._initial_handler_level is not None:
|
if self._initial_handler_level is not None:
|
||||||
@@ -381,6 +411,10 @@ class LogCaptureFixture:
|
|||||||
for logger_name, level in self._initial_logger_levels.items():
|
for logger_name, level in self._initial_logger_levels.items():
|
||||||
logger = logging.getLogger(logger_name)
|
logger = logging.getLogger(logger_name)
|
||||||
logger.setLevel(level)
|
logger.setLevel(level)
|
||||||
|
# Disable logging at the original disabled logging level.
|
||||||
|
if self._initial_disabled_logging_level is not None:
|
||||||
|
logging.disable(self._initial_disabled_logging_level)
|
||||||
|
self._initial_disabled_logging_level = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def handler(self) -> LogCaptureHandler:
|
def handler(self) -> LogCaptureHandler:
|
||||||
@@ -446,6 +480,40 @@ class LogCaptureFixture:
|
|||||||
"""Reset the list of log records and the captured log text."""
|
"""Reset the list of log records and the captured log text."""
|
||||||
self.handler.clear()
|
self.handler.clear()
|
||||||
|
|
||||||
|
def _force_enable_logging(
|
||||||
|
self, level: Union[int, str], logger_obj: logging.Logger
|
||||||
|
) -> int:
|
||||||
|
"""Enable the desired logging level if the global level was disabled via ``logging.disabled``.
|
||||||
|
|
||||||
|
Only enables logging levels greater than or equal to the requested ``level``.
|
||||||
|
|
||||||
|
Does nothing if the desired ``level`` wasn't disabled.
|
||||||
|
|
||||||
|
:param level:
|
||||||
|
The logger level caplog should capture.
|
||||||
|
All logging is enabled if a non-standard logging level string is supplied.
|
||||||
|
Valid level strings are in :data:`logging._nameToLevel`.
|
||||||
|
:param logger_obj: The logger object to check.
|
||||||
|
|
||||||
|
:return: The original disabled logging level.
|
||||||
|
"""
|
||||||
|
original_disable_level: int = logger_obj.manager.disable # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
if isinstance(level, str):
|
||||||
|
# Try to translate the level string to an int for `logging.disable()`
|
||||||
|
level = logging.getLevelName(level)
|
||||||
|
|
||||||
|
if not isinstance(level, int):
|
||||||
|
# The level provided was not valid, so just un-disable all logging.
|
||||||
|
logging.disable(logging.NOTSET)
|
||||||
|
elif not logger_obj.isEnabledFor(level):
|
||||||
|
# Each level is `10` away from other levels.
|
||||||
|
# https://docs.python.org/3/library/logging.html#logging-levels
|
||||||
|
disable_level = max(level - 10, logging.NOTSET)
|
||||||
|
logging.disable(disable_level)
|
||||||
|
|
||||||
|
return original_disable_level
|
||||||
|
|
||||||
def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> None:
|
def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> None:
|
||||||
"""Set the level of a logger for the duration of a test.
|
"""Set the level of a logger for the duration of a test.
|
||||||
|
|
||||||
@@ -453,6 +521,8 @@ class LogCaptureFixture:
|
|||||||
The levels of the loggers changed by this function will be
|
The levels of the loggers changed by this function will be
|
||||||
restored to their initial values at the end of the test.
|
restored to their initial values at the end of the test.
|
||||||
|
|
||||||
|
Will enable the requested logging level if it was disabled via :meth:`logging.disable`.
|
||||||
|
|
||||||
:param level: The level.
|
:param level: The level.
|
||||||
:param logger: The logger to update. If not given, the root logger.
|
:param logger: The logger to update. If not given, the root logger.
|
||||||
"""
|
"""
|
||||||
@@ -463,6 +533,9 @@ class LogCaptureFixture:
|
|||||||
if self._initial_handler_level is None:
|
if self._initial_handler_level is None:
|
||||||
self._initial_handler_level = self.handler.level
|
self._initial_handler_level = self.handler.level
|
||||||
self.handler.setLevel(level)
|
self.handler.setLevel(level)
|
||||||
|
initial_disabled_logging_level = self._force_enable_logging(level, logger_obj)
|
||||||
|
if self._initial_disabled_logging_level is None:
|
||||||
|
self._initial_disabled_logging_level = initial_disabled_logging_level
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def at_level(
|
def at_level(
|
||||||
@@ -472,6 +545,8 @@ class LogCaptureFixture:
|
|||||||
the end of the 'with' statement the level is restored to its original
|
the end of the 'with' statement the level is restored to its original
|
||||||
value.
|
value.
|
||||||
|
|
||||||
|
Will enable the requested logging level if it was disabled via :meth:`logging.disable`.
|
||||||
|
|
||||||
:param level: The level.
|
:param level: The level.
|
||||||
:param logger: The logger to update. If not given, the root logger.
|
:param logger: The logger to update. If not given, the root logger.
|
||||||
"""
|
"""
|
||||||
@@ -480,11 +555,13 @@ class LogCaptureFixture:
|
|||||||
logger_obj.setLevel(level)
|
logger_obj.setLevel(level)
|
||||||
handler_orig_level = self.handler.level
|
handler_orig_level = self.handler.level
|
||||||
self.handler.setLevel(level)
|
self.handler.setLevel(level)
|
||||||
|
original_disable_level = self._force_enable_logging(level, logger_obj)
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
logger_obj.setLevel(orig_level)
|
logger_obj.setLevel(orig_level)
|
||||||
self.handler.setLevel(handler_orig_level)
|
self.handler.setLevel(handler_orig_level)
|
||||||
|
logging.disable(original_disable_level)
|
||||||
|
|
||||||
|
|
||||||
@fixture
|
@fixture
|
||||||
@@ -570,7 +647,7 @@ class LoggingPlugin:
|
|||||||
config, "log_file_date_format", "log_date_format"
|
config, "log_file_date_format", "log_date_format"
|
||||||
)
|
)
|
||||||
|
|
||||||
log_file_formatter = logging.Formatter(
|
log_file_formatter = DatetimeFormatter(
|
||||||
log_file_format, datefmt=log_file_date_format
|
log_file_format, datefmt=log_file_date_format
|
||||||
)
|
)
|
||||||
self.log_file_handler.setFormatter(log_file_formatter)
|
self.log_file_handler.setFormatter(log_file_formatter)
|
||||||
@@ -594,6 +671,15 @@ class LoggingPlugin:
|
|||||||
get_option_ini(config, "log_auto_indent"),
|
get_option_ini(config, "log_auto_indent"),
|
||||||
)
|
)
|
||||||
self.log_cli_handler.setFormatter(log_cli_formatter)
|
self.log_cli_handler.setFormatter(log_cli_formatter)
|
||||||
|
self._disable_loggers(loggers_to_disable=config.option.logger_disable)
|
||||||
|
|
||||||
|
def _disable_loggers(self, loggers_to_disable: List[str]) -> None:
|
||||||
|
if not loggers_to_disable:
|
||||||
|
return
|
||||||
|
|
||||||
|
for name in loggers_to_disable:
|
||||||
|
logger = logging.getLogger(name)
|
||||||
|
logger.disabled = True
|
||||||
|
|
||||||
def _create_formatter(self, log_format, log_date_format, auto_indent):
|
def _create_formatter(self, log_format, log_date_format, auto_indent):
|
||||||
# Color option doesn't exist if terminal plugin is disabled.
|
# Color option doesn't exist if terminal plugin is disabled.
|
||||||
@@ -605,7 +691,7 @@ class LoggingPlugin:
|
|||||||
create_terminal_writer(self._config), log_format, log_date_format
|
create_terminal_writer(self._config), log_format, log_date_format
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
formatter = logging.Formatter(log_format, log_date_format)
|
formatter = DatetimeFormatter(log_format, log_date_format)
|
||||||
|
|
||||||
formatter._style = PercentStyleMultiline(
|
formatter._style = PercentStyleMultiline(
|
||||||
formatter._style._fmt, auto_indent=auto_indent
|
formatter._style._fmt, auto_indent=auto_indent
|
||||||
|
|||||||
+21
-16
@@ -1,5 +1,6 @@
|
|||||||
"""Core implementation of the testing process: init, session, runtest loop."""
|
"""Core implementation of the testing process: init, session, runtest loop."""
|
||||||
import argparse
|
import argparse
|
||||||
|
import dataclasses
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import functools
|
import functools
|
||||||
import importlib
|
import importlib
|
||||||
@@ -19,8 +20,6 @@ from typing import Type
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import attr
|
|
||||||
|
|
||||||
import _pytest._code
|
import _pytest._code
|
||||||
from _pytest import nodes
|
from _pytest import nodes
|
||||||
from _pytest.compat import final
|
from _pytest.compat import final
|
||||||
@@ -123,11 +122,12 @@ def pytest_addoption(parser: Parser) -> None:
|
|||||||
)
|
)
|
||||||
group._addoption(
|
group._addoption(
|
||||||
"-c",
|
"-c",
|
||||||
metavar="file",
|
"--config-file",
|
||||||
|
metavar="FILE",
|
||||||
type=str,
|
type=str,
|
||||||
dest="inifilename",
|
dest="inifilename",
|
||||||
help="Load configuration from `file` instead of trying to locate one of the "
|
help="Load configuration from `FILE` instead of trying to locate one of the "
|
||||||
"implicit configuration files",
|
"implicit configuration files.",
|
||||||
)
|
)
|
||||||
group._addoption(
|
group._addoption(
|
||||||
"--continue-on-collection-errors",
|
"--continue-on-collection-errors",
|
||||||
@@ -400,6 +400,12 @@ def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[boo
|
|||||||
allow_in_venv = config.getoption("collect_in_virtualenv")
|
allow_in_venv = config.getoption("collect_in_virtualenv")
|
||||||
if not allow_in_venv and _in_venv(collection_path):
|
if not allow_in_venv and _in_venv(collection_path):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
if collection_path.is_dir():
|
||||||
|
norecursepatterns = config.getini("norecursedirs")
|
||||||
|
if any(fnmatch_ex(pat, collection_path) for pat in norecursepatterns):
|
||||||
|
return True
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -442,8 +448,10 @@ class Failed(Exception):
|
|||||||
"""Signals a stop as failed test run."""
|
"""Signals a stop as failed test run."""
|
||||||
|
|
||||||
|
|
||||||
@attr.s(slots=True, auto_attribs=True)
|
@dataclasses.dataclass
|
||||||
class _bestrelpath_cache(Dict[Path, str]):
|
class _bestrelpath_cache(Dict[Path, str]):
|
||||||
|
__slots__ = ("path",)
|
||||||
|
|
||||||
path: Path
|
path: Path
|
||||||
|
|
||||||
def __missing__(self, path: Path) -> str:
|
def __missing__(self, path: Path) -> str:
|
||||||
@@ -561,9 +569,6 @@ class Session(nodes.FSCollector):
|
|||||||
ihook = self.gethookproxy(fspath.parent)
|
ihook = self.gethookproxy(fspath.parent)
|
||||||
if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
|
if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
|
||||||
return False
|
return False
|
||||||
norecursepatterns = self.config.getini("norecursedirs")
|
|
||||||
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
|
|
||||||
return False
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _collectfile(
|
def _collectfile(
|
||||||
@@ -684,8 +689,8 @@ class Session(nodes.FSCollector):
|
|||||||
# are not collected more than once.
|
# are not collected more than once.
|
||||||
matchnodes_cache: Dict[Tuple[Type[nodes.Collector], str], CollectReport] = {}
|
matchnodes_cache: Dict[Tuple[Type[nodes.Collector], str], CollectReport] = {}
|
||||||
|
|
||||||
# Dirnames of pkgs with dunder-init files.
|
# Directories of pkgs with dunder-init files.
|
||||||
pkg_roots: Dict[str, Package] = {}
|
pkg_roots: Dict[Path, Package] = {}
|
||||||
|
|
||||||
for argpath, names in self._initial_parts:
|
for argpath, names in self._initial_parts:
|
||||||
self.trace("processing argument", (argpath, names))
|
self.trace("processing argument", (argpath, names))
|
||||||
@@ -706,7 +711,7 @@ class Session(nodes.FSCollector):
|
|||||||
col = self._collectfile(pkginit, handle_dupes=False)
|
col = self._collectfile(pkginit, handle_dupes=False)
|
||||||
if col:
|
if col:
|
||||||
if isinstance(col[0], Package):
|
if isinstance(col[0], Package):
|
||||||
pkg_roots[str(parent)] = col[0]
|
pkg_roots[parent] = col[0]
|
||||||
node_cache1[col[0].path] = [col[0]]
|
node_cache1[col[0].path] = [col[0]]
|
||||||
|
|
||||||
# If it's a directory argument, recurse and look for any Subpackages.
|
# If it's a directory argument, recurse and look for any Subpackages.
|
||||||
@@ -715,7 +720,7 @@ class Session(nodes.FSCollector):
|
|||||||
assert not names, f"invalid arg {(argpath, names)!r}"
|
assert not names, f"invalid arg {(argpath, names)!r}"
|
||||||
|
|
||||||
seen_dirs: Set[Path] = set()
|
seen_dirs: Set[Path] = set()
|
||||||
for direntry in visit(str(argpath), self._recurse):
|
for direntry in visit(argpath, self._recurse):
|
||||||
if not direntry.is_file():
|
if not direntry.is_file():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -730,8 +735,8 @@ class Session(nodes.FSCollector):
|
|||||||
for x in self._collectfile(pkginit):
|
for x in self._collectfile(pkginit):
|
||||||
yield x
|
yield x
|
||||||
if isinstance(x, Package):
|
if isinstance(x, Package):
|
||||||
pkg_roots[str(dirpath)] = x
|
pkg_roots[dirpath] = x
|
||||||
if str(dirpath) in pkg_roots:
|
if dirpath in pkg_roots:
|
||||||
# Do not collect packages here.
|
# Do not collect packages here.
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -748,7 +753,7 @@ class Session(nodes.FSCollector):
|
|||||||
if argpath in node_cache1:
|
if argpath in node_cache1:
|
||||||
col = node_cache1[argpath]
|
col = node_cache1[argpath]
|
||||||
else:
|
else:
|
||||||
collect_root = pkg_roots.get(str(argpath.parent), self)
|
collect_root = pkg_roots.get(argpath.parent, self)
|
||||||
col = collect_root._collectfile(argpath, handle_dupes=False)
|
col = collect_root._collectfile(argpath, handle_dupes=False)
|
||||||
if col:
|
if col:
|
||||||
node_cache1[argpath] = col
|
node_cache1[argpath] = col
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Generic mechanism for marking and selecting python functions."""
|
"""Generic mechanism for marking and selecting python functions."""
|
||||||
|
import dataclasses
|
||||||
from typing import AbstractSet
|
from typing import AbstractSet
|
||||||
from typing import Collection
|
from typing import Collection
|
||||||
from typing import List
|
from typing import List
|
||||||
@@ -6,8 +7,6 @@ from typing import Optional
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import attr
|
|
||||||
|
|
||||||
from .expression import Expression
|
from .expression import Expression
|
||||||
from .expression import ParseError
|
from .expression import ParseError
|
||||||
from .structures import EMPTY_PARAMETERSET_OPTION
|
from .structures import EMPTY_PARAMETERSET_OPTION
|
||||||
@@ -130,7 +129,7 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@attr.s(slots=True, auto_attribs=True)
|
@dataclasses.dataclass
|
||||||
class KeywordMatcher:
|
class KeywordMatcher:
|
||||||
"""A matcher for keywords.
|
"""A matcher for keywords.
|
||||||
|
|
||||||
@@ -145,6 +144,8 @@ class KeywordMatcher:
|
|||||||
any item, as well as names directly assigned to test functions.
|
any item, as well as names directly assigned to test functions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_names",)
|
||||||
|
|
||||||
_names: AbstractSet[str]
|
_names: AbstractSet[str]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -201,13 +202,15 @@ def deselect_by_keyword(items: "List[Item]", config: Config) -> None:
|
|||||||
items[:] = remaining
|
items[:] = remaining
|
||||||
|
|
||||||
|
|
||||||
@attr.s(slots=True, auto_attribs=True)
|
@dataclasses.dataclass
|
||||||
class MarkMatcher:
|
class MarkMatcher:
|
||||||
"""A matcher for markers which are present.
|
"""A matcher for markers which are present.
|
||||||
|
|
||||||
Tries to match on any marker names, attached to the given colitem.
|
Tries to match on any marker names, attached to the given colitem.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("own_mark_names",)
|
||||||
|
|
||||||
own_mark_names: AbstractSet[str]
|
own_mark_names: AbstractSet[str]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -15,8 +15,10 @@ The semantics are:
|
|||||||
- or/and/not evaluate according to the usual boolean semantics.
|
- or/and/not evaluate according to the usual boolean semantics.
|
||||||
"""
|
"""
|
||||||
import ast
|
import ast
|
||||||
|
import dataclasses
|
||||||
import enum
|
import enum
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
import types
|
import types
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
@@ -25,7 +27,10 @@ from typing import NoReturn
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
|
|
||||||
import attr
|
if sys.version_info >= (3, 8):
|
||||||
|
astNameConstant = ast.Constant
|
||||||
|
else:
|
||||||
|
astNameConstant = ast.NameConstant
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -44,8 +49,9 @@ class TokenType(enum.Enum):
|
|||||||
EOF = "end of input"
|
EOF = "end of input"
|
||||||
|
|
||||||
|
|
||||||
@attr.s(frozen=True, slots=True, auto_attribs=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class Token:
|
class Token:
|
||||||
|
__slots__ = ("type", "value", "pos")
|
||||||
type: TokenType
|
type: TokenType
|
||||||
value: str
|
value: str
|
||||||
pos: int
|
pos: int
|
||||||
@@ -132,7 +138,7 @@ IDENT_PREFIX = "$"
|
|||||||
|
|
||||||
def expression(s: Scanner) -> ast.Expression:
|
def expression(s: Scanner) -> ast.Expression:
|
||||||
if s.accept(TokenType.EOF):
|
if s.accept(TokenType.EOF):
|
||||||
ret: ast.expr = ast.NameConstant(False)
|
ret: ast.expr = astNameConstant(False)
|
||||||
else:
|
else:
|
||||||
ret = expr(s)
|
ret = expr(s)
|
||||||
s.accept(TokenType.EOF, reject=True)
|
s.accept(TokenType.EOF, reject=True)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import collections.abc
|
import collections.abc
|
||||||
|
import dataclasses
|
||||||
import inspect
|
import inspect
|
||||||
import warnings
|
import warnings
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -20,8 +21,6 @@ from typing import TYPE_CHECKING
|
|||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import attr
|
|
||||||
|
|
||||||
from .._code import getfslineno
|
from .._code import getfslineno
|
||||||
from ..compat import ascii_escaped
|
from ..compat import ascii_escaped
|
||||||
from ..compat import final
|
from ..compat import final
|
||||||
@@ -191,8 +190,10 @@ class ParameterSet(NamedTuple):
|
|||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@attr.s(frozen=True, init=False, auto_attribs=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class Mark:
|
class Mark:
|
||||||
|
"""A pytest mark."""
|
||||||
|
|
||||||
#: Name of the mark.
|
#: Name of the mark.
|
||||||
name: str
|
name: str
|
||||||
#: Positional arguments of the mark decorator.
|
#: Positional arguments of the mark decorator.
|
||||||
@@ -201,9 +202,11 @@ class Mark:
|
|||||||
kwargs: Mapping[str, Any]
|
kwargs: Mapping[str, Any]
|
||||||
|
|
||||||
#: Source Mark for ids with parametrize Marks.
|
#: Source Mark for ids with parametrize Marks.
|
||||||
_param_ids_from: Optional["Mark"] = attr.ib(default=None, repr=False)
|
_param_ids_from: Optional["Mark"] = dataclasses.field(default=None, repr=False)
|
||||||
#: Resolved/generated ids with parametrize Marks.
|
#: Resolved/generated ids with parametrize Marks.
|
||||||
_param_ids_generated: Optional[Sequence[str]] = attr.ib(default=None, repr=False)
|
_param_ids_generated: Optional[Sequence[str]] = dataclasses.field(
|
||||||
|
default=None, repr=False
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -261,7 +264,7 @@ class Mark:
|
|||||||
Markable = TypeVar("Markable", bound=Union[Callable[..., object], type])
|
Markable = TypeVar("Markable", bound=Union[Callable[..., object], type])
|
||||||
|
|
||||||
|
|
||||||
@attr.s(init=False, auto_attribs=True)
|
@dataclasses.dataclass
|
||||||
class MarkDecorator:
|
class MarkDecorator:
|
||||||
"""A decorator for applying a mark on test functions and classes.
|
"""A decorator for applying a mark on test functions and classes.
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from contextlib import contextmanager
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from typing import Mapping
|
||||||
from typing import MutableMapping
|
from typing import MutableMapping
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import overload
|
from typing import overload
|
||||||
@@ -129,7 +130,7 @@ class MonkeyPatch:
|
|||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._setattr: List[Tuple[object, str, object]] = []
|
self._setattr: List[Tuple[object, str, object]] = []
|
||||||
self._setitem: List[Tuple[MutableMapping[Any, Any], object, object]] = []
|
self._setitem: List[Tuple[Mapping[Any, Any], object, object]] = []
|
||||||
self._cwd: Optional[str] = None
|
self._cwd: Optional[str] = None
|
||||||
self._savesyspath: Optional[List[str]] = None
|
self._savesyspath: Optional[List[str]] = None
|
||||||
|
|
||||||
@@ -290,12 +291,13 @@ class MonkeyPatch:
|
|||||||
self._setattr.append((target, name, oldval))
|
self._setattr.append((target, name, oldval))
|
||||||
delattr(target, name)
|
delattr(target, name)
|
||||||
|
|
||||||
def setitem(self, dic: MutableMapping[K, V], name: K, value: V) -> None:
|
def setitem(self, dic: Mapping[K, V], name: K, value: V) -> None:
|
||||||
"""Set dictionary entry ``name`` to value."""
|
"""Set dictionary entry ``name`` to value."""
|
||||||
self._setitem.append((dic, name, dic.get(name, notset)))
|
self._setitem.append((dic, name, dic.get(name, notset)))
|
||||||
dic[name] = value
|
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
|
||||||
|
dic[name] = value # type: ignore[index]
|
||||||
|
|
||||||
def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> None:
|
def delitem(self, dic: Mapping[K, V], name: K, raising: bool = True) -> None:
|
||||||
"""Delete ``name`` from dict.
|
"""Delete ``name`` from dict.
|
||||||
|
|
||||||
Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to
|
Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to
|
||||||
@@ -306,7 +308,8 @@ class MonkeyPatch:
|
|||||||
raise KeyError(name)
|
raise KeyError(name)
|
||||||
else:
|
else:
|
||||||
self._setitem.append((dic, name, dic.get(name, notset)))
|
self._setitem.append((dic, name, dic.get(name, notset)))
|
||||||
del dic[name]
|
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
|
||||||
|
del dic[name] # type: ignore[attr-defined]
|
||||||
|
|
||||||
def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None:
|
def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None:
|
||||||
"""Set environment variable ``name`` to ``value``.
|
"""Set environment variable ``name`` to ``value``.
|
||||||
@@ -401,11 +404,13 @@ class MonkeyPatch:
|
|||||||
for dictionary, key, value in reversed(self._setitem):
|
for dictionary, key, value in reversed(self._setitem):
|
||||||
if value is notset:
|
if value is notset:
|
||||||
try:
|
try:
|
||||||
del dictionary[key]
|
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
|
||||||
|
del dictionary[key] # type: ignore[attr-defined]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass # Was already deleted, so we have the desired state.
|
pass # Was already deleted, so we have the desired state.
|
||||||
else:
|
else:
|
||||||
dictionary[key] = value
|
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
|
||||||
|
dictionary[key] = value # type: ignore[index]
|
||||||
self._setitem[:] = []
|
self._setitem[:] = []
|
||||||
if self._savesyspath is not None:
|
if self._savesyspath is not None:
|
||||||
sys.path[:] = self._savesyspath
|
sys.path[:] = self._savesyspath
|
||||||
|
|||||||
+18
-11
@@ -22,6 +22,7 @@ import _pytest._code
|
|||||||
from _pytest._code import getfslineno
|
from _pytest._code import getfslineno
|
||||||
from _pytest._code.code import ExceptionInfo
|
from _pytest._code.code import ExceptionInfo
|
||||||
from _pytest._code.code import TerminalRepr
|
from _pytest._code.code import TerminalRepr
|
||||||
|
from _pytest._code.code import Traceback
|
||||||
from _pytest.compat import cached_property
|
from _pytest.compat import cached_property
|
||||||
from _pytest.compat import LEGACY_PATH
|
from _pytest.compat import LEGACY_PATH
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
@@ -432,8 +433,8 @@ class Node(metaclass=NodeMeta):
|
|||||||
assert current is None or isinstance(current, cls)
|
assert current is None or isinstance(current, cls)
|
||||||
return current
|
return current
|
||||||
|
|
||||||
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
|
def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
|
||||||
pass
|
return excinfo.traceback
|
||||||
|
|
||||||
def _repr_failure_py(
|
def _repr_failure_py(
|
||||||
self,
|
self,
|
||||||
@@ -449,13 +450,13 @@ class Node(metaclass=NodeMeta):
|
|||||||
style = "value"
|
style = "value"
|
||||||
if isinstance(excinfo.value, FixtureLookupError):
|
if isinstance(excinfo.value, FixtureLookupError):
|
||||||
return excinfo.value.formatrepr()
|
return excinfo.value.formatrepr()
|
||||||
|
|
||||||
|
tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]]
|
||||||
if self.config.getoption("fulltrace", False):
|
if self.config.getoption("fulltrace", False):
|
||||||
style = "long"
|
style = "long"
|
||||||
|
tbfilter = False
|
||||||
else:
|
else:
|
||||||
tb = _pytest._code.Traceback([excinfo.traceback[-1]])
|
tbfilter = self._traceback_filter
|
||||||
self._prunetraceback(excinfo)
|
|
||||||
if len(excinfo.traceback) == 0:
|
|
||||||
excinfo.traceback = tb
|
|
||||||
if style == "auto":
|
if style == "auto":
|
||||||
style = "long"
|
style = "long"
|
||||||
# XXX should excinfo.getrepr record all data and toterminal() process it?
|
# XXX should excinfo.getrepr record all data and toterminal() process it?
|
||||||
@@ -486,7 +487,7 @@ class Node(metaclass=NodeMeta):
|
|||||||
abspath=abspath,
|
abspath=abspath,
|
||||||
showlocals=self.config.getoption("showlocals", False),
|
showlocals=self.config.getoption("showlocals", False),
|
||||||
style=style,
|
style=style,
|
||||||
tbfilter=False, # pruned already, or in --fulltrace mode.
|
tbfilter=tbfilter,
|
||||||
truncate_locals=truncate_locals,
|
truncate_locals=truncate_locals,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -511,7 +512,7 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i
|
|||||||
* "obj": a Python object that the node wraps.
|
* "obj": a Python object that the node wraps.
|
||||||
* "fspath": just a path
|
* "fspath": just a path
|
||||||
|
|
||||||
:rtype: A tuple of (str|Path, int) with filename and line number.
|
:rtype: A tuple of (str|Path, int) with filename and 0-based line number.
|
||||||
"""
|
"""
|
||||||
# See Item.location.
|
# See Item.location.
|
||||||
location: Optional[Tuple[str, Optional[int], str]] = getattr(node, "location", None)
|
location: Optional[Tuple[str, Optional[int], str]] = getattr(node, "location", None)
|
||||||
@@ -557,13 +558,14 @@ class Collector(Node):
|
|||||||
|
|
||||||
return self._repr_failure_py(excinfo, style=tbstyle)
|
return self._repr_failure_py(excinfo, style=tbstyle)
|
||||||
|
|
||||||
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
|
def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
|
||||||
if hasattr(self, "path"):
|
if hasattr(self, "path"):
|
||||||
traceback = excinfo.traceback
|
traceback = excinfo.traceback
|
||||||
ntraceback = traceback.cut(path=self.path)
|
ntraceback = traceback.cut(path=self.path)
|
||||||
if ntraceback == traceback:
|
if ntraceback == traceback:
|
||||||
ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
|
ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
|
||||||
excinfo.traceback = ntraceback.filter()
|
return excinfo.traceback.filter(excinfo)
|
||||||
|
return excinfo.traceback
|
||||||
|
|
||||||
|
|
||||||
def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[str]:
|
def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[str]:
|
||||||
@@ -755,7 +757,7 @@ class Item(Node):
|
|||||||
Returns a tuple with three elements:
|
Returns a tuple with three elements:
|
||||||
|
|
||||||
- The path of the test (default ``self.path``)
|
- The path of the test (default ``self.path``)
|
||||||
- The line number of the test (default ``None``)
|
- The 0-based line number of the test (default ``None``)
|
||||||
- A name of the test to be shown (default ``""``)
|
- A name of the test to be shown (default ``""``)
|
||||||
|
|
||||||
.. seealso:: :ref:`non-python tests`
|
.. seealso:: :ref:`non-python tests`
|
||||||
@@ -764,6 +766,11 @@ class Item(Node):
|
|||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def location(self) -> Tuple[str, Optional[int], str]:
|
def location(self) -> Tuple[str, Optional[int], str]:
|
||||||
|
"""
|
||||||
|
Returns a tuple of ``(relfspath, lineno, testname)`` for this item
|
||||||
|
where ``relfspath`` is file path relative to ``config.rootpath``
|
||||||
|
and lineno is a 0-based line number.
|
||||||
|
"""
|
||||||
location = self.reportinfo()
|
location = self.reportinfo()
|
||||||
path = absolutepath(os.fspath(location[0]))
|
path = absolutepath(os.fspath(location[0]))
|
||||||
relfspath = self.session._node_location_to_relpath(path)
|
relfspath = self.session._node_location_to_relpath(path)
|
||||||
|
|||||||
@@ -157,8 +157,12 @@ def skip(
|
|||||||
The message to show the user as reason for the skip.
|
The message to show the user as reason for the skip.
|
||||||
|
|
||||||
:param allow_module_level:
|
:param allow_module_level:
|
||||||
Allows this function to be called at module level, skipping the rest
|
Allows this function to be called at module level.
|
||||||
of the module. Defaults to False.
|
Raising the skip exception at module level will stop
|
||||||
|
the execution of the module and prevent the collection of all tests in the module,
|
||||||
|
even those defined before the `skip` call.
|
||||||
|
|
||||||
|
Defaults to False.
|
||||||
|
|
||||||
:param msg:
|
:param msg:
|
||||||
Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
|
Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
|
||||||
@@ -219,7 +223,6 @@ def _resolve_msg_to_reason(
|
|||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
if msg is not None:
|
if msg is not None:
|
||||||
|
|
||||||
if reason:
|
if reason:
|
||||||
from pytest import UsageError
|
from pytest import UsageError
|
||||||
|
|
||||||
|
|||||||
+70
-30
@@ -6,6 +6,7 @@ import itertools
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
import types
|
||||||
import uuid
|
import uuid
|
||||||
import warnings
|
import warnings
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
@@ -26,8 +27,11 @@ from typing import Callable
|
|||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Set
|
from typing import Set
|
||||||
|
from typing import Tuple
|
||||||
|
from typing import Type
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
@@ -63,21 +67,33 @@ def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
|
|||||||
return path.joinpath(".lock")
|
return path.joinpath(".lock")
|
||||||
|
|
||||||
|
|
||||||
def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
|
def on_rm_rf_error(
|
||||||
|
func,
|
||||||
|
path: str,
|
||||||
|
excinfo: Union[
|
||||||
|
BaseException,
|
||||||
|
Tuple[Type[BaseException], BaseException, Optional[types.TracebackType]],
|
||||||
|
],
|
||||||
|
*,
|
||||||
|
start_path: Path,
|
||||||
|
) -> bool:
|
||||||
"""Handle known read-only errors during rmtree.
|
"""Handle known read-only errors during rmtree.
|
||||||
|
|
||||||
The returned value is used only by our own tests.
|
The returned value is used only by our own tests.
|
||||||
"""
|
"""
|
||||||
exctype, excvalue = exc[:2]
|
if isinstance(excinfo, BaseException):
|
||||||
|
exc = excinfo
|
||||||
|
else:
|
||||||
|
exc = excinfo[1]
|
||||||
|
|
||||||
# Another process removed the file in the middle of the "rm_rf" (xdist for example).
|
# Another process removed the file in the middle of the "rm_rf" (xdist for example).
|
||||||
# More context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018
|
# More context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018
|
||||||
if isinstance(excvalue, FileNotFoundError):
|
if isinstance(exc, FileNotFoundError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not isinstance(excvalue, PermissionError):
|
if not isinstance(exc, PermissionError):
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
PytestWarning(f"(rm_rf) error removing {path}\n{exctype}: {excvalue}")
|
PytestWarning(f"(rm_rf) error removing {path}\n{type(exc)}: {exc}")
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -86,7 +102,7 @@ def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
|
|||||||
warnings.warn(
|
warnings.warn(
|
||||||
PytestWarning(
|
PytestWarning(
|
||||||
"(rm_rf) unknown function {} when removing {}:\n{}: {}".format(
|
"(rm_rf) unknown function {} when removing {}:\n{}: {}".format(
|
||||||
func, path, exctype, excvalue
|
func, path, type(exc), exc
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -149,7 +165,10 @@ def rm_rf(path: Path) -> None:
|
|||||||
are read-only."""
|
are read-only."""
|
||||||
path = ensure_extended_length_path(path)
|
path = ensure_extended_length_path(path)
|
||||||
onerror = partial(on_rm_rf_error, start_path=path)
|
onerror = partial(on_rm_rf_error, start_path=path)
|
||||||
shutil.rmtree(str(path), onerror=onerror)
|
if sys.version_info >= (3, 12):
|
||||||
|
shutil.rmtree(str(path), onexc=onerror)
|
||||||
|
else:
|
||||||
|
shutil.rmtree(str(path), onerror=onerror)
|
||||||
|
|
||||||
|
|
||||||
def find_prefixed(root: Path, prefix: str) -> Iterator[Path]:
|
def find_prefixed(root: Path, prefix: str) -> Iterator[Path]:
|
||||||
@@ -335,15 +354,26 @@ def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]:
|
|||||||
yield path
|
yield path
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_dead_symlinks(root: Path):
|
||||||
|
for left_dir in root.iterdir():
|
||||||
|
if left_dir.is_symlink():
|
||||||
|
if not left_dir.resolve().exists():
|
||||||
|
left_dir.unlink()
|
||||||
|
|
||||||
|
|
||||||
def cleanup_numbered_dir(
|
def cleanup_numbered_dir(
|
||||||
root: Path, prefix: str, keep: int, consider_lock_dead_if_created_before: float
|
root: Path, prefix: str, keep: int, consider_lock_dead_if_created_before: float
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Cleanup for lock driven numbered directories."""
|
"""Cleanup for lock driven numbered directories."""
|
||||||
|
if not root.exists():
|
||||||
|
return
|
||||||
for path in cleanup_candidates(root, prefix, keep):
|
for path in cleanup_candidates(root, prefix, keep):
|
||||||
try_cleanup(path, consider_lock_dead_if_created_before)
|
try_cleanup(path, consider_lock_dead_if_created_before)
|
||||||
for path in root.glob("garbage-*"):
|
for path in root.glob("garbage-*"):
|
||||||
try_cleanup(path, consider_lock_dead_if_created_before)
|
try_cleanup(path, consider_lock_dead_if_created_before)
|
||||||
|
|
||||||
|
cleanup_dead_symlinks(root)
|
||||||
|
|
||||||
|
|
||||||
def make_numbered_dir_with_cleanup(
|
def make_numbered_dir_with_cleanup(
|
||||||
root: Path,
|
root: Path,
|
||||||
@@ -357,8 +387,10 @@ def make_numbered_dir_with_cleanup(
|
|||||||
for i in range(10):
|
for i in range(10):
|
||||||
try:
|
try:
|
||||||
p = make_numbered_dir(root, prefix, mode)
|
p = make_numbered_dir(root, prefix, mode)
|
||||||
lock_path = create_cleanup_lock(p)
|
# Only lock the current dir when keep is not 0
|
||||||
register_cleanup_lock_removal(lock_path)
|
if keep != 0:
|
||||||
|
lock_path = create_cleanup_lock(p)
|
||||||
|
register_cleanup_lock_removal(lock_path)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
e = exc
|
e = exc
|
||||||
else:
|
else:
|
||||||
@@ -464,14 +496,14 @@ def import_path(
|
|||||||
|
|
||||||
* `mode == ImportMode.prepend`: the directory containing the module (or package, taking
|
* `mode == ImportMode.prepend`: the directory containing the module (or package, taking
|
||||||
`__init__.py` files into account) will be put at the *start* of `sys.path` before
|
`__init__.py` files into account) will be put at the *start* of `sys.path` before
|
||||||
being imported with `__import__.
|
being imported with `importlib.import_module`.
|
||||||
|
|
||||||
* `mode == ImportMode.append`: same as `prepend`, but the directory will be appended
|
* `mode == ImportMode.append`: same as `prepend`, but the directory will be appended
|
||||||
to the end of `sys.path`, if not already in `sys.path`.
|
to the end of `sys.path`, if not already in `sys.path`.
|
||||||
|
|
||||||
* `mode == ImportMode.importlib`: uses more fine control mechanisms provided by `importlib`
|
* `mode == ImportMode.importlib`: uses more fine control mechanisms provided by `importlib`
|
||||||
to import the module, which avoids having to use `__import__` and muck with `sys.path`
|
to import the module, which avoids having to muck with `sys.path` at all. It effectively
|
||||||
at all. It effectively allows having same-named test modules in different places.
|
allows having same-named test modules in different places.
|
||||||
|
|
||||||
:param root:
|
:param root:
|
||||||
Used as an anchor when mode == ImportMode.importlib to obtain
|
Used as an anchor when mode == ImportMode.importlib to obtain
|
||||||
@@ -544,8 +576,8 @@ def import_path(
|
|||||||
|
|
||||||
if module_file.endswith((".pyc", ".pyo")):
|
if module_file.endswith((".pyc", ".pyo")):
|
||||||
module_file = module_file[:-1]
|
module_file = module_file[:-1]
|
||||||
if module_file.endswith(os.path.sep + "__init__.py"):
|
if module_file.endswith(os.sep + "__init__.py"):
|
||||||
module_file = module_file[: -(len(os.path.sep + "__init__.py"))]
|
module_file = module_file[: -(len(os.sep + "__init__.py"))]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
is_same = _is_same(str(path), module_file)
|
is_same = _is_same(str(path), module_file)
|
||||||
@@ -638,30 +670,38 @@ def resolve_package_path(path: Path) -> Optional[Path]:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def scandir(path: Union[str, "os.PathLike[str]"]) -> List["os.DirEntry[str]"]:
|
||||||
|
"""Scan a directory recursively, in breadth-first order.
|
||||||
|
|
||||||
|
The returned entries are sorted.
|
||||||
|
"""
|
||||||
|
entries = []
|
||||||
|
with os.scandir(path) as s:
|
||||||
|
# Skip entries with symlink loops and other brokenness, so the caller
|
||||||
|
# doesn't have to deal with it.
|
||||||
|
for entry in s:
|
||||||
|
try:
|
||||||
|
entry.is_file()
|
||||||
|
except OSError as err:
|
||||||
|
if _ignore_error(err):
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
entries.append(entry)
|
||||||
|
entries.sort(key=lambda entry: entry.name)
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
def visit(
|
def visit(
|
||||||
path: Union[str, "os.PathLike[str]"], recurse: Callable[["os.DirEntry[str]"], bool]
|
path: Union[str, "os.PathLike[str]"], recurse: Callable[["os.DirEntry[str]"], bool]
|
||||||
) -> Iterator["os.DirEntry[str]"]:
|
) -> Iterator["os.DirEntry[str]"]:
|
||||||
"""Walk a directory recursively, in breadth-first order.
|
"""Walk a directory recursively, in breadth-first order.
|
||||||
|
|
||||||
|
The `recurse` predicate determines whether a directory is recursed.
|
||||||
|
|
||||||
Entries at each directory level are sorted.
|
Entries at each directory level are sorted.
|
||||||
"""
|
"""
|
||||||
|
entries = scandir(path)
|
||||||
# Skip entries with symlink loops and other brokenness, so the caller doesn't
|
|
||||||
# have to deal with it.
|
|
||||||
entries = []
|
|
||||||
for entry in os.scandir(path):
|
|
||||||
try:
|
|
||||||
entry.is_file()
|
|
||||||
except OSError as err:
|
|
||||||
if _ignore_error(err):
|
|
||||||
continue
|
|
||||||
raise
|
|
||||||
entries.append(entry)
|
|
||||||
|
|
||||||
entries.sort(key=lambda entry: entry.name)
|
|
||||||
|
|
||||||
yield from entries
|
yield from entries
|
||||||
|
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
if entry.is_dir() and recurse(entry):
|
if entry.is_dir() and recurse(entry):
|
||||||
yield from visit(entry.path, recurse)
|
yield from visit(entry.path, recurse)
|
||||||
|
|||||||
+47
-41
@@ -1,4 +1,5 @@
|
|||||||
"""Python test discovery, setup and run of test functions."""
|
"""Python test discovery, setup and run of test functions."""
|
||||||
|
import dataclasses
|
||||||
import enum
|
import enum
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import inspect
|
import inspect
|
||||||
@@ -27,8 +28,6 @@ from typing import Tuple
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import attr
|
|
||||||
|
|
||||||
import _pytest
|
import _pytest
|
||||||
from _pytest import fixtures
|
from _pytest import fixtures
|
||||||
from _pytest import nodes
|
from _pytest import nodes
|
||||||
@@ -36,6 +35,7 @@ from _pytest._code import filter_traceback
|
|||||||
from _pytest._code import getfslineno
|
from _pytest._code import getfslineno
|
||||||
from _pytest._code.code import ExceptionInfo
|
from _pytest._code.code import ExceptionInfo
|
||||||
from _pytest._code.code import TerminalRepr
|
from _pytest._code.code import TerminalRepr
|
||||||
|
from _pytest._code.code import Traceback
|
||||||
from _pytest._io import TerminalWriter
|
from _pytest._io import TerminalWriter
|
||||||
from _pytest._io.saferepr import saferepr
|
from _pytest._io.saferepr import saferepr
|
||||||
from _pytest.compat import ascii_escaped
|
from _pytest.compat import ascii_escaped
|
||||||
@@ -57,7 +57,6 @@ from _pytest.config import ExitCode
|
|||||||
from _pytest.config import hookimpl
|
from _pytest.config import hookimpl
|
||||||
from _pytest.config.argparsing import Parser
|
from _pytest.config.argparsing import Parser
|
||||||
from _pytest.deprecated import check_ispytest
|
from _pytest.deprecated import check_ispytest
|
||||||
from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
|
|
||||||
from _pytest.deprecated import INSTANCE_COLLECTOR
|
from _pytest.deprecated import INSTANCE_COLLECTOR
|
||||||
from _pytest.deprecated import NOSE_SUPPORT_METHOD
|
from _pytest.deprecated import NOSE_SUPPORT_METHOD
|
||||||
from _pytest.fixtures import FuncFixtureInfo
|
from _pytest.fixtures import FuncFixtureInfo
|
||||||
@@ -403,8 +402,8 @@ class PyCollector(PyobjMixin, nodes.Collector):
|
|||||||
|
|
||||||
def istestfunction(self, obj: object, name: str) -> bool:
|
def istestfunction(self, obj: object, name: str) -> bool:
|
||||||
if self.funcnamefilter(name) or self.isnosetest(obj):
|
if self.funcnamefilter(name) or self.isnosetest(obj):
|
||||||
if isinstance(obj, staticmethod):
|
if isinstance(obj, (staticmethod, classmethod)):
|
||||||
# staticmethods need to be unwrapped.
|
# staticmethods and classmethods need to be unwrapped.
|
||||||
obj = safe_getattr(obj, "__func__", False)
|
obj = safe_getattr(obj, "__func__", False)
|
||||||
return callable(obj) and fixtures.getfixturemarker(obj) is None
|
return callable(obj) and fixtures.getfixturemarker(obj) is None
|
||||||
else:
|
else:
|
||||||
@@ -668,7 +667,7 @@ class Package(Module):
|
|||||||
config=None,
|
config=None,
|
||||||
session=None,
|
session=None,
|
||||||
nodeid=None,
|
nodeid=None,
|
||||||
path=Optional[Path],
|
path: Optional[Path] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
# NOTE: Could be just the following, but kept as-is for compat.
|
# NOTE: Could be just the following, but kept as-is for compat.
|
||||||
# nodes.FSCollector.__init__(self, fspath, parent=parent)
|
# nodes.FSCollector.__init__(self, fspath, parent=parent)
|
||||||
@@ -700,14 +699,6 @@ class Package(Module):
|
|||||||
func = partial(_call_with_optional_argument, teardown_module, self.obj)
|
func = partial(_call_with_optional_argument, teardown_module, self.obj)
|
||||||
self.addfinalizer(func)
|
self.addfinalizer(func)
|
||||||
|
|
||||||
def gethookproxy(self, fspath: "os.PathLike[str]"):
|
|
||||||
warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
|
|
||||||
return self.session.gethookproxy(fspath)
|
|
||||||
|
|
||||||
def isinitpath(self, path: Union[str, "os.PathLike[str]"]) -> bool:
|
|
||||||
warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
|
|
||||||
return self.session.isinitpath(path)
|
|
||||||
|
|
||||||
def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
|
def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
|
||||||
if direntry.name == "__pycache__":
|
if direntry.name == "__pycache__":
|
||||||
return False
|
return False
|
||||||
@@ -715,9 +706,6 @@ class Package(Module):
|
|||||||
ihook = self.session.gethookproxy(fspath.parent)
|
ihook = self.session.gethookproxy(fspath.parent)
|
||||||
if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
|
if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
|
||||||
return False
|
return False
|
||||||
norecursepatterns = self.config.getini("norecursedirs")
|
|
||||||
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
|
|
||||||
return False
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _collectfile(
|
def _collectfile(
|
||||||
@@ -746,11 +734,11 @@ class Package(Module):
|
|||||||
|
|
||||||
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
|
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
|
||||||
this_path = self.path.parent
|
this_path = self.path.parent
|
||||||
init_module = this_path / "__init__.py"
|
|
||||||
if init_module.is_file() and path_matches_patterns(
|
# Always collect the __init__ first.
|
||||||
init_module, self.config.getini("python_files")
|
if path_matches_patterns(self.path, self.config.getini("python_files")):
|
||||||
):
|
yield Module.from_parent(self, path=self.path)
|
||||||
yield Module.from_parent(self, path=init_module)
|
|
||||||
pkg_prefixes: Set[Path] = set()
|
pkg_prefixes: Set[Path] = set()
|
||||||
for direntry in visit(str(this_path), recurse=self._recurse):
|
for direntry in visit(str(this_path), recurse=self._recurse):
|
||||||
path = Path(direntry.path)
|
path = Path(direntry.path)
|
||||||
@@ -790,7 +778,8 @@ def _call_with_optional_argument(func, arg) -> None:
|
|||||||
|
|
||||||
def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> Optional[object]:
|
def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> Optional[object]:
|
||||||
"""Return the attribute from the given object to be used as a setup/teardown
|
"""Return the attribute from the given object to be used as a setup/teardown
|
||||||
xunit-style function, but only if not marked as a fixture to avoid calling it twice."""
|
xunit-style function, but only if not marked as a fixture to avoid calling it twice.
|
||||||
|
"""
|
||||||
for name in names:
|
for name in names:
|
||||||
meth: Optional[object] = getattr(obj, name, None)
|
meth: Optional[object] = getattr(obj, name, None)
|
||||||
if meth is not None and fixtures.getfixturemarker(meth) is None:
|
if meth is not None and fixtures.getfixturemarker(meth) is None:
|
||||||
@@ -848,7 +837,7 @@ class Class(PyCollector):
|
|||||||
other fixtures (#517).
|
other fixtures (#517).
|
||||||
"""
|
"""
|
||||||
setup_class = _get_first_non_fixture_func(self.obj, ("setup_class",))
|
setup_class = _get_first_non_fixture_func(self.obj, ("setup_class",))
|
||||||
teardown_class = getattr(self.obj, "teardown_class", None)
|
teardown_class = _get_first_non_fixture_func(self.obj, ("teardown_class",))
|
||||||
if setup_class is None and teardown_class is None:
|
if setup_class is None and teardown_class is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -885,12 +874,12 @@ class Class(PyCollector):
|
|||||||
emit_nose_setup_warning = True
|
emit_nose_setup_warning = True
|
||||||
setup_method = _get_first_non_fixture_func(self.obj, (setup_name,))
|
setup_method = _get_first_non_fixture_func(self.obj, (setup_name,))
|
||||||
teardown_name = "teardown_method"
|
teardown_name = "teardown_method"
|
||||||
teardown_method = getattr(self.obj, teardown_name, None)
|
teardown_method = _get_first_non_fixture_func(self.obj, (teardown_name,))
|
||||||
emit_nose_teardown_warning = False
|
emit_nose_teardown_warning = False
|
||||||
if teardown_method is None and has_nose:
|
if teardown_method is None and has_nose:
|
||||||
teardown_name = "teardown"
|
teardown_name = "teardown"
|
||||||
emit_nose_teardown_warning = True
|
emit_nose_teardown_warning = True
|
||||||
teardown_method = getattr(self.obj, teardown_name, None)
|
teardown_method = _get_first_non_fixture_func(self.obj, (teardown_name,))
|
||||||
if setup_method is None and teardown_method is None:
|
if setup_method is None and teardown_method is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -956,10 +945,20 @@ def hasnew(obj: object) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@attr.s(frozen=True, auto_attribs=True, slots=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class IdMaker:
|
class IdMaker:
|
||||||
"""Make IDs for a parametrization."""
|
"""Make IDs for a parametrization."""
|
||||||
|
|
||||||
|
__slots__ = (
|
||||||
|
"argnames",
|
||||||
|
"parametersets",
|
||||||
|
"idfn",
|
||||||
|
"ids",
|
||||||
|
"config",
|
||||||
|
"nodeid",
|
||||||
|
"func_name",
|
||||||
|
)
|
||||||
|
|
||||||
# The argnames of the parametrization.
|
# The argnames of the parametrization.
|
||||||
argnames: Sequence[str]
|
argnames: Sequence[str]
|
||||||
# The ParameterSets of the parametrization.
|
# The ParameterSets of the parametrization.
|
||||||
@@ -1109,7 +1108,7 @@ class IdMaker:
|
|||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@attr.s(frozen=True, slots=True, auto_attribs=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class CallSpec2:
|
class CallSpec2:
|
||||||
"""A planned parameterized invocation of a test function.
|
"""A planned parameterized invocation of a test function.
|
||||||
|
|
||||||
@@ -1120,18 +1119,18 @@ class CallSpec2:
|
|||||||
|
|
||||||
# arg name -> arg value which will be passed to the parametrized test
|
# arg name -> arg value which will be passed to the parametrized test
|
||||||
# function (direct parameterization).
|
# function (direct parameterization).
|
||||||
funcargs: Dict[str, object] = attr.Factory(dict)
|
funcargs: Dict[str, object] = dataclasses.field(default_factory=dict)
|
||||||
# arg name -> arg value which will be passed to a fixture of the same name
|
# arg name -> arg value which will be passed to a fixture of the same name
|
||||||
# (indirect parametrization).
|
# (indirect parametrization).
|
||||||
params: Dict[str, object] = attr.Factory(dict)
|
params: Dict[str, object] = dataclasses.field(default_factory=dict)
|
||||||
# arg name -> arg index.
|
# arg name -> arg index.
|
||||||
indices: Dict[str, int] = attr.Factory(dict)
|
indices: Dict[str, int] = dataclasses.field(default_factory=dict)
|
||||||
# Used for sorting parametrized resources.
|
# Used for sorting parametrized resources.
|
||||||
_arg2scope: Dict[str, Scope] = attr.Factory(dict)
|
_arg2scope: Dict[str, Scope] = dataclasses.field(default_factory=dict)
|
||||||
# Parts which will be added to the item's name in `[..]` separated by "-".
|
# Parts which will be added to the item's name in `[..]` separated by "-".
|
||||||
_idlist: List[str] = attr.Factory(list)
|
_idlist: List[str] = dataclasses.field(default_factory=list)
|
||||||
# Marks which will be applied to the item.
|
# Marks which will be applied to the item.
|
||||||
marks: List[Mark] = attr.Factory(list)
|
marks: List[Mark] = dataclasses.field(default_factory=list)
|
||||||
|
|
||||||
def setmulti(
|
def setmulti(
|
||||||
self,
|
self,
|
||||||
@@ -1163,9 +1162,9 @@ class CallSpec2:
|
|||||||
return CallSpec2(
|
return CallSpec2(
|
||||||
funcargs=funcargs,
|
funcargs=funcargs,
|
||||||
params=params,
|
params=params,
|
||||||
arg2scope=arg2scope,
|
|
||||||
indices=indices,
|
indices=indices,
|
||||||
idlist=[*self._idlist, id],
|
_arg2scope=arg2scope,
|
||||||
|
_idlist=[*self._idlist, id],
|
||||||
marks=[*self.marks, *normalize_mark_list(marks)],
|
marks=[*self.marks, *normalize_mark_list(marks)],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1791,7 +1790,7 @@ class Function(PyobjMixin, nodes.Item):
|
|||||||
def setup(self) -> None:
|
def setup(self) -> None:
|
||||||
self._request._fillfixtures()
|
self._request._fillfixtures()
|
||||||
|
|
||||||
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
|
def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
|
||||||
if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False):
|
if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False):
|
||||||
code = _pytest._code.Code.from_function(get_real_func(self.obj))
|
code = _pytest._code.Code.from_function(get_real_func(self.obj))
|
||||||
path, firstlineno = code.path, code.firstlineno
|
path, firstlineno = code.path, code.firstlineno
|
||||||
@@ -1803,14 +1802,21 @@ class Function(PyobjMixin, nodes.Item):
|
|||||||
ntraceback = ntraceback.filter(filter_traceback)
|
ntraceback = ntraceback.filter(filter_traceback)
|
||||||
if not ntraceback:
|
if not ntraceback:
|
||||||
ntraceback = traceback
|
ntraceback = traceback
|
||||||
|
ntraceback = ntraceback.filter(excinfo)
|
||||||
|
|
||||||
excinfo.traceback = ntraceback.filter()
|
|
||||||
# issue364: mark all but first and last frames to
|
# issue364: mark all but first and last frames to
|
||||||
# only show a single-line message for each frame.
|
# only show a single-line message for each frame.
|
||||||
if self.config.getoption("tbstyle", "auto") == "auto":
|
if self.config.getoption("tbstyle", "auto") == "auto":
|
||||||
if len(excinfo.traceback) > 2:
|
if len(ntraceback) > 2:
|
||||||
for entry in excinfo.traceback[1:-1]:
|
ntraceback = Traceback(
|
||||||
entry.set_repr_style("short")
|
entry
|
||||||
|
if i == 0 or i == len(ntraceback) - 1
|
||||||
|
else entry.with_repr_style("short")
|
||||||
|
for i, entry in enumerate(ntraceback)
|
||||||
|
)
|
||||||
|
|
||||||
|
return ntraceback
|
||||||
|
return excinfo.traceback
|
||||||
|
|
||||||
# TODO: Type ignored -- breaks Liskov Substitution.
|
# TODO: Type ignored -- breaks Liskov Substitution.
|
||||||
def repr_failure( # type: ignore[override]
|
def repr_failure( # type: ignore[override]
|
||||||
|
|||||||
+18
-16
@@ -8,7 +8,7 @@ from types import TracebackType
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import cast
|
from typing import cast
|
||||||
from typing import Generic
|
from typing import ContextManager
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Mapping
|
from typing import Mapping
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@@ -269,10 +269,16 @@ class ApproxMapping(ApproxBase):
|
|||||||
max_abs_diff = max(
|
max_abs_diff = max(
|
||||||
max_abs_diff, abs(approx_value.expected - other_value)
|
max_abs_diff, abs(approx_value.expected - other_value)
|
||||||
)
|
)
|
||||||
max_rel_diff = max(
|
if approx_value.expected == 0.0:
|
||||||
max_rel_diff,
|
max_rel_diff = math.inf
|
||||||
abs((approx_value.expected - other_value) / approx_value.expected),
|
else:
|
||||||
)
|
max_rel_diff = max(
|
||||||
|
max_rel_diff,
|
||||||
|
abs(
|
||||||
|
(approx_value.expected - other_value)
|
||||||
|
/ approx_value.expected
|
||||||
|
),
|
||||||
|
)
|
||||||
different_ids.append(approx_key)
|
different_ids.append(approx_key)
|
||||||
|
|
||||||
message_data = [
|
message_data = [
|
||||||
@@ -801,8 +807,8 @@ def raises( # noqa: F811
|
|||||||
r"""Assert that a code block/function call raises an exception.
|
r"""Assert that a code block/function call raises an exception.
|
||||||
|
|
||||||
:param typing.Type[E] | typing.Tuple[typing.Type[E], ...] expected_exception:
|
:param typing.Type[E] | typing.Tuple[typing.Type[E], ...] expected_exception:
|
||||||
The excpected exception type, or a tuple if one of multiple possible
|
The expected exception type, or a tuple if one of multiple possible
|
||||||
exception types are excepted.
|
exception types are expected.
|
||||||
:kwparam str | typing.Pattern[str] | None match:
|
:kwparam str | typing.Pattern[str] | None match:
|
||||||
If specified, a string containing a regular expression,
|
If specified, a string containing a regular expression,
|
||||||
or a regular expression object, that is tested against the string
|
or a regular expression object, that is tested against the string
|
||||||
@@ -918,10 +924,10 @@ def raises( # noqa: F811
|
|||||||
f"any special code to say 'this should never raise an exception'."
|
f"any special code to say 'this should never raise an exception'."
|
||||||
)
|
)
|
||||||
if isinstance(expected_exception, type):
|
if isinstance(expected_exception, type):
|
||||||
excepted_exceptions: Tuple[Type[E], ...] = (expected_exception,)
|
expected_exceptions: Tuple[Type[E], ...] = (expected_exception,)
|
||||||
else:
|
else:
|
||||||
excepted_exceptions = expected_exception
|
expected_exceptions = expected_exception
|
||||||
for exc in excepted_exceptions:
|
for exc in expected_exceptions:
|
||||||
if not isinstance(exc, type) or not issubclass(exc, BaseException):
|
if not isinstance(exc, type) or not issubclass(exc, BaseException):
|
||||||
msg = "expected exception must be a BaseException type, not {}" # type: ignore[unreachable]
|
msg = "expected exception must be a BaseException type, not {}" # type: ignore[unreachable]
|
||||||
not_a = exc.__name__ if isinstance(exc, type) else type(exc).__name__
|
not_a = exc.__name__ if isinstance(exc, type) else type(exc).__name__
|
||||||
@@ -944,11 +950,7 @@ def raises( # noqa: F811
|
|||||||
try:
|
try:
|
||||||
func(*args[1:], **kwargs)
|
func(*args[1:], **kwargs)
|
||||||
except expected_exception as e:
|
except expected_exception as e:
|
||||||
# We just caught the exception - there is a traceback.
|
return _pytest._code.ExceptionInfo.from_exception(e)
|
||||||
assert e.__traceback__ is not None
|
|
||||||
return _pytest._code.ExceptionInfo.from_exc_info(
|
|
||||||
(type(e), e, e.__traceback__)
|
|
||||||
)
|
|
||||||
fail(message)
|
fail(message)
|
||||||
|
|
||||||
|
|
||||||
@@ -957,7 +959,7 @@ raises.Exception = fail.Exception # type: ignore
|
|||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
class RaisesContext(Generic[E]):
|
class RaisesContext(ContextManager[_pytest._code.ExceptionInfo[E]]):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
expected_exception: Union[Type[E], Tuple[Type[E], ...]],
|
expected_exception: Union[Type[E], Tuple[Type[E], ...]],
|
||||||
|
|||||||
+28
-9
@@ -1,3 +1,4 @@
|
|||||||
|
import dataclasses
|
||||||
import os
|
import os
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
@@ -16,8 +17,6 @@ from typing import TYPE_CHECKING
|
|||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import attr
|
|
||||||
|
|
||||||
from _pytest._code.code import ExceptionChainRepr
|
from _pytest._code.code import ExceptionChainRepr
|
||||||
from _pytest._code.code import ExceptionInfo
|
from _pytest._code.code import ExceptionInfo
|
||||||
from _pytest._code.code import ExceptionRepr
|
from _pytest._code.code import ExceptionRepr
|
||||||
@@ -263,6 +262,8 @@ class TestReport(BaseReport):
|
|||||||
when: "Literal['setup', 'call', 'teardown']",
|
when: "Literal['setup', 'call', 'teardown']",
|
||||||
sections: Iterable[Tuple[str, str]] = (),
|
sections: Iterable[Tuple[str, str]] = (),
|
||||||
duration: float = 0,
|
duration: float = 0,
|
||||||
|
start: float = 0,
|
||||||
|
stop: float = 0,
|
||||||
user_properties: Optional[Iterable[Tuple[str, object]]] = None,
|
user_properties: Optional[Iterable[Tuple[str, object]]] = None,
|
||||||
**extra,
|
**extra,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -272,6 +273,8 @@ class TestReport(BaseReport):
|
|||||||
#: A (filesystempath, lineno, domaininfo) tuple indicating the
|
#: A (filesystempath, lineno, domaininfo) tuple indicating the
|
||||||
#: actual location of a test item - it might be different from the
|
#: actual location of a test item - it might be different from the
|
||||||
#: collected one e.g. if a method is inherited from a different module.
|
#: collected one e.g. if a method is inherited from a different module.
|
||||||
|
#: The filesystempath may be relative to ``config.rootdir``.
|
||||||
|
#: The line number is 0-based.
|
||||||
self.location: Tuple[str, Optional[int], str] = location
|
self.location: Tuple[str, Optional[int], str] = location
|
||||||
|
|
||||||
#: A name -> value dictionary containing all keywords and
|
#: A name -> value dictionary containing all keywords and
|
||||||
@@ -300,6 +303,11 @@ class TestReport(BaseReport):
|
|||||||
#: Time it took to run just the test.
|
#: Time it took to run just the test.
|
||||||
self.duration: float = duration
|
self.duration: float = duration
|
||||||
|
|
||||||
|
#: The system time when the call started, in seconds since the epoch.
|
||||||
|
self.start: float = start
|
||||||
|
#: The system time when the call ended, in seconds since the epoch.
|
||||||
|
self.stop: float = stop
|
||||||
|
|
||||||
self.__dict__.update(extra)
|
self.__dict__.update(extra)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
@@ -318,6 +326,8 @@ class TestReport(BaseReport):
|
|||||||
# Remove "collect" from the Literal type -- only for collection calls.
|
# Remove "collect" from the Literal type -- only for collection calls.
|
||||||
assert when != "collect"
|
assert when != "collect"
|
||||||
duration = call.duration
|
duration = call.duration
|
||||||
|
start = call.start
|
||||||
|
stop = call.stop
|
||||||
keywords = {x: 1 for x in item.keywords}
|
keywords = {x: 1 for x in item.keywords}
|
||||||
excinfo = call.excinfo
|
excinfo = call.excinfo
|
||||||
sections = []
|
sections = []
|
||||||
@@ -337,6 +347,9 @@ class TestReport(BaseReport):
|
|||||||
elif isinstance(excinfo.value, skip.Exception):
|
elif isinstance(excinfo.value, skip.Exception):
|
||||||
outcome = "skipped"
|
outcome = "skipped"
|
||||||
r = excinfo._getreprcrash()
|
r = excinfo._getreprcrash()
|
||||||
|
assert (
|
||||||
|
r is not None
|
||||||
|
), "There should always be a traceback entry for skipping a test."
|
||||||
if excinfo.value._use_item_location:
|
if excinfo.value._use_item_location:
|
||||||
path, line = item.reportinfo()[:2]
|
path, line = item.reportinfo()[:2]
|
||||||
assert line is not None
|
assert line is not None
|
||||||
@@ -362,6 +375,8 @@ class TestReport(BaseReport):
|
|||||||
when,
|
when,
|
||||||
sections,
|
sections,
|
||||||
duration,
|
duration,
|
||||||
|
start,
|
||||||
|
stop,
|
||||||
user_properties=item.user_properties,
|
user_properties=item.user_properties,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -407,7 +422,9 @@ class CollectReport(BaseReport):
|
|||||||
self.__dict__.update(extra)
|
self.__dict__.update(extra)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def location(self):
|
def location( # type:ignore[override]
|
||||||
|
self,
|
||||||
|
) -> Optional[Tuple[str, Optional[int], str]]:
|
||||||
return (self.fspath, None, self.fspath)
|
return (self.fspath, None, self.fspath)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
@@ -459,15 +476,15 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]:
|
|||||||
def serialize_repr_entry(
|
def serialize_repr_entry(
|
||||||
entry: Union[ReprEntry, ReprEntryNative]
|
entry: Union[ReprEntry, ReprEntryNative]
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
data = attr.asdict(entry)
|
data = dataclasses.asdict(entry)
|
||||||
for key, value in data.items():
|
for key, value in data.items():
|
||||||
if hasattr(value, "__dict__"):
|
if hasattr(value, "__dict__"):
|
||||||
data[key] = attr.asdict(value)
|
data[key] = dataclasses.asdict(value)
|
||||||
entry_data = {"type": type(entry).__name__, "data": data}
|
entry_data = {"type": type(entry).__name__, "data": data}
|
||||||
return entry_data
|
return entry_data
|
||||||
|
|
||||||
def serialize_repr_traceback(reprtraceback: ReprTraceback) -> Dict[str, Any]:
|
def serialize_repr_traceback(reprtraceback: ReprTraceback) -> Dict[str, Any]:
|
||||||
result = attr.asdict(reprtraceback)
|
result = dataclasses.asdict(reprtraceback)
|
||||||
result["reprentries"] = [
|
result["reprentries"] = [
|
||||||
serialize_repr_entry(x) for x in reprtraceback.reprentries
|
serialize_repr_entry(x) for x in reprtraceback.reprentries
|
||||||
]
|
]
|
||||||
@@ -477,7 +494,7 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]:
|
|||||||
reprcrash: Optional[ReprFileLocation],
|
reprcrash: Optional[ReprFileLocation],
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[Dict[str, Any]]:
|
||||||
if reprcrash is not None:
|
if reprcrash is not None:
|
||||||
return attr.asdict(reprcrash)
|
return dataclasses.asdict(reprcrash)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -573,7 +590,6 @@ def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
and "reprcrash" in reportdict["longrepr"]
|
and "reprcrash" in reportdict["longrepr"]
|
||||||
and "reprtraceback" in reportdict["longrepr"]
|
and "reprtraceback" in reportdict["longrepr"]
|
||||||
):
|
):
|
||||||
|
|
||||||
reprtraceback = deserialize_repr_traceback(
|
reprtraceback = deserialize_repr_traceback(
|
||||||
reportdict["longrepr"]["reprtraceback"]
|
reportdict["longrepr"]["reprtraceback"]
|
||||||
)
|
)
|
||||||
@@ -594,7 +610,10 @@ def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
ExceptionChainRepr, ReprExceptionInfo
|
ExceptionChainRepr, ReprExceptionInfo
|
||||||
] = ExceptionChainRepr(chain)
|
] = ExceptionChainRepr(chain)
|
||||||
else:
|
else:
|
||||||
exception_info = ReprExceptionInfo(reprtraceback, reprcrash)
|
exception_info = ReprExceptionInfo(
|
||||||
|
reprtraceback=reprtraceback,
|
||||||
|
reprcrash=reprcrash,
|
||||||
|
)
|
||||||
|
|
||||||
for section in reportdict["longrepr"]["sections"]:
|
for section in reportdict["longrepr"]["sections"]:
|
||||||
exception_info.addsection(*section)
|
exception_info.addsection(*section)
|
||||||
|
|||||||
+19
-10
@@ -1,5 +1,6 @@
|
|||||||
"""Basic collect and runtest protocol implementations."""
|
"""Basic collect and runtest protocol implementations."""
|
||||||
import bdb
|
import bdb
|
||||||
|
import dataclasses
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
@@ -14,8 +15,6 @@ from typing import TYPE_CHECKING
|
|||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import attr
|
|
||||||
|
|
||||||
from .reports import BaseReport
|
from .reports import BaseReport
|
||||||
from .reports import CollectErrorRepr
|
from .reports import CollectErrorRepr
|
||||||
from .reports import CollectReport
|
from .reports import CollectReport
|
||||||
@@ -35,6 +34,9 @@ from _pytest.outcomes import OutcomeException
|
|||||||
from _pytest.outcomes import Skipped
|
from _pytest.outcomes import Skipped
|
||||||
from _pytest.outcomes import TEST_OUTCOME
|
from _pytest.outcomes import TEST_OUTCOME
|
||||||
|
|
||||||
|
if sys.version_info[:2] < (3, 11):
|
||||||
|
from exceptiongroup import BaseExceptionGroup
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing_extensions import Literal
|
from typing_extensions import Literal
|
||||||
|
|
||||||
@@ -265,7 +267,7 @@ TResult = TypeVar("TResult", covariant=True)
|
|||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@attr.s(repr=False, init=False, auto_attribs=True)
|
@dataclasses.dataclass
|
||||||
class CallInfo(Generic[TResult]):
|
class CallInfo(Generic[TResult]):
|
||||||
"""Result/Exception info of a function invocation."""
|
"""Result/Exception info of a function invocation."""
|
||||||
|
|
||||||
@@ -512,22 +514,29 @@ class SetupState:
|
|||||||
stack is torn down.
|
stack is torn down.
|
||||||
"""
|
"""
|
||||||
needed_collectors = nextitem and nextitem.listchain() or []
|
needed_collectors = nextitem and nextitem.listchain() or []
|
||||||
exc = None
|
exceptions: List[BaseException] = []
|
||||||
while self.stack:
|
while self.stack:
|
||||||
if list(self.stack.keys()) == needed_collectors[: len(self.stack)]:
|
if list(self.stack.keys()) == needed_collectors[: len(self.stack)]:
|
||||||
break
|
break
|
||||||
node, (finalizers, _) = self.stack.popitem()
|
node, (finalizers, _) = self.stack.popitem()
|
||||||
|
these_exceptions = []
|
||||||
while finalizers:
|
while finalizers:
|
||||||
fin = finalizers.pop()
|
fin = finalizers.pop()
|
||||||
try:
|
try:
|
||||||
fin()
|
fin()
|
||||||
except TEST_OUTCOME as e:
|
except TEST_OUTCOME as e:
|
||||||
# XXX Only first exception will be seen by user,
|
these_exceptions.append(e)
|
||||||
# ideally all should be reported.
|
|
||||||
if exc is None:
|
if len(these_exceptions) == 1:
|
||||||
exc = e
|
exceptions.extend(these_exceptions)
|
||||||
if exc:
|
elif these_exceptions:
|
||||||
raise exc
|
msg = f"errors while tearing down {node!r}"
|
||||||
|
exceptions.append(BaseExceptionGroup(msg, these_exceptions[::-1]))
|
||||||
|
|
||||||
|
if len(exceptions) == 1:
|
||||||
|
raise exceptions[0]
|
||||||
|
elif exceptions:
|
||||||
|
raise BaseExceptionGroup("errors during test teardown", exceptions[::-1])
|
||||||
if nextitem is None:
|
if nextitem is None:
|
||||||
assert not self.stack
|
assert not self.stack
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Support for skip/xfail functions and markers."""
|
"""Support for skip/xfail functions and markers."""
|
||||||
|
import dataclasses
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
@@ -9,8 +10,6 @@ from typing import Optional
|
|||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
import attr
|
|
||||||
|
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config import hookimpl
|
from _pytest.config import hookimpl
|
||||||
from _pytest.config.argparsing import Parser
|
from _pytest.config.argparsing import Parser
|
||||||
@@ -157,7 +156,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool,
|
|||||||
return result, reason
|
return result, reason
|
||||||
|
|
||||||
|
|
||||||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class Skip:
|
class Skip:
|
||||||
"""The result of evaluate_skip_marks()."""
|
"""The result of evaluate_skip_marks()."""
|
||||||
|
|
||||||
@@ -192,10 +191,12 @@ def evaluate_skip_marks(item: Item) -> Optional[Skip]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class Xfail:
|
class Xfail:
|
||||||
"""The result of evaluate_xfail_marks()."""
|
"""The result of evaluate_xfail_marks()."""
|
||||||
|
|
||||||
|
__slots__ = ("reason", "run", "strict", "raises")
|
||||||
|
|
||||||
reason: str
|
reason: str
|
||||||
run: bool
|
run: bool
|
||||||
strict: bool
|
strict: bool
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ def pytest_configure(config: Config) -> None:
|
|||||||
def pytest_sessionfinish(session: Session) -> None:
|
def pytest_sessionfinish(session: Session) -> None:
|
||||||
if not session.config.getoption("stepwise"):
|
if not session.config.getoption("stepwise"):
|
||||||
assert session.config.cache is not None
|
assert session.config.cache is not None
|
||||||
|
if hasattr(session.config, "workerinput"):
|
||||||
|
# Do not update cache if this process is a xdist worker to prevent
|
||||||
|
# race conditions (#10641).
|
||||||
|
return
|
||||||
# Clear the list of failing tests if the plugin is not active.
|
# Clear the list of failing tests if the plugin is not active.
|
||||||
session.config.cache.set(STEPWISE_CACHE_DIR, [])
|
session.config.cache.set(STEPWISE_CACHE_DIR, [])
|
||||||
|
|
||||||
@@ -119,4 +123,8 @@ class StepwisePlugin:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def pytest_sessionfinish(self) -> None:
|
def pytest_sessionfinish(self) -> None:
|
||||||
|
if hasattr(self.config, "workerinput"):
|
||||||
|
# Do not update cache if this process is a xdist worker to prevent
|
||||||
|
# race conditions (#10641).
|
||||||
|
return
|
||||||
self.cache.set(STEPWISE_CACHE_DIR, self.lastfailed)
|
self.cache.set(STEPWISE_CACHE_DIR, self.lastfailed)
|
||||||
|
|||||||
+65
-16
@@ -3,10 +3,12 @@
|
|||||||
This is a good source for looking at the various reporting hooks.
|
This is a good source for looking at the various reporting hooks.
|
||||||
"""
|
"""
|
||||||
import argparse
|
import argparse
|
||||||
|
import dataclasses
|
||||||
import datetime
|
import datetime
|
||||||
import inspect
|
import inspect
|
||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
|
import textwrap
|
||||||
import warnings
|
import warnings
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from functools import partial
|
from functools import partial
|
||||||
@@ -19,6 +21,7 @@ from typing import Dict
|
|||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Mapping
|
from typing import Mapping
|
||||||
|
from typing import NamedTuple
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from typing import Set
|
from typing import Set
|
||||||
@@ -27,7 +30,6 @@ from typing import Tuple
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import attr
|
|
||||||
import pluggy
|
import pluggy
|
||||||
|
|
||||||
import _pytest._version
|
import _pytest._version
|
||||||
@@ -111,6 +113,26 @@ class MoreQuietAction(argparse.Action):
|
|||||||
namespace.quiet = getattr(namespace, "quiet", 0) + 1
|
namespace.quiet = getattr(namespace, "quiet", 0) + 1
|
||||||
|
|
||||||
|
|
||||||
|
class TestShortLogReport(NamedTuple):
|
||||||
|
"""Used to store the test status result category, shortletter and verbose word.
|
||||||
|
For example ``"rerun", "R", ("RERUN", {"yellow": True})``.
|
||||||
|
|
||||||
|
:ivar category:
|
||||||
|
The class of result, for example ``“passed”``, ``“skipped”``, ``“error”``, or the empty string.
|
||||||
|
|
||||||
|
:ivar letter:
|
||||||
|
The short letter shown as testing progresses, for example ``"."``, ``"s"``, ``"E"``, or the empty string.
|
||||||
|
|
||||||
|
:ivar word:
|
||||||
|
Verbose word is shown as testing progresses in verbose mode, for example ``"PASSED"``, ``"SKIPPED"``,
|
||||||
|
``"ERROR"``, or the empty string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
category: str
|
||||||
|
letter: str
|
||||||
|
word: Union[str, Tuple[str, Mapping[str, bool]]]
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser: Parser) -> None:
|
def pytest_addoption(parser: Parser) -> None:
|
||||||
group = parser.getgroup("terminal reporting", "Reporting", after="general")
|
group = parser.getgroup("terminal reporting", "Reporting", after="general")
|
||||||
group._addoption(
|
group._addoption(
|
||||||
@@ -229,7 +251,8 @@ def pytest_addoption(parser: Parser) -> None:
|
|||||||
parser.addini(
|
parser.addini(
|
||||||
"console_output_style",
|
"console_output_style",
|
||||||
help='Console output: "classic", or with additional progress information '
|
help='Console output: "classic", or with additional progress information '
|
||||||
'("progress" (percentage) | "count")',
|
'("progress" (percentage) | "count" | "progress-even-when-capture-no" (forces '
|
||||||
|
"progress even when capture=no)",
|
||||||
default="progress",
|
default="progress",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -287,7 +310,7 @@ def pytest_report_teststatus(report: BaseReport) -> Tuple[str, str, str]:
|
|||||||
return outcome, letter, outcome.upper()
|
return outcome, letter, outcome.upper()
|
||||||
|
|
||||||
|
|
||||||
@attr.s(auto_attribs=True)
|
@dataclasses.dataclass
|
||||||
class WarningReport:
|
class WarningReport:
|
||||||
"""Simple structure to hold warnings information captured by ``pytest_warning_recorded``.
|
"""Simple structure to hold warnings information captured by ``pytest_warning_recorded``.
|
||||||
|
|
||||||
@@ -346,14 +369,19 @@ class TerminalReporter:
|
|||||||
|
|
||||||
def _determine_show_progress_info(self) -> "Literal['progress', 'count', False]":
|
def _determine_show_progress_info(self) -> "Literal['progress', 'count', False]":
|
||||||
"""Return whether we should display progress information based on the current config."""
|
"""Return whether we should display progress information based on the current config."""
|
||||||
# do not show progress if we are not capturing output (#3038)
|
# do not show progress if we are not capturing output (#3038) unless explicitly
|
||||||
if self.config.getoption("capture", "no") == "no":
|
# overridden by progress-even-when-capture-no
|
||||||
|
if (
|
||||||
|
self.config.getoption("capture", "no") == "no"
|
||||||
|
and self.config.getini("console_output_style")
|
||||||
|
!= "progress-even-when-capture-no"
|
||||||
|
):
|
||||||
return False
|
return False
|
||||||
# do not show progress if we are showing fixture setup/teardown
|
# do not show progress if we are showing fixture setup/teardown
|
||||||
if self.config.getoption("setupshow", False):
|
if self.config.getoption("setupshow", False):
|
||||||
return False
|
return False
|
||||||
cfg: str = self.config.getini("console_output_style")
|
cfg: str = self.config.getini("console_output_style")
|
||||||
if cfg == "progress":
|
if cfg == "progress" or cfg == "progress-even-when-capture-no":
|
||||||
return "progress"
|
return "progress"
|
||||||
elif cfg == "count":
|
elif cfg == "count":
|
||||||
return "count"
|
return "count"
|
||||||
@@ -420,6 +448,28 @@ class TerminalReporter:
|
|||||||
self._tw.line()
|
self._tw.line()
|
||||||
self.currentfspath = None
|
self.currentfspath = None
|
||||||
|
|
||||||
|
def wrap_write(
|
||||||
|
self,
|
||||||
|
content: str,
|
||||||
|
*,
|
||||||
|
flush: bool = False,
|
||||||
|
margin: int = 8,
|
||||||
|
line_sep: str = "\n",
|
||||||
|
**markup: bool,
|
||||||
|
) -> None:
|
||||||
|
"""Wrap message with margin for progress info."""
|
||||||
|
width_of_current_line = self._tw.width_of_current_line
|
||||||
|
wrapped = line_sep.join(
|
||||||
|
textwrap.wrap(
|
||||||
|
" " * width_of_current_line + content,
|
||||||
|
width=self._screen_width - margin,
|
||||||
|
drop_whitespace=True,
|
||||||
|
replace_whitespace=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
wrapped = wrapped[width_of_current_line:]
|
||||||
|
self._tw.write(wrapped, flush=flush, **markup)
|
||||||
|
|
||||||
def write(self, content: str, *, flush: bool = False, **markup: bool) -> None:
|
def write(self, content: str, *, flush: bool = False, **markup: bool) -> None:
|
||||||
self._tw.write(content, flush=flush, **markup)
|
self._tw.write(content, flush=flush, **markup)
|
||||||
|
|
||||||
@@ -519,10 +569,11 @@ class TerminalReporter:
|
|||||||
def pytest_runtest_logreport(self, report: TestReport) -> None:
|
def pytest_runtest_logreport(self, report: TestReport) -> None:
|
||||||
self._tests_ran = True
|
self._tests_ran = True
|
||||||
rep = report
|
rep = report
|
||||||
res: Tuple[
|
|
||||||
str, str, Union[str, Tuple[str, Mapping[str, bool]]]
|
res = TestShortLogReport(
|
||||||
] = self.config.hook.pytest_report_teststatus(report=rep, config=self.config)
|
*self.config.hook.pytest_report_teststatus(report=rep, config=self.config)
|
||||||
category, letter, word = res
|
)
|
||||||
|
category, letter, word = res.category, res.letter, res.word
|
||||||
if not isinstance(word, tuple):
|
if not isinstance(word, tuple):
|
||||||
markup = None
|
markup = None
|
||||||
else:
|
else:
|
||||||
@@ -566,7 +617,7 @@ class TerminalReporter:
|
|||||||
formatted_reason = f" ({reason})"
|
formatted_reason = f" ({reason})"
|
||||||
|
|
||||||
if reason and formatted_reason is not None:
|
if reason and formatted_reason is not None:
|
||||||
self._tw.write(formatted_reason)
|
self.wrap_write(formatted_reason)
|
||||||
if self._show_progress_info:
|
if self._show_progress_info:
|
||||||
self._write_progress_information_filling_space()
|
self._write_progress_information_filling_space()
|
||||||
else:
|
else:
|
||||||
@@ -733,16 +784,14 @@ class TerminalReporter:
|
|||||||
self.write_line(line)
|
self.write_line(line)
|
||||||
|
|
||||||
def pytest_report_header(self, config: Config) -> List[str]:
|
def pytest_report_header(self, config: Config) -> List[str]:
|
||||||
line = "rootdir: %s" % config.rootpath
|
result = [f"rootdir: {config.rootpath}"]
|
||||||
|
|
||||||
if config.inipath:
|
if config.inipath:
|
||||||
line += ", configfile: " + bestrelpath(config.rootpath, config.inipath)
|
result.append("configfile: " + bestrelpath(config.rootpath, config.inipath))
|
||||||
|
|
||||||
if config.args_source == Config.ArgsSource.TESTPATHS:
|
if config.args_source == Config.ArgsSource.TESTPATHS:
|
||||||
testpaths: List[str] = config.getini("testpaths")
|
testpaths: List[str] = config.getini("testpaths")
|
||||||
line += ", testpaths: {}".format(", ".join(testpaths))
|
result.append("testpaths: {}".format(", ".join(testpaths)))
|
||||||
|
|
||||||
result = [line]
|
|
||||||
|
|
||||||
plugininfo = config.pluginmanager.list_plugin_distinfo()
|
plugininfo = config.pluginmanager.list_plugin_distinfo()
|
||||||
if plugininfo:
|
if plugininfo:
|
||||||
|
|||||||
+132
-24
@@ -1,40 +1,66 @@
|
|||||||
"""Support for providing temporary directories to test functions."""
|
"""Support for providing temporary directories to test functions."""
|
||||||
|
import dataclasses
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from shutil import rmtree
|
||||||
|
from typing import Any
|
||||||
|
from typing import Dict
|
||||||
|
from typing import Generator
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
import attr
|
from _pytest.nodes import Item
|
||||||
|
from _pytest.reports import CollectReport
|
||||||
|
from _pytest.stash import StashKey
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from typing_extensions import Literal
|
||||||
|
|
||||||
|
RetentionType = Literal["all", "failed", "none"]
|
||||||
|
|
||||||
|
|
||||||
|
from _pytest.config.argparsing import Parser
|
||||||
|
|
||||||
from .pathlib import LOCK_TIMEOUT
|
from .pathlib import LOCK_TIMEOUT
|
||||||
from .pathlib import make_numbered_dir
|
from .pathlib import make_numbered_dir
|
||||||
from .pathlib import make_numbered_dir_with_cleanup
|
from .pathlib import make_numbered_dir_with_cleanup
|
||||||
from .pathlib import rm_rf
|
from .pathlib import rm_rf
|
||||||
from _pytest.compat import final
|
from .pathlib import cleanup_dead_symlinks
|
||||||
|
from _pytest.compat import final, get_user_id
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
|
from _pytest.config import ExitCode
|
||||||
|
from _pytest.config import hookimpl
|
||||||
from _pytest.deprecated import check_ispytest
|
from _pytest.deprecated import check_ispytest
|
||||||
from _pytest.fixtures import fixture
|
from _pytest.fixtures import fixture
|
||||||
from _pytest.fixtures import FixtureRequest
|
from _pytest.fixtures import FixtureRequest
|
||||||
from _pytest.monkeypatch import MonkeyPatch
|
from _pytest.monkeypatch import MonkeyPatch
|
||||||
|
|
||||||
|
tmppath_result_key = StashKey[Dict[str, bool]]()
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@attr.s(init=False)
|
@dataclasses.dataclass
|
||||||
class TempPathFactory:
|
class TempPathFactory:
|
||||||
"""Factory for temporary directories under the common base temp directory.
|
"""Factory for temporary directories under the common base temp directory.
|
||||||
|
|
||||||
The base directory can be configured using the ``--basetemp`` option.
|
The base directory can be configured using the ``--basetemp`` option.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_given_basetemp = attr.ib(type=Optional[Path])
|
_given_basetemp: Optional[Path]
|
||||||
_trace = attr.ib()
|
# pluggy TagTracerSub, not currently exposed, so Any.
|
||||||
_basetemp = attr.ib(type=Optional[Path])
|
_trace: Any
|
||||||
|
_basetemp: Optional[Path]
|
||||||
|
_retention_count: int
|
||||||
|
_retention_policy: "RetentionType"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
given_basetemp: Optional[Path],
|
given_basetemp: Optional[Path],
|
||||||
|
retention_count: int,
|
||||||
|
retention_policy: "RetentionType",
|
||||||
trace,
|
trace,
|
||||||
basetemp: Optional[Path] = None,
|
basetemp: Optional[Path] = None,
|
||||||
*,
|
*,
|
||||||
@@ -49,6 +75,8 @@ class TempPathFactory:
|
|||||||
# Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012).
|
# Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012).
|
||||||
self._given_basetemp = Path(os.path.abspath(str(given_basetemp)))
|
self._given_basetemp = Path(os.path.abspath(str(given_basetemp)))
|
||||||
self._trace = trace
|
self._trace = trace
|
||||||
|
self._retention_count = retention_count
|
||||||
|
self._retention_policy = retention_policy
|
||||||
self._basetemp = basetemp
|
self._basetemp = basetemp
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -63,9 +91,23 @@ class TempPathFactory:
|
|||||||
:meta private:
|
:meta private:
|
||||||
"""
|
"""
|
||||||
check_ispytest(_ispytest)
|
check_ispytest(_ispytest)
|
||||||
|
count = int(config.getini("tmp_path_retention_count"))
|
||||||
|
if count < 0:
|
||||||
|
raise ValueError(
|
||||||
|
f"tmp_path_retention_count must be >= 0. Current input: {count}."
|
||||||
|
)
|
||||||
|
|
||||||
|
policy = config.getini("tmp_path_retention_policy")
|
||||||
|
if policy not in ("all", "failed", "none"):
|
||||||
|
raise ValueError(
|
||||||
|
f"tmp_path_retention_policy must be either all, failed, none. Current input: {policy}."
|
||||||
|
)
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
given_basetemp=config.option.basetemp,
|
given_basetemp=config.option.basetemp,
|
||||||
trace=config.trace.get("tmpdir"),
|
trace=config.trace.get("tmpdir"),
|
||||||
|
retention_count=count,
|
||||||
|
retention_policy=policy,
|
||||||
_ispytest=True,
|
_ispytest=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -133,23 +175,23 @@ class TempPathFactory:
|
|||||||
# Also, to keep things private, fixup any world-readable temp
|
# Also, to keep things private, fixup any world-readable temp
|
||||||
# rootdir's permissions. Historically 0o755 was used, so we can't
|
# rootdir's permissions. Historically 0o755 was used, so we can't
|
||||||
# just error out on this, at least for a while.
|
# just error out on this, at least for a while.
|
||||||
if sys.platform != "win32":
|
uid = get_user_id()
|
||||||
uid = os.getuid()
|
if uid is not None:
|
||||||
rootdir_stat = rootdir.stat()
|
rootdir_stat = rootdir.stat()
|
||||||
# getuid shouldn't fail, but cpython defines such a case.
|
if rootdir_stat.st_uid != uid:
|
||||||
# Let's hope for the best.
|
raise OSError(
|
||||||
if uid != -1:
|
f"The temporary directory {rootdir} is not owned by the current user. "
|
||||||
if rootdir_stat.st_uid != uid:
|
"Fix this and try again."
|
||||||
raise OSError(
|
)
|
||||||
f"The temporary directory {rootdir} is not owned by the current user. "
|
if (rootdir_stat.st_mode & 0o077) != 0:
|
||||||
"Fix this and try again."
|
os.chmod(rootdir, rootdir_stat.st_mode & ~0o077)
|
||||||
)
|
keep = self._retention_count
|
||||||
if (rootdir_stat.st_mode & 0o077) != 0:
|
if self._retention_policy == "none":
|
||||||
os.chmod(rootdir, rootdir_stat.st_mode & ~0o077)
|
keep = 0
|
||||||
basetemp = make_numbered_dir_with_cleanup(
|
basetemp = make_numbered_dir_with_cleanup(
|
||||||
prefix="pytest-",
|
prefix="pytest-",
|
||||||
root=rootdir,
|
root=rootdir,
|
||||||
keep=3,
|
keep=keep,
|
||||||
lock_timeout=LOCK_TIMEOUT,
|
lock_timeout=LOCK_TIMEOUT,
|
||||||
mode=0o700,
|
mode=0o700,
|
||||||
)
|
)
|
||||||
@@ -184,6 +226,21 @@ def pytest_configure(config: Config) -> None:
|
|||||||
mp.setattr(config, "_tmp_path_factory", _tmp_path_factory, raising=False)
|
mp.setattr(config, "_tmp_path_factory", _tmp_path_factory, raising=False)
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_addoption(parser: Parser) -> None:
|
||||||
|
parser.addini(
|
||||||
|
"tmp_path_retention_count",
|
||||||
|
help="How many sessions should we keep the `tmp_path` directories, according to `tmp_path_retention_policy`.",
|
||||||
|
default=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.addini(
|
||||||
|
"tmp_path_retention_policy",
|
||||||
|
help="Controls which directories created by the `tmp_path` fixture are kept around, based on test outcome. "
|
||||||
|
"(all/failed/none)",
|
||||||
|
default="all",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@fixture(scope="session")
|
@fixture(scope="session")
|
||||||
def tmp_path_factory(request: FixtureRequest) -> TempPathFactory:
|
def tmp_path_factory(request: FixtureRequest) -> TempPathFactory:
|
||||||
"""Return a :class:`pytest.TempPathFactory` instance for the test session."""
|
"""Return a :class:`pytest.TempPathFactory` instance for the test session."""
|
||||||
@@ -200,17 +257,68 @@ def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path:
|
|||||||
|
|
||||||
|
|
||||||
@fixture
|
@fixture
|
||||||
def tmp_path(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path:
|
def tmp_path(
|
||||||
|
request: FixtureRequest, tmp_path_factory: TempPathFactory
|
||||||
|
) -> Generator[Path, None, None]:
|
||||||
"""Return a temporary directory path object which is unique to each test
|
"""Return a temporary directory path object which is unique to each test
|
||||||
function invocation, created as a sub directory of the base temporary
|
function invocation, created as a sub directory of the base temporary
|
||||||
directory.
|
directory.
|
||||||
|
|
||||||
By default, a new base temporary directory is created each test session,
|
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
|
and old bases are removed after 3 sessions, to aid in debugging.
|
||||||
``--basetemp`` is used then it is cleared each session. See :ref:`base
|
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`.
|
temporary directory`.
|
||||||
|
|
||||||
The returned object is a :class:`pathlib.Path` object.
|
The returned object is a :class:`pathlib.Path` object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return _mk_tmp(request, tmp_path_factory)
|
path = _mk_tmp(request, tmp_path_factory)
|
||||||
|
yield path
|
||||||
|
|
||||||
|
# Remove the tmpdir if the policy is "failed" and the test passed.
|
||||||
|
tmp_path_factory: TempPathFactory = request.session.config._tmp_path_factory # type: ignore
|
||||||
|
policy = tmp_path_factory._retention_policy
|
||||||
|
result_dict = request.node.stash[tmppath_result_key]
|
||||||
|
|
||||||
|
if policy == "failed" and result_dict.get("call", True):
|
||||||
|
# We do a "best effort" to remove files, but it might not be possible due to some leaked resource,
|
||||||
|
# permissions, etc, in which case we ignore it.
|
||||||
|
rmtree(path, ignore_errors=True)
|
||||||
|
|
||||||
|
del request.node.stash[tmppath_result_key]
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_sessionfinish(session, exitstatus: Union[int, ExitCode]):
|
||||||
|
"""After each session, remove base directory if all the tests passed,
|
||||||
|
the policy is "failed", and the basetemp is not specified by a user.
|
||||||
|
"""
|
||||||
|
tmp_path_factory: TempPathFactory = session.config._tmp_path_factory
|
||||||
|
basetemp = tmp_path_factory._basetemp
|
||||||
|
if basetemp is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
policy = tmp_path_factory._retention_policy
|
||||||
|
if (
|
||||||
|
exitstatus == 0
|
||||||
|
and policy == "failed"
|
||||||
|
and tmp_path_factory._given_basetemp is None
|
||||||
|
):
|
||||||
|
if basetemp.is_dir():
|
||||||
|
# We do a "best effort" to remove files, but it might not be possible due to some leaked resource,
|
||||||
|
# permissions, etc, in which case we ignore it.
|
||||||
|
rmtree(basetemp, ignore_errors=True)
|
||||||
|
|
||||||
|
# Remove dead symlinks.
|
||||||
|
if basetemp.is_dir():
|
||||||
|
cleanup_dead_symlinks(basetemp)
|
||||||
|
|
||||||
|
|
||||||
|
@hookimpl(tryfirst=True, hookwrapper=True)
|
||||||
|
def pytest_runtest_makereport(item: Item, call):
|
||||||
|
outcome = yield
|
||||||
|
result: CollectReport = outcome.get_result()
|
||||||
|
|
||||||
|
empty: Dict[str, bool] = {}
|
||||||
|
item.stash.setdefault(tmppath_result_key, empty)[result.when] = result.passed
|
||||||
|
|||||||
+11
-7
@@ -298,6 +298,9 @@ class TestCaseFunction(Function):
|
|||||||
def stopTest(self, testcase: "unittest.TestCase") -> None:
|
def stopTest(self, testcase: "unittest.TestCase") -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def addDuration(self, testcase: "unittest.TestCase", elapsed: float) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
def runtest(self) -> None:
|
def runtest(self) -> None:
|
||||||
from _pytest.debugging import maybe_wrap_pytest_function_for_tracing
|
from _pytest.debugging import maybe_wrap_pytest_function_for_tracing
|
||||||
|
|
||||||
@@ -331,15 +334,16 @@ class TestCaseFunction(Function):
|
|||||||
finally:
|
finally:
|
||||||
delattr(self._testcase, self.name)
|
delattr(self._testcase, self.name)
|
||||||
|
|
||||||
def _prunetraceback(
|
def _traceback_filter(
|
||||||
self, excinfo: _pytest._code.ExceptionInfo[BaseException]
|
self, excinfo: _pytest._code.ExceptionInfo[BaseException]
|
||||||
) -> None:
|
) -> _pytest._code.Traceback:
|
||||||
super()._prunetraceback(excinfo)
|
traceback = super()._traceback_filter(excinfo)
|
||||||
traceback = excinfo.traceback.filter(
|
ntraceback = traceback.filter(
|
||||||
lambda x: not x.frame.f_globals.get("__unittest")
|
lambda x: not x.frame.f_globals.get("__unittest"),
|
||||||
)
|
)
|
||||||
if traceback:
|
if not ntraceback:
|
||||||
excinfo.traceback = traceback
|
ntraceback = traceback
|
||||||
|
return ntraceback
|
||||||
|
|
||||||
|
|
||||||
@hookimpl(tryfirst=True)
|
@hookimpl(tryfirst=True)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import dataclasses
|
||||||
import inspect
|
import inspect
|
||||||
import warnings
|
import warnings
|
||||||
from types import FunctionType
|
from types import FunctionType
|
||||||
@@ -6,8 +7,6 @@ from typing import Generic
|
|||||||
from typing import Type
|
from typing import Type
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
|
|
||||||
import attr
|
|
||||||
|
|
||||||
from _pytest.compat import final
|
from _pytest.compat import final
|
||||||
|
|
||||||
|
|
||||||
@@ -130,7 +129,7 @@ _W = TypeVar("_W", bound=PytestWarning)
|
|||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@attr.s(auto_attribs=True)
|
@dataclasses.dataclass
|
||||||
class UnformattedWarning(Generic[_W]):
|
class UnformattedWarning(Generic[_W]):
|
||||||
"""A warning meant to be formatted during runtime.
|
"""A warning meant to be formatted during runtime.
|
||||||
|
|
||||||
@@ -150,7 +149,7 @@ def warn_explicit_for(method: FunctionType, message: PytestWarning) -> None:
|
|||||||
"""
|
"""
|
||||||
Issue the warning :param:`message` for the definition of the given :param:`method`
|
Issue the warning :param:`message` for the definition of the given :param:`method`
|
||||||
|
|
||||||
this helps to log warnigns for functions defined prior to finding an issue with them
|
this helps to log warnings for functions defined prior to finding an issue with them
|
||||||
(like hook wrappers being marked in a legacy mechanism)
|
(like hook wrappers being marked in a legacy mechanism)
|
||||||
"""
|
"""
|
||||||
lineno = method.__code__.co_firstlineno
|
lineno = method.__code__.co_firstlineno
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ from _pytest.reports import TestReport
|
|||||||
from _pytest.runner import CallInfo
|
from _pytest.runner import CallInfo
|
||||||
from _pytest.stash import Stash
|
from _pytest.stash import Stash
|
||||||
from _pytest.stash import StashKey
|
from _pytest.stash import StashKey
|
||||||
|
from _pytest.terminal import TestShortLogReport
|
||||||
from _pytest.tmpdir import TempPathFactory
|
from _pytest.tmpdir import TempPathFactory
|
||||||
from _pytest.warning_types import PytestAssertRewriteWarning
|
from _pytest.warning_types import PytestAssertRewriteWarning
|
||||||
from _pytest.warning_types import PytestCacheWarning
|
from _pytest.warning_types import PytestCacheWarning
|
||||||
@@ -152,6 +153,7 @@ __all__ = [
|
|||||||
"TempPathFactory",
|
"TempPathFactory",
|
||||||
"Testdir",
|
"Testdir",
|
||||||
"TestReport",
|
"TestReport",
|
||||||
|
"TestShortLogReport",
|
||||||
"UsageError",
|
"UsageError",
|
||||||
"WarningsRecorder",
|
"WarningsRecorder",
|
||||||
"warns",
|
"warns",
|
||||||
|
|||||||
@@ -803,7 +803,7 @@ class TestLocalPath(CommonFSTests):
|
|||||||
# depending on how the paths are used), but > 4096 (which is the
|
# depending on how the paths are used), but > 4096 (which is the
|
||||||
# Linux' limitation) - the behaviour of paths with names > 4096 chars
|
# Linux' limitation) - the behaviour of paths with names > 4096 chars
|
||||||
# is undetermined
|
# is undetermined
|
||||||
newfilename = "/test" * 60
|
newfilename = "/test" * 60 # type:ignore[unreachable]
|
||||||
l1 = tmpdir.join(newfilename)
|
l1 = tmpdir.join(newfilename)
|
||||||
l1.ensure(file=True)
|
l1.ensure(file=True)
|
||||||
l1.write("foo")
|
l1.write("foo")
|
||||||
@@ -1344,8 +1344,8 @@ class TestPOSIXLocalPath:
|
|||||||
assert realpath.basename == "file"
|
assert realpath.basename == "file"
|
||||||
|
|
||||||
def test_owner(self, path1, tmpdir):
|
def test_owner(self, path1, tmpdir):
|
||||||
from pwd import getpwuid
|
from pwd import getpwuid # type:ignore[attr-defined]
|
||||||
from grp import getgrgid
|
from grp import getgrgid # type:ignore[attr-defined]
|
||||||
|
|
||||||
stat = path1.stat()
|
stat = path1.stat()
|
||||||
assert stat.path == path1
|
assert stat.path == path1
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user