Compare commits
251 Commits
7.1.0.dev0
...
7.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1dbffcc0b4 | ||
|
|
d53a5fb371 | ||
|
|
d306ec0a7e | ||
|
|
3e4c14bfaa | ||
|
|
7f924b13a5 | ||
|
|
4a8f8ada43 | ||
|
|
c0fd2d8839 | ||
|
|
843e01824c | ||
|
|
bc43d66b47 | ||
|
|
e38d1cac48 | ||
|
|
cf0a4f79b0 | ||
|
|
51b86b4dc4 | ||
|
|
f943d1944a | ||
|
|
9318b2cb7f | ||
|
|
269611e0b4 | ||
|
|
e466a87bb0 | ||
|
|
5f3d94c47e | ||
|
|
bcc826d0fb | ||
|
|
4778e999a5 | ||
|
|
7b6e477e2f | ||
|
|
0c80a1c836 | ||
|
|
7f50c521b6 | ||
|
|
9af3e23695 | ||
|
|
bdbad91493 | ||
|
|
fac8f284cd | ||
|
|
afe41e5273 | ||
|
|
c9cf2d4424 | ||
|
|
f22451717d | ||
|
|
04cf8db1d7 | ||
|
|
747b8372ea | ||
|
|
fc72ffa39e | ||
|
|
3e53307586 | ||
|
|
295e7535c9 | ||
|
|
c3aa4647c7 | ||
|
|
c01a5c177b | ||
|
|
68be319165 | ||
|
|
8afec9a64d | ||
|
|
b79eff065e | ||
|
|
6828ec2f9b | ||
|
|
2a5bb3b4e0 | ||
|
|
eb8a3ef849 | ||
|
|
be0c41bf8e | ||
|
|
9e922c4325 | ||
|
|
2b12739412 | ||
|
|
3586edc1c9 | ||
|
|
88c35123b8 | ||
|
|
6aaa017b1e | ||
|
|
2b6708b892 | ||
|
|
23bdf78654 | ||
|
|
01e1de7a1a | ||
|
|
04a6f52d67 | ||
|
|
4bf764f9a3 | ||
|
|
44290d1f6c | ||
|
|
fb378ea269 | ||
|
|
c326c04494 | ||
|
|
efa16c2c9d | ||
|
|
2442034a1e | ||
|
|
579785b6cd | ||
|
|
b80472c5bc | ||
|
|
ef7d67b665 | ||
|
|
1612d3d1af | ||
|
|
d00ca3f8e5 | ||
|
|
2073cce105 | ||
|
|
d45a19cfde | ||
|
|
f86a87a315 | ||
|
|
66dc79efd4 | ||
|
|
5f1a2f33da | ||
|
|
15ddccf700 | ||
|
|
bc33ba0be9 | ||
|
|
b3692fe404 | ||
|
|
6f936aa97c | ||
|
|
2c3be75b09 | ||
|
|
18c0cfc0de | ||
|
|
20c2c30ff2 | ||
|
|
d1b394af88 | ||
|
|
77a38a3b75 | ||
|
|
57f8f5d2b3 | ||
|
|
b5a168aa0e | ||
|
|
48f01bdcc2 | ||
|
|
dff1a15881 | ||
|
|
dfa7023862 | ||
|
|
2982753d80 | ||
|
|
12b288d84a | ||
|
|
90b1c93f7e | ||
|
|
9d2ffe207b | ||
|
|
3aef0b9de6 | ||
|
|
4b1d9092a8 | ||
|
|
232f44369a | ||
|
|
b2701a0272 | ||
|
|
321747628e | ||
|
|
ee9ddff34e | ||
|
|
6e1445b521 | ||
|
|
597bb9376b | ||
|
|
843f03e3ca | ||
|
|
f064942f2e | ||
|
|
396a7def75 | ||
|
|
cd8bfa94ec | ||
|
|
04bddfc655 | ||
|
|
b21b008118 | ||
|
|
acd2034535 | ||
|
|
52fbf3dbaa | ||
|
|
e6166ccc3c | ||
|
|
5c69eced6c | ||
|
|
a17e708352 | ||
|
|
382e3d346e | ||
|
|
2907252693 | ||
|
|
039c3a201d | ||
|
|
b9fc678770 | ||
|
|
9ae64aae56 | ||
|
|
7783fb1b1e | ||
|
|
00085391fb | ||
|
|
bc296443bd | ||
|
|
dddadefa68 | ||
|
|
aee04cd49f | ||
|
|
bb5a4e2d63 | ||
|
|
471634d6bd | ||
|
|
888026f7a6 | ||
|
|
6d128cd52e | ||
|
|
74571ba55f | ||
|
|
456a2538ac | ||
|
|
3c69bc919c | ||
|
|
d9bcfa0c2b | ||
|
|
8713c32462 | ||
|
|
e9bb1aa233 | ||
|
|
4e5fb520b6 | ||
|
|
6672a10354 | ||
|
|
4c8fb6f0af | ||
|
|
1fd0dcd510 | ||
|
|
3f44b4078c | ||
|
|
c393f95c99 | ||
|
|
fe1f0e5376 | ||
|
|
947b5dbc47 | ||
|
|
a6310c20c1 | ||
|
|
3f4eab3f8f | ||
|
|
2ad1b589af | ||
|
|
e9ed4827a4 | ||
|
|
5c2d752e74 | ||
|
|
0fe0b78a9f | ||
|
|
f1aa7a25de | ||
|
|
202e44b5e6 | ||
|
|
8891d1f449 | ||
|
|
a425f15330 | ||
|
|
610edd156e | ||
|
|
0d5f52b127 | ||
|
|
0995e84adb | ||
|
|
161bc48117 | ||
|
|
0ef882364e | ||
|
|
f08a77de77 | ||
|
|
f0dab8ba8d | ||
|
|
d98b695fec | ||
|
|
ed83efaf4b | ||
|
|
0c98f19231 | ||
|
|
1c7644cc7a | ||
|
|
5f23157b39 | ||
|
|
abe2a8f4e1 | ||
|
|
b19374bc19 | ||
|
|
71baf24b6d | ||
|
|
fa43b8dfb2 | ||
|
|
696f955ff8 | ||
|
|
4038752bf3 | ||
|
|
1860140460 | ||
|
|
b6350b8b97 | ||
|
|
56081ca075 | ||
|
|
d60771f986 | ||
|
|
548cc4fc17 | ||
|
|
819c67f58e | ||
|
|
9a992df3c9 | ||
|
|
b0aabe4081 | ||
|
|
cbccc06302 | ||
|
|
3c8c0d2a33 | ||
|
|
61417b2551 | ||
|
|
325744ef86 | ||
|
|
1fd3601caa | ||
|
|
b9663fed6f | ||
|
|
0b7c3d1145 | ||
|
|
79dbd19780 | ||
|
|
4eebfb2f19 | ||
|
|
755e2509b6 | ||
|
|
e5bf3784a4 | ||
|
|
b72ad0fa8e | ||
|
|
77042f77cc | ||
|
|
400915067f | ||
|
|
3d7cd77017 | ||
|
|
1522afa1cd | ||
|
|
1131f23e04 | ||
|
|
0db1ff0d82 | ||
|
|
69da199f6e | ||
|
|
fcef7e49fd | ||
|
|
55debfad1f | ||
|
|
0da4760715 | ||
|
|
7a42db2bf0 | ||
|
|
7fc2cf51c2 | ||
|
|
5599c5ad45 | ||
|
|
09c0bee288 | ||
|
|
d39780f30b | ||
|
|
427f035b76 | ||
|
|
05d46ca850 | ||
|
|
3128080806 | ||
|
|
10e21dadee | ||
|
|
443aa0219c | ||
|
|
8071ad6b90 | ||
|
|
0fecfff2be | ||
|
|
c69b84f236 | ||
|
|
e358bc65a8 | ||
|
|
a73a9a12a2 | ||
|
|
47df71d23f | ||
|
|
913439f5e5 | ||
|
|
3bbadda0cf | ||
|
|
d8ff487b07 | ||
|
|
a3cf2ad3bc | ||
|
|
8040cfd965 | ||
|
|
ab8c9848a0 | ||
|
|
0e69c62ece | ||
|
|
4d6e8a310b | ||
|
|
37d44434d8 | ||
|
|
0bc77de158 | ||
|
|
0ea039db60 | ||
|
|
0c45065040 | ||
|
|
7cf2b51d8e | ||
|
|
2d7905b13b | ||
|
|
df74e5c532 | ||
|
|
9166ff6cb5 | ||
|
|
8c3b17263b | ||
|
|
a67c547536 | ||
|
|
31f42ef83f | ||
|
|
897395afd5 | ||
|
|
99a8be200a | ||
|
|
c85b14391d | ||
|
|
dd609e150b | ||
|
|
0c8a54ab77 | ||
|
|
c7be96dae4 | ||
|
|
ee93557ef3 | ||
|
|
e05e696fda | ||
|
|
7ae23ff8ae | ||
|
|
3ba9c01f9b | ||
|
|
0f39f11d88 | ||
|
|
4a45a5e983 | ||
|
|
927d9d274f | ||
|
|
41d8fb09ca | ||
|
|
0b0e2d2dbb | ||
|
|
4d7a962ca0 | ||
|
|
b691d31897 | ||
|
|
d4120738b5 | ||
|
|
49278c1df8 | ||
|
|
21a186bbda | ||
|
|
5cb50fa13c | ||
|
|
dc7091502d | ||
|
|
ef76c28ea2 | ||
|
|
dd53cc7e38 | ||
|
|
d7e7c32a5f | ||
|
|
43213add57 |
51
.github/workflows/backport.yml
vendored
Normal file
51
.github/workflows/backport.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: backport
|
||||
|
||||
on:
|
||||
# Note that `pull_request_target` has security implications:
|
||||
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
|
||||
# In particular:
|
||||
# - Only allow triggers that can be used only be trusted users
|
||||
# - Don't execute any code from the target branch
|
||||
# - Don't use cache
|
||||
pull_request_target:
|
||||
types: [labeled]
|
||||
|
||||
# Set permissions at the job level.
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
if: startsWith(github.event.label.name, 'backport ') && github.event.pull_request.merged
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
|
||||
- name: Create backport PR
|
||||
run: |
|
||||
set -eux
|
||||
|
||||
git config --global user.name "pytest bot"
|
||||
git config --global user.email "pytestbot@gmail.com"
|
||||
|
||||
label='${{ github.event.label.name }}'
|
||||
target_branch="${label#backport }"
|
||||
backport_branch=backport-${{ github.event.number }}-to-"${target_branch}"
|
||||
subject="[$target_branch] $(gh pr view --json title -q .title ${{ github.event.number }})"
|
||||
|
||||
git checkout origin/"${target_branch}" -b "${backport_branch}"
|
||||
git cherry-pick -x --mainline 1 ${{ github.event.pull_request.merge_commit_sha }}
|
||||
git commit --amend --message "$subject"
|
||||
git push --set-upstream origin --force-with-lease "${backport_branch}"
|
||||
gh pr create \
|
||||
--base "${target_branch}" \
|
||||
--title "${subject}" \
|
||||
--body "Backport of PR #${{ github.event.number }} to $target_branch branch. PR created by backport workflow."
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
39
.github/workflows/main.yml
vendored
39
.github/workflows/main.yml
vendored
@@ -5,6 +5,7 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- "[0-9]+.[0-9]+.x"
|
||||
- "test-me-*"
|
||||
tags:
|
||||
- "[0-9]+.[0-9]+.[0-9]+"
|
||||
- "[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
|
||||
@@ -23,7 +24,7 @@ permissions: {}
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 45
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -31,24 +32,26 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
name: [
|
||||
"windows-py36",
|
||||
"windows-py37",
|
||||
"windows-py37-pluggy",
|
||||
"windows-py38",
|
||||
"windows-py39",
|
||||
"windows-py310",
|
||||
"windows-py311",
|
||||
|
||||
"ubuntu-py36",
|
||||
"ubuntu-py37",
|
||||
"ubuntu-py37-pluggy",
|
||||
"ubuntu-py37-freeze",
|
||||
"ubuntu-py38",
|
||||
"ubuntu-py39",
|
||||
"ubuntu-py310",
|
||||
"ubuntu-py311",
|
||||
"ubuntu-pypy3",
|
||||
|
||||
"macos-py37",
|
||||
"macos-py38",
|
||||
"macos-py39",
|
||||
"macos-py310",
|
||||
|
||||
"docs",
|
||||
"doctesting",
|
||||
@@ -56,10 +59,6 @@ jobs:
|
||||
]
|
||||
|
||||
include:
|
||||
- name: "windows-py36"
|
||||
python: "3.6"
|
||||
os: windows-latest
|
||||
tox_env: "py36-xdist"
|
||||
- name: "windows-py37"
|
||||
python: "3.7"
|
||||
os: windows-latest
|
||||
@@ -78,14 +77,14 @@ jobs:
|
||||
os: windows-latest
|
||||
tox_env: "py39-xdist"
|
||||
- name: "windows-py310"
|
||||
python: "3.10-dev"
|
||||
python: "3.10"
|
||||
os: windows-latest
|
||||
tox_env: "py310-xdist"
|
||||
- name: "windows-py311"
|
||||
python: "3.11-dev"
|
||||
os: windows-latest
|
||||
tox_env: "py311"
|
||||
|
||||
- name: "ubuntu-py36"
|
||||
python: "3.6"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py36-xdist"
|
||||
- name: "ubuntu-py37"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
@@ -108,9 +107,13 @@ jobs:
|
||||
os: ubuntu-latest
|
||||
tox_env: "py39-xdist"
|
||||
- name: "ubuntu-py310"
|
||||
python: "3.10-dev"
|
||||
python: "3.10"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py310-xdist"
|
||||
- name: "ubuntu-py311"
|
||||
python: "3.11-dev"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py311"
|
||||
- name: "ubuntu-pypy3"
|
||||
python: "pypy-3.7"
|
||||
os: ubuntu-latest
|
||||
@@ -125,9 +128,17 @@ jobs:
|
||||
os: macos-latest
|
||||
tox_env: "py38-xdist"
|
||||
use_coverage: true
|
||||
- name: "macos-py39"
|
||||
python: "3.9"
|
||||
os: macos-latest
|
||||
tox_env: "py39-xdist"
|
||||
- name: "macos-py310"
|
||||
python: "3.10"
|
||||
os: macos-latest
|
||||
tox_env: "py310-xdist"
|
||||
|
||||
- name: "plugins"
|
||||
python: "3.7"
|
||||
python: "3.9"
|
||||
os: ubuntu-latest
|
||||
tox_env: "plugins"
|
||||
|
||||
|
||||
1
.github/workflows/update-plugin-list.yml
vendored
1
.github/workflows/update-plugin-list.yml
vendored
@@ -12,6 +12,7 @@ permissions: {}
|
||||
|
||||
jobs:
|
||||
createPullRequest:
|
||||
if: github.repository_owner == 'pytest-dev'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 21.11b1
|
||||
rev: 22.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
args: [--safe, --quiet]
|
||||
- repo: https://github.com/asottile/blacken-docs
|
||||
rev: v1.12.0
|
||||
rev: v1.12.1
|
||||
hooks:
|
||||
- id: blacken-docs
|
||||
additional_dependencies: [black==20.8b1]
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.0.1
|
||||
rev: v4.1.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
@@ -20,24 +20,32 @@ repos:
|
||||
- id: debug-statements
|
||||
exclude: _pytest/(debugging|hookspec).py
|
||||
language_version: python3
|
||||
- repo: https://github.com/myint/autoflake
|
||||
rev: v1.4
|
||||
hooks:
|
||||
- id: autoflake
|
||||
name: autoflake
|
||||
args: ["--in-place", "--remove-unused-variables", "--remove-all-unused-imports"]
|
||||
language: python
|
||||
files: \.py$
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 4.0.1
|
||||
hooks:
|
||||
- id: flake8
|
||||
language_version: python3
|
||||
additional_dependencies:
|
||||
- flake8-typing-imports==1.9.0
|
||||
- flake8-typing-imports==1.12.0
|
||||
- flake8-docstrings==1.5.0
|
||||
- repo: https://github.com/asottile/reorder_python_imports
|
||||
rev: v2.6.0
|
||||
rev: v2.7.1
|
||||
hooks:
|
||||
- id: reorder-python-imports
|
||||
args: ['--application-directories=.:src', --py36-plus]
|
||||
args: ['--application-directories=.:src', --py37-plus]
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.29.1
|
||||
rev: v2.31.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py36-plus]
|
||||
args: [--py37-plus]
|
||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||
rev: v1.20.0
|
||||
hooks:
|
||||
@@ -48,7 +56,7 @@ repos:
|
||||
hooks:
|
||||
- id: python-use-type-annotations
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v0.910-1
|
||||
rev: v0.931
|
||||
hooks:
|
||||
- id: mypy
|
||||
files: ^(src/|testing/)
|
||||
|
||||
3
AUTHORS
3
AUTHORS
@@ -185,7 +185,9 @@ Katerina Koukiou
|
||||
Keri Volans
|
||||
Kevin Cox
|
||||
Kevin J. Foley
|
||||
Kian-Meng Ang
|
||||
Kodi B. Arfer
|
||||
Kojo Idrissa
|
||||
Kostis Anagnostopoulos
|
||||
Kristoffer Nordström
|
||||
Kyle Altendorf
|
||||
@@ -348,6 +350,7 @@ Xixi Zhao
|
||||
Xuan Luong
|
||||
Xuecong Liao
|
||||
Yoav Caspi
|
||||
Yuval Shimon
|
||||
Zac Hatfield-Dodds
|
||||
Zachary Kneupper
|
||||
Zoltán Máté
|
||||
|
||||
@@ -391,6 +391,13 @@ actual latest release). The procedure for this is:
|
||||
request, as described above. An exception to this is if the bug fix is not
|
||||
applicable to ``main`` anymore.
|
||||
|
||||
Automatic method:
|
||||
|
||||
Add a ``backport 1.2.x`` label to the PR you want to backport. This will create
|
||||
a backport PR against the ``1.2.x`` branch.
|
||||
|
||||
Manual method:
|
||||
|
||||
#. ``git checkout origin/1.2.x -b backport-XXXX`` # use the main PR number here
|
||||
|
||||
#. Locate the merge commit on the PR, in the *merged* message, for example:
|
||||
|
||||
@@ -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.6+ and PyPy3
|
||||
- Python 3.7+ or PyPy3
|
||||
|
||||
- Rich plugin architecture, with over 850+ `external plugins <https://docs.pytest.org/en/latest/reference/plugin_list.html>`_ and thriving community
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ breaking changes or new features.
|
||||
|
||||
For a new minor release, first create a new maintenance branch from ``main``::
|
||||
|
||||
git fetch --all
|
||||
git fetch upstream
|
||||
git branch 7.1.x upstream/main
|
||||
git push upstream 7.1.x
|
||||
|
||||
@@ -63,7 +63,7 @@ Major releases
|
||||
|
||||
1. Create a new maintenance branch from ``main``::
|
||||
|
||||
git fetch --all
|
||||
git fetch upstream
|
||||
git branch 8.0.x upstream/main
|
||||
git push upstream 8.0.x
|
||||
|
||||
@@ -136,29 +136,31 @@ Both automatic and manual processes described above follow the same steps from t
|
||||
#. 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::
|
||||
|
||||
git fetch --all
|
||||
git fetch upstream
|
||||
git tag MAJOR.MINOR.PATCH upstream/release-MAJOR.MINOR.PATCH
|
||||
git push git@github.com:pytest-dev/pytest.git 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>`_.
|
||||
|
||||
#. Merge the PR.
|
||||
#. Merge the PR. **Make sure it's not squash-merged**, so that the tagged commit ends up in the main branch.
|
||||
|
||||
#. Cherry-pick the CHANGELOG / announce files to the ``main`` branch::
|
||||
|
||||
git fetch --all --prune
|
||||
git fetch upstream
|
||||
git checkout upstream/main -b cherry-pick-release
|
||||
git cherry-pick -x -m1 upstream/MAJOR.MINOR.x
|
||||
|
||||
#. Open a PR for ``cherry-pick-release`` and merge it once CI passes. No need to wait for approvals if there were no conflicts on the previous step.
|
||||
|
||||
#. For major and minor releases, tag the release cherry-pick merge commit in main with
|
||||
#. For major and minor releases (or the first prerelease of it), tag the release cherry-pick merge commit in main with
|
||||
a dev tag for the next feature release::
|
||||
|
||||
git checkout main
|
||||
git pull
|
||||
git tag MAJOR.{MINOR+1}.0.dev0
|
||||
git push git@github.com:pytest-dev/pytest.git MAJOR.{MINOR+1}.0.dev0
|
||||
git push upstream MAJOR.{MINOR+1}.0.dev0
|
||||
|
||||
#. For major and minor releases, change the default version in the `Read the Docs Settings <https://readthedocs.org/dashboard/pytest/advanced/>`_ to the new branch.
|
||||
|
||||
#. Send an email announcement with the contents from::
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ Release announcements
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
release-7.1.0
|
||||
release-7.0.1
|
||||
release-7.0.0
|
||||
release-7.0.0rc1
|
||||
release-6.2.5
|
||||
release-6.2.4
|
||||
|
||||
@@ -11,7 +11,7 @@ clear information about the circumstances and a simple example which
|
||||
reproduces the problem.
|
||||
|
||||
The issue tracker is of course not empty now. We have many remaining
|
||||
"enhacement" issues which we'll hopefully can tackle in 2014 with your
|
||||
"enhancement" issues which we'll hopefully can tackle in 2014 with your
|
||||
help.
|
||||
|
||||
For those who use older Python versions, please note that pytest is not
|
||||
|
||||
74
doc/en/announce/release-7.0.0.rst
Normal file
74
doc/en/announce/release-7.0.0.rst
Normal file
@@ -0,0 +1,74 @@
|
||||
pytest-7.0.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 7.0.0 release!
|
||||
|
||||
This release contains new features, improvements, bug fixes, and breaking changes, so users
|
||||
are encouraged to take a look at the CHANGELOG carefully:
|
||||
|
||||
https://docs.pytest.org/en/stable/changelog.html
|
||||
|
||||
For complete documentation, please visit:
|
||||
|
||||
https://docs.pytest.org/en/stable/
|
||||
|
||||
As usual, you can upgrade from PyPI via:
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Adam J. Stewart
|
||||
* Alexander King
|
||||
* Amin Alaee
|
||||
* Andrew Neitsch
|
||||
* Anthony Sottile
|
||||
* Ben Davies
|
||||
* Bernát Gábor
|
||||
* Brian Okken
|
||||
* Bruno Oliveira
|
||||
* Cristian Vera
|
||||
* Dan Alvizu
|
||||
* David Szotten
|
||||
* Eddie
|
||||
* Emmanuel Arias
|
||||
* Emmanuel Meric de Bellefon
|
||||
* Eric Liu
|
||||
* Florian Bruhin
|
||||
* GergelyKalmar
|
||||
* Graeme Smecher
|
||||
* Harshna
|
||||
* Hugo van Kemenade
|
||||
* Jakub Kulík
|
||||
* James Myatt
|
||||
* Jeff Rasley
|
||||
* Kale Kundert
|
||||
* Kian Meng, Ang
|
||||
* Miro Hrončok
|
||||
* Naveen-Pratap
|
||||
* Oleg Höfling
|
||||
* Olga Matoula
|
||||
* Ran Benita
|
||||
* Ronny Pfannschmidt
|
||||
* Simon K
|
||||
* Srip
|
||||
* Sören Wegener
|
||||
* Taneli Hukkinen
|
||||
* Terje Runde
|
||||
* Thomas Grainger
|
||||
* Thomas Hisch
|
||||
* William Jamir Silva
|
||||
* Yuval Shimon
|
||||
* Zac Hatfield-Dodds
|
||||
* andrewdotn
|
||||
* denivyruck
|
||||
* ericluoliu
|
||||
* oleg.hoefling
|
||||
* symonk
|
||||
* ziebam
|
||||
* Éloi Rivard
|
||||
* Éric
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
@@ -19,7 +19,7 @@ You can upgrade from PyPI via:
|
||||
|
||||
Users are encouraged to take a look at the CHANGELOG carefully:
|
||||
|
||||
https://docs.pytest.org/en/stable/changelog.html
|
||||
https://docs.pytest.org/en/7.0.x/changelog.html
|
||||
|
||||
Thanks to all the contributors to this release:
|
||||
|
||||
|
||||
20
doc/en/announce/release-7.0.1.rst
Normal file
20
doc/en/announce/release-7.0.1.rst
Normal file
@@ -0,0 +1,20 @@
|
||||
pytest-7.0.1
|
||||
=======================================
|
||||
|
||||
pytest 7.0.1 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* Ran Benita
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
48
doc/en/announce/release-7.1.0.rst
Normal file
48
doc/en/announce/release-7.1.0.rst
Normal file
@@ -0,0 +1,48 @@
|
||||
pytest-7.1.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 7.1.0 release!
|
||||
|
||||
This release contains new features, improvements, and bug fixes,
|
||||
the full list of changes is available in the changelog:
|
||||
|
||||
https://docs.pytest.org/en/stable/changelog.html
|
||||
|
||||
For complete documentation, please visit:
|
||||
|
||||
https://docs.pytest.org/en/stable/
|
||||
|
||||
As usual, you can upgrade from PyPI via:
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Akuli
|
||||
* Andrew Svetlov
|
||||
* Anthony Sottile
|
||||
* Brett Holman
|
||||
* Bruno Oliveira
|
||||
* Chris NeJame
|
||||
* Dan Alvizu
|
||||
* Elijah DeLee
|
||||
* Emmanuel Arias
|
||||
* Fabian Egli
|
||||
* Florian Bruhin
|
||||
* Gabor Szabo
|
||||
* Hasan Ramezani
|
||||
* Hugo van Kemenade
|
||||
* Kian Meng, Ang
|
||||
* Kojo Idrissa
|
||||
* Masaru Tsuchiyama
|
||||
* Olga Matoula
|
||||
* P. L. Lim
|
||||
* Ran Benita
|
||||
* Tobias Deiminger
|
||||
* Yuval Shimon
|
||||
* eduardo naufel schettino
|
||||
* Éric
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
@@ -65,7 +65,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
Fixture that returns a :py:class:`dict` that will be injected into the
|
||||
namespace of doctests.
|
||||
|
||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1365
|
||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1334
|
||||
Session-scoped fixture that returns the session's :class:`pytest.Config`
|
||||
object.
|
||||
|
||||
@@ -117,10 +117,10 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
`pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See
|
||||
:issue:`7767` for details.
|
||||
|
||||
tmpdir_factory [session scope] -- .../_pytest/legacypath.py:292
|
||||
tmpdir_factory [session scope] -- .../_pytest/legacypath.py:295
|
||||
Return a :class:`pytest.TempdirFactory` instance for the test session.
|
||||
|
||||
tmpdir -- .../_pytest/legacypath.py:299
|
||||
tmpdir -- .../_pytest/legacypath.py:302
|
||||
Return a temporary directory path object which is unique to each test
|
||||
function invocation, created as a sub directory of the base temporary
|
||||
directory.
|
||||
@@ -134,7 +134,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
|
||||
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html
|
||||
|
||||
caplog -- .../_pytest/logging.py:483
|
||||
caplog -- .../_pytest/logging.py:487
|
||||
Access and control log capturing.
|
||||
|
||||
Captured logs are available through the following properties/methods::
|
||||
|
||||
@@ -28,6 +28,167 @@ with advance notice in the **Deprecations** section of releases.
|
||||
|
||||
.. towncrier release notes start
|
||||
|
||||
pytest 7.1.0 (2022-03-13)
|
||||
=========================
|
||||
|
||||
Breaking Changes
|
||||
----------------
|
||||
|
||||
- `#8838 <https://github.com/pytest-dev/pytest/issues/8838>`_: As per our policy, the following features have been deprecated in the 6.X series and are now
|
||||
removed:
|
||||
|
||||
* ``pytest._fillfuncargs`` function.
|
||||
|
||||
* ``pytest_warning_captured`` hook - use ``pytest_warning_recorded`` instead.
|
||||
|
||||
* ``-k -foobar`` syntax - use ``-k 'not foobar'`` instead.
|
||||
|
||||
* ``-k foobar:`` syntax.
|
||||
|
||||
* ``pytest.collect`` module - import from ``pytest`` directly.
|
||||
|
||||
For more information consult
|
||||
`Deprecations and Removals <https://docs.pytest.org/en/latest/deprecations.html>`__ in the docs.
|
||||
|
||||
|
||||
- `#9437 <https://github.com/pytest-dev/pytest/issues/9437>`_: Dropped support for Python 3.6, which reached `end-of-life <https://devguide.python.org/#status-of-python-branches>`__ at 2021-12-23.
|
||||
|
||||
|
||||
|
||||
Improvements
|
||||
------------
|
||||
|
||||
- `#5192 <https://github.com/pytest-dev/pytest/issues/5192>`_: Fixed test output for some data types where ``-v`` would show less information.
|
||||
|
||||
Also, when showing diffs for sequences, ``-q`` would produce full diffs instead of the expected diff.
|
||||
|
||||
|
||||
- `#9362 <https://github.com/pytest-dev/pytest/issues/9362>`_: pytest now avoids specialized assert formatting when it is detected that the default ``__eq__`` is overridden in ``attrs`` or ``dataclasses``.
|
||||
|
||||
|
||||
- `#9536 <https://github.com/pytest-dev/pytest/issues/9536>`_: When ``-vv`` is given on command line, show skipping and xfail reasons in full instead of truncating them to fit the terminal width.
|
||||
|
||||
|
||||
- `#9644 <https://github.com/pytest-dev/pytest/issues/9644>`_: More information about the location of resources that led Python to raise :class:`ResourceWarning` can now
|
||||
be obtained by enabling :mod:`tracemalloc`.
|
||||
|
||||
See :ref:`resource-warnings` for more information.
|
||||
|
||||
|
||||
- `#9678 <https://github.com/pytest-dev/pytest/issues/9678>`_: More types are now accepted in the ``ids`` argument to ``@pytest.mark.parametrize``.
|
||||
Previously only `str`, `float`, `int` and `bool` were accepted;
|
||||
now `bytes`, `complex`, `re.Pattern`, `Enum` and anything with a `__name__` are also accepted.
|
||||
|
||||
|
||||
- `#9692 <https://github.com/pytest-dev/pytest/issues/9692>`_: :func:`pytest.approx` now raises a :class:`TypeError` when given an unordered sequence (such as :class:`set`).
|
||||
|
||||
Note that this implies that custom classes which only implement ``__iter__`` and ``__len__`` are no longer supported as they don't guarantee order.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#8242 <https://github.com/pytest-dev/pytest/issues/8242>`_: The deprecation of raising :class:`unittest.SkipTest` to skip collection of
|
||||
tests during the pytest collection phase is reverted - this is now a supported
|
||||
feature again.
|
||||
|
||||
|
||||
- `#9493 <https://github.com/pytest-dev/pytest/issues/9493>`_: Symbolic link components are no longer resolved in conftest paths.
|
||||
This means that if a conftest appears twice in collection tree, using symlinks, it will be executed twice.
|
||||
For example, given
|
||||
|
||||
tests/real/conftest.py
|
||||
tests/real/test_it.py
|
||||
tests/link -> tests/real
|
||||
|
||||
running ``pytest tests`` now imports the conftest twice, once as ``tests/real/conftest.py`` and once as ``tests/link/conftest.py``.
|
||||
This is a fix to match a similar change made to test collection itself in pytest 6.0 (see :pull:`6523` for details).
|
||||
|
||||
|
||||
- `#9626 <https://github.com/pytest-dev/pytest/issues/9626>`_: Fixed count of selected tests on terminal collection summary when there were errors or skipped modules.
|
||||
|
||||
If there were errors or skipped modules on collection, pytest would mistakenly subtract those from the selected count.
|
||||
|
||||
|
||||
- `#9645 <https://github.com/pytest-dev/pytest/issues/9645>`_: Fixed regression where ``--import-mode=importlib`` used together with :envvar:`PYTHONPATH` or :confval:`pythonpath` would cause import errors in test suites.
|
||||
|
||||
|
||||
- `#9708 <https://github.com/pytest-dev/pytest/issues/9708>`_: :fixture:`pytester` now requests a :fixture:`monkeypatch` fixture instead of creating one internally. This solves some issues with tests that involve pytest environment variables.
|
||||
|
||||
|
||||
- `#9730 <https://github.com/pytest-dev/pytest/issues/9730>`_: Malformed ``pyproject.toml`` files now produce a clearer error message.
|
||||
|
||||
|
||||
pytest 7.0.1 (2022-02-11)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#9608 <https://github.com/pytest-dev/pytest/issues/9608>`_: Fix invalid importing of ``importlib.readers`` in Python 3.9.
|
||||
|
||||
|
||||
- `#9610 <https://github.com/pytest-dev/pytest/issues/9610>`_: Restore `UnitTestFunction.obj` to return unbound rather than bound method.
|
||||
Fixes a crash during a failed teardown in unittest TestCases with non-default `__init__`.
|
||||
Regressed in pytest 7.0.0.
|
||||
|
||||
|
||||
- `#9636 <https://github.com/pytest-dev/pytest/issues/9636>`_: The ``pythonpath`` plugin was renamed to ``python_path``. This avoids a conflict with the ``pytest-pythonpath`` plugin.
|
||||
|
||||
|
||||
- `#9642 <https://github.com/pytest-dev/pytest/issues/9642>`_: Fix running tests by id with ``::`` in the parametrize portion.
|
||||
|
||||
|
||||
- `#9643 <https://github.com/pytest-dev/pytest/issues/9643>`_: Delay issuing a :class:`~pytest.PytestWarning` about diamond inheritance involving :class:`~pytest.Item` and
|
||||
:class:`~pytest.Collector` so it can be filtered using :ref:`standard warning filters <warnings>`.
|
||||
|
||||
|
||||
pytest 7.0.0 (2022-02-03)
|
||||
=========================
|
||||
|
||||
(**Please see the full set of changes for this release also in the 7.0.0rc1 notes below**)
|
||||
|
||||
Deprecations
|
||||
------------
|
||||
|
||||
- `#9488 <https://github.com/pytest-dev/pytest/issues/9488>`_: If custom subclasses of nodes like :class:`pytest.Item` override the
|
||||
``__init__`` method, they should take ``**kwargs``. See
|
||||
:ref:`uncooperative-constructors-deprecated` for details.
|
||||
|
||||
Note that a deprection warning is only emitted when there is a conflict in the
|
||||
arguments pytest expected to pass. This deprecation was already part of pytest
|
||||
7.0.0rc1 but wasn't documented.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#9355 <https://github.com/pytest-dev/pytest/issues/9355>`_: Fixed error message prints function decorators when using assert in Python 3.8 and above.
|
||||
|
||||
|
||||
- `#9396 <https://github.com/pytest-dev/pytest/issues/9396>`_: Ensure :attr:`pytest.Config.inifile` is available during the :func:`pytest_cmdline_main <_pytest.hookspec.pytest_cmdline_main>` hook (regression during ``7.0.0rc1``).
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#9404 <https://github.com/pytest-dev/pytest/issues/9404>`_: Added extra documentation on alternatives to common misuses of `pytest.warns(None)` ahead of its deprecation.
|
||||
|
||||
|
||||
- `#9505 <https://github.com/pytest-dev/pytest/issues/9505>`_: Clarify where the configuration files are located. To avoid confusions documentation mentions
|
||||
that configuration file is located in the root of the repository.
|
||||
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#9521 <https://github.com/pytest-dev/pytest/issues/9521>`_: Add test coverage to assertion rewrite path.
|
||||
|
||||
|
||||
pytest 7.0.0rc1 (2021-12-06)
|
||||
============================
|
||||
|
||||
@@ -142,6 +303,8 @@ Deprecations
|
||||
:class:`unittest.SkipTest` / :meth:`unittest.TestCase.skipTest` /
|
||||
:func:`unittest.skip` in unittest test cases is fully supported.
|
||||
|
||||
.. note:: This deprecation has been reverted in pytest 7.1.0.
|
||||
|
||||
|
||||
- `#8315 <https://github.com/pytest-dev/pytest/issues/8315>`_: Several behaviors of :meth:`Parser.addoption <pytest.Parser.addoption>` are now
|
||||
scheduled for removal in pytest 8 (deprecated since pytest 2.4.0):
|
||||
@@ -156,7 +319,7 @@ Deprecations
|
||||
See :ref:`the deprecation note <diamond-inheritance-deprecated>` for full details.
|
||||
|
||||
|
||||
- `#8592 <https://github.com/pytest-dev/pytest/issues/8592>`_: :func:`pytest_cmdline_preparse <_pytest.hookspec.pytest_cmdline_preparse>` has been officially deprecated. It will be removed in a future release. Use :func:`pytest_load_initial_conftests <_pytest.hookspec.pytest_load_initial_conftests>` instead.
|
||||
- `#8592 <https://github.com/pytest-dev/pytest/issues/8592>`_: :hook:`pytest_cmdline_preparse` has been officially deprecated. It will be removed in a future release. Use :hook:`pytest_load_initial_conftests` instead.
|
||||
|
||||
See :ref:`the deprecation note <cmdline-preparse-deprecated>` for full details.
|
||||
|
||||
@@ -173,6 +336,12 @@ Deprecations
|
||||
This was changed for consistency with :func:`pytest.mark.skip <pytest.mark.skip>` and :func:`pytest.mark.xfail <pytest.mark.xfail>` which both accept
|
||||
``reason`` as an argument.
|
||||
|
||||
- `#8174 <https://github.com/pytest-dev/pytest/issues/8174>`_: The following changes have been made to types reachable through :attr:`pytest.ExceptionInfo.traceback`:
|
||||
|
||||
- The ``path`` property of ``_pytest.code.Code`` returns ``Path`` instead of ``py.path.local``.
|
||||
- The ``path`` property of ``_pytest.code.TracebackEntry`` returns ``Path`` instead of ``py.path.local``.
|
||||
|
||||
There was no deprecation period for this change (sorry!).
|
||||
|
||||
|
||||
Features
|
||||
@@ -203,11 +372,11 @@ Features
|
||||
- ``pytest.Mark`` for :class:`marks <pytest.Mark>`.
|
||||
- ``pytest.MarkDecorator`` for :class:`mark decorators <pytest.MarkDecorator>`.
|
||||
- ``pytest.MarkGenerator`` for the :class:`pytest.mark <pytest.MarkGenerator>` singleton.
|
||||
- ``pytest.Metafunc`` for the :class:`metafunc <pytest.MarkGenerator>` argument to the :func:`pytest_generate_tests <pytest.hookspec.pytest_generate_tests>` hook.
|
||||
- ``pytest.Metafunc`` for the :class:`metafunc <pytest.MarkGenerator>` argument to the :hook:`pytest_generate_tests` hook.
|
||||
- ``pytest.CallInfo`` for the :class:`CallInfo <pytest.CallInfo>` type passed to various hooks.
|
||||
- ``pytest.PytestPluginManager`` for :class:`PytestPluginManager <pytest.PytestPluginManager>`.
|
||||
- ``pytest.ExceptionInfo`` for the :class:`ExceptionInfo <pytest.ExceptionInfo>` type returned from :func:`pytest.raises` and passed to various hooks.
|
||||
- ``pytest.Parser`` for the :class:`Parser <pytest.Parser>` type passed to the :func:`pytest_addoption <pytest.hookspec.pytest_addoption>` hook.
|
||||
- ``pytest.Parser`` for the :class:`Parser <pytest.Parser>` type passed to the :hook:`pytest_addoption` hook.
|
||||
- ``pytest.OptionGroup`` for the :class:`OptionGroup <pytest.OptionGroup>` type returned from the :func:`parser.addgroup <pytest.Parser.getgroup>` method.
|
||||
- ``pytest.HookRecorder`` for the :class:`HookRecorder <pytest.HookRecorder>` type returned from :class:`~pytest.Pytester`.
|
||||
- ``pytest.RecordedHookCall`` for the :class:`RecordedHookCall <pytest.HookRecorder>` type returned from :class:`~pytest.HookRecorder`.
|
||||
@@ -228,11 +397,11 @@ Features
|
||||
|
||||
- `#8144 <https://github.com/pytest-dev/pytest/issues/8144>`_: The following hooks now receive an additional ``pathlib.Path`` argument, equivalent to an existing ``py.path.local`` argument:
|
||||
|
||||
- :func:`pytest_ignore_collect <_pytest.hookspec.pytest_ignore_collect>` - The ``collection_path`` parameter (equivalent to existing ``path`` parameter).
|
||||
- :func:`pytest_collect_file <_pytest.hookspec.pytest_collect_file>` - The ``file_path`` parameter (equivalent to existing ``path`` parameter).
|
||||
- :func:`pytest_pycollect_makemodule <_pytest.hookspec.pytest_pycollect_makemodule>` - The ``module_path`` parameter (equivalent to existing ``path`` parameter).
|
||||
- :func:`pytest_report_header <_pytest.hookspec.pytest_report_header>` - The ``start_path`` parameter (equivalent to existing ``startdir`` parameter).
|
||||
- :func:`pytest_report_collectionfinish <_pytest.hookspec.pytest_report_collectionfinish>` - The ``start_path`` parameter (equivalent to existing ``startdir`` parameter).
|
||||
- :hook:`pytest_ignore_collect` - The ``collection_path`` parameter (equivalent to existing ``path`` parameter).
|
||||
- :hook:`pytest_collect_file` - The ``file_path`` parameter (equivalent to existing ``path`` parameter).
|
||||
- :hook:`pytest_pycollect_makemodule` - The ``module_path`` parameter (equivalent to existing ``path`` parameter).
|
||||
- :hook:`pytest_report_header` - The ``start_path`` parameter (equivalent to existing ``startdir`` parameter).
|
||||
- :hook:`pytest_report_collectionfinish` - The ``start_path`` parameter (equivalent to existing ``startdir`` parameter).
|
||||
|
||||
.. note::
|
||||
The name of the :class:`~_pytest.nodes.Node` arguments and attributes (the
|
||||
@@ -473,10 +642,9 @@ Trivial/Internal Changes
|
||||
|
||||
- `#8174 <https://github.com/pytest-dev/pytest/issues/8174>`_: The following changes have been made to internal pytest types/functions:
|
||||
|
||||
- The ``path`` property of ``_pytest.code.Code`` returns ``Path`` instead of ``py.path.local``.
|
||||
- The ``path`` property of ``_pytest.code.TracebackEntry`` returns ``Path`` instead of ``py.path.local``.
|
||||
- The ``_pytest.code.getfslineno()`` function returns ``Path`` instead of ``py.path.local``.
|
||||
- The ``_pytest.python.path_matches_patterns()`` function takes ``Path`` instead of ``py.path.local``.
|
||||
- The ``_pytest._code.Traceback.cut()`` function accepts any ``os.PathLike[str]``, not just ``py.path.local``.
|
||||
|
||||
|
||||
- `#8248 <https://github.com/pytest-dev/pytest/issues/8248>`_: Internal Restructure: let ``python.PyObjMixin`` inherit from ``nodes.Node`` to carry over typing information.
|
||||
@@ -491,7 +659,7 @@ Trivial/Internal Changes
|
||||
- `#8913 <https://github.com/pytest-dev/pytest/issues/8913>`_: The private ``CallSpec2._arg2scopenum`` attribute has been removed after an internal refactoring.
|
||||
|
||||
|
||||
- `#8967 <https://github.com/pytest-dev/pytest/issues/8967>`_: :func:`pytest_assertion_pass <_pytest.hookspec.pytest_assertion_pass>` is no longer considered experimental and
|
||||
- `#8967 <https://github.com/pytest-dev/pytest/issues/8967>`_: :hook:`pytest_assertion_pass` is no longer considered experimental and
|
||||
future changes to it will be considered more carefully.
|
||||
|
||||
|
||||
@@ -538,7 +706,7 @@ Bug Fixes
|
||||
the ``tmp_path``/``tmpdir`` fixture). Now the directories are created with
|
||||
private permissions.
|
||||
|
||||
pytest used to silenty use a pre-existing ``/tmp/pytest-of-<username>`` directory,
|
||||
pytest used to silently use a pre-existing ``/tmp/pytest-of-<username>`` directory,
|
||||
even if owned by another user. This means another user could pre-create such a
|
||||
directory and gain control of another user's temporary directory. Now such a
|
||||
condition results in an error.
|
||||
@@ -853,8 +1021,8 @@ Deprecations
|
||||
if you use this and want a replacement.
|
||||
|
||||
|
||||
- :issue:`7255`: The :func:`pytest_warning_captured <_pytest.hookspec.pytest_warning_captured>` hook is deprecated in favor
|
||||
of :func:`pytest_warning_recorded <_pytest.hookspec.pytest_warning_recorded>`, and will be removed in a future version.
|
||||
- :issue:`7255`: The :hook:`pytest_warning_captured` hook is deprecated in favor
|
||||
of :hook:`pytest_warning_recorded`, and will be removed in a future version.
|
||||
|
||||
|
||||
- :issue:`7648`: The ``gethookproxy()`` and ``isinitpath()`` methods of ``FSCollector`` and ``Package`` are deprecated;
|
||||
@@ -962,7 +1130,7 @@ Trivial/Internal Changes
|
||||
process, pytest now ignores builtin attributes (like ``__class__``,
|
||||
``__delattr__`` and ``__new__``) without consulting the :confval:`python_classes` and
|
||||
:confval:`python_functions` configuration options and without passing them to plugins
|
||||
using the :func:`pytest_pycollect_makeitem <_pytest.hookspec.pytest_pycollect_makeitem>` hook.
|
||||
using the :hook:`pytest_pycollect_makeitem` hook.
|
||||
|
||||
|
||||
pytest 6.0.2 (2020-09-04)
|
||||
@@ -1304,7 +1472,7 @@ Improvements
|
||||
is not displayed by default for passing tests. This change makes the mistake
|
||||
visible during testing.
|
||||
|
||||
You may supress this behavior temporarily or permanently by setting
|
||||
You may suppress this behavior temporarily or permanently by setting
|
||||
``logging.raiseExceptions = False``.
|
||||
|
||||
|
||||
@@ -1751,7 +1919,7 @@ Bug Fixes
|
||||
- :issue:`6646`: Assertion rewriting hooks are (re)stored for the current item, which fixes them being still used after e.g. pytester's :func:`testdir.runpytest <_pytest.pytester.Testdir.runpytest>` etc.
|
||||
|
||||
|
||||
- :issue:`6660`: :py:func:`pytest.exit` is handled when emitted from the :func:`pytest_sessionfinish <_pytest.hookspec.pytest_sessionfinish>` hook. This includes quitting from a debugger.
|
||||
- :issue:`6660`: :py:func:`pytest.exit` is handled when emitted from the :hook:`pytest_sessionfinish` hook. This includes quitting from a debugger.
|
||||
|
||||
|
||||
- :issue:`6752`: When :py:func:`pytest.raises` is used as a function (as opposed to a context manager),
|
||||
@@ -1759,7 +1927,7 @@ Bug Fixes
|
||||
it was swallowed and ignored (regression in pytest 5.1.0).
|
||||
|
||||
|
||||
- :issue:`6801`: Do not display empty lines inbetween traceback for unexpected exceptions with doctests.
|
||||
- :issue:`6801`: Do not display empty lines in between traceback for unexpected exceptions with doctests.
|
||||
|
||||
|
||||
- :issue:`6802`: The :fixture:`testdir fixture <testdir>` works within doctests now.
|
||||
@@ -1863,7 +2031,7 @@ Improvements
|
||||
- :issue:`6231`: Improve check for misspelling of :ref:`pytest.mark.parametrize ref`.
|
||||
|
||||
|
||||
- :issue:`6257`: Handle :py:func:`pytest.exit` being used via :py:func:`~_pytest.hookspec.pytest_internalerror`, e.g. when quitting pdb from post mortem.
|
||||
- :issue:`6257`: Handle :func:`pytest.exit` being used via :hook:`pytest_internalerror`, e.g. when quitting pdb from post mortem.
|
||||
|
||||
|
||||
|
||||
@@ -2497,7 +2665,7 @@ Deprecations
|
||||
Features
|
||||
--------
|
||||
|
||||
- :issue:`3457`: New :func:`~_pytest.hookspec.pytest_assertion_pass`
|
||||
- :issue:`3457`: New :hook:`pytest_assertion_pass`
|
||||
hook, called with context information when an assertion *passes*.
|
||||
|
||||
This hook is still **experimental** so use it with caution.
|
||||
@@ -4628,7 +4796,7 @@ Bug Fixes
|
||||
- Fixed a bug where stdout and stderr were logged twice by junitxml when a test
|
||||
was marked xfail. (:issue:`3491`)
|
||||
|
||||
- Fix ``usefixtures`` mark applyed to unittest tests by correctly instantiating
|
||||
- Fix ``usefixtures`` mark applied to unittest tests by correctly instantiating
|
||||
``FixtureInfo``. (:issue:`3498`)
|
||||
|
||||
- Fix assertion rewriter compatibility with libraries that monkey patch
|
||||
@@ -5017,9 +5185,9 @@ Features
|
||||
- Console output falls back to "classic" mode when capturing is disabled (``-s``),
|
||||
otherwise the output gets garbled to the point of being useless. (:issue:`3038`)
|
||||
|
||||
- New :func:`~_pytest.hookspec.pytest_runtest_logfinish`
|
||||
- New :hook:`pytest_runtest_logfinish`
|
||||
hook which is called when a test item has finished executing, analogous to
|
||||
:func:`~_pytest.hookspec.pytest_runtest_logstart`.
|
||||
:hook:`pytest_runtest_logstart`.
|
||||
(:issue:`3101`)
|
||||
|
||||
- Improve performance when collecting tests using many fixtures. (:issue:`3107`)
|
||||
@@ -8009,7 +8177,7 @@ Bug fixes:
|
||||
or through plugin hooks. Also introduce a "--strict" option which
|
||||
will treat unregistered markers as errors
|
||||
allowing to avoid typos and maintain a well described set of markers
|
||||
for your test suite. See exaples at http://pytest.org/en/stable/how-to/mark.html
|
||||
for your test suite. See examples at http://pytest.org/en/stable/how-to/mark.html
|
||||
and its links.
|
||||
- issue50: introduce "-m marker" option to select tests based on markers
|
||||
(this is a stricter and more predictable version of '-k' in that "-m"
|
||||
@@ -8321,7 +8489,7 @@ Bug fixes:
|
||||
- fix issue57 -f|--looponfail to work with xpassing tests (thanks Ronny)
|
||||
- fix issue92 collectonly reporter and --pastebin (thanks Benjamin Peterson)
|
||||
- fix py.code.compile(source) to generate unique filenames
|
||||
- fix assertion re-interp problems on PyPy, by defering code
|
||||
- fix assertion re-interp problems on PyPy, by deferring code
|
||||
compilation to the (overridable) Frame.eval class. (thanks Amaury Forgeot)
|
||||
- fix py.path.local.pyimport() to work with directories
|
||||
- streamline py.path.local.mkdtemp implementation and usage
|
||||
@@ -8395,7 +8563,7 @@ Bug fixes:
|
||||
- improve support for raises and other dynamically compiled code by
|
||||
manipulating python's linecache.cache instead of the previous
|
||||
rather hacky way of creating custom code objects. This makes
|
||||
it seemlessly work on Jython and PyPy where it previously didn't.
|
||||
it seamlessly work on Jython and PyPy where it previously didn't.
|
||||
|
||||
- fix issue96: make capturing more resilient against Control-C
|
||||
interruptions (involved somewhat substantial refactoring
|
||||
|
||||
@@ -382,7 +382,6 @@ texinfo_documents = [
|
||||
]
|
||||
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {
|
||||
"pluggy": ("https://pluggy.readthedocs.io/en/stable", None),
|
||||
"python": ("https://docs.python.org/3", None),
|
||||
@@ -390,10 +389,6 @@ intersphinx_mapping = {
|
||||
"pip": ("https://pip.pypa.io/en/stable", None),
|
||||
"tox": ("https://tox.wiki/en/stable", None),
|
||||
"virtualenv": ("https://virtualenv.pypa.io/en/stable", None),
|
||||
"django": (
|
||||
"http://docs.djangoproject.com/en/stable",
|
||||
"http://docs.djangoproject.com/en/stable/_objects",
|
||||
),
|
||||
"setuptools": ("https://setuptools.pypa.io/en/stable", None),
|
||||
}
|
||||
|
||||
@@ -445,6 +440,13 @@ def setup(app: "sphinx.application.Sphinx") -> None:
|
||||
indextemplate="pair: %s; global variable interpreted by pytest",
|
||||
)
|
||||
|
||||
app.add_crossref_type(
|
||||
directivename="hook",
|
||||
rolename="hook",
|
||||
objname="pytest hook",
|
||||
indextemplate="pair: %s; hook",
|
||||
)
|
||||
|
||||
configure_logging(app)
|
||||
|
||||
# Make Sphinx mark classes with "final" when decorated with @final.
|
||||
|
||||
@@ -16,7 +16,7 @@ Deprecated Features
|
||||
-------------------
|
||||
|
||||
Below is a complete list of all pytest features which are considered deprecated. Using those features will issue
|
||||
:class:`PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
|
||||
:class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
|
||||
|
||||
.. _instance-collector-deprecation:
|
||||
|
||||
@@ -56,6 +56,10 @@ Plugins which implement custom items and collectors are encouraged to replace
|
||||
``fspath`` parameters (``py.path.local``) with ``path`` parameters
|
||||
(``pathlib.Path``), and drop any other usage of the ``py`` library if possible.
|
||||
|
||||
If possible, plugins with custom items should use :ref:`cooperative
|
||||
constructors <uncooperative-constructors-deprecated>` to avoid hardcoding
|
||||
arguments they only pass on to the superclass.
|
||||
|
||||
.. note::
|
||||
The name of the :class:`~_pytest.nodes.Node` arguments and attributes (the
|
||||
new attribute being ``path``) is **the opposite** of the situation for
|
||||
@@ -81,11 +85,11 @@ no matter what argument was used in the constructor. We expect to deprecate the
|
||||
|
||||
In order to support the transition from ``py.path.local`` to :mod:`pathlib`, the following hooks now receive additional arguments:
|
||||
|
||||
* :func:`pytest_ignore_collect(collection_path: pathlib.Path) <_pytest.hookspec.pytest_ignore_collect>` as equivalent to ``path``
|
||||
* :func:`pytest_collect_file(file_path: pathlib.Path) <_pytest.hookspec.pytest_collect_file>` as equivalent to ``path``
|
||||
* :func:`pytest_pycollect_makemodule(module_path: pathlib.Path) <_pytest.hookspec.pytest_pycollect_makemodule>` as equivalent to ``path``
|
||||
* :func:`pytest_report_header(start_path: pathlib.Path) <_pytest.hookspec.pytest_report_header>` as equivalent to ``startdir``
|
||||
* :func:`pytest_report_collectionfinish(start_path: pathlib.Path) <_pytest.hookspec.pytest_report_collectionfinish>` as equivalent to ``startdir``
|
||||
* :hook:`pytest_ignore_collect(collection_path: pathlib.Path) <pytest_ignore_collect>` as equivalent to ``path``
|
||||
* :hook:`pytest_collect_file(file_path: pathlib.Path) <pytest_collect_file>` as equivalent to ``path``
|
||||
* :hook:`pytest_pycollect_makemodule(module_path: pathlib.Path) <pytest_pycollect_makemodule>` as equivalent to ``path``
|
||||
* :hook:`pytest_report_header(start_path: pathlib.Path) <pytest_report_header>` as equivalent to ``startdir``
|
||||
* :hook:`pytest_report_collectionfinish(start_path: pathlib.Path) <pytest_report_collectionfinish>` as equivalent to ``startdir``
|
||||
|
||||
The accompanying ``py.path.local`` based paths have been deprecated: plugins which manually invoke those hooks should only pass the new ``pathlib.Path`` arguments, and users should change their hook implementations to use the new ``pathlib.Path`` arguments.
|
||||
|
||||
@@ -127,7 +131,7 @@ Passing ``msg=`` to ``pytest.skip``, ``pytest.fail`` or ``pytest.exit``
|
||||
|
||||
Passing the keyword argument ``msg`` to :func:`pytest.skip`, :func:`pytest.fail` or :func:`pytest.exit`
|
||||
is now deprecated and ``reason`` should be used instead. This change is to bring consistency between these
|
||||
functions and the``@pytest.mark.skip`` and ``@pytest.mark.xfail`` markers which already accept a ``reason`` argument.
|
||||
functions and the ``@pytest.mark.skip`` and ``@pytest.mark.xfail`` markers which already accept a ``reason`` argument.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -157,8 +161,8 @@ Implementing the ``pytest_cmdline_preparse`` hook
|
||||
|
||||
.. deprecated:: 7.0
|
||||
|
||||
Implementing the :func:`pytest_cmdline_preparse <_pytest.hookspec.pytest_cmdline_preparse>` hook has been officially deprecated.
|
||||
Implement the :func:`pytest_load_initial_conftests <_pytest.hookspec.pytest_load_initial_conftests>` hook instead.
|
||||
Implementing the :hook:`pytest_cmdline_preparse` hook has been officially deprecated.
|
||||
Implement the :hook:`pytest_load_initial_conftests` hook instead.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -191,6 +195,40 @@ Instead, a separate collector node should be used, which collects the item. See
|
||||
.. _example pr fixing inheritance: https://github.com/asmeurer/pytest-flakes/pull/40/files
|
||||
|
||||
|
||||
.. _uncooperative-constructors-deprecated:
|
||||
|
||||
Constructors of custom :class:`pytest.Node` subclasses should take ``**kwargs``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.0
|
||||
|
||||
If custom subclasses of nodes like :class:`pytest.Item` override the
|
||||
``__init__`` method, they should take ``**kwargs``. Thus,
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class CustomItem(pytest.Item):
|
||||
def __init__(self, name, parent, additional_arg):
|
||||
super().__init__(name, parent)
|
||||
self.additional_arg = additional_arg
|
||||
|
||||
should be turned into:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class CustomItem(pytest.Item):
|
||||
def __init__(self, *, additional_arg, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.additional_arg = additional_arg
|
||||
|
||||
to avoid hard-coding the arguments pytest can pass to the superclass.
|
||||
See :ref:`non-python tests` for a full example.
|
||||
|
||||
For cases without conflicts, no deprecation warning is emitted. For cases with
|
||||
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.
|
||||
|
||||
Backward compatibilities in ``Parser.addoption``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -203,29 +241,16 @@ scheduled for removal in pytest 8 (deprecated since pytest 2.4.0):
|
||||
- ``parser.addoption(..., type="int/string/float/complex")`` - use ``type=int`` etc. instead.
|
||||
|
||||
|
||||
Raising ``unittest.SkipTest`` during collection
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.0
|
||||
|
||||
Raising :class:`unittest.SkipTest` to skip collection of tests during the
|
||||
pytest collection phase is deprecated. Use :func:`pytest.skip` instead.
|
||||
|
||||
Note: This deprecation only relates to using `unittest.SkipTest` during test
|
||||
collection. You are probably not doing that. Ordinary usage of
|
||||
:class:`unittest.SkipTest` / :meth:`unittest.TestCase.skipTest` /
|
||||
:func:`unittest.skip` in unittest test cases is fully supported.
|
||||
|
||||
Using ``pytest.warns(None)``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.0
|
||||
|
||||
:func:`pytest.warns(None) <pytest.warns>` is now deprecated because many people used
|
||||
it to mean "this code does not emit warnings", but it actually had the effect of
|
||||
checking that the code emits at least one warning of any type - like ``pytest.warns()``
|
||||
:func:`pytest.warns(None) <pytest.warns>` is now deprecated because it was frequently misused.
|
||||
Its correct usage was checking that the code emits at least one warning of any type - like ``pytest.warns()``
|
||||
or ``pytest.warns(Warning)``.
|
||||
|
||||
See :ref:`warns use cases` for examples.
|
||||
|
||||
The ``--strict`` command-line option
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -250,29 +275,42 @@ The ``yield_fixture`` function/decorator
|
||||
It has been so for a very long time, so can be search/replaced safely.
|
||||
|
||||
|
||||
The ``pytest_warning_captured`` hook
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Removed Features
|
||||
----------------
|
||||
|
||||
.. deprecated:: 6.0
|
||||
As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after
|
||||
an appropriate period of deprecation has passed.
|
||||
|
||||
This hook has an `item` parameter which cannot be serialized by ``pytest-xdist``.
|
||||
|
||||
Use the ``pytest_warning_recored`` hook instead, which replaces the ``item`` parameter
|
||||
by a ``nodeid`` parameter.
|
||||
|
||||
The ``pytest.collect`` module
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 6.0
|
||||
.. versionremoved:: 7.0
|
||||
|
||||
The ``pytest.collect`` module is no longer part of the public API, all its names
|
||||
should now be imported from ``pytest`` directly instead.
|
||||
|
||||
|
||||
|
||||
The ``pytest_warning_captured`` hook
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 6.0
|
||||
.. versionremoved:: 7.0
|
||||
|
||||
This hook has an `item` parameter which cannot be serialized by ``pytest-xdist``.
|
||||
|
||||
Use the ``pytest_warning_recorded`` hook instead, which replaces the ``item`` parameter
|
||||
by a ``nodeid`` parameter.
|
||||
|
||||
|
||||
|
||||
The ``pytest._fillfuncargs`` function
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 6.0
|
||||
.. versionremoved:: 7.0
|
||||
|
||||
This function was kept for backward compatibility with an older plugin.
|
||||
|
||||
@@ -281,12 +319,6 @@ it, use `function._request._fillfixtures()` instead, though note this is not
|
||||
a public API and may break in the future.
|
||||
|
||||
|
||||
Removed Features
|
||||
----------------
|
||||
|
||||
As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after
|
||||
an appropriate period of deprecation has passed.
|
||||
|
||||
``--no-print-logs`` command-line option
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -324,8 +356,8 @@ at some point, depending on the plans for the plugins and number of users using
|
||||
|
||||
.. versionremoved:: 6.0
|
||||
|
||||
The ``pytest_collect_directory`` has not worked properly for years (it was called
|
||||
but the results were ignored). Users may consider using :func:`pytest_collection_modifyitems <_pytest.hookspec.pytest_collection_modifyitems>` instead.
|
||||
The ``pytest_collect_directory`` hook has not worked properly for years (it was called
|
||||
but the results were ignored). Users may consider using :hook:`pytest_collection_modifyitems` instead.
|
||||
|
||||
TerminalReporter.writer
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
56
doc/en/example/fixtures/test_fixtures_order_autouse_flat.svg
Normal file
56
doc/en/example/fixtures/test_fixtures_order_autouse_flat.svg
Normal file
@@ -0,0 +1,56 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="112" height="682">
|
||||
<style>
|
||||
text {
|
||||
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
|
||||
dominant-baseline: middle;
|
||||
text-anchor: middle;
|
||||
fill: #062886;
|
||||
font-size: medium;
|
||||
}
|
||||
ellipse.fixture, rect.test {
|
||||
fill: #eeffcc;
|
||||
stroke: #007020;
|
||||
stroke-width: 2;
|
||||
}
|
||||
text.fixture {
|
||||
color: #06287e;
|
||||
}
|
||||
circle.class {
|
||||
fill: #c3e0ec;
|
||||
stroke: #0e84b5;
|
||||
stroke-width: 2;
|
||||
}
|
||||
text.class {
|
||||
fill: #0e84b5;
|
||||
}
|
||||
path, line {
|
||||
stroke: black;
|
||||
stroke-width: 2;
|
||||
fill: none;
|
||||
}
|
||||
rect.autouse {
|
||||
fill: #ca7f3d;
|
||||
}
|
||||
</style>
|
||||
<line x1="56" x2="56" y1="681" y2="26" />
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="56" cy="26" />
|
||||
<text x="56" y="26">order</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="56" cy="96" />
|
||||
<text x="56" y="96">a</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="56" cy="166" />
|
||||
<text x="56" y="166">b</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="56" cy="236" />
|
||||
<text x="56" y="236">c</text>
|
||||
<rect class="autouse" width="112" height="40" x="0" y="286" />
|
||||
<text x="56" y="306">autouse</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="56" cy="376" />
|
||||
<text x="56" y="376">d</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="56" cy="446" />
|
||||
<text x="56" y="446">e</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="56" cy="516" />
|
||||
<text x="56" y="516">f</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="56" cy="586" />
|
||||
<text x="56" y="586">g</text>
|
||||
<rect class="test" width="110" height="50" x="1" y="631" />
|
||||
<text x="56" y="656">test_order</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -18,8 +18,8 @@ class YamlFile(pytest.File):
|
||||
|
||||
|
||||
class YamlItem(pytest.Item):
|
||||
def __init__(self, name, parent, spec):
|
||||
super().__init__(name, parent)
|
||||
def __init__(self, *, spec, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.spec = spec
|
||||
|
||||
def runtest(self):
|
||||
|
||||
@@ -155,7 +155,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
> assert [0, 1, 2] == [0, 1, 3]
|
||||
E assert [0, 1, 2] == [0, 1, 3]
|
||||
E At index 2 diff: 2 != 3
|
||||
E Use -v to get the full diff
|
||||
E Use -v to get more diff
|
||||
|
||||
failure_demo.py:63: AssertionError
|
||||
______________ TestSpecialisedExplanations.test_eq_list_long _______________
|
||||
@@ -168,7 +168,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
> assert a == b
|
||||
E assert [0, 0, 0, 0, 0, 0, ...] == [0, 0, 0, 0, 0, 0, ...]
|
||||
E At index 100 diff: 1 != 2
|
||||
E Use -v to get the full diff
|
||||
E Use -v to get more diff
|
||||
|
||||
failure_demo.py:68: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_eq_dict _________________
|
||||
@@ -215,7 +215,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
> assert [1, 2] == [1, 2, 3]
|
||||
E assert [1, 2] == [1, 2, 3]
|
||||
E Right contains one more item: 3
|
||||
E Use -v to get the full diff
|
||||
E Use -v to get more diff
|
||||
|
||||
failure_demo.py:77: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_in_list _________________
|
||||
|
||||
@@ -9,7 +9,7 @@ Get Started
|
||||
Install ``pytest``
|
||||
----------------------------------------
|
||||
|
||||
``pytest`` requires: Python 3.6, 3.7, 3.8, 3.9, or PyPy3.
|
||||
``pytest`` requires: Python 3.7+ or PyPy3.
|
||||
|
||||
1. Run the following command in your command line:
|
||||
|
||||
@@ -22,7 +22,7 @@ Install ``pytest``
|
||||
.. code-block:: bash
|
||||
|
||||
$ pytest --version
|
||||
pytest 7.0.0rc1
|
||||
pytest 7.1.0
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ project:
|
||||
which adds ``pytest`` (rather than ``py.test``) as the recommended
|
||||
command-line entry point
|
||||
|
||||
Due to this history, it's diffcult to answer the question when pytest was started.
|
||||
Due to this history, it's difficult to answer the question when pytest was started.
|
||||
It depends what point should really be seen as the start of it all. One
|
||||
possible interpretation is to pick Europython 2004, i.e. around June/July
|
||||
2004.
|
||||
|
||||
@@ -201,7 +201,7 @@ if you run this module:
|
||||
E '1'
|
||||
E Extra items in the right set:
|
||||
E '5'
|
||||
E Use -v to get the full diff
|
||||
E Use -v to get more diff
|
||||
|
||||
test_assert2.py:4: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
|
||||
@@ -5,7 +5,7 @@ How to set up bash completion
|
||||
=============================
|
||||
|
||||
When using bash as your shell, ``pytest`` can use argcomplete
|
||||
(https://argcomplete.readthedocs.io/) for auto-completion.
|
||||
(https://kislyuk.github.io/argcomplete/) for auto-completion.
|
||||
For this ``argcomplete`` needs to be installed **and** enabled.
|
||||
|
||||
Install argcomplete using:
|
||||
|
||||
@@ -42,8 +42,18 @@ Running pytest now produces this output:
|
||||
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||
======================= 1 passed, 1 warning in 0.12s =======================
|
||||
|
||||
The ``-W`` flag can be passed to control which warnings will be displayed or even turn
|
||||
them into errors:
|
||||
Controlling warnings
|
||||
--------------------
|
||||
|
||||
Similar to Python's `warning filter`_ and :option:`-W option <python:-W>` flag, pytest provides
|
||||
its own ``-W`` flag to control which warnings are ignored, displayed, or turned into
|
||||
errors. See the `warning filter`_ documentation for more
|
||||
advanced use-cases.
|
||||
|
||||
.. _`warning filter`: https://docs.python.org/3/library/warnings.html#warning-filter
|
||||
|
||||
This code sample shows how to treat any ``UserWarning`` category class of warning
|
||||
as an error:
|
||||
|
||||
.. code-block:: pytest
|
||||
|
||||
@@ -96,9 +106,6 @@ all other warnings into errors.
|
||||
When a warning matches more than one option in the list, the action for the last matching option
|
||||
is performed.
|
||||
|
||||
Both ``-W`` command-line option and ``filterwarnings`` ini option are based on Python's own
|
||||
:option:`-W option <python:-W>` and :func:`warnings.simplefilter`, so please refer to those sections in the Python
|
||||
documentation for other examples and advanced usage.
|
||||
|
||||
.. _`filterwarnings`:
|
||||
|
||||
@@ -344,6 +351,37 @@ warnings, or index into it to get a particular recorded warning.
|
||||
|
||||
Full API: :class:`~_pytest.recwarn.WarningsRecorder`.
|
||||
|
||||
.. _`warns use cases`:
|
||||
|
||||
Additional use cases of warnings in tests
|
||||
-----------------------------------------
|
||||
|
||||
Here are some use cases involving warnings that often come up in tests, and suggestions on how to deal with them:
|
||||
|
||||
- To ensure that **at least one** warning is emitted, use:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
with pytest.warns():
|
||||
...
|
||||
|
||||
- To ensure that **no** warnings are emitted, use:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error")
|
||||
...
|
||||
|
||||
- To suppress warnings, use:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
...
|
||||
|
||||
|
||||
.. _custom_failure_messages:
|
||||
|
||||
Custom failure messages
|
||||
@@ -403,3 +441,18 @@ Please read our :ref:`backwards-compatibility` to learn how we proceed about dep
|
||||
features.
|
||||
|
||||
The full list of warnings is listed in :ref:`the reference documentation <warnings ref>`.
|
||||
|
||||
|
||||
.. _`resource-warnings`:
|
||||
|
||||
Resource Warnings
|
||||
-----------------
|
||||
|
||||
Additional information of the source of a :class:`ResourceWarning` can be obtained when captured by pytest if
|
||||
:mod:`tracemalloc` module is enabled.
|
||||
|
||||
One convenient way to enable :mod:`tracemalloc` when running tests is to set the :envvar:`PYTHONTRACEMALLOC` to a large
|
||||
enough number of frames (say ``20``, but that number is application dependent).
|
||||
|
||||
For more information, consult the `Python Development Mode <https://docs.python.org/3/library/devmode.html>`__
|
||||
section in the Python documentation.
|
||||
|
||||
@@ -84,7 +84,7 @@ Executing pytest normally gives us this output (we are skipping the header to fo
|
||||
> assert fruits1 == fruits2
|
||||
E AssertionError: assert ['banana', 'a...elon', 'kiwi'] == ['banana', 'a...elon', 'kiwi']
|
||||
E At index 2 diff: 'grapes' != 'orange'
|
||||
E Use -v to get the full diff
|
||||
E Use -v to get more diff
|
||||
|
||||
test_verbosity_example.py:8: AssertionError
|
||||
____________________________ test_numbers_fail _____________________________
|
||||
@@ -99,7 +99,7 @@ Executing pytest normally gives us this output (we are skipping the header to fo
|
||||
E {'1': 1, '2': 2, '3': 3, '4': 4}
|
||||
E Right contains 4 more items:
|
||||
E {'10': 10, '20': 20, '30': 30, '40': 40}
|
||||
E Use -v to get the full diff
|
||||
E Use -v to get more diff
|
||||
|
||||
test_verbosity_example.py:14: AssertionError
|
||||
___________________________ test_long_text_fail ____________________________
|
||||
|
||||
@@ -21,7 +21,7 @@ there is no need to activate it.
|
||||
Here is a little annotated list for some popular plugins:
|
||||
|
||||
* :pypi:`pytest-django`: write tests
|
||||
for :std:doc:`django <django:index>` apps, using pytest integration.
|
||||
for `django <https://docs.djangoproject.com/>`_ apps, using pytest integration.
|
||||
|
||||
* :pypi:`pytest-twisted`: write tests
|
||||
for `twisted <https://twistedmatrix.com/>`_ apps, starting a reactor and
|
||||
@@ -51,9 +51,6 @@ Here is a little annotated list for some popular plugins:
|
||||
* :pypi:`pytest-flakes`:
|
||||
check source code with pyflakes.
|
||||
|
||||
* :pypi:`oejskit`:
|
||||
a plugin to run javascript unittests in live browsers.
|
||||
|
||||
To see a complete list of all plugins with their latest testing
|
||||
status against different pytest and Python versions, please visit
|
||||
:ref:`plugin-list`.
|
||||
|
||||
@@ -84,14 +84,14 @@ It is also possible to skip the whole module using
|
||||
|
||||
If you wish to skip something conditionally then you can use ``skipif`` instead.
|
||||
Here is an example of marking a test function to be skipped
|
||||
when run on an interpreter earlier than Python3.6:
|
||||
when run on an interpreter earlier than Python3.10:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher")
|
||||
@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires python3.10 or higher")
|
||||
def test_function():
|
||||
...
|
||||
|
||||
@@ -369,7 +369,7 @@ Here is a simple test file with the several usages:
|
||||
|
||||
Running it with the report-on-xfail option gives this output:
|
||||
|
||||
.. FIXME: Use $ instead of ! again to reenable regendoc once it's fixed:
|
||||
.. FIXME: Use $ instead of ! again to re-enable regendoc once it's fixed:
|
||||
https://github.com/pytest-dev/pytest/issues/8807
|
||||
|
||||
.. code-block:: pytest
|
||||
|
||||
@@ -121,6 +121,10 @@ the system temporary directory. The base name will be ``pytest-NUM`` where
|
||||
``NUM`` will be incremented with each test run. Moreover, entries older
|
||||
than 3 temporary directories will be removed.
|
||||
|
||||
The number of entries currently cannot be changed, but using the ``--basetemp``
|
||||
option will remove the directory before every run, effectively meaning the temporary directories
|
||||
of only the most recent run will be kept.
|
||||
|
||||
You can override the default temporary directory setting like this:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
@@ -321,7 +321,7 @@ Plugins often need to store data on :class:`~pytest.Item`\s in one hook
|
||||
implementation, and access it in another. One common solution is to just
|
||||
assign some private attribute directly on the item, but type-checkers like
|
||||
mypy frown upon this, and it may also cause conflicts with other plugins.
|
||||
So pytest offers a better way to do this, :attr:`_pytest.nodes.Node.stash <item.stash>`.
|
||||
So pytest offers a better way to do this, :attr:`item.stash <_pytest.nodes.Node.stash>`.
|
||||
|
||||
To use the "stash" in your plugins, first create "stash keys" somewhere at the
|
||||
top level of your plugin:
|
||||
|
||||
@@ -2,9 +2,15 @@
|
||||
|
||||
.. sidebar:: Next Open Trainings
|
||||
|
||||
- `Professional Testing with Python <https://www.python-academy.com/courses/specialtopics/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, February 1st to 3rd, 2022, Leipzig (Germany) and remote.
|
||||
- `PyConDE <https://2022.pycon.de/program/W93DBJ/>`__, April 11th 2022 (3h), Berlin, Germany
|
||||
- `PyConIT <https://pycon.it/en/talk/pytest-simple-rapid-and-fun-testing-with-python>`__, June 3rd 2022 (4h), Florence, Italy
|
||||
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, March 7th to 9th 2023 (3 day in-depth training), Remote and Leipzig, Germany
|
||||
|
||||
Also see `previous talks and blogposts <talks.html>`_.
|
||||
Also see :doc:`previous talks and blogposts <talks>`.
|
||||
|
||||
..
|
||||
- `Europython <https://ep2022.europython.eu/>`__, July 11th to 17th (3h), Dublin, Ireland
|
||||
- `CH Open Workshoptage <https://workshoptage.ch/>`__ (German), September 6th to 8th (1 day), Bern, Switzerland
|
||||
|
||||
.. _features:
|
||||
|
||||
@@ -17,7 +23,7 @@ The ``pytest`` framework makes it easy to write small, readable tests, and can
|
||||
scale to support complex functional testing for applications and libraries.
|
||||
|
||||
|
||||
**Pythons**: ``pytest`` requires: Python 3.6, 3.7, 3.8, 3.9, or PyPy3.
|
||||
``pytest`` requires: Python 3.7+ or PyPy3.
|
||||
|
||||
**PyPI package name**: :pypi:`pytest`
|
||||
|
||||
@@ -78,7 +84,7 @@ Features
|
||||
|
||||
- Can run :ref:`unittest <unittest>` (including trial) and :ref:`nose <noseintegration>` test suites out of the box
|
||||
|
||||
- Python 3.6+ and PyPy 3
|
||||
- Python 3.7+ or PyPy 3
|
||||
|
||||
- Rich plugin architecture, with over 800+ :ref:`external plugins <plugin-list>` and thriving community
|
||||
|
||||
|
||||
@@ -20,8 +20,7 @@ Configuration file formats
|
||||
--------------------------
|
||||
|
||||
Many :ref:`pytest settings <ini options ref>` can be set in a *configuration file*, which
|
||||
by convention resides on the root of your repository or in your
|
||||
tests folder.
|
||||
by convention resides in the root directory of your repository.
|
||||
|
||||
A quick example of the configuration files supported by pytest:
|
||||
|
||||
|
||||
@@ -208,7 +208,7 @@ the one defined in ``tests/test_top.py`` would be unavailable to it because it
|
||||
would have to step down a level (step inside a circle) to find it.
|
||||
|
||||
The first fixture the test finds is the one that will be used, so
|
||||
:ref:`fixtures can be overriden <override fixtures>` if you need to change or
|
||||
:ref:`fixtures can be overridden <override fixtures>` if you need to change or
|
||||
extend what one does for a particular scope.
|
||||
|
||||
You can also use the ``conftest.py`` file to implement
|
||||
@@ -401,6 +401,9 @@ the graph would look like this:
|
||||
Because ``c`` can now be put above ``d`` in the graph, pytest can once again
|
||||
linearize the graph to this:
|
||||
|
||||
.. image:: /example/fixtures/test_fixtures_order_autouse_flat.*
|
||||
:align: center
|
||||
|
||||
In this example, ``c`` makes ``b`` and ``a`` effectively autouse fixtures as
|
||||
well.
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,39 @@ This page contains the full reference to pytest's API.
|
||||
:depth: 3
|
||||
:local:
|
||||
|
||||
Constants
|
||||
---------
|
||||
|
||||
pytest.__version__
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The current pytest version, as a string::
|
||||
|
||||
>>> import pytest
|
||||
>>> pytest.__version__
|
||||
'7.0.0'
|
||||
|
||||
|
||||
.. _`version-tuple`:
|
||||
|
||||
pytest.version_tuple
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. versionadded:: 7.0
|
||||
|
||||
The current pytest version, as a tuple::
|
||||
|
||||
>>> import pytest
|
||||
>>> pytest.version_tuple
|
||||
(7, 0, 0)
|
||||
|
||||
For pre-releases, the last component will be a string with the prerelease version::
|
||||
|
||||
>>> import pytest
|
||||
>>> pytest.version_tuple
|
||||
(7, 0, '0rc1')
|
||||
|
||||
|
||||
Functions
|
||||
---------
|
||||
|
||||
@@ -226,37 +259,6 @@ Marks a test function as *expected to fail*.
|
||||
a new release of a library fixes a known bug).
|
||||
|
||||
|
||||
pytest.__version__
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The current pytest version, as a string::
|
||||
|
||||
>>> import pytest
|
||||
>>> pytest.__version__
|
||||
'7.0.0'
|
||||
|
||||
|
||||
.. _`version-tuple`:
|
||||
|
||||
pytest.version_tuple
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. versionadded:: 7.0
|
||||
|
||||
The current pytest version, as a tuple::
|
||||
|
||||
>>> import pytest
|
||||
>>> pytest.version_tuple
|
||||
(7, 0, 0)
|
||||
|
||||
For pre-releases, the last component will be a string with the prerelease version::
|
||||
|
||||
>>> import pytest
|
||||
>>> pytest.version_tuple
|
||||
(7, 0, '0rc1')
|
||||
|
||||
|
||||
|
||||
Custom marks
|
||||
~~~~~~~~~~~~
|
||||
|
||||
@@ -671,9 +673,13 @@ Bootstrapping hooks
|
||||
|
||||
Bootstrapping hooks called for plugins registered early enough (internal and setuptools plugins).
|
||||
|
||||
.. hook:: pytest_load_initial_conftests
|
||||
.. autofunction:: pytest_load_initial_conftests
|
||||
.. hook:: pytest_cmdline_preparse
|
||||
.. autofunction:: pytest_cmdline_preparse
|
||||
.. hook:: pytest_cmdline_parse
|
||||
.. autofunction:: pytest_cmdline_parse
|
||||
.. hook:: pytest_cmdline_main
|
||||
.. autofunction:: pytest_cmdline_main
|
||||
|
||||
.. _`initialization-hooks`:
|
||||
@@ -683,13 +689,20 @@ Initialization hooks
|
||||
|
||||
Initialization hooks called for plugins and ``conftest.py`` files.
|
||||
|
||||
.. hook:: pytest_addoption
|
||||
.. autofunction:: pytest_addoption
|
||||
.. hook:: pytest_addhooks
|
||||
.. autofunction:: pytest_addhooks
|
||||
.. hook:: pytest_configure
|
||||
.. autofunction:: pytest_configure
|
||||
.. hook:: pytest_unconfigure
|
||||
.. autofunction:: pytest_unconfigure
|
||||
.. hook:: pytest_sessionstart
|
||||
.. autofunction:: pytest_sessionstart
|
||||
.. hook:: pytest_sessionfinish
|
||||
.. autofunction:: pytest_sessionfinish
|
||||
|
||||
.. hook:: pytest_plugin_registered
|
||||
.. autofunction:: pytest_plugin_registered
|
||||
|
||||
Collection hooks
|
||||
@@ -697,21 +710,34 @@ Collection hooks
|
||||
|
||||
``pytest`` calls the following hooks for collecting files and directories:
|
||||
|
||||
.. hook:: pytest_collection
|
||||
.. autofunction:: pytest_collection
|
||||
.. hook:: pytest_ignore_collect
|
||||
.. autofunction:: pytest_ignore_collect
|
||||
.. hook:: pytest_collect_file
|
||||
.. autofunction:: pytest_collect_file
|
||||
.. hook:: pytest_pycollect_makemodule
|
||||
.. autofunction:: pytest_pycollect_makemodule
|
||||
|
||||
For influencing the collection of objects in Python modules
|
||||
you can use the following hook:
|
||||
|
||||
.. hook:: pytest_pycollect_makeitem
|
||||
.. autofunction:: pytest_pycollect_makeitem
|
||||
.. hook:: pytest_generate_tests
|
||||
.. autofunction:: pytest_generate_tests
|
||||
.. hook:: pytest_make_parametrize_id
|
||||
.. autofunction:: pytest_make_parametrize_id
|
||||
|
||||
Hooks for influencing test skipping:
|
||||
|
||||
.. hook:: pytest_markeval_namespace
|
||||
.. autofunction:: pytest_markeval_namespace
|
||||
|
||||
After collection is complete, you can modify the order of
|
||||
items, delete or otherwise amend the test items:
|
||||
|
||||
.. hook:: pytest_collection_modifyitems
|
||||
.. autofunction:: pytest_collection_modifyitems
|
||||
|
||||
.. note::
|
||||
@@ -725,13 +751,21 @@ Test running (runtest) hooks
|
||||
|
||||
All runtest related hooks receive a :py:class:`pytest.Item <pytest.Item>` object.
|
||||
|
||||
.. hook:: pytest_runtestloop
|
||||
.. autofunction:: pytest_runtestloop
|
||||
.. hook:: pytest_runtest_protocol
|
||||
.. autofunction:: pytest_runtest_protocol
|
||||
.. hook:: pytest_runtest_logstart
|
||||
.. autofunction:: pytest_runtest_logstart
|
||||
.. hook:: pytest_runtest_logfinish
|
||||
.. autofunction:: pytest_runtest_logfinish
|
||||
.. hook:: pytest_runtest_setup
|
||||
.. autofunction:: pytest_runtest_setup
|
||||
.. hook:: pytest_runtest_call
|
||||
.. autofunction:: pytest_runtest_call
|
||||
.. hook:: pytest_runtest_teardown
|
||||
.. autofunction:: pytest_runtest_teardown
|
||||
.. hook:: pytest_runtest_makereport
|
||||
.. autofunction:: pytest_runtest_makereport
|
||||
|
||||
For deeper understanding you may look at the default implementation of
|
||||
@@ -740,6 +774,7 @@ in ``_pytest.pdb`` which interacts with ``_pytest.capture``
|
||||
and its input/output capturing in order to immediately drop
|
||||
into interactive debugging when a test failure occurs.
|
||||
|
||||
.. hook:: pytest_pyfunc_call
|
||||
.. autofunction:: pytest_pyfunc_call
|
||||
|
||||
Reporting hooks
|
||||
@@ -747,27 +782,45 @@ Reporting hooks
|
||||
|
||||
Session related reporting hooks:
|
||||
|
||||
.. hook:: pytest_collectstart
|
||||
.. autofunction:: pytest_collectstart
|
||||
.. hook:: pytest_make_collect_report
|
||||
.. autofunction:: pytest_make_collect_report
|
||||
.. hook:: pytest_itemcollected
|
||||
.. autofunction:: pytest_itemcollected
|
||||
.. hook:: pytest_collectreport
|
||||
.. autofunction:: pytest_collectreport
|
||||
.. hook:: pytest_deselected
|
||||
.. autofunction:: pytest_deselected
|
||||
.. hook:: pytest_report_header
|
||||
.. autofunction:: pytest_report_header
|
||||
.. hook:: pytest_report_collectionfinish
|
||||
.. autofunction:: pytest_report_collectionfinish
|
||||
.. hook:: pytest_report_teststatus
|
||||
.. autofunction:: pytest_report_teststatus
|
||||
.. hook:: pytest_report_to_serializable
|
||||
.. autofunction:: pytest_report_to_serializable
|
||||
.. hook:: pytest_report_from_serializable
|
||||
.. autofunction:: pytest_report_from_serializable
|
||||
.. hook:: pytest_terminal_summary
|
||||
.. autofunction:: pytest_terminal_summary
|
||||
.. hook:: pytest_fixture_setup
|
||||
.. autofunction:: pytest_fixture_setup
|
||||
.. hook:: pytest_fixture_post_finalizer
|
||||
.. autofunction:: pytest_fixture_post_finalizer
|
||||
.. autofunction:: pytest_warning_captured
|
||||
.. hook:: pytest_warning_recorded
|
||||
.. autofunction:: pytest_warning_recorded
|
||||
|
||||
Central hook for reporting about test execution:
|
||||
|
||||
.. hook:: pytest_runtest_logreport
|
||||
.. autofunction:: pytest_runtest_logreport
|
||||
|
||||
Assertion related hooks:
|
||||
|
||||
.. hook:: pytest_assertrepr_compare
|
||||
.. autofunction:: pytest_assertrepr_compare
|
||||
.. hook:: pytest_assertion_pass
|
||||
.. autofunction:: pytest_assertion_pass
|
||||
|
||||
|
||||
@@ -777,10 +830,16 @@ Debugging/Interaction hooks
|
||||
There are few hooks which can be used for special
|
||||
reporting or interaction with exceptions:
|
||||
|
||||
.. hook:: pytest_internalerror
|
||||
.. autofunction:: pytest_internalerror
|
||||
.. hook:: pytest_keyboard_interrupt
|
||||
.. autofunction:: pytest_keyboard_interrupt
|
||||
.. hook:: pytest_exception_interact
|
||||
.. autofunction:: pytest_exception_interact
|
||||
.. hook:: pytest_enter_pdb
|
||||
.. autofunction:: pytest_enter_pdb
|
||||
.. hook:: pytest_leave_pdb
|
||||
.. autofunction:: pytest_leave_pdb
|
||||
|
||||
|
||||
Objects
|
||||
|
||||
@@ -11,9 +11,14 @@ Books
|
||||
- `Python Testing with pytest, by Brian Okken (2017)
|
||||
<https://pragprog.com/book/bopytest/python-testing-with-pytest>`_.
|
||||
|
||||
- `Python Testing with pytest, Second Edition, by Brian Okken (2022)
|
||||
<https://pragprog.com/titles/bopytest2/python-testing-with-pytest-second-edition>`_.
|
||||
|
||||
Talks and blog postings
|
||||
---------------------------------------------
|
||||
|
||||
- `pytest: Simple, rapid and fun testing with Python, <https://youtu.be/cSJ-X3TbQ1c?t=15752>`_ (@ 4:22:32), Florian Bruhin, WeAreDevelopers World Congress 2021
|
||||
|
||||
- Webinar: `pytest: Test Driven Development für Python (German) <https://bruhin.software/ins-pytest/>`_, Florian Bruhin, via mylearning.ch, 2020
|
||||
|
||||
- Webinar: `Simplify Your Tests with Fixtures <https://blog.jetbrains.com/pycharm/2020/08/webinar-recording-simplify-your-tests-with-fixtures-with-oliver-bestwalter/>`_, Oliver Bestwalter, via JetBrains, 2020
|
||||
|
||||
@@ -28,8 +28,6 @@ filterwarnings = [
|
||||
"default:the imp module is deprecated in favour of importlib:DeprecationWarning:nose.*",
|
||||
# distutils is deprecated in 3.10, scheduled for removal in 3.12
|
||||
"ignore:The distutils package is deprecated:DeprecationWarning",
|
||||
# produced by python3.6/site.py itself (3.6.7 on Travis, could not trigger it with 3.6.8)."
|
||||
"ignore:.*U.*mode is deprecated:DeprecationWarning:(?!(pytest|_pytest))",
|
||||
# produced by pytest-xdist
|
||||
"ignore:.*type argument to addoption.*:DeprecationWarning",
|
||||
# produced on execnet (pytest-xdist)
|
||||
@@ -113,4 +111,4 @@ template = "changelog/_template.rst"
|
||||
showcontent = true
|
||||
|
||||
[tool.black]
|
||||
target-version = ['py36']
|
||||
target-version = ['py37']
|
||||
|
||||
@@ -88,7 +88,9 @@ def prepare_release_pr(
|
||||
|
||||
print(f"Branch {Fore.CYAN}{release_branch}{Fore.RESET} created.")
|
||||
|
||||
if prerelease:
|
||||
if is_major:
|
||||
template_name = "release.major.rst"
|
||||
elif prerelease:
|
||||
template_name = "release.pre.rst"
|
||||
elif is_feature_release:
|
||||
template_name = "release.minor.rst"
|
||||
@@ -104,6 +106,7 @@ def prepare_release_pr(
|
||||
"--",
|
||||
version,
|
||||
template_name,
|
||||
release_branch, # doc_version
|
||||
"--skip-check-links",
|
||||
]
|
||||
print("Running", " ".join(cmdline))
|
||||
|
||||
24
scripts/release.major.rst
Normal file
24
scripts/release.major.rst
Normal file
@@ -0,0 +1,24 @@
|
||||
pytest-{version}
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the {version} release!
|
||||
|
||||
This release contains new features, improvements, bug fixes, and breaking changes, so users
|
||||
are encouraged to take a look at the CHANGELOG carefully:
|
||||
|
||||
https://docs.pytest.org/en/stable/changelog.html
|
||||
|
||||
For complete documentation, please visit:
|
||||
|
||||
https://docs.pytest.org/en/stable/
|
||||
|
||||
As usual, you can upgrade from PyPI via:
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
{contributors}
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
@@ -3,8 +3,8 @@ pytest-{version}
|
||||
|
||||
The pytest team is proud to announce the {version} release!
|
||||
|
||||
This release contains new features, improvements, bug fixes, and breaking changes, so users
|
||||
are encouraged to take a look at the CHANGELOG carefully:
|
||||
This release contains new features, improvements, and bug fixes,
|
||||
the full list of changes is available in the changelog:
|
||||
|
||||
https://docs.pytest.org/en/stable/changelog.html
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ You can upgrade from PyPI via:
|
||||
|
||||
Users are encouraged to take a look at the CHANGELOG carefully:
|
||||
|
||||
https://docs.pytest.org/en/stable/changelog.html
|
||||
https://docs.pytest.org/en/{doc_version}/changelog.html
|
||||
|
||||
Thanks to all the contributors to this release:
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from colorama import Fore
|
||||
from colorama import init
|
||||
|
||||
|
||||
def announce(version, template_name):
|
||||
def announce(version, template_name, doc_version):
|
||||
"""Generates a new release announcement entry in the docs."""
|
||||
# Get our list of authors
|
||||
stdout = check_output(["git", "describe", "--abbrev=0", "--tags"])
|
||||
@@ -31,7 +31,9 @@ def announce(version, template_name):
|
||||
)
|
||||
|
||||
contributors_text = "\n".join(f"* {name}" for name in sorted(contributors)) + "\n"
|
||||
text = template_text.format(version=version, contributors=contributors_text)
|
||||
text = template_text.format(
|
||||
version=version, contributors=contributors_text, doc_version=doc_version
|
||||
)
|
||||
|
||||
target = Path(__file__).parent.joinpath(f"../doc/en/announce/release-{version}.rst")
|
||||
target.write_text(text, encoding="UTF-8")
|
||||
@@ -82,9 +84,9 @@ def check_links():
|
||||
check_call(["tox", "-e", "docs-checklinks"])
|
||||
|
||||
|
||||
def pre_release(version, template_name, *, skip_check_links):
|
||||
def pre_release(version, template_name, doc_version, *, skip_check_links):
|
||||
"""Generates new docs, release announcements and creates a local tag."""
|
||||
announce(version, template_name)
|
||||
announce(version, template_name, doc_version)
|
||||
regen(version)
|
||||
changelog(version, write_out=True)
|
||||
fix_formatting()
|
||||
@@ -112,11 +114,15 @@ def main():
|
||||
parser.add_argument(
|
||||
"template_name", help="Name of template file to use for release announcement"
|
||||
)
|
||||
parser.add_argument(
|
||||
"doc_version", help="For prereleases, the version to link to in the docs"
|
||||
)
|
||||
parser.add_argument("--skip-check-links", action="store_true", default=False)
|
||||
options = parser.parse_args()
|
||||
pre_release(
|
||||
options.version,
|
||||
options.template_name,
|
||||
options.doc_version,
|
||||
skip_check_links=options.skip_check_links,
|
||||
)
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ classifiers =
|
||||
Operating System :: POSIX
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3 :: Only
|
||||
Programming Language :: Python :: 3.6
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
@@ -51,7 +50,7 @@ install_requires =
|
||||
atomicwrites>=1.0;sys_platform=="win32"
|
||||
colorama;sys_platform=="win32"
|
||||
importlib-metadata>=0.12;python_version<"3.8"
|
||||
python_requires = >=3.6
|
||||
python_requires = >=3.7
|
||||
package_dir =
|
||||
=src
|
||||
setup_requires =
|
||||
|
||||
@@ -108,7 +108,6 @@ if os.environ.get("_ARGCOMPLETE"):
|
||||
def try_argcomplete(parser: argparse.ArgumentParser) -> None:
|
||||
argcomplete.autocomplete(parser, always_complete_options=False)
|
||||
|
||||
|
||||
else:
|
||||
|
||||
def try_argcomplete(parser: argparse.ArgumentParser) -> None:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import ast
|
||||
import inspect
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
@@ -343,10 +344,10 @@ class Traceback(List[TracebackEntry]):
|
||||
|
||||
def cut(
|
||||
self,
|
||||
path: Optional[Union[Path, str]] = None,
|
||||
path: Optional[Union["os.PathLike[str]", str]] = None,
|
||||
lineno: Optional[int] = None,
|
||||
firstlineno: Optional[int] = None,
|
||||
excludepath: Optional[Path] = None,
|
||||
excludepath: Optional["os.PathLike[str]"] = None,
|
||||
) -> "Traceback":
|
||||
"""Return a Traceback instance wrapping part of this Traceback.
|
||||
|
||||
@@ -357,15 +358,17 @@ class Traceback(List[TracebackEntry]):
|
||||
for formatting reasons (removing some uninteresting bits that deal
|
||||
with handling of the exception/traceback).
|
||||
"""
|
||||
path_ = None if path is None else os.fspath(path)
|
||||
excludepath_ = None if excludepath is None else os.fspath(excludepath)
|
||||
for x in self:
|
||||
code = x.frame.code
|
||||
codepath = code.path
|
||||
if path is not None and codepath != path:
|
||||
if path is not None and str(codepath) != path_:
|
||||
continue
|
||||
if (
|
||||
excludepath is not None
|
||||
and isinstance(codepath, Path)
|
||||
and excludepath in codepath.parents
|
||||
and excludepath_ in (str(p) for p in codepath.parents) # type: ignore[operator]
|
||||
):
|
||||
continue
|
||||
if lineno is not None and x.lineno != lineno:
|
||||
@@ -895,7 +898,7 @@ class FormattedExcinfo:
|
||||
max_frames=max_frames,
|
||||
total=len(traceback),
|
||||
)
|
||||
# Type ignored because adding two instaces of a List subtype
|
||||
# Type ignored because adding two instances of a List subtype
|
||||
# currently incorrectly has type List instead of the subtype.
|
||||
traceback = traceback[:max_frames] + traceback[-max_frames:] # type: ignore
|
||||
else:
|
||||
|
||||
@@ -149,6 +149,11 @@ 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.
|
||||
if isinstance(x, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
for d in x.decorator_list:
|
||||
values.append(d.lineno - 1)
|
||||
values.append(x.lineno - 1)
|
||||
for name in ("finalbody", "orelse"):
|
||||
val: Optional[List[ast.stmt]] = getattr(x, name, None)
|
||||
|
||||
@@ -100,9 +100,6 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
|
||||
spec is None
|
||||
# this is a namespace package (without `__init__.py`)
|
||||
# there's nothing to rewrite there
|
||||
# python3.6: `namespace`
|
||||
# python3.7+: `None`
|
||||
or spec.origin == "namespace"
|
||||
or spec.origin is None
|
||||
# we can only rewrite source files
|
||||
or not isinstance(spec.loader, importlib.machinery.SourceFileLoader)
|
||||
@@ -276,13 +273,15 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
|
||||
with open(pathname, "rb") as f:
|
||||
return f.read()
|
||||
|
||||
if sys.version_info >= (3, 9):
|
||||
if sys.version_info >= (3, 10):
|
||||
|
||||
def get_resource_reader(self, name: str) -> importlib.abc.TraversableResources: # type: ignore
|
||||
from types import SimpleNamespace
|
||||
from importlib.readers import FileReader
|
||||
if sys.version_info < (3, 11):
|
||||
from importlib.readers import FileReader
|
||||
else:
|
||||
from importlib.resources.readers import FileReader
|
||||
|
||||
return FileReader(SimpleNamespace(path=self._rewritten_names[name]))
|
||||
return FileReader(types.SimpleNamespace(path=self._rewritten_names[name]))
|
||||
|
||||
|
||||
def _write_pyc_fp(
|
||||
@@ -293,9 +292,8 @@ def _write_pyc_fp(
|
||||
# import. However, there's little reason to deviate.
|
||||
fp.write(importlib.util.MAGIC_NUMBER)
|
||||
# https://www.python.org/dev/peps/pep-0552/
|
||||
if sys.version_info >= (3, 7):
|
||||
flags = b"\x00\x00\x00\x00"
|
||||
fp.write(flags)
|
||||
flags = b"\x00\x00\x00\x00"
|
||||
fp.write(flags)
|
||||
# as of now, bytecode header expects 32-bit numbers for size and mtime (#4903)
|
||||
mtime = int(source_stat.st_mtime) & 0xFFFFFFFF
|
||||
size = source_stat.st_size & 0xFFFFFFFF
|
||||
@@ -324,7 +322,6 @@ if sys.platform == "win32":
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
else:
|
||||
|
||||
def _write_pyc(
|
||||
@@ -377,31 +374,29 @@ def _read_pyc(
|
||||
except OSError:
|
||||
return None
|
||||
with fp:
|
||||
# https://www.python.org/dev/peps/pep-0552/
|
||||
has_flags = sys.version_info >= (3, 7)
|
||||
try:
|
||||
stat_result = os.stat(source)
|
||||
mtime = int(stat_result.st_mtime)
|
||||
size = stat_result.st_size
|
||||
data = fp.read(16 if has_flags else 12)
|
||||
data = fp.read(16)
|
||||
except OSError as e:
|
||||
trace(f"_read_pyc({source}): OSError {e}")
|
||||
return None
|
||||
# Check for invalid or out of date pyc file.
|
||||
if len(data) != (16 if has_flags else 12):
|
||||
if len(data) != (16):
|
||||
trace("_read_pyc(%s): invalid pyc (too short)" % source)
|
||||
return None
|
||||
if data[:4] != importlib.util.MAGIC_NUMBER:
|
||||
trace("_read_pyc(%s): invalid pyc (bad magic number)" % source)
|
||||
return None
|
||||
if has_flags and data[4:8] != b"\x00\x00\x00\x00":
|
||||
if data[4:8] != b"\x00\x00\x00\x00":
|
||||
trace("_read_pyc(%s): invalid pyc (unsupported flags)" % source)
|
||||
return None
|
||||
mtime_data = data[8 if has_flags else 4 : 12 if has_flags else 8]
|
||||
mtime_data = data[8:12]
|
||||
if int.from_bytes(mtime_data, "little") != mtime & 0xFFFFFFFF:
|
||||
trace("_read_pyc(%s): out of date" % source)
|
||||
return None
|
||||
size_data = data[12 if has_flags else 8 : 16 if has_flags else 12]
|
||||
size_data = data[12:16]
|
||||
if int.from_bytes(size_data, "little") != size & 0xFFFFFFFF:
|
||||
trace("_read_pyc(%s): invalid pyc (incorrect size)" % source)
|
||||
return None
|
||||
@@ -514,7 +509,7 @@ def _call_assertion_pass(lineno: int, orig: str, expl: str) -> None:
|
||||
|
||||
def _check_if_assertion_pass_impl() -> bool:
|
||||
"""Check if any plugins implement the pytest_assertion_pass hook
|
||||
in order not to generate explanation unecessarily (might be expensive)."""
|
||||
in order not to generate explanation unnecessarily (might be expensive)."""
|
||||
return True if util._assertion_pass else False
|
||||
|
||||
|
||||
|
||||
@@ -135,6 +135,27 @@ def isiterable(obj: Any) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def has_default_eq(
|
||||
obj: object,
|
||||
) -> bool:
|
||||
"""Check if an instance of an object contains the default eq
|
||||
|
||||
First, we check if the object's __eq__ attribute has __code__,
|
||||
if so, we check the equally of the method code filename (__code__.co_filename)
|
||||
to the default one generated by the dataclass and attr module
|
||||
for dataclasses the default co_filename is <string>, for attrs class, the __eq__ should contain "attrs eq generated"
|
||||
"""
|
||||
# inspired from https://github.com/willmcgugan/rich/blob/07d51ffc1aee6f16bd2e5a25b4e82850fb9ed778/rich/pretty.py#L68
|
||||
if hasattr(obj.__eq__, "__code__") and hasattr(obj.__eq__.__code__, "co_filename"):
|
||||
code_filename = obj.__eq__.__code__.co_filename
|
||||
|
||||
if isattrs(obj):
|
||||
return "attrs generated eq" in code_filename
|
||||
|
||||
return code_filename == "<string>" # data class
|
||||
return True
|
||||
|
||||
|
||||
def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[str]]:
|
||||
"""Return specialised explanations for some operators/operands."""
|
||||
verbose = config.getoption("verbose")
|
||||
@@ -202,8 +223,6 @@ def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]:
|
||||
explanation = _compare_eq_set(left, right, verbose)
|
||||
elif isdict(left) and isdict(right):
|
||||
explanation = _compare_eq_dict(left, right, verbose)
|
||||
elif verbose > 0:
|
||||
explanation = _compare_eq_verbose(left, right)
|
||||
|
||||
if isiterable(left) and isiterable(right):
|
||||
expl = _compare_eq_iterable(left, right, verbose)
|
||||
@@ -260,18 +279,6 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
|
||||
return explanation
|
||||
|
||||
|
||||
def _compare_eq_verbose(left: Any, right: Any) -> List[str]:
|
||||
keepends = True
|
||||
left_lines = repr(left).splitlines(keepends)
|
||||
right_lines = repr(right).splitlines(keepends)
|
||||
|
||||
explanation: List[str] = []
|
||||
explanation += ["+" + line for line in left_lines]
|
||||
explanation += ["-" + line for line in right_lines]
|
||||
|
||||
return explanation
|
||||
|
||||
|
||||
def _surrounding_parens_on_own_lines(lines: List[str]) -> None:
|
||||
"""Move opening/closing parenthesis/bracket to own lines."""
|
||||
opening = lines[0][:1]
|
||||
@@ -287,8 +294,8 @@ def _surrounding_parens_on_own_lines(lines: List[str]) -> None:
|
||||
def _compare_eq_iterable(
|
||||
left: Iterable[Any], right: Iterable[Any], verbose: int = 0
|
||||
) -> List[str]:
|
||||
if not verbose and not running_on_ci():
|
||||
return ["Use -v to get the full diff"]
|
||||
if verbose <= 0 and not running_on_ci():
|
||||
return ["Use -v to get more diff"]
|
||||
# dynamic import to speedup pytest
|
||||
import difflib
|
||||
|
||||
@@ -427,6 +434,8 @@ def _compare_eq_dict(
|
||||
|
||||
|
||||
def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]:
|
||||
if not has_default_eq(left):
|
||||
return []
|
||||
if isdatacls(left):
|
||||
all_fields = left.__dataclass_fields__
|
||||
fields_to_check = [field for field, info in all_fields.items() if info.compare]
|
||||
|
||||
@@ -68,8 +68,8 @@ def _colorama_workaround() -> None:
|
||||
pass
|
||||
|
||||
|
||||
def _py36_windowsconsoleio_workaround(stream: TextIO) -> None:
|
||||
"""Workaround for Windows Unicode console handling on Python>=3.6.
|
||||
def _windowsconsoleio_workaround(stream: TextIO) -> None:
|
||||
"""Workaround for Windows Unicode console handling.
|
||||
|
||||
Python 3.6 implemented Unicode console handling for Windows. This works
|
||||
by reading/writing to the raw console handle using
|
||||
@@ -112,7 +112,7 @@ def _py36_windowsconsoleio_workaround(stream: TextIO) -> None:
|
||||
buffering = -1
|
||||
|
||||
return io.TextIOWrapper(
|
||||
open(os.dup(f.fileno()), mode, buffering), # type: ignore[arg-type]
|
||||
open(os.dup(f.fileno()), mode, buffering),
|
||||
f.encoding,
|
||||
f.errors,
|
||||
f.newlines,
|
||||
@@ -128,7 +128,7 @@ def _py36_windowsconsoleio_workaround(stream: TextIO) -> None:
|
||||
def pytest_load_initial_conftests(early_config: Config):
|
||||
ns = early_config.known_args_namespace
|
||||
if ns.capture == "fd":
|
||||
_py36_windowsconsoleio_workaround(sys.stdout)
|
||||
_windowsconsoleio_workaround(sys.stdout)
|
||||
_colorama_workaround()
|
||||
pluginmanager = early_config.pluginmanager
|
||||
capman = CaptureManager(ns.capture)
|
||||
|
||||
@@ -4,7 +4,6 @@ import functools
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from inspect import Parameter
|
||||
from inspect import signature
|
||||
from pathlib import Path
|
||||
@@ -186,17 +185,6 @@ def getfuncargnames(
|
||||
return arg_names
|
||||
|
||||
|
||||
if sys.version_info < (3, 7):
|
||||
|
||||
@contextmanager
|
||||
def nullcontext():
|
||||
yield
|
||||
|
||||
|
||||
else:
|
||||
from contextlib import nullcontext as nullcontext # noqa: F401
|
||||
|
||||
|
||||
def get_default_arg_names(function: Callable[..., Any]) -> Tuple[str, ...]:
|
||||
# Note: this code intentionally mirrors the code at the beginning of
|
||||
# getfuncargnames, to get the arguments which were excluded from its result
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Command line options, ini-file and conftest.py processing."""
|
||||
import argparse
|
||||
import collections.abc
|
||||
import contextlib
|
||||
import copy
|
||||
import enum
|
||||
import inspect
|
||||
@@ -255,7 +254,7 @@ default_plugins = essential_plugins + (
|
||||
"warnings",
|
||||
"logging",
|
||||
"reports",
|
||||
"pythonpath",
|
||||
"python_path",
|
||||
*(["unraisableexception", "threadexception"] if sys.version_info >= (3, 8) else []),
|
||||
"faulthandler",
|
||||
)
|
||||
@@ -331,6 +330,14 @@ def _prepareconfig(
|
||||
raise
|
||||
|
||||
|
||||
def _get_directory(path: Path) -> Path:
|
||||
"""Get the directory of a path - itself if already a directory."""
|
||||
if path.is_file():
|
||||
return path.parent
|
||||
else:
|
||||
return path
|
||||
|
||||
|
||||
@final
|
||||
class PytestPluginManager(PluginManager):
|
||||
"""A :py:class:`pluggy.PluginManager <pluggy.PluginManager>` with
|
||||
@@ -345,14 +352,24 @@ class PytestPluginManager(PluginManager):
|
||||
import _pytest.assertion
|
||||
|
||||
super().__init__("pytest")
|
||||
# The objects are module objects, only used generically.
|
||||
self._conftest_plugins: Set[types.ModuleType] = set()
|
||||
|
||||
# State related to local conftest plugins.
|
||||
# -- State related to local conftest plugins.
|
||||
# All loaded conftest modules.
|
||||
self._conftest_plugins: Set[types.ModuleType] = set()
|
||||
# All conftest modules applicable for a directory.
|
||||
# This includes the directory's own conftest modules as well
|
||||
# as those of its parent directories.
|
||||
self._dirpath2confmods: Dict[Path, List[types.ModuleType]] = {}
|
||||
self._conftestpath2mod: Dict[Path, types.ModuleType] = {}
|
||||
# Cutoff directory above which conftests are no longer discovered.
|
||||
self._confcutdir: Optional[Path] = None
|
||||
# If set, conftest loading is skipped.
|
||||
self._noconftest = False
|
||||
|
||||
# _getconftestmodules()'s call to _get_directory() causes a stat
|
||||
# storm when it's called potentially thousands of times in a test
|
||||
# session (#9478), often with the same path, so cache it.
|
||||
self._get_directory = lru_cache(256)(_get_directory)
|
||||
|
||||
self._duplicatepaths: Set[Path] = set()
|
||||
|
||||
# plugins that were explicitly skipped with pytest.skip
|
||||
@@ -514,6 +531,19 @@ class PytestPluginManager(PluginManager):
|
||||
if not foundanchor:
|
||||
self._try_load_conftest(current, namespace.importmode, rootpath)
|
||||
|
||||
def _is_in_confcutdir(self, path: Path) -> bool:
|
||||
"""Whether a path is within the confcutdir.
|
||||
|
||||
When false, should not load conftest.
|
||||
"""
|
||||
if self._confcutdir is None:
|
||||
return True
|
||||
try:
|
||||
path.relative_to(self._confcutdir)
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _try_load_conftest(
|
||||
self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path
|
||||
) -> None:
|
||||
@@ -526,33 +556,28 @@ class PytestPluginManager(PluginManager):
|
||||
|
||||
def _getconftestmodules(
|
||||
self, path: Path, importmode: Union[str, ImportMode], rootpath: Path
|
||||
) -> List[types.ModuleType]:
|
||||
) -> Sequence[types.ModuleType]:
|
||||
if self._noconftest:
|
||||
return []
|
||||
|
||||
if path.is_file():
|
||||
directory = path.parent
|
||||
else:
|
||||
directory = path
|
||||
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:
|
||||
if existing_clist is not None:
|
||||
return existing_clist
|
||||
|
||||
# XXX these days we may rather want to use config.rootpath
|
||||
# and allow users to opt into looking into the rootdir parent
|
||||
# directories instead of requiring to specify confcutdir.
|
||||
clist = []
|
||||
confcutdir_parents = self._confcutdir.parents if self._confcutdir else []
|
||||
for parent in reversed((directory, *directory.parents)):
|
||||
if parent in confcutdir_parents:
|
||||
continue
|
||||
conftestpath = parent / "conftest.py"
|
||||
if conftestpath.is_file():
|
||||
mod = self._importconftest(conftestpath, importmode, rootpath)
|
||||
clist.append(mod)
|
||||
if self._is_in_confcutdir(parent):
|
||||
conftestpath = parent / "conftest.py"
|
||||
if conftestpath.is_file():
|
||||
mod = self._importconftest(conftestpath, importmode, rootpath)
|
||||
clist.append(mod)
|
||||
self._dirpath2confmods[directory] = clist
|
||||
return clist
|
||||
|
||||
@@ -574,15 +599,9 @@ class PytestPluginManager(PluginManager):
|
||||
def _importconftest(
|
||||
self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path
|
||||
) -> types.ModuleType:
|
||||
# Use a resolved Path object as key to avoid loading the same conftest
|
||||
# twice with build systems that create build directories containing
|
||||
# symlinks to actual files.
|
||||
# Using Path().resolve() is better than py.path.realpath because
|
||||
# it resolves to the correct path/drive in case-insensitive file systems (#5792)
|
||||
key = conftestpath.resolve()
|
||||
|
||||
with contextlib.suppress(KeyError):
|
||||
return self._conftestpath2mod[key]
|
||||
existing = self.get_plugin(str(conftestpath))
|
||||
if existing is not None:
|
||||
return cast(types.ModuleType, existing)
|
||||
|
||||
pkgpath = resolve_package_path(conftestpath)
|
||||
if pkgpath is None:
|
||||
@@ -598,11 +617,10 @@ class PytestPluginManager(PluginManager):
|
||||
self._check_non_top_pytest_plugins(mod, conftestpath)
|
||||
|
||||
self._conftest_plugins.add(mod)
|
||||
self._conftestpath2mod[key] = mod
|
||||
dirpath = conftestpath.parent
|
||||
if dirpath in self._dirpath2confmods:
|
||||
for path, mods in self._dirpath2confmods.items():
|
||||
if path and dirpath in path.parents or path == dirpath:
|
||||
if dirpath in path.parents or path == dirpath:
|
||||
assert mod not in mods
|
||||
mods.append(mod)
|
||||
self.trace(f"loading conftestmodule {mod!r}")
|
||||
@@ -970,7 +988,7 @@ class Config:
|
||||
|
||||
def add_cleanup(self, func: Callable[[], None]) -> None:
|
||||
"""Add a function to be called when the config object gets out of
|
||||
use (usually coninciding with pytest_unconfigure)."""
|
||||
use (usually coinciding with pytest_unconfigure)."""
|
||||
self._cleanup.append(func)
|
||||
|
||||
def _do_configure(self) -> None:
|
||||
@@ -1330,14 +1348,6 @@ class Config:
|
||||
if records:
|
||||
frame = sys._getframe(stacklevel - 1)
|
||||
location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name
|
||||
self.hook.pytest_warning_captured.call_historic(
|
||||
kwargs=dict(
|
||||
warning_message=records[0],
|
||||
when="config",
|
||||
item=None,
|
||||
location=location,
|
||||
)
|
||||
)
|
||||
self.hook.pytest_warning_recorded.call_historic(
|
||||
kwargs=dict(
|
||||
warning_message=records[0],
|
||||
@@ -1437,6 +1447,7 @@ class Config:
|
||||
)
|
||||
except KeyError:
|
||||
return None
|
||||
assert mod.__file__ is not None
|
||||
modpath = Path(mod.__file__).parent
|
||||
values: List[Path] = []
|
||||
for relroot in relroots:
|
||||
@@ -1582,7 +1593,7 @@ def _strtobool(val: str) -> bool:
|
||||
@lru_cache(maxsize=50)
|
||||
def parse_warning_filter(
|
||||
arg: str, *, escape: bool
|
||||
) -> Tuple[str, str, Type[Warning], str, int]:
|
||||
) -> Tuple["warnings._ActionKind", str, Type[Warning], str, int]:
|
||||
"""Parse a warnings filter string.
|
||||
|
||||
This is copied from warnings._setoption with the following changes:
|
||||
@@ -1624,7 +1635,7 @@ def parse_warning_filter(
|
||||
parts.append("")
|
||||
action_, message, category_, module, lineno_ = (s.strip() for s in parts)
|
||||
try:
|
||||
action: str = warnings._getaction(action_) # type: ignore[attr-defined]
|
||||
action: "warnings._ActionKind" = warnings._getaction(action_) # type: ignore[attr-defined]
|
||||
except warnings._OptionError as e:
|
||||
raise UsageError(error_template.format(error=str(e)))
|
||||
try:
|
||||
|
||||
@@ -23,7 +23,7 @@ class PathAwareHookProxy:
|
||||
this helper wraps around hook callers
|
||||
until pluggy supports fixingcalls, this one will do
|
||||
|
||||
it currently doesnt return full hook caller proxies for fixed hooks,
|
||||
it currently doesn't return full hook caller proxies for fixed hooks,
|
||||
this may have to be changed later depending on bugs
|
||||
"""
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ def load_config_dict_from_file(
|
||||
try:
|
||||
config = tomli.loads(toml_text)
|
||||
except tomli.TOMLDecodeError as exc:
|
||||
raise UsageError(str(exc)) from exc
|
||||
raise UsageError(f"{filepath}: {exc}") from exc
|
||||
|
||||
result = config.get("tool", {}).get("pytest", {}).get("ini_options", None)
|
||||
if result is not None:
|
||||
|
||||
@@ -11,7 +11,6 @@ in case of warnings which need to format their messages.
|
||||
from warnings import warn
|
||||
|
||||
from _pytest.warning_types import PytestDeprecationWarning
|
||||
from _pytest.warning_types import PytestRemovedIn7Warning
|
||||
from _pytest.warning_types import PytestRemovedIn8Warning
|
||||
from _pytest.warning_types import UnformattedWarning
|
||||
|
||||
@@ -24,18 +23,6 @@ DEPRECATED_EXTERNAL_PLUGINS = {
|
||||
}
|
||||
|
||||
|
||||
FILLFUNCARGS = UnformattedWarning(
|
||||
PytestRemovedIn7Warning,
|
||||
"{name} is deprecated, use "
|
||||
"function._request._fillfixtures() instead if you cannot avoid reaching into internals.",
|
||||
)
|
||||
|
||||
PYTEST_COLLECT_MODULE = UnformattedWarning(
|
||||
PytestRemovedIn7Warning,
|
||||
"pytest.collect.{name} was moved to pytest.{name}\n"
|
||||
"Please update to the new name.",
|
||||
)
|
||||
|
||||
# This can be* removed pytest 8, but it's harmless and common, so no rush to remove.
|
||||
# * If you're in the future: "could have been".
|
||||
YIELD_FIXTURE = PytestDeprecationWarning(
|
||||
@@ -43,20 +30,6 @@ YIELD_FIXTURE = PytestDeprecationWarning(
|
||||
"Use @pytest.fixture instead; they are the same."
|
||||
)
|
||||
|
||||
MINUS_K_DASH = PytestRemovedIn7Warning(
|
||||
"The `-k '-expr'` syntax to -k is deprecated.\nUse `-k 'not expr'` instead."
|
||||
)
|
||||
|
||||
MINUS_K_COLON = PytestRemovedIn7Warning(
|
||||
"The `-k 'expr:'` syntax to -k is deprecated.\n"
|
||||
"Please open an issue if you use this and want a replacement."
|
||||
)
|
||||
|
||||
WARNING_CAPTURED_HOOK = PytestRemovedIn7Warning(
|
||||
"The pytest_warning_captured is deprecated and will be removed in a future release.\n"
|
||||
"Please use pytest_warning_recorded instead."
|
||||
)
|
||||
|
||||
WARNING_CMDLINE_PREPARSE_HOOK = PytestRemovedIn8Warning(
|
||||
"The pytest_cmdline_preparse hook is deprecated and will be removed in a future release. \n"
|
||||
"Please use pytest_load_initial_conftests hook instead."
|
||||
@@ -74,11 +47,6 @@ STRICT_OPTION = PytestRemovedIn8Warning(
|
||||
# This deprecation is never really meant to be removed.
|
||||
PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.")
|
||||
|
||||
UNITTEST_SKIP_DURING_COLLECTION = PytestRemovedIn8Warning(
|
||||
"Raising unittest.SkipTest to skip tests during collection is deprecated. "
|
||||
"Use pytest.skip() instead."
|
||||
)
|
||||
|
||||
ARGUMENT_PERCENT_DEFAULT = PytestRemovedIn8Warning(
|
||||
'pytest now uses argparse. "%default" should be changed to "%(default)s"',
|
||||
)
|
||||
@@ -115,8 +83,10 @@ NODE_CTOR_FSPATH_ARG = UnformattedWarning(
|
||||
)
|
||||
|
||||
WARNS_NONE_ARG = PytestRemovedIn8Warning(
|
||||
"Passing None to catch any warning has been deprecated, pass no arguments instead:\n"
|
||||
" Replace pytest.warns(None) by simply pytest.warns()."
|
||||
"Passing None has been deprecated.\n"
|
||||
"See https://docs.pytest.org/en/latest/how-to/capture-warnings.html"
|
||||
"#additional-use-cases-of-warnings-in-tests"
|
||||
" for alternatives in common use cases."
|
||||
)
|
||||
|
||||
KEYWORD_MSG_ARG = UnformattedWarning(
|
||||
|
||||
@@ -656,7 +656,7 @@ def _init_checker_class() -> Type["doctest.OutputChecker"]:
|
||||
precision = 0 if fraction is None else len(fraction)
|
||||
if exponent is not None:
|
||||
precision -= int(exponent)
|
||||
if float(w.group()) == approx(float(g.group()), abs=10 ** -precision):
|
||||
if float(w.group()) == approx(float(g.group()), abs=10**-precision):
|
||||
# They're close enough. Replace the text we actually
|
||||
# got with the text we want, so that it will match when we
|
||||
# check the string literally.
|
||||
|
||||
@@ -52,7 +52,6 @@ from _pytest.config import _PluggyPlugin
|
||||
from _pytest.config import Config
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.deprecated import check_ispytest
|
||||
from _pytest.deprecated import FILLFUNCARGS
|
||||
from _pytest.deprecated import YIELD_FIXTURE
|
||||
from _pytest.mark import Mark
|
||||
from _pytest.mark import ParameterSet
|
||||
@@ -73,7 +72,6 @@ if TYPE_CHECKING:
|
||||
from _pytest.scope import _ScopeName
|
||||
from _pytest.main import Session
|
||||
from _pytest.python import CallSpec2
|
||||
from _pytest.python import Function
|
||||
from _pytest.python import Metafunc
|
||||
|
||||
|
||||
@@ -352,41 +350,6 @@ def reorder_items_atscope(
|
||||
return items_done
|
||||
|
||||
|
||||
def _fillfuncargs(function: "Function") -> None:
|
||||
"""Fill missing fixtures for a test function, old public API (deprecated)."""
|
||||
warnings.warn(FILLFUNCARGS.format(name="pytest._fillfuncargs()"), stacklevel=2)
|
||||
_fill_fixtures_impl(function)
|
||||
|
||||
|
||||
def fillfixtures(function: "Function") -> None:
|
||||
"""Fill missing fixtures for a test function (deprecated)."""
|
||||
warnings.warn(
|
||||
FILLFUNCARGS.format(name="_pytest.fixtures.fillfixtures()"), stacklevel=2
|
||||
)
|
||||
_fill_fixtures_impl(function)
|
||||
|
||||
|
||||
def _fill_fixtures_impl(function: "Function") -> None:
|
||||
"""Internal implementation to fill fixtures on the given function object."""
|
||||
try:
|
||||
request = function._request
|
||||
except AttributeError:
|
||||
# XXX this special code path is only expected to execute
|
||||
# with the oejskit plugin. It uses classes with funcargs
|
||||
# and we thus have to work a bit to allow this.
|
||||
fm = function.session._fixturemanager
|
||||
assert function.parent is not None
|
||||
fi = fm.getfixtureinfo(function.parent, function.obj, None)
|
||||
function._fixtureinfo = fi
|
||||
request = function._request = FixtureRequest(function, _ispytest=True)
|
||||
fm.session._setupstate.setup(function)
|
||||
request._fillfixtures()
|
||||
# Prune out funcargs for jstests.
|
||||
function.funcargs = {name: function.funcargs[name] for name in fi.argnames}
|
||||
else:
|
||||
request._fillfixtures()
|
||||
|
||||
|
||||
def get_direct_param_fixture_func(request):
|
||||
return request.param
|
||||
|
||||
@@ -634,8 +597,17 @@ class FixtureRequest:
|
||||
funcitem = self._pyfuncitem
|
||||
scope = fixturedef._scope
|
||||
try:
|
||||
param = funcitem.callspec.getparam(argname)
|
||||
except (AttributeError, ValueError):
|
||||
callspec = funcitem.callspec
|
||||
except AttributeError:
|
||||
callspec = None
|
||||
if callspec is not None and argname in callspec.params:
|
||||
param = callspec.params[argname]
|
||||
param_index = callspec.indices[argname]
|
||||
# If a parametrize invocation set a scope it will override
|
||||
# the static scope defined with the fixture function.
|
||||
with suppress(KeyError):
|
||||
scope = callspec._arg2scope[argname]
|
||||
else:
|
||||
param = NOTSET
|
||||
param_index = 0
|
||||
has_params = fixturedef.params is not None
|
||||
@@ -675,12 +647,6 @@ class FixtureRequest:
|
||||
)
|
||||
)
|
||||
fail(msg, pytrace=False)
|
||||
else:
|
||||
param_index = funcitem.callspec.indices[argname]
|
||||
# If a parametrize invocation set a scope it will override
|
||||
# the static scope defined with the fixture function.
|
||||
with suppress(KeyError):
|
||||
scope = funcitem.callspec._arg2scope[argname]
|
||||
|
||||
subrequest = SubRequest(
|
||||
self, scope, param, param_index, fixturedef, _ispytest=True
|
||||
@@ -964,7 +930,7 @@ def _eval_scope_callable(
|
||||
|
||||
@final
|
||||
class FixtureDef(Generic[FixtureValue]):
|
||||
"""A container for a factory definition."""
|
||||
"""A container for a fixture definition."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -976,33 +942,56 @@ class FixtureDef(Generic[FixtureValue]):
|
||||
params: Optional[Sequence[object]],
|
||||
unittest: bool = False,
|
||||
ids: Optional[
|
||||
Union[
|
||||
Tuple[Union[None, str, float, int, bool], ...],
|
||||
Callable[[Any], Optional[object]],
|
||||
]
|
||||
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
|
||||
] = None,
|
||||
) -> None:
|
||||
self._fixturemanager = fixturemanager
|
||||
# The "base" node ID for the fixture.
|
||||
#
|
||||
# This is a node ID prefix. A fixture is only available to a node (e.g.
|
||||
# a `Function` item) if the fixture's baseid is a parent of the node's
|
||||
# nodeid (see the `iterparentnodeids` function for what constitutes a
|
||||
# "parent" and a "prefix" in this context).
|
||||
#
|
||||
# For a fixture found in a Collector's object (e.g. a `Module`s module,
|
||||
# a `Class`'s class), the baseid is the Collector's nodeid.
|
||||
#
|
||||
# For a fixture found in a conftest plugin, the baseid is the conftest's
|
||||
# directory path relative to the rootdir.
|
||||
#
|
||||
# For other plugins, the baseid is the empty string (always matches).
|
||||
self.baseid = baseid or ""
|
||||
# Whether the fixture was found from a node or a conftest in the
|
||||
# collection tree. Will be false for fixtures defined in non-conftest
|
||||
# plugins.
|
||||
self.has_location = baseid is not None
|
||||
# The fixture factory function.
|
||||
self.func = func
|
||||
# The name by which the fixture may be requested.
|
||||
self.argname = argname
|
||||
if scope is None:
|
||||
scope = Scope.Function
|
||||
elif callable(scope):
|
||||
scope = _eval_scope_callable(scope, argname, fixturemanager.config)
|
||||
|
||||
if isinstance(scope, str):
|
||||
scope = Scope.from_user(
|
||||
scope, descr=f"Fixture '{func.__name__}'", where=baseid
|
||||
)
|
||||
self._scope = scope
|
||||
# If the fixture is directly parametrized, the parameter values.
|
||||
self.params: Optional[Sequence[object]] = params
|
||||
self.argnames: Tuple[str, ...] = getfuncargnames(
|
||||
func, name=argname, is_method=unittest
|
||||
)
|
||||
self.unittest = unittest
|
||||
# If the fixture is directly parametrized, a tuple of explicit IDs to
|
||||
# assign to the parameter values, or a callable to generate an ID given
|
||||
# a parameter value.
|
||||
self.ids = ids
|
||||
# The names requested by the fixtures.
|
||||
self.argnames = getfuncargnames(func, name=argname, is_method=unittest)
|
||||
# Whether the fixture was collected from a unittest TestCase class.
|
||||
# Note that it really only makes sense to define autouse fixtures in
|
||||
# unittest TestCases.
|
||||
self.unittest = unittest
|
||||
# If the fixture was executed, the current value of the fixture.
|
||||
# Can change if the fixture is executed with different parameters.
|
||||
self.cached_result: Optional[_FixtureCachedResult[FixtureValue]] = None
|
||||
self._finalizers: List[Callable[[], object]] = []
|
||||
|
||||
@@ -1029,8 +1018,8 @@ class FixtureDef(Generic[FixtureValue]):
|
||||
if exc:
|
||||
raise exc
|
||||
finally:
|
||||
hook = self._fixturemanager.session.gethookproxy(request.node.path)
|
||||
hook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
|
||||
ihook = request.node.ihook
|
||||
ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
|
||||
# Even if finalization fails, we invalidate the cached fixture
|
||||
# value and remove all finalizers because they may be bound methods
|
||||
# which will keep instances alive.
|
||||
@@ -1064,8 +1053,8 @@ class FixtureDef(Generic[FixtureValue]):
|
||||
self.finish(request)
|
||||
assert self.cached_result is None
|
||||
|
||||
hook = self._fixturemanager.session.gethookproxy(request.node.path)
|
||||
result = hook.pytest_fixture_setup(fixturedef=self, request=request)
|
||||
ihook = request.node.ihook
|
||||
result = ihook.pytest_fixture_setup(fixturedef=self, request=request)
|
||||
return result
|
||||
|
||||
def cache_key(self, request: SubRequest) -> object:
|
||||
@@ -1130,18 +1119,8 @@ def pytest_fixture_setup(
|
||||
|
||||
|
||||
def _ensure_immutable_ids(
|
||||
ids: Optional[
|
||||
Union[
|
||||
Iterable[Union[None, str, float, int, bool]],
|
||||
Callable[[Any], Optional[object]],
|
||||
]
|
||||
],
|
||||
) -> Optional[
|
||||
Union[
|
||||
Tuple[Union[None, str, float, int, bool], ...],
|
||||
Callable[[Any], Optional[object]],
|
||||
]
|
||||
]:
|
||||
ids: Optional[Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]]
|
||||
) -> Optional[Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]]:
|
||||
if ids is None:
|
||||
return None
|
||||
if callable(ids):
|
||||
@@ -1185,9 +1164,8 @@ class FixtureFunctionMarker:
|
||||
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]"
|
||||
params: Optional[Tuple[object, ...]] = attr.ib(converter=_params_converter)
|
||||
autouse: bool = False
|
||||
ids: Union[
|
||||
Tuple[Union[None, str, float, int, bool], ...],
|
||||
Callable[[Any], Optional[object]],
|
||||
ids: Optional[
|
||||
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
|
||||
] = attr.ib(
|
||||
default=None,
|
||||
converter=_ensure_immutable_ids,
|
||||
@@ -1228,10 +1206,7 @@ def fixture(
|
||||
params: Optional[Iterable[object]] = ...,
|
||||
autouse: bool = ...,
|
||||
ids: Optional[
|
||||
Union[
|
||||
Iterable[Union[None, str, float, int, bool]],
|
||||
Callable[[Any], Optional[object]],
|
||||
]
|
||||
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
|
||||
] = ...,
|
||||
name: Optional[str] = ...,
|
||||
) -> FixtureFunction:
|
||||
@@ -1246,10 +1221,7 @@ def fixture(
|
||||
params: Optional[Iterable[object]] = ...,
|
||||
autouse: bool = ...,
|
||||
ids: Optional[
|
||||
Union[
|
||||
Iterable[Union[None, str, float, int, bool]],
|
||||
Callable[[Any], Optional[object]],
|
||||
]
|
||||
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
|
||||
] = ...,
|
||||
name: Optional[str] = None,
|
||||
) -> FixtureFunctionMarker:
|
||||
@@ -1263,10 +1235,7 @@ def fixture(
|
||||
params: Optional[Iterable[object]] = None,
|
||||
autouse: bool = False,
|
||||
ids: Optional[
|
||||
Union[
|
||||
Iterable[Union[None, str, float, int, bool]],
|
||||
Callable[[Any], Optional[object]],
|
||||
]
|
||||
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
|
||||
] = None,
|
||||
name: Optional[str] = None,
|
||||
) -> Union[FixtureFunctionMarker, FixtureFunction]:
|
||||
@@ -1308,7 +1277,7 @@ def fixture(
|
||||
the fixture.
|
||||
|
||||
:param ids:
|
||||
List of string ids each corresponding to the params so that they are
|
||||
Sequence of ids each corresponding to the params so that they are
|
||||
part of the test id. If no ids are provided they will be generated
|
||||
automatically from the params.
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ from typing import Union
|
||||
|
||||
from pluggy import HookspecMarker
|
||||
|
||||
from _pytest.deprecated import WARNING_CAPTURED_HOOK
|
||||
from _pytest.deprecated import WARNING_CMDLINE_PREPARSE_HOOK
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -34,10 +33,10 @@ if TYPE_CHECKING:
|
||||
from _pytest.nodes import Collector
|
||||
from _pytest.nodes import Item
|
||||
from _pytest.outcomes import Exit
|
||||
from _pytest.python import Class
|
||||
from _pytest.python import Function
|
||||
from _pytest.python import Metafunc
|
||||
from _pytest.python import Module
|
||||
from _pytest.python import PyCollector
|
||||
from _pytest.reports import CollectReport
|
||||
from _pytest.reports import TestReport
|
||||
from _pytest.runner import CallInfo
|
||||
@@ -163,7 +162,7 @@ def pytest_cmdline_preparse(config: "Config", args: List[str]) -> None:
|
||||
"""(**Deprecated**) modify command line arguments before option parsing.
|
||||
|
||||
This hook is considered deprecated and will be removed in a future pytest version. Consider
|
||||
using :func:`pytest_load_initial_conftests` instead.
|
||||
using :hook:`pytest_load_initial_conftests` instead.
|
||||
|
||||
.. note::
|
||||
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
|
||||
@@ -360,7 +359,7 @@ def pytest_pycollect_makemodule(
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_pycollect_makeitem(
|
||||
collector: "PyCollector", name: str, obj: object
|
||||
collector: Union["Module", "Class"], name: str, obj: object
|
||||
) -> Union[None, "Item", "Collector", List[Union["Item", "Collector"]]]:
|
||||
"""Return a custom item/collector for a Python object in a module, or None.
|
||||
|
||||
@@ -467,7 +466,7 @@ def pytest_runtest_logstart(
|
||||
) -> None:
|
||||
"""Called at the start of running the runtest protocol for a single item.
|
||||
|
||||
See :func:`pytest_runtest_protocol` for a description of the runtest protocol.
|
||||
See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
|
||||
|
||||
:param str nodeid: Full node ID of the item.
|
||||
:param location: A tuple of ``(filename, lineno, testname)``.
|
||||
@@ -479,7 +478,7 @@ def pytest_runtest_logfinish(
|
||||
) -> None:
|
||||
"""Called at the end of running the runtest protocol for a single item.
|
||||
|
||||
See :func:`pytest_runtest_protocol` for a description of the runtest protocol.
|
||||
See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
|
||||
|
||||
:param str nodeid: Full node ID of the item.
|
||||
:param location: A tuple of ``(filename, lineno, testname)``.
|
||||
@@ -526,7 +525,7 @@ def pytest_runtest_makereport(
|
||||
"""Called to create a :class:`~pytest.TestReport` for each of
|
||||
the setup, call and teardown runtest phases of a test item.
|
||||
|
||||
See :func:`pytest_runtest_protocol` for a description of the runtest protocol.
|
||||
See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
|
||||
|
||||
:param call: The :class:`~pytest.CallInfo` for the phase.
|
||||
|
||||
@@ -538,7 +537,7 @@ def pytest_runtest_logreport(report: "TestReport") -> None:
|
||||
"""Process the :class:`~pytest.TestReport` produced for each
|
||||
of the setup, call and teardown runtest phases of an item.
|
||||
|
||||
See :func:`pytest_runtest_protocol` for a description of the runtest protocol.
|
||||
See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
|
||||
"""
|
||||
|
||||
|
||||
@@ -557,7 +556,7 @@ def pytest_report_from_serializable(
|
||||
data: Dict[str, Any],
|
||||
) -> Optional[Union["CollectReport", "TestReport"]]:
|
||||
"""Restore a report object previously serialized with
|
||||
:func:`pytest_report_to_serializable`."""
|
||||
:hook:`pytest_report_to_serializable`."""
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -777,41 +776,6 @@ def pytest_terminal_summary(
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(historic=True, warn_on_impl=WARNING_CAPTURED_HOOK)
|
||||
def pytest_warning_captured(
|
||||
warning_message: "warnings.WarningMessage",
|
||||
when: "Literal['config', 'collect', 'runtest']",
|
||||
item: Optional["Item"],
|
||||
location: Optional[Tuple[str, int, str]],
|
||||
) -> None:
|
||||
"""(**Deprecated**) Process a warning captured by the internal pytest warnings plugin.
|
||||
|
||||
.. deprecated:: 6.0
|
||||
|
||||
This hook is considered deprecated and will be removed in a future pytest version.
|
||||
Use :func:`pytest_warning_recorded` instead.
|
||||
|
||||
:param warnings.WarningMessage warning_message:
|
||||
The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains
|
||||
the same attributes as the parameters of :py:func:`warnings.showwarning`.
|
||||
|
||||
:param str when:
|
||||
Indicates when the warning was captured. Possible values:
|
||||
|
||||
* ``"config"``: during pytest configuration/initialization stage.
|
||||
* ``"collect"``: during test collection.
|
||||
* ``"runtest"``: during test execution.
|
||||
|
||||
:param pytest.Item|None item:
|
||||
The item being executed if ``when`` is ``"runtest"``, otherwise ``None``.
|
||||
|
||||
:param tuple location:
|
||||
When available, holds information about the execution context of the captured
|
||||
warning (filename, linenumber, function). ``function`` evaluates to <module>
|
||||
when the execution context is at the module level.
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(historic=True)
|
||||
def pytest_warning_recorded(
|
||||
warning_message: "warnings.WarningMessage",
|
||||
@@ -895,10 +859,10 @@ def pytest_exception_interact(
|
||||
"""Called when an exception was raised which can potentially be
|
||||
interactively handled.
|
||||
|
||||
May be called during collection (see :py:func:`pytest_make_collect_report`),
|
||||
May be called during collection (see :hook:`pytest_make_collect_report`),
|
||||
in which case ``report`` is a :class:`CollectReport`.
|
||||
|
||||
May be called during runtest of an item (see :py:func:`pytest_runtest_protocol`),
|
||||
May be called during runtest of an item (see :hook:`pytest_runtest_protocol`),
|
||||
in which case ``report`` is a :class:`TestReport`.
|
||||
|
||||
This hook is not called if the exception that was raised is an internal
|
||||
|
||||
@@ -92,7 +92,7 @@ class _NodeReporter:
|
||||
self.xml = xml
|
||||
self.add_stats = self.xml.add_stats
|
||||
self.family = self.xml.family
|
||||
self.duration = 0
|
||||
self.duration = 0.0
|
||||
self.properties: List[Tuple[str, str]] = []
|
||||
self.nodes: List[ET.Element] = []
|
||||
self.attrs: Dict[str, str] = {}
|
||||
@@ -256,7 +256,7 @@ class _NodeReporter:
|
||||
def finalize(self) -> None:
|
||||
data = self.to_xml()
|
||||
self.__dict__.clear()
|
||||
# Type ignored becuase mypy doesn't like overriding a method.
|
||||
# Type ignored because mypy doesn't like overriding a method.
|
||||
# Also the return value doesn't match...
|
||||
self.to_xml = lambda: data # type: ignore[assignment]
|
||||
|
||||
|
||||
@@ -10,13 +10,26 @@ from typing import Union
|
||||
import attr
|
||||
from iniconfig import SectionWrapper
|
||||
|
||||
import pytest
|
||||
from _pytest.cacheprovider import Cache
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import LEGACY_PATH
|
||||
from _pytest.compat import legacy_path
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.config import PytestPluginManager
|
||||
from _pytest.deprecated import check_ispytest
|
||||
from _pytest.fixtures import fixture
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
from _pytest.main import Session
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
from _pytest.nodes import Collector
|
||||
from _pytest.nodes import Item
|
||||
from _pytest.nodes import Node
|
||||
from _pytest.pytester import HookRecorder
|
||||
from _pytest.pytester import Pytester
|
||||
from _pytest.pytester import RunResult
|
||||
from _pytest.terminal import TerminalReporter
|
||||
from _pytest.tmpdir import TempPathFactory
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Final
|
||||
@@ -35,10 +48,10 @@ class Testdir:
|
||||
|
||||
__test__ = False
|
||||
|
||||
CLOSE_STDIN: "Final" = pytest.Pytester.CLOSE_STDIN
|
||||
TimeoutExpired: "Final" = pytest.Pytester.TimeoutExpired
|
||||
CLOSE_STDIN: "Final" = Pytester.CLOSE_STDIN
|
||||
TimeoutExpired: "Final" = Pytester.TimeoutExpired
|
||||
|
||||
def __init__(self, pytester: pytest.Pytester, *, _ispytest: bool = False) -> None:
|
||||
def __init__(self, pytester: Pytester, *, _ispytest: bool = False) -> None:
|
||||
check_ispytest(_ispytest)
|
||||
self._pytester = pytester
|
||||
|
||||
@@ -64,10 +77,10 @@ class Testdir:
|
||||
self._pytester.plugins = plugins
|
||||
|
||||
@property
|
||||
def monkeypatch(self) -> pytest.MonkeyPatch:
|
||||
def monkeypatch(self) -> MonkeyPatch:
|
||||
return self._pytester._monkeypatch
|
||||
|
||||
def make_hook_recorder(self, pluginmanager) -> pytest.HookRecorder:
|
||||
def make_hook_recorder(self, pluginmanager) -> HookRecorder:
|
||||
"""See :meth:`Pytester.make_hook_recorder`."""
|
||||
return self._pytester.make_hook_recorder(pluginmanager)
|
||||
|
||||
@@ -131,9 +144,7 @@ class Testdir:
|
||||
"""See :meth:`Pytester.copy_example`."""
|
||||
return legacy_path(self._pytester.copy_example(name))
|
||||
|
||||
def getnode(
|
||||
self, config: pytest.Config, arg
|
||||
) -> Optional[Union[pytest.Item, pytest.Collector]]:
|
||||
def getnode(self, config: Config, arg) -> Optional[Union[Item, Collector]]:
|
||||
"""See :meth:`Pytester.getnode`."""
|
||||
return self._pytester.getnode(config, arg)
|
||||
|
||||
@@ -141,9 +152,7 @@ class Testdir:
|
||||
"""See :meth:`Pytester.getpathnode`."""
|
||||
return self._pytester.getpathnode(path)
|
||||
|
||||
def genitems(
|
||||
self, colitems: List[Union[pytest.Item, pytest.Collector]]
|
||||
) -> List[pytest.Item]:
|
||||
def genitems(self, colitems: List[Union[Item, Collector]]) -> List[Item]:
|
||||
"""See :meth:`Pytester.genitems`."""
|
||||
return self._pytester.genitems(colitems)
|
||||
|
||||
@@ -165,19 +174,19 @@ class Testdir:
|
||||
*args, plugins=plugins, no_reraise_ctrlc=no_reraise_ctrlc
|
||||
)
|
||||
|
||||
def runpytest_inprocess(self, *args, **kwargs) -> pytest.RunResult:
|
||||
def runpytest_inprocess(self, *args, **kwargs) -> RunResult:
|
||||
"""See :meth:`Pytester.runpytest_inprocess`."""
|
||||
return self._pytester.runpytest_inprocess(*args, **kwargs)
|
||||
|
||||
def runpytest(self, *args, **kwargs) -> pytest.RunResult:
|
||||
def runpytest(self, *args, **kwargs) -> RunResult:
|
||||
"""See :meth:`Pytester.runpytest`."""
|
||||
return self._pytester.runpytest(*args, **kwargs)
|
||||
|
||||
def parseconfig(self, *args) -> pytest.Config:
|
||||
def parseconfig(self, *args) -> Config:
|
||||
"""See :meth:`Pytester.parseconfig`."""
|
||||
return self._pytester.parseconfig(*args)
|
||||
|
||||
def parseconfigure(self, *args) -> pytest.Config:
|
||||
def parseconfigure(self, *args) -> Config:
|
||||
"""See :meth:`Pytester.parseconfigure`."""
|
||||
return self._pytester.parseconfigure(*args)
|
||||
|
||||
@@ -196,8 +205,8 @@ class Testdir:
|
||||
)
|
||||
|
||||
def collect_by_name(
|
||||
self, modcol: pytest.Collector, name: str
|
||||
) -> Optional[Union[pytest.Item, pytest.Collector]]:
|
||||
self, modcol: Collector, name: str
|
||||
) -> Optional[Union[Item, Collector]]:
|
||||
"""See :meth:`Pytester.collect_by_name`."""
|
||||
return self._pytester.collect_by_name(modcol, name)
|
||||
|
||||
@@ -212,11 +221,11 @@ class Testdir:
|
||||
"""See :meth:`Pytester.popen`."""
|
||||
return self._pytester.popen(cmdargs, stdout, stderr, stdin, **kw)
|
||||
|
||||
def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN) -> pytest.RunResult:
|
||||
def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN) -> RunResult:
|
||||
"""See :meth:`Pytester.run`."""
|
||||
return self._pytester.run(*cmdargs, timeout=timeout, stdin=stdin)
|
||||
|
||||
def runpython(self, script) -> pytest.RunResult:
|
||||
def runpython(self, script) -> RunResult:
|
||||
"""See :meth:`Pytester.runpython`."""
|
||||
return self._pytester.runpython(script)
|
||||
|
||||
@@ -224,7 +233,7 @@ class Testdir:
|
||||
"""See :meth:`Pytester.runpython_c`."""
|
||||
return self._pytester.runpython_c(command)
|
||||
|
||||
def runpytest_subprocess(self, *args, timeout=None) -> pytest.RunResult:
|
||||
def runpytest_subprocess(self, *args, timeout=None) -> RunResult:
|
||||
"""See :meth:`Pytester.runpytest_subprocess`."""
|
||||
return self._pytester.runpytest_subprocess(*args, timeout=timeout)
|
||||
|
||||
@@ -245,13 +254,10 @@ class Testdir:
|
||||
return str(self.tmpdir)
|
||||
|
||||
|
||||
pytest.Testdir = Testdir # type: ignore[attr-defined]
|
||||
|
||||
|
||||
class LegacyTestdirPlugin:
|
||||
@staticmethod
|
||||
@pytest.fixture
|
||||
def testdir(pytester: pytest.Pytester) -> Testdir:
|
||||
@fixture
|
||||
def testdir(pytester: Pytester) -> Testdir:
|
||||
"""
|
||||
Identical to :fixture:`pytester`, and provides an instance whose methods return
|
||||
legacy ``LEGACY_PATH`` objects instead when applicable.
|
||||
@@ -267,10 +273,10 @@ class TempdirFactory:
|
||||
"""Backward compatibility wrapper that implements :class:``_pytest.compat.LEGACY_PATH``
|
||||
for :class:``TempPathFactory``."""
|
||||
|
||||
_tmppath_factory: pytest.TempPathFactory
|
||||
_tmppath_factory: TempPathFactory
|
||||
|
||||
def __init__(
|
||||
self, tmppath_factory: pytest.TempPathFactory, *, _ispytest: bool = False
|
||||
self, tmppath_factory: TempPathFactory, *, _ispytest: bool = False
|
||||
) -> None:
|
||||
check_ispytest(_ispytest)
|
||||
self._tmppath_factory = tmppath_factory
|
||||
@@ -284,19 +290,16 @@ class TempdirFactory:
|
||||
return legacy_path(self._tmppath_factory.getbasetemp().resolve())
|
||||
|
||||
|
||||
pytest.TempdirFactory = TempdirFactory # type: ignore[attr-defined]
|
||||
|
||||
|
||||
class LegacyTmpdirPlugin:
|
||||
@staticmethod
|
||||
@pytest.fixture(scope="session")
|
||||
def tmpdir_factory(request: pytest.FixtureRequest) -> TempdirFactory:
|
||||
@fixture(scope="session")
|
||||
def tmpdir_factory(request: FixtureRequest) -> TempdirFactory:
|
||||
"""Return a :class:`pytest.TempdirFactory` instance for the test session."""
|
||||
# Set dynamically by pytest_configure().
|
||||
return request.config._tmpdirhandler # type: ignore
|
||||
|
||||
@staticmethod
|
||||
@pytest.fixture
|
||||
@fixture
|
||||
def tmpdir(tmp_path: Path) -> LEGACY_PATH:
|
||||
"""Return a temporary directory path object which is unique to each test
|
||||
function invocation, created as a sub directory of the base temporary
|
||||
@@ -314,7 +317,7 @@ class LegacyTmpdirPlugin:
|
||||
return legacy_path(tmp_path)
|
||||
|
||||
|
||||
def Cache_makedir(self: pytest.Cache, name: str) -> LEGACY_PATH:
|
||||
def Cache_makedir(self: Cache, name: str) -> LEGACY_PATH:
|
||||
"""Return a directory path object with the given name.
|
||||
|
||||
Same as :func:`mkdir`, but returns a legacy py path instance.
|
||||
@@ -322,7 +325,7 @@ def Cache_makedir(self: pytest.Cache, name: str) -> LEGACY_PATH:
|
||||
return legacy_path(self.mkdir(name))
|
||||
|
||||
|
||||
def FixtureRequest_fspath(self: pytest.FixtureRequest) -> LEGACY_PATH:
|
||||
def FixtureRequest_fspath(self: FixtureRequest) -> LEGACY_PATH:
|
||||
"""(deprecated) The file system path of the test module which collected this test."""
|
||||
return legacy_path(self.path)
|
||||
|
||||
@@ -337,7 +340,7 @@ def TerminalReporter_startdir(self: TerminalReporter) -> LEGACY_PATH:
|
||||
return legacy_path(self.startpath)
|
||||
|
||||
|
||||
def Config_invocation_dir(self: pytest.Config) -> LEGACY_PATH:
|
||||
def Config_invocation_dir(self: Config) -> LEGACY_PATH:
|
||||
"""The directory from which pytest was invoked.
|
||||
|
||||
Prefer to use :attr:`invocation_params.dir <InvocationParams.dir>`,
|
||||
@@ -348,7 +351,7 @@ def Config_invocation_dir(self: pytest.Config) -> LEGACY_PATH:
|
||||
return legacy_path(str(self.invocation_params.dir))
|
||||
|
||||
|
||||
def Config_rootdir(self: pytest.Config) -> LEGACY_PATH:
|
||||
def Config_rootdir(self: Config) -> LEGACY_PATH:
|
||||
"""The path to the :ref:`rootdir <rootdir>`.
|
||||
|
||||
Prefer to use :attr:`rootpath`, which is a :class:`pathlib.Path`.
|
||||
@@ -358,7 +361,7 @@ def Config_rootdir(self: pytest.Config) -> LEGACY_PATH:
|
||||
return legacy_path(str(self.rootpath))
|
||||
|
||||
|
||||
def Config_inifile(self: pytest.Config) -> Optional[LEGACY_PATH]:
|
||||
def Config_inifile(self: Config) -> Optional[LEGACY_PATH]:
|
||||
"""The path to the :ref:`configfile <configfiles>`.
|
||||
|
||||
Prefer to use :attr:`inipath`, which is a :class:`pathlib.Path`.
|
||||
@@ -368,7 +371,7 @@ def Config_inifile(self: pytest.Config) -> Optional[LEGACY_PATH]:
|
||||
return legacy_path(str(self.inipath)) if self.inipath else None
|
||||
|
||||
|
||||
def Session_stardir(self: pytest.Session) -> LEGACY_PATH:
|
||||
def Session_stardir(self: Session) -> LEGACY_PATH:
|
||||
"""The path from which pytest was invoked.
|
||||
|
||||
Prefer to use ``startpath`` which is a :class:`pathlib.Path`.
|
||||
@@ -400,12 +403,44 @@ def Node_fspath_set(self: Node, value: LEGACY_PATH) -> None:
|
||||
self.path = Path(value)
|
||||
|
||||
|
||||
@pytest.hookimpl
|
||||
def pytest_configure(config: pytest.Config) -> None:
|
||||
mp = pytest.MonkeyPatch()
|
||||
config.add_cleanup(mp.undo)
|
||||
@hookimpl(tryfirst=True)
|
||||
def pytest_load_initial_conftests(early_config: Config) -> None:
|
||||
"""Monkeypatch legacy path attributes in several classes, as early as possible."""
|
||||
mp = MonkeyPatch()
|
||||
early_config.add_cleanup(mp.undo)
|
||||
|
||||
# Add Cache.makedir().
|
||||
mp.setattr(Cache, "makedir", Cache_makedir, raising=False)
|
||||
|
||||
# Add FixtureRequest.fspath property.
|
||||
mp.setattr(FixtureRequest, "fspath", property(FixtureRequest_fspath), raising=False)
|
||||
|
||||
# Add TerminalReporter.startdir property.
|
||||
mp.setattr(
|
||||
TerminalReporter, "startdir", property(TerminalReporter_startdir), raising=False
|
||||
)
|
||||
|
||||
# Add Config.{invocation_dir,rootdir,inifile} properties.
|
||||
mp.setattr(Config, "invocation_dir", property(Config_invocation_dir), raising=False)
|
||||
mp.setattr(Config, "rootdir", property(Config_rootdir), raising=False)
|
||||
mp.setattr(Config, "inifile", property(Config_inifile), raising=False)
|
||||
|
||||
# Add Session.startdir property.
|
||||
mp.setattr(Session, "startdir", property(Session_stardir), raising=False)
|
||||
|
||||
# Add pathlist configuration type.
|
||||
mp.setattr(Config, "_getini_unknown_type", Config__getini_unknown_type)
|
||||
|
||||
# Add Node.fspath property.
|
||||
mp.setattr(Node, "fspath", property(Node_fspath, Node_fspath_set), raising=False)
|
||||
|
||||
|
||||
@hookimpl
|
||||
def pytest_configure(config: Config) -> None:
|
||||
"""Installs the LegacyTmpdirPlugin if the ``tmpdir`` plugin is also installed."""
|
||||
if config.pluginmanager.has_plugin("tmpdir"):
|
||||
mp = MonkeyPatch()
|
||||
config.add_cleanup(mp.undo)
|
||||
# Create TmpdirFactory and attach it to the config object.
|
||||
#
|
||||
# This is to comply with existing plugins which expect the handler to be
|
||||
@@ -422,40 +457,9 @@ def pytest_configure(config: pytest.Config) -> None:
|
||||
|
||||
config.pluginmanager.register(LegacyTmpdirPlugin, "legacypath-tmpdir")
|
||||
|
||||
# Add Cache.makedir().
|
||||
mp.setattr(pytest.Cache, "makedir", Cache_makedir, raising=False)
|
||||
|
||||
# Add FixtureRequest.fspath property.
|
||||
mp.setattr(
|
||||
pytest.FixtureRequest, "fspath", property(FixtureRequest_fspath), raising=False
|
||||
)
|
||||
|
||||
# Add TerminalReporter.startdir property.
|
||||
mp.setattr(
|
||||
TerminalReporter, "startdir", property(TerminalReporter_startdir), raising=False
|
||||
)
|
||||
|
||||
# Add Config.{invocation_dir,rootdir,inifile} properties.
|
||||
mp.setattr(
|
||||
pytest.Config, "invocation_dir", property(Config_invocation_dir), raising=False
|
||||
)
|
||||
mp.setattr(pytest.Config, "rootdir", property(Config_rootdir), raising=False)
|
||||
mp.setattr(pytest.Config, "inifile", property(Config_inifile), raising=False)
|
||||
|
||||
# Add Session.startdir property.
|
||||
mp.setattr(pytest.Session, "startdir", property(Session_stardir), raising=False)
|
||||
|
||||
# Add pathlist configuration type.
|
||||
mp.setattr(pytest.Config, "_getini_unknown_type", Config__getini_unknown_type)
|
||||
|
||||
# Add Node.fspath property.
|
||||
mp.setattr(Node, "fspath", property(Node_fspath, Node_fspath_set), raising=False)
|
||||
|
||||
|
||||
@pytest.hookimpl
|
||||
def pytest_plugin_registered(
|
||||
plugin: object, manager: pytest.PytestPluginManager
|
||||
) -> None:
|
||||
@hookimpl
|
||||
def pytest_plugin_registered(plugin: object, manager: PytestPluginManager) -> None:
|
||||
# pytester is not loaded by default and is commonly loaded from a conftest,
|
||||
# so checking for it in `pytest_configure` is not enough.
|
||||
is_pytester = plugin is manager.get_plugin("pytester")
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""Access and control log capturing."""
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from contextlib import nullcontext
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from typing import AbstractSet
|
||||
@@ -13,6 +14,7 @@ from typing import List
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
@@ -20,7 +22,6 @@ from _pytest import nodes
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest.capture import CaptureManager
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import nullcontext
|
||||
from _pytest.config import _strtobool
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import create_terminal_writer
|
||||
@@ -34,6 +35,11 @@ from _pytest.main import Session
|
||||
from _pytest.stash import StashKey
|
||||
from _pytest.terminal import TerminalReporter
|
||||
|
||||
if TYPE_CHECKING:
|
||||
logging_StreamHandler = logging.StreamHandler[StringIO]
|
||||
else:
|
||||
logging_StreamHandler = logging.StreamHandler
|
||||
|
||||
|
||||
DEFAULT_LOG_FORMAT = "%(levelname)-8s %(name)s:%(filename)s:%(lineno)d %(message)s"
|
||||
DEFAULT_LOG_DATE_FORMAT = "%H:%M:%S"
|
||||
@@ -322,11 +328,9 @@ class catching_logs:
|
||||
root_logger.removeHandler(self.handler)
|
||||
|
||||
|
||||
class LogCaptureHandler(logging.StreamHandler):
|
||||
class LogCaptureHandler(logging_StreamHandler):
|
||||
"""A logging handler that stores log records and the log text."""
|
||||
|
||||
stream: StringIO
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Create a new log handler."""
|
||||
super().__init__(StringIO())
|
||||
@@ -621,20 +625,11 @@ class LoggingPlugin:
|
||||
if not fpath.parent.exists():
|
||||
fpath.parent.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
stream = fpath.open(mode="w", encoding="UTF-8")
|
||||
if sys.version_info >= (3, 7):
|
||||
old_stream = self.log_file_handler.setStream(stream)
|
||||
else:
|
||||
old_stream = self.log_file_handler.stream
|
||||
self.log_file_handler.acquire()
|
||||
try:
|
||||
self.log_file_handler.flush()
|
||||
self.log_file_handler.stream = stream
|
||||
finally:
|
||||
self.log_file_handler.release()
|
||||
# https://github.com/python/mypy/issues/11193
|
||||
stream: io.TextIOWrapper = fpath.open(mode="w", encoding="UTF-8") # type: ignore[assignment]
|
||||
old_stream = self.log_file_handler.setStream(stream)
|
||||
if old_stream:
|
||||
# https://github.com/python/typeshed/pull/5663
|
||||
old_stream.close() # type:ignore[attr-defined]
|
||||
old_stream.close()
|
||||
|
||||
def _log_cli_enabled(self):
|
||||
"""Return whether live logging is enabled."""
|
||||
@@ -758,7 +753,7 @@ class _FileHandler(logging.FileHandler):
|
||||
pass
|
||||
|
||||
|
||||
class _LiveLoggingStreamHandler(logging.StreamHandler):
|
||||
class _LiveLoggingStreamHandler(logging_StreamHandler):
|
||||
"""A logging StreamHandler used by the live logging feature: it will
|
||||
write a newline before the first log message in each test.
|
||||
|
||||
|
||||
@@ -605,8 +605,7 @@ class Session(nodes.FSCollector):
|
||||
) -> Sequence[Union[nodes.Item, nodes.Collector]]:
|
||||
"""Perform the collection phase for this session.
|
||||
|
||||
This is called by the default
|
||||
:func:`pytest_collection <_pytest.hookspec.pytest_collection>` hook
|
||||
This is called by the default :hook:`pytest_collection` hook
|
||||
implementation; see the documentation of this hook for more details.
|
||||
For testing purposes, it may also be called directly on a fresh
|
||||
``Session``.
|
||||
@@ -690,9 +689,8 @@ class Session(nodes.FSCollector):
|
||||
# No point in finding packages when collecting doctests.
|
||||
if not self.config.getoption("doctestmodules", False):
|
||||
pm = self.config.pluginmanager
|
||||
confcutdir = pm._confcutdir
|
||||
for parent in (argpath, *argpath.parents):
|
||||
if confcutdir and parent in confcutdir.parents:
|
||||
if not pm._is_in_confcutdir(argpath):
|
||||
break
|
||||
|
||||
if parent.is_dir():
|
||||
@@ -872,7 +870,10 @@ def resolve_collection_argument(
|
||||
If the path doesn't exist, raise UsageError.
|
||||
If the path is a directory and selection parts are present, raise UsageError.
|
||||
"""
|
||||
strpath, *parts = str(arg).split("::")
|
||||
base, squacket, rest = str(arg).partition("[")
|
||||
strpath, *parts = base.split("::")
|
||||
if parts:
|
||||
parts[-1] = f"{parts[-1]}{squacket}{rest}"
|
||||
if as_pypath:
|
||||
strpath = search_pypath(strpath)
|
||||
fspath = invocation_path / strpath
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""Generic mechanism for marking and selecting python functions."""
|
||||
import warnings
|
||||
from typing import AbstractSet
|
||||
from typing import Collection
|
||||
from typing import List
|
||||
@@ -23,8 +22,6 @@ from _pytest.config import ExitCode
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.config import UsageError
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.deprecated import MINUS_K_COLON
|
||||
from _pytest.deprecated import MINUS_K_DASH
|
||||
from _pytest.stash import StashKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -189,27 +186,14 @@ def deselect_by_keyword(items: "List[Item]", config: Config) -> None:
|
||||
if not keywordexpr:
|
||||
return
|
||||
|
||||
if keywordexpr.startswith("-"):
|
||||
# To be removed in pytest 8.0.0.
|
||||
warnings.warn(MINUS_K_DASH, stacklevel=2)
|
||||
keywordexpr = "not " + keywordexpr[1:]
|
||||
selectuntil = False
|
||||
if keywordexpr[-1:] == ":":
|
||||
# To be removed in pytest 8.0.0.
|
||||
warnings.warn(MINUS_K_COLON, stacklevel=2)
|
||||
selectuntil = True
|
||||
keywordexpr = keywordexpr[:-1]
|
||||
|
||||
expr = _parse_expression(keywordexpr, "Wrong expression passed to '-k'")
|
||||
|
||||
remaining = []
|
||||
deselected = []
|
||||
for colitem in items:
|
||||
if keywordexpr and not expr.evaluate(KeywordMatcher.from_item(colitem)):
|
||||
if not expr.evaluate(KeywordMatcher.from_item(colitem)):
|
||||
deselected.append(colitem)
|
||||
else:
|
||||
if selectuntil:
|
||||
keywordexpr = None
|
||||
remaining.append(colitem)
|
||||
|
||||
if deselected:
|
||||
|
||||
@@ -190,7 +190,7 @@ class MatcherAdapter(Mapping[str, bool]):
|
||||
class Expression:
|
||||
"""A compiled match expression as used by -k and -m.
|
||||
|
||||
The expression can be evaulated against different matchers.
|
||||
The expression can be evaluated against different matchers.
|
||||
"""
|
||||
|
||||
__slots__ = ("code",)
|
||||
|
||||
@@ -72,16 +72,11 @@ def get_empty_parameterset_mark(
|
||||
return mark
|
||||
|
||||
|
||||
class ParameterSet(
|
||||
NamedTuple(
|
||||
"ParameterSet",
|
||||
[
|
||||
("values", Sequence[Union[object, NotSetType]]),
|
||||
("marks", Collection[Union["MarkDecorator", "Mark"]]),
|
||||
("id", Optional[str]),
|
||||
],
|
||||
)
|
||||
):
|
||||
class ParameterSet(NamedTuple):
|
||||
values: Sequence[Union[object, NotSetType]]
|
||||
marks: Collection[Union["MarkDecorator", "Mark"]]
|
||||
id: Optional[str]
|
||||
|
||||
@classmethod
|
||||
def param(
|
||||
cls,
|
||||
|
||||
@@ -55,7 +55,7 @@ def resolve(name: str) -> object:
|
||||
parts = name.split(".")
|
||||
|
||||
used = parts.pop(0)
|
||||
found = __import__(used)
|
||||
found: object = __import__(used)
|
||||
for part in parts:
|
||||
used += "." + part
|
||||
try:
|
||||
|
||||
@@ -145,7 +145,10 @@ class NodeMeta(type):
|
||||
|
||||
warnings.warn(
|
||||
PytestDeprecationWarning(
|
||||
f"{self} is not using a cooperative constructor and only takes {set(known_kw)}"
|
||||
f"{self} is not using a cooperative constructor and only takes {set(known_kw)}.\n"
|
||||
"See https://docs.pytest.org/en/stable/deprecations.html"
|
||||
"#constructors-of-custom-pytest-node-subclasses-should-take-kwargs "
|
||||
"for more details."
|
||||
)
|
||||
)
|
||||
|
||||
@@ -653,20 +656,6 @@ class Item(Node):
|
||||
|
||||
nextitem = None
|
||||
|
||||
def __init_subclass__(cls) -> None:
|
||||
problems = ", ".join(
|
||||
base.__name__ for base in cls.__bases__ if issubclass(base, Collector)
|
||||
)
|
||||
if problems:
|
||||
warnings.warn(
|
||||
f"{cls.__name__} is an Item subclass and should not be a collector, "
|
||||
f"however its bases {problems} are collectors.\n"
|
||||
"Please split the Collectors and the Item into separate node types.\n"
|
||||
"Pytest Doc example: https://docs.pytest.org/en/latest/example/nonpython.html\n"
|
||||
"example pull request on a plugin: https://github.com/asmeurer/pytest-flakes/pull/40/",
|
||||
PytestWarning,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
@@ -694,6 +683,37 @@ class Item(Node):
|
||||
#: for this test.
|
||||
self.user_properties: List[Tuple[str, object]] = []
|
||||
|
||||
self._check_item_and_collector_diamond_inheritance()
|
||||
|
||||
def _check_item_and_collector_diamond_inheritance(self) -> None:
|
||||
"""
|
||||
Check if the current type inherits from both File and Collector
|
||||
at the same time, emitting a warning accordingly (#8447).
|
||||
"""
|
||||
cls = type(self)
|
||||
|
||||
# We inject an attribute in the type to avoid issuing this warning
|
||||
# for the same class more than once, which is not helpful.
|
||||
# It is a hack, but was deemed acceptable in order to avoid
|
||||
# flooding the user in the common case.
|
||||
attr_name = "_pytest_diamond_inheritance_warning_shown"
|
||||
if getattr(cls, attr_name, False):
|
||||
return
|
||||
setattr(cls, attr_name, True)
|
||||
|
||||
problems = ", ".join(
|
||||
base.__name__ for base in cls.__bases__ if issubclass(base, Collector)
|
||||
)
|
||||
if problems:
|
||||
warnings.warn(
|
||||
f"{cls.__name__} is an Item subclass and should not be a collector, "
|
||||
f"however its bases {problems} are collectors.\n"
|
||||
"Please split the Collectors and the Item into separate node types.\n"
|
||||
"Pytest Doc example: https://docs.pytest.org/en/latest/example/nonpython.html\n"
|
||||
"example pull request on a plugin: https://github.com/asmeurer/pytest-flakes/pull/40/",
|
||||
PytestWarning,
|
||||
)
|
||||
|
||||
def runtest(self) -> None:
|
||||
"""Run the test case for this item.
|
||||
|
||||
|
||||
@@ -539,6 +539,9 @@ def import_path(
|
||||
ignore = os.environ.get("PY_IGNORE_IMPORTMISMATCH", "")
|
||||
if ignore != "1":
|
||||
module_file = mod.__file__
|
||||
if module_file is None:
|
||||
raise ImportPathMismatchError(module_name, module_file, path)
|
||||
|
||||
if module_file.endswith((".pyc", ".pyo")):
|
||||
module_file = module_file[:-1]
|
||||
if module_file.endswith(os.path.sep + "__init__.py"):
|
||||
@@ -562,7 +565,6 @@ if sys.platform.startswith("win"):
|
||||
def _is_same(f1: str, f2: str) -> bool:
|
||||
return Path(f1) == Path(f2) or os.path.samefile(f1, f2)
|
||||
|
||||
|
||||
else:
|
||||
|
||||
def _is_same(f1: str, f2: str) -> bool:
|
||||
@@ -601,11 +603,20 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) ->
|
||||
module_parts = module_name.split(".")
|
||||
while module_name:
|
||||
if module_name not in modules:
|
||||
module = ModuleType(
|
||||
module_name,
|
||||
doc="Empty module created by pytest's importmode=importlib.",
|
||||
)
|
||||
modules[module_name] = module
|
||||
try:
|
||||
# If sys.meta_path is empty, calling import_module will issue
|
||||
# a warning and raise ModuleNotFoundError. To avoid the
|
||||
# warning, we check sys.meta_path explicitly and raise the error
|
||||
# ourselves to fall back to creating a dummy module.
|
||||
if not sys.meta_path:
|
||||
raise ModuleNotFoundError
|
||||
importlib.import_module(module_name)
|
||||
except ModuleNotFoundError:
|
||||
module = ModuleType(
|
||||
module_name,
|
||||
doc="Empty module created by pytest's importmode=importlib.",
|
||||
)
|
||||
modules[module_name] = module
|
||||
module_parts.pop(-1)
|
||||
module_name = ".".join(module_parts)
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ class LsofFdLeakChecker:
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=True,
|
||||
universal_newlines=True,
|
||||
text=True,
|
||||
).stdout
|
||||
|
||||
def isopen(line: str) -> bool:
|
||||
@@ -477,7 +477,9 @@ def LineMatcher_fixture(request: FixtureRequest) -> Type["LineMatcher"]:
|
||||
|
||||
|
||||
@fixture
|
||||
def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "Pytester":
|
||||
def pytester(
|
||||
request: FixtureRequest, tmp_path_factory: TempPathFactory, monkeypatch: MonkeyPatch
|
||||
) -> "Pytester":
|
||||
"""
|
||||
Facilities to write tests/configuration files, execute pytest in isolation, and match
|
||||
against expected output, perfect for black-box testing of pytest plugins.
|
||||
@@ -488,7 +490,7 @@ def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "Pyt
|
||||
It is particularly useful for testing plugins. It is similar to the :fixture:`tmp_path`
|
||||
fixture but provides methods which aid in testing pytest itself.
|
||||
"""
|
||||
return Pytester(request, tmp_path_factory, _ispytest=True)
|
||||
return Pytester(request, tmp_path_factory, monkeypatch, _ispytest=True)
|
||||
|
||||
|
||||
@fixture
|
||||
@@ -596,11 +598,15 @@ class RunResult:
|
||||
errors: int = 0,
|
||||
xpassed: int = 0,
|
||||
xfailed: int = 0,
|
||||
warnings: int = 0,
|
||||
deselected: int = 0,
|
||||
warnings: Optional[int] = None,
|
||||
deselected: Optional[int] = None,
|
||||
) -> None:
|
||||
"""Assert that the specified outcomes appear with the respective
|
||||
numbers (0 means it didn't occur) in the text output from a test run."""
|
||||
"""
|
||||
Assert that the specified outcomes appear with the respective
|
||||
numbers (0 means it didn't occur) in the text output from a test run.
|
||||
|
||||
``warnings`` and ``deselected`` are only checked if not None.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
from _pytest.pytester_assertions import assert_outcomes
|
||||
|
||||
@@ -679,6 +685,7 @@ class Pytester:
|
||||
self,
|
||||
request: FixtureRequest,
|
||||
tmp_path_factory: TempPathFactory,
|
||||
monkeypatch: MonkeyPatch,
|
||||
*,
|
||||
_ispytest: bool = False,
|
||||
) -> None:
|
||||
@@ -702,7 +709,7 @@ class Pytester:
|
||||
self._method = self._request.config.getoption("--runpytest")
|
||||
self._test_tmproot = tmp_path_factory.mktemp(f"tmp-{name}", numbered=True)
|
||||
|
||||
self._monkeypatch = mp = MonkeyPatch()
|
||||
self._monkeypatch = mp = monkeypatch
|
||||
mp.setenv("PYTEST_DEBUG_TEMPROOT", str(self._test_tmproot))
|
||||
# Ensure no unexpected caching via tox.
|
||||
mp.delenv("TOX_ENV_DIR", raising=False)
|
||||
@@ -734,7 +741,6 @@ class Pytester:
|
||||
self._sys_modules_snapshot.restore()
|
||||
self._sys_path_snapshot.restore()
|
||||
self._cwd_snapshot.restore()
|
||||
self._monkeypatch.undo()
|
||||
|
||||
def __take_sys_modules_snapshot(self) -> SysModulesSnapshot:
|
||||
# Some zope modules used by twisted-related tests keep internal state
|
||||
@@ -1288,7 +1294,7 @@ class Pytester:
|
||||
) -> Optional[Union[Item, Collector]]:
|
||||
"""Return the collection node for name from the module collection.
|
||||
|
||||
Searchs a module collection node for a collection node matching the
|
||||
Searches a module collection node for a collection node matching the
|
||||
given name.
|
||||
|
||||
:param modcol: A module collection node; see :py:meth:`getmodulecol`.
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# hence cannot be subject to assertion rewriting, which requires a
|
||||
# module to not be already imported.
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
from typing import Union
|
||||
@@ -42,8 +43,8 @@ def assert_outcomes(
|
||||
errors: int = 0,
|
||||
xpassed: int = 0,
|
||||
xfailed: int = 0,
|
||||
warnings: int = 0,
|
||||
deselected: int = 0,
|
||||
warnings: Optional[int] = None,
|
||||
deselected: Optional[int] = None,
|
||||
) -> None:
|
||||
"""Assert that the specified outcomes appear with the respective
|
||||
numbers (0 means it didn't occur) in the text output from a test run."""
|
||||
@@ -56,8 +57,6 @@ def assert_outcomes(
|
||||
"errors": outcomes.get("errors", 0),
|
||||
"xpassed": outcomes.get("xpassed", 0),
|
||||
"xfailed": outcomes.get("xfailed", 0),
|
||||
"warnings": outcomes.get("warnings", 0),
|
||||
"deselected": outcomes.get("deselected", 0),
|
||||
}
|
||||
expected = {
|
||||
"passed": passed,
|
||||
@@ -66,7 +65,11 @@ def assert_outcomes(
|
||||
"errors": errors,
|
||||
"xpassed": xpassed,
|
||||
"xfailed": xfailed,
|
||||
"warnings": warnings,
|
||||
"deselected": deselected,
|
||||
}
|
||||
if warnings is not None:
|
||||
obtained["warnings"] = outcomes.get("warnings", 0)
|
||||
expected["warnings"] = warnings
|
||||
if deselected is not None:
|
||||
obtained["deselected"] = outcomes.get("deselected", 0)
|
||||
expected["deselected"] = deselected
|
||||
assert obtained == expected
|
||||
|
||||
@@ -224,11 +224,15 @@ def pytest_pycollect_makemodule(module_path: Path, parent) -> "Module":
|
||||
|
||||
|
||||
@hookimpl(trylast=True)
|
||||
def pytest_pycollect_makeitem(collector: "PyCollector", name: str, obj: object):
|
||||
def pytest_pycollect_makeitem(
|
||||
collector: Union["Module", "Class"], name: str, obj: object
|
||||
) -> Union[None, nodes.Item, nodes.Collector, List[Union[nodes.Item, nodes.Collector]]]:
|
||||
assert isinstance(collector, (Class, Module)), type(collector)
|
||||
# Nothing was collected elsewhere, let's do it here.
|
||||
if safe_isclass(obj):
|
||||
if collector.istestclass(obj, name):
|
||||
return Class.from_parent(collector, name=name, obj=obj)
|
||||
klass: Class = Class.from_parent(collector, name=name, obj=obj)
|
||||
return klass
|
||||
elif collector.istestfunction(obj, name):
|
||||
# mock seems to store unbound methods (issue473), normalize it.
|
||||
obj = getattr(obj, "__func__", obj)
|
||||
@@ -247,15 +251,16 @@ def pytest_pycollect_makeitem(collector: "PyCollector", name: str, obj: object):
|
||||
)
|
||||
elif getattr(obj, "__test__", True):
|
||||
if is_generator(obj):
|
||||
res = Function.from_parent(collector, name=name)
|
||||
res: Function = Function.from_parent(collector, name=name)
|
||||
reason = "yield tests were removed in pytest 4.0 - {name} will be ignored".format(
|
||||
name=name
|
||||
)
|
||||
res.add_marker(MARK_GEN.xfail(run=False, reason=reason))
|
||||
res.warn(PytestCollectionWarning(reason))
|
||||
return res
|
||||
else:
|
||||
res = list(collector._genfunctions(name, obj))
|
||||
return res
|
||||
return list(collector._genfunctions(name, obj))
|
||||
return None
|
||||
|
||||
|
||||
class PyobjMixin(nodes.Node):
|
||||
@@ -278,6 +283,16 @@ class PyobjMixin(nodes.Node):
|
||||
node = self.getparent(Class)
|
||||
return node.obj if node is not None else None
|
||||
|
||||
@property
|
||||
def instance(self):
|
||||
"""Python instance object the function is bound to.
|
||||
|
||||
Returns None if not a test method, e.g. for a standalone test function,
|
||||
a staticmethod, a class or a module.
|
||||
"""
|
||||
node = self.getparent(Function)
|
||||
return getattr(node.obj, "__self__", None) if node is not None else None
|
||||
|
||||
@property
|
||||
def obj(self):
|
||||
"""Underlying Python object."""
|
||||
@@ -288,6 +303,9 @@ class PyobjMixin(nodes.Node):
|
||||
# used to avoid Function marker duplication
|
||||
if self._ALLOW_MARKERS:
|
||||
self.own_markers.extend(get_unpacked_marks(self.obj))
|
||||
# This assumes that `obj` is called before there is a chance
|
||||
# to add custom keys to `self.keywords`, so no fear of overriding.
|
||||
self.keywords.update((mark.name, mark) for mark in self.own_markers)
|
||||
return obj
|
||||
|
||||
@obj.setter
|
||||
@@ -325,6 +343,7 @@ class PyobjMixin(nodes.Node):
|
||||
if isinstance(compat_co_firstlineno, int):
|
||||
# nose compatibility
|
||||
file_path = sys.modules[obj.__module__].__file__
|
||||
assert file_path is not None
|
||||
if file_path.endswith(".pyc"):
|
||||
file_path = file_path[:-1]
|
||||
path: Union["os.PathLike[str]", str] = file_path
|
||||
@@ -409,7 +428,7 @@ class PyCollector(PyobjMixin, nodes.Collector):
|
||||
for basecls in self.obj.__mro__:
|
||||
dicts.append(basecls.__dict__)
|
||||
|
||||
# In each class, nodes should be definition ordered. Since Python 3.6,
|
||||
# In each class, nodes should be definition ordered.
|
||||
# __dict__ is definition ordered.
|
||||
seen: Set[str] = set()
|
||||
dict_values: List[List[Union[nodes.Item, nodes.Collector]]] = []
|
||||
@@ -517,12 +536,18 @@ class Module(nodes.File, PyCollector):
|
||||
self.obj, ("setUpModule", "setup_module")
|
||||
)
|
||||
if setup_module is None and has_nose:
|
||||
# The name "setup" is too common - only treat as fixture if callable.
|
||||
setup_module = _get_first_non_fixture_func(self.obj, ("setup",))
|
||||
if not callable(setup_module):
|
||||
setup_module = None
|
||||
teardown_module = _get_first_non_fixture_func(
|
||||
self.obj, ("tearDownModule", "teardown_module")
|
||||
)
|
||||
if teardown_module is None and has_nose:
|
||||
teardown_module = _get_first_non_fixture_func(self.obj, ("teardown",))
|
||||
# Same as "setup" above - only treat as fixture if callable.
|
||||
if not callable(teardown_module):
|
||||
teardown_module = None
|
||||
|
||||
if setup_module is None and teardown_module is None:
|
||||
return
|
||||
@@ -880,11 +905,7 @@ class InstanceDummy:
|
||||
only to ignore it; this dummy class keeps them working. This will be removed
|
||||
in pytest 8."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
# Note: module __getattr__ only works on Python>=3.7. Unfortunately
|
||||
# we can't provide this deprecation warning on Python 3.6.
|
||||
def __getattr__(name: str) -> object:
|
||||
if name == "Instance":
|
||||
warnings.warn(INSTANCE_COLLECTOR, 2)
|
||||
@@ -906,6 +927,159 @@ def hasnew(obj: object) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
@final
|
||||
@attr.s(frozen=True, auto_attribs=True, slots=True)
|
||||
class IdMaker:
|
||||
"""Make IDs for a parametrization."""
|
||||
|
||||
# The argnames of the parametrization.
|
||||
argnames: Sequence[str]
|
||||
# The ParameterSets of the parametrization.
|
||||
parametersets: Sequence[ParameterSet]
|
||||
# Optionally, a user-provided callable to make IDs for parameters in a
|
||||
# ParameterSet.
|
||||
idfn: Optional[Callable[[Any], Optional[object]]]
|
||||
# Optionally, explicit IDs for ParameterSets by index.
|
||||
ids: Optional[Sequence[Optional[object]]]
|
||||
# Optionally, the pytest config.
|
||||
# Used for controlling ASCII escaping, and for calling the
|
||||
# :hook:`pytest_make_parametrize_id` hook.
|
||||
config: Optional[Config]
|
||||
# Optionally, the ID of the node being parametrized.
|
||||
# Used only for clearer error messages.
|
||||
nodeid: Optional[str]
|
||||
# Optionally, the ID of the function being parametrized.
|
||||
# Used only for clearer error messages.
|
||||
func_name: Optional[str]
|
||||
|
||||
def make_unique_parameterset_ids(self) -> List[str]:
|
||||
"""Make a unique identifier for each ParameterSet, that may be used to
|
||||
identify the parametrization in a node ID.
|
||||
|
||||
Format is <prm_1_token>-...-<prm_n_token>[counter], where prm_x_token is
|
||||
- user-provided id, if given
|
||||
- else an id derived from the value, applicable for certain types
|
||||
- else <argname><parameterset index>
|
||||
The counter suffix is appended only in case a string wouldn't be unique
|
||||
otherwise.
|
||||
"""
|
||||
resolved_ids = list(self._resolve_ids())
|
||||
# All IDs must be unique!
|
||||
if len(resolved_ids) != len(set(resolved_ids)):
|
||||
# Record the number of occurrences of each ID.
|
||||
id_counts = Counter(resolved_ids)
|
||||
# Map the ID to its next suffix.
|
||||
id_suffixes: Dict[str, int] = defaultdict(int)
|
||||
# Suffix non-unique IDs to make them unique.
|
||||
for index, id in enumerate(resolved_ids):
|
||||
if id_counts[id] > 1:
|
||||
resolved_ids[index] = f"{id}{id_suffixes[id]}"
|
||||
id_suffixes[id] += 1
|
||||
return resolved_ids
|
||||
|
||||
def _resolve_ids(self) -> Iterable[str]:
|
||||
"""Resolve IDs for all ParameterSets (may contain duplicates)."""
|
||||
for idx, parameterset in enumerate(self.parametersets):
|
||||
if parameterset.id is not None:
|
||||
# ID provided directly - pytest.param(..., id="...")
|
||||
yield parameterset.id
|
||||
elif self.ids and idx < len(self.ids) and self.ids[idx] is not None:
|
||||
# ID provided in the IDs list - parametrize(..., ids=[...]).
|
||||
yield self._idval_from_value_required(self.ids[idx], idx)
|
||||
else:
|
||||
# ID not provided - generate it.
|
||||
yield "-".join(
|
||||
self._idval(val, argname, idx)
|
||||
for val, argname in zip(parameterset.values, self.argnames)
|
||||
)
|
||||
|
||||
def _idval(self, val: object, argname: str, idx: int) -> str:
|
||||
"""Make an ID for a parameter in a ParameterSet."""
|
||||
idval = self._idval_from_function(val, argname, idx)
|
||||
if idval is not None:
|
||||
return idval
|
||||
idval = self._idval_from_hook(val, argname)
|
||||
if idval is not None:
|
||||
return idval
|
||||
idval = self._idval_from_value(val)
|
||||
if idval is not None:
|
||||
return idval
|
||||
return self._idval_from_argname(argname, idx)
|
||||
|
||||
def _idval_from_function(
|
||||
self, val: object, argname: str, idx: int
|
||||
) -> Optional[str]:
|
||||
"""Try to make an ID for a parameter in a ParameterSet using the
|
||||
user-provided id callable, if given."""
|
||||
if self.idfn is None:
|
||||
return None
|
||||
try:
|
||||
id = self.idfn(val)
|
||||
except Exception as e:
|
||||
prefix = f"{self.nodeid}: " if self.nodeid is not None else ""
|
||||
msg = "error raised while trying to determine id of parameter '{}' at position {}"
|
||||
msg = prefix + msg.format(argname, idx)
|
||||
raise ValueError(msg) from e
|
||||
if id is None:
|
||||
return None
|
||||
return self._idval_from_value(id)
|
||||
|
||||
def _idval_from_hook(self, val: object, argname: str) -> Optional[str]:
|
||||
"""Try to make an ID for a parameter in a ParameterSet by calling the
|
||||
:hook:`pytest_make_parametrize_id` hook."""
|
||||
if self.config:
|
||||
id: Optional[str] = self.config.hook.pytest_make_parametrize_id(
|
||||
config=self.config, val=val, argname=argname
|
||||
)
|
||||
return id
|
||||
return None
|
||||
|
||||
def _idval_from_value(self, val: object) -> Optional[str]:
|
||||
"""Try to make an ID for a parameter in a ParameterSet from its value,
|
||||
if the value type is supported."""
|
||||
if isinstance(val, STRING_TYPES):
|
||||
return _ascii_escaped_by_config(val, self.config)
|
||||
elif val is None or isinstance(val, (float, int, bool, complex)):
|
||||
return str(val)
|
||||
elif isinstance(val, Pattern):
|
||||
return ascii_escaped(val.pattern)
|
||||
elif val is NOTSET:
|
||||
# Fallback to default. Note that NOTSET is an enum.Enum.
|
||||
pass
|
||||
elif isinstance(val, enum.Enum):
|
||||
return str(val)
|
||||
elif isinstance(getattr(val, "__name__", None), str):
|
||||
# Name of a class, function, module, etc.
|
||||
name: str = getattr(val, "__name__")
|
||||
return name
|
||||
return None
|
||||
|
||||
def _idval_from_value_required(self, val: object, idx: int) -> str:
|
||||
"""Like _idval_from_value(), but fails if the type is not supported."""
|
||||
id = self._idval_from_value(val)
|
||||
if id is not None:
|
||||
return id
|
||||
|
||||
# Fail.
|
||||
if self.func_name is not None:
|
||||
prefix = f"In {self.func_name}: "
|
||||
elif self.nodeid is not None:
|
||||
prefix = f"In {self.nodeid}: "
|
||||
else:
|
||||
prefix = ""
|
||||
msg = (
|
||||
f"{prefix}ids contains unsupported value {saferepr(val)} (type: {type(val)!r}) at index {idx}. "
|
||||
"Supported types are: str, bytes, int, float, complex, bool, enum, regex or anything with a __name__."
|
||||
)
|
||||
fail(msg, pytrace=False)
|
||||
|
||||
@staticmethod
|
||||
def _idval_from_argname(argname: str, idx: int) -> str:
|
||||
"""Make an ID for a parameter in a ParameterSet from the argument name
|
||||
and the index of the ParameterSet."""
|
||||
return str(argname) + str(idx)
|
||||
|
||||
|
||||
@final
|
||||
@attr.s(frozen=True, slots=True, auto_attribs=True)
|
||||
class CallSpec2:
|
||||
@@ -980,7 +1154,7 @@ class CallSpec2:
|
||||
|
||||
@final
|
||||
class Metafunc:
|
||||
"""Objects passed to the :func:`pytest_generate_tests <_pytest.hookspec.pytest_generate_tests>` hook.
|
||||
"""Objects passed to the :hook:`pytest_generate_tests` hook.
|
||||
|
||||
They help to inspect a test function and to generate tests according to
|
||||
test configuration or values specified in the class or module where a
|
||||
@@ -1028,10 +1202,7 @@ class Metafunc:
|
||||
argvalues: Iterable[Union[ParameterSet, Sequence[object], object]],
|
||||
indirect: Union[bool, Sequence[str]] = False,
|
||||
ids: Optional[
|
||||
Union[
|
||||
Iterable[Union[None, str, float, int, bool]],
|
||||
Callable[[Any], Optional[object]],
|
||||
]
|
||||
Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
|
||||
] = None,
|
||||
scope: "Optional[_ScopeName]" = None,
|
||||
*,
|
||||
@@ -1098,7 +1269,7 @@ class Metafunc:
|
||||
It will also override any fixture-function defined scope, allowing
|
||||
to set a dynamic scope using test context or configuration.
|
||||
"""
|
||||
argnames, parameters = ParameterSet._for_parametrize(
|
||||
argnames, parametersets = ParameterSet._for_parametrize(
|
||||
argnames,
|
||||
argvalues,
|
||||
self.function,
|
||||
@@ -1130,8 +1301,8 @@ class Metafunc:
|
||||
if generated_ids is not None:
|
||||
ids = generated_ids
|
||||
|
||||
ids = self._resolve_arg_ids(
|
||||
argnames, ids, parameters, nodeid=self.definition.nodeid
|
||||
ids = self._resolve_parameter_set_ids(
|
||||
argnames, ids, parametersets, nodeid=self.definition.nodeid
|
||||
)
|
||||
|
||||
# Store used (possibly generated) ids with parametrize Marks.
|
||||
@@ -1143,7 +1314,9 @@ class Metafunc:
|
||||
# of all calls.
|
||||
newcalls = []
|
||||
for callspec in self._calls or [CallSpec2()]:
|
||||
for param_index, (param_id, param_set) in enumerate(zip(ids, parameters)):
|
||||
for param_index, (param_id, param_set) in enumerate(
|
||||
zip(ids, parametersets)
|
||||
):
|
||||
newcallspec = callspec.setmulti(
|
||||
valtypes=arg_values_types,
|
||||
argnames=argnames,
|
||||
@@ -1156,27 +1329,29 @@ class Metafunc:
|
||||
newcalls.append(newcallspec)
|
||||
self._calls = newcalls
|
||||
|
||||
def _resolve_arg_ids(
|
||||
def _resolve_parameter_set_ids(
|
||||
self,
|
||||
argnames: Sequence[str],
|
||||
ids: Optional[
|
||||
Union[
|
||||
Iterable[Union[None, str, float, int, bool]],
|
||||
Callable[[Any], Optional[object]],
|
||||
]
|
||||
Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
|
||||
],
|
||||
parameters: Sequence[ParameterSet],
|
||||
parametersets: Sequence[ParameterSet],
|
||||
nodeid: str,
|
||||
) -> List[str]:
|
||||
"""Resolve the actual ids for the given argnames, based on the ``ids`` parameter given
|
||||
to ``parametrize``.
|
||||
"""Resolve the actual ids for the given parameter sets.
|
||||
|
||||
:param List[str] argnames: List of argument names passed to ``parametrize()``.
|
||||
:param ids: The ids parameter of the parametrized call (see docs).
|
||||
:param List[ParameterSet] parameters: The list of parameter values, same size as ``argnames``.
|
||||
:param str str: The nodeid of the item that generated this parametrized call.
|
||||
:rtype: List[str]
|
||||
:returns: The list of ids for each argname given.
|
||||
:param argnames:
|
||||
Argument names passed to ``parametrize()``.
|
||||
:param ids:
|
||||
The `ids` parameter of the ``parametrize()`` call (see docs).
|
||||
:param parametersets:
|
||||
The parameter sets, each containing a set of values corresponding
|
||||
to ``argnames``.
|
||||
:param nodeid str:
|
||||
The nodeid of the definition item that generated this
|
||||
parametrization.
|
||||
:returns:
|
||||
List with ids for each parameter set given.
|
||||
"""
|
||||
if ids is None:
|
||||
idfn = None
|
||||
@@ -1186,15 +1361,24 @@ class Metafunc:
|
||||
ids_ = None
|
||||
else:
|
||||
idfn = None
|
||||
ids_ = self._validate_ids(ids, parameters, self.function.__name__)
|
||||
return idmaker(argnames, parameters, idfn, ids_, self.config, nodeid=nodeid)
|
||||
ids_ = self._validate_ids(ids, parametersets, self.function.__name__)
|
||||
id_maker = IdMaker(
|
||||
argnames,
|
||||
parametersets,
|
||||
idfn,
|
||||
ids_,
|
||||
self.config,
|
||||
nodeid=nodeid,
|
||||
func_name=self.function.__name__,
|
||||
)
|
||||
return id_maker.make_unique_parameterset_ids()
|
||||
|
||||
def _validate_ids(
|
||||
self,
|
||||
ids: Iterable[Union[None, str, float, int, bool]],
|
||||
parameters: Sequence[ParameterSet],
|
||||
ids: Iterable[Optional[object]],
|
||||
parametersets: Sequence[ParameterSet],
|
||||
func_name: str,
|
||||
) -> List[Union[None, str]]:
|
||||
) -> List[Optional[object]]:
|
||||
try:
|
||||
num_ids = len(ids) # type: ignore[arg-type]
|
||||
except TypeError:
|
||||
@@ -1202,29 +1386,14 @@ class Metafunc:
|
||||
iter(ids)
|
||||
except TypeError as e:
|
||||
raise TypeError("ids must be a callable or an iterable") from e
|
||||
num_ids = len(parameters)
|
||||
num_ids = len(parametersets)
|
||||
|
||||
# num_ids == 0 is a special case: https://github.com/pytest-dev/pytest/issues/1849
|
||||
if num_ids != len(parameters) and num_ids != 0:
|
||||
if num_ids != len(parametersets) and num_ids != 0:
|
||||
msg = "In {}: {} parameter sets specified, with different number of ids: {}"
|
||||
fail(msg.format(func_name, len(parameters), num_ids), pytrace=False)
|
||||
fail(msg.format(func_name, len(parametersets), num_ids), pytrace=False)
|
||||
|
||||
new_ids = []
|
||||
for idx, id_value in enumerate(itertools.islice(ids, num_ids)):
|
||||
if id_value is None or isinstance(id_value, str):
|
||||
new_ids.append(id_value)
|
||||
elif isinstance(id_value, (float, int, bool)):
|
||||
new_ids.append(str(id_value))
|
||||
else:
|
||||
msg = ( # type: ignore[unreachable]
|
||||
"In {}: ids must be list of string/float/int/bool, "
|
||||
"found: {} (type: {!r}) at index {}"
|
||||
)
|
||||
fail(
|
||||
msg.format(func_name, saferepr(id_value), type(id_value), idx),
|
||||
pytrace=False,
|
||||
)
|
||||
return new_ids
|
||||
return list(itertools.islice(ids, num_ids))
|
||||
|
||||
def _resolve_arg_value_types(
|
||||
self,
|
||||
@@ -1344,105 +1513,6 @@ def _ascii_escaped_by_config(val: Union[str, bytes], config: Optional[Config]) -
|
||||
return val if escape_option else ascii_escaped(val) # type: ignore
|
||||
|
||||
|
||||
def _idval(
|
||||
val: object,
|
||||
argname: str,
|
||||
idx: int,
|
||||
idfn: Optional[Callable[[Any], Optional[object]]],
|
||||
nodeid: Optional[str],
|
||||
config: Optional[Config],
|
||||
) -> str:
|
||||
if idfn:
|
||||
try:
|
||||
generated_id = idfn(val)
|
||||
if generated_id is not None:
|
||||
val = generated_id
|
||||
except Exception as e:
|
||||
prefix = f"{nodeid}: " if nodeid is not None else ""
|
||||
msg = "error raised while trying to determine id of parameter '{}' at position {}"
|
||||
msg = prefix + msg.format(argname, idx)
|
||||
raise ValueError(msg) from e
|
||||
elif config:
|
||||
hook_id: Optional[str] = config.hook.pytest_make_parametrize_id(
|
||||
config=config, val=val, argname=argname
|
||||
)
|
||||
if hook_id:
|
||||
return hook_id
|
||||
|
||||
if isinstance(val, STRING_TYPES):
|
||||
return _ascii_escaped_by_config(val, config)
|
||||
elif val is None or isinstance(val, (float, int, bool, complex)):
|
||||
return str(val)
|
||||
elif isinstance(val, Pattern):
|
||||
return ascii_escaped(val.pattern)
|
||||
elif val is NOTSET:
|
||||
# Fallback to default. Note that NOTSET is an enum.Enum.
|
||||
pass
|
||||
elif isinstance(val, enum.Enum):
|
||||
return str(val)
|
||||
elif isinstance(getattr(val, "__name__", None), str):
|
||||
# Name of a class, function, module, etc.
|
||||
name: str = getattr(val, "__name__")
|
||||
return name
|
||||
return str(argname) + str(idx)
|
||||
|
||||
|
||||
def _idvalset(
|
||||
idx: int,
|
||||
parameterset: ParameterSet,
|
||||
argnames: Iterable[str],
|
||||
idfn: Optional[Callable[[Any], Optional[object]]],
|
||||
ids: Optional[List[Union[None, str]]],
|
||||
nodeid: Optional[str],
|
||||
config: Optional[Config],
|
||||
) -> str:
|
||||
if parameterset.id is not None:
|
||||
return parameterset.id
|
||||
id = None if ids is None or idx >= len(ids) else ids[idx]
|
||||
if id is None:
|
||||
this_id = [
|
||||
_idval(val, argname, idx, idfn, nodeid=nodeid, config=config)
|
||||
for val, argname in zip(parameterset.values, argnames)
|
||||
]
|
||||
return "-".join(this_id)
|
||||
else:
|
||||
return _ascii_escaped_by_config(id, config)
|
||||
|
||||
|
||||
def idmaker(
|
||||
argnames: Iterable[str],
|
||||
parametersets: Iterable[ParameterSet],
|
||||
idfn: Optional[Callable[[Any], Optional[object]]] = None,
|
||||
ids: Optional[List[Union[None, str]]] = None,
|
||||
config: Optional[Config] = None,
|
||||
nodeid: Optional[str] = None,
|
||||
) -> List[str]:
|
||||
resolved_ids = [
|
||||
_idvalset(
|
||||
valindex, parameterset, argnames, idfn, ids, config=config, nodeid=nodeid
|
||||
)
|
||||
for valindex, parameterset in enumerate(parametersets)
|
||||
]
|
||||
|
||||
# All IDs must be unique!
|
||||
unique_ids = set(resolved_ids)
|
||||
if len(unique_ids) != len(resolved_ids):
|
||||
|
||||
# Record the number of occurrences of each test ID.
|
||||
test_id_counts = Counter(resolved_ids)
|
||||
|
||||
# Map the test ID to its next suffix.
|
||||
test_id_suffixes: Dict[str, int] = defaultdict(int)
|
||||
|
||||
# Suffix non-unique IDs to make them unique.
|
||||
for index, test_id in enumerate(resolved_ids):
|
||||
if test_id_counts[test_id] > 1:
|
||||
resolved_ids[index] = f"{test_id}{test_id_suffixes[test_id]}"
|
||||
test_id_suffixes[test_id] += 1
|
||||
|
||||
return resolved_ids
|
||||
|
||||
|
||||
def _pretty_fixture_path(func) -> str:
|
||||
cwd = Path.cwd()
|
||||
loc = Path(getlocation(func, str(cwd)))
|
||||
@@ -1614,7 +1684,7 @@ class Function(PyobjMixin, nodes.Item):
|
||||
config: Optional[Config] = None,
|
||||
callspec: Optional[CallSpec2] = None,
|
||||
callobj=NOTSET,
|
||||
keywords=None,
|
||||
keywords: Optional[Mapping[str, Any]] = None,
|
||||
session: Optional[Session] = None,
|
||||
fixtureinfo: Optional[FuncFixtureInfo] = None,
|
||||
originalname: Optional[str] = None,
|
||||
@@ -1635,31 +1705,20 @@ class Function(PyobjMixin, nodes.Item):
|
||||
# Note: when FunctionDefinition is introduced, we should change ``originalname``
|
||||
# to a readonly property that returns FunctionDefinition.name.
|
||||
|
||||
self.keywords.update(self.obj.__dict__)
|
||||
self.own_markers.extend(get_unpacked_marks(self.obj))
|
||||
if callspec:
|
||||
self.callspec = callspec
|
||||
# this is total hostile and a mess
|
||||
# keywords are broken by design by now
|
||||
# this will be redeemed later
|
||||
for mark in callspec.marks:
|
||||
# feel free to cry, this was broken for years before
|
||||
# and keywords cant fix it per design
|
||||
self.keywords[mark.name] = mark
|
||||
self.own_markers.extend(normalize_mark_list(callspec.marks))
|
||||
if keywords:
|
||||
self.keywords.update(keywords)
|
||||
self.own_markers.extend(callspec.marks)
|
||||
|
||||
# todo: this is a hell of a hack
|
||||
# https://github.com/pytest-dev/pytest/issues/4569
|
||||
|
||||
self.keywords.update(
|
||||
{
|
||||
mark.name: True
|
||||
for mark in self.iter_markers()
|
||||
if mark.name not in self.keywords
|
||||
}
|
||||
)
|
||||
# Note: the order of the updates is important here; indicates what
|
||||
# takes priority (ctor argument over function attributes over markers).
|
||||
# Take own_markers only; NodeKeywords handles parent traversal on its own.
|
||||
self.keywords.update((mark.name, mark) for mark in self.own_markers)
|
||||
self.keywords.update(self.obj.__dict__)
|
||||
if keywords:
|
||||
self.keywords.update(keywords)
|
||||
|
||||
if fixtureinfo is None:
|
||||
fixtureinfo = self.session._fixturemanager.getfixtureinfo(
|
||||
@@ -1683,15 +1742,6 @@ class Function(PyobjMixin, nodes.Item):
|
||||
"""Underlying python 'function' object."""
|
||||
return getimfunc(self.obj)
|
||||
|
||||
@property
|
||||
def instance(self):
|
||||
"""Python instance object the function is bound to.
|
||||
|
||||
Returns None if not a test method, e.g. for a standalone test function
|
||||
or a staticmethod.
|
||||
"""
|
||||
return getattr(self.obj, "__self__", None)
|
||||
|
||||
def _getobj(self):
|
||||
assert self.parent is not None
|
||||
if isinstance(self.parent, Class):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import math
|
||||
import pprint
|
||||
from collections.abc import Collection
|
||||
from collections.abc import Sized
|
||||
from decimal import Decimal
|
||||
from numbers import Complex
|
||||
@@ -8,7 +9,6 @@ from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Generic
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
@@ -101,6 +101,7 @@ class ApproxBase:
|
||||
)
|
||||
|
||||
def __bool__(self):
|
||||
__tracebackhide__ = True
|
||||
raise AssertionError(
|
||||
"approx() is not supported in a boolean context.\nDid you mean: `assert a == approx(b)`?"
|
||||
)
|
||||
@@ -130,7 +131,6 @@ class ApproxBase:
|
||||
# a numeric type. For this reason, the default is to do nothing. The
|
||||
# classes that deal with sequences should reimplement this method to
|
||||
# raise if there are any non-numeric elements in the sequence.
|
||||
pass
|
||||
|
||||
|
||||
def _recursive_list_map(f, x):
|
||||
@@ -306,12 +306,12 @@ class ApproxMapping(ApproxBase):
|
||||
raise TypeError(msg.format(key, value, pprint.pformat(self.expected)))
|
||||
|
||||
|
||||
class ApproxSequencelike(ApproxBase):
|
||||
class ApproxSequenceLike(ApproxBase):
|
||||
"""Perform approximate comparisons where the expected value is a sequence of numbers."""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
seq_type = type(self.expected)
|
||||
if seq_type not in (tuple, list, set):
|
||||
if seq_type not in (tuple, list):
|
||||
seq_type = list
|
||||
return "approx({!r})".format(
|
||||
seq_type(self._approx_scalar(x) for x in self.expected)
|
||||
@@ -515,7 +515,7 @@ class ApproxDecimal(ApproxScalar):
|
||||
|
||||
|
||||
def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
|
||||
"""Assert that two numbers (or two sets of numbers) are equal to each other
|
||||
"""Assert that two numbers (or two ordered sequences of numbers) are equal to each other
|
||||
within some tolerance.
|
||||
|
||||
Due to the :std:doc:`tutorial/floatingpoint`, numbers that we
|
||||
@@ -547,16 +547,11 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
|
||||
>>> 0.1 + 0.2 == approx(0.3)
|
||||
True
|
||||
|
||||
The same syntax also works for sequences of numbers::
|
||||
The same syntax also works for ordered sequences of numbers::
|
||||
|
||||
>>> (0.1 + 0.2, 0.2 + 0.4) == approx((0.3, 0.6))
|
||||
True
|
||||
|
||||
Dictionary *values*::
|
||||
|
||||
>>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == approx({'a': 0.3, 'b': 0.6})
|
||||
True
|
||||
|
||||
``numpy`` arrays::
|
||||
|
||||
>>> import numpy as np # doctest: +SKIP
|
||||
@@ -569,6 +564,20 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
|
||||
>>> np.array([0.1, 0.2]) + np.array([0.2, 0.1]) == approx(0.3) # doctest: +SKIP
|
||||
True
|
||||
|
||||
Only ordered sequences are supported, because ``approx`` needs
|
||||
to infer the relative position of the sequences without ambiguity. This means
|
||||
``sets`` and other unordered sequences are not supported.
|
||||
|
||||
Finally, dictionary *values* can also be compared::
|
||||
|
||||
>>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == approx({'a': 0.3, 'b': 0.6})
|
||||
True
|
||||
|
||||
The comparision will be true if both mappings have the same keys and their
|
||||
respective values match the expected tolerances.
|
||||
|
||||
**Tolerances**
|
||||
|
||||
By default, ``approx`` considers numbers within a relative tolerance of
|
||||
``1e-6`` (i.e. one part in a million) of its expected value to be equal.
|
||||
This treatment would lead to surprising results if the expected value was
|
||||
@@ -708,12 +717,19 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
|
||||
expected = _as_numpy_array(expected)
|
||||
cls = ApproxNumpy
|
||||
elif (
|
||||
isinstance(expected, Iterable)
|
||||
hasattr(expected, "__getitem__")
|
||||
and isinstance(expected, Sized)
|
||||
# Type ignored because the error is wrong -- not unreachable.
|
||||
and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable]
|
||||
):
|
||||
cls = ApproxSequencelike
|
||||
cls = ApproxSequenceLike
|
||||
elif (
|
||||
isinstance(expected, Collection)
|
||||
# Type ignored because the error is wrong -- not unreachable.
|
||||
and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable]
|
||||
):
|
||||
msg = f"pytest.approx() only supports ordered sequences, but got: {repr(expected)}"
|
||||
raise TypeError(msg)
|
||||
else:
|
||||
cls = ApproxScalar
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
@@ -254,7 +255,7 @@ class TestReport(BaseReport):
|
||||
self,
|
||||
nodeid: str,
|
||||
location: Tuple[str, Optional[int], str],
|
||||
keywords,
|
||||
keywords: Mapping[str, Any],
|
||||
outcome: "Literal['passed', 'failed', 'skipped']",
|
||||
longrepr: Union[
|
||||
None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import bdb
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Dict
|
||||
@@ -28,7 +27,6 @@ from _pytest._code.code import TerminalRepr
|
||||
from _pytest.compat import final
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.deprecated import check_ispytest
|
||||
from _pytest.deprecated import UNITTEST_SKIP_DURING_COLLECTION
|
||||
from _pytest.nodes import Collector
|
||||
from _pytest.nodes import Item
|
||||
from _pytest.nodes import Node
|
||||
@@ -379,11 +377,6 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport:
|
||||
# Type ignored because unittest is loaded dynamically.
|
||||
skip_exceptions.append(unittest.SkipTest) # type: ignore
|
||||
if isinstance(call.excinfo.value, tuple(skip_exceptions)):
|
||||
if unittest is not None and isinstance(
|
||||
call.excinfo.value, unittest.SkipTest # type: ignore[attr-defined]
|
||||
):
|
||||
warnings.warn(UNITTEST_SKIP_DURING_COLLECTION, stacklevel=2)
|
||||
|
||||
outcome = "skipped"
|
||||
r_ = collector._repr_failure_py(call.excinfo, "line")
|
||||
assert isinstance(r_, ExceptionChainRepr), repr(r_)
|
||||
|
||||
@@ -542,15 +542,21 @@ class TerminalReporter:
|
||||
if not running_xdist:
|
||||
self.write_ensure_prefix(line, word, **markup)
|
||||
if rep.skipped or hasattr(report, "wasxfail"):
|
||||
available_width = (
|
||||
(self._tw.fullwidth - self._tw.width_of_current_line)
|
||||
- len(" [100%]")
|
||||
- 1
|
||||
)
|
||||
reason = _get_raw_skip_reason(rep)
|
||||
reason_ = _format_trimmed(" ({})", reason, available_width)
|
||||
if reason and reason_ is not None:
|
||||
self._tw.write(reason_)
|
||||
if self.config.option.verbose < 2:
|
||||
available_width = (
|
||||
(self._tw.fullwidth - self._tw.width_of_current_line)
|
||||
- len(" [100%]")
|
||||
- 1
|
||||
)
|
||||
formatted_reason = _format_trimmed(
|
||||
" ({})", reason, available_width
|
||||
)
|
||||
else:
|
||||
formatted_reason = f" ({reason})"
|
||||
|
||||
if reason and formatted_reason is not None:
|
||||
self._tw.write(formatted_reason)
|
||||
if self._show_progress_info:
|
||||
self._write_progress_information_filling_space()
|
||||
else:
|
||||
@@ -657,7 +663,7 @@ class TerminalReporter:
|
||||
errors = len(self.stats.get("error", []))
|
||||
skipped = len(self.stats.get("skipped", []))
|
||||
deselected = len(self.stats.get("deselected", []))
|
||||
selected = self._numcollected - errors - skipped - deselected
|
||||
selected = self._numcollected - deselected
|
||||
line = "collected " if final else "collecting "
|
||||
line += (
|
||||
str(self._numcollected) + " item" + ("" if self._numcollected == 1 else "s")
|
||||
@@ -668,7 +674,7 @@ class TerminalReporter:
|
||||
line += " / %d deselected" % deselected
|
||||
if skipped:
|
||||
line += " / %d skipped" % skipped
|
||||
if self._numcollected > selected > 0:
|
||||
if self._numcollected > selected:
|
||||
line += " / %d selected" % selected
|
||||
if self.isatty:
|
||||
self.rewrite(line, bold=True, erase=True)
|
||||
|
||||
@@ -27,7 +27,7 @@ from _pytest.outcomes import skip
|
||||
from _pytest.outcomes import xfail
|
||||
from _pytest.python import Class
|
||||
from _pytest.python import Function
|
||||
from _pytest.python import PyCollector
|
||||
from _pytest.python import Module
|
||||
from _pytest.runner import CallInfo
|
||||
from _pytest.scope import Scope
|
||||
|
||||
@@ -42,7 +42,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
def pytest_pycollect_makeitem(
|
||||
collector: PyCollector, name: str, obj: object
|
||||
collector: Union[Module, Class], name: str, obj: object
|
||||
) -> Optional["UnitTestCase"]:
|
||||
# Has unittest been imported and is obj a subclass of its TestCase?
|
||||
try:
|
||||
@@ -185,6 +185,15 @@ class TestCaseFunction(Function):
|
||||
_excinfo: Optional[List[_pytest._code.ExceptionInfo[BaseException]]] = None
|
||||
_testcase: Optional["unittest.TestCase"] = None
|
||||
|
||||
def _getobj(self):
|
||||
assert self.parent is not None
|
||||
# Unlike a regular Function in a Class, where `item.obj` returns
|
||||
# a *bound* method (attached to an instance), TestCaseFunction's
|
||||
# `obj` returns an *unbound* method (not attached to an instance).
|
||||
# This inconsistency is probably not desirable, but needs some
|
||||
# consideration before changing.
|
||||
return getattr(self.parent.obj, self.originalname) # type: ignore[attr-defined]
|
||||
|
||||
def setup(self) -> None:
|
||||
# A bound method to be called during teardown() if set (see 'runtest()').
|
||||
self._explicit_tearDown: Optional[Callable[[], None]] = None
|
||||
|
||||
@@ -48,13 +48,6 @@ class PytestDeprecationWarning(PytestWarning, DeprecationWarning):
|
||||
__module__ = "pytest"
|
||||
|
||||
|
||||
@final
|
||||
class PytestRemovedIn7Warning(PytestDeprecationWarning):
|
||||
"""Warning class for features that will be removed in pytest 7."""
|
||||
|
||||
__module__ = "pytest"
|
||||
|
||||
|
||||
@final
|
||||
class PytestRemovedIn8Warning(PytestDeprecationWarning):
|
||||
"""Warning class for features that will be removed in pytest 8."""
|
||||
|
||||
@@ -49,8 +49,6 @@ def catch_warnings_for_item(
|
||||
warnings.filterwarnings("always", category=DeprecationWarning)
|
||||
warnings.filterwarnings("always", category=PendingDeprecationWarning)
|
||||
|
||||
warnings.filterwarnings("error", category=pytest.PytestRemovedIn7Warning)
|
||||
|
||||
apply_warning_filters(config_filters, cmdline_filters)
|
||||
|
||||
# apply filters from "filterwarnings" marks
|
||||
@@ -63,14 +61,6 @@ def catch_warnings_for_item(
|
||||
yield
|
||||
|
||||
for warning_message in log:
|
||||
ihook.pytest_warning_captured.call_historic(
|
||||
kwargs=dict(
|
||||
warning_message=warning_message,
|
||||
when=when,
|
||||
item=item,
|
||||
location=None,
|
||||
)
|
||||
)
|
||||
ihook.pytest_warning_recorded.call_historic(
|
||||
kwargs=dict(
|
||||
warning_message=warning_message,
|
||||
@@ -91,6 +81,23 @@ def warning_record_to_str(warning_message: warnings.WarningMessage) -> str:
|
||||
warning_message.lineno,
|
||||
warning_message.line,
|
||||
)
|
||||
if warning_message.source is not None:
|
||||
try:
|
||||
import tracemalloc
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
tb = tracemalloc.get_object_traceback(warning_message.source)
|
||||
if tb is not None:
|
||||
formatted_tb = "\n".join(tb.format())
|
||||
# Use a leading new line to better separate the (large) output
|
||||
# from the traceback to the previous warning text.
|
||||
msg += f"\nObject allocated at:\n{formatted_tb}"
|
||||
else:
|
||||
# No need for a leading new line.
|
||||
url = "https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings"
|
||||
msg += "Enable tracemalloc to get traceback where the object was allocated.\n"
|
||||
msg += f"See {url} for more info."
|
||||
return msg
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# PYTHON_ARGCOMPLETE_OK
|
||||
"""pytest: unit and functional testing with Python."""
|
||||
from . import collect
|
||||
from _pytest import __version__
|
||||
from _pytest import version_tuple
|
||||
from _pytest._code import ExceptionInfo
|
||||
@@ -19,12 +18,13 @@ from _pytest.config import UsageError
|
||||
from _pytest.config.argparsing import OptionGroup
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.debugging import pytestPDB as __pytestPDB
|
||||
from _pytest.fixtures import _fillfuncargs
|
||||
from _pytest.fixtures import fixture
|
||||
from _pytest.fixtures import FixtureLookupError
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
from _pytest.fixtures import yield_fixture
|
||||
from _pytest.freeze_support import freeze_includes
|
||||
from _pytest.legacypath import TempdirFactory
|
||||
from _pytest.legacypath import Testdir
|
||||
from _pytest.logging import LogCaptureFixture
|
||||
from _pytest.main import Session
|
||||
from _pytest.mark import Mark
|
||||
@@ -68,7 +68,6 @@ from _pytest.warning_types import PytestCollectionWarning
|
||||
from _pytest.warning_types import PytestConfigWarning
|
||||
from _pytest.warning_types import PytestDeprecationWarning
|
||||
from _pytest.warning_types import PytestExperimentalApiWarning
|
||||
from _pytest.warning_types import PytestRemovedIn7Warning
|
||||
from _pytest.warning_types import PytestRemovedIn8Warning
|
||||
from _pytest.warning_types import PytestUnhandledCoroutineWarning
|
||||
from _pytest.warning_types import PytestUnhandledThreadExceptionWarning
|
||||
@@ -81,14 +80,12 @@ set_trace = __pytestPDB.set_trace
|
||||
|
||||
__all__ = [
|
||||
"__version__",
|
||||
"_fillfuncargs",
|
||||
"approx",
|
||||
"Cache",
|
||||
"CallInfo",
|
||||
"CaptureFixture",
|
||||
"Class",
|
||||
"cmdline",
|
||||
"collect",
|
||||
"Collector",
|
||||
"CollectReport",
|
||||
"Config",
|
||||
@@ -129,7 +126,6 @@ __all__ = [
|
||||
"PytestConfigWarning",
|
||||
"PytestDeprecationWarning",
|
||||
"PytestExperimentalApiWarning",
|
||||
"PytestRemovedIn7Warning",
|
||||
"PytestRemovedIn8Warning",
|
||||
"Pytester",
|
||||
"PytestPluginManager",
|
||||
@@ -148,7 +144,9 @@ __all__ = [
|
||||
"Stash",
|
||||
"StashKey",
|
||||
"version_tuple",
|
||||
"TempdirFactory",
|
||||
"TempPathFactory",
|
||||
"Testdir",
|
||||
"TestReport",
|
||||
"UsageError",
|
||||
"WarningsRecorder",
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import sys
|
||||
import warnings
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
from _pytest.deprecated import PYTEST_COLLECT_MODULE
|
||||
|
||||
COLLECT_FAKEMODULE_ATTRIBUTES = [
|
||||
"Collector",
|
||||
"Module",
|
||||
"Function",
|
||||
"Session",
|
||||
"Item",
|
||||
"Class",
|
||||
"File",
|
||||
"_fillfuncargs",
|
||||
]
|
||||
|
||||
|
||||
class FakeCollectModule(ModuleType):
|
||||
def __init__(self) -> None:
|
||||
super().__init__("pytest.collect")
|
||||
self.__all__ = list(COLLECT_FAKEMODULE_ATTRIBUTES)
|
||||
self.__pytest = pytest
|
||||
|
||||
def __dir__(self) -> List[str]:
|
||||
return dir(super()) + self.__all__
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
if name not in self.__all__:
|
||||
raise AttributeError(name)
|
||||
warnings.warn(PYTEST_COLLECT_MODULE.format(name=name), stacklevel=2)
|
||||
return getattr(pytest, name)
|
||||
|
||||
|
||||
sys.modules["pytest.collect"] = FakeCollectModule()
|
||||
@@ -1238,8 +1238,6 @@ def test_pdb_can_be_rewritten(pytester: Pytester) -> None:
|
||||
" def check():",
|
||||
"> assert 1 == 2",
|
||||
"E assert 1 == 2",
|
||||
"E +1",
|
||||
"E -2",
|
||||
"",
|
||||
"pdb.py:2: AssertionError",
|
||||
"*= 1 failed in *",
|
||||
@@ -1281,7 +1279,7 @@ def test_tee_stdio_captures_and_live_prints(pytester: Pytester) -> None:
|
||||
reason="Windows raises `OSError: [Errno 22] Invalid argument` instead",
|
||||
)
|
||||
def test_no_brokenpipeerror_message(pytester: Pytester) -> None:
|
||||
"""Ensure that the broken pipe error message is supressed.
|
||||
"""Ensure that the broken pipe error message is suppressed.
|
||||
|
||||
In some Python versions, it reaches sys.unraisablehook, in others
|
||||
a BrokenPipeError exception is propagated, but either way it prints
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
# flake8: noqa
|
||||
# disable flake check on this file because some constructs are strange
|
||||
# or redundant on purpose and can't be disable on a line-by-line basis
|
||||
import ast
|
||||
import inspect
|
||||
import linecache
|
||||
import sys
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
from types import CodeType
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from _pytest._code import Code
|
||||
@@ -332,8 +329,7 @@ def test_findsource(monkeypatch) -> None:
|
||||
lines = ["if 1:\n", " def x():\n", " pass\n"]
|
||||
co = compile("".join(lines), filename, "exec")
|
||||
|
||||
# Type ignored because linecache.cache is private.
|
||||
monkeypatch.setitem(linecache.cache, filename, (1, None, lines, filename)) # type: ignore[attr-defined]
|
||||
monkeypatch.setitem(linecache.cache, filename, (1, None, lines, filename))
|
||||
|
||||
src, lineno = findsource(co)
|
||||
assert src is not None
|
||||
@@ -484,7 +480,7 @@ def test_source_with_decorator() -> None:
|
||||
|
||||
src = inspect.getsource(deco_fixture)
|
||||
assert src == " @pytest.fixture\n def deco_fixture():\n assert False\n"
|
||||
# currenly Source does not unwrap decorators, testing the
|
||||
# currently Source does not unwrap decorators, testing the
|
||||
# existing behavior here for explicitness, but perhaps we should revisit/change this
|
||||
# in the future
|
||||
assert str(Source(deco_fixture)).startswith("@functools.wraps(function)")
|
||||
@@ -618,6 +614,19 @@ def something():
|
||||
assert str(source) == "def func(): raise ValueError(42)"
|
||||
|
||||
|
||||
def test_decorator() -> None:
|
||||
s = """\
|
||||
def foo(f):
|
||||
pass
|
||||
|
||||
@foo
|
||||
def bar():
|
||||
pass
|
||||
"""
|
||||
source = getstatement(3, s)
|
||||
assert "@foo" in str(source)
|
||||
|
||||
|
||||
def XXX_test_expression_multiline() -> None:
|
||||
source = """\
|
||||
something
|
||||
|
||||
@@ -2,7 +2,6 @@ import re
|
||||
import sys
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from _pytest import deprecated
|
||||
@@ -11,13 +10,6 @@ from _pytest.pytester import Pytester
|
||||
from pytest import PytestDeprecationWarning
|
||||
|
||||
|
||||
@pytest.mark.parametrize("attribute", pytest.collect.__all__) # type: ignore
|
||||
# false positive due to dynamic attribute
|
||||
def test_pytest_collect_module_deprecated(attribute) -> None:
|
||||
with pytest.warns(DeprecationWarning, match=attribute):
|
||||
getattr(pytest.collect, attribute)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("plugin", sorted(deprecated.DEPRECATED_EXTERNAL_PLUGINS))
|
||||
@pytest.mark.filterwarnings("default")
|
||||
def test_external_plugins_integrated(pytester: Pytester, plugin) -> None:
|
||||
@@ -28,54 +20,6 @@ def test_external_plugins_integrated(pytester: Pytester, plugin) -> None:
|
||||
pytester.parseconfig("-p", plugin)
|
||||
|
||||
|
||||
def test_fillfuncargs_is_deprecated() -> None:
|
||||
with pytest.warns(
|
||||
pytest.PytestDeprecationWarning,
|
||||
match=re.escape(
|
||||
"pytest._fillfuncargs() is deprecated, use "
|
||||
"function._request._fillfixtures() instead if you cannot avoid reaching into internals."
|
||||
),
|
||||
):
|
||||
pytest._fillfuncargs(mock.Mock())
|
||||
|
||||
|
||||
def test_fillfixtures_is_deprecated() -> None:
|
||||
import _pytest.fixtures
|
||||
|
||||
with pytest.warns(
|
||||
pytest.PytestDeprecationWarning,
|
||||
match=re.escape(
|
||||
"_pytest.fixtures.fillfixtures() is deprecated, use "
|
||||
"function._request._fillfixtures() instead if you cannot avoid reaching into internals."
|
||||
),
|
||||
):
|
||||
_pytest.fixtures.fillfixtures(mock.Mock())
|
||||
|
||||
|
||||
def test_minus_k_dash_is_deprecated(pytester: Pytester) -> None:
|
||||
threepass = pytester.makepyfile(
|
||||
test_threepass="""
|
||||
def test_one(): assert 1
|
||||
def test_two(): assert 1
|
||||
def test_three(): assert 1
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest("-k=-test_two", threepass)
|
||||
result.stdout.fnmatch_lines(["*The `-k '-expr'` syntax*deprecated*"])
|
||||
|
||||
|
||||
def test_minus_k_colon_is_deprecated(pytester: Pytester) -> None:
|
||||
threepass = pytester.makepyfile(
|
||||
test_threepass="""
|
||||
def test_one(): assert 1
|
||||
def test_two(): assert 1
|
||||
def test_three(): assert 1
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest("-k", "test_two:", threepass)
|
||||
result.stdout.fnmatch_lines(["*The `-k 'expr:'` syntax*deprecated*"])
|
||||
|
||||
|
||||
def test_fscollector_gethookproxy_isinitpath(pytester: Pytester) -> None:
|
||||
module = pytester.getmodulecol(
|
||||
"""
|
||||
@@ -142,23 +86,6 @@ def test_private_is_deprecated() -> None:
|
||||
PrivateInit(10, _ispytest=True)
|
||||
|
||||
|
||||
def test_raising_unittest_skiptest_during_collection_is_deprecated(
|
||||
pytester: Pytester,
|
||||
) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
import unittest
|
||||
raise unittest.SkipTest()
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*PytestRemovedIn8Warning: Raising unittest.SkipTest*",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("hooktype", ["hook", "ihook"])
|
||||
def test_hookproxy_warnings_for_pathlib(tmp_path, hooktype, request):
|
||||
path = legacy_path(tmp_path)
|
||||
@@ -194,8 +121,10 @@ def test_warns_none_is_deprecated():
|
||||
with pytest.warns(
|
||||
PytestDeprecationWarning,
|
||||
match=re.escape(
|
||||
"Passing None to catch any warning has been deprecated, pass no arguments instead:\n "
|
||||
"Replace pytest.warns(None) by simply pytest.warns()."
|
||||
"Passing None has been deprecated.\n"
|
||||
"See https://docs.pytest.org/en/latest/how-to/capture-warnings.html"
|
||||
"#additional-use-cases-of-warnings-in-tests"
|
||||
" for alternatives in common use cases."
|
||||
),
|
||||
):
|
||||
with pytest.warns(None): # type: ignore[call-overload]
|
||||
@@ -290,10 +219,6 @@ def test_node_ctor_fspath_argument_is_deprecated(pytester: Pytester) -> None:
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info < (3, 7),
|
||||
reason="This deprecation can only be emitted on python>=3.7",
|
||||
)
|
||||
def test_importing_instance_is_deprecated(pytester: Pytester) -> None:
|
||||
with pytest.warns(
|
||||
pytest.PytestDeprecationWarning,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import field
|
||||
|
||||
|
||||
def test_dataclasses() -> None:
|
||||
@dataclass
|
||||
class SimpleDataObject:
|
||||
field_a: int = field()
|
||||
field_b: str = field()
|
||||
|
||||
def __eq__(self, __o: object) -> bool:
|
||||
return super().__eq__(__o)
|
||||
|
||||
left = SimpleDataObject(1, "b")
|
||||
right = SimpleDataObject(1, "c")
|
||||
|
||||
assert left == right
|
||||
@@ -56,7 +56,7 @@ def test_terminalwriter_not_unicode() -> None:
|
||||
file = io.TextIOWrapper(buffer, encoding="cp1252")
|
||||
tw = terminalwriter.TerminalWriter(file)
|
||||
tw.write("hello 🌀 wôrld אבג", flush=True)
|
||||
assert buffer.getvalue() == br"hello \U0001f300 w\xf4rld \u05d0\u05d1\u05d2"
|
||||
assert buffer.getvalue() == rb"hello \U0001f300 w\xf4rld \u05d0\u05d1\u05d2"
|
||||
|
||||
|
||||
win32 = int(sys.platform == "win32")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[pytest]
|
||||
addopts = --strict-markers
|
||||
asyncio_mode = strict
|
||||
filterwarnings =
|
||||
error::pytest.PytestWarning
|
||||
ignore:.*.fspath is deprecated and will be replaced by .*.path.*:pytest.PytestDeprecationWarning
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
anyio[curio,trio]==3.4.0
|
||||
django==3.2.9
|
||||
pytest-asyncio==0.16.0
|
||||
anyio[curio,trio]==3.5.0
|
||||
django==4.0.3
|
||||
pytest-asyncio==0.18.2
|
||||
pytest-bdd==5.0.0
|
||||
pytest-cov==3.0.0
|
||||
pytest-django==4.5.1
|
||||
pytest-django==4.5.2
|
||||
pytest-flakes==4.0.5
|
||||
pytest-html==3.1.1
|
||||
pytest-mock==3.6.1
|
||||
pytest-mock==3.7.0
|
||||
pytest-rerunfailures==10.2
|
||||
pytest-sugar==0.9.4
|
||||
pytest-trio==0.7.0
|
||||
pytest-twisted==1.13.4
|
||||
twisted==21.7.0
|
||||
twisted==22.1.0
|
||||
pytest-xvfb==2.0.0
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import operator
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from decimal import Decimal
|
||||
from fractions import Fraction
|
||||
@@ -772,7 +771,7 @@ class TestApprox:
|
||||
def test_expected_value_type_error(self, x, name):
|
||||
with pytest.raises(
|
||||
TypeError,
|
||||
match=fr"pytest.approx\(\) does not support nested {name}:",
|
||||
match=rf"pytest.approx\(\) does not support nested {name}:",
|
||||
):
|
||||
approx(x)
|
||||
|
||||
@@ -810,7 +809,6 @@ class TestApprox:
|
||||
assert 1.0 != approx([None])
|
||||
assert None != approx([1.0]) # noqa: E711
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires ordered dicts")
|
||||
def test_nonnumeric_dict_repr(self):
|
||||
"""Dicts with non-numerics and infinites have no tolerances"""
|
||||
x1 = {"foo": 1.0000005, "bar": None, "foobar": inf}
|
||||
@@ -860,13 +858,21 @@ class TestApprox:
|
||||
assert approx(expected, rel=5e-7, abs=0) == actual
|
||||
assert approx(expected, rel=5e-8, abs=0) != actual
|
||||
|
||||
def test_generic_sized_iterable_object(self):
|
||||
class MySizedIterable:
|
||||
def __iter__(self):
|
||||
return iter([1, 2, 3, 4])
|
||||
def test_generic_ordered_sequence(self):
|
||||
class MySequence:
|
||||
def __getitem__(self, i):
|
||||
return [1, 2, 3, 4][i]
|
||||
|
||||
def __len__(self):
|
||||
return 4
|
||||
|
||||
expected = MySizedIterable()
|
||||
assert [1, 2, 3, 4] == approx(expected)
|
||||
expected = MySequence()
|
||||
assert [1, 2, 3, 4] == approx(expected, abs=1e-4)
|
||||
|
||||
expected_repr = "approx([1 ± 1.0e-06, 2 ± 2.0e-06, 3 ± 3.0e-06, 4 ± 4.0e-06])"
|
||||
assert repr(approx(expected)) == expected_repr
|
||||
|
||||
def test_allow_ordered_sequences_only(self) -> None:
|
||||
"""pytest.approx() should raise an error on unordered sequences (#9692)."""
|
||||
with pytest.raises(TypeError, match="only supports ordered sequences"):
|
||||
assert {1, 2, 3} == approx({1, 2, 3})
|
||||
|
||||
@@ -103,10 +103,6 @@ def test_getfuncargnames_staticmethod_partial():
|
||||
|
||||
@pytest.mark.pytester_example_path("fixtures/fill_fixtures")
|
||||
class TestFillFixtures:
|
||||
def test_fillfuncargs_exposed(self):
|
||||
# used by oejskit, kept for compatibility
|
||||
assert pytest._fillfuncargs == fixtures._fillfuncargs
|
||||
|
||||
def test_funcarg_lookupfails(self, pytester: Pytester) -> None:
|
||||
pytester.copy_example()
|
||||
result = pytester.runpytest() # "--collect-only")
|
||||
|
||||
@@ -1,84 +1,10 @@
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from _pytest import runner
|
||||
from _pytest._code import getfslineno
|
||||
from _pytest.fixtures import getfixturemarker
|
||||
from _pytest.pytester import Pytester
|
||||
from _pytest.python import Function
|
||||
|
||||
|
||||
class TestOEJSKITSpecials:
|
||||
def test_funcarg_non_pycollectobj(
|
||||
self, pytester: Pytester, recwarn
|
||||
) -> None: # rough jstests usage
|
||||
pytester.makeconftest(
|
||||
"""
|
||||
import pytest
|
||||
def pytest_pycollect_makeitem(collector, name, obj):
|
||||
if name == "MyClass":
|
||||
return MyCollector.from_parent(collector, name=name)
|
||||
class MyCollector(pytest.Collector):
|
||||
def reportinfo(self):
|
||||
return self.path, 3, "xyz"
|
||||
"""
|
||||
)
|
||||
modcol = pytester.getmodulecol(
|
||||
"""
|
||||
import pytest
|
||||
@pytest.fixture
|
||||
def arg1(request):
|
||||
return 42
|
||||
class MyClass(object):
|
||||
pass
|
||||
"""
|
||||
)
|
||||
# this hook finds funcarg factories
|
||||
rep = runner.collect_one_node(collector=modcol)
|
||||
# TODO: Don't treat as Any.
|
||||
clscol: Any = rep.result[0]
|
||||
clscol.obj = lambda arg1: None
|
||||
clscol.funcargs = {}
|
||||
pytest._fillfuncargs(clscol)
|
||||
assert clscol.funcargs["arg1"] == 42
|
||||
|
||||
def test_autouse_fixture(
|
||||
self, pytester: Pytester, recwarn
|
||||
) -> None: # rough jstests usage
|
||||
pytester.makeconftest(
|
||||
"""
|
||||
import pytest
|
||||
def pytest_pycollect_makeitem(collector, name, obj):
|
||||
if name == "MyClass":
|
||||
return MyCollector.from_parent(collector, name=name)
|
||||
class MyCollector(pytest.Collector):
|
||||
def reportinfo(self):
|
||||
return self.path, 3, "xyz"
|
||||
"""
|
||||
)
|
||||
modcol = pytester.getmodulecol(
|
||||
"""
|
||||
import pytest
|
||||
@pytest.fixture(autouse=True)
|
||||
def hello():
|
||||
pass
|
||||
@pytest.fixture
|
||||
def arg1(request):
|
||||
return 42
|
||||
class MyClass(object):
|
||||
pass
|
||||
"""
|
||||
)
|
||||
# this hook finds funcarg factories
|
||||
rep = runner.collect_one_node(modcol)
|
||||
# TODO: Don't treat as Any.
|
||||
clscol: Any = rep.result[0]
|
||||
clscol.obj = lambda: None
|
||||
clscol.funcargs = {}
|
||||
pytest._fillfuncargs(clscol)
|
||||
assert not clscol.funcargs
|
||||
|
||||
|
||||
def test_wrapped_getfslineno() -> None:
|
||||
def func():
|
||||
pass
|
||||
|
||||
@@ -24,8 +24,7 @@ from _pytest.compat import getfuncargnames
|
||||
from _pytest.compat import NOTSET
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.pytester import Pytester
|
||||
from _pytest.python import _idval
|
||||
from _pytest.python import idmaker
|
||||
from _pytest.python import IdMaker
|
||||
from _pytest.scope import Scope
|
||||
|
||||
|
||||
@@ -107,8 +106,8 @@ class TestMetafunc:
|
||||
with pytest.raises(
|
||||
fail.Exception,
|
||||
match=(
|
||||
r"In func: ids must be list of string/float/int/bool, found:"
|
||||
r" Exc\(from_gen\) \(type: <class .*Exc'>\) at index 2"
|
||||
r"In func: ids contains unsupported value Exc\(from_gen\) \(type: <class .*Exc'>\) at index 2. "
|
||||
r"Supported types are: .*"
|
||||
),
|
||||
):
|
||||
metafunc.parametrize("x", [1, 2, 3], ids=gen()) # type: ignore[arg-type]
|
||||
@@ -286,7 +285,7 @@ class TestMetafunc:
|
||||
deadline=400.0
|
||||
) # very close to std deadline and CI boxes are not reliable in CPU power
|
||||
def test_idval_hypothesis(self, value) -> None:
|
||||
escaped = _idval(value, "a", 6, None, nodeid=None, config=None)
|
||||
escaped = IdMaker([], [], None, None, None, None, None)._idval(value, "a", 6)
|
||||
assert isinstance(escaped, str)
|
||||
escaped.encode("ascii")
|
||||
|
||||
@@ -308,7 +307,10 @@ class TestMetafunc:
|
||||
),
|
||||
]
|
||||
for val, expected in values:
|
||||
assert _idval(val, "a", 6, None, nodeid=None, config=None) == expected
|
||||
assert (
|
||||
IdMaker([], [], None, None, None, None, None)._idval(val, "a", 6)
|
||||
== expected
|
||||
)
|
||||
|
||||
def test_unicode_idval_with_config(self) -> None:
|
||||
"""Unit test for expected behavior to obtain ids with
|
||||
@@ -336,7 +338,7 @@ class TestMetafunc:
|
||||
("ação", MockConfig({option: False}), "a\\xe7\\xe3o"),
|
||||
]
|
||||
for val, config, expected in values:
|
||||
actual = _idval(val, "a", 6, None, nodeid=None, config=config)
|
||||
actual = IdMaker([], [], None, None, config, None, None)._idval(val, "a", 6)
|
||||
assert actual == expected
|
||||
|
||||
def test_bytes_idval(self) -> None:
|
||||
@@ -349,7 +351,10 @@ class TestMetafunc:
|
||||
("αρά".encode(), r"\xce\xb1\xcf\x81\xce\xac"),
|
||||
]
|
||||
for val, expected in values:
|
||||
assert _idval(val, "a", 6, idfn=None, nodeid=None, config=None) == expected
|
||||
assert (
|
||||
IdMaker([], [], None, None, None, None, None)._idval(val, "a", 6)
|
||||
== expected
|
||||
)
|
||||
|
||||
def test_class_or_function_idval(self) -> None:
|
||||
"""Unit test for the expected behavior to obtain ids for parametrized
|
||||
@@ -363,7 +368,10 @@ class TestMetafunc:
|
||||
|
||||
values = [(TestClass, "TestClass"), (test_function, "test_function")]
|
||||
for val, expected in values:
|
||||
assert _idval(val, "a", 6, None, nodeid=None, config=None) == expected
|
||||
assert (
|
||||
IdMaker([], [], None, None, None, None, None)._idval(val, "a", 6)
|
||||
== expected
|
||||
)
|
||||
|
||||
def test_notset_idval(self) -> None:
|
||||
"""Test that a NOTSET value (used by an empty parameterset) generates
|
||||
@@ -371,29 +379,47 @@ class TestMetafunc:
|
||||
|
||||
Regression test for #7686.
|
||||
"""
|
||||
assert _idval(NOTSET, "a", 0, None, nodeid=None, config=None) == "a0"
|
||||
assert (
|
||||
IdMaker([], [], None, None, None, None, None)._idval(NOTSET, "a", 0) == "a0"
|
||||
)
|
||||
|
||||
def test_idmaker_autoname(self) -> None:
|
||||
"""#250"""
|
||||
result = idmaker(
|
||||
("a", "b"), [pytest.param("string", 1.0), pytest.param("st-ring", 2.0)]
|
||||
)
|
||||
result = IdMaker(
|
||||
("a", "b"),
|
||||
[pytest.param("string", 1.0), pytest.param("st-ring", 2.0)],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
).make_unique_parameterset_ids()
|
||||
assert result == ["string-1.0", "st-ring-2.0"]
|
||||
|
||||
result = idmaker(
|
||||
("a", "b"), [pytest.param(object(), 1.0), pytest.param(object(), object())]
|
||||
)
|
||||
result = IdMaker(
|
||||
("a", "b"),
|
||||
[pytest.param(object(), 1.0), pytest.param(object(), object())],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
).make_unique_parameterset_ids()
|
||||
assert result == ["a0-1.0", "a1-b1"]
|
||||
# unicode mixing, issue250
|
||||
result = idmaker(("a", "b"), [pytest.param({}, b"\xc3\xb4")])
|
||||
result = IdMaker(
|
||||
("a", "b"), [pytest.param({}, b"\xc3\xb4")], None, None, None, None, None
|
||||
).make_unique_parameterset_ids()
|
||||
assert result == ["a0-\\xc3\\xb4"]
|
||||
|
||||
def test_idmaker_with_bytes_regex(self) -> None:
|
||||
result = idmaker(("a"), [pytest.param(re.compile(b"foo"), 1.0)])
|
||||
result = IdMaker(
|
||||
("a"), [pytest.param(re.compile(b"foo"), 1.0)], None, None, None, None, None
|
||||
).make_unique_parameterset_ids()
|
||||
assert result == ["foo"]
|
||||
|
||||
def test_idmaker_native_strings(self) -> None:
|
||||
result = idmaker(
|
||||
result = IdMaker(
|
||||
("a", "b"),
|
||||
[
|
||||
pytest.param(1.0, -1.1),
|
||||
@@ -410,7 +436,12 @@ class TestMetafunc:
|
||||
pytest.param(b"\xc3\xb4", "other"),
|
||||
pytest.param(1.0j, -2.0j),
|
||||
],
|
||||
)
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
).make_unique_parameterset_ids()
|
||||
assert result == [
|
||||
"1.0--1.1",
|
||||
"2--202",
|
||||
@@ -428,7 +459,7 @@ class TestMetafunc:
|
||||
]
|
||||
|
||||
def test_idmaker_non_printable_characters(self) -> None:
|
||||
result = idmaker(
|
||||
result = IdMaker(
|
||||
("s", "n"),
|
||||
[
|
||||
pytest.param("\x00", 1),
|
||||
@@ -438,23 +469,35 @@ class TestMetafunc:
|
||||
pytest.param("\t", 5),
|
||||
pytest.param(b"\t", 6),
|
||||
],
|
||||
)
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
).make_unique_parameterset_ids()
|
||||
assert result == ["\\x00-1", "\\x05-2", "\\x00-3", "\\x05-4", "\\t-5", "\\t-6"]
|
||||
|
||||
def test_idmaker_manual_ids_must_be_printable(self) -> None:
|
||||
result = idmaker(
|
||||
result = IdMaker(
|
||||
("s",),
|
||||
[
|
||||
pytest.param("x00", id="hello \x00"),
|
||||
pytest.param("x05", id="hello \x05"),
|
||||
],
|
||||
)
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
).make_unique_parameterset_ids()
|
||||
assert result == ["hello \\x00", "hello \\x05"]
|
||||
|
||||
def test_idmaker_enum(self) -> None:
|
||||
enum = pytest.importorskip("enum")
|
||||
e = enum.Enum("Foo", "one, two")
|
||||
result = idmaker(("a", "b"), [pytest.param(e.one, e.two)])
|
||||
result = IdMaker(
|
||||
("a", "b"), [pytest.param(e.one, e.two)], None, None, None, None, None
|
||||
).make_unique_parameterset_ids()
|
||||
assert result == ["Foo.one-Foo.two"]
|
||||
|
||||
def test_idmaker_idfn(self) -> None:
|
||||
@@ -465,15 +508,19 @@ class TestMetafunc:
|
||||
return repr(val)
|
||||
return None
|
||||
|
||||
result = idmaker(
|
||||
result = IdMaker(
|
||||
("a", "b"),
|
||||
[
|
||||
pytest.param(10.0, IndexError()),
|
||||
pytest.param(20, KeyError()),
|
||||
pytest.param("three", [1, 2, 3]),
|
||||
],
|
||||
idfn=ids,
|
||||
)
|
||||
ids,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
).make_unique_parameterset_ids()
|
||||
assert result == ["10.0-IndexError()", "20-KeyError()", "three-b2"]
|
||||
|
||||
def test_idmaker_idfn_unique_names(self) -> None:
|
||||
@@ -482,15 +529,19 @@ class TestMetafunc:
|
||||
def ids(val: object) -> str:
|
||||
return "a"
|
||||
|
||||
result = idmaker(
|
||||
result = IdMaker(
|
||||
("a", "b"),
|
||||
[
|
||||
pytest.param(10.0, IndexError()),
|
||||
pytest.param(20, KeyError()),
|
||||
pytest.param("three", [1, 2, 3]),
|
||||
],
|
||||
idfn=ids,
|
||||
)
|
||||
ids,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
).make_unique_parameterset_ids()
|
||||
assert result == ["a-a0", "a-a1", "a-a2"]
|
||||
|
||||
def test_idmaker_with_idfn_and_config(self) -> None:
|
||||
@@ -520,12 +571,15 @@ class TestMetafunc:
|
||||
(MockConfig({option: False}), "a\\xe7\\xe3o"),
|
||||
]
|
||||
for config, expected in values:
|
||||
result = idmaker(
|
||||
result = IdMaker(
|
||||
("a",),
|
||||
[pytest.param("string")],
|
||||
idfn=lambda _: "ação",
|
||||
config=config,
|
||||
)
|
||||
lambda _: "ação",
|
||||
None,
|
||||
config,
|
||||
None,
|
||||
None,
|
||||
).make_unique_parameterset_ids()
|
||||
assert result == [expected]
|
||||
|
||||
def test_idmaker_with_ids_and_config(self) -> None:
|
||||
@@ -555,12 +609,9 @@ class TestMetafunc:
|
||||
(MockConfig({option: False}), "a\\xe7\\xe3o"),
|
||||
]
|
||||
for config, expected in values:
|
||||
result = idmaker(
|
||||
("a",),
|
||||
[pytest.param("string")],
|
||||
ids=["ação"],
|
||||
config=config,
|
||||
)
|
||||
result = IdMaker(
|
||||
("a",), [pytest.param("string")], None, ["ação"], config, None, None
|
||||
).make_unique_parameterset_ids()
|
||||
assert result == [expected]
|
||||
|
||||
def test_parametrize_ids_exception(self, pytester: Pytester) -> None:
|
||||
@@ -617,23 +668,39 @@ class TestMetafunc:
|
||||
)
|
||||
|
||||
def test_idmaker_with_ids(self) -> None:
|
||||
result = idmaker(
|
||||
("a", "b"), [pytest.param(1, 2), pytest.param(3, 4)], ids=["a", None]
|
||||
)
|
||||
result = IdMaker(
|
||||
("a", "b"),
|
||||
[pytest.param(1, 2), pytest.param(3, 4)],
|
||||
None,
|
||||
["a", None],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
).make_unique_parameterset_ids()
|
||||
assert result == ["a", "3-4"]
|
||||
|
||||
def test_idmaker_with_paramset_id(self) -> None:
|
||||
result = idmaker(
|
||||
result = IdMaker(
|
||||
("a", "b"),
|
||||
[pytest.param(1, 2, id="me"), pytest.param(3, 4, id="you")],
|
||||
ids=["a", None],
|
||||
)
|
||||
None,
|
||||
["a", None],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
).make_unique_parameterset_ids()
|
||||
assert result == ["me", "you"]
|
||||
|
||||
def test_idmaker_with_ids_unique_names(self) -> None:
|
||||
result = idmaker(
|
||||
("a"), map(pytest.param, [1, 2, 3, 4, 5]), ids=["a", "a", "b", "c", "b"]
|
||||
)
|
||||
result = IdMaker(
|
||||
("a"),
|
||||
list(map(pytest.param, [1, 2, 3, 4, 5])),
|
||||
None,
|
||||
["a", "a", "b", "c", "b"],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
).make_unique_parameterset_ids()
|
||||
assert result == ["a0", "a1", "b0", "c", "b1"]
|
||||
|
||||
def test_parametrize_indirect(self) -> None:
|
||||
@@ -1272,7 +1339,7 @@ class TestMetafuncFunctional:
|
||||
"""
|
||||
import pytest
|
||||
|
||||
@pytest.mark.parametrize("x, expected", [(1, 2), (3, 4), (5, 6)], ids=(None, 2, type))
|
||||
@pytest.mark.parametrize("x, expected", [(1, 2), (3, 4), (5, 6)], ids=(None, 2, OSError()))
|
||||
def test_ids_numbers(x,expected):
|
||||
assert x * 2 == expected
|
||||
"""
|
||||
@@ -1280,8 +1347,8 @@ class TestMetafuncFunctional:
|
||||
result = pytester.runpytest()
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"In test_ids_numbers: ids must be list of string/float/int/bool,"
|
||||
" found: <class 'type'> (type: <class 'type'>) at index 2"
|
||||
"In test_ids_numbers: ids contains unsupported value OSError() (type: <class 'OSError'>) at index 2. "
|
||||
"Supported types are: str, bytes, int, float, complex, bool, enum, regex or anything with a __name__."
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ class TestImportHookInstallation:
|
||||
"E assert {'failed': 1,... 'skipped': 0} == {'failed': 0,... 'skipped': 0}",
|
||||
"E Omitting 1 identical items, use -vv to show",
|
||||
"E Differing items:",
|
||||
"E Use -v to get the full diff",
|
||||
"E Use -v to get more diff",
|
||||
]
|
||||
)
|
||||
# XXX: unstable output.
|
||||
@@ -376,7 +376,7 @@ class TestAssert_reprcompare:
|
||||
assert diff == [
|
||||
"b'spam' == b'eggs'",
|
||||
"At index 0 diff: b's' != b'e'",
|
||||
"Use -v to get the full diff",
|
||||
"Use -v to get more diff",
|
||||
]
|
||||
|
||||
def test_bytes_diff_verbose(self) -> None:
|
||||
@@ -444,11 +444,19 @@ class TestAssert_reprcompare:
|
||||
"""
|
||||
expl = callequal(left, right, verbose=0)
|
||||
assert expl is not None
|
||||
assert expl[-1] == "Use -v to get the full diff"
|
||||
assert expl[-1] == "Use -v to get more diff"
|
||||
verbose_expl = callequal(left, right, verbose=1)
|
||||
assert verbose_expl is not None
|
||||
assert "\n".join(verbose_expl).endswith(textwrap.dedent(expected).strip())
|
||||
|
||||
def test_iterable_quiet(self) -> None:
|
||||
expl = callequal([1, 2], [10, 2], verbose=-1)
|
||||
assert expl == [
|
||||
"[1, 2] == [10, 2]",
|
||||
"At index 0 diff: 1 != 10",
|
||||
"Use -v to get more diff",
|
||||
]
|
||||
|
||||
def test_iterable_full_diff_ci(
|
||||
self, monkeypatch: MonkeyPatch, pytester: Pytester
|
||||
) -> None:
|
||||
@@ -466,7 +474,7 @@ class TestAssert_reprcompare:
|
||||
|
||||
monkeypatch.delenv("CI", raising=False)
|
||||
result = pytester.runpytest()
|
||||
result.stdout.fnmatch_lines(["E Use -v to get the full diff"])
|
||||
result.stdout.fnmatch_lines(["E Use -v to get more diff"])
|
||||
|
||||
def test_list_different_lengths(self) -> None:
|
||||
expl = callequal([0, 1], [0, 1, 2])
|
||||
@@ -699,32 +707,6 @@ class TestAssert_reprcompare:
|
||||
assert expl is not None
|
||||
assert len(expl) > 1
|
||||
|
||||
def test_repr_verbose(self) -> None:
|
||||
class Nums:
|
||||
def __init__(self, nums):
|
||||
self.nums = nums
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.nums)
|
||||
|
||||
list_x = list(range(5000))
|
||||
list_y = list(range(5000))
|
||||
list_y[len(list_y) // 2] = 3
|
||||
nums_x = Nums(list_x)
|
||||
nums_y = Nums(list_y)
|
||||
|
||||
assert callequal(nums_x, nums_y) is None
|
||||
|
||||
expl = callequal(nums_x, nums_y, verbose=1)
|
||||
assert expl is not None
|
||||
assert "+" + repr(nums_x) in expl
|
||||
assert "-" + repr(nums_y) in expl
|
||||
|
||||
expl = callequal(nums_x, nums_y, verbose=2)
|
||||
assert expl is not None
|
||||
assert "+" + repr(nums_x) in expl
|
||||
assert "-" + repr(nums_y) in expl
|
||||
|
||||
def test_list_bad_repr(self) -> None:
|
||||
class A:
|
||||
def __repr__(self):
|
||||
@@ -796,7 +778,6 @@ class TestAssert_reprcompare:
|
||||
|
||||
|
||||
class TestAssert_reprcompare_dataclass:
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
|
||||
def test_dataclasses(self, pytester: Pytester) -> None:
|
||||
p = pytester.copy_example("dataclasses/test_compare_dataclasses.py")
|
||||
result = pytester.runpytest(p)
|
||||
@@ -815,7 +796,6 @@ class TestAssert_reprcompare_dataclass:
|
||||
consecutive=True,
|
||||
)
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
|
||||
def test_recursive_dataclasses(self, pytester: Pytester) -> None:
|
||||
p = pytester.copy_example("dataclasses/test_compare_recursive_dataclasses.py")
|
||||
result = pytester.runpytest(p)
|
||||
@@ -834,7 +814,6 @@ class TestAssert_reprcompare_dataclass:
|
||||
consecutive=True,
|
||||
)
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
|
||||
def test_recursive_dataclasses_verbose(self, pytester: Pytester) -> None:
|
||||
p = pytester.copy_example("dataclasses/test_compare_recursive_dataclasses.py")
|
||||
result = pytester.runpytest(p, "-vv")
|
||||
@@ -854,8 +833,6 @@ class TestAssert_reprcompare_dataclass:
|
||||
"E ",
|
||||
"E Drill down into differing attribute a:",
|
||||
"E a: 10 != 20",
|
||||
"E +10",
|
||||
"E -20",
|
||||
"E ",
|
||||
"E Drill down into differing attribute b:",
|
||||
"E b: 'ten' != 'xxx'",
|
||||
@@ -867,7 +844,6 @@ class TestAssert_reprcompare_dataclass:
|
||||
consecutive=True,
|
||||
)
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
|
||||
def test_dataclasses_verbose(self, pytester: Pytester) -> None:
|
||||
p = pytester.copy_example("dataclasses/test_compare_dataclasses_verbose.py")
|
||||
result = pytester.runpytest(p, "-vv")
|
||||
@@ -881,7 +857,6 @@ class TestAssert_reprcompare_dataclass:
|
||||
]
|
||||
)
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
|
||||
def test_dataclasses_with_attribute_comparison_off(
|
||||
self, pytester: Pytester
|
||||
) -> None:
|
||||
@@ -891,7 +866,6 @@ class TestAssert_reprcompare_dataclass:
|
||||
result = pytester.runpytest(p, "-vv")
|
||||
result.assert_outcomes(failed=0, passed=1)
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
|
||||
def test_comparing_two_different_data_classes(self, pytester: Pytester) -> None:
|
||||
p = pytester.copy_example(
|
||||
"dataclasses/test_compare_two_different_dataclasses.py"
|
||||
@@ -899,6 +873,15 @@ class TestAssert_reprcompare_dataclass:
|
||||
result = pytester.runpytest(p, "-vv")
|
||||
result.assert_outcomes(failed=0, passed=1)
|
||||
|
||||
def test_data_classes_with_custom_eq(self, pytester: Pytester) -> None:
|
||||
p = pytester.copy_example(
|
||||
"dataclasses/test_compare_dataclasses_with_custom_eq.py"
|
||||
)
|
||||
# issue 9362
|
||||
result = pytester.runpytest(p, "-vv")
|
||||
result.assert_outcomes(failed=1, passed=0)
|
||||
result.stdout.no_re_match_line(".*Differing attributes.*")
|
||||
|
||||
|
||||
class TestAssert_reprcompare_attrsclass:
|
||||
def test_attrs(self) -> None:
|
||||
@@ -982,7 +965,6 @@ class TestAssert_reprcompare_attrsclass:
|
||||
right = SimpleDataObject(1, "b")
|
||||
|
||||
lines = callequal(left, right, verbose=2)
|
||||
print(lines)
|
||||
assert lines is not None
|
||||
assert lines[2].startswith("Matching attributes:")
|
||||
assert "Omitting" not in lines[1]
|
||||
@@ -1007,6 +989,36 @@ class TestAssert_reprcompare_attrsclass:
|
||||
lines = callequal(left, right)
|
||||
assert lines is None
|
||||
|
||||
def test_attrs_with_auto_detect_and_custom_eq(self) -> None:
|
||||
@attr.s(
|
||||
auto_detect=True
|
||||
) # attr.s doesn’t ignore a custom eq if auto_detect=True
|
||||
class SimpleDataObject:
|
||||
field_a = attr.ib()
|
||||
|
||||
def __eq__(self, other): # pragma: no cover
|
||||
return super().__eq__(other)
|
||||
|
||||
left = SimpleDataObject(1)
|
||||
right = SimpleDataObject(2)
|
||||
# issue 9362
|
||||
lines = callequal(left, right, verbose=2)
|
||||
assert lines is None
|
||||
|
||||
def test_attrs_with_custom_eq(self) -> None:
|
||||
@attr.define(slots=False)
|
||||
class SimpleDataObject:
|
||||
field_a = attr.ib()
|
||||
|
||||
def __eq__(self, other): # pragma: no cover
|
||||
return super().__eq__(other)
|
||||
|
||||
left = SimpleDataObject(1)
|
||||
right = SimpleDataObject(2)
|
||||
# issue 9362
|
||||
lines = callequal(left, right, verbose=2)
|
||||
assert lines is None
|
||||
|
||||
|
||||
class TestAssert_reprcompare_namedtuple:
|
||||
def test_namedtuple(self) -> None:
|
||||
@@ -1027,7 +1039,7 @@ class TestAssert_reprcompare_namedtuple:
|
||||
" b: 'b' != 'c'",
|
||||
" - c",
|
||||
" + b",
|
||||
"Use -v to get the full diff",
|
||||
"Use -v to get more diff",
|
||||
]
|
||||
|
||||
def test_comparing_two_different_namedtuple(self) -> None:
|
||||
@@ -1042,7 +1054,7 @@ class TestAssert_reprcompare_namedtuple:
|
||||
assert lines == [
|
||||
"NT1(a=1, b='b') == NT2(a=2, b='b')",
|
||||
"At index 0 diff: 1 != 2",
|
||||
"Use -v to get the full diff",
|
||||
"Use -v to get more diff",
|
||||
]
|
||||
|
||||
|
||||
@@ -1616,7 +1628,7 @@ def test_raise_unprintable_assertion_error(pytester: Pytester) -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_raise_assertion_error_raisin_repr(pytester: Pytester) -> None:
|
||||
def test_raise_assertion_error_raising_repr(pytester: Pytester) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
class RaisingRepr(object):
|
||||
@@ -1627,9 +1639,15 @@ def test_raise_assertion_error_raisin_repr(pytester: Pytester) -> None:
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
result.stdout.fnmatch_lines(
|
||||
["E AssertionError: <unprintable AssertionError object>"]
|
||||
)
|
||||
if sys.version_info >= (3, 11):
|
||||
# python 3.11 has native support for un-str-able exceptions
|
||||
result.stdout.fnmatch_lines(
|
||||
["E AssertionError: <exception str() failed>"]
|
||||
)
|
||||
else:
|
||||
result.stdout.fnmatch_lines(
|
||||
["E AssertionError: <unprintable AssertionError object>"]
|
||||
)
|
||||
|
||||
|
||||
def test_issue_1944(pytester: Pytester) -> None:
|
||||
|
||||
@@ -13,10 +13,12 @@ from functools import partial
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
from typing import Dict
|
||||
from typing import Generator
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
from unittest import mock
|
||||
|
||||
import _pytest._code
|
||||
import pytest
|
||||
@@ -1057,7 +1059,7 @@ class TestAssertionRewriteHookDetails:
|
||||
e = OSError()
|
||||
e.errno = 10
|
||||
raise e
|
||||
yield # type:ignore[unreachable]
|
||||
yield
|
||||
|
||||
monkeypatch.setattr(
|
||||
_pytest.assertion.rewrite, "atomic_write", atomic_write_failed
|
||||
@@ -1123,9 +1125,28 @@ class TestAssertionRewriteHookDetails:
|
||||
|
||||
assert _read_pyc(source, pyc) is None # no error
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info < (3, 7), reason="Only the Python 3.7 format for simplicity"
|
||||
)
|
||||
def test_read_pyc_success(self, tmp_path: Path, pytester: Pytester) -> None:
|
||||
"""
|
||||
Ensure that the _rewrite_test() -> _write_pyc() produces a pyc file
|
||||
that can be properly read with _read_pyc()
|
||||
"""
|
||||
from _pytest.assertion import AssertionState
|
||||
from _pytest.assertion.rewrite import _read_pyc
|
||||
from _pytest.assertion.rewrite import _rewrite_test
|
||||
from _pytest.assertion.rewrite import _write_pyc
|
||||
|
||||
config = pytester.parseconfig()
|
||||
state = AssertionState(config, "rewrite")
|
||||
|
||||
fn = tmp_path / "source.py"
|
||||
pyc = Path(str(fn) + "c")
|
||||
|
||||
fn.write_text("def test(): assert True")
|
||||
|
||||
source_stat, co = _rewrite_test(fn, config)
|
||||
_write_pyc(state, co, source_stat, pyc)
|
||||
assert _read_pyc(fn, pyc, state.trace) is not None
|
||||
|
||||
def test_read_pyc_more_invalid(self, tmp_path: Path) -> None:
|
||||
from _pytest.assertion.rewrite import _read_pyc
|
||||
|
||||
@@ -1294,7 +1315,7 @@ class TestIssue2121:
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.maxsize <= (2 ** 31 - 1), reason="Causes OverflowError on 32bit systems"
|
||||
sys.maxsize <= (2**31 - 1), reason="Causes OverflowError on 32bit systems"
|
||||
)
|
||||
@pytest.mark.parametrize("offset", [-1, +1])
|
||||
def test_source_mtime_long_long(pytester: Pytester, offset) -> None:
|
||||
@@ -1313,7 +1334,7 @@ def test_source_mtime_long_long(pytester: Pytester, offset) -> None:
|
||||
# use unsigned long timestamp which overflows signed long,
|
||||
# which was the cause of the bug
|
||||
# +1 offset also tests masking of 0xFFFFFFFF
|
||||
timestamp = 2 ** 32 + offset
|
||||
timestamp = 2**32 + offset
|
||||
os.utime(str(p), (timestamp, timestamp))
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 0
|
||||
@@ -1357,7 +1378,7 @@ class TestEarlyRewriteBailout:
|
||||
@pytest.fixture
|
||||
def hook(
|
||||
self, pytestconfig, monkeypatch, pytester: Pytester
|
||||
) -> AssertionRewritingHook:
|
||||
) -> Generator[AssertionRewritingHook, None, None]:
|
||||
"""Returns a patched AssertionRewritingHook instance so we can configure its initial paths and track
|
||||
if PathFinder.find_spec has been called.
|
||||
"""
|
||||
@@ -1378,11 +1399,11 @@ class TestEarlyRewriteBailout:
|
||||
|
||||
hook = AssertionRewritingHook(pytestconfig)
|
||||
# use default patterns, otherwise we inherit pytest's testing config
|
||||
hook.fnpats[:] = ["test_*.py", "*_test.py"]
|
||||
monkeypatch.setattr(hook, "_find_spec", spy_find_spec)
|
||||
hook.set_session(StubSession()) # type: ignore[arg-type]
|
||||
pytester.syspathinsert()
|
||||
return hook
|
||||
with mock.patch.object(hook, "fnpats", ["test_*.py", "*_test.py"]):
|
||||
monkeypatch.setattr(hook, "_find_spec", spy_find_spec)
|
||||
hook.set_session(StubSession()) # type: ignore[arg-type]
|
||||
pytester.syspathinsert()
|
||||
yield hook
|
||||
|
||||
def test_basic(self, pytester: Pytester, hook: AssertionRewritingHook) -> None:
|
||||
"""
|
||||
@@ -1432,9 +1453,9 @@ class TestEarlyRewriteBailout:
|
||||
}
|
||||
)
|
||||
pytester.syspathinsert("tests")
|
||||
hook.fnpats[:] = ["tests/**.py"]
|
||||
assert hook.find_spec("file") is not None
|
||||
assert self.find_spec_calls == ["file"]
|
||||
with mock.patch.object(hook, "fnpats", ["tests/**.py"]):
|
||||
assert hook.find_spec("file") is not None
|
||||
assert self.find_spec_calls == ["file"]
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.platform.startswith("win32"), reason="cannot remove cwd on Windows"
|
||||
|
||||
@@ -773,7 +773,7 @@ class TestLastFailed:
|
||||
result = pytester.runpytest("--lf", "--lfnf", "none")
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"collected 2 items / 2 deselected",
|
||||
"collected 2 items / 2 deselected / 0 selected",
|
||||
"run-last-failure: no previously failed tests, deselecting all items.",
|
||||
"deselected=2",
|
||||
"* 2 deselected in *",
|
||||
|
||||
@@ -1433,19 +1433,19 @@ def test_error_attribute_issue555(pytester: Pytester) -> None:
|
||||
not sys.platform.startswith("win"),
|
||||
reason="only on windows",
|
||||
)
|
||||
def test_py36_windowsconsoleio_workaround_non_standard_streams() -> None:
|
||||
def test_windowsconsoleio_workaround_non_standard_streams() -> None:
|
||||
"""
|
||||
Ensure _py36_windowsconsoleio_workaround function works with objects that
|
||||
Ensure _windowsconsoleio_workaround function works with objects that
|
||||
do not implement the full ``io``-based stream protocol, for example execnet channels (#2666).
|
||||
"""
|
||||
from _pytest.capture import _py36_windowsconsoleio_workaround
|
||||
from _pytest.capture import _windowsconsoleio_workaround
|
||||
|
||||
class DummyStream:
|
||||
def write(self, s):
|
||||
pass
|
||||
|
||||
stream = cast(TextIO, DummyStream())
|
||||
_py36_windowsconsoleio_workaround(stream)
|
||||
_windowsconsoleio_workaround(stream)
|
||||
|
||||
|
||||
def test_dontreadfrominput_has_encoding(pytester: Pytester) -> None:
|
||||
|
||||
@@ -64,7 +64,7 @@ class TestCollector:
|
||||
|
||||
assert pytester.collect_by_name(modcol, "doesnotexist") is None
|
||||
|
||||
def test_getparent(self, pytester: Pytester) -> None:
|
||||
def test_getparent_and_accessors(self, pytester: Pytester) -> None:
|
||||
modcol = pytester.getmodulecol(
|
||||
"""
|
||||
class TestClass:
|
||||
@@ -77,14 +77,21 @@ class TestCollector:
|
||||
fn = pytester.collect_by_name(cls, "test_foo")
|
||||
assert isinstance(fn, pytest.Function)
|
||||
|
||||
module_parent = fn.getparent(pytest.Module)
|
||||
assert module_parent is modcol
|
||||
assert fn.getparent(pytest.Module) is modcol
|
||||
assert modcol.module is not None
|
||||
assert modcol.cls is None
|
||||
assert modcol.instance is None
|
||||
|
||||
function_parent = fn.getparent(pytest.Function)
|
||||
assert function_parent is fn
|
||||
assert fn.getparent(pytest.Class) is cls
|
||||
assert cls.module is not None
|
||||
assert cls.cls is not None
|
||||
assert cls.instance is None
|
||||
|
||||
class_parent = fn.getparent(pytest.Class)
|
||||
assert class_parent is cls
|
||||
assert fn.getparent(pytest.Function) is fn
|
||||
assert fn.module is not None
|
||||
assert fn.cls is not None
|
||||
assert fn.instance is not None
|
||||
assert fn.function is not None
|
||||
|
||||
def test_getcustomfile_roundtrip(self, pytester: Pytester) -> None:
|
||||
hello = pytester.makefile(".xxx", hello="world")
|
||||
@@ -874,6 +881,36 @@ class TestNodeKeywords:
|
||||
assert item.keywords["kw"] == "method"
|
||||
assert len(item.keywords) == len(set(item.keywords))
|
||||
|
||||
def test_unpacked_marks_added_to_keywords(self, pytester: Pytester) -> None:
|
||||
item = pytester.getitem(
|
||||
"""
|
||||
import pytest
|
||||
pytestmark = pytest.mark.foo
|
||||
class TestClass:
|
||||
pytestmark = pytest.mark.bar
|
||||
def test_method(self): pass
|
||||
test_method.pytestmark = pytest.mark.baz
|
||||
""",
|
||||
"test_method",
|
||||
)
|
||||
assert isinstance(item, pytest.Function)
|
||||
cls = item.getparent(pytest.Class)
|
||||
assert cls is not None
|
||||
mod = item.getparent(pytest.Module)
|
||||
assert mod is not None
|
||||
|
||||
assert item.keywords["foo"] == pytest.mark.foo.mark
|
||||
assert item.keywords["bar"] == pytest.mark.bar.mark
|
||||
assert item.keywords["baz"] == pytest.mark.baz.mark
|
||||
|
||||
assert cls.keywords["foo"] == pytest.mark.foo.mark
|
||||
assert cls.keywords["bar"] == pytest.mark.bar.mark
|
||||
assert "baz" not in cls.keywords
|
||||
|
||||
assert mod.keywords["foo"] == pytest.mark.foo.mark
|
||||
assert "bar" not in mod.keywords
|
||||
assert "baz" not in mod.keywords
|
||||
|
||||
|
||||
COLLECTION_ERROR_PY_FILES = dict(
|
||||
test_01_failure="""
|
||||
@@ -1470,6 +1507,35 @@ class TestImportModeImportlib:
|
||||
]
|
||||
)
|
||||
|
||||
def test_using_python_path(self, pytester: Pytester) -> None:
|
||||
"""
|
||||
Dummy modules created by insert_missing_modules should not get in
|
||||
the way of modules that could be imported via python path (#9645).
|
||||
"""
|
||||
pytester.makeini(
|
||||
"""
|
||||
[pytest]
|
||||
pythonpath = .
|
||||
addopts = --import-mode importlib
|
||||
"""
|
||||
)
|
||||
pytester.makepyfile(
|
||||
**{
|
||||
"tests/__init__.py": "",
|
||||
"tests/conftest.py": "",
|
||||
"tests/subpath/__init__.py": "",
|
||||
"tests/subpath/helper.py": "",
|
||||
"tests/subpath/test_something.py": """
|
||||
import tests.subpath.helper
|
||||
|
||||
def test_something():
|
||||
assert True
|
||||
""",
|
||||
}
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
result.stdout.fnmatch_lines("*1 passed in*")
|
||||
|
||||
|
||||
def test_does_not_crash_on_error_from_decorated_function(pytester: Pytester) -> None:
|
||||
"""Regression test for an issue around bad exception formatting due to
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import enum
|
||||
import sys
|
||||
from functools import partial
|
||||
from functools import wraps
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -91,6 +92,7 @@ def test_get_real_func_partial() -> None:
|
||||
assert get_real_func(partial(foo)) is foo
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info >= (3, 11), reason="couroutine removed")
|
||||
def test_is_generator_asyncio(pytester: Pytester) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
@@ -133,7 +135,7 @@ def test_is_generator_async_gen_syntax(pytester: Pytester) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
from _pytest.compat import is_generator
|
||||
def test_is_generator_py36():
|
||||
def test_is_generator():
|
||||
async def foo():
|
||||
yield
|
||||
await foo()
|
||||
@@ -156,11 +158,11 @@ class ErrorsHelper:
|
||||
|
||||
@property
|
||||
def raise_exception(self):
|
||||
raise Exception("exception should be catched")
|
||||
raise Exception("exception should be caught")
|
||||
|
||||
@property
|
||||
def raise_fail_outcome(self):
|
||||
pytest.fail("fail should be catched")
|
||||
pytest.fail("fail should be caught")
|
||||
|
||||
|
||||
def test_helper_failures() -> None:
|
||||
|
||||
@@ -163,7 +163,17 @@ class TestParseIni:
|
||||
pytester.path.joinpath("pytest.ini").write_text("addopts = -x")
|
||||
result = pytester.runpytest()
|
||||
assert result.ret != 0
|
||||
result.stderr.fnmatch_lines(["ERROR: *pytest.ini:1: no section header defined"])
|
||||
result.stderr.fnmatch_lines("ERROR: *pytest.ini:1: no section header defined")
|
||||
|
||||
def test_toml_parse_error(self, pytester: Pytester) -> None:
|
||||
pytester.makepyprojecttoml(
|
||||
"""
|
||||
\\"
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret != 0
|
||||
result.stderr.fnmatch_lines("ERROR: *pyproject.toml: Invalid statement*")
|
||||
|
||||
@pytest.mark.xfail(reason="probably not needed")
|
||||
def test_confcutdir(self, pytester: Pytester) -> None:
|
||||
@@ -386,7 +396,7 @@ class TestParseIni:
|
||||
pytest.param(
|
||||
"""
|
||||
[some_other_header]
|
||||
required_plugins = wont be triggered
|
||||
required_plugins = won't be triggered
|
||||
[pytest]
|
||||
""",
|
||||
"1.5",
|
||||
@@ -807,7 +817,7 @@ class TestConfigAPI:
|
||||
with pytest.raises(pytest.UsageError, match=exp_match):
|
||||
pytester.parseconfig("--confcutdir", pytester.path.joinpath("file"))
|
||||
with pytest.raises(pytest.UsageError, match=exp_match):
|
||||
pytester.parseconfig("--confcutdir", pytester.path.joinpath("inexistant"))
|
||||
pytester.parseconfig("--confcutdir", pytester.path.joinpath("nonexistent"))
|
||||
|
||||
p = pytester.mkdir("dir")
|
||||
config = pytester.parseconfig("--confcutdir", p)
|
||||
@@ -1264,15 +1274,21 @@ def test_load_initial_conftest_last_ordering(_config_for_test):
|
||||
m = My()
|
||||
pm.register(m)
|
||||
hc = pm.hook.pytest_load_initial_conftests
|
||||
values = hc._nonwrappers + hc._wrappers
|
||||
expected = [
|
||||
"_pytest.config",
|
||||
m.__module__,
|
||||
"_pytest.pythonpath",
|
||||
"_pytest.capture",
|
||||
"_pytest.warnings",
|
||||
hookimpls = [
|
||||
(
|
||||
hookimpl.function.__module__,
|
||||
"wrapper" if hookimpl.hookwrapper else "nonwrapper",
|
||||
)
|
||||
for hookimpl in hc.get_hookimpls()
|
||||
]
|
||||
assert hookimpls == [
|
||||
("_pytest.config", "nonwrapper"),
|
||||
(m.__module__, "nonwrapper"),
|
||||
("_pytest.legacypath", "nonwrapper"),
|
||||
("_pytest.python_path", "nonwrapper"),
|
||||
("_pytest.capture", "wrapper"),
|
||||
("_pytest.warnings", "wrapper"),
|
||||
]
|
||||
assert [x.function.__module__ for x in values] == expected
|
||||
|
||||
|
||||
def test_get_plugin_specs_as_list() -> None:
|
||||
|
||||
@@ -114,6 +114,7 @@ class TestConftestValueAccessGlobal:
|
||||
"a", startdir, importmode="prepend", rootpath=Path(basedir)
|
||||
)
|
||||
assert value == 1.5
|
||||
assert mod.__file__ is not None
|
||||
path = Path(mod.__file__)
|
||||
assert path.parent == basedir / "adir" / "b"
|
||||
assert path.stem == "conftest"
|
||||
@@ -145,10 +146,9 @@ def test_issue151_load_all_conftests(pytester: Pytester) -> None:
|
||||
p = pytester.mkdir(name)
|
||||
p.joinpath("conftest.py").touch()
|
||||
|
||||
conftest = PytestPluginManager()
|
||||
conftest_setinitial(conftest, names)
|
||||
d = list(conftest._conftestpath2mod.values())
|
||||
assert len(d) == len(names)
|
||||
pm = PytestPluginManager()
|
||||
conftest_setinitial(pm, names)
|
||||
assert len(set(pm.get_plugins()) - {pm}) == len(names)
|
||||
|
||||
|
||||
def test_conftest_global_import(pytester: Pytester) -> None:
|
||||
@@ -191,18 +191,20 @@ def test_conftestcutdir(pytester: Pytester) -> None:
|
||||
conf.parent, importmode="prepend", rootpath=pytester.path
|
||||
)
|
||||
assert len(values) == 0
|
||||
assert Path(conf) not in conftest._conftestpath2mod
|
||||
assert not conftest.has_plugin(str(conf))
|
||||
# but we can still import a conftest directly
|
||||
conftest._importconftest(conf, importmode="prepend", rootpath=pytester.path)
|
||||
values = conftest._getconftestmodules(
|
||||
conf.parent, importmode="prepend", rootpath=pytester.path
|
||||
)
|
||||
assert values[0].__file__ is not None
|
||||
assert values[0].__file__.startswith(str(conf))
|
||||
# and all sub paths get updated properly
|
||||
values = conftest._getconftestmodules(
|
||||
p, importmode="prepend", rootpath=pytester.path
|
||||
)
|
||||
assert len(values) == 1
|
||||
assert values[0].__file__ is not None
|
||||
assert values[0].__file__.startswith(str(conf))
|
||||
|
||||
|
||||
@@ -214,6 +216,7 @@ def test_conftestcutdir_inplace_considered(pytester: Pytester) -> None:
|
||||
conf.parent, importmode="prepend", rootpath=pytester.path
|
||||
)
|
||||
assert len(values) == 1
|
||||
assert values[0].__file__ is not None
|
||||
assert values[0].__file__.startswith(str(conf))
|
||||
|
||||
|
||||
@@ -222,15 +225,15 @@ def test_setinitial_conftest_subdirs(pytester: Pytester, name: str) -> None:
|
||||
sub = pytester.mkdir(name)
|
||||
subconftest = sub.joinpath("conftest.py")
|
||||
subconftest.touch()
|
||||
conftest = PytestPluginManager()
|
||||
conftest_setinitial(conftest, [sub.parent], confcutdir=pytester.path)
|
||||
pm = PytestPluginManager()
|
||||
conftest_setinitial(pm, [sub.parent], confcutdir=pytester.path)
|
||||
key = subconftest.resolve()
|
||||
if name not in ("whatever", ".dotdir"):
|
||||
assert key in conftest._conftestpath2mod
|
||||
assert len(conftest._conftestpath2mod) == 1
|
||||
assert pm.has_plugin(str(key))
|
||||
assert len(set(pm.get_plugins()) - {pm}) == 1
|
||||
else:
|
||||
assert key not in conftest._conftestpath2mod
|
||||
assert len(conftest._conftestpath2mod) == 0
|
||||
assert not pm.has_plugin(str(key))
|
||||
assert len(set(pm.get_plugins()) - {pm}) == 0
|
||||
|
||||
|
||||
def test_conftest_confcutdir(pytester: Pytester) -> None:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user