Compare commits
309 Commits
8.0.0.dev0
...
report_xfa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0de126848e | ||
|
|
b247f574a3 | ||
|
|
0cd183dc0a | ||
|
|
1ce234dc78 | ||
|
|
8fb7e8b31e | ||
|
|
0d74a1c8a0 | ||
|
|
730c7ca0b1 | ||
|
|
4ecf313604 | ||
|
|
d0a09d8627 | ||
|
|
738ae2da9d | ||
|
|
7156a97f9a | ||
|
|
ed8701a57f | ||
|
|
247436819a | ||
|
|
c1728948ac | ||
|
|
dcd8b145d9 | ||
|
|
c7e9b22f37 | ||
|
|
fbe3e29a55 | ||
|
|
667b9fd7fd | ||
|
|
38f7c1e346 | ||
|
|
bcd9664370 | ||
|
|
7e69ce7449 | ||
|
|
395bbae8a2 | ||
|
|
ee53433542 | ||
|
|
304ab8495e | ||
|
|
40e9abd66b | ||
|
|
cac1eed0ea | ||
|
|
cdddd6d695 | ||
|
|
dd68f9c95a | ||
|
|
3ab70cd561 | ||
|
|
23825f2983 | ||
|
|
fb3a46dd8a | ||
|
|
3d6d93d0c2 | ||
|
|
2401d76655 | ||
|
|
af9b1dcc24 | ||
|
|
696859fc43 | ||
|
|
e966dcd93c | ||
|
|
54623f0f33 | ||
|
|
9bbfe995ee | ||
|
|
d015bc1b8f | ||
|
|
b73b4c464c | ||
|
|
1a16bac131 | ||
|
|
81192ca85f | ||
|
|
486a9ed057 | ||
|
|
4ae102c003 | ||
|
|
c614590ec9 | ||
|
|
d2b214220f | ||
|
|
a38ad254ef | ||
|
|
9f22d3281c | ||
|
|
9a58e6283d | ||
|
|
8bac8d7807 | ||
|
|
5e081162df | ||
|
|
8062743f6b | ||
|
|
8b7f94f145 | ||
|
|
5ace48ca5b | ||
|
|
e7caaa0b3e | ||
|
|
a47fcb4873 | ||
|
|
ab8f5ce7f4 | ||
|
|
f43a8db618 | ||
|
|
6c2feb75d2 | ||
|
|
fcb8e73288 | ||
|
|
241f2a890e | ||
|
|
39f9306357 | ||
|
|
e0d04bdfab | ||
|
|
1949b09fd3 | ||
|
|
3d1c52f203 | ||
|
|
a60c23c3d8 | ||
|
|
24a6ee1ffd | ||
|
|
e2acc1a99b | ||
|
|
71f265f1f3 | ||
|
|
7259e8db98 | ||
|
|
dd7beb39d6 | ||
|
|
6ad9499c9c | ||
|
|
2ed2e9208d | ||
|
|
ab63ebb3dc | ||
|
|
b3a981d385 | ||
|
|
48b0395648 | ||
|
|
9c11275553 | ||
|
|
e5c81fa41a | ||
|
|
0a06db0729 | ||
|
|
5936a79fdb | ||
|
|
28ccf476b9 | ||
|
|
333e4eba6b | ||
|
|
e787d2ed48 | ||
|
|
f6b6478868 | ||
|
|
3ce63bc768 | ||
|
|
faa8f2ea08 | ||
|
|
194a782e38 | ||
|
|
bc71561ad9 | ||
|
|
574e0f45d9 | ||
|
|
b8906b29a7 | ||
|
|
d2b5177dd6 | ||
|
|
65c01f531b | ||
|
|
82bd63d318 | ||
|
|
d4872f5df7 | ||
|
|
8d815ca55b | ||
|
|
8032d21271 | ||
|
|
9c8937b480 | ||
|
|
0d8b87f161 | ||
|
|
65c73a09e7 | ||
|
|
917ce9aa01 | ||
|
|
4e3a0874df | ||
|
|
ba0da81f88 | ||
|
|
f08782d8d0 | ||
|
|
5b528bd131 | ||
|
|
e4794b26b2 | ||
|
|
ab6cae2126 | ||
|
|
afb8d66e42 | ||
|
|
76ba7db6ce | ||
|
|
941b203c94 | ||
|
|
19d6b12b2a | ||
|
|
17e8f2b3fc | ||
|
|
ff23347f1f | ||
|
|
00fedcc439 | ||
|
|
77f7f59b2a | ||
|
|
7500fe44b2 | ||
|
|
23b899f31f | ||
|
|
43d1398fc7 | ||
|
|
03832fa31e | ||
|
|
8f36fd5454 | ||
|
|
cada6c105a | ||
|
|
3f446b68fd | ||
|
|
050f402816 | ||
|
|
d1722d5c18 | ||
|
|
4deb38b2ed | ||
|
|
370eacd3ca | ||
|
|
010e1742d8 | ||
|
|
6e5f10b28f | ||
|
|
dd5ae0c3b8 | ||
|
|
fc653d0d12 | ||
|
|
a357c7abc8 | ||
|
|
61133ba83d | ||
|
|
049eec8474 | ||
|
|
37bb186175 | ||
|
|
3cb3cd1a08 | ||
|
|
7a625481da | ||
|
|
0ddfdfcc04 | ||
|
|
87bfc83aa0 | ||
|
|
ebd571bb18 | ||
|
|
176d728b7b | ||
|
|
d4fb6ac9f7 | ||
|
|
15fadd8c5c | ||
|
|
73d754bd74 | ||
|
|
10056865d2 | ||
|
|
1827d8d5f9 | ||
|
|
9e164fc4fe | ||
|
|
c0cf822ca1 | ||
|
|
ec1053cc16 | ||
|
|
d2dc8a70b5 | ||
|
|
b9cb87d862 | ||
|
|
e938580257 | ||
|
|
47c0fc3d78 | ||
|
|
556e075d23 | ||
|
|
3ad3fc6b8f | ||
|
|
09b78737a5 | ||
|
|
b2186e2455 | ||
|
|
18bc6c9a0e | ||
|
|
24c9aa6c30 | ||
|
|
9c67b7aeb6 | ||
|
|
1cc58ed67f | ||
|
|
84a342e27c | ||
|
|
e8a8a5f320 | ||
|
|
1c04a92503 | ||
|
|
cc0adf6bf3 | ||
|
|
b8b74331b4 | ||
|
|
4797deab99 | ||
|
|
c9163402e0 | ||
|
|
01ac13a77d | ||
|
|
485c555812 | ||
|
|
a21fb87a90 | ||
|
|
c754da10d2 | ||
|
|
71e627aa8f | ||
|
|
d3552ef4c0 | ||
|
|
448563caaa | ||
|
|
e8aa906e06 | ||
|
|
12054a4972 | ||
|
|
430ad145c1 | ||
|
|
13e2b00258 | ||
|
|
4e42421ebf | ||
|
|
161ba87300 | ||
|
|
f2b6040e9e | ||
|
|
e3247834e2 | ||
|
|
0b4a557087 | ||
|
|
ffdcce67f4 | ||
|
|
1ded74739b | ||
|
|
497a1d798a | ||
|
|
1de0923e83 | ||
|
|
7c30f674c5 | ||
|
|
396bfbf30b | ||
|
|
02ba39bfcd | ||
|
|
29010d23a6 | ||
|
|
de1f6f58ba | ||
|
|
cfda801ebf | ||
|
|
c5262b0c42 | ||
|
|
ff6e110161 | ||
|
|
0e0ed2af95 | ||
|
|
a3fbf24389 | ||
|
|
bf451d47a1 | ||
|
|
578fbe3dfd | ||
|
|
a668719626 | ||
|
|
04e0db7e48 | ||
|
|
32f480814c | ||
|
|
40ed678885 | ||
|
|
fb55615d5e | ||
|
|
9d0ddb4625 | ||
|
|
01f38aca44 | ||
|
|
78d81ef865 | ||
|
|
b41acaea12 | ||
|
|
7008385253 | ||
|
|
b25a3adff5 | ||
|
|
ecfab4dc8b | ||
|
|
2c80de532f | ||
|
|
7967b2e710 | ||
|
|
f1c9570a0e | ||
|
|
b20e7f6d0c | ||
|
|
b91d5a3112 | ||
|
|
4e75bff71a | ||
|
|
99ab8ae884 | ||
|
|
782cacf86b | ||
|
|
a3b4220d76 | ||
|
|
fd7a4d2429 | ||
|
|
b73ec8e5d1 | ||
|
|
d790e96765 | ||
|
|
d4265448a5 | ||
|
|
db37e34613 | ||
|
|
c5b13099e6 | ||
|
|
97ed533f63 | ||
|
|
f5a9aa0b84 | ||
|
|
901316b3f4 | ||
|
|
b81003f6fb | ||
|
|
ddd773ecb1 | ||
|
|
44604f49cd | ||
|
|
2203897086 | ||
|
|
119cec0279 | ||
|
|
b5bc53e441 | ||
|
|
084d756ae6 | ||
|
|
d7dbadbffc | ||
|
|
cb732f7f49 | ||
|
|
7775e494b1 | ||
|
|
1f5058e972 | ||
|
|
6baf9f2d31 | ||
|
|
c4876c7106 | ||
|
|
6badb6f01e | ||
|
|
4517af1e28 | ||
|
|
3d0dedb5ec | ||
|
|
7b7bd304aa | ||
|
|
2706271f66 | ||
|
|
8ac3c645fa | ||
|
|
18e87c9831 | ||
|
|
6995257cf4 | ||
|
|
bea56b30af | ||
|
|
b847084224 | ||
|
|
024e62e6d2 | ||
|
|
f9410fddcd | ||
|
|
561f1a993b | ||
|
|
b77d0deaf5 | ||
|
|
2f7415cfbc | ||
|
|
ba60649680 | ||
|
|
679bd6f2ed | ||
|
|
a50ea1b8e7 | ||
|
|
0353a94cd1 | ||
|
|
0311fc3384 | ||
|
|
7fdc8391e2 | ||
|
|
15524f34d2 | ||
|
|
2d48171e88 | ||
|
|
7022fb455d | ||
|
|
a1b37022af | ||
|
|
9279ea2882 | ||
|
|
165fbbd12a | ||
|
|
f617bab0a2 | ||
|
|
f4e3b4ad98 | ||
|
|
81cfb3fc87 | ||
|
|
dd5f3773d9 | ||
|
|
e132d6488f | ||
|
|
4719d998d8 | ||
|
|
5332656906 | ||
|
|
a14fc10cac | ||
|
|
05d7e60904 | ||
|
|
4ebb2b94c2 | ||
|
|
9859c110cc | ||
|
|
5eaf8207eb | ||
|
|
518ca37cae | ||
|
|
45f1a462d5 | ||
|
|
4cd95eeabf | ||
|
|
1306b84241 | ||
|
|
fe51121f39 | ||
|
|
c8b1790ee7 | ||
|
|
2870157234 | ||
|
|
1b7896f83d | ||
|
|
a4a189ad99 | ||
|
|
bd88a6412d | ||
|
|
264e7ac327 | ||
|
|
a2feb6bd00 | ||
|
|
246ceb84bd | ||
|
|
2fd160110c | ||
|
|
2998b596b4 | ||
|
|
7f73722c4a | ||
|
|
f7542f6292 | ||
|
|
0de2aa93f0 | ||
|
|
7759a9d3c2 | ||
|
|
d86df89a92 | ||
|
|
24fd292878 | ||
|
|
a1b10b552a | ||
|
|
aa9cc7e8b4 | ||
|
|
5c286d19d8 | ||
|
|
faac197fcf | ||
|
|
5227279c15 | ||
|
|
a7e0ae2455 | ||
|
|
12efc58479 | ||
|
|
3f71680ac0 |
2
.github/workflows/backport.yml
vendored
2
.github/workflows/backport.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
|
||||
66
.github/workflows/deploy.yml
vendored
66
.github/workflows/deploy.yml
vendored
@@ -1,35 +1,44 @@
|
||||
name: deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
# These tags are protected, see:
|
||||
# https://github.com/pytest-dev/pytest/settings/tag_protection
|
||||
- "[0-9]+.[0-9]+.[0-9]+"
|
||||
- "[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Release version'
|
||||
required: true
|
||||
default: '1.2.3'
|
||||
|
||||
|
||||
# Set permissions at the job level.
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
|
||||
deploy:
|
||||
if: github.repository == 'pytest-dev/pytest'
|
||||
|
||||
package:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: write
|
||||
env:
|
||||
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ github.event.inputs.version }}
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Build and Check Package
|
||||
uses: hynek/build-and-inspect-python-package@v1.5
|
||||
uses: hynek/build-and-inspect-python-package@v1.5.3
|
||||
|
||||
deploy:
|
||||
if: github.repository == 'pytest-dev/pytest'
|
||||
needs: [package]
|
||||
runs-on: ubuntu-latest
|
||||
environment: deploy
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download Package
|
||||
uses: actions/download-artifact@v3
|
||||
@@ -38,14 +47,35 @@ jobs:
|
||||
path: dist
|
||||
|
||||
- name: Publish package to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.10
|
||||
|
||||
- name: Push tag
|
||||
run: |
|
||||
git config user.name "pytest bot"
|
||||
git config user.email "pytestbot@gmail.com"
|
||||
git tag --annotate --message=v${{ github.event.inputs.version }} ${{ github.event.inputs.version }} ${{ github.sha }}
|
||||
git push origin ${{ github.event.inputs.version }}
|
||||
|
||||
release-notes:
|
||||
|
||||
# todo: generate the content in the build job
|
||||
# the goal being of using a github action script to push the release data
|
||||
# after success instead of creating a complete python/tox env
|
||||
needs: [deploy]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
password: ${{ secrets.pypi_token }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.7"
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install tox
|
||||
run: |
|
||||
|
||||
2
.github/workflows/prepare-release-pr.yml
vendored
2
.github/workflows/prepare-release-pr.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
94
.github/workflows/test.yml
vendored
94
.github/workflows/test.yml
vendored
@@ -27,7 +27,19 @@ concurrency:
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
package:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Build and Check Package
|
||||
uses: hynek/build-and-inspect-python-package@v1.5.3
|
||||
|
||||
build:
|
||||
needs: [package]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 45
|
||||
permissions:
|
||||
@@ -37,48 +49,41 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
name: [
|
||||
"windows-py37",
|
||||
"windows-py37-pluggy",
|
||||
"windows-py38",
|
||||
"windows-py38-pluggy",
|
||||
"windows-py39",
|
||||
"windows-py310",
|
||||
"windows-py311",
|
||||
"windows-py312",
|
||||
|
||||
"ubuntu-py37",
|
||||
"ubuntu-py37-pluggy",
|
||||
"ubuntu-py37-freeze",
|
||||
"ubuntu-py38",
|
||||
"ubuntu-py38-pluggy",
|
||||
"ubuntu-py38-freeze",
|
||||
"ubuntu-py39",
|
||||
"ubuntu-py310",
|
||||
"ubuntu-py311",
|
||||
"ubuntu-py312",
|
||||
"ubuntu-pypy3",
|
||||
|
||||
"macos-py37",
|
||||
"macos-py38",
|
||||
"macos-py39",
|
||||
"macos-py310",
|
||||
"macos-py312",
|
||||
|
||||
"docs",
|
||||
"doctesting",
|
||||
"plugins",
|
||||
]
|
||||
|
||||
include:
|
||||
- name: "windows-py37"
|
||||
python: "3.7"
|
||||
os: windows-latest
|
||||
tox_env: "py37-numpy"
|
||||
- name: "windows-py37-pluggy"
|
||||
python: "3.7"
|
||||
os: windows-latest
|
||||
tox_env: "py37-pluggymain-pylib-xdist"
|
||||
- name: "windows-py38"
|
||||
python: "3.8"
|
||||
os: windows-latest
|
||||
tox_env: "py38-unittestextras"
|
||||
use_coverage: true
|
||||
- name: "windows-py38-pluggy"
|
||||
python: "3.8"
|
||||
os: windows-latest
|
||||
tox_env: "py38-pluggymain-pylib-xdist"
|
||||
- name: "windows-py39"
|
||||
python: "3.9"
|
||||
os: windows-latest
|
||||
@@ -96,23 +101,19 @@ jobs:
|
||||
os: windows-latest
|
||||
tox_env: "py312"
|
||||
|
||||
- name: "ubuntu-py37"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py37-lsof-numpy-pexpect"
|
||||
use_coverage: true
|
||||
- name: "ubuntu-py37-pluggy"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py37-pluggymain-pylib-xdist"
|
||||
- name: "ubuntu-py37-freeze"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py37-freeze"
|
||||
- name: "ubuntu-py38"
|
||||
python: "3.8"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py38-xdist"
|
||||
tox_env: "py38-lsof-numpy-pexpect"
|
||||
use_coverage: true
|
||||
- name: "ubuntu-py38-pluggy"
|
||||
python: "3.8"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py38-pluggymain-pylib-xdist"
|
||||
- name: "ubuntu-py38-freeze"
|
||||
python: "3.8"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py38-freeze"
|
||||
- name: "ubuntu-py39"
|
||||
python: "3.9"
|
||||
os: ubuntu-latest
|
||||
@@ -132,14 +133,14 @@ jobs:
|
||||
tox_env: "py312"
|
||||
use_coverage: true
|
||||
- name: "ubuntu-pypy3"
|
||||
python: "pypy-3.7"
|
||||
python: "pypy-3.8"
|
||||
os: ubuntu-latest
|
||||
tox_env: "pypy3-xdist"
|
||||
|
||||
- name: "macos-py37"
|
||||
python: "3.7"
|
||||
- name: "macos-py38"
|
||||
python: "3.8"
|
||||
os: macos-latest
|
||||
tox_env: "py37-xdist"
|
||||
tox_env: "py38-xdist"
|
||||
- name: "macos-py39"
|
||||
python: "3.9"
|
||||
os: macos-latest
|
||||
@@ -159,22 +160,24 @@ jobs:
|
||||
os: ubuntu-latest
|
||||
tox_env: "plugins"
|
||||
|
||||
- name: "docs"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
tox_env: "docs"
|
||||
- name: "doctesting"
|
||||
python: "3.7"
|
||||
python: "3.8"
|
||||
os: ubuntu-latest
|
||||
tox_env: "doctesting"
|
||||
use_coverage: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download Package
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: Packages
|
||||
path: dist
|
||||
|
||||
- name: Set up Python ${{ matrix.python }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
@@ -188,11 +191,13 @@ jobs:
|
||||
|
||||
- name: Test without coverage
|
||||
if: "! matrix.use_coverage"
|
||||
run: "tox -e ${{ matrix.tox_env }}"
|
||||
shell: bash
|
||||
run: tox run -e ${{ matrix.tox_env }} --installpkg `find dist/*.tar.gz`
|
||||
|
||||
- name: Test with coverage
|
||||
if: "matrix.use_coverage"
|
||||
run: "tox -e ${{ matrix.tox_env }}-coverage"
|
||||
shell: bash
|
||||
run: tox run -e ${{ matrix.tox_env }}-coverage --installpkg `find dist/*.tar.gz`
|
||||
|
||||
- name: Generate coverage report
|
||||
if: "matrix.use_coverage"
|
||||
@@ -206,10 +211,3 @@ jobs:
|
||||
fail_ci_if_error: true
|
||||
files: ./coverage.xml
|
||||
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
|
||||
|
||||
14
.github/workflows/update-plugin-list.yml
vendored
14
.github/workflows/update-plugin-list.yml
vendored
@@ -20,19 +20,27 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: "3.11"
|
||||
cache: pip
|
||||
- name: requests-cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/pytest-plugin-list/
|
||||
key: plugins-http-cache-${{ github.run_id }} # Can use time based key as well
|
||||
restore-keys: plugins-http-cache-
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install packaging requests tabulate[widechars] tqdm
|
||||
pip install packaging requests tabulate[widechars] tqdm requests-cache platformdirs
|
||||
|
||||
|
||||
- name: Update Plugin List
|
||||
run: python scripts/update-plugin-list.py
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.3.0
|
||||
rev: 23.10.1
|
||||
hooks:
|
||||
- id: black
|
||||
args: [--safe, --quiet]
|
||||
- repo: https://github.com/asottile/blacken-docs
|
||||
rev: 1.14.0
|
||||
rev: 1.16.0
|
||||
hooks:
|
||||
- id: blacken-docs
|
||||
additional_dependencies: [black==23.1.0]
|
||||
additional_dependencies: [black==23.7.0]
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
rev: v4.5.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
@@ -21,7 +21,7 @@ repos:
|
||||
exclude: _pytest/(debugging|hookspec).py
|
||||
language_version: python3
|
||||
- repo: https://github.com/PyCQA/autoflake
|
||||
rev: v2.1.1
|
||||
rev: v2.2.1
|
||||
hooks:
|
||||
- id: autoflake
|
||||
name: autoflake
|
||||
@@ -29,7 +29,7 @@ repos:
|
||||
language: python
|
||||
files: \.py$
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.0.0
|
||||
rev: 6.1.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
language_version: python3
|
||||
@@ -37,17 +37,17 @@ repos:
|
||||
- flake8-typing-imports==1.12.0
|
||||
- flake8-docstrings==1.5.0
|
||||
- repo: https://github.com/asottile/reorder-python-imports
|
||||
rev: v3.10.0
|
||||
rev: v3.12.0
|
||||
hooks:
|
||||
- id: reorder-python-imports
|
||||
args: ['--application-directories=.:src', --py37-plus]
|
||||
args: ['--application-directories=.:src', --py38-plus]
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.7.0
|
||||
rev: v3.15.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py37-plus]
|
||||
args: [--py38-plus]
|
||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||
rev: v2.3.0
|
||||
rev: v2.5.0
|
||||
hooks:
|
||||
- id: setup-cfg-fmt
|
||||
args: ["--max-py-version=3.12", "--include-version-classifiers"]
|
||||
@@ -56,7 +56,7 @@ repos:
|
||||
hooks:
|
||||
- id: python-use-type-annotations
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.3.0
|
||||
rev: v1.6.1
|
||||
hooks:
|
||||
- id: mypy
|
||||
files: ^(src/|testing/)
|
||||
|
||||
@@ -9,6 +9,10 @@ python:
|
||||
path: .
|
||||
- requirements: doc/en/requirements.txt
|
||||
|
||||
sphinx:
|
||||
configuration: doc/en/conf.py
|
||||
fail_on_warning: true
|
||||
|
||||
build:
|
||||
os: ubuntu-20.04
|
||||
tools:
|
||||
|
||||
20
AUTHORS
20
AUTHORS
@@ -11,6 +11,7 @@ Adam Johnson
|
||||
Adam Stewart
|
||||
Adam Uhlir
|
||||
Ahn Ki-Wook
|
||||
Akhilesh Ramakrishnan
|
||||
Akiomi Kamakura
|
||||
Alan Velasco
|
||||
Alessio Izzo
|
||||
@@ -55,7 +56,9 @@ Barney Gale
|
||||
Ben Gartner
|
||||
Ben Webb
|
||||
Benjamin Peterson
|
||||
Benjamin Schubert
|
||||
Bernard Pratz
|
||||
Bo Wu
|
||||
Bob Ippolito
|
||||
Brian Dorsey
|
||||
Brian Larsen
|
||||
@@ -129,6 +132,7 @@ Eric Hunsberger
|
||||
Eric Liu
|
||||
Eric Siegerman
|
||||
Erik Aronesty
|
||||
Erik Hasse
|
||||
Erik M. Bray
|
||||
Evan Kepner
|
||||
Evgeny Seliverstov
|
||||
@@ -140,6 +144,7 @@ Feng Ma
|
||||
Florian Bruhin
|
||||
Florian Dahlitz
|
||||
Floris Bruynooghe
|
||||
Fraser Stark
|
||||
Gabriel Landau
|
||||
Gabriel Reis
|
||||
Garvit Shubham
|
||||
@@ -166,6 +171,8 @@ Ian Bicking
|
||||
Ian Lesperance
|
||||
Ilya Konstantinov
|
||||
Ionuț Turturică
|
||||
Isaac Virshup
|
||||
Israel Fruchter
|
||||
Itxaso Aizpurua
|
||||
Iwan Briquemont
|
||||
Jaap Broekhuizen
|
||||
@@ -229,6 +236,7 @@ Maho
|
||||
Maik Figura
|
||||
Mandeep Bhutani
|
||||
Manuel Krebber
|
||||
Marc Mueller
|
||||
Marc Schlaich
|
||||
Marcelo Duarte Trevisani
|
||||
Marcin Bachry
|
||||
@@ -259,8 +267,10 @@ Michal Wajszczuk
|
||||
Michał Zięba
|
||||
Mickey Pashov
|
||||
Mihai Capotă
|
||||
Mihail Milushev
|
||||
Mike Hoyle (hoylemd)
|
||||
Mike Lundy
|
||||
Milan Lesnek
|
||||
Miro Hrončok
|
||||
Nathaniel Compton
|
||||
Nathaniel Waisbrot
|
||||
@@ -310,6 +320,7 @@ Raphael Pierzina
|
||||
Rafal Semik
|
||||
Raquel Alegre
|
||||
Ravi Chandra
|
||||
Reagan Lee
|
||||
Robert Holt
|
||||
Roberto Aldera
|
||||
Roberto Polli
|
||||
@@ -320,7 +331,9 @@ Ronny Pfannschmidt
|
||||
Ross Lawley
|
||||
Ruaridh Williamson
|
||||
Russel Winder
|
||||
Ryan Puddephatt
|
||||
Ryan Wooden
|
||||
Sadra Barikbin
|
||||
Saiprasad Kale
|
||||
Samuel Colvin
|
||||
Samuel Dion-Girardeau
|
||||
@@ -329,16 +342,20 @@ Samuele Pedroni
|
||||
Sanket Duthade
|
||||
Sankt Petersbug
|
||||
Saravanan Padmanaban
|
||||
Sean Malloy
|
||||
Segev Finer
|
||||
Serhii Mozghovyi
|
||||
Seth Junot
|
||||
Shantanu Jain
|
||||
Sharad Nair
|
||||
Shubham Adep
|
||||
Simon Blanchard
|
||||
Simon Gomizelj
|
||||
Simon Holesch
|
||||
Simon Kerr
|
||||
Skylar Downes
|
||||
Srinivas Reddy Thatiparthy
|
||||
Stefaan Lippens
|
||||
Stefan Farmbauer
|
||||
Stefan Scherfke
|
||||
Stefan Zimmermann
|
||||
@@ -352,6 +369,7 @@ Tadek Teleżyński
|
||||
Takafumi Arakaki
|
||||
Taneli Hukkinen
|
||||
Tanvi Mehta
|
||||
Tanya Agarwal
|
||||
Tarcisio Fischer
|
||||
Tareq Alayan
|
||||
Tatiana Ovary
|
||||
@@ -370,7 +388,9 @@ Tomer Keren
|
||||
Tony Narlock
|
||||
Tor Colvin
|
||||
Trevor Bekolay
|
||||
Tushar Sadhwani
|
||||
Tyler Goodlet
|
||||
Tyler Smart
|
||||
Tzu-ping Chung
|
||||
Vasily Kuznetsov
|
||||
Victor Maryama
|
||||
|
||||
@@ -50,7 +50,7 @@ Fix bugs
|
||||
--------
|
||||
|
||||
Look through the `GitHub issues for bugs <https://github.com/pytest-dev/pytest/labels/type:%20bug>`_.
|
||||
See also the `"status: easy" issues <https://github.com/pytest-dev/pytest/labels/status%3A%20easy>`_
|
||||
See also the `"good first issue" issues <https://github.com/pytest-dev/pytest/labels/good%20first%20issue>`_
|
||||
that are friendly to new contributors.
|
||||
|
||||
:ref:`Talk <contact>` to developers to find out how you can fix specific bugs. To indicate that you are going
|
||||
@@ -197,11 +197,12 @@ Short version
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
#. Fork the repository.
|
||||
#. Fetch tags from upstream if necessary (if you cloned only main `git fetch --tags https://github.com/pytest-dev/pytest`).
|
||||
#. Enable and install `pre-commit <https://pre-commit.com>`_ to ensure style-guides and code checks are followed.
|
||||
#. Follow **PEP-8** for naming and `black <https://github.com/psf/black>`_ for formatting.
|
||||
#. Tests are run using ``tox``::
|
||||
|
||||
tox -e linting,py37
|
||||
tox -e linting,py39
|
||||
|
||||
The test environments above are usually enough to cover most cases locally.
|
||||
|
||||
@@ -236,6 +237,7 @@ Here is a simple overview, with pytest-specific bits:
|
||||
|
||||
$ git clone git@github.com:YOUR_GITHUB_USERNAME/pytest.git
|
||||
$ cd pytest
|
||||
$ git fetch --tags https://github.com/pytest-dev/pytest
|
||||
# now, create your own branch off "main":
|
||||
|
||||
$ git checkout -b your-bugfix-branch-name main
|
||||
@@ -272,24 +274,24 @@ Here is a simple overview, with pytest-specific bits:
|
||||
|
||||
#. Run all the tests
|
||||
|
||||
You need to have Python 3.7 available in your system. Now
|
||||
You need to have Python 3.8 or later available in your system. Now
|
||||
running tests is as simple as issuing this command::
|
||||
|
||||
$ tox -e linting,py37
|
||||
$ tox -e linting,py39
|
||||
|
||||
This command will run tests via the "tox" tool against Python 3.7
|
||||
This command will run tests via the "tox" tool against Python 3.9
|
||||
and also perform "lint" coding-style checks.
|
||||
|
||||
#. You can now edit your local working copy and run the tests again as necessary. Please follow PEP-8 for naming.
|
||||
|
||||
You can pass different options to ``tox``. For example, to run tests on Python 3.7 and pass options to pytest
|
||||
You can pass different options to ``tox``. For example, to run tests on Python 3.9 and pass options to pytest
|
||||
(e.g. enter pdb on failure) to pytest you can do::
|
||||
|
||||
$ tox -e py37 -- --pdb
|
||||
$ tox -e py39 -- --pdb
|
||||
|
||||
Or to only run tests in a particular test module on Python 3.7::
|
||||
Or to only run tests in a particular test module on Python 3.9::
|
||||
|
||||
$ tox -e py37 -- testing/test_config.py
|
||||
$ tox -e py39 -- testing/test_config.py
|
||||
|
||||
|
||||
When committing, ``pre-commit`` will re-format the files if necessary.
|
||||
|
||||
@@ -100,7 +100,7 @@ Features
|
||||
- Can run `unittest <https://docs.pytest.org/en/stable/how-to/unittest.html>`_ (or trial),
|
||||
`nose <https://docs.pytest.org/en/stable/how-to/nose.html>`_ test suites out of the box
|
||||
|
||||
- Python 3.7+ or PyPy3
|
||||
- Python 3.8+ or PyPy3
|
||||
|
||||
- Rich plugin architecture, with over 850+ `external plugins <https://docs.pytest.org/en/latest/reference/plugin_list.html>`_ and thriving community
|
||||
|
||||
|
||||
@@ -133,14 +133,12 @@ Releasing
|
||||
|
||||
Both automatic and manual processes described above follow the same steps from this point onward.
|
||||
|
||||
#. After all tests pass and the PR has been approved, tag the release commit
|
||||
in the ``release-MAJOR.MINOR.PATCH`` branch and push it. This will publish to PyPI::
|
||||
#. After all tests pass and the PR has been approved, trigger the ``deploy`` job
|
||||
in https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml, using the ``release-MAJOR.MINOR.PATCH`` branch
|
||||
as source.
|
||||
|
||||
git fetch upstream
|
||||
git tag MAJOR.MINOR.PATCH upstream/release-MAJOR.MINOR.PATCH
|
||||
git push upstream MAJOR.MINOR.PATCH
|
||||
|
||||
Wait for the deploy to complete, then make sure it is `available on PyPI <https://pypi.org/project/pytest>`_.
|
||||
This job will require approval from ``pytest-dev/core``, after which it will publish to PyPI
|
||||
and tag the repository.
|
||||
|
||||
#. Merge the PR. **Make sure it's not squash-merged**, so that the tagged commit ends up in the main branch.
|
||||
|
||||
|
||||
2
changelog/10441.feature.rst
Normal file
2
changelog/10441.feature.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
Added :func:`ExceptionInfo.group_contains() <pytest.ExceptionInfo.group_contains>`, an assertion
|
||||
helper that tests if an `ExceptionGroup` contains a matching exception.
|
||||
1
changelog/10465.deprecation.rst
Normal file
1
changelog/10465.deprecation.rst
Normal file
@@ -0,0 +1 @@
|
||||
Test functions returning a value other than None will now issue a :class:`pytest.PytestWarning` instead of :class:`pytest.PytestRemovedIn8Warning`, meaning this will stay a warning instead of becoming an error in the future.
|
||||
2
changelog/10617.feature.rst
Normal file
2
changelog/10617.feature.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
Added more comprehensive set assertion rewrites for comparisons other than equality ``==``, with
|
||||
the following operations now providing better failure messages: ``!=``, ``<=``, ``>=``, ``<``, and ``>``.
|
||||
2
changelog/10701.bugfix.rst
Normal file
2
changelog/10701.bugfix.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
:meth:`pytest.WarningsRecorder.pop` will return the most-closely-matched warning in the list,
|
||||
rather than the first warning which is an instance of the requested type.
|
||||
1
changelog/11011.doc.rst
Normal file
1
changelog/11011.doc.rst
Normal file
@@ -0,0 +1 @@
|
||||
Added a warning about modifying the root logger during tests when using ``caplog``.
|
||||
3
changelog/11065.doc.rst
Normal file
3
changelog/11065.doc.rst
Normal file
@@ -0,0 +1,3 @@
|
||||
Use pytestconfig instead of request.config in cache example
|
||||
|
||||
to be consistent with the API documentation.
|
||||
1
changelog/11091.doc.rst
Normal file
1
changelog/11091.doc.rst
Normal file
@@ -0,0 +1 @@
|
||||
Updated documentation and tests to refer to hyphonated options: replaced ``--junitxml`` with ``--junit-xml`` and ``--collectonly`` with ``--collect-only``.
|
||||
6
changelog/11122.improvement.rst
Normal file
6
changelog/11122.improvement.rst
Normal file
@@ -0,0 +1,6 @@
|
||||
``pluggy>=1.2.0`` is now required.
|
||||
|
||||
pytest now uses "new-style" hook wrappers internally, available since pluggy 1.2.0.
|
||||
See `pluggy's 1.2.0 changelog <https://pluggy.readthedocs.io/en/latest/changelog.html#pluggy-1-2-0-2023-06-21>`_ and the :ref:`updated docs <hookwrapper>` for details.
|
||||
|
||||
Plugins which want to use new-style wrappers can do so if they require this version of pytest or later.
|
||||
11
changelog/11137.breaking.rst
Normal file
11
changelog/11137.breaking.rst
Normal file
@@ -0,0 +1,11 @@
|
||||
:class:`pytest.Package` is no longer a :class:`pytest.Module` or :class:`pytest.File`.
|
||||
|
||||
The ``Package`` collector node designates a Python package, that is, a directory with an `__init__.py` file.
|
||||
Previously ``Package`` was a subtype of ``pytest.Module`` (which represents a single Python module),
|
||||
the module being the `__init__.py` file.
|
||||
This has been deemed a design mistake (see :issue:`11137` and :issue:`7777` for details).
|
||||
|
||||
The ``path`` property of ``Package`` nodes now points to the package directory instead of the ``__init__.py`` file.
|
||||
|
||||
Note that a ``Module`` node for ``__init__.py`` (which is not a ``Package``) may still exist,
|
||||
if it is picked up during collection (e.g. if you configured :confval:`python_files` to include ``__init__.py`` files).
|
||||
1
changelog/11146.bugfix.rst
Normal file
1
changelog/11146.bugfix.rst
Normal file
@@ -0,0 +1 @@
|
||||
- Prevent constants at the top of file from being detected as docstrings.
|
||||
1
changelog/11151.breaking.rst
Normal file
1
changelog/11151.breaking.rst
Normal file
@@ -0,0 +1 @@
|
||||
Dropped support for Python 3.7, which `reached end-of-life on 2023-06-27 <https://devguide.python.org/versions/>`__.
|
||||
2
changelog/11208.trivial.rst
Normal file
2
changelog/11208.trivial.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
The (internal) ``FixtureDef.cached_result`` type has changed.
|
||||
Now the third item ``cached_result[2]``, when set, is an exception instance instead of an exception triplet.
|
||||
1
changelog/11216.improvement.rst
Normal file
1
changelog/11216.improvement.rst
Normal file
@@ -0,0 +1 @@
|
||||
If a test is skipped from inside an :ref:`xunit setup fixture <classic xunit>`, the test summary now shows the test location instead of the fixture location.
|
||||
5
changelog/11218.trivial.rst
Normal file
5
changelog/11218.trivial.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
(This entry is meant to assist plugins which access private pytest internals to instantiate ``FixtureRequest`` objects.)
|
||||
|
||||
:class:`~pytest.FixtureRequest` is now an abstract class which can't be instantiated directly.
|
||||
A new concrete ``TopRequest`` subclass of ``FixtureRequest`` has been added for the ``request`` fixture in test functions,
|
||||
as counterpart to the existing ``SubRequest`` subclass for the ``request`` fixture in fixture functions.
|
||||
1
changelog/11227.improvement.rst
Normal file
1
changelog/11227.improvement.rst
Normal file
@@ -0,0 +1 @@
|
||||
Allow :func:`pytest.raises` ``match`` argument to match against `PEP-678 <https://peps.python.org/pep-0678/>` ``__notes__``.
|
||||
1
changelog/11255.bugfix.rst
Normal file
1
changelog/11255.bugfix.rst
Normal file
@@ -0,0 +1 @@
|
||||
Fixed crash on `parametrize(..., scope="package")` without a package present.
|
||||
2
changelog/11277.bugfix.rst
Normal file
2
changelog/11277.bugfix.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
Fixed a bug that when there are multiple fixtures for an indirect parameter,
|
||||
the scope of the highest-scope fixture is picked for the parameter set, instead of that of the one with the narrowest scope.
|
||||
2
changelog/11314.improvement.rst
Normal file
2
changelog/11314.improvement.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
Logging to a file using the ``--log-file`` option will use ``--log-level``, ``--log-format`` and ``--log-date-format`` as fallback
|
||||
if ``--log-file-level``, ``--log-file-format`` and ``--log-file-date-format`` are not provided respectively.
|
||||
3
changelog/11315.trivial.rst
Normal file
3
changelog/11315.trivial.rst
Normal file
@@ -0,0 +1,3 @@
|
||||
The :fixture:`pytester` fixture now uses the :fixture:`monkeypatch` fixture to manage the current working directory.
|
||||
If you use ``pytester`` in combination with :func:`monkeypatch.undo() <pytest.MonkeyPatch.undo>`, the CWD might get restored.
|
||||
Use :func:`monkeypatch.context() <pytest.MonkeyPatch.context>` instead.
|
||||
2
changelog/11333.trivial.rst
Normal file
2
changelog/11333.trivial.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
Corrected the spelling of ``Config.ArgsSource.INVOCATION_DIR``.
|
||||
The previous spelling ``INCOVATION_DIR`` remains as an alias.
|
||||
1
changelog/11353.trivial.rst
Normal file
1
changelog/11353.trivial.rst
Normal file
@@ -0,0 +1 @@
|
||||
pluggy>=1.3.0 is now required. This adds typing to :class:`~pytest.PytestPluginManager`.
|
||||
1
changelog/11447.improvement.rst
Normal file
1
changelog/11447.improvement.rst
Normal file
@@ -0,0 +1 @@
|
||||
:func:`pytest.deprecated_call` now also considers warnings of type :class:`FutureWarning`.
|
||||
4
changelog/11456.bugfix.rst
Normal file
4
changelog/11456.bugfix.rst
Normal file
@@ -0,0 +1,4 @@
|
||||
Parametrized tests now *really do* ensure that the ids given to each input are unique - for
|
||||
example, ``a, a, a0`` now results in ``a1, a2, a0`` instead of the previous (buggy) ``a0, a1, a0``.
|
||||
This necessarily means changing nodeids where these were previously colliding, and for
|
||||
readability adds an underscore when non-unique ids end in a number.
|
||||
1
changelog/11520.improvement.rst
Normal file
1
changelog/11520.improvement.rst
Normal file
@@ -0,0 +1 @@
|
||||
Improved very verbose diff output to color it as a diff instead of only red.
|
||||
1
changelog/11563.bugfix.rst
Normal file
1
changelog/11563.bugfix.rst
Normal file
@@ -0,0 +1 @@
|
||||
Fixed crash when using an empty string for the same parametrized value more than once.
|
||||
3
changelog/3664.deprecation.rst
Normal file
3
changelog/3664.deprecation.rst
Normal file
@@ -0,0 +1,3 @@
|
||||
Applying a mark to a fixture function now issues a warning: marks in fixtures never had any effect, but it is a common user error to apply a mark to a fixture (for example ``usefixtures``) and expect it to work.
|
||||
|
||||
This will become an error in the future.
|
||||
22
changelog/7363.breaking.rst
Normal file
22
changelog/7363.breaking.rst
Normal file
@@ -0,0 +1,22 @@
|
||||
**PytestRemovedIn8Warning deprecation warnings are now errors by default.**
|
||||
|
||||
Following our plan to remove deprecated features with as little disruption as
|
||||
possible, all warnings of type ``PytestRemovedIn8Warning`` now generate errors
|
||||
instead of warning messages by default.
|
||||
|
||||
**The affected features will be effectively removed in pytest 8.1**, so please consult the
|
||||
:ref:`deprecations` section in the docs for directions on how to update existing code.
|
||||
|
||||
In the pytest ``8.0.X`` series, it is possible to change the errors back into warnings as a
|
||||
stopgap measure by adding this to your ``pytest.ini`` file:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
filterwarnings =
|
||||
ignore::pytest.PytestRemovedIn8Warning
|
||||
|
||||
But this will stop working when pytest ``8.1`` is released.
|
||||
|
||||
**If you have concerns** about the removal of a specific feature, please add a
|
||||
comment to :issue:`7363`.
|
||||
1
changelog/7469.feature.rst
Normal file
1
changelog/7469.feature.rst
Normal file
@@ -0,0 +1 @@
|
||||
:class:`~pytest.FixtureDef` is now exported as ``pytest.FixtureDef`` for typing purposes.
|
||||
1
changelog/7966.bugfix.rst
Normal file
1
changelog/7966.bugfix.rst
Normal file
@@ -0,0 +1 @@
|
||||
Removes unhelpful error message from assertion rewrite mechanism when exceptions raised in __iter__ methods, and instead treats them as un-iterable.
|
||||
5
changelog/8976.breaking.rst
Normal file
5
changelog/8976.breaking.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
Running `pytest pkg/__init__.py` now collects the `pkg/__init__.py` file (module) only.
|
||||
Previously, it collected the entire `pkg` package, including other test files in the directory, but excluding tests in the `__init__.py` file itself
|
||||
(unless :confval:`python_files` was changed to allow `__init__.py` file).
|
||||
|
||||
To collect the entire package, specify just the directory: `pytest pkg`.
|
||||
1
changelog/9036.bugfix.rst
Normal file
1
changelog/9036.bugfix.rst
Normal file
@@ -0,0 +1 @@
|
||||
``pytest.warns`` and similar functions now capture warnings when an exception is raised inside a ``with`` block.
|
||||
7
changelog/9288.breaking.rst
Normal file
7
changelog/9288.breaking.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
:func:`pytest.warns <warns>` now re-emits unmatched warnings when the context
|
||||
closes -- previously it would consume all warnings, hiding those that were not
|
||||
matched by the function.
|
||||
|
||||
While this is a new feature, we decided to announce this as a breaking change
|
||||
because many test suites are configured to error-out on warnings, and will
|
||||
therefore fail on the newly-re-emitted warnings.
|
||||
@@ -14,7 +14,7 @@ Each file should be named like ``<ISSUE>.<TYPE>.rst``, where
|
||||
``<ISSUE>`` is an issue number, and ``<TYPE>`` is one of:
|
||||
|
||||
* ``feature``: new user facing features, like new command-line options and new behavior.
|
||||
* ``improvement``: improvement of existing functionality, usually without requiring user intervention (for example, new fields being written in ``--junitxml``, improved colors in terminal, etc).
|
||||
* ``improvement``: improvement of existing functionality, usually without requiring user intervention (for example, new fields being written in ``--junit-xml``, improved colors in terminal, etc).
|
||||
* ``bugfix``: fixes a bug.
|
||||
* ``doc``: documentation improvement, like rewording an entire session or adding missing docs.
|
||||
* ``deprecation``: feature deprecation.
|
||||
|
||||
@@ -6,6 +6,9 @@ Release announcements
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
release-7.4.3
|
||||
release-7.4.2
|
||||
release-7.4.1
|
||||
release-7.4.0
|
||||
release-7.3.2
|
||||
release-7.3.1
|
||||
|
||||
20
doc/en/announce/release-7.4.1.rst
Normal file
20
doc/en/announce/release-7.4.1.rst
Normal file
@@ -0,0 +1,20 @@
|
||||
pytest-7.4.1
|
||||
=======================================
|
||||
|
||||
pytest 7.4.1 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Bruno Oliveira
|
||||
* Florian Bruhin
|
||||
* Ran Benita
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
18
doc/en/announce/release-7.4.2.rst
Normal file
18
doc/en/announce/release-7.4.2.rst
Normal file
@@ -0,0 +1,18 @@
|
||||
pytest-7.4.2
|
||||
=======================================
|
||||
|
||||
pytest 7.4.2 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Bruno Oliveira
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
19
doc/en/announce/release-7.4.3.rst
Normal file
19
doc/en/announce/release-7.4.3.rst
Normal file
@@ -0,0 +1,19 @@
|
||||
pytest-7.4.3
|
||||
=======================================
|
||||
|
||||
pytest 7.4.3 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Bruno Oliveira
|
||||
* Marc Mueller
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
@@ -87,6 +87,7 @@ Released pytest versions support all Python versions that are actively maintaine
|
||||
============== ===================
|
||||
pytest version min. Python version
|
||||
============== ===================
|
||||
8.0+ 3.8+
|
||||
7.1+ 3.7+
|
||||
6.2 - 7.0 3.6+
|
||||
5.0 - 6.1 3.5+
|
||||
|
||||
@@ -22,7 +22,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
cachedir: .pytest_cache
|
||||
rootdir: /home/sweet/project
|
||||
collected 0 items
|
||||
cache -- .../_pytest/cacheprovider.py:528
|
||||
cache -- .../_pytest/cacheprovider.py:532
|
||||
Return a cache object that can persist state between testing sessions.
|
||||
|
||||
cache.get(key, default)
|
||||
@@ -105,7 +105,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
|
||||
doctest_namespace [session scope] -- .../_pytest/doctest.py:737
|
||||
doctest_namespace [session scope] -- .../_pytest/doctest.py:757
|
||||
Fixture that returns a :py:class:`dict` that will be injected into the
|
||||
namespace of doctests.
|
||||
|
||||
|
||||
@@ -28,6 +28,63 @@ with advance notice in the **Deprecations** section of releases.
|
||||
|
||||
.. towncrier release notes start
|
||||
|
||||
pytest 7.4.3 (2023-10-24)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#10447 <https://github.com/pytest-dev/pytest/issues/10447>`_: Markers are now considered in the reverse mro order to ensure base class markers are considered first -- this resolves a regression.
|
||||
|
||||
|
||||
- `#11239 <https://github.com/pytest-dev/pytest/issues/11239>`_: Fixed ``:=`` in asserts impacting unrelated test cases.
|
||||
|
||||
|
||||
- `#11439 <https://github.com/pytest-dev/pytest/issues/11439>`_: Handled an edge case where :data:`sys.stderr` might already be closed when :ref:`faulthandler` is tearing down.
|
||||
|
||||
|
||||
pytest 7.4.2 (2023-09-07)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#11237 <https://github.com/pytest-dev/pytest/issues/11237>`_: Fix doctest collection of `functools.cached_property` objects.
|
||||
|
||||
|
||||
- `#11306 <https://github.com/pytest-dev/pytest/issues/11306>`_: Fixed bug using ``--importmode=importlib`` which would cause package ``__init__.py`` files to be imported more than once in some cases.
|
||||
|
||||
|
||||
- `#11367 <https://github.com/pytest-dev/pytest/issues/11367>`_: Fixed bug where `user_properties` where not being saved in the JUnit XML file if a fixture failed during teardown.
|
||||
|
||||
|
||||
- `#11394 <https://github.com/pytest-dev/pytest/issues/11394>`_: Fixed crash when parsing long command line arguments that might be interpreted as files.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#11391 <https://github.com/pytest-dev/pytest/issues/11391>`_: Improved disclaimer on pytest plugin reference page to better indicate this is an automated, non-curated listing.
|
||||
|
||||
|
||||
pytest 7.4.1 (2023-09-02)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#10337 <https://github.com/pytest-dev/pytest/issues/10337>`_: Fixed bug where fake intermediate modules generated by ``--import-mode=importlib`` would not include the
|
||||
child modules as attributes of the parent modules.
|
||||
|
||||
|
||||
- `#10702 <https://github.com/pytest-dev/pytest/issues/10702>`_: Fixed error assertion handling in :func:`pytest.approx` when ``None`` is an expected or received value when comparing dictionaries.
|
||||
|
||||
|
||||
- `#10811 <https://github.com/pytest-dev/pytest/issues/10811>`_: Fixed issue when using ``--import-mode=importlib`` together with ``--doctest-modules`` that caused modules
|
||||
to be imported more than once, causing problems with modules that have import side effects.
|
||||
|
||||
|
||||
pytest 7.4.0 (2023-06-23)
|
||||
=========================
|
||||
|
||||
|
||||
@@ -15,12 +15,10 @@
|
||||
#
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
# The short X.Y version.
|
||||
import ast
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from textwrap import dedent
|
||||
from typing import List
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from _pytest import __version__ as version
|
||||
@@ -451,25 +449,6 @@ def setup(app: "sphinx.application.Sphinx") -> None:
|
||||
|
||||
configure_logging(app)
|
||||
|
||||
# Make Sphinx mark classes with "final" when decorated with @final.
|
||||
# We need this because we import final from pytest._compat, not from
|
||||
# typing (for Python < 3.8 compat), so Sphinx doesn't detect it.
|
||||
# To keep things simple we accept any `@final` decorator.
|
||||
# Ref: https://github.com/pytest-dev/pytest/pull/7780
|
||||
import sphinx.pycode.ast
|
||||
import sphinx.pycode.parser
|
||||
|
||||
original_is_final = sphinx.pycode.parser.VariableCommentPicker.is_final
|
||||
|
||||
def patched_is_final(self, decorators: List[ast.expr]) -> bool:
|
||||
if original_is_final(self, decorators):
|
||||
return True
|
||||
return any(
|
||||
sphinx.pycode.ast.unparse(decorator) == "final" for decorator in decorators
|
||||
)
|
||||
|
||||
sphinx.pycode.parser.VariableCommentPicker.is_final = patched_is_final
|
||||
|
||||
# legacypath.py monkey-patches pytest.Testdir in. Import the file so
|
||||
# that autodoc can discover references to it.
|
||||
import _pytest.legacypath # noqa: F401
|
||||
|
||||
@@ -380,6 +380,25 @@ conflicts (such as :class:`pytest.File` now taking ``path`` instead of
|
||||
``fspath``, as :ref:`outlined above <node-ctor-fspath-deprecation>`), a
|
||||
deprecation warning is now raised.
|
||||
|
||||
Applying a mark to a fixture function
|
||||
-------------------------------------
|
||||
|
||||
.. deprecated:: 7.4
|
||||
|
||||
Applying a mark to a fixture function never had any effect, but it is a common user error.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.usefixtures("clean_database")
|
||||
@pytest.fixture
|
||||
def user() -> User:
|
||||
...
|
||||
|
||||
Users expected in this case that the ``usefixtures`` mark would have its intended effect of using the ``clean_database`` fixture when ``user`` was invoked, when in fact it has no effect at all.
|
||||
|
||||
Now pytest will issue a warning when it encounters this problem, and will raise an error in the future versions.
|
||||
|
||||
|
||||
Backward compatibilities in ``Parser.addoption``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -467,12 +486,42 @@ The ``yield_fixture`` function/decorator
|
||||
It has been so for a very long time, so can be search/replaced safely.
|
||||
|
||||
|
||||
Removed Features
|
||||
----------------
|
||||
Removed Features and Breaking Changes
|
||||
-------------------------------------
|
||||
|
||||
As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after
|
||||
an appropriate period of deprecation has passed.
|
||||
|
||||
Some breaking changes which could not be deprecated are also listed.
|
||||
|
||||
|
||||
:class:`pytest.Package` is no longer a :class:`pytest.Module` or :class:`pytest.File`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. versionchanged:: 8.0
|
||||
|
||||
The ``Package`` collector node designates a Python package, that is, a directory with an `__init__.py` file.
|
||||
Previously ``Package`` was a subtype of ``pytest.Module`` (which represents a single Python module),
|
||||
the module being the `__init__.py` file.
|
||||
This has been deemed a design mistake (see :issue:`11137` and :issue:`7777` for details).
|
||||
|
||||
The ``path`` property of ``Package`` nodes now points to the package directory instead of the ``__init__.py`` file.
|
||||
|
||||
Note that a ``Module`` node for ``__init__.py`` (which is not a ``Package``) may still exist,
|
||||
if it is picked up during collection (e.g. if you configured :confval:`python_files` to include ``__init__.py`` files).
|
||||
|
||||
|
||||
Collecting ``__init__.py`` files no longer collects package
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. versionremoved:: 8.0
|
||||
|
||||
Running `pytest pkg/__init__.py` now collects the `pkg/__init__.py` file (module) only.
|
||||
Previously, it collected the entire `pkg` package, including other test files in the directory, but excluding tests in the `__init__.py` file itself
|
||||
(unless :confval:`python_files` was changed to allow `__init__.py` file).
|
||||
|
||||
To collect the entire package, specify just the directory: `pytest pkg`.
|
||||
|
||||
|
||||
The ``pytest.collect`` module
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -596,7 +645,7 @@ By using ``legacy`` you will keep using the legacy/xunit1 format when upgrading
|
||||
pytest 6.0, where the default format will be ``xunit2``.
|
||||
|
||||
In order to let users know about the transition, pytest will issue a warning in case
|
||||
the ``--junitxml`` option is given in the command line but ``junit_family`` is not explicitly
|
||||
the ``--junit-xml`` option is given in the command line but ``junit_family`` is not explicitly
|
||||
configured in ``pytest.ini``.
|
||||
|
||||
Services known to support the ``xunit2`` format:
|
||||
|
||||
@@ -136,7 +136,7 @@ Or select multiple nodes:
|
||||
|
||||
Node IDs for failing tests are displayed in the test summary info
|
||||
when running pytest with the ``-rf`` option. You can also
|
||||
construct Node IDs from the output of ``pytest --collectonly``.
|
||||
construct Node IDs from the output of ``pytest --collect-only``.
|
||||
|
||||
Using ``-k expr`` to select tests based on their name
|
||||
-------------------------------------------------------
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
"""
|
||||
module containing a parametrized tests testing cross-python
|
||||
serialization via the pickle module.
|
||||
"""
|
||||
"""Module containing a parametrized tests testing cross-python serialization
|
||||
via the pickle module."""
|
||||
import shutil
|
||||
import subprocess
|
||||
import textwrap
|
||||
|
||||
import pytest
|
||||
|
||||
pythonlist = ["python3.5", "python3.6", "python3.7"]
|
||||
|
||||
pythonlist = ["python3.9", "python3.10", "python3.11"]
|
||||
|
||||
|
||||
@pytest.fixture(params=pythonlist)
|
||||
@@ -43,7 +42,7 @@ class Python:
|
||||
)
|
||||
)
|
||||
)
|
||||
subprocess.check_call((self.pythonpath, str(dumpfile)))
|
||||
subprocess.run((self.pythonpath, str(dumpfile)), check=True)
|
||||
|
||||
def load_and_is_true(self, expression):
|
||||
loadfile = self.picklefile.with_name("load.py")
|
||||
@@ -63,7 +62,7 @@ class Python:
|
||||
)
|
||||
)
|
||||
print(loadfile)
|
||||
subprocess.check_call((self.pythonpath, str(loadfile)))
|
||||
subprocess.run((self.pythonpath, str(loadfile)), check=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("obj", [42, {}, {1: 3}])
|
||||
|
||||
@@ -12,7 +12,7 @@ class YamlFile(pytest.File):
|
||||
# We need a yaml parser, e.g. PyYAML.
|
||||
import yaml
|
||||
|
||||
raw = yaml.safe_load(self.path.open())
|
||||
raw = yaml.safe_load(self.path.open(encoding="utf-8"))
|
||||
for name, spec in sorted(raw.items()):
|
||||
yield YamlItem.from_parent(self, name=name, spec=spec)
|
||||
|
||||
|
||||
@@ -483,8 +483,8 @@ argument sets to use for each test function. Let's run it:
|
||||
FAILED test_parametrize.py::TestClass::test_equals[1-2] - assert 1 == 2
|
||||
1 failed, 2 passed in 0.12s
|
||||
|
||||
Indirect parametrization with multiple fixtures
|
||||
--------------------------------------------------------------
|
||||
Parametrization with multiple fixtures
|
||||
--------------------------------------
|
||||
|
||||
Here is a stripped down real-life example of using parametrized
|
||||
testing for testing serialization of objects between different python
|
||||
@@ -509,8 +509,8 @@ Running it results in some skips if we don't have all the python interpreters in
|
||||
SKIPPED [9] multipython.py:69: 'python3.7' not found
|
||||
27 skipped in 0.12s
|
||||
|
||||
Indirect parametrization of optional implementations/imports
|
||||
--------------------------------------------------------------------
|
||||
Parametrization of optional implementations/imports
|
||||
---------------------------------------------------
|
||||
|
||||
If you want to compare the outcomes of several implementations of a given
|
||||
API, you can write test functions that receive the already imported implementations
|
||||
@@ -657,13 +657,16 @@ Use :func:`pytest.raises` with the
|
||||
:ref:`pytest.mark.parametrize ref` decorator to write parametrized tests
|
||||
in which some tests raise exceptions and others do not.
|
||||
|
||||
It may be helpful to use ``nullcontext`` as a complement to ``raises``.
|
||||
``contextlib.nullcontext`` can be used to test cases that are not expected to
|
||||
raise exceptions but that should result in some value. The value is given as the
|
||||
``enter_result`` parameter, which will be available as the ``with`` statement’s
|
||||
target (``e`` in the example below).
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from contextlib import nullcontext as does_not_raise
|
||||
from contextlib import nullcontext
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -671,16 +674,17 @@ For example:
|
||||
@pytest.mark.parametrize(
|
||||
"example_input,expectation",
|
||||
[
|
||||
(3, does_not_raise()),
|
||||
(2, does_not_raise()),
|
||||
(1, does_not_raise()),
|
||||
(3, nullcontext(2)),
|
||||
(2, nullcontext(3)),
|
||||
(1, nullcontext(6)),
|
||||
(0, pytest.raises(ZeroDivisionError)),
|
||||
],
|
||||
)
|
||||
def test_division(example_input, expectation):
|
||||
"""Test how much I know division."""
|
||||
with expectation:
|
||||
assert (6 / example_input) is not None
|
||||
with expectation as e:
|
||||
assert (6 / example_input) == e
|
||||
|
||||
In the example above, the first three test cases should run unexceptionally,
|
||||
while the fourth should raise ``ZeroDivisionError``.
|
||||
In the example above, the first three test cases should run without any
|
||||
exceptions, while the fourth should raise a``ZeroDivisionError`` exception,
|
||||
which is expected by pytest.
|
||||
|
||||
@@ -808,16 +808,15 @@ case we just write some information out to a ``failures`` file:
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
||||
@pytest.hookimpl(wrapper=True, tryfirst=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
# execute all other hooks to obtain the report object
|
||||
outcome = yield
|
||||
rep = outcome.get_result()
|
||||
rep = yield
|
||||
|
||||
# we only look at actual failing test calls, not setup/teardown
|
||||
if rep.when == "call" and rep.failed:
|
||||
mode = "a" if os.path.exists("failures") else "w"
|
||||
with open("failures", mode) as f:
|
||||
with open("failures", mode, encoding="utf-8") as f:
|
||||
# let's also access a fixture for the fun of it
|
||||
if "tmp_path" in item.fixturenames:
|
||||
extra = " ({})".format(item.funcargs["tmp_path"])
|
||||
@@ -826,6 +825,8 @@ case we just write some information out to a ``failures`` file:
|
||||
|
||||
f.write(rep.nodeid + extra + "\n")
|
||||
|
||||
return rep
|
||||
|
||||
|
||||
if you then have failing tests:
|
||||
|
||||
@@ -899,16 +900,17 @@ here is a little example implemented via a local plugin:
|
||||
phase_report_key = StashKey[Dict[str, CollectReport]]()
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
||||
@pytest.hookimpl(wrapper=True, tryfirst=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
# execute all other hooks to obtain the report object
|
||||
outcome = yield
|
||||
rep = outcome.get_result()
|
||||
rep = yield
|
||||
|
||||
# store test results for each phase of a call, which can
|
||||
# be "setup", "call", "teardown"
|
||||
item.stash.setdefault(phase_report_key, {})[rep.when] = rep
|
||||
|
||||
return rep
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def something(request):
|
||||
@@ -1088,4 +1090,4 @@ application with standard ``pytest`` command-line options:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
./app_main --pytest --verbose --tb=long --junitxml=results.xml test-suite/
|
||||
./app_main --pytest --verbose --tb=long --junit=xml=results.xml test-suite/
|
||||
|
||||
@@ -34,7 +34,7 @@ a function/method call.
|
||||
|
||||
**Assert** is where we look at that resulting state and check if it looks how
|
||||
we'd expect after the dust has settled. It's where we gather evidence to say the
|
||||
behavior does or does not aligns with what we expect. The ``assert`` in our test
|
||||
behavior does or does not align with what we expect. The ``assert`` in our test
|
||||
is where we take that measurement/observation and apply our judgement to it. If
|
||||
something should be green, we'd say ``assert thing == "green"``.
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ Get Started
|
||||
Install ``pytest``
|
||||
----------------------------------------
|
||||
|
||||
``pytest`` requires: Python 3.7+ or PyPy3.
|
||||
``pytest`` requires: Python 3.8+ or PyPy3.
|
||||
|
||||
1. Run the following command in your command line:
|
||||
|
||||
@@ -22,7 +22,7 @@ Install ``pytest``
|
||||
.. code-block:: bash
|
||||
|
||||
$ pytest --version
|
||||
pytest 7.4.0
|
||||
pytest 7.4.3
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
@@ -97,6 +97,30 @@ Use the :ref:`raises <assertraises>` helper to assert that some code raises an e
|
||||
with pytest.raises(SystemExit):
|
||||
f()
|
||||
|
||||
You can also use the context provided by :ref:`raises <assertraises>` to
|
||||
assert that an expected exception is part of a raised ``ExceptionGroup``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# content of test_exceptiongroup.py
|
||||
import pytest
|
||||
|
||||
|
||||
def f():
|
||||
raise ExceptionGroup(
|
||||
"Group message",
|
||||
[
|
||||
RuntimeError(),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def test_exception_in_group():
|
||||
with pytest.raises(ExceptionGroup) as excinfo:
|
||||
f()
|
||||
assert excinfo.group_contains(RuntimeError)
|
||||
assert not excinfo.group_contains(TypeError)
|
||||
|
||||
Execute the test function with “quiet” reporting mode:
|
||||
|
||||
.. code-block:: pytest
|
||||
|
||||
@@ -54,14 +54,13 @@ operators. (See :ref:`tbreportdemo`). This allows you to use the
|
||||
idiomatic python constructs without boilerplate code while not losing
|
||||
introspection information.
|
||||
|
||||
However, if you specify a message with the assertion like this:
|
||||
If a message is specified with the assertion like this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
assert a % 2 == 0, "value was odd, should be even"
|
||||
|
||||
then no assertion introspection takes places at all and the message
|
||||
will be simply shown in the traceback.
|
||||
it is printed alongside the assertion introspection in the traceback.
|
||||
|
||||
See :ref:`assert-details` for more information on assertion introspection.
|
||||
|
||||
@@ -116,10 +115,56 @@ that a regular expression matches on the string representation of an exception
|
||||
with pytest.raises(ValueError, match=r".* 123 .*"):
|
||||
myfunc()
|
||||
|
||||
The regexp parameter of the ``match`` method is matched with the ``re.search``
|
||||
The regexp parameter of the ``match`` parameter is matched with the ``re.search``
|
||||
function, so in the above example ``match='123'`` would have worked as
|
||||
well.
|
||||
|
||||
You can also use the :func:`excinfo.group_contains() <pytest.ExceptionInfo.group_contains>`
|
||||
method to test for exceptions returned as part of an ``ExceptionGroup``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_exception_in_group():
|
||||
with pytest.raises(RuntimeError) as excinfo:
|
||||
raise ExceptionGroup(
|
||||
"Group message",
|
||||
[
|
||||
RuntimeError("Exception 123 raised"),
|
||||
],
|
||||
)
|
||||
assert excinfo.group_contains(RuntimeError, match=r".* 123 .*")
|
||||
assert not excinfo.group_contains(TypeError)
|
||||
|
||||
The optional ``match`` keyword parameter works the same way as for
|
||||
:func:`pytest.raises`.
|
||||
|
||||
By default ``group_contains()`` will recursively search for a matching
|
||||
exception at any level of nested ``ExceptionGroup`` instances. You can
|
||||
specify a ``depth`` keyword parameter if you only want to match an
|
||||
exception at a specific level; exceptions contained directly in the top
|
||||
``ExceptionGroup`` would match ``depth=1``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_exception_in_group_at_given_depth():
|
||||
with pytest.raises(RuntimeError) as excinfo:
|
||||
raise ExceptionGroup(
|
||||
"Group message",
|
||||
[
|
||||
RuntimeError(),
|
||||
ExceptionGroup(
|
||||
"Nested group",
|
||||
[
|
||||
TypeError(),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
assert excinfo.group_contains(RuntimeError, depth=1)
|
||||
assert excinfo.group_contains(TypeError, depth=2)
|
||||
assert not excinfo.group_contains(RuntimeError, depth=2)
|
||||
assert not excinfo.group_contains(TypeError, depth=1)
|
||||
|
||||
There's an alternate form of the :func:`pytest.raises` function where you pass
|
||||
a function that will be executed with the given ``*args`` and ``**kwargs`` and
|
||||
assert that the given exception is raised:
|
||||
|
||||
@@ -176,14 +176,21 @@ with more recent files coming first.
|
||||
Behavior when no tests failed in the last run
|
||||
---------------------------------------------
|
||||
|
||||
When no tests failed in the last run, or when no cached ``lastfailed`` data was
|
||||
found, ``pytest`` can be configured either to run all of the tests or no tests,
|
||||
using the ``--last-failed-no-failures`` option, which takes one of the following values:
|
||||
The ``--lfnf/--last-failed-no-failures`` option governs the behavior of ``--last-failed``.
|
||||
Determines whether to execute tests when there are no previously (known)
|
||||
failures or when no cached ``lastfailed`` data was found.
|
||||
|
||||
There are two options:
|
||||
|
||||
* ``all``: when there are no known test failures, runs all tests (the full test suite). This is the default.
|
||||
* ``none``: when there are no known test failures, just emits a message stating this and exit successfully.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pytest --last-failed --last-failed-no-failures all # run all tests (default behavior)
|
||||
pytest --last-failed --last-failed-no-failures none # run no tests and exit
|
||||
pytest --last-failed --last-failed-no-failures all # runs the full test suite (default behavior)
|
||||
pytest --last-failed --last-failed-no-failures none # runs no tests and exits successfully
|
||||
|
||||
The new config.cache object
|
||||
--------------------------------
|
||||
@@ -206,12 +213,12 @@ across pytest invocations:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mydata(request):
|
||||
val = request.config.cache.get("example/value", None)
|
||||
def mydata(pytestconfig):
|
||||
val = pytestconfig.cache.get("example/value", None)
|
||||
if val is None:
|
||||
expensive_computation()
|
||||
val = 42
|
||||
request.config.cache.set("example/value", val)
|
||||
pytestconfig.cache.set("example/value", val)
|
||||
return val
|
||||
|
||||
|
||||
|
||||
@@ -135,10 +135,6 @@ Warning about unraisable exceptions and unhandled thread exceptions
|
||||
|
||||
.. versionadded:: 6.2
|
||||
|
||||
.. note::
|
||||
|
||||
These features only work on Python>=3.8.
|
||||
|
||||
Unhandled exceptions are exceptions that are raised in a situation in which
|
||||
they cannot propagate to a caller. The most common case is an exception raised
|
||||
in a :meth:`__del__ <object.__del__>` implementation.
|
||||
|
||||
@@ -1698,7 +1698,7 @@ and declare its use in a test module via a ``usefixtures`` marker:
|
||||
class TestDirectoryInit:
|
||||
def test_cwd_starts_empty(self):
|
||||
assert os.listdir(os.getcwd()) == []
|
||||
with open("myfile", "w") as f:
|
||||
with open("myfile", "w", encoding="utf-8") as f:
|
||||
f.write("hello")
|
||||
|
||||
def test_cwd_again_starts_empty(self):
|
||||
@@ -1752,8 +1752,7 @@ into an ini-file:
|
||||
def my_fixture_that_sadly_wont_use_my_other_fixture():
|
||||
...
|
||||
|
||||
Currently this will not generate any error or warning, but this is intended
|
||||
to be handled by :issue:`3664`.
|
||||
This generates a deprecation warning, and will become an error in Pytest 8.
|
||||
|
||||
.. _`override fixtures`:
|
||||
|
||||
|
||||
@@ -172,6 +172,13 @@ the records for the ``setup`` and ``call`` stages during teardown like so:
|
||||
|
||||
The full API is available at :class:`pytest.LogCaptureFixture`.
|
||||
|
||||
.. warning::
|
||||
|
||||
The ``caplog`` fixture adds a handler to the root logger to capture logs. If the root logger is
|
||||
modified during a test, for example with ``logging.config.dictConfig``, this handler may be
|
||||
removed and cause no logs to be captured. To avoid this, ensure that any root logger configuration
|
||||
only adds to the existing handlers.
|
||||
|
||||
|
||||
.. _live_logs:
|
||||
|
||||
|
||||
@@ -47,8 +47,7 @@ Unsupported idioms / known issues
|
||||
- nose imports test modules with the same import path (e.g.
|
||||
``tests.test_mode``) but different file system paths
|
||||
(e.g. ``tests/test_mode.py`` and ``other/tests/test_mode.py``)
|
||||
by extending sys.path/import semantics. pytest does not do that
|
||||
but there is discussion in :issue:`268` for adding some support. Note that
|
||||
by extending sys.path/import semantics. pytest does not do that. Note that
|
||||
`nose2 choose to avoid this sys.path/import hackery <https://nose2.readthedocs.io/en/latest/differences.html#test-discovery-and-loading>`_.
|
||||
|
||||
If you place a conftest.py file in the root directory of your project
|
||||
@@ -66,16 +65,34 @@ Unsupported idioms / known issues
|
||||
|
||||
- no nose-configuration is recognized.
|
||||
|
||||
- ``yield``-based methods are unsupported as of pytest 4.1.0. They are
|
||||
- ``yield``-based methods are
|
||||
fundamentally incompatible with pytest because they don't support fixtures
|
||||
properly since collection and test execution are separated.
|
||||
|
||||
Here is a table comparing the default supported naming conventions for both
|
||||
nose and pytest.
|
||||
|
||||
========= ========================== ======= =====
|
||||
what default naming convention pytest nose
|
||||
========= ========================== ======= =====
|
||||
module ``test*.py`` ✅
|
||||
module ``test_*.py`` ✅ ✅
|
||||
module ``*_test.py`` ✅
|
||||
module ``*_tests.py``
|
||||
class ``*(unittest.TestCase)`` ✅ ✅
|
||||
method ``test_*`` ✅ ✅
|
||||
class ``Test*`` ✅
|
||||
method ``test_*`` ✅
|
||||
function ``test_*`` ✅
|
||||
========= ========================== ======= =====
|
||||
|
||||
|
||||
Migrating from nose to pytest
|
||||
------------------------------
|
||||
|
||||
`nose2pytest <https://github.com/pytest-dev/nose2pytest>`_ is a Python script
|
||||
and pytest plugin to help convert Nose-based tests into pytest-based tests.
|
||||
Specifically, the script transforms nose.tools.assert_* function calls into
|
||||
Specifically, the script transforms ``nose.tools.assert_*`` function calls into
|
||||
raw assert statements, while preserving format of original arguments
|
||||
as much as possible.
|
||||
|
||||
|
||||
@@ -16,6 +16,12 @@ Examples for modifying traceback printing:
|
||||
pytest -l # show local variables (shortcut)
|
||||
pytest --no-showlocals # hide local variables (if addopts enables them)
|
||||
|
||||
pytest --capture=fd # default, capture at the file descriptor level
|
||||
pytest --capture=sys # capture at the sys level
|
||||
pytest --capture=no # don't capture
|
||||
pytest -s # don't capture (shortcut)
|
||||
pytest --capture=tee-sys # capture to logs but also output to sys level streams
|
||||
|
||||
pytest --tb=auto # (default) 'long' tracebacks for the first and last
|
||||
# entry, but 'short' style for the other entries
|
||||
pytest --tb=long # exhaustive, informative traceback formatting
|
||||
@@ -36,6 +42,16 @@ option you make sure a trace is shown.
|
||||
Verbosity
|
||||
--------------------------------------------------
|
||||
|
||||
Examples for modifying printing verbosity:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pytest --quiet # quiet - less verbose - mode
|
||||
pytest -q # quiet - less verbose - mode (shortcut)
|
||||
pytest -v # increase verbosity, display individual test names
|
||||
pytest -vv # more verbose, display more details from the test output
|
||||
pytest -vvv # not a standard , but may be used for even more detail in certain setups
|
||||
|
||||
The ``-v`` flag controls the verbosity of pytest output in various aspects: test session progress, assertion
|
||||
details when tests fail, fixtures details with ``--fixtures``, etc.
|
||||
|
||||
@@ -478,7 +494,7 @@ integration servers, use this invocation:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pytest --junitxml=path
|
||||
pytest --junit-xml=path
|
||||
|
||||
to create an XML file at ``path``.
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ created in the `base temporary directory`_.
|
||||
d = tmp_path / "sub"
|
||||
d.mkdir()
|
||||
p = d / "hello.txt"
|
||||
p.write_text(CONTENT)
|
||||
assert p.read_text() == CONTENT
|
||||
p.write_text(CONTENT, encoding="utf-8")
|
||||
assert p.read_text(encoding="utf-8") == CONTENT
|
||||
assert len(list(tmp_path.iterdir())) == 1
|
||||
assert 0
|
||||
|
||||
@@ -51,8 +51,8 @@ Running this would result in a passed test except for the last
|
||||
d = tmp_path / "sub"
|
||||
d.mkdir()
|
||||
p = d / "hello.txt"
|
||||
p.write_text(CONTENT)
|
||||
assert p.read_text() == CONTENT
|
||||
p.write_text(CONTENT, encoding="utf-8")
|
||||
assert p.read_text(encoding="utf-8") == CONTENT
|
||||
assert len(list(tmp_path.iterdir())) == 1
|
||||
> assert 0
|
||||
E assert 0
|
||||
|
||||
@@ -207,10 +207,10 @@ creation of a per-test temporary directory:
|
||||
@pytest.fixture(autouse=True)
|
||||
def initdir(self, tmp_path, monkeypatch):
|
||||
monkeypatch.chdir(tmp_path) # change to pytest-provided temporary directory
|
||||
tmp_path.joinpath("samplefile.ini").write_text("# testdata")
|
||||
tmp_path.joinpath("samplefile.ini").write_text("# testdata", encoding="utf-8")
|
||||
|
||||
def test_method(self):
|
||||
with open("samplefile.ini") as f:
|
||||
with open("samplefile.ini", encoding="utf-8") as f:
|
||||
s = f.read()
|
||||
assert "testdata" in s
|
||||
|
||||
|
||||
@@ -44,23 +44,34 @@ Use ``""`` instead of ``''`` in expression when running this on Windows
|
||||
|
||||
.. _nodeids:
|
||||
|
||||
**Run tests by node ids**
|
||||
**Run tests by collection arguments**
|
||||
|
||||
Each collected test is assigned a unique ``nodeid`` which consist of the module filename followed
|
||||
by specifiers like class names, function names and parameters from parametrization, separated by ``::`` characters.
|
||||
Pass the module filename relative to the working directory, followed by specifiers like the class name and function name
|
||||
separated by ``::`` characters, and parameters from parameterization enclosed in ``[]``.
|
||||
|
||||
To run a specific test within a module:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pytest test_mod.py::test_func
|
||||
pytest tests/test_mod.py::test_func
|
||||
|
||||
|
||||
Another example specifying a test method in the command line:
|
||||
To run all tests in a class:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pytest test_mod.py::TestClass::test_method
|
||||
pytest tests/test_mod.py::TestClass
|
||||
|
||||
Specifying a specific test method:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pytest tests/test_mod.py::TestClass::test_method
|
||||
|
||||
Specifying a specific parametrization of a test:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pytest tests/test_mod.py::test_func[x1,y2]
|
||||
|
||||
**Run tests by marker expressions**
|
||||
|
||||
@@ -173,7 +184,8 @@ You can invoke ``pytest`` from Python code directly:
|
||||
|
||||
this acts as if you would call "pytest" from the command line.
|
||||
It will not raise :class:`SystemExit` but return the :ref:`exit code <exit-codes>` instead.
|
||||
You can pass in options and arguments:
|
||||
If you don't pass it any arguments, ``main`` reads the arguments from the command line arguments of the process (:data:`sys.argv`), which may be undesirable.
|
||||
You can pass in options and arguments explicitly:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ The remaining hook functions will not be called in this case.
|
||||
|
||||
.. _`hookwrapper`:
|
||||
|
||||
hookwrapper: executing around other hooks
|
||||
hook wrappers: executing around other hooks
|
||||
-------------------------------------------------
|
||||
|
||||
.. currentmodule:: _pytest.core
|
||||
@@ -69,10 +69,8 @@ which yields exactly once. When pytest invokes hooks it first executes
|
||||
hook wrappers and passes the same arguments as to the regular hooks.
|
||||
|
||||
At the yield point of the hook wrapper pytest will execute the next hook
|
||||
implementations and return their result to the yield point in the form of
|
||||
a :py:class:`Result <pluggy._Result>` instance which encapsulates a result or
|
||||
exception info. The yield point itself will thus typically not raise
|
||||
exceptions (unless there are bugs).
|
||||
implementations and return their result to the yield point, or will
|
||||
propagate an exception if they raised.
|
||||
|
||||
Here is an example definition of a hook wrapper:
|
||||
|
||||
@@ -81,26 +79,35 @@ Here is an example definition of a hook wrapper:
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
@pytest.hookimpl(wrapper=True)
|
||||
def pytest_pyfunc_call(pyfuncitem):
|
||||
do_something_before_next_hook_executes()
|
||||
|
||||
outcome = yield
|
||||
# outcome.excinfo may be None or a (cls, val, tb) tuple
|
||||
# If the outcome is an exception, will raise the exception.
|
||||
res = yield
|
||||
|
||||
res = outcome.get_result() # will raise if outcome was exception
|
||||
new_res = post_process_result(res)
|
||||
|
||||
post_process_result(res)
|
||||
# Override the return value to the plugin system.
|
||||
return new_res
|
||||
|
||||
outcome.force_result(new_res) # to override the return value to the plugin system
|
||||
The hook wrapper needs to return a result for the hook, or raise an exception.
|
||||
|
||||
Note that hook wrappers don't return results themselves, they merely
|
||||
perform tracing or other side effects around the actual hook implementations.
|
||||
If the result of the underlying hook is a mutable object, they may modify
|
||||
that result but it's probably better to avoid it.
|
||||
In many cases, the wrapper only needs to perform tracing or other side effects
|
||||
around the actual hook implementations, in which case it can return the result
|
||||
value of the ``yield``. The simplest (though useless) hook wrapper is
|
||||
``return (yield)``.
|
||||
|
||||
In other cases, the wrapper wants the adjust or adapt the result, in which case
|
||||
it can return a new value. If the result of the underlying hook is a mutable
|
||||
object, the wrapper may modify that result, but it's probably better to avoid it.
|
||||
|
||||
If the hook implementation failed with an exception, the wrapper can handle that
|
||||
exception using a ``try-catch-finally`` around the ``yield``, by propagating it,
|
||||
supressing it, or raising a different exception entirely.
|
||||
|
||||
For more information, consult the
|
||||
:ref:`pluggy documentation about hookwrappers <pluggy:hookwrappers>`.
|
||||
:ref:`pluggy documentation about hook wrappers <pluggy:hookwrappers>`.
|
||||
|
||||
.. _plugin-hookorder:
|
||||
|
||||
@@ -130,11 +137,14 @@ after others, i.e. the position in the ``N``-sized list of functions:
|
||||
|
||||
|
||||
# Plugin 3
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
@pytest.hookimpl(wrapper=True)
|
||||
def pytest_collection_modifyitems(items):
|
||||
# will execute even before the tryfirst one above!
|
||||
outcome = yield
|
||||
# will execute after all non-hookwrappers executed
|
||||
try:
|
||||
return (yield)
|
||||
finally:
|
||||
# will execute after all non-wrappers executed
|
||||
...
|
||||
|
||||
Here is the order of execution:
|
||||
|
||||
@@ -149,12 +159,11 @@ Here is the order of execution:
|
||||
Plugin1).
|
||||
|
||||
4. Plugin3's pytest_collection_modifyitems then executing the code after the yield
|
||||
point. The yield receives a :py:class:`Result <pluggy._Result>` instance which encapsulates
|
||||
the result from calling the non-wrappers. Wrappers shall not modify the result.
|
||||
point. The yield receives the result from calling the non-wrappers, or raises
|
||||
an exception if the non-wrappers raised.
|
||||
|
||||
It's possible to use ``tryfirst`` and ``trylast`` also in conjunction with
|
||||
``hookwrapper=True`` in which case it will influence the ordering of hookwrappers
|
||||
among each other.
|
||||
It's possible to use ``tryfirst`` and ``trylast`` also on hook wrappers
|
||||
in which case it will influence the ordering of hook wrappers among each other.
|
||||
|
||||
|
||||
Declaring new hooks
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
.. sidebar:: Next Open Trainings
|
||||
|
||||
- `pytest tips and tricks for a better testsuite <https://ep2023.europython.eu/session/pytest-tips-and-tricks-for-a-better-testsuite>`_, at `Europython 2023 <https://ep2023.europython.eu/>`_, July 18th (3h), Prague/Remote
|
||||
- `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 5th to 7th 2024** (3 day in-depth training), **Leipzig, Germany / Remote**
|
||||
|
||||
Also see :doc:`previous talks and blogposts <talks>`.
|
||||
|
||||
@@ -18,7 +17,7 @@ The ``pytest`` framework makes it easy to write small, readable tests, and can
|
||||
scale to support complex functional testing for applications and libraries.
|
||||
|
||||
|
||||
``pytest`` requires: Python 3.7+ or PyPy3.
|
||||
``pytest`` requires: Python 3.8+ or PyPy3.
|
||||
|
||||
**PyPI package name**: :pypi:`pytest`
|
||||
|
||||
@@ -77,7 +76,7 @@ Features
|
||||
|
||||
- Can run :ref:`unittest <unittest>` (including trial) and :ref:`nose <noseintegration>` test suites out of the box
|
||||
|
||||
- Python 3.7+ or PyPy 3
|
||||
- Python 3.8+ or PyPy 3
|
||||
|
||||
- Rich plugin architecture, with over 800+ :ref:`external plugins <plugin-list>` and thriving community
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ and can also be used to hold pytest configuration if they have a ``[pytest]`` se
|
||||
setup.cfg
|
||||
~~~~~~~~~
|
||||
|
||||
``setup.cfg`` files are general purpose configuration files, used originally by :doc:`distutils <python:distutils/configfile>`, and can also be used to hold pytest configuration
|
||||
``setup.cfg`` files are general purpose configuration files, used originally by ``distutils`` (now deprecated) and `setuptools <https://setuptools.pypa.io/en/latest/userguide/declarative_config.html>`__, and can also be used to hold pytest configuration
|
||||
if they have a ``[tool:pytest]`` section.
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
:tocdepth: 3
|
||||
|
||||
.. _`api-reference`:
|
||||
|
||||
API Reference
|
||||
@@ -82,6 +84,8 @@ pytest.exit
|
||||
pytest.main
|
||||
~~~~~~~~~~~
|
||||
|
||||
**Tutorial**: :ref:`pytest.main-usage`
|
||||
|
||||
.. autofunction:: pytest.main
|
||||
|
||||
pytest.param
|
||||
@@ -235,7 +239,7 @@ pytest.mark.xfail
|
||||
|
||||
Marks a test function as *expected to fail*.
|
||||
|
||||
.. py:function:: pytest.mark.xfail(condition=None, *, reason=None, raises=None, run=True, strict=False)
|
||||
.. py:function:: pytest.mark.xfail(condition=None, *, reason=None, raises=None, run=True, strict=xfail_strict)
|
||||
|
||||
:type condition: bool or str
|
||||
:param condition:
|
||||
@@ -247,10 +251,10 @@ Marks a test function as *expected to fail*.
|
||||
:keyword Type[Exception] raises:
|
||||
Exception subclass (or tuple of subclasses) expected to be raised by the test function; other exceptions will fail the test.
|
||||
:keyword bool run:
|
||||
If the test function should actually be executed. If ``False``, the function will always xfail and will
|
||||
Whether the test function should actually be executed. If ``False``, the function will always xfail and will
|
||||
not be executed (useful if a function is segfaulting).
|
||||
:keyword bool strict:
|
||||
* If ``False`` (the default) the function will be shown in the terminal output as ``xfailed`` if it fails
|
||||
* If ``False`` the function will be shown in the terminal output as ``xfailed`` if it fails
|
||||
and as ``xpass`` if it passes. In both cases this will not cause the test suite to fail as a whole. This
|
||||
is particularly useful to mark *flaky* tests (tests that fail at random) to be tackled later.
|
||||
* If ``True``, the function will be shown in the terminal output as ``xfailed`` if it fails, but if it
|
||||
@@ -258,6 +262,8 @@ Marks a test function as *expected to fail*.
|
||||
that are always failing and there should be a clear indication if they unexpectedly start to pass (for example
|
||||
a new release of a library fixes a known bug).
|
||||
|
||||
Defaults to :confval:`xfail_strict`, which is ``False`` by default.
|
||||
|
||||
|
||||
Custom marks
|
||||
~~~~~~~~~~~~
|
||||
@@ -783,18 +789,66 @@ reporting or interaction with exceptions:
|
||||
.. autofunction:: pytest_leave_pdb
|
||||
|
||||
|
||||
Objects
|
||||
-------
|
||||
Collection tree objects
|
||||
-----------------------
|
||||
|
||||
Full reference to objects accessible from :ref:`fixtures <fixture>` or :ref:`hooks <hook-reference>`.
|
||||
These are the collector and item classes (collectively called "nodes") which
|
||||
make up the collection tree.
|
||||
|
||||
Node
|
||||
~~~~
|
||||
|
||||
CallInfo
|
||||
~~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.CallInfo()
|
||||
.. autoclass:: _pytest.nodes.Node()
|
||||
:members:
|
||||
|
||||
Collector
|
||||
~~~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Collector()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Item
|
||||
~~~~
|
||||
|
||||
.. autoclass:: pytest.Item()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
File
|
||||
~~~~
|
||||
|
||||
.. autoclass:: pytest.File()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
FSCollector
|
||||
~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: _pytest.nodes.FSCollector()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Session
|
||||
~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Session()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Package
|
||||
~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Package()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Module
|
||||
~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Module()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Class
|
||||
~~~~~
|
||||
@@ -803,13 +857,34 @@ Class
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Collector
|
||||
~~~~~~~~~
|
||||
Function
|
||||
~~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Collector()
|
||||
.. autoclass:: pytest.Function()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
FunctionDefinition
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: _pytest.python.FunctionDefinition()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Objects
|
||||
-------
|
||||
|
||||
Objects accessible from :ref:`fixtures <fixture>` or :ref:`hooks <hook-reference>`
|
||||
or importable from ``pytest``.
|
||||
|
||||
|
||||
CallInfo
|
||||
~~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.CallInfo()
|
||||
:members:
|
||||
|
||||
CollectReport
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
@@ -837,46 +912,11 @@ ExitCode
|
||||
.. autoclass:: pytest.ExitCode
|
||||
:members:
|
||||
|
||||
File
|
||||
~~~~
|
||||
|
||||
.. autoclass:: pytest.File()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
FixtureDef
|
||||
~~~~~~~~~~
|
||||
|
||||
.. autoclass:: _pytest.fixtures.FixtureDef()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
FSCollector
|
||||
~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: _pytest.nodes.FSCollector()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Function
|
||||
~~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Function()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
FunctionDefinition
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: _pytest.python.FunctionDefinition()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Item
|
||||
~~~~
|
||||
|
||||
.. autoclass:: pytest.Item()
|
||||
.. autoclass:: pytest.FixtureDef()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
@@ -907,19 +947,6 @@ Metafunc
|
||||
.. autoclass:: pytest.Metafunc()
|
||||
:members:
|
||||
|
||||
Module
|
||||
~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Module()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Node
|
||||
~~~~
|
||||
|
||||
.. autoclass:: _pytest.nodes.Node()
|
||||
:members:
|
||||
|
||||
Parser
|
||||
~~~~~~
|
||||
|
||||
@@ -941,13 +968,6 @@ PytestPluginManager
|
||||
:inherited-members:
|
||||
:show-inheritance:
|
||||
|
||||
Session
|
||||
~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Session()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
TestReport
|
||||
~~~~~~~~~~
|
||||
|
||||
@@ -962,10 +982,10 @@ TestShortLogReport
|
||||
.. autoclass:: pytest.TestShortLogReport()
|
||||
:members:
|
||||
|
||||
_Result
|
||||
Result
|
||||
~~~~~~~
|
||||
|
||||
Result object used within :ref:`hook wrappers <hookwrapper>`, see :py:class:`_Result in the pluggy documentation <pluggy._callers._Result>` for more information.
|
||||
Result object used within :ref:`hook wrappers <hookwrapper>`, see :py:class:`Result in the pluggy documentation <pluggy.Result>` for more information.
|
||||
|
||||
Stash
|
||||
~~~~~
|
||||
@@ -1153,6 +1173,9 @@ Custom warnings generated in some situations such as improper usage or deprecate
|
||||
.. autoclass:: pytest.PytestRemovedIn8Warning
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: pytest.PytestRemovedIn9Warning
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: pytest.PytestUnhandledCoroutineWarning
|
||||
:show-inheritance:
|
||||
|
||||
@@ -1619,11 +1642,11 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
Additionally, ``pytest`` will attempt to intelligently identify and ignore a
|
||||
virtualenv by the presence of an activation script. Any directory deemed to
|
||||
be the root of a virtual environment will not be considered during test
|
||||
collection unless ``‑‑collect‑in‑virtualenv`` is given. Note also that
|
||||
``norecursedirs`` takes precedence over ``‑‑collect‑in‑virtualenv``; e.g. if
|
||||
collection unless ``--collect-in-virtualenv`` is given. Note also that
|
||||
``norecursedirs`` takes precedence over ``--collect-in-virtualenv``; e.g. if
|
||||
you intend to run tests in a virtualenv with a base directory that matches
|
||||
``'.*'`` you *must* override ``norecursedirs`` in addition to using the
|
||||
``‑‑collect‑in‑virtualenv`` flag.
|
||||
``--collect-in-virtualenv`` flag.
|
||||
|
||||
|
||||
.. confval:: python_classes
|
||||
@@ -1871,8 +1894,12 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
tests. Optional argument: glob (default: '*').
|
||||
--cache-clear Remove all cache contents at start of test run
|
||||
--lfnf={all,none}, --last-failed-no-failures={all,none}
|
||||
Which tests to run with no previously (known)
|
||||
failures
|
||||
With ``--lf``, determines whether to execute tests
|
||||
when there are no previously (known) failures or
|
||||
when no cached ``lastfailed`` data was found.
|
||||
``all`` (the default) runs the full test suite
|
||||
again. ``none`` just emits a message about no known
|
||||
failures and exits successfully.
|
||||
--sw, --stepwise Exit on test failure and continue from last failing
|
||||
test next time
|
||||
--sw-skip, --stepwise-skip
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
pallets-sphinx-themes
|
||||
pluggy>=1.0
|
||||
pluggy>=1.2.0
|
||||
pygments-pytest>=2.3.0
|
||||
sphinx-removed-in>=0.2.0
|
||||
sphinx>=5,<6
|
||||
sphinx>=5,<8
|
||||
sphinxcontrib-trio
|
||||
sphinxcontrib-svg2pdfconverter
|
||||
# Pin packaging because it no longer handles 'latest' version, which
|
||||
|
||||
@@ -17,7 +17,12 @@ python_classes = ["Test", "Acceptance"]
|
||||
python_functions = ["test"]
|
||||
# NOTE: "doc" is not included here, but gets tested explicitly via "doctesting".
|
||||
testpaths = ["testing"]
|
||||
norecursedirs = ["testing/example_scripts"]
|
||||
norecursedirs = [
|
||||
"testing/example_scripts",
|
||||
".*",
|
||||
"build",
|
||||
"dist",
|
||||
]
|
||||
xfail_strict = true
|
||||
filterwarnings = [
|
||||
"error",
|
||||
@@ -113,7 +118,7 @@ template = "changelog/_template.rst"
|
||||
showcontent = true
|
||||
|
||||
[tool.black]
|
||||
target-version = ['py37']
|
||||
target-version = ['py38']
|
||||
|
||||
# check-wheel-contents is executed by the build-and-inspect-python-package action.
|
||||
[tool.check-wheel-contents]
|
||||
|
||||
@@ -31,10 +31,22 @@ class InvalidFeatureRelease(Exception):
|
||||
SLUG = "pytest-dev/pytest"
|
||||
|
||||
PR_BODY = """\
|
||||
Created automatically from manual trigger.
|
||||
Created by the [prepare release pr]\
|
||||
(https://github.com/pytest-dev/pytest/actions/workflows/prepare-release-pr.yml) workflow.
|
||||
|
||||
Once all builds pass and it has been **approved** by one or more maintainers, the build
|
||||
can be released by pushing a tag `{version}` to this repository.
|
||||
Once all builds pass and it has been **approved** by one or more maintainers, start the \
|
||||
[deploy](https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml) workflow, using these parameters:
|
||||
|
||||
* `Use workflow from`: `release-{version}`.
|
||||
* `Release version`: `{version}`.
|
||||
|
||||
Or execute on the command line:
|
||||
|
||||
```console
|
||||
gh workflow run deploy.yml -r release-{version} -f version={version}
|
||||
```
|
||||
|
||||
After the workflow has been approved by a core maintainer, the package will be uploaded to PyPI automatically.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -5,22 +5,41 @@ from textwrap import dedent
|
||||
from textwrap import indent
|
||||
|
||||
import packaging.version
|
||||
import requests
|
||||
import platformdirs
|
||||
import tabulate
|
||||
import wcwidth
|
||||
from requests_cache import CachedResponse
|
||||
from requests_cache import CachedSession
|
||||
from requests_cache import OriginalResponse
|
||||
from requests_cache import SQLiteCache
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
FILE_HEAD = r"""
|
||||
.. Note this file is autogenerated by scripts/update-plugin-list.py - usually weekly via github action
|
||||
|
||||
.. _plugin-list:
|
||||
|
||||
Plugin List
|
||||
===========
|
||||
Pytest Plugin List
|
||||
==================
|
||||
|
||||
PyPI projects that match "pytest-\*" are considered plugins and are listed
|
||||
automatically together with a manually-maintained list in `the source
|
||||
code <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
|
||||
Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
|
||||
It includes PyPI projects whose names begin with "pytest-" and a handful of manually selected projects.
|
||||
Packages classified as inactive are excluded.
|
||||
|
||||
For detailed insights into how this list is generated,
|
||||
please refer to `the update script <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
|
||||
|
||||
.. warning::
|
||||
|
||||
Please be aware that this list is not a curated collection of projects
|
||||
and does not undergo a systematic review process.
|
||||
It serves purely as an informational resource to aid in the discovery of ``pytest`` plugins.
|
||||
|
||||
Do not presume any endorsement from the ``pytest`` project or its developers,
|
||||
and always conduct your own quality assessment before incorporating any of these plugins into your own projects.
|
||||
|
||||
|
||||
.. The following conditional uses a different format for this list when
|
||||
creating a PDF, because otherwise the table gets far too wide for the
|
||||
page.
|
||||
@@ -37,6 +56,8 @@ DEVELOPMENT_STATUS_CLASSIFIERS = (
|
||||
)
|
||||
ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins
|
||||
"logassert",
|
||||
"nuts",
|
||||
"flask_fixture",
|
||||
}
|
||||
|
||||
|
||||
@@ -53,19 +74,47 @@ def escape_rst(text: str) -> str:
|
||||
return text
|
||||
|
||||
|
||||
def project_response_with_refresh(
|
||||
session: CachedSession, name: str, last_serial: int
|
||||
) -> OriginalResponse | CachedResponse:
|
||||
"""Get a http cached pypi project
|
||||
|
||||
force refresh in case of last serial mismatch
|
||||
"""
|
||||
|
||||
response = session.get(f"https://pypi.org/pypi/{name}/json")
|
||||
if int(response.headers.get("X-PyPI-Last-Serial", -1)) != last_serial:
|
||||
response = session.get(f"https://pypi.org/pypi/{name}/json", refresh=True)
|
||||
return response
|
||||
|
||||
|
||||
def get_session() -> CachedSession:
|
||||
"""Configures the requests-cache session"""
|
||||
cache_path = platformdirs.user_cache_path("pytest-plugin-list")
|
||||
cache_path.mkdir(exist_ok=True, parents=True)
|
||||
cache_file = cache_path.joinpath("http_cache.sqlite3")
|
||||
return CachedSession(backend=SQLiteCache(cache_file))
|
||||
|
||||
|
||||
def pytest_plugin_projects_from_pypi(session: CachedSession) -> dict[str, int]:
|
||||
response = session.get(
|
||||
"https://pypi.org/simple",
|
||||
headers={"Accept": "application/vnd.pypi.simple.v1+json"},
|
||||
refresh=True,
|
||||
)
|
||||
return {
|
||||
name: p["_last-serial"]
|
||||
for p in response.json()["projects"]
|
||||
if (name := p["name"]).startswith("pytest-") or name in ADDITIONAL_PROJECTS
|
||||
}
|
||||
|
||||
|
||||
def iter_plugins():
|
||||
regex = r">([\d\w-]*)</a>"
|
||||
response = requests.get("https://pypi.org/simple")
|
||||
session = get_session()
|
||||
name_2_serial = pytest_plugin_projects_from_pypi(session)
|
||||
|
||||
match_names = (match.groups()[0] for match in re.finditer(regex, response.text))
|
||||
plugin_names = [
|
||||
name
|
||||
for name in match_names
|
||||
if name.startswith("pytest-") or name in ADDITIONAL_PROJECTS
|
||||
]
|
||||
|
||||
for name in tqdm(plugin_names, smoothing=0):
|
||||
response = requests.get(f"https://pypi.org/pypi/{name}/json")
|
||||
for name, last_serial in tqdm(name_2_serial.items(), smoothing=0):
|
||||
response = project_response_with_refresh(session, name, last_serial)
|
||||
if response.status_code == 404:
|
||||
# Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple
|
||||
# but return 404 on the JSON API. Skip.
|
||||
@@ -136,7 +185,7 @@ def plugin_definitions(plugins):
|
||||
|
||||
|
||||
def main():
|
||||
plugins = list(iter_plugins())
|
||||
plugins = [*iter_plugins()]
|
||||
|
||||
reference_dir = pathlib.Path("doc", "en", "reference")
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ classifiers =
|
||||
Operating System :: POSIX
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3 :: Only
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: 3.10
|
||||
@@ -47,12 +46,11 @@ py_modules = py
|
||||
install_requires =
|
||||
iniconfig
|
||||
packaging
|
||||
pluggy>=0.12,<2.0
|
||||
pluggy>=1.3.0,<2.0
|
||||
colorama;sys_platform=="win32"
|
||||
exceptiongroup>=1.0.0rc8;python_version<"3.11"
|
||||
importlib-metadata>=0.12;python_version<"3.8"
|
||||
tomli>=1.0.0;python_version<"3.11"
|
||||
python_requires = >=3.7
|
||||
python_requires = >=3.8
|
||||
package_dir =
|
||||
=src
|
||||
setup_requires =
|
||||
|
||||
@@ -17,18 +17,21 @@ from typing import Any
|
||||
from typing import Callable
|
||||
from typing import ClassVar
|
||||
from typing import Dict
|
||||
from typing import Final
|
||||
from typing import final
|
||||
from typing import Generic
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
from typing import Literal
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import overload
|
||||
from typing import Pattern
|
||||
from typing import Sequence
|
||||
from typing import Set
|
||||
from typing import SupportsIndex
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
@@ -42,22 +45,16 @@ from _pytest._code.source import Source
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest._io.saferepr import safeformat
|
||||
from _pytest._io.saferepr import saferepr
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import get_real_func
|
||||
from _pytest.deprecated import check_ispytest
|
||||
from _pytest.pathlib import absolutepath
|
||||
from _pytest.pathlib import bestrelpath
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Final
|
||||
from typing_extensions import Literal
|
||||
from typing_extensions import SupportsIndex
|
||||
|
||||
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
|
||||
|
||||
if sys.version_info[:2] < (3, 11):
|
||||
from exceptiongroup import BaseExceptionGroup
|
||||
|
||||
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
|
||||
|
||||
|
||||
class Code:
|
||||
"""Wrapper around Python code objects."""
|
||||
@@ -396,11 +393,11 @@ class Traceback(List[TracebackEntry]):
|
||||
|
||||
def filter(
|
||||
self,
|
||||
# TODO(py38): change to positional only.
|
||||
_excinfo_or_fn: Union[
|
||||
excinfo_or_fn: Union[
|
||||
"ExceptionInfo[BaseException]",
|
||||
Callable[[TracebackEntry], bool],
|
||||
],
|
||||
/,
|
||||
) -> "Traceback":
|
||||
"""Return a Traceback instance with certain items removed.
|
||||
|
||||
@@ -411,10 +408,10 @@ class Traceback(List[TracebackEntry]):
|
||||
``TracebackEntry`` instance, and should return True when the item should
|
||||
be added to the ``Traceback``, False when not.
|
||||
"""
|
||||
if isinstance(_excinfo_or_fn, ExceptionInfo):
|
||||
fn = lambda x: not x.ishidden(_excinfo_or_fn) # noqa: E731
|
||||
if isinstance(excinfo_or_fn, ExceptionInfo):
|
||||
fn = lambda x: not x.ishidden(excinfo_or_fn) # noqa: E731
|
||||
else:
|
||||
fn = _excinfo_or_fn
|
||||
fn = excinfo_or_fn
|
||||
return Traceback(filter(fn, self))
|
||||
|
||||
def recursionindex(self) -> Optional[int]:
|
||||
@@ -633,7 +630,7 @@ class ExceptionInfo(Generic[E]):
|
||||
def getrepr(
|
||||
self,
|
||||
showlocals: bool = False,
|
||||
style: "_TracebackStyle" = "long",
|
||||
style: _TracebackStyle = "long",
|
||||
abspath: bool = False,
|
||||
tbfilter: Union[
|
||||
bool, Callable[["ExceptionInfo[BaseException]"], Traceback]
|
||||
@@ -700,6 +697,14 @@ class ExceptionInfo(Generic[E]):
|
||||
)
|
||||
return fmt.repr_excinfo(self)
|
||||
|
||||
def _stringify_exception(self, exc: BaseException) -> str:
|
||||
return "\n".join(
|
||||
[
|
||||
str(exc),
|
||||
*getattr(exc, "__notes__", []),
|
||||
]
|
||||
)
|
||||
|
||||
def match(self, regexp: Union[str, Pattern[str]]) -> "Literal[True]":
|
||||
"""Check whether the regular expression `regexp` matches the string
|
||||
representation of the exception using :func:`python:re.search`.
|
||||
@@ -707,7 +712,7 @@ class ExceptionInfo(Generic[E]):
|
||||
If it matches `True` is returned, otherwise an `AssertionError` is raised.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
value = str(self.value)
|
||||
value = self._stringify_exception(self.value)
|
||||
msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}"
|
||||
if regexp == value:
|
||||
msg += "\n Did you mean to `re.escape()` the regex?"
|
||||
@@ -715,6 +720,69 @@ class ExceptionInfo(Generic[E]):
|
||||
# Return True to allow for "assert excinfo.match()".
|
||||
return True
|
||||
|
||||
def _group_contains(
|
||||
self,
|
||||
exc_group: BaseExceptionGroup[BaseException],
|
||||
expected_exception: Union[Type[BaseException], Tuple[Type[BaseException], ...]],
|
||||
match: Union[str, Pattern[str], None],
|
||||
target_depth: Optional[int] = None,
|
||||
current_depth: int = 1,
|
||||
) -> bool:
|
||||
"""Return `True` if a `BaseExceptionGroup` contains a matching exception."""
|
||||
if (target_depth is not None) and (current_depth > target_depth):
|
||||
# already descended past the target depth
|
||||
return False
|
||||
for exc in exc_group.exceptions:
|
||||
if isinstance(exc, BaseExceptionGroup):
|
||||
if self._group_contains(
|
||||
exc, expected_exception, match, target_depth, current_depth + 1
|
||||
):
|
||||
return True
|
||||
if (target_depth is not None) and (current_depth != target_depth):
|
||||
# not at the target depth, no match
|
||||
continue
|
||||
if not isinstance(exc, expected_exception):
|
||||
continue
|
||||
if match is not None:
|
||||
value = self._stringify_exception(exc)
|
||||
if not re.search(match, value):
|
||||
continue
|
||||
return True
|
||||
return False
|
||||
|
||||
def group_contains(
|
||||
self,
|
||||
expected_exception: Union[Type[BaseException], Tuple[Type[BaseException], ...]],
|
||||
*,
|
||||
match: Union[str, Pattern[str], None] = None,
|
||||
depth: Optional[int] = None,
|
||||
) -> bool:
|
||||
"""Check whether a captured exception group contains a matching exception.
|
||||
|
||||
:param Type[BaseException] | Tuple[Type[BaseException]] expected_exception:
|
||||
The expected exception type, or a tuple if one of multiple possible
|
||||
exception types are expected.
|
||||
|
||||
:param str | Pattern[str] | None match:
|
||||
If specified, a string containing a regular expression,
|
||||
or a regular expression object, that is tested against the string
|
||||
representation of the exception and its `PEP-678 <https://peps.python.org/pep-0678/>` `__notes__`
|
||||
using :func:`re.search`.
|
||||
|
||||
To match a literal string that may contain :ref:`special characters
|
||||
<re-syntax>`, the pattern can first be escaped with :func:`re.escape`.
|
||||
|
||||
:param Optional[int] depth:
|
||||
If `None`, will search for a matching exception at any nesting depth.
|
||||
If >= 1, will only match an exception if it's at the specified depth (depth = 1 being
|
||||
the exceptions contained within the topmost exception group).
|
||||
"""
|
||||
msg = "Captured exception is not an instance of `BaseExceptionGroup`"
|
||||
assert isinstance(self.value, BaseExceptionGroup), msg
|
||||
msg = "`depth` must be >= 1 if specified"
|
||||
assert (depth is None) or (depth >= 1), msg
|
||||
return self._group_contains(self.value, expected_exception, match, depth)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class FormattedExcinfo:
|
||||
@@ -725,7 +793,7 @@ class FormattedExcinfo:
|
||||
fail_marker: ClassVar = "E"
|
||||
|
||||
showlocals: bool = False
|
||||
style: "_TracebackStyle" = "long"
|
||||
style: _TracebackStyle = "long"
|
||||
abspath: bool = True
|
||||
tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True
|
||||
funcargs: bool = False
|
||||
@@ -1090,7 +1158,7 @@ class ReprExceptionInfo(ExceptionRepr):
|
||||
class ReprTraceback(TerminalRepr):
|
||||
reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]]
|
||||
extraline: Optional[str]
|
||||
style: "_TracebackStyle"
|
||||
style: _TracebackStyle
|
||||
|
||||
entrysep: ClassVar = "_ "
|
||||
|
||||
@@ -1124,7 +1192,7 @@ class ReprTracebackNative(ReprTraceback):
|
||||
class ReprEntryNative(TerminalRepr):
|
||||
lines: Sequence[str]
|
||||
|
||||
style: ClassVar["_TracebackStyle"] = "native"
|
||||
style: ClassVar[_TracebackStyle] = "native"
|
||||
|
||||
def toterminal(self, tw: TerminalWriter) -> None:
|
||||
tw.write("".join(self.lines))
|
||||
@@ -1136,7 +1204,7 @@ class ReprEntry(TerminalRepr):
|
||||
reprfuncargs: Optional["ReprFuncArgs"]
|
||||
reprlocals: Optional["ReprLocals"]
|
||||
reprfileloc: Optional["ReprFileLocation"]
|
||||
style: "_TracebackStyle"
|
||||
style: _TracebackStyle
|
||||
|
||||
def _write_entry_lines(self, tw: TerminalWriter) -> None:
|
||||
"""Write the source code portions of a list of traceback entries with syntax highlighting.
|
||||
|
||||
@@ -149,8 +149,7 @@ def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[i
|
||||
values: List[int] = []
|
||||
for x in ast.walk(node):
|
||||
if isinstance(x, (ast.stmt, ast.ExceptHandler)):
|
||||
# Before Python 3.8, the lineno of a decorated class or function pointed at the decorator.
|
||||
# Since Python 3.8, the lineno points to the class/def, so need to include the decorators.
|
||||
# The lineno points to the class/def, so need to include the decorators.
|
||||
if isinstance(x, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
for d in x.decorator_list:
|
||||
values.append(d.lineno - 1)
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from typing import final
|
||||
from typing import Literal
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import TextIO
|
||||
|
||||
from .wcwidth import wcswidth
|
||||
from _pytest.compat import final
|
||||
|
||||
|
||||
# This code was initially copied from py 1.8.1, file _io/terminalwriter.py.
|
||||
@@ -193,15 +194,21 @@ class TerminalWriter:
|
||||
for indent, new_line in zip(indents, new_lines):
|
||||
self.line(indent + new_line)
|
||||
|
||||
def _highlight(self, source: str) -> str:
|
||||
"""Highlight the given source code if we have markup support."""
|
||||
def _highlight(
|
||||
self, source: str, lexer: Literal["diff", "python"] = "python"
|
||||
) -> str:
|
||||
"""Highlight the given source if we have markup support."""
|
||||
from _pytest.config.exceptions import UsageError
|
||||
|
||||
if not self.hasmarkup or not self.code_highlight:
|
||||
return source
|
||||
try:
|
||||
from pygments.formatters.terminal import TerminalFormatter
|
||||
from pygments.lexers.python import PythonLexer
|
||||
|
||||
if lexer == "python":
|
||||
from pygments.lexers.python import PythonLexer as Lexer
|
||||
elif lexer == "diff":
|
||||
from pygments.lexers.diff import DiffLexer as Lexer
|
||||
from pygments import highlight
|
||||
import pygments.util
|
||||
except ImportError:
|
||||
@@ -210,7 +217,7 @@ class TerminalWriter:
|
||||
try:
|
||||
highlighted: str = highlight(
|
||||
source,
|
||||
PythonLexer(),
|
||||
Lexer(),
|
||||
TerminalFormatter(
|
||||
bg=os.getenv("PYTEST_THEME_MODE", "dark"),
|
||||
style=os.getenv("PYTEST_THEME"),
|
||||
|
||||
@@ -25,14 +25,12 @@ from stat import S_ISREG
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Literal
|
||||
from typing import overload
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from . import error
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Literal
|
||||
|
||||
# Moved from local.py.
|
||||
iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt")
|
||||
|
||||
@@ -757,7 +755,13 @@ class LocalPath:
|
||||
if ensure:
|
||||
self.dirpath().ensure(dir=1)
|
||||
if encoding:
|
||||
return error.checked_call(io.open, self.strpath, mode, encoding=encoding)
|
||||
# Using type ignore here because of this error:
|
||||
# error: Argument 1 has incompatible type overloaded function;
|
||||
# expected "Callable[[str, Any, Any], TextIOWrapper]" [arg-type]
|
||||
# Which seems incorrect, given io.open supports the given argument types.
|
||||
return error.checked_call(
|
||||
io.open, self.strpath, mode, encoding=encoding # type:ignore[arg-type]
|
||||
)
|
||||
return error.checked_call(open, self.strpath, mode)
|
||||
|
||||
def _fastjoin(self, name):
|
||||
@@ -1263,13 +1267,19 @@ class LocalPath:
|
||||
@classmethod
|
||||
def mkdtemp(cls, rootdir=None):
|
||||
"""Return a Path object pointing to a fresh new temporary directory
|
||||
(which we created ourself).
|
||||
(which we created ourselves).
|
||||
"""
|
||||
import tempfile
|
||||
|
||||
if rootdir is None:
|
||||
rootdir = cls.get_temproot()
|
||||
return cls(error.checked_call(tempfile.mkdtemp, dir=str(rootdir)))
|
||||
# Using type ignore here because of this error:
|
||||
# error: Argument 1 has incompatible type overloaded function; expected "Callable[[str], str]" [arg-type]
|
||||
# Which seems incorrect, given tempfile.mkdtemp supports the given argument types.
|
||||
path = error.checked_call(
|
||||
tempfile.mkdtemp, dir=str(rootdir) # type:ignore[arg-type]
|
||||
)
|
||||
return cls(path)
|
||||
|
||||
@classmethod
|
||||
def make_numbered_dir(
|
||||
|
||||
@@ -112,8 +112,8 @@ def pytest_collection(session: "Session") -> None:
|
||||
assertstate.hook.set_session(session)
|
||||
|
||||
|
||||
@hookimpl(tryfirst=True, hookwrapper=True)
|
||||
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
|
||||
@hookimpl(wrapper=True, tryfirst=True)
|
||||
def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
|
||||
"""Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks.
|
||||
|
||||
The rewrite module will use util._reprcompare if it exists to use custom
|
||||
@@ -162,10 +162,11 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
|
||||
|
||||
util._assertion_pass = call_assertion_pass_hook
|
||||
|
||||
yield
|
||||
|
||||
util._reprcompare, util._assertion_pass = saved_assert_hooks
|
||||
util._config = None
|
||||
try:
|
||||
return (yield)
|
||||
finally:
|
||||
util._reprcompare, util._assertion_pass = saved_assert_hooks
|
||||
util._config = None
|
||||
|
||||
|
||||
def pytest_sessionfinish(session: "Session") -> None:
|
||||
|
||||
@@ -13,6 +13,7 @@ import struct
|
||||
import sys
|
||||
import tokenize
|
||||
import types
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from pathlib import PurePath
|
||||
from typing import Callable
|
||||
@@ -44,16 +45,9 @@ from _pytest.stash import StashKey
|
||||
if TYPE_CHECKING:
|
||||
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
|
||||
|
||||
class Sentinel:
|
||||
pass
|
||||
|
||||
|
||||
assertstate_key = StashKey["AssertionState"]()
|
||||
@@ -63,6 +57,9 @@ PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}"
|
||||
PYC_EXT = ".py" + (__debug__ and "c" or "o")
|
||||
PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
|
||||
|
||||
# Special marker that denotes we have just left a scope definition
|
||||
_SCOPE_END_MARKER = Sentinel()
|
||||
|
||||
|
||||
class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader):
|
||||
"""PEP302/PEP451 import hook which rewrites asserts."""
|
||||
@@ -645,6 +642,8 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
.push_format_context() and .pop_format_context() which allows
|
||||
to build another %-formatted string while already building one.
|
||||
|
||||
:scope: A tuple containing the current scope used for variables_overwrite.
|
||||
|
||||
:variables_overwrite: A dict filled with references to variables
|
||||
that change value within an assert. This happens when a variable is
|
||||
reassigned with the walrus operator
|
||||
@@ -666,7 +665,10 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
else:
|
||||
self.enable_assertion_pass_hook = False
|
||||
self.source = source
|
||||
self.variables_overwrite: Dict[str, str] = {}
|
||||
self.scope: tuple[ast.AST, ...] = ()
|
||||
self.variables_overwrite: defaultdict[
|
||||
tuple[ast.AST, ...], Dict[str, str]
|
||||
] = defaultdict(dict)
|
||||
|
||||
def run(self, mod: ast.Module) -> None:
|
||||
"""Find all assert statements in *mod* and rewrite them."""
|
||||
@@ -686,12 +688,10 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
if (
|
||||
expect_docstring
|
||||
and isinstance(item, ast.Expr)
|
||||
and isinstance(item.value, astStr)
|
||||
and isinstance(item.value, ast.Constant)
|
||||
and isinstance(item.value.value, str)
|
||||
):
|
||||
if sys.version_info >= (3, 8):
|
||||
doc = item.value.value
|
||||
else:
|
||||
doc = item.value.s
|
||||
doc = item.value.value
|
||||
if self.is_rewrite_disabled(doc):
|
||||
return
|
||||
expect_docstring = False
|
||||
@@ -732,9 +732,17 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
mod.body[pos:pos] = imports
|
||||
|
||||
# Collect asserts.
|
||||
nodes: List[ast.AST] = [mod]
|
||||
self.scope = (mod,)
|
||||
nodes: List[Union[ast.AST, Sentinel]] = [mod]
|
||||
while nodes:
|
||||
node = nodes.pop()
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
||||
self.scope = tuple((*self.scope, node))
|
||||
nodes.append(_SCOPE_END_MARKER)
|
||||
if node == _SCOPE_END_MARKER:
|
||||
self.scope = self.scope[:-1]
|
||||
continue
|
||||
assert isinstance(node, ast.AST)
|
||||
for name, field in ast.iter_fields(node):
|
||||
if isinstance(field, list):
|
||||
new: List[ast.AST] = []
|
||||
@@ -823,7 +831,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
current = self.stack.pop()
|
||||
if self.stack:
|
||||
self.explanation_specifiers = self.stack[-1]
|
||||
keys = [astStr(key) for key in current.keys()]
|
||||
keys = [ast.Constant(key) for key in current.keys()]
|
||||
format_dict = ast.Dict(keys, list(current.values()))
|
||||
form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
|
||||
name = "@py_format" + str(next(self.variable_counter))
|
||||
@@ -877,16 +885,16 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
negation = ast.UnaryOp(ast.Not(), top_condition)
|
||||
|
||||
if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
|
||||
msg = self.pop_format_context(astStr(explanation))
|
||||
msg = self.pop_format_context(ast.Constant(explanation))
|
||||
|
||||
# Failed
|
||||
if assert_.msg:
|
||||
assertmsg = self.helper("_format_assertmsg", assert_.msg)
|
||||
gluestr = "\n>assert "
|
||||
else:
|
||||
assertmsg = astStr("")
|
||||
assertmsg = ast.Constant("")
|
||||
gluestr = "assert "
|
||||
err_explanation = ast.BinOp(astStr(gluestr), ast.Add(), msg)
|
||||
err_explanation = ast.BinOp(ast.Constant(gluestr), ast.Add(), msg)
|
||||
err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation)
|
||||
err_name = ast.Name("AssertionError", ast.Load())
|
||||
fmt = self.helper("_format_explanation", err_msg)
|
||||
@@ -902,8 +910,8 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
hook_call_pass = ast.Expr(
|
||||
self.helper(
|
||||
"_call_assertion_pass",
|
||||
astNum(assert_.lineno),
|
||||
astStr(orig),
|
||||
ast.Constant(assert_.lineno),
|
||||
ast.Constant(orig),
|
||||
fmt_pass,
|
||||
)
|
||||
)
|
||||
@@ -922,7 +930,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
variables = [
|
||||
ast.Name(name, ast.Store()) for name in self.format_variables
|
||||
]
|
||||
clear_format = ast.Assign(variables, astNameConstant(None))
|
||||
clear_format = ast.Assign(variables, ast.Constant(None))
|
||||
self.statements.append(clear_format)
|
||||
|
||||
else: # Original assertion rewriting
|
||||
@@ -933,9 +941,9 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
assertmsg = self.helper("_format_assertmsg", assert_.msg)
|
||||
explanation = "\n>assert " + explanation
|
||||
else:
|
||||
assertmsg = astStr("")
|
||||
assertmsg = ast.Constant("")
|
||||
explanation = "assert " + explanation
|
||||
template = ast.BinOp(assertmsg, ast.Add(), astStr(explanation))
|
||||
template = ast.BinOp(assertmsg, ast.Add(), ast.Constant(explanation))
|
||||
msg = self.pop_format_context(template)
|
||||
fmt = self.helper("_format_explanation", msg)
|
||||
err_name = ast.Name("AssertionError", ast.Load())
|
||||
@@ -947,7 +955,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
# Clear temporary variables by setting them to None.
|
||||
if self.variables:
|
||||
variables = [ast.Name(name, ast.Store()) for name in self.variables]
|
||||
clear = ast.Assign(variables, astNameConstant(None))
|
||||
clear = ast.Assign(variables, ast.Constant(None))
|
||||
self.statements.append(clear)
|
||||
# Fix locations (line numbers/column offsets).
|
||||
for stmt in self.statements:
|
||||
@@ -955,26 +963,26 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
ast.copy_location(node, assert_)
|
||||
return self.statements
|
||||
|
||||
def visit_NamedExpr(self, name: namedExpr) -> Tuple[namedExpr, str]:
|
||||
def visit_NamedExpr(self, name: ast.NamedExpr) -> Tuple[ast.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])
|
||||
inlocs = ast.Compare(ast.Constant(target_id), [ast.In()], [locs])
|
||||
dorepr = self.helper("_should_repr_global_name", name)
|
||||
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
|
||||
expr = ast.IfExp(test, self.display(name), astStr(target_id))
|
||||
expr = ast.IfExp(test, self.display(name), ast.Constant(target_id))
|
||||
return name, self.explanation_param(expr)
|
||||
|
||||
def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]:
|
||||
# Display the repr of the name if it's a local variable or
|
||||
# _should_repr_global_name() thinks it's acceptable.
|
||||
locs = ast.Call(self.builtin("locals"), [], [])
|
||||
inlocs = ast.Compare(astStr(name.id), [ast.In()], [locs])
|
||||
inlocs = ast.Compare(ast.Constant(name.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(name.id))
|
||||
expr = ast.IfExp(test, self.display(name), ast.Constant(name.id))
|
||||
return name, self.explanation_param(expr)
|
||||
|
||||
def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
|
||||
@@ -993,10 +1001,10 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
# cond is set in a prior loop iteration below
|
||||
self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa
|
||||
self.expl_stmts = fail_inner
|
||||
# Check if the left operand is a namedExpr and the value has already been visited
|
||||
# Check if the left operand is a ast.NamedExpr and the value has already been visited
|
||||
if (
|
||||
isinstance(v, ast.Compare)
|
||||
and isinstance(v.left, namedExpr)
|
||||
and isinstance(v.left, ast.NamedExpr)
|
||||
and v.left.target.id
|
||||
in [
|
||||
ast_expr.id
|
||||
@@ -1005,14 +1013,14 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
]
|
||||
):
|
||||
pytest_temp = self.variable()
|
||||
self.variables_overwrite[
|
||||
self.variables_overwrite[self.scope][
|
||||
v.left.target.id
|
||||
] = v.left # type:ignore[assignment]
|
||||
v.left.target.id = pytest_temp
|
||||
self.push_format_context()
|
||||
res, expl = self.visit(v)
|
||||
body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
|
||||
expl_format = self.pop_format_context(astStr(expl))
|
||||
expl_format = self.pop_format_context(ast.Constant(expl))
|
||||
call = ast.Call(app, [expl_format], [])
|
||||
self.expl_stmts.append(ast.Expr(call))
|
||||
if i < levels:
|
||||
@@ -1024,7 +1032,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
self.statements = body = inner
|
||||
self.statements = save
|
||||
self.expl_stmts = fail_save
|
||||
expl_template = self.helper("_format_boolop", expl_list, astNum(is_or))
|
||||
expl_template = self.helper("_format_boolop", expl_list, ast.Constant(is_or))
|
||||
expl = self.pop_format_context(expl_template)
|
||||
return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
|
||||
|
||||
@@ -1048,17 +1056,20 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
new_args = []
|
||||
new_kwargs = []
|
||||
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]
|
||||
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get(
|
||||
self.scope, {}
|
||||
):
|
||||
arg = self.variables_overwrite[self.scope][
|
||||
arg.id
|
||||
] # type:ignore[assignment]
|
||||
res, expl = self.visit(arg)
|
||||
arg_expls.append(expl)
|
||||
new_args.append(res)
|
||||
for keyword in call.keywords:
|
||||
if (
|
||||
isinstance(keyword.value, ast.Name)
|
||||
and keyword.value.id in self.variables_overwrite
|
||||
):
|
||||
keyword.value = self.variables_overwrite[
|
||||
if isinstance(
|
||||
keyword.value, ast.Name
|
||||
) and keyword.value.id in self.variables_overwrite.get(self.scope, {}):
|
||||
keyword.value = self.variables_overwrite[self.scope][
|
||||
keyword.value.id
|
||||
] # type:ignore[assignment]
|
||||
res, expl = self.visit(keyword.value)
|
||||
@@ -1094,12 +1105,14 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]:
|
||||
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[
|
||||
if isinstance(
|
||||
comp.left, ast.Name
|
||||
) and comp.left.id in self.variables_overwrite.get(self.scope, {}):
|
||||
comp.left = self.variables_overwrite[self.scope][
|
||||
comp.left.id
|
||||
] # type:ignore[assignment]
|
||||
if isinstance(comp.left, namedExpr):
|
||||
self.variables_overwrite[
|
||||
if isinstance(comp.left, ast.NamedExpr):
|
||||
self.variables_overwrite[self.scope][
|
||||
comp.left.target.id
|
||||
] = comp.left # type:ignore[assignment]
|
||||
left_res, left_expl = self.visit(comp.left)
|
||||
@@ -1114,12 +1127,12 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
results = [left_res]
|
||||
for i, op, next_operand in it:
|
||||
if (
|
||||
isinstance(next_operand, namedExpr)
|
||||
isinstance(next_operand, ast.NamedExpr)
|
||||
and isinstance(left_res, ast.Name)
|
||||
and next_operand.target.id == left_res.id
|
||||
):
|
||||
next_operand.target.id = self.variable()
|
||||
self.variables_overwrite[
|
||||
self.variables_overwrite[self.scope][
|
||||
left_res.id
|
||||
] = next_operand # type:ignore[assignment]
|
||||
next_res, next_expl = self.visit(next_operand)
|
||||
@@ -1127,9 +1140,9 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
next_expl = f"({next_expl})"
|
||||
results.append(next_res)
|
||||
sym = BINOP_MAP[op.__class__]
|
||||
syms.append(astStr(sym))
|
||||
syms.append(ast.Constant(sym))
|
||||
expl = f"{left_expl} {sym} {next_expl}"
|
||||
expls.append(astStr(expl))
|
||||
expls.append(ast.Constant(expl))
|
||||
res_expr = ast.Compare(left_res, [op], [next_res])
|
||||
self.statements.append(ast.Assign([store_names[i]], res_expr))
|
||||
left_res, left_expl = next_res, next_expl
|
||||
@@ -1173,7 +1186,7 @@ def try_makedirs(cache_dir: Path) -> bool:
|
||||
|
||||
def get_cache_dir(file_path: Path) -> Path:
|
||||
"""Return the cache directory to write .pyc files for the given .py file path."""
|
||||
if sys.version_info >= (3, 8) and sys.pycache_prefix:
|
||||
if sys.pycache_prefix:
|
||||
# given:
|
||||
# prefix = '/tmp/pycs'
|
||||
# path = '/home/user/proj/test_app.py'
|
||||
|
||||
@@ -7,8 +7,10 @@ from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
from typing import Literal
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import Protocol
|
||||
from typing import Sequence
|
||||
from unicodedata import normalize
|
||||
|
||||
@@ -33,6 +35,11 @@ _assertion_pass: Optional[Callable[[int, str, str], None]] = None
|
||||
_config: Optional[Config] = None
|
||||
|
||||
|
||||
class _HighlightFunc(Protocol):
|
||||
def __call__(self, source: str, lexer: Literal["diff", "python"] = "python") -> str:
|
||||
"""Apply highlighting to the given source."""
|
||||
|
||||
|
||||
def format_explanation(explanation: str) -> str:
|
||||
r"""Format an explanation.
|
||||
|
||||
@@ -132,7 +139,7 @@ def isiterable(obj: Any) -> bool:
|
||||
try:
|
||||
iter(obj)
|
||||
return not istext(obj)
|
||||
except TypeError:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
@@ -189,10 +196,27 @@ def assertrepr_compare(
|
||||
explanation = None
|
||||
try:
|
||||
if op == "==":
|
||||
explanation = _compare_eq_any(left, right, verbose)
|
||||
writer = config.get_terminal_writer()
|
||||
explanation = _compare_eq_any(left, right, writer._highlight, verbose)
|
||||
elif op == "not in":
|
||||
if istext(left) and istext(right):
|
||||
explanation = _notin_text(left, right, verbose)
|
||||
elif op == "!=":
|
||||
if isset(left) and isset(right):
|
||||
explanation = ["Both sets are equal"]
|
||||
elif op == ">=":
|
||||
if isset(left) and isset(right):
|
||||
explanation = _compare_gte_set(left, right, verbose)
|
||||
elif op == "<=":
|
||||
if isset(left) and isset(right):
|
||||
explanation = _compare_lte_set(left, right, verbose)
|
||||
elif op == ">":
|
||||
if isset(left) and isset(right):
|
||||
explanation = _compare_gt_set(left, right, verbose)
|
||||
elif op == "<":
|
||||
if isset(left) and isset(right):
|
||||
explanation = _compare_lt_set(left, right, verbose)
|
||||
|
||||
except outcomes.Exit:
|
||||
raise
|
||||
except Exception:
|
||||
@@ -209,7 +233,9 @@ def assertrepr_compare(
|
||||
return [summary] + explanation
|
||||
|
||||
|
||||
def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]:
|
||||
def _compare_eq_any(
|
||||
left: Any, right: Any, highlighter: _HighlightFunc, verbose: int = 0
|
||||
) -> List[str]:
|
||||
explanation = []
|
||||
if istext(left) and istext(right):
|
||||
explanation = _diff_text(left, right, verbose)
|
||||
@@ -222,14 +248,14 @@ def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]:
|
||||
other_side = right if isinstance(left, ApproxBase) else left
|
||||
|
||||
explanation = approx_side._repr_compare(other_side)
|
||||
elif type(left) == type(right) and (
|
||||
elif type(left) is type(right) and (
|
||||
isdatacls(left) or isattrs(left) or isnamedtuple(left)
|
||||
):
|
||||
# Note: unlike dataclasses/attrs, namedtuples compare only the
|
||||
# field values, not the type or field names. But this branch
|
||||
# intentionally only handles the same-type case, which was often
|
||||
# used in older code bases before dataclasses/attrs were available.
|
||||
explanation = _compare_eq_cls(left, right, verbose)
|
||||
explanation = _compare_eq_cls(left, right, highlighter, verbose)
|
||||
elif issequence(left) and issequence(right):
|
||||
explanation = _compare_eq_sequence(left, right, verbose)
|
||||
elif isset(left) and isset(right):
|
||||
@@ -238,7 +264,7 @@ def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]:
|
||||
explanation = _compare_eq_dict(left, right, verbose)
|
||||
|
||||
if isiterable(left) and isiterable(right):
|
||||
expl = _compare_eq_iterable(left, right, verbose)
|
||||
expl = _compare_eq_iterable(left, right, highlighter, verbose)
|
||||
explanation.extend(expl)
|
||||
|
||||
return explanation
|
||||
@@ -305,7 +331,10 @@ def _surrounding_parens_on_own_lines(lines: List[str]) -> None:
|
||||
|
||||
|
||||
def _compare_eq_iterable(
|
||||
left: Iterable[Any], right: Iterable[Any], verbose: int = 0
|
||||
left: Iterable[Any],
|
||||
right: Iterable[Any],
|
||||
highligher: _HighlightFunc,
|
||||
verbose: int = 0,
|
||||
) -> List[str]:
|
||||
if verbose <= 0 and not running_on_ci():
|
||||
return ["Use -v to get more diff"]
|
||||
@@ -330,7 +359,13 @@ def _compare_eq_iterable(
|
||||
# "right" is the expected base against which we compare "left",
|
||||
# see https://github.com/pytest-dev/pytest/issues/3333
|
||||
explanation.extend(
|
||||
line.rstrip() for line in difflib.ndiff(right_formatting, left_formatting)
|
||||
highligher(
|
||||
"\n".join(
|
||||
line.rstrip()
|
||||
for line in difflib.ndiff(right_formatting, left_formatting)
|
||||
),
|
||||
lexer="diff",
|
||||
).splitlines()
|
||||
)
|
||||
return explanation
|
||||
|
||||
@@ -392,15 +427,49 @@ def _compare_eq_set(
|
||||
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
|
||||
) -> List[str]:
|
||||
explanation = []
|
||||
diff_left = left - right
|
||||
diff_right = right - left
|
||||
if diff_left:
|
||||
explanation.append("Extra items in the left set:")
|
||||
for item in diff_left:
|
||||
explanation.append(saferepr(item))
|
||||
if diff_right:
|
||||
explanation.append("Extra items in the right set:")
|
||||
for item in diff_right:
|
||||
explanation.extend(_set_one_sided_diff("left", left, right))
|
||||
explanation.extend(_set_one_sided_diff("right", right, left))
|
||||
return explanation
|
||||
|
||||
|
||||
def _compare_gt_set(
|
||||
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
|
||||
) -> List[str]:
|
||||
explanation = _compare_gte_set(left, right, verbose)
|
||||
if not explanation:
|
||||
return ["Both sets are equal"]
|
||||
return explanation
|
||||
|
||||
|
||||
def _compare_lt_set(
|
||||
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
|
||||
) -> List[str]:
|
||||
explanation = _compare_lte_set(left, right, verbose)
|
||||
if not explanation:
|
||||
return ["Both sets are equal"]
|
||||
return explanation
|
||||
|
||||
|
||||
def _compare_gte_set(
|
||||
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
|
||||
) -> List[str]:
|
||||
return _set_one_sided_diff("right", right, left)
|
||||
|
||||
|
||||
def _compare_lte_set(
|
||||
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
|
||||
) -> List[str]:
|
||||
return _set_one_sided_diff("left", left, right)
|
||||
|
||||
|
||||
def _set_one_sided_diff(
|
||||
posn: str, set1: AbstractSet[Any], set2: AbstractSet[Any]
|
||||
) -> List[str]:
|
||||
explanation = []
|
||||
diff = set1 - set2
|
||||
if diff:
|
||||
explanation.append(f"Extra items in the {posn} set:")
|
||||
for item in diff:
|
||||
explanation.append(saferepr(item))
|
||||
return explanation
|
||||
|
||||
@@ -446,7 +515,9 @@ def _compare_eq_dict(
|
||||
return explanation
|
||||
|
||||
|
||||
def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]:
|
||||
def _compare_eq_cls(
|
||||
left: Any, right: Any, highlighter: _HighlightFunc, verbose: int
|
||||
) -> List[str]:
|
||||
if not has_default_eq(left):
|
||||
return []
|
||||
if isdatacls(left):
|
||||
@@ -492,7 +563,9 @@ def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]:
|
||||
]
|
||||
explanation += [
|
||||
indent + line
|
||||
for line in _compare_eq_any(field_left, field_right, verbose)
|
||||
for line in _compare_eq_any(
|
||||
field_left, field_right, highlighter, verbose
|
||||
)
|
||||
]
|
||||
return explanation
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
from typing import final
|
||||
from typing import Generator
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
@@ -18,7 +19,6 @@ from .pathlib import rm_rf
|
||||
from .reports import CollectReport
|
||||
from _pytest import nodes
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest.compat import final
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ExitCode
|
||||
from _pytest.config import hookimpl
|
||||
@@ -217,42 +217,34 @@ class LFPluginCollWrapper:
|
||||
self.lfplugin = lfplugin
|
||||
self._collected_at_least_one_failure = False
|
||||
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_make_collect_report(self, collector: nodes.Collector):
|
||||
@hookimpl(wrapper=True)
|
||||
def pytest_make_collect_report(
|
||||
self, collector: nodes.Collector
|
||||
) -> Generator[None, CollectReport, CollectReport]:
|
||||
res = yield
|
||||
if isinstance(collector, (Session, Package)):
|
||||
out = yield
|
||||
res: CollectReport = out.get_result()
|
||||
|
||||
# Sort any lf-paths to the beginning.
|
||||
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
|
||||
return node.path in lf_paths
|
||||
|
||||
res.result = sorted(
|
||||
res.result,
|
||||
key=sort_key,
|
||||
reverse=True,
|
||||
)
|
||||
return
|
||||
|
||||
elif isinstance(collector, File):
|
||||
if collector.path in self.lfplugin._last_failed_paths:
|
||||
out = yield
|
||||
res = out.get_result()
|
||||
result = res.result
|
||||
lastfailed = self.lfplugin.lastfailed
|
||||
|
||||
# Only filter with known failures.
|
||||
if not self._collected_at_least_one_failure:
|
||||
if not any(x.nodeid in lastfailed for x in result):
|
||||
return
|
||||
return res
|
||||
self.lfplugin.config.pluginmanager.register(
|
||||
LFPluginCollSkipfiles(self.lfplugin), "lfplugin-collskip"
|
||||
)
|
||||
@@ -268,8 +260,8 @@ class LFPluginCollWrapper:
|
||||
# Keep all sub-collectors.
|
||||
or isinstance(x, nodes.Collector)
|
||||
]
|
||||
return
|
||||
yield
|
||||
|
||||
return res
|
||||
|
||||
|
||||
class LFPluginCollSkipfiles:
|
||||
@@ -280,9 +272,7 @@ class LFPluginCollSkipfiles:
|
||||
def pytest_make_collect_report(
|
||||
self, collector: nodes.Collector
|
||||
) -> Optional[CollectReport]:
|
||||
# Packages are Files, but we only want to skip test-bearing Files,
|
||||
# so don't filter Packages.
|
||||
if isinstance(collector, File) and not isinstance(collector, Package):
|
||||
if isinstance(collector, File):
|
||||
if collector.path not in self.lfplugin._last_failed_paths:
|
||||
self.lfplugin._skipped_files += 1
|
||||
|
||||
@@ -342,14 +332,14 @@ class LFPlugin:
|
||||
else:
|
||||
self.lastfailed[report.nodeid] = True
|
||||
|
||||
@hookimpl(hookwrapper=True, tryfirst=True)
|
||||
@hookimpl(wrapper=True, tryfirst=True)
|
||||
def pytest_collection_modifyitems(
|
||||
self, config: Config, items: List[nodes.Item]
|
||||
) -> Generator[None, None, None]:
|
||||
yield
|
||||
res = yield
|
||||
|
||||
if not self.active:
|
||||
return
|
||||
return res
|
||||
|
||||
if self.lastfailed:
|
||||
previously_failed = []
|
||||
@@ -394,6 +384,8 @@ class LFPlugin:
|
||||
else:
|
||||
self._report_status += "not deselecting items."
|
||||
|
||||
return res
|
||||
|
||||
def pytest_sessionfinish(self, session: Session) -> None:
|
||||
config = self.config
|
||||
if config.getoption("cacheshow") or hasattr(config, "workerinput"):
|
||||
@@ -414,11 +406,11 @@ class NFPlugin:
|
||||
assert config.cache is not None
|
||||
self.cached_nodeids = set(config.cache.get("cache/nodeids", []))
|
||||
|
||||
@hookimpl(hookwrapper=True, tryfirst=True)
|
||||
@hookimpl(wrapper=True, tryfirst=True)
|
||||
def pytest_collection_modifyitems(
|
||||
self, items: List[nodes.Item]
|
||||
) -> Generator[None, None, None]:
|
||||
yield
|
||||
res = yield
|
||||
|
||||
if self.active:
|
||||
new_items: Dict[str, nodes.Item] = {}
|
||||
@@ -436,6 +428,8 @@ class NFPlugin:
|
||||
else:
|
||||
self.cached_nodeids.update(item.nodeid for item in items)
|
||||
|
||||
return res
|
||||
|
||||
def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]:
|
||||
return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True) # type: ignore[no-any-return]
|
||||
|
||||
@@ -505,7 +499,11 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
dest="last_failed_no_failures",
|
||||
choices=("all", "none"),
|
||||
default="all",
|
||||
help="Which tests to run with no previously (known) failures",
|
||||
help="With ``--lf``, determines whether to execute tests when there "
|
||||
"are no previously (known) failures or when no "
|
||||
"cached ``lastfailed`` data was found. "
|
||||
"``all`` (the default) runs the full test suite again. "
|
||||
"``none`` just emits a message about no known failures and exits successfully.",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -11,11 +11,14 @@ from types import TracebackType
|
||||
from typing import Any
|
||||
from typing import AnyStr
|
||||
from typing import BinaryIO
|
||||
from typing import Final
|
||||
from typing import final
|
||||
from typing import Generator
|
||||
from typing import Generic
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Literal
|
||||
from typing import NamedTuple
|
||||
from typing import Optional
|
||||
from typing import TextIO
|
||||
@@ -24,7 +27,6 @@ from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
from _pytest.compat import final
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.config.argparsing import Parser
|
||||
@@ -34,12 +36,9 @@ from _pytest.fixtures import SubRequest
|
||||
from _pytest.nodes import Collector
|
||||
from _pytest.nodes import File
|
||||
from _pytest.nodes import Item
|
||||
from _pytest.reports import CollectReport
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Final
|
||||
from typing_extensions import Literal
|
||||
|
||||
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
|
||||
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
|
||||
|
||||
|
||||
def pytest_addoption(parser: Parser) -> None:
|
||||
@@ -132,8 +131,8 @@ def _windowsconsoleio_workaround(stream: TextIO) -> None:
|
||||
sys.stderr = _reopen_stdio(sys.stderr, "wb")
|
||||
|
||||
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_load_initial_conftests(early_config: Config):
|
||||
@hookimpl(wrapper=True)
|
||||
def pytest_load_initial_conftests(early_config: Config) -> Generator[None, None, None]:
|
||||
ns = early_config.known_args_namespace
|
||||
if ns.capture == "fd":
|
||||
_windowsconsoleio_workaround(sys.stdout)
|
||||
@@ -147,12 +146,16 @@ def pytest_load_initial_conftests(early_config: Config):
|
||||
|
||||
# Finally trigger conftest loading but while capturing (issue #93).
|
||||
capman.start_global_capturing()
|
||||
outcome = yield
|
||||
capman.suspend_global_capture()
|
||||
if outcome.excinfo is not None:
|
||||
try:
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
capman.suspend_global_capture()
|
||||
except BaseException:
|
||||
out, err = capman.read_global_capture()
|
||||
sys.stdout.write(out)
|
||||
sys.stderr.write(err)
|
||||
raise
|
||||
|
||||
|
||||
# IO Helpers.
|
||||
@@ -687,7 +690,7 @@ class MultiCapture(Generic[AnyStr]):
|
||||
return CaptureResult(out, err) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]:
|
||||
def _get_multicapture(method: _CaptureMethod) -> MultiCapture[str]:
|
||||
if method == "fd":
|
||||
return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2))
|
||||
elif method == "sys":
|
||||
@@ -723,7 +726,7 @@ class CaptureManager:
|
||||
needed to ensure the fixtures take precedence over the global capture.
|
||||
"""
|
||||
|
||||
def __init__(self, method: "_CaptureMethod") -> None:
|
||||
def __init__(self, method: _CaptureMethod) -> None:
|
||||
self._method: Final = method
|
||||
self._global_capturing: Optional[MultiCapture[str]] = None
|
||||
self._capture_fixture: Optional[CaptureFixture[Any]] = None
|
||||
@@ -843,41 +846,45 @@ class CaptureManager:
|
||||
self.deactivate_fixture()
|
||||
self.suspend_global_capture(in_=False)
|
||||
|
||||
out, err = self.read_global_capture()
|
||||
item.add_report_section(when, "stdout", out)
|
||||
item.add_report_section(when, "stderr", err)
|
||||
out, err = self.read_global_capture()
|
||||
item.add_report_section(when, "stdout", out)
|
||||
item.add_report_section(when, "stderr", err)
|
||||
|
||||
# Hooks
|
||||
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_make_collect_report(self, collector: Collector):
|
||||
@hookimpl(wrapper=True)
|
||||
def pytest_make_collect_report(
|
||||
self, collector: Collector
|
||||
) -> Generator[None, CollectReport, CollectReport]:
|
||||
if isinstance(collector, File):
|
||||
self.resume_global_capture()
|
||||
outcome = yield
|
||||
self.suspend_global_capture()
|
||||
try:
|
||||
rep = yield
|
||||
finally:
|
||||
self.suspend_global_capture()
|
||||
out, err = self.read_global_capture()
|
||||
rep = outcome.get_result()
|
||||
if out:
|
||||
rep.sections.append(("Captured stdout", out))
|
||||
if err:
|
||||
rep.sections.append(("Captured stderr", err))
|
||||
else:
|
||||
yield
|
||||
rep = yield
|
||||
return rep
|
||||
|
||||
@hookimpl(hookwrapper=True)
|
||||
@hookimpl(wrapper=True)
|
||||
def pytest_runtest_setup(self, item: Item) -> Generator[None, None, None]:
|
||||
with self.item_capture("setup", item):
|
||||
yield
|
||||
return (yield)
|
||||
|
||||
@hookimpl(hookwrapper=True)
|
||||
@hookimpl(wrapper=True)
|
||||
def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]:
|
||||
with self.item_capture("call", item):
|
||||
yield
|
||||
return (yield)
|
||||
|
||||
@hookimpl(hookwrapper=True)
|
||||
@hookimpl(wrapper=True)
|
||||
def pytest_runtest_teardown(self, item: Item) -> Generator[None, None, None]:
|
||||
with self.item_capture("teardown", item):
|
||||
yield
|
||||
return (yield)
|
||||
|
||||
@hookimpl(tryfirst=True)
|
||||
def pytest_keyboard_interrupt(self) -> None:
|
||||
|
||||
@@ -12,26 +12,12 @@ from inspect import signature
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Generic
|
||||
from typing import Final
|
||||
from typing import NoReturn
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
|
||||
import py
|
||||
|
||||
# fmt: off
|
||||
# Workaround for https://github.com/sphinx-doc/sphinx/issues/10351.
|
||||
# If `overload` is imported from `compat` instead of from `typing`,
|
||||
# Sphinx doesn't recognize it as `overload` and the API docs for
|
||||
# overloaded functions look good again. But type checkers handle
|
||||
# it fine.
|
||||
# fmt: on
|
||||
if True:
|
||||
from typing import overload as overload
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Final
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_S = TypeVar("_S")
|
||||
@@ -58,17 +44,6 @@ class NotSetType(enum.Enum):
|
||||
NOTSET: Final = NotSetType.token # noqa: E305
|
||||
# fmt: on
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
import importlib.metadata
|
||||
|
||||
importlib_metadata = importlib.metadata
|
||||
else:
|
||||
import importlib_metadata as importlib_metadata # noqa: F401
|
||||
|
||||
|
||||
def _format_args(func: Callable[..., Any]) -> str:
|
||||
return str(signature(func))
|
||||
|
||||
|
||||
def is_generator(func: object) -> bool:
|
||||
genfunc = inspect.isgeneratorfunction(func)
|
||||
@@ -93,7 +68,7 @@ def is_async_function(func: object) -> bool:
|
||||
return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
|
||||
|
||||
|
||||
def getlocation(function, curdir: str | None = None) -> str:
|
||||
def getlocation(function, curdir: str | os.PathLike[str] | None = None) -> str:
|
||||
function = get_real_func(function)
|
||||
fn = Path(inspect.getfile(function))
|
||||
lineno = function.__code__.co_firstlineno
|
||||
@@ -127,7 +102,7 @@ def num_mock_patch_args(function) -> int:
|
||||
|
||||
|
||||
def getfuncargnames(
|
||||
function: Callable[..., Any],
|
||||
function: Callable[..., object],
|
||||
*,
|
||||
name: str = "",
|
||||
is_method: bool = False,
|
||||
@@ -338,57 +313,25 @@ def safe_isclass(obj: object) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if sys.version_info >= (3, 8):
|
||||
from typing import final as final
|
||||
else:
|
||||
from typing_extensions import final as final
|
||||
elif sys.version_info >= (3, 8):
|
||||
from typing import final as final
|
||||
else:
|
||||
|
||||
def final(f):
|
||||
return f
|
||||
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from functools import cached_property as cached_property
|
||||
else:
|
||||
|
||||
class cached_property(Generic[_S, _T]):
|
||||
__slots__ = ("func", "__doc__")
|
||||
|
||||
def __init__(self, func: Callable[[_S], _T]) -> None:
|
||||
self.func = func
|
||||
self.__doc__ = func.__doc__
|
||||
|
||||
@overload
|
||||
def __get__(
|
||||
self, instance: None, owner: type[_S] | None = ...
|
||||
) -> cached_property[_S, _T]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __get__(self, instance: _S, owner: type[_S] | None = ...) -> _T:
|
||||
...
|
||||
|
||||
def __get__(self, instance, owner=None):
|
||||
if instance is None:
|
||||
return self
|
||||
value = instance.__dict__[self.func.__name__] = self.func(instance)
|
||||
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 the current process's real user id or None if it could not be
|
||||
determined.
|
||||
|
||||
:return: The user id or None if it could not be determined.
|
||||
"""
|
||||
# mypy follows the version and platform checking expectation of PEP 484:
|
||||
# https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks
|
||||
# Containment checks are too complex for mypy v1.5.0 and cause failure.
|
||||
if sys.platform == "win32" or sys.platform == "emscripten":
|
||||
# win32 does not have a getuid() function.
|
||||
# Emscripten has a return 0 stub.
|
||||
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
|
||||
else:
|
||||
# On other platforms, a return value of -1 is assumed to indicate that
|
||||
# the current process's real user id could not be determined.
|
||||
ERROR = -1
|
||||
uid = os.getuid()
|
||||
return uid if uid != ERROR else None
|
||||
|
||||
|
||||
# Perform exhaustiveness checking.
|
||||
|
||||
@@ -5,6 +5,7 @@ import copy
|
||||
import dataclasses
|
||||
import enum
|
||||
import glob
|
||||
import importlib.metadata
|
||||
import inspect
|
||||
import os
|
||||
import re
|
||||
@@ -21,6 +22,7 @@ from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Dict
|
||||
from typing import final
|
||||
from typing import Generator
|
||||
from typing import IO
|
||||
from typing import Iterable
|
||||
@@ -35,21 +37,23 @@ from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
import pluggy
|
||||
from pluggy import HookimplMarker
|
||||
from pluggy import HookimplOpts
|
||||
from pluggy import HookspecMarker
|
||||
from pluggy import HookspecOpts
|
||||
from pluggy import PluginManager
|
||||
|
||||
import _pytest._code
|
||||
import _pytest.deprecated
|
||||
import _pytest.hookspec
|
||||
from .compat import PathAwareHookProxy
|
||||
from .exceptions import PrintHelp as PrintHelp
|
||||
from .exceptions import UsageError as UsageError
|
||||
from .findpaths import determine_setup
|
||||
from _pytest._code import ExceptionInfo
|
||||
from _pytest._code import filter_traceback
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import importlib_metadata # type: ignore[attr-defined]
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.outcomes import Skipped
|
||||
from _pytest.pathlib import absolutepath
|
||||
@@ -57,6 +61,7 @@ from _pytest.pathlib import bestrelpath
|
||||
from _pytest.pathlib import import_path
|
||||
from _pytest.pathlib import ImportMode
|
||||
from _pytest.pathlib import resolve_package_path
|
||||
from _pytest.pathlib import safe_exists
|
||||
from _pytest.stash import Stash
|
||||
from _pytest.warning_types import PytestConfigWarning
|
||||
from _pytest.warning_types import warn_explicit_for
|
||||
@@ -137,7 +142,9 @@ def main(
|
||||
) -> Union[int, ExitCode]:
|
||||
"""Perform an in-process test run.
|
||||
|
||||
:param args: List of command line arguments.
|
||||
:param args:
|
||||
List of command line arguments. If `None` or not given, defaults to reading
|
||||
arguments directly from the process command line (:data:`sys.argv`).
|
||||
:param plugins: List of plugin objects to be auto-registered during initialization.
|
||||
|
||||
:returns: An exit code.
|
||||
@@ -257,7 +264,8 @@ default_plugins = essential_plugins + (
|
||||
"logging",
|
||||
"reports",
|
||||
"python_path",
|
||||
*(["unraisableexception", "threadexception"] if sys.version_info >= (3, 8) else []),
|
||||
"unraisableexception",
|
||||
"threadexception",
|
||||
"faulthandler",
|
||||
)
|
||||
|
||||
@@ -350,9 +358,9 @@ def _get_legacy_hook_marks(
|
||||
if TYPE_CHECKING:
|
||||
# abuse typeguard from importlib to avoid massive method type union thats lacking a alias
|
||||
assert inspect.isroutine(method)
|
||||
known_marks: set[str] = {m.name for m in getattr(method, "pytestmark", [])}
|
||||
must_warn: list[str] = []
|
||||
opts: dict[str, bool] = {}
|
||||
known_marks: Set[str] = {m.name for m in getattr(method, "pytestmark", [])}
|
||||
must_warn: List[str] = []
|
||||
opts: Dict[str, bool] = {}
|
||||
for opt_name in opt_names:
|
||||
opt_attr = getattr(method, opt_name, AttributeError)
|
||||
if opt_attr is not AttributeError:
|
||||
@@ -437,15 +445,17 @@ class PytestPluginManager(PluginManager):
|
||||
# Used to know when we are importing conftests after the pytest_configure stage.
|
||||
self._configured = False
|
||||
|
||||
def parse_hookimpl_opts(self, plugin: _PluggyPlugin, name: str):
|
||||
def parse_hookimpl_opts(
|
||||
self, plugin: _PluggyPlugin, name: str
|
||||
) -> Optional[HookimplOpts]:
|
||||
# pytest hooks are always prefixed with "pytest_",
|
||||
# so we avoid accessing possibly non-readable attributes
|
||||
# (see issue #1073).
|
||||
if not name.startswith("pytest_"):
|
||||
return
|
||||
return None
|
||||
# Ignore names which can not be hooks.
|
||||
if name == "pytest_plugins":
|
||||
return
|
||||
return None
|
||||
|
||||
opts = super().parse_hookimpl_opts(plugin, name)
|
||||
if opts is not None:
|
||||
@@ -454,18 +464,18 @@ class PytestPluginManager(PluginManager):
|
||||
method = getattr(plugin, name)
|
||||
# Consider only actual functions for hooks (#3775).
|
||||
if not inspect.isroutine(method):
|
||||
return
|
||||
return None
|
||||
# Collect unmarked hooks as long as they have the `pytest_' prefix.
|
||||
return _get_legacy_hook_marks(
|
||||
return _get_legacy_hook_marks( # type: ignore[return-value]
|
||||
method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper")
|
||||
)
|
||||
|
||||
def parse_hookspec_opts(self, module_or_class, name: str):
|
||||
def parse_hookspec_opts(self, module_or_class, name: str) -> Optional[HookspecOpts]:
|
||||
opts = super().parse_hookspec_opts(module_or_class, name)
|
||||
if opts is None:
|
||||
method = getattr(module_or_class, name)
|
||||
if name.startswith("pytest_"):
|
||||
opts = _get_legacy_hook_marks(
|
||||
opts = _get_legacy_hook_marks( # type: ignore[assignment]
|
||||
method,
|
||||
"spec",
|
||||
("firstresult", "historic"),
|
||||
@@ -555,12 +565,8 @@ class PytestPluginManager(PluginManager):
|
||||
anchor = absolutepath(current / path)
|
||||
|
||||
# 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:
|
||||
# is in fact a very long option (#10169, #11394).
|
||||
if safe_exists(anchor):
|
||||
self._try_load_conftest(anchor, importmode, rootpath)
|
||||
foundanchor = True
|
||||
if not foundanchor:
|
||||
@@ -578,26 +584,25 @@ class PytestPluginManager(PluginManager):
|
||||
def _try_load_conftest(
|
||||
self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path
|
||||
) -> None:
|
||||
self._getconftestmodules(anchor, importmode, rootpath)
|
||||
self._loadconftestmodules(anchor, importmode, rootpath)
|
||||
# let's also consider test* subdirs
|
||||
if anchor.is_dir():
|
||||
for x in anchor.glob("test*"):
|
||||
if x.is_dir():
|
||||
self._getconftestmodules(x, importmode, rootpath)
|
||||
self._loadconftestmodules(x, importmode, rootpath)
|
||||
|
||||
def _getconftestmodules(
|
||||
def _loadconftestmodules(
|
||||
self, path: Path, importmode: Union[str, ImportMode], rootpath: Path
|
||||
) -> Sequence[types.ModuleType]:
|
||||
) -> None:
|
||||
if self._noconftest:
|
||||
return []
|
||||
return
|
||||
|
||||
directory = self._get_directory(path)
|
||||
|
||||
# Optimization: avoid repeated searches in the same directory.
|
||||
# Assumes always called with same importmode and rootpath.
|
||||
existing_clist = self._dirpath2confmods.get(directory)
|
||||
if existing_clist is not None:
|
||||
return existing_clist
|
||||
if directory in self._dirpath2confmods:
|
||||
return
|
||||
|
||||
# XXX these days we may rather want to use config.rootpath
|
||||
# and allow users to opt into looking into the rootdir parent
|
||||
@@ -610,16 +615,17 @@ class PytestPluginManager(PluginManager):
|
||||
mod = self._importconftest(conftestpath, importmode, rootpath)
|
||||
clist.append(mod)
|
||||
self._dirpath2confmods[directory] = clist
|
||||
return clist
|
||||
|
||||
def _getconftestmodules(self, path: Path) -> Sequence[types.ModuleType]:
|
||||
directory = self._get_directory(path)
|
||||
return self._dirpath2confmods.get(directory, ())
|
||||
|
||||
def _rget_with_confmod(
|
||||
self,
|
||||
name: str,
|
||||
path: Path,
|
||||
importmode: Union[str, ImportMode],
|
||||
rootpath: Path,
|
||||
) -> Tuple[types.ModuleType, Any]:
|
||||
modules = self._getconftestmodules(path, importmode, rootpath=rootpath)
|
||||
modules = self._getconftestmodules(path)
|
||||
for mod in reversed(modules):
|
||||
try:
|
||||
return mod, getattr(mod, name)
|
||||
@@ -950,7 +956,8 @@ class Config:
|
||||
#: Command line arguments.
|
||||
ARGS = enum.auto()
|
||||
#: Invocation directory.
|
||||
INCOVATION_DIR = enum.auto()
|
||||
INVOCATION_DIR = enum.auto()
|
||||
INCOVATION_DIR = INVOCATION_DIR # backwards compatibility alias
|
||||
#: 'testpaths' configuration value.
|
||||
TESTPATHS = enum.auto()
|
||||
|
||||
@@ -1000,10 +1007,8 @@ class Config:
|
||||
# Deprecated alias. Was never public. Can be removed in a few releases.
|
||||
self._store = self.stash
|
||||
|
||||
from .compat import PathAwareHookProxy
|
||||
|
||||
self.trace = self.pluginmanager.trace.root.get("config")
|
||||
self.hook = PathAwareHookProxy(self.pluginmanager.hook)
|
||||
self.hook: pluggy.HookRelay = PathAwareHookProxy(self.pluginmanager.hook) # type: ignore[assignment]
|
||||
self._inicache: Dict[str, Any] = {}
|
||||
self._override_ini: Sequence[str] = ()
|
||||
self._opt2dest: Dict[str, str] = {}
|
||||
@@ -1063,9 +1068,10 @@ class Config:
|
||||
fin()
|
||||
|
||||
def get_terminal_writer(self) -> TerminalWriter:
|
||||
terminalreporter: TerminalReporter = self.pluginmanager.get_plugin(
|
||||
terminalreporter: Optional[TerminalReporter] = self.pluginmanager.get_plugin(
|
||||
"terminalreporter"
|
||||
)
|
||||
assert terminalreporter is not None
|
||||
return terminalreporter._tw
|
||||
|
||||
def pytest_cmdline_parse(
|
||||
@@ -1167,7 +1173,7 @@ class Config:
|
||||
ns.inifilename,
|
||||
ns.file_or_dir + unknown_args,
|
||||
rootdir_cmd_arg=ns.rootdir or None,
|
||||
config=self,
|
||||
invocation_dir=self.invocation_params.dir,
|
||||
)
|
||||
self._rootpath = rootpath
|
||||
self._inipath = inipath
|
||||
@@ -1216,7 +1222,7 @@ class Config:
|
||||
|
||||
package_files = (
|
||||
str(file)
|
||||
for dist in importlib_metadata.distributions()
|
||||
for dist in importlib.metadata.distributions()
|
||||
if any(ep.group == "pytest11" for ep in dist.entry_points)
|
||||
for file in dist.files or []
|
||||
)
|
||||
@@ -1240,7 +1246,7 @@ class Config:
|
||||
self,
|
||||
*,
|
||||
args: List[str],
|
||||
pyargs: List[str],
|
||||
pyargs: bool,
|
||||
testpaths: List[str],
|
||||
invocation_dir: Path,
|
||||
rootpath: Path,
|
||||
@@ -1275,7 +1281,7 @@ class Config:
|
||||
else:
|
||||
result = []
|
||||
if not result:
|
||||
source = Config.ArgsSource.INCOVATION_DIR
|
||||
source = Config.ArgsSource.INVOCATION_DIR
|
||||
result = [str(invocation_dir)]
|
||||
return result, source
|
||||
|
||||
@@ -1338,12 +1344,14 @@ class Config:
|
||||
else:
|
||||
raise
|
||||
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_collection(self) -> Generator[None, None, None]:
|
||||
@hookimpl(wrapper=True)
|
||||
def pytest_collection(self) -> Generator[None, object, object]:
|
||||
# Validate invalid ini keys after collection is done so we take in account
|
||||
# options added by late-loading conftest files.
|
||||
yield
|
||||
self._validate_config_options()
|
||||
try:
|
||||
return (yield)
|
||||
finally:
|
||||
self._validate_config_options()
|
||||
|
||||
def _checkversion(self) -> None:
|
||||
import pytest
|
||||
@@ -1445,7 +1453,7 @@ class Config:
|
||||
"""Issue and handle a warning during the "configure" stage.
|
||||
|
||||
During ``pytest_configure`` we can't capture warnings using the ``catch_warnings_for_item``
|
||||
function because it is not possible to have hookwrappers around ``pytest_configure``.
|
||||
function because it is not possible to have hook wrappers around ``pytest_configure``.
|
||||
|
||||
This function is mainly intended for plugins that need to issue warnings during
|
||||
``pytest_configure`` (or similar stages).
|
||||
@@ -1557,13 +1565,9 @@ class Config:
|
||||
else:
|
||||
return self._getini_unknown_type(name, type, value)
|
||||
|
||||
def _getconftest_pathlist(
|
||||
self, name: str, path: Path, rootpath: Path
|
||||
) -> Optional[List[Path]]:
|
||||
def _getconftest_pathlist(self, name: str, path: Path) -> Optional[List[Path]]:
|
||||
try:
|
||||
mod, relroots = self.pluginmanager._rget_with_confmod(
|
||||
name, path, self.getoption("importmode"), rootpath
|
||||
)
|
||||
mod, relroots = self.pluginmanager._rget_with_confmod(name, path)
|
||||
except KeyError:
|
||||
return None
|
||||
assert mod.__file__ is not None
|
||||
|
||||
@@ -7,26 +7,23 @@ from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Dict
|
||||
from typing import final
|
||||
from typing import List
|
||||
from typing import Literal
|
||||
from typing import Mapping
|
||||
from typing import NoReturn
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
import _pytest._io
|
||||
from _pytest.compat import final
|
||||
from _pytest.config.exceptions import UsageError
|
||||
from _pytest.deprecated import ARGUMENT_PERCENT_DEFAULT
|
||||
from _pytest.deprecated import ARGUMENT_TYPE_STR
|
||||
from _pytest.deprecated import ARGUMENT_TYPE_STR_CHOICE
|
||||
from _pytest.deprecated import check_ispytest
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Literal
|
||||
|
||||
FILE_OR_DIR = "file_or_dir"
|
||||
|
||||
|
||||
@@ -177,7 +174,7 @@ class Parser:
|
||||
name: str,
|
||||
help: str,
|
||||
type: Optional[
|
||||
"Literal['string', 'paths', 'pathlist', 'args', 'linelist', 'bool']"
|
||||
Literal["string", "paths", "pathlist", "args", "linelist", "bool"]
|
||||
] = None,
|
||||
default: Any = None,
|
||||
) -> None:
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from typing import Mapping
|
||||
|
||||
import pluggy
|
||||
|
||||
from ..compat import LEGACY_PATH
|
||||
from ..compat import legacy_path
|
||||
from ..deprecated import HOOK_LEGACY_PATH_ARG
|
||||
from _pytest.nodes import _check_path
|
||||
|
||||
# hookname: (Path, LEGACY_PATH)
|
||||
imply_paths_hooks = {
|
||||
imply_paths_hooks: Mapping[str, tuple[str, str]] = {
|
||||
"pytest_ignore_collect": ("collection_path", "path"),
|
||||
"pytest_collect_file": ("file_path", "path"),
|
||||
"pytest_pycollect_makemodule": ("module_path", "path"),
|
||||
@@ -18,6 +21,14 @@ imply_paths_hooks = {
|
||||
}
|
||||
|
||||
|
||||
def _check_path(path: Path, fspath: LEGACY_PATH) -> None:
|
||||
if Path(fspath) != path:
|
||||
raise ValueError(
|
||||
f"Path({fspath!r}) != {path!r}\n"
|
||||
"if both path and fspath are given they need to be equal"
|
||||
)
|
||||
|
||||
|
||||
class PathAwareHookProxy:
|
||||
"""
|
||||
this helper wraps around hook callers
|
||||
@@ -27,24 +38,24 @@ class PathAwareHookProxy:
|
||||
this may have to be changed later depending on bugs
|
||||
"""
|
||||
|
||||
def __init__(self, hook_caller):
|
||||
self.__hook_caller = hook_caller
|
||||
def __init__(self, hook_relay: pluggy.HookRelay) -> None:
|
||||
self._hook_relay = hook_relay
|
||||
|
||||
def __dir__(self):
|
||||
return dir(self.__hook_caller)
|
||||
def __dir__(self) -> list[str]:
|
||||
return dir(self._hook_relay)
|
||||
|
||||
def __getattr__(self, key, _wraps=functools.wraps):
|
||||
hook = getattr(self.__hook_caller, key)
|
||||
def __getattr__(self, key: str) -> pluggy.HookCaller:
|
||||
hook: pluggy.HookCaller = getattr(self._hook_relay, key)
|
||||
if key not in imply_paths_hooks:
|
||||
self.__dict__[key] = hook
|
||||
return hook
|
||||
else:
|
||||
path_var, fspath_var = imply_paths_hooks[key]
|
||||
|
||||
@_wraps(hook)
|
||||
@functools.wraps(hook)
|
||||
def fixed_hook(**kw):
|
||||
path_value: Optional[Path] = kw.pop(path_var, None)
|
||||
fspath_value: Optional[LEGACY_PATH] = kw.pop(fspath_var, None)
|
||||
path_value: Path | None = kw.pop(path_var, None)
|
||||
fspath_value: LEGACY_PATH | None = kw.pop(fspath_var, None)
|
||||
if fspath_value is not None:
|
||||
warnings.warn(
|
||||
HOOK_LEGACY_PATH_ARG.format(
|
||||
@@ -65,6 +76,8 @@ class PathAwareHookProxy:
|
||||
kw[fspath_var] = fspath_value
|
||||
return hook(**kw)
|
||||
|
||||
fixed_hook.name = hook.name # type: ignore[attr-defined]
|
||||
fixed_hook.spec = hook.spec # type: ignore[attr-defined]
|
||||
fixed_hook.__name__ = key
|
||||
self.__dict__[key] = fixed_hook
|
||||
return fixed_hook
|
||||
return fixed_hook # type: ignore[return-value]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from _pytest.compat import final
|
||||
from typing import final
|
||||
|
||||
|
||||
@final
|
||||
|
||||
@@ -7,7 +7,6 @@ from typing import List
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
import iniconfig
|
||||
@@ -16,9 +15,7 @@ from .exceptions import UsageError
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.pathlib import absolutepath
|
||||
from _pytest.pathlib import commonpath
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import Config
|
||||
from _pytest.pathlib import safe_exists
|
||||
|
||||
|
||||
def _parse_ini_config(path: Path) -> iniconfig.IniConfig:
|
||||
@@ -151,14 +148,6 @@ def get_dirs_from_args(args: Iterable[str]) -> List[Path]:
|
||||
return path
|
||||
return path.parent
|
||||
|
||||
def safe_exists(path: Path) -> bool:
|
||||
# This can throw on paths that contain characters unrepresentable at the OS level,
|
||||
# or with invalid syntax on Windows (https://bugs.python.org/issue35306)
|
||||
try:
|
||||
return path.exists()
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
# These look like paths but may not exist
|
||||
possible_paths = (
|
||||
absolutepath(get_file_part_from_node_id(arg))
|
||||
@@ -176,8 +165,21 @@ def determine_setup(
|
||||
inifile: Optional[str],
|
||||
args: Sequence[str],
|
||||
rootdir_cmd_arg: Optional[str] = None,
|
||||
config: Optional["Config"] = None,
|
||||
invocation_dir: Optional[Path] = None,
|
||||
) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]:
|
||||
"""Determine the rootdir, inifile and ini configuration values from the
|
||||
command line arguments.
|
||||
|
||||
:param inifile:
|
||||
The `--inifile` command line argument, if given.
|
||||
:param args:
|
||||
The free command line arguments.
|
||||
:param rootdir_cmd_arg:
|
||||
The `--rootdir` command line argument, if given.
|
||||
:param invocation_dir:
|
||||
The working directory when pytest was invoked, if known.
|
||||
If not known, the current working directory is used.
|
||||
"""
|
||||
rootdir = None
|
||||
dirs = get_dirs_from_args(args)
|
||||
if inifile:
|
||||
@@ -198,8 +200,8 @@ def determine_setup(
|
||||
if dirs != [ancestor]:
|
||||
rootdir, inipath, inicfg = locate_config(dirs)
|
||||
if rootdir is None:
|
||||
if config is not None:
|
||||
cwd = config.invocation_params.dir
|
||||
if invocation_dir is not None:
|
||||
cwd = invocation_dir
|
||||
else:
|
||||
cwd = Path.cwd()
|
||||
rootdir = get_common_ancestor([cwd, ancestor])
|
||||
|
||||
@@ -304,10 +304,10 @@ class PdbInvoke:
|
||||
|
||||
|
||||
class PdbTrace:
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, None, None]:
|
||||
@hookimpl(wrapper=True)
|
||||
def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, object, object]:
|
||||
wrap_pytest_function_for_tracing(pyfuncitem)
|
||||
yield
|
||||
return (yield)
|
||||
|
||||
|
||||
def wrap_pytest_function_for_tracing(pyfuncitem):
|
||||
|
||||
@@ -122,6 +122,11 @@ HOOK_LEGACY_MARKING = UnformattedWarning(
|
||||
"#configuring-hook-specs-impls-using-markers",
|
||||
)
|
||||
|
||||
MARKED_FIXTURE = PytestRemovedIn8Warning(
|
||||
"Marks applied to fixtures have no effect\n"
|
||||
"See docs: https://docs.pytest.org/en/stable/deprecations.html#applying-a-mark-to-a-fixture-function"
|
||||
)
|
||||
|
||||
# You want to make some `__init__` or function "private".
|
||||
#
|
||||
# def my_private_function(some, args):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Discover and run doctests in modules and test files."""
|
||||
import bdb
|
||||
import functools
|
||||
import inspect
|
||||
import os
|
||||
import platform
|
||||
@@ -32,7 +33,7 @@ from _pytest.compat import safe_getattr
|
||||
from _pytest.config import Config
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.fixtures import fixture
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
from _pytest.fixtures import TopRequest
|
||||
from _pytest.nodes import Collector
|
||||
from _pytest.nodes import Item
|
||||
from _pytest.outcomes import OutcomeException
|
||||
@@ -254,14 +255,20 @@ class DoctestItem(Item):
|
||||
self,
|
||||
name: str,
|
||||
parent: "Union[DoctestTextfile, DoctestModule]",
|
||||
runner: Optional["doctest.DocTestRunner"] = None,
|
||||
dtest: Optional["doctest.DocTest"] = None,
|
||||
runner: "doctest.DocTestRunner",
|
||||
dtest: "doctest.DocTest",
|
||||
) -> None:
|
||||
super().__init__(name, parent)
|
||||
self.runner = runner
|
||||
self.dtest = dtest
|
||||
|
||||
# Stuff needed for fixture support.
|
||||
self.obj = None
|
||||
self.fixture_request: Optional[FixtureRequest] = None
|
||||
fm = self.session._fixturemanager
|
||||
fixtureinfo = fm.getfixtureinfo(node=self, func=None, cls=None)
|
||||
self._fixtureinfo = fixtureinfo
|
||||
self.fixturenames = fixtureinfo.names_closure
|
||||
self._initrequest()
|
||||
|
||||
@classmethod
|
||||
def from_parent( # type: ignore
|
||||
@@ -276,19 +283,18 @@ class DoctestItem(Item):
|
||||
"""The public named constructor."""
|
||||
return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
|
||||
|
||||
def _initrequest(self) -> None:
|
||||
self.funcargs: Dict[str, object] = {}
|
||||
self._request = TopRequest(self, _ispytest=True) # type: ignore[arg-type]
|
||||
|
||||
def setup(self) -> None:
|
||||
if self.dtest is not None:
|
||||
self.fixture_request = _setup_fixtures(self)
|
||||
globs = dict(getfixture=self.fixture_request.getfixturevalue)
|
||||
for name, value in self.fixture_request.getfixturevalue(
|
||||
"doctest_namespace"
|
||||
).items():
|
||||
globs[name] = value
|
||||
self.dtest.globs.update(globs)
|
||||
self._request._fillfixtures()
|
||||
globs = dict(getfixture=self._request.getfixturevalue)
|
||||
for name, value in self._request.getfixturevalue("doctest_namespace").items():
|
||||
globs[name] = value
|
||||
self.dtest.globs.update(globs)
|
||||
|
||||
def runtest(self) -> None:
|
||||
assert self.dtest is not None
|
||||
assert self.runner is not None
|
||||
_check_all_skipped(self.dtest)
|
||||
self._disable_output_capturing_for_darwin()
|
||||
failures: List["doctest.DocTestFailure"] = []
|
||||
@@ -375,7 +381,6 @@ class DoctestItem(Item):
|
||||
return ReprFailDoctest(reprlocation_lines)
|
||||
|
||||
def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
|
||||
assert self.dtest is not None
|
||||
return self.path, self.dtest.lineno, "[doctest] %s" % self.name
|
||||
|
||||
|
||||
@@ -395,8 +400,8 @@ def _get_flag_lookup() -> Dict[str, int]:
|
||||
)
|
||||
|
||||
|
||||
def get_optionflags(parent):
|
||||
optionflags_str = parent.config.getini("doctest_optionflags")
|
||||
def get_optionflags(config: Config) -> int:
|
||||
optionflags_str = config.getini("doctest_optionflags")
|
||||
flag_lookup_table = _get_flag_lookup()
|
||||
flag_acc = 0
|
||||
for flag in optionflags_str:
|
||||
@@ -404,8 +409,8 @@ def get_optionflags(parent):
|
||||
return flag_acc
|
||||
|
||||
|
||||
def _get_continue_on_failure(config):
|
||||
continue_on_failure = config.getvalue("doctest_continue_on_failure")
|
||||
def _get_continue_on_failure(config: Config) -> bool:
|
||||
continue_on_failure: bool = config.getvalue("doctest_continue_on_failure")
|
||||
if continue_on_failure:
|
||||
# We need to turn off this if we use pdb since we should stop at
|
||||
# the first failure.
|
||||
@@ -428,7 +433,7 @@ class DoctestTextfile(Module):
|
||||
name = self.path.name
|
||||
globs = {"__name__": "__main__"}
|
||||
|
||||
optionflags = get_optionflags(self)
|
||||
optionflags = get_optionflags(self.config)
|
||||
|
||||
runner = _get_runner(
|
||||
verbose=False,
|
||||
@@ -536,6 +541,23 @@ class DoctestModule(Module):
|
||||
tests, obj, name, module, source_lines, globs, seen
|
||||
)
|
||||
|
||||
if sys.version_info < (3, 13):
|
||||
|
||||
def _from_module(self, module, object):
|
||||
"""`cached_property` objects are never considered a part
|
||||
of the 'current module'. As such they are skipped by doctest.
|
||||
Here we override `_from_module` to check the underlying
|
||||
function instead. https://github.com/python/cpython/issues/107995
|
||||
"""
|
||||
if isinstance(object, functools.cached_property):
|
||||
object = object.func
|
||||
|
||||
# Type ignored because this is a private function.
|
||||
return super()._from_module(module, object) # type: ignore[misc]
|
||||
|
||||
else: # pragma: no cover
|
||||
pass
|
||||
|
||||
if self.path.name == "conftest.py":
|
||||
module = self.config.pluginmanager._importconftest(
|
||||
self.path,
|
||||
@@ -556,7 +578,7 @@ class DoctestModule(Module):
|
||||
raise
|
||||
# Uses internal doctest module parsing mechanism.
|
||||
finder = MockAwareDocTestFinder()
|
||||
optionflags = get_optionflags(self)
|
||||
optionflags = get_optionflags(self.config)
|
||||
runner = _get_runner(
|
||||
verbose=False,
|
||||
optionflags=optionflags,
|
||||
@@ -571,22 +593,6 @@ class DoctestModule(Module):
|
||||
)
|
||||
|
||||
|
||||
def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest:
|
||||
"""Used by DoctestTextfile and DoctestItem to setup fixture information."""
|
||||
|
||||
def func() -> None:
|
||||
pass
|
||||
|
||||
doctest_item.funcargs = {} # type: ignore[attr-defined]
|
||||
fm = doctest_item.session._fixturemanager
|
||||
doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined]
|
||||
node=doctest_item, func=func, cls=None, funcargs=False
|
||||
)
|
||||
fixture_request = FixtureRequest(doctest_item, _ispytest=True)
|
||||
fixture_request._fillfixtures()
|
||||
return fixture_request
|
||||
|
||||
|
||||
def _init_checker_class() -> Type["doctest.OutputChecker"]:
|
||||
import doctest
|
||||
import re
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
from typing import Generator
|
||||
@@ -51,7 +50,7 @@ def get_stderr_fileno() -> int:
|
||||
if fileno == -1:
|
||||
raise AttributeError()
|
||||
return fileno
|
||||
except (AttributeError, io.UnsupportedOperation):
|
||||
except (AttributeError, ValueError):
|
||||
# pytest-xdist monkeypatches sys.stderr with an object that is not an actual file.
|
||||
# https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors
|
||||
# This is potentially dangerous, but the best we can do.
|
||||
@@ -62,8 +61,8 @@ def get_timeout_config_value(config: Config) -> float:
|
||||
return float(config.getini("faulthandler_timeout") or 0.0)
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, trylast=True)
|
||||
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
|
||||
@pytest.hookimpl(wrapper=True, trylast=True)
|
||||
def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
|
||||
timeout = get_timeout_config_value(item.config)
|
||||
if timeout > 0:
|
||||
import faulthandler
|
||||
@@ -71,11 +70,11 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
|
||||
stderr = item.config.stash[fault_handler_stderr_fd_key]
|
||||
faulthandler.dump_traceback_later(timeout, file=stderr)
|
||||
try:
|
||||
yield
|
||||
return (yield)
|
||||
finally:
|
||||
faulthandler.cancel_dump_traceback_later()
|
||||
else:
|
||||
yield
|
||||
return (yield)
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user