Compare commits
296 Commits
6.2.0.dev0
...
6.2.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9c98762f5 | ||
|
|
8003fd23b9 | ||
|
|
8d605b9b26 | ||
|
|
14e0c3e105 | ||
|
|
45facc16c8 | ||
|
|
99fe887d7c | ||
|
|
8dbf9dc1aa | ||
|
|
baaee2148d | ||
|
|
f7d1ab870f | ||
|
|
b8201c280e | ||
|
|
1f0c50b475 | ||
|
|
da82e1853c | ||
|
|
a566eb9c70 | ||
|
|
d3971c30f4 | ||
|
|
780044b64a | ||
|
|
8354995abc | ||
|
|
8b8b1214f4 | ||
|
|
f854cf66f4 | ||
|
|
c475106f12 | ||
|
|
e7073afe6e | ||
|
|
683f29f84d | ||
|
|
0feeddf8ed | ||
|
|
b478275777 | ||
|
|
3302ff9949 | ||
|
|
59bd0f6912 | ||
|
|
6298ff1f4e | ||
|
|
d51ecbd44d | ||
|
|
f237b077fc | ||
|
|
95e0e19b8d | ||
|
|
cf1051cfba | ||
|
|
b16c091253 | ||
|
|
902739cfc3 | ||
|
|
612f157dbd | ||
|
|
810b878ef8 | ||
|
|
059f6ff315 | ||
|
|
f3006ecb2b | ||
|
|
19de6bccff | ||
|
|
8c120c042c | ||
|
|
68a0f22eee | ||
|
|
1a1bb61340 | ||
|
|
e398c93884 | ||
|
|
760a73c08c | ||
|
|
4fc20c8d28 | ||
|
|
954151cdbd | ||
|
|
eeb3afb8ab | ||
|
|
64bb5f2ad1 | ||
|
|
0d0dfdd7aa | ||
|
|
7a06bc2416 | ||
|
|
4abd71121d | ||
|
|
d4c81ffab4 | ||
|
|
d1cb9de211 | ||
|
|
6a256606c6 | ||
|
|
3405c7e6a8 | ||
|
|
775ba63c67 | ||
|
|
70823da7ed | ||
|
|
d27806295a | ||
|
|
b310872300 | ||
|
|
d50df85e26 | ||
|
|
d59a4996ae | ||
|
|
0cef530d10 | ||
|
|
31021ac8d5 | ||
|
|
52fef811c2 | ||
|
|
148e3c582a | ||
|
|
3e0bbd2f57 | ||
|
|
afd53ede6f | ||
|
|
329e66c22e | ||
|
|
c6ac618baf | ||
|
|
42f9622a90 | ||
|
|
ce825ed16c | ||
|
|
eda681af2b | ||
|
|
b7ba76653d | ||
|
|
30d89fd07e | ||
|
|
8ea8cdb36d | ||
|
|
3b677f79f4 | ||
|
|
537215a16c | ||
|
|
c2f949d68e | ||
|
|
6fe9d2fb9f | ||
|
|
3a899ced76 | ||
|
|
825b81ba52 | ||
|
|
1d532da49e | ||
|
|
767cbeb086 | ||
|
|
25e4dd0d2c | ||
|
|
c14f498622 | ||
|
|
f6b682ad49 | ||
|
|
701ff1f5a1 | ||
|
|
f1e6fdcddb | ||
|
|
b050578882 | ||
|
|
66311ff702 | ||
|
|
ea3c0aa245 | ||
|
|
843bca8c0c | ||
|
|
fa148eadfe | ||
|
|
ff9e35243e | ||
|
|
06a597db14 | ||
|
|
1ed8159c7d | ||
|
|
8320c07134 | ||
|
|
39b2706f6a | ||
|
|
265cc2cfec | ||
|
|
043ed55056 | ||
|
|
e986d84466 | ||
|
|
6f13d1b03b | ||
|
|
7aa5e49fc4 | ||
|
|
02d4b3d75f | ||
|
|
b2e7b9df9e | ||
|
|
c7f8ad17f5 | ||
|
|
5b2e5e8a40 | ||
|
|
29f2f4e854 | ||
|
|
10a3a49bd6 | ||
|
|
9bc633064b | ||
|
|
361f9e20c3 | ||
|
|
1cbb0c3554 | ||
|
|
c784c142a4 | ||
|
|
4c0513bc18 | ||
|
|
3bcd316f07 | ||
|
|
6a5037a25b | ||
|
|
a73fb6e006 | ||
|
|
30287b49cd | ||
|
|
4cd0fde277 | ||
|
|
070f8e0f9d | ||
|
|
1d4cc7eb36 | ||
|
|
b815f430e5 | ||
|
|
3adece9fb7 | ||
|
|
489f6f4499 | ||
|
|
a95da7a425 | ||
|
|
8aa9ea95e1 | ||
|
|
76226182ae | ||
|
|
f9d82a34f4 | ||
|
|
7fb0ea3f68 | ||
|
|
8a38e7a6e8 | ||
|
|
1c18fb8ccc | ||
|
|
2753859ff0 | ||
|
|
a14a229d1b | ||
|
|
9a0f4e57ee | ||
|
|
dd323980f9 | ||
|
|
6cdae8ed40 | ||
|
|
569c091769 | ||
|
|
0c7233032f | ||
|
|
531416cc5a | ||
|
|
6506f016ac | ||
|
|
a1df458e85 | ||
|
|
a7e38c5c61 | ||
|
|
3c7eb5a398 | ||
|
|
ad94456ca0 | ||
|
|
aa843746a4 | ||
|
|
c58abf7ad1 | ||
|
|
5913cd20ec | ||
|
|
6cddeb8cb3 | ||
|
|
0cd190f037 | ||
|
|
47ff911c8f | ||
|
|
8f52fc777a | ||
|
|
65dfa98877 | ||
|
|
65148e3120 | ||
|
|
460b51dd95 | ||
|
|
efe470bf1c | ||
|
|
e3ce5d6b53 | ||
|
|
de810152ec | ||
|
|
b95991aeea | ||
|
|
5711ced250 | ||
|
|
8d369f73ba | ||
|
|
6cd6d9b61a | ||
|
|
a431310c0a | ||
|
|
78c09b9931 | ||
|
|
434e30424e | ||
|
|
b308c6ddb3 | ||
|
|
0fe8a8dfe6 | ||
|
|
cd9b3618c7 | ||
|
|
20b710c4b4 | ||
|
|
c31f4dc112 | ||
|
|
096d096539 | ||
|
|
f7c5067823 | ||
|
|
cde50db6e7 | ||
|
|
c818ac2248 | ||
|
|
6b7203aba7 | ||
|
|
7495d2c345 | ||
|
|
1bd83e75a4 | ||
|
|
8105e60f20 | ||
|
|
ca82214444 | ||
|
|
897f151e94 | ||
|
|
25dee8fef6 | ||
|
|
d9ac2efbcd | ||
|
|
65e6e39b76 | ||
|
|
470ea504e2 | ||
|
|
d6becfa177 | ||
|
|
aa0e2d654f | ||
|
|
f7d4f457d0 | ||
|
|
751575fa97 | ||
|
|
e14b724ff4 | ||
|
|
daa11ab9f1 | ||
|
|
0b14350f23 | ||
|
|
50114d4731 | ||
|
|
1c0c56dfb9 | ||
|
|
0d9e27a363 | ||
|
|
0cdbf8b377 | ||
|
|
e1848349a7 | ||
|
|
03363473f7 | ||
|
|
e8504e04f3 | ||
|
|
824e9cf67a | ||
|
|
fe69d0d680 | ||
|
|
a66b6b857a | ||
|
|
afaabdda8c | ||
|
|
f453460ae7 | ||
|
|
c9e5042d6d | ||
|
|
a642650e17 | ||
|
|
f335144d1d | ||
|
|
23aac10391 | ||
|
|
f61d4ed9d5 | ||
|
|
09e38b1697 | ||
|
|
1b23a111d2 | ||
|
|
e5e47c1097 | ||
|
|
0a258f534f | ||
|
|
f58d0a8c3d | ||
|
|
991bc7bd50 | ||
|
|
fc70fd23a2 | ||
|
|
b4c28dcaa2 | ||
|
|
5182c73fea | ||
|
|
3cae145e41 | ||
|
|
69419cb700 | ||
|
|
cb578a918e | ||
|
|
b53a8bb60f | ||
|
|
3434488af4 | ||
|
|
cdaa1b52be | ||
|
|
008863aeb9 | ||
|
|
37cf4693cf | ||
|
|
3059caf1ee | ||
|
|
55127b2142 | ||
|
|
2e322f183c | ||
|
|
dbd082af96 | ||
|
|
d093931464 | ||
|
|
779b511bfe | ||
|
|
43b1eb3c9e | ||
|
|
5acc55e838 | ||
|
|
1630c37266 | ||
|
|
76acb44330 | ||
|
|
af3759a503 | ||
|
|
95917f8833 | ||
|
|
13ddec9a00 | ||
|
|
b6b75383ce | ||
|
|
f54ec30a6d | ||
|
|
33d119f71a | ||
|
|
703e89134c | ||
|
|
f81c6c00a9 | ||
|
|
1c08f1dd0f | ||
|
|
7581f0b3a1 | ||
|
|
8593b57666 | ||
|
|
d0a3f1dcbc | ||
|
|
bf09e7792f | ||
|
|
7f794b7aff | ||
|
|
66bd44c13a | ||
|
|
b1bcb9fba8 | ||
|
|
fd74dd3dcb | ||
|
|
fb1d550aac | ||
|
|
022ac9b9e8 | ||
|
|
3b957c2244 | ||
|
|
133e8af4ee | ||
|
|
f295b0267d | ||
|
|
7f0d2beb50 | ||
|
|
6ed07a1c25 | ||
|
|
aa077ab188 | ||
|
|
c2a197f351 | ||
|
|
5efddd32db | ||
|
|
7705e5e624 | ||
|
|
a6a7ba57e0 | ||
|
|
53b5f64b4b | ||
|
|
bfadd4060e | ||
|
|
be43c7c67b | ||
|
|
a23666d554 | ||
|
|
ced0a52a87 | ||
|
|
2c7b7d8f66 | ||
|
|
ac189885f6 | ||
|
|
6ba13ed528 | ||
|
|
a6ef0f8f67 | ||
|
|
7836c2c371 | ||
|
|
6ee1eadd1c | ||
|
|
daba7ceb71 | ||
|
|
e622cb7c41 | ||
|
|
cf220b92a2 | ||
|
|
284fd45a08 | ||
|
|
a238d1f37d | ||
|
|
1f57fb079d | ||
|
|
3c93eb0f04 | ||
|
|
325b988ca8 | ||
|
|
179f4326df | ||
|
|
cb0a13a523 | ||
|
|
b250c9d615 | ||
|
|
3ecdad67b7 | ||
|
|
61f80a783a | ||
|
|
db08c7fbb0 | ||
|
|
875f226d3b | ||
|
|
cd67c2a8cf | ||
|
|
4a9192f727 | ||
|
|
91fa11bed0 | ||
|
|
f324b27d02 | ||
|
|
32bb8f3a63 | ||
|
|
28ba9ab737 | ||
|
|
14de6781d8 | ||
|
|
7324e90199 | ||
|
|
541b30a044 |
20
.github/ISSUE_TEMPLATE/2_feature_request.md
vendored
@@ -3,3 +3,23 @@ name: 🚀 Feature Request
|
||||
about: Ideas for new features and improvements
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
Thanks for suggesting a feature!
|
||||
|
||||
Quick check-list while suggesting features:
|
||||
-->
|
||||
|
||||
#### What's the problem this feature will solve?
|
||||
<!-- What are you trying to do, that you are unable to achieve with pytest as it currently stands? -->
|
||||
|
||||
#### Describe the solution you'd like
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
<!-- Provide examples of real-world use cases that this would enable and how it solves the problem described above. -->
|
||||
|
||||
#### Alternative Solutions
|
||||
<!-- Have you tried to workaround the problem using a pytest plugin or other tools? Or a different approach to solving this issue? Please elaborate here. -->
|
||||
|
||||
#### Additional context
|
||||
<!-- Add any other context, links, etc. about the feature here. -->
|
||||
|
||||
11
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: pip
|
||||
directory: "/testing/plugins_integration"
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: "03:00"
|
||||
open-pull-requests-limit: 10
|
||||
allow:
|
||||
- dependency-type: direct
|
||||
- dependency-type: indirect
|
||||
35
.github/workflows/main.yml
vendored
@@ -6,7 +6,8 @@ on:
|
||||
- master
|
||||
- "[0-9]+.[0-9]+.x"
|
||||
tags:
|
||||
- "*"
|
||||
- "[0-9]+.[0-9]+.[0-9]+"
|
||||
- "[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
@@ -16,18 +17,17 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 30
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
name: [
|
||||
"windows-py35",
|
||||
"windows-py36",
|
||||
"windows-py37",
|
||||
"windows-py37-pluggy",
|
||||
"windows-py38",
|
||||
|
||||
"ubuntu-py35",
|
||||
"ubuntu-py36",
|
||||
"ubuntu-py37",
|
||||
"ubuntu-py37-pluggy",
|
||||
@@ -45,11 +45,6 @@ jobs:
|
||||
]
|
||||
|
||||
include:
|
||||
- name: "windows-py35"
|
||||
python: "3.5"
|
||||
os: windows-latest
|
||||
tox_env: "py35-xdist"
|
||||
use_coverage: true
|
||||
- name: "windows-py36"
|
||||
python: "3.6"
|
||||
os: windows-latest
|
||||
@@ -68,10 +63,6 @@ jobs:
|
||||
tox_env: "py38-unittestextras"
|
||||
use_coverage: true
|
||||
|
||||
- name: "ubuntu-py35"
|
||||
python: "3.5"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py35-xdist"
|
||||
- name: "ubuntu-py36"
|
||||
python: "3.6"
|
||||
os: ubuntu-latest
|
||||
@@ -79,7 +70,7 @@ jobs:
|
||||
- name: "ubuntu-py37"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py37-lsof-numpy-oldattrs-pexpect"
|
||||
tox_env: "py37-lsof-numpy-pexpect"
|
||||
use_coverage: true
|
||||
- name: "ubuntu-py37-pluggy"
|
||||
python: "3.7"
|
||||
@@ -94,7 +85,7 @@ jobs:
|
||||
os: ubuntu-latest
|
||||
tox_env: "py38-xdist"
|
||||
- name: "ubuntu-py39"
|
||||
python: "3.9-dev"
|
||||
python: "3.9"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py39-xdist"
|
||||
- name: "ubuntu-pypy3"
|
||||
@@ -133,12 +124,6 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python ${{ matrix.python }}
|
||||
uses: actions/setup-python@v2
|
||||
if: matrix.python != '3.9-dev'
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
- name: Set up Python ${{ matrix.python }} (deadsnakes)
|
||||
uses: deadsnakes/action@v2.0.0
|
||||
if: matrix.python == '3.9-dev'
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
- name: Install dependencies
|
||||
@@ -175,18 +160,22 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
- name: set PY
|
||||
run: echo "::set-env name=PY::$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')"
|
||||
- uses: actions/cache@v1
|
||||
run: echo "name=PY::$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')" >> $GITHUB_ENV
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pre-commit
|
||||
key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
- run: pip install tox
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install tox
|
||||
- run: tox -e linting
|
||||
|
||||
deploy:
|
||||
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && github.repository == 'pytest-dev/pytest'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
needs: [build]
|
||||
|
||||
|
||||
1
.gitignore
vendored
@@ -34,6 +34,7 @@ issue/
|
||||
env/
|
||||
.env/
|
||||
.venv/
|
||||
/pythonenv*/
|
||||
3rdparty/
|
||||
.tox
|
||||
.cache
|
||||
|
||||
@@ -5,12 +5,12 @@ repos:
|
||||
- id: black
|
||||
args: [--safe, --quiet]
|
||||
- repo: https://github.com/asottile/blacken-docs
|
||||
rev: v1.7.0
|
||||
rev: v1.8.0
|
||||
hooks:
|
||||
- id: blacken-docs
|
||||
additional_dependencies: [black==19.10b0]
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v3.1.0
|
||||
rev: v3.2.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
@@ -21,7 +21,7 @@ repos:
|
||||
exclude: _pytest/(debugging|hookspec).py
|
||||
language_version: python3
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 3.8.2
|
||||
rev: 3.8.3
|
||||
hooks:
|
||||
- id: flake8
|
||||
language_version: python3
|
||||
@@ -29,27 +29,36 @@ repos:
|
||||
- flake8-typing-imports==1.9.0
|
||||
- flake8-docstrings==1.5.0
|
||||
- repo: https://github.com/asottile/reorder_python_imports
|
||||
rev: v2.3.0
|
||||
rev: v2.3.5
|
||||
hooks:
|
||||
- id: reorder-python-imports
|
||||
args: ['--application-directories=.:src', --py3-plus]
|
||||
args: ['--application-directories=.:src', --py36-plus]
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.4.4
|
||||
rev: v2.7.2
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py3-plus]
|
||||
args: [--py36-plus]
|
||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||
rev: v1.9.0
|
||||
rev: v1.11.0
|
||||
hooks:
|
||||
- id: setup-cfg-fmt
|
||||
# TODO: when upgrading setup-cfg-fmt this can be removed
|
||||
args: [--max-py-version=3.9]
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: v1.6.0
|
||||
hooks:
|
||||
- id: python-use-type-annotations
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v0.780 # NOTE: keep this in sync with setup.cfg.
|
||||
rev: v0.790
|
||||
hooks:
|
||||
- id: mypy
|
||||
files: ^(src/|testing/)
|
||||
args: []
|
||||
additional_dependencies:
|
||||
- iniconfig>=1.1.0
|
||||
- py>=1.8.2
|
||||
- attrs>=19.2.0
|
||||
- packaging
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: rst
|
||||
|
||||
60
.travis.yml
@@ -1,60 +0,0 @@
|
||||
language: python
|
||||
dist: trusty
|
||||
python: '3.5.1'
|
||||
cache: false
|
||||
|
||||
env:
|
||||
global:
|
||||
- PYTEST_ADDOPTS=-vv
|
||||
|
||||
# setuptools-scm needs all tags in order to obtain a proper version
|
||||
git:
|
||||
depth: false
|
||||
|
||||
install:
|
||||
- python -m pip install --upgrade --pre tox
|
||||
|
||||
jobs:
|
||||
include:
|
||||
# Coverage for Python 3.5.{0,1} specific code, mostly typing related.
|
||||
- env: TOXENV=py35 PYTEST_COVERAGE=1 PYTEST_ADDOPTS="-k test_raises_cyclic_reference"
|
||||
before_install:
|
||||
# Work around https://github.com/jaraco/zipp/issues/40.
|
||||
- python -m pip install -U 'setuptools>=34.4.0' virtualenv==16.7.9
|
||||
|
||||
before_script:
|
||||
- |
|
||||
# Do not (re-)upload coverage with cron runs.
|
||||
if [[ "$TRAVIS_EVENT_TYPE" = cron ]]; then
|
||||
PYTEST_COVERAGE=0
|
||||
fi
|
||||
- |
|
||||
if [[ "$PYTEST_COVERAGE" = 1 ]]; then
|
||||
export COVERAGE_FILE="$PWD/.coverage"
|
||||
export COVERAGE_PROCESS_START="$PWD/.coveragerc"
|
||||
export _PYTEST_TOX_COVERAGE_RUN="coverage run -m"
|
||||
export _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess
|
||||
fi
|
||||
|
||||
script: tox
|
||||
|
||||
after_success:
|
||||
- |
|
||||
if [[ "$PYTEST_COVERAGE" = 1 ]]; then
|
||||
env CODECOV_NAME="$TOXENV-$TRAVIS_OS_NAME" scripts/report-coverage.sh -F Travis
|
||||
fi
|
||||
|
||||
notifications:
|
||||
irc:
|
||||
channels:
|
||||
- "chat.freenode.net#pytest"
|
||||
on_success: change
|
||||
on_failure: change
|
||||
skip_join: true
|
||||
email:
|
||||
- pytest-commit@python.org
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^\d+\.\d+\.x$/
|
||||
16
AUTHORS
@@ -21,6 +21,7 @@ Anders Hovmöller
|
||||
Andras Mitzki
|
||||
Andras Tim
|
||||
Andrea Cimatoribus
|
||||
Andreas Motl
|
||||
Andreas Zeidler
|
||||
Andrey Paramonov
|
||||
Andrzej Klajnert
|
||||
@@ -32,6 +33,7 @@ Anthony Sottile
|
||||
Anton Lodder
|
||||
Antony Lee
|
||||
Arel Cordero
|
||||
Ariel Pillemer
|
||||
Armin Rigo
|
||||
Aron Coyle
|
||||
Aron Curzon
|
||||
@@ -55,11 +57,13 @@ Charles Cloud
|
||||
Charles Machalow
|
||||
Charnjit SiNGH (CCSJ)
|
||||
Chris Lamb
|
||||
Chris NeJame
|
||||
Christian Boelsen
|
||||
Christian Fetzer
|
||||
Christian Neumüller
|
||||
Christian Theunert
|
||||
Christian Tismer
|
||||
Christine Mecklenborg
|
||||
Christoph Buelter
|
||||
Christopher Dignam
|
||||
Christopher Gilling
|
||||
@@ -87,6 +91,7 @@ Dhiren Serai
|
||||
Diego Russo
|
||||
Dmitry Dygalo
|
||||
Dmitry Pribysh
|
||||
Dominic Mortlock
|
||||
Duncan Betts
|
||||
Edison Gustavo Muenz
|
||||
Edoardo Batini
|
||||
@@ -107,6 +112,7 @@ Florian Bruhin
|
||||
Florian Dahlitz
|
||||
Floris Bruynooghe
|
||||
Gabriel Reis
|
||||
Garvit Shubham
|
||||
Gene Wood
|
||||
George Kussumoto
|
||||
Georgy Dyuldin
|
||||
@@ -129,6 +135,7 @@ Ilya Konstantinov
|
||||
Ionuț Turturică
|
||||
Iwan Briquemont
|
||||
Jaap Broekhuizen
|
||||
Jakob van Santen
|
||||
Jakub Mitoraj
|
||||
Jan Balster
|
||||
Janne Vanhala
|
||||
@@ -153,6 +160,7 @@ Justyna Janczyszyn
|
||||
Kale Kundert
|
||||
Kamran Ahmad
|
||||
Karl O. Pinc
|
||||
Karthikeyan Singaravelan
|
||||
Katarzyna Jachim
|
||||
Katarzyna Król
|
||||
Katerina Koukiou
|
||||
@@ -195,6 +203,7 @@ Matthias Hafner
|
||||
Maxim Filipenko
|
||||
Maximilian Cosmo Sitter
|
||||
mbyt
|
||||
Mickey Pashov
|
||||
Michael Aquilina
|
||||
Michael Birtwell
|
||||
Michael Droettboom
|
||||
@@ -227,11 +236,14 @@ Pauli Virtanen
|
||||
Pavel Karateev
|
||||
Paweł Adamczak
|
||||
Pedro Algarvio
|
||||
Petter Strandmark
|
||||
Philipp Loose
|
||||
Pieter Mulder
|
||||
Piotr Banaszkiewicz
|
||||
Piotr Helm
|
||||
Prakhar Gurunani
|
||||
Prashant Anand
|
||||
Prashant Sharma
|
||||
Pulkit Goyal
|
||||
Punyashloka Biswal
|
||||
Quentin Pradet
|
||||
@@ -256,10 +268,12 @@ Ryan Wooden
|
||||
Samuel Dion-Girardeau
|
||||
Samuel Searles-Bryant
|
||||
Samuele Pedroni
|
||||
Sanket Duthade
|
||||
Sankt Petersbug
|
||||
Segev Finer
|
||||
Serhii Mozghovyi
|
||||
Seth Junot
|
||||
Shubham Adep
|
||||
Simon Gomizelj
|
||||
Simon Kerr
|
||||
Skylar Downes
|
||||
@@ -274,6 +288,7 @@ Sven-Hendrik Haase
|
||||
Sylvain Marié
|
||||
Tadek Teleżyński
|
||||
Takafumi Arakaki
|
||||
Tanvi Mehta
|
||||
Tarcisio Fischer
|
||||
Tareq Alayan
|
||||
Ted Xiao
|
||||
@@ -310,3 +325,4 @@ Xuecong Liao
|
||||
Yoav Caspi
|
||||
Zac Hatfield-Dodds
|
||||
Zoltán Máté
|
||||
Zsolt Cserna
|
||||
|
||||
@@ -299,12 +299,6 @@ Here is a simple overview, with pytest-specific bits:
|
||||
|
||||
$ pytest testing/test_config.py
|
||||
|
||||
|
||||
#. Commit and push once your tests pass and you are happy with your change(s)::
|
||||
|
||||
$ git commit -a -m "<commit message>"
|
||||
$ git push -u
|
||||
|
||||
#. Create a new changelog entry in ``changelog``. The file should be named ``<issueid>.<type>.rst``,
|
||||
where *issueid* is the number of the issue related to the change and *type* is one of
|
||||
``feature``, ``improvement``, ``bugfix``, ``doc``, ``deprecation``, ``breaking``, ``vendor``
|
||||
@@ -313,6 +307,11 @@ Here is a simple overview, with pytest-specific bits:
|
||||
|
||||
#. Add yourself to ``AUTHORS`` file if not there yet, in alphabetical order.
|
||||
|
||||
#. Commit and push once your tests pass and you are happy with your change(s)::
|
||||
|
||||
$ git commit -a -m "<commit message>"
|
||||
$ git push -u
|
||||
|
||||
#. Finally, submit a pull request through the GitHub website using this data::
|
||||
|
||||
head-fork: YOUR_GITHUB_USERNAME/pytest
|
||||
|
||||
@@ -89,7 +89,7 @@ Features
|
||||
- Can run `unittest <https://docs.pytest.org/en/stable/unittest.html>`_ (or trial),
|
||||
`nose <https://docs.pytest.org/en/stable/nose.html>`_ test suites out of the box
|
||||
|
||||
- Python 3.5+ and PyPy3
|
||||
- Python 3.6+ and PyPy3
|
||||
|
||||
- Rich plugin architecture, with over 850+ `external plugins <http://plugincompat.herokuapp.com>`_ and thriving community
|
||||
|
||||
|
||||
@@ -122,6 +122,14 @@ Both automatic and manual processes described above follow the same steps from t
|
||||
|
||||
#. 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 master with
|
||||
a dev tag for the next feature release::
|
||||
|
||||
git checkout master
|
||||
git pull
|
||||
git tag MAJOR.{MINOR+1}.0.dev0
|
||||
git push git@github.com:pytest-dev/pytest.git MAJOR.{MINOR+1}.0.dev0
|
||||
|
||||
#. Send an email announcement with the contents from::
|
||||
|
||||
doc/en/announce/release-<VERSION>.rst
|
||||
|
||||
13
bench/unit_test.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from unittest import TestCase # noqa: F401
|
||||
|
||||
for i in range(15000):
|
||||
exec(
|
||||
f"""
|
||||
class Test{i}(TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls): pass
|
||||
def test_1(self): pass
|
||||
def test_2(self): pass
|
||||
def test_3(self): pass
|
||||
"""
|
||||
)
|
||||
11
bench/xunit.py
Normal file
@@ -0,0 +1,11 @@
|
||||
for i in range(5000):
|
||||
exec(
|
||||
f"""
|
||||
class Test{i}:
|
||||
@classmethod
|
||||
def setup_class(cls): pass
|
||||
def test_1(self): pass
|
||||
def test_2(self): pass
|
||||
def test_3(self): pass
|
||||
"""
|
||||
)
|
||||
@@ -6,6 +6,11 @@ Release announcements
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
release-6.2.2
|
||||
release-6.2.1
|
||||
release-6.2.0
|
||||
release-6.1.2
|
||||
release-6.1.1
|
||||
release-6.1.0
|
||||
release-6.0.2
|
||||
release-6.0.1
|
||||
|
||||
18
doc/en/announce/release-6.1.1.rst
Normal file
@@ -0,0 +1,18 @@
|
||||
pytest-6.1.1
|
||||
=======================================
|
||||
|
||||
pytest 6.1.1 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Ran Benita
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
22
doc/en/announce/release-6.1.2.rst
Normal file
@@ -0,0 +1,22 @@
|
||||
pytest-6.1.2
|
||||
=======================================
|
||||
|
||||
pytest 6.1.2 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Bruno Oliveira
|
||||
* Manuel Mariñez
|
||||
* Ran Benita
|
||||
* Vasilis Gerakaris
|
||||
* William Jamir Silva
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
76
doc/en/announce/release-6.2.0.rst
Normal file
@@ -0,0 +1,76 @@
|
||||
pytest-6.2.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 6.2.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 Johnson
|
||||
* Albert Villanova del Moral
|
||||
* Anthony Sottile
|
||||
* Anton
|
||||
* Ariel Pillemer
|
||||
* Bruno Oliveira
|
||||
* Charles Aracil
|
||||
* Christine M
|
||||
* Christine Mecklenborg
|
||||
* Cserna Zsolt
|
||||
* Dominic Mortlock
|
||||
* Emiel van de Laar
|
||||
* Florian Bruhin
|
||||
* Garvit Shubham
|
||||
* Gustavo Camargo
|
||||
* Hugo Martins
|
||||
* Hugo van Kemenade
|
||||
* Jakob van Santen
|
||||
* Josias Aurel
|
||||
* Jürgen Gmach
|
||||
* Karthikeyan Singaravelan
|
||||
* Katarzyna
|
||||
* Kyle Altendorf
|
||||
* Manuel Mariñez
|
||||
* Matthew Hughes
|
||||
* Matthias Gabriel
|
||||
* Max Voitko
|
||||
* Maximilian Cosmo Sitter
|
||||
* Mikhail Fesenko
|
||||
* Nimesh Vashistha
|
||||
* Pedro Algarvio
|
||||
* Petter Strandmark
|
||||
* Prakhar Gurunani
|
||||
* Prashant Sharma
|
||||
* Ran Benita
|
||||
* Ronny Pfannschmidt
|
||||
* Sanket Duthade
|
||||
* Shubham Adep
|
||||
* Simon K
|
||||
* Tanvi Mehta
|
||||
* Thomas Grainger
|
||||
* Tim Hoffmann
|
||||
* Vasilis Gerakaris
|
||||
* William Jamir Silva
|
||||
* Zac Hatfield-Dodds
|
||||
* crricks
|
||||
* dependabot[bot]
|
||||
* duthades
|
||||
* frankgerhardt
|
||||
* kwgchi
|
||||
* mickeypash
|
||||
* symonk
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
20
doc/en/announce/release-6.2.1.rst
Normal file
@@ -0,0 +1,20 @@
|
||||
pytest-6.2.1
|
||||
=======================================
|
||||
|
||||
pytest 6.2.1 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Bruno Oliveira
|
||||
* Jakob van Santen
|
||||
* Ran Benita
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
21
doc/en/announce/release-6.2.2.rst
Normal file
@@ -0,0 +1,21 @@
|
||||
pytest-6.2.2
|
||||
=======================================
|
||||
|
||||
pytest 6.2.2 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Adam Johnson
|
||||
* Bruno Oliveira
|
||||
* Chris NeJame
|
||||
* Ran Benita
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
@@ -74,7 +74,7 @@ Assertions about expected exceptions
|
||||
------------------------------------------
|
||||
|
||||
In order to write assertions about raised exceptions, you can use
|
||||
``pytest.raises`` as a context manager like this:
|
||||
:func:`pytest.raises` as a context manager like this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -123,7 +123,7 @@ The regexp parameter of the ``match`` method is matched with the ``re.search``
|
||||
function, so in the above example ``match='123'`` would have worked as
|
||||
well.
|
||||
|
||||
There's an alternate form of the ``pytest.raises`` function where you pass
|
||||
There's an alternate form of the :func:`pytest.raises` function where you pass
|
||||
a function that will be executed with the given ``*args`` and ``**kwargs`` and
|
||||
assert that the given exception is raised:
|
||||
|
||||
@@ -144,8 +144,8 @@ specific way than just having any exception raised:
|
||||
def test_f():
|
||||
f()
|
||||
|
||||
Using ``pytest.raises`` is likely to be better for cases where you are testing
|
||||
exceptions your own code is deliberately raising, whereas using
|
||||
Using :func:`pytest.raises` is likely to be better for cases where you are
|
||||
testing exceptions your own code is deliberately raising, whereas using
|
||||
``@pytest.mark.xfail`` with a check function is probably better for something
|
||||
like documenting unfixed bugs (where the test describes what "should" happen)
|
||||
or bugs in dependencies.
|
||||
|
||||
@@ -10,7 +10,7 @@ we keep learning about new and better structures to express different details ab
|
||||
|
||||
While we implement those modifications we try to ensure an easy transition and don't want to impose unnecessary churn on our users and community/plugin authors.
|
||||
|
||||
As of now, pytest considers multipe types of backward compatibility transitions:
|
||||
As of now, pytest considers multiple types of backward compatibility transitions:
|
||||
|
||||
a) trivial: APIs which trivially translate to the new mechanism,
|
||||
and do not cause problematic changes.
|
||||
@@ -25,7 +25,7 @@ b) transitional: the old and new API don't conflict
|
||||
When the deprecation expires (e.g. 4.0 is released), we won't remove the deprecated functionality immediately, but will use the standard warning filters to turn them into **errors** by default. This approach makes it explicit that removal is imminent, and still gives you time to turn the deprecated feature into a warning instead of an error so it can be dealt with in your own time. In the next minor release (e.g. 4.1), the feature will be effectively removed.
|
||||
|
||||
|
||||
c) true breakage: should only to be considered when normal transition is unreasonably unsustainable and would offset important development/features by years.
|
||||
c) true breakage: should only be considered when normal transition is unreasonably unsustainable and would offset important development/features by years.
|
||||
In addition, they should be limited to APIs where the number of actual users is very small (for example only impacting some plugins), and can be coordinated with the community in advance.
|
||||
|
||||
Examples for such upcoming changes:
|
||||
@@ -42,7 +42,7 @@ c) true breakage: should only to be considered when normal transition is unreaso
|
||||
|
||||
After there's no hard *-1* on the issue it should be followed up by an initial proof-of-concept Pull Request.
|
||||
|
||||
This POC serves as both a coordination point to assess impact and potential inspriation to come up with a transitional solution after all.
|
||||
This POC serves as both a coordination point to assess impact and potential inspiration to come up with a transitional solution after all.
|
||||
|
||||
After a reasonable amount of time the PR can be merged to base a new major release.
|
||||
|
||||
|
||||
@@ -158,6 +158,11 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
function invocation, created as a sub directory of the base temporary
|
||||
directory.
|
||||
|
||||
By default, a new base temporary directory is created each test session,
|
||||
and old bases are removed after 3 sessions, to aid in debugging. If
|
||||
``--basetemp`` is used then it is cleared each session. See :ref:`base
|
||||
temporary directory`.
|
||||
|
||||
The returned object is a `py.path.local`_ path object.
|
||||
|
||||
.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
|
||||
@@ -167,12 +172,13 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
function invocation, created as a sub directory of the base temporary
|
||||
directory.
|
||||
|
||||
By default, a new base temporary directory is created each test session,
|
||||
and old bases are removed after 3 sessions, to aid in debugging. If
|
||||
``--basetemp`` is used then it is cleared each session. See :ref:`base
|
||||
temporary directory`.
|
||||
|
||||
The returned object is a :class:`pathlib.Path` object.
|
||||
|
||||
.. note::
|
||||
|
||||
In python < 3.6 this is a pathlib2.Path.
|
||||
|
||||
|
||||
no tests ran in 0.12s
|
||||
|
||||
|
||||
@@ -28,6 +28,268 @@ with advance notice in the **Deprecations** section of releases.
|
||||
|
||||
.. towncrier release notes start
|
||||
|
||||
pytest 6.2.2 (2021-01-25)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#8152 <https://github.com/pytest-dev/pytest/issues/8152>`_: Fixed "(<Skipped instance>)" being shown as a skip reason in the verbose test summary line when the reason is empty.
|
||||
|
||||
|
||||
- `#8249 <https://github.com/pytest-dev/pytest/issues/8249>`_: Fix the ``faulthandler`` plugin for occasions when running with ``twisted.logger`` and using ``pytest --capture=no``.
|
||||
|
||||
|
||||
pytest 6.2.1 (2020-12-15)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#7678 <https://github.com/pytest-dev/pytest/issues/7678>`_: Fixed bug where ``ImportPathMismatchError`` would be raised for files compiled in
|
||||
the host and loaded later from an UNC mounted path (Windows).
|
||||
|
||||
|
||||
- `#8132 <https://github.com/pytest-dev/pytest/issues/8132>`_: Fixed regression in ``approx``: in 6.2.0 ``approx`` no longer raises
|
||||
``TypeError`` when dealing with non-numeric types, falling back to normal comparison.
|
||||
Before 6.2.0, array types like tf.DeviceArray fell through to the scalar case,
|
||||
and happened to compare correctly to a scalar if they had only one element.
|
||||
After 6.2.0, these types began failing, because they inherited neither from
|
||||
standard Python number hierarchy nor from ``numpy.ndarray``.
|
||||
|
||||
``approx`` now converts arguments to ``numpy.ndarray`` if they expose the array
|
||||
protocol and are not scalars. This treats array-like objects like numpy arrays,
|
||||
regardless of size.
|
||||
|
||||
|
||||
pytest 6.2.0 (2020-12-12)
|
||||
=========================
|
||||
|
||||
Breaking Changes
|
||||
----------------
|
||||
|
||||
- `#7808 <https://github.com/pytest-dev/pytest/issues/7808>`_: pytest now supports python3.6+ only.
|
||||
|
||||
|
||||
|
||||
Deprecations
|
||||
------------
|
||||
|
||||
- `#7469 <https://github.com/pytest-dev/pytest/issues/7469>`_: Directly constructing/calling the following classes/functions is now deprecated:
|
||||
|
||||
- ``_pytest.cacheprovider.Cache``
|
||||
- ``_pytest.cacheprovider.Cache.for_config()``
|
||||
- ``_pytest.cacheprovider.Cache.clear_cache()``
|
||||
- ``_pytest.cacheprovider.Cache.cache_dir_from_config()``
|
||||
- ``_pytest.capture.CaptureFixture``
|
||||
- ``_pytest.fixtures.FixtureRequest``
|
||||
- ``_pytest.fixtures.SubRequest``
|
||||
- ``_pytest.logging.LogCaptureFixture``
|
||||
- ``_pytest.pytester.Pytester``
|
||||
- ``_pytest.pytester.Testdir``
|
||||
- ``_pytest.recwarn.WarningsRecorder``
|
||||
- ``_pytest.recwarn.WarningsChecker``
|
||||
- ``_pytest.tmpdir.TempPathFactory``
|
||||
- ``_pytest.tmpdir.TempdirFactory``
|
||||
|
||||
These have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 7.0.0.
|
||||
|
||||
|
||||
- `#7530 <https://github.com/pytest-dev/pytest/issues/7530>`_: The ``--strict`` command-line option has been deprecated, use ``--strict-markers`` instead.
|
||||
|
||||
We have plans to maybe in the future to reintroduce ``--strict`` and make it an encompassing flag for all strictness
|
||||
related options (``--strict-markers`` and ``--strict-config`` at the moment, more might be introduced in the future).
|
||||
|
||||
|
||||
- `#7988 <https://github.com/pytest-dev/pytest/issues/7988>`_: The ``@pytest.yield_fixture`` decorator/function is now deprecated. Use :func:`pytest.fixture` instead.
|
||||
|
||||
``yield_fixture`` has been an alias for ``fixture`` for a very long time, so can be search/replaced safely.
|
||||
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#5299 <https://github.com/pytest-dev/pytest/issues/5299>`_: pytest now warns about unraisable exceptions and unhandled thread exceptions that occur in tests on Python>=3.8.
|
||||
See :ref:`unraisable` for more information.
|
||||
|
||||
|
||||
- `#7425 <https://github.com/pytest-dev/pytest/issues/7425>`_: New :fixture:`pytester` fixture, which is identical to :fixture:`testdir` but its methods return :class:`pathlib.Path` when appropriate instead of ``py.path.local``.
|
||||
|
||||
This is part of the movement to use :class:`pathlib.Path` objects internally, in order to remove the dependency to ``py`` in the future.
|
||||
|
||||
Internally, the old :class:`Testdir <_pytest.pytester.Testdir>` is now a thin wrapper around :class:`Pytester <_pytest.pytester.Pytester>`, preserving the old interface.
|
||||
|
||||
|
||||
- `#7695 <https://github.com/pytest-dev/pytest/issues/7695>`_: A new hook was added, `pytest_markeval_namespace` which should return a dictionary.
|
||||
This dictionary will be used to augment the "global" variables available to evaluate skipif/xfail/xpass markers.
|
||||
|
||||
Pseudo example
|
||||
|
||||
``conftest.py``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def pytest_markeval_namespace():
|
||||
return {"color": "red"}
|
||||
|
||||
``test_func.py``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.skipif("color == 'blue'", reason="Color is not red")
|
||||
def test_func():
|
||||
assert False
|
||||
|
||||
|
||||
- `#8006 <https://github.com/pytest-dev/pytest/issues/8006>`_: It is now possible to construct a :class:`~pytest.MonkeyPatch` object directly as ``pytest.MonkeyPatch()``,
|
||||
in cases when the :fixture:`monkeypatch` fixture cannot be used. Previously some users imported it
|
||||
from the private `_pytest.monkeypatch.MonkeyPatch` namespace.
|
||||
|
||||
Additionally, :meth:`MonkeyPatch.context <pytest.MonkeyPatch.context>` is now a classmethod,
|
||||
and can be used as ``with MonkeyPatch.context() as mp: ...``. This is the recommended way to use
|
||||
``MonkeyPatch`` directly, since unlike the ``monkeypatch`` fixture, an instance created directly
|
||||
is not ``undo()``-ed automatically.
|
||||
|
||||
|
||||
|
||||
Improvements
|
||||
------------
|
||||
|
||||
- `#1265 <https://github.com/pytest-dev/pytest/issues/1265>`_: Added an ``__str__`` implementation to the :class:`~pytest.pytester.LineMatcher` class which is returned from ``pytester.run_pytest().stdout`` and similar. It returns the entire output, like the existing ``str()`` method.
|
||||
|
||||
|
||||
- `#2044 <https://github.com/pytest-dev/pytest/issues/2044>`_: Verbose mode now shows the reason that a test was skipped in the test's terminal line after the "SKIPPED", "XFAIL" or "XPASS".
|
||||
|
||||
|
||||
- `#7469 <https://github.com/pytest-dev/pytest/issues/7469>`_ The types of builtin pytest fixtures are now exported so they may be used in type annotations of test functions.
|
||||
The newly-exported types are:
|
||||
|
||||
- ``pytest.FixtureRequest`` for the :fixture:`request` fixture.
|
||||
- ``pytest.Cache`` for the :fixture:`cache` fixture.
|
||||
- ``pytest.CaptureFixture[str]`` for the :fixture:`capfd` and :fixture:`capsys` fixtures.
|
||||
- ``pytest.CaptureFixture[bytes]`` for the :fixture:`capfdbinary` and :fixture:`capsysbinary` fixtures.
|
||||
- ``pytest.LogCaptureFixture`` for the :fixture:`caplog` fixture.
|
||||
- ``pytest.Pytester`` for the :fixture:`pytester` fixture.
|
||||
- ``pytest.Testdir`` for the :fixture:`testdir` fixture.
|
||||
- ``pytest.TempdirFactory`` for the :fixture:`tmpdir_factory` fixture.
|
||||
- ``pytest.TempPathFactory`` for the :fixture:`tmp_path_factory` fixture.
|
||||
- ``pytest.MonkeyPatch`` for the :fixture:`monkeypatch` fixture.
|
||||
- ``pytest.WarningsRecorder`` for the :fixture:`recwarn` fixture.
|
||||
|
||||
Constructing them is not supported (except for `MonkeyPatch`); they are only meant for use in type annotations.
|
||||
Doing so will emit a deprecation warning, and may become a hard-error in pytest 7.0.
|
||||
|
||||
Subclassing them is also not supported. This is not currently enforced at runtime, but is detected by type-checkers such as mypy.
|
||||
|
||||
|
||||
- `#7527 <https://github.com/pytest-dev/pytest/issues/7527>`_: When a comparison between :func:`namedtuple <collections.namedtuple>` instances of the same type fails, pytest now shows the differing field names (possibly nested) instead of their indexes.
|
||||
|
||||
|
||||
- `#7615 <https://github.com/pytest-dev/pytest/issues/7615>`_: :meth:`Node.warn <_pytest.nodes.Node.warn>` now permits any subclass of :class:`Warning`, not just :class:`PytestWarning <pytest.PytestWarning>`.
|
||||
|
||||
|
||||
- `#7701 <https://github.com/pytest-dev/pytest/issues/7701>`_: Improved reporting when using ``--collected-only``. It will now show the number of collected tests in the summary stats.
|
||||
|
||||
|
||||
- `#7710 <https://github.com/pytest-dev/pytest/issues/7710>`_: Use strict equality comparison for non-numeric types in :func:`pytest.approx` instead of
|
||||
raising :class:`TypeError`.
|
||||
|
||||
This was the undocumented behavior before 3.7, but is now officially a supported feature.
|
||||
|
||||
|
||||
- `#7938 <https://github.com/pytest-dev/pytest/issues/7938>`_: New ``--sw-skip`` argument which is a shorthand for ``--stepwise-skip``.
|
||||
|
||||
|
||||
- `#8023 <https://github.com/pytest-dev/pytest/issues/8023>`_: Added ``'node_modules'`` to default value for :confval:`norecursedirs`.
|
||||
|
||||
|
||||
- `#8032 <https://github.com/pytest-dev/pytest/issues/8032>`_: :meth:`doClassCleanups <unittest.TestCase.doClassCleanups>` (introduced in :mod:`unittest` in Python and 3.8) is now called appropriately.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#4824 <https://github.com/pytest-dev/pytest/issues/4824>`_: Fixed quadratic behavior and improved performance of collection of items using autouse fixtures and xunit fixtures.
|
||||
|
||||
|
||||
- `#7758 <https://github.com/pytest-dev/pytest/issues/7758>`_: Fixed an issue where some files in packages are getting lost from ``--lf`` even though they contain tests that failed. Regressed in pytest 5.4.0.
|
||||
|
||||
|
||||
- `#7911 <https://github.com/pytest-dev/pytest/issues/7911>`_: Directories created by by :fixture:`tmp_path` and :fixture:`tmpdir` are now considered stale after 3 days without modification (previous value was 3 hours) to avoid deleting directories still in use in long running test suites.
|
||||
|
||||
|
||||
- `#7913 <https://github.com/pytest-dev/pytest/issues/7913>`_: Fixed a crash or hang in :meth:`pytester.spawn <_pytest.pytester.Pytester.spawn>` when the :mod:`readline` module is involved.
|
||||
|
||||
|
||||
- `#7951 <https://github.com/pytest-dev/pytest/issues/7951>`_: Fixed handling of recursive symlinks when collecting tests.
|
||||
|
||||
|
||||
- `#7981 <https://github.com/pytest-dev/pytest/issues/7981>`_: Fixed symlinked directories not being followed during collection. Regressed in pytest 6.1.0.
|
||||
|
||||
|
||||
- `#8016 <https://github.com/pytest-dev/pytest/issues/8016>`_: Fixed only one doctest being collected when using ``pytest --doctest-modules path/to/an/__init__.py``.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#7429 <https://github.com/pytest-dev/pytest/issues/7429>`_: Add more information and use cases about skipping doctests.
|
||||
|
||||
|
||||
- `#7780 <https://github.com/pytest-dev/pytest/issues/7780>`_: Classes which should not be inherited from are now marked ``final class`` in the API reference.
|
||||
|
||||
|
||||
- `#7872 <https://github.com/pytest-dev/pytest/issues/7872>`_: ``_pytest.config.argparsing.Parser.addini()`` accepts explicit ``None`` and ``"string"``.
|
||||
|
||||
|
||||
- `#7878 <https://github.com/pytest-dev/pytest/issues/7878>`_: In pull request section, ask to commit after editing changelog and authors file.
|
||||
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#7802 <https://github.com/pytest-dev/pytest/issues/7802>`_: The ``attrs`` dependency requirement is now >=19.2.0 instead of >=17.4.0.
|
||||
|
||||
|
||||
- `#8014 <https://github.com/pytest-dev/pytest/issues/8014>`_: `.pyc` files created by pytest's assertion rewriting now conform to the newer PEP-552 format on Python>=3.7.
|
||||
(These files are internal and only interpreted by pytest itself.)
|
||||
|
||||
|
||||
pytest 6.1.2 (2020-10-28)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#7758 <https://github.com/pytest-dev/pytest/issues/7758>`_: Fixed an issue where some files in packages are getting lost from ``--lf`` even though they contain tests that failed. Regressed in pytest 5.4.0.
|
||||
|
||||
|
||||
- `#7911 <https://github.com/pytest-dev/pytest/issues/7911>`_: Directories created by `tmpdir` are now considered stale after 3 days without modification (previous value was 3 hours) to avoid deleting directories still in use in long running test suites.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#7815 <https://github.com/pytest-dev/pytest/issues/7815>`_: Improve deprecation warning message for ``pytest._fillfuncargs()``.
|
||||
|
||||
|
||||
pytest 6.1.1 (2020-10-03)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#7807 <https://github.com/pytest-dev/pytest/issues/7807>`_: Fixed regression in pytest 6.1.0 causing incorrect rootdir to be determined in some non-trivial cases where parent directories have config files as well.
|
||||
|
||||
|
||||
- `#7814 <https://github.com/pytest-dev/pytest/issues/7814>`_: Fixed crash in header reporting when :confval:`testpaths` is used and contains absolute paths (regression in 6.1.0).
|
||||
|
||||
|
||||
pytest 6.1.0 (2020-09-26)
|
||||
=========================
|
||||
|
||||
@@ -1842,6 +2104,44 @@ Improved Documentation
|
||||
- `#5416 <https://github.com/pytest-dev/pytest/issues/5416>`_: Fix PytestUnknownMarkWarning in run/skip example.
|
||||
|
||||
|
||||
pytest 4.6.11 (2020-06-04)
|
||||
==========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#6334 <https://github.com/pytest-dev/pytest/issues/6334>`_: Fix summary entries appearing twice when ``f/F`` and ``s/S`` report chars were used at the same time in the ``-r`` command-line option (for example ``-rFf``).
|
||||
|
||||
The upper case variants were never documented and the preferred form should be the lower case.
|
||||
|
||||
|
||||
- `#7310 <https://github.com/pytest-dev/pytest/issues/7310>`_: Fix ``UnboundLocalError: local variable 'letter' referenced before
|
||||
assignment`` in ``_pytest.terminal.pytest_report_teststatus()``
|
||||
when plugins return report objects in an unconventional state.
|
||||
|
||||
This was making ``pytest_report_teststatus()`` skip
|
||||
entering if-block branches that declare the ``letter`` variable.
|
||||
|
||||
The fix was to set the initial value of the ``letter`` before
|
||||
the if-block cascade so that it always has a value.
|
||||
|
||||
|
||||
pytest 4.6.10 (2020-05-08)
|
||||
==========================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#6870 <https://github.com/pytest-dev/pytest/issues/6870>`_: New ``Config.invocation_args`` attribute containing the unchanged arguments passed to ``pytest.main()``.
|
||||
|
||||
Remark: while this is technically a new feature and according to our `policy <https://docs.pytest.org/en/latest/py27-py34-deprecation.html#what-goes-into-4-6-x-releases>`_ it should not have been backported, we have opened an exception in this particular case because it fixes a serious interaction with ``pytest-xdist``, so it can also be considered a bugfix.
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#6404 <https://github.com/pytest-dev/pytest/issues/6404>`_: Remove usage of ``parser`` module, deprecated in Python 3.9.
|
||||
|
||||
|
||||
pytest 4.6.9 (2020-01-04)
|
||||
=========================
|
||||
|
||||
|
||||
@@ -15,11 +15,13 @@
|
||||
#
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
# The short X.Y version.
|
||||
import ast
|
||||
import os
|
||||
import sys
|
||||
from typing import List
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from _pytest import __version__ as version
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import sphinx.application
|
||||
@@ -398,3 +400,22 @@ def setup(app: "sphinx.application.Sphinx") -> None:
|
||||
)
|
||||
|
||||
configure_logging(app)
|
||||
|
||||
# Make Sphinx mark classes with "final" when decorated with @final.
|
||||
# We need this because we import final from pytest._compat, not from
|
||||
# typing (for Python < 3.8 compat), so Sphinx doesn't detect it.
|
||||
# To keep things simple we accept any `@final` decorator.
|
||||
# Ref: https://github.com/pytest-dev/pytest/pull/7780
|
||||
import sphinx.pycode.ast
|
||||
import sphinx.pycode.parser
|
||||
|
||||
original_is_final = sphinx.pycode.parser.VariableCommentPicker.is_final
|
||||
|
||||
def patched_is_final(self, decorators: List[ast.expr]) -> bool:
|
||||
if original_is_final(self, decorators):
|
||||
return True
|
||||
return any(
|
||||
sphinx.pycode.ast.unparse(decorator) == "final" for decorator in decorators
|
||||
)
|
||||
|
||||
sphinx.pycode.parser.VariableCommentPicker.is_final = patched_is_final
|
||||
|
||||
@@ -18,6 +18,28 @@ 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>`.
|
||||
|
||||
The ``--strict`` command-line option
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 6.2
|
||||
|
||||
The ``--strict`` command-line option has been deprecated in favor of ``--strict-markers``, which
|
||||
better conveys what the option does.
|
||||
|
||||
We have plans to maybe in the future to reintroduce ``--strict`` and make it an encompassing
|
||||
flag for all strictness related options (``--strict-markers`` and ``--strict-config``
|
||||
at the moment, more might be introduced in the future).
|
||||
|
||||
|
||||
The ``yield_fixture`` function/decorator
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 6.2
|
||||
|
||||
``pytest.yield_fixture`` is a deprecated alias for :func:`pytest.fixture`.
|
||||
|
||||
It has been so for a very long time, so can be search/replaced safely.
|
||||
|
||||
|
||||
The ``pytest_warning_captured`` hook
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Doctest integration for modules and test files
|
||||
=========================================================
|
||||
|
||||
By default all files matching the ``test*.txt`` pattern will
|
||||
By default, all files matching the ``test*.txt`` pattern will
|
||||
be run through the python standard ``doctest`` module. You
|
||||
can change the pattern by issuing:
|
||||
|
||||
@@ -77,15 +77,6 @@ putting them into a pytest.ini file like this:
|
||||
[pytest]
|
||||
addopts = --doctest-modules
|
||||
|
||||
.. note::
|
||||
|
||||
The builtin pytest doctest supports only ``doctest`` blocks, but if you are looking
|
||||
for more advanced checking over *all* your documentation,
|
||||
including doctests, ``.. codeblock:: python`` Sphinx directive support,
|
||||
and any other examples your documentation may include, you may wish to
|
||||
consider `Sybil <https://sybil.readthedocs.io/en/latest/index.html>`__.
|
||||
It provides pytest integration out of the box.
|
||||
|
||||
|
||||
Encoding
|
||||
--------
|
||||
@@ -113,7 +104,7 @@ lengthy exception stack traces you can just write:
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
doctest_optionflags= NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL
|
||||
doctest_optionflags = NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL
|
||||
|
||||
Alternatively, options can be enabled by an inline comment in the doc test
|
||||
itself:
|
||||
@@ -206,7 +197,7 @@ It is possible to use fixtures using the ``getfixture`` helper:
|
||||
>>> ...
|
||||
>>>
|
||||
|
||||
Note that the fixture needs to be defined in a place visible by pytest, for example a `conftest.py`
|
||||
Note that the fixture needs to be defined in a place visible by pytest, for example, a `conftest.py`
|
||||
file or plugin; normal python files containing docstrings are not normally scanned for fixtures
|
||||
unless explicitly configured by :confval:`python_files`.
|
||||
|
||||
@@ -253,12 +244,32 @@ Note that like the normal ``conftest.py``, the fixtures are discovered in the di
|
||||
Meaning that if you put your doctest with your source code, the relevant conftest.py needs to be in the same directory tree.
|
||||
Fixtures will not be discovered in a sibling directory tree!
|
||||
|
||||
Skipping tests dynamically
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Skipping tests
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
.. versionadded:: 4.4
|
||||
For the same reasons one might want to skip normal tests, it is also possible to skip
|
||||
tests inside doctests.
|
||||
|
||||
To skip a single check inside a doctest you can use the standard
|
||||
`doctest.SKIP <https://docs.python.org/3/library/doctest.html#doctest.SKIP>`__ directive:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_random(y):
|
||||
"""
|
||||
>>> random.random() # doctest: +SKIP
|
||||
0.156231223
|
||||
|
||||
>>> 1 + 1
|
||||
2
|
||||
"""
|
||||
|
||||
This will skip the first check, but not the second.
|
||||
|
||||
pytest also allows using the standard pytest functions :func:`pytest.skip` and
|
||||
:func:`pytest.xfail` inside doctests, which might be useful because you can
|
||||
then skip/xfail tests based on external conditions:
|
||||
|
||||
You can use ``pytest.skip`` to dynamically skip doctests. For example:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
@@ -266,3 +277,35 @@ You can use ``pytest.skip`` to dynamically skip doctests. For example:
|
||||
>>> if sys.platform.startswith('win'):
|
||||
... pytest.skip('this doctest does not work on Windows')
|
||||
...
|
||||
>>> import fcntl
|
||||
>>> ...
|
||||
|
||||
However using those functions is discouraged because it reduces the readability of the
|
||||
docstring.
|
||||
|
||||
.. note::
|
||||
|
||||
:func:`pytest.skip` and :func:`pytest.xfail` behave differently depending
|
||||
if the doctests are in a Python file (in docstrings) or a text file containing
|
||||
doctests intermingled with text:
|
||||
|
||||
* Python modules (docstrings): the functions only act in that specific docstring,
|
||||
letting the other docstrings in the same module execute as normal.
|
||||
|
||||
* Text files: the functions will skip/xfail the checks for the rest of the entire
|
||||
file.
|
||||
|
||||
|
||||
Alternatives
|
||||
------------
|
||||
|
||||
While the built-in pytest support provides a good set of functionalities for using
|
||||
doctests, if you use them extensively you might be interested in those external packages
|
||||
which add many more features, and include pytest integration:
|
||||
|
||||
* `pytest-doctestplus <https://github.com/astropy/pytest-doctestplus>`__: provides
|
||||
advanced doctest support and enables the testing of reStructuredText (".rst") files.
|
||||
|
||||
* `Sybil <https://sybil.readthedocs.io>`__: provides a way to test examples in
|
||||
your documentation by parsing them from the documentation source and evaluating
|
||||
the parsed examples as part of your normal test run.
|
||||
|
||||
@@ -176,7 +176,7 @@ class TestRaises:
|
||||
|
||||
def test_reinterpret_fails_with_print_for_the_fun_of_it(self):
|
||||
items = [1, 2, 3]
|
||||
print("items is {!r}".format(items))
|
||||
print(f"items is {items!r}")
|
||||
a, b = items.pop()
|
||||
|
||||
def test_some_error(self):
|
||||
|
||||
@@ -11,4 +11,4 @@ def pytest_runtest_setup(item):
|
||||
return
|
||||
mod = item.getparent(pytest.Module).obj
|
||||
if hasattr(mod, "hello"):
|
||||
print("mod.hello {!r}".format(mod.hello))
|
||||
print(f"mod.hello {mod.hello!r}")
|
||||
|
||||
132
doc/en/example/fixtures/fixture_availability.svg
Normal file
@@ -0,0 +1,132 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="572" height="542">
|
||||
<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, circle.module, circle.package {
|
||||
fill: #c3e0ec;
|
||||
stroke: #0e84b5;
|
||||
stroke-width: 2;
|
||||
}
|
||||
text.class, text.module, text.package {
|
||||
fill: #0e84b5;
|
||||
}
|
||||
line, path {
|
||||
stroke: black;
|
||||
stroke-width: 2;
|
||||
fill: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- main scope -->
|
||||
<circle class="package" r="270" cx="286" cy="271" />
|
||||
<!-- scope name -->
|
||||
<defs>
|
||||
<path d="M 26,271 A 260 260 0 0 1 546 271" id="testp"/>
|
||||
</defs>
|
||||
<text class="package">
|
||||
<textPath href="#testp" startOffset="50%">tests</textPath>
|
||||
</text>
|
||||
|
||||
<!-- subpackage -->
|
||||
<circle class="package" r="140" cx="186" cy="271" />
|
||||
<!-- scope name -->
|
||||
<defs>
|
||||
<path d="M 56,271 A 130 130 0 0 1 316 271" id="subpackage"/>
|
||||
</defs>
|
||||
<text class="package">
|
||||
<textPath href="#subpackage" startOffset="50%">subpackage</textPath>
|
||||
</text>
|
||||
|
||||
<!-- test_subpackage.py -->
|
||||
<circle class="module" r="90" cx="186" cy="311" />
|
||||
<!-- scope name -->
|
||||
<defs>
|
||||
<path d="M 106,311 A 80 80 0 0 1 266 311" id="testSubpackage"/>
|
||||
</defs>
|
||||
<text class="module">
|
||||
<textPath href="#testSubpackage" startOffset="50%">test_subpackage.py</textPath>
|
||||
</text>
|
||||
<!-- innermost -->
|
||||
<line x1="186" x2="186" y1="271" y2="351"/>
|
||||
<!-- mid -->
|
||||
<path d="M 186 351 L 136 351 L 106 331 L 106 196" />
|
||||
<!-- order -->
|
||||
<path d="M 186 351 L 256 351 L 316 291 L 316 136" />
|
||||
<!-- top -->
|
||||
<path d="M 186 351 L 186 391 L 231 436 L 331 436" />
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="186" cy="271" />
|
||||
<text x="186" y="271">innermost</text>
|
||||
<rect class="test" width="110" height="50" x="131" y="326" />
|
||||
<text x="186" y="351">test_order</text>
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="126" cy="196" />
|
||||
<text x="126" y="196">mid</text>
|
||||
<!-- scope order number -->
|
||||
<mask id="testSubpackageOrderMask">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="white"/>
|
||||
<circle fill="black" stroke="white" stroke-width="2" r="90" cx="186" cy="311" />
|
||||
</mask>
|
||||
<circle class="module" r="15" cx="96" cy="311" mask="url(#testSubpackageOrderMask)"/>
|
||||
<text class="module" x="96" y="311">1</text>
|
||||
<!-- scope order number -->
|
||||
<mask id="subpackageOrderMask">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="white"/>
|
||||
<circle fill="black" stroke="white" stroke-width="2" r="140" cx="186" cy="271" />
|
||||
</mask>
|
||||
<circle class="module" r="15" cx="46" cy="271" mask="url(#subpackageOrderMask)"/>
|
||||
<text class="module" x="46" y="271">2</text>
|
||||
<!-- scope order number -->
|
||||
<mask id="testsOrderMask">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="white"/>
|
||||
<circle fill="black" stroke="white" stroke-width="2" r="270" cx="286" cy="271" />
|
||||
</mask>
|
||||
<circle class="module" r="15" cx="16" cy="271" mask="url(#testsOrderMask)"/>
|
||||
<text class="module" x="16" y="271">3</text>
|
||||
|
||||
<!-- test_top.py -->
|
||||
<circle class="module" r="85" cx="441" cy="271" />
|
||||
<!-- scope name -->
|
||||
<defs>
|
||||
<path d="M 366,271 A 75 75 0 0 1 516 271" id="testTop"/>
|
||||
</defs>
|
||||
<text class="module">
|
||||
<textPath href="#testTop" startOffset="50%">test_top.py</textPath>
|
||||
</text>
|
||||
<!-- innermost -->
|
||||
<line x1="441" x2="441" y1="306" y2="236"/>
|
||||
<!-- order -->
|
||||
<path d="M 441 306 L 376 306 L 346 276 L 346 136" />
|
||||
<!-- top -->
|
||||
<path d="M 441 306 L 441 411 L 411 436 L 331 436" />
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="441" cy="236" />
|
||||
<text x="441" y="236">innermost</text>
|
||||
<rect class="test" width="110" height="50" x="386" y="281" />
|
||||
<text x="441" y="306">test_order</text>
|
||||
<!-- scope order number -->
|
||||
<mask id="testTopOrderMask">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="white"/>
|
||||
<circle fill="black" stroke="white" stroke-width="2" r="85" cx="441" cy="271" />
|
||||
</mask>
|
||||
<circle class="module" r="15" cx="526" cy="271" mask="url(#testTopOrderMask)"/>
|
||||
<text class="module" x="526" y="271">1</text>
|
||||
<!-- scope order number -->
|
||||
<circle class="module" r="15" cx="556" cy="271" mask="url(#testsOrderMask)"/>
|
||||
<text class="module" x="556" y="271">2</text>
|
||||
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="331" cy="436" />
|
||||
<text x="331" y="436">top</text>
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="331" cy="136" />
|
||||
<text x="331" y="136">order</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
142
doc/en/example/fixtures/fixture_availability_plugins.svg
Normal file
@@ -0,0 +1,142 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="587" height="382">
|
||||
<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;
|
||||
alignment-baseline: center;
|
||||
}
|
||||
ellipse.fixture, rect.test {
|
||||
fill: #eeffcc;
|
||||
stroke: #007020;
|
||||
stroke-width: 2;
|
||||
}
|
||||
text.fixture {
|
||||
color: #06287e;
|
||||
}
|
||||
circle.class, circle.module, circle.package, circle.plugin {
|
||||
fill: #c3e0ec;
|
||||
stroke: #0e84b5;
|
||||
stroke-width: 2;
|
||||
}
|
||||
text.class, text.module, text.package, text.plugin {
|
||||
fill: #0e84b5;
|
||||
}
|
||||
line, path {
|
||||
stroke: black;
|
||||
stroke-width: 2;
|
||||
fill: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- plugin_a.py scope -->
|
||||
<circle class="plugin" r="85" cx="486" cy="86" />
|
||||
<!-- plugin name -->
|
||||
<defs>
|
||||
<path d="M 411,86 A 75 75 0 0 1 561 86" id="pluginA"/>
|
||||
</defs>
|
||||
<text class="plugin">
|
||||
<textPath href="#pluginA" startOffset="50%">plugin_a</textPath>
|
||||
</text>
|
||||
<!-- scope order number -->
|
||||
<mask id="pluginAOrderMask">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="white"/>
|
||||
<circle fill="black" stroke="white" stroke-width="2" r="85" cx="486" cy="86" />
|
||||
</mask>
|
||||
<circle class="module" r="15" cx="571" cy="86" mask="url(#pluginAOrderMask)"/>
|
||||
<text class="module" x="571" y="86">4</text>
|
||||
|
||||
<!-- plugin_b.py scope -->
|
||||
<circle class="plugin" r="85" cx="486" cy="296" />
|
||||
<!-- plugin name -->
|
||||
<defs>
|
||||
<path d="M 411,296 A 75 75 0 0 1 561 296" id="pluginB"/>
|
||||
</defs>
|
||||
<text class="plugin">
|
||||
<textPath href="#pluginB" startOffset="50%">plugin_b</textPath>
|
||||
</text>
|
||||
<!-- scope order number -->
|
||||
<mask id="pluginBOrderMask">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="white"/>
|
||||
<circle fill="black" stroke="white" stroke-width="2" r="85" cx="486" cy="296" />
|
||||
</mask>
|
||||
<circle class="module" r="15" cx="571" cy="296" mask="url(#pluginBOrderMask)"/>
|
||||
<text class="module" x="571" y="296">4</text>
|
||||
|
||||
<!-- main scope -->
|
||||
<circle class="package" r="190" cx="191" cy="191" />
|
||||
<!-- scope name -->
|
||||
<defs>
|
||||
<path d="M 11,191 A 180 180 0 0 1 371 191" id="testp"/>
|
||||
</defs>
|
||||
<text class="package">
|
||||
<textPath href="#testp" startOffset="50%">tests</textPath>
|
||||
</text>
|
||||
<!-- scope order number -->
|
||||
<mask id="mainOrderMask">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="white"/>
|
||||
<circle fill="black" stroke="white" stroke-width="2" r="190" cx="191" cy="191" />
|
||||
</mask>
|
||||
<circle class="module" r="15" cx="381" cy="191" mask="url(#mainOrderMask)"/>
|
||||
<text class="module" x="381" y="191">3</text>
|
||||
|
||||
<!-- subpackage -->
|
||||
<circle class="package" r="140" cx="191" cy="231" />
|
||||
<!-- scope name -->
|
||||
<defs>
|
||||
<path d="M 61,231 A 130 130 0 0 1 321 231" id="subpackage"/>
|
||||
</defs>
|
||||
<text class="package">
|
||||
<textPath href="#subpackage" startOffset="50%">subpackage</textPath>
|
||||
</text>
|
||||
<!-- scope order number -->
|
||||
<mask id="subpackageOrderMask">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="white"/>
|
||||
<circle fill="black" stroke="white" stroke-width="2" r="140" cx="191" cy="231" />
|
||||
</mask>
|
||||
<circle class="module" r="15" cx="331" cy="231" mask="url(#subpackageOrderMask)"/>
|
||||
<text class="module" x="331" y="231">2</text>
|
||||
|
||||
<!-- test_subpackage.py -->
|
||||
<circle class="module" r="90" cx="191" cy="271" />
|
||||
<!-- scope name -->
|
||||
<defs>
|
||||
<path d="M 111,271 A 80 80 0 0 1 271 271" id="testSubpackage"/>
|
||||
</defs>
|
||||
<text class="module">
|
||||
<textPath href="#testSubpackage" startOffset="50%">test_subpackage.py</textPath>
|
||||
</text>
|
||||
<!-- scope order number -->
|
||||
<mask id="testSubpackageOrderMask">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="white"/>
|
||||
<circle fill="black" stroke="white" stroke-width="2" r="90" cx="191" cy="271" />
|
||||
</mask>
|
||||
<circle class="module" r="15" cx="281" cy="271" mask="url(#testSubpackageOrderMask)"/>
|
||||
<text class="module" x="281" y="271">1</text>
|
||||
|
||||
<!-- innermost -->
|
||||
<line x1="191" x2="191" y1="231" y2="311"/>
|
||||
<!-- mid -->
|
||||
<path d="M 191 306 L 101 306 L 91 296 L 91 156 L 101 146 L 191 146" />
|
||||
<!-- order -->
|
||||
<path d="M 191 316 L 91 316 L 81 306 L 81 61 L 91 51 L 191 51" />
|
||||
<!-- a_fix -->
|
||||
<path d="M 191 306 L 291 306 L 301 296 L 301 96 L 311 86 L 486 86" />
|
||||
<!-- b_fix -->
|
||||
<path d="M 191 316 L 316 316 L 336 296 L 486 296" />
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="191" cy="231" />
|
||||
<text x="191" y="231">inner</text>
|
||||
<rect class="test" width="110" height="50" x="136" y="286" />
|
||||
<text x="191" y="311">test_order</text>
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="191" cy="146" />
|
||||
<text x="191" y="146">mid</text>
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="191" cy="51" />
|
||||
<text x="191" y="51">order</text>
|
||||
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="486" cy="86" />
|
||||
<text x="486" y="86">a_fix</text>
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="486" cy="296" />
|
||||
<text x="486" y="296">b_fix</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
@@ -1,38 +0,0 @@
|
||||
import pytest
|
||||
|
||||
# fixtures documentation order example
|
||||
order = []
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def s1():
|
||||
order.append("s1")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def m1():
|
||||
order.append("m1")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def f1(f3):
|
||||
order.append("f1")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def f3():
|
||||
order.append("f3")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def a1():
|
||||
order.append("a1")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def f2():
|
||||
order.append("f2")
|
||||
|
||||
|
||||
def test_order(f1, m1, f2, s1):
|
||||
assert order == ["s1", "m1", "a1", "f3", "f1", "f2"]
|
||||
45
doc/en/example/fixtures/test_fixtures_order_autouse.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def order():
|
||||
return []
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def a(order):
|
||||
order.append("a")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def b(a, order):
|
||||
order.append("b")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def c(b, order):
|
||||
order.append("c")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def d(b, order):
|
||||
order.append("d")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def e(d, order):
|
||||
order.append("e")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def f(e, order):
|
||||
order.append("f")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def g(f, c, order):
|
||||
order.append("g")
|
||||
|
||||
|
||||
def test_order_and_g(g, order):
|
||||
assert order == ["a", "b", "c", "d", "e", "f", "g"]
|
||||
64
doc/en/example/fixtures/test_fixtures_order_autouse.svg
Normal file
@@ -0,0 +1,64 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="252" 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>
|
||||
<path d="M126,586 L26,506 L26,236" />
|
||||
<path d="M226,446 L226,236 L126,166" />
|
||||
<line x1="126" x2="126" y1="656" y2="516" />
|
||||
<line x1="126" x2="226" y1="516" y2="446" />
|
||||
<line x1="226" x2="126" y1="446" y2="376" />
|
||||
<line x1="126" x2="126" y1="376" y2="166" />
|
||||
<line x1="26" x2="126" y1="236" y2="166" />
|
||||
<line x1="126" x2="126" y1="166" y2="26" />
|
||||
<line x1="126" x2="126" y1="96" y2="26" />
|
||||
<rect class="autouse" width="251" height="40" x="0" y="286" />
|
||||
<text x="126" y="306">autouse</text>
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="126" cy="26" />
|
||||
<text x="126" y="26">order</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="126" cy="96" />
|
||||
<text x="126" y="96">a</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="126" cy="166" />
|
||||
<text x="126" y="166">b</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="26" cy="236" />
|
||||
<text x="26" y="236">c</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="126" cy="376" />
|
||||
<text x="126" y="376">d</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="226" cy="446" />
|
||||
<text x="226" y="446">e</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="126" cy="516" />
|
||||
<text x="126" y="516">f</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="126" cy="586" />
|
||||
<text x="126" y="586">g</text>
|
||||
<rect class="test" width="110" height="50" x="71" y="631" />
|
||||
<text x="126" y="656">test_order</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,31 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def order():
|
||||
return []
|
||||
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def c1(order):
|
||||
order.append("c1")
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def c2(order):
|
||||
order.append("c2")
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def c3(order, c1):
|
||||
order.append("c3")
|
||||
|
||||
|
||||
class TestClassWithC1Request:
|
||||
def test_order(self, order, c1, c3):
|
||||
assert order == ["c1", "c3"]
|
||||
|
||||
|
||||
class TestClassWithoutC1Request:
|
||||
def test_order(self, order, c2):
|
||||
assert order == ["c1", "c2"]
|
||||
@@ -0,0 +1,76 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="862" height="402">
|
||||
<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;
|
||||
}
|
||||
line {
|
||||
stroke: black;
|
||||
stroke-width: 2;
|
||||
}
|
||||
rect.autouse {
|
||||
fill: #ca7f3d;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- TestWithC1Request -->
|
||||
<circle class="class" r="200" cx="221" cy="201" />
|
||||
<line x1="221" x2="221" y1="61" y2="316"/>
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="221" cy="61" />
|
||||
<text x="221" y="61">order</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="221" cy="131" />
|
||||
<text x="221" y="131">c1</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="221" cy="271" />
|
||||
<text x="221" y="271">c3</text>
|
||||
<rect class="test" width="110" height="50" x="166" y="316" />
|
||||
<text x="221" y="341">test_order</text>
|
||||
<!-- scope name -->
|
||||
<defs>
|
||||
<path d="M31,201 A 190 190 0 0 1 411 201" id="testClassWith"/>
|
||||
</defs>
|
||||
<text class="class">
|
||||
<textPath href="#testClassWith" startOffset="50%">TestWithC1Request</textPath>
|
||||
</text>
|
||||
|
||||
<!-- TestWithoutC1Request -->
|
||||
<circle class="class" r="200" cx="641" cy="201" />
|
||||
<line x1="641" x2="641" y1="61" y2="316"/>
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="641" cy="61" />
|
||||
<text x="641" y="61">order</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="641" cy="131" />
|
||||
<text x="641" y="131">c1</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="641" cy="271" />
|
||||
<text x="641" y="271">c2</text>
|
||||
<rect class="test" width="110" height="50" x="586" y="316" />
|
||||
<text x="641" y="341">test_order</text>
|
||||
<!-- scope name -->
|
||||
<defs>
|
||||
<path d="M451,201 A 190 190 0 0 1 831 201" id="testClassWithout"/>
|
||||
</defs>
|
||||
<text class="class">
|
||||
<textPath href="#testClassWithout" startOffset="50%">TestWithoutC1Request</textPath>
|
||||
</text>
|
||||
|
||||
<rect class="autouse" width="862" height="40" x="1" y="181" />
|
||||
<rect width="10" height="100" class="autouse" x="426" y="151" />
|
||||
<text x="431" y="201">autouse</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,36 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def order():
|
||||
return []
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def c1(order):
|
||||
order.append("c1")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def c2(order):
|
||||
order.append("c2")
|
||||
|
||||
|
||||
class TestClassWithAutouse:
|
||||
@pytest.fixture(autouse=True)
|
||||
def c3(self, order, c2):
|
||||
order.append("c3")
|
||||
|
||||
def test_req(self, order, c1):
|
||||
assert order == ["c2", "c3", "c1"]
|
||||
|
||||
def test_no_req(self, order):
|
||||
assert order == ["c2", "c3"]
|
||||
|
||||
|
||||
class TestClassWithoutAutouse:
|
||||
def test_req(self, order, c1):
|
||||
assert order == ["c1"]
|
||||
|
||||
def test_no_req(self, order):
|
||||
assert order == []
|
||||
@@ -0,0 +1,100 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="862" height="502">
|
||||
<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;
|
||||
}
|
||||
line {
|
||||
stroke: black;
|
||||
stroke-width: 2;
|
||||
}
|
||||
rect.autouse {
|
||||
fill: #ca7f3d;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- TestWithAutouse -->
|
||||
<circle class="class" r="250" cx="251" cy="251" />
|
||||
<!-- scope name -->
|
||||
<defs>
|
||||
<path d="M11,251 A 240 240 0 0 1 491 251" id="testClassWith"/>
|
||||
</defs>
|
||||
<text class="class">
|
||||
<textPath href="#testClassWith" startOffset="50%">TestWithAutouse</textPath>
|
||||
</text>
|
||||
<mask id="autouseScope">
|
||||
<circle fill="white" r="249" cx="251" cy="251" />
|
||||
</mask>
|
||||
|
||||
<!-- TestWithAutouse.test_req -->
|
||||
<line x1="176" x2="176" y1="76" y2="426"/>
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="176" cy="76" />
|
||||
<text x="176" y="76">order</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="176" cy="146" />
|
||||
<text x="176" y="146">c2</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="176" cy="216" />
|
||||
<text x="176" y="216">c3</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="176" cy="356" />
|
||||
<text x="176" y="356">c1</text>
|
||||
<rect class="test" width="100" height="50" x="126" y="401" />
|
||||
<text x="176" y="426">test_req</text>
|
||||
|
||||
<!-- TestWithAutouse.test_no_req -->
|
||||
<line x1="326" x2="326" y1="76" y2="346"/>
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="326" cy="76" />
|
||||
<text x="326" y="76">order</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="326" cy="146" />
|
||||
<text x="326" y="146">c2</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="326" cy="216" />
|
||||
<text x="326" y="216">c3</text>
|
||||
<rect class="test" width="120" height="50" x="266" y="331" />
|
||||
<text x="326" y="356">test_no_req</text>
|
||||
|
||||
<rect class="autouse" width="500" height="40" x="1" y="266" mask="url(#autouseScope)"/>
|
||||
<text x="261" y="286">autouse</text>
|
||||
|
||||
<!-- TestWithoutAutouse -->
|
||||
<circle class="class" r="170" cx="691" cy="251" />
|
||||
<!-- scope name -->
|
||||
<defs>
|
||||
<path d="M 531,251 A 160 160 0 0 1 851 251" id="testClassWithout"/>
|
||||
</defs>
|
||||
<text class="class">
|
||||
<textPath href="#testClassWithout" startOffset="50%">TestWithoutAutouse</textPath>
|
||||
</text>
|
||||
|
||||
<!-- TestWithoutAutouse.test_req -->
|
||||
<line x1="616" x2="616" y1="181" y2="321"/>
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="616" cy="181" />
|
||||
<text x="616" y="181">order</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="616" cy="251" />
|
||||
<text x="616" y="251">c1</text>
|
||||
<rect class="test" width="100" height="50" x="566" y="296" />
|
||||
<text x="616" y="321">test_req</text>
|
||||
|
||||
<!-- TestWithoutAutouse.test_no_req -->
|
||||
<line x1="766" x2="766" y1="181" y2="251"/>
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="766" cy="181" />
|
||||
<text x="766" y="181">order</text>
|
||||
<rect class="test" width="120" height="50" x="706" y="226" />
|
||||
<text x="766" y="251">test_no_req</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
45
doc/en/example/fixtures/test_fixtures_order_dependencies.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def order():
|
||||
return []
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def a(order):
|
||||
order.append("a")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def b(a, order):
|
||||
order.append("b")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def c(a, b, order):
|
||||
order.append("c")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def d(c, b, order):
|
||||
order.append("d")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def e(d, b, order):
|
||||
order.append("e")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def f(e, order):
|
||||
order.append("f")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def g(f, c, order):
|
||||
order.append("g")
|
||||
|
||||
|
||||
def test_order(g, order):
|
||||
assert order == ["a", "b", "c", "d", "e", "f", "g"]
|
||||
60
doc/en/example/fixtures/test_fixtures_order_dependencies.svg
Normal file
@@ -0,0 +1,60 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="252" height="612">
|
||||
<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;
|
||||
}
|
||||
</style>
|
||||
<path d="M126,516 L26,436 L26,236" />
|
||||
<path d="M226,376 L226,236 L126,166" />
|
||||
<line x1="126" x2="126" y1="586" y2="446" />
|
||||
<line x1="126" x2="226" y1="446" y2="376" />
|
||||
<line x1="226" x2="126" y1="376" y2="306" />
|
||||
<line x1="126" x2="26" y1="306" y2="236" />
|
||||
<line x1="126" x2="126" y1="306" y2="166" />
|
||||
<line x1="26" x2="126" y1="236" y2="166" />
|
||||
<line x1="126" x2="126" y1="166" y2="26" />
|
||||
<line x1="126" x2="126" y1="96" y2="26" />
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="126" cy="26" />
|
||||
<text x="126" y="26">order</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="126" cy="96" />
|
||||
<text x="126" y="96">a</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="126" cy="166" />
|
||||
<text x="126" y="166">b</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="26" cy="236" />
|
||||
<text x="26" y="236">c</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="126" cy="306" />
|
||||
<text x="126" y="306">d</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="226" cy="376" />
|
||||
<text x="226" y="376">e</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="126" cy="446" />
|
||||
<text x="126" y="446">f</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="126" cy="516" />
|
||||
<text x="126" y="516">g</text>
|
||||
<rect class="test" width="110" height="50" x="71" y="561" />
|
||||
<text x="126" y="586">test_order</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,51 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="112" height="612">
|
||||
<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;
|
||||
}
|
||||
</style>
|
||||
<line x1="56" x2="56" y1="611" 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>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="56" cy="306" />
|
||||
<text x="56" y="306">d</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="56" cy="376" />
|
||||
<text x="56" y="376">e</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="56" cy="446" />
|
||||
<text x="56" y="446">f</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="56" cy="516" />
|
||||
<text x="56" y="516">g</text>
|
||||
<rect class="test" width="110" height="50" x="1" y="561" />
|
||||
<text x="56" y="586">test_order</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,60 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="252" height="542">
|
||||
<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;
|
||||
}
|
||||
</style>
|
||||
<path d="M126,446 L26,376 L26,236" />
|
||||
<path d="M226,306 L126,236 L126,166" />
|
||||
<line x1="126" x2="126" y1="516" y2="446" />
|
||||
<line x1="226" x2="226" y1="376" y2="306" />
|
||||
<line x1="226" x2="226" y1="306" y2="236" />
|
||||
<line x1="226" x2="126" y1="236" y2="166" />
|
||||
<line x1="126" x2="226" y1="446" y2="376" />
|
||||
<line x1="26" x2="126" y1="236" y2="166" />
|
||||
<line x1="126" x2="126" y1="166" y2="96" />
|
||||
<line x1="126" x2="126" y1="96" y2="26" />
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="126" cy="26" />
|
||||
<text x="126" y="26">order</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="126" cy="96" />
|
||||
<text x="126" y="96">a</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="126" cy="166" />
|
||||
<text x="126" y="166">b</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="26" cy="236" />
|
||||
<text x="26" y="236">c</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="226" cy="236" />
|
||||
<text x="226" y="236">d</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="226" cy="306" />
|
||||
<text x="226" y="306">e</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="226" cy="376" />
|
||||
<text x="226" y="376">f</text>
|
||||
<ellipse class="fixture" rx="25" ry="25" cx="126" cy="446" />
|
||||
<text x="126" y="446">g</text>
|
||||
<rect class="test" width="110" height="50" x="71" y="491" />
|
||||
<text x="126" y="516">test_order</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
36
doc/en/example/fixtures/test_fixtures_order_scope.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def order():
|
||||
return []
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def func(order):
|
||||
order.append("function")
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def cls(order):
|
||||
order.append("class")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def mod(order):
|
||||
order.append("module")
|
||||
|
||||
|
||||
@pytest.fixture(scope="package")
|
||||
def pack(order):
|
||||
order.append("package")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def sess(order):
|
||||
order.append("session")
|
||||
|
||||
|
||||
class TestClass:
|
||||
def test_order(self, func, cls, mod, pack, sess, order):
|
||||
assert order == ["session", "package", "module", "class", "function"]
|
||||
55
doc/en/example/fixtures/test_fixtures_order_scope.svg
Normal file
@@ -0,0 +1,55 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="262" height="537">
|
||||
<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;
|
||||
}
|
||||
line {
|
||||
stroke: black;
|
||||
stroke-width: 2;
|
||||
}
|
||||
</style>
|
||||
<!-- TestClass -->
|
||||
<circle class="class" r="130" cx="131" cy="406" />
|
||||
<line x1="131" x2="131" y1="21" y2="446"/>
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="131" cy="26" />
|
||||
<text x="131" y="26">order</text>
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="131" cy="96" />
|
||||
<text x="131" y="96">sess</text>
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="131" cy="166" />
|
||||
<text x="131" y="166">pack</text>
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="131" cy="236" />
|
||||
<text x="131" y="236">mod</text>
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="131" cy="306" />
|
||||
<text x="131" y="306">cls</text>
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="131" cy="376" />
|
||||
<text x="131" y="376">func</text>
|
||||
<rect class="test" width="110" height="50" x="76" y="421" />
|
||||
<text x="131" y="446">test_order</text>
|
||||
<!-- scope name -->
|
||||
<defs>
|
||||
<path d="M131,526 A 120 120 0 0 1 136 286" id="testClass"/>
|
||||
</defs>
|
||||
<text class="class">
|
||||
<textPath href="#testClass" startOffset="50%">TestClass</textPath>
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,29 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def order():
|
||||
return []
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def outer(order, inner):
|
||||
order.append("outer")
|
||||
|
||||
|
||||
class TestOne:
|
||||
@pytest.fixture
|
||||
def inner(self, order):
|
||||
order.append("one")
|
||||
|
||||
def test_order(self, order, outer):
|
||||
assert order == ["one", "outer"]
|
||||
|
||||
|
||||
class TestTwo:
|
||||
@pytest.fixture
|
||||
def inner(self, order):
|
||||
order.append("two")
|
||||
|
||||
def test_order(self, order, outer):
|
||||
assert order == ["two", "outer"]
|
||||
@@ -0,0 +1,115 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="562" height="532">
|
||||
<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;
|
||||
}
|
||||
circle.module {
|
||||
fill: #c3e0ec;
|
||||
stroke: #0e84b5;
|
||||
stroke-width: 2;
|
||||
}
|
||||
text.class, text.module {
|
||||
fill: #0e84b5;
|
||||
}
|
||||
line, path {
|
||||
stroke: black;
|
||||
stroke-width: 2;
|
||||
fill: none;
|
||||
}
|
||||
</style>
|
||||
<!-- main scope -->
|
||||
<circle class="module" r="265" cx="281" cy="266" />
|
||||
<!-- scope name -->
|
||||
<defs>
|
||||
<path d="M 26,266 A 255 255 0 0 1 536 266" id="testModule"/>
|
||||
</defs>
|
||||
<text class="module">
|
||||
<textPath href="#testModule" startOffset="50%">test_fixtures_request_different_scope.py</textPath>
|
||||
</text>
|
||||
|
||||
<!-- TestOne -->
|
||||
<circle class="class" r="100" cx="141" cy="266" />
|
||||
<!-- inner -->
|
||||
<line x1="141" x2="141" y1="231" y2="301"/>
|
||||
<!-- order -->
|
||||
<path d="M 141 296 L 201 296 L 211 286 L 211 146 L 221 136 L 281 136" />
|
||||
<!-- outer -->
|
||||
<path d="M 141 306 L 201 306 L 211 316 L 211 386 L 221 396 L 281 396" />
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="141" cy="231" />
|
||||
<text x="141" y="231">inner</text>
|
||||
<rect class="test" width="110" height="50" x="86" y="276" />
|
||||
<text x="141" y="301">test_order</text>
|
||||
<!-- scope name -->
|
||||
<defs>
|
||||
<path d="M 51,266 A 90 90 0 0 1 231 266" id="testOne"/>
|
||||
</defs>
|
||||
<text class="class">
|
||||
<textPath href="#testOne" startOffset="50%">TestOne</textPath>
|
||||
</text>
|
||||
<!-- scope order number -->
|
||||
<mask id="testOneOrderMask">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="white"/>
|
||||
<circle fill="black" stroke="white" stroke-width="2" r="100" cx="141" cy="266" />
|
||||
</mask>
|
||||
<circle class="module" r="15" cx="41" cy="266" mask="url(#testOneOrderMask)"/>
|
||||
<text class="module" x="41" y="266">1</text>
|
||||
<!-- scope order number -->
|
||||
<mask id="testMainOrderMask">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="white"/>
|
||||
<circle fill="black" stroke="white" stroke-width="2" r="265" cx="281" cy="266" />
|
||||
</mask>
|
||||
<circle class="module" r="15" cx="16" cy="266" mask="url(#testMainOrderMask)"/>
|
||||
<text class="module" x="16" y="266">2</text>
|
||||
|
||||
<!-- TestTwo -->
|
||||
<circle class="class" r="100" cx="421" cy="266" />
|
||||
<!-- inner -->
|
||||
<line x1="421" x2="421" y1="231" y2="301"/>
|
||||
<!-- order -->
|
||||
<path d="M 421 296 L 361 296 L 351 286 L 351 146 L 341 136 L 281 136" />
|
||||
<!-- outer -->
|
||||
<path d="M 421 306 L 361 306 L 351 316 L 351 386 L 341 396 L 281 396" />
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="421" cy="231" />
|
||||
<text x="421" y="231">inner</text>
|
||||
<rect class="test" width="110" height="50" x="366" y="276" />
|
||||
<text x="421" y="301">test_order</text>
|
||||
<!-- scope name -->
|
||||
<defs>
|
||||
<path d="M 331,266 A 90 90 0 0 1 511 266" id="testTwo"/>
|
||||
</defs>
|
||||
<text class="class">
|
||||
<textPath href="#testTwo" startOffset="50%">TestTwo</textPath>
|
||||
</text>
|
||||
<!-- scope order number -->
|
||||
<mask id="testTwoOrderMask">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="white"/>
|
||||
<circle fill="black" stroke="white" stroke-width="2" r="100" cx="421" cy="266" />
|
||||
</mask>
|
||||
<circle class="module" r="15" cx="521" cy="266" mask="url(#testTwoOrderMask)"/>
|
||||
<text class="module" x="521" y="266">1</text>
|
||||
<!-- scope order number -->
|
||||
<circle class="module" r="15" cx="546" cy="266" mask="url(#testMainOrderMask)"/>
|
||||
<text class="module" x="546" y="266">2</text>
|
||||
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="281" cy="396" />
|
||||
<text x="281" y="396">outer</text>
|
||||
<ellipse class="fixture" rx="50" ry="25" cx="281" cy="136" />
|
||||
<text x="281" y="136">order</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
@@ -221,14 +221,19 @@ Registering markers for your test suite is simple:
|
||||
[pytest]
|
||||
markers =
|
||||
webtest: mark a test as a webtest.
|
||||
slow: mark test as slow.
|
||||
|
||||
You can ask which markers exist for your test suite - the list includes our just defined ``webtest`` markers:
|
||||
Multiple custom markers can be registered, by defining each one in its own line, as shown in above example.
|
||||
|
||||
You can ask which markers exist for your test suite - the list includes our just defined ``webtest`` and ``slow`` markers:
|
||||
|
||||
.. code-block:: pytest
|
||||
|
||||
$ pytest --markers
|
||||
@pytest.mark.webtest: mark a test as a webtest.
|
||||
|
||||
@pytest.mark.slow: mark test as slow.
|
||||
|
||||
@pytest.mark.filterwarnings(warning): add a warning filter to the given test. see https://docs.pytest.org/en/stable/warnings.html#pytest-mark-filterwarnings
|
||||
|
||||
@pytest.mark.skip(reason=None): skip the given test function with an optional reason. Example: skip(reason="no way of currently testing this") skips the test.
|
||||
@@ -539,7 +544,7 @@ Let's run this without capturing output and see what we get:
|
||||
.
|
||||
1 passed in 0.12s
|
||||
|
||||
marking platform specific tests with pytest
|
||||
Marking platform specific tests with pytest
|
||||
--------------------------------------------------------------
|
||||
|
||||
.. regendoc:wipe
|
||||
|
||||
@@ -26,7 +26,7 @@ class Python:
|
||||
def __init__(self, version, picklefile):
|
||||
self.pythonpath = shutil.which(version)
|
||||
if not self.pythonpath:
|
||||
pytest.skip("{!r} not found".format(version))
|
||||
pytest.skip(f"{version!r} not found")
|
||||
self.picklefile = picklefile
|
||||
|
||||
def dumps(self, obj):
|
||||
@@ -69,4 +69,4 @@ class Python:
|
||||
@pytest.mark.parametrize("obj", [42, {}, {1: 3}])
|
||||
def test_basic_objects(python1, python2, obj):
|
||||
python1.dumps(obj)
|
||||
python2.load_and_is_true("obj == {}".format(obj))
|
||||
python2.load_and_is_true(f"obj == {obj}")
|
||||
|
||||
@@ -102,4 +102,4 @@ interesting to just look at the collection tree:
|
||||
<YamlItem hello>
|
||||
<YamlItem ok>
|
||||
|
||||
========================== no tests ran in 0.12s ===========================
|
||||
======================== 2 tests collected in 0.12s ========================
|
||||
|
||||
@@ -40,7 +40,7 @@ class YamlItem(pytest.Item):
|
||||
)
|
||||
|
||||
def reportinfo(self):
|
||||
return self.fspath, 0, "usecase: {}".format(self.name)
|
||||
return self.fspath, 0, f"usecase: {self.name}"
|
||||
|
||||
|
||||
class YamlException(Exception):
|
||||
|
||||
@@ -175,7 +175,7 @@ objects, they are still using the default pytest representation:
|
||||
<Function test_timedistance_v3[forward]>
|
||||
<Function test_timedistance_v3[backward]>
|
||||
|
||||
========================== no tests ran in 0.12s ===========================
|
||||
======================== 8 tests collected in 0.12s ========================
|
||||
|
||||
In ``test_timedistance_v3``, we used ``pytest.param`` to specify the test IDs
|
||||
together with the actual data, instead of listing them separately.
|
||||
@@ -252,7 +252,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia
|
||||
<Function test_demo1[advanced]>
|
||||
<Function test_demo2[advanced]>
|
||||
|
||||
========================== no tests ran in 0.12s ===========================
|
||||
======================== 4 tests collected in 0.12s ========================
|
||||
|
||||
Note that we told ``metafunc.parametrize()`` that your scenario values
|
||||
should be considered class-scoped. With pytest-2.3 this leads to a
|
||||
@@ -328,7 +328,7 @@ Let's first see how it looks like at collection time:
|
||||
<Function test_db_initialized[d1]>
|
||||
<Function test_db_initialized[d2]>
|
||||
|
||||
========================== no tests ran in 0.12s ===========================
|
||||
======================== 2 tests collected in 0.12s ========================
|
||||
|
||||
And then when we run the test:
|
||||
|
||||
@@ -637,13 +637,13 @@ Then run ``pytest`` with verbose mode and with only the ``basic`` marker:
|
||||
platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python
|
||||
cachedir: $PYTHON_PREFIX/.pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR
|
||||
collecting ... collected 14 items / 11 deselected / 3 selected
|
||||
collecting ... collected 24 items / 21 deselected / 3 selected
|
||||
|
||||
test_pytest_param_example.py::test_eval[1+7-8] PASSED [ 33%]
|
||||
test_pytest_param_example.py::test_eval[basic_2+4] PASSED [ 66%]
|
||||
test_pytest_param_example.py::test_eval[basic_6*9] XFAIL [100%]
|
||||
|
||||
=============== 2 passed, 11 deselected, 1 xfailed in 0.12s ================
|
||||
=============== 2 passed, 21 deselected, 1 xfailed in 0.12s ================
|
||||
|
||||
As the result:
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ The test collection would look like this:
|
||||
<Function simple_check>
|
||||
<Function complex_check>
|
||||
|
||||
========================== no tests ran in 0.12s ===========================
|
||||
======================== 2 tests collected in 0.12s ========================
|
||||
|
||||
You can check for multiple glob patterns by adding a space between the patterns:
|
||||
|
||||
@@ -220,7 +220,7 @@ You can always peek at the collection tree without running tests like this:
|
||||
<Function test_method>
|
||||
<Function test_anothermethod>
|
||||
|
||||
========================== no tests ran in 0.12s ===========================
|
||||
======================== 3 tests collected in 0.12s ========================
|
||||
|
||||
.. _customizing-test-collection:
|
||||
|
||||
@@ -282,7 +282,7 @@ leave out the ``setup.py`` file:
|
||||
<Module 'pkg/module_py2.py'>
|
||||
<Function 'test_only_on_python2'>
|
||||
|
||||
====== no tests ran in 0.04 seconds ======
|
||||
====== 1 tests found in 0.04 seconds ======
|
||||
|
||||
If you run with a Python 3 interpreter both the one test and the ``setup.py``
|
||||
file will be left out:
|
||||
@@ -296,7 +296,7 @@ file will be left out:
|
||||
rootdir: $REGENDOC_TMPDIR, configfile: pytest.ini
|
||||
collected 0 items
|
||||
|
||||
========================== no tests ran in 0.12s ===========================
|
||||
======================= no tests collected in 0.12s ========================
|
||||
|
||||
It's also possible to ignore files based on Unix shell-style wildcards by adding
|
||||
patterns to :globalvar:`collect_ignore_glob`.
|
||||
|
||||
@@ -446,7 +446,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
|
||||
def test_reinterpret_fails_with_print_for_the_fun_of_it(self):
|
||||
items = [1, 2, 3]
|
||||
print("items is {!r}".format(items))
|
||||
print(f"items is {items!r}")
|
||||
> a, b = items.pop()
|
||||
E TypeError: cannot unpack non-iterable int object
|
||||
|
||||
|
||||
@@ -452,7 +452,7 @@ and nothing when run plainly:
|
||||
|
||||
========================== no tests ran in 0.12s ===========================
|
||||
|
||||
profiling test duration
|
||||
Profiling test duration
|
||||
--------------------------
|
||||
|
||||
.. regendoc:wipe
|
||||
@@ -498,7 +498,7 @@ Now we can profile which test functions execute the slowest:
|
||||
0.10s call test_some_are_slow.py::test_funcfast
|
||||
============================ 3 passed in 0.12s =============================
|
||||
|
||||
incremental testing - test steps
|
||||
Incremental testing - test steps
|
||||
---------------------------------------------------
|
||||
|
||||
.. regendoc:wipe
|
||||
@@ -739,7 +739,7 @@ it (unless you use "autouse" fixture which are always executed ahead of the firs
|
||||
executing).
|
||||
|
||||
|
||||
post-process test reports / failures
|
||||
Post-process test reports / failures
|
||||
---------------------------------------
|
||||
|
||||
If you want to postprocess test reports and need access to the executing
|
||||
|
||||
1693
doc/en/fixture.rst
@@ -1,7 +1,7 @@
|
||||
Installation and Getting Started
|
||||
===================================
|
||||
|
||||
**Pythons**: Python 3.5, 3.6, 3.7, 3.8, 3.9, PyPy3
|
||||
**Pythons**: Python 3.6, 3.7, 3.8, 3.9, PyPy3
|
||||
|
||||
**Platforms**: Linux and Windows
|
||||
|
||||
@@ -28,7 +28,7 @@ Install ``pytest``
|
||||
.. code-block:: bash
|
||||
|
||||
$ pytest --version
|
||||
pytest 6.1.0
|
||||
pytest 6.2.2
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
.. sidebar:: Next Open Trainings
|
||||
|
||||
- `Professional testing with Python <https://www.python-academy.com/courses/specialtopics/python_course_testing.html>`_, via Python Academy, February 1-3 2021, Leipzig (Germany) and remote.
|
||||
- `Professional testing with Python <https://www.python-academy.com/courses/specialtopics/python_course_testing.html>`_, via Python Academy, February 1-3 2021, remote and Leipzig (Germany). **Early-bird discount available until January 15th**.
|
||||
|
||||
Also see `previous talks and blogposts <talks.html>`_.
|
||||
|
||||
@@ -69,7 +69,7 @@ Features
|
||||
|
||||
- Can run :ref:`unittest <unittest>` (including trial) and :ref:`nose <noseintegration>` test suites out of the box
|
||||
|
||||
- Python 3.5+ and PyPy 3
|
||||
- Python 3.6+ and PyPy 3
|
||||
|
||||
- Rich plugin architecture, with over 315+ `external plugins <http://plugincompat.herokuapp.com>`_ and thriving community
|
||||
|
||||
|
||||
@@ -68,4 +68,13 @@ Unsupported idioms / known issues
|
||||
fundamentally incompatible with pytest because they don't support fixtures
|
||||
properly since collection and test execution are separated.
|
||||
|
||||
Migrating from nose to pytest
|
||||
------------------------------
|
||||
|
||||
`nose2pytest <https://github.com/pytest-dev/nose2pytest>`_ is a Python script
|
||||
and pytest plugin to help convert Nose-based tests into pytest-based tests.
|
||||
Specifically, the script transforms nose.tools.assert_* function calls into
|
||||
raw assert statements, while preserving format of original arguments
|
||||
as much as possible.
|
||||
|
||||
.. _nose: https://nose.readthedocs.io/en/latest/
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
API Reference
|
||||
=============
|
||||
|
||||
.. module:: pytest
|
||||
|
||||
This page contains the full reference to pytest's API.
|
||||
|
||||
.. contents::
|
||||
@@ -189,7 +191,7 @@ Mark a test function as using the given fixture names.
|
||||
When using `usefixtures` in hooks, it can only load fixtures when applied to a test function before test setup
|
||||
(for example in the `pytest_collection_modifyitems` hook).
|
||||
|
||||
Also not that his mark has no effect when applied to **fixtures**.
|
||||
Also note that this mark has no effect when applied to **fixtures**.
|
||||
|
||||
|
||||
|
||||
@@ -248,6 +250,16 @@ Will create and attach a :class:`Mark <_pytest.mark.structures.Mark>` object to
|
||||
mark.args == (10, "slow")
|
||||
mark.kwargs == {"method": "thread"}
|
||||
|
||||
Example for using multiple custom markers:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.timeout(10, "slow", method="thread")
|
||||
@pytest.mark.slow
|
||||
def test_function():
|
||||
...
|
||||
|
||||
When :meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>` or :meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers_with_node>` is used with multiple markers, the marker closest to the function will be iterated over first. The above example will result in ``@pytest.mark.slow`` followed by ``@pytest.mark.timeout(...)``.
|
||||
|
||||
.. _`fixtures-api`:
|
||||
|
||||
@@ -304,11 +316,10 @@ request ``pytestconfig`` into your fixture and get it with ``pytestconfig.cache`
|
||||
Under the hood, the cache plugin uses the simple
|
||||
``dumps``/``loads`` API of the :py:mod:`json` stdlib module.
|
||||
|
||||
.. currentmodule:: _pytest.cacheprovider
|
||||
``config.cache`` is an instance of :class:`pytest.Cache`:
|
||||
|
||||
.. automethod:: Cache.get
|
||||
.. automethod:: Cache.set
|
||||
.. automethod:: Cache.makedir
|
||||
.. autoclass:: pytest.Cache()
|
||||
:members:
|
||||
|
||||
|
||||
.. fixture:: capsys
|
||||
@@ -318,12 +329,10 @@ capsys
|
||||
|
||||
**Tutorial**: :doc:`capture`.
|
||||
|
||||
.. currentmodule:: _pytest.capture
|
||||
|
||||
.. autofunction:: capsys()
|
||||
.. autofunction:: _pytest.capture.capsys()
|
||||
:no-auto-options:
|
||||
|
||||
Returns an instance of :py:class:`CaptureFixture`.
|
||||
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -334,7 +343,7 @@ capsys
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
|
||||
.. autoclass:: CaptureFixture()
|
||||
.. autoclass:: pytest.CaptureFixture()
|
||||
:members:
|
||||
|
||||
|
||||
@@ -345,10 +354,10 @@ capsysbinary
|
||||
|
||||
**Tutorial**: :doc:`capture`.
|
||||
|
||||
.. autofunction:: capsysbinary()
|
||||
.. autofunction:: _pytest.capture.capsysbinary()
|
||||
:no-auto-options:
|
||||
|
||||
Returns an instance of :py:class:`CaptureFixture`.
|
||||
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -367,10 +376,10 @@ capfd
|
||||
|
||||
**Tutorial**: :doc:`capture`.
|
||||
|
||||
.. autofunction:: capfd()
|
||||
.. autofunction:: _pytest.capture.capfd()
|
||||
:no-auto-options:
|
||||
|
||||
Returns an instance of :py:class:`CaptureFixture`.
|
||||
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -389,10 +398,10 @@ capfdbinary
|
||||
|
||||
**Tutorial**: :doc:`capture`.
|
||||
|
||||
.. autofunction:: capfdbinary()
|
||||
.. autofunction:: _pytest.capture.capfdbinary()
|
||||
:no-auto-options:
|
||||
|
||||
Returns an instance of :py:class:`CaptureFixture`.
|
||||
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -433,7 +442,7 @@ request
|
||||
|
||||
The ``request`` fixture is a special fixture providing information of the requesting test function.
|
||||
|
||||
.. autoclass:: _pytest.fixtures.FixtureRequest()
|
||||
.. autoclass:: pytest.FixtureRequest()
|
||||
:members:
|
||||
|
||||
|
||||
@@ -475,9 +484,9 @@ caplog
|
||||
.. autofunction:: _pytest.logging.caplog()
|
||||
:no-auto-options:
|
||||
|
||||
Returns a :class:`_pytest.logging.LogCaptureFixture` instance.
|
||||
Returns a :class:`pytest.LogCaptureFixture` instance.
|
||||
|
||||
.. autoclass:: _pytest.logging.LogCaptureFixture
|
||||
.. autoclass:: pytest.LogCaptureFixture()
|
||||
:members:
|
||||
|
||||
|
||||
@@ -486,30 +495,30 @@ caplog
|
||||
monkeypatch
|
||||
~~~~~~~~~~~
|
||||
|
||||
.. currentmodule:: _pytest.monkeypatch
|
||||
|
||||
**Tutorial**: :doc:`monkeypatch`.
|
||||
|
||||
.. autofunction:: _pytest.monkeypatch.monkeypatch()
|
||||
:no-auto-options:
|
||||
|
||||
Returns a :class:`MonkeyPatch` instance.
|
||||
Returns a :class:`~pytest.MonkeyPatch` instance.
|
||||
|
||||
.. autoclass:: _pytest.monkeypatch.MonkeyPatch
|
||||
.. autoclass:: pytest.MonkeyPatch
|
||||
:members:
|
||||
|
||||
|
||||
.. fixture:: testdir
|
||||
.. fixture:: pytester
|
||||
|
||||
testdir
|
||||
~~~~~~~
|
||||
pytester
|
||||
~~~~~~~~
|
||||
|
||||
.. currentmodule:: _pytest.pytester
|
||||
.. versionadded:: 6.2
|
||||
|
||||
This fixture provides a :class:`Testdir` instance useful for black-box testing of test files, making it ideal to
|
||||
test plugins.
|
||||
Provides a :class:`~pytest.Pytester` instance that can be used to run and test pytest itself.
|
||||
|
||||
To use it, include in your top-most ``conftest.py`` file:
|
||||
It provides an empty directory where pytest can be executed in isolation, and contains facilities
|
||||
to write tests, configuration files, and match against expected output.
|
||||
|
||||
To use it, include in your topmost ``conftest.py`` file:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -517,13 +526,30 @@ To use it, include in your top-most ``conftest.py`` file:
|
||||
|
||||
|
||||
|
||||
.. autoclass:: Testdir()
|
||||
.. autoclass:: pytest.Pytester()
|
||||
:members:
|
||||
|
||||
.. autoclass:: RunResult()
|
||||
.. autoclass:: _pytest.pytester.RunResult()
|
||||
:members:
|
||||
|
||||
.. autoclass:: LineMatcher()
|
||||
.. autoclass:: _pytest.pytester.LineMatcher()
|
||||
:members:
|
||||
:special-members: __str__
|
||||
|
||||
.. autoclass:: _pytest.pytester.HookRecorder()
|
||||
:members:
|
||||
|
||||
.. fixture:: testdir
|
||||
|
||||
testdir
|
||||
~~~~~~~
|
||||
|
||||
Identical to :fixture:`pytester`, but provides an instance whose methods return
|
||||
legacy ``py.path.local`` objects instead when applicable.
|
||||
|
||||
New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`.
|
||||
|
||||
.. autoclass:: pytest.Testdir()
|
||||
:members:
|
||||
|
||||
|
||||
@@ -534,12 +560,10 @@ recwarn
|
||||
|
||||
**Tutorial**: :ref:`assertwarnings`
|
||||
|
||||
.. currentmodule:: _pytest.recwarn
|
||||
|
||||
.. autofunction:: recwarn()
|
||||
.. autofunction:: _pytest.recwarn.recwarn()
|
||||
:no-auto-options:
|
||||
|
||||
.. autoclass:: WarningsRecorder()
|
||||
.. autoclass:: pytest.WarningsRecorder()
|
||||
:members:
|
||||
|
||||
Each recorded warning is an instance of :class:`warnings.WarningMessage`.
|
||||
@@ -556,13 +580,11 @@ tmp_path
|
||||
|
||||
**Tutorial**: :doc:`tmpdir`
|
||||
|
||||
.. currentmodule:: _pytest.tmpdir
|
||||
|
||||
.. autofunction:: tmp_path()
|
||||
.. autofunction:: _pytest.tmpdir.tmp_path()
|
||||
:no-auto-options:
|
||||
|
||||
|
||||
.. fixture:: tmp_path_factory
|
||||
.. fixture:: _pytest.tmpdir.tmp_path_factory
|
||||
|
||||
tmp_path_factory
|
||||
~~~~~~~~~~~~~~~~
|
||||
@@ -571,12 +593,9 @@ tmp_path_factory
|
||||
|
||||
.. _`tmp_path_factory factory api`:
|
||||
|
||||
``tmp_path_factory`` instances have the following methods:
|
||||
``tmp_path_factory`` is an instance of :class:`~pytest.TempPathFactory`:
|
||||
|
||||
.. currentmodule:: _pytest.tmpdir
|
||||
|
||||
.. automethod:: TempPathFactory.mktemp
|
||||
.. automethod:: TempPathFactory.getbasetemp
|
||||
.. autoclass:: pytest.TempPathFactory()
|
||||
|
||||
|
||||
.. fixture:: tmpdir
|
||||
@@ -586,9 +605,7 @@ tmpdir
|
||||
|
||||
**Tutorial**: :doc:`tmpdir`
|
||||
|
||||
.. currentmodule:: _pytest.tmpdir
|
||||
|
||||
.. autofunction:: tmpdir()
|
||||
.. autofunction:: _pytest.tmpdir.tmpdir()
|
||||
:no-auto-options:
|
||||
|
||||
|
||||
@@ -601,12 +618,9 @@ tmpdir_factory
|
||||
|
||||
.. _`tmpdir factory api`:
|
||||
|
||||
``tmpdir_factory`` instances have the following methods:
|
||||
``tmp_path_factory`` is an instance of :class:`~pytest.TempdirFactory`:
|
||||
|
||||
.. currentmodule:: _pytest.tmpdir
|
||||
|
||||
.. automethod:: TempdirFactory.mktemp
|
||||
.. automethod:: TempdirFactory.getbasetemp
|
||||
.. autoclass:: pytest.TempdirFactory()
|
||||
|
||||
|
||||
.. _`hook-reference`:
|
||||
@@ -668,6 +682,10 @@ items, delete or otherwise amend the test items:
|
||||
|
||||
.. autofunction:: pytest_collection_modifyitems
|
||||
|
||||
.. note::
|
||||
If this hook is implemented in ``conftest.py`` files, it always receives all collected items, not only those
|
||||
under the ``conftest.py`` where it is implemented.
|
||||
|
||||
.. autofunction:: pytest_collection_finish
|
||||
|
||||
Test running (runtest) hooks
|
||||
@@ -816,6 +834,13 @@ Function
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
FunctionDefinition
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: _pytest.python.FunctionDefinition()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Item
|
||||
~~~~
|
||||
|
||||
@@ -1061,6 +1086,12 @@ Custom warnings generated in some situations such as improper usage or deprecate
|
||||
.. autoclass:: pytest.PytestUnknownMarkWarning
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: pytest.PytestUnraisableExceptionWarning
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: pytest.PytestUnhandledThreadExceptionWarning
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Consult the :ref:`internal-warnings` section in the documentation for more information.
|
||||
|
||||
@@ -1236,12 +1267,13 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
.. confval:: junit_family
|
||||
|
||||
.. versionadded:: 4.2
|
||||
.. versionchanged:: 6.1
|
||||
Default changed to ``xunit2``.
|
||||
|
||||
Configures the format of the generated JUnit XML file. The possible options are:
|
||||
|
||||
* ``xunit1`` (or ``legacy``): produces old style output, compatible with the xunit 1.0 format. **This is the default**.
|
||||
* ``xunit2``: produces `xunit 2.0 style output <https://github.com/jenkinsci/xunit-plugin/blob/xunit-2.3.2/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd>`__,
|
||||
which should be more compatible with latest Jenkins versions.
|
||||
* ``xunit1`` (or ``legacy``): produces old style output, compatible with the xunit 1.0 format.
|
||||
* ``xunit2``: produces `xunit 2.0 style output <https://github.com/jenkinsci/xunit-plugin/blob/xunit-2.3.2/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd>`__, which should be more compatible with latest Jenkins versions. **This is the default**.
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
@@ -1511,7 +1543,8 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
[seq] matches any character in seq
|
||||
[!seq] matches any char not in seq
|
||||
|
||||
Default patterns are ``'.*', 'build', 'dist', 'CVS', '_darcs', '{arch}', '*.egg', 'venv'``.
|
||||
Default patterns are ``'*.egg'``, ``'.*'``, ``'_darcs'``, ``'build'``,
|
||||
``'CVS'``, ``'dist'``, ``'node_modules'``, ``'venv'``, ``'{arch}'``.
|
||||
Setting a ``norecursedirs`` replaces the default. Here is an example of
|
||||
how to avoid certain directories:
|
||||
|
||||
@@ -1718,7 +1751,8 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
failures.
|
||||
--sw, --stepwise exit on test failure and continue from last failing
|
||||
test next time
|
||||
--stepwise-skip ignore the first failing test but stop on the next
|
||||
--sw-skip, --stepwise-skip
|
||||
ignore the first failing test but stop on the next
|
||||
failing test
|
||||
|
||||
reporting:
|
||||
@@ -1760,9 +1794,9 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
--maxfail=num exit after first num failures or errors.
|
||||
--strict-config any warnings encountered while parsing the `pytest`
|
||||
section of the configuration file raise errors.
|
||||
--strict-markers, --strict
|
||||
markers not registered in the `markers` section of
|
||||
--strict-markers markers not registered in the `markers` section of
|
||||
the configuration file raise errors.
|
||||
--strict (deprecated) alias to --strict-markers.
|
||||
-c file load configuration from `file` instead of trying to
|
||||
locate one of the implicit configuration files.
|
||||
--continue-on-collection-errors
|
||||
|
||||
@@ -91,7 +91,7 @@ when run on an interpreter earlier than Python3.6:
|
||||
import sys
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6 or higher")
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher")
|
||||
def test_function():
|
||||
...
|
||||
|
||||
@@ -259,7 +259,7 @@ These two examples illustrate situations where you don't want to check for a con
|
||||
at the module level, which is when a condition would otherwise be evaluated for marks.
|
||||
|
||||
This will make ``test_function`` ``XFAIL``. Note that no other code is executed after
|
||||
the ``pytest.xfail`` call, differently from the marker. That's because it is implemented
|
||||
the :func:`pytest.xfail` call, differently from the marker. That's because it is implemented
|
||||
internally by raising a known exception.
|
||||
|
||||
**Reference**: :ref:`pytest.mark.xfail ref`
|
||||
@@ -358,7 +358,7 @@ By specifying on the commandline:
|
||||
pytest --runxfail
|
||||
|
||||
you can force the running and reporting of an ``xfail`` marked test
|
||||
as if it weren't marked at all. This also causes ``pytest.xfail`` to produce no effect.
|
||||
as if it weren't marked at all. This also causes :func:`pytest.xfail` to produce no effect.
|
||||
|
||||
Examples
|
||||
~~~~~~~~
|
||||
|
||||
@@ -15,13 +15,11 @@ You can use the ``tmp_path`` fixture which will
|
||||
provide a temporary directory unique to the test invocation,
|
||||
created in the `base temporary directory`_.
|
||||
|
||||
``tmp_path`` is a ``pathlib/pathlib2.Path`` object. Here is an example test usage:
|
||||
``tmp_path`` is a ``pathlib.Path`` object. Here is an example test usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# content of test_tmp_path.py
|
||||
import os
|
||||
|
||||
CONTENT = "content"
|
||||
|
||||
|
||||
@@ -63,7 +61,7 @@ Running this would result in a passed test except for the last
|
||||
> assert 0
|
||||
E assert 0
|
||||
|
||||
test_tmp_path.py:13: AssertionError
|
||||
test_tmp_path.py:11: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_tmp_path.py::test_create_file - assert 0
|
||||
============================ 1 failed in 0.12s =============================
|
||||
@@ -97,9 +95,6 @@ and more. Here is an example test usage:
|
||||
.. code-block:: python
|
||||
|
||||
# content of test_tmpdir.py
|
||||
import os
|
||||
|
||||
|
||||
def test_create_file(tmpdir):
|
||||
p = tmpdir.mkdir("sub").join("hello.txt")
|
||||
p.write("content")
|
||||
@@ -134,7 +129,7 @@ Running this would result in a passed test except for the last
|
||||
> assert 0
|
||||
E assert 0
|
||||
|
||||
test_tmpdir.py:9: AssertionError
|
||||
test_tmpdir.py:6: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_tmpdir.py::test_create_file - assert 0
|
||||
============================ 1 failed in 0.12s =============================
|
||||
|
||||
@@ -470,6 +470,38 @@ seconds to finish (not available on Windows).
|
||||
the command-line using ``-o faulthandler_timeout=X``.
|
||||
|
||||
|
||||
.. _unraisable:
|
||||
|
||||
Warning about unraisable exceptions and unhandled thread exceptions
|
||||
-------------------------------------------------------------------
|
||||
|
||||
.. versionadded:: 6.2
|
||||
|
||||
.. note::
|
||||
|
||||
These features only work on Python>=3.8.
|
||||
|
||||
Unhandled exceptions are exceptions that are raised in a situation in which
|
||||
they cannot propagate to a caller. The most common case is an exception raised
|
||||
in a :meth:`__del__ <object.__del__>` implementation.
|
||||
|
||||
Unhandled thread exceptions are exceptions raised in a :class:`~threading.Thread`
|
||||
but not handled, causing the thread to terminate uncleanly.
|
||||
|
||||
Both types of exceptions are normally considered bugs, but may go unnoticed
|
||||
because they don't cause the program itself to crash. Pytest detects these
|
||||
conditions and issues a warning that is visible in the test run summary.
|
||||
|
||||
The plugins are automatically enabled for pytest runs, unless the
|
||||
``-p no:unraisableexception`` (for unraisable exceptions) and
|
||||
``-p no:threadexception`` (for thread exceptions) options are given on the
|
||||
command-line.
|
||||
|
||||
The warnings may be silenced selectivly using the :ref:`pytest.mark.filterwarnings ref`
|
||||
mark. The warning categories are :class:`pytest.PytestUnraisableExceptionWarning` and
|
||||
:class:`pytest.PytestUnhandledThreadExceptionWarning`.
|
||||
|
||||
|
||||
Creating JUnitXML format files
|
||||
----------------------------------------------------
|
||||
|
||||
|
||||
@@ -265,7 +265,7 @@ Asserting warnings with the warns function
|
||||
|
||||
|
||||
|
||||
You can check that code raises a particular warning using ``pytest.warns``,
|
||||
You can check that code raises a particular warning using func:`pytest.warns`,
|
||||
which works in a similar manner to :ref:`raises <assertraises>`:
|
||||
|
||||
.. code-block:: python
|
||||
@@ -293,7 +293,7 @@ argument ``match`` to assert that the exception matches a text or regex::
|
||||
...
|
||||
Failed: DID NOT WARN. No warnings of type ...UserWarning... was emitted...
|
||||
|
||||
You can also call ``pytest.warns`` on a function or code string:
|
||||
You can also call func:`pytest.warns` on a function or code string:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -328,10 +328,10 @@ Alternatively, you can examine raised warnings in detail using the
|
||||
Recording warnings
|
||||
------------------
|
||||
|
||||
You can record raised warnings either using ``pytest.warns`` or with
|
||||
You can record raised warnings either using func:`pytest.warns` or with
|
||||
the ``recwarn`` fixture.
|
||||
|
||||
To record with ``pytest.warns`` without asserting anything about the warnings,
|
||||
To record with func:`pytest.warns` without asserting anything about the warnings,
|
||||
pass ``None`` as the expected warning type:
|
||||
|
||||
.. code-block:: python
|
||||
@@ -360,7 +360,7 @@ The ``recwarn`` fixture will record warnings for the whole function:
|
||||
assert w.filename
|
||||
assert w.lineno
|
||||
|
||||
Both ``recwarn`` and ``pytest.warns`` return the same interface for recorded
|
||||
Both ``recwarn`` and func:`pytest.warns` return the same interface for recorded
|
||||
warnings: a WarningsRecorder instance. To view the recorded warnings, you can
|
||||
iterate over this instance, call ``len`` on it to get the number of recorded
|
||||
warnings, or index into it to get a particular recorded warning.
|
||||
@@ -387,7 +387,7 @@ are met.
|
||||
pytest.fail("Expected a warning!")
|
||||
|
||||
If no warnings are issued when calling ``f``, then ``not record`` will
|
||||
evaluate to ``True``. You can then call ``pytest.fail`` with a
|
||||
evaluate to ``True``. You can then call :func:`pytest.fail` with a
|
||||
custom error message.
|
||||
|
||||
.. _internal-warnings:
|
||||
|
||||
@@ -33,26 +33,34 @@ Plugin discovery order at tool startup
|
||||
|
||||
``pytest`` loads plugin modules at tool startup in the following way:
|
||||
|
||||
* by loading all builtin plugins
|
||||
1. by scanning the command line for the ``-p no:name`` option
|
||||
and *blocking* that plugin from being loaded (even builtin plugins can
|
||||
be blocked this way). This happens before normal command-line parsing.
|
||||
|
||||
* by loading all plugins registered through `setuptools entry points`_.
|
||||
2. by loading all builtin plugins.
|
||||
|
||||
* by pre-scanning the command line for the ``-p name`` option
|
||||
and loading the specified plugin before actual command line parsing.
|
||||
3. by scanning the command line for the ``-p name`` option
|
||||
and loading the specified plugin. This happens before normal command-line parsing.
|
||||
|
||||
* by loading all :file:`conftest.py` files as inferred by the command line
|
||||
invocation:
|
||||
4. by loading all plugins registered through `setuptools entry points`_.
|
||||
|
||||
- if no test paths are specified use current dir as a test path
|
||||
- if exists, load ``conftest.py`` and ``test*/conftest.py`` relative
|
||||
to the directory part of the first test path.
|
||||
5. by loading all plugins specified through the :envvar:`PYTEST_PLUGINS` environment variable.
|
||||
|
||||
Note that pytest does not find ``conftest.py`` files in deeper nested
|
||||
sub directories at tool startup. It is usually a good idea to keep
|
||||
your ``conftest.py`` file in the top level test or project root directory.
|
||||
6. by loading all :file:`conftest.py` files as inferred by the command line
|
||||
invocation:
|
||||
|
||||
* by recursively loading all plugins specified by the
|
||||
:globalvar:`pytest_plugins` variable in ``conftest.py`` files
|
||||
- if no test paths are specified, use the current dir as a test path
|
||||
- if exists, load ``conftest.py`` and ``test*/conftest.py`` relative
|
||||
to the directory part of the first test path. After the ``conftest.py``
|
||||
file is loaded, load all plugins specified in its
|
||||
:globalvar:`pytest_plugins` variable if present.
|
||||
|
||||
Note that pytest does not find ``conftest.py`` files in deeper nested
|
||||
sub directories at tool startup. It is usually a good idea to keep
|
||||
your ``conftest.py`` file in the top level test or project root directory.
|
||||
|
||||
7. by recursively loading all plugins specified by the
|
||||
:globalvar:`pytest_plugins` variable in ``conftest.py`` files.
|
||||
|
||||
|
||||
.. _`pytest/plugin`: http://bitbucket.org/pytest-dev/pytest/src/tip/pytest/plugin/
|
||||
@@ -99,6 +107,10 @@ Here is how you might run it::
|
||||
|
||||
See also: :ref:`pythonpath`.
|
||||
|
||||
.. note::
|
||||
Some hooks should be implemented only in plugins or conftest.py files situated at the
|
||||
tests root directory due to how pytest discovers plugins during startup,
|
||||
see the documentation of each hook for details.
|
||||
|
||||
Writing your own plugin
|
||||
-----------------------
|
||||
@@ -437,13 +449,7 @@ Additionally it is possible to copy examples for an example folder before runnin
|
||||
|
||||
test_example.py .. [100%]
|
||||
|
||||
============================= warnings summary =============================
|
||||
test_example.py::test_plugin
|
||||
$REGENDOC_TMPDIR/test_example.py:4: PytestExperimentalApiWarning: testdir.copy_example is an experimental api that may change over time
|
||||
testdir.copy_example("test_example.py")
|
||||
|
||||
-- Docs: https://docs.pytest.org/en/stable/warnings.html
|
||||
======================= 2 passed, 1 warning in 0.12s =======================
|
||||
============================ 2 passed in 0.12s =============================
|
||||
|
||||
For more information about the result object that ``runpytest()`` returns, and
|
||||
the methods that it provides please check out the :py:class:`RunResult
|
||||
|
||||
@@ -23,13 +23,14 @@ xfail_strict = true
|
||||
filterwarnings = [
|
||||
"error",
|
||||
"default:Using or importing the ABCs:DeprecationWarning:unittest2.*",
|
||||
# produced by older pyparsing<=2.2.0.
|
||||
"default:Using or importing the ABCs:DeprecationWarning:pyparsing.*",
|
||||
"default:the imp module is deprecated in favour of importlib:DeprecationWarning:nose.*",
|
||||
"ignore:Module already imported so cannot be rewritten:pytest.PytestWarning",
|
||||
# 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 by python >=3.5 on execnet (pytest-xdist)
|
||||
# produced on execnet (pytest-xdist)
|
||||
"ignore:.*inspect.getargspec.*deprecated, use inspect.signature.*:DeprecationWarning",
|
||||
# pytest's own futurewarnings
|
||||
"ignore::pytest.PytestExperimentalApiWarning",
|
||||
@@ -102,4 +103,4 @@ template = "changelog/_template.rst"
|
||||
showcontent = true
|
||||
|
||||
[tool.black]
|
||||
target-version = ['py35']
|
||||
target-version = ['py36']
|
||||
|
||||
@@ -57,6 +57,8 @@ Created automatically from {comment_url}.
|
||||
|
||||
Once all builds pass and it has been **approved** by one or more maintainers, the build
|
||||
can be released by pushing a tag `{version}` to this repository.
|
||||
|
||||
Closes #{issue_number}.
|
||||
"""
|
||||
|
||||
|
||||
@@ -164,7 +166,9 @@ def trigger_release(payload_path: Path, token: str) -> None:
|
||||
print(f"Branch {Fore.CYAN}{release_branch}{Fore.RESET} pushed.")
|
||||
|
||||
body = PR_BODY.format(
|
||||
comment_url=get_comment_data(payload)["html_url"], version=version
|
||||
comment_url=get_comment_data(payload)["html_url"],
|
||||
version=version,
|
||||
issue_number=issue_number,
|
||||
)
|
||||
pr = repo.create_pull(
|
||||
f"Prepare release {version}",
|
||||
@@ -227,7 +231,7 @@ def find_next_version(base_branch: str, is_major: bool) -> str:
|
||||
msg = dedent(
|
||||
f"""
|
||||
Found features or breaking changes in `{base_branch}`, and feature releases can only be
|
||||
created from `master`.":
|
||||
created from `master`:
|
||||
"""
|
||||
)
|
||||
msg += "\n".join(f"* `{x.name}`" for x in sorted(features + breaking))
|
||||
|
||||
@@ -17,9 +17,7 @@ def announce(version):
|
||||
stdout = stdout.decode("utf-8")
|
||||
last_version = stdout.strip()
|
||||
|
||||
stdout = check_output(
|
||||
["git", "log", "{}..HEAD".format(last_version), "--format=%aN"]
|
||||
)
|
||||
stdout = check_output(["git", "log", f"{last_version}..HEAD", "--format=%aN"])
|
||||
stdout = stdout.decode("utf-8")
|
||||
|
||||
contributors = set(stdout.splitlines())
|
||||
@@ -31,14 +29,10 @@ def announce(version):
|
||||
Path(__file__).parent.joinpath(template_name).read_text(encoding="UTF-8")
|
||||
)
|
||||
|
||||
contributors_text = (
|
||||
"\n".join("* {}".format(name) for name in sorted(contributors)) + "\n"
|
||||
)
|
||||
contributors_text = "\n".join(f"* {name}" for name in sorted(contributors)) + "\n"
|
||||
text = template_text.format(version=version, contributors=contributors_text)
|
||||
|
||||
target = Path(__file__).parent.joinpath(
|
||||
"../doc/en/announce/release-{}.rst".format(version)
|
||||
)
|
||||
target = Path(__file__).parent.joinpath(f"../doc/en/announce/release-{version}.rst")
|
||||
target.write_text(text, encoding="UTF-8")
|
||||
print(f"{Fore.CYAN}[generate.announce] {Fore.RESET}Generated {target.name}")
|
||||
|
||||
@@ -47,7 +41,7 @@ def announce(version):
|
||||
lines = index_path.read_text(encoding="UTF-8").splitlines()
|
||||
indent = " "
|
||||
for index, line in enumerate(lines):
|
||||
if line.startswith("{}release-".format(indent)):
|
||||
if line.startswith(f"{indent}release-"):
|
||||
new_line = indent + target.stem
|
||||
if line != new_line:
|
||||
lines.insert(index, new_line)
|
||||
@@ -96,7 +90,7 @@ def pre_release(version, *, skip_check_links):
|
||||
if not skip_check_links:
|
||||
check_links()
|
||||
|
||||
msg = "Prepare release version {}".format(version)
|
||||
msg = f"Prepare release version {version}"
|
||||
check_call(["git", "commit", "-a", "-m", msg])
|
||||
|
||||
print()
|
||||
|
||||
16
setup.cfg
@@ -17,7 +17,6 @@ classifiers =
|
||||
Operating System :: POSIX
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3 :: Only
|
||||
Programming Language :: Python :: 3.5
|
||||
Programming Language :: Python :: 3.6
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
@@ -27,6 +26,8 @@ classifiers =
|
||||
Topic :: Utilities
|
||||
keywords = test, unittest
|
||||
project_urls =
|
||||
Changelog=https://docs.pytest.org/en/stable/changelog.html
|
||||
Twitter=https://twitter.com/pytestdotorg
|
||||
Source=https://github.com/pytest-dev/pytest
|
||||
Tracker=https://github.com/pytest-dev/pytest/issues
|
||||
|
||||
@@ -40,22 +41,21 @@ packages =
|
||||
_pytest.mark
|
||||
pytest
|
||||
install_requires =
|
||||
attrs>=17.4.0
|
||||
attrs>=19.2.0
|
||||
iniconfig
|
||||
packaging
|
||||
pluggy>=0.12,<1.0
|
||||
pluggy>=0.12,<1.0.0a1
|
||||
py>=1.8.2
|
||||
toml
|
||||
atomicwrites>=1.0;sys_platform=="win32"
|
||||
colorama;sys_platform=="win32"
|
||||
importlib-metadata>=0.12;python_version<"3.8"
|
||||
pathlib2>=2.2.0;python_version<"3.6"
|
||||
python_requires = >=3.5
|
||||
python_requires = >=3.6
|
||||
package_dir =
|
||||
=src
|
||||
setup_requires =
|
||||
setuptools>=40.0
|
||||
setuptools-scm
|
||||
setuptools>=>=42.0
|
||||
setuptools-scm>=3.4
|
||||
zip_safe = no
|
||||
|
||||
[options.entry_points]
|
||||
@@ -64,8 +64,6 @@ console_scripts =
|
||||
py.test=pytest:console_main
|
||||
|
||||
[options.extras_require]
|
||||
checkqa-mypy =
|
||||
mypy==0.780
|
||||
testing =
|
||||
argcomplete
|
||||
hypothesis>=3.56
|
||||
|
||||
@@ -26,7 +26,7 @@ The generic argcomplete script for bash-completion
|
||||
uses a python program to determine startup script generated by pip.
|
||||
You can speed up completion somewhat by changing this script to include
|
||||
# PYTHON_ARGCOMPLETE_OK
|
||||
so the the python-argcomplete-check-easy-install-script does not
|
||||
so the python-argcomplete-check-easy-install-script does not
|
||||
need to be called to find the entry point of the code and see if that is
|
||||
marked with PYTHON_ARGCOMPLETE_OK.
|
||||
|
||||
@@ -103,7 +103,7 @@ if os.environ.get("_ARGCOMPLETE"):
|
||||
import argcomplete.completers
|
||||
except ImportError:
|
||||
sys.exit(-1)
|
||||
filescompleter = FastFilesCompleter() # type: Optional[FastFilesCompleter]
|
||||
filescompleter: Optional[FastFilesCompleter] = FastFilesCompleter()
|
||||
|
||||
def try_argcomplete(parser: argparse.ArgumentParser) -> None:
|
||||
argcomplete.autocomplete(parser, always_complete_options=False)
|
||||
|
||||
@@ -5,6 +5,7 @@ import traceback
|
||||
from inspect import CO_VARARGS
|
||||
from inspect import CO_VARKEYWORDS
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from traceback import format_exception_only
|
||||
from types import CodeType
|
||||
from types import FrameType
|
||||
@@ -17,10 +18,13 @@ from typing import Iterable
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import overload
|
||||
from typing import Pattern
|
||||
from typing import Sequence
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
from weakref import ref
|
||||
@@ -37,15 +41,10 @@ from _pytest._code.source import Source
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest._io.saferepr import safeformat
|
||||
from _pytest._io.saferepr import saferepr
|
||||
from _pytest.compat import ATTRS_EQ_FIELD
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import get_real_func
|
||||
from _pytest.compat import overload
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.pathlib import Path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Type
|
||||
from typing_extensions import Literal
|
||||
from weakref import ReferenceType
|
||||
|
||||
@@ -55,15 +54,14 @@ if TYPE_CHECKING:
|
||||
class Code:
|
||||
"""Wrapper around Python code objects."""
|
||||
|
||||
def __init__(self, rawcode) -> None:
|
||||
if not hasattr(rawcode, "co_filename"):
|
||||
rawcode = getrawcode(rawcode)
|
||||
if not isinstance(rawcode, CodeType):
|
||||
raise TypeError("not a code object: {!r}".format(rawcode))
|
||||
self.filename = rawcode.co_filename
|
||||
self.firstlineno = rawcode.co_firstlineno - 1
|
||||
self.name = rawcode.co_name
|
||||
self.raw = rawcode
|
||||
__slots__ = ("raw",)
|
||||
|
||||
def __init__(self, obj: CodeType) -> None:
|
||||
self.raw = obj
|
||||
|
||||
@classmethod
|
||||
def from_function(cls, obj: object) -> "Code":
|
||||
return cls(getrawcode(obj))
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.raw == other.raw
|
||||
@@ -71,6 +69,14 @@ class Code:
|
||||
# Ignore type because of https://github.com/python/mypy/issues/4266.
|
||||
__hash__ = None # type: ignore
|
||||
|
||||
@property
|
||||
def firstlineno(self) -> int:
|
||||
return self.raw.co_firstlineno - 1
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.raw.co_name
|
||||
|
||||
@property
|
||||
def path(self) -> Union[py.path.local, str]:
|
||||
"""Return a path object pointing to source code, or an ``str`` in
|
||||
@@ -118,12 +124,26 @@ class Frame:
|
||||
"""Wrapper around a Python frame holding f_locals and f_globals
|
||||
in which expressions can be evaluated."""
|
||||
|
||||
__slots__ = ("raw",)
|
||||
|
||||
def __init__(self, frame: FrameType) -> None:
|
||||
self.lineno = frame.f_lineno - 1
|
||||
self.f_globals = frame.f_globals
|
||||
self.f_locals = frame.f_locals
|
||||
self.raw = frame
|
||||
self.code = Code(frame.f_code)
|
||||
|
||||
@property
|
||||
def lineno(self) -> int:
|
||||
return self.raw.f_lineno - 1
|
||||
|
||||
@property
|
||||
def f_globals(self) -> Dict[str, Any]:
|
||||
return self.raw.f_globals
|
||||
|
||||
@property
|
||||
def f_locals(self) -> Dict[str, Any]:
|
||||
return self.raw.f_locals
|
||||
|
||||
@property
|
||||
def code(self) -> Code:
|
||||
return Code(self.raw.f_code)
|
||||
|
||||
@property
|
||||
def statement(self) -> "Source":
|
||||
@@ -165,17 +185,20 @@ class Frame:
|
||||
class TracebackEntry:
|
||||
"""A single entry in a Traceback."""
|
||||
|
||||
_repr_style = None # type: Optional[Literal["short", "long"]]
|
||||
exprinfo = None
|
||||
__slots__ = ("_rawentry", "_excinfo", "_repr_style")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
rawentry: TracebackType,
|
||||
excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,
|
||||
) -> None:
|
||||
self._excinfo = excinfo
|
||||
self._rawentry = rawentry
|
||||
self.lineno = rawentry.tb_lineno - 1
|
||||
self._excinfo = excinfo
|
||||
self._repr_style: Optional['Literal["short", "long"]'] = None
|
||||
|
||||
@property
|
||||
def lineno(self) -> int:
|
||||
return self._rawentry.tb_lineno - 1
|
||||
|
||||
def set_repr_style(self, mode: "Literal['short', 'long']") -> None:
|
||||
assert mode in ("short", "long")
|
||||
@@ -247,9 +270,9 @@ class TracebackEntry:
|
||||
|
||||
Mostly for internal use.
|
||||
"""
|
||||
tbh = (
|
||||
tbh: Union[bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]] = (
|
||||
False
|
||||
) # type: Union[bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]]
|
||||
)
|
||||
for maybe_ns_dct in (self.frame.f_locals, self.frame.f_globals):
|
||||
# in normal cases, f_locals and f_globals are dictionaries
|
||||
# however via `exec(...)` / `eval(...)` they can be other types
|
||||
@@ -302,7 +325,7 @@ class Traceback(List[TracebackEntry]):
|
||||
if isinstance(tb, TracebackType):
|
||||
|
||||
def f(cur: TracebackType) -> Iterable[TracebackEntry]:
|
||||
cur_ = cur # type: Optional[TracebackType]
|
||||
cur_: Optional[TracebackType] = cur
|
||||
while cur_ is not None:
|
||||
yield TracebackEntry(cur_, excinfo=excinfo)
|
||||
cur_ = cur_.tb_next
|
||||
@@ -347,13 +370,11 @@ class Traceback(List[TracebackEntry]):
|
||||
def __getitem__(self, key: int) -> TracebackEntry:
|
||||
...
|
||||
|
||||
@overload # noqa: F811
|
||||
def __getitem__(self, key: slice) -> "Traceback": # noqa: F811
|
||||
@overload
|
||||
def __getitem__(self, key: slice) -> "Traceback":
|
||||
...
|
||||
|
||||
def __getitem__( # noqa: F811
|
||||
self, key: Union[int, slice]
|
||||
) -> Union[TracebackEntry, "Traceback"]:
|
||||
def __getitem__(self, key: Union[int, slice]) -> Union[TracebackEntry, "Traceback"]:
|
||||
if isinstance(key, slice):
|
||||
return self.__class__(super().__getitem__(key))
|
||||
else:
|
||||
@@ -384,7 +405,7 @@ class Traceback(List[TracebackEntry]):
|
||||
def recursionindex(self) -> Optional[int]:
|
||||
"""Return the index of the frame/TracebackEntry where recursion originates if
|
||||
appropriate, None if no recursion occurred."""
|
||||
cache = {} # type: Dict[Tuple[Any, int, int], List[Dict[str, Any]]]
|
||||
cache: Dict[Tuple[Any, int, int], List[Dict[str, Any]]] = {}
|
||||
for i, entry in enumerate(self):
|
||||
# id for the code.raw is needed to work around
|
||||
# the strange metaprogramming in the decorator lib from pypi
|
||||
@@ -422,14 +443,14 @@ class ExceptionInfo(Generic[_E]):
|
||||
|
||||
_assert_start_repr = "AssertionError('assert "
|
||||
|
||||
_excinfo = attr.ib(type=Optional[Tuple["Type[_E]", "_E", TracebackType]])
|
||||
_excinfo = attr.ib(type=Optional[Tuple[Type["_E"], "_E", TracebackType]])
|
||||
_striptext = attr.ib(type=str, default="")
|
||||
_traceback = attr.ib(type=Optional[Traceback], default=None)
|
||||
|
||||
@classmethod
|
||||
def from_exc_info(
|
||||
cls,
|
||||
exc_info: Tuple["Type[_E]", "_E", TracebackType],
|
||||
exc_info: Tuple[Type[_E], _E, TracebackType],
|
||||
exprinfo: Optional[str] = None,
|
||||
) -> "ExceptionInfo[_E]":
|
||||
"""Return an ExceptionInfo for an existing exc_info tuple.
|
||||
@@ -480,13 +501,13 @@ class ExceptionInfo(Generic[_E]):
|
||||
"""Return an unfilled ExceptionInfo."""
|
||||
return cls(None)
|
||||
|
||||
def fill_unfilled(self, exc_info: Tuple["Type[_E]", _E, TracebackType]) -> None:
|
||||
def fill_unfilled(self, exc_info: Tuple[Type[_E], _E, TracebackType]) -> None:
|
||||
"""Fill an unfilled ExceptionInfo created with ``for_later()``."""
|
||||
assert self._excinfo is None, "ExceptionInfo was already filled"
|
||||
self._excinfo = exc_info
|
||||
|
||||
@property
|
||||
def type(self) -> "Type[_E]":
|
||||
def type(self) -> Type[_E]:
|
||||
"""The exception class."""
|
||||
assert (
|
||||
self._excinfo is not None
|
||||
@@ -552,7 +573,7 @@ class ExceptionInfo(Generic[_E]):
|
||||
return text
|
||||
|
||||
def errisinstance(
|
||||
self, exc: Union["Type[BaseException]", Tuple["Type[BaseException]", ...]]
|
||||
self, exc: Union[Type[BaseException], Tuple[Type[BaseException], ...]]
|
||||
) -> bool:
|
||||
"""Return True if the exception is an instance of exc.
|
||||
|
||||
@@ -626,7 +647,7 @@ class ExceptionInfo(Generic[_E]):
|
||||
)
|
||||
return fmt.repr_excinfo(self)
|
||||
|
||||
def match(self, regexp: "Union[str, Pattern[str]]") -> "Literal[True]":
|
||||
def match(self, regexp: Union[str, Pattern[str]]) -> "Literal[True]":
|
||||
"""Check whether the regular expression `regexp` matches the string
|
||||
representation of the exception using :func:`python:re.search`.
|
||||
|
||||
@@ -750,7 +771,7 @@ class FormattedExcinfo:
|
||||
else:
|
||||
str_repr = safeformat(value)
|
||||
# if len(str_repr) < 70 or not isinstance(value, (list, tuple, dict)):
|
||||
lines.append("{:<10} = {}".format(name, str_repr))
|
||||
lines.append(f"{name:<10} = {str_repr}")
|
||||
# else:
|
||||
# self._line("%-10s =\\" % (name,))
|
||||
# # XXX
|
||||
@@ -763,7 +784,7 @@ class FormattedExcinfo:
|
||||
entry: TracebackEntry,
|
||||
excinfo: Optional[ExceptionInfo[BaseException]] = None,
|
||||
) -> "ReprEntry":
|
||||
lines = [] # type: List[str]
|
||||
lines: List[str] = []
|
||||
style = entry._repr_style if entry._repr_style is not None else self.style
|
||||
if style in ("short", "long"):
|
||||
source = self._getentrysource(entry)
|
||||
@@ -845,7 +866,7 @@ class FormattedExcinfo:
|
||||
recursionindex = traceback.recursionindex()
|
||||
except Exception as e:
|
||||
max_frames = 10
|
||||
extraline = (
|
||||
extraline: Optional[str] = (
|
||||
"!!! Recursion error detected, but an error occurred locating the origin of recursion.\n"
|
||||
" The following exception happened when comparing locals in the stack frame:\n"
|
||||
" {exc_type}: {exc_msg}\n"
|
||||
@@ -855,7 +876,7 @@ class FormattedExcinfo:
|
||||
exc_msg=str(e),
|
||||
max_frames=max_frames,
|
||||
total=len(traceback),
|
||||
) # type: Optional[str]
|
||||
)
|
||||
# Type ignored because adding two instaces of a List subtype
|
||||
# currently incorrectly has type List instead of the subtype.
|
||||
traceback = traceback[:max_frames] + traceback[-max_frames:] # type: ignore
|
||||
@@ -871,20 +892,20 @@ class FormattedExcinfo:
|
||||
def repr_excinfo(
|
||||
self, excinfo: ExceptionInfo[BaseException]
|
||||
) -> "ExceptionChainRepr":
|
||||
repr_chain = (
|
||||
[]
|
||||
) # type: List[Tuple[ReprTraceback, Optional[ReprFileLocation], Optional[str]]]
|
||||
e = excinfo.value # type: Optional[BaseException]
|
||||
excinfo_ = excinfo # type: Optional[ExceptionInfo[BaseException]]
|
||||
repr_chain: List[
|
||||
Tuple[ReprTraceback, Optional[ReprFileLocation], Optional[str]]
|
||||
] = []
|
||||
e: Optional[BaseException] = excinfo.value
|
||||
excinfo_: Optional[ExceptionInfo[BaseException]] = excinfo
|
||||
descr = None
|
||||
seen = set() # type: Set[int]
|
||||
seen: Set[int] = set()
|
||||
while e is not None and id(e) not in seen:
|
||||
seen.add(id(e))
|
||||
if excinfo_:
|
||||
reprtraceback = self.repr_traceback(excinfo_)
|
||||
reprcrash = (
|
||||
reprcrash: Optional[ReprFileLocation] = (
|
||||
excinfo_._getreprcrash() if self.style != "value" else None
|
||||
) # type: Optional[ReprFileLocation]
|
||||
)
|
||||
else:
|
||||
# Fallback to native repr if the exception doesn't have a traceback:
|
||||
# ExceptionInfo objects require a full traceback to work.
|
||||
@@ -918,7 +939,7 @@ class FormattedExcinfo:
|
||||
return ExceptionChainRepr(repr_chain)
|
||||
|
||||
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
@attr.s(eq=False)
|
||||
class TerminalRepr:
|
||||
def __str__(self) -> str:
|
||||
# FYI this is called from pytest-xdist's serialization of exception
|
||||
@@ -936,14 +957,14 @@ class TerminalRepr:
|
||||
|
||||
|
||||
# This class is abstract -- only subclasses are instantiated.
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
@attr.s(eq=False)
|
||||
class ExceptionRepr(TerminalRepr):
|
||||
# Provided by subclasses.
|
||||
reprcrash = None # type: Optional[ReprFileLocation]
|
||||
reprtraceback = None # type: ReprTraceback
|
||||
reprcrash: Optional["ReprFileLocation"]
|
||||
reprtraceback: "ReprTraceback"
|
||||
|
||||
def __attrs_post_init__(self) -> None:
|
||||
self.sections = [] # type: List[Tuple[str, str, str]]
|
||||
self.sections: List[Tuple[str, str, str]] = []
|
||||
|
||||
def addsection(self, name: str, content: str, sep: str = "-") -> None:
|
||||
self.sections.append((name, content, sep))
|
||||
@@ -954,7 +975,7 @@ class ExceptionRepr(TerminalRepr):
|
||||
tw.line(content)
|
||||
|
||||
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
@attr.s(eq=False)
|
||||
class ExceptionChainRepr(ExceptionRepr):
|
||||
chain = attr.ib(
|
||||
type=Sequence[
|
||||
@@ -978,7 +999,7 @@ class ExceptionChainRepr(ExceptionRepr):
|
||||
super().toterminal(tw)
|
||||
|
||||
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
@attr.s(eq=False)
|
||||
class ReprExceptionInfo(ExceptionRepr):
|
||||
reprtraceback = attr.ib(type="ReprTraceback")
|
||||
reprcrash = attr.ib(type="ReprFileLocation")
|
||||
@@ -988,7 +1009,7 @@ class ReprExceptionInfo(ExceptionRepr):
|
||||
super().toterminal(tw)
|
||||
|
||||
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
@attr.s(eq=False)
|
||||
class ReprTraceback(TerminalRepr):
|
||||
reprentries = attr.ib(type=Sequence[Union["ReprEntry", "ReprEntryNative"]])
|
||||
extraline = attr.ib(type=Optional[str])
|
||||
@@ -1022,16 +1043,16 @@ class ReprTracebackNative(ReprTraceback):
|
||||
self.extraline = None
|
||||
|
||||
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
@attr.s(eq=False)
|
||||
class ReprEntryNative(TerminalRepr):
|
||||
lines = attr.ib(type=Sequence[str])
|
||||
style = "native" # type: _TracebackStyle
|
||||
style: "_TracebackStyle" = "native"
|
||||
|
||||
def toterminal(self, tw: TerminalWriter) -> None:
|
||||
tw.write("".join(self.lines))
|
||||
|
||||
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
@attr.s(eq=False)
|
||||
class ReprEntry(TerminalRepr):
|
||||
lines = attr.ib(type=Sequence[str])
|
||||
reprfuncargs = attr.ib(type=Optional["ReprFuncArgs"])
|
||||
@@ -1059,11 +1080,11 @@ class ReprEntry(TerminalRepr):
|
||||
# separate indents and source lines that are not failures: we want to
|
||||
# highlight the code but not the indentation, which may contain markers
|
||||
# such as "> assert 0"
|
||||
fail_marker = "{} ".format(FormattedExcinfo.fail_marker)
|
||||
fail_marker = f"{FormattedExcinfo.fail_marker} "
|
||||
indent_size = len(fail_marker)
|
||||
indents = [] # type: List[str]
|
||||
source_lines = [] # type: List[str]
|
||||
failure_lines = [] # type: List[str]
|
||||
indents: List[str] = []
|
||||
source_lines: List[str] = []
|
||||
failure_lines: List[str] = []
|
||||
for index, line in enumerate(self.lines):
|
||||
is_failure_line = line.startswith(fail_marker)
|
||||
if is_failure_line:
|
||||
@@ -1111,7 +1132,7 @@ class ReprEntry(TerminalRepr):
|
||||
)
|
||||
|
||||
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
@attr.s(eq=False)
|
||||
class ReprFileLocation(TerminalRepr):
|
||||
path = attr.ib(type=str, converter=str)
|
||||
lineno = attr.ib(type=int)
|
||||
@@ -1125,10 +1146,10 @@ class ReprFileLocation(TerminalRepr):
|
||||
if i != -1:
|
||||
msg = msg[:i]
|
||||
tw.write(self.path, bold=True, red=True)
|
||||
tw.line(":{}: {}".format(self.lineno, msg))
|
||||
tw.line(f":{self.lineno}: {msg}")
|
||||
|
||||
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
@attr.s(eq=False)
|
||||
class ReprLocals(TerminalRepr):
|
||||
lines = attr.ib(type=Sequence[str])
|
||||
|
||||
@@ -1137,7 +1158,7 @@ class ReprLocals(TerminalRepr):
|
||||
tw.line(indent + line)
|
||||
|
||||
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
@attr.s(eq=False)
|
||||
class ReprFuncArgs(TerminalRepr):
|
||||
args = attr.ib(type=Sequence[Tuple[str, object]])
|
||||
|
||||
@@ -1145,7 +1166,7 @@ class ReprFuncArgs(TerminalRepr):
|
||||
if self.args:
|
||||
linesofar = ""
|
||||
for name, value in self.args:
|
||||
ns = "{} = {}".format(name, value)
|
||||
ns = f"{name} = {value}"
|
||||
if len(ns) + len(linesofar) + 2 > tw.fullwidth:
|
||||
if linesofar:
|
||||
tw.line(linesofar)
|
||||
@@ -1175,7 +1196,7 @@ def getfslineno(obj: object) -> Tuple[Union[str, py.path.local], int]:
|
||||
obj = obj.place_as # type: ignore[attr-defined]
|
||||
|
||||
try:
|
||||
code = Code(obj)
|
||||
code = Code.from_function(obj)
|
||||
except TypeError:
|
||||
try:
|
||||
fn = inspect.getsourcefile(obj) or inspect.getfile(obj) # type: ignore[arg-type]
|
||||
|
||||
@@ -2,17 +2,17 @@ import ast
|
||||
import inspect
|
||||
import textwrap
|
||||
import tokenize
|
||||
import types
|
||||
import warnings
|
||||
from bisect import bisect_right
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import overload
|
||||
from typing import Tuple
|
||||
from typing import Union
|
||||
|
||||
from _pytest.compat import overload
|
||||
|
||||
|
||||
class Source:
|
||||
"""An immutable object holding a source code fragment.
|
||||
@@ -22,7 +22,7 @@ class Source:
|
||||
|
||||
def __init__(self, obj: object = None) -> None:
|
||||
if not obj:
|
||||
self.lines = [] # type: List[str]
|
||||
self.lines: List[str] = []
|
||||
elif isinstance(obj, Source):
|
||||
self.lines = obj.lines
|
||||
elif isinstance(obj, (tuple, list)):
|
||||
@@ -30,8 +30,11 @@ class Source:
|
||||
elif isinstance(obj, str):
|
||||
self.lines = deindent(obj.split("\n"))
|
||||
else:
|
||||
rawcode = getrawcode(obj)
|
||||
src = inspect.getsource(rawcode)
|
||||
try:
|
||||
rawcode = getrawcode(obj)
|
||||
src = inspect.getsource(rawcode)
|
||||
except TypeError:
|
||||
src = inspect.getsource(obj) # type: ignore[arg-type]
|
||||
self.lines = deindent(src.split("\n"))
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
@@ -46,11 +49,11 @@ class Source:
|
||||
def __getitem__(self, key: int) -> str:
|
||||
...
|
||||
|
||||
@overload # noqa: F811
|
||||
def __getitem__(self, key: slice) -> "Source": # noqa: F811
|
||||
@overload
|
||||
def __getitem__(self, key: slice) -> "Source":
|
||||
...
|
||||
|
||||
def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]: # noqa: F811
|
||||
def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]:
|
||||
if isinstance(key, int):
|
||||
return self.lines[key]
|
||||
else:
|
||||
@@ -123,19 +126,17 @@ def findsource(obj) -> Tuple[Optional[Source], int]:
|
||||
return source, lineno
|
||||
|
||||
|
||||
def getrawcode(obj, trycall: bool = True):
|
||||
def getrawcode(obj: object, trycall: bool = True) -> types.CodeType:
|
||||
"""Return code object for given function."""
|
||||
try:
|
||||
return obj.__code__
|
||||
return obj.__code__ # type: ignore[attr-defined,no-any-return]
|
||||
except AttributeError:
|
||||
obj = getattr(obj, "f_code", obj)
|
||||
obj = getattr(obj, "__code__", obj)
|
||||
if trycall and not hasattr(obj, "co_firstlineno"):
|
||||
if hasattr(obj, "__call__") and not inspect.isclass(obj):
|
||||
x = getrawcode(obj.__call__, trycall=False)
|
||||
if hasattr(x, "co_firstlineno"):
|
||||
return x
|
||||
return obj
|
||||
pass
|
||||
if trycall:
|
||||
call = getattr(obj, "__call__", None)
|
||||
if call and not isinstance(obj, type):
|
||||
return getrawcode(call, trycall=False)
|
||||
raise TypeError(f"could not get code object for {obj!r}")
|
||||
|
||||
|
||||
def deindent(lines: Iterable[str]) -> List[str]:
|
||||
@@ -145,12 +146,12 @@ def deindent(lines: Iterable[str]) -> List[str]:
|
||||
def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[int]]:
|
||||
# Flatten all statements and except handlers into one lineno-list.
|
||||
# AST's line numbers start indexing at 1.
|
||||
values = [] # type: List[int]
|
||||
values: List[int] = []
|
||||
for x in ast.walk(node):
|
||||
if isinstance(x, (ast.stmt, ast.ExceptHandler)):
|
||||
values.append(x.lineno - 1)
|
||||
for name in ("finalbody", "orelse"):
|
||||
val = getattr(x, name, None) # type: Optional[List[ast.stmt]]
|
||||
val: Optional[List[ast.stmt]] = getattr(x, name, None)
|
||||
if val:
|
||||
# Treat the finally/orelse part as its own statement.
|
||||
values.append(val[0].lineno - 1 - 1)
|
||||
|
||||
@@ -122,7 +122,7 @@ def _pformat_dispatch(
|
||||
width: int = 80,
|
||||
depth: Optional[int] = None,
|
||||
*,
|
||||
compact: bool = False
|
||||
compact: bool = False,
|
||||
) -> str:
|
||||
return AlwaysDispatchingPrettyPrinter(
|
||||
indent=indent, width=width, depth=depth, compact=compact
|
||||
|
||||
@@ -76,7 +76,7 @@ class TerminalWriter:
|
||||
self._file = file
|
||||
self.hasmarkup = should_do_markup(file)
|
||||
self._current_line = ""
|
||||
self._terminal_width = None # type: Optional[int]
|
||||
self._terminal_width: Optional[int] = None
|
||||
self.code_highlight = True
|
||||
|
||||
@property
|
||||
@@ -97,7 +97,7 @@ class TerminalWriter:
|
||||
def markup(self, text: str, **markup: bool) -> str:
|
||||
for name in markup:
|
||||
if name not in self._esctable:
|
||||
raise ValueError("unknown markup: {!r}".format(name))
|
||||
raise ValueError(f"unknown markup: {name!r}")
|
||||
if self.hasmarkup:
|
||||
esc = [self._esctable[name] for name, on in markup.items() if on]
|
||||
if esc:
|
||||
@@ -109,7 +109,7 @@ class TerminalWriter:
|
||||
sepchar: str,
|
||||
title: Optional[str] = None,
|
||||
fullwidth: Optional[int] = None,
|
||||
**markup: bool
|
||||
**markup: bool,
|
||||
) -> None:
|
||||
if fullwidth is None:
|
||||
fullwidth = self.fullwidth
|
||||
@@ -128,7 +128,7 @@ class TerminalWriter:
|
||||
# N <= (fullwidth - len(title) - 2) // (2*len(sepchar))
|
||||
N = max((fullwidth - len(title) - 2) // (2 * len(sepchar)), 1)
|
||||
fill = sepchar * N
|
||||
line = "{} {} {}".format(fill, title, fill)
|
||||
line = f"{fill} {title} {fill}"
|
||||
else:
|
||||
# we want len(sepchar)*N <= fullwidth
|
||||
# i.e. N <= fullwidth // len(sepchar)
|
||||
@@ -204,7 +204,7 @@ class TerminalWriter:
|
||||
except ImportError:
|
||||
return source
|
||||
else:
|
||||
highlighted = highlight(
|
||||
highlighted: str = highlight(
|
||||
source, PythonLexer(), TerminalFormatter(bg="dark")
|
||||
) # type: str
|
||||
)
|
||||
return highlighted
|
||||
|
||||
@@ -4,12 +4,12 @@ from typing import Any
|
||||
from typing import Generator
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from _pytest.assertion import rewrite
|
||||
from _pytest.assertion import truncate
|
||||
from _pytest.assertion import util
|
||||
from _pytest.assertion.rewrite import assertstate_key
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.config.argparsing import Parser
|
||||
@@ -83,7 +83,7 @@ class AssertionState:
|
||||
def __init__(self, config: Config, mode) -> None:
|
||||
self.mode = mode
|
||||
self.trace = config.trace.root.get("assertion")
|
||||
self.hook = None # type: Optional[rewrite.AssertionRewritingHook]
|
||||
self.hook: Optional[rewrite.AssertionRewritingHook] = None
|
||||
|
||||
|
||||
def install_importhook(config: Config) -> rewrite.AssertionRewritingHook:
|
||||
|
||||
@@ -13,6 +13,8 @@ import struct
|
||||
import sys
|
||||
import tokenize
|
||||
import types
|
||||
from pathlib import Path
|
||||
from pathlib import PurePath
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import IO
|
||||
@@ -22,6 +24,7 @@ from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
import py
|
||||
@@ -32,24 +35,20 @@ from _pytest.assertion import util
|
||||
from _pytest.assertion.util import ( # noqa: F401
|
||||
format_explanation as _format_explanation,
|
||||
)
|
||||
from _pytest.compat import fspath
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.config import Config
|
||||
from _pytest.main import Session
|
||||
from _pytest.pathlib import fnmatch_ex
|
||||
from _pytest.pathlib import Path
|
||||
from _pytest.pathlib import PurePath
|
||||
from _pytest.store import StoreKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.assertion import AssertionState # noqa: F401
|
||||
from _pytest.assertion import AssertionState
|
||||
|
||||
|
||||
assertstate_key = StoreKey["AssertionState"]()
|
||||
|
||||
|
||||
# pytest caches rewritten pycs in pycache dirs
|
||||
PYTEST_TAG = "{}-pytest-{}".format(sys.implementation.cache_tag, version)
|
||||
PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}"
|
||||
PYC_EXT = ".py" + (__debug__ and "c" or "o")
|
||||
PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
|
||||
|
||||
@@ -63,14 +62,14 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
|
||||
self.fnpats = config.getini("python_files")
|
||||
except ValueError:
|
||||
self.fnpats = ["test_*.py", "*_test.py"]
|
||||
self.session = None # type: Optional[Session]
|
||||
self._rewritten_names = set() # type: Set[str]
|
||||
self._must_rewrite = set() # type: Set[str]
|
||||
self.session: Optional[Session] = None
|
||||
self._rewritten_names: Set[str] = set()
|
||||
self._must_rewrite: Set[str] = set()
|
||||
# flag to guard against trying to rewrite a pyc file while we are already writing another pyc file,
|
||||
# which might result in infinite recursion (#3506)
|
||||
self._writing_pyc = False
|
||||
self._basenames_to_check_rewrite = {"conftest"}
|
||||
self._marked_for_rewrite_cache = {} # type: Dict[str, bool]
|
||||
self._marked_for_rewrite_cache: Dict[str, bool] = {}
|
||||
self._session_paths_checked = False
|
||||
|
||||
def set_session(self, session: Optional[Session]) -> None:
|
||||
@@ -100,7 +99,7 @@ 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.5 - python3.6: `namespace`
|
||||
# python3.6: `namespace`
|
||||
# python3.7+: `None`
|
||||
or spec.origin == "namespace"
|
||||
or spec.origin is None
|
||||
@@ -150,7 +149,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
|
||||
ok = try_makedirs(cache_dir)
|
||||
if not ok:
|
||||
write = False
|
||||
state.trace("read only directory: {}".format(cache_dir))
|
||||
state.trace(f"read only directory: {cache_dir}")
|
||||
|
||||
cache_name = fn.name[:-3] + PYC_TAIL
|
||||
pyc = cache_dir / cache_name
|
||||
@@ -158,7 +157,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
|
||||
# to check for a cached pyc. This may not be optimal...
|
||||
co = _read_pyc(fn, pyc, state.trace)
|
||||
if co is None:
|
||||
state.trace("rewriting {!r}".format(fn))
|
||||
state.trace(f"rewriting {fn!r}")
|
||||
source_stat, co = _rewrite_test(fn, self.config)
|
||||
if write:
|
||||
self._writing_pyc = True
|
||||
@@ -167,7 +166,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
|
||||
finally:
|
||||
self._writing_pyc = False
|
||||
else:
|
||||
state.trace("found cached rewritten pyc for {}".format(fn))
|
||||
state.trace(f"found cached rewritten pyc for {fn}")
|
||||
exec(co, module.__dict__)
|
||||
|
||||
def _early_rewrite_bailout(self, name: str, state: "AssertionState") -> bool:
|
||||
@@ -206,20 +205,18 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
|
||||
if self._is_marked_for_rewrite(name, state):
|
||||
return False
|
||||
|
||||
state.trace("early skip of rewriting module: {}".format(name))
|
||||
state.trace(f"early skip of rewriting module: {name}")
|
||||
return True
|
||||
|
||||
def _should_rewrite(self, name: str, fn: str, state: "AssertionState") -> bool:
|
||||
# always rewrite conftest files
|
||||
if os.path.basename(fn) == "conftest.py":
|
||||
state.trace("rewriting conftest file: {!r}".format(fn))
|
||||
state.trace(f"rewriting conftest file: {fn!r}")
|
||||
return True
|
||||
|
||||
if self.session is not None:
|
||||
if self.session.isinitpath(py.path.local(fn)):
|
||||
state.trace(
|
||||
"matched test file (was specified on cmdline): {!r}".format(fn)
|
||||
)
|
||||
state.trace(f"matched test file (was specified on cmdline): {fn!r}")
|
||||
return True
|
||||
|
||||
# modules not passed explicitly on the command line are only
|
||||
@@ -227,7 +224,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
|
||||
fn_path = PurePath(fn)
|
||||
for pat in self.fnpats:
|
||||
if fnmatch_ex(pat, fn_path):
|
||||
state.trace("matched test file {!r}".format(fn))
|
||||
state.trace(f"matched test file {fn!r}")
|
||||
return True
|
||||
|
||||
return self._is_marked_for_rewrite(name, state)
|
||||
@@ -238,9 +235,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
|
||||
except KeyError:
|
||||
for marked in self._must_rewrite:
|
||||
if name == marked or name.startswith(marked + "."):
|
||||
state.trace(
|
||||
"matched marked file {!r} (from {!r})".format(name, marked)
|
||||
)
|
||||
state.trace(f"matched marked file {name!r} (from {marked!r})")
|
||||
self._marked_for_rewrite_cache[name] = True
|
||||
return True
|
||||
|
||||
@@ -286,12 +281,16 @@ def _write_pyc_fp(
|
||||
) -> None:
|
||||
# Technically, we don't have to have the same pyc format as
|
||||
# (C)Python, since these "pycs" should never be seen by builtin
|
||||
# import. However, there's little reason deviate.
|
||||
# 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)
|
||||
# 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
|
||||
# "<LL" stands for 2 unsigned longs, little-ending
|
||||
# "<LL" stands for 2 unsigned longs, little-endian.
|
||||
fp.write(struct.pack("<LL", mtime, size))
|
||||
fp.write(marshal.dumps(co))
|
||||
|
||||
@@ -306,10 +305,10 @@ if sys.platform == "win32":
|
||||
pyc: Path,
|
||||
) -> bool:
|
||||
try:
|
||||
with atomic_write(fspath(pyc), mode="wb", overwrite=True) as fp:
|
||||
with atomic_write(os.fspath(pyc), mode="wb", overwrite=True) as fp:
|
||||
_write_pyc_fp(fp, source_stat, co)
|
||||
except OSError as e:
|
||||
state.trace("error writing pyc file at {}: {}".format(pyc, e))
|
||||
state.trace(f"error writing pyc file at {pyc}: {e}")
|
||||
# we ignore any failure to write the cache file
|
||||
# there are many reasons, permission-denied, pycache dir being a
|
||||
# file etc.
|
||||
@@ -325,20 +324,18 @@ else:
|
||||
source_stat: os.stat_result,
|
||||
pyc: Path,
|
||||
) -> bool:
|
||||
proc_pyc = "{}.{}".format(pyc, os.getpid())
|
||||
proc_pyc = f"{pyc}.{os.getpid()}"
|
||||
try:
|
||||
fp = open(proc_pyc, "wb")
|
||||
except OSError as e:
|
||||
state.trace(
|
||||
"error writing pyc file at {}: errno={}".format(proc_pyc, e.errno)
|
||||
)
|
||||
state.trace(f"error writing pyc file at {proc_pyc}: errno={e.errno}")
|
||||
return False
|
||||
|
||||
try:
|
||||
_write_pyc_fp(fp, source_stat, co)
|
||||
os.rename(proc_pyc, fspath(pyc))
|
||||
os.rename(proc_pyc, os.fspath(pyc))
|
||||
except OSError as e:
|
||||
state.trace("error writing pyc file at {}: {}".format(pyc, e))
|
||||
state.trace(f"error writing pyc file at {pyc}: {e}")
|
||||
# we ignore any failure to write the cache file
|
||||
# there are many reasons, permission-denied, pycache dir being a
|
||||
# file etc.
|
||||
@@ -350,7 +347,7 @@ else:
|
||||
|
||||
def _rewrite_test(fn: Path, config: Config) -> Tuple[os.stat_result, types.CodeType]:
|
||||
"""Read and rewrite *fn* and return the code object."""
|
||||
fn_ = fspath(fn)
|
||||
fn_ = os.fspath(fn)
|
||||
stat = os.stat(fn_)
|
||||
with open(fn_, "rb") as f:
|
||||
source = f.read()
|
||||
@@ -368,30 +365,42 @@ def _read_pyc(
|
||||
Return rewritten code if successful or None if not.
|
||||
"""
|
||||
try:
|
||||
fp = open(fspath(pyc), "rb")
|
||||
fp = open(os.fspath(pyc), "rb")
|
||||
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(fspath(source))
|
||||
stat_result = os.stat(os.fspath(source))
|
||||
mtime = int(stat_result.st_mtime)
|
||||
size = stat_result.st_size
|
||||
data = fp.read(12)
|
||||
data = fp.read(16 if has_flags else 12)
|
||||
except OSError as e:
|
||||
trace("_read_pyc({}): OSError {}".format(source, e))
|
||||
trace(f"_read_pyc({source}): OSError {e}")
|
||||
return None
|
||||
# Check for invalid or out of date pyc file.
|
||||
if (
|
||||
len(data) != 12
|
||||
or data[:4] != importlib.util.MAGIC_NUMBER
|
||||
or struct.unpack("<LL", data[4:]) != (mtime & 0xFFFFFFFF, size & 0xFFFFFFFF)
|
||||
):
|
||||
trace("_read_pyc(%s): invalid or out of date pyc" % source)
|
||||
if len(data) != (16 if has_flags else 12):
|
||||
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":
|
||||
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]
|
||||
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]
|
||||
if int.from_bytes(size_data, "little") != size & 0xFFFFFFFF:
|
||||
trace("_read_pyc(%s): invalid pyc (incorrect size)" % source)
|
||||
return None
|
||||
try:
|
||||
co = marshal.load(fp)
|
||||
except Exception as e:
|
||||
trace("_read_pyc({}): marshal.load error {}".format(source, e))
|
||||
trace(f"_read_pyc({source}): marshal.load error {e}")
|
||||
return None
|
||||
if not isinstance(co, types.CodeType):
|
||||
trace("_read_pyc(%s): not a code object" % source)
|
||||
@@ -536,12 +545,12 @@ def set_location(node, lineno, col_offset):
|
||||
|
||||
def _get_assertion_exprs(src: bytes) -> Dict[int, str]:
|
||||
"""Return a mapping from {lineno: "assertion test expression"}."""
|
||||
ret = {} # type: Dict[int, str]
|
||||
ret: Dict[int, str] = {}
|
||||
|
||||
depth = 0
|
||||
lines = [] # type: List[str]
|
||||
assert_lineno = None # type: Optional[int]
|
||||
seen_lines = set() # type: Set[int]
|
||||
lines: List[str] = []
|
||||
assert_lineno: Optional[int] = None
|
||||
seen_lines: Set[int] = set()
|
||||
|
||||
def _write_and_reset() -> None:
|
||||
nonlocal depth, lines, assert_lineno, seen_lines
|
||||
@@ -706,12 +715,12 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
]
|
||||
mod.body[pos:pos] = imports
|
||||
# Collect asserts.
|
||||
nodes = [mod] # type: List[ast.AST]
|
||||
nodes: List[ast.AST] = [mod]
|
||||
while nodes:
|
||||
node = nodes.pop()
|
||||
for name, field in ast.iter_fields(node):
|
||||
if isinstance(field, list):
|
||||
new = [] # type: List[ast.AST]
|
||||
new: List[ast.AST] = []
|
||||
for i, child in enumerate(field):
|
||||
if isinstance(child, ast.Assert):
|
||||
# Transform assert.
|
||||
@@ -783,7 +792,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
to format a string of %-formatted values as added by
|
||||
.explanation_param().
|
||||
"""
|
||||
self.explanation_specifiers = {} # type: Dict[str, ast.expr]
|
||||
self.explanation_specifiers: Dict[str, ast.expr] = {}
|
||||
self.stack.append(self.explanation_specifiers)
|
||||
|
||||
def pop_format_context(self, expl_expr: ast.expr) -> ast.Name:
|
||||
@@ -831,19 +840,19 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
"assertion is always true, perhaps remove parentheses?"
|
||||
),
|
||||
category=None,
|
||||
filename=fspath(self.module_path),
|
||||
filename=os.fspath(self.module_path),
|
||||
lineno=assert_.lineno,
|
||||
)
|
||||
|
||||
self.statements = [] # type: List[ast.stmt]
|
||||
self.variables = [] # type: List[str]
|
||||
self.statements: List[ast.stmt] = []
|
||||
self.variables: List[str] = []
|
||||
self.variable_counter = itertools.count()
|
||||
|
||||
if self.enable_assertion_pass_hook:
|
||||
self.format_variables = [] # type: List[str]
|
||||
self.format_variables: List[str] = []
|
||||
|
||||
self.stack = [] # type: List[Dict[str, ast.expr]]
|
||||
self.expl_stmts = [] # type: List[ast.stmt]
|
||||
self.stack: List[Dict[str, ast.expr]] = []
|
||||
self.expl_stmts: List[ast.stmt] = []
|
||||
self.push_format_context()
|
||||
# Rewrite assert into a bunch of statements.
|
||||
top_condition, explanation = self.visit(assert_.test)
|
||||
@@ -950,7 +959,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
# Process each operand, short-circuiting if needed.
|
||||
for i, v in enumerate(boolop.values):
|
||||
if i:
|
||||
fail_inner = [] # type: List[ast.stmt]
|
||||
fail_inner: List[ast.stmt] = []
|
||||
# cond is set in a prior loop iteration below
|
||||
self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa
|
||||
self.expl_stmts = fail_inner
|
||||
@@ -961,10 +970,10 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
call = ast.Call(app, [expl_format], [])
|
||||
self.expl_stmts.append(ast.Expr(call))
|
||||
if i < levels:
|
||||
cond = res # type: ast.expr
|
||||
cond: ast.expr = res
|
||||
if is_or:
|
||||
cond = ast.UnaryOp(ast.Not(), cond)
|
||||
inner = [] # type: List[ast.stmt]
|
||||
inner: List[ast.stmt] = []
|
||||
self.statements.append(ast.If(cond, inner, []))
|
||||
self.statements = body = inner
|
||||
self.statements = save
|
||||
@@ -983,7 +992,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
symbol = BINOP_MAP[binop.op.__class__]
|
||||
left_expr, left_expl = self.visit(binop.left)
|
||||
right_expr, right_expl = self.visit(binop.right)
|
||||
explanation = "({} {} {})".format(left_expl, symbol, right_expl)
|
||||
explanation = f"({left_expl} {symbol} {right_expl})"
|
||||
res = self.assign(ast.BinOp(left_expr, binop.op, right_expr))
|
||||
return res, explanation
|
||||
|
||||
@@ -1008,11 +1017,11 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
new_call = ast.Call(new_func, new_args, new_kwargs)
|
||||
res = self.assign(new_call)
|
||||
res_expl = self.explanation_param(self.display(res))
|
||||
outer_expl = "{}\n{{{} = {}\n}}".format(res_expl, res_expl, expl)
|
||||
outer_expl = f"{res_expl}\n{{{res_expl} = {expl}\n}}"
|
||||
return res, outer_expl
|
||||
|
||||
def visit_Starred(self, starred: ast.Starred) -> Tuple[ast.Starred, str]:
|
||||
# From Python 3.5, a Starred node can appear in a function call.
|
||||
# A Starred node can appear in a function call.
|
||||
res, expl = self.visit(starred.value)
|
||||
new_starred = ast.Starred(res, starred.ctx)
|
||||
return new_starred, "*" + expl
|
||||
@@ -1031,7 +1040,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
self.push_format_context()
|
||||
left_res, left_expl = self.visit(comp.left)
|
||||
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
|
||||
left_expl = "({})".format(left_expl)
|
||||
left_expl = f"({left_expl})"
|
||||
res_variables = [self.variable() for i in range(len(comp.ops))]
|
||||
load_names = [ast.Name(v, ast.Load()) for v in res_variables]
|
||||
store_names = [ast.Name(v, ast.Store()) for v in res_variables]
|
||||
@@ -1042,11 +1051,11 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
for i, op, next_operand in it:
|
||||
next_res, next_expl = self.visit(next_operand)
|
||||
if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
|
||||
next_expl = "({})".format(next_expl)
|
||||
next_expl = f"({next_expl})"
|
||||
results.append(next_res)
|
||||
sym = BINOP_MAP[op.__class__]
|
||||
syms.append(ast.Str(sym))
|
||||
expl = "{} {} {}".format(left_expl, sym, next_expl)
|
||||
expl = f"{left_expl} {sym} {next_expl}"
|
||||
expls.append(ast.Str(expl))
|
||||
res_expr = ast.Compare(left_res, [op], [next_res])
|
||||
self.statements.append(ast.Assign([store_names[i]], res_expr))
|
||||
@@ -1060,7 +1069,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
ast.Tuple(results, ast.Load()),
|
||||
)
|
||||
if len(comp.ops) > 1:
|
||||
res = ast.BoolOp(ast.And(), load_names) # type: ast.expr
|
||||
res: ast.expr = ast.BoolOp(ast.And(), load_names)
|
||||
else:
|
||||
res = load_names[0]
|
||||
return res, self.explanation_param(self.pop_format_context(expl_call))
|
||||
@@ -1072,7 +1081,7 @@ def try_makedirs(cache_dir: Path) -> bool:
|
||||
Returns True if successful or if it already exists.
|
||||
"""
|
||||
try:
|
||||
os.makedirs(fspath(cache_dir), exist_ok=True)
|
||||
os.makedirs(os.fspath(cache_dir), exist_ok=True)
|
||||
except (FileNotFoundError, NotADirectoryError, FileExistsError):
|
||||
# One of the path components was not a directory:
|
||||
# - we're in a zip file
|
||||
|
||||
@@ -70,10 +70,10 @@ def _truncate_explanation(
|
||||
truncated_line_count += 1 # Account for the part-truncated final line
|
||||
msg = "...Full output truncated"
|
||||
if truncated_line_count == 1:
|
||||
msg += " ({} line hidden)".format(truncated_line_count)
|
||||
msg += f" ({truncated_line_count} line hidden)"
|
||||
else:
|
||||
msg += " ({} lines hidden)".format(truncated_line_count)
|
||||
msg += ", {}".format(USAGE_MSG)
|
||||
msg += f" ({truncated_line_count} lines hidden)"
|
||||
msg += f", {USAGE_MSG}"
|
||||
truncated_explanation.extend(["", str(msg)])
|
||||
return truncated_explanation
|
||||
|
||||
|
||||
@@ -9,24 +9,22 @@ from typing import List
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
|
||||
import _pytest._code
|
||||
from _pytest import outcomes
|
||||
from _pytest._io.saferepr import _pformat_dispatch
|
||||
from _pytest._io.saferepr import safeformat
|
||||
from _pytest._io.saferepr import saferepr
|
||||
from _pytest.compat import ATTRS_EQ_FIELD
|
||||
|
||||
# The _reprcompare attribute on the util module is used by the new assertion
|
||||
# interpretation code and assertion rewriter to detect this plugin was
|
||||
# loaded and in turn call the hooks defined here as part of the
|
||||
# DebugInterpreter.
|
||||
_reprcompare = None # type: Optional[Callable[[str, object, object], Optional[str]]]
|
||||
_reprcompare: Optional[Callable[[str, object, object], Optional[str]]] = None
|
||||
|
||||
# Works similarly as _reprcompare attribute. Is populated with the hook call
|
||||
# when pytest_runtest_setup is called.
|
||||
_assertion_pass = None # type: Optional[Callable[[int, str, str], None]]
|
||||
_assertion_pass: Optional[Callable[[int, str, str], None]] = None
|
||||
|
||||
|
||||
def format_explanation(explanation: str) -> str:
|
||||
@@ -112,6 +110,10 @@ def isset(x: Any) -> bool:
|
||||
return isinstance(x, (set, frozenset))
|
||||
|
||||
|
||||
def isnamedtuple(obj: Any) -> bool:
|
||||
return isinstance(obj, tuple) and getattr(obj, "_fields", None) is not None
|
||||
|
||||
|
||||
def isdatacls(obj: Any) -> bool:
|
||||
return getattr(obj, "__dataclass_fields__", None) is not None
|
||||
|
||||
@@ -143,7 +145,7 @@ def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[
|
||||
left_repr = saferepr(left, maxsize=maxsize)
|
||||
right_repr = saferepr(right, maxsize=maxsize)
|
||||
|
||||
summary = "{} {} {}".format(left_repr, op, right_repr)
|
||||
summary = f"{left_repr} {op} {right_repr}"
|
||||
|
||||
explanation = None
|
||||
try:
|
||||
@@ -173,15 +175,20 @@ def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]:
|
||||
if istext(left) and istext(right):
|
||||
explanation = _diff_text(left, right, verbose)
|
||||
else:
|
||||
if issequence(left) and issequence(right):
|
||||
if type(left) == type(right) and (
|
||||
isdatacls(left) or isattrs(left) or isnamedtuple(left)
|
||||
):
|
||||
# Note: unlike dataclasses/attrs, namedtuples compare only the
|
||||
# field values, not the type or field names. But this branch
|
||||
# intentionally only handles the same-type case, which was often
|
||||
# used in older code bases before dataclasses/attrs were available.
|
||||
explanation = _compare_eq_cls(left, right, verbose)
|
||||
elif issequence(left) and issequence(right):
|
||||
explanation = _compare_eq_sequence(left, right, verbose)
|
||||
elif isset(left) and isset(right):
|
||||
explanation = _compare_eq_set(left, right, verbose)
|
||||
elif isdict(left) and isdict(right):
|
||||
explanation = _compare_eq_dict(left, right, verbose)
|
||||
elif type(left) == type(right) and (isdatacls(left) or isattrs(left)):
|
||||
type_fn = (isdatacls, isattrs)
|
||||
explanation = _compare_eq_cls(left, right, verbose, type_fn)
|
||||
elif verbose > 0:
|
||||
explanation = _compare_eq_verbose(left, right)
|
||||
if isiterable(left) and isiterable(right):
|
||||
@@ -198,7 +205,7 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
|
||||
"""
|
||||
from difflib import ndiff
|
||||
|
||||
explanation = [] # type: List[str]
|
||||
explanation: List[str] = []
|
||||
|
||||
if verbose < 1:
|
||||
i = 0 # just in case left or right has zero length
|
||||
@@ -243,7 +250,7 @@ def _compare_eq_verbose(left: Any, right: Any) -> List[str]:
|
||||
left_lines = repr(left).splitlines(keepends)
|
||||
right_lines = repr(right).splitlines(keepends)
|
||||
|
||||
explanation = [] # type: List[str]
|
||||
explanation: List[str] = []
|
||||
explanation += ["+" + line for line in left_lines]
|
||||
explanation += ["-" + line for line in right_lines]
|
||||
|
||||
@@ -297,7 +304,7 @@ def _compare_eq_sequence(
|
||||
left: Sequence[Any], right: Sequence[Any], verbose: int = 0
|
||||
) -> List[str]:
|
||||
comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes)
|
||||
explanation = [] # type: List[str]
|
||||
explanation: List[str] = []
|
||||
len_left = len(left)
|
||||
len_right = len(right)
|
||||
for i in range(min(len_left, len_right)):
|
||||
@@ -317,9 +324,7 @@ def _compare_eq_sequence(
|
||||
left_value = left[i]
|
||||
right_value = right[i]
|
||||
|
||||
explanation += [
|
||||
"At index {} diff: {!r} != {!r}".format(i, left_value, right_value)
|
||||
]
|
||||
explanation += [f"At index {i} diff: {left_value!r} != {right_value!r}"]
|
||||
break
|
||||
|
||||
if comparing_bytes:
|
||||
@@ -339,9 +344,7 @@ def _compare_eq_sequence(
|
||||
extra = saferepr(right[len_left])
|
||||
|
||||
if len_diff == 1:
|
||||
explanation += [
|
||||
"{} contains one more item: {}".format(dir_with_more, extra)
|
||||
]
|
||||
explanation += [f"{dir_with_more} contains one more item: {extra}"]
|
||||
else:
|
||||
explanation += [
|
||||
"%s contains %d more items, first extra item: %s"
|
||||
@@ -370,7 +373,7 @@ def _compare_eq_set(
|
||||
def _compare_eq_dict(
|
||||
left: Mapping[Any, Any], right: Mapping[Any, Any], verbose: int = 0
|
||||
) -> List[str]:
|
||||
explanation = [] # type: List[str]
|
||||
explanation: List[str] = []
|
||||
set_left = set(left)
|
||||
set_right = set(right)
|
||||
common = set_left.intersection(set_right)
|
||||
@@ -408,21 +411,17 @@ def _compare_eq_dict(
|
||||
return explanation
|
||||
|
||||
|
||||
def _compare_eq_cls(
|
||||
left: Any,
|
||||
right: Any,
|
||||
verbose: int,
|
||||
type_fns: Tuple[Callable[[Any], bool], Callable[[Any], bool]],
|
||||
) -> List[str]:
|
||||
isdatacls, isattrs = type_fns
|
||||
def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]:
|
||||
if isdatacls(left):
|
||||
all_fields = left.__dataclass_fields__
|
||||
fields_to_check = [field for field, info in all_fields.items() if info.compare]
|
||||
elif isattrs(left):
|
||||
all_fields = left.__attrs_attrs__
|
||||
fields_to_check = [
|
||||
field.name for field in all_fields if getattr(field, ATTRS_EQ_FIELD)
|
||||
]
|
||||
fields_to_check = [field.name for field in all_fields if getattr(field, "eq")]
|
||||
elif isnamedtuple(left):
|
||||
fields_to_check = left._fields
|
||||
else:
|
||||
assert False
|
||||
|
||||
indent = " "
|
||||
same = []
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# pytest-cache version.
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
from typing import Generator
|
||||
from typing import Iterable
|
||||
@@ -14,21 +15,22 @@ from typing import Union
|
||||
import attr
|
||||
import py
|
||||
|
||||
import pytest
|
||||
from .pathlib import Path
|
||||
from .pathlib import resolve_from_str
|
||||
from .pathlib import rm_rf
|
||||
from .reports import CollectReport
|
||||
from _pytest import nodes
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import order_preserving_dict
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ExitCode
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.deprecated import check_ispytest
|
||||
from _pytest.fixtures import fixture
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
from _pytest.main import Session
|
||||
from _pytest.python import Module
|
||||
from _pytest.python import Package
|
||||
from _pytest.reports import TestReport
|
||||
|
||||
|
||||
@@ -52,7 +54,7 @@ Signature: 8a477f597d28d172789f06886806bc55
|
||||
|
||||
|
||||
@final
|
||||
@attr.s
|
||||
@attr.s(init=False)
|
||||
class Cache:
|
||||
_cachedir = attr.ib(type=Path, repr=False)
|
||||
_config = attr.ib(type=Config, repr=False)
|
||||
@@ -63,26 +65,52 @@ class Cache:
|
||||
# sub-directory under cache-dir for values created by "set"
|
||||
_CACHE_PREFIX_VALUES = "v"
|
||||
|
||||
@classmethod
|
||||
def for_config(cls, config: Config) -> "Cache":
|
||||
cachedir = cls.cache_dir_from_config(config)
|
||||
if config.getoption("cacheclear") and cachedir.is_dir():
|
||||
cls.clear_cache(cachedir)
|
||||
return cls(cachedir, config)
|
||||
def __init__(
|
||||
self, cachedir: Path, config: Config, *, _ispytest: bool = False
|
||||
) -> None:
|
||||
check_ispytest(_ispytest)
|
||||
self._cachedir = cachedir
|
||||
self._config = config
|
||||
|
||||
@classmethod
|
||||
def clear_cache(cls, cachedir: Path) -> None:
|
||||
"""Clear the sub-directories used to hold cached directories and values."""
|
||||
def for_config(cls, config: Config, *, _ispytest: bool = False) -> "Cache":
|
||||
"""Create the Cache instance for a Config.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
check_ispytest(_ispytest)
|
||||
cachedir = cls.cache_dir_from_config(config, _ispytest=True)
|
||||
if config.getoption("cacheclear") and cachedir.is_dir():
|
||||
cls.clear_cache(cachedir, _ispytest=True)
|
||||
return cls(cachedir, config, _ispytest=True)
|
||||
|
||||
@classmethod
|
||||
def clear_cache(cls, cachedir: Path, _ispytest: bool = False) -> None:
|
||||
"""Clear the sub-directories used to hold cached directories and values.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
check_ispytest(_ispytest)
|
||||
for prefix in (cls._CACHE_PREFIX_DIRS, cls._CACHE_PREFIX_VALUES):
|
||||
d = cachedir / prefix
|
||||
if d.is_dir():
|
||||
rm_rf(d)
|
||||
|
||||
@staticmethod
|
||||
def cache_dir_from_config(config: Config) -> Path:
|
||||
def cache_dir_from_config(config: Config, *, _ispytest: bool = False) -> Path:
|
||||
"""Get the path to the cache directory for a Config.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
check_ispytest(_ispytest)
|
||||
return resolve_from_str(config.getini("cache_dir"), config.rootpath)
|
||||
|
||||
def warn(self, fmt: str, **args: object) -> None:
|
||||
def warn(self, fmt: str, *, _ispytest: bool = False, **args: object) -> None:
|
||||
"""Issue a cache warning.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
check_ispytest(_ispytest)
|
||||
import warnings
|
||||
from _pytest.warning_types import PytestCacheWarning
|
||||
|
||||
@@ -151,7 +179,7 @@ class Cache:
|
||||
cache_dir_exists_already = self._cachedir.exists()
|
||||
path.parent.mkdir(exist_ok=True, parents=True)
|
||||
except OSError:
|
||||
self.warn("could not create cache path {path}", path=path)
|
||||
self.warn("could not create cache path {path}", path=path, _ispytest=True)
|
||||
return
|
||||
if not cache_dir_exists_already:
|
||||
self._ensure_supporting_files()
|
||||
@@ -159,7 +187,7 @@ class Cache:
|
||||
try:
|
||||
f = path.open("w")
|
||||
except OSError:
|
||||
self.warn("cache could not write path {path}", path=path)
|
||||
self.warn("cache could not write path {path}", path=path, _ispytest=True)
|
||||
else:
|
||||
with f:
|
||||
f.write(data)
|
||||
@@ -182,11 +210,11 @@ class LFPluginCollWrapper:
|
||||
self.lfplugin = lfplugin
|
||||
self._collected_at_least_one_failure = False
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_make_collect_report(self, collector: nodes.Collector):
|
||||
if isinstance(collector, Session):
|
||||
out = yield
|
||||
res = out.get_result() # type: CollectReport
|
||||
res: CollectReport = out.get_result()
|
||||
|
||||
# Sort any lf-paths to the beginning.
|
||||
lf_paths = self.lfplugin._last_failed_paths
|
||||
@@ -229,11 +257,14 @@ class LFPluginCollSkipfiles:
|
||||
def __init__(self, lfplugin: "LFPlugin") -> None:
|
||||
self.lfplugin = lfplugin
|
||||
|
||||
@pytest.hookimpl
|
||||
@hookimpl
|
||||
def pytest_make_collect_report(
|
||||
self, collector: nodes.Collector
|
||||
) -> Optional[CollectReport]:
|
||||
if isinstance(collector, Module):
|
||||
# Packages are Modules, but _last_failed_paths only contains
|
||||
# test-bearing paths and doesn't try to include the paths of their
|
||||
# packages, so don't filter them.
|
||||
if isinstance(collector, Module) and not isinstance(collector, Package):
|
||||
if Path(str(collector.fspath)) not in self.lfplugin._last_failed_paths:
|
||||
self.lfplugin._skipped_files += 1
|
||||
|
||||
@@ -251,11 +282,9 @@ class LFPlugin:
|
||||
active_keys = "lf", "failedfirst"
|
||||
self.active = any(config.getoption(key) for key in active_keys)
|
||||
assert config.cache
|
||||
self.lastfailed = config.cache.get(
|
||||
"cache/lastfailed", {}
|
||||
) # type: Dict[str, bool]
|
||||
self._previously_failed_count = None # type: Optional[int]
|
||||
self._report_status = None # type: Optional[str]
|
||||
self.lastfailed: Dict[str, bool] = config.cache.get("cache/lastfailed", {})
|
||||
self._previously_failed_count: Optional[int] = None
|
||||
self._report_status: Optional[str] = None
|
||||
self._skipped_files = 0 # count skipped files during collection due to --lf
|
||||
|
||||
if config.getoption("lf"):
|
||||
@@ -290,7 +319,7 @@ class LFPlugin:
|
||||
else:
|
||||
self.lastfailed[report.nodeid] = True
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||
@hookimpl(hookwrapper=True, tryfirst=True)
|
||||
def pytest_collection_modifyitems(
|
||||
self, config: Config, items: List[nodes.Item]
|
||||
) -> Generator[None, None, None]:
|
||||
@@ -362,15 +391,15 @@ class NFPlugin:
|
||||
assert config.cache is not None
|
||||
self.cached_nodeids = set(config.cache.get("cache/nodeids", []))
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||
@hookimpl(hookwrapper=True, tryfirst=True)
|
||||
def pytest_collection_modifyitems(
|
||||
self, items: List[nodes.Item]
|
||||
) -> Generator[None, None, None]:
|
||||
yield
|
||||
|
||||
if self.active:
|
||||
new_items = order_preserving_dict() # type: Dict[str, nodes.Item]
|
||||
other_items = order_preserving_dict() # type: Dict[str, nodes.Item]
|
||||
new_items: Dict[str, nodes.Item] = {}
|
||||
other_items: Dict[str, nodes.Item] = {}
|
||||
for item in items:
|
||||
if item.nodeid not in self.cached_nodeids:
|
||||
new_items[item.nodeid] = item
|
||||
@@ -385,7 +414,7 @@ class NFPlugin:
|
||||
self.cached_nodeids.update(item.nodeid for item in items)
|
||||
|
||||
def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]:
|
||||
return sorted(items, key=lambda item: item.fspath.mtime(), reverse=True)
|
||||
return sorted(items, key=lambda item: item.fspath.mtime(), reverse=True) # type: ignore[no-any-return]
|
||||
|
||||
def pytest_sessionfinish(self) -> None:
|
||||
config = self.config
|
||||
@@ -465,14 +494,14 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
|
||||
return None
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
@hookimpl(tryfirst=True)
|
||||
def pytest_configure(config: Config) -> None:
|
||||
config.cache = Cache.for_config(config)
|
||||
config.cache = Cache.for_config(config, _ispytest=True)
|
||||
config.pluginmanager.register(LFPlugin(config), "lfplugin")
|
||||
config.pluginmanager.register(NFPlugin(config), "nfplugin")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@fixture
|
||||
def cache(request: FixtureRequest) -> Cache:
|
||||
"""Return a cache object that can persist state between testing sessions.
|
||||
|
||||
@@ -500,7 +529,7 @@ def pytest_report_header(config: Config) -> Optional[str]:
|
||||
displaypath = cachedir.relative_to(config.rootpath)
|
||||
except ValueError:
|
||||
displaypath = cachedir
|
||||
return "cachedir: {}".format(displaypath)
|
||||
return f"cachedir: {displaypath}"
|
||||
return None
|
||||
|
||||
|
||||
@@ -542,5 +571,5 @@ def cacheshow(config: Config, session: Session) -> int:
|
||||
# print("%s/" % p.relto(basedir))
|
||||
if p.is_file():
|
||||
key = str(p.relative_to(basedir))
|
||||
tw.line("{} is a file of length {:d}".format(key, p.stat().st_size))
|
||||
tw.line(f"{key} is a file of length {p.stat().st_size:d}")
|
||||
return 0
|
||||
|
||||
@@ -14,15 +14,18 @@ from typing import Iterator
|
||||
from typing import Optional
|
||||
from typing import TextIO
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
import pytest
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.deprecated import check_ispytest
|
||||
from _pytest.fixtures import fixture
|
||||
from _pytest.fixtures import SubRequest
|
||||
from _pytest.nodes import Collector
|
||||
from _pytest.nodes import File
|
||||
from _pytest.nodes import Item
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -113,11 +116,7 @@ def _py36_windowsconsoleio_workaround(stream: TextIO) -> None:
|
||||
|
||||
See https://github.com/pytest-dev/py/issues/103.
|
||||
"""
|
||||
if (
|
||||
not sys.platform.startswith("win32")
|
||||
or sys.version_info[:2] < (3, 6)
|
||||
or hasattr(sys, "pypy_version_info")
|
||||
):
|
||||
if not sys.platform.startswith("win32") or hasattr(sys, "pypy_version_info"):
|
||||
return
|
||||
|
||||
# Bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666).
|
||||
@@ -149,7 +148,7 @@ def _py36_windowsconsoleio_workaround(stream: TextIO) -> None:
|
||||
sys.stderr = _reopen_stdio(sys.stderr, "wb")
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_load_initial_conftests(early_config: Config):
|
||||
ns = early_config.known_args_namespace
|
||||
if ns.capture == "fd":
|
||||
@@ -373,9 +372,7 @@ class FDCaptureBinary:
|
||||
# Further complications are the need to support suspend() and the
|
||||
# possibility of FD reuse (e.g. the tmpfile getting the very same
|
||||
# target FD). The following approach is robust, I believe.
|
||||
self.targetfd_invalid = os.open(
|
||||
os.devnull, os.O_RDWR
|
||||
) # type: Optional[int]
|
||||
self.targetfd_invalid: Optional[int] = os.open(os.devnull, os.O_RDWR)
|
||||
os.dup2(self.targetfd_invalid, targetfd)
|
||||
else:
|
||||
self.targetfd_invalid = None
|
||||
@@ -386,8 +383,7 @@ class FDCaptureBinary:
|
||||
self.syscapture = SysCapture(targetfd)
|
||||
else:
|
||||
self.tmpfile = EncodedFile(
|
||||
# TODO: Remove type ignore, fixed in next mypy release.
|
||||
TemporaryFile(buffering=0), # type: ignore[arg-type]
|
||||
TemporaryFile(buffering=0),
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
newline="",
|
||||
@@ -504,13 +500,11 @@ class FDCapture(FDCaptureBinary):
|
||||
class CaptureResult(Generic[AnyStr]):
|
||||
"""The result of :method:`CaptureFixture.readouterr`."""
|
||||
|
||||
# Can't use slots in Python<3.5.3 due to https://bugs.python.org/issue31272
|
||||
if sys.version_info >= (3, 5, 3):
|
||||
__slots__ = ("out", "err")
|
||||
__slots__ = ("out", "err")
|
||||
|
||||
def __init__(self, out: AnyStr, err: AnyStr) -> None:
|
||||
self.out = out # type: AnyStr
|
||||
self.err = err # type: AnyStr
|
||||
self.out: AnyStr = out
|
||||
self.err: AnyStr = err
|
||||
|
||||
def __len__(self) -> int:
|
||||
return 2
|
||||
@@ -548,7 +542,7 @@ class CaptureResult(Generic[AnyStr]):
|
||||
return tuple(self) < tuple(other)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "CaptureResult(out={!r}, err={!r})".format(self.out, self.err)
|
||||
return f"CaptureResult(out={self.out!r}, err={self.err!r})"
|
||||
|
||||
|
||||
class MultiCapture(Generic[AnyStr]):
|
||||
@@ -642,7 +636,7 @@ def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]:
|
||||
return MultiCapture(
|
||||
in_=None, out=SysCapture(1, tee=True), err=SysCapture(2, tee=True)
|
||||
)
|
||||
raise ValueError("unknown capturing method: {!r}".format(method))
|
||||
raise ValueError(f"unknown capturing method: {method!r}")
|
||||
|
||||
|
||||
# CaptureManager and CaptureFixture
|
||||
@@ -669,8 +663,8 @@ class CaptureManager:
|
||||
|
||||
def __init__(self, method: "_CaptureMethod") -> None:
|
||||
self._method = method
|
||||
self._global_capturing = None # type: Optional[MultiCapture[str]]
|
||||
self._capture_fixture = None # type: Optional[CaptureFixture[Any]]
|
||||
self._global_capturing: Optional[MultiCapture[str]] = None
|
||||
self._capture_fixture: Optional[CaptureFixture[Any]] = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<CaptureManager _method={!r} _global_capturing={!r} _capture_fixture={!r}>".format(
|
||||
@@ -793,9 +787,9 @@ class CaptureManager:
|
||||
|
||||
# Hooks
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_make_collect_report(self, collector: Collector):
|
||||
if isinstance(collector, pytest.File):
|
||||
if isinstance(collector, File):
|
||||
self.resume_global_capture()
|
||||
outcome = yield
|
||||
self.suspend_global_capture()
|
||||
@@ -808,38 +802,41 @@ class CaptureManager:
|
||||
else:
|
||||
yield
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_setup(self, item: Item) -> Generator[None, None, None]:
|
||||
with self.item_capture("setup", item):
|
||||
yield
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]:
|
||||
with self.item_capture("call", item):
|
||||
yield
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_teardown(self, item: Item) -> Generator[None, None, None]:
|
||||
with self.item_capture("teardown", item):
|
||||
yield
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
@hookimpl(tryfirst=True)
|
||||
def pytest_keyboard_interrupt(self) -> None:
|
||||
self.stop_global_capturing()
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
@hookimpl(tryfirst=True)
|
||||
def pytest_internalerror(self) -> None:
|
||||
self.stop_global_capturing()
|
||||
|
||||
|
||||
class CaptureFixture(Generic[AnyStr]):
|
||||
"""Object returned by the :py:func:`capsys`, :py:func:`capsysbinary`,
|
||||
:py:func:`capfd` and :py:func:`capfdbinary` fixtures."""
|
||||
"""Object returned by the :fixture:`capsys`, :fixture:`capsysbinary`,
|
||||
:fixture:`capfd` and :fixture:`capfdbinary` fixtures."""
|
||||
|
||||
def __init__(self, captureclass, request: SubRequest) -> None:
|
||||
def __init__(
|
||||
self, captureclass, request: SubRequest, *, _ispytest: bool = False
|
||||
) -> None:
|
||||
check_ispytest(_ispytest)
|
||||
self.captureclass = captureclass
|
||||
self.request = request
|
||||
self._capture = None # type: Optional[MultiCapture[AnyStr]]
|
||||
self._capture: Optional[MultiCapture[AnyStr]] = None
|
||||
self._captured_out = self.captureclass.EMPTY_BUFFER
|
||||
self._captured_err = self.captureclass.EMPTY_BUFFER
|
||||
|
||||
@@ -902,7 +899,7 @@ class CaptureFixture(Generic[AnyStr]):
|
||||
# The fixtures.
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@fixture
|
||||
def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
|
||||
"""Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||
|
||||
@@ -911,7 +908,7 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
|
||||
``out`` and ``err`` will be ``text`` objects.
|
||||
"""
|
||||
capman = request.config.pluginmanager.getplugin("capturemanager")
|
||||
capture_fixture = CaptureFixture[str](SysCapture, request)
|
||||
capture_fixture = CaptureFixture[str](SysCapture, request, _ispytest=True)
|
||||
capman.set_fixture(capture_fixture)
|
||||
capture_fixture._start()
|
||||
yield capture_fixture
|
||||
@@ -919,7 +916,7 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
|
||||
capman.unset_fixture()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@fixture
|
||||
def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]:
|
||||
"""Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||
|
||||
@@ -928,7 +925,7 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None,
|
||||
``out`` and ``err`` will be ``bytes`` objects.
|
||||
"""
|
||||
capman = request.config.pluginmanager.getplugin("capturemanager")
|
||||
capture_fixture = CaptureFixture[bytes](SysCaptureBinary, request)
|
||||
capture_fixture = CaptureFixture[bytes](SysCaptureBinary, request, _ispytest=True)
|
||||
capman.set_fixture(capture_fixture)
|
||||
capture_fixture._start()
|
||||
yield capture_fixture
|
||||
@@ -936,7 +933,7 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None,
|
||||
capman.unset_fixture()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@fixture
|
||||
def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
|
||||
"""Enable text capturing of writes to file descriptors ``1`` and ``2``.
|
||||
|
||||
@@ -945,7 +942,7 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
|
||||
``out`` and ``err`` will be ``text`` objects.
|
||||
"""
|
||||
capman = request.config.pluginmanager.getplugin("capturemanager")
|
||||
capture_fixture = CaptureFixture[str](FDCapture, request)
|
||||
capture_fixture = CaptureFixture[str](FDCapture, request, _ispytest=True)
|
||||
capman.set_fixture(capture_fixture)
|
||||
capture_fixture._start()
|
||||
yield capture_fixture
|
||||
@@ -953,7 +950,7 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
|
||||
capman.unset_fixture()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@fixture
|
||||
def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]:
|
||||
"""Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
|
||||
|
||||
@@ -962,7 +959,7 @@ def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, N
|
||||
``out`` and ``err`` will be ``byte`` objects.
|
||||
"""
|
||||
capman = request.config.pluginmanager.getplugin("capturemanager")
|
||||
capture_fixture = CaptureFixture[bytes](FDCaptureBinary, request)
|
||||
capture_fixture = CaptureFixture[bytes](FDCaptureBinary, request, _ispytest=True)
|
||||
capman.set_fixture(capture_fixture)
|
||||
capture_fixture._start()
|
||||
yield capture_fixture
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
import enum
|
||||
import functools
|
||||
import inspect
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from inspect import Parameter
|
||||
from inspect import signature
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Generic
|
||||
from typing import Optional
|
||||
from typing import overload as overload
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
@@ -22,15 +22,8 @@ import attr
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.outcomes import TEST_OUTCOME
|
||||
|
||||
if sys.version_info < (3, 5, 2):
|
||||
TYPE_CHECKING = False # type: bool
|
||||
else:
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import NoReturn
|
||||
from typing import Type
|
||||
from typing_extensions import Final
|
||||
|
||||
|
||||
@@ -43,14 +36,9 @@ _S = TypeVar("_S")
|
||||
# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
|
||||
class NotSetType(enum.Enum):
|
||||
token = 0
|
||||
NOTSET = NotSetType.token # type: Final # noqa: E305
|
||||
NOTSET: "Final" = NotSetType.token # noqa: E305
|
||||
# fmt: on
|
||||
|
||||
MODULE_NOT_FOUND_ERROR = (
|
||||
"ModuleNotFoundError" if sys.version_info[:2] >= (3, 6) else "ImportError"
|
||||
)
|
||||
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from importlib import metadata as importlib_metadata
|
||||
else:
|
||||
@@ -65,18 +53,6 @@ def _format_args(func: Callable[..., Any]) -> str:
|
||||
REGEX_TYPE = type(re.compile(""))
|
||||
|
||||
|
||||
if sys.version_info < (3, 6):
|
||||
|
||||
def fspath(p):
|
||||
"""os.fspath replacement, useful to point out when we should replace it by the
|
||||
real function once we drop py35."""
|
||||
return str(p)
|
||||
|
||||
|
||||
else:
|
||||
fspath = os.fspath
|
||||
|
||||
|
||||
def is_generator(func: object) -> bool:
|
||||
genfunc = inspect.isgeneratorfunction(func)
|
||||
return genfunc and not iscoroutinefunction(func)
|
||||
@@ -97,14 +73,10 @@ def iscoroutinefunction(func: object) -> bool:
|
||||
def is_async_function(func: object) -> bool:
|
||||
"""Return True if the given function seems to be an async function or
|
||||
an async generator."""
|
||||
return iscoroutinefunction(func) or (
|
||||
sys.version_info >= (3, 6) and inspect.isasyncgenfunction(func)
|
||||
)
|
||||
return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
|
||||
|
||||
|
||||
def getlocation(function, curdir: Optional[str] = None) -> str:
|
||||
from _pytest.pathlib import Path
|
||||
|
||||
function = get_real_func(function)
|
||||
fn = Path(inspect.getfile(function))
|
||||
lineno = function.__code__.co_firstlineno
|
||||
@@ -142,7 +114,7 @@ def getfuncargnames(
|
||||
*,
|
||||
name: str = "",
|
||||
is_method: bool = False,
|
||||
cls: Optional[type] = None
|
||||
cls: Optional[type] = None,
|
||||
) -> Tuple[str, ...]:
|
||||
"""Return the names of a function's mandatory arguments.
|
||||
|
||||
@@ -171,17 +143,15 @@ def getfuncargnames(
|
||||
parameters = signature(function).parameters
|
||||
except (ValueError, TypeError) as e:
|
||||
fail(
|
||||
"Could not determine arguments of {!r}: {}".format(function, e),
|
||||
pytrace=False,
|
||||
f"Could not determine arguments of {function!r}: {e}", pytrace=False,
|
||||
)
|
||||
|
||||
arg_names = tuple(
|
||||
p.name
|
||||
for p in parameters.values()
|
||||
if (
|
||||
# TODO: Remove type ignore after https://github.com/python/typeshed/pull/4383
|
||||
p.kind is Parameter.POSITIONAL_OR_KEYWORD # type: ignore[unreachable]
|
||||
or p.kind is Parameter.KEYWORD_ONLY # type: ignore[unreachable]
|
||||
p.kind is Parameter.POSITIONAL_OR_KEYWORD
|
||||
or p.kind is Parameter.KEYWORD_ONLY
|
||||
)
|
||||
and p.default is Parameter.empty
|
||||
)
|
||||
@@ -225,7 +195,7 @@ def get_default_arg_names(function: Callable[..., Any]) -> Tuple[str, ...]:
|
||||
|
||||
|
||||
_non_printable_ascii_translate_table = {
|
||||
i: "\\x{:02x}".format(i) for i in range(128) if i not in range(32, 127)
|
||||
i: f"\\x{i:02x}" for i in range(128) if i not in range(32, 127)
|
||||
}
|
||||
_non_printable_ascii_translate_table.update(
|
||||
{ord("\t"): "\\t", ord("\r"): "\\r", ord("\n"): "\\n"}
|
||||
@@ -352,12 +322,6 @@ def safe_isclass(obj: object) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
if sys.version_info < (3, 5, 2):
|
||||
|
||||
def overload(f): # noqa: F811
|
||||
return f
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if sys.version_info >= (3, 8):
|
||||
from typing import final as final
|
||||
@@ -367,19 +331,15 @@ elif sys.version_info >= (3, 8):
|
||||
from typing import final as final
|
||||
else:
|
||||
|
||||
def final(f): # noqa: F811
|
||||
def final(f):
|
||||
return f
|
||||
|
||||
|
||||
if getattr(attr, "__version_info__", ()) >= (19, 2):
|
||||
ATTRS_EQ_FIELD = "eq"
|
||||
else:
|
||||
ATTRS_EQ_FIELD = "cmp"
|
||||
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from functools import cached_property as cached_property
|
||||
else:
|
||||
from typing import overload
|
||||
from typing import Type
|
||||
|
||||
class cached_property(Generic[_S, _T]):
|
||||
__slots__ = ("func", "__doc__")
|
||||
@@ -390,35 +350,21 @@ else:
|
||||
|
||||
@overload
|
||||
def __get__(
|
||||
self, instance: None, owner: Optional["Type[_S]"] = ...
|
||||
self, instance: None, owner: Optional[Type[_S]] = ...
|
||||
) -> "cached_property[_S, _T]":
|
||||
...
|
||||
|
||||
@overload # noqa: F811
|
||||
def __get__( # noqa: F811
|
||||
self, instance: _S, owner: Optional["Type[_S]"] = ...
|
||||
) -> _T:
|
||||
@overload
|
||||
def __get__(self, instance: _S, owner: Optional[Type[_S]] = ...) -> _T:
|
||||
...
|
||||
|
||||
def __get__(self, instance, owner=None): # noqa: F811
|
||||
def __get__(self, instance, owner=None):
|
||||
if instance is None:
|
||||
return self
|
||||
value = instance.__dict__[self.func.__name__] = self.func(instance)
|
||||
return value
|
||||
|
||||
|
||||
# Sometimes an algorithm needs a dict which yields items in the order in which
|
||||
# they were inserted when iterated. Since Python 3.7, `dict` preserves
|
||||
# insertion order. Since `dict` is faster and uses less memory than
|
||||
# `OrderedDict`, prefer to use it if possible.
|
||||
if sys.version_info >= (3, 7):
|
||||
order_preserving_dict = dict
|
||||
else:
|
||||
from collections import OrderedDict
|
||||
|
||||
order_preserving_dict = OrderedDict
|
||||
|
||||
|
||||
# Perform exhaustiveness checking.
|
||||
#
|
||||
# Consider this example:
|
||||
|
||||
@@ -12,6 +12,7 @@ import sys
|
||||
import types
|
||||
import warnings
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from types import TracebackType
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
@@ -26,6 +27,8 @@ from typing import Sequence
|
||||
from typing import Set
|
||||
from typing import TextIO
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
import attr
|
||||
@@ -45,18 +48,15 @@ from _pytest._code import filter_traceback
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import importlib_metadata
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.outcomes import Skipped
|
||||
from _pytest.pathlib import bestrelpath
|
||||
from _pytest.pathlib import import_path
|
||||
from _pytest.pathlib import ImportMode
|
||||
from _pytest.pathlib import Path
|
||||
from _pytest.store import Store
|
||||
from _pytest.warning_types import PytestConfigWarning
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Type
|
||||
|
||||
from _pytest._code.code import _TracebackStyle
|
||||
from _pytest.terminal import TerminalReporter
|
||||
@@ -104,7 +104,7 @@ class ConftestImportFailure(Exception):
|
||||
def __init__(
|
||||
self,
|
||||
path: py.path.local,
|
||||
excinfo: Tuple["Type[Exception]", Exception, TracebackType],
|
||||
excinfo: Tuple[Type[Exception], Exception, TracebackType],
|
||||
) -> None:
|
||||
super().__init__(path, excinfo)
|
||||
self.path = path
|
||||
@@ -144,9 +144,7 @@ def main(
|
||||
except ConftestImportFailure as e:
|
||||
exc_info = ExceptionInfo(e.excinfo)
|
||||
tw = TerminalWriter(sys.stderr)
|
||||
tw.line(
|
||||
"ImportError while loading conftest '{e.path}'.".format(e=e), red=True
|
||||
)
|
||||
tw.line(f"ImportError while loading conftest '{e.path}'.", red=True)
|
||||
exc_info.traceback = exc_info.traceback.filter(
|
||||
filter_traceback_for_conftest_import_failure
|
||||
)
|
||||
@@ -161,9 +159,9 @@ def main(
|
||||
return ExitCode.USAGE_ERROR
|
||||
else:
|
||||
try:
|
||||
ret = config.hook.pytest_cmdline_main(
|
||||
ret: Union[ExitCode, int] = config.hook.pytest_cmdline_main(
|
||||
config=config
|
||||
) # type: Union[ExitCode, int]
|
||||
)
|
||||
try:
|
||||
return ExitCode(ret)
|
||||
except ValueError:
|
||||
@@ -173,7 +171,7 @@ def main(
|
||||
except UsageError as e:
|
||||
tw = TerminalWriter(sys.stderr)
|
||||
for msg in e.args:
|
||||
tw.line("ERROR: {}\n".format(msg), red=True)
|
||||
tw.line(f"ERROR: {msg}\n", red=True)
|
||||
return ExitCode.USAGE_ERROR
|
||||
|
||||
|
||||
@@ -206,7 +204,7 @@ def filename_arg(path: str, optname: str) -> str:
|
||||
:optname: Name of the option.
|
||||
"""
|
||||
if os.path.isdir(path):
|
||||
raise UsageError("{} must be a filename, given: {}".format(optname, path))
|
||||
raise UsageError(f"{optname} must be a filename, given: {path}")
|
||||
return path
|
||||
|
||||
|
||||
@@ -217,7 +215,7 @@ def directory_arg(path: str, optname: str) -> str:
|
||||
:optname: Name of the option.
|
||||
"""
|
||||
if not os.path.isdir(path):
|
||||
raise UsageError("{} must be a directory, given: {}".format(optname, path))
|
||||
raise UsageError(f"{optname} must be a directory, given: {path}")
|
||||
return path
|
||||
|
||||
|
||||
@@ -253,11 +251,13 @@ default_plugins = essential_plugins + (
|
||||
"warnings",
|
||||
"logging",
|
||||
"reports",
|
||||
*(["unraisableexception", "threadexception"] if sys.version_info >= (3, 8) else []),
|
||||
"faulthandler",
|
||||
)
|
||||
|
||||
builtin_plugins = set(default_plugins)
|
||||
builtin_plugins.add("pytester")
|
||||
builtin_plugins.add("pytester_assertions")
|
||||
|
||||
|
||||
def get_config(
|
||||
@@ -339,27 +339,27 @@ class PytestPluginManager(PluginManager):
|
||||
|
||||
super().__init__("pytest")
|
||||
# The objects are module objects, only used generically.
|
||||
self._conftest_plugins = set() # type: Set[types.ModuleType]
|
||||
self._conftest_plugins: Set[types.ModuleType] = set()
|
||||
|
||||
# State related to local conftest plugins.
|
||||
self._dirpath2confmods = {} # type: Dict[py.path.local, List[types.ModuleType]]
|
||||
self._conftestpath2mod = {} # type: Dict[Path, types.ModuleType]
|
||||
self._confcutdir = None # type: Optional[py.path.local]
|
||||
self._dirpath2confmods: Dict[py.path.local, List[types.ModuleType]] = {}
|
||||
self._conftestpath2mod: Dict[Path, types.ModuleType] = {}
|
||||
self._confcutdir: Optional[py.path.local] = None
|
||||
self._noconftest = False
|
||||
self._duplicatepaths = set() # type: Set[py.path.local]
|
||||
self._duplicatepaths: Set[py.path.local] = set()
|
||||
|
||||
# plugins that were explicitly skipped with pytest.skip
|
||||
# list of (module name, skip reason)
|
||||
# previously we would issue a warning when a plugin was skipped, but
|
||||
# since we refactored warnings as first citizens of Config, they are
|
||||
# just stored here to be used later.
|
||||
self.skipped_plugins = [] # type: List[Tuple[str, str]]
|
||||
self.skipped_plugins: List[Tuple[str, str]] = []
|
||||
|
||||
self.add_hookspecs(_pytest.hookspec)
|
||||
self.register(self)
|
||||
if os.environ.get("PYTEST_DEBUG"):
|
||||
err = sys.stderr # type: IO[str]
|
||||
encoding = getattr(err, "encoding", "utf8") # type: str
|
||||
err: IO[str] = sys.stderr
|
||||
encoding: str = getattr(err, "encoding", "utf8")
|
||||
try:
|
||||
err = open(
|
||||
os.dup(err.fileno()), mode=err.mode, buffering=1, encoding=encoding,
|
||||
@@ -433,7 +433,7 @@ class PytestPluginManager(PluginManager):
|
||||
)
|
||||
)
|
||||
return None
|
||||
ret = super().register(plugin, name) # type: Optional[str]
|
||||
ret: Optional[str] = super().register(plugin, name)
|
||||
if ret:
|
||||
self.hook.pytest_plugin_registered.call_historic(
|
||||
kwargs=dict(plugin=plugin, manager=self)
|
||||
@@ -445,7 +445,7 @@ class PytestPluginManager(PluginManager):
|
||||
|
||||
def getplugin(self, name: str):
|
||||
# Support deprecated naming because plugins (xdist e.g.) use it.
|
||||
plugin = self.get_plugin(name) # type: Optional[_PluggyPlugin]
|
||||
plugin: Optional[_PluggyPlugin] = self.get_plugin(name)
|
||||
return plugin
|
||||
|
||||
def hasplugin(self, name: str) -> bool:
|
||||
@@ -583,7 +583,7 @@ class PytestPluginManager(PluginManager):
|
||||
if path and path.relto(dirpath) or path == dirpath:
|
||||
assert mod not in mods
|
||||
mods.append(mod)
|
||||
self.trace("loading conftestmodule {!r}".format(mod))
|
||||
self.trace(f"loading conftestmodule {mod!r}")
|
||||
self.consider_conftest(mod)
|
||||
return mod
|
||||
|
||||
@@ -866,7 +866,7 @@ class Config:
|
||||
self,
|
||||
pluginmanager: PytestPluginManager,
|
||||
*,
|
||||
invocation_params: Optional[InvocationParams] = None
|
||||
invocation_params: Optional[InvocationParams] = None,
|
||||
) -> None:
|
||||
from .argparsing import Parser, FILE_OR_DIR
|
||||
|
||||
@@ -889,7 +889,7 @@ class Config:
|
||||
|
||||
_a = FILE_OR_DIR
|
||||
self._parser = Parser(
|
||||
usage="%(prog)s [options] [{}] [{}] [...]".format(_a, _a),
|
||||
usage=f"%(prog)s [options] [{_a}] [{_a}] [...]",
|
||||
processopt=self._processopt,
|
||||
)
|
||||
self.pluginmanager = pluginmanager
|
||||
@@ -900,10 +900,10 @@ class Config:
|
||||
|
||||
self.trace = self.pluginmanager.trace.root.get("config")
|
||||
self.hook = self.pluginmanager.hook
|
||||
self._inicache = {} # type: Dict[str, Any]
|
||||
self._override_ini = () # type: Sequence[str]
|
||||
self._opt2dest = {} # type: Dict[str, str]
|
||||
self._cleanup = [] # type: List[Callable[[], None]]
|
||||
self._inicache: Dict[str, Any] = {}
|
||||
self._override_ini: Sequence[str] = ()
|
||||
self._opt2dest: Dict[str, str] = {}
|
||||
self._cleanup: List[Callable[[], None]] = []
|
||||
# A place where plugins can store information on the config for their
|
||||
# own use. Currently only intended for internal plugins.
|
||||
self._store = Store()
|
||||
@@ -916,7 +916,7 @@ class Config:
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.cacheprovider import Cache
|
||||
|
||||
self.cache = None # type: Optional[Cache]
|
||||
self.cache: Optional[Cache] = None
|
||||
|
||||
@property
|
||||
def invocation_dir(self) -> py.path.local:
|
||||
@@ -991,9 +991,9 @@ class Config:
|
||||
fin()
|
||||
|
||||
def get_terminal_writer(self) -> TerminalWriter:
|
||||
terminalreporter = self.pluginmanager.get_plugin(
|
||||
terminalreporter: TerminalReporter = self.pluginmanager.get_plugin(
|
||||
"terminalreporter"
|
||||
) # type: TerminalReporter
|
||||
)
|
||||
return terminalreporter._tw
|
||||
|
||||
def pytest_cmdline_parse(
|
||||
@@ -1028,7 +1028,7 @@ class Config:
|
||||
option: Optional[argparse.Namespace] = None,
|
||||
) -> None:
|
||||
if option and getattr(option, "fulltrace", False):
|
||||
style = "long" # type: _TracebackStyle
|
||||
style: _TracebackStyle = "long"
|
||||
else:
|
||||
style = "native"
|
||||
excrepr = excinfo.getrepr(
|
||||
@@ -1179,6 +1179,11 @@ class Config:
|
||||
self._validate_plugins()
|
||||
self._warn_about_skipped_plugins()
|
||||
|
||||
if self.known_args_namespace.strict:
|
||||
self.issue_config_time_warning(
|
||||
_pytest.deprecated.STRICT_OPTION, stacklevel=2
|
||||
)
|
||||
|
||||
if self.known_args_namespace.confcutdir is None and self.inipath is not None:
|
||||
confcutdir = str(self.inipath.parent)
|
||||
self.known_args_namespace.confcutdir = confcutdir
|
||||
@@ -1191,9 +1196,7 @@ class Config:
|
||||
# we don't want to prevent --help/--version to work
|
||||
# so just let is pass and print a warning at the end
|
||||
self.issue_config_time_warning(
|
||||
PytestConfigWarning(
|
||||
"could not load initial conftests: {}".format(e.path)
|
||||
),
|
||||
PytestConfigWarning(f"could not load initial conftests: {e.path}"),
|
||||
stacklevel=2,
|
||||
)
|
||||
else:
|
||||
@@ -1227,7 +1230,7 @@ class Config:
|
||||
|
||||
def _validate_config_options(self) -> None:
|
||||
for key in sorted(self._get_unknown_ini_keys()):
|
||||
self._warn_or_fail_if_strict("Unknown config option: {}\n".format(key))
|
||||
self._warn_or_fail_if_strict(f"Unknown config option: {key}\n")
|
||||
|
||||
def _validate_plugins(self) -> None:
|
||||
required_plugins = sorted(self.getini("required_plugins"))
|
||||
@@ -1362,7 +1365,7 @@ class Config:
|
||||
try:
|
||||
description, type, default = self._parser._inidict[name]
|
||||
except KeyError as e:
|
||||
raise ValueError("unknown configuration value: {!r}".format(name)) from e
|
||||
raise ValueError(f"unknown configuration value: {name!r}") from e
|
||||
override_value = self._get_override_ini_value(name)
|
||||
if override_value is None:
|
||||
try:
|
||||
@@ -1406,7 +1409,7 @@ class Config:
|
||||
elif type == "bool":
|
||||
return _strtobool(str(value).strip())
|
||||
else:
|
||||
assert type is None
|
||||
assert type in [None, "string"]
|
||||
return value
|
||||
|
||||
def _getconftest_pathlist(
|
||||
@@ -1419,7 +1422,7 @@ class Config:
|
||||
except KeyError:
|
||||
return None
|
||||
modpath = py.path.local(mod.__file__).dirpath()
|
||||
values = [] # type: List[py.path.local]
|
||||
values: List[py.path.local] = []
|
||||
for relroot in relroots:
|
||||
if not isinstance(relroot, py.path.local):
|
||||
relroot = relroot.replace("/", os.sep)
|
||||
@@ -1467,8 +1470,8 @@ class Config:
|
||||
if skip:
|
||||
import pytest
|
||||
|
||||
pytest.skip("no {!r} option found".format(name))
|
||||
raise ValueError("no option named {!r}".format(name)) from e
|
||||
pytest.skip(f"no {name!r} option found")
|
||||
raise ValueError(f"no option named {name!r}") from e
|
||||
|
||||
def getvalue(self, name: str, path=None):
|
||||
"""Deprecated, use getoption() instead."""
|
||||
@@ -1501,7 +1504,7 @@ class Config:
|
||||
def _warn_about_skipped_plugins(self) -> None:
|
||||
for module_name, msg in self.pluginmanager.skipped_plugins:
|
||||
self.issue_config_time_warning(
|
||||
PytestConfigWarning("skipped plugin {!r}: {}".format(module_name, msg)),
|
||||
PytestConfigWarning(f"skipped plugin {module_name!r}: {msg}"),
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
@@ -1554,13 +1557,13 @@ def _strtobool(val: str) -> bool:
|
||||
elif val in ("n", "no", "f", "false", "off", "0"):
|
||||
return False
|
||||
else:
|
||||
raise ValueError("invalid truth value {!r}".format(val))
|
||||
raise ValueError(f"invalid truth value {val!r}")
|
||||
|
||||
|
||||
@lru_cache(maxsize=50)
|
||||
def parse_warning_filter(
|
||||
arg: str, *, escape: bool
|
||||
) -> "Tuple[str, str, Type[Warning], str, int]":
|
||||
) -> Tuple[str, str, Type[Warning], str, int]:
|
||||
"""Parse a warnings filter string.
|
||||
|
||||
This is copied from warnings._setoption, but does not apply the filter,
|
||||
@@ -1568,14 +1571,12 @@ def parse_warning_filter(
|
||||
"""
|
||||
parts = arg.split(":")
|
||||
if len(parts) > 5:
|
||||
raise warnings._OptionError("too many fields (max 5): {!r}".format(arg))
|
||||
raise warnings._OptionError(f"too many fields (max 5): {arg!r}")
|
||||
while len(parts) < 5:
|
||||
parts.append("")
|
||||
action_, message, category_, module, lineno_ = [s.strip() for s in parts]
|
||||
action = warnings._getaction(action_) # type: str # type: ignore[attr-defined]
|
||||
category = warnings._getcategory(
|
||||
category_
|
||||
) # type: Type[Warning] # type: ignore[attr-defined]
|
||||
action: str = warnings._getaction(action_) # type: ignore[attr-defined]
|
||||
category: Type[Warning] = warnings._getcategory(category_) # type: ignore[attr-defined]
|
||||
if message and escape:
|
||||
message = re.escape(message)
|
||||
if module and escape:
|
||||
@@ -1586,7 +1587,7 @@ def parse_warning_filter(
|
||||
if lineno < 0:
|
||||
raise ValueError
|
||||
except (ValueError, OverflowError) as e:
|
||||
raise warnings._OptionError("invalid lineno {!r}".format(lineno_)) from e
|
||||
raise warnings._OptionError(f"invalid lineno {lineno_!r}") from e
|
||||
else:
|
||||
lineno = 0
|
||||
return action, message, category, module, lineno
|
||||
|
||||
@@ -11,13 +11,13 @@ from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
import py
|
||||
|
||||
import _pytest._io
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.config.exceptions import UsageError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -35,7 +35,7 @@ class Parser:
|
||||
there's an error processing the command line arguments.
|
||||
"""
|
||||
|
||||
prog = None # type: Optional[str]
|
||||
prog: Optional[str] = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -43,12 +43,12 @@ class Parser:
|
||||
processopt: Optional[Callable[["Argument"], None]] = None,
|
||||
) -> None:
|
||||
self._anonymous = OptionGroup("custom options", parser=self)
|
||||
self._groups = [] # type: List[OptionGroup]
|
||||
self._groups: List[OptionGroup] = []
|
||||
self._processopt = processopt
|
||||
self._usage = usage
|
||||
self._inidict = {} # type: Dict[str, Tuple[str, Optional[str], Any]]
|
||||
self._ininames = [] # type: List[str]
|
||||
self.extra_info = {} # type: Dict[str, Any]
|
||||
self._inidict: Dict[str, Tuple[str, Optional[str], Any]] = {}
|
||||
self._ininames: List[str] = []
|
||||
self.extra_info: Dict[str, Any] = {}
|
||||
|
||||
def processoption(self, option: "Argument") -> None:
|
||||
if self._processopt:
|
||||
@@ -160,20 +160,23 @@ class Parser:
|
||||
self,
|
||||
name: str,
|
||||
help: str,
|
||||
type: Optional["Literal['pathlist', 'args', 'linelist', 'bool']"] = None,
|
||||
type: Optional[
|
||||
"Literal['string', 'pathlist', 'args', 'linelist', 'bool']"
|
||||
] = None,
|
||||
default=None,
|
||||
) -> None:
|
||||
"""Register an ini-file option.
|
||||
|
||||
:name: Name of the ini-variable.
|
||||
:type: Type of the variable, can be ``pathlist``, ``args``, ``linelist``
|
||||
or ``bool``.
|
||||
:type: Type of the variable, can be ``string``, ``pathlist``, ``args``,
|
||||
``linelist`` or ``bool``. Defaults to ``string`` if ``None`` or
|
||||
not passed.
|
||||
:default: Default value if no ini-file option exists but is queried.
|
||||
|
||||
The value of ini-variables can be retrieved via a call to
|
||||
:py:func:`config.getini(name) <_pytest.config.Config.getini>`.
|
||||
"""
|
||||
assert type in (None, "pathlist", "args", "linelist", "bool")
|
||||
assert type in (None, "string", "pathlist", "args", "linelist", "bool")
|
||||
self._inidict[name] = (help, type, default)
|
||||
self._ininames.append(name)
|
||||
|
||||
@@ -188,7 +191,7 @@ class ArgumentError(Exception):
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.option_id:
|
||||
return "option {}: {}".format(self.option_id, self.msg)
|
||||
return f"option {self.option_id}: {self.msg}"
|
||||
else:
|
||||
return self.msg
|
||||
|
||||
@@ -207,8 +210,8 @@ class Argument:
|
||||
def __init__(self, *names: str, **attrs: Any) -> None:
|
||||
"""Store parms in private vars for use in add_argument."""
|
||||
self._attrs = attrs
|
||||
self._short_opts = [] # type: List[str]
|
||||
self._long_opts = [] # type: List[str]
|
||||
self._short_opts: List[str] = []
|
||||
self._long_opts: List[str] = []
|
||||
if "%default" in (attrs.get("help") or ""):
|
||||
warnings.warn(
|
||||
'pytest now uses argparse. "%default" should be'
|
||||
@@ -254,7 +257,7 @@ class Argument:
|
||||
except KeyError:
|
||||
pass
|
||||
self._set_opt_strings(names)
|
||||
dest = attrs.get("dest") # type: Optional[str]
|
||||
dest: Optional[str] = attrs.get("dest")
|
||||
if dest:
|
||||
self.dest = dest
|
||||
elif self._long_opts:
|
||||
@@ -315,7 +318,7 @@ class Argument:
|
||||
self._long_opts.append(opt)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
args = [] # type: List[str]
|
||||
args: List[str] = []
|
||||
if self._short_opts:
|
||||
args += ["_short_opts: " + repr(self._short_opts)]
|
||||
if self._long_opts:
|
||||
@@ -334,7 +337,7 @@ class OptionGroup:
|
||||
) -> None:
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.options = [] # type: List[Argument]
|
||||
self.options: List[Argument] = []
|
||||
self.parser = parser
|
||||
|
||||
def addoption(self, *optnames: str, **attrs: Any) -> None:
|
||||
@@ -389,11 +392,11 @@ class MyOptionParser(argparse.ArgumentParser):
|
||||
|
||||
def error(self, message: str) -> "NoReturn":
|
||||
"""Transform argparse error message into UsageError."""
|
||||
msg = "{}: error: {}".format(self.prog, message)
|
||||
msg = f"{self.prog}: error: {message}"
|
||||
|
||||
if hasattr(self._parser, "_config_source_hint"):
|
||||
# Type ignored because the attribute is set dynamically.
|
||||
msg = "{} ({})".format(msg, self._parser._config_source_hint) # type: ignore
|
||||
msg = f"{msg} ({self._parser._config_source_hint})" # type: ignore
|
||||
|
||||
raise UsageError(self.format_usage() + msg)
|
||||
|
||||
@@ -410,7 +413,7 @@ class MyOptionParser(argparse.ArgumentParser):
|
||||
if arg and arg[0] == "-":
|
||||
lines = ["unrecognized arguments: %s" % (" ".join(unrecognized))]
|
||||
for k, v in sorted(self.extra_info.items()):
|
||||
lines.append(" {}: {}".format(k, v))
|
||||
lines.append(f" {k}: {v}")
|
||||
self.error("\n".join(lines))
|
||||
getattr(parsed, FILE_OR_DIR).extend(unrecognized)
|
||||
return parsed
|
||||
@@ -472,9 +475,7 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter):
|
||||
orgstr = argparse.HelpFormatter._format_action_invocation(self, action)
|
||||
if orgstr and orgstr[0] != "-": # only optional arguments
|
||||
return orgstr
|
||||
res = getattr(
|
||||
action, "_formatted_action_invocation", None
|
||||
) # type: Optional[str]
|
||||
res: Optional[str] = getattr(action, "_formatted_action_invocation", None)
|
||||
if res:
|
||||
return res
|
||||
options = orgstr.split(", ")
|
||||
@@ -483,7 +484,7 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter):
|
||||
action._formatted_action_invocation = orgstr # type: ignore
|
||||
return orgstr
|
||||
return_list = []
|
||||
short_long = {} # type: Dict[str, str]
|
||||
short_long: Dict[str, str] = {}
|
||||
for option in options:
|
||||
if len(option) == 2 or option[2] == " ":
|
||||
continue
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import itertools
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
import iniconfig
|
||||
|
||||
from .exceptions import UsageError
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.pathlib import absolutepath
|
||||
from _pytest.pathlib import commonpath
|
||||
from _pytest.pathlib import Path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import Config
|
||||
@@ -28,7 +27,7 @@ def _parse_ini_config(path: Path) -> iniconfig.IniConfig:
|
||||
Raise UsageError if the file cannot be parsed.
|
||||
"""
|
||||
try:
|
||||
return iniconfig.IniConfig(path)
|
||||
return iniconfig.IniConfig(str(path))
|
||||
except iniconfig.ParseError as exc:
|
||||
raise UsageError(str(exc)) from exc
|
||||
|
||||
@@ -100,7 +99,7 @@ def locate_config(
|
||||
args = [Path.cwd()]
|
||||
for arg in args:
|
||||
argpath = absolutepath(arg)
|
||||
for base in itertools.chain((argpath,), reversed(argpath.parents)):
|
||||
for base in (argpath, *argpath.parents):
|
||||
for config_name in config_names:
|
||||
p = base / config_name
|
||||
if p.is_file():
|
||||
@@ -111,7 +110,7 @@ def locate_config(
|
||||
|
||||
|
||||
def get_common_ancestor(paths: Iterable[Path]) -> Path:
|
||||
common_ancestor = None # type: Optional[Path]
|
||||
common_ancestor: Optional[Path] = None
|
||||
for path in paths:
|
||||
if not path.exists():
|
||||
continue
|
||||
@@ -176,7 +175,7 @@ def determine_setup(
|
||||
dirs = get_dirs_from_args(args)
|
||||
if inifile:
|
||||
inipath_ = absolutepath(inifile)
|
||||
inipath = inipath_ # type: Optional[Path]
|
||||
inipath: Optional[Path] = inipath_
|
||||
inicfg = load_config_dict_from_file(inipath_) or {}
|
||||
if rootdir_cmd_arg is None:
|
||||
rootdir = get_common_ancestor(dirs)
|
||||
@@ -184,9 +183,7 @@ def determine_setup(
|
||||
ancestor = get_common_ancestor(dirs)
|
||||
rootdir, inipath, inicfg = locate_config([ancestor])
|
||||
if rootdir is None and rootdir_cmd_arg is None:
|
||||
for possible_rootdir in itertools.chain(
|
||||
(ancestor,), reversed(ancestor.parents)
|
||||
):
|
||||
for possible_rootdir in (ancestor, *ancestor.parents):
|
||||
if (possible_rootdir / "setup.py").is_file():
|
||||
rootdir = possible_rootdir
|
||||
break
|
||||
|
||||
@@ -9,11 +9,12 @@ from typing import Generator
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
from _pytest import outcomes
|
||||
from _pytest._code import ExceptionInfo
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ConftestImportFailure
|
||||
from _pytest.config import hookimpl
|
||||
@@ -24,8 +25,6 @@ from _pytest.nodes import Node
|
||||
from _pytest.reports import BaseReport
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Type
|
||||
|
||||
from _pytest.capture import CaptureManager
|
||||
from _pytest.runner import CallInfo
|
||||
|
||||
@@ -36,7 +35,7 @@ def _validate_usepdb_cls(value: str) -> Tuple[str, str]:
|
||||
modname, classname = value.split(":")
|
||||
except ValueError as e:
|
||||
raise argparse.ArgumentTypeError(
|
||||
"{!r} is not in the format 'modname:classname'".format(value)
|
||||
f"{value!r} is not in the format 'modname:classname'"
|
||||
) from e
|
||||
return (modname, classname)
|
||||
|
||||
@@ -95,13 +94,13 @@ def pytest_configure(config: Config) -> None:
|
||||
class pytestPDB:
|
||||
"""Pseudo PDB that defers to the real pdb."""
|
||||
|
||||
_pluginmanager = None # type: Optional[PytestPluginManager]
|
||||
_config = None # type: Config
|
||||
_saved = (
|
||||
[]
|
||||
) # type: List[Tuple[Callable[..., None], Optional[PytestPluginManager], Config]]
|
||||
_pluginmanager: Optional[PytestPluginManager] = None
|
||||
_config: Optional[Config] = None
|
||||
_saved: List[
|
||||
Tuple[Callable[..., None], Optional[PytestPluginManager], Optional[Config]]
|
||||
] = []
|
||||
_recursive_debug = 0
|
||||
_wrapped_pdb_cls = None # type: Optional[Tuple[Type[Any], Type[Any]]]
|
||||
_wrapped_pdb_cls: Optional[Tuple[Type[Any], Type[Any]]] = None
|
||||
|
||||
@classmethod
|
||||
def _is_capturing(cls, capman: Optional["CaptureManager"]) -> Union[str, bool]:
|
||||
@@ -137,7 +136,7 @@ class pytestPDB:
|
||||
except Exception as exc:
|
||||
value = ":".join((modname, classname))
|
||||
raise UsageError(
|
||||
"--pdbcls: could not import {!r}: {}".format(value, exc)
|
||||
f"--pdbcls: could not import {value!r}: {exc}"
|
||||
) from exc
|
||||
else:
|
||||
import pdb
|
||||
@@ -167,6 +166,7 @@ class pytestPDB:
|
||||
def do_continue(self, arg):
|
||||
ret = super().do_continue(arg)
|
||||
if cls._recursive_debug == 0:
|
||||
assert cls._config is not None
|
||||
tw = _pytest.config.create_terminal_writer(cls._config)
|
||||
tw.line()
|
||||
|
||||
@@ -240,7 +240,7 @@ class pytestPDB:
|
||||
import _pytest.config
|
||||
|
||||
if cls._pluginmanager is None:
|
||||
capman = None # type: Optional[CaptureManager]
|
||||
capman: Optional[CaptureManager] = None
|
||||
else:
|
||||
capman = cls._pluginmanager.getplugin("capturemanager")
|
||||
if capman:
|
||||
@@ -258,7 +258,7 @@ class pytestPDB:
|
||||
else:
|
||||
capturing = cls._is_capturing(capman)
|
||||
if capturing == "global":
|
||||
tw.sep(">", "PDB {} (IO-capturing turned off)".format(method))
|
||||
tw.sep(">", f"PDB {method} (IO-capturing turned off)")
|
||||
elif capturing:
|
||||
tw.sep(
|
||||
">",
|
||||
@@ -266,7 +266,7 @@ class pytestPDB:
|
||||
% (method, capturing),
|
||||
)
|
||||
else:
|
||||
tw.sep(">", "PDB {}".format(method))
|
||||
tw.sep(">", f"PDB {method}")
|
||||
|
||||
_pdb = cls._import_pdb_cls(capman)(**kwargs)
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ All constants defined in this module should be either instances of
|
||||
:class:`PytestWarning`, or :class:`UnformattedWarning`
|
||||
in case of warnings which need to format their messages.
|
||||
"""
|
||||
from warnings import warn
|
||||
|
||||
from _pytest.warning_types import PytestDeprecationWarning
|
||||
from _pytest.warning_types import UnformattedWarning
|
||||
|
||||
@@ -20,9 +22,10 @@ DEPRECATED_EXTERNAL_PLUGINS = {
|
||||
}
|
||||
|
||||
|
||||
FILLFUNCARGS = PytestDeprecationWarning(
|
||||
"The `_fillfuncargs` function is deprecated, use "
|
||||
"function._request._fillfixtures() instead if you cannot avoid reaching into internals."
|
||||
FILLFUNCARGS = UnformattedWarning(
|
||||
PytestDeprecationWarning,
|
||||
"{name} is deprecated, use "
|
||||
"function._request._fillfixtures() instead if you cannot avoid reaching into internals.",
|
||||
)
|
||||
|
||||
PYTEST_COLLECT_MODULE = UnformattedWarning(
|
||||
@@ -31,6 +34,10 @@ PYTEST_COLLECT_MODULE = UnformattedWarning(
|
||||
"Please update to the new name.",
|
||||
)
|
||||
|
||||
YIELD_FIXTURE = PytestDeprecationWarning(
|
||||
"@pytest.yield_fixture is deprecated.\n"
|
||||
"Use @pytest.fixture instead; they are the same."
|
||||
)
|
||||
|
||||
MINUS_K_DASH = PytestDeprecationWarning(
|
||||
"The `-k '-expr'` syntax to -k is deprecated.\nUse `-k 'not expr'` instead."
|
||||
@@ -50,3 +57,31 @@ FSCOLLECTOR_GETHOOKPROXY_ISINITPATH = PytestDeprecationWarning(
|
||||
"The gethookproxy() and isinitpath() methods of FSCollector and Package are deprecated; "
|
||||
"use self.session.gethookproxy() and self.session.isinitpath() instead. "
|
||||
)
|
||||
|
||||
STRICT_OPTION = PytestDeprecationWarning(
|
||||
"The --strict option is deprecated, use --strict-markers instead."
|
||||
)
|
||||
|
||||
PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.")
|
||||
|
||||
|
||||
# You want to make some `__init__` or function "private".
|
||||
#
|
||||
# def my_private_function(some, args):
|
||||
# ...
|
||||
#
|
||||
# Do this:
|
||||
#
|
||||
# def my_private_function(some, args, *, _ispytest: bool = False):
|
||||
# check_ispytest(_ispytest)
|
||||
# ...
|
||||
#
|
||||
# Change all internal/allowed calls to
|
||||
#
|
||||
# my_private_function(some, args, _ispytest=True)
|
||||
#
|
||||
# All other calls will get the default _ispytest=False and trigger
|
||||
# the warning (possibly error in the future).
|
||||
def check_ispytest(ispytest: bool) -> None:
|
||||
if not ispytest:
|
||||
warn(PRIVATE, stacklevel=3)
|
||||
|
||||
@@ -17,6 +17,8 @@ from typing import Optional
|
||||
from typing import Pattern
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
import py.path
|
||||
@@ -28,7 +30,6 @@ from _pytest._code.code import ReprFileLocation
|
||||
from _pytest._code.code import TerminalRepr
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest.compat import safe_getattr
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.config import Config
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
@@ -40,7 +41,6 @@ from _pytest.warning_types import PytestWarning
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import doctest
|
||||
from typing import Type
|
||||
|
||||
DOCTEST_REPORT_CHOICE_NONE = "none"
|
||||
DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
|
||||
@@ -59,7 +59,7 @@ DOCTEST_REPORT_CHOICES = (
|
||||
# Lazy definition of runner class
|
||||
RUNNER_CLASS = None
|
||||
# Lazy definition of output checker class
|
||||
CHECKER_CLASS = None # type: Optional[Type[doctest.OutputChecker]]
|
||||
CHECKER_CLASS: Optional[Type["doctest.OutputChecker"]] = None
|
||||
|
||||
|
||||
def pytest_addoption(parser: Parser) -> None:
|
||||
@@ -124,10 +124,10 @@ def pytest_collect_file(
|
||||
config = parent.config
|
||||
if path.ext == ".py":
|
||||
if config.option.doctestmodules and not _is_setup_py(path):
|
||||
mod = DoctestModule.from_parent(parent, fspath=path) # type: DoctestModule
|
||||
mod: DoctestModule = DoctestModule.from_parent(parent, fspath=path)
|
||||
return mod
|
||||
elif _is_doctest(config, path, parent):
|
||||
txt = DoctestTextfile.from_parent(parent, fspath=path) # type: DoctestTextfile
|
||||
txt: DoctestTextfile = DoctestTextfile.from_parent(parent, fspath=path)
|
||||
return txt
|
||||
return None
|
||||
|
||||
@@ -163,12 +163,12 @@ class ReprFailDoctest(TerminalRepr):
|
||||
|
||||
|
||||
class MultipleDoctestFailures(Exception):
|
||||
def __init__(self, failures: "Sequence[doctest.DocTestFailure]") -> None:
|
||||
def __init__(self, failures: Sequence["doctest.DocTestFailure"]) -> None:
|
||||
super().__init__()
|
||||
self.failures = failures
|
||||
|
||||
|
||||
def _init_runner_class() -> "Type[doctest.DocTestRunner]":
|
||||
def _init_runner_class() -> Type["doctest.DocTestRunner"]:
|
||||
import doctest
|
||||
|
||||
class PytestDoctestRunner(doctest.DebugRunner):
|
||||
@@ -180,7 +180,7 @@ def _init_runner_class() -> "Type[doctest.DocTestRunner]":
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
checker: Optional[doctest.OutputChecker] = None,
|
||||
checker: Optional["doctest.OutputChecker"] = None,
|
||||
verbose: Optional[bool] = None,
|
||||
optionflags: int = 0,
|
||||
continue_on_failure: bool = True,
|
||||
@@ -204,7 +204,7 @@ def _init_runner_class() -> "Type[doctest.DocTestRunner]":
|
||||
out,
|
||||
test: "doctest.DocTest",
|
||||
example: "doctest.Example",
|
||||
exc_info: "Tuple[Type[BaseException], BaseException, types.TracebackType]",
|
||||
exc_info: Tuple[Type[BaseException], BaseException, types.TracebackType],
|
||||
) -> None:
|
||||
if isinstance(exc_info[1], OutcomeException):
|
||||
raise exc_info[1]
|
||||
@@ -251,7 +251,7 @@ class DoctestItem(pytest.Item):
|
||||
self.runner = runner
|
||||
self.dtest = dtest
|
||||
self.obj = None
|
||||
self.fixture_request = None # type: Optional[FixtureRequest]
|
||||
self.fixture_request: Optional[FixtureRequest] = None
|
||||
|
||||
@classmethod
|
||||
def from_parent( # type: ignore
|
||||
@@ -260,7 +260,7 @@ class DoctestItem(pytest.Item):
|
||||
*,
|
||||
name: str,
|
||||
runner: "doctest.DocTestRunner",
|
||||
dtest: "doctest.DocTest"
|
||||
dtest: "doctest.DocTest",
|
||||
):
|
||||
# incompatible signature due to to imposed limits on sublcass
|
||||
"""The public named constructor."""
|
||||
@@ -281,7 +281,7 @@ class DoctestItem(pytest.Item):
|
||||
assert self.runner is not None
|
||||
_check_all_skipped(self.dtest)
|
||||
self._disable_output_capturing_for_darwin()
|
||||
failures = [] # type: List[doctest.DocTestFailure]
|
||||
failures: List["doctest.DocTestFailure"] = []
|
||||
# Type ignored because we change the type of `out` from what
|
||||
# doctest expects.
|
||||
self.runner.run(self.dtest, out=failures) # type: ignore[arg-type]
|
||||
@@ -305,9 +305,9 @@ class DoctestItem(pytest.Item):
|
||||
) -> Union[str, TerminalRepr]:
|
||||
import doctest
|
||||
|
||||
failures = (
|
||||
None
|
||||
) # type: Optional[Sequence[Union[doctest.DocTestFailure, doctest.UnexpectedException]]]
|
||||
failures: Optional[
|
||||
Sequence[Union[doctest.DocTestFailure, doctest.UnexpectedException]]
|
||||
] = (None)
|
||||
if isinstance(
|
||||
excinfo.value, (doctest.DocTestFailure, doctest.UnexpectedException)
|
||||
):
|
||||
@@ -349,7 +349,7 @@ class DoctestItem(pytest.Item):
|
||||
]
|
||||
indent = ">>>"
|
||||
for line in example.source.splitlines():
|
||||
lines.append("??? {} {}".format(indent, line))
|
||||
lines.append(f"??? {indent} {line}")
|
||||
indent = "..."
|
||||
if isinstance(failure, doctest.DocTestFailure):
|
||||
lines += checker.output_difference(
|
||||
@@ -563,12 +563,12 @@ def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest:
|
||||
doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined]
|
||||
node=doctest_item, func=func, cls=None, funcargs=False
|
||||
)
|
||||
fixture_request = FixtureRequest(doctest_item)
|
||||
fixture_request = FixtureRequest(doctest_item, _ispytest=True)
|
||||
fixture_request._fillfixtures()
|
||||
return fixture_request
|
||||
|
||||
|
||||
def _init_checker_class() -> "Type[doctest.OutputChecker]":
|
||||
def _init_checker_class() -> Type["doctest.OutputChecker"]:
|
||||
import doctest
|
||||
import re
|
||||
|
||||
@@ -636,8 +636,8 @@ def _init_checker_class() -> "Type[doctest.OutputChecker]":
|
||||
return got
|
||||
offset = 0
|
||||
for w, g in zip(wants, gots):
|
||||
fraction = w.group("fraction") # type: Optional[str]
|
||||
exponent = w.group("exponent1") # type: Optional[str]
|
||||
fraction: Optional[str] = w.group("fraction")
|
||||
exponent: Optional[str] = w.group("exponent1")
|
||||
if exponent is None:
|
||||
exponent = w.group("exponent2")
|
||||
if fraction is None:
|
||||
|
||||
@@ -69,7 +69,12 @@ class FaultHandlerHooks:
|
||||
@staticmethod
|
||||
def _get_stderr_fileno():
|
||||
try:
|
||||
return sys.stderr.fileno()
|
||||
fileno = sys.stderr.fileno()
|
||||
# The Twisted Logger will return an invalid file descriptor since it is not backed
|
||||
# by an FD. So, let's also forward this to the same code path as with pytest-xdist.
|
||||
if fileno == -1:
|
||||
raise AttributeError()
|
||||
return fileno
|
||||
except (AttributeError, io.UnsupportedOperation):
|
||||
# pytest-xdist monkeypatches sys.stderr with an object that is not an actual file.
|
||||
# https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors
|
||||
|
||||
@@ -16,9 +16,12 @@ from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import overload
|
||||
from typing import Sequence
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
@@ -26,12 +29,14 @@ import attr
|
||||
import py
|
||||
|
||||
import _pytest
|
||||
from _pytest import nodes
|
||||
from _pytest._code import getfslineno
|
||||
from _pytest._code.code import FormattedExcinfo
|
||||
from _pytest._code.code import TerminalRepr
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest.compat import _format_args
|
||||
from _pytest.compat import _PytestWrapper
|
||||
from _pytest.compat import assert_never
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import get_real_func
|
||||
from _pytest.compat import get_real_method
|
||||
@@ -40,27 +45,26 @@ from _pytest.compat import getimfunc
|
||||
from _pytest.compat import getlocation
|
||||
from _pytest.compat import is_generator
|
||||
from _pytest.compat import NOTSET
|
||||
from _pytest.compat import order_preserving_dict
|
||||
from _pytest.compat import overload
|
||||
from _pytest.compat import safe_getattr
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
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
|
||||
from _pytest.mark.structures import MarkDecorator
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.outcomes import TEST_OUTCOME
|
||||
from _pytest.pathlib import absolutepath
|
||||
from _pytest.store import StoreKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Deque
|
||||
from typing import NoReturn
|
||||
from typing import Type
|
||||
from typing_extensions import Literal
|
||||
|
||||
from _pytest import nodes
|
||||
from _pytest.main import Session
|
||||
from _pytest.python import CallSpec2
|
||||
from _pytest.python import Function
|
||||
@@ -91,7 +95,7 @@ _FixtureCachedResult = Union[
|
||||
# Cache key.
|
||||
object,
|
||||
# Exc info if raised.
|
||||
Tuple["Type[BaseException]", BaseException, TracebackType],
|
||||
Tuple[Type[BaseException], BaseException, TracebackType],
|
||||
],
|
||||
]
|
||||
|
||||
@@ -103,24 +107,9 @@ class PseudoFixtureDef(Generic[_FixtureValue]):
|
||||
|
||||
|
||||
def pytest_sessionstart(session: "Session") -> None:
|
||||
import _pytest.python
|
||||
import _pytest.nodes
|
||||
|
||||
scopename2class.update(
|
||||
{
|
||||
"package": _pytest.python.Package,
|
||||
"class": _pytest.python.Class,
|
||||
"module": _pytest.python.Module,
|
||||
"function": _pytest.nodes.Item,
|
||||
"session": _pytest.main.Session,
|
||||
}
|
||||
)
|
||||
session._fixturemanager = FixtureManager(session)
|
||||
|
||||
|
||||
scopename2class = {} # type: Dict[str, Type[nodes.Node]]
|
||||
|
||||
|
||||
def get_scope_package(node, fixturedef: "FixtureDef[object]"):
|
||||
import pytest
|
||||
|
||||
@@ -136,15 +125,31 @@ def get_scope_package(node, fixturedef: "FixtureDef[object]"):
|
||||
return current
|
||||
|
||||
|
||||
def get_scope_node(node, scope):
|
||||
cls = scopename2class.get(scope)
|
||||
if cls is None:
|
||||
raise ValueError("unknown scope")
|
||||
return node.getparent(cls)
|
||||
def get_scope_node(
|
||||
node: nodes.Node, scope: "_Scope"
|
||||
) -> Optional[Union[nodes.Item, nodes.Collector]]:
|
||||
import _pytest.python
|
||||
|
||||
if scope == "function":
|
||||
return node.getparent(nodes.Item)
|
||||
elif scope == "class":
|
||||
return node.getparent(_pytest.python.Class)
|
||||
elif scope == "module":
|
||||
return node.getparent(_pytest.python.Module)
|
||||
elif scope == "package":
|
||||
return node.getparent(_pytest.python.Package)
|
||||
elif scope == "session":
|
||||
return node.getparent(_pytest.main.Session)
|
||||
else:
|
||||
assert_never(scope)
|
||||
|
||||
|
||||
# Used for storing artificial fixturedefs for direct parametrization.
|
||||
name2pseudofixturedef_key = StoreKey[Dict[str, "FixtureDef[Any]"]]()
|
||||
|
||||
|
||||
def add_funcarg_pseudo_fixture_def(
|
||||
collector, metafunc: "Metafunc", fixturemanager: "FixtureManager"
|
||||
collector: nodes.Collector, metafunc: "Metafunc", fixturemanager: "FixtureManager"
|
||||
) -> None:
|
||||
# This function will transform all collected calls to functions
|
||||
# if they use direct funcargs (i.e. direct parametrization)
|
||||
@@ -156,8 +161,8 @@ def add_funcarg_pseudo_fixture_def(
|
||||
# This function call does not have direct parametrization.
|
||||
return
|
||||
# Collect funcargs of all callspecs into a list of values.
|
||||
arg2params = {} # type: Dict[str, List[object]]
|
||||
arg2scope = {} # type: Dict[str, _Scope]
|
||||
arg2params: Dict[str, List[object]] = {}
|
||||
arg2scope: Dict[str, _Scope] = {}
|
||||
for callspec in metafunc._calls:
|
||||
for argname, argvalue in callspec.funcargs.items():
|
||||
assert argname not in callspec.params
|
||||
@@ -186,8 +191,15 @@ def add_funcarg_pseudo_fixture_def(
|
||||
assert scope == "class" and isinstance(collector, _pytest.python.Module)
|
||||
# Use module-level collector for class-scope (for now).
|
||||
node = collector
|
||||
if node and argname in node._name2pseudofixturedef:
|
||||
arg2fixturedefs[argname] = [node._name2pseudofixturedef[argname]]
|
||||
if node is None:
|
||||
name2pseudofixturedef = None
|
||||
else:
|
||||
default: Dict[str, FixtureDef[Any]] = {}
|
||||
name2pseudofixturedef = node._store.setdefault(
|
||||
name2pseudofixturedef_key, default
|
||||
)
|
||||
if name2pseudofixturedef is not None and argname in name2pseudofixturedef:
|
||||
arg2fixturedefs[argname] = [name2pseudofixturedef[argname]]
|
||||
else:
|
||||
fixturedef = FixtureDef(
|
||||
fixturemanager=fixturemanager,
|
||||
@@ -200,17 +212,17 @@ def add_funcarg_pseudo_fixture_def(
|
||||
ids=None,
|
||||
)
|
||||
arg2fixturedefs[argname] = [fixturedef]
|
||||
if node is not None:
|
||||
node._name2pseudofixturedef[argname] = fixturedef
|
||||
if name2pseudofixturedef is not None:
|
||||
name2pseudofixturedef[argname] = fixturedef
|
||||
|
||||
|
||||
def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
|
||||
"""Return fixturemarker or None if it doesn't exist or raised
|
||||
exceptions."""
|
||||
try:
|
||||
fixturemarker = getattr(
|
||||
fixturemarker: Optional[FixtureFunctionMarker] = getattr(
|
||||
obj, "_pytestfixturefunction", None
|
||||
) # type: Optional[FixtureFunctionMarker]
|
||||
)
|
||||
except TEST_OUTCOME:
|
||||
# some objects raise errors like request (from flask import request)
|
||||
# we don't expect them to be fixture functions
|
||||
@@ -222,7 +234,7 @@ def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
|
||||
_Key = Tuple[object, ...]
|
||||
|
||||
|
||||
def get_parametrized_fixture_keys(item: "nodes.Item", scopenum: int) -> Iterator[_Key]:
|
||||
def get_parametrized_fixture_keys(item: nodes.Item, scopenum: int) -> Iterator[_Key]:
|
||||
"""Return list of keys for all parametrized arguments which match
|
||||
the specified scope. """
|
||||
assert scopenum < scopenum_function # function
|
||||
@@ -231,7 +243,7 @@ def get_parametrized_fixture_keys(item: "nodes.Item", scopenum: int) -> Iterator
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
cs = callspec # type: CallSpec2
|
||||
cs: CallSpec2 = callspec
|
||||
# cs.indices.items() is random order of argnames. Need to
|
||||
# sort this so that different calls to
|
||||
# get_parametrized_fixture_keys will be deterministic.
|
||||
@@ -239,7 +251,7 @@ def get_parametrized_fixture_keys(item: "nodes.Item", scopenum: int) -> Iterator
|
||||
if cs._arg2scopenum[argname] != scopenum:
|
||||
continue
|
||||
if scopenum == 0: # session
|
||||
key = (argname, param_index) # type: _Key
|
||||
key: _Key = (argname, param_index)
|
||||
elif scopenum == 1: # package
|
||||
key = (argname, param_index, item.fspath.dirpath())
|
||||
elif scopenum == 2: # module
|
||||
@@ -256,37 +268,28 @@ def get_parametrized_fixture_keys(item: "nodes.Item", scopenum: int) -> Iterator
|
||||
# setups and teardowns.
|
||||
|
||||
|
||||
def reorder_items(items: "Sequence[nodes.Item]") -> "List[nodes.Item]":
|
||||
argkeys_cache = {} # type: Dict[int, Dict[nodes.Item, Dict[_Key, None]]]
|
||||
items_by_argkey = {} # type: Dict[int, Dict[_Key, Deque[nodes.Item]]]
|
||||
def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
|
||||
argkeys_cache: Dict[int, Dict[nodes.Item, Dict[_Key, None]]] = {}
|
||||
items_by_argkey: Dict[int, Dict[_Key, Deque[nodes.Item]]] = {}
|
||||
for scopenum in range(0, scopenum_function):
|
||||
d = {} # type: Dict[nodes.Item, Dict[_Key, None]]
|
||||
d: Dict[nodes.Item, Dict[_Key, None]] = {}
|
||||
argkeys_cache[scopenum] = d
|
||||
item_d = defaultdict(deque) # type: Dict[_Key, Deque[nodes.Item]]
|
||||
item_d: Dict[_Key, Deque[nodes.Item]] = defaultdict(deque)
|
||||
items_by_argkey[scopenum] = item_d
|
||||
for item in items:
|
||||
# cast is a workaround for https://github.com/python/typeshed/issues/3800.
|
||||
keys = cast(
|
||||
"Dict[_Key, None]",
|
||||
order_preserving_dict.fromkeys(
|
||||
get_parametrized_fixture_keys(item, scopenum), None
|
||||
),
|
||||
)
|
||||
keys = dict.fromkeys(get_parametrized_fixture_keys(item, scopenum), None)
|
||||
if keys:
|
||||
d[item] = keys
|
||||
for key in keys:
|
||||
item_d[key].append(item)
|
||||
# cast is a workaround for https://github.com/python/typeshed/issues/3800.
|
||||
items_dict = cast(
|
||||
"Dict[nodes.Item, None]", order_preserving_dict.fromkeys(items, None)
|
||||
)
|
||||
items_dict = dict.fromkeys(items, None)
|
||||
return list(reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, 0))
|
||||
|
||||
|
||||
def fix_cache_order(
|
||||
item: "nodes.Item",
|
||||
argkeys_cache: "Dict[int, Dict[nodes.Item, Dict[_Key, None]]]",
|
||||
items_by_argkey: "Dict[int, Dict[_Key, Deque[nodes.Item]]]",
|
||||
item: nodes.Item,
|
||||
argkeys_cache: Dict[int, Dict[nodes.Item, Dict[_Key, None]]],
|
||||
items_by_argkey: Dict[int, Dict[_Key, "Deque[nodes.Item]"]],
|
||||
) -> None:
|
||||
for scopenum in range(0, scopenum_function):
|
||||
for key in argkeys_cache[scopenum].get(item, []):
|
||||
@@ -294,26 +297,26 @@ def fix_cache_order(
|
||||
|
||||
|
||||
def reorder_items_atscope(
|
||||
items: "Dict[nodes.Item, None]",
|
||||
argkeys_cache: "Dict[int, Dict[nodes.Item, Dict[_Key, None]]]",
|
||||
items_by_argkey: "Dict[int, Dict[_Key, Deque[nodes.Item]]]",
|
||||
items: Dict[nodes.Item, None],
|
||||
argkeys_cache: Dict[int, Dict[nodes.Item, Dict[_Key, None]]],
|
||||
items_by_argkey: Dict[int, Dict[_Key, "Deque[nodes.Item]"]],
|
||||
scopenum: int,
|
||||
) -> "Dict[nodes.Item, None]":
|
||||
) -> Dict[nodes.Item, None]:
|
||||
if scopenum >= scopenum_function or len(items) < 3:
|
||||
return items
|
||||
ignore = set() # type: Set[Optional[_Key]]
|
||||
ignore: Set[Optional[_Key]] = set()
|
||||
items_deque = deque(items)
|
||||
items_done = order_preserving_dict() # type: Dict[nodes.Item, None]
|
||||
items_done: Dict[nodes.Item, None] = {}
|
||||
scoped_items_by_argkey = items_by_argkey[scopenum]
|
||||
scoped_argkeys_cache = argkeys_cache[scopenum]
|
||||
while items_deque:
|
||||
no_argkey_group = order_preserving_dict() # type: Dict[nodes.Item, None]
|
||||
no_argkey_group: Dict[nodes.Item, None] = {}
|
||||
slicing_argkey = None
|
||||
while items_deque:
|
||||
item = items_deque.popleft()
|
||||
if item in items_done or item in no_argkey_group:
|
||||
continue
|
||||
argkeys = order_preserving_dict.fromkeys(
|
||||
argkeys = dict.fromkeys(
|
||||
(k for k in scoped_argkeys_cache.get(item, []) if k not in ignore), None
|
||||
)
|
||||
if not argkeys:
|
||||
@@ -339,9 +342,22 @@ 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 funcargs for a test function."""
|
||||
warnings.warn(FILLFUNCARGS, stacklevel=2)
|
||||
"""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:
|
||||
@@ -352,7 +368,7 @@ def fillfixtures(function: "Function") -> None:
|
||||
assert function.parent is not None
|
||||
fi = fm.getfixtureinfo(function.parent, function.obj, None)
|
||||
function._fixtureinfo = fi
|
||||
request = function._request = FixtureRequest(function)
|
||||
request = function._request = FixtureRequest(function, _ispytest=True)
|
||||
request._fillfixtures()
|
||||
# Prune out funcargs for jstests.
|
||||
newfuncargs = {}
|
||||
@@ -389,7 +405,7 @@ class FuncFixtureInfo:
|
||||
tree. In this way the dependency tree can get pruned, and the closure
|
||||
of argnames may get reduced.
|
||||
"""
|
||||
closure = set() # type: Set[str]
|
||||
closure: Set[str] = set()
|
||||
working_set = set(self.initialnames)
|
||||
while working_set:
|
||||
argname = working_set.pop()
|
||||
@@ -414,19 +430,18 @@ class FixtureRequest:
|
||||
indirectly.
|
||||
"""
|
||||
|
||||
def __init__(self, pyfuncitem) -> None:
|
||||
def __init__(self, pyfuncitem, *, _ispytest: bool = False) -> None:
|
||||
check_ispytest(_ispytest)
|
||||
self._pyfuncitem = pyfuncitem
|
||||
#: Fixture for which this request is being performed.
|
||||
self.fixturename = None # type: Optional[str]
|
||||
self.fixturename: Optional[str] = None
|
||||
#: Scope string, one of "function", "class", "module", "session".
|
||||
self.scope = "function" # type: _Scope
|
||||
self._fixture_defs = {} # type: Dict[str, FixtureDef[Any]]
|
||||
fixtureinfo = pyfuncitem._fixtureinfo # type: FuncFixtureInfo
|
||||
self.scope: _Scope = "function"
|
||||
self._fixture_defs: Dict[str, FixtureDef[Any]] = {}
|
||||
fixtureinfo: FuncFixtureInfo = pyfuncitem._fixtureinfo
|
||||
self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy()
|
||||
self._arg2index = {} # type: Dict[str, int]
|
||||
self._fixturemanager = (
|
||||
pyfuncitem.session._fixturemanager
|
||||
) # type: FixtureManager
|
||||
self._arg2index: Dict[str, int] = {}
|
||||
self._fixturemanager: FixtureManager = (pyfuncitem.session._fixturemanager)
|
||||
|
||||
@property
|
||||
def fixturenames(self) -> List[str]:
|
||||
@@ -462,14 +477,14 @@ class FixtureRequest:
|
||||
@property
|
||||
def config(self) -> Config:
|
||||
"""The pytest config object associated with this request."""
|
||||
return self._pyfuncitem.config # type: ignore[no-any-return] # noqa: F723
|
||||
return self._pyfuncitem.config # type: ignore[no-any-return]
|
||||
|
||||
@property
|
||||
def function(self):
|
||||
"""Test function object if the request has a per-function scope."""
|
||||
if self.scope != "function":
|
||||
raise AttributeError(
|
||||
"function not available in {}-scoped context".format(self.scope)
|
||||
f"function not available in {self.scope}-scoped context"
|
||||
)
|
||||
return self._pyfuncitem.obj
|
||||
|
||||
@@ -477,9 +492,7 @@ class FixtureRequest:
|
||||
def cls(self):
|
||||
"""Class (can be None) where the test function was collected."""
|
||||
if self.scope not in ("class", "function"):
|
||||
raise AttributeError(
|
||||
"cls not available in {}-scoped context".format(self.scope)
|
||||
)
|
||||
raise AttributeError(f"cls not available in {self.scope}-scoped context")
|
||||
clscol = self._pyfuncitem.getparent(_pytest.python.Class)
|
||||
if clscol:
|
||||
return clscol.obj
|
||||
@@ -498,18 +511,14 @@ class FixtureRequest:
|
||||
def module(self):
|
||||
"""Python module object where the test function was collected."""
|
||||
if self.scope not in ("function", "class", "module"):
|
||||
raise AttributeError(
|
||||
"module not available in {}-scoped context".format(self.scope)
|
||||
)
|
||||
raise AttributeError(f"module not available in {self.scope}-scoped context")
|
||||
return self._pyfuncitem.getparent(_pytest.python.Module).obj
|
||||
|
||||
@property
|
||||
def fspath(self) -> py.path.local:
|
||||
"""The file system path of the test module which collected this test."""
|
||||
if self.scope not in ("function", "class", "module", "package"):
|
||||
raise AttributeError(
|
||||
"module not available in {}-scoped context".format(self.scope)
|
||||
)
|
||||
raise AttributeError(f"module not available in {self.scope}-scoped context")
|
||||
# TODO: Remove ignore once _pyfuncitem is properly typed.
|
||||
return self._pyfuncitem.fspath # type: ignore
|
||||
|
||||
@@ -519,9 +528,9 @@ class FixtureRequest:
|
||||
return self.node.keywords
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
def session(self) -> "Session":
|
||||
"""Pytest session object."""
|
||||
return self._pyfuncitem.session
|
||||
return self._pyfuncitem.session # type: ignore[no-any-return]
|
||||
|
||||
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
|
||||
"""Add finalizer/teardown function to be called after the last test
|
||||
@@ -535,7 +544,7 @@ class FixtureRequest:
|
||||
finalizer=finalizer, colitem=colitem
|
||||
)
|
||||
|
||||
def applymarker(self, marker) -> None:
|
||||
def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
|
||||
"""Apply a marker to a single test function invocation.
|
||||
|
||||
This method is useful if you don't want to have a keyword/marker
|
||||
@@ -584,7 +593,7 @@ class FixtureRequest:
|
||||
except FixtureLookupError:
|
||||
if argname == "request":
|
||||
cached_result = (self, [0], None)
|
||||
scope = "function" # type: _Scope
|
||||
scope: _Scope = "function"
|
||||
return PseudoFixtureDef(cached_result, scope)
|
||||
raise
|
||||
# Remove indent to prevent the python3 exception
|
||||
@@ -595,7 +604,7 @@ class FixtureRequest:
|
||||
|
||||
def _get_fixturestack(self) -> List["FixtureDef[Any]"]:
|
||||
current = self
|
||||
values = [] # type: List[FixtureDef[Any]]
|
||||
values: List[FixtureDef[Any]] = []
|
||||
while 1:
|
||||
fixturedef = getattr(current, "_fixturedef", None)
|
||||
if fixturedef is None:
|
||||
@@ -667,7 +676,9 @@ class FixtureRequest:
|
||||
if paramscopenum is not None:
|
||||
scope = scopes[paramscopenum]
|
||||
|
||||
subrequest = SubRequest(self, scope, param, param_index, fixturedef)
|
||||
subrequest = SubRequest(
|
||||
self, scope, param, param_index, fixturedef, _ispytest=True
|
||||
)
|
||||
|
||||
# Check if a higher-level scoped fixture accesses a lower level one.
|
||||
subrequest._check_scope(argname, self.scope, scope)
|
||||
@@ -685,7 +696,9 @@ class FixtureRequest:
|
||||
functools.partial(fixturedef.finish, request=subrequest), subrequest.node
|
||||
)
|
||||
|
||||
def _check_scope(self, argname, invoking_scope: "_Scope", requested_scope) -> None:
|
||||
def _check_scope(
|
||||
self, argname: str, invoking_scope: "_Scope", requested_scope: "_Scope",
|
||||
) -> None:
|
||||
if argname == "request":
|
||||
return
|
||||
if scopemismatch(invoking_scope, requested_scope):
|
||||
@@ -709,11 +722,11 @@ class FixtureRequest:
|
||||
lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args))
|
||||
return lines
|
||||
|
||||
def _getscopeitem(self, scope):
|
||||
def _getscopeitem(self, scope: "_Scope") -> Union[nodes.Item, nodes.Collector]:
|
||||
if scope == "function":
|
||||
# This might also be a non-function Item despite its attribute name.
|
||||
return self._pyfuncitem
|
||||
if scope == "package":
|
||||
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
|
||||
elif scope == "package":
|
||||
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
|
||||
# but on FixtureRequest (a subclass).
|
||||
node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
|
||||
@@ -742,7 +755,10 @@ class SubRequest(FixtureRequest):
|
||||
param,
|
||||
param_index: int,
|
||||
fixturedef: "FixtureDef[object]",
|
||||
*,
|
||||
_ispytest: bool = False,
|
||||
) -> None:
|
||||
check_ispytest(_ispytest)
|
||||
self._parent_request = request
|
||||
self.fixturename = fixturedef.argname
|
||||
if param is not NOTSET:
|
||||
@@ -757,9 +773,11 @@ class SubRequest(FixtureRequest):
|
||||
self._fixturemanager = request._fixturemanager
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<SubRequest {!r} for {!r}>".format(self.fixturename, self._pyfuncitem)
|
||||
return f"<SubRequest {self.fixturename!r} for {self._pyfuncitem!r}>"
|
||||
|
||||
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
|
||||
"""Add finalizer/teardown function to be called after the last test
|
||||
within the requesting test context finished execution."""
|
||||
self._fixturedef.addfinalizer(finalizer)
|
||||
|
||||
def _schedule_finalizers(
|
||||
@@ -775,7 +793,7 @@ class SubRequest(FixtureRequest):
|
||||
super()._schedule_finalizers(fixturedef, subrequest)
|
||||
|
||||
|
||||
scopes = ["session", "package", "module", "class", "function"] # type: List[_Scope]
|
||||
scopes: List["_Scope"] = ["session", "package", "module", "class", "function"]
|
||||
scopenum_function = scopes.index("function")
|
||||
|
||||
|
||||
@@ -786,13 +804,13 @@ def scopemismatch(currentscope: "_Scope", newscope: "_Scope") -> bool:
|
||||
def scope2index(scope: str, descr: str, where: Optional[str] = None) -> int:
|
||||
"""Look up the index of ``scope`` and raise a descriptive value error
|
||||
if not defined."""
|
||||
strscopes = scopes # type: Sequence[str]
|
||||
strscopes: Sequence[str] = scopes
|
||||
try:
|
||||
return strscopes.index(scope)
|
||||
except ValueError:
|
||||
fail(
|
||||
"{} {}got an unexpected scope value '{}'".format(
|
||||
descr, "from {} ".format(where) if where else "", scope
|
||||
descr, f"from {where} " if where else "", scope
|
||||
),
|
||||
pytrace=False,
|
||||
)
|
||||
@@ -811,7 +829,7 @@ class FixtureLookupError(LookupError):
|
||||
self.msg = msg
|
||||
|
||||
def formatrepr(self) -> "FixtureLookupErrorRepr":
|
||||
tblines = [] # type: List[str]
|
||||
tblines: List[str] = []
|
||||
addline = tblines.append
|
||||
stack = [self.request._pyfuncitem.obj]
|
||||
stack.extend(map(lambda x: x.func, self.fixturestack))
|
||||
@@ -848,7 +866,7 @@ class FixtureLookupError(LookupError):
|
||||
self.argname
|
||||
)
|
||||
else:
|
||||
msg = "fixture '{}' not found".format(self.argname)
|
||||
msg = f"fixture '{self.argname}' not found"
|
||||
msg += "\n available fixtures: {}".format(", ".join(sorted(available)))
|
||||
msg += "\n use 'pytest --fixtures [testpath]' for help on them."
|
||||
|
||||
@@ -882,8 +900,7 @@ class FixtureLookupErrorRepr(TerminalRepr):
|
||||
)
|
||||
for line in lines[1:]:
|
||||
tw.line(
|
||||
"{} {}".format(FormattedExcinfo.flow_marker, line.strip()),
|
||||
red=True,
|
||||
f"{FormattedExcinfo.flow_marker} {line.strip()}", red=True,
|
||||
)
|
||||
tw.line()
|
||||
tw.line("%s:%d" % (self.filename, self.firstlineno + 1))
|
||||
@@ -907,9 +924,7 @@ def call_fixture_func(
|
||||
try:
|
||||
fixture_result = next(generator)
|
||||
except StopIteration:
|
||||
raise ValueError(
|
||||
"{} did not yield a value".format(request.fixturename)
|
||||
) from None
|
||||
raise ValueError(f"{request.fixturename} did not yield a value") from None
|
||||
finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, generator)
|
||||
request.addfinalizer(finalizer)
|
||||
else:
|
||||
@@ -962,7 +977,7 @@ class FixtureDef(Generic[_FixtureValue]):
|
||||
def __init__(
|
||||
self,
|
||||
fixturemanager: "FixtureManager",
|
||||
baseid,
|
||||
baseid: Optional[str],
|
||||
argname: str,
|
||||
func: "_FixtureFunc[_FixtureValue]",
|
||||
scope: "Union[_Scope, Callable[[str, Config], _Scope]]",
|
||||
@@ -987,18 +1002,18 @@ class FixtureDef(Generic[_FixtureValue]):
|
||||
self.scopenum = scope2index(
|
||||
# TODO: Check if the `or` here is really necessary.
|
||||
scope_ or "function", # type: ignore[unreachable]
|
||||
descr="Fixture '{}'".format(func.__name__),
|
||||
descr=f"Fixture '{func.__name__}'",
|
||||
where=baseid,
|
||||
)
|
||||
self.scope = scope_
|
||||
self.params = params # type: Optional[Sequence[object]]
|
||||
self.argnames = getfuncargnames(
|
||||
self.params: Optional[Sequence[object]] = params
|
||||
self.argnames: Tuple[str, ...] = getfuncargnames(
|
||||
func, name=argname, is_method=unittest
|
||||
) # type: Tuple[str, ...]
|
||||
)
|
||||
self.unittest = unittest
|
||||
self.ids = ids
|
||||
self.cached_result = None # type: Optional[_FixtureCachedResult[_FixtureValue]]
|
||||
self._finalizers = [] # type: List[Callable[[], object]]
|
||||
self.cached_result: Optional[_FixtureCachedResult[_FixtureValue]] = None
|
||||
self._finalizers: List[Callable[[], object]] = []
|
||||
|
||||
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
|
||||
self._finalizers.append(finalizer)
|
||||
@@ -1144,7 +1159,9 @@ def _params_converter(
|
||||
return tuple(params) if params is not None else None
|
||||
|
||||
|
||||
def wrap_function_to_error_out_if_called_directly(function, fixture_marker):
|
||||
def wrap_function_to_error_out_if_called_directly(
|
||||
function: _FixtureFunction, fixture_marker: "FixtureFunctionMarker",
|
||||
) -> _FixtureFunction:
|
||||
"""Wrap the given fixture function so we can raise an error about it being called directly,
|
||||
instead of used as an argument in a test function."""
|
||||
message = (
|
||||
@@ -1162,7 +1179,7 @@ def wrap_function_to_error_out_if_called_directly(function, fixture_marker):
|
||||
# further than this point and lose useful wrappings like @mock.patch (#3774).
|
||||
result.__pytest_wrapped__ = _PytestWrapper(function) # type: ignore[attr-defined]
|
||||
|
||||
return result
|
||||
return cast(_FixtureFunction, result)
|
||||
|
||||
|
||||
@final
|
||||
@@ -1220,13 +1237,13 @@ def fixture(
|
||||
Callable[[Any], Optional[object]],
|
||||
]
|
||||
] = ...,
|
||||
name: Optional[str] = ...
|
||||
name: Optional[str] = ...,
|
||||
) -> _FixtureFunction:
|
||||
...
|
||||
|
||||
|
||||
@overload # noqa: F811
|
||||
def fixture( # noqa: F811
|
||||
@overload
|
||||
def fixture(
|
||||
fixture_function: None = ...,
|
||||
*,
|
||||
scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = ...,
|
||||
@@ -1238,12 +1255,12 @@ def fixture( # noqa: F811
|
||||
Callable[[Any], Optional[object]],
|
||||
]
|
||||
] = ...,
|
||||
name: Optional[str] = None
|
||||
name: Optional[str] = None,
|
||||
) -> FixtureFunctionMarker:
|
||||
...
|
||||
|
||||
|
||||
def fixture( # noqa: F811
|
||||
def fixture(
|
||||
fixture_function: Optional[_FixtureFunction] = None,
|
||||
*,
|
||||
scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = "function",
|
||||
@@ -1255,7 +1272,7 @@ def fixture( # noqa: F811
|
||||
Callable[[Any], Optional[object]],
|
||||
]
|
||||
] = None,
|
||||
name: Optional[str] = None
|
||||
name: Optional[str] = None,
|
||||
) -> Union[FixtureFunctionMarker, _FixtureFunction]:
|
||||
"""Decorator to mark a fixture factory function.
|
||||
|
||||
@@ -1325,13 +1342,14 @@ def yield_fixture(
|
||||
params=None,
|
||||
autouse=False,
|
||||
ids=None,
|
||||
name=None
|
||||
name=None,
|
||||
):
|
||||
"""(Return a) decorator to mark a yield-fixture factory function.
|
||||
|
||||
.. deprecated:: 3.0
|
||||
Use :py:func:`pytest.fixture` directly instead.
|
||||
"""
|
||||
warnings.warn(YIELD_FIXTURE, stacklevel=2)
|
||||
return fixture(
|
||||
fixture_function,
|
||||
*args,
|
||||
@@ -1402,15 +1420,16 @@ class FixtureManager:
|
||||
|
||||
def __init__(self, session: "Session") -> None:
|
||||
self.session = session
|
||||
self.config = session.config # type: Config
|
||||
self._arg2fixturedefs = {} # type: Dict[str, List[FixtureDef[Any]]]
|
||||
self._holderobjseen = set() # type: Set[object]
|
||||
self._nodeid_and_autousenames = [
|
||||
("", self.config.getini("usefixtures"))
|
||||
] # type: List[Tuple[str, List[str]]]
|
||||
self.config: Config = session.config
|
||||
self._arg2fixturedefs: Dict[str, List[FixtureDef[Any]]] = {}
|
||||
self._holderobjseen: Set[object] = set()
|
||||
# A mapping from a nodeid to a list of autouse fixtures it defines.
|
||||
self._nodeid_autousenames: Dict[str, List[str]] = {
|
||||
"": self.config.getini("usefixtures"),
|
||||
}
|
||||
session.config.pluginmanager.register(self, "funcmanage")
|
||||
|
||||
def _get_direct_parametrize_args(self, node: "nodes.Node") -> List[str]:
|
||||
def _get_direct_parametrize_args(self, node: nodes.Node) -> List[str]:
|
||||
"""Return all direct parametrization arguments of a node, so we don't
|
||||
mistake them for fixtures.
|
||||
|
||||
@@ -1419,7 +1438,7 @@ class FixtureManager:
|
||||
These things are done later as well when dealing with parametrization
|
||||
so this could be improved.
|
||||
"""
|
||||
parametrize_argnames = [] # type: List[str]
|
||||
parametrize_argnames: List[str] = []
|
||||
for marker in node.iter_markers(name="parametrize"):
|
||||
if not marker.kwargs.get("indirect", False):
|
||||
p_argnames, _ = ParameterSet._parse_parametrize_args(
|
||||
@@ -1430,7 +1449,7 @@ class FixtureManager:
|
||||
return parametrize_argnames
|
||||
|
||||
def getfixtureinfo(
|
||||
self, node: "nodes.Node", func, cls, funcargs: bool = True
|
||||
self, node: nodes.Node, func, cls, funcargs: bool = True
|
||||
) -> FuncFixtureInfo:
|
||||
if funcargs and not getattr(node, "nofuncargs", False):
|
||||
argnames = getfuncargnames(func, name=node.name, cls=cls)
|
||||
@@ -1454,8 +1473,6 @@ class FixtureManager:
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
from _pytest import nodes
|
||||
|
||||
# Construct the base nodeid which is later used to check
|
||||
# what fixtures are visible for particular tests (as denoted
|
||||
# by their test id).
|
||||
@@ -1471,21 +1488,18 @@ class FixtureManager:
|
||||
|
||||
self.parsefactories(plugin, nodeid)
|
||||
|
||||
def _getautousenames(self, nodeid: str) -> List[str]:
|
||||
"""Return a list of fixture names to be used."""
|
||||
autousenames = [] # type: List[str]
|
||||
for baseid, basenames in self._nodeid_and_autousenames:
|
||||
if nodeid.startswith(baseid):
|
||||
if baseid:
|
||||
i = len(baseid)
|
||||
nextchar = nodeid[i : i + 1]
|
||||
if nextchar and nextchar not in ":/":
|
||||
continue
|
||||
autousenames.extend(basenames)
|
||||
return autousenames
|
||||
def _getautousenames(self, nodeid: str) -> Iterator[str]:
|
||||
"""Return the names of autouse fixtures applicable to nodeid."""
|
||||
for parentnodeid in nodes.iterparentnodeids(nodeid):
|
||||
basenames = self._nodeid_autousenames.get(parentnodeid)
|
||||
if basenames:
|
||||
yield from basenames
|
||||
|
||||
def getfixtureclosure(
|
||||
self, fixturenames: Tuple[str, ...], parentnode, ignore_args: Sequence[str] = ()
|
||||
self,
|
||||
fixturenames: Tuple[str, ...],
|
||||
parentnode: nodes.Node,
|
||||
ignore_args: Sequence[str] = (),
|
||||
) -> Tuple[Tuple[str, ...], List[str], Dict[str, Sequence[FixtureDef[Any]]]]:
|
||||
# Collect the closure of all fixtures, starting with the given
|
||||
# fixturenames as the initial set. As we have to visit all
|
||||
@@ -1495,7 +1509,7 @@ class FixtureManager:
|
||||
# (discovering matching fixtures for a given name/node is expensive).
|
||||
|
||||
parentid = parentnode.nodeid
|
||||
fixturenames_closure = self._getautousenames(parentid)
|
||||
fixturenames_closure = list(self._getautousenames(parentid))
|
||||
|
||||
def merge(otherlist: Iterable[str]) -> None:
|
||||
for arg in otherlist:
|
||||
@@ -1509,7 +1523,7 @@ class FixtureManager:
|
||||
# need to return it as well, so save this.
|
||||
initialnames = tuple(fixturenames_closure)
|
||||
|
||||
arg2fixturedefs = {} # type: Dict[str, Sequence[FixtureDef[Any]]]
|
||||
arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {}
|
||||
lastlen = -1
|
||||
while lastlen != len(fixturenames_closure):
|
||||
lastlen = len(fixturenames_closure)
|
||||
@@ -1579,7 +1593,7 @@ class FixtureManager:
|
||||
|
||||
# Try next super fixture, if any.
|
||||
|
||||
def pytest_collection_modifyitems(self, items: "List[nodes.Item]") -> None:
|
||||
def pytest_collection_modifyitems(self, items: List[nodes.Item]) -> None:
|
||||
# Separate parametrized setups.
|
||||
items[:] = reorder_items(items)
|
||||
|
||||
@@ -1640,7 +1654,7 @@ class FixtureManager:
|
||||
autousenames.append(name)
|
||||
|
||||
if autousenames:
|
||||
self._nodeid_and_autousenames.append((nodeid or "", autousenames))
|
||||
self._nodeid_autousenames.setdefault(nodeid or "", []).extend(autousenames)
|
||||
|
||||
def getfixturedefs(
|
||||
self, argname: str, nodeid: str
|
||||
@@ -1660,8 +1674,7 @@ class FixtureManager:
|
||||
def _matchfactories(
|
||||
self, fixturedefs: Iterable[FixtureDef[Any]], nodeid: str
|
||||
) -> Iterator[FixtureDef[Any]]:
|
||||
from _pytest import nodes
|
||||
|
||||
parentnodeids = set(nodes.iterparentnodeids(nodeid))
|
||||
for fixturedef in fixturedefs:
|
||||
if nodes.ischildnode(fixturedef.baseid, nodeid):
|
||||
if fixturedef.baseid in parentnodeids:
|
||||
yield fixturedef
|
||||
|
||||
@@ -97,7 +97,7 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_cmdline_parse():
|
||||
outcome = yield
|
||||
config = outcome.get_result() # type: Config
|
||||
config: Config = outcome.get_result()
|
||||
if config.option.debug:
|
||||
path = os.path.abspath("pytestdebug.log")
|
||||
debugfile = open(path, "w")
|
||||
@@ -137,7 +137,7 @@ def showversion(config: Config) -> None:
|
||||
for line in plugininfo:
|
||||
sys.stderr.write(line + "\n")
|
||||
else:
|
||||
sys.stderr.write("pytest {}\n".format(pytest.__version__))
|
||||
sys.stderr.write(f"pytest {pytest.__version__}\n")
|
||||
|
||||
|
||||
def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
|
||||
@@ -172,8 +172,8 @@ def showhelp(config: Config) -> None:
|
||||
if type is None:
|
||||
type = "string"
|
||||
if help is None:
|
||||
raise TypeError("help argument cannot be None for {}".format(name))
|
||||
spec = "{} ({}):".format(name, type)
|
||||
raise TypeError(f"help argument cannot be None for {name}")
|
||||
spec = f"{name} ({type}):"
|
||||
tw.write(" %s" % spec)
|
||||
spec_len = len(spec)
|
||||
if spec_len > (indent_len - 3):
|
||||
@@ -208,7 +208,7 @@ def showhelp(config: Config) -> None:
|
||||
("PYTEST_DEBUG", "set to enable debug tracing of pytest's internals"),
|
||||
]
|
||||
for name, help in vars:
|
||||
tw.line(" {:<24} {}".format(name, help))
|
||||
tw.line(f" {name:<24} {help}")
|
||||
tw.line()
|
||||
tw.line()
|
||||
|
||||
@@ -235,7 +235,7 @@ def getpluginversioninfo(config: Config) -> List[str]:
|
||||
lines.append("setuptools registered plugins:")
|
||||
for plugin, dist in plugininfo:
|
||||
loc = getattr(plugin, "__file__", repr(plugin))
|
||||
content = "{}-{} at {}".format(dist.project_name, dist.version, loc)
|
||||
content = f"{dist.project_name}-{dist.version} at {loc}"
|
||||
lines.append(" " + content)
|
||||
return lines
|
||||
|
||||
@@ -243,9 +243,7 @@ def getpluginversioninfo(config: Config) -> List[str]:
|
||||
def pytest_report_header(config: Config) -> List[str]:
|
||||
lines = []
|
||||
if config.option.debug or config.option.traceconfig:
|
||||
lines.append(
|
||||
"using: pytest-{} pylib-{}".format(pytest.__version__, py.__version__)
|
||||
)
|
||||
lines.append(f"using: pytest-{pytest.__version__} pylib-{py.__version__}")
|
||||
|
||||
verinfo = getpluginversioninfo(config)
|
||||
if verinfo:
|
||||
@@ -259,5 +257,5 @@ def pytest_report_header(config: Config) -> List[str]:
|
||||
r = plugin.__file__
|
||||
else:
|
||||
r = repr(plugin)
|
||||
lines.append(" {:<20}: {}".format(name, r))
|
||||
lines.append(f" {name:<20}: {r}")
|
||||
return lines
|
||||
|
||||
@@ -7,12 +7,12 @@ from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
import py.path
|
||||
from pluggy import HookspecMarker
|
||||
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.deprecated import WARNING_CAPTURED_HOOK
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -446,7 +446,7 @@ def pytest_runtest_logstart(
|
||||
See :func:`pytest_runtest_protocol` for a description of the runtest protocol.
|
||||
|
||||
:param str nodeid: Full node ID of the item.
|
||||
:param location: A triple of ``(filename, lineno, testname)``.
|
||||
:param location: A tuple of ``(filename, lineno, testname)``.
|
||||
"""
|
||||
|
||||
|
||||
@@ -458,7 +458,7 @@ def pytest_runtest_logfinish(
|
||||
See :func:`pytest_runtest_protocol` for a description of the runtest protocol.
|
||||
|
||||
:param str nodeid: Full node ID of the item.
|
||||
:param location: A triple of ``(filename, lineno, testname)``.
|
||||
:param location: A tuple of ``(filename, lineno, testname)``.
|
||||
"""
|
||||
|
||||
|
||||
@@ -808,6 +808,27 @@ def pytest_warning_recorded(
|
||||
"""
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Hooks for influencing skipping
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
def pytest_markeval_namespace(config: "Config") -> Dict[str, Any]:
|
||||
"""Called when constructing the globals dictionary used for
|
||||
evaluating string conditions in xfail/skipif markers.
|
||||
|
||||
This is useful when the condition for a marker requires
|
||||
objects that are expensive or impossible to obtain during
|
||||
collection time, which is required by normal boolean
|
||||
conditions.
|
||||
|
||||
.. versionadded:: 6.2
|
||||
|
||||
:param _pytest.config.Config config: The pytest config object.
|
||||
:returns: A dictionary of additional globals to add.
|
||||
"""
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# error handling and internal debugging hooks
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@@ -93,9 +93,9 @@ class _NodeReporter:
|
||||
self.add_stats = self.xml.add_stats
|
||||
self.family = self.xml.family
|
||||
self.duration = 0
|
||||
self.properties = [] # type: List[Tuple[str, str]]
|
||||
self.nodes = [] # type: List[ET.Element]
|
||||
self.attrs = {} # type: Dict[str, str]
|
||||
self.properties: List[Tuple[str, str]] = []
|
||||
self.nodes: List[ET.Element] = []
|
||||
self.attrs: Dict[str, str] = {}
|
||||
|
||||
def append(self, node: ET.Element) -> None:
|
||||
self.xml.add_stats(node.tag)
|
||||
@@ -122,11 +122,11 @@ class _NodeReporter:
|
||||
classnames = names[:-1]
|
||||
if self.xml.prefix:
|
||||
classnames.insert(0, self.xml.prefix)
|
||||
attrs = {
|
||||
attrs: Dict[str, str] = {
|
||||
"classname": ".".join(classnames),
|
||||
"name": bin_xml_escape(names[-1]),
|
||||
"file": testreport.location[0],
|
||||
} # type: Dict[str, str]
|
||||
}
|
||||
if testreport.location[1] is not None:
|
||||
attrs["line"] = str(testreport.location[1])
|
||||
if hasattr(testreport, "url"):
|
||||
@@ -199,9 +199,9 @@ class _NodeReporter:
|
||||
self._add_simple("skipped", "xfail-marked test passes unexpectedly")
|
||||
else:
|
||||
assert report.longrepr is not None
|
||||
reprcrash = getattr(
|
||||
reprcrash: Optional[ReprFileLocation] = getattr(
|
||||
report.longrepr, "reprcrash", None
|
||||
) # type: Optional[ReprFileLocation]
|
||||
)
|
||||
if reprcrash is not None:
|
||||
message = reprcrash.message
|
||||
else:
|
||||
@@ -219,18 +219,18 @@ class _NodeReporter:
|
||||
|
||||
def append_error(self, report: TestReport) -> None:
|
||||
assert report.longrepr is not None
|
||||
reprcrash = getattr(
|
||||
reprcrash: Optional[ReprFileLocation] = getattr(
|
||||
report.longrepr, "reprcrash", None
|
||||
) # type: Optional[ReprFileLocation]
|
||||
)
|
||||
if reprcrash is not None:
|
||||
reason = reprcrash.message
|
||||
else:
|
||||
reason = str(report.longrepr)
|
||||
|
||||
if report.when == "teardown":
|
||||
msg = 'failed on teardown with "{}"'.format(reason)
|
||||
msg = f'failed on teardown with "{reason}"'
|
||||
else:
|
||||
msg = 'failed on setup with "{}"'.format(reason)
|
||||
msg = f'failed on setup with "{reason}"'
|
||||
self._add_simple("error", msg, str(report.longrepr))
|
||||
|
||||
def append_skipped(self, report: TestReport) -> None:
|
||||
@@ -246,7 +246,7 @@ class _NodeReporter:
|
||||
filename, lineno, skipreason = report.longrepr
|
||||
if skipreason.startswith("Skipped: "):
|
||||
skipreason = skipreason[9:]
|
||||
details = "{}:{}: {}".format(filename, lineno, skipreason)
|
||||
details = f"{filename}:{lineno}: {skipreason}"
|
||||
|
||||
skipped = ET.Element("skipped", type="pytest.skip", message=skipreason)
|
||||
skipped.text = bin_xml_escape(details)
|
||||
@@ -481,17 +481,17 @@ class LogXML:
|
||||
self.log_passing_tests = log_passing_tests
|
||||
self.report_duration = report_duration
|
||||
self.family = family
|
||||
self.stats = dict.fromkeys(
|
||||
self.stats: Dict[str, int] = dict.fromkeys(
|
||||
["error", "passed", "failure", "skipped"], 0
|
||||
) # type: Dict[str, int]
|
||||
self.node_reporters = (
|
||||
{}
|
||||
) # type: Dict[Tuple[Union[str, TestReport], object], _NodeReporter]
|
||||
self.node_reporters_ordered = [] # type: List[_NodeReporter]
|
||||
self.global_properties = [] # type: List[Tuple[str, str]]
|
||||
)
|
||||
self.node_reporters: Dict[
|
||||
Tuple[Union[str, TestReport], object], _NodeReporter
|
||||
] = ({})
|
||||
self.node_reporters_ordered: List[_NodeReporter] = []
|
||||
self.global_properties: List[Tuple[str, str]] = []
|
||||
|
||||
# List of reports that failed on call but teardown is pending.
|
||||
self.open_reports = [] # type: List[TestReport]
|
||||
self.open_reports: List[TestReport] = []
|
||||
self.cnt_double_fail_tests = 0
|
||||
|
||||
# Replaces convenience family with real family.
|
||||
@@ -507,7 +507,7 @@ class LogXML:
|
||||
reporter.finalize()
|
||||
|
||||
def node_reporter(self, report: Union[TestReport, str]) -> _NodeReporter:
|
||||
nodeid = getattr(report, "nodeid", report) # type: Union[str, TestReport]
|
||||
nodeid: Union[str, TestReport] = getattr(report, "nodeid", report)
|
||||
# Local hack to handle xdist report order.
|
||||
workernode = getattr(report, "node", None)
|
||||
|
||||
@@ -683,7 +683,7 @@ class LogXML:
|
||||
logfile.close()
|
||||
|
||||
def pytest_terminal_summary(self, terminalreporter: TerminalReporter) -> None:
|
||||
terminalreporter.write_sep("-", "generated xml file: {}".format(self.logfile))
|
||||
terminalreporter.write_sep("-", f"generated xml file: {self.logfile}")
|
||||
|
||||
def add_global_property(self, name: str, value: object) -> None:
|
||||
__tracebackhide__ = True
|
||||
|
||||
@@ -5,6 +5,7 @@ import re
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from typing import AbstractSet
|
||||
from typing import Dict
|
||||
from typing import Generator
|
||||
@@ -15,7 +16,6 @@ from typing import Tuple
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
import pytest
|
||||
from _pytest import nodes
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest.capture import CaptureManager
|
||||
@@ -24,10 +24,13 @@ from _pytest.compat import nullcontext
|
||||
from _pytest.config import _strtobool
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import create_terminal_writer
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.config import UsageError
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.deprecated import check_ispytest
|
||||
from _pytest.fixtures import fixture
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
from _pytest.main import Session
|
||||
from _pytest.pathlib import Path
|
||||
from _pytest.store import StoreKey
|
||||
from _pytest.terminal import TerminalReporter
|
||||
|
||||
@@ -47,7 +50,7 @@ class ColoredLevelFormatter(logging.Formatter):
|
||||
"""A logging formatter which colorizes the %(levelname)..s part of the
|
||||
log format passed to __init__."""
|
||||
|
||||
LOGLEVEL_COLOROPTS = {
|
||||
LOGLEVEL_COLOROPTS: Mapping[int, AbstractSet[str]] = {
|
||||
logging.CRITICAL: {"red"},
|
||||
logging.ERROR: {"red", "bold"},
|
||||
logging.WARNING: {"yellow"},
|
||||
@@ -55,13 +58,13 @@ class ColoredLevelFormatter(logging.Formatter):
|
||||
logging.INFO: {"green"},
|
||||
logging.DEBUG: {"purple"},
|
||||
logging.NOTSET: set(),
|
||||
} # type: Mapping[int, AbstractSet[str]]
|
||||
}
|
||||
LEVELNAME_FMT_REGEX = re.compile(r"%\(levelname\)([+-.]?\d*s)")
|
||||
|
||||
def __init__(self, terminalwriter: TerminalWriter, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self._original_fmt = self._style._fmt
|
||||
self._level_to_fmt_mapping = {} # type: Dict[int, str]
|
||||
self._level_to_fmt_mapping: Dict[int, str] = {}
|
||||
|
||||
assert self._fmt is not None
|
||||
levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt)
|
||||
@@ -315,12 +318,12 @@ class catching_logs:
|
||||
class LogCaptureHandler(logging.StreamHandler):
|
||||
"""A logging handler that stores log records and the log text."""
|
||||
|
||||
stream = None # type: StringIO
|
||||
stream: StringIO
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Create a new log handler."""
|
||||
super().__init__(StringIO())
|
||||
self.records = [] # type: List[logging.LogRecord]
|
||||
self.records: List[logging.LogRecord] = []
|
||||
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
"""Keep the log records in a list in addition to the log text."""
|
||||
@@ -344,11 +347,12 @@ class LogCaptureHandler(logging.StreamHandler):
|
||||
class LogCaptureFixture:
|
||||
"""Provides access and control of log capturing."""
|
||||
|
||||
def __init__(self, item: nodes.Node) -> None:
|
||||
def __init__(self, item: nodes.Node, *, _ispytest: bool = False) -> None:
|
||||
check_ispytest(_ispytest)
|
||||
self._item = item
|
||||
self._initial_handler_level = None # type: Optional[int]
|
||||
self._initial_handler_level: Optional[int] = None
|
||||
# Dict of log name -> log level.
|
||||
self._initial_logger_levels = {} # type: Dict[Optional[str], int]
|
||||
self._initial_logger_levels: Dict[Optional[str], int] = {}
|
||||
|
||||
def _finalize(self) -> None:
|
||||
"""Finalize the fixture.
|
||||
@@ -468,7 +472,7 @@ class LogCaptureFixture:
|
||||
self.handler.setLevel(handler_orig_level)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@fixture
|
||||
def caplog(request: FixtureRequest) -> Generator[LogCaptureFixture, None, None]:
|
||||
"""Access and control log capturing.
|
||||
|
||||
@@ -480,7 +484,7 @@ def caplog(request: FixtureRequest) -> Generator[LogCaptureFixture, None, None]:
|
||||
* caplog.record_tuples -> list of (logger_name, level, message) tuples
|
||||
* caplog.clear() -> clear captured records and formatted log output string
|
||||
"""
|
||||
result = LogCaptureFixture(request.node)
|
||||
result = LogCaptureFixture(request.node, _ispytest=True)
|
||||
yield result
|
||||
result._finalize()
|
||||
|
||||
@@ -501,7 +505,7 @@ def get_log_level_for_setting(config: Config, *setting_names: str) -> Optional[i
|
||||
return int(getattr(logging, log_level, log_level))
|
||||
except ValueError as e:
|
||||
# Python logging does not recognise this as a logging level
|
||||
raise pytest.UsageError(
|
||||
raise UsageError(
|
||||
"'{}' is not recognized as a logging level name for "
|
||||
"'{}'. Please consider passing the "
|
||||
"logging level num instead.".format(log_level, setting_name)
|
||||
@@ -509,7 +513,7 @@ def get_log_level_for_setting(config: Config, *setting_names: str) -> Optional[i
|
||||
|
||||
|
||||
# run after terminalreporter/capturemanager are configured
|
||||
@pytest.hookimpl(trylast=True)
|
||||
@hookimpl(trylast=True)
|
||||
def pytest_configure(config: Config) -> None:
|
||||
config.pluginmanager.register(LoggingPlugin(config), "logging-plugin")
|
||||
|
||||
@@ -564,9 +568,9 @@ class LoggingPlugin:
|
||||
terminal_reporter = config.pluginmanager.get_plugin("terminalreporter")
|
||||
capture_manager = config.pluginmanager.get_plugin("capturemanager")
|
||||
# if capturemanager plugin is disabled, live logging still works.
|
||||
self.log_cli_handler = _LiveLoggingStreamHandler(
|
||||
terminal_reporter, capture_manager
|
||||
) # type: Union[_LiveLoggingStreamHandler, _LiveLoggingNullHandler]
|
||||
self.log_cli_handler: Union[
|
||||
_LiveLoggingStreamHandler, _LiveLoggingNullHandler
|
||||
] = _LiveLoggingStreamHandler(terminal_reporter, capture_manager)
|
||||
else:
|
||||
self.log_cli_handler = _LiveLoggingNullHandler()
|
||||
log_cli_formatter = self._create_formatter(
|
||||
@@ -582,9 +586,9 @@ class LoggingPlugin:
|
||||
if color != "no" and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(
|
||||
log_format
|
||||
):
|
||||
formatter = ColoredLevelFormatter(
|
||||
formatter: logging.Formatter = ColoredLevelFormatter(
|
||||
create_terminal_writer(self._config), log_format, log_date_format
|
||||
) # type: logging.Formatter
|
||||
)
|
||||
else:
|
||||
formatter = logging.Formatter(log_format, log_date_format)
|
||||
|
||||
@@ -639,7 +643,7 @@ class LoggingPlugin:
|
||||
|
||||
return True
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||
@hookimpl(hookwrapper=True, tryfirst=True)
|
||||
def pytest_sessionstart(self) -> Generator[None, None, None]:
|
||||
self.log_cli_handler.set_when("sessionstart")
|
||||
|
||||
@@ -647,7 +651,7 @@ class LoggingPlugin:
|
||||
with catching_logs(self.log_file_handler, level=self.log_file_level):
|
||||
yield
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||
@hookimpl(hookwrapper=True, tryfirst=True)
|
||||
def pytest_collection(self) -> Generator[None, None, None]:
|
||||
self.log_cli_handler.set_when("collection")
|
||||
|
||||
@@ -655,7 +659,7 @@ class LoggingPlugin:
|
||||
with catching_logs(self.log_file_handler, level=self.log_file_level):
|
||||
yield
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_runtestloop(self, session: Session) -> Generator[None, None, None]:
|
||||
if session.config.option.collectonly:
|
||||
yield
|
||||
@@ -669,12 +673,12 @@ class LoggingPlugin:
|
||||
with catching_logs(self.log_file_handler, level=self.log_file_level):
|
||||
yield # Run all the tests.
|
||||
|
||||
@pytest.hookimpl
|
||||
@hookimpl
|
||||
def pytest_runtest_logstart(self) -> None:
|
||||
self.log_cli_handler.reset()
|
||||
self.log_cli_handler.set_when("start")
|
||||
|
||||
@pytest.hookimpl
|
||||
@hookimpl
|
||||
def pytest_runtest_logreport(self) -> None:
|
||||
self.log_cli_handler.set_when("logreport")
|
||||
|
||||
@@ -695,21 +699,21 @@ class LoggingPlugin:
|
||||
log = report_handler.stream.getvalue().strip()
|
||||
item.add_report_section(when, "log", log)
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None, None, None]:
|
||||
self.log_cli_handler.set_when("setup")
|
||||
|
||||
empty = {} # type: Dict[str, List[logging.LogRecord]]
|
||||
empty: Dict[str, List[logging.LogRecord]] = {}
|
||||
item._store[caplog_records_key] = empty
|
||||
yield from self._runtest_for(item, "setup")
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_call(self, item: nodes.Item) -> Generator[None, None, None]:
|
||||
self.log_cli_handler.set_when("call")
|
||||
|
||||
yield from self._runtest_for(item, "call")
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_teardown(self, item: nodes.Item) -> Generator[None, None, None]:
|
||||
self.log_cli_handler.set_when("teardown")
|
||||
|
||||
@@ -717,11 +721,11 @@ class LoggingPlugin:
|
||||
del item._store[caplog_records_key]
|
||||
del item._store[caplog_handler_key]
|
||||
|
||||
@pytest.hookimpl
|
||||
@hookimpl
|
||||
def pytest_runtest_logfinish(self) -> None:
|
||||
self.log_cli_handler.set_when("finish")
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||
@hookimpl(hookwrapper=True, tryfirst=True)
|
||||
def pytest_sessionfinish(self) -> Generator[None, None, None]:
|
||||
self.log_cli_handler.set_when("sessionfinish")
|
||||
|
||||
@@ -729,7 +733,7 @@ class LoggingPlugin:
|
||||
with catching_logs(self.log_file_handler, level=self.log_file_level):
|
||||
yield
|
||||
|
||||
@pytest.hookimpl
|
||||
@hookimpl
|
||||
def pytest_unconfigure(self) -> None:
|
||||
# Close the FileHandler explicitly.
|
||||
# (logging.shutdown might have lost the weakref?!)
|
||||
@@ -755,7 +759,7 @@ class _LiveLoggingStreamHandler(logging.StreamHandler):
|
||||
|
||||
# Officially stream needs to be a IO[str], but TerminalReporter
|
||||
# isn't. So force it.
|
||||
stream = None # type: TerminalReporter # type: ignore
|
||||
stream: TerminalReporter = None # type: ignore
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -5,15 +5,19 @@ import functools
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import FrozenSet
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import overload
|
||||
from typing import Sequence
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
import attr
|
||||
@@ -22,8 +26,6 @@ import py
|
||||
import _pytest._code
|
||||
from _pytest import nodes
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import overload
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import directory_arg
|
||||
from _pytest.config import ExitCode
|
||||
@@ -35,7 +37,6 @@ from _pytest.fixtures import FixtureManager
|
||||
from _pytest.outcomes import exit
|
||||
from _pytest.pathlib import absolutepath
|
||||
from _pytest.pathlib import bestrelpath
|
||||
from _pytest.pathlib import Path
|
||||
from _pytest.pathlib import visit
|
||||
from _pytest.reports import CollectReport
|
||||
from _pytest.reports import TestReport
|
||||
@@ -44,7 +45,6 @@ from _pytest.runner import SetupState
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Type
|
||||
from typing_extensions import Literal
|
||||
|
||||
|
||||
@@ -53,7 +53,17 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
"norecursedirs",
|
||||
"directory patterns to avoid for recursion",
|
||||
type="args",
|
||||
default=[".*", "build", "dist", "CVS", "_darcs", "{arch}", "*.egg", "venv"],
|
||||
default=[
|
||||
"*.egg",
|
||||
".*",
|
||||
"_darcs",
|
||||
"build",
|
||||
"CVS",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"venv",
|
||||
"{arch}",
|
||||
],
|
||||
)
|
||||
parser.addini(
|
||||
"testpaths",
|
||||
@@ -101,10 +111,12 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
)
|
||||
group._addoption(
|
||||
"--strict-markers",
|
||||
"--strict",
|
||||
action="store_true",
|
||||
help="markers not registered in the `markers` section of the configuration file raise errors.",
|
||||
)
|
||||
group._addoption(
|
||||
"--strict", action="store_true", help="(deprecated) alias to --strict-markers.",
|
||||
)
|
||||
group._addoption(
|
||||
"-c",
|
||||
metavar="file",
|
||||
@@ -262,14 +274,12 @@ def wrap_session(
|
||||
session.exitstatus = ExitCode.TESTS_FAILED
|
||||
except (KeyboardInterrupt, exit.Exception):
|
||||
excinfo = _pytest._code.ExceptionInfo.from_current()
|
||||
exitstatus = ExitCode.INTERRUPTED # type: Union[int, ExitCode]
|
||||
exitstatus: Union[int, ExitCode] = ExitCode.INTERRUPTED
|
||||
if isinstance(excinfo.value, exit.Exception):
|
||||
if excinfo.value.returncode is not None:
|
||||
exitstatus = excinfo.value.returncode
|
||||
if initstate < 2:
|
||||
sys.stderr.write(
|
||||
"{}: {}\n".format(excinfo.typename, excinfo.value.msg)
|
||||
)
|
||||
sys.stderr.write(f"{excinfo.typename}: {excinfo.value.msg}\n")
|
||||
config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
|
||||
session.exitstatus = exitstatus
|
||||
except BaseException:
|
||||
@@ -441,10 +451,10 @@ class Session(nodes.FSCollector):
|
||||
Interrupted = Interrupted
|
||||
Failed = Failed
|
||||
# Set on the session by runner.pytest_sessionstart.
|
||||
_setupstate = None # type: SetupState
|
||||
_setupstate: SetupState
|
||||
# Set on the session by fixtures.pytest_sessionstart.
|
||||
_fixturemanager = None # type: FixtureManager
|
||||
exitstatus = None # type: Union[int, ExitCode]
|
||||
_fixturemanager: FixtureManager
|
||||
exitstatus: Union[int, ExitCode]
|
||||
|
||||
def __init__(self, config: Config) -> None:
|
||||
super().__init__(
|
||||
@@ -452,21 +462,19 @@ class Session(nodes.FSCollector):
|
||||
)
|
||||
self.testsfailed = 0
|
||||
self.testscollected = 0
|
||||
self.shouldstop = False # type: Union[bool, str]
|
||||
self.shouldfail = False # type: Union[bool, str]
|
||||
self.shouldstop: Union[bool, str] = False
|
||||
self.shouldfail: Union[bool, str] = False
|
||||
self.trace = config.trace.root.get("collection")
|
||||
self.startdir = config.invocation_dir
|
||||
self._initialpaths = frozenset() # type: FrozenSet[py.path.local]
|
||||
self._initialpaths: FrozenSet[py.path.local] = frozenset()
|
||||
|
||||
self._bestrelpathcache = _bestrelpath_cache(
|
||||
config.rootpath
|
||||
) # type: Dict[Path, str]
|
||||
self._bestrelpathcache: Dict[Path, str] = _bestrelpath_cache(config.rootpath)
|
||||
|
||||
self.config.pluginmanager.register(self, name="session")
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config: Config) -> "Session":
|
||||
session = cls._create(config) # type: Session
|
||||
session: Session = cls._create(config)
|
||||
return session
|
||||
|
||||
def __repr__(self) -> str:
|
||||
@@ -562,13 +570,13 @@ class Session(nodes.FSCollector):
|
||||
) -> Sequence[nodes.Item]:
|
||||
...
|
||||
|
||||
@overload # noqa: F811
|
||||
def perform_collect( # noqa: F811
|
||||
@overload
|
||||
def perform_collect(
|
||||
self, args: Optional[Sequence[str]] = ..., genitems: bool = ...
|
||||
) -> Sequence[Union[nodes.Item, nodes.Collector]]:
|
||||
...
|
||||
|
||||
def perform_collect( # noqa: F811
|
||||
def perform_collect(
|
||||
self, args: Optional[Sequence[str]] = None, genitems: bool = True
|
||||
) -> Sequence[Union[nodes.Item, nodes.Collector]]:
|
||||
"""Perform the collection phase for this session.
|
||||
@@ -591,15 +599,15 @@ class Session(nodes.FSCollector):
|
||||
self.trace("perform_collect", self, args)
|
||||
self.trace.root.indent += 1
|
||||
|
||||
self._notfound = [] # type: List[Tuple[str, Sequence[nodes.Collector]]]
|
||||
self._initial_parts = [] # type: List[Tuple[py.path.local, List[str]]]
|
||||
self.items = [] # type: List[nodes.Item]
|
||||
self._notfound: List[Tuple[str, Sequence[nodes.Collector]]] = []
|
||||
self._initial_parts: List[Tuple[py.path.local, List[str]]] = []
|
||||
self.items: List[nodes.Item] = []
|
||||
|
||||
hook = self.config.hook
|
||||
|
||||
items = self.items # type: Sequence[Union[nodes.Item, nodes.Collector]]
|
||||
items: Sequence[Union[nodes.Item, nodes.Collector]] = self.items
|
||||
try:
|
||||
initialpaths = [] # type: List[py.path.local]
|
||||
initialpaths: List[py.path.local] = []
|
||||
for arg in args:
|
||||
fspath, parts = resolve_collection_argument(
|
||||
self.config.invocation_params.dir,
|
||||
@@ -615,8 +623,8 @@ class Session(nodes.FSCollector):
|
||||
if self._notfound:
|
||||
errors = []
|
||||
for arg, cols in self._notfound:
|
||||
line = "(no name {!r} in any of {!r})".format(arg, cols)
|
||||
errors.append("not found: {}\n{}".format(arg, line))
|
||||
line = f"(no name {arg!r} in any of {cols!r})"
|
||||
errors.append(f"not found: {arg}\n{line}")
|
||||
raise UsageError(*errors)
|
||||
if not genitems:
|
||||
items = rep.result
|
||||
@@ -639,19 +647,17 @@ class Session(nodes.FSCollector):
|
||||
from _pytest.python import Package
|
||||
|
||||
# Keep track of any collected nodes in here, so we don't duplicate fixtures.
|
||||
node_cache1 = {} # type: Dict[py.path.local, Sequence[nodes.Collector]]
|
||||
node_cache2 = (
|
||||
{}
|
||||
) # type: Dict[Tuple[Type[nodes.Collector], py.path.local], nodes.Collector]
|
||||
node_cache1: Dict[py.path.local, Sequence[nodes.Collector]] = {}
|
||||
node_cache2: Dict[
|
||||
Tuple[Type[nodes.Collector], py.path.local], nodes.Collector
|
||||
] = ({})
|
||||
|
||||
# Keep track of any collected collectors in matchnodes paths, so they
|
||||
# are not collected more than once.
|
||||
matchnodes_cache = (
|
||||
{}
|
||||
) # type: Dict[Tuple[Type[nodes.Collector], str], CollectReport]
|
||||
matchnodes_cache: Dict[Tuple[Type[nodes.Collector], str], CollectReport] = ({})
|
||||
|
||||
# Dirnames of pkgs with dunder-init files.
|
||||
pkg_roots = {} # type: Dict[str, Package]
|
||||
pkg_roots: Dict[str, Package] = {}
|
||||
|
||||
for argpath, names in self._initial_parts:
|
||||
self.trace("processing argument", (argpath, names))
|
||||
@@ -680,7 +686,7 @@ class Session(nodes.FSCollector):
|
||||
if argpath.check(dir=1):
|
||||
assert not names, "invalid arg {!r}".format((argpath, names))
|
||||
|
||||
seen_dirs = set() # type: Set[py.path.local]
|
||||
seen_dirs: Set[py.path.local] = set()
|
||||
for direntry in visit(str(argpath), self._recurse):
|
||||
if not direntry.is_file():
|
||||
continue
|
||||
@@ -720,9 +726,9 @@ class Session(nodes.FSCollector):
|
||||
node_cache1[argpath] = col
|
||||
|
||||
matching = []
|
||||
work = [
|
||||
(col, names)
|
||||
] # type: List[Tuple[Sequence[Union[nodes.Item, nodes.Collector]], Sequence[str]]]
|
||||
work: List[
|
||||
Tuple[Sequence[Union[nodes.Item, nodes.Collector]], Sequence[str]]
|
||||
] = [(col, names)]
|
||||
while work:
|
||||
self.trace("matchnodes", col, names)
|
||||
self.trace.root.indent += 1
|
||||
@@ -769,12 +775,14 @@ class Session(nodes.FSCollector):
|
||||
self._notfound.append((report_arg, col))
|
||||
continue
|
||||
|
||||
# If __init__.py was the only file requested, then the matched node will be
|
||||
# the corresponding Package, and the first yielded item will be the __init__
|
||||
# Module itself, so just use that. If this special case isn't taken, then all
|
||||
# the files in the package will be yielded.
|
||||
if argpath.basename == "__init__.py":
|
||||
assert isinstance(matching[0], nodes.Collector)
|
||||
# If __init__.py was the only file requested, then the matched
|
||||
# node will be the corresponding Package (by default), and the
|
||||
# first yielded item will be the __init__ Module itself, so
|
||||
# just use that. If this special case isn't taken, then all the
|
||||
# files in the package will be yielded.
|
||||
if argpath.basename == "__init__.py" and isinstance(
|
||||
matching[0], Package
|
||||
):
|
||||
try:
|
||||
yield next(iter(matching[0].collect()))
|
||||
except StopIteration:
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""Generic mechanism for marking and selecting python functions."""
|
||||
import typing
|
||||
import warnings
|
||||
from typing import AbstractSet
|
||||
from typing import Collection
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
import attr
|
||||
@@ -17,7 +18,6 @@ from .structures import MARK_GEN
|
||||
from .structures import MarkDecorator
|
||||
from .structures import MarkGenerator
|
||||
from .structures import ParameterSet
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ExitCode
|
||||
from _pytest.config import hookimpl
|
||||
@@ -46,8 +46,8 @@ old_mark_config_key = StoreKey[Optional[Config]]()
|
||||
|
||||
def param(
|
||||
*values: object,
|
||||
marks: "Union[MarkDecorator, typing.Collection[Union[MarkDecorator, Mark]]]" = (),
|
||||
id: Optional[str] = None
|
||||
marks: Union[MarkDecorator, Collection[Union[MarkDecorator, Mark]]] = (),
|
||||
id: Optional[str] = None,
|
||||
) -> ParameterSet:
|
||||
"""Specify a parameter in `pytest.mark.parametrize`_ calls or
|
||||
:ref:`parametrized fixtures <fixture-parametrize-marks>`.
|
||||
@@ -201,7 +201,7 @@ def deselect_by_keyword(items: "List[Item]", config: Config) -> None:
|
||||
expression = Expression.compile(keywordexpr)
|
||||
except ParseError as e:
|
||||
raise UsageError(
|
||||
"Wrong expression passed to '-k': {}: {}".format(keywordexpr, e)
|
||||
f"Wrong expression passed to '-k': {keywordexpr}: {e}"
|
||||
) from None
|
||||
|
||||
remaining = []
|
||||
@@ -245,9 +245,7 @@ def deselect_by_mark(items: "List[Item]", config: Config) -> None:
|
||||
try:
|
||||
expression = Expression.compile(matchexpr)
|
||||
except ParseError as e:
|
||||
raise UsageError(
|
||||
"Wrong expression passed to '-m': {}: {}".format(matchexpr, e)
|
||||
) from None
|
||||
raise UsageError(f"Wrong expression passed to '-m': {matchexpr}: {e}") from None
|
||||
|
||||
remaining = []
|
||||
deselected = []
|
||||
|
||||
@@ -23,11 +23,10 @@ from typing import Iterator
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import attr
|
||||
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import NoReturn
|
||||
|
||||
@@ -67,7 +66,7 @@ class ParseError(Exception):
|
||||
self.message = message
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "at column {}: {}".format(self.column, self.message)
|
||||
return f"at column {self.column}: {self.message}"
|
||||
|
||||
|
||||
class Scanner:
|
||||
@@ -134,7 +133,7 @@ IDENT_PREFIX = "$"
|
||||
|
||||
def expression(s: Scanner) -> ast.Expression:
|
||||
if s.accept(TokenType.EOF):
|
||||
ret = ast.NameConstant(False) # type: ast.expr
|
||||
ret: ast.expr = ast.NameConstant(False)
|
||||
else:
|
||||
ret = expr(s)
|
||||
s.accept(TokenType.EOF, reject=True)
|
||||
@@ -204,9 +203,9 @@ class Expression:
|
||||
:param input: The input expression - one line.
|
||||
"""
|
||||
astexpr = expression(Scanner(input))
|
||||
code = compile(
|
||||
code: types.CodeType = compile(
|
||||
astexpr, filename="<pytest match expression>", mode="eval",
|
||||
) # type: types.CodeType
|
||||
)
|
||||
return Expression(code)
|
||||
|
||||
def evaluate(self, matcher: Callable[[str], bool]) -> bool:
|
||||
@@ -218,7 +217,5 @@ class Expression:
|
||||
|
||||
:returns: Whether the expression matches or not.
|
||||
"""
|
||||
ret = eval(
|
||||
self.code, {"__builtins__": {}}, MatcherAdapter(matcher)
|
||||
) # type: bool
|
||||
ret: bool = eval(self.code, {"__builtins__": {}}, MatcherAdapter(matcher))
|
||||
return ret
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import collections.abc
|
||||
import inspect
|
||||
import typing
|
||||
import warnings
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Collection
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import MutableMapping
|
||||
from typing import NamedTuple
|
||||
from typing import Optional
|
||||
from typing import overload
|
||||
from typing import Sequence
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
@@ -23,15 +27,11 @@ from ..compat import ascii_escaped
|
||||
from ..compat import final
|
||||
from ..compat import NOTSET
|
||||
from ..compat import NotSetType
|
||||
from ..compat import overload
|
||||
from ..compat import TYPE_CHECKING
|
||||
from _pytest.config import Config
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.warning_types import PytestUnknownMarkWarning
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Type
|
||||
|
||||
from ..nodes import Node
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ class ParameterSet(
|
||||
"ParameterSet",
|
||||
[
|
||||
("values", Sequence[Union[object, NotSetType]]),
|
||||
("marks", "typing.Collection[Union[MarkDecorator, Mark]]"),
|
||||
("marks", Collection[Union["MarkDecorator", "Mark"]]),
|
||||
("id", Optional[str]),
|
||||
],
|
||||
)
|
||||
@@ -88,14 +88,13 @@ class ParameterSet(
|
||||
def param(
|
||||
cls,
|
||||
*values: object,
|
||||
marks: "Union[MarkDecorator, typing.Collection[Union[MarkDecorator, Mark]]]" = (),
|
||||
id: Optional[str] = None
|
||||
marks: Union["MarkDecorator", Collection[Union["MarkDecorator", "Mark"]]] = (),
|
||||
id: Optional[str] = None,
|
||||
) -> "ParameterSet":
|
||||
if isinstance(marks, MarkDecorator):
|
||||
marks = (marks,)
|
||||
else:
|
||||
# TODO(py36): Change to collections.abc.Collection.
|
||||
assert isinstance(marks, (collections.abc.Sequence, set))
|
||||
assert isinstance(marks, collections.abc.Collection)
|
||||
|
||||
if id is not None:
|
||||
if not isinstance(id, str):
|
||||
@@ -128,7 +127,7 @@ class ParameterSet(
|
||||
return cls.param(parameterset)
|
||||
else:
|
||||
# TODO: Refactor to fix this type-ignore. Currently the following
|
||||
# type-checks but crashes:
|
||||
# passes type-checking but crashes:
|
||||
#
|
||||
# @pytest.mark.parametrize(('x', 'y'), [1, 2])
|
||||
# def test_foo(x, y): pass
|
||||
@@ -139,7 +138,7 @@ class ParameterSet(
|
||||
argnames: Union[str, List[str], Tuple[str, ...]],
|
||||
argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
|
||||
*args,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
) -> Tuple[Union[List[str], Tuple[str, ...]], bool]:
|
||||
if not isinstance(argnames, (tuple, list)):
|
||||
argnames = [x.strip() for x in argnames.split(",") if x.strip()]
|
||||
@@ -232,7 +231,7 @@ class Mark:
|
||||
assert self.name == other.name
|
||||
|
||||
# Remember source of ids with parametrize Marks.
|
||||
param_ids_from = None # type: Optional[Mark]
|
||||
param_ids_from: Optional[Mark] = None
|
||||
if self.name == "parametrize":
|
||||
if other._has_param_ids():
|
||||
param_ids_from = other
|
||||
@@ -311,7 +310,7 @@ class MarkDecorator:
|
||||
return self.name # for backward-compat (2.4.1 had this attr)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<MarkDecorator {!r}>".format(self.mark)
|
||||
return f"<MarkDecorator {self.mark!r}>"
|
||||
|
||||
def with_args(self, *args: object, **kwargs: object) -> "MarkDecorator":
|
||||
"""Return a MarkDecorator with extra arguments added.
|
||||
@@ -331,13 +330,11 @@ class MarkDecorator:
|
||||
def __call__(self, arg: _Markable) -> _Markable: # type: ignore[misc]
|
||||
pass
|
||||
|
||||
@overload # noqa: F811
|
||||
def __call__( # noqa: F811
|
||||
self, *args: object, **kwargs: object
|
||||
) -> "MarkDecorator":
|
||||
@overload
|
||||
def __call__(self, *args: object, **kwargs: object) -> "MarkDecorator":
|
||||
pass
|
||||
|
||||
def __call__(self, *args: object, **kwargs: object): # noqa: F811
|
||||
def __call__(self, *args: object, **kwargs: object):
|
||||
"""Call the MarkDecorator."""
|
||||
if args and not kwargs:
|
||||
func = args[0]
|
||||
@@ -367,7 +364,7 @@ def normalize_mark_list(mark_list: Iterable[Union[Mark, MarkDecorator]]) -> List
|
||||
] # unpack MarkDecorator
|
||||
for mark in extracted:
|
||||
if not isinstance(mark, Mark):
|
||||
raise TypeError("got {!r} instead of Mark".format(mark))
|
||||
raise TypeError(f"got {mark!r} instead of Mark")
|
||||
return [x for x in extracted if isinstance(x, Mark)]
|
||||
|
||||
|
||||
@@ -392,8 +389,8 @@ if TYPE_CHECKING:
|
||||
def __call__(self, arg: _Markable) -> _Markable:
|
||||
...
|
||||
|
||||
@overload # noqa: F811
|
||||
def __call__(self, reason: str = ...) -> "MarkDecorator": # noqa: F811
|
||||
@overload
|
||||
def __call__(self, reason: str = ...) -> "MarkDecorator":
|
||||
...
|
||||
|
||||
class _SkipifMarkDecorator(MarkDecorator):
|
||||
@@ -401,7 +398,7 @@ if TYPE_CHECKING:
|
||||
self,
|
||||
condition: Union[str, bool] = ...,
|
||||
*conditions: Union[str, bool],
|
||||
reason: str = ...
|
||||
reason: str = ...,
|
||||
) -> MarkDecorator:
|
||||
...
|
||||
|
||||
@@ -410,17 +407,15 @@ if TYPE_CHECKING:
|
||||
def __call__(self, arg: _Markable) -> _Markable:
|
||||
...
|
||||
|
||||
@overload # noqa: F811
|
||||
def __call__( # noqa: F811
|
||||
@overload
|
||||
def __call__(
|
||||
self,
|
||||
condition: Union[str, bool] = ...,
|
||||
*conditions: Union[str, bool],
|
||||
reason: str = ...,
|
||||
run: bool = ...,
|
||||
raises: Union[
|
||||
"Type[BaseException]", Tuple["Type[BaseException]", ...]
|
||||
] = ...,
|
||||
strict: bool = ...
|
||||
raises: Union[Type[BaseException], Tuple[Type[BaseException], ...]] = ...,
|
||||
strict: bool = ...,
|
||||
) -> MarkDecorator:
|
||||
...
|
||||
|
||||
@@ -437,7 +432,7 @@ if TYPE_CHECKING:
|
||||
Callable[[Any], Optional[object]],
|
||||
]
|
||||
] = ...,
|
||||
scope: Optional[_Scope] = ...
|
||||
scope: Optional[_Scope] = ...,
|
||||
) -> MarkDecorator:
|
||||
...
|
||||
|
||||
@@ -470,18 +465,17 @@ class MarkGenerator:
|
||||
applies a 'slowtest' :class:`Mark` on ``test_function``.
|
||||
"""
|
||||
|
||||
_config = None # type: Optional[Config]
|
||||
_markers = set() # type: Set[str]
|
||||
_config: Optional[Config] = None
|
||||
_markers: Set[str] = set()
|
||||
|
||||
# See TYPE_CHECKING above.
|
||||
if TYPE_CHECKING:
|
||||
# TODO(py36): Change to builtin annotation syntax.
|
||||
skip = _SkipMarkDecorator(Mark("skip", (), {}))
|
||||
skipif = _SkipifMarkDecorator(Mark("skipif", (), {}))
|
||||
xfail = _XfailMarkDecorator(Mark("xfail", (), {}))
|
||||
parametrize = _ParametrizeMarkDecorator(Mark("parametrize", (), {}))
|
||||
usefixtures = _UsefixturesMarkDecorator(Mark("usefixtures", (), {}))
|
||||
filterwarnings = _FilterwarningsMarkDecorator(Mark("filterwarnings", (), {}))
|
||||
skip: _SkipMarkDecorator
|
||||
skipif: _SkipifMarkDecorator
|
||||
xfail: _XfailMarkDecorator
|
||||
parametrize: _ParametrizeMarkDecorator
|
||||
usefixtures: _UsefixturesMarkDecorator
|
||||
filterwarnings: _FilterwarningsMarkDecorator
|
||||
|
||||
def __getattr__(self, name: str) -> MarkDecorator:
|
||||
if name[0] == "_":
|
||||
@@ -502,16 +496,16 @@ class MarkGenerator:
|
||||
# If the name is not in the set of known marks after updating,
|
||||
# then it really is time to issue a warning or an error.
|
||||
if name not in self._markers:
|
||||
if self._config.option.strict_markers:
|
||||
if self._config.option.strict_markers or self._config.option.strict:
|
||||
fail(
|
||||
"{!r} not found in `markers` configuration option".format(name),
|
||||
f"{name!r} not found in `markers` configuration option",
|
||||
pytrace=False,
|
||||
)
|
||||
|
||||
# Raise a specific error for common misspellings of "parametrize".
|
||||
if name in ["parameterize", "parametrise", "parameterise"]:
|
||||
__tracebackhide__ = True
|
||||
fail("Unknown '{}' mark, did you mean 'parametrize'?".format(name))
|
||||
fail(f"Unknown '{name}' mark, did you mean 'parametrize'?")
|
||||
|
||||
warnings.warn(
|
||||
"Unknown pytest.mark.%s - is this a typo? You can register "
|
||||
@@ -527,9 +521,8 @@ class MarkGenerator:
|
||||
MARK_GEN = MarkGenerator()
|
||||
|
||||
|
||||
# TODO(py36): inherit from typing.MutableMapping[str, Any].
|
||||
@final
|
||||
class NodeKeywords(collections.abc.MutableMapping): # type: ignore[type-arg]
|
||||
class NodeKeywords(MutableMapping[str, Any]):
|
||||
def __init__(self, node: "Node") -> None:
|
||||
self.node = node
|
||||
self.parent = node.parent
|
||||
@@ -563,4 +556,4 @@ class NodeKeywords(collections.abc.MutableMapping): # type: ignore[type-arg]
|
||||
return len(self._seen())
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<NodeKeywords for node {}>".format(self.node)
|
||||
return f"<NodeKeywords for node {self.node}>"
|
||||
|
||||
@@ -4,20 +4,20 @@ import re
|
||||
import sys
|
||||
import warnings
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Generator
|
||||
from typing import List
|
||||
from typing import MutableMapping
|
||||
from typing import Optional
|
||||
from typing import overload
|
||||
from typing import Tuple
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
import pytest
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import overload
|
||||
from _pytest.fixtures import fixture
|
||||
from _pytest.pathlib import Path
|
||||
from _pytest.warning_types import PytestWarning
|
||||
|
||||
RE_IMPORT_ERROR_NAME = re.compile(r"^No module named (.*)$")
|
||||
|
||||
@@ -74,7 +74,7 @@ def resolve(name: str) -> object:
|
||||
if expected == used:
|
||||
raise
|
||||
else:
|
||||
raise ImportError("import error in {}: {}".format(used, ex)) from ex
|
||||
raise ImportError(f"import error in {used}: {ex}") from ex
|
||||
found = annotated_getattr(found, part, used)
|
||||
return found
|
||||
|
||||
@@ -93,9 +93,7 @@ def annotated_getattr(obj: object, name: str, ann: str) -> object:
|
||||
|
||||
def derive_importpath(import_path: str, raising: bool) -> Tuple[str, object]:
|
||||
if not isinstance(import_path, str) or "." not in import_path: # type: ignore[unreachable]
|
||||
raise TypeError(
|
||||
"must be absolute import path string, not {!r}".format(import_path)
|
||||
)
|
||||
raise TypeError(f"must be absolute import path string, not {import_path!r}")
|
||||
module, attr = import_path.rsplit(".", 1)
|
||||
target = resolve(module)
|
||||
if raising:
|
||||
@@ -113,19 +111,27 @@ notset = Notset()
|
||||
|
||||
@final
|
||||
class MonkeyPatch:
|
||||
"""Object returned by the ``monkeypatch`` fixture keeping a record of
|
||||
setattr/item/env/syspath changes."""
|
||||
"""Helper to conveniently monkeypatch attributes/items/environment
|
||||
variables/syspath.
|
||||
|
||||
Returned by the :fixture:`monkeypatch` fixture.
|
||||
|
||||
:versionchanged:: 6.2
|
||||
Can now also be used directly as `pytest.MonkeyPatch()`, for when
|
||||
the fixture is not available. In this case, use
|
||||
:meth:`with MonkeyPatch.context() as mp: <context>` or remember to call
|
||||
:meth:`undo` explicitly.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._setattr = [] # type: List[Tuple[object, str, object]]
|
||||
self._setitem = (
|
||||
[]
|
||||
) # type: List[Tuple[MutableMapping[Any, Any], object, object]]
|
||||
self._cwd = None # type: Optional[str]
|
||||
self._savesyspath = None # type: Optional[List[str]]
|
||||
self._setattr: List[Tuple[object, str, object]] = []
|
||||
self._setitem: List[Tuple[MutableMapping[Any, Any], object, object]] = ([])
|
||||
self._cwd: Optional[str] = None
|
||||
self._savesyspath: Optional[List[str]] = None
|
||||
|
||||
@classmethod
|
||||
@contextmanager
|
||||
def context(self) -> Generator["MonkeyPatch", None, None]:
|
||||
def context(cls) -> Generator["MonkeyPatch", None, None]:
|
||||
"""Context manager that returns a new :class:`MonkeyPatch` object
|
||||
which undoes any patching done inside the ``with`` block upon exit.
|
||||
|
||||
@@ -144,7 +150,7 @@ class MonkeyPatch:
|
||||
such as mocking ``stdlib`` functions that might break pytest itself if mocked (for examples
|
||||
of this see `#3290 <https://github.com/pytest-dev/pytest/issues/3290>`_.
|
||||
"""
|
||||
m = MonkeyPatch()
|
||||
m = cls()
|
||||
try:
|
||||
yield m
|
||||
finally:
|
||||
@@ -156,13 +162,13 @@ class MonkeyPatch:
|
||||
) -> None:
|
||||
...
|
||||
|
||||
@overload # noqa: F811
|
||||
def setattr( # noqa: F811
|
||||
@overload
|
||||
def setattr(
|
||||
self, target: object, name: str, value: object, raising: bool = ...,
|
||||
) -> None:
|
||||
...
|
||||
|
||||
def setattr( # noqa: F811
|
||||
def setattr(
|
||||
self,
|
||||
target: Union[str, object],
|
||||
name: Union[object, str],
|
||||
@@ -202,7 +208,7 @@ class MonkeyPatch:
|
||||
|
||||
oldval = getattr(target, name, notset)
|
||||
if raising and oldval is notset:
|
||||
raise AttributeError("{!r} has no attribute {!r}".format(target, name))
|
||||
raise AttributeError(f"{target!r} has no attribute {name!r}")
|
||||
|
||||
# avoid class descriptors like staticmethod/classmethod
|
||||
if inspect.isclass(target):
|
||||
@@ -275,7 +281,7 @@ class MonkeyPatch:
|
||||
"""
|
||||
if not isinstance(value, str):
|
||||
warnings.warn( # type: ignore[unreachable]
|
||||
pytest.PytestWarning(
|
||||
PytestWarning(
|
||||
"Value of environment variable {name} type should be str, but got "
|
||||
"{value!r} (type: {type}); converted to str implicitly".format(
|
||||
name=name, value=value, type=type(value).__name__
|
||||
@@ -294,7 +300,7 @@ class MonkeyPatch:
|
||||
Raises ``KeyError`` if it does not exist, unless ``raising`` is set to
|
||||
False.
|
||||
"""
|
||||
environ = os.environ # type: MutableMapping[str, str]
|
||||
environ: MutableMapping[str, str] = os.environ
|
||||
self.delitem(environ, name, raising=raising)
|
||||
|
||||
def syspath_prepend(self, path) -> None:
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import os
|
||||
import warnings
|
||||
from functools import lru_cache
|
||||
from typing import Any
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import overload
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
@@ -20,27 +21,19 @@ from _pytest._code import getfslineno
|
||||
from _pytest._code.code import ExceptionInfo
|
||||
from _pytest._code.code import TerminalRepr
|
||||
from _pytest.compat import cached_property
|
||||
from _pytest.compat import overload
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ConftestImportFailure
|
||||
from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
|
||||
from _pytest.fixtures import FixtureDef
|
||||
from _pytest.fixtures import FixtureLookupError
|
||||
from _pytest.mark.structures import Mark
|
||||
from _pytest.mark.structures import MarkDecorator
|
||||
from _pytest.mark.structures import NodeKeywords
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.pathlib import absolutepath
|
||||
from _pytest.pathlib import Path
|
||||
from _pytest.store import Store
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Type
|
||||
|
||||
# Imported here due to circular import.
|
||||
from _pytest.main import Session
|
||||
from _pytest.warning_types import PytestWarning
|
||||
from _pytest._code.code import _TracebackStyle
|
||||
|
||||
|
||||
@@ -49,46 +42,39 @@ SEP = "/"
|
||||
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def _splitnode(nodeid: str) -> Tuple[str, ...]:
|
||||
"""Split a nodeid into constituent 'parts'.
|
||||
def iterparentnodeids(nodeid: str) -> Iterator[str]:
|
||||
"""Return the parent node IDs of a given node ID, inclusive.
|
||||
|
||||
Node IDs are strings, and can be things like:
|
||||
''
|
||||
'testing/code'
|
||||
'testing/code/test_excinfo.py'
|
||||
'testing/code/test_excinfo.py::TestFormattedExcinfo'
|
||||
For the node ID
|
||||
|
||||
Return values are lists e.g.
|
||||
[]
|
||||
['testing', 'code']
|
||||
['testing', 'code', 'test_excinfo.py']
|
||||
['testing', 'code', 'test_excinfo.py', 'TestFormattedExcinfo']
|
||||
"testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source"
|
||||
|
||||
the result would be
|
||||
|
||||
""
|
||||
"testing"
|
||||
"testing/code"
|
||||
"testing/code/test_excinfo.py"
|
||||
"testing/code/test_excinfo.py::TestFormattedExcinfo"
|
||||
"testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source"
|
||||
|
||||
Note that :: parts are only considered at the last / component.
|
||||
"""
|
||||
if nodeid == "":
|
||||
# If there is no root node at all, return an empty list so the caller's
|
||||
# logic can remain sane.
|
||||
return ()
|
||||
parts = nodeid.split(SEP)
|
||||
# Replace single last element 'test_foo.py::Bar' with multiple elements
|
||||
# 'test_foo.py', 'Bar'.
|
||||
parts[-1:] = parts[-1].split("::")
|
||||
# Convert parts into a tuple to avoid possible errors with caching of a
|
||||
# mutable type.
|
||||
return tuple(parts)
|
||||
|
||||
|
||||
def ischildnode(baseid: str, nodeid: str) -> bool:
|
||||
"""Return True if the nodeid is a child node of the baseid.
|
||||
|
||||
E.g. 'foo/bar::Baz' is a child of 'foo', 'foo/bar' and 'foo/bar::Baz',
|
||||
but not of 'foo/blorp'.
|
||||
"""
|
||||
base_parts = _splitnode(baseid)
|
||||
node_parts = _splitnode(nodeid)
|
||||
if len(node_parts) < len(base_parts):
|
||||
return False
|
||||
return node_parts[: len(base_parts)] == base_parts
|
||||
pos = 0
|
||||
sep = SEP
|
||||
yield ""
|
||||
while True:
|
||||
at = nodeid.find(sep, pos)
|
||||
if at == -1 and sep == SEP:
|
||||
sep = "::"
|
||||
elif at == -1:
|
||||
if nodeid:
|
||||
yield nodeid
|
||||
break
|
||||
else:
|
||||
if at:
|
||||
yield nodeid[:at]
|
||||
pos = at + len(sep)
|
||||
|
||||
|
||||
_NodeType = TypeVar("_NodeType", bound="Node")
|
||||
@@ -145,7 +131,7 @@ class Node(metaclass=NodeMeta):
|
||||
|
||||
#: The pytest config object.
|
||||
if config:
|
||||
self.config = config # type: Config
|
||||
self.config: Config = config
|
||||
else:
|
||||
if not parent:
|
||||
raise TypeError("config or parent must be provided")
|
||||
@@ -166,13 +152,10 @@ class Node(metaclass=NodeMeta):
|
||||
self.keywords = NodeKeywords(self)
|
||||
|
||||
#: The marker objects belonging to this node.
|
||||
self.own_markers = [] # type: List[Mark]
|
||||
self.own_markers: List[Mark] = []
|
||||
|
||||
#: Allow adding of extra keywords to use for matching.
|
||||
self.extra_keyword_matches = set() # type: Set[str]
|
||||
|
||||
# Used for storing artificial fixturedefs for direct parametrization.
|
||||
self._name2pseudofixturedef = {} # type: Dict[str, FixtureDef[Any]]
|
||||
self.extra_keyword_matches: Set[str] = set()
|
||||
|
||||
if nodeid is not None:
|
||||
assert "::()" not in nodeid
|
||||
@@ -214,27 +197,31 @@ class Node(metaclass=NodeMeta):
|
||||
def __repr__(self) -> str:
|
||||
return "<{} {}>".format(self.__class__.__name__, getattr(self, "name", None))
|
||||
|
||||
def warn(self, warning: "PytestWarning") -> None:
|
||||
def warn(self, warning: Warning) -> None:
|
||||
"""Issue a warning for this Node.
|
||||
|
||||
Warnings will be displayed after the test session, unless explicitly suppressed.
|
||||
|
||||
:param Warning warning:
|
||||
The warning instance to issue. Must be a subclass of PytestWarning.
|
||||
The warning instance to issue.
|
||||
|
||||
:raises ValueError: If ``warning`` instance is not a subclass of PytestWarning.
|
||||
:raises ValueError: If ``warning`` instance is not a subclass of Warning.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
node.warn(PytestWarning("some message"))
|
||||
"""
|
||||
from _pytest.warning_types import PytestWarning
|
||||
node.warn(UserWarning("some message"))
|
||||
|
||||
if not isinstance(warning, PytestWarning):
|
||||
.. versionchanged:: 6.2
|
||||
Any subclass of :class:`Warning` is now accepted, rather than only
|
||||
:class:`PytestWarning <pytest.PytestWarning>` subclasses.
|
||||
"""
|
||||
# enforce type checks here to avoid getting a generic type error later otherwise.
|
||||
if not isinstance(warning, Warning):
|
||||
raise ValueError(
|
||||
"warning must be an instance of PytestWarning or subclass, got {!r}".format(
|
||||
"warning must be an instance of Warning or subclass, got {!r}".format(
|
||||
warning
|
||||
)
|
||||
)
|
||||
@@ -264,7 +251,7 @@ class Node(metaclass=NodeMeta):
|
||||
"""Return list of all parent collectors up to self, starting from
|
||||
the root of collection tree."""
|
||||
chain = []
|
||||
item = self # type: Optional[Node]
|
||||
item: Optional[Node] = self
|
||||
while item is not None:
|
||||
chain.append(item)
|
||||
item = item.parent
|
||||
@@ -317,11 +304,11 @@ class Node(metaclass=NodeMeta):
|
||||
def get_closest_marker(self, name: str) -> Optional[Mark]:
|
||||
...
|
||||
|
||||
@overload # noqa: F811
|
||||
def get_closest_marker(self, name: str, default: Mark) -> Mark: # noqa: F811
|
||||
@overload
|
||||
def get_closest_marker(self, name: str, default: Mark) -> Mark:
|
||||
...
|
||||
|
||||
def get_closest_marker( # noqa: F811
|
||||
def get_closest_marker(
|
||||
self, name: str, default: Optional[Mark] = None
|
||||
) -> Optional[Mark]:
|
||||
"""Return the first marker matching the name, from closest (for
|
||||
@@ -334,7 +321,7 @@ class Node(metaclass=NodeMeta):
|
||||
|
||||
def listextrakeywords(self) -> Set[str]:
|
||||
"""Return a set of all extra keywords in self and any parents."""
|
||||
extra_keywords = set() # type: Set[str]
|
||||
extra_keywords: Set[str] = set()
|
||||
for item in self.listchain():
|
||||
extra_keywords.update(item.extra_keyword_matches)
|
||||
return extra_keywords
|
||||
@@ -350,10 +337,10 @@ class Node(metaclass=NodeMeta):
|
||||
"""
|
||||
self.session._setupstate.addfinalizer(fin, self)
|
||||
|
||||
def getparent(self, cls: "Type[_NodeType]") -> Optional[_NodeType]:
|
||||
def getparent(self, cls: Type[_NodeType]) -> Optional[_NodeType]:
|
||||
"""Get the next parent node (including self) which is an instance of
|
||||
the given class."""
|
||||
current = self # type: Optional[Node]
|
||||
current: Optional[Node] = self
|
||||
while current and not isinstance(current, cls):
|
||||
current = current.parent
|
||||
assert current is None or isinstance(current, cls)
|
||||
@@ -367,6 +354,8 @@ class Node(metaclass=NodeMeta):
|
||||
excinfo: ExceptionInfo[BaseException],
|
||||
style: "Optional[_TracebackStyle]" = None,
|
||||
) -> TerminalRepr:
|
||||
from _pytest.fixtures import FixtureLookupError
|
||||
|
||||
if isinstance(excinfo.value, ConftestImportFailure):
|
||||
excinfo = ExceptionInfo(excinfo.value.excinfo)
|
||||
if isinstance(excinfo.value, fail.Exception):
|
||||
@@ -439,9 +428,7 @@ def get_fslocation_from_item(
|
||||
:rtype: A tuple of (str|py.path.local, int) with filename and line number.
|
||||
"""
|
||||
# See Item.location.
|
||||
location = getattr(
|
||||
node, "location", None
|
||||
) # type: Optional[Tuple[str, Optional[int], str]]
|
||||
location: Optional[Tuple[str, Optional[int], str]] = getattr(node, "location", None)
|
||||
if location is not None:
|
||||
return location[:2]
|
||||
obj = getattr(node, "obj", None)
|
||||
@@ -566,11 +553,11 @@ class Item(Node):
|
||||
nodeid: Optional[str] = None,
|
||||
) -> None:
|
||||
super().__init__(name, parent, config, session, nodeid=nodeid)
|
||||
self._report_sections = [] # type: List[Tuple[str, str, str]]
|
||||
self._report_sections: List[Tuple[str, str, str]] = []
|
||||
|
||||
#: A list of tuples (name, value) that holds user defined properties
|
||||
#: for this test.
|
||||
self.user_properties = [] # type: List[Tuple[str, object]]
|
||||
self.user_properties: List[Tuple[str, object]] = []
|
||||
|
||||
def runtest(self) -> None:
|
||||
raise NotImplementedError("runtest must be implemented by Item subclass")
|
||||
|
||||
@@ -5,13 +5,13 @@ from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Optional
|
||||
from typing import Type
|
||||
from typing import TypeVar
|
||||
|
||||
TYPE_CHECKING = False # Avoid circular import through compat.
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import NoReturn
|
||||
from typing import Type # noqa: F401 (used in type string)
|
||||
from typing_extensions import Protocol
|
||||
else:
|
||||
# typing.Protocol is only available starting from Python 3.8. It is also
|
||||
@@ -38,9 +38,9 @@ class OutcomeException(BaseException):
|
||||
self.pytrace = pytrace
|
||||
|
||||
def __repr__(self) -> str:
|
||||
if self.msg:
|
||||
if self.msg is not None:
|
||||
return self.msg
|
||||
return "<{} instance>".format(self.__class__.__name__)
|
||||
return f"<{self.__class__.__name__} instance>"
|
||||
|
||||
__str__ = __repr__
|
||||
|
||||
@@ -84,12 +84,12 @@ class Exit(Exception):
|
||||
# Ideally would just be `exit.Exception = Exit` etc.
|
||||
|
||||
_F = TypeVar("_F", bound=Callable[..., object])
|
||||
_ET = TypeVar("_ET", bound="Type[BaseException]")
|
||||
_ET = TypeVar("_ET", bound=Type[BaseException])
|
||||
|
||||
|
||||
class _WithException(Protocol[_F, _ET]):
|
||||
Exception = None # type: _ET
|
||||
__call__ = None # type: _F
|
||||
Exception: _ET
|
||||
__call__: _F
|
||||
|
||||
|
||||
def _with_exception(exception_type: _ET) -> Callable[[_F], _WithException[_F, _ET]]:
|
||||
@@ -208,7 +208,7 @@ def importorskip(
|
||||
__import__(modname)
|
||||
except ImportError as exc:
|
||||
if reason is None:
|
||||
reason = "could not import {!r}: {}".format(modname, exc)
|
||||
reason = f"could not import {modname!r}: {exc}"
|
||||
raise Skipped(reason, allow_module_level=True) from None
|
||||
mod = sys.modules[modname]
|
||||
if minversion is None:
|
||||
|
||||
@@ -79,9 +79,9 @@ def create_new_paste(contents: Union[str, bytes]) -> str:
|
||||
params = {"code": contents, "lexer": "text", "expiry": "1week"}
|
||||
url = "https://bpaste.net"
|
||||
try:
|
||||
response = (
|
||||
response: str = (
|
||||
urlopen(url, data=urlencode(params).encode("ascii")).read().decode("utf-8")
|
||||
) # type: str
|
||||
)
|
||||
except OSError as exc_info: # urllib errors
|
||||
return "bad response: %s" % exc_info
|
||||
m = re.search(r'href="/raw/(\w+)"', response)
|
||||
@@ -107,4 +107,4 @@ def pytest_terminal_summary(terminalreporter: TerminalReporter) -> None:
|
||||
s = file.getvalue()
|
||||
assert len(s)
|
||||
pastebinurl = create_new_paste(s)
|
||||
terminalreporter.write_line("{} --> {}".format(msg, pastebinurl))
|
||||
terminalreporter.write_line(f"{msg} --> {pastebinurl}")
|
||||
|
||||
@@ -9,11 +9,17 @@ import sys
|
||||
import uuid
|
||||
import warnings
|
||||
from enum import Enum
|
||||
from errno import EBADF
|
||||
from errno import ELOOP
|
||||
from errno import ENOENT
|
||||
from errno import ENOTDIR
|
||||
from functools import partial
|
||||
from os.path import expanduser
|
||||
from os.path import expandvars
|
||||
from os.path import isabs
|
||||
from os.path import sep
|
||||
from pathlib import Path
|
||||
from pathlib import PurePath
|
||||
from posixpath import sep as posix_sep
|
||||
from types import ModuleType
|
||||
from typing import Callable
|
||||
@@ -30,19 +36,29 @@ from _pytest.compat import assert_never
|
||||
from _pytest.outcomes import skip
|
||||
from _pytest.warning_types import PytestWarning
|
||||
|
||||
if sys.version_info[:2] >= (3, 6):
|
||||
from pathlib import Path, PurePath
|
||||
else:
|
||||
from pathlib2 import Path, PurePath
|
||||
|
||||
__all__ = ["Path", "PurePath"]
|
||||
|
||||
|
||||
LOCK_TIMEOUT = 60 * 60 * 3
|
||||
LOCK_TIMEOUT = 60 * 60 * 24 * 3
|
||||
|
||||
|
||||
_AnyPurePath = TypeVar("_AnyPurePath", bound=PurePath)
|
||||
|
||||
# The following function, variables and comments were
|
||||
# copied from cpython 3.9 Lib/pathlib.py file.
|
||||
|
||||
# EBADF - guard against macOS `stat` throwing EBADF
|
||||
_IGNORED_ERRORS = (ENOENT, ENOTDIR, EBADF, ELOOP)
|
||||
|
||||
_IGNORED_WINERRORS = (
|
||||
21, # ERROR_NOT_READY - drive exists but is not accessible
|
||||
1921, # ERROR_CANT_RESOLVE_FILENAME - fix for broken symlink pointing to itself
|
||||
)
|
||||
|
||||
|
||||
def _ignore_error(exception):
|
||||
return (
|
||||
getattr(exception, "errno", None) in _IGNORED_ERRORS
|
||||
or getattr(exception, "winerror", None) in _IGNORED_WINERRORS
|
||||
)
|
||||
|
||||
|
||||
def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
|
||||
return path.joinpath(".lock")
|
||||
@@ -69,9 +85,7 @@ def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
|
||||
|
||||
if not isinstance(excvalue, PermissionError):
|
||||
warnings.warn(
|
||||
PytestWarning(
|
||||
"(rm_rf) error removing {}\n{}: {}".format(path, exctype, excvalue)
|
||||
)
|
||||
PytestWarning(f"(rm_rf) error removing {path}\n{exctype}: {excvalue}")
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -206,7 +220,7 @@ def make_numbered_dir(root: Path, prefix: str) -> Path:
|
||||
# try up to 10 times to create the folder
|
||||
max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1)
|
||||
new_number = max_existing + 1
|
||||
new_path = root.joinpath("{}{}".format(prefix, new_number))
|
||||
new_path = root.joinpath(f"{prefix}{new_number}")
|
||||
try:
|
||||
new_path.mkdir()
|
||||
except Exception:
|
||||
@@ -227,7 +241,7 @@ def create_cleanup_lock(p: Path) -> Path:
|
||||
try:
|
||||
fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644)
|
||||
except FileExistsError as e:
|
||||
raise OSError("cannot create lockfile in {path}".format(path=p)) from e
|
||||
raise OSError(f"cannot create lockfile in {p}") from e
|
||||
else:
|
||||
pid = os.getpid()
|
||||
spid = str(pid).encode()
|
||||
@@ -264,7 +278,7 @@ def maybe_delete_a_numbered_dir(path: Path) -> None:
|
||||
lock_path = create_cleanup_lock(path)
|
||||
parent = path.parent
|
||||
|
||||
garbage = parent.joinpath("garbage-{}".format(uuid.uuid4()))
|
||||
garbage = parent.joinpath(f"garbage-{uuid.uuid4()}")
|
||||
path.rename(garbage)
|
||||
rm_rf(garbage)
|
||||
except OSError:
|
||||
@@ -407,7 +421,7 @@ def fnmatch_ex(pattern: str, path) -> bool:
|
||||
else:
|
||||
name = str(path)
|
||||
if path.is_absolute() and not os.path.isabs(pattern):
|
||||
pattern = "*{}{}".format(os.sep, pattern)
|
||||
pattern = f"*{os.sep}{pattern}"
|
||||
return fnmatch.fnmatch(name, pattern)
|
||||
|
||||
|
||||
@@ -421,7 +435,7 @@ def symlink_or_skip(src, dst, **kwargs):
|
||||
try:
|
||||
os.symlink(str(src), str(dst), **kwargs)
|
||||
except OSError as e:
|
||||
skip("symlinks not supported: {}".format(e))
|
||||
skip(f"symlinks not supported: {e}")
|
||||
|
||||
|
||||
class ImportMode(Enum):
|
||||
@@ -444,7 +458,7 @@ class ImportPathMismatchError(ImportError):
|
||||
def import_path(
|
||||
p: Union[str, py.path.local, Path],
|
||||
*,
|
||||
mode: Union[str, ImportMode] = ImportMode.prepend
|
||||
mode: Union[str, ImportMode] = ImportMode.prepend,
|
||||
) -> ModuleType:
|
||||
"""Import and return a module from the given path, which can be a file (a module) or
|
||||
a directory (a package).
|
||||
@@ -529,7 +543,7 @@ def import_path(
|
||||
module_file = module_file[: -(len(os.path.sep + "__init__.py"))]
|
||||
|
||||
try:
|
||||
is_same = os.path.samefile(str(path), module_file)
|
||||
is_same = _is_same(str(path), module_file)
|
||||
except FileNotFoundError:
|
||||
is_same = False
|
||||
|
||||
@@ -539,6 +553,20 @@ def import_path(
|
||||
return mod
|
||||
|
||||
|
||||
# Implement a special _is_same function on Windows which returns True if the two filenames
|
||||
# compare equal, to circumvent os.path.samefile returning False for mounts in UNC (#7678).
|
||||
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:
|
||||
return os.path.samefile(f1, f2)
|
||||
|
||||
|
||||
def resolve_package_path(path: Path) -> Optional[Path]:
|
||||
"""Return the Python package path by looking for the last
|
||||
directory upwards which still contains an __init__.py.
|
||||
@@ -563,10 +591,25 @@ def visit(
|
||||
|
||||
Entries at each directory level are sorted.
|
||||
"""
|
||||
entries = sorted(os.scandir(path), key=lambda entry: entry.name)
|
||||
|
||||
# Skip entries with symlink loops and other brokenness, so the caller doesn't
|
||||
# have to deal with it.
|
||||
entries = []
|
||||
for entry in os.scandir(path):
|
||||
try:
|
||||
entry.is_file()
|
||||
except OSError as err:
|
||||
if _ignore_error(err):
|
||||
continue
|
||||
raise
|
||||
entries.append(entry)
|
||||
|
||||
entries.sort(key=lambda entry: entry.name)
|
||||
|
||||
yield from entries
|
||||
|
||||
for entry in entries:
|
||||
if entry.is_dir(follow_symlinks=False) and recurse(entry):
|
||||
if entry.is_dir() and recurse(entry):
|
||||
yield from visit(entry.path, recurse)
|
||||
|
||||
|
||||
@@ -581,7 +624,10 @@ def absolutepath(path: Union[Path, str]) -> Path:
|
||||
|
||||
def commonpath(path1: Path, path2: Path) -> Optional[Path]:
|
||||
"""Return the common part shared with the other path, or None if there is
|
||||
no common part."""
|
||||
no common part.
|
||||
|
||||
If one path is relative and one is absolute, returns None.
|
||||
"""
|
||||
try:
|
||||
return Path(os.path.commonpath((str(path1), str(path2))))
|
||||
except ValueError:
|
||||
@@ -592,13 +638,17 @@ def bestrelpath(directory: Path, dest: Path) -> str:
|
||||
"""Return a string which is a relative path from directory to dest such
|
||||
that directory/bestrelpath == dest.
|
||||
|
||||
The paths must be either both absolute or both relative.
|
||||
|
||||
If no such path can be determined, returns dest.
|
||||
"""
|
||||
if dest == directory:
|
||||
return os.curdir
|
||||
# Find the longest common directory.
|
||||
base = commonpath(directory, dest)
|
||||
# Can be the case on Windows.
|
||||
# Can be the case on Windows for two absolute paths on different drives.
|
||||
# Can be the case for two relative paths without common prefix.
|
||||
# Can be the case for a relative path and an absolute path.
|
||||
if not base:
|
||||
return str(dest)
|
||||
reldirectory = directory.relative_to(base)
|
||||
|
||||