Compare commits
278 Commits
6.2.0.dev0
...
6.3.0.dev0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09582048be | ||
|
|
54a7356a9f | ||
|
|
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
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
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
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
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
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$/
|
||||
14
AUTHORS
14
AUTHORS
@@ -32,6 +32,7 @@ Anthony Sottile
|
||||
Anton Lodder
|
||||
Antony Lee
|
||||
Arel Cordero
|
||||
Ariel Pillemer
|
||||
Armin Rigo
|
||||
Aron Coyle
|
||||
Aron Curzon
|
||||
@@ -60,6 +61,7 @@ Christian Fetzer
|
||||
Christian Neumüller
|
||||
Christian Theunert
|
||||
Christian Tismer
|
||||
Christine Mecklenborg
|
||||
Christoph Buelter
|
||||
Christopher Dignam
|
||||
Christopher Gilling
|
||||
@@ -87,6 +89,7 @@ Dhiren Serai
|
||||
Diego Russo
|
||||
Dmitry Dygalo
|
||||
Dmitry Pribysh
|
||||
Dominic Mortlock
|
||||
Duncan Betts
|
||||
Edison Gustavo Muenz
|
||||
Edoardo Batini
|
||||
@@ -107,6 +110,7 @@ Florian Bruhin
|
||||
Florian Dahlitz
|
||||
Floris Bruynooghe
|
||||
Gabriel Reis
|
||||
Garvit Shubham
|
||||
Gene Wood
|
||||
George Kussumoto
|
||||
Georgy Dyuldin
|
||||
@@ -129,6 +133,7 @@ Ilya Konstantinov
|
||||
Ionuț Turturică
|
||||
Iwan Briquemont
|
||||
Jaap Broekhuizen
|
||||
Jakob van Santen
|
||||
Jakub Mitoraj
|
||||
Jan Balster
|
||||
Janne Vanhala
|
||||
@@ -153,6 +158,7 @@ Justyna Janczyszyn
|
||||
Kale Kundert
|
||||
Kamran Ahmad
|
||||
Karl O. Pinc
|
||||
Karthikeyan Singaravelan
|
||||
Katarzyna Jachim
|
||||
Katarzyna Król
|
||||
Katerina Koukiou
|
||||
@@ -195,6 +201,7 @@ Matthias Hafner
|
||||
Maxim Filipenko
|
||||
Maximilian Cosmo Sitter
|
||||
mbyt
|
||||
Mickey Pashov
|
||||
Michael Aquilina
|
||||
Michael Birtwell
|
||||
Michael Droettboom
|
||||
@@ -227,11 +234,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 +266,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 +286,7 @@ Sven-Hendrik Haase
|
||||
Sylvain Marié
|
||||
Tadek Teleżyński
|
||||
Takafumi Arakaki
|
||||
Tanvi Mehta
|
||||
Tarcisio Fischer
|
||||
Tareq Alayan
|
||||
Ted Xiao
|
||||
@@ -310,3 +323,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
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
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,9 @@ Release announcements
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
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
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
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
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
|
||||
@@ -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,234 @@ with advance notice in the **Deprecations** section of releases.
|
||||
|
||||
.. towncrier release notes start
|
||||
|
||||
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 +2070,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}")
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -919,7 +919,7 @@ Running the above tests results in the following test IDs being used:
|
||||
<Function test_ehlo[mail.python.org]>
|
||||
<Function test_noop[mail.python.org]>
|
||||
|
||||
========================== no tests ran in 0.12s ===========================
|
||||
======================= 10 tests collected in 0.12s ========================
|
||||
|
||||
.. _`fixture-parametrize-marks`:
|
||||
|
||||
@@ -947,7 +947,7 @@ Example:
|
||||
|
||||
Running this test will *skip* the invocation of ``data_set`` with value ``2``:
|
||||
|
||||
.. code-block:: pytest
|
||||
.. code-block::
|
||||
|
||||
$ pytest test_fixture_marks.py -v
|
||||
=========================== test session starts ============================
|
||||
@@ -958,7 +958,7 @@ Running this test will *skip* the invocation of ``data_set`` with value ``2``:
|
||||
|
||||
test_fixture_marks.py::test_data[0] PASSED [ 33%]
|
||||
test_fixture_marks.py::test_data[1] PASSED [ 66%]
|
||||
test_fixture_marks.py::test_data[2] SKIPPED [100%]
|
||||
test_fixture_marks.py::test_data[2] SKIPPED (unconditional skip) [100%]
|
||||
|
||||
======================= 2 passed, 1 skipped in 0.12s =======================
|
||||
|
||||
|
||||
@@ -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.0
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -189,7 +189,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 +248,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 +314,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 +327,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 +341,7 @@ capsys
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
|
||||
.. autoclass:: CaptureFixture()
|
||||
.. autoclass:: pytest.CaptureFixture()
|
||||
:members:
|
||||
|
||||
|
||||
@@ -345,10 +352,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 +374,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 +396,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 +440,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 +482,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 +493,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 +524,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 +558,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 +578,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 +591,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 +603,7 @@ tmpdir
|
||||
|
||||
**Tutorial**: :doc:`tmpdir`
|
||||
|
||||
.. currentmodule:: _pytest.tmpdir
|
||||
|
||||
.. autofunction:: tmpdir()
|
||||
.. autofunction:: _pytest.tmpdir.tmpdir()
|
||||
:no-auto-options:
|
||||
|
||||
|
||||
@@ -601,12 +616,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 +680,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 +832,13 @@ Function
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
FunctionDefinition
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: _pytest.python.FunctionDefinition()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Item
|
||||
~~~~
|
||||
|
||||
@@ -1061,6 +1084,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 +1265,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 +1541,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 +1749,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 +1792,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()
|
||||
|
||||
14
setup.cfg
14
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
|
||||
@@ -40,22 +39,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 +62,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:
|
||||
|
||||
@@ -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
|
||||
@@ -40,7 +40,7 @@ class OutcomeException(BaseException):
|
||||
def __repr__(self) -> str:
|
||||
if self.msg:
|
||||
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).
|
||||
@@ -563,10 +577,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 +610,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 +624,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)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
66
src/_pytest/pytester_assertions.py
Normal file
66
src/_pytest/pytester_assertions.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""Helper plugin for pytester; should not be loaded on its own."""
|
||||
# This plugin contains assertions used by pytester. pytester cannot
|
||||
# contain them itself, since it is imported by the `pytest` module,
|
||||
# hence cannot be subject to assertion rewriting, which requires a
|
||||
# module to not be already imported.
|
||||
from typing import Dict
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
from typing import Union
|
||||
|
||||
from _pytest.reports import CollectReport
|
||||
from _pytest.reports import TestReport
|
||||
|
||||
|
||||
def assertoutcome(
|
||||
outcomes: Tuple[
|
||||
Sequence[TestReport],
|
||||
Sequence[Union[CollectReport, TestReport]],
|
||||
Sequence[Union[CollectReport, TestReport]],
|
||||
],
|
||||
passed: int = 0,
|
||||
skipped: int = 0,
|
||||
failed: int = 0,
|
||||
) -> None:
|
||||
__tracebackhide__ = True
|
||||
|
||||
realpassed, realskipped, realfailed = outcomes
|
||||
obtained = {
|
||||
"passed": len(realpassed),
|
||||
"skipped": len(realskipped),
|
||||
"failed": len(realfailed),
|
||||
}
|
||||
expected = {"passed": passed, "skipped": skipped, "failed": failed}
|
||||
assert obtained == expected, outcomes
|
||||
|
||||
|
||||
def assert_outcomes(
|
||||
outcomes: Dict[str, int],
|
||||
passed: int = 0,
|
||||
skipped: int = 0,
|
||||
failed: int = 0,
|
||||
errors: int = 0,
|
||||
xpassed: int = 0,
|
||||
xfailed: int = 0,
|
||||
) -> None:
|
||||
"""Assert that the specified outcomes appear with the respective
|
||||
numbers (0 means it didn't occur) in the text output from a test run."""
|
||||
__tracebackhide__ = True
|
||||
|
||||
obtained = {
|
||||
"passed": outcomes.get("passed", 0),
|
||||
"skipped": outcomes.get("skipped", 0),
|
||||
"failed": outcomes.get("failed", 0),
|
||||
"errors": outcomes.get("errors", 0),
|
||||
"xpassed": outcomes.get("xpassed", 0),
|
||||
"xfailed": outcomes.get("xfailed", 0),
|
||||
}
|
||||
expected = {
|
||||
"passed": passed,
|
||||
"skipped": skipped,
|
||||
"failed": failed,
|
||||
"errors": errors,
|
||||
"xpassed": xpassed,
|
||||
"xfailed": xfailed,
|
||||
}
|
||||
assert obtained == expected
|
||||
@@ -6,11 +6,9 @@ import itertools
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
import typing
|
||||
import warnings
|
||||
from collections import Counter
|
||||
from collections import defaultdict
|
||||
from collections.abc import Sequence
|
||||
from functools import partial
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
@@ -21,8 +19,11 @@ from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
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 py
|
||||
@@ -49,7 +50,6 @@ from _pytest.compat import REGEX_TYPE
|
||||
from _pytest.compat import safe_getattr
|
||||
from _pytest.compat import safe_isclass
|
||||
from _pytest.compat import STRING_TYPES
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ExitCode
|
||||
from _pytest.config import hookimpl
|
||||
@@ -73,7 +73,6 @@ from _pytest.warning_types import PytestCollectionWarning
|
||||
from _pytest.warning_types import PytestUnhandledCoroutineWarning
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Type
|
||||
from typing_extensions import Literal
|
||||
from _pytest.fixtures import _Scope
|
||||
|
||||
@@ -198,9 +197,7 @@ def pytest_collect_file(
|
||||
):
|
||||
return None
|
||||
ihook = parent.session.gethookproxy(path)
|
||||
module = ihook.pytest_pycollect_makemodule(
|
||||
path=path, parent=parent
|
||||
) # type: Module
|
||||
module: Module = ihook.pytest_pycollect_makemodule(path=path, parent=parent)
|
||||
return module
|
||||
return None
|
||||
|
||||
@@ -212,9 +209,9 @@ def path_matches_patterns(path: py.path.local, patterns: Iterable[str]) -> bool:
|
||||
|
||||
def pytest_pycollect_makemodule(path: py.path.local, parent) -> "Module":
|
||||
if path.basename == "__init__.py":
|
||||
pkg = Package.from_parent(parent, fspath=path) # type: Package
|
||||
pkg: Package = Package.from_parent(parent, fspath=path)
|
||||
return pkg
|
||||
mod = Module.from_parent(parent, fspath=path) # type: Module
|
||||
mod: Module = Module.from_parent(parent, fspath=path)
|
||||
return mod
|
||||
|
||||
|
||||
@@ -258,9 +255,9 @@ class PyobjMixin:
|
||||
|
||||
# Function and attributes that the mixin needs (for type-checking only).
|
||||
if TYPE_CHECKING:
|
||||
name = "" # type: str
|
||||
parent = None # type: Optional[nodes.Node]
|
||||
own_markers = [] # type: List[Mark]
|
||||
name: str = ""
|
||||
parent: Optional[nodes.Node] = None
|
||||
own_markers: List[Mark] = []
|
||||
|
||||
def getparent(self, cls: Type[nodes._NodeType]) -> Optional[nodes._NodeType]:
|
||||
...
|
||||
@@ -337,7 +334,7 @@ class PyobjMixin:
|
||||
file_path = sys.modules[obj.__module__].__file__
|
||||
if file_path.endswith(".pyc"):
|
||||
file_path = file_path[:-1]
|
||||
fspath = file_path # type: Union[py.path.local, str]
|
||||
fspath: Union[py.path.local, str] = file_path
|
||||
lineno = compat_co_firstlineno
|
||||
else:
|
||||
fspath, lineno = getfslineno(obj)
|
||||
@@ -421,8 +418,8 @@ class PyCollector(PyobjMixin, nodes.Collector):
|
||||
dicts = [getattr(self.obj, "__dict__", {})]
|
||||
for basecls in self.obj.__class__.__mro__:
|
||||
dicts.append(basecls.__dict__)
|
||||
seen = set() # type: Set[str]
|
||||
values = [] # type: List[Union[nodes.Item, nodes.Collector]]
|
||||
seen: Set[str] = set()
|
||||
values: List[Union[nodes.Item, nodes.Collector]] = []
|
||||
ihook = self.ihook
|
||||
for dic in dicts:
|
||||
# Note: seems like the dict can change during iteration -
|
||||
@@ -484,7 +481,7 @@ class PyCollector(PyobjMixin, nodes.Collector):
|
||||
fixtureinfo.prune_dependency_tree()
|
||||
|
||||
for callspec in metafunc._calls:
|
||||
subname = "{}[{}]".format(name, callspec.id)
|
||||
subname = f"{name}[{callspec.id}]"
|
||||
yield Function.from_parent(
|
||||
self,
|
||||
name=subname,
|
||||
@@ -525,7 +522,12 @@ class Module(nodes.File, PyCollector):
|
||||
if setup_module is None and teardown_module is None:
|
||||
return
|
||||
|
||||
@fixtures.fixture(autouse=True, scope="module")
|
||||
@fixtures.fixture(
|
||||
autouse=True,
|
||||
scope="module",
|
||||
# Use a unique name to speed up lookup.
|
||||
name=f"xunit_setup_module_fixture_{self.obj.__name__}",
|
||||
)
|
||||
def xunit_setup_module_fixture(request) -> Generator[None, None, None]:
|
||||
if setup_module is not None:
|
||||
_call_with_optional_argument(setup_module, request.module)
|
||||
@@ -549,7 +551,12 @@ class Module(nodes.File, PyCollector):
|
||||
if setup_function is None and teardown_function is None:
|
||||
return
|
||||
|
||||
@fixtures.fixture(autouse=True, scope="function")
|
||||
@fixtures.fixture(
|
||||
autouse=True,
|
||||
scope="function",
|
||||
# Use a unique name to speed up lookup.
|
||||
name=f"xunit_setup_function_fixture_{self.obj.__name__}",
|
||||
)
|
||||
def xunit_setup_function_fixture(request) -> Generator[None, None, None]:
|
||||
if request.instance is not None:
|
||||
# in this case we are bound to an instance, so we need to let
|
||||
@@ -668,7 +675,7 @@ class Package(Module):
|
||||
|
||||
def _collectfile(
|
||||
self, path: py.path.local, handle_dupes: bool = True
|
||||
) -> typing.Sequence[nodes.Collector]:
|
||||
) -> Sequence[nodes.Collector]:
|
||||
assert (
|
||||
path.isfile()
|
||||
), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format(
|
||||
@@ -697,7 +704,7 @@ class Package(Module):
|
||||
init_module, self.config.getini("python_files")
|
||||
):
|
||||
yield Module.from_parent(self, fspath=init_module)
|
||||
pkg_prefixes = set() # type: Set[py.path.local]
|
||||
pkg_prefixes: Set[py.path.local] = set()
|
||||
for direntry in visit(str(this_path), recurse=self._recurse):
|
||||
path = py.path.local(direntry.path)
|
||||
|
||||
@@ -792,7 +799,12 @@ class Class(PyCollector):
|
||||
if setup_class is None and teardown_class is None:
|
||||
return
|
||||
|
||||
@fixtures.fixture(autouse=True, scope="class")
|
||||
@fixtures.fixture(
|
||||
autouse=True,
|
||||
scope="class",
|
||||
# Use a unique name to speed up lookup.
|
||||
name=f"xunit_setup_class_fixture_{self.obj.__qualname__}",
|
||||
)
|
||||
def xunit_setup_class_fixture(cls) -> Generator[None, None, None]:
|
||||
if setup_class is not None:
|
||||
func = getimfunc(setup_class)
|
||||
@@ -816,7 +828,12 @@ class Class(PyCollector):
|
||||
if setup_method is None and teardown_method is None:
|
||||
return
|
||||
|
||||
@fixtures.fixture(autouse=True, scope="function")
|
||||
@fixtures.fixture(
|
||||
autouse=True,
|
||||
scope="function",
|
||||
# Use a unique name to speed up lookup.
|
||||
name=f"xunit_setup_method_fixture_{self.obj.__qualname__}",
|
||||
)
|
||||
def xunit_setup_method_fixture(self, request) -> Generator[None, None, None]:
|
||||
method = request.function
|
||||
if setup_method is not None:
|
||||
@@ -852,14 +869,14 @@ class Instance(PyCollector):
|
||||
|
||||
|
||||
def hasinit(obj: object) -> bool:
|
||||
init = getattr(obj, "__init__", None) # type: object
|
||||
init: object = getattr(obj, "__init__", None)
|
||||
if init:
|
||||
return init != object.__init__
|
||||
return False
|
||||
|
||||
|
||||
def hasnew(obj: object) -> bool:
|
||||
new = getattr(obj, "__new__", None) # type: object
|
||||
new: object = getattr(obj, "__new__", None)
|
||||
if new:
|
||||
return new != object.__new__
|
||||
return False
|
||||
@@ -869,13 +886,13 @@ def hasnew(obj: object) -> bool:
|
||||
class CallSpec2:
|
||||
def __init__(self, metafunc: "Metafunc") -> None:
|
||||
self.metafunc = metafunc
|
||||
self.funcargs = {} # type: Dict[str, object]
|
||||
self._idlist = [] # type: List[str]
|
||||
self.params = {} # type: Dict[str, object]
|
||||
self.funcargs: Dict[str, object] = {}
|
||||
self._idlist: List[str] = []
|
||||
self.params: Dict[str, object] = {}
|
||||
# Used for sorting parametrized resources.
|
||||
self._arg2scopenum = {} # type: Dict[str, int]
|
||||
self.marks = [] # type: List[Mark]
|
||||
self.indices = {} # type: Dict[str, int]
|
||||
self._arg2scopenum: Dict[str, int] = {}
|
||||
self.marks: List[Mark] = []
|
||||
self.indices: Dict[str, int] = {}
|
||||
|
||||
def copy(self) -> "CallSpec2":
|
||||
cs = CallSpec2(self.metafunc)
|
||||
@@ -889,7 +906,7 @@ class CallSpec2:
|
||||
|
||||
def _checkargnotcontained(self, arg: str) -> None:
|
||||
if arg in self.params or arg in self.funcargs:
|
||||
raise ValueError("duplicate {!r}".format(arg))
|
||||
raise ValueError(f"duplicate {arg!r}")
|
||||
|
||||
def getparam(self, name: str) -> object:
|
||||
try:
|
||||
@@ -904,7 +921,7 @@ class CallSpec2:
|
||||
def setmulti2(
|
||||
self,
|
||||
valtypes: Mapping[str, "Literal['params', 'funcargs']"],
|
||||
argnames: typing.Sequence[str],
|
||||
argnames: Sequence[str],
|
||||
valset: Iterable[object],
|
||||
id: str,
|
||||
marks: Iterable[Union[Mark, MarkDecorator]],
|
||||
@@ -919,7 +936,7 @@ class CallSpec2:
|
||||
elif valtype_for_arg == "funcargs":
|
||||
self.funcargs[arg] = val
|
||||
else: # pragma: no cover
|
||||
assert False, "Unhandled valtype for arg: {}".format(valtype_for_arg)
|
||||
assert False, f"Unhandled valtype for arg: {valtype_for_arg}"
|
||||
self.indices[arg] = param_index
|
||||
self._arg2scopenum[arg] = scopenum
|
||||
self._idlist.append(id)
|
||||
@@ -943,6 +960,7 @@ class Metafunc:
|
||||
cls=None,
|
||||
module=None,
|
||||
) -> None:
|
||||
#: Access to the underlying :class:`_pytest.python.FunctionDefinition`.
|
||||
self.definition = definition
|
||||
|
||||
#: Access to the :class:`_pytest.config.Config` object for the test session.
|
||||
@@ -960,14 +978,14 @@ class Metafunc:
|
||||
#: Class object where the test function is defined in or ``None``.
|
||||
self.cls = cls
|
||||
|
||||
self._calls = [] # type: List[CallSpec2]
|
||||
self._calls: List[CallSpec2] = []
|
||||
self._arg2fixturedefs = fixtureinfo.name2fixturedefs
|
||||
|
||||
def parametrize(
|
||||
self,
|
||||
argnames: Union[str, List[str], Tuple[str, ...]],
|
||||
argvalues: Iterable[Union[ParameterSet, typing.Sequence[object], object]],
|
||||
indirect: Union[bool, typing.Sequence[str]] = False,
|
||||
argvalues: Iterable[Union[ParameterSet, Sequence[object], object]],
|
||||
indirect: Union[bool, Sequence[str]] = False,
|
||||
ids: Optional[
|
||||
Union[
|
||||
Iterable[Union[None, str, float, int, bool]],
|
||||
@@ -976,7 +994,7 @@ class Metafunc:
|
||||
] = None,
|
||||
scope: "Optional[_Scope]" = None,
|
||||
*,
|
||||
_param_mark: Optional[Mark] = None
|
||||
_param_mark: Optional[Mark] = None,
|
||||
) -> None:
|
||||
"""Add new invocations to the underlying test function using the list
|
||||
of argvalues for the given argnames. Parametrization is performed
|
||||
@@ -1069,7 +1087,7 @@ class Metafunc:
|
||||
object.__setattr__(_param_mark._param_ids_from, "_param_ids_generated", ids)
|
||||
|
||||
scopenum = scope2index(
|
||||
scope, descr="parametrize() call in {}".format(self.function.__name__)
|
||||
scope, descr=f"parametrize() call in {self.function.__name__}"
|
||||
)
|
||||
|
||||
# Create the new calls: if we are parametrize() multiple times (by applying the decorator
|
||||
@@ -1093,14 +1111,14 @@ class Metafunc:
|
||||
|
||||
def _resolve_arg_ids(
|
||||
self,
|
||||
argnames: typing.Sequence[str],
|
||||
argnames: Sequence[str],
|
||||
ids: Optional[
|
||||
Union[
|
||||
Iterable[Union[None, str, float, int, bool]],
|
||||
Callable[[Any], Optional[object]],
|
||||
]
|
||||
],
|
||||
parameters: typing.Sequence[ParameterSet],
|
||||
parameters: Sequence[ParameterSet],
|
||||
nodeid: str,
|
||||
) -> List[str]:
|
||||
"""Resolve the actual ids for the given argnames, based on the ``ids`` parameter given
|
||||
@@ -1127,7 +1145,7 @@ class Metafunc:
|
||||
def _validate_ids(
|
||||
self,
|
||||
ids: Iterable[Union[None, str, float, int, bool]],
|
||||
parameters: typing.Sequence[ParameterSet],
|
||||
parameters: Sequence[ParameterSet],
|
||||
func_name: str,
|
||||
) -> List[Union[None, str]]:
|
||||
try:
|
||||
@@ -1162,9 +1180,7 @@ class Metafunc:
|
||||
return new_ids
|
||||
|
||||
def _resolve_arg_value_types(
|
||||
self,
|
||||
argnames: typing.Sequence[str],
|
||||
indirect: Union[bool, typing.Sequence[str]],
|
||||
self, argnames: Sequence[str], indirect: Union[bool, Sequence[str]],
|
||||
) -> Dict[str, "Literal['params', 'funcargs']"]:
|
||||
"""Resolve if each parametrized argument must be considered a
|
||||
parameter to a fixture or a "funcarg" to the function, based on the
|
||||
@@ -1178,9 +1194,9 @@ class Metafunc:
|
||||
* "funcargs" if the argname should be a parameter to the parametrized test function.
|
||||
"""
|
||||
if isinstance(indirect, bool):
|
||||
valtypes = dict.fromkeys(
|
||||
valtypes: Dict[str, Literal["params", "funcargs"]] = dict.fromkeys(
|
||||
argnames, "params" if indirect else "funcargs"
|
||||
) # type: Dict[str, Literal["params", "funcargs"]]
|
||||
)
|
||||
elif isinstance(indirect, Sequence):
|
||||
valtypes = dict.fromkeys(argnames, "funcargs")
|
||||
for arg in indirect:
|
||||
@@ -1202,9 +1218,7 @@ class Metafunc:
|
||||
return valtypes
|
||||
|
||||
def _validate_if_using_arg_names(
|
||||
self,
|
||||
argnames: typing.Sequence[str],
|
||||
indirect: Union[bool, typing.Sequence[str]],
|
||||
self, argnames: Sequence[str], indirect: Union[bool, Sequence[str]],
|
||||
) -> None:
|
||||
"""Check if all argnames are being used, by default values, or directly/indirectly.
|
||||
|
||||
@@ -1229,15 +1243,15 @@ class Metafunc:
|
||||
else:
|
||||
name = "fixture" if indirect else "argument"
|
||||
fail(
|
||||
"In {}: function uses no {} '{}'".format(func_name, name, arg),
|
||||
f"In {func_name}: function uses no {name} '{arg}'",
|
||||
pytrace=False,
|
||||
)
|
||||
|
||||
|
||||
def _find_parametrized_scope(
|
||||
argnames: typing.Sequence[str],
|
||||
arg2fixturedefs: Mapping[str, typing.Sequence[fixtures.FixtureDef[object]]],
|
||||
indirect: Union[bool, typing.Sequence[str]],
|
||||
argnames: Sequence[str],
|
||||
arg2fixturedefs: Mapping[str, Sequence[fixtures.FixtureDef[object]]],
|
||||
indirect: Union[bool, Sequence[str]],
|
||||
) -> "fixtures._Scope":
|
||||
"""Find the most appropriate scope for a parametrized call based on its arguments.
|
||||
|
||||
@@ -1296,14 +1310,14 @@ def _idval(
|
||||
if generated_id is not None:
|
||||
val = generated_id
|
||||
except Exception as e:
|
||||
prefix = "{}: ".format(nodeid) if nodeid is not None else ""
|
||||
prefix = f"{nodeid}: " if nodeid is not None else ""
|
||||
msg = "error raised while trying to determine id of parameter '{}' at position {}"
|
||||
msg = prefix + msg.format(argname, idx)
|
||||
raise ValueError(msg) from e
|
||||
elif config:
|
||||
hook_id = config.hook.pytest_make_parametrize_id(
|
||||
hook_id: Optional[str] = config.hook.pytest_make_parametrize_id(
|
||||
config=config, val=val, argname=argname
|
||||
) # type: Optional[str]
|
||||
)
|
||||
if hook_id:
|
||||
return hook_id
|
||||
|
||||
@@ -1320,7 +1334,7 @@ def _idval(
|
||||
return str(val)
|
||||
elif isinstance(getattr(val, "__name__", None), str):
|
||||
# Name of a class, function, module, etc.
|
||||
name = getattr(val, "__name__") # type: str
|
||||
name: str = getattr(val, "__name__")
|
||||
return name
|
||||
return str(argname) + str(idx)
|
||||
|
||||
@@ -1370,7 +1384,7 @@ def idmaker(
|
||||
test_id_counts = Counter(resolved_ids)
|
||||
|
||||
# Map the test ID to its next suffix.
|
||||
test_id_suffixes = defaultdict(int) # type: Dict[str, int]
|
||||
test_id_suffixes: Dict[str, int] = defaultdict(int)
|
||||
|
||||
# Suffix non-unique IDs to make them unique.
|
||||
for index, test_id in enumerate(resolved_ids):
|
||||
@@ -1405,7 +1419,7 @@ def _show_fixtures_per_test(config: Config, session: Session) -> None:
|
||||
return
|
||||
if verbose > 0:
|
||||
bestrel = get_best_relpath(fixture_def.func)
|
||||
funcargspec = "{} -- {}".format(argname, bestrel)
|
||||
funcargspec = f"{argname} -- {bestrel}"
|
||||
else:
|
||||
funcargspec = argname
|
||||
tw.line(funcargspec, green=True)
|
||||
@@ -1417,12 +1431,12 @@ def _show_fixtures_per_test(config: Config, session: Session) -> None:
|
||||
|
||||
def write_item(item: nodes.Item) -> None:
|
||||
# Not all items have _fixtureinfo attribute.
|
||||
info = getattr(item, "_fixtureinfo", None) # type: Optional[FuncFixtureInfo]
|
||||
info: Optional[FuncFixtureInfo] = getattr(item, "_fixtureinfo", None)
|
||||
if info is None or not info.name2fixturedefs:
|
||||
# This test item does not use any fixtures.
|
||||
return
|
||||
tw.line()
|
||||
tw.sep("-", "fixtures used by {}".format(item.name))
|
||||
tw.sep("-", f"fixtures used by {item.name}")
|
||||
# TODO: Fix this type ignore.
|
||||
tw.sep("-", "({})".format(get_best_relpath(item.function))) # type: ignore[attr-defined]
|
||||
# dict key not used in loop but needed for sorting.
|
||||
@@ -1454,7 +1468,7 @@ def _showfixtures_main(config: Config, session: Session) -> None:
|
||||
fm = session._fixturemanager
|
||||
|
||||
available = []
|
||||
seen = set() # type: Set[Tuple[str, str]]
|
||||
seen: Set[Tuple[str, str]] = set()
|
||||
|
||||
for argname, fixturedefs in fm._arg2fixturedefs.items():
|
||||
assert fixturedefs is not None
|
||||
@@ -1481,7 +1495,7 @@ def _showfixtures_main(config: Config, session: Session) -> None:
|
||||
if currentmodule != module:
|
||||
if not module.startswith("_pytest."):
|
||||
tw.line()
|
||||
tw.sep("-", "fixtures defined from {}".format(module))
|
||||
tw.sep("-", f"fixtures defined from {module}")
|
||||
currentmodule = module
|
||||
if verbose <= 0 and argname[0] == "_":
|
||||
continue
|
||||
@@ -1496,7 +1510,7 @@ def _showfixtures_main(config: Config, session: Session) -> None:
|
||||
if doc:
|
||||
write_docstring(tw, doc)
|
||||
else:
|
||||
tw.line(" {}: no docstring available".format(loc), red=True)
|
||||
tw.line(f" {loc}: no docstring available", red=True)
|
||||
tw.line()
|
||||
|
||||
|
||||
@@ -1595,7 +1609,7 @@ class Function(PyobjMixin, nodes.Item):
|
||||
fixtureinfo = self.session._fixturemanager.getfixtureinfo(
|
||||
self, self.obj, self.cls, funcargs=True
|
||||
)
|
||||
self._fixtureinfo = fixtureinfo # type: FuncFixtureInfo
|
||||
self._fixtureinfo: FuncFixtureInfo = fixtureinfo
|
||||
self.fixturenames = fixtureinfo.names_closure
|
||||
self._initrequest()
|
||||
|
||||
@@ -1605,8 +1619,8 @@ class Function(PyobjMixin, nodes.Item):
|
||||
return super().from_parent(parent=parent, **kw)
|
||||
|
||||
def _initrequest(self) -> None:
|
||||
self.funcargs = {} # type: Dict[str, object]
|
||||
self._request = fixtures.FixtureRequest(self)
|
||||
self.funcargs: Dict[str, object] = {}
|
||||
self._request = fixtures.FixtureRequest(self, _ispytest=True)
|
||||
|
||||
@property
|
||||
def function(self):
|
||||
@@ -1634,7 +1648,7 @@ class Function(PyobjMixin, nodes.Item):
|
||||
|
||||
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
|
||||
if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False):
|
||||
code = _pytest._code.Code(get_real_func(self.obj))
|
||||
code = _pytest._code.Code.from_function(get_real_func(self.obj))
|
||||
path, firstlineno = code.path, code.firstlineno
|
||||
traceback = excinfo.traceback
|
||||
ntraceback = traceback.cut(path=path, firstlineno=firstlineno)
|
||||
@@ -1664,10 +1678,12 @@ class Function(PyobjMixin, nodes.Item):
|
||||
|
||||
|
||||
class FunctionDefinition(Function):
|
||||
"""Internal hack until we get actual definition nodes instead of the
|
||||
crappy metafunc hack."""
|
||||
"""
|
||||
This class is a step gap solution until we evolve to have actual function definition nodes
|
||||
and manage to get rid of ``metafunc``.
|
||||
"""
|
||||
|
||||
def runtest(self) -> None:
|
||||
raise RuntimeError("function definitions are not supposed to be used")
|
||||
raise RuntimeError("function definitions are not supposed to be run as tests")
|
||||
|
||||
setup = runtest
|
||||
|
||||
@@ -4,31 +4,28 @@ from collections.abc import Iterable
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Sized
|
||||
from decimal import Decimal
|
||||
from numbers import Number
|
||||
from numbers import Complex
|
||||
from types import TracebackType
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Generic
|
||||
from typing import Optional
|
||||
from typing import overload
|
||||
from typing import Pattern
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
import _pytest._code
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import overload
|
||||
from _pytest.compat import STRING_TYPES
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.outcomes import fail
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Type
|
||||
|
||||
|
||||
def _non_numeric_type_error(value, at: Optional[str]) -> TypeError:
|
||||
at_str = " at {}".format(at) if at else ""
|
||||
at_str = f" at {at}" if at else ""
|
||||
return TypeError(
|
||||
"cannot make approximate comparisons to non-numeric values: {!r} {}".format(
|
||||
value, at_str
|
||||
@@ -101,7 +98,7 @@ class ApproxNumpy(ApproxBase):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
list_scalars = _recursive_list_map(self._approx_scalar, self.expected.tolist())
|
||||
return "approx({!r})".format(list_scalars)
|
||||
return f"approx({list_scalars!r})"
|
||||
|
||||
def __eq__(self, actual) -> bool:
|
||||
import numpy as np
|
||||
@@ -112,9 +109,7 @@ class ApproxNumpy(ApproxBase):
|
||||
try:
|
||||
actual = np.asarray(actual)
|
||||
except Exception as e:
|
||||
raise TypeError(
|
||||
"cannot compare '{}' to numpy.ndarray".format(actual)
|
||||
) from e
|
||||
raise TypeError(f"cannot compare '{actual}' to numpy.ndarray") from e
|
||||
|
||||
if not np.isscalar(actual) and actual.shape != self.expected.shape:
|
||||
return False
|
||||
@@ -146,7 +141,10 @@ class ApproxMapping(ApproxBase):
|
||||
)
|
||||
|
||||
def __eq__(self, actual) -> bool:
|
||||
if set(actual.keys()) != set(self.expected.keys()):
|
||||
try:
|
||||
if set(actual.keys()) != set(self.expected.keys()):
|
||||
return False
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
return ApproxBase.__eq__(self, actual)
|
||||
@@ -161,8 +159,6 @@ class ApproxMapping(ApproxBase):
|
||||
if isinstance(value, type(self.expected)):
|
||||
msg = "pytest.approx() does not support nested dictionaries: key={!r} value={!r}\n full mapping={}"
|
||||
raise TypeError(msg.format(key, value, pprint.pformat(self.expected)))
|
||||
elif not isinstance(value, Number):
|
||||
raise _non_numeric_type_error(self.expected, at="key={!r}".format(key))
|
||||
|
||||
|
||||
class ApproxSequencelike(ApproxBase):
|
||||
@@ -177,7 +173,10 @@ class ApproxSequencelike(ApproxBase):
|
||||
)
|
||||
|
||||
def __eq__(self, actual) -> bool:
|
||||
if len(actual) != len(self.expected):
|
||||
try:
|
||||
if len(actual) != len(self.expected):
|
||||
return False
|
||||
except TypeError:
|
||||
return False
|
||||
return ApproxBase.__eq__(self, actual)
|
||||
|
||||
@@ -190,10 +189,6 @@ class ApproxSequencelike(ApproxBase):
|
||||
if isinstance(x, type(self.expected)):
|
||||
msg = "pytest.approx() does not support nested data structures: {!r} at index {}\n full sequence: {}"
|
||||
raise TypeError(msg.format(x, index, pprint.pformat(self.expected)))
|
||||
elif not isinstance(x, Number):
|
||||
raise _non_numeric_type_error(
|
||||
self.expected, at="index {}".format(index)
|
||||
)
|
||||
|
||||
|
||||
class ApproxScalar(ApproxBase):
|
||||
@@ -201,8 +196,8 @@ class ApproxScalar(ApproxBase):
|
||||
|
||||
# Using Real should be better than this Union, but not possible yet:
|
||||
# https://github.com/python/typeshed/pull/3108
|
||||
DEFAULT_ABSOLUTE_TOLERANCE = 1e-12 # type: Union[float, Decimal]
|
||||
DEFAULT_RELATIVE_TOLERANCE = 1e-6 # type: Union[float, Decimal]
|
||||
DEFAULT_ABSOLUTE_TOLERANCE: Union[float, Decimal] = 1e-12
|
||||
DEFAULT_RELATIVE_TOLERANCE: Union[float, Decimal] = 1e-6
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return a string communicating both the expected value and the
|
||||
@@ -211,21 +206,28 @@ class ApproxScalar(ApproxBase):
|
||||
For example, ``1.0 ± 1e-6``, ``(3+4j) ± 5e-6 ∠ ±180°``.
|
||||
"""
|
||||
|
||||
# Infinities aren't compared using tolerances, so don't show a
|
||||
# tolerance. Need to call abs to handle complex numbers, e.g. (inf + 1j).
|
||||
if math.isinf(abs(self.expected)):
|
||||
# Don't show a tolerance for values that aren't compared using
|
||||
# tolerances, i.e. non-numerics and infinities. Need to call abs to
|
||||
# handle complex numbers, e.g. (inf + 1j).
|
||||
if (not isinstance(self.expected, (Complex, Decimal))) or math.isinf(
|
||||
abs(self.expected) # type: ignore[arg-type]
|
||||
):
|
||||
return str(self.expected)
|
||||
|
||||
# If a sensible tolerance can't be calculated, self.tolerance will
|
||||
# raise a ValueError. In this case, display '???'.
|
||||
try:
|
||||
vetted_tolerance = "{:.1e}".format(self.tolerance)
|
||||
if isinstance(self.expected, complex) and not math.isinf(self.tolerance):
|
||||
vetted_tolerance = f"{self.tolerance:.1e}"
|
||||
if (
|
||||
isinstance(self.expected, Complex)
|
||||
and self.expected.imag
|
||||
and not math.isinf(self.tolerance)
|
||||
):
|
||||
vetted_tolerance += " ∠ ±180°"
|
||||
except ValueError:
|
||||
vetted_tolerance = "???"
|
||||
|
||||
return "{} ± {}".format(self.expected, vetted_tolerance)
|
||||
return f"{self.expected} ± {vetted_tolerance}"
|
||||
|
||||
def __eq__(self, actual) -> bool:
|
||||
"""Return whether the given value is equal to the expected value
|
||||
@@ -239,11 +241,20 @@ class ApproxScalar(ApproxBase):
|
||||
if actual == self.expected:
|
||||
return True
|
||||
|
||||
# If either type is non-numeric, fall back to strict equality.
|
||||
# NB: we need Complex, rather than just Number, to ensure that __abs__,
|
||||
# __sub__, and __float__ are defined.
|
||||
if not (
|
||||
isinstance(self.expected, (Complex, Decimal))
|
||||
and isinstance(actual, (Complex, Decimal))
|
||||
):
|
||||
return False
|
||||
|
||||
# Allow the user to control whether NaNs are considered equal to each
|
||||
# other or not. The abs() calls are for compatibility with complex
|
||||
# numbers.
|
||||
if math.isnan(abs(self.expected)):
|
||||
return self.nan_ok and math.isnan(abs(actual))
|
||||
if math.isnan(abs(self.expected)): # type: ignore[arg-type]
|
||||
return self.nan_ok and math.isnan(abs(actual)) # type: ignore[arg-type]
|
||||
|
||||
# Infinity shouldn't be approximately equal to anything but itself, but
|
||||
# if there's a relative tolerance, it will be infinite and infinity
|
||||
@@ -251,11 +262,11 @@ class ApproxScalar(ApproxBase):
|
||||
# case would have been short circuited above, so here we can just
|
||||
# return false if the expected value is infinite. The abs() call is
|
||||
# for compatibility with complex numbers.
|
||||
if math.isinf(abs(self.expected)):
|
||||
if math.isinf(abs(self.expected)): # type: ignore[arg-type]
|
||||
return False
|
||||
|
||||
# Return true if the two numbers are within the tolerance.
|
||||
result = abs(self.expected - actual) <= self.tolerance # type: bool
|
||||
result: bool = abs(self.expected - actual) <= self.tolerance
|
||||
return result
|
||||
|
||||
# Ignore type because of https://github.com/python/mypy/issues/4266.
|
||||
@@ -278,7 +289,7 @@ class ApproxScalar(ApproxBase):
|
||||
|
||||
if absolute_tolerance < 0:
|
||||
raise ValueError(
|
||||
"absolute tolerance can't be negative: {}".format(absolute_tolerance)
|
||||
f"absolute tolerance can't be negative: {absolute_tolerance}"
|
||||
)
|
||||
if math.isnan(absolute_tolerance):
|
||||
raise ValueError("absolute tolerance can't be NaN.")
|
||||
@@ -300,7 +311,7 @@ class ApproxScalar(ApproxBase):
|
||||
|
||||
if relative_tolerance < 0:
|
||||
raise ValueError(
|
||||
"relative tolerance can't be negative: {}".format(absolute_tolerance)
|
||||
f"relative tolerance can't be negative: {absolute_tolerance}"
|
||||
)
|
||||
if math.isnan(relative_tolerance):
|
||||
raise ValueError("relative tolerance can't be NaN.")
|
||||
@@ -409,6 +420,18 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
|
||||
>>> 1 + 1e-8 == approx(1, rel=1e-6, abs=1e-12)
|
||||
True
|
||||
|
||||
You can also use ``approx`` to compare nonnumeric types, or dicts and
|
||||
sequences containing nonnumeric types, in which case it falls back to
|
||||
strict equality. This can be useful for comparing dicts and sequences that
|
||||
can contain optional values::
|
||||
|
||||
>>> {"required": 1.0000005, "optional": None} == approx({"required": 1, "optional": None})
|
||||
True
|
||||
>>> [None, 1.0000005] == approx([None,1])
|
||||
True
|
||||
>>> ["foo", 1.0000005] == approx([None,1])
|
||||
False
|
||||
|
||||
If you're thinking about using ``approx``, then you might want to know how
|
||||
it compares to other good ways of comparing floating-point numbers. All of
|
||||
these algorithms are based on relative and absolute tolerances and should
|
||||
@@ -420,7 +443,7 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
|
||||
both ``a`` and ``b``, this test is symmetric (i.e. neither ``a`` nor
|
||||
``b`` is a "reference value"). You have to specify an absolute tolerance
|
||||
if you want to compare to ``0.0`` because there is no tolerance by
|
||||
default. Only available in python>=3.5. `More information...`__
|
||||
default. `More information...`__
|
||||
|
||||
__ https://docs.python.org/3/library/math.html#math.isclose
|
||||
|
||||
@@ -431,7 +454,7 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
|
||||
think of ``b`` as the reference value. Support for comparing sequences
|
||||
is provided by ``numpy.allclose``. `More information...`__
|
||||
|
||||
__ http://docs.scipy.org/doc/numpy-1.10.0/reference/generated/numpy.isclose.html
|
||||
__ https://numpy.org/doc/stable/reference/generated/numpy.isclose.html
|
||||
|
||||
- ``unittest.TestCase.assertAlmostEqual(a, b)``: True if ``a`` and ``b``
|
||||
are within an absolute tolerance of ``1e-7``. No relative tolerance is
|
||||
@@ -466,6 +489,14 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
|
||||
follows a fixed behavior. `More information...`__
|
||||
|
||||
__ https://docs.python.org/3/reference/datamodel.html#object.__ge__
|
||||
|
||||
.. versionchanged:: 3.7.1
|
||||
``approx`` raises ``TypeError`` when it encounters a dict value or
|
||||
sequence element of nonnumeric type.
|
||||
|
||||
.. versionchanged:: 6.1.0
|
||||
``approx`` falls back to strict equality for nonnumeric types instead
|
||||
of raising ``TypeError``.
|
||||
"""
|
||||
|
||||
# Delegate the comparison to a class that knows how to deal with the type
|
||||
@@ -486,9 +517,7 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
|
||||
__tracebackhide__ = True
|
||||
|
||||
if isinstance(expected, Decimal):
|
||||
cls = ApproxDecimal # type: Type[ApproxBase]
|
||||
elif isinstance(expected, Number):
|
||||
cls = ApproxScalar
|
||||
cls: Type[ApproxBase] = ApproxDecimal
|
||||
elif isinstance(expected, Mapping):
|
||||
cls = ApproxMapping
|
||||
elif _is_numpy_array(expected):
|
||||
@@ -501,7 +530,7 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
|
||||
):
|
||||
cls = ApproxSequencelike
|
||||
else:
|
||||
raise _non_numeric_type_error(expected, at=None)
|
||||
cls = ApproxScalar
|
||||
|
||||
return cls(expected, rel, abs, nan_ok)
|
||||
|
||||
@@ -513,7 +542,7 @@ def _is_numpy_array(obj: object) -> bool:
|
||||
"""
|
||||
import sys
|
||||
|
||||
np = sys.modules.get("numpy") # type: Any
|
||||
np: Any = sys.modules.get("numpy")
|
||||
if np is not None:
|
||||
return isinstance(obj, np.ndarray)
|
||||
return False
|
||||
@@ -526,27 +555,25 @@ _E = TypeVar("_E", bound=BaseException)
|
||||
|
||||
@overload
|
||||
def raises(
|
||||
expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]],
|
||||
expected_exception: Union[Type[_E], Tuple[Type[_E], ...]],
|
||||
*,
|
||||
match: "Optional[Union[str, Pattern[str]]]" = ...
|
||||
match: Optional[Union[str, Pattern[str]]] = ...,
|
||||
) -> "RaisesContext[_E]":
|
||||
...
|
||||
|
||||
|
||||
@overload # noqa: F811
|
||||
def raises( # noqa: F811
|
||||
expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]],
|
||||
@overload
|
||||
def raises(
|
||||
expected_exception: Union[Type[_E], Tuple[Type[_E], ...]],
|
||||
func: Callable[..., Any],
|
||||
*args: Any,
|
||||
**kwargs: Any
|
||||
**kwargs: Any,
|
||||
) -> _pytest._code.ExceptionInfo[_E]:
|
||||
...
|
||||
|
||||
|
||||
def raises( # noqa: F811
|
||||
expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]],
|
||||
*args: Any,
|
||||
**kwargs: Any
|
||||
def raises(
|
||||
expected_exception: Union[Type[_E], Tuple[Type[_E], ...]], *args: Any, **kwargs: Any
|
||||
) -> Union["RaisesContext[_E]", _pytest._code.ExceptionInfo[_E]]:
|
||||
r"""Assert that a code block/function call raises ``expected_exception``
|
||||
or raise a failure exception otherwise.
|
||||
@@ -570,7 +597,8 @@ def raises( # noqa: F811
|
||||
Use ``pytest.raises`` as a context manager, which will capture the exception of the given
|
||||
type::
|
||||
|
||||
>>> with raises(ZeroDivisionError):
|
||||
>>> import pytest
|
||||
>>> with pytest.raises(ZeroDivisionError):
|
||||
... 1/0
|
||||
|
||||
If the code block does not raise the expected exception (``ZeroDivisionError`` in the example
|
||||
@@ -579,16 +607,16 @@ def raises( # noqa: F811
|
||||
You can also use the keyword argument ``match`` to assert that the
|
||||
exception matches a text or regex::
|
||||
|
||||
>>> with raises(ValueError, match='must be 0 or None'):
|
||||
>>> with pytest.raises(ValueError, match='must be 0 or None'):
|
||||
... raise ValueError("value must be 0 or None")
|
||||
|
||||
>>> with raises(ValueError, match=r'must be \d+$'):
|
||||
>>> with pytest.raises(ValueError, match=r'must be \d+$'):
|
||||
... raise ValueError("value must be 42")
|
||||
|
||||
The context manager produces an :class:`ExceptionInfo` object which can be used to inspect the
|
||||
details of the captured exception::
|
||||
|
||||
>>> with raises(ValueError) as exc_info:
|
||||
>>> with pytest.raises(ValueError) as exc_info:
|
||||
... raise ValueError("value must be 42")
|
||||
>>> assert exc_info.type is ValueError
|
||||
>>> assert exc_info.value.args[0] == "value must be 42"
|
||||
@@ -602,7 +630,7 @@ def raises( # noqa: F811
|
||||
not be executed. For example::
|
||||
|
||||
>>> value = 15
|
||||
>>> with raises(ValueError) as exc_info:
|
||||
>>> with pytest.raises(ValueError) as exc_info:
|
||||
... if value > 10:
|
||||
... raise ValueError("value must be <= 10")
|
||||
... assert exc_info.type is ValueError # this will not execute
|
||||
@@ -610,7 +638,7 @@ def raises( # noqa: F811
|
||||
Instead, the following approach must be taken (note the difference in
|
||||
scope)::
|
||||
|
||||
>>> with raises(ValueError) as exc_info:
|
||||
>>> with pytest.raises(ValueError) as exc_info:
|
||||
... if value > 10:
|
||||
... raise ValueError("value must be <= 10")
|
||||
...
|
||||
@@ -660,7 +688,7 @@ def raises( # noqa: F811
|
||||
__tracebackhide__ = True
|
||||
|
||||
if isinstance(expected_exception, type):
|
||||
excepted_exceptions = (expected_exception,) # type: Tuple[Type[_E], ...]
|
||||
excepted_exceptions: Tuple[Type[_E], ...] = (expected_exception,)
|
||||
else:
|
||||
excepted_exceptions = expected_exception
|
||||
for exc in excepted_exceptions:
|
||||
@@ -669,10 +697,10 @@ def raises( # noqa: F811
|
||||
not_a = exc.__name__ if isinstance(exc, type) else type(exc).__name__
|
||||
raise TypeError(msg.format(not_a))
|
||||
|
||||
message = "DID NOT RAISE {}".format(expected_exception)
|
||||
message = f"DID NOT RAISE {expected_exception}"
|
||||
|
||||
if not args:
|
||||
match = kwargs.pop("match", None) # type: Optional[Union[str, Pattern[str]]]
|
||||
match: Optional[Union[str, Pattern[str]]] = kwargs.pop("match", None)
|
||||
if kwargs:
|
||||
msg = "Unexpected keyword arguments passed to pytest.raises: "
|
||||
msg += ", ".join(sorted(kwargs))
|
||||
@@ -704,14 +732,14 @@ raises.Exception = fail.Exception # type: ignore
|
||||
class RaisesContext(Generic[_E]):
|
||||
def __init__(
|
||||
self,
|
||||
expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]],
|
||||
expected_exception: Union[Type[_E], Tuple[Type[_E], ...]],
|
||||
message: str,
|
||||
match_expr: Optional[Union[str, "Pattern[str]"]] = None,
|
||||
match_expr: Optional[Union[str, Pattern[str]]] = None,
|
||||
) -> None:
|
||||
self.expected_exception = expected_exception
|
||||
self.message = message
|
||||
self.match_expr = match_expr
|
||||
self.excinfo = None # type: Optional[_pytest._code.ExceptionInfo[_E]]
|
||||
self.excinfo: Optional[_pytest._code.ExceptionInfo[_E]] = None
|
||||
|
||||
def __enter__(self) -> _pytest._code.ExceptionInfo[_E]:
|
||||
self.excinfo = _pytest._code.ExceptionInfo.for_later()
|
||||
@@ -719,7 +747,7 @@ class RaisesContext(Generic[_E]):
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: Optional["Type[BaseException]"],
|
||||
exc_type: Optional[Type[BaseException]],
|
||||
exc_val: Optional[BaseException],
|
||||
exc_tb: Optional[TracebackType],
|
||||
) -> bool:
|
||||
@@ -730,9 +758,7 @@ class RaisesContext(Generic[_E]):
|
||||
if not issubclass(exc_type, self.expected_exception):
|
||||
return False
|
||||
# Cast to narrow the exception type now that it's verified.
|
||||
exc_info = cast(
|
||||
Tuple["Type[_E]", _E, TracebackType], (exc_type, exc_val, exc_tb)
|
||||
)
|
||||
exc_info = cast(Tuple[Type[_E], _E, TracebackType], (exc_type, exc_val, exc_tb))
|
||||
self.excinfo.fill_unfilled(exc_info)
|
||||
if self.match_expr is not None:
|
||||
self.excinfo.match(self.match_expr)
|
||||
|
||||
@@ -8,20 +8,18 @@ from typing import Generator
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import overload
|
||||
from typing import Pattern
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import overload
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.deprecated import check_ispytest
|
||||
from _pytest.fixtures import fixture
|
||||
from _pytest.outcomes import fail
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Type
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
@@ -33,7 +31,7 @@ def recwarn() -> Generator["WarningsRecorder", None, None]:
|
||||
See http://docs.python.org/library/warnings.html for information
|
||||
on warning categories.
|
||||
"""
|
||||
wrec = WarningsRecorder()
|
||||
wrec = WarningsRecorder(_ispytest=True)
|
||||
with wrec:
|
||||
warnings.simplefilter("default")
|
||||
yield wrec
|
||||
@@ -41,19 +39,17 @@ def recwarn() -> Generator["WarningsRecorder", None, None]:
|
||||
|
||||
@overload
|
||||
def deprecated_call(
|
||||
*, match: Optional[Union[str, "Pattern[str]"]] = ...
|
||||
*, match: Optional[Union[str, Pattern[str]]] = ...
|
||||
) -> "WarningsRecorder":
|
||||
...
|
||||
|
||||
|
||||
@overload # noqa: F811
|
||||
def deprecated_call( # noqa: F811
|
||||
func: Callable[..., T], *args: Any, **kwargs: Any
|
||||
) -> T:
|
||||
@overload
|
||||
def deprecated_call(func: Callable[..., T], *args: Any, **kwargs: Any) -> T:
|
||||
...
|
||||
|
||||
|
||||
def deprecated_call( # noqa: F811
|
||||
def deprecated_call(
|
||||
func: Optional[Callable[..., Any]] = None, *args: Any, **kwargs: Any
|
||||
) -> Union["WarningsRecorder", Any]:
|
||||
"""Assert that code produces a ``DeprecationWarning`` or ``PendingDeprecationWarning``.
|
||||
@@ -65,7 +61,8 @@ def deprecated_call( # noqa: F811
|
||||
... warnings.warn('use v3 of this api', DeprecationWarning)
|
||||
... return 200
|
||||
|
||||
>>> with deprecated_call():
|
||||
>>> import pytest
|
||||
>>> with pytest.deprecated_call():
|
||||
... assert api_call_v2() == 200
|
||||
|
||||
It can also be used by passing a function and ``*args`` and ``**kwargs``,
|
||||
@@ -86,28 +83,28 @@ def deprecated_call( # noqa: F811
|
||||
|
||||
@overload
|
||||
def warns(
|
||||
expected_warning: Optional[Union["Type[Warning]", Tuple["Type[Warning]", ...]]],
|
||||
expected_warning: Optional[Union[Type[Warning], Tuple[Type[Warning], ...]]],
|
||||
*,
|
||||
match: "Optional[Union[str, Pattern[str]]]" = ...
|
||||
match: Optional[Union[str, Pattern[str]]] = ...,
|
||||
) -> "WarningsChecker":
|
||||
...
|
||||
|
||||
|
||||
@overload # noqa: F811
|
||||
def warns( # noqa: F811
|
||||
expected_warning: Optional[Union["Type[Warning]", Tuple["Type[Warning]", ...]]],
|
||||
@overload
|
||||
def warns(
|
||||
expected_warning: Optional[Union[Type[Warning], Tuple[Type[Warning], ...]]],
|
||||
func: Callable[..., T],
|
||||
*args: Any,
|
||||
**kwargs: Any
|
||||
**kwargs: Any,
|
||||
) -> T:
|
||||
...
|
||||
|
||||
|
||||
def warns( # noqa: F811
|
||||
expected_warning: Optional[Union["Type[Warning]", Tuple["Type[Warning]", ...]]],
|
||||
def warns(
|
||||
expected_warning: Optional[Union[Type[Warning], Tuple[Type[Warning], ...]]],
|
||||
*args: Any,
|
||||
match: Optional[Union[str, "Pattern[str]"]] = None,
|
||||
**kwargs: Any
|
||||
match: Optional[Union[str, Pattern[str]]] = None,
|
||||
**kwargs: Any,
|
||||
) -> Union["WarningsChecker", Any]:
|
||||
r"""Assert that code raises a particular class of warning.
|
||||
|
||||
@@ -119,21 +116,22 @@ def warns( # noqa: F811
|
||||
one for each warning raised.
|
||||
|
||||
This function can be used as a context manager, or any of the other ways
|
||||
``pytest.raises`` can be used::
|
||||
:func:`pytest.raises` can be used::
|
||||
|
||||
>>> with warns(RuntimeWarning):
|
||||
>>> import pytest
|
||||
>>> with pytest.warns(RuntimeWarning):
|
||||
... warnings.warn("my warning", RuntimeWarning)
|
||||
|
||||
In the context manager form you may use the keyword argument ``match`` to assert
|
||||
that the warning matches a text or regex::
|
||||
|
||||
>>> with warns(UserWarning, match='must be 0 or None'):
|
||||
>>> with pytest.warns(UserWarning, match='must be 0 or None'):
|
||||
... warnings.warn("value must be 0 or None", UserWarning)
|
||||
|
||||
>>> with warns(UserWarning, match=r'must be \d+$'):
|
||||
>>> with pytest.warns(UserWarning, match=r'must be \d+$'):
|
||||
... warnings.warn("value must be 42", UserWarning)
|
||||
|
||||
>>> with warns(UserWarning, match=r'must be \d+$'):
|
||||
>>> with pytest.warns(UserWarning, match=r'must be \d+$'):
|
||||
... warnings.warn("this is not here", UserWarning)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
@@ -147,14 +145,14 @@ def warns( # noqa: F811
|
||||
msg += ", ".join(sorted(kwargs))
|
||||
msg += "\nUse context-manager form instead?"
|
||||
raise TypeError(msg)
|
||||
return WarningsChecker(expected_warning, match_expr=match)
|
||||
return WarningsChecker(expected_warning, match_expr=match, _ispytest=True)
|
||||
else:
|
||||
func = args[0]
|
||||
if not callable(func):
|
||||
raise TypeError(
|
||||
"{!r} object (type: {}) must be callable".format(func, type(func))
|
||||
)
|
||||
with WarningsChecker(expected_warning):
|
||||
with WarningsChecker(expected_warning, _ispytest=True):
|
||||
return func(*args[1:], **kwargs)
|
||||
|
||||
|
||||
@@ -164,11 +162,12 @@ class WarningsRecorder(warnings.catch_warnings):
|
||||
Adapted from `warnings.catch_warnings`.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, *, _ispytest: bool = False) -> None:
|
||||
check_ispytest(_ispytest)
|
||||
# Type ignored due to the way typeshed handles warnings.catch_warnings.
|
||||
super().__init__(record=True) # type: ignore[call-arg]
|
||||
self._entered = False
|
||||
self._list = [] # type: List[warnings.WarningMessage]
|
||||
self._list: List[warnings.WarningMessage] = []
|
||||
|
||||
@property
|
||||
def list(self) -> List["warnings.WarningMessage"]:
|
||||
@@ -187,7 +186,7 @@ class WarningsRecorder(warnings.catch_warnings):
|
||||
"""The number of recorded warnings."""
|
||||
return len(self._list)
|
||||
|
||||
def pop(self, cls: "Type[Warning]" = Warning) -> "warnings.WarningMessage":
|
||||
def pop(self, cls: Type[Warning] = Warning) -> "warnings.WarningMessage":
|
||||
"""Pop the first recorded warning, raise exception if not exists."""
|
||||
for i, w in enumerate(self._list):
|
||||
if issubclass(w.category, cls):
|
||||
@@ -214,7 +213,7 @@ class WarningsRecorder(warnings.catch_warnings):
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: Optional["Type[BaseException]"],
|
||||
exc_type: Optional[Type[BaseException]],
|
||||
exc_val: Optional[BaseException],
|
||||
exc_tb: Optional[TracebackType],
|
||||
) -> None:
|
||||
@@ -234,11 +233,14 @@ class WarningsChecker(WarningsRecorder):
|
||||
def __init__(
|
||||
self,
|
||||
expected_warning: Optional[
|
||||
Union["Type[Warning]", Tuple["Type[Warning]", ...]]
|
||||
Union[Type[Warning], Tuple[Type[Warning], ...]]
|
||||
] = None,
|
||||
match_expr: Optional[Union[str, "Pattern[str]"]] = None,
|
||||
match_expr: Optional[Union[str, Pattern[str]]] = None,
|
||||
*,
|
||||
_ispytest: bool = False,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
check_ispytest(_ispytest)
|
||||
super().__init__(_ispytest=True)
|
||||
|
||||
msg = "exceptions must be derived from Warning, not %s"
|
||||
if expected_warning is None:
|
||||
@@ -258,7 +260,7 @@ class WarningsChecker(WarningsRecorder):
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: Optional["Type[BaseException]"],
|
||||
exc_type: Optional[Type[BaseException]],
|
||||
exc_val: Optional[BaseException],
|
||||
exc_tb: Optional[TracebackType],
|
||||
) -> None:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from pprint import pprint
|
||||
from typing import Any
|
||||
from typing import cast
|
||||
@@ -8,6 +9,8 @@ from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
@@ -27,16 +30,13 @@ from _pytest._code.code import ReprTraceback
|
||||
from _pytest._code.code import TerminalRepr
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.config import Config
|
||||
from _pytest.nodes import Collector
|
||||
from _pytest.nodes import Item
|
||||
from _pytest.outcomes import skip
|
||||
from _pytest.pathlib import Path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import NoReturn
|
||||
from typing_extensions import Type
|
||||
from typing_extensions import Literal
|
||||
|
||||
from _pytest.runner import CallInfo
|
||||
@@ -58,13 +58,13 @@ _R = TypeVar("_R", bound="BaseReport")
|
||||
|
||||
|
||||
class BaseReport:
|
||||
when = None # type: Optional[str]
|
||||
location = None # type: Optional[Tuple[str, Optional[int], str]]
|
||||
longrepr = (
|
||||
None
|
||||
) # type: Union[None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr]
|
||||
sections = [] # type: List[Tuple[str, str]]
|
||||
nodeid = None # type: str
|
||||
when: Optional[str]
|
||||
location: Optional[Tuple[str, Optional[int], str]]
|
||||
longrepr: Union[
|
||||
None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr
|
||||
]
|
||||
sections: List[Tuple[str, str]]
|
||||
nodeid: str
|
||||
|
||||
def __init__(self, **kw: Any) -> None:
|
||||
self.__dict__.update(kw)
|
||||
@@ -199,7 +199,7 @@ class BaseReport:
|
||||
return _report_to_json(self)
|
||||
|
||||
@classmethod
|
||||
def _from_json(cls: "Type[_R]", reportdict: Dict[str, object]) -> _R:
|
||||
def _from_json(cls: Type[_R], reportdict: Dict[str, object]) -> _R:
|
||||
"""Create either a TestReport or CollectReport, depending on the calling class.
|
||||
|
||||
It is the callers responsibility to know which class to pass here.
|
||||
@@ -213,7 +213,7 @@ class BaseReport:
|
||||
|
||||
|
||||
def _report_unserialization_failure(
|
||||
type_name: str, report_class: "Type[BaseReport]", reportdict
|
||||
type_name: str, report_class: Type[BaseReport], reportdict
|
||||
) -> "NoReturn":
|
||||
url = "https://github.com/pytest-dev/pytest/issues"
|
||||
stream = StringIO()
|
||||
@@ -246,7 +246,7 @@ class TestReport(BaseReport):
|
||||
sections: Iterable[Tuple[str, str]] = (),
|
||||
duration: float = 0,
|
||||
user_properties: Optional[Iterable[Tuple[str, object]]] = None,
|
||||
**extra
|
||||
**extra,
|
||||
) -> None:
|
||||
#: Normalized collection nodeid.
|
||||
self.nodeid = nodeid
|
||||
@@ -254,7 +254,7 @@ class TestReport(BaseReport):
|
||||
#: A (filesystempath, lineno, domaininfo) tuple indicating the
|
||||
#: actual location of a test item - it might be different from the
|
||||
#: collected one e.g. if a method is inherited from a different module.
|
||||
self.location = location # type: Tuple[str, Optional[int], str]
|
||||
self.location: Tuple[str, Optional[int], str] = location
|
||||
|
||||
#: A name -> value dictionary containing all keywords and
|
||||
#: markers associated with a test invocation.
|
||||
@@ -300,10 +300,14 @@ class TestReport(BaseReport):
|
||||
excinfo = call.excinfo
|
||||
sections = []
|
||||
if not call.excinfo:
|
||||
outcome = "passed" # type: Literal["passed", "failed", "skipped"]
|
||||
longrepr = (
|
||||
None
|
||||
) # type: Union[None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr]
|
||||
outcome: Literal["passed", "failed", "skipped"] = "passed"
|
||||
longrepr: Union[
|
||||
None,
|
||||
ExceptionInfo[BaseException],
|
||||
Tuple[str, int, str],
|
||||
str,
|
||||
TerminalRepr,
|
||||
] = (None)
|
||||
else:
|
||||
if not isinstance(excinfo, ExceptionInfo):
|
||||
outcome = "failed"
|
||||
@@ -321,7 +325,7 @@ class TestReport(BaseReport):
|
||||
excinfo, style=item.config.getoption("tbstyle", "auto")
|
||||
)
|
||||
for rwhen, key, content in item._report_sections:
|
||||
sections.append(("Captured {} {}".format(key, rwhen), content))
|
||||
sections.append((f"Captured {key} {rwhen}", content))
|
||||
return cls(
|
||||
item.nodeid,
|
||||
item.location,
|
||||
@@ -348,7 +352,7 @@ class CollectReport(BaseReport):
|
||||
longrepr,
|
||||
result: Optional[List[Union[Item, Collector]]],
|
||||
sections: Iterable[Tuple[str, str]] = (),
|
||||
**extra
|
||||
**extra,
|
||||
) -> None:
|
||||
#: Normalized collection nodeid.
|
||||
self.nodeid = nodeid
|
||||
@@ -450,11 +454,11 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]:
|
||||
assert rep.longrepr is not None
|
||||
# TODO: Investigate whether the duck typing is really necessary here.
|
||||
longrepr = cast(ExceptionRepr, rep.longrepr)
|
||||
result = {
|
||||
result: Dict[str, Any] = {
|
||||
"reprcrash": serialize_repr_crash(longrepr.reprcrash),
|
||||
"reprtraceback": serialize_repr_traceback(longrepr.reprtraceback),
|
||||
"sections": longrepr.sections,
|
||||
} # type: Dict[str, Any]
|
||||
}
|
||||
if isinstance(longrepr, ExceptionChainRepr):
|
||||
result["chain"] = []
|
||||
for repr_traceback, repr_crash, description in longrepr.chain:
|
||||
@@ -508,13 +512,13 @@ def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if data["reprlocals"]:
|
||||
reprlocals = ReprLocals(data["reprlocals"]["lines"])
|
||||
|
||||
reprentry = ReprEntry(
|
||||
reprentry: Union[ReprEntry, ReprEntryNative] = ReprEntry(
|
||||
lines=data["lines"],
|
||||
reprfuncargs=reprfuncargs,
|
||||
reprlocals=reprlocals,
|
||||
reprfileloc=reprfileloc,
|
||||
style=data["style"],
|
||||
) # type: Union[ReprEntry, ReprEntryNative]
|
||||
)
|
||||
elif entry_type == "ReprEntryNative":
|
||||
reprentry = ReprEntryNative(data["lines"])
|
||||
else:
|
||||
@@ -555,9 +559,9 @@ def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
|
||||
description,
|
||||
)
|
||||
)
|
||||
exception_info = ExceptionChainRepr(
|
||||
chain
|
||||
) # type: Union[ExceptionChainRepr,ReprExceptionInfo]
|
||||
exception_info: Union[
|
||||
ExceptionChainRepr, ReprExceptionInfo
|
||||
] = ExceptionChainRepr(chain)
|
||||
else:
|
||||
exception_info = ReprExceptionInfo(reprtraceback, reprcrash)
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ from typing import Generic
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
@@ -23,7 +25,6 @@ from _pytest._code.code import ExceptionChainRepr
|
||||
from _pytest._code.code import ExceptionInfo
|
||||
from _pytest._code.code import TerminalRepr
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.nodes import Collector
|
||||
from _pytest.nodes import Item
|
||||
@@ -33,7 +34,6 @@ from _pytest.outcomes import Skipped
|
||||
from _pytest.outcomes import TEST_OUTCOME
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Type
|
||||
from typing_extensions import Literal
|
||||
|
||||
from _pytest.main import Session
|
||||
@@ -77,8 +77,7 @@ def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None:
|
||||
dlist.append(rep)
|
||||
if not dlist:
|
||||
return
|
||||
dlist.sort(key=lambda x: x.duration)
|
||||
dlist.reverse()
|
||||
dlist.sort(key=lambda x: x.duration, reverse=True) # type: ignore[no-any-return]
|
||||
if not durations:
|
||||
tr.write_sep("=", "slowest durations")
|
||||
else:
|
||||
@@ -93,7 +92,7 @@ def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None:
|
||||
% (len(dlist) - i, durations_min)
|
||||
)
|
||||
break
|
||||
tr.write_line("{:02.2f}s {:<8} {}".format(rep.duration, rep.when, rep.nodeid))
|
||||
tr.write_line(f"{rep.duration:02.2f}s {rep.when:<8} {rep.nodeid}")
|
||||
|
||||
|
||||
def pytest_sessionstart(session: "Session") -> None:
|
||||
@@ -186,7 +185,7 @@ def _update_current_test_var(
|
||||
"""
|
||||
var_name = "PYTEST_CURRENT_TEST"
|
||||
if when:
|
||||
value = "{} ({})".format(item.nodeid, when)
|
||||
value = f"{item.nodeid} ({when})"
|
||||
# don't allow null bytes on environment variables (see #2644, #2957)
|
||||
value = value.replace("\x00", "(null)")
|
||||
os.environ[var_name] = value
|
||||
@@ -215,7 +214,7 @@ def call_and_report(
|
||||
) -> TestReport:
|
||||
call = call_runtest_hook(item, when, **kwds)
|
||||
hook = item.ihook
|
||||
report = hook.pytest_runtest_makereport(item=item, call=call) # type: TestReport
|
||||
report: TestReport = hook.pytest_runtest_makereport(item=item, call=call)
|
||||
if log:
|
||||
hook.pytest_runtest_logreport(report=report)
|
||||
if check_interactive_exception(call, report):
|
||||
@@ -242,14 +241,14 @@ def call_runtest_hook(
|
||||
item: Item, when: "Literal['setup', 'call', 'teardown']", **kwds
|
||||
) -> "CallInfo[None]":
|
||||
if when == "setup":
|
||||
ihook = item.ihook.pytest_runtest_setup # type: Callable[..., None]
|
||||
ihook: Callable[..., None] = item.ihook.pytest_runtest_setup
|
||||
elif when == "call":
|
||||
ihook = item.ihook.pytest_runtest_call
|
||||
elif when == "teardown":
|
||||
ihook = item.ihook.pytest_runtest_teardown
|
||||
else:
|
||||
assert False, "Unhandled runtest hook case: {}".format(when)
|
||||
reraise = (Exit,) # type: Tuple[Type[BaseException], ...]
|
||||
assert False, f"Unhandled runtest hook case: {when}"
|
||||
reraise: Tuple[Type[BaseException], ...] = (Exit,)
|
||||
if not item.config.getoption("usepdb", False):
|
||||
reraise += (KeyboardInterrupt,)
|
||||
return CallInfo.from_call(
|
||||
@@ -290,7 +289,7 @@ class CallInfo(Generic[TResult]):
|
||||
@property
|
||||
def result(self) -> TResult:
|
||||
if self.excinfo is not None:
|
||||
raise AttributeError("{!r} has no valid result".format(self))
|
||||
raise AttributeError(f"{self!r} has no valid result")
|
||||
# The cast is safe because an exception wasn't raised, hence
|
||||
# _result has the expected function return type (which may be
|
||||
# None, that's why a cast and not an assert).
|
||||
@@ -301,13 +300,15 @@ class CallInfo(Generic[TResult]):
|
||||
cls,
|
||||
func: "Callable[[], TResult]",
|
||||
when: "Literal['collect', 'setup', 'call', 'teardown']",
|
||||
reraise: "Optional[Union[Type[BaseException], Tuple[Type[BaseException], ...]]]" = None,
|
||||
reraise: Optional[
|
||||
Union[Type[BaseException], Tuple[Type[BaseException], ...]]
|
||||
] = None,
|
||||
) -> "CallInfo[TResult]":
|
||||
excinfo = None
|
||||
start = timing.time()
|
||||
precise_start = timing.perf_counter()
|
||||
try:
|
||||
result = func() # type: Optional[TResult]
|
||||
result: Optional[TResult] = func()
|
||||
except BaseException:
|
||||
excinfo = ExceptionInfo.from_current()
|
||||
if reraise is not None and isinstance(excinfo.value, reraise):
|
||||
@@ -328,8 +329,8 @@ class CallInfo(Generic[TResult]):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
if self.excinfo is None:
|
||||
return "<CallInfo when={!r} result: {!r}>".format(self.when, self._result)
|
||||
return "<CallInfo when={!r} excinfo={!r}>".format(self.when, self.excinfo)
|
||||
return f"<CallInfo when={self.when!r} result: {self._result!r}>"
|
||||
return f"<CallInfo when={self.when!r} excinfo={self.excinfo!r}>"
|
||||
|
||||
|
||||
def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> TestReport:
|
||||
@@ -338,9 +339,9 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> TestReport:
|
||||
|
||||
def pytest_make_collect_report(collector: Collector) -> CollectReport:
|
||||
call = CallInfo.from_call(lambda: list(collector.collect()), "collect")
|
||||
longrepr = None # type: Union[None, Tuple[str, int, str], str, TerminalRepr]
|
||||
longrepr: Union[None, Tuple[str, int, str], str, TerminalRepr] = None
|
||||
if not call.excinfo:
|
||||
outcome = "passed" # type: Literal["passed", "skipped", "failed"]
|
||||
outcome: Literal["passed", "skipped", "failed"] = "passed"
|
||||
else:
|
||||
skip_exceptions = [Skipped]
|
||||
unittest = sys.modules.get("unittest")
|
||||
@@ -371,8 +372,8 @@ class SetupState:
|
||||
"""Shared state for setting up/tearing down test items or collectors."""
|
||||
|
||||
def __init__(self):
|
||||
self.stack = [] # type: List[Node]
|
||||
self._finalizers = {} # type: Dict[Node, List[Callable[[], object]]]
|
||||
self.stack: List[Node] = []
|
||||
self._finalizers: Dict[Node, List[Callable[[], object]]] = {}
|
||||
|
||||
def addfinalizer(self, finalizer: Callable[[], object], colitem) -> None:
|
||||
"""Attach a finalizer to the given colitem."""
|
||||
@@ -454,7 +455,7 @@ class SetupState:
|
||||
def collect_one_node(collector: Collector) -> CollectReport:
|
||||
ihook = collector.ihook
|
||||
ihook.pytest_collectstart(collector=collector)
|
||||
rep = ihook.pytest_make_collect_report(collector=collector) # type: CollectReport
|
||||
rep: CollectReport = ihook.pytest_make_collect_report(collector=collector)
|
||||
call = rep.__dict__.pop("call", None)
|
||||
if call and check_interactive_exception(call, rep):
|
||||
ihook.pytest_exception_interact(node=collector, call=call, report=rep)
|
||||
|
||||
@@ -3,13 +3,14 @@ import os
|
||||
import platform
|
||||
import sys
|
||||
import traceback
|
||||
from collections.abc import Mapping
|
||||
from typing import Generator
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
|
||||
import attr
|
||||
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.config.argparsing import Parser
|
||||
@@ -22,9 +23,6 @@ from _pytest.reports import BaseReport
|
||||
from _pytest.runner import CallInfo
|
||||
from _pytest.store import StoreKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Type
|
||||
|
||||
|
||||
def pytest_addoption(parser: Parser) -> None:
|
||||
group = parser.getgroup("general")
|
||||
@@ -101,10 +99,20 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool,
|
||||
"platform": platform,
|
||||
"config": item.config,
|
||||
}
|
||||
for dictionary in reversed(
|
||||
item.ihook.pytest_markeval_namespace(config=item.config)
|
||||
):
|
||||
if not isinstance(dictionary, Mapping):
|
||||
raise ValueError(
|
||||
"pytest_markeval_namespace() needs to return a dict, got {!r}".format(
|
||||
dictionary
|
||||
)
|
||||
)
|
||||
globals_.update(dictionary)
|
||||
if hasattr(item, "obj"):
|
||||
globals_.update(item.obj.__globals__) # type: ignore[attr-defined]
|
||||
try:
|
||||
filename = "<{} condition>".format(mark.name)
|
||||
filename = f"<{mark.name} condition>"
|
||||
condition_code = compile(condition, filename, "eval")
|
||||
result = eval(condition_code, globals_)
|
||||
except SyntaxError as exc:
|
||||
@@ -194,7 +202,7 @@ class Xfail:
|
||||
reason = attr.ib(type=str)
|
||||
run = attr.ib(type=bool)
|
||||
strict = attr.ib(type=bool)
|
||||
raises = attr.ib(type=Optional[Tuple["Type[BaseException]", ...]])
|
||||
raises = attr.ib(type=Optional[Tuple[Type[BaseException], ...]])
|
||||
|
||||
|
||||
def evaluate_xfail_marks(item: Item) -> Optional[Xfail]:
|
||||
@@ -267,7 +275,7 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]):
|
||||
if unexpectedsuccess_key in item._store and rep.when == "call":
|
||||
reason = item._store[unexpectedsuccess_key]
|
||||
if reason:
|
||||
rep.longrepr = "Unexpected success: {}".format(reason)
|
||||
rep.longrepr = f"Unexpected success: {reason}"
|
||||
else:
|
||||
rep.longrepr = "Unexpected success"
|
||||
rep.outcome = "failed"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
from _pytest import nodes
|
||||
@@ -8,6 +9,11 @@ from _pytest.config.argparsing import Parser
|
||||
from _pytest.main import Session
|
||||
from _pytest.reports import TestReport
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.cacheprovider import Cache
|
||||
|
||||
STEPWISE_CACHE_DIR = "cache/stepwise"
|
||||
|
||||
|
||||
def pytest_addoption(parser: Parser) -> None:
|
||||
group = parser.getgroup("general")
|
||||
@@ -15,12 +21,15 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
"--sw",
|
||||
"--stepwise",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="stepwise",
|
||||
help="exit on test failure and continue from last failing test next time",
|
||||
)
|
||||
group.addoption(
|
||||
"--sw-skip",
|
||||
"--stepwise-skip",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="stepwise_skip",
|
||||
help="ignore the first failing test but stop on the next failing test",
|
||||
)
|
||||
@@ -28,63 +37,56 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
|
||||
@pytest.hookimpl
|
||||
def pytest_configure(config: Config) -> None:
|
||||
config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin")
|
||||
# We should always have a cache as cache provider plugin uses tryfirst=True
|
||||
if config.getoption("stepwise"):
|
||||
config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin")
|
||||
|
||||
|
||||
def pytest_sessionfinish(session: Session) -> None:
|
||||
if not session.config.getoption("stepwise"):
|
||||
assert session.config.cache is not None
|
||||
# Clear the list of failing tests if the plugin is not active.
|
||||
session.config.cache.set(STEPWISE_CACHE_DIR, [])
|
||||
|
||||
|
||||
class StepwisePlugin:
|
||||
def __init__(self, config: Config) -> None:
|
||||
self.config = config
|
||||
self.active = config.getvalue("stepwise")
|
||||
self.session = None # type: Optional[Session]
|
||||
self.session: Optional[Session] = None
|
||||
self.report_status = ""
|
||||
|
||||
if self.active:
|
||||
assert config.cache is not None
|
||||
self.lastfailed = config.cache.get("cache/stepwise", None)
|
||||
self.skip = config.getvalue("stepwise_skip")
|
||||
assert config.cache is not None
|
||||
self.cache: Cache = config.cache
|
||||
self.lastfailed: Optional[str] = self.cache.get(STEPWISE_CACHE_DIR, None)
|
||||
self.skip: bool = config.getoption("stepwise_skip")
|
||||
|
||||
def pytest_sessionstart(self, session: Session) -> None:
|
||||
self.session = session
|
||||
|
||||
def pytest_collection_modifyitems(
|
||||
self, session: Session, config: Config, items: List[nodes.Item]
|
||||
self, config: Config, items: List[nodes.Item]
|
||||
) -> None:
|
||||
if not self.active:
|
||||
return
|
||||
if not self.lastfailed:
|
||||
self.report_status = "no previously failed tests, not skipping."
|
||||
return
|
||||
|
||||
already_passed = []
|
||||
found = False
|
||||
|
||||
# Make a list of all tests that have been run before the last failing one.
|
||||
for item in items:
|
||||
# check all item nodes until we find a match on last failed
|
||||
failed_index = None
|
||||
for index, item in enumerate(items):
|
||||
if item.nodeid == self.lastfailed:
|
||||
found = True
|
||||
failed_index = index
|
||||
break
|
||||
else:
|
||||
already_passed.append(item)
|
||||
|
||||
# If the previously failed test was not found among the test items,
|
||||
# do not skip any tests.
|
||||
if not found:
|
||||
if failed_index is None:
|
||||
self.report_status = "previously failed test not found, not skipping."
|
||||
already_passed = []
|
||||
else:
|
||||
self.report_status = "skipping {} already passed items.".format(
|
||||
len(already_passed)
|
||||
)
|
||||
|
||||
for item in already_passed:
|
||||
items.remove(item)
|
||||
|
||||
config.hook.pytest_deselected(items=already_passed)
|
||||
self.report_status = f"skipping {failed_index} already passed items."
|
||||
deselected = items[:failed_index]
|
||||
del items[:failed_index]
|
||||
config.hook.pytest_deselected(items=deselected)
|
||||
|
||||
def pytest_runtest_logreport(self, report: TestReport) -> None:
|
||||
if not self.active:
|
||||
return
|
||||
|
||||
if report.failed:
|
||||
if self.skip:
|
||||
# Remove test from the failed ones (if it exists) and unset the skip option
|
||||
@@ -109,14 +111,9 @@ class StepwisePlugin:
|
||||
self.lastfailed = None
|
||||
|
||||
def pytest_report_collectionfinish(self) -> Optional[str]:
|
||||
if self.active and self.config.getoption("verbose") >= 0 and self.report_status:
|
||||
return "stepwise: %s" % self.report_status
|
||||
if self.config.getoption("verbose") >= 0 and self.report_status:
|
||||
return f"stepwise: {self.report_status}"
|
||||
return None
|
||||
|
||||
def pytest_sessionfinish(self, session: Session) -> None:
|
||||
assert self.config.cache is not None
|
||||
if self.active:
|
||||
self.config.cache.set("cache/stepwise", self.lastfailed)
|
||||
else:
|
||||
# Clear the list of failing tests if the plugin is not active.
|
||||
self.config.cache.set("cache/stepwise", [])
|
||||
def pytest_sessionfinish(self) -> None:
|
||||
self.cache.set(STEPWISE_CACHE_DIR, self.lastfailed)
|
||||
|
||||
@@ -83,7 +83,7 @@ class Store:
|
||||
__slots__ = ("_store",)
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._store = {} # type: Dict[StoreKey[Any], object]
|
||||
self._store: Dict[StoreKey[Any], object] = {}
|
||||
|
||||
def __setitem__(self, key: StoreKey[T], value: T) -> None:
|
||||
"""Set a value for key."""
|
||||
|
||||
@@ -8,9 +8,12 @@ import inspect
|
||||
import platform
|
||||
import sys
|
||||
import warnings
|
||||
from collections import Counter
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Dict
|
||||
from typing import Generator
|
||||
from typing import List
|
||||
@@ -20,30 +23,29 @@ from typing import Sequence
|
||||
from typing import Set
|
||||
from typing import TextIO
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
import attr
|
||||
import pluggy
|
||||
import py
|
||||
|
||||
import pytest
|
||||
import _pytest._version
|
||||
from _pytest import nodes
|
||||
from _pytest import timing
|
||||
from _pytest._code import ExceptionInfo
|
||||
from _pytest._code.code import ExceptionRepr
|
||||
from _pytest._io.wcwidth import wcswidth
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import order_preserving_dict
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.config import _PluggyPlugin
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ExitCode
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.nodes import Item
|
||||
from _pytest.nodes import Node
|
||||
from _pytest.pathlib import absolutepath
|
||||
from _pytest.pathlib import bestrelpath
|
||||
from _pytest.pathlib import Path
|
||||
from _pytest.reports import BaseReport
|
||||
from _pytest.reports import CollectReport
|
||||
from _pytest.reports import TestReport
|
||||
@@ -235,7 +237,7 @@ def pytest_configure(config: Config) -> None:
|
||||
|
||||
|
||||
def getreportopt(config: Config) -> str:
|
||||
reportchars = config.option.reportchars # type: str
|
||||
reportchars: str = config.option.reportchars
|
||||
|
||||
old_aliases = {"F", "S"}
|
||||
reportopts = ""
|
||||
@@ -259,7 +261,7 @@ def getreportopt(config: Config) -> str:
|
||||
return reportopts
|
||||
|
||||
|
||||
@pytest.hookimpl(trylast=True) # after _pytest.runner
|
||||
@hookimpl(trylast=True) # after _pytest.runner
|
||||
def pytest_report_teststatus(report: BaseReport) -> Tuple[str, str, str]:
|
||||
letter = "F"
|
||||
if report.passed:
|
||||
@@ -267,7 +269,7 @@ def pytest_report_teststatus(report: BaseReport) -> Tuple[str, str, str]:
|
||||
elif report.skipped:
|
||||
letter = "s"
|
||||
|
||||
outcome = report.outcome # type: str
|
||||
outcome: str = report.outcome
|
||||
if report.when in ("collect", "setup", "teardown") and outcome == "failed":
|
||||
outcome = "error"
|
||||
letter = "E"
|
||||
@@ -304,7 +306,7 @@ class WarningReport:
|
||||
relpath = bestrelpath(
|
||||
config.invocation_params.dir, absolutepath(filename)
|
||||
)
|
||||
return "{}:{}".format(relpath, linenum)
|
||||
return f"{relpath}:{linenum}"
|
||||
else:
|
||||
return str(self.fslocation)
|
||||
return None
|
||||
@@ -317,27 +319,27 @@ class TerminalReporter:
|
||||
|
||||
self.config = config
|
||||
self._numcollected = 0
|
||||
self._session = None # type: Optional[Session]
|
||||
self._showfspath = None # type: Optional[bool]
|
||||
self._session: Optional[Session] = None
|
||||
self._showfspath: Optional[bool] = None
|
||||
|
||||
self.stats = {} # type: Dict[str, List[Any]]
|
||||
self._main_color = None # type: Optional[str]
|
||||
self._known_types = None # type: Optional[List[str]]
|
||||
self.stats: Dict[str, List[Any]] = {}
|
||||
self._main_color: Optional[str] = None
|
||||
self._known_types: Optional[List[str]] = None
|
||||
self.startdir = config.invocation_dir
|
||||
self.startpath = config.invocation_params.dir
|
||||
if file is None:
|
||||
file = sys.stdout
|
||||
self._tw = _pytest.config.create_terminal_writer(config, file)
|
||||
self._screen_width = self._tw.fullwidth
|
||||
self.currentfspath = None # type: Union[None, Path, str, int]
|
||||
self.currentfspath: Union[None, Path, str, int] = None
|
||||
self.reportchars = getreportopt(config)
|
||||
self.hasmarkup = self._tw.hasmarkup
|
||||
self.isatty = file.isatty()
|
||||
self._progress_nodeids_reported = set() # type: Set[str]
|
||||
self._progress_nodeids_reported: Set[str] = set()
|
||||
self._show_progress_info = self._determine_show_progress_info()
|
||||
self._collect_report_last_write = None # type: Optional[float]
|
||||
self._already_displayed_warnings = None # type: Optional[int]
|
||||
self._keyboardinterrupt_memo = None # type: Optional[ExceptionRepr]
|
||||
self._collect_report_last_write: Optional[float] = None
|
||||
self._already_displayed_warnings: Optional[int] = None
|
||||
self._keyboardinterrupt_memo: Optional[ExceptionRepr] = None
|
||||
|
||||
def _determine_show_progress_info(self) -> "Literal['progress', 'count', False]":
|
||||
"""Return whether we should display progress information based on the current config."""
|
||||
@@ -347,7 +349,7 @@ class TerminalReporter:
|
||||
# do not show progress if we are showing fixture setup/teardown
|
||||
if self.config.getoption("setupshow", False):
|
||||
return False
|
||||
cfg = self.config.getini("console_output_style") # type: str
|
||||
cfg: str = self.config.getini("console_output_style")
|
||||
if cfg == "progress":
|
||||
return "progress"
|
||||
elif cfg == "count":
|
||||
@@ -357,7 +359,7 @@ class TerminalReporter:
|
||||
|
||||
@property
|
||||
def verbosity(self) -> int:
|
||||
verbosity = self.config.option.verbose # type: int
|
||||
verbosity: int = self.config.option.verbose
|
||||
return verbosity
|
||||
|
||||
@property
|
||||
@@ -450,7 +452,7 @@ class TerminalReporter:
|
||||
sep: str,
|
||||
title: Optional[str] = None,
|
||||
fullwidth: Optional[int] = None,
|
||||
**markup: bool
|
||||
**markup: bool,
|
||||
) -> None:
|
||||
self.ensure_newline()
|
||||
self._tw.sep(sep, title, fullwidth, **markup)
|
||||
@@ -487,7 +489,7 @@ class TerminalReporter:
|
||||
|
||||
def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
|
||||
if self.config.option.traceconfig:
|
||||
msg = "PLUGIN registered: {}".format(plugin)
|
||||
msg = f"PLUGIN registered: {plugin}"
|
||||
# XXX This event may happen during setup/teardown time
|
||||
# which unfortunately captures our output here
|
||||
# which garbles our output if we use self.write_line.
|
||||
@@ -512,9 +514,9 @@ class TerminalReporter:
|
||||
def pytest_runtest_logreport(self, report: TestReport) -> None:
|
||||
self._tests_ran = True
|
||||
rep = report
|
||||
res = self.config.hook.pytest_report_teststatus(
|
||||
report=rep, config=self.config
|
||||
) # type: Tuple[str, str, Union[str, Tuple[str, Mapping[str, bool]]]]
|
||||
res: Tuple[
|
||||
str, str, Union[str, Tuple[str, Mapping[str, bool]]]
|
||||
] = self.config.hook.pytest_report_teststatus(report=rep, config=self.config)
|
||||
category, letter, word = res
|
||||
if not isinstance(word, tuple):
|
||||
markup = None
|
||||
@@ -544,6 +546,16 @@ class TerminalReporter:
|
||||
line = self._locationline(rep.nodeid, *rep.location)
|
||||
if not running_xdist:
|
||||
self.write_ensure_prefix(line, word, **markup)
|
||||
if rep.skipped or hasattr(report, "wasxfail"):
|
||||
available_width = (
|
||||
(self._tw.fullwidth - self._tw.width_of_current_line)
|
||||
- len(" [100%]")
|
||||
- 1
|
||||
)
|
||||
reason = _get_raw_skip_reason(rep)
|
||||
reason_ = _format_trimmed(" ({})", reason, available_width)
|
||||
if reason and reason_ is not None:
|
||||
self._tw.write(reason_)
|
||||
if self._show_progress_info:
|
||||
self._write_progress_information_filling_space()
|
||||
else:
|
||||
@@ -593,9 +605,9 @@ class TerminalReporter:
|
||||
if collected:
|
||||
progress = self._progress_nodeids_reported
|
||||
counter_format = "{{:{}d}}".format(len(str(collected)))
|
||||
format_string = " [{}/{{}}]".format(counter_format)
|
||||
format_string = f" [{counter_format}/{{}}]"
|
||||
return format_string.format(len(progress), collected)
|
||||
return " [ {} / {} ]".format(collected, collected)
|
||||
return f" [ {collected} / {collected} ]"
|
||||
else:
|
||||
if collected:
|
||||
return " [{:3d}%]".format(
|
||||
@@ -628,7 +640,7 @@ class TerminalReporter:
|
||||
self._add_stats("error", [report])
|
||||
elif report.skipped:
|
||||
self._add_stats("skipped", [report])
|
||||
items = [x for x in report.result if isinstance(x, pytest.Item)]
|
||||
items = [x for x in report.result if isinstance(x, Item)]
|
||||
self._numcollected += len(items)
|
||||
if self.isatty:
|
||||
self.report_collect()
|
||||
@@ -673,7 +685,7 @@ class TerminalReporter:
|
||||
else:
|
||||
self.write_line(line)
|
||||
|
||||
@pytest.hookimpl(trylast=True)
|
||||
@hookimpl(trylast=True)
|
||||
def pytest_sessionstart(self, session: "Session") -> None:
|
||||
self._session = session
|
||||
self._sessionstarttime = timing.time()
|
||||
@@ -682,13 +694,13 @@ class TerminalReporter:
|
||||
self.write_sep("=", "test session starts", bold=True)
|
||||
verinfo = platform.python_version()
|
||||
if not self.no_header:
|
||||
msg = "platform {} -- Python {}".format(sys.platform, verinfo)
|
||||
msg = f"platform {sys.platform} -- Python {verinfo}"
|
||||
pypy_version_info = getattr(sys, "pypy_version_info", None)
|
||||
if pypy_version_info:
|
||||
verinfo = ".".join(map(str, pypy_version_info[:3]))
|
||||
msg += "[pypy-{}-{}]".format(verinfo, pypy_version_info[3])
|
||||
msg += ", pytest-{}, py-{}, pluggy-{}".format(
|
||||
pytest.__version__, py.__version__, pluggy.__version__
|
||||
_pytest._version.version, py.__version__, pluggy.__version__
|
||||
)
|
||||
if (
|
||||
self.verbosity > 0
|
||||
@@ -718,10 +730,10 @@ class TerminalReporter:
|
||||
if config.inipath:
|
||||
line += ", configfile: " + bestrelpath(config.rootpath, config.inipath)
|
||||
|
||||
testpaths = config.getini("testpaths")
|
||||
if testpaths and config.args == testpaths:
|
||||
rel_paths = [bestrelpath(config.rootpath, x) for x in testpaths]
|
||||
line += ", testpaths: {}".format(", ".join(rel_paths))
|
||||
testpaths: List[str] = config.getini("testpaths")
|
||||
if config.invocation_params.dir == config.rootpath and config.args == testpaths:
|
||||
line += ", testpaths: {}".format(", ".join(testpaths))
|
||||
|
||||
result = [line]
|
||||
|
||||
plugininfo = config.pluginmanager.list_plugin_distinfo()
|
||||
@@ -755,17 +767,14 @@ class TerminalReporter:
|
||||
# because later versions are going to get rid of them anyway.
|
||||
if self.config.option.verbose < 0:
|
||||
if self.config.option.verbose < -1:
|
||||
counts = {} # type: Dict[str, int]
|
||||
for item in items:
|
||||
name = item.nodeid.split("::", 1)[0]
|
||||
counts[name] = counts.get(name, 0) + 1
|
||||
counts = Counter(item.nodeid.split("::", 1)[0] for item in items)
|
||||
for name, count in sorted(counts.items()):
|
||||
self._tw.line("%s: %d" % (name, count))
|
||||
else:
|
||||
for item in items:
|
||||
self._tw.line(item.nodeid)
|
||||
return
|
||||
stack = [] # type: List[Node]
|
||||
stack: List[Node] = []
|
||||
indent = ""
|
||||
for item in items:
|
||||
needed_collectors = item.listchain()[1:] # strip root node
|
||||
@@ -778,7 +787,7 @@ class TerminalReporter:
|
||||
if col.name == "()": # Skip Instances.
|
||||
continue
|
||||
indent = (len(stack) - 1) * " "
|
||||
self._tw.line("{}{}".format(indent, col))
|
||||
self._tw.line(f"{indent}{col}")
|
||||
if self.config.option.verbose >= 1:
|
||||
obj = getattr(col, "obj", None)
|
||||
doc = inspect.getdoc(obj) if obj else None
|
||||
@@ -786,7 +795,7 @@ class TerminalReporter:
|
||||
for line in doc.splitlines():
|
||||
self._tw.line("{}{}".format(indent + " ", line))
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_sessionfinish(
|
||||
self, session: "Session", exitstatus: Union[int, ExitCode]
|
||||
):
|
||||
@@ -813,7 +822,7 @@ class TerminalReporter:
|
||||
self.write_sep("!", str(session.shouldstop), red=True)
|
||||
self.summary_stats()
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_terminal_summary(self) -> Generator[None, None, None]:
|
||||
self.summary_errors()
|
||||
self.summary_failures()
|
||||
@@ -896,9 +905,7 @@ class TerminalReporter:
|
||||
|
||||
def summary_warnings(self) -> None:
|
||||
if self.hasopt("w"):
|
||||
all_warnings = self.stats.get(
|
||||
"warnings"
|
||||
) # type: Optional[List[WarningReport]]
|
||||
all_warnings: Optional[List[WarningReport]] = self.stats.get("warnings")
|
||||
if not all_warnings:
|
||||
return
|
||||
|
||||
@@ -911,9 +918,7 @@ class TerminalReporter:
|
||||
if not warning_reports:
|
||||
return
|
||||
|
||||
reports_grouped_by_message = (
|
||||
order_preserving_dict()
|
||||
) # type: Dict[str, List[WarningReport]]
|
||||
reports_grouped_by_message: Dict[str, List[WarningReport]] = {}
|
||||
for wr in warning_reports:
|
||||
reports_grouped_by_message.setdefault(wr.message, []).append(wr)
|
||||
|
||||
@@ -927,10 +932,9 @@ class TerminalReporter:
|
||||
if len(locations) < 10:
|
||||
return "\n".join(map(str, locations))
|
||||
|
||||
counts_by_filename = order_preserving_dict() # type: Dict[str, int]
|
||||
for loc in locations:
|
||||
key = str(loc).split("::", 1)[0]
|
||||
counts_by_filename[key] = counts_by_filename.get(key, 0) + 1
|
||||
counts_by_filename = Counter(
|
||||
str(loc).split("::", 1)[0] for loc in locations
|
||||
)
|
||||
return "\n".join(
|
||||
"{}: {} warning{}".format(k, v, "s" if v > 1 else "")
|
||||
for k, v in counts_by_filename.items()
|
||||
@@ -954,7 +958,7 @@ class TerminalReporter:
|
||||
def summary_passes(self) -> None:
|
||||
if self.config.option.tbstyle != "no":
|
||||
if self.hasopt("P"):
|
||||
reports = self.getreports("passed") # type: List[TestReport]
|
||||
reports: List[TestReport] = self.getreports("passed")
|
||||
if not reports:
|
||||
return
|
||||
self.write_sep("=", "PASSES")
|
||||
@@ -992,7 +996,7 @@ class TerminalReporter:
|
||||
|
||||
def summary_failures(self) -> None:
|
||||
if self.config.option.tbstyle != "no":
|
||||
reports = self.getreports("failed") # type: List[BaseReport]
|
||||
reports: List[BaseReport] = self.getreports("failed")
|
||||
if not reports:
|
||||
return
|
||||
self.write_sep("=", "FAILURES")
|
||||
@@ -1009,7 +1013,7 @@ class TerminalReporter:
|
||||
|
||||
def summary_errors(self) -> None:
|
||||
if self.config.option.tbstyle != "no":
|
||||
reports = self.getreports("error") # type: List[BaseReport]
|
||||
reports: List[BaseReport] = self.getreports("error")
|
||||
if not reports:
|
||||
return
|
||||
self.write_sep("=", "ERRORS")
|
||||
@@ -1018,7 +1022,7 @@ class TerminalReporter:
|
||||
if rep.when == "collect":
|
||||
msg = "ERROR collecting " + msg
|
||||
else:
|
||||
msg = "ERROR at {} of {}".format(rep.when, msg)
|
||||
msg = f"ERROR at {rep.when} of {msg}"
|
||||
self.write_sep("_", msg, red=True, bold=True)
|
||||
self._outrep_summary(rep)
|
||||
|
||||
@@ -1091,7 +1095,7 @@ class TerminalReporter:
|
||||
for rep in xfailed:
|
||||
verbose_word = rep._get_verbose_word(self.config)
|
||||
pos = _get_pos(self.config, rep)
|
||||
lines.append("{} {}".format(verbose_word, pos))
|
||||
lines.append(f"{verbose_word} {pos}")
|
||||
reason = rep.wasxfail
|
||||
if reason:
|
||||
lines.append(" " + str(reason))
|
||||
@@ -1102,10 +1106,10 @@ class TerminalReporter:
|
||||
verbose_word = rep._get_verbose_word(self.config)
|
||||
pos = _get_pos(self.config, rep)
|
||||
reason = rep.wasxfail
|
||||
lines.append("{} {} {}".format(verbose_word, pos, reason))
|
||||
lines.append(f"{verbose_word} {pos} {reason}")
|
||||
|
||||
def show_skipped(lines: List[str]) -> None:
|
||||
skipped = self.stats.get("skipped", []) # type: List[CollectReport]
|
||||
skipped: List[CollectReport] = self.stats.get("skipped", [])
|
||||
fskips = _folded_skips(self.startpath, skipped) if skipped else []
|
||||
if not fskips:
|
||||
return
|
||||
@@ -1121,16 +1125,16 @@ class TerminalReporter:
|
||||
else:
|
||||
lines.append("%s [%d] %s: %s" % (verbose_word, num, fspath, reason))
|
||||
|
||||
REPORTCHAR_ACTIONS = {
|
||||
REPORTCHAR_ACTIONS: Mapping[str, Callable[[List[str]], None]] = {
|
||||
"x": show_xfailed,
|
||||
"X": show_xpassed,
|
||||
"f": partial(show_simple, "failed"),
|
||||
"s": show_skipped,
|
||||
"p": partial(show_simple, "passed"),
|
||||
"E": partial(show_simple, "error"),
|
||||
} # type: Mapping[str, Callable[[List[str]], None]]
|
||||
}
|
||||
|
||||
lines = [] # type: List[str]
|
||||
lines: List[str] = []
|
||||
for char in self.reportchars:
|
||||
action = REPORTCHAR_ACTIONS.get(char)
|
||||
if action: # skipping e.g. "P" (passed with output) here.
|
||||
@@ -1161,7 +1165,7 @@ class TerminalReporter:
|
||||
return main_color
|
||||
|
||||
def _set_main_color(self) -> None:
|
||||
unknown_types = [] # type: List[str]
|
||||
unknown_types: List[str] = []
|
||||
for found_type in self.stats.keys():
|
||||
if found_type: # setup/teardown reports have an empty key, ignore them
|
||||
if found_type not in KNOWN_TYPES and found_type not in unknown_types:
|
||||
@@ -1170,30 +1174,117 @@ class TerminalReporter:
|
||||
self._main_color = self._determine_main_color(bool(unknown_types))
|
||||
|
||||
def build_summary_stats_line(self) -> Tuple[List[Tuple[str, Dict[str, bool]]], str]:
|
||||
main_color, known_types = self._get_main_color()
|
||||
"""
|
||||
Build the parts used in the last summary stats line.
|
||||
|
||||
The summary stats line is the line shown at the end, "=== 12 passed, 2 errors in Xs===".
|
||||
|
||||
This function builds a list of the "parts" that make up for the text in that line, in
|
||||
the example above it would be:
|
||||
|
||||
[
|
||||
("12 passed", {"green": True}),
|
||||
("2 errors", {"red": True}
|
||||
]
|
||||
|
||||
That last dict for each line is a "markup dictionary", used by TerminalWriter to
|
||||
color output.
|
||||
|
||||
The final color of the line is also determined by this function, and is the second
|
||||
element of the returned tuple.
|
||||
"""
|
||||
if self.config.getoption("collectonly"):
|
||||
return self._build_collect_only_summary_stats_line()
|
||||
else:
|
||||
return self._build_normal_summary_stats_line()
|
||||
|
||||
def _get_reports_to_display(self, key: str) -> List[Any]:
|
||||
"""Get test/collection reports for the given status key, such as `passed` or `error`."""
|
||||
reports = self.stats.get(key, [])
|
||||
return [x for x in reports if getattr(x, "count_towards_summary", True)]
|
||||
|
||||
def _build_normal_summary_stats_line(
|
||||
self,
|
||||
) -> Tuple[List[Tuple[str, Dict[str, bool]]], str]:
|
||||
main_color, known_types = self._get_main_color()
|
||||
parts = []
|
||||
|
||||
for key in known_types:
|
||||
reports = self.stats.get(key, None)
|
||||
reports = self._get_reports_to_display(key)
|
||||
if reports:
|
||||
count = sum(
|
||||
1 for rep in reports if getattr(rep, "count_towards_summary", True)
|
||||
)
|
||||
count = len(reports)
|
||||
color = _color_for_type.get(key, _color_for_type_default)
|
||||
markup = {color: True, "bold": color == main_color}
|
||||
parts.append(("%d %s" % _make_plural(count, key), markup))
|
||||
parts.append(("%d %s" % pluralize(count, key), markup))
|
||||
|
||||
if not parts:
|
||||
parts = [("no tests ran", {_color_for_type_default: True})]
|
||||
|
||||
return parts, main_color
|
||||
|
||||
def _build_collect_only_summary_stats_line(
|
||||
self,
|
||||
) -> Tuple[List[Tuple[str, Dict[str, bool]]], str]:
|
||||
deselected = len(self._get_reports_to_display("deselected"))
|
||||
errors = len(self._get_reports_to_display("error"))
|
||||
|
||||
if self._numcollected == 0:
|
||||
parts = [("no tests collected", {"yellow": True})]
|
||||
main_color = "yellow"
|
||||
|
||||
elif deselected == 0:
|
||||
main_color = "green"
|
||||
collected_output = "%d %s collected" % pluralize(self._numcollected, "test")
|
||||
parts = [(collected_output, {main_color: True})]
|
||||
else:
|
||||
all_tests_were_deselected = self._numcollected == deselected
|
||||
if all_tests_were_deselected:
|
||||
main_color = "yellow"
|
||||
collected_output = f"no tests collected ({deselected} deselected)"
|
||||
else:
|
||||
main_color = "green"
|
||||
selected = self._numcollected - deselected
|
||||
collected_output = f"{selected}/{self._numcollected} tests collected ({deselected} deselected)"
|
||||
|
||||
parts = [(collected_output, {main_color: True})]
|
||||
|
||||
if errors:
|
||||
main_color = _color_for_type["error"]
|
||||
parts += [("%d %s" % pluralize(errors, "error"), {main_color: True})]
|
||||
|
||||
return parts, main_color
|
||||
|
||||
|
||||
def _get_pos(config: Config, rep: BaseReport):
|
||||
nodeid = config.cwd_relative_nodeid(rep.nodeid)
|
||||
return nodeid
|
||||
|
||||
|
||||
def _format_trimmed(format: str, msg: str, available_width: int) -> Optional[str]:
|
||||
"""Format msg into format, ellipsizing it if doesn't fit in available_width.
|
||||
|
||||
Returns None if even the ellipsis can't fit.
|
||||
"""
|
||||
# Only use the first line.
|
||||
i = msg.find("\n")
|
||||
if i != -1:
|
||||
msg = msg[:i]
|
||||
|
||||
ellipsis = "..."
|
||||
format_width = wcswidth(format.format(""))
|
||||
if format_width + len(ellipsis) > available_width:
|
||||
return None
|
||||
|
||||
if format_width + wcswidth(msg) > available_width:
|
||||
available_width -= len(ellipsis)
|
||||
msg = msg[:available_width]
|
||||
while format_width + wcswidth(msg) > available_width:
|
||||
msg = msg[:-1]
|
||||
msg += ellipsis
|
||||
|
||||
return format.format(msg)
|
||||
|
||||
|
||||
def _get_line_with_reprcrash_message(
|
||||
config: Config, rep: BaseReport, termwidth: int
|
||||
) -> str:
|
||||
@@ -1201,12 +1292,8 @@ def _get_line_with_reprcrash_message(
|
||||
verbose_word = rep._get_verbose_word(config)
|
||||
pos = _get_pos(config, rep)
|
||||
|
||||
line = "{} {}".format(verbose_word, pos)
|
||||
len_line = wcswidth(line)
|
||||
ellipsis, len_ellipsis = "...", 3
|
||||
if len_line > termwidth - len_ellipsis:
|
||||
# No space for an additional message.
|
||||
return line
|
||||
line = f"{verbose_word} {pos}"
|
||||
line_width = wcswidth(line)
|
||||
|
||||
try:
|
||||
# Type ignored intentionally -- possible AttributeError expected.
|
||||
@@ -1214,29 +1301,18 @@ def _get_line_with_reprcrash_message(
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
# Only use the first line.
|
||||
i = msg.find("\n")
|
||||
if i != -1:
|
||||
msg = msg[:i]
|
||||
len_msg = wcswidth(msg)
|
||||
available_width = termwidth - line_width
|
||||
msg = _format_trimmed(" - {}", msg, available_width)
|
||||
if msg is not None:
|
||||
line += msg
|
||||
|
||||
sep, len_sep = " - ", 3
|
||||
max_len_msg = termwidth - len_line - len_sep
|
||||
if max_len_msg >= len_ellipsis:
|
||||
if len_msg > max_len_msg:
|
||||
max_len_msg -= len_ellipsis
|
||||
msg = msg[:max_len_msg]
|
||||
while wcswidth(msg) > max_len_msg:
|
||||
msg = msg[:-1]
|
||||
msg += ellipsis
|
||||
line += sep + msg
|
||||
return line
|
||||
|
||||
|
||||
def _folded_skips(
|
||||
startpath: Path, skipped: Sequence[CollectReport],
|
||||
) -> List[Tuple[int, str, Optional[int], str]]:
|
||||
d = {} # type: Dict[Tuple[str, Optional[int], str], List[CollectReport]]
|
||||
d: Dict[Tuple[str, Optional[int], str], List[CollectReport]] = {}
|
||||
for event in skipped:
|
||||
assert event.longrepr is not None
|
||||
assert isinstance(event.longrepr, tuple), (event, event.longrepr)
|
||||
@@ -1253,11 +1329,11 @@ def _folded_skips(
|
||||
and "skip" in keywords
|
||||
and "pytestmark" not in keywords
|
||||
):
|
||||
key = (fspath, None, reason) # type: Tuple[str, Optional[int], str]
|
||||
key: Tuple[str, Optional[int], str] = (fspath, None, reason)
|
||||
else:
|
||||
key = (fspath, lineno, reason)
|
||||
d.setdefault(key, []).append(event)
|
||||
values = [] # type: List[Tuple[int, str, Optional[int], str]]
|
||||
values: List[Tuple[int, str, Optional[int], str]] = []
|
||||
for key, events in d.items():
|
||||
values.append((len(events), *key))
|
||||
return values
|
||||
@@ -1272,9 +1348,9 @@ _color_for_type = {
|
||||
_color_for_type_default = "yellow"
|
||||
|
||||
|
||||
def _make_plural(count: int, noun: str) -> Tuple[int, str]:
|
||||
def pluralize(count: int, noun: str) -> Tuple[int, str]:
|
||||
# No need to pluralize words such as `failed` or `passed`.
|
||||
if noun not in ["error", "warnings"]:
|
||||
if noun not in ["error", "warnings", "test"]:
|
||||
return count, noun
|
||||
|
||||
# The `warnings` key is plural. To avoid API breakage, we keep it that way but
|
||||
@@ -1286,7 +1362,7 @@ def _make_plural(count: int, noun: str) -> Tuple[int, str]:
|
||||
|
||||
|
||||
def _plugin_nameversions(plugininfo) -> List[str]:
|
||||
values = [] # type: List[str]
|
||||
values: List[str] = []
|
||||
for plugin, dist in plugininfo:
|
||||
# Gets us name and version!
|
||||
name = "{dist.project_name}-{dist.version}".format(dist=dist)
|
||||
@@ -1302,7 +1378,26 @@ def _plugin_nameversions(plugininfo) -> List[str]:
|
||||
def format_session_duration(seconds: float) -> str:
|
||||
"""Format the given seconds in a human readable manner to show in the final summary."""
|
||||
if seconds < 60:
|
||||
return "{:.2f}s".format(seconds)
|
||||
return f"{seconds:.2f}s"
|
||||
else:
|
||||
dt = datetime.timedelta(seconds=int(seconds))
|
||||
return "{:.2f}s ({})".format(seconds, dt)
|
||||
return f"{seconds:.2f}s ({dt})"
|
||||
|
||||
|
||||
def _get_raw_skip_reason(report: TestReport) -> str:
|
||||
"""Get the reason string of a skip/xfail/xpass test report.
|
||||
|
||||
The string is just the part given by the user.
|
||||
"""
|
||||
if hasattr(report, "wasxfail"):
|
||||
reason = cast(str, report.wasxfail)
|
||||
if reason.startswith("reason: "):
|
||||
reason = reason[len("reason: ") :]
|
||||
return reason
|
||||
else:
|
||||
assert report.skipped
|
||||
assert isinstance(report.longrepr, tuple)
|
||||
_, _, reason = report.longrepr
|
||||
if reason.startswith("Skipped: "):
|
||||
reason = reason[len("Skipped: ") :]
|
||||
return reason
|
||||
|
||||
90
src/_pytest/threadexception.py
Normal file
90
src/_pytest/threadexception.py
Normal file
@@ -0,0 +1,90 @@
|
||||
import threading
|
||||
import traceback
|
||||
import warnings
|
||||
from types import TracebackType
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Generator
|
||||
from typing import Optional
|
||||
from typing import Type
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
# Copied from cpython/Lib/test/support/threading_helper.py, with modifications.
|
||||
class catch_threading_exception:
|
||||
"""Context manager catching threading.Thread exception using
|
||||
threading.excepthook.
|
||||
|
||||
Storing exc_value using a custom hook can create a reference cycle. The
|
||||
reference cycle is broken explicitly when the context manager exits.
|
||||
|
||||
Storing thread using a custom hook can resurrect it if it is set to an
|
||||
object which is being finalized. Exiting the context manager clears the
|
||||
stored object.
|
||||
|
||||
Usage:
|
||||
with threading_helper.catch_threading_exception() as cm:
|
||||
# code spawning a thread which raises an exception
|
||||
...
|
||||
# check the thread exception: use cm.args
|
||||
...
|
||||
# cm.args attribute no longer exists at this point
|
||||
# (to break a reference cycle)
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
# See https://github.com/python/typeshed/issues/4767 regarding the underscore.
|
||||
self.args: Optional["threading._ExceptHookArgs"] = None
|
||||
self._old_hook: Optional[Callable[["threading._ExceptHookArgs"], Any]] = None
|
||||
|
||||
def _hook(self, args: "threading._ExceptHookArgs") -> None:
|
||||
self.args = args
|
||||
|
||||
def __enter__(self) -> "catch_threading_exception":
|
||||
self._old_hook = threading.excepthook
|
||||
threading.excepthook = self._hook
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: Optional[Type[BaseException]],
|
||||
exc_val: Optional[BaseException],
|
||||
exc_tb: Optional[TracebackType],
|
||||
) -> None:
|
||||
assert self._old_hook is not None
|
||||
threading.excepthook = self._old_hook
|
||||
self._old_hook = None
|
||||
del self.args
|
||||
|
||||
|
||||
def thread_exception_runtest_hook() -> Generator[None, None, None]:
|
||||
with catch_threading_exception() as cm:
|
||||
yield
|
||||
if cm.args:
|
||||
if cm.args.thread is not None:
|
||||
thread_name = cm.args.thread.name
|
||||
else:
|
||||
thread_name = "<unknown>"
|
||||
msg = f"Exception in thread {thread_name}\n\n"
|
||||
msg += "".join(
|
||||
traceback.format_exception(
|
||||
cm.args.exc_type, cm.args.exc_value, cm.args.exc_traceback,
|
||||
)
|
||||
)
|
||||
warnings.warn(pytest.PytestUnhandledThreadExceptionWarning(msg))
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, trylast=True)
|
||||
def pytest_runtest_setup() -> Generator[None, None, None]:
|
||||
yield from thread_exception_runtest_hook()
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||
def pytest_runtest_call() -> Generator[None, None, None]:
|
||||
yield from thread_exception_runtest_hook()
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||
def pytest_runtest_teardown() -> Generator[None, None, None]:
|
||||
yield from thread_exception_runtest_hook()
|
||||
@@ -3,7 +3,7 @@
|
||||
We intentionally grab some "time" functions internally to avoid tests mocking "time" to affect
|
||||
pytest runtime information (issue #185).
|
||||
|
||||
Fixture "mock_timinig" also interacts with this module for pytest's own tests.
|
||||
Fixture "mock_timing" also interacts with this module for pytest's own tests.
|
||||
"""
|
||||
from time import perf_counter
|
||||
from time import sleep
|
||||
|
||||
@@ -2,57 +2,74 @@
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import attr
|
||||
import py
|
||||
|
||||
import pytest
|
||||
from .pathlib import ensure_reset_dir
|
||||
from .pathlib import LOCK_TIMEOUT
|
||||
from .pathlib import make_numbered_dir
|
||||
from .pathlib import make_numbered_dir_with_cleanup
|
||||
from .pathlib import Path
|
||||
from _pytest.compat import final
|
||||
from _pytest.config import Config
|
||||
from _pytest.deprecated import check_ispytest
|
||||
from _pytest.fixtures import fixture
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
|
||||
|
||||
@final
|
||||
@attr.s
|
||||
@attr.s(init=False)
|
||||
class TempPathFactory:
|
||||
"""Factory for temporary directories under the common base temp directory.
|
||||
|
||||
The base directory can be configured using the ``--basetemp`` option.
|
||||
"""
|
||||
|
||||
_given_basetemp = attr.ib(
|
||||
type=Optional[Path],
|
||||
# Use os.path.abspath() to get absolute path instead of resolve() as it
|
||||
# does not work the same in all platforms (see #4427).
|
||||
# Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012).
|
||||
# Ignore type because of https://github.com/python/mypy/issues/6172.
|
||||
converter=attr.converters.optional(
|
||||
lambda p: Path(os.path.abspath(str(p))) # type: ignore
|
||||
),
|
||||
)
|
||||
_given_basetemp = attr.ib(type=Optional[Path])
|
||||
_trace = attr.ib()
|
||||
_basetemp = attr.ib(type=Optional[Path], default=None)
|
||||
_basetemp = attr.ib(type=Optional[Path])
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
given_basetemp: Optional[Path],
|
||||
trace,
|
||||
basetemp: Optional[Path] = None,
|
||||
*,
|
||||
_ispytest: bool = False,
|
||||
) -> None:
|
||||
check_ispytest(_ispytest)
|
||||
if given_basetemp is None:
|
||||
self._given_basetemp = None
|
||||
else:
|
||||
# Use os.path.abspath() to get absolute path instead of resolve() as it
|
||||
# does not work the same in all platforms (see #4427).
|
||||
# Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012).
|
||||
self._given_basetemp = Path(os.path.abspath(str(given_basetemp)))
|
||||
self._trace = trace
|
||||
self._basetemp = basetemp
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config: Config) -> "TempPathFactory":
|
||||
"""Create a factory according to pytest configuration."""
|
||||
def from_config(
|
||||
cls, config: Config, *, _ispytest: bool = False,
|
||||
) -> "TempPathFactory":
|
||||
"""Create a factory according to pytest configuration.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
check_ispytest(_ispytest)
|
||||
return cls(
|
||||
given_basetemp=config.option.basetemp, trace=config.trace.get("tmpdir")
|
||||
given_basetemp=config.option.basetemp,
|
||||
trace=config.trace.get("tmpdir"),
|
||||
_ispytest=True,
|
||||
)
|
||||
|
||||
def _ensure_relative_to_basetemp(self, basename: str) -> str:
|
||||
basename = os.path.normpath(basename)
|
||||
if (self.getbasetemp() / basename).resolve().parent != self.getbasetemp():
|
||||
raise ValueError(
|
||||
"{} is not a normalized and relative path".format(basename)
|
||||
)
|
||||
raise ValueError(f"{basename} is not a normalized and relative path")
|
||||
return basename
|
||||
|
||||
def mktemp(self, basename: str, numbered: bool = True) -> Path:
|
||||
@@ -94,7 +111,7 @@ class TempPathFactory:
|
||||
user = get_user() or "unknown"
|
||||
# use a sub-directory in the temproot to speed-up
|
||||
# make_numbered_dir() call
|
||||
rootdir = temproot.joinpath("pytest-of-{}".format(user))
|
||||
rootdir = temproot.joinpath(f"pytest-of-{user}")
|
||||
rootdir.mkdir(exist_ok=True)
|
||||
basetemp = make_numbered_dir_with_cleanup(
|
||||
prefix="pytest-", root=rootdir, keep=3, lock_timeout=LOCK_TIMEOUT
|
||||
@@ -106,13 +123,19 @@ class TempPathFactory:
|
||||
|
||||
|
||||
@final
|
||||
@attr.s
|
||||
@attr.s(init=False)
|
||||
class TempdirFactory:
|
||||
"""Backward comptibility wrapper that implements :class:``py.path.local``
|
||||
for :class:``TempPathFactory``."""
|
||||
|
||||
_tmppath_factory = attr.ib(type=TempPathFactory)
|
||||
|
||||
def __init__(
|
||||
self, tmppath_factory: TempPathFactory, *, _ispytest: bool = False
|
||||
) -> None:
|
||||
check_ispytest(_ispytest)
|
||||
self._tmppath_factory = tmppath_factory
|
||||
|
||||
def mktemp(self, basename: str, numbered: bool = True) -> py.path.local:
|
||||
"""Same as :meth:`TempPathFactory.mktemp`, but returns a ``py.path.local`` object."""
|
||||
return py.path.local(self._tmppath_factory.mktemp(basename, numbered).resolve())
|
||||
@@ -141,21 +164,21 @@ def pytest_configure(config: Config) -> None:
|
||||
to the tmpdir_factory session fixture.
|
||||
"""
|
||||
mp = MonkeyPatch()
|
||||
tmppath_handler = TempPathFactory.from_config(config)
|
||||
t = TempdirFactory(tmppath_handler)
|
||||
tmppath_handler = TempPathFactory.from_config(config, _ispytest=True)
|
||||
t = TempdirFactory(tmppath_handler, _ispytest=True)
|
||||
config._cleanup.append(mp.undo)
|
||||
mp.setattr(config, "_tmp_path_factory", tmppath_handler, raising=False)
|
||||
mp.setattr(config, "_tmpdirhandler", t, raising=False)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@fixture(scope="session")
|
||||
def tmpdir_factory(request: FixtureRequest) -> TempdirFactory:
|
||||
"""Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session."""
|
||||
# Set dynamically by pytest_configure() above.
|
||||
return request.config._tmpdirhandler # type: ignore
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@fixture(scope="session")
|
||||
def tmp_path_factory(request: FixtureRequest) -> TempPathFactory:
|
||||
"""Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session."""
|
||||
# Set dynamically by pytest_configure() above.
|
||||
@@ -170,12 +193,17 @@ def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path:
|
||||
return factory.mktemp(name, numbered=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@fixture
|
||||
def tmpdir(tmp_path: Path) -> py.path.local:
|
||||
"""Return a temporary directory path object which is unique to each test
|
||||
function invocation, created as a sub directory of the base temporary
|
||||
directory.
|
||||
|
||||
By default, a new base temporary directory is created each test session,
|
||||
and old bases are removed after 3 sessions, to aid in debugging. If
|
||||
``--basetemp`` is used then it is cleared each session. See :ref:`base
|
||||
temporary directory`.
|
||||
|
||||
The returned object is a `py.path.local`_ path object.
|
||||
|
||||
.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
|
||||
@@ -183,17 +211,18 @@ def tmpdir(tmp_path: Path) -> py.path.local:
|
||||
return py.path.local(tmp_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@fixture
|
||||
def tmp_path(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path:
|
||||
"""Return a temporary directory path object which is unique to each test
|
||||
function invocation, created as a sub directory of the base temporary
|
||||
directory.
|
||||
|
||||
By default, a new base temporary directory is created each test session,
|
||||
and old bases are removed after 3 sessions, to aid in debugging. If
|
||||
``--basetemp`` is used then it is cleared each session. See :ref:`base
|
||||
temporary directory`.
|
||||
|
||||
The returned object is a :class:`pathlib.Path` object.
|
||||
|
||||
.. note::
|
||||
|
||||
In python < 3.6 this is a pathlib2.Path.
|
||||
"""
|
||||
|
||||
return _mk_tmp(request, tmp_path_factory)
|
||||
|
||||
@@ -9,13 +9,14 @@ from typing import Iterable
|
||||
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
|
||||
|
||||
import _pytest._code
|
||||
import pytest
|
||||
from _pytest.compat import getimfunc
|
||||
from _pytest.compat import is_async_function
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
from _pytest.nodes import Collector
|
||||
@@ -33,7 +34,6 @@ from _pytest.skipping import unexpectedsuccess_key
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import unittest
|
||||
from typing import Type
|
||||
|
||||
from _pytest.fixtures import _Scope
|
||||
|
||||
@@ -55,7 +55,7 @@ def pytest_pycollect_makeitem(
|
||||
except Exception:
|
||||
return None
|
||||
# Yes, so let's collect it.
|
||||
item = UnitTestCase.from_parent(collector, name=name, obj=obj) # type: UnitTestCase
|
||||
item: UnitTestCase = UnitTestCase.from_parent(collector, name=name, obj=obj)
|
||||
return item
|
||||
|
||||
|
||||
@@ -99,54 +99,97 @@ class UnitTestCase(Class):
|
||||
"""Injects a hidden auto-use fixture to invoke setUpClass/setup_method and corresponding
|
||||
teardown functions (#517)."""
|
||||
class_fixture = _make_xunit_fixture(
|
||||
cls, "setUpClass", "tearDownClass", scope="class", pass_self=False
|
||||
cls,
|
||||
"setUpClass",
|
||||
"tearDownClass",
|
||||
"doClassCleanups",
|
||||
scope="class",
|
||||
pass_self=False,
|
||||
)
|
||||
if class_fixture:
|
||||
cls.__pytest_class_setup = class_fixture # type: ignore[attr-defined]
|
||||
|
||||
method_fixture = _make_xunit_fixture(
|
||||
cls, "setup_method", "teardown_method", scope="function", pass_self=True
|
||||
cls,
|
||||
"setup_method",
|
||||
"teardown_method",
|
||||
None,
|
||||
scope="function",
|
||||
pass_self=True,
|
||||
)
|
||||
if method_fixture:
|
||||
cls.__pytest_method_setup = method_fixture # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def _make_xunit_fixture(
|
||||
obj: type, setup_name: str, teardown_name: str, scope: "_Scope", pass_self: bool
|
||||
obj: type,
|
||||
setup_name: str,
|
||||
teardown_name: str,
|
||||
cleanup_name: Optional[str],
|
||||
scope: "_Scope",
|
||||
pass_self: bool,
|
||||
):
|
||||
setup = getattr(obj, setup_name, None)
|
||||
teardown = getattr(obj, teardown_name, None)
|
||||
if setup is None and teardown is None:
|
||||
return None
|
||||
|
||||
@pytest.fixture(scope=scope, autouse=True)
|
||||
if cleanup_name:
|
||||
cleanup = getattr(obj, cleanup_name, lambda *args: None)
|
||||
else:
|
||||
|
||||
def cleanup(*args):
|
||||
pass
|
||||
|
||||
@pytest.fixture(
|
||||
scope=scope,
|
||||
autouse=True,
|
||||
# Use a unique name to speed up lookup.
|
||||
name=f"unittest_{setup_name}_fixture_{obj.__qualname__}",
|
||||
)
|
||||
def fixture(self, request: FixtureRequest) -> Generator[None, None, None]:
|
||||
if _is_skipped(self):
|
||||
reason = self.__unittest_skip_why__
|
||||
pytest.skip(reason)
|
||||
if setup is not None:
|
||||
if pass_self:
|
||||
setup(self, request.function)
|
||||
else:
|
||||
setup()
|
||||
try:
|
||||
if pass_self:
|
||||
setup(self, request.function)
|
||||
else:
|
||||
setup()
|
||||
# unittest does not call the cleanup function for every BaseException, so we
|
||||
# follow this here.
|
||||
except Exception:
|
||||
if pass_self:
|
||||
cleanup(self)
|
||||
else:
|
||||
cleanup()
|
||||
|
||||
raise
|
||||
yield
|
||||
if teardown is not None:
|
||||
try:
|
||||
if teardown is not None:
|
||||
if pass_self:
|
||||
teardown(self, request.function)
|
||||
else:
|
||||
teardown()
|
||||
finally:
|
||||
if pass_self:
|
||||
teardown(self, request.function)
|
||||
cleanup(self)
|
||||
else:
|
||||
teardown()
|
||||
cleanup()
|
||||
|
||||
return fixture
|
||||
|
||||
|
||||
class TestCaseFunction(Function):
|
||||
nofuncargs = True
|
||||
_excinfo = None # type: Optional[List[_pytest._code.ExceptionInfo[BaseException]]]
|
||||
_testcase = None # type: Optional[unittest.TestCase]
|
||||
_excinfo: Optional[List[_pytest._code.ExceptionInfo[BaseException]]] = None
|
||||
_testcase: Optional["unittest.TestCase"] = None
|
||||
|
||||
def setup(self) -> None:
|
||||
# A bound method to be called during teardown() if set (see 'runtest()').
|
||||
self._explicit_tearDown = None # type: Optional[Callable[[], None]]
|
||||
self._explicit_tearDown: Optional[Callable[[], None]] = None
|
||||
assert self.parent is not None
|
||||
self._testcase = self.parent.obj(self.name) # type: ignore[attr-defined]
|
||||
self._obj = getattr(self._testcase, self.name)
|
||||
@@ -320,7 +363,7 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None:
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
|
||||
if isinstance(item, TestCaseFunction) and "twisted.trial.unittest" in sys.modules:
|
||||
ut = sys.modules["twisted.python.failure"] # type: Any
|
||||
ut: Any = sys.modules["twisted.python.failure"]
|
||||
Failure__init__ = ut.Failure.__init__
|
||||
check_testcase_implements_trial_reporter()
|
||||
|
||||
|
||||
93
src/_pytest/unraisableexception.py
Normal file
93
src/_pytest/unraisableexception.py
Normal file
@@ -0,0 +1,93 @@
|
||||
import sys
|
||||
import traceback
|
||||
import warnings
|
||||
from types import TracebackType
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Generator
|
||||
from typing import Optional
|
||||
from typing import Type
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
# Copied from cpython/Lib/test/support/__init__.py, with modifications.
|
||||
class catch_unraisable_exception:
|
||||
"""Context manager catching unraisable exception using sys.unraisablehook.
|
||||
|
||||
Storing the exception value (cm.unraisable.exc_value) creates a reference
|
||||
cycle. The reference cycle is broken explicitly when the context manager
|
||||
exits.
|
||||
|
||||
Storing the object (cm.unraisable.object) can resurrect it if it is set to
|
||||
an object which is being finalized. Exiting the context manager clears the
|
||||
stored object.
|
||||
|
||||
Usage:
|
||||
with catch_unraisable_exception() as cm:
|
||||
# code creating an "unraisable exception"
|
||||
...
|
||||
# check the unraisable exception: use cm.unraisable
|
||||
...
|
||||
# cm.unraisable attribute no longer exists at this point
|
||||
# (to break a reference cycle)
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.unraisable: Optional["sys.UnraisableHookArgs"] = None
|
||||
self._old_hook: Optional[Callable[["sys.UnraisableHookArgs"], Any]] = None
|
||||
|
||||
def _hook(self, unraisable: "sys.UnraisableHookArgs") -> None:
|
||||
# Storing unraisable.object can resurrect an object which is being
|
||||
# finalized. Storing unraisable.exc_value creates a reference cycle.
|
||||
self.unraisable = unraisable
|
||||
|
||||
def __enter__(self) -> "catch_unraisable_exception":
|
||||
self._old_hook = sys.unraisablehook
|
||||
sys.unraisablehook = self._hook
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: Optional[Type[BaseException]],
|
||||
exc_val: Optional[BaseException],
|
||||
exc_tb: Optional[TracebackType],
|
||||
) -> None:
|
||||
assert self._old_hook is not None
|
||||
sys.unraisablehook = self._old_hook
|
||||
self._old_hook = None
|
||||
del self.unraisable
|
||||
|
||||
|
||||
def unraisable_exception_runtest_hook() -> Generator[None, None, None]:
|
||||
with catch_unraisable_exception() as cm:
|
||||
yield
|
||||
if cm.unraisable:
|
||||
if cm.unraisable.err_msg is not None:
|
||||
err_msg = cm.unraisable.err_msg
|
||||
else:
|
||||
err_msg = "Exception ignored in"
|
||||
msg = f"{err_msg}: {cm.unraisable.object!r}\n\n"
|
||||
msg += "".join(
|
||||
traceback.format_exception(
|
||||
cm.unraisable.exc_type,
|
||||
cm.unraisable.exc_value,
|
||||
cm.unraisable.exc_traceback,
|
||||
)
|
||||
)
|
||||
warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||
def pytest_runtest_setup() -> Generator[None, None, None]:
|
||||
yield from unraisable_exception_runtest_hook()
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||
def pytest_runtest_call() -> Generator[None, None, None]:
|
||||
yield from unraisable_exception_runtest_hook()
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||
def pytest_runtest_teardown() -> Generator[None, None, None]:
|
||||
yield from unraisable_exception_runtest_hook()
|
||||
@@ -1,14 +1,11 @@
|
||||
from typing import Any
|
||||
from typing import Generic
|
||||
from typing import Type
|
||||
from typing import TypeVar
|
||||
|
||||
import attr
|
||||
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Type # noqa: F401 (used in type string)
|
||||
|
||||
|
||||
class PytestWarning(UserWarning):
|
||||
@@ -93,6 +90,28 @@ class PytestUnknownMarkWarning(PytestWarning):
|
||||
__module__ = "pytest"
|
||||
|
||||
|
||||
@final
|
||||
class PytestUnraisableExceptionWarning(PytestWarning):
|
||||
"""An unraisable exception was reported.
|
||||
|
||||
Unraisable exceptions are exceptions raised in :meth:`__del__ <object.__del__>`
|
||||
implementations and similar situations when the exception cannot be raised
|
||||
as normal.
|
||||
"""
|
||||
|
||||
__module__ = "pytest"
|
||||
|
||||
|
||||
@final
|
||||
class PytestUnhandledThreadExceptionWarning(PytestWarning):
|
||||
"""An unhandled exception occurred in a :class:`~threading.Thread`.
|
||||
|
||||
Such exceptions don't propagate normally.
|
||||
"""
|
||||
|
||||
__module__ = "pytest"
|
||||
|
||||
|
||||
_W = TypeVar("_W", bound=PytestWarning)
|
||||
|
||||
|
||||
@@ -105,12 +124,9 @@ class UnformattedWarning(Generic[_W]):
|
||||
as opposed to a direct message.
|
||||
"""
|
||||
|
||||
category = attr.ib(type="Type[_W]")
|
||||
category = attr.ib(type=Type["_W"])
|
||||
template = attr.ib(type=str)
|
||||
|
||||
def format(self, **kwargs: Any) -> _W:
|
||||
"""Return an instance of the warning category, formatted with given kwargs."""
|
||||
return self.category(self.template.format(**kwargs))
|
||||
|
||||
|
||||
PYTESTER_COPY_EXAMPLE = PytestExperimentalApiWarning.simple("testdir.copy_example")
|
||||
|
||||
@@ -3,9 +3,9 @@ import warnings
|
||||
from contextlib import contextmanager
|
||||
from typing import Generator
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.config import apply_warning_filters
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import parse_warning_filter
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
from . import collect
|
||||
from _pytest import __version__
|
||||
from _pytest.assertion import register_assert_rewrite
|
||||
from _pytest.cacheprovider import Cache
|
||||
from _pytest.capture import CaptureFixture
|
||||
from _pytest.config import cmdline
|
||||
from _pytest.config import console_main
|
||||
from _pytest.config import ExitCode
|
||||
@@ -11,14 +13,17 @@ from _pytest.config import hookspec
|
||||
from _pytest.config import main
|
||||
from _pytest.config import UsageError
|
||||
from _pytest.debugging import pytestPDB as __pytestPDB
|
||||
from _pytest.fixtures import fillfixtures as _fillfuncargs
|
||||
from _pytest.fixtures import _fillfuncargs
|
||||
from _pytest.fixtures import fixture
|
||||
from _pytest.fixtures import FixtureLookupError
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
from _pytest.fixtures import yield_fixture
|
||||
from _pytest.freeze_support import freeze_includes
|
||||
from _pytest.logging import LogCaptureFixture
|
||||
from _pytest.main import Session
|
||||
from _pytest.mark import MARK_GEN as mark
|
||||
from _pytest.mark import param
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
from _pytest.nodes import Collector
|
||||
from _pytest.nodes import File
|
||||
from _pytest.nodes import Item
|
||||
@@ -27,6 +32,8 @@ from _pytest.outcomes import fail
|
||||
from _pytest.outcomes import importorskip
|
||||
from _pytest.outcomes import skip
|
||||
from _pytest.outcomes import xfail
|
||||
from _pytest.pytester import Pytester
|
||||
from _pytest.pytester import Testdir
|
||||
from _pytest.python import Class
|
||||
from _pytest.python import Function
|
||||
from _pytest.python import Instance
|
||||
@@ -35,7 +42,10 @@ from _pytest.python import Package
|
||||
from _pytest.python_api import approx
|
||||
from _pytest.python_api import raises
|
||||
from _pytest.recwarn import deprecated_call
|
||||
from _pytest.recwarn import WarningsRecorder
|
||||
from _pytest.recwarn import warns
|
||||
from _pytest.tmpdir import TempdirFactory
|
||||
from _pytest.tmpdir import TempPathFactory
|
||||
from _pytest.warning_types import PytestAssertRewriteWarning
|
||||
from _pytest.warning_types import PytestCacheWarning
|
||||
from _pytest.warning_types import PytestCollectionWarning
|
||||
@@ -43,7 +53,9 @@ from _pytest.warning_types import PytestConfigWarning
|
||||
from _pytest.warning_types import PytestDeprecationWarning
|
||||
from _pytest.warning_types import PytestExperimentalApiWarning
|
||||
from _pytest.warning_types import PytestUnhandledCoroutineWarning
|
||||
from _pytest.warning_types import PytestUnhandledThreadExceptionWarning
|
||||
from _pytest.warning_types import PytestUnknownMarkWarning
|
||||
from _pytest.warning_types import PytestUnraisableExceptionWarning
|
||||
from _pytest.warning_types import PytestWarning
|
||||
|
||||
set_trace = __pytestPDB.set_trace
|
||||
@@ -52,6 +64,8 @@ __all__ = [
|
||||
"__version__",
|
||||
"_fillfuncargs",
|
||||
"approx",
|
||||
"Cache",
|
||||
"CaptureFixture",
|
||||
"Class",
|
||||
"cmdline",
|
||||
"collect",
|
||||
@@ -64,6 +78,7 @@ __all__ = [
|
||||
"File",
|
||||
"fixture",
|
||||
"FixtureLookupError",
|
||||
"FixtureRequest",
|
||||
"freeze_includes",
|
||||
"Function",
|
||||
"hookimpl",
|
||||
@@ -71,9 +86,11 @@ __all__ = [
|
||||
"importorskip",
|
||||
"Instance",
|
||||
"Item",
|
||||
"LogCaptureFixture",
|
||||
"main",
|
||||
"mark",
|
||||
"Module",
|
||||
"MonkeyPatch",
|
||||
"Package",
|
||||
"param",
|
||||
"PytestAssertRewriteWarning",
|
||||
@@ -82,15 +99,22 @@ __all__ = [
|
||||
"PytestConfigWarning",
|
||||
"PytestDeprecationWarning",
|
||||
"PytestExperimentalApiWarning",
|
||||
"Pytester",
|
||||
"PytestUnhandledCoroutineWarning",
|
||||
"PytestUnhandledThreadExceptionWarning",
|
||||
"PytestUnknownMarkWarning",
|
||||
"PytestUnraisableExceptionWarning",
|
||||
"PytestWarning",
|
||||
"raises",
|
||||
"register_assert_rewrite",
|
||||
"Session",
|
||||
"set_trace",
|
||||
"skip",
|
||||
"TempPathFactory",
|
||||
"Testdir",
|
||||
"TempdirFactory",
|
||||
"UsageError",
|
||||
"WarningsRecorder",
|
||||
"warns",
|
||||
"xfail",
|
||||
"yield_fixture",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -28,11 +28,12 @@ def test_code_gives_back_name_for_not_existing_file() -> None:
|
||||
assert code.fullsource is None
|
||||
|
||||
|
||||
def test_code_with_class() -> None:
|
||||
def test_code_from_function_with_class() -> None:
|
||||
class A:
|
||||
pass
|
||||
|
||||
pytest.raises(TypeError, Code, A)
|
||||
with pytest.raises(TypeError):
|
||||
Code.from_function(A)
|
||||
|
||||
|
||||
def x() -> None:
|
||||
@@ -40,13 +41,13 @@ def x() -> None:
|
||||
|
||||
|
||||
def test_code_fullsource() -> None:
|
||||
code = Code(x)
|
||||
code = Code.from_function(x)
|
||||
full = code.fullsource
|
||||
assert "test_code_fullsource()" in str(full)
|
||||
|
||||
|
||||
def test_code_source() -> None:
|
||||
code = Code(x)
|
||||
code = Code.from_function(x)
|
||||
src = code.source()
|
||||
expected = """def x() -> None:
|
||||
raise NotImplementedError()"""
|
||||
@@ -73,7 +74,7 @@ def test_getstatement_empty_fullsource() -> None:
|
||||
|
||||
|
||||
def test_code_from_func() -> None:
|
||||
co = Code(test_frame_getsourcelineno_myself)
|
||||
co = Code.from_function(test_frame_getsourcelineno_myself)
|
||||
assert co.firstlineno
|
||||
assert co.path
|
||||
|
||||
@@ -92,25 +93,25 @@ def test_code_getargs() -> None:
|
||||
def f1(x):
|
||||
raise NotImplementedError()
|
||||
|
||||
c1 = Code(f1)
|
||||
c1 = Code.from_function(f1)
|
||||
assert c1.getargs(var=True) == ("x",)
|
||||
|
||||
def f2(x, *y):
|
||||
raise NotImplementedError()
|
||||
|
||||
c2 = Code(f2)
|
||||
c2 = Code.from_function(f2)
|
||||
assert c2.getargs(var=True) == ("x", "y")
|
||||
|
||||
def f3(x, **z):
|
||||
raise NotImplementedError()
|
||||
|
||||
c3 = Code(f3)
|
||||
c3 = Code.from_function(f3)
|
||||
assert c3.getargs(var=True) == ("x", "z")
|
||||
|
||||
def f4(x, *y, **z):
|
||||
raise NotImplementedError()
|
||||
|
||||
c4 = Code(f4)
|
||||
c4 = Code.from_function(f4)
|
||||
assert c4.getargs(var=True) == ("x", "y", "z")
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user