Merge branch 'main' into RonnyPfannschmidt-pypi-oidc-deploy-env
This commit is contained in:
commit
d7def89b2f
|
@ -58,7 +58,8 @@ jobs:
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.8"
|
python-version: "3.11"
|
||||||
|
|
||||||
|
|
||||||
- name: Install tox
|
- name: Install tox
|
||||||
run: |
|
run: |
|
||||||
|
|
|
@ -10,12 +10,12 @@ jobs:
|
||||||
permissions:
|
permissions:
|
||||||
issues: write
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v5
|
- uses: actions/stale@v8
|
||||||
with:
|
with:
|
||||||
debug-only: true
|
debug-only: false
|
||||||
days-before-issue-stale: 14
|
days-before-issue-stale: 14
|
||||||
days-before-issue-close: 7
|
days-before-issue-close: 7
|
||||||
only-labels: ["status: needs information"]
|
only-labels: "status: needs information"
|
||||||
stale-issue-label: "stale"
|
stale-issue-label: "stale"
|
||||||
stale-issue-message: "This issue is stale because it has been open for 14 days with no activity."
|
stale-issue-message: "This issue is stale because it has been open for 14 days with no activity."
|
||||||
close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale."
|
close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale."
|
||||||
|
|
|
@ -37,26 +37,26 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
name: [
|
name: [
|
||||||
"windows-py37",
|
|
||||||
"windows-py37-pluggy",
|
|
||||||
"windows-py38",
|
"windows-py38",
|
||||||
|
"windows-py38-pluggy",
|
||||||
"windows-py39",
|
"windows-py39",
|
||||||
"windows-py310",
|
"windows-py310",
|
||||||
"windows-py311",
|
"windows-py311",
|
||||||
|
"windows-py312",
|
||||||
|
|
||||||
"ubuntu-py37",
|
|
||||||
"ubuntu-py37-pluggy",
|
|
||||||
"ubuntu-py37-freeze",
|
|
||||||
"ubuntu-py38",
|
"ubuntu-py38",
|
||||||
|
"ubuntu-py38-pluggy",
|
||||||
|
"ubuntu-py38-freeze",
|
||||||
"ubuntu-py39",
|
"ubuntu-py39",
|
||||||
"ubuntu-py310",
|
"ubuntu-py310",
|
||||||
"ubuntu-py311",
|
"ubuntu-py311",
|
||||||
|
"ubuntu-py312",
|
||||||
"ubuntu-pypy3",
|
"ubuntu-pypy3",
|
||||||
|
|
||||||
"macos-py37",
|
|
||||||
"macos-py38",
|
"macos-py38",
|
||||||
"macos-py39",
|
"macos-py39",
|
||||||
"macos-py310",
|
"macos-py310",
|
||||||
|
"macos-py312",
|
||||||
|
|
||||||
"docs",
|
"docs",
|
||||||
"doctesting",
|
"doctesting",
|
||||||
|
@ -64,19 +64,15 @@ jobs:
|
||||||
]
|
]
|
||||||
|
|
||||||
include:
|
include:
|
||||||
- name: "windows-py37"
|
|
||||||
python: "3.7"
|
|
||||||
os: windows-latest
|
|
||||||
tox_env: "py37-numpy"
|
|
||||||
- name: "windows-py37-pluggy"
|
|
||||||
python: "3.7"
|
|
||||||
os: windows-latest
|
|
||||||
tox_env: "py37-pluggymain-pylib-xdist"
|
|
||||||
- name: "windows-py38"
|
- name: "windows-py38"
|
||||||
python: "3.8"
|
python: "3.8"
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
tox_env: "py38-unittestextras"
|
tox_env: "py38-unittestextras"
|
||||||
use_coverage: true
|
use_coverage: true
|
||||||
|
- name: "windows-py38-pluggy"
|
||||||
|
python: "3.8"
|
||||||
|
os: windows-latest
|
||||||
|
tox_env: "py38-pluggymain-pylib-xdist"
|
||||||
- name: "windows-py39"
|
- name: "windows-py39"
|
||||||
python: "3.9"
|
python: "3.9"
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
|
@ -86,27 +82,27 @@ jobs:
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
tox_env: "py310-xdist"
|
tox_env: "py310-xdist"
|
||||||
- name: "windows-py311"
|
- name: "windows-py311"
|
||||||
python: "3.11-dev"
|
python: "3.11"
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
tox_env: "py311"
|
tox_env: "py311"
|
||||||
|
- name: "windows-py312"
|
||||||
|
python: "3.12-dev"
|
||||||
|
os: windows-latest
|
||||||
|
tox_env: "py312"
|
||||||
|
|
||||||
- name: "ubuntu-py37"
|
|
||||||
python: "3.7"
|
|
||||||
os: ubuntu-latest
|
|
||||||
tox_env: "py37-lsof-numpy-pexpect"
|
|
||||||
use_coverage: true
|
|
||||||
- name: "ubuntu-py37-pluggy"
|
|
||||||
python: "3.7"
|
|
||||||
os: ubuntu-latest
|
|
||||||
tox_env: "py37-pluggymain-pylib-xdist"
|
|
||||||
- name: "ubuntu-py37-freeze"
|
|
||||||
python: "3.7"
|
|
||||||
os: ubuntu-latest
|
|
||||||
tox_env: "py37-freeze"
|
|
||||||
- name: "ubuntu-py38"
|
- name: "ubuntu-py38"
|
||||||
python: "3.8"
|
python: "3.8"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
tox_env: "py38-xdist"
|
tox_env: "py38-lsof-numpy-pexpect"
|
||||||
|
use_coverage: true
|
||||||
|
- name: "ubuntu-py38-pluggy"
|
||||||
|
python: "3.8"
|
||||||
|
os: ubuntu-latest
|
||||||
|
tox_env: "py38-pluggymain-pylib-xdist"
|
||||||
|
- name: "ubuntu-py38-freeze"
|
||||||
|
python: "3.8"
|
||||||
|
os: ubuntu-latest
|
||||||
|
tox_env: "py38-freeze"
|
||||||
- name: "ubuntu-py39"
|
- name: "ubuntu-py39"
|
||||||
python: "3.9"
|
python: "3.9"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
|
@ -116,32 +112,37 @@ jobs:
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
tox_env: "py310-xdist"
|
tox_env: "py310-xdist"
|
||||||
- name: "ubuntu-py311"
|
- name: "ubuntu-py311"
|
||||||
python: "3.11-dev"
|
python: "3.11"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
tox_env: "py311"
|
tox_env: "py311"
|
||||||
use_coverage: true
|
use_coverage: true
|
||||||
|
- name: "ubuntu-py312"
|
||||||
|
python: "3.12-dev"
|
||||||
|
os: ubuntu-latest
|
||||||
|
tox_env: "py312"
|
||||||
|
use_coverage: true
|
||||||
- name: "ubuntu-pypy3"
|
- name: "ubuntu-pypy3"
|
||||||
python: "pypy-3.7"
|
python: "pypy-3.8"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
tox_env: "pypy3-xdist"
|
tox_env: "pypy3-xdist"
|
||||||
|
|
||||||
- name: "macos-py37"
|
|
||||||
python: "3.7"
|
|
||||||
os: macos-latest
|
|
||||||
tox_env: "py37-xdist"
|
|
||||||
- name: "macos-py38"
|
- name: "macos-py38"
|
||||||
python: "3.8"
|
python: "3.8"
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
tox_env: "py38-xdist"
|
tox_env: "py38-xdist"
|
||||||
use_coverage: true
|
|
||||||
- name: "macos-py39"
|
- name: "macos-py39"
|
||||||
python: "3.9"
|
python: "3.9"
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
tox_env: "py39-xdist"
|
tox_env: "py39-xdist"
|
||||||
|
use_coverage: true
|
||||||
- name: "macos-py310"
|
- name: "macos-py310"
|
||||||
python: "3.10"
|
python: "3.10"
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
tox_env: "py310-xdist"
|
tox_env: "py310-xdist"
|
||||||
|
- name: "macos-py312"
|
||||||
|
python: "3.12-dev"
|
||||||
|
os: macos-latest
|
||||||
|
tox_env: "py312-xdist"
|
||||||
|
|
||||||
- name: "plugins"
|
- name: "plugins"
|
||||||
python: "3.9"
|
python: "3.9"
|
||||||
|
@ -149,11 +150,11 @@ jobs:
|
||||||
tox_env: "plugins"
|
tox_env: "plugins"
|
||||||
|
|
||||||
- name: "docs"
|
- name: "docs"
|
||||||
python: "3.7"
|
python: "3.8"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
tox_env: "docs"
|
tox_env: "docs"
|
||||||
- name: "doctesting"
|
- name: "doctesting"
|
||||||
python: "3.7"
|
python: "3.8"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
tox_env: "doctesting"
|
tox_env: "doctesting"
|
||||||
use_coverage: true
|
use_coverage: true
|
||||||
|
@ -168,6 +169,7 @@ jobs:
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python }}
|
python-version: ${{ matrix.python }}
|
||||||
|
check-latest: ${{ endsWith(matrix.python, '-dev') }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
|
|
|
@ -38,7 +38,7 @@ jobs:
|
||||||
run: python scripts/update-plugin-list.py
|
run: python scripts/update-plugin-list.py
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@5b4a9f6a9e2af26e5f02351490b90d01eb8ec1e5
|
uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38
|
||||||
with:
|
with:
|
||||||
commit-message: '[automated] Update plugin list'
|
commit-message: '[automated] Update plugin list'
|
||||||
author: 'pytest bot <pytestbot@users.noreply.github.com>'
|
author: 'pytest bot <pytestbot@users.noreply.github.com>'
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
default_language_version:
|
|
||||||
python: "3.10"
|
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 23.3.0
|
rev: 23.3.0
|
||||||
|
@ -7,7 +5,7 @@ repos:
|
||||||
- id: black
|
- id: black
|
||||||
args: [--safe, --quiet]
|
args: [--safe, --quiet]
|
||||||
- repo: https://github.com/asottile/blacken-docs
|
- repo: https://github.com/asottile/blacken-docs
|
||||||
rev: 1.13.0
|
rev: 1.14.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: blacken-docs
|
- id: blacken-docs
|
||||||
additional_dependencies: [black==23.1.0]
|
additional_dependencies: [black==23.1.0]
|
||||||
|
@ -23,7 +21,7 @@ repos:
|
||||||
exclude: _pytest/(debugging|hookspec).py
|
exclude: _pytest/(debugging|hookspec).py
|
||||||
language_version: python3
|
language_version: python3
|
||||||
- repo: https://github.com/PyCQA/autoflake
|
- repo: https://github.com/PyCQA/autoflake
|
||||||
rev: v2.1.1
|
rev: v2.2.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: autoflake
|
- id: autoflake
|
||||||
name: autoflake
|
name: autoflake
|
||||||
|
@ -39,26 +37,26 @@ repos:
|
||||||
- flake8-typing-imports==1.12.0
|
- flake8-typing-imports==1.12.0
|
||||||
- flake8-docstrings==1.5.0
|
- flake8-docstrings==1.5.0
|
||||||
- repo: https://github.com/asottile/reorder-python-imports
|
- repo: https://github.com/asottile/reorder-python-imports
|
||||||
rev: v3.9.0
|
rev: v3.10.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: reorder-python-imports
|
- id: reorder-python-imports
|
||||||
args: ['--application-directories=.:src', --py37-plus]
|
args: ['--application-directories=.:src', --py38-plus]
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v3.4.0
|
rev: v3.7.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args: [--py37-plus]
|
args: [--py38-plus]
|
||||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||||
rev: v2.2.0
|
rev: v2.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: setup-cfg-fmt
|
- id: setup-cfg-fmt
|
||||||
args: ["--max-py-version=3.11", "--include-version-classifiers"]
|
args: ["--max-py-version=3.12", "--include-version-classifiers"]
|
||||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||||
rev: v1.10.0
|
rev: v1.10.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: python-use-type-annotations
|
- id: python-use-type-annotations
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: v1.2.0
|
rev: v1.4.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
files: ^(src/|testing/)
|
files: ^(src/|testing/)
|
||||||
|
|
9
AUTHORS
9
AUTHORS
|
@ -8,12 +8,14 @@ Abdeali JK
|
||||||
Abdelrahman Elbehery
|
Abdelrahman Elbehery
|
||||||
Abhijeet Kasurde
|
Abhijeet Kasurde
|
||||||
Adam Johnson
|
Adam Johnson
|
||||||
|
Adam Stewart
|
||||||
Adam Uhlir
|
Adam Uhlir
|
||||||
Ahn Ki-Wook
|
Ahn Ki-Wook
|
||||||
Akiomi Kamakura
|
Akiomi Kamakura
|
||||||
Alan Velasco
|
Alan Velasco
|
||||||
Alessio Izzo
|
Alessio Izzo
|
||||||
Alex Jones
|
Alex Jones
|
||||||
|
Alex Lambson
|
||||||
Alexander Johnson
|
Alexander Johnson
|
||||||
Alexander King
|
Alexander King
|
||||||
Alexei Kozlenok
|
Alexei Kozlenok
|
||||||
|
@ -70,6 +72,7 @@ Charles Cloud
|
||||||
Charles Machalow
|
Charles Machalow
|
||||||
Charnjit SiNGH (CCSJ)
|
Charnjit SiNGH (CCSJ)
|
||||||
Cheuk Ting Ho
|
Cheuk Ting Ho
|
||||||
|
Chris Mahoney
|
||||||
Chris Lamb
|
Chris Lamb
|
||||||
Chris NeJame
|
Chris NeJame
|
||||||
Chris Rose
|
Chris Rose
|
||||||
|
@ -126,8 +129,10 @@ Eric Hunsberger
|
||||||
Eric Liu
|
Eric Liu
|
||||||
Eric Siegerman
|
Eric Siegerman
|
||||||
Erik Aronesty
|
Erik Aronesty
|
||||||
|
Erik Hasse
|
||||||
Erik M. Bray
|
Erik M. Bray
|
||||||
Evan Kepner
|
Evan Kepner
|
||||||
|
Evgeny Seliverstov
|
||||||
Fabien Zarifian
|
Fabien Zarifian
|
||||||
Fabio Zadrozny
|
Fabio Zadrozny
|
||||||
Felix Hofstätter
|
Felix Hofstätter
|
||||||
|
@ -194,6 +199,7 @@ Justice Ndou
|
||||||
Justyna Janczyszyn
|
Justyna Janczyszyn
|
||||||
Kale Kundert
|
Kale Kundert
|
||||||
Kamran Ahmad
|
Kamran Ahmad
|
||||||
|
Kenny Y
|
||||||
Karl O. Pinc
|
Karl O. Pinc
|
||||||
Karthikeyan Singaravelan
|
Karthikeyan Singaravelan
|
||||||
Katarzyna Jachim
|
Katarzyna Jachim
|
||||||
|
@ -305,7 +311,9 @@ Raphael Pierzina
|
||||||
Rafal Semik
|
Rafal Semik
|
||||||
Raquel Alegre
|
Raquel Alegre
|
||||||
Ravi Chandra
|
Ravi Chandra
|
||||||
|
Reagan Lee
|
||||||
Robert Holt
|
Robert Holt
|
||||||
|
Roberto Aldera
|
||||||
Roberto Polli
|
Roberto Polli
|
||||||
Roland Puntaier
|
Roland Puntaier
|
||||||
Romain Dorgueil
|
Romain Dorgueil
|
||||||
|
@ -371,6 +379,7 @@ Victor Maryama
|
||||||
Victor Rodriguez
|
Victor Rodriguez
|
||||||
Victor Uriarte
|
Victor Uriarte
|
||||||
Vidar T. Fauske
|
Vidar T. Fauske
|
||||||
|
Vijay Arora
|
||||||
Virgil Dupras
|
Virgil Dupras
|
||||||
Vitaly Lashmanov
|
Vitaly Lashmanov
|
||||||
Vivaan Verma
|
Vivaan Verma
|
||||||
|
|
|
@ -201,7 +201,7 @@ Short version
|
||||||
#. Follow **PEP-8** for naming and `black <https://github.com/psf/black>`_ for formatting.
|
#. Follow **PEP-8** for naming and `black <https://github.com/psf/black>`_ for formatting.
|
||||||
#. Tests are run using ``tox``::
|
#. Tests are run using ``tox``::
|
||||||
|
|
||||||
tox -e linting,py37
|
tox -e linting,py39
|
||||||
|
|
||||||
The test environments above are usually enough to cover most cases locally.
|
The test environments above are usually enough to cover most cases locally.
|
||||||
|
|
||||||
|
@ -272,24 +272,24 @@ Here is a simple overview, with pytest-specific bits:
|
||||||
|
|
||||||
#. Run all the tests
|
#. Run all the tests
|
||||||
|
|
||||||
You need to have Python 3.7 available in your system. Now
|
You need to have Python 3.8 or later available in your system. Now
|
||||||
running tests is as simple as issuing this command::
|
running tests is as simple as issuing this command::
|
||||||
|
|
||||||
$ tox -e linting,py37
|
$ tox -e linting,py39
|
||||||
|
|
||||||
This command will run tests via the "tox" tool against Python 3.7
|
This command will run tests via the "tox" tool against Python 3.9
|
||||||
and also perform "lint" coding-style checks.
|
and also perform "lint" coding-style checks.
|
||||||
|
|
||||||
#. You can now edit your local working copy and run the tests again as necessary. Please follow PEP-8 for naming.
|
#. You can now edit your local working copy and run the tests again as necessary. Please follow PEP-8 for naming.
|
||||||
|
|
||||||
You can pass different options to ``tox``. For example, to run tests on Python 3.7 and pass options to pytest
|
You can pass different options to ``tox``. For example, to run tests on Python 3.9 and pass options to pytest
|
||||||
(e.g. enter pdb on failure) to pytest you can do::
|
(e.g. enter pdb on failure) to pytest you can do::
|
||||||
|
|
||||||
$ tox -e py37 -- --pdb
|
$ tox -e py39 -- --pdb
|
||||||
|
|
||||||
Or to only run tests in a particular test module on Python 3.7::
|
Or to only run tests in a particular test module on Python 3.9::
|
||||||
|
|
||||||
$ tox -e py37 -- testing/test_config.py
|
$ tox -e py39 -- testing/test_config.py
|
||||||
|
|
||||||
|
|
||||||
When committing, ``pre-commit`` will re-format the files if necessary.
|
When committing, ``pre-commit`` will re-format the files if necessary.
|
||||||
|
|
|
@ -100,7 +100,7 @@ Features
|
||||||
- Can run `unittest <https://docs.pytest.org/en/stable/how-to/unittest.html>`_ (or trial),
|
- Can run `unittest <https://docs.pytest.org/en/stable/how-to/unittest.html>`_ (or trial),
|
||||||
`nose <https://docs.pytest.org/en/stable/how-to/nose.html>`_ test suites out of the box
|
`nose <https://docs.pytest.org/en/stable/how-to/nose.html>`_ test suites out of the box
|
||||||
|
|
||||||
- Python 3.7+ or PyPy3
|
- Python 3.8+ or PyPy3
|
||||||
|
|
||||||
- Rich plugin architecture, with over 850+ `external plugins <https://docs.pytest.org/en/latest/reference/plugin_list.html>`_ and thriving community
|
- Rich plugin architecture, with over 850+ `external plugins <https://docs.pytest.org/en/latest/reference/plugin_list.html>`_ and thriving community
|
||||||
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
Fix bug where very long option names could cause pytest to break with ``OSError: [Errno 36] File name too long`` on some systems.
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Fixed but that fake intermediate modules generated by ``--import-mode=importlib`` would not include the
|
||||||
|
child modules as attributes of the parent modules.
|
|
@ -0,0 +1,2 @@
|
||||||
|
Fixed issue when using ``--import-mode=importlib`` together with ``--doctest-modules`` that caused modules
|
||||||
|
to be imported more than once, causing problems with modules that have import side effects.
|
|
@ -1 +0,0 @@
|
||||||
Terminal Reporting: Fixed bug when running in ``--tb=line`` mode where ``pytest.fail(pytrace=False)`` tests report ``None``.
|
|
|
@ -1,2 +0,0 @@
|
||||||
Added :func:`ExceptionInfo.from_exception() <pytest.ExceptionInfo.from_exception>`, a simpler way to create an :class:`~pytest.ExceptionInfo` from an exception.
|
|
||||||
This can replace :func:`ExceptionInfo.from_exc_info() <pytest.ExceptionInfo.from_exc_info()>` for most uses.
|
|
|
@ -1,5 +0,0 @@
|
||||||
When an exception traceback to be displayed is completely filtered out (by mechanisms such as ``__tracebackhide__``, internal frames, and similar), now only the exception string and the following message are shown:
|
|
||||||
|
|
||||||
"All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames.".
|
|
||||||
|
|
||||||
Previously, the last frame of the traceback was shown, even though it was hidden.
|
|
|
@ -1,3 +0,0 @@
|
||||||
Improved verbose output (``-vv``) of ``skip`` and ``xfail`` reasons by performing text wrapping while leaving a clear margin for progress output.
|
|
||||||
|
|
||||||
Added :func:`TerminalReporter.wrap_write() <pytest.TerminalReporter.wrap_write>` as a helper for that.
|
|
|
@ -1 +0,0 @@
|
||||||
:confval:`testpaths` is now honored to load root ``conftests``.
|
|
|
@ -0,0 +1 @@
|
||||||
|
Added a warning about modifying the root logger during tests when using ``caplog``.
|
|
@ -0,0 +1,2 @@
|
||||||
|
Dropped support for Python 3.7, which `reached end-of-life on 2023-06-27
|
||||||
|
<https://devguide.python.org/versions/>`__.
|
|
@ -0,0 +1,3 @@
|
||||||
|
Applying a mark to a fixture function now issues a warning: marks in fixtures never had any effect, but it is a common user error to apply a mark to a fixture (for example ``usefixtures``) and expect it to work.
|
||||||
|
|
||||||
|
This will become an error in the future.
|
|
@ -0,0 +1,22 @@
|
||||||
|
**PytestRemovedIn8Warning deprecation warnings are now errors by default.**
|
||||||
|
|
||||||
|
Following our plan to remove deprecated features with as little disruption as
|
||||||
|
possible, all warnings of type ``PytestRemovedIn8Warning`` now generate errors
|
||||||
|
instead of warning messages by default.
|
||||||
|
|
||||||
|
**The affected features will be effectively removed in pytest 8.1**, so please consult the
|
||||||
|
:ref:`deprecations` section in the docs for directions on how to update existing code.
|
||||||
|
|
||||||
|
In the pytest ``8.0.X`` series, it is possible to change the errors back into warnings as a
|
||||||
|
stopgap measure by adding this to your ``pytest.ini`` file:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[pytest]
|
||||||
|
filterwarnings =
|
||||||
|
ignore::pytest.PytestRemovedIn8Warning
|
||||||
|
|
||||||
|
But this will stop working when pytest ``8.1`` is released.
|
||||||
|
|
||||||
|
**If you have concerns** about the removal of a specific feature, please add a
|
||||||
|
comment to :issue:`7363`.
|
|
@ -0,0 +1,5 @@
|
||||||
|
Running `pytest pkg/__init__.py` now collects the `pkg/__init__.py` file (module) only.
|
||||||
|
Previously, it collected the entire `pkg` package, including other test files in the directory, but excluding tests in the `__init__.py` file itself
|
||||||
|
(unless :confval:`python_files` was changed to allow `__init__.py` file).
|
||||||
|
|
||||||
|
To collect the entire package, specify just the directory: `pytest pkg`.
|
|
@ -0,0 +1 @@
|
||||||
|
``pytest.warns`` and similar functions now capture warnings when an exception is raised inside a ``with`` block.
|
|
@ -0,0 +1,7 @@
|
||||||
|
:func:`pytest.warns <warns>` now re-emits unmatched warnings when the context
|
||||||
|
closes -- previously it would consume all warnings, hiding those that were not
|
||||||
|
matched by the function.
|
||||||
|
|
||||||
|
While this is a new feature, we decided to announce this as a breaking change
|
||||||
|
because many test suites are configured to error-out on warnings, and will
|
||||||
|
therefore fail on the newly-re-emitted warnings.
|
|
@ -6,6 +6,8 @@ Release announcements
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
|
||||||
|
release-7.4.0
|
||||||
|
release-7.3.2
|
||||||
release-7.3.1
|
release-7.3.1
|
||||||
release-7.3.0
|
release-7.3.0
|
||||||
release-7.2.2
|
release-7.2.2
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
pytest-7.3.2
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 7.3.2 has just been released to PyPI.
|
||||||
|
|
||||||
|
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||||
|
|
||||||
|
pip install --upgrade pytest
|
||||||
|
|
||||||
|
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||||
|
|
||||||
|
Thanks to all of the contributors to this release:
|
||||||
|
|
||||||
|
* Adam J. Stewart
|
||||||
|
* Alessio Izzo
|
||||||
|
* Bruno Oliveira
|
||||||
|
* Ran Benita
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -0,0 +1,49 @@
|
||||||
|
pytest-7.4.0
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
The pytest team is proud to announce the 7.4.0 release!
|
||||||
|
|
||||||
|
This release contains new features, improvements, and bug fixes,
|
||||||
|
the full list of changes is available in the changelog:
|
||||||
|
|
||||||
|
https://docs.pytest.org/en/stable/changelog.html
|
||||||
|
|
||||||
|
For complete documentation, please visit:
|
||||||
|
|
||||||
|
https://docs.pytest.org/en/stable/
|
||||||
|
|
||||||
|
As usual, you can upgrade from PyPI via:
|
||||||
|
|
||||||
|
pip install -U pytest
|
||||||
|
|
||||||
|
Thanks to all of the contributors to this release:
|
||||||
|
|
||||||
|
* Adam J. Stewart
|
||||||
|
* Alessio Izzo
|
||||||
|
* Alex
|
||||||
|
* Alex Lambson
|
||||||
|
* Brian Larsen
|
||||||
|
* Bruno Oliveira
|
||||||
|
* Bryan Ricker
|
||||||
|
* Chris Mahoney
|
||||||
|
* Facundo Batista
|
||||||
|
* Florian Bruhin
|
||||||
|
* Jarrett Keifer
|
||||||
|
* Kenny Y
|
||||||
|
* Miro Hrončok
|
||||||
|
* Ran Benita
|
||||||
|
* Roberto Aldera
|
||||||
|
* Ronny Pfannschmidt
|
||||||
|
* Sergey Kim
|
||||||
|
* Stefanie Molin
|
||||||
|
* Vijay Arora
|
||||||
|
* Ville Skyttä
|
||||||
|
* Zac Hatfield-Dodds
|
||||||
|
* bzoracler
|
||||||
|
* leeyueh
|
||||||
|
* nondescryptid
|
||||||
|
* theirix
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -87,8 +87,11 @@ Released pytest versions support all Python versions that are actively maintaine
|
||||||
============== ===================
|
============== ===================
|
||||||
pytest version min. Python version
|
pytest version min. Python version
|
||||||
============== ===================
|
============== ===================
|
||||||
|
8.0+ 3.8+
|
||||||
7.1+ 3.7+
|
7.1+ 3.7+
|
||||||
6.2 - 7.0 3.6+
|
6.2 - 7.0 3.6+
|
||||||
5.0 - 6.1 3.5+
|
5.0 - 6.1 3.5+
|
||||||
3.3 - 4.6 2.7, 3.4+
|
3.3 - 4.6 2.7, 3.4+
|
||||||
============== ===================
|
============== ===================
|
||||||
|
|
||||||
|
`Status of Python Versions <https://devguide.python.org/versions/>`__.
|
||||||
|
|
|
@ -22,7 +22,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
cachedir: .pytest_cache
|
cachedir: .pytest_cache
|
||||||
rootdir: /home/sweet/project
|
rootdir: /home/sweet/project
|
||||||
collected 0 items
|
collected 0 items
|
||||||
cache -- .../_pytest/cacheprovider.py:510
|
cache -- .../_pytest/cacheprovider.py:528
|
||||||
Return a cache object that can persist state between testing sessions.
|
Return a cache object that can persist state between testing sessions.
|
||||||
|
|
||||||
cache.get(key, default)
|
cache.get(key, default)
|
||||||
|
@ -119,7 +119,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
|
|
||||||
For more details: :ref:`doctest_namespace`.
|
For more details: :ref:`doctest_namespace`.
|
||||||
|
|
||||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1360
|
pytestconfig [session scope] -- .../_pytest/fixtures.py:1353
|
||||||
Session-scoped fixture that returns the session's :class:`pytest.Config`
|
Session-scoped fixture that returns the session's :class:`pytest.Config`
|
||||||
object.
|
object.
|
||||||
|
|
||||||
|
@ -196,7 +196,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
|
|
||||||
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html
|
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html
|
||||||
|
|
||||||
caplog -- .../_pytest/logging.py:498
|
caplog -- .../_pytest/logging.py:570
|
||||||
Access and control log capturing.
|
Access and control log capturing.
|
||||||
|
|
||||||
Captured logs are available through the following properties/methods::
|
Captured logs are available through the following properties/methods::
|
||||||
|
@ -207,7 +207,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
* caplog.record_tuples -> list of (logger_name, level, message) tuples
|
* caplog.record_tuples -> list of (logger_name, level, message) tuples
|
||||||
* caplog.clear() -> clear captured records and formatted log output string
|
* caplog.clear() -> clear captured records and formatted log output string
|
||||||
|
|
||||||
monkeypatch -- .../_pytest/monkeypatch.py:29
|
monkeypatch -- .../_pytest/monkeypatch.py:30
|
||||||
A convenient fixture for monkey-patching.
|
A convenient fixture for monkey-patching.
|
||||||
|
|
||||||
The fixture provides these methods to modify objects, dictionaries, or
|
The fixture provides these methods to modify objects, dictionaries, or
|
||||||
|
|
|
@ -28,6 +28,122 @@ with advance notice in the **Deprecations** section of releases.
|
||||||
|
|
||||||
.. towncrier release notes start
|
.. towncrier release notes start
|
||||||
|
|
||||||
|
pytest 7.4.0 (2023-06-23)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
|
||||||
|
- `#10901 <https://github.com/pytest-dev/pytest/issues/10901>`_: Added :func:`ExceptionInfo.from_exception() <pytest.ExceptionInfo.from_exception>`, a simpler way to create an :class:`~pytest.ExceptionInfo` from an exception.
|
||||||
|
This can replace :func:`ExceptionInfo.from_exc_info() <pytest.ExceptionInfo.from_exc_info()>` for most uses.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Improvements
|
||||||
|
------------
|
||||||
|
|
||||||
|
- `#10872 <https://github.com/pytest-dev/pytest/issues/10872>`_: Update test log report annotation to named tuple and fixed inconsistency in docs for :hook:`pytest_report_teststatus` hook.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10907 <https://github.com/pytest-dev/pytest/issues/10907>`_: When an exception traceback to be displayed is completely filtered out (by mechanisms such as ``__tracebackhide__``, internal frames, and similar), now only the exception string and the following message are shown:
|
||||||
|
|
||||||
|
"All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames.".
|
||||||
|
|
||||||
|
Previously, the last frame of the traceback was shown, even though it was hidden.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10940 <https://github.com/pytest-dev/pytest/issues/10940>`_: Improved verbose output (``-vv``) of ``skip`` and ``xfail`` reasons by performing text wrapping while leaving a clear margin for progress output.
|
||||||
|
|
||||||
|
Added ``TerminalReporter.wrap_write()`` as a helper for that.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10991 <https://github.com/pytest-dev/pytest/issues/10991>`_: Added handling of ``%f`` directive to print microseconds in log format options, such as ``log-date-format``.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11005 <https://github.com/pytest-dev/pytest/issues/11005>`_: Added the underlying exception to the cache provider's path creation and write warning messages.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11013 <https://github.com/pytest-dev/pytest/issues/11013>`_: Added warning when :confval:`testpaths` is set, but paths are not found by glob. In this case, pytest will fall back to searching from the current directory.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11043 <https://github.com/pytest-dev/pytest/issues/11043>`_: When `--confcutdir` is not specified, and there is no config file present, the conftest cutoff directory (`--confcutdir`) is now set to the :ref:`rootdir <rootdir>`.
|
||||||
|
Previously in such cases, `conftest.py` files would be probed all the way to the root directory of the filesystem.
|
||||||
|
If you are badly affected by this change, consider adding an empty config file to your desired cutoff directory, or explicitly set `--confcutdir`.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11081 <https://github.com/pytest-dev/pytest/issues/11081>`_: The :confval:`norecursedirs` check is now performed in a :hook:`pytest_ignore_collect` implementation, so plugins can affect it.
|
||||||
|
|
||||||
|
If after updating to this version you see that your `norecursedirs` setting is not being respected,
|
||||||
|
it means that a conftest or a plugin you use has a bad `pytest_ignore_collect` implementation.
|
||||||
|
Most likely, your hook returns `False` for paths it does not want to ignore,
|
||||||
|
which ends the processing and doesn't allow other plugins, including pytest itself, to ignore the path.
|
||||||
|
The fix is to return `None` instead of `False` for paths your hook doesn't want to ignore.
|
||||||
|
|
||||||
|
|
||||||
|
- `#8711 <https://github.com/pytest-dev/pytest/issues/8711>`_: :func:`caplog.set_level() <pytest.LogCaptureFixture.set_level>` and :func:`caplog.at_level() <pytest.LogCaptureFixture.at_level>`
|
||||||
|
will temporarily enable the requested ``level`` if ``level`` was disabled globally via
|
||||||
|
``logging.disable(LEVEL)``.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#10831 <https://github.com/pytest-dev/pytest/issues/10831>`_: Terminal Reporting: Fixed bug when running in ``--tb=line`` mode where ``pytest.fail(pytrace=False)`` tests report ``None``.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11068 <https://github.com/pytest-dev/pytest/issues/11068>`_: Fixed the ``--last-failed`` whole-file skipping functionality ("skipped N files") for :ref:`non-python test files <non-python tests>`.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11104 <https://github.com/pytest-dev/pytest/issues/11104>`_: Fixed a regression in pytest 7.3.2 which caused to :confval:`testpaths` to be considered for loading initial conftests,
|
||||||
|
even when it was not utilized (e.g. when explicit paths were given on the command line).
|
||||||
|
Now the ``testpaths`` are only considered when they are in use.
|
||||||
|
|
||||||
|
|
||||||
|
- `#1904 <https://github.com/pytest-dev/pytest/issues/1904>`_: Fixed traceback entries hidden with ``__tracebackhide__ = True`` still being shown for chained exceptions (parts after "... the above exception ..." message).
|
||||||
|
|
||||||
|
|
||||||
|
- `#7781 <https://github.com/pytest-dev/pytest/issues/7781>`_: Fix writing non-encodable text to log file when using ``--debug``.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Improved Documentation
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
- `#9146 <https://github.com/pytest-dev/pytest/issues/9146>`_: Improved documentation for :func:`caplog.set_level() <pytest.LogCaptureFixture.set_level>`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Trivial/Internal Changes
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
- `#11031 <https://github.com/pytest-dev/pytest/issues/11031>`_: Enhanced the CLI flag for ``-c`` to now include ``--config-file`` to make it clear that this flag applies to the usage of a custom config file.
|
||||||
|
|
||||||
|
|
||||||
|
pytest 7.3.2 (2023-06-10)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#10169 <https://github.com/pytest-dev/pytest/issues/10169>`_: Fix bug where very long option names could cause pytest to break with ``OSError: [Errno 36] File name too long`` on some systems.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10894 <https://github.com/pytest-dev/pytest/issues/10894>`_: Support for Python 3.12 (beta at the time of writing).
|
||||||
|
|
||||||
|
|
||||||
|
- `#10987 <https://github.com/pytest-dev/pytest/issues/10987>`_: :confval:`testpaths` is now honored to load root ``conftests``.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10999 <https://github.com/pytest-dev/pytest/issues/10999>`_: The `monkeypatch` `setitem`/`delitem` type annotations now allow `TypedDict` arguments.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11028 <https://github.com/pytest-dev/pytest/issues/11028>`_: Fixed bug in assertion rewriting where a variable assigned with the walrus operator could not be used later in a function call.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11054 <https://github.com/pytest-dev/pytest/issues/11054>`_: Fixed ``--last-failed``'s "(skipped N files)" functionality for files inside of packages (directories with `__init__.py` files).
|
||||||
|
|
||||||
|
|
||||||
pytest 7.3.1 (2023-04-14)
|
pytest 7.3.1 (2023-04-14)
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
|
@ -567,7 +683,7 @@ Breaking Changes
|
||||||
- `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: The :ref:`Node.reportinfo() <non-python tests>` function first return value type has been expanded from `py.path.local | str` to `os.PathLike[str] | str`.
|
- `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: The :ref:`Node.reportinfo() <non-python tests>` function first return value type has been expanded from `py.path.local | str` to `os.PathLike[str] | str`.
|
||||||
|
|
||||||
Most plugins which refer to `reportinfo()` only define it as part of a custom :class:`pytest.Item` implementation.
|
Most plugins which refer to `reportinfo()` only define it as part of a custom :class:`pytest.Item` implementation.
|
||||||
Since `py.path.local` is a `os.PathLike[str]`, these plugins are unaffacted.
|
Since `py.path.local` is an `os.PathLike[str]`, these plugins are unaffacted.
|
||||||
|
|
||||||
Plugins and users which call `reportinfo()`, use the first return value and interact with it as a `py.path.local`, would need to adjust by calling `py.path.local(fspath)`.
|
Plugins and users which call `reportinfo()`, use the first return value and interact with it as a `py.path.local`, would need to adjust by calling `py.path.local(fspath)`.
|
||||||
Although preferably, avoid the legacy `py.path.local` and use `pathlib.Path`, or use `item.location` or `item.path`, instead.
|
Although preferably, avoid the legacy `py.path.local` and use `pathlib.Path`, or use `item.location` or `item.path`, instead.
|
||||||
|
@ -4067,7 +4183,7 @@ Removals
|
||||||
See our :ref:`docs <calling fixtures directly deprecated>` on information on how to update your code.
|
See our :ref:`docs <calling fixtures directly deprecated>` on information on how to update your code.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than a existence check.
|
- :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than an existence check.
|
||||||
|
|
||||||
Use ``Node.get_closest_marker(name)`` as a replacement.
|
Use ``Node.get_closest_marker(name)`` as a replacement.
|
||||||
|
|
||||||
|
|
|
@ -15,12 +15,10 @@
|
||||||
#
|
#
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
import ast
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from typing import List
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from _pytest import __version__ as version
|
from _pytest import __version__ as version
|
||||||
|
@ -341,7 +339,7 @@ epub_copyright = "2013, holger krekel et alii"
|
||||||
# The scheme of the identifier. Typical schemes are ISBN or URL.
|
# The scheme of the identifier. Typical schemes are ISBN or URL.
|
||||||
# epub_scheme = ''
|
# epub_scheme = ''
|
||||||
|
|
||||||
# The unique identifier of the text. This can be a ISBN number
|
# The unique identifier of the text. This can be an ISBN number
|
||||||
# or the project homepage.
|
# or the project homepage.
|
||||||
# epub_identifier = ''
|
# epub_identifier = ''
|
||||||
|
|
||||||
|
@ -451,25 +449,6 @@ def setup(app: "sphinx.application.Sphinx") -> None:
|
||||||
|
|
||||||
configure_logging(app)
|
configure_logging(app)
|
||||||
|
|
||||||
# Make Sphinx mark classes with "final" when decorated with @final.
|
|
||||||
# We need this because we import final from pytest._compat, not from
|
|
||||||
# typing (for Python < 3.8 compat), so Sphinx doesn't detect it.
|
|
||||||
# To keep things simple we accept any `@final` decorator.
|
|
||||||
# Ref: https://github.com/pytest-dev/pytest/pull/7780
|
|
||||||
import sphinx.pycode.ast
|
|
||||||
import sphinx.pycode.parser
|
|
||||||
|
|
||||||
original_is_final = sphinx.pycode.parser.VariableCommentPicker.is_final
|
|
||||||
|
|
||||||
def patched_is_final(self, decorators: List[ast.expr]) -> bool:
|
|
||||||
if original_is_final(self, decorators):
|
|
||||||
return True
|
|
||||||
return any(
|
|
||||||
sphinx.pycode.ast.unparse(decorator) == "final" for decorator in decorators
|
|
||||||
)
|
|
||||||
|
|
||||||
sphinx.pycode.parser.VariableCommentPicker.is_final = patched_is_final
|
|
||||||
|
|
||||||
# legacypath.py monkey-patches pytest.Testdir in. Import the file so
|
# legacypath.py monkey-patches pytest.Testdir in. Import the file so
|
||||||
# that autodoc can discover references to it.
|
# that autodoc can discover references to it.
|
||||||
import _pytest.legacypath # noqa: F401
|
import _pytest.legacypath # noqa: F401
|
||||||
|
|
|
@ -380,6 +380,25 @@ conflicts (such as :class:`pytest.File` now taking ``path`` instead of
|
||||||
``fspath``, as :ref:`outlined above <node-ctor-fspath-deprecation>`), a
|
``fspath``, as :ref:`outlined above <node-ctor-fspath-deprecation>`), a
|
||||||
deprecation warning is now raised.
|
deprecation warning is now raised.
|
||||||
|
|
||||||
|
Applying a mark to a fixture function
|
||||||
|
-------------------------------------
|
||||||
|
|
||||||
|
.. deprecated:: 7.4
|
||||||
|
|
||||||
|
Applying a mark to a fixture function never had any effect, but it is a common user error.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("clean_database")
|
||||||
|
@pytest.fixture
|
||||||
|
def user() -> User:
|
||||||
|
...
|
||||||
|
|
||||||
|
Users expected in this case that the ``usefixtures`` mark would have its intended effect of using the ``clean_database`` fixture when ``user`` was invoked, when in fact it has no effect at all.
|
||||||
|
|
||||||
|
Now pytest will issue a warning when it encounters this problem, and will raise an error in the future versions.
|
||||||
|
|
||||||
|
|
||||||
Backward compatibilities in ``Parser.addoption``
|
Backward compatibilities in ``Parser.addoption``
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -467,12 +486,26 @@ The ``yield_fixture`` function/decorator
|
||||||
It has been so for a very long time, so can be search/replaced safely.
|
It has been so for a very long time, so can be search/replaced safely.
|
||||||
|
|
||||||
|
|
||||||
Removed Features
|
Removed Features and Breaking Changes
|
||||||
----------------
|
-------------------------------------
|
||||||
|
|
||||||
As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after
|
As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after
|
||||||
an appropriate period of deprecation has passed.
|
an appropriate period of deprecation has passed.
|
||||||
|
|
||||||
|
Some breaking changes which could not be deprecated are also listed.
|
||||||
|
|
||||||
|
|
||||||
|
Collecting ``__init__.py`` files no longer collects package
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. versionremoved:: 8.0
|
||||||
|
|
||||||
|
Running `pytest pkg/__init__.py` now collects the `pkg/__init__.py` file (module) only.
|
||||||
|
Previously, it collected the entire `pkg` package, including other test files in the directory, but excluding tests in the `__init__.py` file itself
|
||||||
|
(unless :confval:`python_files` was changed to allow `__init__.py` file).
|
||||||
|
|
||||||
|
To collect the entire package, specify just the directory: `pytest pkg`.
|
||||||
|
|
||||||
|
|
||||||
The ``pytest.collect`` module
|
The ``pytest.collect`` module
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
|
@ -38,6 +38,7 @@ class YamlItem(pytest.Item):
|
||||||
" no further details known at this point.",
|
" no further details known at this point.",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
return super().repr_failure(excinfo)
|
||||||
|
|
||||||
def reportinfo(self):
|
def reportinfo(self):
|
||||||
return self.path, 0, f"usecase: {self.name}"
|
return self.path, 0, f"usecase: {self.name}"
|
||||||
|
|
|
@ -691,7 +691,7 @@ Here is an example for making a ``db`` fixture available in a directory:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="package")
|
||||||
def db():
|
def db():
|
||||||
return DB()
|
return DB()
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ Get Started
|
||||||
Install ``pytest``
|
Install ``pytest``
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
|
|
||||||
``pytest`` requires: Python 3.7+ or PyPy3.
|
``pytest`` requires: Python 3.8+ or PyPy3.
|
||||||
|
|
||||||
1. Run the following command in your command line:
|
1. Run the following command in your command line:
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ Install ``pytest``
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ pytest --version
|
$ pytest --version
|
||||||
pytest 7.3.1
|
pytest 7.4.0
|
||||||
|
|
||||||
.. _`simpletest`:
|
.. _`simpletest`:
|
||||||
|
|
||||||
|
|
|
@ -1752,8 +1752,7 @@ into an ini-file:
|
||||||
def my_fixture_that_sadly_wont_use_my_other_fixture():
|
def my_fixture_that_sadly_wont_use_my_other_fixture():
|
||||||
...
|
...
|
||||||
|
|
||||||
Currently this will not generate any error or warning, but this is intended
|
This generates a deprecation warning, and will become an error in Pytest 8.
|
||||||
to be handled by :issue:`3664`.
|
|
||||||
|
|
||||||
.. _`override fixtures`:
|
.. _`override fixtures`:
|
||||||
|
|
||||||
|
|
|
@ -172,6 +172,13 @@ the records for the ``setup`` and ``call`` stages during teardown like so:
|
||||||
|
|
||||||
The full API is available at :class:`pytest.LogCaptureFixture`.
|
The full API is available at :class:`pytest.LogCaptureFixture`.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
The ``caplog`` fixture adds a handler to the root logger to capture logs. If the root logger is
|
||||||
|
modified during a test, for example with ``logging.config.dictConfig``, this handler may be
|
||||||
|
removed and cause no logs to be captured. To avoid this, ensure that any root logger configuration
|
||||||
|
only adds to the existing handlers.
|
||||||
|
|
||||||
|
|
||||||
.. _live_logs:
|
.. _live_logs:
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
.. sidebar:: Next Open Trainings
|
.. sidebar:: Next Open Trainings
|
||||||
|
|
||||||
|
- `pytest tips and tricks for a better testsuite <https://ep2023.europython.eu/session/pytest-tips-and-tricks-for-a-better-testsuite>`_, at `Europython 2023 <https://ep2023.europython.eu/>`_, July 18th (3h), Prague/Remote
|
||||||
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, March 5th to 7th 2024 (3 day in-depth training), Leipzig/Remote
|
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, March 5th to 7th 2024 (3 day in-depth training), Leipzig/Remote
|
||||||
|
|
||||||
Also see :doc:`previous talks and blogposts <talks>`.
|
Also see :doc:`previous talks and blogposts <talks>`.
|
||||||
|
@ -76,7 +77,7 @@ Features
|
||||||
|
|
||||||
- Can run :ref:`unittest <unittest>` (including trial) and :ref:`nose <noseintegration>` test suites out of the box
|
- Can run :ref:`unittest <unittest>` (including trial) and :ref:`nose <noseintegration>` test suites out of the box
|
||||||
|
|
||||||
- Python 3.7+ or PyPy 3
|
- Python 3.8+ or PyPy 3
|
||||||
|
|
||||||
- Rich plugin architecture, with over 800+ :ref:`external plugins <plugin-list>` and thriving community
|
- Rich plugin architecture, with over 800+ :ref:`external plugins <plugin-list>` and thriving community
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -956,6 +956,12 @@ TestReport
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
:inherited-members:
|
:inherited-members:
|
||||||
|
|
||||||
|
TestShortLogReport
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. autoclass:: pytest.TestShortLogReport()
|
||||||
|
:members:
|
||||||
|
|
||||||
_Result
|
_Result
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
|
@ -1049,11 +1055,11 @@ Environment variables that can be used to change pytest's behavior.
|
||||||
|
|
||||||
.. envvar:: CI
|
.. envvar:: CI
|
||||||
|
|
||||||
When set (regardless of value), pytest acknowledges that is running in a CI process. Alterative to ``BUILD_NUMBER`` variable.
|
When set (regardless of value), pytest acknowledges that is running in a CI process. Alternative to ``BUILD_NUMBER`` variable.
|
||||||
|
|
||||||
.. envvar:: BUILD_NUMBER
|
.. envvar:: BUILD_NUMBER
|
||||||
|
|
||||||
When set (regardless of value), pytest acknowledges that is running in a CI process. Alterative to CI variable.
|
When set (regardless of value), pytest acknowledges that is running in a CI process. Alternative to CI variable.
|
||||||
|
|
||||||
.. envvar:: PYTEST_ADDOPTS
|
.. envvar:: PYTEST_ADDOPTS
|
||||||
|
|
||||||
|
@ -1147,6 +1153,9 @@ Custom warnings generated in some situations such as improper usage or deprecate
|
||||||
.. autoclass:: pytest.PytestRemovedIn8Warning
|
.. autoclass:: pytest.PytestRemovedIn8Warning
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. autoclass:: pytest.PytestRemovedIn9Warning
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
.. autoclass:: pytest.PytestUnhandledCoroutineWarning
|
.. autoclass:: pytest.PytestUnhandledCoroutineWarning
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
@ -1697,6 +1706,11 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||||
[pytest]
|
[pytest]
|
||||||
pythonpath = src1 src2
|
pythonpath = src1 src2
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
``pythonpath`` does not affect some imports that happen very early,
|
||||||
|
most notably plugins loaded using the ``-p`` command line option.
|
||||||
|
|
||||||
|
|
||||||
.. confval:: required_plugins
|
.. confval:: required_plugins
|
||||||
|
|
||||||
|
@ -1912,8 +1926,9 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||||
--strict-markers Markers not registered in the `markers` section of
|
--strict-markers Markers not registered in the `markers` section of
|
||||||
the configuration file raise errors
|
the configuration file raise errors
|
||||||
--strict (Deprecated) alias to --strict-markers
|
--strict (Deprecated) alias to --strict-markers
|
||||||
-c file Load configuration from `file` instead of trying to
|
-c FILE, --config-file=FILE
|
||||||
locate one of the implicit configuration files
|
Load configuration from `FILE` instead of trying to
|
||||||
|
locate one of the implicit configuration files.
|
||||||
--continue-on-collection-errors
|
--continue-on-collection-errors
|
||||||
Force test execution even if collection errors occur
|
Force test execution even if collection errors occur
|
||||||
--rootdir=ROOTDIR Define root directory for tests. Can be relative
|
--rootdir=ROOTDIR Define root directory for tests. Can be relative
|
||||||
|
|
|
@ -7,7 +7,9 @@ def main():
|
||||||
Platform agnostic wrapper script for towncrier.
|
Platform agnostic wrapper script for towncrier.
|
||||||
Fixes the issue (#7251) where windows users are unable to natively run tox -e docs to build pytest docs.
|
Fixes the issue (#7251) where windows users are unable to natively run tox -e docs to build pytest docs.
|
||||||
"""
|
"""
|
||||||
with open("doc/en/_changelog_towncrier_draft.rst", "w") as draft_file:
|
with open(
|
||||||
|
"doc/en/_changelog_towncrier_draft.rst", "w", encoding="utf-8"
|
||||||
|
) as draft_file:
|
||||||
return call(("towncrier", "--draft"), stdout=draft_file)
|
return call(("towncrier", "--draft"), stdout=draft_file)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,9 @@ Plugin List
|
||||||
===========
|
===========
|
||||||
|
|
||||||
PyPI projects that match "pytest-\*" are considered plugins and are listed
|
PyPI projects that match "pytest-\*" are considered plugins and are listed
|
||||||
automatically. Packages classified as inactive are excluded.
|
automatically together with a manually-maintained list in `the source
|
||||||
|
code <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
|
||||||
|
Packages classified as inactive are excluded.
|
||||||
|
|
||||||
.. The following conditional uses a different format for this list when
|
.. The following conditional uses a different format for this list when
|
||||||
creating a PDF, because otherwise the table gets far too wide for the
|
creating a PDF, because otherwise the table gets far too wide for the
|
||||||
|
@ -33,6 +35,9 @@ DEVELOPMENT_STATUS_CLASSIFIERS = (
|
||||||
"Development Status :: 6 - Mature",
|
"Development Status :: 6 - Mature",
|
||||||
"Development Status :: 7 - Inactive",
|
"Development Status :: 7 - Inactive",
|
||||||
)
|
)
|
||||||
|
ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins
|
||||||
|
"logassert",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def escape_rst(text: str) -> str:
|
def escape_rst(text: str) -> str:
|
||||||
|
@ -52,18 +57,18 @@ def iter_plugins():
|
||||||
regex = r">([\d\w-]*)</a>"
|
regex = r">([\d\w-]*)</a>"
|
||||||
response = requests.get("https://pypi.org/simple")
|
response = requests.get("https://pypi.org/simple")
|
||||||
|
|
||||||
matches = list(
|
match_names = (match.groups()[0] for match in re.finditer(regex, response.text))
|
||||||
match
|
plugin_names = [
|
||||||
for match in re.finditer(regex, response.text)
|
name
|
||||||
if match.groups()[0].startswith("pytest-")
|
for name in match_names
|
||||||
)
|
if name.startswith("pytest-") or name in ADDITIONAL_PROJECTS
|
||||||
|
]
|
||||||
|
|
||||||
for match in tqdm(matches, smoothing=0):
|
for name in tqdm(plugin_names, smoothing=0):
|
||||||
name = match.groups()[0]
|
|
||||||
response = requests.get(f"https://pypi.org/pypi/{name}/json")
|
response = requests.get(f"https://pypi.org/pypi/{name}/json")
|
||||||
if response.status_code == 404:
|
if response.status_code == 404:
|
||||||
# Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple but
|
# Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple
|
||||||
# return 404 on the JSON API. Skip.
|
# but return 404 on the JSON API. Skip.
|
||||||
continue
|
continue
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
info = response.json()["info"]
|
info = response.json()["info"]
|
||||||
|
|
|
@ -6,7 +6,7 @@ long_description_content_type = text/x-rst
|
||||||
url = https://docs.pytest.org/en/latest/
|
url = https://docs.pytest.org/en/latest/
|
||||||
author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others
|
author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others
|
||||||
license = MIT
|
license = MIT
|
||||||
license_file = LICENSE
|
license_files = LICENSE
|
||||||
platforms = unix, linux, osx, cygwin, win32
|
platforms = unix, linux, osx, cygwin, win32
|
||||||
classifiers =
|
classifiers =
|
||||||
Development Status :: 6 - Mature
|
Development Status :: 6 - Mature
|
||||||
|
@ -17,11 +17,11 @@ classifiers =
|
||||||
Operating System :: POSIX
|
Operating System :: POSIX
|
||||||
Programming Language :: Python :: 3
|
Programming Language :: Python :: 3
|
||||||
Programming Language :: Python :: 3 :: Only
|
Programming Language :: Python :: 3 :: Only
|
||||||
Programming Language :: Python :: 3.7
|
|
||||||
Programming Language :: Python :: 3.8
|
Programming Language :: Python :: 3.8
|
||||||
Programming Language :: Python :: 3.9
|
Programming Language :: Python :: 3.9
|
||||||
Programming Language :: Python :: 3.10
|
Programming Language :: Python :: 3.10
|
||||||
Programming Language :: Python :: 3.11
|
Programming Language :: Python :: 3.11
|
||||||
|
Programming Language :: Python :: 3.12
|
||||||
Topic :: Software Development :: Libraries
|
Topic :: Software Development :: Libraries
|
||||||
Topic :: Software Development :: Testing
|
Topic :: Software Development :: Testing
|
||||||
Topic :: Utilities
|
Topic :: Utilities
|
||||||
|
@ -49,9 +49,8 @@ install_requires =
|
||||||
pluggy>=0.12,<2.0
|
pluggy>=0.12,<2.0
|
||||||
colorama;sys_platform=="win32"
|
colorama;sys_platform=="win32"
|
||||||
exceptiongroup>=1.0.0rc8;python_version<"3.11"
|
exceptiongroup>=1.0.0rc8;python_version<"3.11"
|
||||||
importlib-metadata>=0.12;python_version<"3.8"
|
|
||||||
tomli>=1.0.0;python_version<"3.11"
|
tomli>=1.0.0;python_version<"3.11"
|
||||||
python_requires = >=3.7
|
python_requires = >=3.8
|
||||||
package_dir =
|
package_dir =
|
||||||
=src
|
=src
|
||||||
setup_requires =
|
setup_requires =
|
||||||
|
@ -73,6 +72,7 @@ testing =
|
||||||
nose
|
nose
|
||||||
pygments>=2.7.2
|
pygments>=2.7.2
|
||||||
requests
|
requests
|
||||||
|
setuptools
|
||||||
xmlschema
|
xmlschema
|
||||||
|
|
||||||
[options.package_data]
|
[options.package_data]
|
||||||
|
|
|
@ -17,21 +17,23 @@ from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import ClassVar
|
from typing import ClassVar
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
from typing import Final
|
||||||
|
from typing import final
|
||||||
from typing import Generic
|
from typing import Generic
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from typing import Literal
|
||||||
from typing import Mapping
|
from typing import Mapping
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import overload
|
from typing import overload
|
||||||
from typing import Pattern
|
from typing import Pattern
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from typing import Set
|
from typing import Set
|
||||||
|
from typing import SupportsIndex
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
from typing import Type
|
from typing import Type
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
from typing import Union
|
from typing import Union
|
||||||
from weakref import ref
|
|
||||||
|
|
||||||
import pluggy
|
import pluggy
|
||||||
|
|
||||||
|
@ -43,22 +45,16 @@ from _pytest._code.source import Source
|
||||||
from _pytest._io import TerminalWriter
|
from _pytest._io import TerminalWriter
|
||||||
from _pytest._io.saferepr import safeformat
|
from _pytest._io.saferepr import safeformat
|
||||||
from _pytest._io.saferepr import saferepr
|
from _pytest._io.saferepr import saferepr
|
||||||
from _pytest.compat import final
|
|
||||||
from _pytest.compat import get_real_func
|
from _pytest.compat import get_real_func
|
||||||
from _pytest.deprecated import check_ispytest
|
from _pytest.deprecated import check_ispytest
|
||||||
from _pytest.pathlib import absolutepath
|
from _pytest.pathlib import absolutepath
|
||||||
from _pytest.pathlib import bestrelpath
|
from _pytest.pathlib import bestrelpath
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from typing_extensions import Literal
|
|
||||||
from typing_extensions import SupportsIndex
|
|
||||||
from weakref import ReferenceType
|
|
||||||
|
|
||||||
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
|
|
||||||
|
|
||||||
if sys.version_info[:2] < (3, 11):
|
if sys.version_info[:2] < (3, 11):
|
||||||
from exceptiongroup import BaseExceptionGroup
|
from exceptiongroup import BaseExceptionGroup
|
||||||
|
|
||||||
|
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
|
||||||
|
|
||||||
|
|
||||||
class Code:
|
class Code:
|
||||||
"""Wrapper around Python code objects."""
|
"""Wrapper around Python code objects."""
|
||||||
|
@ -194,25 +190,25 @@ class Frame:
|
||||||
class TracebackEntry:
|
class TracebackEntry:
|
||||||
"""A single entry in a Traceback."""
|
"""A single entry in a Traceback."""
|
||||||
|
|
||||||
__slots__ = ("_rawentry", "_excinfo", "_repr_style")
|
__slots__ = ("_rawentry", "_repr_style")
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
rawentry: TracebackType,
|
rawentry: TracebackType,
|
||||||
excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,
|
repr_style: Optional['Literal["short", "long"]'] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._rawentry = rawentry
|
self._rawentry: "Final" = rawentry
|
||||||
self._excinfo = excinfo
|
self._repr_style: "Final" = repr_style
|
||||||
self._repr_style: Optional['Literal["short", "long"]'] = None
|
|
||||||
|
def with_repr_style(
|
||||||
|
self, repr_style: Optional['Literal["short", "long"]']
|
||||||
|
) -> "TracebackEntry":
|
||||||
|
return TracebackEntry(self._rawentry, repr_style)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def lineno(self) -> int:
|
def lineno(self) -> int:
|
||||||
return self._rawentry.tb_lineno - 1
|
return self._rawentry.tb_lineno - 1
|
||||||
|
|
||||||
def set_repr_style(self, mode: "Literal['short', 'long']") -> None:
|
|
||||||
assert mode in ("short", "long")
|
|
||||||
self._repr_style = mode
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def frame(self) -> Frame:
|
def frame(self) -> Frame:
|
||||||
return Frame(self._rawentry.tb_frame)
|
return Frame(self._rawentry.tb_frame)
|
||||||
|
@ -272,7 +268,7 @@ class TracebackEntry:
|
||||||
|
|
||||||
source = property(getsource)
|
source = property(getsource)
|
||||||
|
|
||||||
def ishidden(self) -> bool:
|
def ishidden(self, excinfo: Optional["ExceptionInfo[BaseException]"]) -> bool:
|
||||||
"""Return True if the current frame has a var __tracebackhide__
|
"""Return True if the current frame has a var __tracebackhide__
|
||||||
resolving to True.
|
resolving to True.
|
||||||
|
|
||||||
|
@ -296,7 +292,7 @@ class TracebackEntry:
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
if tbh and callable(tbh):
|
if tbh and callable(tbh):
|
||||||
return tbh(None if self._excinfo is None else self._excinfo())
|
return tbh(excinfo)
|
||||||
return tbh
|
return tbh
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
|
@ -329,16 +325,14 @@ class Traceback(List[TracebackEntry]):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
tb: Union[TracebackType, Iterable[TracebackEntry]],
|
tb: Union[TracebackType, Iterable[TracebackEntry]],
|
||||||
excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize from given python traceback object and ExceptionInfo."""
|
"""Initialize from given python traceback object and ExceptionInfo."""
|
||||||
self._excinfo = excinfo
|
|
||||||
if isinstance(tb, TracebackType):
|
if isinstance(tb, TracebackType):
|
||||||
|
|
||||||
def f(cur: TracebackType) -> Iterable[TracebackEntry]:
|
def f(cur: TracebackType) -> Iterable[TracebackEntry]:
|
||||||
cur_: Optional[TracebackType] = cur
|
cur_: Optional[TracebackType] = cur
|
||||||
while cur_ is not None:
|
while cur_ is not None:
|
||||||
yield TracebackEntry(cur_, excinfo=excinfo)
|
yield TracebackEntry(cur_)
|
||||||
cur_ = cur_.tb_next
|
cur_ = cur_.tb_next
|
||||||
|
|
||||||
super().__init__(f(tb))
|
super().__init__(f(tb))
|
||||||
|
@ -378,7 +372,7 @@ class Traceback(List[TracebackEntry]):
|
||||||
continue
|
continue
|
||||||
if firstlineno is not None and x.frame.code.firstlineno != firstlineno:
|
if firstlineno is not None and x.frame.code.firstlineno != firstlineno:
|
||||||
continue
|
continue
|
||||||
return Traceback(x._rawentry, self._excinfo)
|
return Traceback(x._rawentry)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
|
@ -398,27 +392,27 @@ class Traceback(List[TracebackEntry]):
|
||||||
return super().__getitem__(key)
|
return super().__getitem__(key)
|
||||||
|
|
||||||
def filter(
|
def filter(
|
||||||
self, fn: Callable[[TracebackEntry], bool] = lambda x: not x.ishidden()
|
self,
|
||||||
|
# TODO(py38): change to positional only.
|
||||||
|
_excinfo_or_fn: Union[
|
||||||
|
"ExceptionInfo[BaseException]",
|
||||||
|
Callable[[TracebackEntry], bool],
|
||||||
|
],
|
||||||
) -> "Traceback":
|
) -> "Traceback":
|
||||||
"""Return a Traceback instance with certain items removed
|
"""Return a Traceback instance with certain items removed.
|
||||||
|
|
||||||
fn is a function that gets a single argument, a TracebackEntry
|
If the filter is an `ExceptionInfo`, removes all the ``TracebackEntry``s
|
||||||
instance, and should return True when the item should be added
|
which are hidden (see ishidden() above).
|
||||||
to the Traceback, False when not.
|
|
||||||
|
|
||||||
By default this removes all the TracebackEntries which are hidden
|
Otherwise, the filter is a function that gets a single argument, a
|
||||||
(see ishidden() above).
|
``TracebackEntry`` instance, and should return True when the item should
|
||||||
|
be added to the ``Traceback``, False when not.
|
||||||
"""
|
"""
|
||||||
return Traceback(filter(fn, self), self._excinfo)
|
if isinstance(_excinfo_or_fn, ExceptionInfo):
|
||||||
|
fn = lambda x: not x.ishidden(_excinfo_or_fn) # noqa: E731
|
||||||
def getcrashentry(self) -> Optional[TracebackEntry]:
|
else:
|
||||||
"""Return last non-hidden traceback entry that lead to the exception of
|
fn = _excinfo_or_fn
|
||||||
a traceback, or None if all hidden."""
|
return Traceback(filter(fn, self))
|
||||||
for i in range(-1, -len(self) - 1, -1):
|
|
||||||
entry = self[i]
|
|
||||||
if not entry.ishidden():
|
|
||||||
return entry
|
|
||||||
return None
|
|
||||||
|
|
||||||
def recursionindex(self) -> Optional[int]:
|
def recursionindex(self) -> Optional[int]:
|
||||||
"""Return the index of the frame/TracebackEntry where recursion originates if
|
"""Return the index of the frame/TracebackEntry where recursion originates if
|
||||||
|
@ -583,7 +577,7 @@ class ExceptionInfo(Generic[E]):
|
||||||
def traceback(self) -> Traceback:
|
def traceback(self) -> Traceback:
|
||||||
"""The traceback."""
|
"""The traceback."""
|
||||||
if self._traceback is None:
|
if self._traceback is None:
|
||||||
self._traceback = Traceback(self.tb, excinfo=ref(self))
|
self._traceback = Traceback(self.tb)
|
||||||
return self._traceback
|
return self._traceback
|
||||||
|
|
||||||
@traceback.setter
|
@traceback.setter
|
||||||
|
@ -623,19 +617,24 @@ class ExceptionInfo(Generic[E]):
|
||||||
return isinstance(self.value, exc)
|
return isinstance(self.value, exc)
|
||||||
|
|
||||||
def _getreprcrash(self) -> Optional["ReprFileLocation"]:
|
def _getreprcrash(self) -> Optional["ReprFileLocation"]:
|
||||||
exconly = self.exconly(tryshort=True)
|
# Find last non-hidden traceback entry that led to the exception of the
|
||||||
entry = self.traceback.getcrashentry()
|
# traceback, or None if all hidden.
|
||||||
if entry is None:
|
for i in range(-1, -len(self.traceback) - 1, -1):
|
||||||
return None
|
entry = self.traceback[i]
|
||||||
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
|
if not entry.ishidden(self):
|
||||||
return ReprFileLocation(path, lineno + 1, exconly)
|
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
|
||||||
|
exconly = self.exconly(tryshort=True)
|
||||||
|
return ReprFileLocation(path, lineno + 1, exconly)
|
||||||
|
return None
|
||||||
|
|
||||||
def getrepr(
|
def getrepr(
|
||||||
self,
|
self,
|
||||||
showlocals: bool = False,
|
showlocals: bool = False,
|
||||||
style: "_TracebackStyle" = "long",
|
style: _TracebackStyle = "long",
|
||||||
abspath: bool = False,
|
abspath: bool = False,
|
||||||
tbfilter: bool = True,
|
tbfilter: Union[
|
||||||
|
bool, Callable[["ExceptionInfo[BaseException]"], Traceback]
|
||||||
|
] = True,
|
||||||
funcargs: bool = False,
|
funcargs: bool = False,
|
||||||
truncate_locals: bool = True,
|
truncate_locals: bool = True,
|
||||||
chain: bool = True,
|
chain: bool = True,
|
||||||
|
@ -652,9 +651,15 @@ class ExceptionInfo(Generic[E]):
|
||||||
:param bool abspath:
|
:param bool abspath:
|
||||||
If paths should be changed to absolute or left unchanged.
|
If paths should be changed to absolute or left unchanged.
|
||||||
|
|
||||||
:param bool tbfilter:
|
:param tbfilter:
|
||||||
Hide entries that contain a local variable ``__tracebackhide__==True``.
|
A filter for traceback entries.
|
||||||
Ignored if ``style=="native"``.
|
|
||||||
|
* If false, don't hide any entries.
|
||||||
|
* If true, hide internal entries and entries that contain a local
|
||||||
|
variable ``__tracebackhide__ = True``.
|
||||||
|
* If a callable, delegates the filtering to the callable.
|
||||||
|
|
||||||
|
Ignored if ``style`` is ``"native"``.
|
||||||
|
|
||||||
:param bool funcargs:
|
:param bool funcargs:
|
||||||
Show fixtures ("funcargs" for legacy purposes) per traceback entry.
|
Show fixtures ("funcargs" for legacy purposes) per traceback entry.
|
||||||
|
@ -717,9 +722,9 @@ class FormattedExcinfo:
|
||||||
fail_marker: ClassVar = "E"
|
fail_marker: ClassVar = "E"
|
||||||
|
|
||||||
showlocals: bool = False
|
showlocals: bool = False
|
||||||
style: "_TracebackStyle" = "long"
|
style: _TracebackStyle = "long"
|
||||||
abspath: bool = True
|
abspath: bool = True
|
||||||
tbfilter: bool = True
|
tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True
|
||||||
funcargs: bool = False
|
funcargs: bool = False
|
||||||
truncate_locals: bool = True
|
truncate_locals: bool = True
|
||||||
chain: bool = True
|
chain: bool = True
|
||||||
|
@ -881,8 +886,10 @@ class FormattedExcinfo:
|
||||||
|
|
||||||
def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback":
|
def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback":
|
||||||
traceback = excinfo.traceback
|
traceback = excinfo.traceback
|
||||||
if self.tbfilter:
|
if callable(self.tbfilter):
|
||||||
traceback = traceback.filter()
|
traceback = self.tbfilter(excinfo)
|
||||||
|
elif self.tbfilter:
|
||||||
|
traceback = traceback.filter(excinfo)
|
||||||
|
|
||||||
if isinstance(excinfo.value, RecursionError):
|
if isinstance(excinfo.value, RecursionError):
|
||||||
traceback, extraline = self._truncate_recursive_traceback(traceback)
|
traceback, extraline = self._truncate_recursive_traceback(traceback)
|
||||||
|
@ -1080,7 +1087,7 @@ class ReprExceptionInfo(ExceptionRepr):
|
||||||
class ReprTraceback(TerminalRepr):
|
class ReprTraceback(TerminalRepr):
|
||||||
reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]]
|
reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]]
|
||||||
extraline: Optional[str]
|
extraline: Optional[str]
|
||||||
style: "_TracebackStyle"
|
style: _TracebackStyle
|
||||||
|
|
||||||
entrysep: ClassVar = "_ "
|
entrysep: ClassVar = "_ "
|
||||||
|
|
||||||
|
@ -1114,7 +1121,7 @@ class ReprTracebackNative(ReprTraceback):
|
||||||
class ReprEntryNative(TerminalRepr):
|
class ReprEntryNative(TerminalRepr):
|
||||||
lines: Sequence[str]
|
lines: Sequence[str]
|
||||||
|
|
||||||
style: ClassVar["_TracebackStyle"] = "native"
|
style: ClassVar[_TracebackStyle] = "native"
|
||||||
|
|
||||||
def toterminal(self, tw: TerminalWriter) -> None:
|
def toterminal(self, tw: TerminalWriter) -> None:
|
||||||
tw.write("".join(self.lines))
|
tw.write("".join(self.lines))
|
||||||
|
@ -1126,7 +1133,7 @@ class ReprEntry(TerminalRepr):
|
||||||
reprfuncargs: Optional["ReprFuncArgs"]
|
reprfuncargs: Optional["ReprFuncArgs"]
|
||||||
reprlocals: Optional["ReprLocals"]
|
reprlocals: Optional["ReprLocals"]
|
||||||
reprfileloc: Optional["ReprFileLocation"]
|
reprfileloc: Optional["ReprFileLocation"]
|
||||||
style: "_TracebackStyle"
|
style: _TracebackStyle
|
||||||
|
|
||||||
def _write_entry_lines(self, tw: TerminalWriter) -> None:
|
def _write_entry_lines(self, tw: TerminalWriter) -> None:
|
||||||
"""Write the source code portions of a list of traceback entries with syntax highlighting.
|
"""Write the source code portions of a list of traceback entries with syntax highlighting.
|
||||||
|
|
|
@ -149,8 +149,7 @@ def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[i
|
||||||
values: List[int] = []
|
values: List[int] = []
|
||||||
for x in ast.walk(node):
|
for x in ast.walk(node):
|
||||||
if isinstance(x, (ast.stmt, ast.ExceptHandler)):
|
if isinstance(x, (ast.stmt, ast.ExceptHandler)):
|
||||||
# Before Python 3.8, the lineno of a decorated class or function pointed at the decorator.
|
# The lineno points to the class/def, so need to include the decorators.
|
||||||
# Since Python 3.8, the lineno points to the class/def, so need to include the decorators.
|
|
||||||
if isinstance(x, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
|
if isinstance(x, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||||
for d in x.decorator_list:
|
for d in x.decorator_list:
|
||||||
values.append(d.lineno - 1)
|
values.append(d.lineno - 1)
|
||||||
|
|
|
@ -2,12 +2,12 @@
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
from typing import final
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from typing import TextIO
|
from typing import TextIO
|
||||||
|
|
||||||
from .wcwidth import wcswidth
|
from .wcwidth import wcswidth
|
||||||
from _pytest.compat import final
|
|
||||||
|
|
||||||
|
|
||||||
# This code was initially copied from py 1.8.1, file _io/terminalwriter.py.
|
# This code was initially copied from py 1.8.1, file _io/terminalwriter.py.
|
||||||
|
|
|
@ -44,11 +44,6 @@ from _pytest.stash import StashKey
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from _pytest.assertion import AssertionState
|
from _pytest.assertion import AssertionState
|
||||||
|
|
||||||
if sys.version_info >= (3, 8):
|
|
||||||
namedExpr = ast.NamedExpr
|
|
||||||
else:
|
|
||||||
namedExpr = ast.Expr
|
|
||||||
|
|
||||||
|
|
||||||
assertstate_key = StashKey["AssertionState"]()
|
assertstate_key = StashKey["AssertionState"]()
|
||||||
|
|
||||||
|
@ -680,9 +675,9 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
if (
|
if (
|
||||||
expect_docstring
|
expect_docstring
|
||||||
and isinstance(item, ast.Expr)
|
and isinstance(item, ast.Expr)
|
||||||
and isinstance(item.value, ast.Str)
|
and isinstance(item.value, ast.Constant)
|
||||||
):
|
):
|
||||||
doc = item.value.s
|
doc = item.value.value
|
||||||
if self.is_rewrite_disabled(doc):
|
if self.is_rewrite_disabled(doc):
|
||||||
return
|
return
|
||||||
expect_docstring = False
|
expect_docstring = False
|
||||||
|
@ -814,7 +809,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
current = self.stack.pop()
|
current = self.stack.pop()
|
||||||
if self.stack:
|
if self.stack:
|
||||||
self.explanation_specifiers = self.stack[-1]
|
self.explanation_specifiers = self.stack[-1]
|
||||||
keys = [ast.Str(key) for key in current.keys()]
|
keys = [ast.Constant(key) for key in current.keys()]
|
||||||
format_dict = ast.Dict(keys, list(current.values()))
|
format_dict = ast.Dict(keys, list(current.values()))
|
||||||
form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
|
form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
|
||||||
name = "@py_format" + str(next(self.variable_counter))
|
name = "@py_format" + str(next(self.variable_counter))
|
||||||
|
@ -868,16 +863,16 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
negation = ast.UnaryOp(ast.Not(), top_condition)
|
negation = ast.UnaryOp(ast.Not(), top_condition)
|
||||||
|
|
||||||
if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
|
if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
|
||||||
msg = self.pop_format_context(ast.Str(explanation))
|
msg = self.pop_format_context(ast.Constant(explanation))
|
||||||
|
|
||||||
# Failed
|
# Failed
|
||||||
if assert_.msg:
|
if assert_.msg:
|
||||||
assertmsg = self.helper("_format_assertmsg", assert_.msg)
|
assertmsg = self.helper("_format_assertmsg", assert_.msg)
|
||||||
gluestr = "\n>assert "
|
gluestr = "\n>assert "
|
||||||
else:
|
else:
|
||||||
assertmsg = ast.Str("")
|
assertmsg = ast.Constant("")
|
||||||
gluestr = "assert "
|
gluestr = "assert "
|
||||||
err_explanation = ast.BinOp(ast.Str(gluestr), ast.Add(), msg)
|
err_explanation = ast.BinOp(ast.Constant(gluestr), ast.Add(), msg)
|
||||||
err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation)
|
err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation)
|
||||||
err_name = ast.Name("AssertionError", ast.Load())
|
err_name = ast.Name("AssertionError", ast.Load())
|
||||||
fmt = self.helper("_format_explanation", err_msg)
|
fmt = self.helper("_format_explanation", err_msg)
|
||||||
|
@ -893,8 +888,8 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
hook_call_pass = ast.Expr(
|
hook_call_pass = ast.Expr(
|
||||||
self.helper(
|
self.helper(
|
||||||
"_call_assertion_pass",
|
"_call_assertion_pass",
|
||||||
ast.Num(assert_.lineno),
|
ast.Constant(assert_.lineno),
|
||||||
ast.Str(orig),
|
ast.Constant(orig),
|
||||||
fmt_pass,
|
fmt_pass,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -913,7 +908,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
variables = [
|
variables = [
|
||||||
ast.Name(name, ast.Store()) for name in self.format_variables
|
ast.Name(name, ast.Store()) for name in self.format_variables
|
||||||
]
|
]
|
||||||
clear_format = ast.Assign(variables, ast.NameConstant(None))
|
clear_format = ast.Assign(variables, ast.Constant(None))
|
||||||
self.statements.append(clear_format)
|
self.statements.append(clear_format)
|
||||||
|
|
||||||
else: # Original assertion rewriting
|
else: # Original assertion rewriting
|
||||||
|
@ -924,9 +919,9 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
assertmsg = self.helper("_format_assertmsg", assert_.msg)
|
assertmsg = self.helper("_format_assertmsg", assert_.msg)
|
||||||
explanation = "\n>assert " + explanation
|
explanation = "\n>assert " + explanation
|
||||||
else:
|
else:
|
||||||
assertmsg = ast.Str("")
|
assertmsg = ast.Constant("")
|
||||||
explanation = "assert " + explanation
|
explanation = "assert " + explanation
|
||||||
template = ast.BinOp(assertmsg, ast.Add(), ast.Str(explanation))
|
template = ast.BinOp(assertmsg, ast.Add(), ast.Constant(explanation))
|
||||||
msg = self.pop_format_context(template)
|
msg = self.pop_format_context(template)
|
||||||
fmt = self.helper("_format_explanation", msg)
|
fmt = self.helper("_format_explanation", msg)
|
||||||
err_name = ast.Name("AssertionError", ast.Load())
|
err_name = ast.Name("AssertionError", ast.Load())
|
||||||
|
@ -938,7 +933,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
# Clear temporary variables by setting them to None.
|
# Clear temporary variables by setting them to None.
|
||||||
if self.variables:
|
if self.variables:
|
||||||
variables = [ast.Name(name, ast.Store()) for name in self.variables]
|
variables = [ast.Name(name, ast.Store()) for name in self.variables]
|
||||||
clear = ast.Assign(variables, ast.NameConstant(None))
|
clear = ast.Assign(variables, ast.Constant(None))
|
||||||
self.statements.append(clear)
|
self.statements.append(clear)
|
||||||
# Fix locations (line numbers/column offsets).
|
# Fix locations (line numbers/column offsets).
|
||||||
for stmt in self.statements:
|
for stmt in self.statements:
|
||||||
|
@ -946,26 +941,26 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
ast.copy_location(node, assert_)
|
ast.copy_location(node, assert_)
|
||||||
return self.statements
|
return self.statements
|
||||||
|
|
||||||
def visit_NamedExpr(self, name: namedExpr) -> Tuple[namedExpr, str]:
|
def visit_NamedExpr(self, name: ast.NamedExpr) -> Tuple[ast.NamedExpr, str]:
|
||||||
# This method handles the 'walrus operator' repr of the target
|
# This method handles the 'walrus operator' repr of the target
|
||||||
# name if it's a local variable or _should_repr_global_name()
|
# name if it's a local variable or _should_repr_global_name()
|
||||||
# thinks it's acceptable.
|
# thinks it's acceptable.
|
||||||
locs = ast.Call(self.builtin("locals"), [], [])
|
locs = ast.Call(self.builtin("locals"), [], [])
|
||||||
target_id = name.target.id # type: ignore[attr-defined]
|
target_id = name.target.id # type: ignore[attr-defined]
|
||||||
inlocs = ast.Compare(ast.Str(target_id), [ast.In()], [locs])
|
inlocs = ast.Compare(ast.Constant(target_id), [ast.In()], [locs])
|
||||||
dorepr = self.helper("_should_repr_global_name", name)
|
dorepr = self.helper("_should_repr_global_name", name)
|
||||||
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
|
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
|
||||||
expr = ast.IfExp(test, self.display(name), ast.Str(target_id))
|
expr = ast.IfExp(test, self.display(name), ast.Constant(target_id))
|
||||||
return name, self.explanation_param(expr)
|
return name, self.explanation_param(expr)
|
||||||
|
|
||||||
def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]:
|
def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]:
|
||||||
# Display the repr of the name if it's a local variable or
|
# Display the repr of the name if it's a local variable or
|
||||||
# _should_repr_global_name() thinks it's acceptable.
|
# _should_repr_global_name() thinks it's acceptable.
|
||||||
locs = ast.Call(self.builtin("locals"), [], [])
|
locs = ast.Call(self.builtin("locals"), [], [])
|
||||||
inlocs = ast.Compare(ast.Str(name.id), [ast.In()], [locs])
|
inlocs = ast.Compare(ast.Constant(name.id), [ast.In()], [locs])
|
||||||
dorepr = self.helper("_should_repr_global_name", name)
|
dorepr = self.helper("_should_repr_global_name", name)
|
||||||
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
|
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
|
||||||
expr = ast.IfExp(test, self.display(name), ast.Str(name.id))
|
expr = ast.IfExp(test, self.display(name), ast.Constant(name.id))
|
||||||
return name, self.explanation_param(expr)
|
return name, self.explanation_param(expr)
|
||||||
|
|
||||||
def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
|
def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
|
||||||
|
@ -984,10 +979,10 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
# cond is set in a prior loop iteration below
|
# cond is set in a prior loop iteration below
|
||||||
self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa
|
self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa
|
||||||
self.expl_stmts = fail_inner
|
self.expl_stmts = fail_inner
|
||||||
# Check if the left operand is a namedExpr and the value has already been visited
|
# Check if the left operand is a ast.NamedExpr and the value has already been visited
|
||||||
if (
|
if (
|
||||||
isinstance(v, ast.Compare)
|
isinstance(v, ast.Compare)
|
||||||
and isinstance(v.left, namedExpr)
|
and isinstance(v.left, ast.NamedExpr)
|
||||||
and v.left.target.id
|
and v.left.target.id
|
||||||
in [
|
in [
|
||||||
ast_expr.id
|
ast_expr.id
|
||||||
|
@ -996,12 +991,14 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
]
|
]
|
||||||
):
|
):
|
||||||
pytest_temp = self.variable()
|
pytest_temp = self.variable()
|
||||||
self.variables_overwrite[v.left.target.id] = pytest_temp
|
self.variables_overwrite[
|
||||||
|
v.left.target.id
|
||||||
|
] = v.left # type:ignore[assignment]
|
||||||
v.left.target.id = pytest_temp
|
v.left.target.id = pytest_temp
|
||||||
self.push_format_context()
|
self.push_format_context()
|
||||||
res, expl = self.visit(v)
|
res, expl = self.visit(v)
|
||||||
body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
|
body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
|
||||||
expl_format = self.pop_format_context(ast.Str(expl))
|
expl_format = self.pop_format_context(ast.Constant(expl))
|
||||||
call = ast.Call(app, [expl_format], [])
|
call = ast.Call(app, [expl_format], [])
|
||||||
self.expl_stmts.append(ast.Expr(call))
|
self.expl_stmts.append(ast.Expr(call))
|
||||||
if i < levels:
|
if i < levels:
|
||||||
|
@ -1013,7 +1010,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
self.statements = body = inner
|
self.statements = body = inner
|
||||||
self.statements = save
|
self.statements = save
|
||||||
self.expl_stmts = fail_save
|
self.expl_stmts = fail_save
|
||||||
expl_template = self.helper("_format_boolop", expl_list, ast.Num(is_or))
|
expl_template = self.helper("_format_boolop", expl_list, ast.Constant(is_or))
|
||||||
expl = self.pop_format_context(expl_template)
|
expl = self.pop_format_context(expl_template)
|
||||||
return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
|
return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
|
||||||
|
|
||||||
|
@ -1037,10 +1034,19 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
new_args = []
|
new_args = []
|
||||||
new_kwargs = []
|
new_kwargs = []
|
||||||
for arg in call.args:
|
for arg in call.args:
|
||||||
|
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite:
|
||||||
|
arg = self.variables_overwrite[arg.id] # type:ignore[assignment]
|
||||||
res, expl = self.visit(arg)
|
res, expl = self.visit(arg)
|
||||||
arg_expls.append(expl)
|
arg_expls.append(expl)
|
||||||
new_args.append(res)
|
new_args.append(res)
|
||||||
for keyword in call.keywords:
|
for keyword in call.keywords:
|
||||||
|
if (
|
||||||
|
isinstance(keyword.value, ast.Name)
|
||||||
|
and keyword.value.id in self.variables_overwrite
|
||||||
|
):
|
||||||
|
keyword.value = self.variables_overwrite[
|
||||||
|
keyword.value.id
|
||||||
|
] # type:ignore[assignment]
|
||||||
res, expl = self.visit(keyword.value)
|
res, expl = self.visit(keyword.value)
|
||||||
new_kwargs.append(ast.keyword(keyword.arg, res))
|
new_kwargs.append(ast.keyword(keyword.arg, res))
|
||||||
if keyword.arg:
|
if keyword.arg:
|
||||||
|
@ -1075,7 +1081,13 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
self.push_format_context()
|
self.push_format_context()
|
||||||
# We first check if we have overwritten a variable in the previous assert
|
# We first check if we have overwritten a variable in the previous assert
|
||||||
if isinstance(comp.left, ast.Name) and comp.left.id in self.variables_overwrite:
|
if isinstance(comp.left, ast.Name) and comp.left.id in self.variables_overwrite:
|
||||||
comp.left.id = self.variables_overwrite[comp.left.id]
|
comp.left = self.variables_overwrite[
|
||||||
|
comp.left.id
|
||||||
|
] # type:ignore[assignment]
|
||||||
|
if isinstance(comp.left, ast.NamedExpr):
|
||||||
|
self.variables_overwrite[
|
||||||
|
comp.left.target.id
|
||||||
|
] = comp.left # type:ignore[assignment]
|
||||||
left_res, left_expl = self.visit(comp.left)
|
left_res, left_expl = self.visit(comp.left)
|
||||||
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
|
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
|
||||||
left_expl = f"({left_expl})"
|
left_expl = f"({left_expl})"
|
||||||
|
@ -1088,20 +1100,22 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
results = [left_res]
|
results = [left_res]
|
||||||
for i, op, next_operand in it:
|
for i, op, next_operand in it:
|
||||||
if (
|
if (
|
||||||
isinstance(next_operand, namedExpr)
|
isinstance(next_operand, ast.NamedExpr)
|
||||||
and isinstance(left_res, ast.Name)
|
and isinstance(left_res, ast.Name)
|
||||||
and next_operand.target.id == left_res.id
|
and next_operand.target.id == left_res.id
|
||||||
):
|
):
|
||||||
next_operand.target.id = self.variable()
|
next_operand.target.id = self.variable()
|
||||||
self.variables_overwrite[left_res.id] = next_operand.target.id
|
self.variables_overwrite[
|
||||||
|
left_res.id
|
||||||
|
] = next_operand # type:ignore[assignment]
|
||||||
next_res, next_expl = self.visit(next_operand)
|
next_res, next_expl = self.visit(next_operand)
|
||||||
if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
|
if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
|
||||||
next_expl = f"({next_expl})"
|
next_expl = f"({next_expl})"
|
||||||
results.append(next_res)
|
results.append(next_res)
|
||||||
sym = BINOP_MAP[op.__class__]
|
sym = BINOP_MAP[op.__class__]
|
||||||
syms.append(ast.Str(sym))
|
syms.append(ast.Constant(sym))
|
||||||
expl = f"{left_expl} {sym} {next_expl}"
|
expl = f"{left_expl} {sym} {next_expl}"
|
||||||
expls.append(ast.Str(expl))
|
expls.append(ast.Constant(expl))
|
||||||
res_expr = ast.Compare(left_res, [op], [next_res])
|
res_expr = ast.Compare(left_res, [op], [next_res])
|
||||||
self.statements.append(ast.Assign([store_names[i]], res_expr))
|
self.statements.append(ast.Assign([store_names[i]], res_expr))
|
||||||
left_res, left_expl = next_res, next_expl
|
left_res, left_expl = next_res, next_expl
|
||||||
|
@ -1145,7 +1159,7 @@ def try_makedirs(cache_dir: Path) -> bool:
|
||||||
|
|
||||||
def get_cache_dir(file_path: Path) -> Path:
|
def get_cache_dir(file_path: Path) -> Path:
|
||||||
"""Return the cache directory to write .pyc files for the given .py file path."""
|
"""Return the cache directory to write .pyc files for the given .py file path."""
|
||||||
if sys.version_info >= (3, 8) and sys.pycache_prefix:
|
if sys.pycache_prefix:
|
||||||
# given:
|
# given:
|
||||||
# prefix = '/tmp/pycs'
|
# prefix = '/tmp/pycs'
|
||||||
# path = '/home/user/proj/test_app.py'
|
# path = '/home/user/proj/test_app.py'
|
||||||
|
|
|
@ -6,6 +6,7 @@ import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
from typing import final
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
from typing import List
|
from typing import List
|
||||||
|
@ -18,7 +19,6 @@ from .pathlib import rm_rf
|
||||||
from .reports import CollectReport
|
from .reports import CollectReport
|
||||||
from _pytest import nodes
|
from _pytest import nodes
|
||||||
from _pytest._io import TerminalWriter
|
from _pytest._io import TerminalWriter
|
||||||
from _pytest.compat import final
|
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config import ExitCode
|
from _pytest.config import ExitCode
|
||||||
from _pytest.config import hookimpl
|
from _pytest.config import hookimpl
|
||||||
|
@ -27,7 +27,7 @@ from _pytest.deprecated import check_ispytest
|
||||||
from _pytest.fixtures import fixture
|
from _pytest.fixtures import fixture
|
||||||
from _pytest.fixtures import FixtureRequest
|
from _pytest.fixtures import FixtureRequest
|
||||||
from _pytest.main import Session
|
from _pytest.main import Session
|
||||||
from _pytest.python import Module
|
from _pytest.nodes import File
|
||||||
from _pytest.python import Package
|
from _pytest.python import Package
|
||||||
from _pytest.reports import TestReport
|
from _pytest.reports import TestReport
|
||||||
|
|
||||||
|
@ -179,16 +179,22 @@ class Cache:
|
||||||
else:
|
else:
|
||||||
cache_dir_exists_already = self._cachedir.exists()
|
cache_dir_exists_already = self._cachedir.exists()
|
||||||
path.parent.mkdir(exist_ok=True, parents=True)
|
path.parent.mkdir(exist_ok=True, parents=True)
|
||||||
except OSError:
|
except OSError as exc:
|
||||||
self.warn("could not create cache path {path}", path=path, _ispytest=True)
|
self.warn(
|
||||||
|
f"could not create cache path {path}: {exc}",
|
||||||
|
_ispytest=True,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
if not cache_dir_exists_already:
|
if not cache_dir_exists_already:
|
||||||
self._ensure_supporting_files()
|
self._ensure_supporting_files()
|
||||||
data = json.dumps(value, ensure_ascii=False, indent=2)
|
data = json.dumps(value, ensure_ascii=False, indent=2)
|
||||||
try:
|
try:
|
||||||
f = path.open("w", encoding="UTF-8")
|
f = path.open("w", encoding="UTF-8")
|
||||||
except OSError:
|
except OSError as exc:
|
||||||
self.warn("cache could not write path {path}", path=path, _ispytest=True)
|
self.warn(
|
||||||
|
f"cache could not write path {path}: {exc}",
|
||||||
|
_ispytest=True,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
with f:
|
with f:
|
||||||
f.write(data)
|
f.write(data)
|
||||||
|
@ -213,22 +219,30 @@ class LFPluginCollWrapper:
|
||||||
|
|
||||||
@hookimpl(hookwrapper=True)
|
@hookimpl(hookwrapper=True)
|
||||||
def pytest_make_collect_report(self, collector: nodes.Collector):
|
def pytest_make_collect_report(self, collector: nodes.Collector):
|
||||||
if isinstance(collector, Session):
|
if isinstance(collector, (Session, Package)):
|
||||||
out = yield
|
out = yield
|
||||||
res: CollectReport = out.get_result()
|
res: CollectReport = out.get_result()
|
||||||
|
|
||||||
# Sort any lf-paths to the beginning.
|
# Sort any lf-paths to the beginning.
|
||||||
lf_paths = self.lfplugin._last_failed_paths
|
lf_paths = self.lfplugin._last_failed_paths
|
||||||
|
|
||||||
|
# Use stable sort to priorize last failed.
|
||||||
|
def sort_key(node: Union[nodes.Item, nodes.Collector]) -> bool:
|
||||||
|
# Package.path is the __init__.py file, we need the directory.
|
||||||
|
if isinstance(node, Package):
|
||||||
|
path = node.path.parent
|
||||||
|
else:
|
||||||
|
path = node.path
|
||||||
|
return path in lf_paths
|
||||||
|
|
||||||
res.result = sorted(
|
res.result = sorted(
|
||||||
res.result,
|
res.result,
|
||||||
# use stable sort to priorize last failed
|
key=sort_key,
|
||||||
key=lambda x: x.path in lf_paths,
|
|
||||||
reverse=True,
|
reverse=True,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
elif isinstance(collector, Module):
|
elif isinstance(collector, File):
|
||||||
if collector.path in self.lfplugin._last_failed_paths:
|
if collector.path in self.lfplugin._last_failed_paths:
|
||||||
out = yield
|
out = yield
|
||||||
res = out.get_result()
|
res = out.get_result()
|
||||||
|
@ -266,10 +280,9 @@ class LFPluginCollSkipfiles:
|
||||||
def pytest_make_collect_report(
|
def pytest_make_collect_report(
|
||||||
self, collector: nodes.Collector
|
self, collector: nodes.Collector
|
||||||
) -> Optional[CollectReport]:
|
) -> Optional[CollectReport]:
|
||||||
# Packages are Modules, but _last_failed_paths only contains
|
# Packages are Files, but we only want to skip test-bearing Files,
|
||||||
# test-bearing paths and doesn't try to include the paths of their
|
# so don't filter Packages.
|
||||||
# packages, so don't filter them.
|
if isinstance(collector, File) and not isinstance(collector, Package):
|
||||||
if isinstance(collector, Module) and not isinstance(collector, Package):
|
|
||||||
if collector.path not in self.lfplugin._last_failed_paths:
|
if collector.path not in self.lfplugin._last_failed_paths:
|
||||||
self.lfplugin._skipped_files += 1
|
self.lfplugin._skipped_files += 1
|
||||||
|
|
||||||
|
@ -299,9 +312,14 @@ class LFPlugin:
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_last_failed_paths(self) -> Set[Path]:
|
def get_last_failed_paths(self) -> Set[Path]:
|
||||||
"""Return a set with all Paths()s of the previously failed nodeids."""
|
"""Return a set with all Paths of the previously failed nodeids and
|
||||||
|
their parents."""
|
||||||
rootpath = self.config.rootpath
|
rootpath = self.config.rootpath
|
||||||
result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed}
|
result = set()
|
||||||
|
for nodeid in self.lastfailed:
|
||||||
|
path = rootpath / nodeid.split("::")[0]
|
||||||
|
result.add(path)
|
||||||
|
result.update(path.parents)
|
||||||
return {x for x in result if x.exists()}
|
return {x for x in result if x.exists()}
|
||||||
|
|
||||||
def pytest_report_collectionfinish(self) -> Optional[str]:
|
def pytest_report_collectionfinish(self) -> Optional[str]:
|
||||||
|
|
|
@ -11,11 +11,14 @@ from types import TracebackType
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import AnyStr
|
from typing import AnyStr
|
||||||
from typing import BinaryIO
|
from typing import BinaryIO
|
||||||
|
from typing import Final
|
||||||
|
from typing import final
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import Generic
|
from typing import Generic
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from typing import Literal
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import TextIO
|
from typing import TextIO
|
||||||
|
@ -24,7 +27,6 @@ from typing import Type
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
from _pytest.compat import final
|
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config import hookimpl
|
from _pytest.config import hookimpl
|
||||||
from _pytest.config.argparsing import Parser
|
from _pytest.config.argparsing import Parser
|
||||||
|
@ -35,11 +37,7 @@ from _pytest.nodes import Collector
|
||||||
from _pytest.nodes import File
|
from _pytest.nodes import File
|
||||||
from _pytest.nodes import Item
|
from _pytest.nodes import Item
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
|
||||||
from typing_extensions import Final
|
|
||||||
from typing_extensions import Literal
|
|
||||||
|
|
||||||
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser: Parser) -> None:
|
def pytest_addoption(parser: Parser) -> None:
|
||||||
|
@ -241,7 +239,7 @@ class DontReadFromInput(TextIO):
|
||||||
raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
|
raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
|
||||||
|
|
||||||
def truncate(self, size: Optional[int] = None) -> int:
|
def truncate(self, size: Optional[int] = None) -> int:
|
||||||
raise UnsupportedOperation("cannont truncate stdin")
|
raise UnsupportedOperation("cannot truncate stdin")
|
||||||
|
|
||||||
def write(self, data: str) -> int:
|
def write(self, data: str) -> int:
|
||||||
raise UnsupportedOperation("cannot write to stdin")
|
raise UnsupportedOperation("cannot write to stdin")
|
||||||
|
@ -687,7 +685,7 @@ class MultiCapture(Generic[AnyStr]):
|
||||||
return CaptureResult(out, err) # type: ignore[arg-type]
|
return CaptureResult(out, err) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
||||||
def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]:
|
def _get_multicapture(method: _CaptureMethod) -> MultiCapture[str]:
|
||||||
if method == "fd":
|
if method == "fd":
|
||||||
return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2))
|
return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2))
|
||||||
elif method == "sys":
|
elif method == "sys":
|
||||||
|
@ -723,7 +721,7 @@ class CaptureManager:
|
||||||
needed to ensure the fixtures take precedence over the global capture.
|
needed to ensure the fixtures take precedence over the global capture.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, method: "_CaptureMethod") -> None:
|
def __init__(self, method: _CaptureMethod) -> None:
|
||||||
self._method: Final = method
|
self._method: Final = method
|
||||||
self._global_capturing: Optional[MultiCapture[str]] = None
|
self._global_capturing: Optional[MultiCapture[str]] = None
|
||||||
self._capture_fixture: Optional[CaptureFixture[Any]] = None
|
self._capture_fixture: Optional[CaptureFixture[Any]] = None
|
||||||
|
|
|
@ -12,26 +12,12 @@ from inspect import signature
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Generic
|
from typing import Final
|
||||||
from typing import NoReturn
|
from typing import NoReturn
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
|
|
||||||
import py
|
import py
|
||||||
|
|
||||||
# fmt: off
|
|
||||||
# Workaround for https://github.com/sphinx-doc/sphinx/issues/10351.
|
|
||||||
# If `overload` is imported from `compat` instead of from `typing`,
|
|
||||||
# Sphinx doesn't recognize it as `overload` and the API docs for
|
|
||||||
# overloaded functions look good again. But type checkers handle
|
|
||||||
# it fine.
|
|
||||||
# fmt: on
|
|
||||||
if True:
|
|
||||||
from typing import overload as overload
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from typing_extensions import Final
|
|
||||||
|
|
||||||
|
|
||||||
_T = TypeVar("_T")
|
_T = TypeVar("_T")
|
||||||
_S = TypeVar("_S")
|
_S = TypeVar("_S")
|
||||||
|
@ -58,17 +44,6 @@ class NotSetType(enum.Enum):
|
||||||
NOTSET: Final = NotSetType.token # noqa: E305
|
NOTSET: Final = NotSetType.token # noqa: E305
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
if sys.version_info >= (3, 8):
|
|
||||||
import importlib.metadata
|
|
||||||
|
|
||||||
importlib_metadata = importlib.metadata
|
|
||||||
else:
|
|
||||||
import importlib_metadata as importlib_metadata # noqa: F401
|
|
||||||
|
|
||||||
|
|
||||||
def _format_args(func: Callable[..., Any]) -> str:
|
|
||||||
return str(signature(func))
|
|
||||||
|
|
||||||
|
|
||||||
def is_generator(func: object) -> bool:
|
def is_generator(func: object) -> bool:
|
||||||
genfunc = inspect.isgeneratorfunction(func)
|
genfunc = inspect.isgeneratorfunction(func)
|
||||||
|
@ -338,47 +313,6 @@ def safe_isclass(obj: object) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
if sys.version_info >= (3, 8):
|
|
||||||
from typing import final as final
|
|
||||||
else:
|
|
||||||
from typing_extensions import final as final
|
|
||||||
elif sys.version_info >= (3, 8):
|
|
||||||
from typing import final as final
|
|
||||||
else:
|
|
||||||
|
|
||||||
def final(f):
|
|
||||||
return f
|
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info >= (3, 8):
|
|
||||||
from functools import cached_property as cached_property
|
|
||||||
else:
|
|
||||||
|
|
||||||
class cached_property(Generic[_S, _T]):
|
|
||||||
__slots__ = ("func", "__doc__")
|
|
||||||
|
|
||||||
def __init__(self, func: Callable[[_S], _T]) -> None:
|
|
||||||
self.func = func
|
|
||||||
self.__doc__ = func.__doc__
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def __get__(
|
|
||||||
self, instance: None, owner: type[_S] | None = ...
|
|
||||||
) -> cached_property[_S, _T]:
|
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def __get__(self, instance: _S, owner: type[_S] | None = ...) -> _T:
|
|
||||||
...
|
|
||||||
|
|
||||||
def __get__(self, instance, owner=None):
|
|
||||||
if instance is None:
|
|
||||||
return self
|
|
||||||
value = instance.__dict__[self.func.__name__] = self.func(instance)
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def get_user_id() -> int | None:
|
def get_user_id() -> int | None:
|
||||||
"""Return the current user id, or None if we cannot get it reliably on the current platform."""
|
"""Return the current user id, or None if we cannot get it reliably on the current platform."""
|
||||||
# win32 does not have a getuid() function.
|
# win32 does not have a getuid() function.
|
||||||
|
|
|
@ -5,6 +5,7 @@ import copy
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import enum
|
import enum
|
||||||
import glob
|
import glob
|
||||||
|
import importlib.metadata
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
@ -21,6 +22,7 @@ from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import cast
|
from typing import cast
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
from typing import final
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import IO
|
from typing import IO
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
|
@ -48,8 +50,6 @@ from .findpaths import determine_setup
|
||||||
from _pytest._code import ExceptionInfo
|
from _pytest._code import ExceptionInfo
|
||||||
from _pytest._code import filter_traceback
|
from _pytest._code import filter_traceback
|
||||||
from _pytest._io import TerminalWriter
|
from _pytest._io import TerminalWriter
|
||||||
from _pytest.compat import final
|
|
||||||
from _pytest.compat import importlib_metadata
|
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
from _pytest.outcomes import Skipped
|
from _pytest.outcomes import Skipped
|
||||||
from _pytest.pathlib import absolutepath
|
from _pytest.pathlib import absolutepath
|
||||||
|
@ -257,7 +257,8 @@ default_plugins = essential_plugins + (
|
||||||
"logging",
|
"logging",
|
||||||
"reports",
|
"reports",
|
||||||
"python_path",
|
"python_path",
|
||||||
*(["unraisableexception", "threadexception"] if sys.version_info >= (3, 8) else []),
|
"unraisableexception",
|
||||||
|
"threadexception",
|
||||||
"faulthandler",
|
"faulthandler",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -527,9 +528,12 @@ class PytestPluginManager(PluginManager):
|
||||||
#
|
#
|
||||||
def _set_initial_conftests(
|
def _set_initial_conftests(
|
||||||
self,
|
self,
|
||||||
namespace: argparse.Namespace,
|
args: Sequence[Union[str, Path]],
|
||||||
|
pyargs: bool,
|
||||||
|
noconftest: bool,
|
||||||
rootpath: Path,
|
rootpath: Path,
|
||||||
testpaths_ini: Sequence[str],
|
confcutdir: Optional[Path],
|
||||||
|
importmode: Union[ImportMode, str],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Load initial conftest files given a preparsed "namespace".
|
"""Load initial conftest files given a preparsed "namespace".
|
||||||
|
|
||||||
|
@ -539,17 +543,12 @@ class PytestPluginManager(PluginManager):
|
||||||
common options will not confuse our logic here.
|
common options will not confuse our logic here.
|
||||||
"""
|
"""
|
||||||
current = Path.cwd()
|
current = Path.cwd()
|
||||||
self._confcutdir = (
|
self._confcutdir = absolutepath(current / confcutdir) if confcutdir else None
|
||||||
absolutepath(current / namespace.confcutdir)
|
self._noconftest = noconftest
|
||||||
if namespace.confcutdir
|
self._using_pyargs = pyargs
|
||||||
else None
|
|
||||||
)
|
|
||||||
self._noconftest = namespace.noconftest
|
|
||||||
self._using_pyargs = namespace.pyargs
|
|
||||||
testpaths = namespace.file_or_dir + testpaths_ini
|
|
||||||
foundanchor = False
|
foundanchor = False
|
||||||
for testpath in testpaths:
|
for intitial_path in args:
|
||||||
path = str(testpath)
|
path = str(intitial_path)
|
||||||
# remove node-id syntax
|
# remove node-id syntax
|
||||||
i = path.find("::")
|
i = path.find("::")
|
||||||
if i != -1:
|
if i != -1:
|
||||||
|
@ -563,10 +562,10 @@ class PytestPluginManager(PluginManager):
|
||||||
except OSError: # pragma: no cover
|
except OSError: # pragma: no cover
|
||||||
anchor_exists = False
|
anchor_exists = False
|
||||||
if anchor_exists:
|
if anchor_exists:
|
||||||
self._try_load_conftest(anchor, namespace.importmode, rootpath)
|
self._try_load_conftest(anchor, importmode, rootpath)
|
||||||
foundanchor = True
|
foundanchor = True
|
||||||
if not foundanchor:
|
if not foundanchor:
|
||||||
self._try_load_conftest(current, namespace.importmode, rootpath)
|
self._try_load_conftest(current, importmode, rootpath)
|
||||||
|
|
||||||
def _is_in_confcutdir(self, path: Path) -> bool:
|
def _is_in_confcutdir(self, path: Path) -> bool:
|
||||||
"""Whether a path is within the confcutdir.
|
"""Whether a path is within the confcutdir.
|
||||||
|
@ -1140,10 +1139,25 @@ class Config:
|
||||||
|
|
||||||
@hookimpl(trylast=True)
|
@hookimpl(trylast=True)
|
||||||
def pytest_load_initial_conftests(self, early_config: "Config") -> None:
|
def pytest_load_initial_conftests(self, early_config: "Config") -> None:
|
||||||
self.pluginmanager._set_initial_conftests(
|
# We haven't fully parsed the command line arguments yet, so
|
||||||
early_config.known_args_namespace,
|
# early_config.args it not set yet. But we need it for
|
||||||
|
# discovering the initial conftests. So "pre-run" the logic here.
|
||||||
|
# It will be done for real in `parse()`.
|
||||||
|
args, args_source = early_config._decide_args(
|
||||||
|
args=early_config.known_args_namespace.file_or_dir,
|
||||||
|
pyargs=early_config.known_args_namespace.pyargs,
|
||||||
|
testpaths=early_config.getini("testpaths"),
|
||||||
|
invocation_dir=early_config.invocation_params.dir,
|
||||||
rootpath=early_config.rootpath,
|
rootpath=early_config.rootpath,
|
||||||
testpaths_ini=self.getini("testpaths"),
|
warn=False,
|
||||||
|
)
|
||||||
|
self.pluginmanager._set_initial_conftests(
|
||||||
|
args=args,
|
||||||
|
pyargs=early_config.known_args_namespace.pyargs,
|
||||||
|
noconftest=early_config.known_args_namespace.noconftest,
|
||||||
|
rootpath=early_config.rootpath,
|
||||||
|
confcutdir=early_config.known_args_namespace.confcutdir,
|
||||||
|
importmode=early_config.known_args_namespace.importmode,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _initini(self, args: Sequence[str]) -> None:
|
def _initini(self, args: Sequence[str]) -> None:
|
||||||
|
@ -1203,7 +1217,7 @@ class Config:
|
||||||
|
|
||||||
package_files = (
|
package_files = (
|
||||||
str(file)
|
str(file)
|
||||||
for dist in importlib_metadata.distributions()
|
for dist in importlib.metadata.distributions()
|
||||||
if any(ep.group == "pytest11" for ep in dist.entry_points)
|
if any(ep.group == "pytest11" for ep in dist.entry_points)
|
||||||
for file in dist.files or []
|
for file in dist.files or []
|
||||||
)
|
)
|
||||||
|
@ -1223,6 +1237,49 @@ class Config:
|
||||||
|
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
def _decide_args(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
args: List[str],
|
||||||
|
pyargs: List[str],
|
||||||
|
testpaths: List[str],
|
||||||
|
invocation_dir: Path,
|
||||||
|
rootpath: Path,
|
||||||
|
warn: bool,
|
||||||
|
) -> Tuple[List[str], ArgsSource]:
|
||||||
|
"""Decide the args (initial paths/nodeids) to use given the relevant inputs.
|
||||||
|
|
||||||
|
:param warn: Whether can issue warnings.
|
||||||
|
"""
|
||||||
|
if args:
|
||||||
|
source = Config.ArgsSource.ARGS
|
||||||
|
result = args
|
||||||
|
else:
|
||||||
|
if invocation_dir == rootpath:
|
||||||
|
source = Config.ArgsSource.TESTPATHS
|
||||||
|
if pyargs:
|
||||||
|
result = testpaths
|
||||||
|
else:
|
||||||
|
result = []
|
||||||
|
for path in testpaths:
|
||||||
|
result.extend(sorted(glob.iglob(path, recursive=True)))
|
||||||
|
if testpaths and not result:
|
||||||
|
if warn:
|
||||||
|
warning_text = (
|
||||||
|
"No files were found in testpaths; "
|
||||||
|
"consider removing or adjusting your testpaths configuration. "
|
||||||
|
"Searching recursively from the current directory instead."
|
||||||
|
)
|
||||||
|
self.issue_config_time_warning(
|
||||||
|
PytestConfigWarning(warning_text), stacklevel=3
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
result = []
|
||||||
|
if not result:
|
||||||
|
source = Config.ArgsSource.INCOVATION_DIR
|
||||||
|
result = [str(invocation_dir)]
|
||||||
|
return result, source
|
||||||
|
|
||||||
def _preparse(self, args: List[str], addopts: bool = True) -> None:
|
def _preparse(self, args: List[str], addopts: bool = True) -> None:
|
||||||
if addopts:
|
if addopts:
|
||||||
env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
|
env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
|
||||||
|
@ -1261,8 +1318,11 @@ class Config:
|
||||||
_pytest.deprecated.STRICT_OPTION, stacklevel=2
|
_pytest.deprecated.STRICT_OPTION, stacklevel=2
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.known_args_namespace.confcutdir is None and self.inipath is not None:
|
if self.known_args_namespace.confcutdir is None:
|
||||||
confcutdir = str(self.inipath.parent)
|
if self.inipath is not None:
|
||||||
|
confcutdir = str(self.inipath.parent)
|
||||||
|
else:
|
||||||
|
confcutdir = str(self.rootpath)
|
||||||
self.known_args_namespace.confcutdir = confcutdir
|
self.known_args_namespace.confcutdir = confcutdir
|
||||||
try:
|
try:
|
||||||
self.hook.pytest_load_initial_conftests(
|
self.hook.pytest_load_initial_conftests(
|
||||||
|
@ -1368,25 +1428,17 @@ class Config:
|
||||||
self.hook.pytest_cmdline_preparse(config=self, args=args)
|
self.hook.pytest_cmdline_preparse(config=self, args=args)
|
||||||
self._parser.after_preparse = True # type: ignore
|
self._parser.after_preparse = True # type: ignore
|
||||||
try:
|
try:
|
||||||
source = Config.ArgsSource.ARGS
|
|
||||||
args = self._parser.parse_setoption(
|
args = self._parser.parse_setoption(
|
||||||
args, self.option, namespace=self.option
|
args, self.option, namespace=self.option
|
||||||
)
|
)
|
||||||
if not args:
|
self.args, self.args_source = self._decide_args(
|
||||||
if self.invocation_params.dir == self.rootpath:
|
args=args,
|
||||||
source = Config.ArgsSource.TESTPATHS
|
pyargs=self.known_args_namespace.pyargs,
|
||||||
testpaths: List[str] = self.getini("testpaths")
|
testpaths=self.getini("testpaths"),
|
||||||
if self.known_args_namespace.pyargs:
|
invocation_dir=self.invocation_params.dir,
|
||||||
args = testpaths
|
rootpath=self.rootpath,
|
||||||
else:
|
warn=True,
|
||||||
args = []
|
)
|
||||||
for path in testpaths:
|
|
||||||
args.extend(sorted(glob.iglob(path, recursive=True)))
|
|
||||||
if not args:
|
|
||||||
source = Config.ArgsSource.INCOVATION_DIR
|
|
||||||
args = [str(self.invocation_params.dir)]
|
|
||||||
self.args = args
|
|
||||||
self.args_source = source
|
|
||||||
except PrintHelp:
|
except PrintHelp:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import cast
|
from typing import cast
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
from typing import final
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Mapping
|
from typing import Mapping
|
||||||
from typing import NoReturn
|
from typing import NoReturn
|
||||||
|
@ -17,7 +18,6 @@ from typing import TYPE_CHECKING
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import _pytest._io
|
import _pytest._io
|
||||||
from _pytest.compat import final
|
|
||||||
from _pytest.config.exceptions import UsageError
|
from _pytest.config.exceptions import UsageError
|
||||||
from _pytest.deprecated import ARGUMENT_PERCENT_DEFAULT
|
from _pytest.deprecated import ARGUMENT_PERCENT_DEFAULT
|
||||||
from _pytest.deprecated import ARGUMENT_TYPE_STR
|
from _pytest.deprecated import ARGUMENT_TYPE_STR
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from _pytest.compat import final
|
from typing import final
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
|
|
|
@ -122,6 +122,11 @@ HOOK_LEGACY_MARKING = UnformattedWarning(
|
||||||
"#configuring-hook-specs-impls-using-markers",
|
"#configuring-hook-specs-impls-using-markers",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
MARKED_FIXTURE = PytestRemovedIn8Warning(
|
||||||
|
"Marks applied to fixtures have no effect\n"
|
||||||
|
"See docs: https://docs.pytest.org/en/stable/deprecations.html#applying-a-mark-to-a-fixture-function"
|
||||||
|
)
|
||||||
|
|
||||||
# You want to make some `__init__` or function "private".
|
# You want to make some `__init__` or function "private".
|
||||||
#
|
#
|
||||||
# def my_private_function(some, args):
|
# def my_private_function(some, args):
|
||||||
|
|
|
@ -13,6 +13,7 @@ from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import cast
|
from typing import cast
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
from typing import final
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import Generic
|
from typing import Generic
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
|
@ -21,6 +22,7 @@ from typing import List
|
||||||
from typing import MutableMapping
|
from typing import MutableMapping
|
||||||
from typing import NoReturn
|
from typing import NoReturn
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from typing import overload
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from typing import Set
|
from typing import Set
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
@ -35,10 +37,8 @@ from _pytest._code import getfslineno
|
||||||
from _pytest._code.code import FormattedExcinfo
|
from _pytest._code.code import FormattedExcinfo
|
||||||
from _pytest._code.code import TerminalRepr
|
from _pytest._code.code import TerminalRepr
|
||||||
from _pytest._io import TerminalWriter
|
from _pytest._io import TerminalWriter
|
||||||
from _pytest.compat import _format_args
|
|
||||||
from _pytest.compat import _PytestWrapper
|
from _pytest.compat import _PytestWrapper
|
||||||
from _pytest.compat import assert_never
|
from _pytest.compat import assert_never
|
||||||
from _pytest.compat import final
|
|
||||||
from _pytest.compat import get_real_func
|
from _pytest.compat import get_real_func
|
||||||
from _pytest.compat import get_real_method
|
from _pytest.compat import get_real_method
|
||||||
from _pytest.compat import getfuncargnames
|
from _pytest.compat import getfuncargnames
|
||||||
|
@ -46,12 +46,13 @@ from _pytest.compat import getimfunc
|
||||||
from _pytest.compat import getlocation
|
from _pytest.compat import getlocation
|
||||||
from _pytest.compat import is_generator
|
from _pytest.compat import is_generator
|
||||||
from _pytest.compat import NOTSET
|
from _pytest.compat import NOTSET
|
||||||
from _pytest.compat import overload
|
from _pytest.compat import NotSetType
|
||||||
from _pytest.compat import safe_getattr
|
from _pytest.compat import safe_getattr
|
||||||
from _pytest.config import _PluggyPlugin
|
from _pytest.config import _PluggyPlugin
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config.argparsing import Parser
|
from _pytest.config.argparsing import Parser
|
||||||
from _pytest.deprecated import check_ispytest
|
from _pytest.deprecated import check_ispytest
|
||||||
|
from _pytest.deprecated import MARKED_FIXTURE
|
||||||
from _pytest.deprecated import YIELD_FIXTURE
|
from _pytest.deprecated import YIELD_FIXTURE
|
||||||
from _pytest.mark import Mark
|
from _pytest.mark import Mark
|
||||||
from _pytest.mark import ParameterSet
|
from _pytest.mark import ParameterSet
|
||||||
|
@ -112,16 +113,18 @@ def pytest_sessionstart(session: "Session") -> None:
|
||||||
session._fixturemanager = FixtureManager(session)
|
session._fixturemanager = FixtureManager(session)
|
||||||
|
|
||||||
|
|
||||||
def get_scope_package(node, fixturedef: "FixtureDef[object]"):
|
def get_scope_package(
|
||||||
import pytest
|
node: nodes.Item,
|
||||||
|
fixturedef: "FixtureDef[object]",
|
||||||
|
) -> Optional[Union[nodes.Item, nodes.Collector]]:
|
||||||
|
from _pytest.python import Package
|
||||||
|
|
||||||
cls = pytest.Package
|
current: Optional[Union[nodes.Item, nodes.Collector]] = node
|
||||||
current = node
|
|
||||||
fixture_package_name = "{}/{}".format(fixturedef.baseid, "__init__.py")
|
fixture_package_name = "{}/{}".format(fixturedef.baseid, "__init__.py")
|
||||||
while current and (
|
while current and (
|
||||||
type(current) is not cls or fixture_package_name != current.nodeid
|
not isinstance(current, Package) or fixture_package_name != current.nodeid
|
||||||
):
|
):
|
||||||
current = current.parent
|
current = current.parent # type: ignore[assignment]
|
||||||
if current is None:
|
if current is None:
|
||||||
return node.session
|
return node.session
|
||||||
return current
|
return current
|
||||||
|
@ -434,7 +437,23 @@ class FixtureRequest:
|
||||||
@property
|
@property
|
||||||
def node(self):
|
def node(self):
|
||||||
"""Underlying collection node (depends on current request scope)."""
|
"""Underlying collection node (depends on current request scope)."""
|
||||||
return self._getscopeitem(self._scope)
|
scope = self._scope
|
||||||
|
if scope is Scope.Function:
|
||||||
|
# This might also be a non-function Item despite its attribute name.
|
||||||
|
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
|
||||||
|
elif scope is Scope.Package:
|
||||||
|
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
|
||||||
|
# but on FixtureRequest (a subclass).
|
||||||
|
node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
|
||||||
|
else:
|
||||||
|
node = get_scope_node(self._pyfuncitem, scope)
|
||||||
|
if node is None and scope is Scope.Class:
|
||||||
|
# Fallback to function item itself.
|
||||||
|
node = self._pyfuncitem
|
||||||
|
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
|
||||||
|
scope, self._pyfuncitem
|
||||||
|
)
|
||||||
|
return node
|
||||||
|
|
||||||
def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
|
def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
|
||||||
fixturedefs = self._arg2fixturedefs.get(argname, None)
|
fixturedefs = self._arg2fixturedefs.get(argname, None)
|
||||||
|
@ -518,11 +537,7 @@ class FixtureRequest:
|
||||||
"""Add finalizer/teardown function to be called without arguments after
|
"""Add finalizer/teardown function to be called without arguments after
|
||||||
the last test within the requesting test context finished execution."""
|
the last test within the requesting test context finished execution."""
|
||||||
# XXX usually this method is shadowed by fixturedef specific ones.
|
# XXX usually this method is shadowed by fixturedef specific ones.
|
||||||
self._addfinalizer(finalizer, scope=self.scope)
|
self.node.addfinalizer(finalizer)
|
||||||
|
|
||||||
def _addfinalizer(self, finalizer: Callable[[], object], scope) -> None:
|
|
||||||
node = self._getscopeitem(scope)
|
|
||||||
node.addfinalizer(finalizer)
|
|
||||||
|
|
||||||
def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
|
def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
|
||||||
"""Apply a marker to a single test function invocation.
|
"""Apply a marker to a single test function invocation.
|
||||||
|
@ -713,32 +728,12 @@ class FixtureRequest:
|
||||||
p = bestrelpath(session.path, fs)
|
p = bestrelpath(session.path, fs)
|
||||||
else:
|
else:
|
||||||
p = fs
|
p = fs
|
||||||
args = _format_args(factory)
|
lines.append(
|
||||||
lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args))
|
"%s:%d: def %s%s"
|
||||||
|
% (p, lineno + 1, factory.__name__, inspect.signature(factory))
|
||||||
|
)
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
def _getscopeitem(
|
|
||||||
self, scope: Union[Scope, "_ScopeName"]
|
|
||||||
) -> Union[nodes.Item, nodes.Collector]:
|
|
||||||
if isinstance(scope, str):
|
|
||||||
scope = Scope(scope)
|
|
||||||
if scope is Scope.Function:
|
|
||||||
# This might also be a non-function Item despite its attribute name.
|
|
||||||
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
|
|
||||||
elif scope is Scope.Package:
|
|
||||||
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
|
|
||||||
# but on FixtureRequest (a subclass).
|
|
||||||
node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
|
|
||||||
else:
|
|
||||||
node = get_scope_node(self._pyfuncitem, scope)
|
|
||||||
if node is None and scope is Scope.Class:
|
|
||||||
# Fallback to function item itself.
|
|
||||||
node = self._pyfuncitem
|
|
||||||
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
|
|
||||||
scope, self._pyfuncitem
|
|
||||||
)
|
|
||||||
return node
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return "<FixtureRequest for %r>" % (self.node)
|
return "<FixtureRequest for %r>" % (self.node)
|
||||||
|
|
||||||
|
@ -1206,6 +1201,9 @@ class FixtureFunctionMarker:
|
||||||
"fixture is being applied more than once to the same function"
|
"fixture is being applied more than once to the same function"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if hasattr(function, "pytestmark"):
|
||||||
|
warnings.warn(MARKED_FIXTURE, stacklevel=2)
|
||||||
|
|
||||||
function = wrap_function_to_error_out_if_called_directly(function, self)
|
function = wrap_function_to_error_out_if_called_directly(function, self)
|
||||||
|
|
||||||
name = self.name or function.__name__
|
name = self.name or function.__name__
|
||||||
|
@ -1593,13 +1591,52 @@ class FixtureManager:
|
||||||
# Separate parametrized setups.
|
# Separate parametrized setups.
|
||||||
items[:] = reorder_items(items)
|
items[:] = reorder_items(items)
|
||||||
|
|
||||||
|
@overload
|
||||||
def parsefactories(
|
def parsefactories(
|
||||||
self, node_or_obj, nodeid=NOTSET, unittest: bool = False
|
self,
|
||||||
|
node_or_obj: nodes.Node,
|
||||||
|
*,
|
||||||
|
unittest: bool = ...,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def parsefactories( # noqa: F811
|
||||||
|
self,
|
||||||
|
node_or_obj: object,
|
||||||
|
nodeid: Optional[str],
|
||||||
|
*,
|
||||||
|
unittest: bool = ...,
|
||||||
|
) -> None:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def parsefactories( # noqa: F811
|
||||||
|
self,
|
||||||
|
node_or_obj: Union[nodes.Node, object],
|
||||||
|
nodeid: Union[str, NotSetType, None] = NOTSET,
|
||||||
|
*,
|
||||||
|
unittest: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""Collect fixtures from a collection node or object.
|
||||||
|
|
||||||
|
Found fixtures are parsed into `FixtureDef`s and saved.
|
||||||
|
|
||||||
|
If `node_or_object` is a collection node (with an underlying Python
|
||||||
|
object), the node's object is traversed and the node's nodeid is used to
|
||||||
|
determine the fixtures' visibilty. `nodeid` must not be specified in
|
||||||
|
this case.
|
||||||
|
|
||||||
|
If `node_or_object` is an object (e.g. a plugin), the object is
|
||||||
|
traversed and the given `nodeid` is used to determine the fixtures'
|
||||||
|
visibility. `nodeid` must be specified in this case; None and "" mean
|
||||||
|
total visibility.
|
||||||
|
"""
|
||||||
if nodeid is not NOTSET:
|
if nodeid is not NOTSET:
|
||||||
holderobj = node_or_obj
|
holderobj = node_or_obj
|
||||||
else:
|
else:
|
||||||
holderobj = node_or_obj.obj
|
assert isinstance(node_or_obj, nodes.Node)
|
||||||
|
holderobj = cast(object, node_or_obj.obj) # type: ignore[attr-defined]
|
||||||
|
assert isinstance(node_or_obj.nodeid, str)
|
||||||
nodeid = node_or_obj.nodeid
|
nodeid = node_or_obj.nodeid
|
||||||
if holderobj in self._holderobjseen:
|
if holderobj in self._holderobjseen:
|
||||||
return
|
return
|
||||||
|
|
|
@ -105,7 +105,7 @@ def pytest_cmdline_parse():
|
||||||
if config.option.debug:
|
if config.option.debug:
|
||||||
# --debug | --debug <file.log> was provided.
|
# --debug | --debug <file.log> was provided.
|
||||||
path = config.option.debug
|
path = config.option.debug
|
||||||
debugfile = open(path, "w")
|
debugfile = open(path, "w", encoding="utf-8")
|
||||||
debugfile.write(
|
debugfile.write(
|
||||||
"versions pytest-%s, "
|
"versions pytest-%s, "
|
||||||
"python-%s\ncwd=%s\nargs=%s\n\n"
|
"python-%s\ncwd=%s\nargs=%s\n\n"
|
||||||
|
|
|
@ -41,6 +41,7 @@ if TYPE_CHECKING:
|
||||||
from _pytest.reports import TestReport
|
from _pytest.reports import TestReport
|
||||||
from _pytest.runner import CallInfo
|
from _pytest.runner import CallInfo
|
||||||
from _pytest.terminal import TerminalReporter
|
from _pytest.terminal import TerminalReporter
|
||||||
|
from _pytest.terminal import TestShortLogReport
|
||||||
from _pytest.compat import LEGACY_PATH
|
from _pytest.compat import LEGACY_PATH
|
||||||
|
|
||||||
|
|
||||||
|
@ -806,7 +807,7 @@ def pytest_report_collectionfinish( # type:ignore[empty-body]
|
||||||
@hookspec(firstresult=True)
|
@hookspec(firstresult=True)
|
||||||
def pytest_report_teststatus( # type:ignore[empty-body]
|
def pytest_report_teststatus( # type:ignore[empty-body]
|
||||||
report: Union["CollectReport", "TestReport"], config: "Config"
|
report: Union["CollectReport", "TestReport"], config: "Config"
|
||||||
) -> Tuple[str, str, Union[str, Mapping[str, bool]]]:
|
) -> "TestShortLogReport | Tuple[str, str, Union[str, Tuple[str, Mapping[str, bool]]]]":
|
||||||
"""Return result-category, shortletter and verbose word for status
|
"""Return result-category, shortletter and verbose word for status
|
||||||
reporting.
|
reporting.
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@ import dataclasses
|
||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Final
|
||||||
|
from typing import final
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
@ -11,7 +13,6 @@ from typing import Union
|
||||||
from iniconfig import SectionWrapper
|
from iniconfig import SectionWrapper
|
||||||
|
|
||||||
from _pytest.cacheprovider import Cache
|
from _pytest.cacheprovider import Cache
|
||||||
from _pytest.compat import final
|
|
||||||
from _pytest.compat import LEGACY_PATH
|
from _pytest.compat import LEGACY_PATH
|
||||||
from _pytest.compat import legacy_path
|
from _pytest.compat import legacy_path
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
|
@ -32,8 +33,6 @@ from _pytest.terminal import TerminalReporter
|
||||||
from _pytest.tmpdir import TempPathFactory
|
from _pytest.tmpdir import TempPathFactory
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing_extensions import Final
|
|
||||||
|
|
||||||
import pexpect
|
import pexpect
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,10 +5,15 @@ import os
|
||||||
import re
|
import re
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from contextlib import nullcontext
|
from contextlib import nullcontext
|
||||||
|
from datetime import datetime
|
||||||
|
from datetime import timedelta
|
||||||
|
from datetime import timezone
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
from logging import LogRecord
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import AbstractSet
|
from typing import AbstractSet
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
from typing import final
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Mapping
|
from typing import Mapping
|
||||||
|
@ -21,7 +26,6 @@ from typing import Union
|
||||||
from _pytest import nodes
|
from _pytest import nodes
|
||||||
from _pytest._io import TerminalWriter
|
from _pytest._io import TerminalWriter
|
||||||
from _pytest.capture import CaptureManager
|
from _pytest.capture import CaptureManager
|
||||||
from _pytest.compat import final
|
|
||||||
from _pytest.config import _strtobool
|
from _pytest.config import _strtobool
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config import create_terminal_writer
|
from _pytest.config import create_terminal_writer
|
||||||
|
@ -53,7 +57,25 @@ def _remove_ansi_escape_sequences(text: str) -> str:
|
||||||
return _ANSI_ESCAPE_SEQ.sub("", text)
|
return _ANSI_ESCAPE_SEQ.sub("", text)
|
||||||
|
|
||||||
|
|
||||||
class ColoredLevelFormatter(logging.Formatter):
|
class DatetimeFormatter(logging.Formatter):
|
||||||
|
"""A logging formatter which formats record with
|
||||||
|
:func:`datetime.datetime.strftime` formatter instead of
|
||||||
|
:func:`time.strftime` in case of microseconds in format string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def formatTime(self, record: LogRecord, datefmt=None) -> str:
|
||||||
|
if datefmt and "%f" in datefmt:
|
||||||
|
ct = self.converter(record.created)
|
||||||
|
tz = timezone(timedelta(seconds=ct.tm_gmtoff), ct.tm_zone)
|
||||||
|
# Construct `datetime.datetime` object from `struct_time`
|
||||||
|
# and msecs information from `record`
|
||||||
|
dt = datetime(*ct[0:6], microsecond=round(record.msecs * 1000), tzinfo=tz)
|
||||||
|
return dt.strftime(datefmt)
|
||||||
|
# Use `logging.Formatter` for non-microsecond formats
|
||||||
|
return super().formatTime(record, datefmt)
|
||||||
|
|
||||||
|
|
||||||
|
class ColoredLevelFormatter(DatetimeFormatter):
|
||||||
"""A logging formatter which colorizes the %(levelname)..s part of the
|
"""A logging formatter which colorizes the %(levelname)..s part of the
|
||||||
log format passed to __init__."""
|
log format passed to __init__."""
|
||||||
|
|
||||||
|
@ -376,11 +398,12 @@ class LogCaptureFixture:
|
||||||
self._initial_handler_level: Optional[int] = None
|
self._initial_handler_level: Optional[int] = None
|
||||||
# Dict of log name -> log level.
|
# Dict of log name -> log level.
|
||||||
self._initial_logger_levels: Dict[Optional[str], int] = {}
|
self._initial_logger_levels: Dict[Optional[str], int] = {}
|
||||||
|
self._initial_disabled_logging_level: Optional[int] = None
|
||||||
|
|
||||||
def _finalize(self) -> None:
|
def _finalize(self) -> None:
|
||||||
"""Finalize the fixture.
|
"""Finalize the fixture.
|
||||||
|
|
||||||
This restores the log levels changed by :meth:`set_level`.
|
This restores the log levels and the disabled logging levels changed by :meth:`set_level`.
|
||||||
"""
|
"""
|
||||||
# Restore log levels.
|
# Restore log levels.
|
||||||
if self._initial_handler_level is not None:
|
if self._initial_handler_level is not None:
|
||||||
|
@ -388,6 +411,10 @@ class LogCaptureFixture:
|
||||||
for logger_name, level in self._initial_logger_levels.items():
|
for logger_name, level in self._initial_logger_levels.items():
|
||||||
logger = logging.getLogger(logger_name)
|
logger = logging.getLogger(logger_name)
|
||||||
logger.setLevel(level)
|
logger.setLevel(level)
|
||||||
|
# Disable logging at the original disabled logging level.
|
||||||
|
if self._initial_disabled_logging_level is not None:
|
||||||
|
logging.disable(self._initial_disabled_logging_level)
|
||||||
|
self._initial_disabled_logging_level = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def handler(self) -> LogCaptureHandler:
|
def handler(self) -> LogCaptureHandler:
|
||||||
|
@ -453,13 +480,51 @@ class LogCaptureFixture:
|
||||||
"""Reset the list of log records and the captured log text."""
|
"""Reset the list of log records and the captured log text."""
|
||||||
self.handler.clear()
|
self.handler.clear()
|
||||||
|
|
||||||
|
def _force_enable_logging(
|
||||||
|
self, level: Union[int, str], logger_obj: logging.Logger
|
||||||
|
) -> int:
|
||||||
|
"""Enable the desired logging level if the global level was disabled via ``logging.disabled``.
|
||||||
|
|
||||||
|
Only enables logging levels greater than or equal to the requested ``level``.
|
||||||
|
|
||||||
|
Does nothing if the desired ``level`` wasn't disabled.
|
||||||
|
|
||||||
|
:param level:
|
||||||
|
The logger level caplog should capture.
|
||||||
|
All logging is enabled if a non-standard logging level string is supplied.
|
||||||
|
Valid level strings are in :data:`logging._nameToLevel`.
|
||||||
|
:param logger_obj: The logger object to check.
|
||||||
|
|
||||||
|
:return: The original disabled logging level.
|
||||||
|
"""
|
||||||
|
original_disable_level: int = logger_obj.manager.disable # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
if isinstance(level, str):
|
||||||
|
# Try to translate the level string to an int for `logging.disable()`
|
||||||
|
level = logging.getLevelName(level)
|
||||||
|
|
||||||
|
if not isinstance(level, int):
|
||||||
|
# The level provided was not valid, so just un-disable all logging.
|
||||||
|
logging.disable(logging.NOTSET)
|
||||||
|
elif not logger_obj.isEnabledFor(level):
|
||||||
|
# Each level is `10` away from other levels.
|
||||||
|
# https://docs.python.org/3/library/logging.html#logging-levels
|
||||||
|
disable_level = max(level - 10, logging.NOTSET)
|
||||||
|
logging.disable(disable_level)
|
||||||
|
|
||||||
|
return original_disable_level
|
||||||
|
|
||||||
def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> None:
|
def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> None:
|
||||||
"""Set the level of a logger for the duration of a test.
|
"""Set the threshold level of a logger for the duration of a test.
|
||||||
|
|
||||||
|
Logging messages which are less severe than this level will not be captured.
|
||||||
|
|
||||||
.. versionchanged:: 3.4
|
.. versionchanged:: 3.4
|
||||||
The levels of the loggers changed by this function will be
|
The levels of the loggers changed by this function will be
|
||||||
restored to their initial values at the end of the test.
|
restored to their initial values at the end of the test.
|
||||||
|
|
||||||
|
Will enable the requested logging level if it was disabled via :meth:`logging.disable`.
|
||||||
|
|
||||||
:param level: The level.
|
:param level: The level.
|
||||||
:param logger: The logger to update. If not given, the root logger.
|
:param logger: The logger to update. If not given, the root logger.
|
||||||
"""
|
"""
|
||||||
|
@ -470,6 +535,9 @@ class LogCaptureFixture:
|
||||||
if self._initial_handler_level is None:
|
if self._initial_handler_level is None:
|
||||||
self._initial_handler_level = self.handler.level
|
self._initial_handler_level = self.handler.level
|
||||||
self.handler.setLevel(level)
|
self.handler.setLevel(level)
|
||||||
|
initial_disabled_logging_level = self._force_enable_logging(level, logger_obj)
|
||||||
|
if self._initial_disabled_logging_level is None:
|
||||||
|
self._initial_disabled_logging_level = initial_disabled_logging_level
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def at_level(
|
def at_level(
|
||||||
|
@ -479,6 +547,8 @@ class LogCaptureFixture:
|
||||||
the end of the 'with' statement the level is restored to its original
|
the end of the 'with' statement the level is restored to its original
|
||||||
value.
|
value.
|
||||||
|
|
||||||
|
Will enable the requested logging level if it was disabled via :meth:`logging.disable`.
|
||||||
|
|
||||||
:param level: The level.
|
:param level: The level.
|
||||||
:param logger: The logger to update. If not given, the root logger.
|
:param logger: The logger to update. If not given, the root logger.
|
||||||
"""
|
"""
|
||||||
|
@ -487,11 +557,13 @@ class LogCaptureFixture:
|
||||||
logger_obj.setLevel(level)
|
logger_obj.setLevel(level)
|
||||||
handler_orig_level = self.handler.level
|
handler_orig_level = self.handler.level
|
||||||
self.handler.setLevel(level)
|
self.handler.setLevel(level)
|
||||||
|
original_disable_level = self._force_enable_logging(level, logger_obj)
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
logger_obj.setLevel(orig_level)
|
logger_obj.setLevel(orig_level)
|
||||||
self.handler.setLevel(handler_orig_level)
|
self.handler.setLevel(handler_orig_level)
|
||||||
|
logging.disable(original_disable_level)
|
||||||
|
|
||||||
|
|
||||||
@fixture
|
@fixture
|
||||||
|
@ -577,7 +649,7 @@ class LoggingPlugin:
|
||||||
config, "log_file_date_format", "log_date_format"
|
config, "log_file_date_format", "log_date_format"
|
||||||
)
|
)
|
||||||
|
|
||||||
log_file_formatter = logging.Formatter(
|
log_file_formatter = DatetimeFormatter(
|
||||||
log_file_format, datefmt=log_file_date_format
|
log_file_format, datefmt=log_file_date_format
|
||||||
)
|
)
|
||||||
self.log_file_handler.setFormatter(log_file_formatter)
|
self.log_file_handler.setFormatter(log_file_formatter)
|
||||||
|
@ -621,7 +693,7 @@ class LoggingPlugin:
|
||||||
create_terminal_writer(self._config), log_format, log_date_format
|
create_terminal_writer(self._config), log_format, log_date_format
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
formatter = logging.Formatter(log_format, log_date_format)
|
formatter = DatetimeFormatter(log_format, log_date_format)
|
||||||
|
|
||||||
formatter._style = PercentStyleMultiline(
|
formatter._style = PercentStyleMultiline(
|
||||||
formatter._style._fmt, auto_indent=auto_indent
|
formatter._style._fmt, auto_indent=auto_indent
|
||||||
|
|
|
@ -9,10 +9,12 @@ import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
from typing import final
|
||||||
from typing import FrozenSet
|
from typing import FrozenSet
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from typing import overload
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from typing import Set
|
from typing import Set
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
@ -22,8 +24,6 @@ from typing import Union
|
||||||
|
|
||||||
import _pytest._code
|
import _pytest._code
|
||||||
from _pytest import nodes
|
from _pytest import nodes
|
||||||
from _pytest.compat import final
|
|
||||||
from _pytest.compat import overload
|
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config import directory_arg
|
from _pytest.config import directory_arg
|
||||||
from _pytest.config import ExitCode
|
from _pytest.config import ExitCode
|
||||||
|
@ -122,11 +122,12 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
)
|
)
|
||||||
group._addoption(
|
group._addoption(
|
||||||
"-c",
|
"-c",
|
||||||
metavar="file",
|
"--config-file",
|
||||||
|
metavar="FILE",
|
||||||
type=str,
|
type=str,
|
||||||
dest="inifilename",
|
dest="inifilename",
|
||||||
help="Load configuration from `file` instead of trying to locate one of the "
|
help="Load configuration from `FILE` instead of trying to locate one of the "
|
||||||
"implicit configuration files",
|
"implicit configuration files.",
|
||||||
)
|
)
|
||||||
group._addoption(
|
group._addoption(
|
||||||
"--continue-on-collection-errors",
|
"--continue-on-collection-errors",
|
||||||
|
@ -399,6 +400,12 @@ def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[boo
|
||||||
allow_in_venv = config.getoption("collect_in_virtualenv")
|
allow_in_venv = config.getoption("collect_in_virtualenv")
|
||||||
if not allow_in_venv and _in_venv(collection_path):
|
if not allow_in_venv and _in_venv(collection_path):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
if collection_path.is_dir():
|
||||||
|
norecursepatterns = config.getini("norecursedirs")
|
||||||
|
if any(fnmatch_ex(pat, collection_path) for pat in norecursepatterns):
|
||||||
|
return True
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@ -562,9 +569,6 @@ class Session(nodes.FSCollector):
|
||||||
ihook = self.gethookproxy(fspath.parent)
|
ihook = self.gethookproxy(fspath.parent)
|
||||||
if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
|
if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
|
||||||
return False
|
return False
|
||||||
norecursepatterns = self.config.getini("norecursedirs")
|
|
||||||
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
|
|
||||||
return False
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _collectfile(
|
def _collectfile(
|
||||||
|
@ -685,8 +689,8 @@ class Session(nodes.FSCollector):
|
||||||
# are not collected more than once.
|
# are not collected more than once.
|
||||||
matchnodes_cache: Dict[Tuple[Type[nodes.Collector], str], CollectReport] = {}
|
matchnodes_cache: Dict[Tuple[Type[nodes.Collector], str], CollectReport] = {}
|
||||||
|
|
||||||
# Dirnames of pkgs with dunder-init files.
|
# Directories of pkgs with dunder-init files.
|
||||||
pkg_roots: Dict[str, Package] = {}
|
pkg_roots: Dict[Path, Package] = {}
|
||||||
|
|
||||||
for argpath, names in self._initial_parts:
|
for argpath, names in self._initial_parts:
|
||||||
self.trace("processing argument", (argpath, names))
|
self.trace("processing argument", (argpath, names))
|
||||||
|
@ -707,7 +711,7 @@ class Session(nodes.FSCollector):
|
||||||
col = self._collectfile(pkginit, handle_dupes=False)
|
col = self._collectfile(pkginit, handle_dupes=False)
|
||||||
if col:
|
if col:
|
||||||
if isinstance(col[0], Package):
|
if isinstance(col[0], Package):
|
||||||
pkg_roots[str(parent)] = col[0]
|
pkg_roots[parent] = col[0]
|
||||||
node_cache1[col[0].path] = [col[0]]
|
node_cache1[col[0].path] = [col[0]]
|
||||||
|
|
||||||
# If it's a directory argument, recurse and look for any Subpackages.
|
# If it's a directory argument, recurse and look for any Subpackages.
|
||||||
|
@ -716,7 +720,7 @@ class Session(nodes.FSCollector):
|
||||||
assert not names, f"invalid arg {(argpath, names)!r}"
|
assert not names, f"invalid arg {(argpath, names)!r}"
|
||||||
|
|
||||||
seen_dirs: Set[Path] = set()
|
seen_dirs: Set[Path] = set()
|
||||||
for direntry in visit(str(argpath), self._recurse):
|
for direntry in visit(argpath, self._recurse):
|
||||||
if not direntry.is_file():
|
if not direntry.is_file():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -731,8 +735,8 @@ class Session(nodes.FSCollector):
|
||||||
for x in self._collectfile(pkginit):
|
for x in self._collectfile(pkginit):
|
||||||
yield x
|
yield x
|
||||||
if isinstance(x, Package):
|
if isinstance(x, Package):
|
||||||
pkg_roots[str(dirpath)] = x
|
pkg_roots[dirpath] = x
|
||||||
if str(dirpath) in pkg_roots:
|
if dirpath in pkg_roots:
|
||||||
# Do not collect packages here.
|
# Do not collect packages here.
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -749,7 +753,7 @@ class Session(nodes.FSCollector):
|
||||||
if argpath in node_cache1:
|
if argpath in node_cache1:
|
||||||
col = node_cache1[argpath]
|
col = node_cache1[argpath]
|
||||||
else:
|
else:
|
||||||
collect_root = pkg_roots.get(str(argpath.parent), self)
|
collect_root = pkg_roots.get(argpath.parent, self)
|
||||||
col = collect_root._collectfile(argpath, handle_dupes=False)
|
col = collect_root._collectfile(argpath, handle_dupes=False)
|
||||||
if col:
|
if col:
|
||||||
node_cache1[argpath] = col
|
node_cache1[argpath] = col
|
||||||
|
|
|
@ -26,7 +26,6 @@ from typing import NoReturn
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Expression",
|
"Expression",
|
||||||
"ParseError",
|
"ParseError",
|
||||||
|
@ -132,7 +131,7 @@ IDENT_PREFIX = "$"
|
||||||
|
|
||||||
def expression(s: Scanner) -> ast.Expression:
|
def expression(s: Scanner) -> ast.Expression:
|
||||||
if s.accept(TokenType.EOF):
|
if s.accept(TokenType.EOF):
|
||||||
ret: ast.expr = ast.NameConstant(False)
|
ret: ast.expr = ast.Constant(False)
|
||||||
else:
|
else:
|
||||||
ret = expr(s)
|
ret = expr(s)
|
||||||
s.accept(TokenType.EOF, reject=True)
|
s.accept(TokenType.EOF, reject=True)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import warnings
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Collection
|
from typing import Collection
|
||||||
|
from typing import final
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
from typing import List
|
from typing import List
|
||||||
|
@ -23,11 +24,11 @@ from typing import Union
|
||||||
|
|
||||||
from .._code import getfslineno
|
from .._code import getfslineno
|
||||||
from ..compat import ascii_escaped
|
from ..compat import ascii_escaped
|
||||||
from ..compat import final
|
|
||||||
from ..compat import NOTSET
|
from ..compat import NOTSET
|
||||||
from ..compat import NotSetType
|
from ..compat import NotSetType
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.deprecated import check_ispytest
|
from _pytest.deprecated import check_ispytest
|
||||||
|
from _pytest.deprecated import MARKED_FIXTURE
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
from _pytest.warning_types import PytestUnknownMarkWarning
|
from _pytest.warning_types import PytestUnknownMarkWarning
|
||||||
|
|
||||||
|
@ -412,6 +413,12 @@ def store_mark(obj, mark: Mark) -> None:
|
||||||
This is used to implement the Mark declarations/decorators correctly.
|
This is used to implement the Mark declarations/decorators correctly.
|
||||||
"""
|
"""
|
||||||
assert isinstance(mark, Mark), mark
|
assert isinstance(mark, Mark), mark
|
||||||
|
|
||||||
|
from ..fixtures import getfixturemarker
|
||||||
|
|
||||||
|
if getfixturemarker(obj) is not None:
|
||||||
|
warnings.warn(MARKED_FIXTURE, stacklevel=2)
|
||||||
|
|
||||||
# Always reassign name to avoid updating pytestmark in a reference that
|
# Always reassign name to avoid updating pytestmark in a reference that
|
||||||
# was only borrowed.
|
# was only borrowed.
|
||||||
obj.pytestmark = [*get_unpacked_marks(obj, consider_mro=False), mark]
|
obj.pytestmark = [*get_unpacked_marks(obj, consider_mro=False), mark]
|
||||||
|
|
|
@ -5,8 +5,10 @@ import sys
|
||||||
import warnings
|
import warnings
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from typing import final
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from typing import Mapping
|
||||||
from typing import MutableMapping
|
from typing import MutableMapping
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import overload
|
from typing import overload
|
||||||
|
@ -14,7 +16,6 @@ from typing import Tuple
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
from _pytest.compat import final
|
|
||||||
from _pytest.fixtures import fixture
|
from _pytest.fixtures import fixture
|
||||||
from _pytest.warning_types import PytestWarning
|
from _pytest.warning_types import PytestWarning
|
||||||
|
|
||||||
|
@ -129,7 +130,7 @@ class MonkeyPatch:
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._setattr: List[Tuple[object, str, object]] = []
|
self._setattr: List[Tuple[object, str, object]] = []
|
||||||
self._setitem: List[Tuple[MutableMapping[Any, Any], object, object]] = []
|
self._setitem: List[Tuple[Mapping[Any, Any], object, object]] = []
|
||||||
self._cwd: Optional[str] = None
|
self._cwd: Optional[str] = None
|
||||||
self._savesyspath: Optional[List[str]] = None
|
self._savesyspath: Optional[List[str]] = None
|
||||||
|
|
||||||
|
@ -290,12 +291,13 @@ class MonkeyPatch:
|
||||||
self._setattr.append((target, name, oldval))
|
self._setattr.append((target, name, oldval))
|
||||||
delattr(target, name)
|
delattr(target, name)
|
||||||
|
|
||||||
def setitem(self, dic: MutableMapping[K, V], name: K, value: V) -> None:
|
def setitem(self, dic: Mapping[K, V], name: K, value: V) -> None:
|
||||||
"""Set dictionary entry ``name`` to value."""
|
"""Set dictionary entry ``name`` to value."""
|
||||||
self._setitem.append((dic, name, dic.get(name, notset)))
|
self._setitem.append((dic, name, dic.get(name, notset)))
|
||||||
dic[name] = value
|
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
|
||||||
|
dic[name] = value # type: ignore[index]
|
||||||
|
|
||||||
def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> None:
|
def delitem(self, dic: Mapping[K, V], name: K, raising: bool = True) -> None:
|
||||||
"""Delete ``name`` from dict.
|
"""Delete ``name`` from dict.
|
||||||
|
|
||||||
Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to
|
Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to
|
||||||
|
@ -306,7 +308,8 @@ class MonkeyPatch:
|
||||||
raise KeyError(name)
|
raise KeyError(name)
|
||||||
else:
|
else:
|
||||||
self._setitem.append((dic, name, dic.get(name, notset)))
|
self._setitem.append((dic, name, dic.get(name, notset)))
|
||||||
del dic[name]
|
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
|
||||||
|
del dic[name] # type: ignore[attr-defined]
|
||||||
|
|
||||||
def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None:
|
def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None:
|
||||||
"""Set environment variable ``name`` to ``value``.
|
"""Set environment variable ``name`` to ``value``.
|
||||||
|
@ -401,11 +404,13 @@ class MonkeyPatch:
|
||||||
for dictionary, key, value in reversed(self._setitem):
|
for dictionary, key, value in reversed(self._setitem):
|
||||||
if value is notset:
|
if value is notset:
|
||||||
try:
|
try:
|
||||||
del dictionary[key]
|
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
|
||||||
|
del dictionary[key] # type: ignore[attr-defined]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass # Was already deleted, so we have the desired state.
|
pass # Was already deleted, so we have the desired state.
|
||||||
else:
|
else:
|
||||||
dictionary[key] = value
|
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
|
||||||
|
dictionary[key] = value # type: ignore[index]
|
||||||
self._setitem[:] = []
|
self._setitem[:] = []
|
||||||
if self._savesyspath is not None:
|
if self._savesyspath is not None:
|
||||||
sys.path[:] = self._savesyspath
|
sys.path[:] = self._savesyspath
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import os
|
import os
|
||||||
import warnings
|
import warnings
|
||||||
|
from functools import cached_property
|
||||||
from inspect import signature
|
from inspect import signature
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
@ -22,7 +23,7 @@ import _pytest._code
|
||||||
from _pytest._code import getfslineno
|
from _pytest._code import getfslineno
|
||||||
from _pytest._code.code import ExceptionInfo
|
from _pytest._code.code import ExceptionInfo
|
||||||
from _pytest._code.code import TerminalRepr
|
from _pytest._code.code import TerminalRepr
|
||||||
from _pytest.compat import cached_property
|
from _pytest._code.code import Traceback
|
||||||
from _pytest.compat import LEGACY_PATH
|
from _pytest.compat import LEGACY_PATH
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config import ConftestImportFailure
|
from _pytest.config import ConftestImportFailure
|
||||||
|
@ -432,8 +433,8 @@ class Node(metaclass=NodeMeta):
|
||||||
assert current is None or isinstance(current, cls)
|
assert current is None or isinstance(current, cls)
|
||||||
return current
|
return current
|
||||||
|
|
||||||
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
|
def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
|
||||||
pass
|
return excinfo.traceback
|
||||||
|
|
||||||
def _repr_failure_py(
|
def _repr_failure_py(
|
||||||
self,
|
self,
|
||||||
|
@ -449,10 +450,13 @@ class Node(metaclass=NodeMeta):
|
||||||
style = "value"
|
style = "value"
|
||||||
if isinstance(excinfo.value, FixtureLookupError):
|
if isinstance(excinfo.value, FixtureLookupError):
|
||||||
return excinfo.value.formatrepr()
|
return excinfo.value.formatrepr()
|
||||||
|
|
||||||
|
tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]]
|
||||||
if self.config.getoption("fulltrace", False):
|
if self.config.getoption("fulltrace", False):
|
||||||
style = "long"
|
style = "long"
|
||||||
|
tbfilter = False
|
||||||
else:
|
else:
|
||||||
self._prunetraceback(excinfo)
|
tbfilter = self._traceback_filter
|
||||||
if style == "auto":
|
if style == "auto":
|
||||||
style = "long"
|
style = "long"
|
||||||
# XXX should excinfo.getrepr record all data and toterminal() process it?
|
# XXX should excinfo.getrepr record all data and toterminal() process it?
|
||||||
|
@ -483,7 +487,7 @@ class Node(metaclass=NodeMeta):
|
||||||
abspath=abspath,
|
abspath=abspath,
|
||||||
showlocals=self.config.getoption("showlocals", False),
|
showlocals=self.config.getoption("showlocals", False),
|
||||||
style=style,
|
style=style,
|
||||||
tbfilter=False, # pruned already, or in --fulltrace mode.
|
tbfilter=tbfilter,
|
||||||
truncate_locals=truncate_locals,
|
truncate_locals=truncate_locals,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -554,13 +558,14 @@ class Collector(Node):
|
||||||
|
|
||||||
return self._repr_failure_py(excinfo, style=tbstyle)
|
return self._repr_failure_py(excinfo, style=tbstyle)
|
||||||
|
|
||||||
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
|
def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
|
||||||
if hasattr(self, "path"):
|
if hasattr(self, "path"):
|
||||||
traceback = excinfo.traceback
|
traceback = excinfo.traceback
|
||||||
ntraceback = traceback.cut(path=self.path)
|
ntraceback = traceback.cut(path=self.path)
|
||||||
if ntraceback == traceback:
|
if ntraceback == traceback:
|
||||||
ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
|
ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
|
||||||
excinfo.traceback = ntraceback.filter()
|
return excinfo.traceback.filter(excinfo)
|
||||||
|
return excinfo.traceback
|
||||||
|
|
||||||
|
|
||||||
def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[str]:
|
def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[str]:
|
||||||
|
|
|
@ -7,23 +7,12 @@ from typing import Callable
|
||||||
from typing import cast
|
from typing import cast
|
||||||
from typing import NoReturn
|
from typing import NoReturn
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from typing import Protocol
|
||||||
from typing import Type
|
from typing import Type
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
|
|
||||||
from _pytest.deprecated import KEYWORD_MSG_ARG
|
from _pytest.deprecated import KEYWORD_MSG_ARG
|
||||||
|
|
||||||
TYPE_CHECKING = False # Avoid circular import through compat.
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from typing_extensions import Protocol
|
|
||||||
else:
|
|
||||||
# typing.Protocol is only available starting from Python 3.8. It is also
|
|
||||||
# available from typing_extensions, but we don't want a runtime dependency
|
|
||||||
# on that. So use a dummy runtime implementation.
|
|
||||||
from typing import Generic
|
|
||||||
|
|
||||||
Protocol = Generic
|
|
||||||
|
|
||||||
|
|
||||||
class OutcomeException(BaseException):
|
class OutcomeException(BaseException):
|
||||||
"""OutcomeException and its subclass instances indicate and contain info
|
"""OutcomeException and its subclass instances indicate and contain info
|
||||||
|
|
|
@ -27,6 +27,7 @@ from typing import Callable
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Set
|
from typing import Set
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
@ -522,6 +523,8 @@ def import_path(
|
||||||
|
|
||||||
if mode is ImportMode.importlib:
|
if mode is ImportMode.importlib:
|
||||||
module_name = module_name_from_path(path, root)
|
module_name = module_name_from_path(path, root)
|
||||||
|
with contextlib.suppress(KeyError):
|
||||||
|
return sys.modules[module_name]
|
||||||
|
|
||||||
for meta_importer in sys.meta_path:
|
for meta_importer in sys.meta_path:
|
||||||
spec = meta_importer.find_spec(module_name, [str(path.parent)])
|
spec = meta_importer.find_spec(module_name, [str(path.parent)])
|
||||||
|
@ -632,6 +635,9 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) ->
|
||||||
otherwise "src.tests.test_foo" is not importable by ``__import__``.
|
otherwise "src.tests.test_foo" is not importable by ``__import__``.
|
||||||
"""
|
"""
|
||||||
module_parts = module_name.split(".")
|
module_parts = module_name.split(".")
|
||||||
|
child_module: Union[ModuleType, None] = None
|
||||||
|
module: Union[ModuleType, None] = None
|
||||||
|
child_name: str = ""
|
||||||
while module_name:
|
while module_name:
|
||||||
if module_name not in modules:
|
if module_name not in modules:
|
||||||
try:
|
try:
|
||||||
|
@ -641,13 +647,22 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) ->
|
||||||
# ourselves to fall back to creating a dummy module.
|
# ourselves to fall back to creating a dummy module.
|
||||||
if not sys.meta_path:
|
if not sys.meta_path:
|
||||||
raise ModuleNotFoundError
|
raise ModuleNotFoundError
|
||||||
importlib.import_module(module_name)
|
module = importlib.import_module(module_name)
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
module = ModuleType(
|
module = ModuleType(
|
||||||
module_name,
|
module_name,
|
||||||
doc="Empty module created by pytest's importmode=importlib.",
|
doc="Empty module created by pytest's importmode=importlib.",
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
module = modules[module_name]
|
||||||
|
if child_module:
|
||||||
|
# Add child attribute to the parent that can reference the child
|
||||||
|
# modules.
|
||||||
|
if not hasattr(module, child_name):
|
||||||
|
setattr(module, child_name, child_module)
|
||||||
modules[module_name] = module
|
modules[module_name] = module
|
||||||
|
# Keep track of the child module while moving up the tree.
|
||||||
|
child_module, child_name = module, module_name.rpartition(".")[-1]
|
||||||
module_parts.pop(-1)
|
module_parts.pop(-1)
|
||||||
module_name = ".".join(module_parts)
|
module_name = ".".join(module_parts)
|
||||||
|
|
||||||
|
@ -669,30 +684,38 @@ def resolve_package_path(path: Path) -> Optional[Path]:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def scandir(path: Union[str, "os.PathLike[str]"]) -> List["os.DirEntry[str]"]:
|
||||||
|
"""Scan a directory recursively, in breadth-first order.
|
||||||
|
|
||||||
|
The returned entries are sorted.
|
||||||
|
"""
|
||||||
|
entries = []
|
||||||
|
with os.scandir(path) as s:
|
||||||
|
# Skip entries with symlink loops and other brokenness, so the caller
|
||||||
|
# doesn't have to deal with it.
|
||||||
|
for entry in s:
|
||||||
|
try:
|
||||||
|
entry.is_file()
|
||||||
|
except OSError as err:
|
||||||
|
if _ignore_error(err):
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
entries.append(entry)
|
||||||
|
entries.sort(key=lambda entry: entry.name)
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
def visit(
|
def visit(
|
||||||
path: Union[str, "os.PathLike[str]"], recurse: Callable[["os.DirEntry[str]"], bool]
|
path: Union[str, "os.PathLike[str]"], recurse: Callable[["os.DirEntry[str]"], bool]
|
||||||
) -> Iterator["os.DirEntry[str]"]:
|
) -> Iterator["os.DirEntry[str]"]:
|
||||||
"""Walk a directory recursively, in breadth-first order.
|
"""Walk a directory recursively, in breadth-first order.
|
||||||
|
|
||||||
|
The `recurse` predicate determines whether a directory is recursed.
|
||||||
|
|
||||||
Entries at each directory level are sorted.
|
Entries at each directory level are sorted.
|
||||||
"""
|
"""
|
||||||
|
entries = scandir(path)
|
||||||
# Skip entries with symlink loops and other brokenness, so the caller doesn't
|
|
||||||
# have to deal with it.
|
|
||||||
entries = []
|
|
||||||
for entry in os.scandir(path):
|
|
||||||
try:
|
|
||||||
entry.is_file()
|
|
||||||
except OSError as err:
|
|
||||||
if _ignore_error(err):
|
|
||||||
continue
|
|
||||||
raise
|
|
||||||
entries.append(entry)
|
|
||||||
|
|
||||||
entries.sort(key=lambda entry: entry.name)
|
|
||||||
|
|
||||||
yield from entries
|
yield from entries
|
||||||
|
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
if entry.is_dir() and recurse(entry):
|
if entry.is_dir() and recurse(entry):
|
||||||
yield from visit(entry.path, recurse)
|
yield from visit(entry.path, recurse)
|
||||||
|
|
|
@ -6,6 +6,7 @@ import collections.abc
|
||||||
import contextlib
|
import contextlib
|
||||||
import gc
|
import gc
|
||||||
import importlib
|
import importlib
|
||||||
|
import locale
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import re
|
import re
|
||||||
|
@ -19,10 +20,13 @@ from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
from typing import Final
|
||||||
|
from typing import final
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import IO
|
from typing import IO
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from typing import Literal
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import overload
|
from typing import overload
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
|
@ -39,7 +43,6 @@ from iniconfig import SectionWrapper
|
||||||
from _pytest import timing
|
from _pytest import timing
|
||||||
from _pytest._code import Source
|
from _pytest._code import Source
|
||||||
from _pytest.capture import _get_multicapture
|
from _pytest.capture import _get_multicapture
|
||||||
from _pytest.compat import final
|
|
||||||
from _pytest.compat import NOTSET
|
from _pytest.compat import NOTSET
|
||||||
from _pytest.compat import NotSetType
|
from _pytest.compat import NotSetType
|
||||||
from _pytest.config import _PluggyPlugin
|
from _pytest.config import _PluggyPlugin
|
||||||
|
@ -67,11 +70,7 @@ from _pytest.reports import TestReport
|
||||||
from _pytest.tmpdir import TempPathFactory
|
from _pytest.tmpdir import TempPathFactory
|
||||||
from _pytest.warning_types import PytestWarning
|
from _pytest.warning_types import PytestWarning
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing_extensions import Final
|
|
||||||
from typing_extensions import Literal
|
|
||||||
|
|
||||||
import pexpect
|
import pexpect
|
||||||
|
|
||||||
|
|
||||||
|
@ -129,6 +128,7 @@ class LsofFdLeakChecker:
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
check=True,
|
check=True,
|
||||||
text=True,
|
text=True,
|
||||||
|
encoding=locale.getpreferredencoding(False),
|
||||||
).stdout
|
).stdout
|
||||||
|
|
||||||
def isopen(line: str) -> bool:
|
def isopen(line: str) -> bool:
|
||||||
|
|
|
@ -15,6 +15,7 @@ from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
from typing import final
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
|
@ -35,11 +36,11 @@ from _pytest._code import filter_traceback
|
||||||
from _pytest._code import getfslineno
|
from _pytest._code import getfslineno
|
||||||
from _pytest._code.code import ExceptionInfo
|
from _pytest._code.code import ExceptionInfo
|
||||||
from _pytest._code.code import TerminalRepr
|
from _pytest._code.code import TerminalRepr
|
||||||
|
from _pytest._code.code import Traceback
|
||||||
from _pytest._io import TerminalWriter
|
from _pytest._io import TerminalWriter
|
||||||
from _pytest._io.saferepr import saferepr
|
from _pytest._io.saferepr import saferepr
|
||||||
from _pytest.compat import ascii_escaped
|
from _pytest.compat import ascii_escaped
|
||||||
from _pytest.compat import assert_never
|
from _pytest.compat import assert_never
|
||||||
from _pytest.compat import final
|
|
||||||
from _pytest.compat import get_default_arg_names
|
from _pytest.compat import get_default_arg_names
|
||||||
from _pytest.compat import get_real_func
|
from _pytest.compat import get_real_func
|
||||||
from _pytest.compat import getimfunc
|
from _pytest.compat import getimfunc
|
||||||
|
@ -56,7 +57,6 @@ from _pytest.config import ExitCode
|
||||||
from _pytest.config import hookimpl
|
from _pytest.config import hookimpl
|
||||||
from _pytest.config.argparsing import Parser
|
from _pytest.config.argparsing import Parser
|
||||||
from _pytest.deprecated import check_ispytest
|
from _pytest.deprecated import check_ispytest
|
||||||
from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
|
|
||||||
from _pytest.deprecated import INSTANCE_COLLECTOR
|
from _pytest.deprecated import INSTANCE_COLLECTOR
|
||||||
from _pytest.deprecated import NOSE_SUPPORT_METHOD
|
from _pytest.deprecated import NOSE_SUPPORT_METHOD
|
||||||
from _pytest.fixtures import FuncFixtureInfo
|
from _pytest.fixtures import FuncFixtureInfo
|
||||||
|
@ -667,7 +667,7 @@ class Package(Module):
|
||||||
config=None,
|
config=None,
|
||||||
session=None,
|
session=None,
|
||||||
nodeid=None,
|
nodeid=None,
|
||||||
path=Optional[Path],
|
path: Optional[Path] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
# NOTE: Could be just the following, but kept as-is for compat.
|
# NOTE: Could be just the following, but kept as-is for compat.
|
||||||
# nodes.FSCollector.__init__(self, fspath, parent=parent)
|
# nodes.FSCollector.__init__(self, fspath, parent=parent)
|
||||||
|
@ -699,14 +699,6 @@ class Package(Module):
|
||||||
func = partial(_call_with_optional_argument, teardown_module, self.obj)
|
func = partial(_call_with_optional_argument, teardown_module, self.obj)
|
||||||
self.addfinalizer(func)
|
self.addfinalizer(func)
|
||||||
|
|
||||||
def gethookproxy(self, fspath: "os.PathLike[str]"):
|
|
||||||
warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
|
|
||||||
return self.session.gethookproxy(fspath)
|
|
||||||
|
|
||||||
def isinitpath(self, path: Union[str, "os.PathLike[str]"]) -> bool:
|
|
||||||
warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
|
|
||||||
return self.session.isinitpath(path)
|
|
||||||
|
|
||||||
def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
|
def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
|
||||||
if direntry.name == "__pycache__":
|
if direntry.name == "__pycache__":
|
||||||
return False
|
return False
|
||||||
|
@ -714,9 +706,6 @@ class Package(Module):
|
||||||
ihook = self.session.gethookproxy(fspath.parent)
|
ihook = self.session.gethookproxy(fspath.parent)
|
||||||
if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
|
if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
|
||||||
return False
|
return False
|
||||||
norecursepatterns = self.config.getini("norecursedirs")
|
|
||||||
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
|
|
||||||
return False
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _collectfile(
|
def _collectfile(
|
||||||
|
@ -745,11 +734,13 @@ class Package(Module):
|
||||||
|
|
||||||
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
|
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
|
||||||
this_path = self.path.parent
|
this_path = self.path.parent
|
||||||
init_module = this_path / "__init__.py"
|
|
||||||
if init_module.is_file() and path_matches_patterns(
|
# Always collect the __init__ first.
|
||||||
init_module, self.config.getini("python_files")
|
if self.session.isinitpath(self.path) or path_matches_patterns(
|
||||||
|
self.path, self.config.getini("python_files")
|
||||||
):
|
):
|
||||||
yield Module.from_parent(self, path=init_module)
|
yield Module.from_parent(self, path=self.path)
|
||||||
|
|
||||||
pkg_prefixes: Set[Path] = set()
|
pkg_prefixes: Set[Path] = set()
|
||||||
for direntry in visit(str(this_path), recurse=self._recurse):
|
for direntry in visit(str(this_path), recurse=self._recurse):
|
||||||
path = Path(direntry.path)
|
path = Path(direntry.path)
|
||||||
|
@ -1801,7 +1792,7 @@ class Function(PyobjMixin, nodes.Item):
|
||||||
def setup(self) -> None:
|
def setup(self) -> None:
|
||||||
self._request._fillfixtures()
|
self._request._fillfixtures()
|
||||||
|
|
||||||
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
|
def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
|
||||||
if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False):
|
if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False):
|
||||||
code = _pytest._code.Code.from_function(get_real_func(self.obj))
|
code = _pytest._code.Code.from_function(get_real_func(self.obj))
|
||||||
path, firstlineno = code.path, code.firstlineno
|
path, firstlineno = code.path, code.firstlineno
|
||||||
|
@ -1813,14 +1804,21 @@ class Function(PyobjMixin, nodes.Item):
|
||||||
ntraceback = ntraceback.filter(filter_traceback)
|
ntraceback = ntraceback.filter(filter_traceback)
|
||||||
if not ntraceback:
|
if not ntraceback:
|
||||||
ntraceback = traceback
|
ntraceback = traceback
|
||||||
|
ntraceback = ntraceback.filter(excinfo)
|
||||||
|
|
||||||
excinfo.traceback = ntraceback.filter()
|
|
||||||
# issue364: mark all but first and last frames to
|
# issue364: mark all but first and last frames to
|
||||||
# only show a single-line message for each frame.
|
# only show a single-line message for each frame.
|
||||||
if self.config.getoption("tbstyle", "auto") == "auto":
|
if self.config.getoption("tbstyle", "auto") == "auto":
|
||||||
if len(excinfo.traceback) > 2:
|
if len(ntraceback) > 2:
|
||||||
for entry in excinfo.traceback[1:-1]:
|
ntraceback = Traceback(
|
||||||
entry.set_repr_style("short")
|
entry
|
||||||
|
if i == 0 or i == len(ntraceback) - 1
|
||||||
|
else entry.with_repr_style("short")
|
||||||
|
for i, entry in enumerate(ntraceback)
|
||||||
|
)
|
||||||
|
|
||||||
|
return ntraceback
|
||||||
|
return excinfo.traceback
|
||||||
|
|
||||||
# TODO: Type ignored -- breaks Liskov Substitution.
|
# TODO: Type ignored -- breaks Liskov Substitution.
|
||||||
def repr_failure( # type: ignore[override]
|
def repr_failure( # type: ignore[override]
|
||||||
|
|
|
@ -9,9 +9,11 @@ from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import cast
|
from typing import cast
|
||||||
from typing import ContextManager
|
from typing import ContextManager
|
||||||
|
from typing import final
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Mapping
|
from typing import Mapping
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from typing import overload
|
||||||
from typing import Pattern
|
from typing import Pattern
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
@ -20,17 +22,14 @@ from typing import TYPE_CHECKING
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
import _pytest._code
|
||||||
|
from _pytest.compat import STRING_TYPES
|
||||||
|
from _pytest.outcomes import fail
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from numpy import ndarray
|
from numpy import ndarray
|
||||||
|
|
||||||
|
|
||||||
import _pytest._code
|
|
||||||
from _pytest.compat import final
|
|
||||||
from _pytest.compat import STRING_TYPES
|
|
||||||
from _pytest.compat import overload
|
|
||||||
from _pytest.outcomes import fail
|
|
||||||
|
|
||||||
|
|
||||||
def _non_numeric_type_error(value, at: Optional[str]) -> TypeError:
|
def _non_numeric_type_error(value, at: Optional[str]) -> TypeError:
|
||||||
at_str = f" at {at}" if at else ""
|
at_str = f" at {at}" if at else ""
|
||||||
return TypeError(
|
return TypeError(
|
||||||
|
|
|
@ -5,18 +5,18 @@ from pprint import pformat
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
from typing import final
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from typing import overload
|
||||||
from typing import Pattern
|
from typing import Pattern
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
from typing import Type
|
from typing import Type
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
from _pytest.compat import final
|
|
||||||
from _pytest.compat import overload
|
|
||||||
from _pytest.deprecated import check_ispytest
|
from _pytest.deprecated import check_ispytest
|
||||||
from _pytest.deprecated import WARNS_NONE_ARG
|
from _pytest.deprecated import WARNS_NONE_ARG
|
||||||
from _pytest.fixtures import fixture
|
from _pytest.fixtures import fixture
|
||||||
|
@ -117,10 +117,10 @@ def warns( # noqa: F811
|
||||||
warning of that class or classes.
|
warning of that class or classes.
|
||||||
|
|
||||||
This helper produces a list of :class:`warnings.WarningMessage` objects, one for
|
This helper produces a list of :class:`warnings.WarningMessage` objects, one for
|
||||||
each warning raised (regardless of whether it is an ``expected_warning`` or not).
|
each warning emitted (regardless of whether it is an ``expected_warning`` or not).
|
||||||
|
Since pytest 8.0, unmatched warnings are also re-emitted when the context closes.
|
||||||
|
|
||||||
This function can be used as a context manager, which will capture all the raised
|
This function can be used as a context manager::
|
||||||
warnings inside it::
|
|
||||||
|
|
||||||
>>> import pytest
|
>>> import pytest
|
||||||
>>> with pytest.warns(RuntimeWarning):
|
>>> with pytest.warns(RuntimeWarning):
|
||||||
|
@ -135,8 +135,9 @@ def warns( # noqa: F811
|
||||||
>>> with pytest.warns(UserWarning, match=r'must be \d+$'):
|
>>> with pytest.warns(UserWarning, match=r'must be \d+$'):
|
||||||
... warnings.warn("value must be 42", UserWarning)
|
... warnings.warn("value must be 42", UserWarning)
|
||||||
|
|
||||||
>>> with pytest.warns(UserWarning, match=r'must be \d+$'):
|
>>> with pytest.warns(UserWarning): # catch re-emitted warning
|
||||||
... warnings.warn("this is not here", UserWarning)
|
... with pytest.warns(UserWarning, match=r'must be \d+$'):
|
||||||
|
... warnings.warn("this is not here", UserWarning)
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted...
|
Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted...
|
||||||
|
@ -277,6 +278,12 @@ class WarningsChecker(WarningsRecorder):
|
||||||
self.expected_warning = expected_warning_tup
|
self.expected_warning = expected_warning_tup
|
||||||
self.match_expr = match_expr
|
self.match_expr = match_expr
|
||||||
|
|
||||||
|
def matches(self, warning: warnings.WarningMessage) -> bool:
|
||||||
|
assert self.expected_warning is not None
|
||||||
|
return issubclass(warning.category, self.expected_warning) and bool(
|
||||||
|
self.match_expr is None or re.search(self.match_expr, str(warning.message))
|
||||||
|
)
|
||||||
|
|
||||||
def __exit__(
|
def __exit__(
|
||||||
self,
|
self,
|
||||||
exc_type: Optional[Type[BaseException]],
|
exc_type: Optional[Type[BaseException]],
|
||||||
|
@ -287,27 +294,34 @@ class WarningsChecker(WarningsRecorder):
|
||||||
|
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
|
|
||||||
|
if self.expected_warning is None:
|
||||||
|
# nothing to do in this deprecated case, see WARNS_NONE_ARG above
|
||||||
|
return
|
||||||
|
|
||||||
def found_str():
|
def found_str():
|
||||||
return pformat([record.message for record in self], indent=2)
|
return pformat([record.message for record in self], indent=2)
|
||||||
|
|
||||||
# only check if we're not currently handling an exception
|
try:
|
||||||
if exc_type is None and exc_val is None and exc_tb is None:
|
if not any(issubclass(w.category, self.expected_warning) for w in self):
|
||||||
if self.expected_warning is not None:
|
fail(
|
||||||
if not any(issubclass(r.category, self.expected_warning) for r in self):
|
f"DID NOT WARN. No warnings of type {self.expected_warning} were emitted.\n"
|
||||||
__tracebackhide__ = True
|
f" Emitted warnings: {found_str()}."
|
||||||
fail(
|
)
|
||||||
f"DID NOT WARN. No warnings of type {self.expected_warning} were emitted.\n"
|
elif not any(self.matches(w) for w in self):
|
||||||
f"The list of emitted warnings is: {found_str()}."
|
fail(
|
||||||
|
f"DID NOT WARN. No warnings of type {self.expected_warning} matching the regex were emitted.\n"
|
||||||
|
f" Regex: {self.match_expr}\n"
|
||||||
|
f" Emitted warnings: {found_str()}."
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
# Whether or not any warnings matched, we want to re-emit all unmatched warnings.
|
||||||
|
for w in self:
|
||||||
|
if not self.matches(w):
|
||||||
|
warnings.warn_explicit(
|
||||||
|
str(w.message),
|
||||||
|
w.message.__class__, # type: ignore[arg-type]
|
||||||
|
w.filename,
|
||||||
|
w.lineno,
|
||||||
|
module=w.__module__,
|
||||||
|
source=w.source,
|
||||||
)
|
)
|
||||||
elif self.match_expr is not None:
|
|
||||||
for r in self:
|
|
||||||
if issubclass(r.category, self.expected_warning):
|
|
||||||
if re.compile(self.match_expr).search(str(r.message)):
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
fail(
|
|
||||||
f"""\
|
|
||||||
DID NOT WARN. No warnings of type {self.expected_warning} matching the regex were emitted.
|
|
||||||
Regex: {self.match_expr}
|
|
||||||
Emitted warnings: {found_str()}"""
|
|
||||||
)
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ from pprint import pprint
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import cast
|
from typing import cast
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
from typing import final
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
from typing import List
|
from typing import List
|
||||||
|
@ -29,7 +30,6 @@ from _pytest._code.code import ReprLocals
|
||||||
from _pytest._code.code import ReprTraceback
|
from _pytest._code.code import ReprTraceback
|
||||||
from _pytest._code.code import TerminalRepr
|
from _pytest._code.code import TerminalRepr
|
||||||
from _pytest._io import TerminalWriter
|
from _pytest._io import TerminalWriter
|
||||||
from _pytest.compat import final
|
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.nodes import Collector
|
from _pytest.nodes import Collector
|
||||||
from _pytest.nodes import Item
|
from _pytest.nodes import Item
|
||||||
|
|
|
@ -6,6 +6,7 @@ import sys
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import cast
|
from typing import cast
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
from typing import final
|
||||||
from typing import Generic
|
from typing import Generic
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
@ -23,7 +24,6 @@ from _pytest import timing
|
||||||
from _pytest._code.code import ExceptionChainRepr
|
from _pytest._code.code import ExceptionChainRepr
|
||||||
from _pytest._code.code import ExceptionInfo
|
from _pytest._code.code import ExceptionInfo
|
||||||
from _pytest._code.code import TerminalRepr
|
from _pytest._code.code import TerminalRepr
|
||||||
from _pytest.compat import final
|
|
||||||
from _pytest.config.argparsing import Parser
|
from _pytest.config.argparsing import Parser
|
||||||
from _pytest.deprecated import check_ispytest
|
from _pytest.deprecated import check_ispytest
|
||||||
from _pytest.nodes import Collector
|
from _pytest.nodes import Collector
|
||||||
|
|
|
@ -18,9 +18,11 @@ from typing import Callable
|
||||||
from typing import cast
|
from typing import cast
|
||||||
from typing import ClassVar
|
from typing import ClassVar
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
from typing import final
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Mapping
|
from typing import Mapping
|
||||||
|
from typing import NamedTuple
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from typing import Set
|
from typing import Set
|
||||||
|
@ -39,7 +41,6 @@ from _pytest._code.code import ExceptionRepr
|
||||||
from _pytest._io import TerminalWriter
|
from _pytest._io import TerminalWriter
|
||||||
from _pytest._io.wcwidth import wcswidth
|
from _pytest._io.wcwidth import wcswidth
|
||||||
from _pytest.assertion.util import running_on_ci
|
from _pytest.assertion.util import running_on_ci
|
||||||
from _pytest.compat import final
|
|
||||||
from _pytest.config import _PluggyPlugin
|
from _pytest.config import _PluggyPlugin
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config import ExitCode
|
from _pytest.config import ExitCode
|
||||||
|
@ -112,6 +113,26 @@ class MoreQuietAction(argparse.Action):
|
||||||
namespace.quiet = getattr(namespace, "quiet", 0) + 1
|
namespace.quiet = getattr(namespace, "quiet", 0) + 1
|
||||||
|
|
||||||
|
|
||||||
|
class TestShortLogReport(NamedTuple):
|
||||||
|
"""Used to store the test status result category, shortletter and verbose word.
|
||||||
|
For example ``"rerun", "R", ("RERUN", {"yellow": True})``.
|
||||||
|
|
||||||
|
:ivar category:
|
||||||
|
The class of result, for example ``“passed”``, ``“skipped”``, ``“error”``, or the empty string.
|
||||||
|
|
||||||
|
:ivar letter:
|
||||||
|
The short letter shown as testing progresses, for example ``"."``, ``"s"``, ``"E"``, or the empty string.
|
||||||
|
|
||||||
|
:ivar word:
|
||||||
|
Verbose word is shown as testing progresses in verbose mode, for example ``"PASSED"``, ``"SKIPPED"``,
|
||||||
|
``"ERROR"``, or the empty string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
category: str
|
||||||
|
letter: str
|
||||||
|
word: Union[str, Tuple[str, Mapping[str, bool]]]
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser: Parser) -> None:
|
def pytest_addoption(parser: Parser) -> None:
|
||||||
group = parser.getgroup("terminal reporting", "Reporting", after="general")
|
group = parser.getgroup("terminal reporting", "Reporting", after="general")
|
||||||
group._addoption(
|
group._addoption(
|
||||||
|
@ -548,10 +569,11 @@ class TerminalReporter:
|
||||||
def pytest_runtest_logreport(self, report: TestReport) -> None:
|
def pytest_runtest_logreport(self, report: TestReport) -> None:
|
||||||
self._tests_ran = True
|
self._tests_ran = True
|
||||||
rep = report
|
rep = report
|
||||||
res: Tuple[
|
|
||||||
str, str, Union[str, Tuple[str, Mapping[str, bool]]]
|
res = TestShortLogReport(
|
||||||
] = self.config.hook.pytest_report_teststatus(report=rep, config=self.config)
|
*self.config.hook.pytest_report_teststatus(report=rep, config=self.config)
|
||||||
category, letter, word = res
|
)
|
||||||
|
category, letter, word = res.category, res.letter, res.word
|
||||||
if not isinstance(word, tuple):
|
if not isinstance(word, tuple):
|
||||||
markup = None
|
markup = None
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -7,38 +7,32 @@ from pathlib import Path
|
||||||
from shutil import rmtree
|
from shutil import rmtree
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
from typing import final
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
|
from typing import Literal
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
from _pytest.nodes import Item
|
from .pathlib import cleanup_dead_symlinks
|
||||||
from _pytest.reports import CollectReport
|
|
||||||
from _pytest.stash import StashKey
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from typing_extensions import Literal
|
|
||||||
|
|
||||||
RetentionType = Literal["all", "failed", "none"]
|
|
||||||
|
|
||||||
|
|
||||||
from _pytest.config.argparsing import Parser
|
|
||||||
|
|
||||||
from .pathlib import LOCK_TIMEOUT
|
from .pathlib import LOCK_TIMEOUT
|
||||||
from .pathlib import make_numbered_dir
|
from .pathlib import make_numbered_dir
|
||||||
from .pathlib import make_numbered_dir_with_cleanup
|
from .pathlib import make_numbered_dir_with_cleanup
|
||||||
from .pathlib import rm_rf
|
from .pathlib import rm_rf
|
||||||
from .pathlib import cleanup_dead_symlinks
|
from _pytest.compat import get_user_id
|
||||||
from _pytest.compat import final, get_user_id
|
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config import ExitCode
|
from _pytest.config import ExitCode
|
||||||
from _pytest.config import hookimpl
|
from _pytest.config import hookimpl
|
||||||
|
from _pytest.config.argparsing import Parser
|
||||||
from _pytest.deprecated import check_ispytest
|
from _pytest.deprecated import check_ispytest
|
||||||
from _pytest.fixtures import fixture
|
from _pytest.fixtures import fixture
|
||||||
from _pytest.fixtures import FixtureRequest
|
from _pytest.fixtures import FixtureRequest
|
||||||
from _pytest.monkeypatch import MonkeyPatch
|
from _pytest.monkeypatch import MonkeyPatch
|
||||||
|
from _pytest.nodes import Item
|
||||||
|
from _pytest.reports import CollectReport
|
||||||
|
from _pytest.stash import StashKey
|
||||||
|
|
||||||
tmppath_result_key = StashKey[Dict[str, bool]]()
|
tmppath_result_key = StashKey[Dict[str, bool]]()
|
||||||
|
RetentionType = Literal["all", "failed", "none"]
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
|
@ -100,7 +94,7 @@ class TempPathFactory:
|
||||||
policy = config.getini("tmp_path_retention_policy")
|
policy = config.getini("tmp_path_retention_policy")
|
||||||
if policy not in ("all", "failed", "none"):
|
if policy not in ("all", "failed", "none"):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"tmp_path_retention_policy must be either all, failed, none. Current intput: {policy}."
|
f"tmp_path_retention_policy must be either all, failed, none. Current input: {policy}."
|
||||||
)
|
)
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
|
|
|
@ -334,15 +334,16 @@ class TestCaseFunction(Function):
|
||||||
finally:
|
finally:
|
||||||
delattr(self._testcase, self.name)
|
delattr(self._testcase, self.name)
|
||||||
|
|
||||||
def _prunetraceback(
|
def _traceback_filter(
|
||||||
self, excinfo: _pytest._code.ExceptionInfo[BaseException]
|
self, excinfo: _pytest._code.ExceptionInfo[BaseException]
|
||||||
) -> None:
|
) -> _pytest._code.Traceback:
|
||||||
super()._prunetraceback(excinfo)
|
traceback = super()._traceback_filter(excinfo)
|
||||||
traceback = excinfo.traceback.filter(
|
ntraceback = traceback.filter(
|
||||||
lambda x: not x.frame.f_globals.get("__unittest")
|
lambda x: not x.frame.f_globals.get("__unittest"),
|
||||||
)
|
)
|
||||||
if traceback:
|
if not ntraceback:
|
||||||
excinfo.traceback = traceback
|
ntraceback = traceback
|
||||||
|
return ntraceback
|
||||||
|
|
||||||
|
|
||||||
@hookimpl(tryfirst=True)
|
@hookimpl(tryfirst=True)
|
||||||
|
|
|
@ -3,12 +3,11 @@ import inspect
|
||||||
import warnings
|
import warnings
|
||||||
from types import FunctionType
|
from types import FunctionType
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from typing import final
|
||||||
from typing import Generic
|
from typing import Generic
|
||||||
from typing import Type
|
from typing import Type
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
|
|
||||||
from _pytest.compat import final
|
|
||||||
|
|
||||||
|
|
||||||
class PytestWarning(UserWarning):
|
class PytestWarning(UserWarning):
|
||||||
"""Base class for all warnings emitted by pytest."""
|
"""Base class for all warnings emitted by pytest."""
|
||||||
|
@ -56,6 +55,12 @@ class PytestRemovedIn8Warning(PytestDeprecationWarning):
|
||||||
__module__ = "pytest"
|
__module__ = "pytest"
|
||||||
|
|
||||||
|
|
||||||
|
class PytestRemovedIn9Warning(PytestDeprecationWarning):
|
||||||
|
"""Warning class for features that will be removed in pytest 9."""
|
||||||
|
|
||||||
|
__module__ = "pytest"
|
||||||
|
|
||||||
|
|
||||||
class PytestReturnNotNoneWarning(PytestRemovedIn8Warning):
|
class PytestReturnNotNoneWarning(PytestRemovedIn8Warning):
|
||||||
"""Warning emitted when a test function is returning value other than None."""
|
"""Warning emitted when a test function is returning value other than None."""
|
||||||
|
|
||||||
|
@ -149,7 +154,7 @@ def warn_explicit_for(method: FunctionType, message: PytestWarning) -> None:
|
||||||
"""
|
"""
|
||||||
Issue the warning :param:`message` for the definition of the given :param:`method`
|
Issue the warning :param:`message` for the definition of the given :param:`method`
|
||||||
|
|
||||||
this helps to log warnigns for functions defined prior to finding an issue with them
|
this helps to log warnings for functions defined prior to finding an issue with them
|
||||||
(like hook wrappers being marked in a legacy mechanism)
|
(like hook wrappers being marked in a legacy mechanism)
|
||||||
"""
|
"""
|
||||||
lineno = method.__code__.co_firstlineno
|
lineno = method.__code__.co_firstlineno
|
||||||
|
|
|
@ -49,6 +49,8 @@ def catch_warnings_for_item(
|
||||||
warnings.filterwarnings("always", category=DeprecationWarning)
|
warnings.filterwarnings("always", category=DeprecationWarning)
|
||||||
warnings.filterwarnings("always", category=PendingDeprecationWarning)
|
warnings.filterwarnings("always", category=PendingDeprecationWarning)
|
||||||
|
|
||||||
|
warnings.filterwarnings("error", category=pytest.PytestRemovedIn8Warning)
|
||||||
|
|
||||||
apply_warning_filters(config_filters, cmdline_filters)
|
apply_warning_filters(config_filters, cmdline_filters)
|
||||||
|
|
||||||
# apply filters from "filterwarnings" marks
|
# apply filters from "filterwarnings" marks
|
||||||
|
|
|
@ -62,6 +62,7 @@ from _pytest.reports import TestReport
|
||||||
from _pytest.runner import CallInfo
|
from _pytest.runner import CallInfo
|
||||||
from _pytest.stash import Stash
|
from _pytest.stash import Stash
|
||||||
from _pytest.stash import StashKey
|
from _pytest.stash import StashKey
|
||||||
|
from _pytest.terminal import TestShortLogReport
|
||||||
from _pytest.tmpdir import TempPathFactory
|
from _pytest.tmpdir import TempPathFactory
|
||||||
from _pytest.warning_types import PytestAssertRewriteWarning
|
from _pytest.warning_types import PytestAssertRewriteWarning
|
||||||
from _pytest.warning_types import PytestCacheWarning
|
from _pytest.warning_types import PytestCacheWarning
|
||||||
|
@ -70,6 +71,7 @@ from _pytest.warning_types import PytestConfigWarning
|
||||||
from _pytest.warning_types import PytestDeprecationWarning
|
from _pytest.warning_types import PytestDeprecationWarning
|
||||||
from _pytest.warning_types import PytestExperimentalApiWarning
|
from _pytest.warning_types import PytestExperimentalApiWarning
|
||||||
from _pytest.warning_types import PytestRemovedIn8Warning
|
from _pytest.warning_types import PytestRemovedIn8Warning
|
||||||
|
from _pytest.warning_types import PytestRemovedIn9Warning
|
||||||
from _pytest.warning_types import PytestReturnNotNoneWarning
|
from _pytest.warning_types import PytestReturnNotNoneWarning
|
||||||
from _pytest.warning_types import PytestUnhandledCoroutineWarning
|
from _pytest.warning_types import PytestUnhandledCoroutineWarning
|
||||||
from _pytest.warning_types import PytestUnhandledThreadExceptionWarning
|
from _pytest.warning_types import PytestUnhandledThreadExceptionWarning
|
||||||
|
@ -130,6 +132,7 @@ __all__ = [
|
||||||
"PytestDeprecationWarning",
|
"PytestDeprecationWarning",
|
||||||
"PytestExperimentalApiWarning",
|
"PytestExperimentalApiWarning",
|
||||||
"PytestRemovedIn8Warning",
|
"PytestRemovedIn8Warning",
|
||||||
|
"PytestRemovedIn9Warning",
|
||||||
"PytestReturnNotNoneWarning",
|
"PytestReturnNotNoneWarning",
|
||||||
"Pytester",
|
"Pytester",
|
||||||
"PytestPluginManager",
|
"PytestPluginManager",
|
||||||
|
@ -152,6 +155,7 @@ __all__ = [
|
||||||
"TempPathFactory",
|
"TempPathFactory",
|
||||||
"Testdir",
|
"Testdir",
|
||||||
"TestReport",
|
"TestReport",
|
||||||
|
"TestShortLogReport",
|
||||||
"UsageError",
|
"UsageError",
|
||||||
"WarningsRecorder",
|
"WarningsRecorder",
|
||||||
"warns",
|
"warns",
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
import contextlib
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import warnings
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -9,6 +11,14 @@ from py import error
|
||||||
from py.path import local
|
from py.path import local
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def ignore_encoding_warning():
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
with contextlib.suppress(NameError): # new in 3.10
|
||||||
|
warnings.simplefilter("ignore", EncodingWarning)
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
class CommonFSTests:
|
class CommonFSTests:
|
||||||
def test_constructor_equality(self, path1):
|
def test_constructor_equality(self, path1):
|
||||||
p = path1.__class__(path1)
|
p = path1.__class__(path1)
|
||||||
|
@ -223,7 +233,8 @@ class CommonFSTests:
|
||||||
assert not (path1 < path1)
|
assert not (path1 < path1)
|
||||||
|
|
||||||
def test_simple_read(self, path1):
|
def test_simple_read(self, path1):
|
||||||
x = path1.join("samplefile").read("r")
|
with ignore_encoding_warning():
|
||||||
|
x = path1.join("samplefile").read("r")
|
||||||
assert x == "samplefile\n"
|
assert x == "samplefile\n"
|
||||||
|
|
||||||
def test_join_div_operator(self, path1):
|
def test_join_div_operator(self, path1):
|
||||||
|
@ -265,12 +276,14 @@ class CommonFSTests:
|
||||||
|
|
||||||
def test_readlines(self, path1):
|
def test_readlines(self, path1):
|
||||||
fn = path1.join("samplefile")
|
fn = path1.join("samplefile")
|
||||||
contents = fn.readlines()
|
with ignore_encoding_warning():
|
||||||
|
contents = fn.readlines()
|
||||||
assert contents == ["samplefile\n"]
|
assert contents == ["samplefile\n"]
|
||||||
|
|
||||||
def test_readlines_nocr(self, path1):
|
def test_readlines_nocr(self, path1):
|
||||||
fn = path1.join("samplefile")
|
fn = path1.join("samplefile")
|
||||||
contents = fn.readlines(cr=0)
|
with ignore_encoding_warning():
|
||||||
|
contents = fn.readlines(cr=0)
|
||||||
assert contents == ["samplefile", ""]
|
assert contents == ["samplefile", ""]
|
||||||
|
|
||||||
def test_file(self, path1):
|
def test_file(self, path1):
|
||||||
|
@ -362,8 +375,8 @@ class CommonFSTests:
|
||||||
initpy.copy(copied)
|
initpy.copy(copied)
|
||||||
try:
|
try:
|
||||||
assert copied.check()
|
assert copied.check()
|
||||||
s1 = initpy.read()
|
s1 = initpy.read_text(encoding="utf-8")
|
||||||
s2 = copied.read()
|
s2 = copied.read_text(encoding="utf-8")
|
||||||
assert s1 == s2
|
assert s1 == s2
|
||||||
finally:
|
finally:
|
||||||
if copied.check():
|
if copied.check():
|
||||||
|
@ -376,8 +389,8 @@ class CommonFSTests:
|
||||||
otherdir.copy(copied)
|
otherdir.copy(copied)
|
||||||
assert copied.check(dir=1)
|
assert copied.check(dir=1)
|
||||||
assert copied.join("__init__.py").check(file=1)
|
assert copied.join("__init__.py").check(file=1)
|
||||||
s1 = otherdir.join("__init__.py").read()
|
s1 = otherdir.join("__init__.py").read_text(encoding="utf-8")
|
||||||
s2 = copied.join("__init__.py").read()
|
s2 = copied.join("__init__.py").read_text(encoding="utf-8")
|
||||||
assert s1 == s2
|
assert s1 == s2
|
||||||
finally:
|
finally:
|
||||||
if copied.check(dir=1):
|
if copied.check(dir=1):
|
||||||
|
@ -463,13 +476,13 @@ def setuptestfs(path):
|
||||||
return
|
return
|
||||||
# print "setting up test fs for", repr(path)
|
# print "setting up test fs for", repr(path)
|
||||||
samplefile = path.ensure("samplefile")
|
samplefile = path.ensure("samplefile")
|
||||||
samplefile.write("samplefile\n")
|
samplefile.write_text("samplefile\n", encoding="utf-8")
|
||||||
|
|
||||||
execfile = path.ensure("execfile")
|
execfile = path.ensure("execfile")
|
||||||
execfile.write("x=42")
|
execfile.write_text("x=42", encoding="utf-8")
|
||||||
|
|
||||||
execfilepy = path.ensure("execfile.py")
|
execfilepy = path.ensure("execfile.py")
|
||||||
execfilepy.write("x=42")
|
execfilepy.write_text("x=42", encoding="utf-8")
|
||||||
|
|
||||||
d = {1: 2, "hello": "world", "answer": 42}
|
d = {1: 2, "hello": "world", "answer": 42}
|
||||||
path.ensure("samplepickle").dump(d)
|
path.ensure("samplepickle").dump(d)
|
||||||
|
@ -481,22 +494,24 @@ def setuptestfs(path):
|
||||||
otherdir.ensure("__init__.py")
|
otherdir.ensure("__init__.py")
|
||||||
|
|
||||||
module_a = otherdir.ensure("a.py")
|
module_a = otherdir.ensure("a.py")
|
||||||
module_a.write("from .b import stuff as result\n")
|
module_a.write_text("from .b import stuff as result\n", encoding="utf-8")
|
||||||
module_b = otherdir.ensure("b.py")
|
module_b = otherdir.ensure("b.py")
|
||||||
module_b.write('stuff="got it"\n')
|
module_b.write_text('stuff="got it"\n', encoding="utf-8")
|
||||||
module_c = otherdir.ensure("c.py")
|
module_c = otherdir.ensure("c.py")
|
||||||
module_c.write(
|
module_c.write_text(
|
||||||
"""import py;
|
"""import py;
|
||||||
import otherdir.a
|
import otherdir.a
|
||||||
value = otherdir.a.result
|
value = otherdir.a.result
|
||||||
"""
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
module_d = otherdir.ensure("d.py")
|
module_d = otherdir.ensure("d.py")
|
||||||
module_d.write(
|
module_d.write_text(
|
||||||
"""import py;
|
"""import py;
|
||||||
from otherdir import a
|
from otherdir import a
|
||||||
value2 = a.result
|
value2 = a.result
|
||||||
"""
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -534,9 +549,11 @@ def batch_make_numbered_dirs(rootdir, repeats):
|
||||||
for i in range(repeats):
|
for i in range(repeats):
|
||||||
dir_ = local.make_numbered_dir(prefix="repro-", rootdir=rootdir)
|
dir_ = local.make_numbered_dir(prefix="repro-", rootdir=rootdir)
|
||||||
file_ = dir_.join("foo")
|
file_ = dir_.join("foo")
|
||||||
file_.write("%s" % i)
|
file_.write_text("%s" % i, encoding="utf-8")
|
||||||
actual = int(file_.read())
|
actual = int(file_.read_text(encoding="utf-8"))
|
||||||
assert actual == i, f"int(file_.read()) is {actual} instead of {i}"
|
assert (
|
||||||
|
actual == i
|
||||||
|
), f"int(file_.read_text(encoding='utf-8')) is {actual} instead of {i}"
|
||||||
dir_.join(".lock").remove(ignore_errors=True)
|
dir_.join(".lock").remove(ignore_errors=True)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -692,14 +709,14 @@ class TestLocalPath(CommonFSTests):
|
||||||
|
|
||||||
def test_open_and_ensure(self, path1):
|
def test_open_and_ensure(self, path1):
|
||||||
p = path1.join("sub1", "sub2", "file")
|
p = path1.join("sub1", "sub2", "file")
|
||||||
with p.open("w", ensure=1) as f:
|
with p.open("w", ensure=1, encoding="utf-8") as f:
|
||||||
f.write("hello")
|
f.write("hello")
|
||||||
assert p.read() == "hello"
|
assert p.read_text(encoding="utf-8") == "hello"
|
||||||
|
|
||||||
def test_write_and_ensure(self, path1):
|
def test_write_and_ensure(self, path1):
|
||||||
p = path1.join("sub1", "sub2", "file")
|
p = path1.join("sub1", "sub2", "file")
|
||||||
p.write("hello", ensure=1)
|
p.write_text("hello", ensure=1, encoding="utf-8")
|
||||||
assert p.read() == "hello"
|
assert p.read_text(encoding="utf-8") == "hello"
|
||||||
|
|
||||||
@pytest.mark.parametrize("bin", (False, True))
|
@pytest.mark.parametrize("bin", (False, True))
|
||||||
def test_dump(self, tmpdir, bin):
|
def test_dump(self, tmpdir, bin):
|
||||||
|
@ -770,9 +787,9 @@ class TestLocalPath(CommonFSTests):
|
||||||
newfile = tmpdir.join("test1", "test")
|
newfile = tmpdir.join("test1", "test")
|
||||||
newfile.ensure()
|
newfile.ensure()
|
||||||
assert newfile.check(file=1)
|
assert newfile.check(file=1)
|
||||||
newfile.write("42")
|
newfile.write_text("42", encoding="utf-8")
|
||||||
newfile.ensure()
|
newfile.ensure()
|
||||||
s = newfile.read()
|
s = newfile.read_text(encoding="utf-8")
|
||||||
assert s == "42"
|
assert s == "42"
|
||||||
|
|
||||||
def test_ensure_filepath_withoutdir(self, tmpdir):
|
def test_ensure_filepath_withoutdir(self, tmpdir):
|
||||||
|
@ -806,9 +823,9 @@ class TestLocalPath(CommonFSTests):
|
||||||
newfilename = "/test" * 60 # type:ignore[unreachable]
|
newfilename = "/test" * 60 # type:ignore[unreachable]
|
||||||
l1 = tmpdir.join(newfilename)
|
l1 = tmpdir.join(newfilename)
|
||||||
l1.ensure(file=True)
|
l1.ensure(file=True)
|
||||||
l1.write("foo")
|
l1.write_text("foo", encoding="utf-8")
|
||||||
l2 = tmpdir.join(newfilename)
|
l2 = tmpdir.join(newfilename)
|
||||||
assert l2.read() == "foo"
|
assert l2.read_text(encoding="utf-8") == "foo"
|
||||||
|
|
||||||
def test_visit_depth_first(self, tmpdir):
|
def test_visit_depth_first(self, tmpdir):
|
||||||
tmpdir.ensure("a", "1")
|
tmpdir.ensure("a", "1")
|
||||||
|
@ -1278,14 +1295,14 @@ class TestPOSIXLocalPath:
|
||||||
def test_hardlink(self, tmpdir):
|
def test_hardlink(self, tmpdir):
|
||||||
linkpath = tmpdir.join("test")
|
linkpath = tmpdir.join("test")
|
||||||
filepath = tmpdir.join("file")
|
filepath = tmpdir.join("file")
|
||||||
filepath.write("Hello")
|
filepath.write_text("Hello", encoding="utf-8")
|
||||||
nlink = filepath.stat().nlink
|
nlink = filepath.stat().nlink
|
||||||
linkpath.mklinkto(filepath)
|
linkpath.mklinkto(filepath)
|
||||||
assert filepath.stat().nlink == nlink + 1
|
assert filepath.stat().nlink == nlink + 1
|
||||||
|
|
||||||
def test_symlink_are_identical(self, tmpdir):
|
def test_symlink_are_identical(self, tmpdir):
|
||||||
filepath = tmpdir.join("file")
|
filepath = tmpdir.join("file")
|
||||||
filepath.write("Hello")
|
filepath.write_text("Hello", encoding="utf-8")
|
||||||
linkpath = tmpdir.join("test")
|
linkpath = tmpdir.join("test")
|
||||||
linkpath.mksymlinkto(filepath)
|
linkpath.mksymlinkto(filepath)
|
||||||
assert linkpath.readlink() == str(filepath)
|
assert linkpath.readlink() == str(filepath)
|
||||||
|
@ -1293,7 +1310,7 @@ class TestPOSIXLocalPath:
|
||||||
def test_symlink_isfile(self, tmpdir):
|
def test_symlink_isfile(self, tmpdir):
|
||||||
linkpath = tmpdir.join("test")
|
linkpath = tmpdir.join("test")
|
||||||
filepath = tmpdir.join("file")
|
filepath = tmpdir.join("file")
|
||||||
filepath.write("")
|
filepath.write_text("", encoding="utf-8")
|
||||||
linkpath.mksymlinkto(filepath)
|
linkpath.mksymlinkto(filepath)
|
||||||
assert linkpath.check(file=1)
|
assert linkpath.check(file=1)
|
||||||
assert not linkpath.check(link=0, file=1)
|
assert not linkpath.check(link=0, file=1)
|
||||||
|
@ -1302,10 +1319,12 @@ class TestPOSIXLocalPath:
|
||||||
def test_symlink_relative(self, tmpdir):
|
def test_symlink_relative(self, tmpdir):
|
||||||
linkpath = tmpdir.join("test")
|
linkpath = tmpdir.join("test")
|
||||||
filepath = tmpdir.join("file")
|
filepath = tmpdir.join("file")
|
||||||
filepath.write("Hello")
|
filepath.write_text("Hello", encoding="utf-8")
|
||||||
linkpath.mksymlinkto(filepath, absolute=False)
|
linkpath.mksymlinkto(filepath, absolute=False)
|
||||||
assert linkpath.readlink() == "file"
|
assert linkpath.readlink() == "file"
|
||||||
assert filepath.read() == linkpath.read()
|
assert filepath.read_text(encoding="utf-8") == linkpath.read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
def test_symlink_not_existing(self, tmpdir):
|
def test_symlink_not_existing(self, tmpdir):
|
||||||
linkpath = tmpdir.join("testnotexisting")
|
linkpath = tmpdir.join("testnotexisting")
|
||||||
|
@ -1338,7 +1357,7 @@ class TestPOSIXLocalPath:
|
||||||
def test_realpath_file(self, tmpdir):
|
def test_realpath_file(self, tmpdir):
|
||||||
linkpath = tmpdir.join("test")
|
linkpath = tmpdir.join("test")
|
||||||
filepath = tmpdir.join("file")
|
filepath = tmpdir.join("file")
|
||||||
filepath.write("")
|
filepath.write_text("", encoding="utf-8")
|
||||||
linkpath.mksymlinkto(filepath)
|
linkpath.mksymlinkto(filepath)
|
||||||
realpath = linkpath.realpath()
|
realpath = linkpath.realpath()
|
||||||
assert realpath.basename == "file"
|
assert realpath.basename == "file"
|
||||||
|
@ -1383,7 +1402,7 @@ class TestPOSIXLocalPath:
|
||||||
atime1 = path.atime()
|
atime1 = path.atime()
|
||||||
# we could wait here but timer resolution is very
|
# we could wait here but timer resolution is very
|
||||||
# system dependent
|
# system dependent
|
||||||
path.read()
|
path.read_binary()
|
||||||
time.sleep(ATIME_RESOLUTION)
|
time.sleep(ATIME_RESOLUTION)
|
||||||
atime2 = path.atime()
|
atime2 = path.atime()
|
||||||
time.sleep(ATIME_RESOLUTION)
|
time.sleep(ATIME_RESOLUTION)
|
||||||
|
@ -1467,7 +1486,7 @@ class TestPOSIXLocalPath:
|
||||||
test_files = ["a", "b", "c"]
|
test_files = ["a", "b", "c"]
|
||||||
src = tmpdir.join("src")
|
src = tmpdir.join("src")
|
||||||
for f in test_files:
|
for f in test_files:
|
||||||
src.join(f).write(f, ensure=True)
|
src.join(f).write_text(f, ensure=True, encoding="utf-8")
|
||||||
dst = tmpdir.join("dst")
|
dst = tmpdir.join("dst")
|
||||||
# a small delay before the copy
|
# a small delay before the copy
|
||||||
time.sleep(ATIME_RESOLUTION)
|
time.sleep(ATIME_RESOLUTION)
|
||||||
|
@ -1521,10 +1540,11 @@ class TestUnicodePy2Py3:
|
||||||
def test_read_write(self, tmpdir):
|
def test_read_write(self, tmpdir):
|
||||||
x = tmpdir.join("hello")
|
x = tmpdir.join("hello")
|
||||||
part = "hällo"
|
part = "hällo"
|
||||||
x.write(part)
|
with ignore_encoding_warning():
|
||||||
assert x.read() == part
|
x.write(part)
|
||||||
x.write(part.encode(sys.getdefaultencoding()))
|
assert x.read() == part
|
||||||
assert x.read() == part.encode(sys.getdefaultencoding())
|
x.write(part.encode(sys.getdefaultencoding()))
|
||||||
|
assert x.read() == part.encode(sys.getdefaultencoding())
|
||||||
|
|
||||||
|
|
||||||
class TestBinaryAndTextMethods:
|
class TestBinaryAndTextMethods:
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
import importlib.metadata
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest.compat import importlib_metadata
|
|
||||||
from _pytest.config import ExitCode
|
from _pytest.config import ExitCode
|
||||||
from _pytest.pathlib import symlink_or_skip
|
from _pytest.pathlib import symlink_or_skip
|
||||||
from _pytest.pytester import Pytester
|
from _pytest.pytester import Pytester
|
||||||
|
@ -139,7 +139,7 @@ class TestGeneralUsage:
|
||||||
def my_dists():
|
def my_dists():
|
||||||
return (DummyDist(entry_points),)
|
return (DummyDist(entry_points),)
|
||||||
|
|
||||||
monkeypatch.setattr(importlib_metadata, "distributions", my_dists)
|
monkeypatch.setattr(importlib.metadata, "distributions", my_dists)
|
||||||
params = ("-p", "mycov") if load_cov_early else ()
|
params = ("-p", "mycov") if load_cov_early else ()
|
||||||
pytester.runpytest_inprocess(*params)
|
pytester.runpytest_inprocess(*params)
|
||||||
if load_cov_early:
|
if load_cov_early:
|
||||||
|
@ -267,7 +267,7 @@ class TestGeneralUsage:
|
||||||
def test_issue109_sibling_conftests_not_loaded(self, pytester: Pytester) -> None:
|
def test_issue109_sibling_conftests_not_loaded(self, pytester: Pytester) -> None:
|
||||||
sub1 = pytester.mkdir("sub1")
|
sub1 = pytester.mkdir("sub1")
|
||||||
sub2 = pytester.mkdir("sub2")
|
sub2 = pytester.mkdir("sub2")
|
||||||
sub1.joinpath("conftest.py").write_text("assert 0")
|
sub1.joinpath("conftest.py").write_text("assert 0", encoding="utf-8")
|
||||||
result = pytester.runpytest(sub2)
|
result = pytester.runpytest(sub2)
|
||||||
assert result.ret == ExitCode.NO_TESTS_COLLECTED
|
assert result.ret == ExitCode.NO_TESTS_COLLECTED
|
||||||
sub2.joinpath("__init__.py").touch()
|
sub2.joinpath("__init__.py").touch()
|
||||||
|
@ -467,7 +467,7 @@ class TestGeneralUsage:
|
||||||
assert "invalid" in str(excinfo.value)
|
assert "invalid" in str(excinfo.value)
|
||||||
|
|
||||||
p = pytester.path.joinpath("test_test_plugins_given_as_strings.py")
|
p = pytester.path.joinpath("test_test_plugins_given_as_strings.py")
|
||||||
p.write_text("def test_foo(): pass")
|
p.write_text("def test_foo(): pass", encoding="utf-8")
|
||||||
mod = types.ModuleType("myplugin")
|
mod = types.ModuleType("myplugin")
|
||||||
monkeypatch.setitem(sys.modules, "myplugin", mod)
|
monkeypatch.setitem(sys.modules, "myplugin", mod)
|
||||||
assert pytest.main(args=[str(pytester.path)], plugins=["myplugin"]) == 0
|
assert pytest.main(args=[str(pytester.path)], plugins=["myplugin"]) == 0
|
||||||
|
@ -587,7 +587,7 @@ class TestInvocationVariants:
|
||||||
def test_pyargs_importerror(self, pytester: Pytester, monkeypatch) -> None:
|
def test_pyargs_importerror(self, pytester: Pytester, monkeypatch) -> None:
|
||||||
monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", False)
|
monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", False)
|
||||||
path = pytester.mkpydir("tpkg")
|
path = pytester.mkpydir("tpkg")
|
||||||
path.joinpath("test_hello.py").write_text("raise ImportError")
|
path.joinpath("test_hello.py").write_text("raise ImportError", encoding="utf-8")
|
||||||
|
|
||||||
result = pytester.runpytest("--pyargs", "tpkg.test_hello", syspathinsert=True)
|
result = pytester.runpytest("--pyargs", "tpkg.test_hello", syspathinsert=True)
|
||||||
assert result.ret != 0
|
assert result.ret != 0
|
||||||
|
@ -597,10 +597,10 @@ class TestInvocationVariants:
|
||||||
def test_pyargs_only_imported_once(self, pytester: Pytester) -> None:
|
def test_pyargs_only_imported_once(self, pytester: Pytester) -> None:
|
||||||
pkg = pytester.mkpydir("foo")
|
pkg = pytester.mkpydir("foo")
|
||||||
pkg.joinpath("test_foo.py").write_text(
|
pkg.joinpath("test_foo.py").write_text(
|
||||||
"print('hello from test_foo')\ndef test(): pass"
|
"print('hello from test_foo')\ndef test(): pass", encoding="utf-8"
|
||||||
)
|
)
|
||||||
pkg.joinpath("conftest.py").write_text(
|
pkg.joinpath("conftest.py").write_text(
|
||||||
"def pytest_configure(config): print('configuring')"
|
"def pytest_configure(config): print('configuring')", encoding="utf-8"
|
||||||
)
|
)
|
||||||
|
|
||||||
result = pytester.runpytest(
|
result = pytester.runpytest(
|
||||||
|
@ -613,7 +613,7 @@ class TestInvocationVariants:
|
||||||
|
|
||||||
def test_pyargs_filename_looks_like_module(self, pytester: Pytester) -> None:
|
def test_pyargs_filename_looks_like_module(self, pytester: Pytester) -> None:
|
||||||
pytester.path.joinpath("conftest.py").touch()
|
pytester.path.joinpath("conftest.py").touch()
|
||||||
pytester.path.joinpath("t.py").write_text("def test(): pass")
|
pytester.path.joinpath("t.py").write_text("def test(): pass", encoding="utf-8")
|
||||||
result = pytester.runpytest("--pyargs", "t.py")
|
result = pytester.runpytest("--pyargs", "t.py")
|
||||||
assert result.ret == ExitCode.OK
|
assert result.ret == ExitCode.OK
|
||||||
|
|
||||||
|
@ -622,8 +622,12 @@ class TestInvocationVariants:
|
||||||
|
|
||||||
monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", False)
|
monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", False)
|
||||||
path = pytester.mkpydir("tpkg")
|
path = pytester.mkpydir("tpkg")
|
||||||
path.joinpath("test_hello.py").write_text("def test_hello(): pass")
|
path.joinpath("test_hello.py").write_text(
|
||||||
path.joinpath("test_world.py").write_text("def test_world(): pass")
|
"def test_hello(): pass", encoding="utf-8"
|
||||||
|
)
|
||||||
|
path.joinpath("test_world.py").write_text(
|
||||||
|
"def test_world(): pass", encoding="utf-8"
|
||||||
|
)
|
||||||
result = pytester.runpytest("--pyargs", "tpkg")
|
result = pytester.runpytest("--pyargs", "tpkg")
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
result.stdout.fnmatch_lines(["*2 passed*"])
|
result.stdout.fnmatch_lines(["*2 passed*"])
|
||||||
|
@ -662,13 +666,15 @@ class TestInvocationVariants:
|
||||||
ns = d.joinpath("ns_pkg")
|
ns = d.joinpath("ns_pkg")
|
||||||
ns.mkdir()
|
ns.mkdir()
|
||||||
ns.joinpath("__init__.py").write_text(
|
ns.joinpath("__init__.py").write_text(
|
||||||
"__import__('pkg_resources').declare_namespace(__name__)"
|
"__import__('pkg_resources').declare_namespace(__name__)",
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
lib = ns.joinpath(dirname)
|
lib = ns.joinpath(dirname)
|
||||||
lib.mkdir()
|
lib.mkdir()
|
||||||
lib.joinpath("__init__.py").touch()
|
lib.joinpath("__init__.py").touch()
|
||||||
lib.joinpath(f"test_{dirname}.py").write_text(
|
lib.joinpath(f"test_{dirname}.py").write_text(
|
||||||
f"def test_{dirname}(): pass\ndef test_other():pass"
|
f"def test_{dirname}(): pass\ndef test_other():pass",
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
# The structure of the test directory is now:
|
# The structure of the test directory is now:
|
||||||
|
@ -754,10 +760,10 @@ class TestInvocationVariants:
|
||||||
lib.mkdir()
|
lib.mkdir()
|
||||||
lib.joinpath("__init__.py").touch()
|
lib.joinpath("__init__.py").touch()
|
||||||
lib.joinpath("test_bar.py").write_text(
|
lib.joinpath("test_bar.py").write_text(
|
||||||
"def test_bar(): pass\ndef test_other(a_fixture):pass"
|
"def test_bar(): pass\ndef test_other(a_fixture):pass", encoding="utf-8"
|
||||||
)
|
)
|
||||||
lib.joinpath("conftest.py").write_text(
|
lib.joinpath("conftest.py").write_text(
|
||||||
"import pytest\n@pytest.fixture\ndef a_fixture():pass"
|
"import pytest\n@pytest.fixture\ndef a_fixture():pass", encoding="utf-8"
|
||||||
)
|
)
|
||||||
|
|
||||||
d_local = pytester.mkdir("symlink_root")
|
d_local = pytester.mkdir("symlink_root")
|
||||||
|
@ -1158,7 +1164,6 @@ def test_usage_error_code(pytester: Pytester) -> None:
|
||||||
assert result.ret == ExitCode.USAGE_ERROR
|
assert result.ret == ExitCode.USAGE_ERROR
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.filterwarnings("default::pytest.PytestUnhandledCoroutineWarning")
|
|
||||||
def test_warn_on_async_function(pytester: Pytester) -> None:
|
def test_warn_on_async_function(pytester: Pytester) -> None:
|
||||||
# In the below we .close() the coroutine only to avoid
|
# In the below we .close() the coroutine only to avoid
|
||||||
# "RuntimeWarning: coroutine 'test_2' was never awaited"
|
# "RuntimeWarning: coroutine 'test_2' was never awaited"
|
||||||
|
@ -1175,7 +1180,7 @@ def test_warn_on_async_function(pytester: Pytester) -> None:
|
||||||
return coro
|
return coro
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest("-Wdefault")
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
[
|
[
|
||||||
"test_async.py::test_1",
|
"test_async.py::test_1",
|
||||||
|
@ -1191,7 +1196,6 @@ def test_warn_on_async_function(pytester: Pytester) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.filterwarnings("default::pytest.PytestUnhandledCoroutineWarning")
|
|
||||||
def test_warn_on_async_gen_function(pytester: Pytester) -> None:
|
def test_warn_on_async_gen_function(pytester: Pytester) -> None:
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
test_async="""
|
test_async="""
|
||||||
|
@ -1203,7 +1207,7 @@ def test_warn_on_async_gen_function(pytester: Pytester) -> None:
|
||||||
return test_2()
|
return test_2()
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest("-Wdefault")
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
[
|
[
|
||||||
"test_async.py::test_1",
|
"test_async.py::test_1",
|
||||||
|
@ -1276,8 +1280,7 @@ def test_tee_stdio_captures_and_live_prints(pytester: Pytester) -> None:
|
||||||
result.stderr.fnmatch_lines(["*@this is stderr@*"])
|
result.stderr.fnmatch_lines(["*@this is stderr@*"])
|
||||||
|
|
||||||
# now ensure the output is in the junitxml
|
# now ensure the output is in the junitxml
|
||||||
with open(pytester.path.joinpath("output.xml")) as f:
|
fullXml = pytester.path.joinpath("output.xml").read_text(encoding="utf-8")
|
||||||
fullXml = f.read()
|
|
||||||
assert "@this is stdout@\n" in fullXml
|
assert "@this is stdout@\n" in fullXml
|
||||||
assert "@this is stderr@\n" in fullXml
|
assert "@this is stderr@\n" in fullXml
|
||||||
|
|
||||||
|
@ -1312,3 +1315,38 @@ def test_function_return_non_none_warning(pytester: Pytester) -> None:
|
||||||
)
|
)
|
||||||
res = pytester.runpytest()
|
res = pytester.runpytest()
|
||||||
res.stdout.fnmatch_lines(["*Did you mean to use `assert` instead of `return`?*"])
|
res.stdout.fnmatch_lines(["*Did you mean to use `assert` instead of `return`?*"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_doctest_and_normal_imports_with_importlib(pytester: Pytester) -> None:
|
||||||
|
"""
|
||||||
|
Regression test for #10811: previously import_path with ImportMode.importlib would
|
||||||
|
not return a module if already in sys.modules, resulting in modules being imported
|
||||||
|
multiple times, which causes problems with modules that have import side effects.
|
||||||
|
"""
|
||||||
|
# Uses the exact reproducer form #10811, given it is very minimal
|
||||||
|
# and illustrates the problem well.
|
||||||
|
pytester.makepyfile(
|
||||||
|
**{
|
||||||
|
"pmxbot/commands.py": "from . import logging",
|
||||||
|
"pmxbot/logging.py": "",
|
||||||
|
"tests/__init__.py": "",
|
||||||
|
"tests/test_commands.py": """
|
||||||
|
import importlib
|
||||||
|
from pmxbot import logging
|
||||||
|
|
||||||
|
class TestCommands:
|
||||||
|
def test_boo(self):
|
||||||
|
assert importlib.import_module('pmxbot.logging') is logging
|
||||||
|
""",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
pytester.makeini(
|
||||||
|
"""
|
||||||
|
[pytest]
|
||||||
|
addopts=
|
||||||
|
--doctest-modules
|
||||||
|
--import-mode importlib
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest_subprocess()
|
||||||
|
result.stdout.fnmatch_lines("*1 passed*")
|
||||||
|
|
|
@ -11,7 +11,7 @@ from typing import Tuple
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import _pytest
|
import _pytest._code
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest._code.code import ExceptionChainRepr
|
from _pytest._code.code import ExceptionChainRepr
|
||||||
from _pytest._code.code import ExceptionInfo
|
from _pytest._code.code import ExceptionInfo
|
||||||
|
@ -186,7 +186,7 @@ class TestTraceback_f_g_h:
|
||||||
|
|
||||||
def test_traceback_filter(self):
|
def test_traceback_filter(self):
|
||||||
traceback = self.excinfo.traceback
|
traceback = self.excinfo.traceback
|
||||||
ntraceback = traceback.filter()
|
ntraceback = traceback.filter(self.excinfo)
|
||||||
assert len(ntraceback) == len(traceback) - 1
|
assert len(ntraceback) == len(traceback) - 1
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
@ -217,7 +217,7 @@ class TestTraceback_f_g_h:
|
||||||
|
|
||||||
excinfo = pytest.raises(ValueError, h)
|
excinfo = pytest.raises(ValueError, h)
|
||||||
traceback = excinfo.traceback
|
traceback = excinfo.traceback
|
||||||
ntraceback = traceback.filter()
|
ntraceback = traceback.filter(excinfo)
|
||||||
print(f"old: {traceback!r}")
|
print(f"old: {traceback!r}")
|
||||||
print(f"new: {ntraceback!r}")
|
print(f"new: {ntraceback!r}")
|
||||||
|
|
||||||
|
@ -290,7 +290,7 @@ class TestTraceback_f_g_h:
|
||||||
excinfo = pytest.raises(ValueError, fail)
|
excinfo = pytest.raises(ValueError, fail)
|
||||||
assert excinfo.traceback.recursionindex() is None
|
assert excinfo.traceback.recursionindex() is None
|
||||||
|
|
||||||
def test_traceback_getcrashentry(self):
|
def test_getreprcrash(self):
|
||||||
def i():
|
def i():
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
raise ValueError
|
raise ValueError
|
||||||
|
@ -306,15 +306,13 @@ class TestTraceback_f_g_h:
|
||||||
g()
|
g()
|
||||||
|
|
||||||
excinfo = pytest.raises(ValueError, f)
|
excinfo = pytest.raises(ValueError, f)
|
||||||
tb = excinfo.traceback
|
reprcrash = excinfo._getreprcrash()
|
||||||
entry = tb.getcrashentry()
|
assert reprcrash is not None
|
||||||
assert entry is not None
|
|
||||||
co = _pytest._code.Code.from_function(h)
|
co = _pytest._code.Code.from_function(h)
|
||||||
assert entry.frame.code.path == co.path
|
assert reprcrash.path == str(co.path)
|
||||||
assert entry.lineno == co.firstlineno + 1
|
assert reprcrash.lineno == co.firstlineno + 1 + 1
|
||||||
assert entry.frame.code.name == "h"
|
|
||||||
|
|
||||||
def test_traceback_getcrashentry_empty(self):
|
def test_getreprcrash_empty(self):
|
||||||
def g():
|
def g():
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
raise ValueError
|
raise ValueError
|
||||||
|
@ -324,7 +322,7 @@ class TestTraceback_f_g_h:
|
||||||
g()
|
g()
|
||||||
|
|
||||||
excinfo = pytest.raises(ValueError, f)
|
excinfo = pytest.raises(ValueError, f)
|
||||||
assert excinfo.traceback.getcrashentry() is None
|
assert excinfo._getreprcrash() is None
|
||||||
|
|
||||||
|
|
||||||
def test_excinfo_exconly():
|
def test_excinfo_exconly():
|
||||||
|
@ -376,7 +374,7 @@ def test_excinfo_no_sourcecode():
|
||||||
|
|
||||||
def test_excinfo_no_python_sourcecode(tmp_path: Path) -> None:
|
def test_excinfo_no_python_sourcecode(tmp_path: Path) -> None:
|
||||||
# XXX: simplified locally testable version
|
# XXX: simplified locally testable version
|
||||||
tmp_path.joinpath("test.txt").write_text("{{ h()}}:")
|
tmp_path.joinpath("test.txt").write_text("{{ h()}}:", encoding="utf-8")
|
||||||
|
|
||||||
jinja2 = pytest.importorskip("jinja2")
|
jinja2 = pytest.importorskip("jinja2")
|
||||||
loader = jinja2.FileSystemLoader(str(tmp_path))
|
loader = jinja2.FileSystemLoader(str(tmp_path))
|
||||||
|
@ -453,7 +451,7 @@ class TestFormattedExcinfo:
|
||||||
source = textwrap.dedent(source)
|
source = textwrap.dedent(source)
|
||||||
modpath = tmp_path.joinpath("mod.py")
|
modpath = tmp_path.joinpath("mod.py")
|
||||||
tmp_path.joinpath("__init__.py").touch()
|
tmp_path.joinpath("__init__.py").touch()
|
||||||
modpath.write_text(source)
|
modpath.write_text(source, encoding="utf-8")
|
||||||
importlib.invalidate_caches()
|
importlib.invalidate_caches()
|
||||||
return import_path(modpath, root=tmp_path)
|
return import_path(modpath, root=tmp_path)
|
||||||
|
|
||||||
|
@ -626,7 +624,7 @@ raise ValueError()
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
excinfo = pytest.raises(ValueError, mod.func1)
|
excinfo = pytest.raises(ValueError, mod.func1)
|
||||||
excinfo.traceback = excinfo.traceback.filter()
|
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
||||||
p = FormattedExcinfo()
|
p = FormattedExcinfo()
|
||||||
reprtb = p.repr_traceback_entry(excinfo.traceback[-1])
|
reprtb = p.repr_traceback_entry(excinfo.traceback[-1])
|
||||||
|
|
||||||
|
@ -659,7 +657,7 @@ raise ValueError()
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
excinfo = pytest.raises(ValueError, mod.func1, "m" * 90, 5, 13, "z" * 120)
|
excinfo = pytest.raises(ValueError, mod.func1, "m" * 90, 5, 13, "z" * 120)
|
||||||
excinfo.traceback = excinfo.traceback.filter()
|
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
||||||
entry = excinfo.traceback[-1]
|
entry = excinfo.traceback[-1]
|
||||||
p = FormattedExcinfo(funcargs=True)
|
p = FormattedExcinfo(funcargs=True)
|
||||||
reprfuncargs = p.repr_args(entry)
|
reprfuncargs = p.repr_args(entry)
|
||||||
|
@ -686,7 +684,7 @@ raise ValueError()
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
excinfo = pytest.raises(ValueError, mod.func1, "a", "b", c="d")
|
excinfo = pytest.raises(ValueError, mod.func1, "a", "b", c="d")
|
||||||
excinfo.traceback = excinfo.traceback.filter()
|
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
||||||
entry = excinfo.traceback[-1]
|
entry = excinfo.traceback[-1]
|
||||||
p = FormattedExcinfo(funcargs=True)
|
p = FormattedExcinfo(funcargs=True)
|
||||||
reprfuncargs = p.repr_args(entry)
|
reprfuncargs = p.repr_args(entry)
|
||||||
|
@ -960,7 +958,7 @@ raise ValueError()
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
excinfo = pytest.raises(ValueError, mod.f)
|
excinfo = pytest.raises(ValueError, mod.f)
|
||||||
excinfo.traceback = excinfo.traceback.filter()
|
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
||||||
repr = excinfo.getrepr()
|
repr = excinfo.getrepr()
|
||||||
repr.toterminal(tw_mock)
|
repr.toterminal(tw_mock)
|
||||||
assert tw_mock.lines[0] == ""
|
assert tw_mock.lines[0] == ""
|
||||||
|
@ -994,7 +992,7 @@ raise ValueError()
|
||||||
)
|
)
|
||||||
excinfo = pytest.raises(ValueError, mod.f)
|
excinfo = pytest.raises(ValueError, mod.f)
|
||||||
tmp_path.joinpath("mod.py").unlink()
|
tmp_path.joinpath("mod.py").unlink()
|
||||||
excinfo.traceback = excinfo.traceback.filter()
|
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
||||||
repr = excinfo.getrepr()
|
repr = excinfo.getrepr()
|
||||||
repr.toterminal(tw_mock)
|
repr.toterminal(tw_mock)
|
||||||
assert tw_mock.lines[0] == ""
|
assert tw_mock.lines[0] == ""
|
||||||
|
@ -1025,8 +1023,8 @@ raise ValueError()
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
excinfo = pytest.raises(ValueError, mod.f)
|
excinfo = pytest.raises(ValueError, mod.f)
|
||||||
tmp_path.joinpath("mod.py").write_text("asdf")
|
tmp_path.joinpath("mod.py").write_text("asdf", encoding="utf-8")
|
||||||
excinfo.traceback = excinfo.traceback.filter()
|
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
||||||
repr = excinfo.getrepr()
|
repr = excinfo.getrepr()
|
||||||
repr.toterminal(tw_mock)
|
repr.toterminal(tw_mock)
|
||||||
assert tw_mock.lines[0] == ""
|
assert tw_mock.lines[0] == ""
|
||||||
|
@ -1123,9 +1121,11 @@ raise ValueError()
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
excinfo = pytest.raises(ValueError, mod.f)
|
excinfo = pytest.raises(ValueError, mod.f)
|
||||||
excinfo.traceback = excinfo.traceback.filter()
|
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
||||||
excinfo.traceback[1].set_repr_style("short")
|
excinfo.traceback = _pytest._code.Traceback(
|
||||||
excinfo.traceback[2].set_repr_style("short")
|
entry if i not in (1, 2) else entry.with_repr_style("short")
|
||||||
|
for i, entry in enumerate(excinfo.traceback)
|
||||||
|
)
|
||||||
r = excinfo.getrepr(style="long")
|
r = excinfo.getrepr(style="long")
|
||||||
r.toterminal(tw_mock)
|
r.toterminal(tw_mock)
|
||||||
for line in tw_mock.lines:
|
for line in tw_mock.lines:
|
||||||
|
@ -1391,7 +1391,7 @@ raise ValueError()
|
||||||
with pytest.raises(TypeError) as excinfo:
|
with pytest.raises(TypeError) as excinfo:
|
||||||
mod.f()
|
mod.f()
|
||||||
# previously crashed with `AttributeError: list has no attribute get`
|
# previously crashed with `AttributeError: list has no attribute get`
|
||||||
excinfo.traceback.filter()
|
excinfo.traceback.filter(excinfo)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("style", ["short", "long"])
|
@pytest.mark.parametrize("style", ["short", "long"])
|
||||||
|
@ -1603,3 +1603,48 @@ def test_all_entries_hidden(pytester: Pytester, tbstyle: str) -> None:
|
||||||
result.stdout.fnmatch_lines(["*ZeroDivisionError: division by zero"])
|
result.stdout.fnmatch_lines(["*ZeroDivisionError: division by zero"])
|
||||||
if tbstyle not in ("line", "native"):
|
if tbstyle not in ("line", "native"):
|
||||||
result.stdout.fnmatch_lines(["All traceback entries are hidden.*"])
|
result.stdout.fnmatch_lines(["All traceback entries are hidden.*"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_hidden_entries_of_chained_exceptions_are_not_shown(pytester: Pytester) -> None:
|
||||||
|
"""Hidden entries of chained exceptions are not shown (#1904)."""
|
||||||
|
p = pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
def g1():
|
||||||
|
__tracebackhide__ = True
|
||||||
|
str.does_not_exist
|
||||||
|
|
||||||
|
def f3():
|
||||||
|
__tracebackhide__ = True
|
||||||
|
1 / 0
|
||||||
|
|
||||||
|
def f2():
|
||||||
|
try:
|
||||||
|
f3()
|
||||||
|
except Exception:
|
||||||
|
g1()
|
||||||
|
|
||||||
|
def f1():
|
||||||
|
__tracebackhide__ = True
|
||||||
|
f2()
|
||||||
|
|
||||||
|
def test():
|
||||||
|
f1()
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest(str(p), "--tb=short")
|
||||||
|
assert result.ret == 1
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
[
|
||||||
|
"*.py:11: in f2",
|
||||||
|
" f3()",
|
||||||
|
"E ZeroDivisionError: division by zero",
|
||||||
|
"",
|
||||||
|
"During handling of the above exception, another exception occurred:",
|
||||||
|
"*.py:20: in test",
|
||||||
|
" f1()",
|
||||||
|
"*.py:13: in f2",
|
||||||
|
" g1()",
|
||||||
|
"E AttributeError:*'does_not_exist'",
|
||||||
|
],
|
||||||
|
consecutive=True,
|
||||||
|
)
|
||||||
|
|
|
@ -294,7 +294,7 @@ def test_source_of_class_at_eof_without_newline(_sys_snapshot, tmp_path: Path) -
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
path = tmp_path.joinpath("a.py")
|
path = tmp_path.joinpath("a.py")
|
||||||
path.write_text(str(source))
|
path.write_text(str(source), encoding="utf-8")
|
||||||
mod: Any = import_path(path, root=tmp_path)
|
mod: Any = import_path(path, root=tmp_path)
|
||||||
s2 = Source(mod.A)
|
s2 = Source(mod.A)
|
||||||
assert str(source).strip() == str(s2).strip()
|
assert str(source).strip() == str(s2).strip()
|
||||||
|
@ -439,14 +439,9 @@ comment 4
|
||||||
'''
|
'''
|
||||||
for line in range(2, 6):
|
for line in range(2, 6):
|
||||||
assert str(getstatement(line, source)) == " x = 1"
|
assert str(getstatement(line, source)) == " x = 1"
|
||||||
if sys.version_info >= (3, 8) or hasattr(sys, "pypy_version_info"):
|
for line in range(6, 8):
|
||||||
tqs_start = 8
|
|
||||||
else:
|
|
||||||
tqs_start = 10
|
|
||||||
assert str(getstatement(10, source)) == '"""'
|
|
||||||
for line in range(6, tqs_start):
|
|
||||||
assert str(getstatement(line, source)) == " assert False"
|
assert str(getstatement(line, source)) == " assert False"
|
||||||
for line in range(tqs_start, 10):
|
for line in range(8, 10):
|
||||||
assert str(getstatement(line, source)) == '"""\ncomment 4\n"""'
|
assert str(getstatement(line, source)) == '"""\ncomment 4\n"""'
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -105,7 +105,7 @@ def tw_mock():
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def dummy_yaml_custom_test(pytester: Pytester):
|
def dummy_yaml_custom_test(pytester: Pytester) -> None:
|
||||||
"""Writes a conftest file that collects and executes a dummy yaml test.
|
"""Writes a conftest file that collects and executes a dummy yaml test.
|
||||||
|
|
||||||
Taken from the docs, but stripped down to the bare minimum, useful for
|
Taken from the docs, but stripped down to the bare minimum, useful for
|
||||||
|
|
|
@ -103,7 +103,7 @@ def test_strict_option_is_deprecated(pytester: Pytester) -> None:
|
||||||
def test_foo(): pass
|
def test_foo(): pass
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = pytester.runpytest("--strict")
|
result = pytester.runpytest("--strict", "-Wdefault::pytest.PytestRemovedIn8Warning")
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
[
|
[
|
||||||
"'unknown' not found in `markers` configuration option",
|
"'unknown' not found in `markers` configuration option",
|
||||||
|
@ -189,7 +189,7 @@ class TestSkipMsgArgumentDeprecated:
|
||||||
pytest.skip(msg="skippedmsg")
|
pytest.skip(msg="skippedmsg")
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = pytester.runpytest(p)
|
result = pytester.runpytest(p, "-Wdefault::pytest.PytestRemovedIn8Warning")
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
[
|
[
|
||||||
"*PytestRemovedIn8Warning: pytest.skip(msg=...) is now deprecated, "
|
"*PytestRemovedIn8Warning: pytest.skip(msg=...) is now deprecated, "
|
||||||
|
@ -208,7 +208,7 @@ class TestSkipMsgArgumentDeprecated:
|
||||||
pytest.fail(msg="failedmsg")
|
pytest.fail(msg="failedmsg")
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = pytester.runpytest(p)
|
result = pytester.runpytest(p, "-Wdefault::pytest.PytestRemovedIn8Warning")
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
[
|
[
|
||||||
"*PytestRemovedIn8Warning: pytest.fail(msg=...) is now deprecated, "
|
"*PytestRemovedIn8Warning: pytest.fail(msg=...) is now deprecated, "
|
||||||
|
@ -227,7 +227,7 @@ class TestSkipMsgArgumentDeprecated:
|
||||||
pytest.exit(msg="exitmsg")
|
pytest.exit(msg="exitmsg")
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = pytester.runpytest(p)
|
result = pytester.runpytest(p, "-Wdefault::pytest.PytestRemovedIn8Warning")
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
[
|
[
|
||||||
"*PytestRemovedIn8Warning: pytest.exit(msg=...) is now deprecated, "
|
"*PytestRemovedIn8Warning: pytest.exit(msg=...) is now deprecated, "
|
||||||
|
@ -245,7 +245,7 @@ def test_deprecation_of_cmdline_preparse(pytester: Pytester) -> None:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest("-Wdefault::pytest.PytestRemovedIn8Warning")
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
[
|
[
|
||||||
"*PytestRemovedIn8Warning: The pytest_cmdline_preparse hook is deprecated*",
|
"*PytestRemovedIn8Warning: The pytest_cmdline_preparse hook is deprecated*",
|
||||||
|
@ -281,6 +281,57 @@ def test_importing_instance_is_deprecated(pytester: Pytester) -> None:
|
||||||
from _pytest.python import Instance # noqa: F401
|
from _pytest.python import Instance # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
|
def test_fixture_disallow_on_marked_functions():
|
||||||
|
"""Test that applying @pytest.fixture to a marked function warns (#3364)."""
|
||||||
|
with pytest.warns(
|
||||||
|
pytest.PytestRemovedIn8Warning,
|
||||||
|
match=r"Marks applied to fixtures have no effect",
|
||||||
|
) as record:
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
@pytest.mark.parametrize("example", ["hello"])
|
||||||
|
@pytest.mark.usefixtures("tmp_path")
|
||||||
|
def foo():
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
# it's only possible to get one warning here because you're already prevented
|
||||||
|
# from applying @fixture twice
|
||||||
|
# ValueError("fixture is being applied more than once to the same function")
|
||||||
|
assert len(record) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_fixture_disallow_marks_on_fixtures():
|
||||||
|
"""Test that applying a mark to a fixture warns (#3364)."""
|
||||||
|
with pytest.warns(
|
||||||
|
pytest.PytestRemovedIn8Warning,
|
||||||
|
match=r"Marks applied to fixtures have no effect",
|
||||||
|
) as record:
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("example", ["hello"])
|
||||||
|
@pytest.mark.usefixtures("tmp_path")
|
||||||
|
@pytest.fixture
|
||||||
|
def foo():
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
assert len(record) == 2 # one for each mark decorator
|
||||||
|
|
||||||
|
|
||||||
|
def test_fixture_disallowed_between_marks():
|
||||||
|
"""Test that applying a mark to a fixture warns (#3364)."""
|
||||||
|
with pytest.warns(
|
||||||
|
pytest.PytestRemovedIn8Warning,
|
||||||
|
match=r"Marks applied to fixtures have no effect",
|
||||||
|
) as record:
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("example", ["hello"])
|
||||||
|
@pytest.fixture
|
||||||
|
@pytest.mark.usefixtures("tmp_path")
|
||||||
|
def foo():
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
assert len(record) == 2 # one for each mark decorator
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.filterwarnings("default")
|
@pytest.mark.filterwarnings("default")
|
||||||
def test_nose_deprecated_with_setup(pytester: Pytester) -> None:
|
def test_nose_deprecated_with_setup(pytester: Pytester) -> None:
|
||||||
pytest.importorskip("nose")
|
pytest.importorskip("nose")
|
||||||
|
@ -299,7 +350,7 @@ def test_nose_deprecated_with_setup(pytester: Pytester) -> None:
|
||||||
...
|
...
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
output = pytester.runpytest()
|
output = pytester.runpytest("-Wdefault::pytest.PytestRemovedIn8Warning")
|
||||||
message = [
|
message = [
|
||||||
"*PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.",
|
"*PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.",
|
||||||
"*test_nose_deprecated_with_setup.py::test_omits_warnings is using nose method: `setup_fn_no_op` (setup)",
|
"*test_nose_deprecated_with_setup.py::test_omits_warnings is using nose method: `setup_fn_no_op` (setup)",
|
||||||
|
@ -327,7 +378,7 @@ def test_nose_deprecated_setup_teardown(pytester: Pytester) -> None:
|
||||||
...
|
...
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
output = pytester.runpytest()
|
output = pytester.runpytest("-Wdefault::pytest.PytestRemovedIn8Warning")
|
||||||
message = [
|
message = [
|
||||||
"*PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.",
|
"*PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.",
|
||||||
"*test_nose_deprecated_setup_teardown.py::Test::test is using nose-specific method: `setup(self)`",
|
"*test_nose_deprecated_setup_teardown.py::Test::test is using nose-specific method: `setup(self)`",
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
def test_init():
|
||||||
|
pass
|
|
@ -1,2 +1,2 @@
|
||||||
def test():
|
def test_foo():
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# mypy: disable-error-code="attr-defined"
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -8,6 +9,19 @@ logger = logging.getLogger(__name__)
|
||||||
sublogger = logging.getLogger(__name__ + ".baz")
|
sublogger = logging.getLogger(__name__ + ".baz")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def cleanup_disabled_logging():
|
||||||
|
"""Simple fixture that ensures that a test doesn't disable logging.
|
||||||
|
|
||||||
|
This is necessary because ``logging.disable()`` is global, so a test disabling logging
|
||||||
|
and not cleaning up after will break every test that runs after it.
|
||||||
|
|
||||||
|
This behavior was moved to a fixture so that logging will be un-disabled even if the test fails an assertion.
|
||||||
|
"""
|
||||||
|
yield
|
||||||
|
logging.disable(logging.NOTSET)
|
||||||
|
|
||||||
|
|
||||||
def test_fixture_help(pytester: Pytester) -> None:
|
def test_fixture_help(pytester: Pytester) -> None:
|
||||||
result = pytester.runpytest("--fixtures")
|
result = pytester.runpytest("--fixtures")
|
||||||
result.stdout.fnmatch_lines(["*caplog*"])
|
result.stdout.fnmatch_lines(["*caplog*"])
|
||||||
|
@ -28,10 +42,27 @@ def test_change_level(caplog):
|
||||||
assert "CRITICAL" in caplog.text
|
assert "CRITICAL" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_change_level_logging_disabled(caplog, cleanup_disabled_logging):
|
||||||
|
logging.disable(logging.CRITICAL)
|
||||||
|
assert logging.root.manager.disable == logging.CRITICAL
|
||||||
|
caplog.set_level(logging.WARNING)
|
||||||
|
logger.info("handler INFO level")
|
||||||
|
logger.warning("handler WARNING level")
|
||||||
|
|
||||||
|
caplog.set_level(logging.CRITICAL, logger=sublogger.name)
|
||||||
|
sublogger.warning("logger SUB_WARNING level")
|
||||||
|
sublogger.critical("logger SUB_CRITICAL level")
|
||||||
|
|
||||||
|
assert "INFO" not in caplog.text
|
||||||
|
assert "WARNING" in caplog.text
|
||||||
|
assert "SUB_WARNING" not in caplog.text
|
||||||
|
assert "SUB_CRITICAL" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
def test_change_level_undo(pytester: Pytester) -> None:
|
def test_change_level_undo(pytester: Pytester) -> None:
|
||||||
"""Ensure that 'set_level' is undone after the end of the test.
|
"""Ensure that 'set_level' is undone after the end of the test.
|
||||||
|
|
||||||
Tests the logging output themselves (affacted both by logger and handler levels).
|
Tests the logging output themselves (affected both by logger and handler levels).
|
||||||
"""
|
"""
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
"""
|
"""
|
||||||
|
@ -54,6 +85,37 @@ def test_change_level_undo(pytester: Pytester) -> None:
|
||||||
result.stdout.no_fnmatch_line("*log from test2*")
|
result.stdout.no_fnmatch_line("*log from test2*")
|
||||||
|
|
||||||
|
|
||||||
|
def test_change_disabled_level_undo(
|
||||||
|
pytester: Pytester, cleanup_disabled_logging
|
||||||
|
) -> None:
|
||||||
|
"""Ensure that '_force_enable_logging' in 'set_level' is undone after the end of the test.
|
||||||
|
|
||||||
|
Tests the logging output themselves (affected by disabled logging level).
|
||||||
|
"""
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
def test1(caplog):
|
||||||
|
logging.disable(logging.CRITICAL)
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
# using + operator here so fnmatch_lines doesn't match the code in the traceback
|
||||||
|
logging.info('log from ' + 'test1')
|
||||||
|
assert 0
|
||||||
|
|
||||||
|
def test2(caplog):
|
||||||
|
# using + operator here so fnmatch_lines doesn't match the code in the traceback
|
||||||
|
# use logging.warning because we need a level that will show up if logging.disabled
|
||||||
|
# isn't reset to ``CRITICAL`` after test1.
|
||||||
|
logging.warning('log from ' + 'test2')
|
||||||
|
assert 0
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest()
|
||||||
|
result.stdout.fnmatch_lines(["*log from test1*", "*2 failed in *"])
|
||||||
|
result.stdout.no_fnmatch_line("*log from test2*")
|
||||||
|
|
||||||
|
|
||||||
def test_change_level_undos_handler_level(pytester: Pytester) -> None:
|
def test_change_level_undos_handler_level(pytester: Pytester) -> None:
|
||||||
"""Ensure that 'set_level' is undone after the end of the test (handler).
|
"""Ensure that 'set_level' is undone after the end of the test (handler).
|
||||||
|
|
||||||
|
@ -97,6 +159,65 @@ def test_with_statement(caplog):
|
||||||
assert "CRITICAL" in caplog.text
|
assert "CRITICAL" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_with_statement_logging_disabled(caplog, cleanup_disabled_logging):
|
||||||
|
logging.disable(logging.CRITICAL)
|
||||||
|
assert logging.root.manager.disable == logging.CRITICAL
|
||||||
|
with caplog.at_level(logging.WARNING):
|
||||||
|
logger.debug("handler DEBUG level")
|
||||||
|
logger.info("handler INFO level")
|
||||||
|
logger.warning("handler WARNING level")
|
||||||
|
logger.error("handler ERROR level")
|
||||||
|
logger.critical("handler CRITICAL level")
|
||||||
|
|
||||||
|
assert logging.root.manager.disable == logging.INFO
|
||||||
|
|
||||||
|
with caplog.at_level(logging.CRITICAL, logger=sublogger.name):
|
||||||
|
sublogger.warning("logger SUB_WARNING level")
|
||||||
|
sublogger.critical("logger SUB_CRITICAL level")
|
||||||
|
|
||||||
|
assert "DEBUG" not in caplog.text
|
||||||
|
assert "INFO" not in caplog.text
|
||||||
|
assert "WARNING" in caplog.text
|
||||||
|
assert "ERROR" in caplog.text
|
||||||
|
assert " CRITICAL" in caplog.text
|
||||||
|
assert "SUB_WARNING" not in caplog.text
|
||||||
|
assert "SUB_CRITICAL" in caplog.text
|
||||||
|
assert logging.root.manager.disable == logging.CRITICAL
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"level_str,expected_disable_level",
|
||||||
|
[
|
||||||
|
("CRITICAL", logging.ERROR),
|
||||||
|
("ERROR", logging.WARNING),
|
||||||
|
("WARNING", logging.INFO),
|
||||||
|
("INFO", logging.DEBUG),
|
||||||
|
("DEBUG", logging.NOTSET),
|
||||||
|
("NOTSET", logging.NOTSET),
|
||||||
|
("NOTVALIDLEVEL", logging.NOTSET),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_force_enable_logging_level_string(
|
||||||
|
caplog, cleanup_disabled_logging, level_str, expected_disable_level
|
||||||
|
):
|
||||||
|
"""Test _force_enable_logging using a level string.
|
||||||
|
|
||||||
|
``expected_disable_level`` is one level below ``level_str`` because the disabled log level
|
||||||
|
always needs to be *at least* one level lower than the level that caplog is trying to capture.
|
||||||
|
"""
|
||||||
|
test_logger = logging.getLogger("test_str_level_force_enable")
|
||||||
|
# Emulate a testing environment where all logging is disabled.
|
||||||
|
logging.disable(logging.CRITICAL)
|
||||||
|
# Make sure all logging is disabled.
|
||||||
|
assert not test_logger.isEnabledFor(logging.CRITICAL)
|
||||||
|
# Un-disable logging for `level_str`.
|
||||||
|
caplog._force_enable_logging(level_str, test_logger)
|
||||||
|
# Make sure that the disabled level is now one below the requested logging level.
|
||||||
|
# We don't use `isEnabledFor` here because that also checks the level set by
|
||||||
|
# `logging.setLevel()` which is irrelevant to `logging.disable()`.
|
||||||
|
assert test_logger.manager.disable == expected_disable_level
|
||||||
|
|
||||||
|
|
||||||
def test_log_access(caplog):
|
def test_log_access(caplog):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
logger.info("boo %s", "arg")
|
logger.info("boo %s", "arg")
|
||||||
|
|
|
@ -81,7 +81,7 @@ def test_root_logger_affected(pytester: Pytester) -> None:
|
||||||
# not the info one, because the default level of the root logger is
|
# not the info one, because the default level of the root logger is
|
||||||
# WARNING.
|
# WARNING.
|
||||||
assert os.path.isfile(log_file)
|
assert os.path.isfile(log_file)
|
||||||
with open(log_file) as rfh:
|
with open(log_file, encoding="utf-8") as rfh:
|
||||||
contents = rfh.read()
|
contents = rfh.read()
|
||||||
assert "info text going to logger" not in contents
|
assert "info text going to logger" not in contents
|
||||||
assert "warning text going to logger" in contents
|
assert "warning text going to logger" in contents
|
||||||
|
@ -656,7 +656,7 @@ def test_log_file_cli(pytester: Pytester) -> None:
|
||||||
# make sure that we get a '0' exit code for the testsuite
|
# make sure that we get a '0' exit code for the testsuite
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
assert os.path.isfile(log_file)
|
assert os.path.isfile(log_file)
|
||||||
with open(log_file) as rfh:
|
with open(log_file, encoding="utf-8") as rfh:
|
||||||
contents = rfh.read()
|
contents = rfh.read()
|
||||||
assert "This log message will be shown" in contents
|
assert "This log message will be shown" in contents
|
||||||
assert "This log message won't be shown" not in contents
|
assert "This log message won't be shown" not in contents
|
||||||
|
@ -687,7 +687,7 @@ def test_log_file_cli_level(pytester: Pytester) -> None:
|
||||||
# make sure that we get a '0' exit code for the testsuite
|
# make sure that we get a '0' exit code for the testsuite
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
assert os.path.isfile(log_file)
|
assert os.path.isfile(log_file)
|
||||||
with open(log_file) as rfh:
|
with open(log_file, encoding="utf-8") as rfh:
|
||||||
contents = rfh.read()
|
contents = rfh.read()
|
||||||
assert "This log message will be shown" in contents
|
assert "This log message will be shown" in contents
|
||||||
assert "This log message won't be shown" not in contents
|
assert "This log message won't be shown" not in contents
|
||||||
|
@ -738,7 +738,7 @@ def test_log_file_ini(pytester: Pytester) -> None:
|
||||||
# make sure that we get a '0' exit code for the testsuite
|
# make sure that we get a '0' exit code for the testsuite
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
assert os.path.isfile(log_file)
|
assert os.path.isfile(log_file)
|
||||||
with open(log_file) as rfh:
|
with open(log_file, encoding="utf-8") as rfh:
|
||||||
contents = rfh.read()
|
contents = rfh.read()
|
||||||
assert "This log message will be shown" in contents
|
assert "This log message will be shown" in contents
|
||||||
assert "This log message won't be shown" not in contents
|
assert "This log message won't be shown" not in contents
|
||||||
|
@ -777,7 +777,7 @@ def test_log_file_ini_level(pytester: Pytester) -> None:
|
||||||
# make sure that we get a '0' exit code for the testsuite
|
# make sure that we get a '0' exit code for the testsuite
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
assert os.path.isfile(log_file)
|
assert os.path.isfile(log_file)
|
||||||
with open(log_file) as rfh:
|
with open(log_file, encoding="utf-8") as rfh:
|
||||||
contents = rfh.read()
|
contents = rfh.read()
|
||||||
assert "This log message will be shown" in contents
|
assert "This log message will be shown" in contents
|
||||||
assert "This log message won't be shown" not in contents
|
assert "This log message won't be shown" not in contents
|
||||||
|
@ -985,7 +985,7 @@ def test_log_in_hooks(pytester: Pytester) -> None:
|
||||||
)
|
)
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest()
|
||||||
result.stdout.fnmatch_lines(["*sessionstart*", "*runtestloop*", "*sessionfinish*"])
|
result.stdout.fnmatch_lines(["*sessionstart*", "*runtestloop*", "*sessionfinish*"])
|
||||||
with open(log_file) as rfh:
|
with open(log_file, encoding="utf-8") as rfh:
|
||||||
contents = rfh.read()
|
contents = rfh.read()
|
||||||
assert "sessionstart" in contents
|
assert "sessionstart" in contents
|
||||||
assert "runtestloop" in contents
|
assert "runtestloop" in contents
|
||||||
|
@ -1021,7 +1021,7 @@ def test_log_in_runtest_logreport(pytester: Pytester) -> None:
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
pytester.runpytest()
|
pytester.runpytest()
|
||||||
with open(log_file) as rfh:
|
with open(log_file, encoding="utf-8") as rfh:
|
||||||
contents = rfh.read()
|
contents = rfh.read()
|
||||||
assert contents.count("logreport") == 3
|
assert contents.count("logreport") == 3
|
||||||
|
|
||||||
|
@ -1065,11 +1065,11 @@ def test_log_set_path(pytester: Pytester) -> None:
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
pytester.runpytest()
|
pytester.runpytest()
|
||||||
with open(os.path.join(report_dir_base, "test_first")) as rfh:
|
with open(os.path.join(report_dir_base, "test_first"), encoding="utf-8") as rfh:
|
||||||
content = rfh.read()
|
content = rfh.read()
|
||||||
assert "message from test 1" in content
|
assert "message from test 1" in content
|
||||||
|
|
||||||
with open(os.path.join(report_dir_base, "test_second")) as rfh:
|
with open(os.path.join(report_dir_base, "test_second"), encoding="utf-8") as rfh:
|
||||||
content = rfh.read()
|
content = rfh.read()
|
||||||
assert "message from test 2" in content
|
assert "message from test 2" in content
|
||||||
|
|
||||||
|
@ -1234,3 +1234,100 @@ def test_log_disabling_works_with_log_cli(pytester: Pytester) -> None:
|
||||||
"WARNING disabled:test_log_disabling_works_with_log_cli.py:7 This string will be suppressed."
|
"WARNING disabled:test_log_disabling_works_with_log_cli.py:7 This string will be suppressed."
|
||||||
)
|
)
|
||||||
assert not result.stderr.lines
|
assert not result.stderr.lines
|
||||||
|
|
||||||
|
|
||||||
|
def test_without_date_format_log(pytester: Pytester) -> None:
|
||||||
|
"""Check that date is not printed by default."""
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def test_foo():
|
||||||
|
logger.warning('text')
|
||||||
|
assert False
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest()
|
||||||
|
assert result.ret == 1
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
["WARNING test_without_date_format_log:test_without_date_format_log.py:6 text"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_date_format_log(pytester: Pytester) -> None:
|
||||||
|
"""Check that log_date_format affects output."""
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def test_foo():
|
||||||
|
logger.warning('text')
|
||||||
|
assert False
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
pytester.makeini(
|
||||||
|
"""
|
||||||
|
[pytest]
|
||||||
|
log_format=%(asctime)s; %(levelname)s; %(message)s
|
||||||
|
log_date_format=%Y-%m-%d %H:%M:%S
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest()
|
||||||
|
assert result.ret == 1
|
||||||
|
result.stdout.re_match_lines([r"^[0-9-]{10} [0-9:]{8}; WARNING; text"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_date_format_percentf_log(pytester: Pytester) -> None:
|
||||||
|
"""Make sure that microseconds are printed in log."""
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def test_foo():
|
||||||
|
logger.warning('text')
|
||||||
|
assert False
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
pytester.makeini(
|
||||||
|
"""
|
||||||
|
[pytest]
|
||||||
|
log_format=%(asctime)s; %(levelname)s; %(message)s
|
||||||
|
log_date_format=%Y-%m-%d %H:%M:%S.%f
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest()
|
||||||
|
assert result.ret == 1
|
||||||
|
result.stdout.re_match_lines([r"^[0-9-]{10} [0-9:]{8}.[0-9]{6}; WARNING; text"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_date_format_percentf_tz_log(pytester: Pytester) -> None:
|
||||||
|
"""Make sure that timezone and microseconds are properly formatted together."""
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def test_foo():
|
||||||
|
logger.warning('text')
|
||||||
|
assert False
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
pytester.makeini(
|
||||||
|
"""
|
||||||
|
[pytest]
|
||||||
|
log_format=%(asctime)s; %(levelname)s; %(message)s
|
||||||
|
log_date_format=%Y-%m-%d %H:%M:%S.%f%z
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest()
|
||||||
|
assert result.ret == 1
|
||||||
|
result.stdout.re_match_lines(
|
||||||
|
[r"^[0-9-]{10} [0-9:]{8}.[0-9]{6}[+-][0-9\.]+; WARNING; text"]
|
||||||
|
)
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
anyio[curio,trio]==3.6.2
|
anyio[curio,trio]==3.7.0
|
||||||
django==4.2.1
|
django==4.2.2
|
||||||
pytest-asyncio==0.21.0
|
pytest-asyncio==0.21.0
|
||||||
pytest-bdd==6.1.1
|
pytest-bdd==6.1.1
|
||||||
pytest-cov==4.0.0
|
pytest-cov==4.1.0
|
||||||
pytest-django==4.5.2
|
pytest-django==4.5.2
|
||||||
pytest-flakes==4.0.5
|
pytest-flakes==4.0.5
|
||||||
pytest-html==3.2.0
|
pytest-html==3.2.0
|
||||||
pytest-mock==3.10.0
|
pytest-mock==3.11.1
|
||||||
pytest-rerunfailures==11.1.2
|
pytest-rerunfailures==11.1.2
|
||||||
pytest-sugar==0.9.7
|
pytest-sugar==0.9.7
|
||||||
pytest-trio==0.7.0
|
pytest-trio==0.7.0
|
||||||
pytest-twisted==1.14.0
|
pytest-twisted==1.14.0
|
||||||
twisted==22.8.0
|
twisted==22.8.0
|
||||||
pytest-xvfb==2.0.0
|
pytest-xvfb==3.0.0
|
||||||
|
|
|
@ -60,7 +60,8 @@ class TestModule:
|
||||||
""".format(
|
""".format(
|
||||||
str(root2)
|
str(root2)
|
||||||
)
|
)
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
with monkeypatch.context() as mp:
|
with monkeypatch.context() as mp:
|
||||||
mp.chdir(root2)
|
mp.chdir(root2)
|
||||||
|
@ -832,7 +833,8 @@ class TestConftestCustomization:
|
||||||
mod = outcome.get_result()
|
mod = outcome.get_result()
|
||||||
mod.obj.hello = "world"
|
mod.obj.hello = "world"
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
b.joinpath("test_module.py").write_text(
|
b.joinpath("test_module.py").write_text(
|
||||||
textwrap.dedent(
|
textwrap.dedent(
|
||||||
|
@ -840,7 +842,8 @@ class TestConftestCustomization:
|
||||||
def test_hello():
|
def test_hello():
|
||||||
assert hello == "world"
|
assert hello == "world"
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
reprec = pytester.inline_run()
|
reprec = pytester.inline_run()
|
||||||
reprec.assertoutcome(passed=1)
|
reprec.assertoutcome(passed=1)
|
||||||
|
@ -861,7 +864,8 @@ class TestConftestCustomization:
|
||||||
for func in result:
|
for func in result:
|
||||||
func._some123 = "world"
|
func._some123 = "world"
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
b.joinpath("test_module.py").write_text(
|
b.joinpath("test_module.py").write_text(
|
||||||
textwrap.dedent(
|
textwrap.dedent(
|
||||||
|
@ -874,7 +878,8 @@ class TestConftestCustomization:
|
||||||
def test_hello(obj):
|
def test_hello(obj):
|
||||||
assert obj == "world"
|
assert obj == "world"
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
reprec = pytester.inline_run()
|
reprec = pytester.inline_run()
|
||||||
reprec.assertoutcome(passed=1)
|
reprec.assertoutcome(passed=1)
|
||||||
|
@ -897,25 +902,29 @@ class TestConftestCustomization:
|
||||||
def test_issue2369_collect_module_fileext(self, pytester: Pytester) -> None:
|
def test_issue2369_collect_module_fileext(self, pytester: Pytester) -> None:
|
||||||
"""Ensure we can collect files with weird file extensions as Python
|
"""Ensure we can collect files with weird file extensions as Python
|
||||||
modules (#2369)"""
|
modules (#2369)"""
|
||||||
# We'll implement a little finder and loader to import files containing
|
# Implement a little meta path finder to import files containing
|
||||||
# Python source code whose file extension is ".narf".
|
# Python source code whose file extension is ".narf".
|
||||||
pytester.makeconftest(
|
pytester.makeconftest(
|
||||||
"""
|
"""
|
||||||
import sys, os, imp
|
import sys
|
||||||
|
import os.path
|
||||||
|
from importlib.util import spec_from_loader
|
||||||
|
from importlib.machinery import SourceFileLoader
|
||||||
from _pytest.python import Module
|
from _pytest.python import Module
|
||||||
|
|
||||||
class Loader(object):
|
class MetaPathFinder:
|
||||||
def load_module(self, name):
|
def find_spec(self, fullname, path, target=None):
|
||||||
return imp.load_source(name, name + ".narf")
|
if os.path.exists(fullname + ".narf"):
|
||||||
class Finder(object):
|
return spec_from_loader(
|
||||||
def find_module(self, name, path=None):
|
fullname,
|
||||||
if os.path.exists(name + ".narf"):
|
SourceFileLoader(fullname, fullname + ".narf"),
|
||||||
return Loader()
|
)
|
||||||
sys.meta_path.append(Finder())
|
sys.meta_path.append(MetaPathFinder())
|
||||||
|
|
||||||
def pytest_collect_file(file_path, parent):
|
def pytest_collect_file(file_path, parent):
|
||||||
if file_path.suffix == ".narf":
|
if file_path.suffix == ".narf":
|
||||||
return Module.from_parent(path=file_path, parent=parent)"""
|
return Module.from_parent(path=file_path, parent=parent)
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
pytester.makefile(
|
pytester.makefile(
|
||||||
".narf",
|
".narf",
|
||||||
|
@ -970,7 +979,8 @@ def test_setup_only_available_in_subdir(pytester: Pytester) -> None:
|
||||||
def pytest_runtest_teardown(item):
|
def pytest_runtest_teardown(item):
|
||||||
assert item.path.stem == "test_in_sub1"
|
assert item.path.stem == "test_in_sub1"
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
sub2.joinpath("conftest.py").write_text(
|
sub2.joinpath("conftest.py").write_text(
|
||||||
textwrap.dedent(
|
textwrap.dedent(
|
||||||
|
@ -983,10 +993,11 @@ def test_setup_only_available_in_subdir(pytester: Pytester) -> None:
|
||||||
def pytest_runtest_teardown(item):
|
def pytest_runtest_teardown(item):
|
||||||
assert item.path.stem == "test_in_sub2"
|
assert item.path.stem == "test_in_sub2"
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
sub1.joinpath("test_in_sub1.py").write_text("def test_1(): pass")
|
sub1.joinpath("test_in_sub1.py").write_text("def test_1(): pass", encoding="utf-8")
|
||||||
sub2.joinpath("test_in_sub2.py").write_text("def test_2(): pass")
|
sub2.joinpath("test_in_sub2.py").write_text("def test_2(): pass", encoding="utf-8")
|
||||||
result = pytester.runpytest("-v", "-s")
|
result = pytester.runpytest("-v", "-s")
|
||||||
result.assert_outcomes(passed=2)
|
result.assert_outcomes(passed=2)
|
||||||
|
|
||||||
|
@ -1003,9 +1014,9 @@ class TestTracebackCutting:
|
||||||
with pytest.raises(pytest.skip.Exception) as excinfo:
|
with pytest.raises(pytest.skip.Exception) as excinfo:
|
||||||
pytest.skip("xxx")
|
pytest.skip("xxx")
|
||||||
assert excinfo.traceback[-1].frame.code.name == "skip"
|
assert excinfo.traceback[-1].frame.code.name == "skip"
|
||||||
assert excinfo.traceback[-1].ishidden()
|
assert excinfo.traceback[-1].ishidden(excinfo)
|
||||||
assert excinfo.traceback[-2].frame.code.name == "test_skip_simple"
|
assert excinfo.traceback[-2].frame.code.name == "test_skip_simple"
|
||||||
assert not excinfo.traceback[-2].ishidden()
|
assert not excinfo.traceback[-2].ishidden(excinfo)
|
||||||
|
|
||||||
def test_traceback_argsetup(self, pytester: Pytester) -> None:
|
def test_traceback_argsetup(self, pytester: Pytester) -> None:
|
||||||
pytester.makeconftest(
|
pytester.makeconftest(
|
||||||
|
@ -1374,7 +1385,8 @@ def test_skip_duplicates_by_default(pytester: Pytester) -> None:
|
||||||
def test_real():
|
def test_real():
|
||||||
pass
|
pass
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
result = pytester.runpytest(str(a), str(a))
|
result = pytester.runpytest(str(a), str(a))
|
||||||
result.stdout.fnmatch_lines(["*collected 1 item*"])
|
result.stdout.fnmatch_lines(["*collected 1 item*"])
|
||||||
|
@ -1394,7 +1406,8 @@ def test_keep_duplicates(pytester: Pytester) -> None:
|
||||||
def test_real():
|
def test_real():
|
||||||
pass
|
pass
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
result = pytester.runpytest("--keep-duplicates", str(a), str(a))
|
result = pytester.runpytest("--keep-duplicates", str(a), str(a))
|
||||||
result.stdout.fnmatch_lines(["*collected 2 item*"])
|
result.stdout.fnmatch_lines(["*collected 2 item*"])
|
||||||
|
@ -1407,10 +1420,15 @@ def test_package_collection_infinite_recursion(pytester: Pytester) -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_package_collection_init_given_as_argument(pytester: Pytester) -> None:
|
def test_package_collection_init_given_as_argument(pytester: Pytester) -> None:
|
||||||
"""Regression test for #3749"""
|
"""Regression test for #3749, #8976, #9263, #9313.
|
||||||
|
|
||||||
|
Specifying an __init__.py file directly should collect only the __init__.py
|
||||||
|
Module, not the entire package.
|
||||||
|
"""
|
||||||
p = pytester.copy_example("collect/package_init_given_as_arg")
|
p = pytester.copy_example("collect/package_init_given_as_arg")
|
||||||
result = pytester.runpytest(p / "pkg" / "__init__.py")
|
items, hookrecorder = pytester.inline_genitems(p / "pkg" / "__init__.py")
|
||||||
result.stdout.fnmatch_lines(["*1 passed*"])
|
assert len(items) == 1
|
||||||
|
assert items[0].name == "test_init"
|
||||||
|
|
||||||
|
|
||||||
def test_package_with_modules(pytester: Pytester) -> None:
|
def test_package_with_modules(pytester: Pytester) -> None:
|
||||||
|
@ -1439,8 +1457,12 @@ def test_package_with_modules(pytester: Pytester) -> None:
|
||||||
sub2_test = sub2.joinpath("test")
|
sub2_test = sub2.joinpath("test")
|
||||||
sub2_test.mkdir(parents=True)
|
sub2_test.mkdir(parents=True)
|
||||||
|
|
||||||
sub1_test.joinpath("test_in_sub1.py").write_text("def test_1(): pass")
|
sub1_test.joinpath("test_in_sub1.py").write_text(
|
||||||
sub2_test.joinpath("test_in_sub2.py").write_text("def test_2(): pass")
|
"def test_1(): pass", encoding="utf-8"
|
||||||
|
)
|
||||||
|
sub2_test.joinpath("test_in_sub2.py").write_text(
|
||||||
|
"def test_2(): pass", encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
# Execute from .
|
# Execute from .
|
||||||
result = pytester.runpytest("-v", "-s")
|
result = pytester.runpytest("-v", "-s")
|
||||||
|
@ -1484,9 +1506,11 @@ def test_package_ordering(pytester: Pytester) -> None:
|
||||||
sub2_test = sub2.joinpath("test")
|
sub2_test = sub2.joinpath("test")
|
||||||
sub2_test.mkdir(parents=True)
|
sub2_test.mkdir(parents=True)
|
||||||
|
|
||||||
root.joinpath("Test_root.py").write_text("def test_1(): pass")
|
root.joinpath("Test_root.py").write_text("def test_1(): pass", encoding="utf-8")
|
||||||
sub1.joinpath("Test_sub1.py").write_text("def test_2(): pass")
|
sub1.joinpath("Test_sub1.py").write_text("def test_2(): pass", encoding="utf-8")
|
||||||
sub2_test.joinpath("test_sub2.py").write_text("def test_3(): pass")
|
sub2_test.joinpath("test_sub2.py").write_text(
|
||||||
|
"def test_3(): pass", encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
# Execute from .
|
# Execute from .
|
||||||
result = pytester.runpytest("-v", "-s")
|
result = pytester.runpytest("-v", "-s")
|
||||||
|
|
|
@ -287,7 +287,8 @@ class TestFillFixtures:
|
||||||
def spam():
|
def spam():
|
||||||
return 'spam'
|
return 'spam'
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
testfile = subdir.joinpath("test_spam.py")
|
testfile = subdir.joinpath("test_spam.py")
|
||||||
testfile.write_text(
|
testfile.write_text(
|
||||||
|
@ -296,7 +297,8 @@ class TestFillFixtures:
|
||||||
def test_spam(spam):
|
def test_spam(spam):
|
||||||
assert spam == "spam"
|
assert spam == "spam"
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest()
|
||||||
result.stdout.fnmatch_lines(["*1 passed*"])
|
result.stdout.fnmatch_lines(["*1 passed*"])
|
||||||
|
@ -359,7 +361,8 @@ class TestFillFixtures:
|
||||||
def spam(request):
|
def spam(request):
|
||||||
return request.param
|
return request.param
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
testfile = subdir.joinpath("test_spam.py")
|
testfile = subdir.joinpath("test_spam.py")
|
||||||
testfile.write_text(
|
testfile.write_text(
|
||||||
|
@ -371,7 +374,8 @@ class TestFillFixtures:
|
||||||
assert spam == params['spam']
|
assert spam == params['spam']
|
||||||
params['spam'] += 1
|
params['spam'] += 1
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest()
|
||||||
result.stdout.fnmatch_lines(["*3 passed*"])
|
result.stdout.fnmatch_lines(["*3 passed*"])
|
||||||
|
@ -403,7 +407,8 @@ class TestFillFixtures:
|
||||||
def spam(request):
|
def spam(request):
|
||||||
return request.param
|
return request.param
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
testfile = subdir.joinpath("test_spam.py")
|
testfile = subdir.joinpath("test_spam.py")
|
||||||
testfile.write_text(
|
testfile.write_text(
|
||||||
|
@ -415,7 +420,8 @@ class TestFillFixtures:
|
||||||
assert spam == params['spam']
|
assert spam == params['spam']
|
||||||
params['spam'] += 1
|
params['spam'] += 1
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest()
|
||||||
result.stdout.fnmatch_lines(["*3 passed*"])
|
result.stdout.fnmatch_lines(["*3 passed*"])
|
||||||
|
@ -1037,10 +1043,11 @@ class TestRequestBasic:
|
||||||
def arg1():
|
def arg1():
|
||||||
pass
|
pass
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
p = b.joinpath("test_module.py")
|
p = b.joinpath("test_module.py")
|
||||||
p.write_text("def test_func(arg1): pass")
|
p.write_text("def test_func(arg1): pass", encoding="utf-8")
|
||||||
result = pytester.runpytest(p, "--fixtures")
|
result = pytester.runpytest(p, "--fixtures")
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
|
@ -1617,7 +1624,8 @@ class TestFixtureManagerParseFactories:
|
||||||
def one():
|
def one():
|
||||||
return 1
|
return 1
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
package.joinpath("test_x.py").write_text(
|
package.joinpath("test_x.py").write_text(
|
||||||
textwrap.dedent(
|
textwrap.dedent(
|
||||||
|
@ -1625,7 +1633,8 @@ class TestFixtureManagerParseFactories:
|
||||||
def test_x(one):
|
def test_x(one):
|
||||||
assert one == 1
|
assert one == 1
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
sub = package.joinpath("sub")
|
sub = package.joinpath("sub")
|
||||||
sub.mkdir()
|
sub.mkdir()
|
||||||
|
@ -1638,7 +1647,8 @@ class TestFixtureManagerParseFactories:
|
||||||
def one():
|
def one():
|
||||||
return 2
|
return 2
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
sub.joinpath("test_y.py").write_text(
|
sub.joinpath("test_y.py").write_text(
|
||||||
textwrap.dedent(
|
textwrap.dedent(
|
||||||
|
@ -1646,7 +1656,8 @@ class TestFixtureManagerParseFactories:
|
||||||
def test_x(one):
|
def test_x(one):
|
||||||
assert one == 2
|
assert one == 2
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
reprec = pytester.inline_run()
|
reprec = pytester.inline_run()
|
||||||
reprec.assertoutcome(passed=2)
|
reprec.assertoutcome(passed=2)
|
||||||
|
@ -1671,7 +1682,8 @@ class TestFixtureManagerParseFactories:
|
||||||
def teardown_module():
|
def teardown_module():
|
||||||
values[:] = []
|
values[:] = []
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
package.joinpath("test_x.py").write_text(
|
package.joinpath("test_x.py").write_text(
|
||||||
textwrap.dedent(
|
textwrap.dedent(
|
||||||
|
@ -1680,7 +1692,8 @@ class TestFixtureManagerParseFactories:
|
||||||
def test_x():
|
def test_x():
|
||||||
assert values == ["package"]
|
assert values == ["package"]
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
package = pytester.mkdir("package2")
|
package = pytester.mkdir("package2")
|
||||||
package.joinpath("__init__.py").write_text(
|
package.joinpath("__init__.py").write_text(
|
||||||
|
@ -1692,7 +1705,8 @@ class TestFixtureManagerParseFactories:
|
||||||
def teardown_module():
|
def teardown_module():
|
||||||
values[:] = []
|
values[:] = []
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
package.joinpath("test_x.py").write_text(
|
package.joinpath("test_x.py").write_text(
|
||||||
textwrap.dedent(
|
textwrap.dedent(
|
||||||
|
@ -1701,7 +1715,8 @@ class TestFixtureManagerParseFactories:
|
||||||
def test_x():
|
def test_x():
|
||||||
assert values == ["package2"]
|
assert values == ["package2"]
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
reprec = pytester.inline_run()
|
reprec = pytester.inline_run()
|
||||||
reprec.assertoutcome(passed=2)
|
reprec.assertoutcome(passed=2)
|
||||||
|
@ -1714,7 +1729,7 @@ class TestFixtureManagerParseFactories:
|
||||||
)
|
)
|
||||||
pytester.syspathinsert(pytester.path.name)
|
pytester.syspathinsert(pytester.path.name)
|
||||||
package = pytester.mkdir("package")
|
package = pytester.mkdir("package")
|
||||||
package.joinpath("__init__.py").write_text("")
|
package.joinpath("__init__.py").write_text("", encoding="utf-8")
|
||||||
package.joinpath("conftest.py").write_text(
|
package.joinpath("conftest.py").write_text(
|
||||||
textwrap.dedent(
|
textwrap.dedent(
|
||||||
"""\
|
"""\
|
||||||
|
@ -1731,7 +1746,8 @@ class TestFixtureManagerParseFactories:
|
||||||
yield values
|
yield values
|
||||||
values.pop()
|
values.pop()
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
package.joinpath("test_x.py").write_text(
|
package.joinpath("test_x.py").write_text(
|
||||||
textwrap.dedent(
|
textwrap.dedent(
|
||||||
|
@ -1742,7 +1758,8 @@ class TestFixtureManagerParseFactories:
|
||||||
def test_package(one):
|
def test_package(one):
|
||||||
assert values == ["package-auto", "package"]
|
assert values == ["package-auto", "package"]
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
reprec = pytester.inline_run()
|
reprec = pytester.inline_run()
|
||||||
reprec.assertoutcome(passed=2)
|
reprec.assertoutcome(passed=2)
|
||||||
|
@ -1892,8 +1909,12 @@ class TestAutouseDiscovery:
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
conftest.rename(a.joinpath(conftest.name))
|
conftest.rename(a.joinpath(conftest.name))
|
||||||
a.joinpath("test_something.py").write_text("def test_func(): pass")
|
a.joinpath("test_something.py").write_text(
|
||||||
b.joinpath("test_otherthing.py").write_text("def test_func(): pass")
|
"def test_func(): pass", encoding="utf-8"
|
||||||
|
)
|
||||||
|
b.joinpath("test_otherthing.py").write_text(
|
||||||
|
"def test_func(): pass", encoding="utf-8"
|
||||||
|
)
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest()
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
"""
|
"""
|
||||||
|
@ -1939,7 +1960,8 @@ class TestAutouseManagement:
|
||||||
import sys
|
import sys
|
||||||
sys._myapp = "hello"
|
sys._myapp = "hello"
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
sub = pkgdir.joinpath("tests")
|
sub = pkgdir.joinpath("tests")
|
||||||
sub.mkdir()
|
sub.mkdir()
|
||||||
|
@ -1952,7 +1974,8 @@ class TestAutouseManagement:
|
||||||
def test_app():
|
def test_app():
|
||||||
assert sys._myapp == "hello"
|
assert sys._myapp == "hello"
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
reprec = pytester.inline_run("-s")
|
reprec = pytester.inline_run("-s")
|
||||||
reprec.assertoutcome(passed=1)
|
reprec.assertoutcome(passed=1)
|
||||||
|
@ -2882,7 +2905,7 @@ class TestFixtureMarker:
|
||||||
def browser(request):
|
def browser(request):
|
||||||
|
|
||||||
def finalize():
|
def finalize():
|
||||||
sys.stdout.write_text('Finalized')
|
sys.stdout.write_text('Finalized', encoding='utf-8')
|
||||||
request.addfinalizer(finalize)
|
request.addfinalizer(finalize)
|
||||||
return {}
|
return {}
|
||||||
"""
|
"""
|
||||||
|
@ -2900,7 +2923,8 @@ class TestFixtureMarker:
|
||||||
def test_browser(browser):
|
def test_browser(browser):
|
||||||
assert browser['visited'] is True
|
assert browser['visited'] is True
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
reprec = pytester.runpytest("-s")
|
reprec = pytester.runpytest("-s")
|
||||||
for test in ["test_browser"]:
|
for test in ["test_browser"]:
|
||||||
|
@ -3855,7 +3879,8 @@ class TestParameterizedSubRequest:
|
||||||
def fix_with_param(request):
|
def fix_with_param(request):
|
||||||
return request.param
|
return request.param
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
testfile = tests_dir.joinpath("test_foos.py")
|
testfile = tests_dir.joinpath("test_foos.py")
|
||||||
|
@ -3867,7 +3892,8 @@ class TestParameterizedSubRequest:
|
||||||
def test_foo(request):
|
def test_foo(request):
|
||||||
request.getfixturevalue('fix_with_param')
|
request.getfixturevalue('fix_with_param')
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
os.chdir(tests_dir)
|
os.chdir(tests_dir)
|
||||||
|
@ -4196,7 +4222,7 @@ class TestScopeOrdering:
|
||||||
└── test_2.py
|
└── test_2.py
|
||||||
"""
|
"""
|
||||||
root = pytester.mkdir("root")
|
root = pytester.mkdir("root")
|
||||||
root.joinpath("__init__.py").write_text("values = []")
|
root.joinpath("__init__.py").write_text("values = []", encoding="utf-8")
|
||||||
sub1 = root.joinpath("sub1")
|
sub1 = root.joinpath("sub1")
|
||||||
sub1.mkdir()
|
sub1.mkdir()
|
||||||
sub1.joinpath("__init__.py").touch()
|
sub1.joinpath("__init__.py").touch()
|
||||||
|
@ -4211,7 +4237,8 @@ class TestScopeOrdering:
|
||||||
yield values
|
yield values
|
||||||
assert values.pop() == "pre-sub1"
|
assert values.pop() == "pre-sub1"
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
sub1.joinpath("test_1.py").write_text(
|
sub1.joinpath("test_1.py").write_text(
|
||||||
textwrap.dedent(
|
textwrap.dedent(
|
||||||
|
@ -4220,7 +4247,8 @@ class TestScopeOrdering:
|
||||||
def test_1(fix):
|
def test_1(fix):
|
||||||
assert values == ["pre-sub1"]
|
assert values == ["pre-sub1"]
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
sub2 = root.joinpath("sub2")
|
sub2 = root.joinpath("sub2")
|
||||||
sub2.mkdir()
|
sub2.mkdir()
|
||||||
|
@ -4236,7 +4264,8 @@ class TestScopeOrdering:
|
||||||
yield values
|
yield values
|
||||||
assert values.pop() == "pre-sub2"
|
assert values.pop() == "pre-sub2"
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
sub2.joinpath("test_2.py").write_text(
|
sub2.joinpath("test_2.py").write_text(
|
||||||
textwrap.dedent(
|
textwrap.dedent(
|
||||||
|
@ -4245,7 +4274,8 @@ class TestScopeOrdering:
|
||||||
def test_2(fix):
|
def test_2(fix):
|
||||||
assert values == ["pre-sub2"]
|
assert values == ["pre-sub2"]
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
reprec = pytester.inline_run()
|
reprec = pytester.inline_run()
|
||||||
reprec.assertoutcome(passed=2)
|
reprec.assertoutcome(passed=2)
|
||||||
|
|
|
@ -19,7 +19,6 @@ from hypothesis import strategies
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest import fixtures
|
from _pytest import fixtures
|
||||||
from _pytest import python
|
from _pytest import python
|
||||||
from _pytest.compat import _format_args
|
|
||||||
from _pytest.compat import getfuncargnames
|
from _pytest.compat import getfuncargnames
|
||||||
from _pytest.compat import NOTSET
|
from _pytest.compat import NOTSET
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
|
@ -1036,27 +1035,6 @@ class TestMetafunc:
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_format_args(self) -> None:
|
|
||||||
def function1():
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert _format_args(function1) == "()"
|
|
||||||
|
|
||||||
def function2(arg1):
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert _format_args(function2) == "(arg1)"
|
|
||||||
|
|
||||||
def function3(arg1, arg2="qwe"):
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert _format_args(function3) == "(arg1, arg2='qwe')"
|
|
||||||
|
|
||||||
def function4(arg1, *args, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert _format_args(function4) == "(arg1, *args, **kwargs)"
|
|
||||||
|
|
||||||
|
|
||||||
class TestMetafuncFunctional:
|
class TestMetafuncFunctional:
|
||||||
def test_attributes(self, pytester: Pytester) -> None:
|
def test_attributes(self, pytester: Pytester) -> None:
|
||||||
|
@ -1443,7 +1421,8 @@ class TestMetafuncFunctional:
|
||||||
def pytest_generate_tests(metafunc):
|
def pytest_generate_tests(metafunc):
|
||||||
assert metafunc.function.__name__ == "test_1"
|
assert metafunc.function.__name__ == "test_1"
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
sub2.joinpath("conftest.py").write_text(
|
sub2.joinpath("conftest.py").write_text(
|
||||||
textwrap.dedent(
|
textwrap.dedent(
|
||||||
|
@ -1451,10 +1430,15 @@ class TestMetafuncFunctional:
|
||||||
def pytest_generate_tests(metafunc):
|
def pytest_generate_tests(metafunc):
|
||||||
assert metafunc.function.__name__ == "test_2"
|
assert metafunc.function.__name__ == "test_2"
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
sub1.joinpath("test_in_sub1.py").write_text(
|
||||||
|
"def test_1(): pass", encoding="utf-8"
|
||||||
|
)
|
||||||
|
sub2.joinpath("test_in_sub2.py").write_text(
|
||||||
|
"def test_2(): pass", encoding="utf-8"
|
||||||
)
|
)
|
||||||
sub1.joinpath("test_in_sub1.py").write_text("def test_1(): pass")
|
|
||||||
sub2.joinpath("test_in_sub2.py").write_text("def test_2(): pass")
|
|
||||||
result = pytester.runpytest("--keep-duplicates", "-v", "-s", sub1, sub2, sub1)
|
result = pytester.runpytest("--keep-duplicates", "-v", "-s", sub1, sub2, sub1)
|
||||||
result.assert_outcomes(passed=3)
|
result.assert_outcomes(passed=3)
|
||||||
|
|
||||||
|
|
|
@ -199,8 +199,8 @@ class TestImportHookInstallation:
|
||||||
return check
|
return check
|
||||||
""",
|
""",
|
||||||
"mainwrapper.py": """\
|
"mainwrapper.py": """\
|
||||||
|
import importlib.metadata
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest.compat import importlib_metadata
|
|
||||||
|
|
||||||
class DummyEntryPoint(object):
|
class DummyEntryPoint(object):
|
||||||
name = 'spam'
|
name = 'spam'
|
||||||
|
@ -220,7 +220,7 @@ class TestImportHookInstallation:
|
||||||
def distributions():
|
def distributions():
|
||||||
return (DummyDistInfo(),)
|
return (DummyDistInfo(),)
|
||||||
|
|
||||||
importlib_metadata.distributions = distributions
|
importlib.metadata.distributions = distributions
|
||||||
pytest.main()
|
pytest.main()
|
||||||
""",
|
""",
|
||||||
"test_foo.py": """\
|
"test_foo.py": """\
|
||||||
|
@ -1392,14 +1392,14 @@ def test_sequence_comparison_uses_repr(pytester: Pytester) -> None:
|
||||||
def test_assertrepr_loaded_per_dir(pytester: Pytester) -> None:
|
def test_assertrepr_loaded_per_dir(pytester: Pytester) -> None:
|
||||||
pytester.makepyfile(test_base=["def test_base(): assert 1 == 2"])
|
pytester.makepyfile(test_base=["def test_base(): assert 1 == 2"])
|
||||||
a = pytester.mkdir("a")
|
a = pytester.mkdir("a")
|
||||||
a.joinpath("test_a.py").write_text("def test_a(): assert 1 == 2")
|
a.joinpath("test_a.py").write_text("def test_a(): assert 1 == 2", encoding="utf-8")
|
||||||
a.joinpath("conftest.py").write_text(
|
a.joinpath("conftest.py").write_text(
|
||||||
'def pytest_assertrepr_compare(): return ["summary a"]'
|
'def pytest_assertrepr_compare(): return ["summary a"]', encoding="utf-8"
|
||||||
)
|
)
|
||||||
b = pytester.mkdir("b")
|
b = pytester.mkdir("b")
|
||||||
b.joinpath("test_b.py").write_text("def test_b(): assert 1 == 2")
|
b.joinpath("test_b.py").write_text("def test_b(): assert 1 == 2", encoding="utf-8")
|
||||||
b.joinpath("conftest.py").write_text(
|
b.joinpath("conftest.py").write_text(
|
||||||
'def pytest_assertrepr_compare(): return ["summary b"]'
|
'def pytest_assertrepr_compare(): return ["summary b"]', encoding="utf-8"
|
||||||
)
|
)
|
||||||
|
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest()
|
||||||
|
|
|
@ -131,9 +131,8 @@ class TestAssertionRewrite:
|
||||||
for n in [node, *ast.iter_child_nodes(node)]:
|
for n in [node, *ast.iter_child_nodes(node)]:
|
||||||
assert n.lineno == 3
|
assert n.lineno == 3
|
||||||
assert n.col_offset == 0
|
assert n.col_offset == 0
|
||||||
if sys.version_info >= (3, 8):
|
assert n.end_lineno == 6
|
||||||
assert n.end_lineno == 6
|
assert n.end_col_offset == 3
|
||||||
assert n.end_col_offset == 3
|
|
||||||
|
|
||||||
def test_dont_rewrite(self) -> None:
|
def test_dont_rewrite(self) -> None:
|
||||||
s = """'PYTEST_DONT_REWRITE'\nassert 14"""
|
s = """'PYTEST_DONT_REWRITE'\nassert 14"""
|
||||||
|
@ -160,7 +159,8 @@ class TestAssertionRewrite:
|
||||||
"def special_asserter():\n"
|
"def special_asserter():\n"
|
||||||
" def special_assert(x, y):\n"
|
" def special_assert(x, y):\n"
|
||||||
" assert x == y\n"
|
" assert x == y\n"
|
||||||
" return special_assert\n"
|
" return special_assert\n",
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
pytester.makeconftest('pytest_plugins = ["plugin"]')
|
pytester.makeconftest('pytest_plugins = ["plugin"]')
|
||||||
pytester.makepyfile("def test(special_asserter): special_asserter(1, 2)\n")
|
pytester.makepyfile("def test(special_asserter): special_asserter(1, 2)\n")
|
||||||
|
@ -173,7 +173,9 @@ class TestAssertionRewrite:
|
||||||
pytester.makepyfile(test_y="x = 1")
|
pytester.makepyfile(test_y="x = 1")
|
||||||
xdir = pytester.mkdir("x")
|
xdir = pytester.mkdir("x")
|
||||||
pytester.mkpydir(str(xdir.joinpath("test_Y")))
|
pytester.mkpydir(str(xdir.joinpath("test_Y")))
|
||||||
xdir.joinpath("test_Y").joinpath("__init__.py").write_text("x = 2")
|
xdir.joinpath("test_Y").joinpath("__init__.py").write_text(
|
||||||
|
"x = 2", encoding="utf-8"
|
||||||
|
)
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
"import test_y\n"
|
"import test_y\n"
|
||||||
"import test_Y\n"
|
"import test_Y\n"
|
||||||
|
@ -726,7 +728,7 @@ class TestAssertionRewrite:
|
||||||
|
|
||||||
class TestRewriteOnImport:
|
class TestRewriteOnImport:
|
||||||
def test_pycache_is_a_file(self, pytester: Pytester) -> None:
|
def test_pycache_is_a_file(self, pytester: Pytester) -> None:
|
||||||
pytester.path.joinpath("__pycache__").write_text("Hello")
|
pytester.path.joinpath("__pycache__").write_text("Hello", encoding="utf-8")
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
"""
|
"""
|
||||||
def test_rewritten():
|
def test_rewritten():
|
||||||
|
@ -903,7 +905,8 @@ def test_rewritten():
|
||||||
pkg.joinpath("test_blah.py").write_text(
|
pkg.joinpath("test_blah.py").write_text(
|
||||||
"""
|
"""
|
||||||
def test_rewritten():
|
def test_rewritten():
|
||||||
assert "@py_builtins" in globals()"""
|
assert "@py_builtins" in globals()""",
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
assert pytester.runpytest().ret == 0
|
assert pytester.runpytest().ret == 0
|
||||||
|
|
||||||
|
@ -1066,7 +1069,7 @@ class TestAssertionRewriteHookDetails:
|
||||||
source = tmp_path / "source.py"
|
source = tmp_path / "source.py"
|
||||||
pyc = Path(str(source) + "c")
|
pyc = Path(str(source) + "c")
|
||||||
|
|
||||||
source.write_text("def test(): pass")
|
source.write_text("def test(): pass", encoding="utf-8")
|
||||||
py_compile.compile(str(source), str(pyc))
|
py_compile.compile(str(source), str(pyc))
|
||||||
|
|
||||||
contents = pyc.read_bytes()
|
contents = pyc.read_bytes()
|
||||||
|
@ -1092,7 +1095,7 @@ class TestAssertionRewriteHookDetails:
|
||||||
fn = tmp_path / "source.py"
|
fn = tmp_path / "source.py"
|
||||||
pyc = Path(str(fn) + "c")
|
pyc = Path(str(fn) + "c")
|
||||||
|
|
||||||
fn.write_text("def test(): assert True")
|
fn.write_text("def test(): assert True", encoding="utf-8")
|
||||||
|
|
||||||
source_stat, co = _rewrite_test(fn, config)
|
source_stat, co = _rewrite_test(fn, config)
|
||||||
_write_pyc(state, co, source_stat, pyc)
|
_write_pyc(state, co, source_stat, pyc)
|
||||||
|
@ -1157,7 +1160,7 @@ class TestAssertionRewriteHookDetails:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def rewrite_self():
|
def rewrite_self():
|
||||||
with open(__file__, 'w') as self:
|
with open(__file__, 'w', encoding='utf-8') as self:
|
||||||
self.write('def reloaded(): return True')
|
self.write('def reloaded(): return True')
|
||||||
""",
|
""",
|
||||||
test_fun="""
|
test_fun="""
|
||||||
|
@ -1187,9 +1190,10 @@ class TestAssertionRewriteHookDetails:
|
||||||
data = pkgutil.get_data('foo.test_foo', 'data.txt')
|
data = pkgutil.get_data('foo.test_foo', 'data.txt')
|
||||||
assert data == b'Hey'
|
assert data == b'Hey'
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
path.joinpath("data.txt").write_text("Hey")
|
path.joinpath("data.txt").write_text("Hey", encoding="utf-8")
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest()
|
||||||
result.stdout.fnmatch_lines(["*1 passed*"])
|
result.stdout.fnmatch_lines(["*1 passed*"])
|
||||||
|
|
||||||
|
@ -1265,9 +1269,6 @@ class TestIssue2121:
|
||||||
result.stdout.fnmatch_lines(["*E*assert (1 + 1) == 3"])
|
result.stdout.fnmatch_lines(["*E*assert (1 + 1) == 3"])
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
|
||||||
sys.version_info < (3, 8), reason="walrus operator not available in py<38"
|
|
||||||
)
|
|
||||||
class TestIssue10743:
|
class TestIssue10743:
|
||||||
def test_assertion_walrus_operator(self, pytester: Pytester) -> None:
|
def test_assertion_walrus_operator(self, pytester: Pytester) -> None:
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
|
@ -1436,6 +1437,93 @@ class TestIssue10743:
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestIssue11028:
|
||||||
|
def test_assertion_walrus_operator_in_operand(self, pytester: Pytester) -> None:
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
def test_in_string():
|
||||||
|
assert (obj := "foo") in obj
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest()
|
||||||
|
assert result.ret == 0
|
||||||
|
|
||||||
|
def test_assertion_walrus_operator_in_operand_json_dumps(
|
||||||
|
self, pytester: Pytester
|
||||||
|
) -> None:
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
def test_json_encoder():
|
||||||
|
assert (obj := "foo") in json.dumps(obj)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest()
|
||||||
|
assert result.ret == 0
|
||||||
|
|
||||||
|
def test_assertion_walrus_operator_equals_operand_function(
|
||||||
|
self, pytester: Pytester
|
||||||
|
) -> None:
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
def f(a):
|
||||||
|
return a
|
||||||
|
|
||||||
|
def test_call_other_function_arg():
|
||||||
|
assert (obj := "foo") == f(obj)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest()
|
||||||
|
assert result.ret == 0
|
||||||
|
|
||||||
|
def test_assertion_walrus_operator_equals_operand_function_keyword_arg(
|
||||||
|
self, pytester: Pytester
|
||||||
|
) -> None:
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
def f(a='test'):
|
||||||
|
return a
|
||||||
|
|
||||||
|
def test_call_other_function_k_arg():
|
||||||
|
assert (obj := "foo") == f(a=obj)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest()
|
||||||
|
assert result.ret == 0
|
||||||
|
|
||||||
|
def test_assertion_walrus_operator_equals_operand_function_arg_as_function(
|
||||||
|
self, pytester: Pytester
|
||||||
|
) -> None:
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
def f(a='test'):
|
||||||
|
return a
|
||||||
|
|
||||||
|
def test_function_of_function():
|
||||||
|
assert (obj := "foo") == f(f(obj))
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest()
|
||||||
|
assert result.ret == 0
|
||||||
|
|
||||||
|
def test_assertion_walrus_operator_gt_operand_function(
|
||||||
|
self, pytester: Pytester
|
||||||
|
) -> None:
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
def add_one(a):
|
||||||
|
return a + 1
|
||||||
|
|
||||||
|
def test_gt():
|
||||||
|
assert (obj := 4) > add_one(obj)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest()
|
||||||
|
assert result.ret == 1
|
||||||
|
result.stdout.fnmatch_lines(["*assert 4 > 5", "*where 5 = add_one(4)"])
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
sys.maxsize <= (2**31 - 1), reason="Causes OverflowError on 32bit systems"
|
sys.maxsize <= (2**31 - 1), reason="Causes OverflowError on 32bit systems"
|
||||||
)
|
)
|
||||||
|
@ -1862,16 +1950,10 @@ class TestPyCacheDir:
|
||||||
)
|
)
|
||||||
def test_get_cache_dir(self, monkeypatch, prefix, source, expected) -> None:
|
def test_get_cache_dir(self, monkeypatch, prefix, source, expected) -> None:
|
||||||
monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False)
|
monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False)
|
||||||
|
|
||||||
if prefix is not None and sys.version_info < (3, 8):
|
|
||||||
pytest.skip("pycache_prefix not available in py<38")
|
|
||||||
monkeypatch.setattr(sys, "pycache_prefix", prefix, raising=False)
|
monkeypatch.setattr(sys, "pycache_prefix", prefix, raising=False)
|
||||||
|
|
||||||
assert get_cache_dir(Path(source)) == Path(expected)
|
assert get_cache_dir(Path(source)) == Path(expected)
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
|
||||||
sys.version_info < (3, 8), reason="pycache_prefix not available in py<38"
|
|
||||||
)
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
sys.version_info[:2] == (3, 9) and sys.platform.startswith("win"),
|
sys.version_info[:2] == (3, 9) and sys.platform.startswith("win"),
|
||||||
reason="#9298",
|
reason="#9298",
|
||||||
|
|
|
@ -38,7 +38,9 @@ class TestNewAPI:
|
||||||
@pytest.mark.filterwarnings("ignore:could not create cache path")
|
@pytest.mark.filterwarnings("ignore:could not create cache path")
|
||||||
def test_cache_writefail_cachfile_silent(self, pytester: Pytester) -> None:
|
def test_cache_writefail_cachfile_silent(self, pytester: Pytester) -> None:
|
||||||
pytester.makeini("[pytest]")
|
pytester.makeini("[pytest]")
|
||||||
pytester.path.joinpath(".pytest_cache").write_text("gone wrong")
|
pytester.path.joinpath(".pytest_cache").write_text(
|
||||||
|
"gone wrong", encoding="utf-8"
|
||||||
|
)
|
||||||
config = pytester.parseconfigure()
|
config = pytester.parseconfigure()
|
||||||
cache = config.cache
|
cache = config.cache
|
||||||
assert cache is not None
|
assert cache is not None
|
||||||
|
@ -87,7 +89,7 @@ class TestNewAPI:
|
||||||
"*= warnings summary =*",
|
"*= warnings summary =*",
|
||||||
"*/cacheprovider.py:*",
|
"*/cacheprovider.py:*",
|
||||||
" */cacheprovider.py:*: PytestCacheWarning: could not create cache path "
|
" */cacheprovider.py:*: PytestCacheWarning: could not create cache path "
|
||||||
f"{unwritable_cache_dir}/v/cache/nodeids",
|
f"{unwritable_cache_dir}/v/cache/nodeids: *",
|
||||||
' config.cache.set("cache/nodeids", sorted(self.cached_nodeids))',
|
' config.cache.set("cache/nodeids", sorted(self.cached_nodeids))',
|
||||||
"*1 failed, 3 warnings in*",
|
"*1 failed, 3 warnings in*",
|
||||||
]
|
]
|
||||||
|
@ -420,7 +422,13 @@ class TestLastFailed:
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest()
|
||||||
result.stdout.fnmatch_lines(["*1 failed in*"])
|
result.stdout.fnmatch_lines(["*1 failed in*"])
|
||||||
|
|
||||||
def test_terminal_report_lastfailed(self, pytester: Pytester) -> None:
|
@pytest.mark.parametrize("parent", ("session", "package"))
|
||||||
|
def test_terminal_report_lastfailed(self, pytester: Pytester, parent: str) -> None:
|
||||||
|
if parent == "package":
|
||||||
|
pytester.makepyfile(
|
||||||
|
__init__="",
|
||||||
|
)
|
||||||
|
|
||||||
test_a = pytester.makepyfile(
|
test_a = pytester.makepyfile(
|
||||||
test_a="""
|
test_a="""
|
||||||
def test_a1(): pass
|
def test_a1(): pass
|
||||||
|
@ -848,6 +856,33 @@ class TestLastFailed:
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_lastfailed_skip_collection_with_nesting(self, pytester: Pytester) -> None:
|
||||||
|
"""Check that file skipping works even when the file with failures is
|
||||||
|
nested at a different level of the collection tree."""
|
||||||
|
pytester.makepyfile(
|
||||||
|
**{
|
||||||
|
"test_1.py": """
|
||||||
|
def test_1(): pass
|
||||||
|
""",
|
||||||
|
"pkg/__init__.py": "",
|
||||||
|
"pkg/test_2.py": """
|
||||||
|
def test_2(): assert False
|
||||||
|
""",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# first run
|
||||||
|
result = pytester.runpytest()
|
||||||
|
result.stdout.fnmatch_lines(["collected 2 items", "*1 failed*1 passed*"])
|
||||||
|
# second run - test_1.py is skipped.
|
||||||
|
result = pytester.runpytest("--lf")
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
[
|
||||||
|
"collected 1 item",
|
||||||
|
"run-last-failure: rerun previous 1 failure (skipped 1 file)",
|
||||||
|
"*= 1 failed in *",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
def test_lastfailed_with_known_failures_not_being_selected(
|
def test_lastfailed_with_known_failures_not_being_selected(
|
||||||
self, pytester: Pytester
|
self, pytester: Pytester
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -1052,6 +1087,28 @@ class TestLastFailed:
|
||||||
result = pytester.runpytest("--lf")
|
result = pytester.runpytest("--lf")
|
||||||
result.assert_outcomes(failed=3)
|
result.assert_outcomes(failed=3)
|
||||||
|
|
||||||
|
def test_non_python_file_skipped(
|
||||||
|
self,
|
||||||
|
pytester: Pytester,
|
||||||
|
dummy_yaml_custom_test: None,
|
||||||
|
) -> None:
|
||||||
|
pytester.makepyfile(
|
||||||
|
**{
|
||||||
|
"test_bad.py": """def test_bad(): assert False""",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
result = pytester.runpytest()
|
||||||
|
result.stdout.fnmatch_lines(["collected 2 items", "* 1 failed, 1 passed in *"])
|
||||||
|
|
||||||
|
result = pytester.runpytest("--lf")
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
[
|
||||||
|
"collected 1 item",
|
||||||
|
"run-last-failure: rerun previous 1 failure (skipped 1 file)",
|
||||||
|
"* 1 failed in *",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestNewFirst:
|
class TestNewFirst:
|
||||||
def test_newfirst_usecase(self, pytester: Pytester) -> None:
|
def test_newfirst_usecase(self, pytester: Pytester) -> None:
|
||||||
|
@ -1079,7 +1136,9 @@ class TestNewFirst:
|
||||||
["*test_2/test_2.py::test_1 PASSED*", "*test_1/test_1.py::test_1 PASSED*"]
|
["*test_2/test_2.py::test_1 PASSED*", "*test_1/test_1.py::test_1 PASSED*"]
|
||||||
)
|
)
|
||||||
|
|
||||||
p1.write_text("def test_1(): assert 1\n" "def test_2(): assert 1\n")
|
p1.write_text(
|
||||||
|
"def test_1(): assert 1\n" "def test_2(): assert 1\n", encoding="utf-8"
|
||||||
|
)
|
||||||
os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9)))
|
os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9)))
|
||||||
|
|
||||||
result = pytester.runpytest("--nf", "--collect-only", "-q")
|
result = pytester.runpytest("--nf", "--collect-only", "-q")
|
||||||
|
@ -1152,7 +1211,8 @@ class TestNewFirst:
|
||||||
p1.write_text(
|
p1.write_text(
|
||||||
"import pytest\n"
|
"import pytest\n"
|
||||||
"@pytest.mark.parametrize('num', [1, 2, 3])\n"
|
"@pytest.mark.parametrize('num', [1, 2, 3])\n"
|
||||||
"def test_1(num): assert num\n"
|
"def test_1(num): assert num\n",
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9)))
|
os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9)))
|
||||||
|
|
||||||
|
@ -1204,7 +1264,7 @@ def test_gitignore(pytester: Pytester) -> None:
|
||||||
assert gitignore_path.read_text(encoding="UTF-8") == msg
|
assert gitignore_path.read_text(encoding="UTF-8") == msg
|
||||||
|
|
||||||
# Does not overwrite existing/custom one.
|
# Does not overwrite existing/custom one.
|
||||||
gitignore_path.write_text("custom")
|
gitignore_path.write_text("custom", encoding="utf-8")
|
||||||
cache.set("something", "else")
|
cache.set("something", "else")
|
||||||
assert gitignore_path.read_text(encoding="UTF-8") == "custom"
|
assert gitignore_path.read_text(encoding="UTF-8") == "custom"
|
||||||
|
|
||||||
|
|
|
@ -750,9 +750,10 @@ def test_setup_failure_does_not_kill_capturing(pytester: Pytester) -> None:
|
||||||
def pytest_runtest_setup(item):
|
def pytest_runtest_setup(item):
|
||||||
raise ValueError(42)
|
raise ValueError(42)
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
sub1.joinpath("test_mod.py").write_text("def test_func1(): pass")
|
sub1.joinpath("test_mod.py").write_text("def test_func1(): pass", encoding="utf-8")
|
||||||
result = pytester.runpytest(pytester.path, "--traceconfig")
|
result = pytester.runpytest(pytester.path, "--traceconfig")
|
||||||
result.stdout.fnmatch_lines(["*ValueError(42)*", "*1 error*"])
|
result.stdout.fnmatch_lines(["*ValueError(42)*", "*1 error*"])
|
||||||
|
|
||||||
|
@ -1523,9 +1524,9 @@ def test_global_capture_with_live_logging(pytester: Pytester) -> None:
|
||||||
def pytest_runtest_logreport(report):
|
def pytest_runtest_logreport(report):
|
||||||
if "test_global" in report.nodeid:
|
if "test_global" in report.nodeid:
|
||||||
if report.when == "teardown":
|
if report.when == "teardown":
|
||||||
with open("caplog", "w") as f:
|
with open("caplog", "w", encoding="utf-8") as f:
|
||||||
f.write(report.caplog)
|
f.write(report.caplog)
|
||||||
with open("capstdout", "w") as f:
|
with open("capstdout", "w", encoding="utf-8") as f:
|
||||||
f.write(report.capstdout)
|
f.write(report.capstdout)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
@ -1555,14 +1556,14 @@ def test_global_capture_with_live_logging(pytester: Pytester) -> None:
|
||||||
result = pytester.runpytest_subprocess("--log-cli-level=INFO")
|
result = pytester.runpytest_subprocess("--log-cli-level=INFO")
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
|
|
||||||
with open("caplog") as f:
|
with open("caplog", encoding="utf-8") as f:
|
||||||
caplog = f.read()
|
caplog = f.read()
|
||||||
|
|
||||||
assert "fix setup" in caplog
|
assert "fix setup" in caplog
|
||||||
assert "something in test" in caplog
|
assert "something in test" in caplog
|
||||||
assert "fix teardown" in caplog
|
assert "fix teardown" in caplog
|
||||||
|
|
||||||
with open("capstdout") as f:
|
with open("capstdout", encoding="utf-8") as f:
|
||||||
capstdout = f.read()
|
capstdout = f.read()
|
||||||
|
|
||||||
assert "fix setup" in capstdout
|
assert "fix setup" in capstdout
|
||||||
|
|
|
@ -140,7 +140,7 @@ class TestCollectFS:
|
||||||
ensure_file(tmp_path / ".bzr" / "test_notfound.py")
|
ensure_file(tmp_path / ".bzr" / "test_notfound.py")
|
||||||
ensure_file(tmp_path / "normal" / "test_found.py")
|
ensure_file(tmp_path / "normal" / "test_found.py")
|
||||||
for x in tmp_path.rglob("test_*.py"):
|
for x in tmp_path.rglob("test_*.py"):
|
||||||
x.write_text("def test_hello(): pass", "utf-8")
|
x.write_text("def test_hello(): pass", encoding="utf-8")
|
||||||
|
|
||||||
result = pytester.runpytest("--collect-only")
|
result = pytester.runpytest("--collect-only")
|
||||||
s = result.stdout.str()
|
s = result.stdout.str()
|
||||||
|
@ -162,7 +162,7 @@ class TestCollectFS:
|
||||||
bindir = "Scripts" if sys.platform.startswith("win") else "bin"
|
bindir = "Scripts" if sys.platform.startswith("win") else "bin"
|
||||||
ensure_file(pytester.path / "virtual" / bindir / fname)
|
ensure_file(pytester.path / "virtual" / bindir / fname)
|
||||||
testfile = ensure_file(pytester.path / "virtual" / "test_invenv.py")
|
testfile = ensure_file(pytester.path / "virtual" / "test_invenv.py")
|
||||||
testfile.write_text("def test_hello(): pass")
|
testfile.write_text("def test_hello(): pass", encoding="utf-8")
|
||||||
|
|
||||||
# by default, ignore tests inside a virtualenv
|
# by default, ignore tests inside a virtualenv
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest()
|
||||||
|
@ -192,7 +192,7 @@ class TestCollectFS:
|
||||||
# norecursedirs takes priority
|
# norecursedirs takes priority
|
||||||
ensure_file(pytester.path / ".virtual" / bindir / fname)
|
ensure_file(pytester.path / ".virtual" / bindir / fname)
|
||||||
testfile = ensure_file(pytester.path / ".virtual" / "test_invenv.py")
|
testfile = ensure_file(pytester.path / ".virtual" / "test_invenv.py")
|
||||||
testfile.write_text("def test_hello(): pass")
|
testfile.write_text("def test_hello(): pass", encoding="utf-8")
|
||||||
result = pytester.runpytest("--collect-in-virtualenv")
|
result = pytester.runpytest("--collect-in-virtualenv")
|
||||||
result.stdout.no_fnmatch_line("*test_invenv*")
|
result.stdout.no_fnmatch_line("*test_invenv*")
|
||||||
# ...unless the virtualenv is explicitly given on the CLI
|
# ...unless the virtualenv is explicitly given on the CLI
|
||||||
|
@ -231,10 +231,14 @@ class TestCollectFS:
|
||||||
)
|
)
|
||||||
tmp_path = pytester.path
|
tmp_path = pytester.path
|
||||||
ensure_file(tmp_path / "mydir" / "test_hello.py").write_text(
|
ensure_file(tmp_path / "mydir" / "test_hello.py").write_text(
|
||||||
"def test_1(): pass"
|
"def test_1(): pass", encoding="utf-8"
|
||||||
|
)
|
||||||
|
ensure_file(tmp_path / "xyz123" / "test_2.py").write_text(
|
||||||
|
"def test_2(): 0/0", encoding="utf-8"
|
||||||
|
)
|
||||||
|
ensure_file(tmp_path / "xy" / "test_ok.py").write_text(
|
||||||
|
"def test_3(): pass", encoding="utf-8"
|
||||||
)
|
)
|
||||||
ensure_file(tmp_path / "xyz123" / "test_2.py").write_text("def test_2(): 0/0")
|
|
||||||
ensure_file(tmp_path / "xy" / "test_ok.py").write_text("def test_3(): pass")
|
|
||||||
rec = pytester.inline_run()
|
rec = pytester.inline_run()
|
||||||
rec.assertoutcome(passed=1)
|
rec.assertoutcome(passed=1)
|
||||||
rec = pytester.inline_run("xyz123/test_2.py")
|
rec = pytester.inline_run("xyz123/test_2.py")
|
||||||
|
@ -248,12 +252,14 @@ class TestCollectFS:
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
tmp_path = pytester.path
|
tmp_path = pytester.path
|
||||||
ensure_file(tmp_path / "a" / "test_1.py").write_text("def test_a(): pass")
|
ensure_file(tmp_path / "a" / "test_1.py").write_text(
|
||||||
|
"def test_a(): pass", encoding="utf-8"
|
||||||
|
)
|
||||||
ensure_file(tmp_path / "b" / "tests" / "test_2.py").write_text(
|
ensure_file(tmp_path / "b" / "tests" / "test_2.py").write_text(
|
||||||
"def test_b(): pass"
|
"def test_b(): pass", encoding="utf-8"
|
||||||
)
|
)
|
||||||
ensure_file(tmp_path / "c" / "tests" / "test_3.py").write_text(
|
ensure_file(tmp_path / "c" / "tests" / "test_3.py").write_text(
|
||||||
"def test_c(): pass"
|
"def test_c(): pass", encoding="utf-8"
|
||||||
)
|
)
|
||||||
|
|
||||||
# executing from rootdir only tests from `testpaths` directories
|
# executing from rootdir only tests from `testpaths` directories
|
||||||
|
@ -349,8 +355,8 @@ class TestCustomConftests:
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
sub = pytester.mkdir("xy123")
|
sub = pytester.mkdir("xy123")
|
||||||
ensure_file(sub / "test_hello.py").write_text("syntax error")
|
ensure_file(sub / "test_hello.py").write_text("syntax error", encoding="utf-8")
|
||||||
sub.joinpath("conftest.py").write_text("syntax error")
|
sub.joinpath("conftest.py").write_text("syntax error", encoding="utf-8")
|
||||||
pytester.makepyfile("def test_hello(): pass")
|
pytester.makepyfile("def test_hello(): pass")
|
||||||
pytester.makepyfile(test_one="syntax error")
|
pytester.makepyfile(test_one="syntax error")
|
||||||
result = pytester.runpytest("--fulltrace")
|
result = pytester.runpytest("--fulltrace")
|
||||||
|
@ -1060,13 +1066,18 @@ def test_fixture_scope_sibling_conftests(pytester: Pytester) -> None:
|
||||||
def fix():
|
def fix():
|
||||||
return 1
|
return 1
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
foo_path.joinpath("test_foo.py").write_text(
|
||||||
|
"def test_foo(fix): assert fix == 1", encoding="utf-8"
|
||||||
)
|
)
|
||||||
foo_path.joinpath("test_foo.py").write_text("def test_foo(fix): assert fix == 1")
|
|
||||||
|
|
||||||
# Tests in `food/` should not see the conftest fixture from `foo/`
|
# Tests in `food/` should not see the conftest fixture from `foo/`
|
||||||
food_path = pytester.mkpydir("food")
|
food_path = pytester.mkpydir("food")
|
||||||
food_path.joinpath("test_food.py").write_text("def test_food(fix): assert fix == 1")
|
food_path.joinpath("test_food.py").write_text(
|
||||||
|
"def test_food(fix): assert fix == 1", encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
res = pytester.runpytest()
|
res = pytester.runpytest()
|
||||||
assert res.ret == 1
|
assert res.ret == 1
|
||||||
|
@ -1197,7 +1208,8 @@ def test_collect_with_chdir_during_import(pytester: Pytester) -> None:
|
||||||
os.chdir(%r)
|
os.chdir(%r)
|
||||||
"""
|
"""
|
||||||
% (str(subdir),)
|
% (str(subdir),)
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
"""
|
"""
|
||||||
|
@ -1227,8 +1239,12 @@ def test_collect_pyargs_with_testpaths(
|
||||||
) -> None:
|
) -> None:
|
||||||
testmod = pytester.mkdir("testmod")
|
testmod = pytester.mkdir("testmod")
|
||||||
# NOTE: __init__.py is not collected since it does not match python_files.
|
# NOTE: __init__.py is not collected since it does not match python_files.
|
||||||
testmod.joinpath("__init__.py").write_text("def test_func(): pass")
|
testmod.joinpath("__init__.py").write_text(
|
||||||
testmod.joinpath("test_file.py").write_text("def test_func(): pass")
|
"def test_func(): pass", encoding="utf-8"
|
||||||
|
)
|
||||||
|
testmod.joinpath("test_file.py").write_text(
|
||||||
|
"def test_func(): pass", encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
root = pytester.mkdir("root")
|
root = pytester.mkdir("root")
|
||||||
root.joinpath("pytest.ini").write_text(
|
root.joinpath("pytest.ini").write_text(
|
||||||
|
@ -1238,7 +1254,8 @@ def test_collect_pyargs_with_testpaths(
|
||||||
addopts = --pyargs
|
addopts = --pyargs
|
||||||
testpaths = testmod
|
testpaths = testmod
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
monkeypatch.setenv("PYTHONPATH", str(pytester.path), prepend=os.pathsep)
|
monkeypatch.setenv("PYTHONPATH", str(pytester.path), prepend=os.pathsep)
|
||||||
with monkeypatch.context() as mp:
|
with monkeypatch.context() as mp:
|
||||||
|
@ -1256,7 +1273,8 @@ def test_initial_conftests_with_testpaths(pytester: Pytester) -> None:
|
||||||
def pytest_sessionstart(session):
|
def pytest_sessionstart(session):
|
||||||
raise Exception("pytest_sessionstart hook successfully run")
|
raise Exception("pytest_sessionstart hook successfully run")
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
pytester.makeini(
|
pytester.makeini(
|
||||||
"""
|
"""
|
||||||
|
@ -1264,11 +1282,18 @@ def test_initial_conftests_with_testpaths(pytester: Pytester) -> None:
|
||||||
testpaths = some_path
|
testpaths = some_path
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# No command line args - falls back to testpaths.
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest()
|
||||||
|
assert result.ret == ExitCode.INTERNAL_ERROR
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
"INTERNALERROR* Exception: pytest_sessionstart hook successfully run"
|
"INTERNALERROR* Exception: pytest_sessionstart hook successfully run"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# No fallback.
|
||||||
|
result = pytester.runpytest(".")
|
||||||
|
assert result.ret == ExitCode.NO_TESTS_COLLECTED
|
||||||
|
|
||||||
|
|
||||||
def test_large_option_breaks_initial_conftests(pytester: Pytester) -> None:
|
def test_large_option_breaks_initial_conftests(pytester: Pytester) -> None:
|
||||||
"""Long option values do not break initial conftests handling (#10169)."""
|
"""Long option values do not break initial conftests handling (#10169)."""
|
||||||
|
@ -1316,6 +1341,7 @@ def test_collect_symlink_out_of_tree(pytester: Pytester) -> None:
|
||||||
assert request.node.nodeid == "test_real.py::test_nodeid"
|
assert request.node.nodeid == "test_real.py::test_nodeid"
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
out_of_tree = pytester.mkdir("out_of_tree")
|
out_of_tree = pytester.mkdir("out_of_tree")
|
||||||
|
@ -1344,12 +1370,16 @@ def test_collect_symlink_dir(pytester: Pytester) -> None:
|
||||||
def test_collectignore_via_conftest(pytester: Pytester) -> None:
|
def test_collectignore_via_conftest(pytester: Pytester) -> None:
|
||||||
"""collect_ignore in parent conftest skips importing child (issue #4592)."""
|
"""collect_ignore in parent conftest skips importing child (issue #4592)."""
|
||||||
tests = pytester.mkpydir("tests")
|
tests = pytester.mkpydir("tests")
|
||||||
tests.joinpath("conftest.py").write_text("collect_ignore = ['ignore_me']")
|
tests.joinpath("conftest.py").write_text(
|
||||||
|
"collect_ignore = ['ignore_me']", encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
ignore_me = tests.joinpath("ignore_me")
|
ignore_me = tests.joinpath("ignore_me")
|
||||||
ignore_me.mkdir()
|
ignore_me.mkdir()
|
||||||
ignore_me.joinpath("__init__.py").touch()
|
ignore_me.joinpath("__init__.py").touch()
|
||||||
ignore_me.joinpath("conftest.py").write_text("assert 0, 'should_not_be_called'")
|
ignore_me.joinpath("conftest.py").write_text(
|
||||||
|
"assert 0, 'should_not_be_called'", encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest()
|
||||||
assert result.ret == ExitCode.NO_TESTS_COLLECTED
|
assert result.ret == ExitCode.NO_TESTS_COLLECTED
|
||||||
|
@ -1358,23 +1388,31 @@ def test_collectignore_via_conftest(pytester: Pytester) -> None:
|
||||||
def test_collect_pkg_init_and_file_in_args(pytester: Pytester) -> None:
|
def test_collect_pkg_init_and_file_in_args(pytester: Pytester) -> None:
|
||||||
subdir = pytester.mkdir("sub")
|
subdir = pytester.mkdir("sub")
|
||||||
init = subdir.joinpath("__init__.py")
|
init = subdir.joinpath("__init__.py")
|
||||||
init.write_text("def test_init(): pass")
|
init.write_text("def test_init(): pass", encoding="utf-8")
|
||||||
p = subdir.joinpath("test_file.py")
|
p = subdir.joinpath("test_file.py")
|
||||||
p.write_text("def test_file(): pass")
|
p.write_text("def test_file(): pass", encoding="utf-8")
|
||||||
|
|
||||||
# NOTE: without "-o python_files=*.py" this collects test_file.py twice.
|
# Just the package directory, the __init__.py module is filtered out.
|
||||||
# This changed/broke with "Add package scoped fixtures #2283" (2b1410895)
|
result = pytester.runpytest("-v", subdir)
|
||||||
# initially (causing a RecursionError).
|
|
||||||
result = pytester.runpytest("-v", str(init), str(p))
|
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
[
|
[
|
||||||
"sub/test_file.py::test_file PASSED*",
|
"sub/test_file.py::test_file PASSED*",
|
||||||
|
"*1 passed in*",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# But it's included if specified directly.
|
||||||
|
result = pytester.runpytest("-v", init, p)
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
[
|
||||||
|
"sub/__init__.py::test_init PASSED*",
|
||||||
"sub/test_file.py::test_file PASSED*",
|
"sub/test_file.py::test_file PASSED*",
|
||||||
"*2 passed in*",
|
"*2 passed in*",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
result = pytester.runpytest("-v", "-o", "python_files=*.py", str(init), str(p))
|
# Or if the pattern allows it.
|
||||||
|
result = pytester.runpytest("-v", "-o", "python_files=*.py", subdir)
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
[
|
[
|
||||||
"sub/__init__.py::test_init PASSED*",
|
"sub/__init__.py::test_init PASSED*",
|
||||||
|
@ -1387,12 +1425,15 @@ def test_collect_pkg_init_and_file_in_args(pytester: Pytester) -> None:
|
||||||
def test_collect_pkg_init_only(pytester: Pytester) -> None:
|
def test_collect_pkg_init_only(pytester: Pytester) -> None:
|
||||||
subdir = pytester.mkdir("sub")
|
subdir = pytester.mkdir("sub")
|
||||||
init = subdir.joinpath("__init__.py")
|
init = subdir.joinpath("__init__.py")
|
||||||
init.write_text("def test_init(): pass")
|
init.write_text("def test_init(): pass", encoding="utf-8")
|
||||||
|
|
||||||
result = pytester.runpytest(str(init))
|
result = pytester.runpytest(subdir)
|
||||||
result.stdout.fnmatch_lines(["*no tests ran in*"])
|
result.stdout.fnmatch_lines(["*no tests ran in*"])
|
||||||
|
|
||||||
result = pytester.runpytest("-v", "-o", "python_files=*.py", str(init))
|
result = pytester.runpytest("-v", init)
|
||||||
|
result.stdout.fnmatch_lines(["sub/__init__.py::test_init PASSED*", "*1 passed in*"])
|
||||||
|
|
||||||
|
result = pytester.runpytest("-v", "-o", "python_files=*.py", subdir)
|
||||||
result.stdout.fnmatch_lines(["sub/__init__.py::test_init PASSED*", "*1 passed in*"])
|
result.stdout.fnmatch_lines(["sub/__init__.py::test_init PASSED*", "*1 passed in*"])
|
||||||
|
|
||||||
|
|
||||||
|
@ -1402,7 +1443,7 @@ def test_collect_sub_with_symlinks(use_pkg: bool, pytester: Pytester) -> None:
|
||||||
sub = pytester.mkdir("sub")
|
sub = pytester.mkdir("sub")
|
||||||
if use_pkg:
|
if use_pkg:
|
||||||
sub.joinpath("__init__.py").touch()
|
sub.joinpath("__init__.py").touch()
|
||||||
sub.joinpath("test_file.py").write_text("def test_file(): pass")
|
sub.joinpath("test_file.py").write_text("def test_file(): pass", encoding="utf-8")
|
||||||
|
|
||||||
# Create a broken symlink.
|
# Create a broken symlink.
|
||||||
symlink_or_skip("test_doesnotexist.py", sub.joinpath("test_broken.py"))
|
symlink_or_skip("test_doesnotexist.py", sub.joinpath("test_broken.py"))
|
||||||
|
@ -1440,7 +1481,7 @@ def test_collector_respects_tbstyle(pytester: Pytester) -> None:
|
||||||
def test_does_not_eagerly_collect_packages(pytester: Pytester) -> None:
|
def test_does_not_eagerly_collect_packages(pytester: Pytester) -> None:
|
||||||
pytester.makepyfile("def test(): pass")
|
pytester.makepyfile("def test(): pass")
|
||||||
pydir = pytester.mkpydir("foopkg")
|
pydir = pytester.mkpydir("foopkg")
|
||||||
pydir.joinpath("__init__.py").write_text("assert False")
|
pydir.joinpath("__init__.py").write_text("assert False", encoding="utf-8")
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest()
|
||||||
assert result.ret == ExitCode.OK
|
assert result.ret == ExitCode.OK
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import enum
|
import enum
|
||||||
import sys
|
import sys
|
||||||
|
from functools import cached_property
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
@ -8,7 +9,6 @@ from typing import Union
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest.compat import _PytestWrapper
|
from _pytest.compat import _PytestWrapper
|
||||||
from _pytest.compat import assert_never
|
from _pytest.compat import assert_never
|
||||||
from _pytest.compat import cached_property
|
|
||||||
from _pytest.compat import get_real_func
|
from _pytest.compat import get_real_func
|
||||||
from _pytest.compat import is_generator
|
from _pytest.compat import is_generator
|
||||||
from _pytest.compat import safe_getattr
|
from _pytest.compat import safe_getattr
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
import importlib.metadata
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
@ -13,7 +14,6 @@ from typing import Union
|
||||||
|
|
||||||
import _pytest._code
|
import _pytest._code
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest.compat import importlib_metadata
|
|
||||||
from _pytest.config import _get_plugin_specs_as_list
|
from _pytest.config import _get_plugin_specs_as_list
|
||||||
from _pytest.config import _iter_rewritable_modules
|
from _pytest.config import _iter_rewritable_modules
|
||||||
from _pytest.config import _strtobool
|
from _pytest.config import _strtobool
|
||||||
|
@ -87,7 +87,8 @@ class TestParseIni:
|
||||||
[pytest]
|
[pytest]
|
||||||
addopts = --verbose
|
addopts = --verbose
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
config = pytester.parseconfig(tmp_path)
|
config = pytester.parseconfig(tmp_path)
|
||||||
assert config.option.color == "no"
|
assert config.option.color == "no"
|
||||||
|
@ -127,7 +128,8 @@ class TestParseIni:
|
||||||
""".format(
|
""".format(
|
||||||
section=section
|
section=section
|
||||||
)
|
)
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
config = pytester.parseconfig()
|
config = pytester.parseconfig()
|
||||||
assert config.getini("minversion") == "3.36"
|
assert config.getini("minversion") == "3.36"
|
||||||
|
@ -150,7 +152,8 @@ class TestParseIni:
|
||||||
[pytest]
|
[pytest]
|
||||||
minversion = 2.0
|
minversion = 2.0
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
pytester.path.joinpath("pytest.ini").write_text(
|
pytester.path.joinpath("pytest.ini").write_text(
|
||||||
textwrap.dedent(
|
textwrap.dedent(
|
||||||
|
@ -158,13 +161,16 @@ class TestParseIni:
|
||||||
[pytest]
|
[pytest]
|
||||||
minversion = 1.5
|
minversion = 1.5
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
config = pytester.parseconfigure(sub)
|
config = pytester.parseconfigure(sub)
|
||||||
assert config.getini("minversion") == "2.0"
|
assert config.getini("minversion") == "2.0"
|
||||||
|
|
||||||
def test_ini_parse_error(self, pytester: Pytester) -> None:
|
def test_ini_parse_error(self, pytester: Pytester) -> None:
|
||||||
pytester.path.joinpath("pytest.ini").write_text("addopts = -x")
|
pytester.path.joinpath("pytest.ini").write_text(
|
||||||
|
"addopts = -x", encoding="utf-8"
|
||||||
|
)
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest()
|
||||||
assert result.ret != 0
|
assert result.ret != 0
|
||||||
result.stderr.fnmatch_lines("ERROR: *pytest.ini:1: no section header defined")
|
result.stderr.fnmatch_lines("ERROR: *pytest.ini:1: no section header defined")
|
||||||
|
@ -179,6 +185,23 @@ class TestParseIni:
|
||||||
assert result.ret != 0
|
assert result.ret != 0
|
||||||
result.stderr.fnmatch_lines("ERROR: *pyproject.toml: Invalid statement*")
|
result.stderr.fnmatch_lines("ERROR: *pyproject.toml: Invalid statement*")
|
||||||
|
|
||||||
|
def test_confcutdir_default_without_configfile(self, pytester: Pytester) -> None:
|
||||||
|
# If --confcutdir is not specified, and there is no configfile, default
|
||||||
|
# to the roothpath.
|
||||||
|
sub = pytester.mkdir("sub")
|
||||||
|
os.chdir(sub)
|
||||||
|
config = pytester.parseconfigure()
|
||||||
|
assert config.pluginmanager._confcutdir == sub
|
||||||
|
|
||||||
|
def test_confcutdir_default_with_configfile(self, pytester: Pytester) -> None:
|
||||||
|
# If --confcutdir is not specified, and there is a configfile, default
|
||||||
|
# to the configfile's directory.
|
||||||
|
pytester.makeini("[pytest]")
|
||||||
|
sub = pytester.mkdir("sub")
|
||||||
|
os.chdir(sub)
|
||||||
|
config = pytester.parseconfigure()
|
||||||
|
assert config.pluginmanager._confcutdir == pytester.path
|
||||||
|
|
||||||
@pytest.mark.xfail(reason="probably not needed")
|
@pytest.mark.xfail(reason="probably not needed")
|
||||||
def test_confcutdir(self, pytester: Pytester) -> None:
|
def test_confcutdir(self, pytester: Pytester) -> None:
|
||||||
sub = pytester.mkdir("sub")
|
sub = pytester.mkdir("sub")
|
||||||
|
@ -452,7 +475,7 @@ class TestParseIni:
|
||||||
pytester.makepyfile(myplugin1_module="# my plugin module")
|
pytester.makepyfile(myplugin1_module="# my plugin module")
|
||||||
pytester.syspathinsert()
|
pytester.syspathinsert()
|
||||||
|
|
||||||
monkeypatch.setattr(importlib_metadata, "distributions", my_dists)
|
monkeypatch.setattr(importlib.metadata, "distributions", my_dists)
|
||||||
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
|
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
|
||||||
|
|
||||||
pytester.makeini(ini_file_text)
|
pytester.makeini(ini_file_text)
|
||||||
|
@ -514,6 +537,8 @@ class TestConfigCmdlineParsing:
|
||||||
)
|
)
|
||||||
config = pytester.parseconfig("-c", "custom.ini")
|
config = pytester.parseconfig("-c", "custom.ini")
|
||||||
assert config.getini("custom") == "1"
|
assert config.getini("custom") == "1"
|
||||||
|
config = pytester.parseconfig("--config-file", "custom.ini")
|
||||||
|
assert config.getini("custom") == "1"
|
||||||
|
|
||||||
pytester.makefile(
|
pytester.makefile(
|
||||||
".cfg",
|
".cfg",
|
||||||
|
@ -524,6 +549,8 @@ class TestConfigCmdlineParsing:
|
||||||
)
|
)
|
||||||
config = pytester.parseconfig("-c", "custom_tool_pytest_section.cfg")
|
config = pytester.parseconfig("-c", "custom_tool_pytest_section.cfg")
|
||||||
assert config.getini("custom") == "1"
|
assert config.getini("custom") == "1"
|
||||||
|
config = pytester.parseconfig("--config-file", "custom_tool_pytest_section.cfg")
|
||||||
|
assert config.getini("custom") == "1"
|
||||||
|
|
||||||
pytester.makefile(
|
pytester.makefile(
|
||||||
".toml",
|
".toml",
|
||||||
|
@ -536,6 +563,8 @@ class TestConfigCmdlineParsing:
|
||||||
)
|
)
|
||||||
config = pytester.parseconfig("-c", "custom.toml")
|
config = pytester.parseconfig("-c", "custom.toml")
|
||||||
assert config.getini("custom") == "1"
|
assert config.getini("custom") == "1"
|
||||||
|
config = pytester.parseconfig("--config-file", "custom.toml")
|
||||||
|
assert config.getini("custom") == "1"
|
||||||
|
|
||||||
def test_absolute_win32_path(self, pytester: Pytester) -> None:
|
def test_absolute_win32_path(self, pytester: Pytester) -> None:
|
||||||
temp_ini_file = pytester.makefile(
|
temp_ini_file = pytester.makefile(
|
||||||
|
@ -550,6 +579,8 @@ class TestConfigCmdlineParsing:
|
||||||
temp_ini_file_norm = normpath(str(temp_ini_file))
|
temp_ini_file_norm = normpath(str(temp_ini_file))
|
||||||
ret = pytest.main(["-c", temp_ini_file_norm])
|
ret = pytest.main(["-c", temp_ini_file_norm])
|
||||||
assert ret == ExitCode.OK
|
assert ret == ExitCode.OK
|
||||||
|
ret = pytest.main(["--config-file", temp_ini_file_norm])
|
||||||
|
assert ret == ExitCode.OK
|
||||||
|
|
||||||
|
|
||||||
class TestConfigAPI:
|
class TestConfigAPI:
|
||||||
|
@ -609,7 +640,7 @@ class TestConfigAPI:
|
||||||
def test_getconftest_pathlist(self, pytester: Pytester, tmp_path: Path) -> None:
|
def test_getconftest_pathlist(self, pytester: Pytester, tmp_path: Path) -> None:
|
||||||
somepath = tmp_path.joinpath("x", "y", "z")
|
somepath = tmp_path.joinpath("x", "y", "z")
|
||||||
p = tmp_path.joinpath("conftest.py")
|
p = tmp_path.joinpath("conftest.py")
|
||||||
p.write_text(f"mylist = {['.', str(somepath)]}")
|
p.write_text(f"mylist = {['.', str(somepath)]}", encoding="utf-8")
|
||||||
config = pytester.parseconfigure(p)
|
config = pytester.parseconfigure(p)
|
||||||
assert (
|
assert (
|
||||||
config._getconftest_pathlist("notexist", path=tmp_path, rootpath=tmp_path)
|
config._getconftest_pathlist("notexist", path=tmp_path, rootpath=tmp_path)
|
||||||
|
@ -885,7 +916,8 @@ class TestConfigFromdictargs:
|
||||||
[pytest]
|
[pytest]
|
||||||
name = value
|
name = value
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
inifilename = "../../foo/bar.ini"
|
inifilename = "../../foo/bar.ini"
|
||||||
|
@ -902,7 +934,8 @@ class TestConfigFromdictargs:
|
||||||
name = wrong-value
|
name = wrong-value
|
||||||
should_not_be_set = true
|
should_not_be_set = true
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
with MonkeyPatch.context() as mp:
|
with MonkeyPatch.context() as mp:
|
||||||
mp.chdir(cwd)
|
mp.chdir(cwd)
|
||||||
|
@ -970,7 +1003,7 @@ def test_preparse_ordering_with_setuptools(
|
||||||
def my_dists():
|
def my_dists():
|
||||||
return (Dist,)
|
return (Dist,)
|
||||||
|
|
||||||
monkeypatch.setattr(importlib_metadata, "distributions", my_dists)
|
monkeypatch.setattr(importlib.metadata, "distributions", my_dists)
|
||||||
pytester.makeconftest(
|
pytester.makeconftest(
|
||||||
"""
|
"""
|
||||||
pytest_plugins = "mytestplugin",
|
pytest_plugins = "mytestplugin",
|
||||||
|
@ -1003,7 +1036,7 @@ def test_setuptools_importerror_issue1479(
|
||||||
def distributions():
|
def distributions():
|
||||||
return (Distribution(),)
|
return (Distribution(),)
|
||||||
|
|
||||||
monkeypatch.setattr(importlib_metadata, "distributions", distributions)
|
monkeypatch.setattr(importlib.metadata, "distributions", distributions)
|
||||||
with pytest.raises(ImportError):
|
with pytest.raises(ImportError):
|
||||||
pytester.parseconfig()
|
pytester.parseconfig()
|
||||||
|
|
||||||
|
@ -1030,7 +1063,7 @@ def test_importlib_metadata_broken_distribution(
|
||||||
def distributions():
|
def distributions():
|
||||||
return (Distribution(),)
|
return (Distribution(),)
|
||||||
|
|
||||||
monkeypatch.setattr(importlib_metadata, "distributions", distributions)
|
monkeypatch.setattr(importlib.metadata, "distributions", distributions)
|
||||||
pytester.parseconfig()
|
pytester.parseconfig()
|
||||||
|
|
||||||
|
|
||||||
|
@ -1058,7 +1091,7 @@ def test_plugin_preparse_prevents_setuptools_loading(
|
||||||
def distributions():
|
def distributions():
|
||||||
return (Distribution(),)
|
return (Distribution(),)
|
||||||
|
|
||||||
monkeypatch.setattr(importlib_metadata, "distributions", distributions)
|
monkeypatch.setattr(importlib.metadata, "distributions", distributions)
|
||||||
args = ("-p", "no:mytestplugin") if block_it else ()
|
args = ("-p", "no:mytestplugin") if block_it else ()
|
||||||
config = pytester.parseconfig(*args)
|
config = pytester.parseconfig(*args)
|
||||||
config.pluginmanager.import_plugin("mytestplugin")
|
config.pluginmanager.import_plugin("mytestplugin")
|
||||||
|
@ -1107,7 +1140,7 @@ def test_disable_plugin_autoload(
|
||||||
return (Distribution(),)
|
return (Distribution(),)
|
||||||
|
|
||||||
monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1")
|
monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1")
|
||||||
monkeypatch.setattr(importlib_metadata, "distributions", distributions)
|
monkeypatch.setattr(importlib.metadata, "distributions", distributions)
|
||||||
monkeypatch.setitem(sys.modules, "mytestplugin", PseudoPlugin()) # type: ignore[misc]
|
monkeypatch.setitem(sys.modules, "mytestplugin", PseudoPlugin()) # type: ignore[misc]
|
||||||
config = pytester.parseconfig(*parse_args)
|
config = pytester.parseconfig(*parse_args)
|
||||||
has_loaded = config.pluginmanager.get_plugin("mytestplugin") is not None
|
has_loaded = config.pluginmanager.get_plugin("mytestplugin") is not None
|
||||||
|
@ -1151,7 +1184,7 @@ def test_cmdline_processargs_simple(pytester: Pytester) -> None:
|
||||||
args.append("-h")
|
args.append("-h")
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest("-Wignore::pytest.PytestRemovedIn8Warning")
|
||||||
result.stdout.fnmatch_lines(["*pytest*", "*-h*"])
|
result.stdout.fnmatch_lines(["*pytest*", "*-h*"])
|
||||||
|
|
||||||
|
|
||||||
|
@ -1362,7 +1395,7 @@ class TestRootdir:
|
||||||
)
|
)
|
||||||
def test_with_ini(self, tmp_path: Path, name: str, contents: str) -> None:
|
def test_with_ini(self, tmp_path: Path, name: str, contents: str) -> None:
|
||||||
inipath = tmp_path / name
|
inipath = tmp_path / name
|
||||||
inipath.write_text(contents, "utf-8")
|
inipath.write_text(contents, encoding="utf-8")
|
||||||
|
|
||||||
a = tmp_path / "a"
|
a = tmp_path / "a"
|
||||||
a.mkdir()
|
a.mkdir()
|
||||||
|
@ -1421,7 +1454,7 @@ class TestRootdir:
|
||||||
) -> None:
|
) -> None:
|
||||||
p = tmp_path / name
|
p = tmp_path / name
|
||||||
p.touch()
|
p.touch()
|
||||||
p.write_text(contents, "utf-8")
|
p.write_text(contents, encoding="utf-8")
|
||||||
rootpath, inipath, ini_config = determine_setup(str(p), [str(tmp_path)])
|
rootpath, inipath, ini_config = determine_setup(str(p), [str(tmp_path)])
|
||||||
assert rootpath == tmp_path
|
assert rootpath == tmp_path
|
||||||
assert inipath == p
|
assert inipath == p
|
||||||
|
@ -1517,7 +1550,8 @@ class TestOverrideIniArgs:
|
||||||
custom = 1.0""".format(
|
custom = 1.0""".format(
|
||||||
section=section
|
section=section
|
||||||
)
|
)
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
pytester.makeconftest(
|
pytester.makeconftest(
|
||||||
"""
|
"""
|
||||||
|
@ -1907,6 +1941,9 @@ class TestSetupCfg:
|
||||||
with pytest.raises(pytest.fail.Exception):
|
with pytest.raises(pytest.fail.Exception):
|
||||||
pytester.runpytest("-c", "custom.cfg")
|
pytester.runpytest("-c", "custom.cfg")
|
||||||
|
|
||||||
|
with pytest.raises(pytest.fail.Exception):
|
||||||
|
pytester.runpytest("--config-file", "custom.cfg")
|
||||||
|
|
||||||
|
|
||||||
class TestPytestPluginsVariable:
|
class TestPytestPluginsVariable:
|
||||||
def test_pytest_plugins_in_non_top_level_conftest_unsupported(
|
def test_pytest_plugins_in_non_top_level_conftest_unsupported(
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import argparse
|
|
||||||
import os
|
import os
|
||||||
import textwrap
|
import textwrap
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
@ -7,6 +6,8 @@ from typing import Dict
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from typing import Sequence
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest.config import ExitCode
|
from _pytest.config import ExitCode
|
||||||
|
@ -24,18 +25,18 @@ def ConftestWithSetinitial(path) -> PytestPluginManager:
|
||||||
|
|
||||||
|
|
||||||
def conftest_setinitial(
|
def conftest_setinitial(
|
||||||
conftest: PytestPluginManager, args, confcutdir: Optional["os.PathLike[str]"] = None
|
conftest: PytestPluginManager,
|
||||||
|
args: Sequence[Union[str, Path]],
|
||||||
|
confcutdir: Optional[Path] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
class Namespace:
|
conftest._set_initial_conftests(
|
||||||
def __init__(self) -> None:
|
args=args,
|
||||||
self.file_or_dir = args
|
pyargs=False,
|
||||||
self.confcutdir = os.fspath(confcutdir) if confcutdir is not None else None
|
noconftest=False,
|
||||||
self.noconftest = False
|
rootpath=Path(args[0]),
|
||||||
self.pyargs = False
|
confcutdir=confcutdir,
|
||||||
self.importmode = "prepend"
|
importmode="prepend",
|
||||||
|
)
|
||||||
namespace = cast(argparse.Namespace, Namespace())
|
|
||||||
conftest._set_initial_conftests(namespace, rootpath=Path(args[0]), testpaths_ini=[])
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("_sys_snapshot")
|
@pytest.mark.usefixtures("_sys_snapshot")
|
||||||
|
@ -46,8 +47,12 @@ class TestConftestValueAccessGlobal:
|
||||||
) -> Generator[Path, None, None]:
|
) -> Generator[Path, None, None]:
|
||||||
tmp_path = tmp_path_factory.mktemp("basedir", numbered=True)
|
tmp_path = tmp_path_factory.mktemp("basedir", numbered=True)
|
||||||
tmp_path.joinpath("adir/b").mkdir(parents=True)
|
tmp_path.joinpath("adir/b").mkdir(parents=True)
|
||||||
tmp_path.joinpath("adir/conftest.py").write_text("a=1 ; Directory = 3")
|
tmp_path.joinpath("adir/conftest.py").write_text(
|
||||||
tmp_path.joinpath("adir/b/conftest.py").write_text("b=2 ; a = 1.5")
|
"a=1 ; Directory = 3", encoding="utf-8"
|
||||||
|
)
|
||||||
|
tmp_path.joinpath("adir/b/conftest.py").write_text(
|
||||||
|
"b=2 ; a = 1.5", encoding="utf-8"
|
||||||
|
)
|
||||||
if request.param == "inpackage":
|
if request.param == "inpackage":
|
||||||
tmp_path.joinpath("adir/__init__.py").touch()
|
tmp_path.joinpath("adir/__init__.py").touch()
|
||||||
tmp_path.joinpath("adir/b/__init__.py").touch()
|
tmp_path.joinpath("adir/b/__init__.py").touch()
|
||||||
|
@ -122,8 +127,12 @@ class TestConftestValueAccessGlobal:
|
||||||
|
|
||||||
def test_conftest_in_nonpkg_with_init(tmp_path: Path, _sys_snapshot) -> None:
|
def test_conftest_in_nonpkg_with_init(tmp_path: Path, _sys_snapshot) -> None:
|
||||||
tmp_path.joinpath("adir-1.0/b").mkdir(parents=True)
|
tmp_path.joinpath("adir-1.0/b").mkdir(parents=True)
|
||||||
tmp_path.joinpath("adir-1.0/conftest.py").write_text("a=1 ; Directory = 3")
|
tmp_path.joinpath("adir-1.0/conftest.py").write_text(
|
||||||
tmp_path.joinpath("adir-1.0/b/conftest.py").write_text("b=2 ; a = 1.5")
|
"a=1 ; Directory = 3", encoding="utf-8"
|
||||||
|
)
|
||||||
|
tmp_path.joinpath("adir-1.0/b/conftest.py").write_text(
|
||||||
|
"b=2 ; a = 1.5", encoding="utf-8"
|
||||||
|
)
|
||||||
tmp_path.joinpath("adir-1.0/b/__init__.py").touch()
|
tmp_path.joinpath("adir-1.0/b/__init__.py").touch()
|
||||||
tmp_path.joinpath("adir-1.0/__init__.py").touch()
|
tmp_path.joinpath("adir-1.0/__init__.py").touch()
|
||||||
ConftestWithSetinitial(tmp_path.joinpath("adir-1.0", "b"))
|
ConftestWithSetinitial(tmp_path.joinpath("adir-1.0", "b"))
|
||||||
|
@ -166,7 +175,7 @@ def test_conftest_global_import(pytester: Pytester) -> None:
|
||||||
sub = Path("sub")
|
sub = Path("sub")
|
||||||
sub.mkdir()
|
sub.mkdir()
|
||||||
subconf = sub / "conftest.py"
|
subconf = sub / "conftest.py"
|
||||||
subconf.write_text("y=4")
|
subconf.write_text("y=4", encoding="utf-8")
|
||||||
mod2 = conf._importconftest(subconf, importmode="prepend", rootpath=Path.cwd())
|
mod2 = conf._importconftest(subconf, importmode="prepend", rootpath=Path.cwd())
|
||||||
assert mod != mod2
|
assert mod != mod2
|
||||||
assert mod2.y == 4
|
assert mod2.y == 4
|
||||||
|
@ -245,7 +254,8 @@ def test_conftest_confcutdir(pytester: Pytester) -> None:
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
parser.addoption("--xyz", action="store_true")
|
parser.addoption("--xyz", action="store_true")
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
result = pytester.runpytest("-h", "--confcutdir=%s" % x, x)
|
result = pytester.runpytest("-h", "--confcutdir=%s" % x, x)
|
||||||
result.stdout.fnmatch_lines(["*--xyz*"])
|
result.stdout.fnmatch_lines(["*--xyz*"])
|
||||||
|
@ -273,9 +283,12 @@ def test_installed_conftest_is_picked_up(pytester: Pytester, tmp_path: Path) ->
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def fix(): return None
|
def fix(): return None
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
tmp_path.joinpath("foo", "test_it.py").write_text(
|
||||||
|
"def test_it(fix): pass", encoding="utf-8"
|
||||||
)
|
)
|
||||||
tmp_path.joinpath("foo", "test_it.py").write_text("def test_it(fix): pass")
|
|
||||||
result = pytester.runpytest("--pyargs", "foo")
|
result = pytester.runpytest("--pyargs", "foo")
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
|
|
||||||
|
@ -400,7 +413,8 @@ def test_conftest_existing_junitxml(pytester: Pytester) -> None:
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
parser.addoption("--xyz", action="store_true")
|
parser.addoption("--xyz", action="store_true")
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
pytester.makefile(ext=".xml", junit="") # Writes junit.xml
|
pytester.makefile(ext=".xml", junit="") # Writes junit.xml
|
||||||
result = pytester.runpytest("-h", "--junitxml", "junit.xml")
|
result = pytester.runpytest("-h", "--junitxml", "junit.xml")
|
||||||
|
@ -411,7 +425,7 @@ def test_conftest_import_order(pytester: Pytester, monkeypatch: MonkeyPatch) ->
|
||||||
ct1 = pytester.makeconftest("")
|
ct1 = pytester.makeconftest("")
|
||||||
sub = pytester.mkdir("sub")
|
sub = pytester.mkdir("sub")
|
||||||
ct2 = sub / "conftest.py"
|
ct2 = sub / "conftest.py"
|
||||||
ct2.write_text("")
|
ct2.write_text("", encoding="utf-8")
|
||||||
|
|
||||||
def impct(p, importmode, root):
|
def impct(p, importmode, root):
|
||||||
return p
|
return p
|
||||||
|
@ -449,7 +463,8 @@ def test_fixture_dependency(pytester: Pytester) -> None:
|
||||||
def bar(foo):
|
def bar(foo):
|
||||||
return 'bar'
|
return 'bar'
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
subsub = sub.joinpath("subsub")
|
subsub = sub.joinpath("subsub")
|
||||||
subsub.mkdir()
|
subsub.mkdir()
|
||||||
|
@ -466,7 +481,8 @@ def test_fixture_dependency(pytester: Pytester) -> None:
|
||||||
def test_event_fixture(bar):
|
def test_event_fixture(bar):
|
||||||
assert bar == 'sub bar'
|
assert bar == 'sub bar'
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
result = pytester.runpytest("sub")
|
result = pytester.runpytest("sub")
|
||||||
result.stdout.fnmatch_lines(["*1 passed*"])
|
result.stdout.fnmatch_lines(["*1 passed*"])
|
||||||
|
@ -480,10 +496,11 @@ def test_conftest_found_with_double_dash(pytester: Pytester) -> None:
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
parser.addoption("--hello-world", action="store_true")
|
parser.addoption("--hello-world", action="store_true")
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
p = sub.joinpath("test_hello.py")
|
p = sub.joinpath("test_hello.py")
|
||||||
p.write_text("def test_hello(): pass")
|
p.write_text("def test_hello(): pass", encoding="utf-8")
|
||||||
result = pytester.runpytest(str(p) + "::test_hello", "-h")
|
result = pytester.runpytest(str(p) + "::test_hello", "-h")
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
"""
|
"""
|
||||||
|
@ -507,7 +524,8 @@ class TestConftestVisibility:
|
||||||
def fxtr():
|
def fxtr():
|
||||||
return "from-package"
|
return "from-package"
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
package.joinpath("test_pkgroot.py").write_text(
|
package.joinpath("test_pkgroot.py").write_text(
|
||||||
textwrap.dedent(
|
textwrap.dedent(
|
||||||
|
@ -515,7 +533,8 @@ class TestConftestVisibility:
|
||||||
def test_pkgroot(fxtr):
|
def test_pkgroot(fxtr):
|
||||||
assert fxtr == "from-package"
|
assert fxtr == "from-package"
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
swc = package.joinpath("swc")
|
swc = package.joinpath("swc")
|
||||||
|
@ -529,7 +548,8 @@ class TestConftestVisibility:
|
||||||
def fxtr():
|
def fxtr():
|
||||||
return "from-swc"
|
return "from-swc"
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
swc.joinpath("test_with_conftest.py").write_text(
|
swc.joinpath("test_with_conftest.py").write_text(
|
||||||
textwrap.dedent(
|
textwrap.dedent(
|
||||||
|
@ -537,7 +557,8 @@ class TestConftestVisibility:
|
||||||
def test_with_conftest(fxtr):
|
def test_with_conftest(fxtr):
|
||||||
assert fxtr == "from-swc"
|
assert fxtr == "from-swc"
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
snc = package.joinpath("snc")
|
snc = package.joinpath("snc")
|
||||||
|
@ -550,7 +571,8 @@ class TestConftestVisibility:
|
||||||
assert fxtr == "from-package" # No local conftest.py, so should
|
assert fxtr == "from-package" # No local conftest.py, so should
|
||||||
# use value from parent dir's
|
# use value from parent dir's
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
print("created directory structure:")
|
print("created directory structure:")
|
||||||
for x in pytester.path.glob("**/"):
|
for x in pytester.path.glob("**/"):
|
||||||
|
@ -594,7 +616,13 @@ class TestConftestVisibility:
|
||||||
print("pytestarg : %s" % testarg)
|
print("pytestarg : %s" % testarg)
|
||||||
print("expected pass : %s" % expect_ntests_passed)
|
print("expected pass : %s" % expect_ntests_passed)
|
||||||
os.chdir(dirs[chdir])
|
os.chdir(dirs[chdir])
|
||||||
reprec = pytester.inline_run(testarg, "-q", "--traceconfig")
|
reprec = pytester.inline_run(
|
||||||
|
testarg,
|
||||||
|
"-q",
|
||||||
|
"--traceconfig",
|
||||||
|
"--confcutdir",
|
||||||
|
pytester.path,
|
||||||
|
)
|
||||||
reprec.assertoutcome(passed=expect_ntests_passed)
|
reprec.assertoutcome(passed=expect_ntests_passed)
|
||||||
|
|
||||||
|
|
||||||
|
@ -610,7 +638,7 @@ def test_search_conftest_up_to_inifile(
|
||||||
root = pytester.path
|
root = pytester.path
|
||||||
src = root.joinpath("src")
|
src = root.joinpath("src")
|
||||||
src.mkdir()
|
src.mkdir()
|
||||||
src.joinpath("pytest.ini").write_text("[pytest]")
|
src.joinpath("pytest.ini").write_text("[pytest]", encoding="utf-8")
|
||||||
src.joinpath("conftest.py").write_text(
|
src.joinpath("conftest.py").write_text(
|
||||||
textwrap.dedent(
|
textwrap.dedent(
|
||||||
"""\
|
"""\
|
||||||
|
@ -618,7 +646,8 @@ def test_search_conftest_up_to_inifile(
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def fix1(): pass
|
def fix1(): pass
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
src.joinpath("test_foo.py").write_text(
|
src.joinpath("test_foo.py").write_text(
|
||||||
textwrap.dedent(
|
textwrap.dedent(
|
||||||
|
@ -628,7 +657,8 @@ def test_search_conftest_up_to_inifile(
|
||||||
def test_2(out_of_reach):
|
def test_2(out_of_reach):
|
||||||
pass
|
pass
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
root.joinpath("conftest.py").write_text(
|
root.joinpath("conftest.py").write_text(
|
||||||
textwrap.dedent(
|
textwrap.dedent(
|
||||||
|
@ -637,7 +667,8 @@ def test_search_conftest_up_to_inifile(
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def out_of_reach(): pass
|
def out_of_reach(): pass
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
args = [str(src)]
|
args = [str(src)]
|
||||||
|
@ -720,7 +751,8 @@ def test_required_option_help(pytester: Pytester) -> None:
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
parser.addoption("--xyz", action="store_true", required=True)
|
parser.addoption("--xyz", action="store_true", required=True)
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
result = pytester.runpytest("-h", x)
|
result = pytester.runpytest("-h", x)
|
||||||
result.stdout.no_fnmatch_line("*argument --xyz is required*")
|
result.stdout.no_fnmatch_line("*argument --xyz is required*")
|
||||||
|
|
|
@ -114,7 +114,7 @@ class TestDoctests:
|
||||||
reprec.assertoutcome(failed=1)
|
reprec.assertoutcome(failed=1)
|
||||||
|
|
||||||
def test_importmode(self, pytester: Pytester):
|
def test_importmode(self, pytester: Pytester):
|
||||||
p = pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
**{
|
**{
|
||||||
"namespacepkg/innerpkg/__init__.py": "",
|
"namespacepkg/innerpkg/__init__.py": "",
|
||||||
"namespacepkg/innerpkg/a.py": """
|
"namespacepkg/innerpkg/a.py": """
|
||||||
|
@ -132,7 +132,7 @@ class TestDoctests:
|
||||||
""",
|
""",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
reprec = pytester.inline_run(p, "--doctest-modules", "--import-mode=importlib")
|
reprec = pytester.inline_run("--doctest-modules", "--import-mode=importlib")
|
||||||
reprec.assertoutcome(passed=1)
|
reprec.assertoutcome(passed=1)
|
||||||
|
|
||||||
def test_new_pattern(self, pytester: Pytester):
|
def test_new_pattern(self, pytester: Pytester):
|
||||||
|
@ -357,7 +357,8 @@ class TestDoctests:
|
||||||
>>> 1/0
|
>>> 1/0
|
||||||
'''
|
'''
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
result = pytester.runpytest("--doctest-modules")
|
result = pytester.runpytest("--doctest-modules")
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
|
@ -448,7 +449,8 @@ class TestDoctests:
|
||||||
"""\
|
"""\
|
||||||
import asdalsdkjaslkdjasd
|
import asdalsdkjaslkdjasd
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
pytester.maketxtfile(
|
pytester.maketxtfile(
|
||||||
"""
|
"""
|
||||||
|
@ -492,7 +494,8 @@ class TestDoctests:
|
||||||
2
|
2
|
||||||
'''
|
'''
|
||||||
"""
|
"""
|
||||||
)
|
),
|
||||||
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
result = pytester.runpytest(p, "--doctest-modules")
|
result = pytester.runpytest(p, "--doctest-modules")
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
|
@ -1566,7 +1569,9 @@ def test_warning_on_unwrap_of_broken_object(
|
||||||
|
|
||||||
def test_is_setup_py_not_named_setup_py(tmp_path: Path) -> None:
|
def test_is_setup_py_not_named_setup_py(tmp_path: Path) -> None:
|
||||||
not_setup_py = tmp_path.joinpath("not_setup.py")
|
not_setup_py = tmp_path.joinpath("not_setup.py")
|
||||||
not_setup_py.write_text('from setuptools import setup; setup(name="foo")')
|
not_setup_py.write_text(
|
||||||
|
'from setuptools import setup; setup(name="foo")', encoding="utf-8"
|
||||||
|
)
|
||||||
assert not _is_setup_py(not_setup_py)
|
assert not _is_setup_py(not_setup_py)
|
||||||
|
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue