Merge upstream
This commit is contained in:
commit
5877bb8e8d
|
@ -13,39 +13,53 @@ on:
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
build:
|
||||||
deploy:
|
|
||||||
if: github.repository == 'pytest-dev/pytest'
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 30
|
timeout-minutes: 10
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Build and Check Package
|
- name: Build and Check Package
|
||||||
uses: hynek/build-and-inspect-python-package@v1.5
|
uses: hynek/build-and-inspect-python-package@v1.5
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
if: github.repository == 'pytest-dev/pytest'
|
||||||
|
needs: [build]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
steps:
|
||||||
- name: Download Package
|
- name: Download Package
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: Packages
|
name: Packages
|
||||||
path: dist
|
path: dist
|
||||||
|
|
||||||
- name: Publish package to PyPI
|
- name: Publish package to PyPI
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
uses: pypa/gh-action-pypi-publish@v1.8.8
|
||||||
with:
|
|
||||||
password: ${{ secrets.pypi_token }}
|
|
||||||
|
|
||||||
|
release-notes:
|
||||||
|
|
||||||
|
# todo: generate the content in the build job
|
||||||
|
# the goal being of using a github action script to push the release data
|
||||||
|
# after success instead of creating a complete python/tox env
|
||||||
|
needs: [deploy]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.7"
|
python-version: "3.11"
|
||||||
|
|
||||||
|
|
||||||
- name: Install tox
|
- name: Install tox
|
||||||
run: |
|
run: |
|
||||||
|
|
|
@ -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,14 +1,14 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 23.3.0
|
rev: 23.7.0
|
||||||
hooks:
|
hooks:
|
||||||
- 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.15.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: blacken-docs
|
- id: blacken-docs
|
||||||
additional_dependencies: [black==23.1.0]
|
additional_dependencies: [black==23.7.0]
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.4.0
|
rev: v4.4.0
|
||||||
hooks:
|
hooks:
|
||||||
|
@ -21,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
|
||||||
|
@ -37,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.9.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.4.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.3.0
|
rev: v1.4.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
files: ^(src/|testing/)
|
files: ^(src/|testing/)
|
||||||
|
|
7
AUTHORS
7
AUTHORS
|
@ -11,6 +11,7 @@ Adam Johnson
|
||||||
Adam Stewart
|
Adam Stewart
|
||||||
Adam Uhlir
|
Adam Uhlir
|
||||||
Ahn Ki-Wook
|
Ahn Ki-Wook
|
||||||
|
Akhilesh Ramakrishnan
|
||||||
Akiomi Kamakura
|
Akiomi Kamakura
|
||||||
Alan Velasco
|
Alan Velasco
|
||||||
Alessio Izzo
|
Alessio Izzo
|
||||||
|
@ -130,6 +131,7 @@ 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
|
Evgeny Seliverstov
|
||||||
|
@ -167,6 +169,7 @@ Ian Bicking
|
||||||
Ian Lesperance
|
Ian Lesperance
|
||||||
Ilya Konstantinov
|
Ilya Konstantinov
|
||||||
Ionuț Turturică
|
Ionuț Turturică
|
||||||
|
Isaac Virshup
|
||||||
Itxaso Aizpurua
|
Itxaso Aizpurua
|
||||||
Iwan Briquemont
|
Iwan Briquemont
|
||||||
Jaap Broekhuizen
|
Jaap Broekhuizen
|
||||||
|
@ -262,6 +265,7 @@ Mickey Pashov
|
||||||
Mihai Capotă
|
Mihai Capotă
|
||||||
Mike Hoyle (hoylemd)
|
Mike Hoyle (hoylemd)
|
||||||
Mike Lundy
|
Mike Lundy
|
||||||
|
Milan Lesnek
|
||||||
Miro Hrončok
|
Miro Hrončok
|
||||||
Nathaniel Compton
|
Nathaniel Compton
|
||||||
Nathaniel Waisbrot
|
Nathaniel Waisbrot
|
||||||
|
@ -311,6 +315,7 @@ Raphael Pierzina
|
||||||
Rafal Semik
|
Rafal Semik
|
||||||
Raquel Alegre
|
Raquel Alegre
|
||||||
Ravi Chandra
|
Ravi Chandra
|
||||||
|
Reagan Lee
|
||||||
Robert Holt
|
Robert Holt
|
||||||
Roberto Aldera
|
Roberto Aldera
|
||||||
Roberto Polli
|
Roberto Polli
|
||||||
|
@ -371,6 +376,7 @@ Tomer Keren
|
||||||
Tony Narlock
|
Tony Narlock
|
||||||
Tor Colvin
|
Tor Colvin
|
||||||
Trevor Bekolay
|
Trevor Bekolay
|
||||||
|
Tushar Sadhwani
|
||||||
Tyler Goodlet
|
Tyler Goodlet
|
||||||
Tzu-ping Chung
|
Tzu-ping Chung
|
||||||
Vasily Kuznetsov
|
Vasily Kuznetsov
|
||||||
|
@ -378,6 +384,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 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 @@
|
||||||
|
markers are now considered in the reverse mro order to ensure base class markers are considered first
|
||||||
|
this resolves a regression.
|
|
@ -0,0 +1,2 @@
|
||||||
|
:meth:`pytest.WarningsRecorder.pop` will return the most-closely-matched warning in the list,
|
||||||
|
rather than the first warning which is an instance of the requested type.
|
|
@ -0,0 +1 @@
|
||||||
|
Fixed error assertion handling in :func:`pytest.approx` when ``None`` is an expected or received value when comparing dictionaries.
|
|
@ -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 +0,0 @@
|
||||||
Update test log report annotation to named tuple and fixed inconsistency in docs for :hook:`pytest_report_teststatus` hook.
|
|
|
@ -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``.
|
|
|
@ -1 +0,0 @@
|
||||||
Added handling of ``%f`` directive to print microseconds in log format options, such as ``log-date-format``.
|
|
|
@ -1 +0,0 @@
|
||||||
The `monkeypatch` `setitem`/`delitem` type annotations now allow `TypedDict` arguments.
|
|
|
@ -1 +0,0 @@
|
||||||
Added underlying exception to cache provider path creation and write warning messages.
|
|
|
@ -0,0 +1 @@
|
||||||
|
Added a warning about modifying the root logger during tests when using ``caplog``.
|
|
@ -1 +0,0 @@
|
||||||
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.
|
|
|
@ -1 +0,0 @@
|
||||||
Fixed bug in assertion rewriting where a variable assigned with the walrus operator could not be used later in a function call.
|
|
|
@ -1 +0,0 @@
|
||||||
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.
|
|
|
@ -1,3 +0,0 @@
|
||||||
When `--confcutdir` is not specified, and there is no config file present, the conftest cutoff directory (`--confcutdir`) is now set to the :ref:`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`.
|
|
|
@ -1 +0,0 @@
|
||||||
Fixed ``--last-failed``'s "(skipped N files)" functionality for files inside of packages (directories with `__init__.py` files).
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
``pluggy>=1.2.0`` is now required.
|
||||||
|
|
||||||
|
pytest now uses "new-style" hook wrappers internally, available since pluggy 1.2.0.
|
||||||
|
See `pluggy's 1.2.0 changelog <https://pluggy.readthedocs.io/en/latest/changelog.html#pluggy-1-2-0-2023-06-21>`_ and the :ref:`updated docs <hookwrapper>` for details.
|
||||||
|
|
||||||
|
Plugins which want to use new-style wrappers can do so if they require this version of pytest or later.
|
|
@ -0,0 +1 @@
|
||||||
|
- Prevent constants at the top of file from being detected as docstrings.
|
|
@ -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,2 @@
|
||||||
|
The (internal) ``FixtureDef.cached_result`` type has changed.
|
||||||
|
Now the third item ``cached_result[2]``, when set, is an exception instance instead of an exception triplet.
|
|
@ -0,0 +1 @@
|
||||||
|
If a test is skipped from inside an :ref:`xunit setup fixture <classic xunit>`, the test summary now shows the test location instead of the fixture location.
|
|
@ -0,0 +1 @@
|
||||||
|
Allow :func:`pytest.raises` ``match`` argument to match against `PEP-678 <https://peps.python.org/pep-0678/>` ``__notes__``.
|
|
@ -1 +0,0 @@
|
||||||
Fixed traceback entries hidden with ``__tracebackhide__ = True`` still being shown for chained exceptions (parts after "... the above exception ..." message).
|
|
|
@ -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 @@
|
||||||
|
:class:`~pytest.FixtureDef` is now exported as ``pytest.FixtureDef`` for typing purposes.
|
|
@ -1,3 +0,0 @@
|
||||||
:func:`_pytest.logging.LogCaptureFixture.set_level` and :func:`_pytest.logging.LogCaptureFixture.at_level`
|
|
||||||
will temporarily enable the requested ``level`` if ``level`` was disabled globally via
|
|
||||||
``logging.disable(LEVEL)``.
|
|
|
@ -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,6 +87,7 @@ 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+
|
||||||
|
|
|
@ -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)
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
@ -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
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
|
@ -12,7 +12,7 @@ class YamlFile(pytest.File):
|
||||||
# We need a yaml parser, e.g. PyYAML.
|
# We need a yaml parser, e.g. PyYAML.
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
raw = yaml.safe_load(self.path.open())
|
raw = yaml.safe_load(self.path.open(encoding="utf-8"))
|
||||||
for name, spec in sorted(raw.items()):
|
for name, spec in sorted(raw.items()):
|
||||||
yield YamlItem.from_parent(self, name=name, spec=spec)
|
yield YamlItem.from_parent(self, name=name, spec=spec)
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
@ -808,16 +808,15 @@ case we just write some information out to a ``failures`` file:
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
@pytest.hookimpl(wrapper=True, tryfirst=True)
|
||||||
def pytest_runtest_makereport(item, call):
|
def pytest_runtest_makereport(item, call):
|
||||||
# execute all other hooks to obtain the report object
|
# execute all other hooks to obtain the report object
|
||||||
outcome = yield
|
rep = yield
|
||||||
rep = outcome.get_result()
|
|
||||||
|
|
||||||
# we only look at actual failing test calls, not setup/teardown
|
# we only look at actual failing test calls, not setup/teardown
|
||||||
if rep.when == "call" and rep.failed:
|
if rep.when == "call" and rep.failed:
|
||||||
mode = "a" if os.path.exists("failures") else "w"
|
mode = "a" if os.path.exists("failures") else "w"
|
||||||
with open("failures", mode) as f:
|
with open("failures", mode, encoding="utf-8") as f:
|
||||||
# let's also access a fixture for the fun of it
|
# let's also access a fixture for the fun of it
|
||||||
if "tmp_path" in item.fixturenames:
|
if "tmp_path" in item.fixturenames:
|
||||||
extra = " ({})".format(item.funcargs["tmp_path"])
|
extra = " ({})".format(item.funcargs["tmp_path"])
|
||||||
|
@ -826,6 +825,8 @@ case we just write some information out to a ``failures`` file:
|
||||||
|
|
||||||
f.write(rep.nodeid + extra + "\n")
|
f.write(rep.nodeid + extra + "\n")
|
||||||
|
|
||||||
|
return rep
|
||||||
|
|
||||||
|
|
||||||
if you then have failing tests:
|
if you then have failing tests:
|
||||||
|
|
||||||
|
@ -899,16 +900,17 @@ here is a little example implemented via a local plugin:
|
||||||
phase_report_key = StashKey[Dict[str, CollectReport]]()
|
phase_report_key = StashKey[Dict[str, CollectReport]]()
|
||||||
|
|
||||||
|
|
||||||
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
@pytest.hookimpl(wrapper=True, tryfirst=True)
|
||||||
def pytest_runtest_makereport(item, call):
|
def pytest_runtest_makereport(item, call):
|
||||||
# execute all other hooks to obtain the report object
|
# execute all other hooks to obtain the report object
|
||||||
outcome = yield
|
rep = yield
|
||||||
rep = outcome.get_result()
|
|
||||||
|
|
||||||
# store test results for each phase of a call, which can
|
# store test results for each phase of a call, which can
|
||||||
# be "setup", "call", "teardown"
|
# be "setup", "call", "teardown"
|
||||||
item.stash.setdefault(phase_report_key, {})[rep.when] = rep
|
item.stash.setdefault(phase_report_key, {})[rep.when] = rep
|
||||||
|
|
||||||
|
return rep
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def something(request):
|
def something(request):
|
||||||
|
|
|
@ -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`:
|
||||||
|
|
||||||
|
|
|
@ -135,10 +135,6 @@ Warning about unraisable exceptions and unhandled thread exceptions
|
||||||
|
|
||||||
.. versionadded:: 6.2
|
.. versionadded:: 6.2
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
These features only work on Python>=3.8.
|
|
||||||
|
|
||||||
Unhandled exceptions are exceptions that are raised in a situation in which
|
Unhandled exceptions are exceptions that are raised in a situation in which
|
||||||
they cannot propagate to a caller. The most common case is an exception raised
|
they cannot propagate to a caller. The most common case is an exception raised
|
||||||
in a :meth:`__del__ <object.__del__>` implementation.
|
in a :meth:`__del__ <object.__del__>` implementation.
|
||||||
|
|
|
@ -1698,7 +1698,7 @@ and declare its use in a test module via a ``usefixtures`` marker:
|
||||||
class TestDirectoryInit:
|
class TestDirectoryInit:
|
||||||
def test_cwd_starts_empty(self):
|
def test_cwd_starts_empty(self):
|
||||||
assert os.listdir(os.getcwd()) == []
|
assert os.listdir(os.getcwd()) == []
|
||||||
with open("myfile", "w") as f:
|
with open("myfile", "w", encoding="utf-8") as f:
|
||||||
f.write("hello")
|
f.write("hello")
|
||||||
|
|
||||||
def test_cwd_again_starts_empty(self):
|
def test_cwd_again_starts_empty(self):
|
||||||
|
@ -1752,8 +1752,7 @@ into an ini-file:
|
||||||
def my_fixture_that_sadly_wont_use_my_other_fixture():
|
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:
|
||||||
|
|
||||||
|
|
|
@ -47,8 +47,7 @@ Unsupported idioms / known issues
|
||||||
- nose imports test modules with the same import path (e.g.
|
- nose imports test modules with the same import path (e.g.
|
||||||
``tests.test_mode``) but different file system paths
|
``tests.test_mode``) but different file system paths
|
||||||
(e.g. ``tests/test_mode.py`` and ``other/tests/test_mode.py``)
|
(e.g. ``tests/test_mode.py`` and ``other/tests/test_mode.py``)
|
||||||
by extending sys.path/import semantics. pytest does not do that
|
by extending sys.path/import semantics. pytest does not do that. Note that
|
||||||
but there is discussion in :issue:`268` for adding some support. Note that
|
|
||||||
`nose2 choose to avoid this sys.path/import hackery <https://nose2.readthedocs.io/en/latest/differences.html#test-discovery-and-loading>`_.
|
`nose2 choose to avoid this sys.path/import hackery <https://nose2.readthedocs.io/en/latest/differences.html#test-discovery-and-loading>`_.
|
||||||
|
|
||||||
If you place a conftest.py file in the root directory of your project
|
If you place a conftest.py file in the root directory of your project
|
||||||
|
@ -66,16 +65,34 @@ Unsupported idioms / known issues
|
||||||
|
|
||||||
- no nose-configuration is recognized.
|
- no nose-configuration is recognized.
|
||||||
|
|
||||||
- ``yield``-based methods are unsupported as of pytest 4.1.0. They are
|
- ``yield``-based methods are
|
||||||
fundamentally incompatible with pytest because they don't support fixtures
|
fundamentally incompatible with pytest because they don't support fixtures
|
||||||
properly since collection and test execution are separated.
|
properly since collection and test execution are separated.
|
||||||
|
|
||||||
|
Here is a table comparing the default supported naming conventions for both
|
||||||
|
nose and pytest.
|
||||||
|
|
||||||
|
========= ========================== ======= =====
|
||||||
|
what default naming convention pytest nose
|
||||||
|
========= ========================== ======= =====
|
||||||
|
module ``test*.py`` ✅
|
||||||
|
module ``test_*.py`` ✅ ✅
|
||||||
|
module ``*_test.py`` ✅
|
||||||
|
module ``*_tests.py``
|
||||||
|
class ``*(unittest.TestCase)`` ✅ ✅
|
||||||
|
method ``test_*`` ✅ ✅
|
||||||
|
class ``Test*`` ✅
|
||||||
|
method ``test_*`` ✅
|
||||||
|
function ``test_*`` ✅
|
||||||
|
========= ========================== ======= =====
|
||||||
|
|
||||||
|
|
||||||
Migrating from nose to pytest
|
Migrating from nose to pytest
|
||||||
------------------------------
|
------------------------------
|
||||||
|
|
||||||
`nose2pytest <https://github.com/pytest-dev/nose2pytest>`_ is a Python script
|
`nose2pytest <https://github.com/pytest-dev/nose2pytest>`_ is a Python script
|
||||||
and pytest plugin to help convert Nose-based tests into pytest-based tests.
|
and pytest plugin to help convert Nose-based tests into pytest-based tests.
|
||||||
Specifically, the script transforms nose.tools.assert_* function calls into
|
Specifically, the script transforms ``nose.tools.assert_*`` function calls into
|
||||||
raw assert statements, while preserving format of original arguments
|
raw assert statements, while preserving format of original arguments
|
||||||
as much as possible.
|
as much as possible.
|
||||||
|
|
||||||
|
|
|
@ -24,8 +24,8 @@ created in the `base temporary directory`_.
|
||||||
d = tmp_path / "sub"
|
d = tmp_path / "sub"
|
||||||
d.mkdir()
|
d.mkdir()
|
||||||
p = d / "hello.txt"
|
p = d / "hello.txt"
|
||||||
p.write_text(CONTENT)
|
p.write_text(CONTENT, encoding="utf-8")
|
||||||
assert p.read_text() == CONTENT
|
assert p.read_text(encoding="utf-8") == CONTENT
|
||||||
assert len(list(tmp_path.iterdir())) == 1
|
assert len(list(tmp_path.iterdir())) == 1
|
||||||
assert 0
|
assert 0
|
||||||
|
|
||||||
|
|
|
@ -207,10 +207,10 @@ creation of a per-test temporary directory:
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def initdir(self, tmp_path, monkeypatch):
|
def initdir(self, tmp_path, monkeypatch):
|
||||||
monkeypatch.chdir(tmp_path) # change to pytest-provided temporary directory
|
monkeypatch.chdir(tmp_path) # change to pytest-provided temporary directory
|
||||||
tmp_path.joinpath("samplefile.ini").write_text("# testdata")
|
tmp_path.joinpath("samplefile.ini").write_text("# testdata", encoding="utf-8")
|
||||||
|
|
||||||
def test_method(self):
|
def test_method(self):
|
||||||
with open("samplefile.ini") as f:
|
with open("samplefile.ini", encoding="utf-8") as f:
|
||||||
s = f.read()
|
s = f.read()
|
||||||
assert "testdata" in s
|
assert "testdata" in s
|
||||||
|
|
||||||
|
|
|
@ -173,7 +173,8 @@ You can invoke ``pytest`` from Python code directly:
|
||||||
|
|
||||||
this acts as if you would call "pytest" from the command line.
|
this acts as if you would call "pytest" from the command line.
|
||||||
It will not raise :class:`SystemExit` but return the :ref:`exit code <exit-codes>` instead.
|
It will not raise :class:`SystemExit` but return the :ref:`exit code <exit-codes>` instead.
|
||||||
You can pass in options and arguments:
|
If you don't pass it any arguments, ``main`` reads the arguments from the command line arguments of the process (:data:`sys.argv`), which may be undesirable.
|
||||||
|
You can pass in options and arguments explicitly:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,7 @@ The remaining hook functions will not be called in this case.
|
||||||
|
|
||||||
.. _`hookwrapper`:
|
.. _`hookwrapper`:
|
||||||
|
|
||||||
hookwrapper: executing around other hooks
|
hook wrappers: executing around other hooks
|
||||||
-------------------------------------------------
|
-------------------------------------------------
|
||||||
|
|
||||||
.. currentmodule:: _pytest.core
|
.. currentmodule:: _pytest.core
|
||||||
|
@ -69,10 +69,8 @@ which yields exactly once. When pytest invokes hooks it first executes
|
||||||
hook wrappers and passes the same arguments as to the regular hooks.
|
hook wrappers and passes the same arguments as to the regular hooks.
|
||||||
|
|
||||||
At the yield point of the hook wrapper pytest will execute the next hook
|
At the yield point of the hook wrapper pytest will execute the next hook
|
||||||
implementations and return their result to the yield point in the form of
|
implementations and return their result to the yield point, or will
|
||||||
a :py:class:`Result <pluggy._Result>` instance which encapsulates a result or
|
propagate an exception if they raised.
|
||||||
exception info. The yield point itself will thus typically not raise
|
|
||||||
exceptions (unless there are bugs).
|
|
||||||
|
|
||||||
Here is an example definition of a hook wrapper:
|
Here is an example definition of a hook wrapper:
|
||||||
|
|
||||||
|
@ -81,26 +79,35 @@ Here is an example definition of a hook wrapper:
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
@pytest.hookimpl(hookwrapper=True)
|
@pytest.hookimpl(wrapper=True)
|
||||||
def pytest_pyfunc_call(pyfuncitem):
|
def pytest_pyfunc_call(pyfuncitem):
|
||||||
do_something_before_next_hook_executes()
|
do_something_before_next_hook_executes()
|
||||||
|
|
||||||
outcome = yield
|
# If the outcome is an exception, will raise the exception.
|
||||||
# outcome.excinfo may be None or a (cls, val, tb) tuple
|
res = yield
|
||||||
|
|
||||||
res = outcome.get_result() # will raise if outcome was exception
|
new_res = post_process_result(res)
|
||||||
|
|
||||||
post_process_result(res)
|
# Override the return value to the plugin system.
|
||||||
|
return new_res
|
||||||
|
|
||||||
outcome.force_result(new_res) # to override the return value to the plugin system
|
The hook wrapper needs to return a result for the hook, or raise an exception.
|
||||||
|
|
||||||
Note that hook wrappers don't return results themselves, they merely
|
In many cases, the wrapper only needs to perform tracing or other side effects
|
||||||
perform tracing or other side effects around the actual hook implementations.
|
around the actual hook implementations, in which case it can return the result
|
||||||
If the result of the underlying hook is a mutable object, they may modify
|
value of the ``yield``. The simplest (though useless) hook wrapper is
|
||||||
that result but it's probably better to avoid it.
|
``return (yield)``.
|
||||||
|
|
||||||
|
In other cases, the wrapper wants the adjust or adapt the result, in which case
|
||||||
|
it can return a new value. If the result of the underlying hook is a mutable
|
||||||
|
object, the wrapper may modify that result, but it's probably better to avoid it.
|
||||||
|
|
||||||
|
If the hook implementation failed with an exception, the wrapper can handle that
|
||||||
|
exception using a ``try-catch-finally`` around the ``yield``, by propagating it,
|
||||||
|
supressing it, or raising a different exception entirely.
|
||||||
|
|
||||||
For more information, consult the
|
For more information, consult the
|
||||||
:ref:`pluggy documentation about hookwrappers <pluggy:hookwrappers>`.
|
:ref:`pluggy documentation about hook wrappers <pluggy:hookwrappers>`.
|
||||||
|
|
||||||
.. _plugin-hookorder:
|
.. _plugin-hookorder:
|
||||||
|
|
||||||
|
@ -130,11 +137,14 @@ after others, i.e. the position in the ``N``-sized list of functions:
|
||||||
|
|
||||||
|
|
||||||
# Plugin 3
|
# Plugin 3
|
||||||
@pytest.hookimpl(hookwrapper=True)
|
@pytest.hookimpl(wrapper=True)
|
||||||
def pytest_collection_modifyitems(items):
|
def pytest_collection_modifyitems(items):
|
||||||
# will execute even before the tryfirst one above!
|
# will execute even before the tryfirst one above!
|
||||||
outcome = yield
|
try:
|
||||||
# will execute after all non-hookwrappers executed
|
return (yield)
|
||||||
|
finally:
|
||||||
|
# will execute after all non-wrappers executed
|
||||||
|
...
|
||||||
|
|
||||||
Here is the order of execution:
|
Here is the order of execution:
|
||||||
|
|
||||||
|
@ -149,12 +159,11 @@ Here is the order of execution:
|
||||||
Plugin1).
|
Plugin1).
|
||||||
|
|
||||||
4. Plugin3's pytest_collection_modifyitems then executing the code after the yield
|
4. Plugin3's pytest_collection_modifyitems then executing the code after the yield
|
||||||
point. The yield receives a :py:class:`Result <pluggy._Result>` instance which encapsulates
|
point. The yield receives the result from calling the non-wrappers, or raises
|
||||||
the result from calling the non-wrappers. Wrappers shall not modify the result.
|
an exception if the non-wrappers raised.
|
||||||
|
|
||||||
It's possible to use ``tryfirst`` and ``trylast`` also in conjunction with
|
It's possible to use ``tryfirst`` and ``trylast`` also on hook wrappers
|
||||||
``hookwrapper=True`` in which case it will influence the ordering of hookwrappers
|
in which case it will influence the ordering of hook wrappers among each other.
|
||||||
among each other.
|
|
||||||
|
|
||||||
|
|
||||||
Declaring new hooks
|
Declaring new hooks
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
.. sidebar:: Next Open Trainings
|
.. sidebar:: Next Open Trainings
|
||||||
|
|
||||||
- `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
|
- `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, Czech Republic / Remote**
|
||||||
|
- `pytest: Professionelles Testen (nicht nur) für Python <https://workshoptage.ch/workshops/2023/pytest-professionelles-testen-nicht-nur-fuer-python-2/>`_, at `Workshoptage 2023 <https://workshoptage.ch/>`_, **September 5th**, `OST <https://www.ost.ch/en>`_ Campus **Rapperswil, Switzerland**
|
||||||
|
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, **March 5th to 7th 2024** (3 day in-depth training), **Leipzig, Germany / Remote**
|
||||||
|
|
||||||
Also see :doc:`previous talks and blogposts <talks>`.
|
Also see :doc:`previous talks and blogposts <talks>`.
|
||||||
|
|
||||||
|
@ -17,7 +19,7 @@ The ``pytest`` framework makes it easy to write small, readable tests, and can
|
||||||
scale to support complex functional testing for applications and libraries.
|
scale to support complex functional testing for applications and libraries.
|
||||||
|
|
||||||
|
|
||||||
``pytest`` requires: Python 3.7+ or PyPy3.
|
``pytest`` requires: Python 3.8+ or PyPy3.
|
||||||
|
|
||||||
**PyPI package name**: :pypi:`pytest`
|
**PyPI package name**: :pypi:`pytest`
|
||||||
|
|
||||||
|
@ -76,7 +78,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
|
@ -783,18 +783,66 @@ reporting or interaction with exceptions:
|
||||||
.. autofunction:: pytest_leave_pdb
|
.. autofunction:: pytest_leave_pdb
|
||||||
|
|
||||||
|
|
||||||
Objects
|
Collection tree objects
|
||||||
-------
|
-----------------------
|
||||||
|
|
||||||
Full reference to objects accessible from :ref:`fixtures <fixture>` or :ref:`hooks <hook-reference>`.
|
These are the collector and item classes (collectively called "nodes") which
|
||||||
|
make up the collection tree.
|
||||||
|
|
||||||
|
Node
|
||||||
|
~~~~
|
||||||
|
|
||||||
CallInfo
|
.. autoclass:: _pytest.nodes.Node()
|
||||||
~~~~~~~~
|
|
||||||
|
|
||||||
.. autoclass:: pytest.CallInfo()
|
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
Collector
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
.. autoclass:: pytest.Collector()
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
Item
|
||||||
|
~~~~
|
||||||
|
|
||||||
|
.. autoclass:: pytest.Item()
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
File
|
||||||
|
~~~~
|
||||||
|
|
||||||
|
.. autoclass:: pytest.File()
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
FSCollector
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. autoclass:: _pytest.nodes.FSCollector()
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
Session
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
.. autoclass:: pytest.Session()
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
Package
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
.. autoclass:: pytest.Package()
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
Module
|
||||||
|
~~~~~~
|
||||||
|
|
||||||
|
.. autoclass:: pytest.Module()
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
Class
|
Class
|
||||||
~~~~~
|
~~~~~
|
||||||
|
@ -803,13 +851,34 @@ Class
|
||||||
:members:
|
:members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
Collector
|
Function
|
||||||
~~~~~~~~~
|
~~~~~~~~
|
||||||
|
|
||||||
.. autoclass:: pytest.Collector()
|
.. autoclass:: pytest.Function()
|
||||||
:members:
|
:members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
FunctionDefinition
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. autoclass:: _pytest.python.FunctionDefinition()
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
Objects
|
||||||
|
-------
|
||||||
|
|
||||||
|
Objects accessible from :ref:`fixtures <fixture>` or :ref:`hooks <hook-reference>`
|
||||||
|
or importable from ``pytest``.
|
||||||
|
|
||||||
|
|
||||||
|
CallInfo
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
.. autoclass:: pytest.CallInfo()
|
||||||
|
:members:
|
||||||
|
|
||||||
CollectReport
|
CollectReport
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -837,46 +906,11 @@ ExitCode
|
||||||
.. autoclass:: pytest.ExitCode
|
.. autoclass:: pytest.ExitCode
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
File
|
|
||||||
~~~~
|
|
||||||
|
|
||||||
.. autoclass:: pytest.File()
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
|
|
||||||
FixtureDef
|
FixtureDef
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
||||||
.. autoclass:: _pytest.fixtures.FixtureDef()
|
.. autoclass:: pytest.FixtureDef()
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
FSCollector
|
|
||||||
~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. autoclass:: _pytest.nodes.FSCollector()
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
Function
|
|
||||||
~~~~~~~~
|
|
||||||
|
|
||||||
.. autoclass:: pytest.Function()
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
FunctionDefinition
|
|
||||||
~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. autoclass:: _pytest.python.FunctionDefinition()
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
Item
|
|
||||||
~~~~
|
|
||||||
|
|
||||||
.. autoclass:: pytest.Item()
|
|
||||||
:members:
|
:members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
@ -907,19 +941,6 @@ Metafunc
|
||||||
.. autoclass:: pytest.Metafunc()
|
.. autoclass:: pytest.Metafunc()
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
Module
|
|
||||||
~~~~~~
|
|
||||||
|
|
||||||
.. autoclass:: pytest.Module()
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
Node
|
|
||||||
~~~~
|
|
||||||
|
|
||||||
.. autoclass:: _pytest.nodes.Node()
|
|
||||||
:members:
|
|
||||||
|
|
||||||
Parser
|
Parser
|
||||||
~~~~~~
|
~~~~~~
|
||||||
|
|
||||||
|
@ -941,13 +962,6 @@ PytestPluginManager
|
||||||
:inherited-members:
|
:inherited-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
Session
|
|
||||||
~~~~~~~
|
|
||||||
|
|
||||||
.. autoclass:: pytest.Session()
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
TestReport
|
TestReport
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -1153,6 +1167,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:
|
||||||
|
|
||||||
|
@ -1703,6 +1720,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
|
||||||
|
|
||||||
|
@ -1918,9 +1940,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, --config-file FILE
|
-c FILE, --config-file=FILE
|
||||||
Load configuration from `FILE` instead of trying to
|
Load configuration from `FILE` instead of trying to
|
||||||
locate one of the implicit configuration files
|
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
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
pallets-sphinx-themes
|
pallets-sphinx-themes
|
||||||
pluggy>=1.0
|
pluggy>=1.2.0
|
||||||
pygments-pytest>=2.3.0
|
pygments-pytest>=2.3.0
|
||||||
sphinx-removed-in>=0.2.0
|
sphinx-removed-in>=0.2.0
|
||||||
sphinx>=5,<6
|
sphinx>=5,<6
|
||||||
|
|
|
@ -113,7 +113,7 @@ template = "changelog/_template.rst"
|
||||||
showcontent = true
|
showcontent = true
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
target-version = ['py37']
|
target-version = ['py38']
|
||||||
|
|
||||||
# check-wheel-contents is executed by the build-and-inspect-python-package action.
|
# check-wheel-contents is executed by the build-and-inspect-python-package action.
|
||||||
[tool.check-wheel-contents]
|
[tool.check-wheel-contents]
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
10
setup.cfg
10
setup.cfg
|
@ -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
|
||||||
|
@ -46,12 +46,11 @@ py_modules = py
|
||||||
install_requires =
|
install_requires =
|
||||||
iniconfig
|
iniconfig
|
||||||
packaging
|
packaging
|
||||||
pluggy>=0.12,<2.0
|
pluggy>=1.2.0,<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,18 +17,21 @@ 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
|
||||||
|
|
||||||
|
@ -42,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 Final
|
|
||||||
from typing_extensions import Literal
|
|
||||||
from typing_extensions import SupportsIndex
|
|
||||||
|
|
||||||
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
|
|
||||||
|
|
||||||
if sys.version_info[:2] < (3, 11):
|
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."""
|
||||||
|
@ -396,11 +393,11 @@ class Traceback(List[TracebackEntry]):
|
||||||
|
|
||||||
def filter(
|
def filter(
|
||||||
self,
|
self,
|
||||||
# TODO(py38): change to positional only.
|
excinfo_or_fn: Union[
|
||||||
_excinfo_or_fn: Union[
|
|
||||||
"ExceptionInfo[BaseException]",
|
"ExceptionInfo[BaseException]",
|
||||||
Callable[[TracebackEntry], bool],
|
Callable[[TracebackEntry], bool],
|
||||||
],
|
],
|
||||||
|
/,
|
||||||
) -> "Traceback":
|
) -> "Traceback":
|
||||||
"""Return a Traceback instance with certain items removed.
|
"""Return a Traceback instance with certain items removed.
|
||||||
|
|
||||||
|
@ -411,10 +408,10 @@ class Traceback(List[TracebackEntry]):
|
||||||
``TracebackEntry`` instance, and should return True when the item should
|
``TracebackEntry`` instance, and should return True when the item should
|
||||||
be added to the ``Traceback``, False when not.
|
be added to the ``Traceback``, False when not.
|
||||||
"""
|
"""
|
||||||
if isinstance(_excinfo_or_fn, ExceptionInfo):
|
if isinstance(excinfo_or_fn, ExceptionInfo):
|
||||||
fn = lambda x: not x.ishidden(_excinfo_or_fn) # noqa: E731
|
fn = lambda x: not x.ishidden(excinfo_or_fn) # noqa: E731
|
||||||
else:
|
else:
|
||||||
fn = _excinfo_or_fn
|
fn = excinfo_or_fn
|
||||||
return Traceback(filter(fn, self))
|
return Traceback(filter(fn, self))
|
||||||
|
|
||||||
def recursionindex(self) -> Optional[int]:
|
def recursionindex(self) -> Optional[int]:
|
||||||
|
@ -633,7 +630,7 @@ class ExceptionInfo(Generic[E]):
|
||||||
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: Union[
|
tbfilter: Union[
|
||||||
bool, Callable[["ExceptionInfo[BaseException]"], Traceback]
|
bool, Callable[["ExceptionInfo[BaseException]"], Traceback]
|
||||||
|
@ -707,7 +704,12 @@ class ExceptionInfo(Generic[E]):
|
||||||
If it matches `True` is returned, otherwise an `AssertionError` is raised.
|
If it matches `True` is returned, otherwise an `AssertionError` is raised.
|
||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
value = str(self.value)
|
value = "\n".join(
|
||||||
|
[
|
||||||
|
str(self.value),
|
||||||
|
*getattr(self.value, "__notes__", []),
|
||||||
|
]
|
||||||
|
)
|
||||||
msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n str(exception): {value!r}"
|
msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n str(exception): {value!r}"
|
||||||
if regexp == value:
|
if regexp == value:
|
||||||
msg += "\n Did you mean to `re.escape()` the regex?"
|
msg += "\n Did you mean to `re.escape()` the regex?"
|
||||||
|
@ -725,7 +727,7 @@ 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: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True
|
tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True
|
||||||
funcargs: bool = False
|
funcargs: bool = False
|
||||||
|
@ -1090,7 +1092,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 = "_ "
|
||||||
|
|
||||||
|
@ -1124,7 +1126,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))
|
||||||
|
@ -1136,7 +1138,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.
|
||||||
|
|
|
@ -25,14 +25,12 @@ from stat import S_ISREG
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
from typing import Literal
|
||||||
from typing import overload
|
from typing import overload
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from . import error
|
from . import error
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from typing import Literal
|
|
||||||
|
|
||||||
# Moved from local.py.
|
# Moved from local.py.
|
||||||
iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt")
|
iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt")
|
||||||
|
|
||||||
|
|
|
@ -112,8 +112,8 @@ def pytest_collection(session: "Session") -> None:
|
||||||
assertstate.hook.set_session(session)
|
assertstate.hook.set_session(session)
|
||||||
|
|
||||||
|
|
||||||
@hookimpl(tryfirst=True, hookwrapper=True)
|
@hookimpl(wrapper=True, tryfirst=True)
|
||||||
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
|
def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
|
||||||
"""Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks.
|
"""Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks.
|
||||||
|
|
||||||
The rewrite module will use util._reprcompare if it exists to use custom
|
The rewrite module will use util._reprcompare if it exists to use custom
|
||||||
|
@ -162,8 +162,9 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
|
||||||
|
|
||||||
util._assertion_pass = call_assertion_pass_hook
|
util._assertion_pass = call_assertion_pass_hook
|
||||||
|
|
||||||
yield
|
try:
|
||||||
|
return (yield)
|
||||||
|
finally:
|
||||||
util._reprcompare, util._assertion_pass = saved_assert_hooks
|
util._reprcompare, util._assertion_pass = saved_assert_hooks
|
||||||
util._config = None
|
util._config = None
|
||||||
|
|
||||||
|
|
|
@ -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,10 @@ 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)
|
||||||
|
and isinstance(item.value.value, str)
|
||||||
):
|
):
|
||||||
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 +810,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 +864,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 +889,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 +909,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 +920,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 +934,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 +942,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 +980,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
|
||||||
|
@ -1003,7 +999,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
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:
|
||||||
|
@ -1015,7 +1011,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)
|
||||||
|
|
||||||
|
@ -1089,7 +1085,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
comp.left = self.variables_overwrite[
|
comp.left = self.variables_overwrite[
|
||||||
comp.left.id
|
comp.left.id
|
||||||
] # type:ignore[assignment]
|
] # type:ignore[assignment]
|
||||||
if isinstance(comp.left, namedExpr):
|
if isinstance(comp.left, ast.NamedExpr):
|
||||||
self.variables_overwrite[
|
self.variables_overwrite[
|
||||||
comp.left.target.id
|
comp.left.target.id
|
||||||
] = comp.left # type:ignore[assignment]
|
] = comp.left # type:ignore[assignment]
|
||||||
|
@ -1105,7 +1101,7 @@ 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
|
||||||
):
|
):
|
||||||
|
@ -1118,9 +1114,9 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
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
|
||||||
|
@ -1164,7 +1160,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
|
||||||
|
|
||||||
|
@ -217,12 +217,12 @@ class LFPluginCollWrapper:
|
||||||
self.lfplugin = lfplugin
|
self.lfplugin = lfplugin
|
||||||
self._collected_at_least_one_failure = False
|
self._collected_at_least_one_failure = False
|
||||||
|
|
||||||
@hookimpl(hookwrapper=True)
|
@hookimpl(wrapper=True)
|
||||||
def pytest_make_collect_report(self, collector: nodes.Collector):
|
def pytest_make_collect_report(
|
||||||
|
self, collector: nodes.Collector
|
||||||
|
) -> Generator[None, CollectReport, CollectReport]:
|
||||||
|
res = yield
|
||||||
if isinstance(collector, (Session, Package)):
|
if isinstance(collector, (Session, Package)):
|
||||||
out = yield
|
|
||||||
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
|
||||||
|
|
||||||
|
@ -240,19 +240,16 @@ class LFPluginCollWrapper:
|
||||||
key=sort_key,
|
key=sort_key,
|
||||||
reverse=True,
|
reverse=True,
|
||||||
)
|
)
|
||||||
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
|
|
||||||
res = out.get_result()
|
|
||||||
result = res.result
|
result = res.result
|
||||||
lastfailed = self.lfplugin.lastfailed
|
lastfailed = self.lfplugin.lastfailed
|
||||||
|
|
||||||
# Only filter with known failures.
|
# Only filter with known failures.
|
||||||
if not self._collected_at_least_one_failure:
|
if not self._collected_at_least_one_failure:
|
||||||
if not any(x.nodeid in lastfailed for x in result):
|
if not any(x.nodeid in lastfailed for x in result):
|
||||||
return
|
return res
|
||||||
self.lfplugin.config.pluginmanager.register(
|
self.lfplugin.config.pluginmanager.register(
|
||||||
LFPluginCollSkipfiles(self.lfplugin), "lfplugin-collskip"
|
LFPluginCollSkipfiles(self.lfplugin), "lfplugin-collskip"
|
||||||
)
|
)
|
||||||
|
@ -268,8 +265,8 @@ class LFPluginCollWrapper:
|
||||||
# Keep all sub-collectors.
|
# Keep all sub-collectors.
|
||||||
or isinstance(x, nodes.Collector)
|
or isinstance(x, nodes.Collector)
|
||||||
]
|
]
|
||||||
return
|
|
||||||
yield
|
return res
|
||||||
|
|
||||||
|
|
||||||
class LFPluginCollSkipfiles:
|
class LFPluginCollSkipfiles:
|
||||||
|
@ -280,9 +277,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 we only want to skip test-bearing Modules,
|
# Packages are Files, but we only want to skip test-bearing Files,
|
||||||
# so don't filter Packages.
|
# so don't filter Packages.
|
||||||
if isinstance(collector, Module) and not isinstance(collector, Package):
|
if isinstance(collector, File) 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
|
||||||
|
|
||||||
|
@ -342,14 +339,14 @@ class LFPlugin:
|
||||||
else:
|
else:
|
||||||
self.lastfailed[report.nodeid] = True
|
self.lastfailed[report.nodeid] = True
|
||||||
|
|
||||||
@hookimpl(hookwrapper=True, tryfirst=True)
|
@hookimpl(wrapper=True, tryfirst=True)
|
||||||
def pytest_collection_modifyitems(
|
def pytest_collection_modifyitems(
|
||||||
self, config: Config, items: List[nodes.Item]
|
self, config: Config, items: List[nodes.Item]
|
||||||
) -> Generator[None, None, None]:
|
) -> Generator[None, None, None]:
|
||||||
yield
|
res = yield
|
||||||
|
|
||||||
if not self.active:
|
if not self.active:
|
||||||
return
|
return res
|
||||||
|
|
||||||
if self.lastfailed:
|
if self.lastfailed:
|
||||||
previously_failed = []
|
previously_failed = []
|
||||||
|
@ -394,6 +391,8 @@ class LFPlugin:
|
||||||
else:
|
else:
|
||||||
self._report_status += "not deselecting items."
|
self._report_status += "not deselecting items."
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
def pytest_sessionfinish(self, session: Session) -> None:
|
def pytest_sessionfinish(self, session: Session) -> None:
|
||||||
config = self.config
|
config = self.config
|
||||||
if config.getoption("cacheshow") or hasattr(config, "workerinput"):
|
if config.getoption("cacheshow") or hasattr(config, "workerinput"):
|
||||||
|
@ -414,11 +413,11 @@ class NFPlugin:
|
||||||
assert config.cache is not None
|
assert config.cache is not None
|
||||||
self.cached_nodeids = set(config.cache.get("cache/nodeids", []))
|
self.cached_nodeids = set(config.cache.get("cache/nodeids", []))
|
||||||
|
|
||||||
@hookimpl(hookwrapper=True, tryfirst=True)
|
@hookimpl(wrapper=True, tryfirst=True)
|
||||||
def pytest_collection_modifyitems(
|
def pytest_collection_modifyitems(
|
||||||
self, items: List[nodes.Item]
|
self, items: List[nodes.Item]
|
||||||
) -> Generator[None, None, None]:
|
) -> Generator[None, None, None]:
|
||||||
yield
|
res = yield
|
||||||
|
|
||||||
if self.active:
|
if self.active:
|
||||||
new_items: Dict[str, nodes.Item] = {}
|
new_items: Dict[str, nodes.Item] = {}
|
||||||
|
@ -436,6 +435,8 @@ class NFPlugin:
|
||||||
else:
|
else:
|
||||||
self.cached_nodeids.update(item.nodeid for item in items)
|
self.cached_nodeids.update(item.nodeid for item in items)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]:
|
def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]:
|
||||||
return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True) # type: ignore[no-any-return]
|
return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True) # type: ignore[no-any-return]
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
@ -34,12 +36,9 @@ from _pytest.fixtures import SubRequest
|
||||||
from _pytest.nodes import Collector
|
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
|
||||||
|
from _pytest.reports import CollectReport
|
||||||
|
|
||||||
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:
|
||||||
|
@ -132,8 +131,8 @@ def _windowsconsoleio_workaround(stream: TextIO) -> None:
|
||||||
sys.stderr = _reopen_stdio(sys.stderr, "wb")
|
sys.stderr = _reopen_stdio(sys.stderr, "wb")
|
||||||
|
|
||||||
|
|
||||||
@hookimpl(hookwrapper=True)
|
@hookimpl(wrapper=True)
|
||||||
def pytest_load_initial_conftests(early_config: Config):
|
def pytest_load_initial_conftests(early_config: Config) -> Generator[None, None, None]:
|
||||||
ns = early_config.known_args_namespace
|
ns = early_config.known_args_namespace
|
||||||
if ns.capture == "fd":
|
if ns.capture == "fd":
|
||||||
_windowsconsoleio_workaround(sys.stdout)
|
_windowsconsoleio_workaround(sys.stdout)
|
||||||
|
@ -147,12 +146,16 @@ def pytest_load_initial_conftests(early_config: Config):
|
||||||
|
|
||||||
# Finally trigger conftest loading but while capturing (issue #93).
|
# Finally trigger conftest loading but while capturing (issue #93).
|
||||||
capman.start_global_capturing()
|
capman.start_global_capturing()
|
||||||
outcome = yield
|
try:
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
capman.suspend_global_capture()
|
capman.suspend_global_capture()
|
||||||
if outcome.excinfo is not None:
|
except BaseException:
|
||||||
out, err = capman.read_global_capture()
|
out, err = capman.read_global_capture()
|
||||||
sys.stdout.write(out)
|
sys.stdout.write(out)
|
||||||
sys.stderr.write(err)
|
sys.stderr.write(err)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
# IO Helpers.
|
# IO Helpers.
|
||||||
|
@ -687,7 +690,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 +726,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
|
||||||
|
@ -849,35 +852,39 @@ class CaptureManager:
|
||||||
|
|
||||||
# Hooks
|
# Hooks
|
||||||
|
|
||||||
@hookimpl(hookwrapper=True)
|
@hookimpl(wrapper=True)
|
||||||
def pytest_make_collect_report(self, collector: Collector):
|
def pytest_make_collect_report(
|
||||||
|
self, collector: Collector
|
||||||
|
) -> Generator[None, CollectReport, CollectReport]:
|
||||||
if isinstance(collector, File):
|
if isinstance(collector, File):
|
||||||
self.resume_global_capture()
|
self.resume_global_capture()
|
||||||
outcome = yield
|
try:
|
||||||
|
rep = yield
|
||||||
|
finally:
|
||||||
self.suspend_global_capture()
|
self.suspend_global_capture()
|
||||||
out, err = self.read_global_capture()
|
out, err = self.read_global_capture()
|
||||||
rep = outcome.get_result()
|
|
||||||
if out:
|
if out:
|
||||||
rep.sections.append(("Captured stdout", out))
|
rep.sections.append(("Captured stdout", out))
|
||||||
if err:
|
if err:
|
||||||
rep.sections.append(("Captured stderr", err))
|
rep.sections.append(("Captured stderr", err))
|
||||||
else:
|
else:
|
||||||
yield
|
rep = yield
|
||||||
|
return rep
|
||||||
|
|
||||||
@hookimpl(hookwrapper=True)
|
@hookimpl(wrapper=True)
|
||||||
def pytest_runtest_setup(self, item: Item) -> Generator[None, None, None]:
|
def pytest_runtest_setup(self, item: Item) -> Generator[None, None, None]:
|
||||||
with self.item_capture("setup", item):
|
with self.item_capture("setup", item):
|
||||||
yield
|
return (yield)
|
||||||
|
|
||||||
@hookimpl(hookwrapper=True)
|
@hookimpl(wrapper=True)
|
||||||
def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]:
|
def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]:
|
||||||
with self.item_capture("call", item):
|
with self.item_capture("call", item):
|
||||||
yield
|
return (yield)
|
||||||
|
|
||||||
@hookimpl(hookwrapper=True)
|
@hookimpl(wrapper=True)
|
||||||
def pytest_runtest_teardown(self, item: Item) -> Generator[None, None, None]:
|
def pytest_runtest_teardown(self, item: Item) -> Generator[None, None, None]:
|
||||||
with self.item_capture("teardown", item):
|
with self.item_capture("teardown", item):
|
||||||
yield
|
return (yield)
|
||||||
|
|
||||||
@hookimpl(tryfirst=True)
|
@hookimpl(tryfirst=True)
|
||||||
def pytest_keyboard_interrupt(self) -> None:
|
def pytest_keyboard_interrupt(self) -> 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)
|
||||||
|
@ -93,7 +68,7 @@ def is_async_function(func: object) -> bool:
|
||||||
return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
|
return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
|
||||||
|
|
||||||
|
|
||||||
def getlocation(function, curdir: str | None = None) -> str:
|
def getlocation(function, curdir: str | os.PathLike[str] | None = None) -> str:
|
||||||
function = get_real_func(function)
|
function = get_real_func(function)
|
||||||
fn = Path(inspect.getfile(function))
|
fn = Path(inspect.getfile(function))
|
||||||
lineno = function.__code__.co_firstlineno
|
lineno = function.__code__.co_firstlineno
|
||||||
|
@ -127,7 +102,7 @@ def num_mock_patch_args(function) -> int:
|
||||||
|
|
||||||
|
|
||||||
def getfuncargnames(
|
def getfuncargnames(
|
||||||
function: Callable[..., Any],
|
function: Callable[..., object],
|
||||||
*,
|
*,
|
||||||
name: str = "",
|
name: str = "",
|
||||||
is_method: bool = False,
|
is_method: bool = False,
|
||||||
|
@ -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 # type: ignore[attr-defined]
|
|
||||||
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
|
||||||
|
@ -137,7 +137,9 @@ def main(
|
||||||
) -> Union[int, ExitCode]:
|
) -> Union[int, ExitCode]:
|
||||||
"""Perform an in-process test run.
|
"""Perform an in-process test run.
|
||||||
|
|
||||||
:param args: List of command line arguments.
|
:param args:
|
||||||
|
List of command line arguments. If `None` or not given, defaults to reading
|
||||||
|
arguments directly from the process command line (:data:`sys.argv`).
|
||||||
:param plugins: List of plugin objects to be auto-registered during initialization.
|
:param plugins: List of plugin objects to be auto-registered during initialization.
|
||||||
|
|
||||||
:returns: An exit code.
|
:returns: An exit code.
|
||||||
|
@ -257,7 +259,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 +530,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 +545,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 +564,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 +1141,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 +1219,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 +1239,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", "")
|
||||||
|
@ -1282,11 +1341,13 @@ class Config:
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@hookimpl(hookwrapper=True)
|
@hookimpl(wrapper=True)
|
||||||
def pytest_collection(self) -> Generator[None, None, None]:
|
def pytest_collection(self) -> Generator[None, object, object]:
|
||||||
# Validate invalid ini keys after collection is done so we take in account
|
# Validate invalid ini keys after collection is done so we take in account
|
||||||
# options added by late-loading conftest files.
|
# options added by late-loading conftest files.
|
||||||
yield
|
try:
|
||||||
|
return (yield)
|
||||||
|
finally:
|
||||||
self._validate_config_options()
|
self._validate_config_options()
|
||||||
|
|
||||||
def _checkversion(self) -> None:
|
def _checkversion(self) -> None:
|
||||||
|
@ -1371,34 +1432,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 testpaths and not args:
|
|
||||||
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
|
|
||||||
)
|
|
||||||
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
|
||||||
|
|
||||||
|
@ -1406,7 +1450,7 @@ class Config:
|
||||||
"""Issue and handle a warning during the "configure" stage.
|
"""Issue and handle a warning during the "configure" stage.
|
||||||
|
|
||||||
During ``pytest_configure`` we can't capture warnings using the ``catch_warnings_for_item``
|
During ``pytest_configure`` we can't capture warnings using the ``catch_warnings_for_item``
|
||||||
function because it is not possible to have hookwrappers around ``pytest_configure``.
|
function because it is not possible to have hook wrappers around ``pytest_configure``.
|
||||||
|
|
||||||
This function is mainly intended for plugins that need to issue warnings during
|
This function is mainly intended for plugins that need to issue warnings during
|
||||||
``pytest_configure`` (or similar stages).
|
``pytest_configure`` (or similar stages).
|
||||||
|
|
|
@ -7,26 +7,23 @@ 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 Literal
|
||||||
from typing import Mapping
|
from typing import Mapping
|
||||||
from typing import NoReturn
|
from typing import NoReturn
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
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
|
||||||
from _pytest.deprecated import ARGUMENT_TYPE_STR_CHOICE
|
from _pytest.deprecated import ARGUMENT_TYPE_STR_CHOICE
|
||||||
from _pytest.deprecated import check_ispytest
|
from _pytest.deprecated import check_ispytest
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from typing_extensions import Literal
|
|
||||||
|
|
||||||
FILE_OR_DIR = "file_or_dir"
|
FILE_OR_DIR = "file_or_dir"
|
||||||
|
|
||||||
|
|
||||||
|
@ -177,7 +174,7 @@ class Parser:
|
||||||
name: str,
|
name: str,
|
||||||
help: str,
|
help: str,
|
||||||
type: Optional[
|
type: Optional[
|
||||||
"Literal['string', 'paths', 'pathlist', 'args', 'linelist', 'bool']"
|
Literal["string", "paths", "pathlist", "args", "linelist", "bool"]
|
||||||
] = None,
|
] = None,
|
||||||
default: Any = None,
|
default: Any = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from _pytest.compat import final
|
from typing import final
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
|
|
|
@ -304,10 +304,10 @@ class PdbInvoke:
|
||||||
|
|
||||||
|
|
||||||
class PdbTrace:
|
class PdbTrace:
|
||||||
@hookimpl(hookwrapper=True)
|
@hookimpl(wrapper=True)
|
||||||
def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, None, None]:
|
def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, object, object]:
|
||||||
wrap_pytest_function_for_tracing(pyfuncitem)
|
wrap_pytest_function_for_tracing(pyfuncitem)
|
||||||
yield
|
return (yield)
|
||||||
|
|
||||||
|
|
||||||
def wrap_pytest_function_for_tracing(pyfuncitem):
|
def wrap_pytest_function_for_tracing(pyfuncitem):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -582,7 +582,7 @@ def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest:
|
||||||
doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined]
|
doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined]
|
||||||
node=doctest_item, func=func, cls=None, funcargs=False
|
node=doctest_item, func=func, cls=None, funcargs=False
|
||||||
)
|
)
|
||||||
fixture_request = FixtureRequest(doctest_item, _ispytest=True)
|
fixture_request = FixtureRequest(doctest_item, _ispytest=True) # type: ignore[arg-type]
|
||||||
fixture_request._fillfixtures()
|
fixture_request._fillfixtures()
|
||||||
return fixture_request
|
return fixture_request
|
||||||
|
|
||||||
|
|
|
@ -62,8 +62,8 @@ def get_timeout_config_value(config: Config) -> float:
|
||||||
return float(config.getini("faulthandler_timeout") or 0.0)
|
return float(config.getini("faulthandler_timeout") or 0.0)
|
||||||
|
|
||||||
|
|
||||||
@pytest.hookimpl(hookwrapper=True, trylast=True)
|
@pytest.hookimpl(wrapper=True, trylast=True)
|
||||||
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
|
def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
|
||||||
timeout = get_timeout_config_value(item.config)
|
timeout = get_timeout_config_value(item.config)
|
||||||
if timeout > 0:
|
if timeout > 0:
|
||||||
import faulthandler
|
import faulthandler
|
||||||
|
@ -71,11 +71,11 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
|
||||||
stderr = item.config.stash[fault_handler_stderr_fd_key]
|
stderr = item.config.stash[fault_handler_stderr_fd_key]
|
||||||
faulthandler.dump_traceback_later(timeout, file=stderr)
|
faulthandler.dump_traceback_later(timeout, file=stderr)
|
||||||
try:
|
try:
|
||||||
yield
|
return (yield)
|
||||||
finally:
|
finally:
|
||||||
faulthandler.cancel_dump_traceback_later()
|
faulthandler.cancel_dump_traceback_later()
|
||||||
else:
|
else:
|
||||||
yield
|
return (yield)
|
||||||
|
|
||||||
|
|
||||||
@pytest.hookimpl(tryfirst=True)
|
@pytest.hookimpl(tryfirst=True)
|
||||||
|
|
|
@ -2,17 +2,17 @@ import dataclasses
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import warnings
|
import warnings
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import TracebackType
|
|
||||||
from typing import Any
|
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 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,10 +21,10 @@ 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
|
||||||
from typing import Type
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
@ -35,10 +35,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
|
||||||
|
@ -47,12 +45,12 @@ 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 NotSetType
|
from _pytest.compat import NotSetType
|
||||||
from _pytest.compat import overload
|
|
||||||
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
|
||||||
|
@ -62,6 +60,7 @@ from _pytest.outcomes import skip
|
||||||
from _pytest.outcomes import TEST_OUTCOME
|
from _pytest.outcomes import TEST_OUTCOME
|
||||||
from _pytest.pathlib import absolutepath
|
from _pytest.pathlib import absolutepath
|
||||||
from _pytest.pathlib import bestrelpath
|
from _pytest.pathlib import bestrelpath
|
||||||
|
from _pytest.scope import _ScopeName
|
||||||
from _pytest.scope import HIGH_SCOPES
|
from _pytest.scope import HIGH_SCOPES
|
||||||
from _pytest.scope import Scope
|
from _pytest.scope import Scope
|
||||||
from _pytest.stash import StashKey
|
from _pytest.stash import StashKey
|
||||||
|
@ -70,9 +69,9 @@ from _pytest.stash import StashKey
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Deque
|
from typing import Deque
|
||||||
|
|
||||||
from _pytest.scope import _ScopeName
|
|
||||||
from _pytest.main import Session
|
from _pytest.main import Session
|
||||||
from _pytest.python import CallSpec2
|
from _pytest.python import CallSpec2
|
||||||
|
from _pytest.python import Function
|
||||||
from _pytest.python import Metafunc
|
from _pytest.python import Metafunc
|
||||||
|
|
||||||
|
|
||||||
|
@ -97,8 +96,8 @@ _FixtureCachedResult = Union[
|
||||||
None,
|
None,
|
||||||
# Cache key.
|
# Cache key.
|
||||||
object,
|
object,
|
||||||
# Exc info if raised.
|
# Exception if raised.
|
||||||
Tuple[Type[BaseException], BaseException, TracebackType],
|
BaseException,
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -217,6 +216,7 @@ def add_funcarg_pseudo_fixture_def(
|
||||||
params=valuelist,
|
params=valuelist,
|
||||||
unittest=False,
|
unittest=False,
|
||||||
ids=None,
|
ids=None,
|
||||||
|
_ispytest=True,
|
||||||
)
|
)
|
||||||
arg2fixturedefs[argname] = [fixturedef]
|
arg2fixturedefs[argname] = [fixturedef]
|
||||||
if name2pseudofixturedef is not None:
|
if name2pseudofixturedef is not None:
|
||||||
|
@ -352,17 +352,35 @@ def get_direct_param_fixture_func(request: "FixtureRequest") -> Any:
|
||||||
return request.param
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass(frozen=True)
|
||||||
class FuncFixtureInfo:
|
class FuncFixtureInfo:
|
||||||
|
"""Fixture-related information for a fixture-requesting item (e.g. test
|
||||||
|
function).
|
||||||
|
|
||||||
|
This is used to examine the fixtures which an item requests statically
|
||||||
|
(known during collection). This includes autouse fixtures, fixtures
|
||||||
|
requested by the `usefixtures` marker, fixtures requested in the function
|
||||||
|
parameters, and the transitive closure of these.
|
||||||
|
|
||||||
|
An item may also request fixtures dynamically (using `request.getfixturevalue`);
|
||||||
|
these are not reflected here.
|
||||||
|
"""
|
||||||
|
|
||||||
__slots__ = ("argnames", "initialnames", "names_closure", "name2fixturedefs")
|
__slots__ = ("argnames", "initialnames", "names_closure", "name2fixturedefs")
|
||||||
|
|
||||||
# Original function argument names.
|
# Fixture names that the item requests directly by function parameters.
|
||||||
argnames: Tuple[str, ...]
|
argnames: Tuple[str, ...]
|
||||||
# Argnames that function immediately requires. These include argnames +
|
# Fixture names that the item immediately requires. These include
|
||||||
# fixture names specified via usefixtures and via autouse=True in fixture
|
# argnames + fixture names specified via usefixtures and via autouse=True in
|
||||||
# definitions.
|
# fixture definitions.
|
||||||
initialnames: Tuple[str, ...]
|
initialnames: Tuple[str, ...]
|
||||||
|
# The transitive closure of the fixture names that the item requires.
|
||||||
|
# Note: can't include dynamic dependencies (`request.getfixturevalue` calls).
|
||||||
names_closure: List[str]
|
names_closure: List[str]
|
||||||
|
# A map from a fixture name in the transitive closure to the FixtureDefs
|
||||||
|
# matching the name which are applicable to this function.
|
||||||
|
# There may be multiple overriding fixtures with the same name. The
|
||||||
|
# sequence is ordered from furthest to closes to the function.
|
||||||
name2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]]
|
name2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]]
|
||||||
|
|
||||||
def prune_dependency_tree(self) -> None:
|
def prune_dependency_tree(self) -> None:
|
||||||
|
@ -401,17 +419,31 @@ class FixtureRequest:
|
||||||
indirectly.
|
indirectly.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, pyfuncitem, *, _ispytest: bool = False) -> None:
|
def __init__(self, pyfuncitem: "Function", *, _ispytest: bool = False) -> None:
|
||||||
check_ispytest(_ispytest)
|
check_ispytest(_ispytest)
|
||||||
self._pyfuncitem = pyfuncitem
|
|
||||||
#: Fixture for which this request is being performed.
|
#: Fixture for which this request is being performed.
|
||||||
self.fixturename: Optional[str] = None
|
self.fixturename: Optional[str] = None
|
||||||
|
self._pyfuncitem = pyfuncitem
|
||||||
|
self._fixturemanager = pyfuncitem.session._fixturemanager
|
||||||
self._scope = Scope.Function
|
self._scope = Scope.Function
|
||||||
self._fixture_defs: Dict[str, FixtureDef[Any]] = {}
|
# The FixtureDefs for each fixture name requested by this item.
|
||||||
fixtureinfo: FuncFixtureInfo = pyfuncitem._fixtureinfo
|
# Starts from the statically-known fixturedefs resolved during
|
||||||
self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy()
|
# collection. Dynamically requested fixtures (using
|
||||||
|
# `request.getfixturevalue("foo")`) are added dynamically.
|
||||||
|
self._arg2fixturedefs = pyfuncitem._fixtureinfo.name2fixturedefs.copy()
|
||||||
|
# A fixture may override another fixture with the same name, e.g. a fixture
|
||||||
|
# in a module can override a fixture in a conftest, a fixture in a class can
|
||||||
|
# override a fixture in the module, and so on.
|
||||||
|
# An overriding fixture can request its own name; in this case it gets
|
||||||
|
# the value of the fixture it overrides, one level up.
|
||||||
|
# The _arg2index state keeps the current depth in the overriding chain.
|
||||||
|
# The fixturedefs list in _arg2fixturedefs for a given name is ordered from
|
||||||
|
# furthest to closest, so we use negative indexing -1, -2, ... to go from
|
||||||
|
# last to first.
|
||||||
self._arg2index: Dict[str, int] = {}
|
self._arg2index: Dict[str, int] = {}
|
||||||
self._fixturemanager: FixtureManager = pyfuncitem.session._fixturemanager
|
# The evaluated argnames so far, mapping to the FixtureDef they resolved
|
||||||
|
# to.
|
||||||
|
self._fixture_defs: Dict[str, FixtureDef[Any]] = {}
|
||||||
# Notes on the type of `param`:
|
# Notes on the type of `param`:
|
||||||
# -`request.param` is only defined in parametrized fixtures, and will raise
|
# -`request.param` is only defined in parametrized fixtures, and will raise
|
||||||
# AttributeError otherwise. Python typing has no notion of "undefined", so
|
# AttributeError otherwise. Python typing has no notion of "undefined", so
|
||||||
|
@ -423,7 +455,7 @@ class FixtureRequest:
|
||||||
self.param: Any
|
self.param: Any
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def scope(self) -> "_ScopeName":
|
def scope(self) -> _ScopeName:
|
||||||
"""Scope string, one of "function", "class", "module", "package", "session"."""
|
"""Scope string, one of "function", "class", "module", "package", "session"."""
|
||||||
return self._scope.value
|
return self._scope.value
|
||||||
|
|
||||||
|
@ -464,12 +496,17 @@ class FixtureRequest:
|
||||||
assert self._pyfuncitem.parent is not None
|
assert self._pyfuncitem.parent is not None
|
||||||
parentid = self._pyfuncitem.parent.nodeid
|
parentid = self._pyfuncitem.parent.nodeid
|
||||||
fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid)
|
fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid)
|
||||||
# TODO: Fix this type ignore. Either add assert or adjust types.
|
if fixturedefs is not None:
|
||||||
# Can this be None here?
|
self._arg2fixturedefs[argname] = fixturedefs
|
||||||
self._arg2fixturedefs[argname] = fixturedefs # type: ignore[assignment]
|
# No fixtures defined with this name.
|
||||||
# fixturedefs list is immutable so we maintain a decreasing index.
|
if fixturedefs is None:
|
||||||
|
raise FixtureLookupError(argname, self)
|
||||||
|
# The are no fixtures with this name applicable for the function.
|
||||||
|
if not fixturedefs:
|
||||||
|
raise FixtureLookupError(argname, self)
|
||||||
index = self._arg2index.get(argname, 0) - 1
|
index = self._arg2index.get(argname, 0) - 1
|
||||||
if fixturedefs is None or (-index > len(fixturedefs)):
|
# The fixture requested its own name, but no remaining to override.
|
||||||
|
if -index > len(fixturedefs):
|
||||||
raise FixtureLookupError(argname, self)
|
raise FixtureLookupError(argname, self)
|
||||||
self._arg2index[argname] = index
|
self._arg2index[argname] = index
|
||||||
return fixturedefs[index]
|
return fixturedefs[index]
|
||||||
|
@ -502,7 +539,7 @@ class FixtureRequest:
|
||||||
"""Instance (can be None) on which test function was collected."""
|
"""Instance (can be None) on which test function was collected."""
|
||||||
# unittest support hack, see _pytest.unittest.TestCaseFunction.
|
# unittest support hack, see _pytest.unittest.TestCaseFunction.
|
||||||
try:
|
try:
|
||||||
return self._pyfuncitem._testcase
|
return self._pyfuncitem._testcase # type: ignore[attr-defined]
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
function = getattr(self, "function", None)
|
function = getattr(self, "function", None)
|
||||||
return getattr(function, "__self__", None)
|
return getattr(function, "__self__", None)
|
||||||
|
@ -512,15 +549,16 @@ class FixtureRequest:
|
||||||
"""Python module object where the test function was collected."""
|
"""Python module object where the test function was collected."""
|
||||||
if self.scope not in ("function", "class", "module"):
|
if self.scope not in ("function", "class", "module"):
|
||||||
raise AttributeError(f"module not available in {self.scope}-scoped context")
|
raise AttributeError(f"module not available in {self.scope}-scoped context")
|
||||||
return self._pyfuncitem.getparent(_pytest.python.Module).obj
|
mod = self._pyfuncitem.getparent(_pytest.python.Module)
|
||||||
|
assert mod is not None
|
||||||
|
return mod.obj
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path(self) -> Path:
|
def path(self) -> Path:
|
||||||
"""Path where the test function was collected."""
|
"""Path where the test function was collected."""
|
||||||
if self.scope not in ("function", "class", "module", "package"):
|
if self.scope not in ("function", "class", "module", "package"):
|
||||||
raise AttributeError(f"path not available in {self.scope}-scoped context")
|
raise AttributeError(f"path not available in {self.scope}-scoped context")
|
||||||
# TODO: Remove ignore once _pyfuncitem is properly typed.
|
return self._pyfuncitem.path
|
||||||
return self._pyfuncitem.path # type: ignore
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def keywords(self) -> MutableMapping[str, Any]:
|
def keywords(self) -> MutableMapping[str, Any]:
|
||||||
|
@ -592,9 +630,8 @@ class FixtureRequest:
|
||||||
def _get_active_fixturedef(
|
def _get_active_fixturedef(
|
||||||
self, argname: str
|
self, argname: str
|
||||||
) -> Union["FixtureDef[object]", PseudoFixtureDef[object]]:
|
) -> Union["FixtureDef[object]", PseudoFixtureDef[object]]:
|
||||||
try:
|
fixturedef = self._fixture_defs.get(argname)
|
||||||
return self._fixture_defs[argname]
|
if fixturedef is None:
|
||||||
except KeyError:
|
|
||||||
try:
|
try:
|
||||||
fixturedef = self._getnextfixturedef(argname)
|
fixturedef = self._getnextfixturedef(argname)
|
||||||
except FixtureLookupError:
|
except FixtureLookupError:
|
||||||
|
@ -602,8 +639,6 @@ class FixtureRequest:
|
||||||
cached_result = (self, [0], None)
|
cached_result = (self, [0], None)
|
||||||
return PseudoFixtureDef(cached_result, Scope.Function)
|
return PseudoFixtureDef(cached_result, Scope.Function)
|
||||||
raise
|
raise
|
||||||
# Remove indent to prevent the python3 exception
|
|
||||||
# from leaking into the call.
|
|
||||||
self._compute_fixture_value(fixturedef)
|
self._compute_fixture_value(fixturedef)
|
||||||
self._fixture_defs[argname] = fixturedef
|
self._fixture_defs[argname] = fixturedef
|
||||||
return fixturedef
|
return fixturedef
|
||||||
|
@ -698,7 +733,8 @@ class FixtureRequest:
|
||||||
self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest"
|
self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest"
|
||||||
) -> None:
|
) -> None:
|
||||||
# If fixture function failed it might have registered finalizers.
|
# If fixture function failed it might have registered finalizers.
|
||||||
subrequest.node.addfinalizer(lambda: fixturedef.finish(request=subrequest))
|
finalizer = functools.partial(fixturedef.finish, request=subrequest)
|
||||||
|
subrequest.node.addfinalizer(finalizer)
|
||||||
|
|
||||||
def _check_scope(
|
def _check_scope(
|
||||||
self,
|
self,
|
||||||
|
@ -728,8 +764,10 @@ 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 __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
|
@ -825,7 +863,9 @@ class FixtureLookupError(LookupError):
|
||||||
if msg is None:
|
if msg is None:
|
||||||
fm = self.request._fixturemanager
|
fm = self.request._fixturemanager
|
||||||
available = set()
|
available = set()
|
||||||
parentid = self.request._pyfuncitem.parent.nodeid
|
parent = self.request._pyfuncitem.parent
|
||||||
|
assert parent is not None
|
||||||
|
parentid = parent.nodeid
|
||||||
for name, fixturedefs in fm._arg2fixturedefs.items():
|
for name, fixturedefs in fm._arg2fixturedefs.items():
|
||||||
faclist = list(fm._matchfactories(fixturedefs, parentid))
|
faclist = list(fm._matchfactories(fixturedefs, parentid))
|
||||||
if faclist:
|
if faclist:
|
||||||
|
@ -916,10 +956,10 @@ def _teardown_yield_fixture(fixturefunc, it) -> None:
|
||||||
|
|
||||||
|
|
||||||
def _eval_scope_callable(
|
def _eval_scope_callable(
|
||||||
scope_callable: "Callable[[str, Config], _ScopeName]",
|
scope_callable: Callable[[str, Config], _ScopeName],
|
||||||
fixture_name: str,
|
fixture_name: str,
|
||||||
config: Config,
|
config: Config,
|
||||||
) -> "_ScopeName":
|
) -> _ScopeName:
|
||||||
try:
|
try:
|
||||||
# Type ignored because there is no typing mechanism to specify
|
# Type ignored because there is no typing mechanism to specify
|
||||||
# keyword arguments, currently.
|
# keyword arguments, currently.
|
||||||
|
@ -942,7 +982,11 @@ def _eval_scope_callable(
|
||||||
|
|
||||||
@final
|
@final
|
||||||
class FixtureDef(Generic[FixtureValue]):
|
class FixtureDef(Generic[FixtureValue]):
|
||||||
"""A container for a fixture definition."""
|
"""A container for a fixture definition.
|
||||||
|
|
||||||
|
Note: At this time, only explicitly documented fields and methods are
|
||||||
|
considered public stable API.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -950,13 +994,16 @@ class FixtureDef(Generic[FixtureValue]):
|
||||||
baseid: Optional[str],
|
baseid: Optional[str],
|
||||||
argname: str,
|
argname: str,
|
||||||
func: "_FixtureFunc[FixtureValue]",
|
func: "_FixtureFunc[FixtureValue]",
|
||||||
scope: Union[Scope, "_ScopeName", Callable[[str, Config], "_ScopeName"], None],
|
scope: Union[Scope, _ScopeName, Callable[[str, Config], _ScopeName], None],
|
||||||
params: Optional[Sequence[object]],
|
params: Optional[Sequence[object]],
|
||||||
unittest: bool = False,
|
unittest: bool = False,
|
||||||
ids: Optional[
|
ids: Optional[
|
||||||
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
|
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
|
||||||
] = None,
|
] = None,
|
||||||
|
*,
|
||||||
|
_ispytest: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
check_ispytest(_ispytest)
|
||||||
self._fixturemanager = fixturemanager
|
self._fixturemanager = fixturemanager
|
||||||
# The "base" node ID for the fixture.
|
# The "base" node ID for the fixture.
|
||||||
#
|
#
|
||||||
|
@ -972,15 +1019,15 @@ class FixtureDef(Generic[FixtureValue]):
|
||||||
# directory path relative to the rootdir.
|
# directory path relative to the rootdir.
|
||||||
#
|
#
|
||||||
# For other plugins, the baseid is the empty string (always matches).
|
# For other plugins, the baseid is the empty string (always matches).
|
||||||
self.baseid = baseid or ""
|
self.baseid: Final = baseid or ""
|
||||||
# Whether the fixture was found from a node or a conftest in the
|
# Whether the fixture was found from a node or a conftest in the
|
||||||
# collection tree. Will be false for fixtures defined in non-conftest
|
# collection tree. Will be false for fixtures defined in non-conftest
|
||||||
# plugins.
|
# plugins.
|
||||||
self.has_location = baseid is not None
|
self.has_location: Final = baseid is not None
|
||||||
# The fixture factory function.
|
# The fixture factory function.
|
||||||
self.func = func
|
self.func: Final = func
|
||||||
# The name by which the fixture may be requested.
|
# The name by which the fixture may be requested.
|
||||||
self.argname = argname
|
self.argname: Final = argname
|
||||||
if scope is None:
|
if scope is None:
|
||||||
scope = Scope.Function
|
scope = Scope.Function
|
||||||
elif callable(scope):
|
elif callable(scope):
|
||||||
|
@ -989,26 +1036,24 @@ class FixtureDef(Generic[FixtureValue]):
|
||||||
scope = Scope.from_user(
|
scope = Scope.from_user(
|
||||||
scope, descr=f"Fixture '{func.__name__}'", where=baseid
|
scope, descr=f"Fixture '{func.__name__}'", where=baseid
|
||||||
)
|
)
|
||||||
self._scope = scope
|
self._scope: Final = scope
|
||||||
# If the fixture is directly parametrized, the parameter values.
|
# If the fixture is directly parametrized, the parameter values.
|
||||||
self.params: Optional[Sequence[object]] = params
|
self.params: Final = params
|
||||||
# If the fixture is directly parametrized, a tuple of explicit IDs to
|
# If the fixture is directly parametrized, a tuple of explicit IDs to
|
||||||
# assign to the parameter values, or a callable to generate an ID given
|
# assign to the parameter values, or a callable to generate an ID given
|
||||||
# a parameter value.
|
# a parameter value.
|
||||||
self.ids = ids
|
self.ids: Final = ids
|
||||||
# The names requested by the fixtures.
|
# The names requested by the fixtures.
|
||||||
self.argnames = getfuncargnames(func, name=argname, is_method=unittest)
|
self.argnames: Final = getfuncargnames(func, name=argname, is_method=unittest)
|
||||||
# Whether the fixture was collected from a unittest TestCase class.
|
# Whether the fixture was collected from a unittest TestCase class.
|
||||||
# Note that it really only makes sense to define autouse fixtures in
|
self.unittest: Final = unittest
|
||||||
# unittest TestCases.
|
|
||||||
self.unittest = unittest
|
|
||||||
# If the fixture was executed, the current value of the fixture.
|
# If the fixture was executed, the current value of the fixture.
|
||||||
# Can change if the fixture is executed with different parameters.
|
# Can change if the fixture is executed with different parameters.
|
||||||
self.cached_result: Optional[_FixtureCachedResult[FixtureValue]] = None
|
self.cached_result: Optional[_FixtureCachedResult[FixtureValue]] = None
|
||||||
self._finalizers: List[Callable[[], object]] = []
|
self._finalizers: Final[List[Callable[[], object]]] = []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def scope(self) -> "_ScopeName":
|
def scope(self) -> _ScopeName:
|
||||||
"""Scope string, one of "function", "class", "module", "package", "session"."""
|
"""Scope string, one of "function", "class", "module", "package", "session"."""
|
||||||
return self._scope.value
|
return self._scope.value
|
||||||
|
|
||||||
|
@ -1036,7 +1081,7 @@ class FixtureDef(Generic[FixtureValue]):
|
||||||
# value and remove all finalizers because they may be bound methods
|
# value and remove all finalizers because they may be bound methods
|
||||||
# which will keep instances alive.
|
# which will keep instances alive.
|
||||||
self.cached_result = None
|
self.cached_result = None
|
||||||
self._finalizers = []
|
self._finalizers.clear()
|
||||||
|
|
||||||
def execute(self, request: SubRequest) -> FixtureValue:
|
def execute(self, request: SubRequest) -> FixtureValue:
|
||||||
# Get required arguments and register our own finish()
|
# Get required arguments and register our own finish()
|
||||||
|
@ -1050,13 +1095,13 @@ class FixtureDef(Generic[FixtureValue]):
|
||||||
|
|
||||||
my_cache_key = self.cache_key(request)
|
my_cache_key = self.cache_key(request)
|
||||||
if self.cached_result is not None:
|
if self.cached_result is not None:
|
||||||
|
cache_key = self.cached_result[1]
|
||||||
# note: comparison with `==` can fail (or be expensive) for e.g.
|
# note: comparison with `==` can fail (or be expensive) for e.g.
|
||||||
# numpy arrays (#6497).
|
# numpy arrays (#6497).
|
||||||
cache_key = self.cached_result[1]
|
|
||||||
if my_cache_key is cache_key:
|
if my_cache_key is cache_key:
|
||||||
if self.cached_result[2] is not None:
|
if self.cached_result[2] is not None:
|
||||||
_, val, tb = self.cached_result[2]
|
exc = self.cached_result[2]
|
||||||
raise val.with_traceback(tb)
|
raise exc
|
||||||
else:
|
else:
|
||||||
result = self.cached_result[0]
|
result = self.cached_result[0]
|
||||||
return result
|
return result
|
||||||
|
@ -1121,35 +1166,18 @@ def pytest_fixture_setup(
|
||||||
my_cache_key = fixturedef.cache_key(request)
|
my_cache_key = fixturedef.cache_key(request)
|
||||||
try:
|
try:
|
||||||
result = call_fixture_func(fixturefunc, request, kwargs)
|
result = call_fixture_func(fixturefunc, request, kwargs)
|
||||||
except TEST_OUTCOME:
|
except TEST_OUTCOME as e:
|
||||||
exc_info = sys.exc_info()
|
if isinstance(e, skip.Exception):
|
||||||
assert exc_info[0] is not None
|
# The test requested a fixture which caused a skip.
|
||||||
if isinstance(
|
# Don't show the fixture as the skip location, as then the user
|
||||||
exc_info[1], skip.Exception
|
# wouldn't know which test skipped.
|
||||||
) and not fixturefunc.__name__.startswith("xunit_setup"):
|
e._use_item_location = True
|
||||||
exc_info[1]._use_item_location = True # type: ignore[attr-defined]
|
fixturedef.cached_result = (None, my_cache_key, e)
|
||||||
fixturedef.cached_result = (None, my_cache_key, exc_info)
|
|
||||||
raise
|
raise
|
||||||
fixturedef.cached_result = (result, my_cache_key, None)
|
fixturedef.cached_result = (result, my_cache_key, None)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _ensure_immutable_ids(
|
|
||||||
ids: Optional[Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]]
|
|
||||||
) -> Optional[Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]]:
|
|
||||||
if ids is None:
|
|
||||||
return None
|
|
||||||
if callable(ids):
|
|
||||||
return ids
|
|
||||||
return tuple(ids)
|
|
||||||
|
|
||||||
|
|
||||||
def _params_converter(
|
|
||||||
params: Optional[Iterable[object]],
|
|
||||||
) -> Optional[Tuple[object, ...]]:
|
|
||||||
return tuple(params) if params is not None else None
|
|
||||||
|
|
||||||
|
|
||||||
def wrap_function_to_error_out_if_called_directly(
|
def wrap_function_to_error_out_if_called_directly(
|
||||||
function: FixtureFunction,
|
function: FixtureFunction,
|
||||||
fixture_marker: "FixtureFunctionMarker",
|
fixture_marker: "FixtureFunctionMarker",
|
||||||
|
@ -1199,6 +1227,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__
|
||||||
|
@ -1410,10 +1441,14 @@ class FixtureManager:
|
||||||
def __init__(self, session: "Session") -> None:
|
def __init__(self, session: "Session") -> None:
|
||||||
self.session = session
|
self.session = session
|
||||||
self.config: Config = session.config
|
self.config: Config = session.config
|
||||||
self._arg2fixturedefs: Dict[str, List[FixtureDef[Any]]] = {}
|
# Maps a fixture name (argname) to all of the FixtureDefs in the test
|
||||||
self._holderobjseen: Set[object] = set()
|
# suite/plugins defined with this name. Populated by parsefactories().
|
||||||
|
# TODO: The order of the FixtureDefs list of each arg is significant,
|
||||||
|
# explain.
|
||||||
|
self._arg2fixturedefs: Final[Dict[str, List[FixtureDef[Any]]]] = {}
|
||||||
|
self._holderobjseen: Final[Set[object]] = set()
|
||||||
# A mapping from a nodeid to a list of autouse fixtures it defines.
|
# A mapping from a nodeid to a list of autouse fixtures it defines.
|
||||||
self._nodeid_autousenames: Dict[str, List[str]] = {
|
self._nodeid_autousenames: Final[Dict[str, List[str]]] = {
|
||||||
"": self.config.getini("usefixtures"),
|
"": self.config.getini("usefixtures"),
|
||||||
}
|
}
|
||||||
session.config.pluginmanager.register(self, "funcmanage")
|
session.config.pluginmanager.register(self, "funcmanage")
|
||||||
|
@ -1438,8 +1473,26 @@ class FixtureManager:
|
||||||
return parametrize_argnames
|
return parametrize_argnames
|
||||||
|
|
||||||
def getfixtureinfo(
|
def getfixtureinfo(
|
||||||
self, node: nodes.Node, func, cls, funcargs: bool = True
|
self,
|
||||||
|
node: nodes.Item,
|
||||||
|
func: Callable[..., object],
|
||||||
|
cls: Optional[type],
|
||||||
|
funcargs: bool = True,
|
||||||
) -> FuncFixtureInfo:
|
) -> FuncFixtureInfo:
|
||||||
|
"""Calculate the :class:`FuncFixtureInfo` for an item.
|
||||||
|
|
||||||
|
If ``funcargs`` is false, or if the item sets an attribute
|
||||||
|
``nofuncargs = True``, then ``func`` is not examined at all.
|
||||||
|
|
||||||
|
:param node:
|
||||||
|
The item requesting the fixtures.
|
||||||
|
:param func:
|
||||||
|
The item's function.
|
||||||
|
:param cls:
|
||||||
|
If the function is a method, the method's class.
|
||||||
|
:param funcargs:
|
||||||
|
Whether to look into func's parameters as fixture requests.
|
||||||
|
"""
|
||||||
if funcargs and not getattr(node, "nofuncargs", False):
|
if funcargs and not getattr(node, "nofuncargs", False):
|
||||||
argnames = getfuncargnames(func, name=node.name, cls=cls)
|
argnames = getfuncargnames(func, name=node.name, cls=cls)
|
||||||
else:
|
else:
|
||||||
|
@ -1449,8 +1502,7 @@ class FixtureManager:
|
||||||
arg for mark in node.iter_markers(name="usefixtures") for arg in mark.args
|
arg for mark in node.iter_markers(name="usefixtures") for arg in mark.args
|
||||||
)
|
)
|
||||||
initialnames = usefixtures + argnames
|
initialnames = usefixtures + argnames
|
||||||
fm = node.session._fixturemanager
|
initialnames, names_closure, arg2fixturedefs = self.getfixtureclosure(
|
||||||
initialnames, names_closure, arg2fixturedefs = fm.getfixtureclosure(
|
|
||||||
initialnames, node, ignore_args=self._get_direct_parametrize_args(node)
|
initialnames, node, ignore_args=self._get_direct_parametrize_args(node)
|
||||||
)
|
)
|
||||||
return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
|
return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
|
||||||
|
@ -1465,7 +1517,7 @@ class FixtureManager:
|
||||||
# Construct the base nodeid which is later used to check
|
# Construct the base nodeid which is later used to check
|
||||||
# what fixtures are visible for particular tests (as denoted
|
# what fixtures are visible for particular tests (as denoted
|
||||||
# by their test id).
|
# by their test id).
|
||||||
if p.name.startswith("conftest.py"):
|
if p.name == "conftest.py":
|
||||||
try:
|
try:
|
||||||
nodeid = str(p.parent.relative_to(self.config.rootpath))
|
nodeid = str(p.parent.relative_to(self.config.rootpath))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
@ -1671,6 +1723,7 @@ class FixtureManager:
|
||||||
params=marker.params,
|
params=marker.params,
|
||||||
unittest=unittest,
|
unittest=unittest,
|
||||||
ids=marker.ids,
|
ids=marker.ids,
|
||||||
|
_ispytest=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
faclist = self._arg2fixturedefs.setdefault(name, [])
|
faclist = self._arg2fixturedefs.setdefault(name, [])
|
||||||
|
@ -1692,11 +1745,16 @@ class FixtureManager:
|
||||||
def getfixturedefs(
|
def getfixturedefs(
|
||||||
self, argname: str, nodeid: str
|
self, argname: str, nodeid: str
|
||||||
) -> Optional[Sequence[FixtureDef[Any]]]:
|
) -> Optional[Sequence[FixtureDef[Any]]]:
|
||||||
"""Get a list of fixtures which are applicable to the given node id.
|
"""Get FixtureDefs for a fixture name which are applicable
|
||||||
|
to a given node.
|
||||||
|
|
||||||
:param str argname: Name of the fixture to search for.
|
Returns None if there are no fixtures at all defined with the given
|
||||||
:param str nodeid: Full node id of the requesting test.
|
name. (This is different from the case in which there are fixtures
|
||||||
:rtype: Sequence[FixtureDef]
|
with the given name, but none applicable to the node. In this case,
|
||||||
|
an empty result is returned).
|
||||||
|
|
||||||
|
:param argname: Name of the fixture to search for.
|
||||||
|
:param nodeid: Full node id of the requesting test.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
fixturedefs = self._arg2fixturedefs[argname]
|
fixturedefs = self._arg2fixturedefs[argname]
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from argparse import Action
|
from argparse import Action
|
||||||
|
from typing import Generator
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
@ -97,15 +98,14 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.hookimpl(hookwrapper=True)
|
@pytest.hookimpl(wrapper=True)
|
||||||
def pytest_cmdline_parse():
|
def pytest_cmdline_parse() -> Generator[None, Config, Config]:
|
||||||
outcome = yield
|
config = yield
|
||||||
config: Config = outcome.get_result()
|
|
||||||
|
|
||||||
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"
|
||||||
|
@ -128,6 +128,8 @@ def pytest_cmdline_parse():
|
||||||
|
|
||||||
config.add_cleanup(unset_tracing)
|
config.add_cleanup(unset_tracing)
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
def showversion(config: Config) -> None:
|
def showversion(config: Config) -> None:
|
||||||
if config.option.version > 1:
|
if config.option.version > 1:
|
||||||
|
|
|
@ -18,7 +18,7 @@ from _pytest.deprecated import WARNING_CMDLINE_PREPARSE_HOOK
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import pdb
|
import pdb
|
||||||
import warnings
|
import warnings
|
||||||
from typing_extensions import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from _pytest._code.code import ExceptionRepr
|
from _pytest._code.code import ExceptionRepr
|
||||||
from _pytest._code.code import ExceptionInfo
|
from _pytest._code.code import ExceptionInfo
|
||||||
|
@ -60,7 +60,7 @@ def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None:
|
||||||
:param pytest.PytestPluginManager pluginmanager: The pytest plugin manager.
|
:param pytest.PytestPluginManager pluginmanager: The pytest plugin manager.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
This hook is incompatible with ``hookwrapper=True``.
|
This hook is incompatible with hook wrappers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ def pytest_plugin_registered(
|
||||||
:param pytest.PytestPluginManager manager: pytest plugin manager.
|
:param pytest.PytestPluginManager manager: pytest plugin manager.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
This hook is incompatible with ``hookwrapper=True``.
|
This hook is incompatible with hook wrappers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -113,7 +113,7 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
|
||||||
attribute or can be retrieved as the ``pytestconfig`` fixture.
|
attribute or can be retrieved as the ``pytestconfig`` fixture.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
This hook is incompatible with ``hookwrapper=True``.
|
This hook is incompatible with hook wrappers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -128,7 +128,7 @@ def pytest_configure(config: "Config") -> None:
|
||||||
imported.
|
imported.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
This hook is incompatible with ``hookwrapper=True``.
|
This hook is incompatible with hook wrappers.
|
||||||
|
|
||||||
:param pytest.Config config: The pytest config object.
|
:param pytest.Config config: The pytest config object.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -13,8 +13,10 @@ 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 Literal
|
||||||
from typing import Mapping
|
from typing import Mapping
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
@ -25,7 +27,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
|
||||||
|
@ -41,8 +42,6 @@ from _pytest.terminal import TerminalReporter
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
logging_StreamHandler = logging.StreamHandler[StringIO]
|
logging_StreamHandler = logging.StreamHandler[StringIO]
|
||||||
|
|
||||||
from typing_extensions import Literal
|
|
||||||
else:
|
else:
|
||||||
logging_StreamHandler = logging.StreamHandler
|
logging_StreamHandler = logging.StreamHandler
|
||||||
|
|
||||||
|
@ -515,7 +514,9 @@ class LogCaptureFixture:
|
||||||
return original_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
|
||||||
|
@ -736,27 +737,26 @@ class LoggingPlugin:
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@hookimpl(hookwrapper=True, tryfirst=True)
|
@hookimpl(wrapper=True, tryfirst=True)
|
||||||
def pytest_sessionstart(self) -> Generator[None, None, None]:
|
def pytest_sessionstart(self) -> Generator[None, None, None]:
|
||||||
self.log_cli_handler.set_when("sessionstart")
|
self.log_cli_handler.set_when("sessionstart")
|
||||||
|
|
||||||
with catching_logs(self.log_cli_handler, level=self.log_cli_level):
|
with catching_logs(self.log_cli_handler, level=self.log_cli_level):
|
||||||
with catching_logs(self.log_file_handler, level=self.log_file_level):
|
with catching_logs(self.log_file_handler, level=self.log_file_level):
|
||||||
yield
|
return (yield)
|
||||||
|
|
||||||
@hookimpl(hookwrapper=True, tryfirst=True)
|
@hookimpl(wrapper=True, tryfirst=True)
|
||||||
def pytest_collection(self) -> Generator[None, None, None]:
|
def pytest_collection(self) -> Generator[None, None, None]:
|
||||||
self.log_cli_handler.set_when("collection")
|
self.log_cli_handler.set_when("collection")
|
||||||
|
|
||||||
with catching_logs(self.log_cli_handler, level=self.log_cli_level):
|
with catching_logs(self.log_cli_handler, level=self.log_cli_level):
|
||||||
with catching_logs(self.log_file_handler, level=self.log_file_level):
|
with catching_logs(self.log_file_handler, level=self.log_file_level):
|
||||||
yield
|
return (yield)
|
||||||
|
|
||||||
@hookimpl(hookwrapper=True)
|
@hookimpl(wrapper=True)
|
||||||
def pytest_runtestloop(self, session: Session) -> Generator[None, None, None]:
|
def pytest_runtestloop(self, session: Session) -> Generator[None, object, object]:
|
||||||
if session.config.option.collectonly:
|
if session.config.option.collectonly:
|
||||||
yield
|
return (yield)
|
||||||
return
|
|
||||||
|
|
||||||
if self._log_cli_enabled() and self._config.getoption("verbose") < 1:
|
if self._log_cli_enabled() and self._config.getoption("verbose") < 1:
|
||||||
# The verbose flag is needed to avoid messy test progress output.
|
# The verbose flag is needed to avoid messy test progress output.
|
||||||
|
@ -764,7 +764,7 @@ class LoggingPlugin:
|
||||||
|
|
||||||
with catching_logs(self.log_cli_handler, level=self.log_cli_level):
|
with catching_logs(self.log_cli_handler, level=self.log_cli_level):
|
||||||
with catching_logs(self.log_file_handler, level=self.log_file_level):
|
with catching_logs(self.log_file_handler, level=self.log_file_level):
|
||||||
yield # Run all the tests.
|
return (yield) # Run all the tests.
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
def pytest_runtest_logstart(self) -> None:
|
def pytest_runtest_logstart(self) -> None:
|
||||||
|
@ -789,12 +789,13 @@ class LoggingPlugin:
|
||||||
item.stash[caplog_records_key][when] = caplog_handler.records
|
item.stash[caplog_records_key][when] = caplog_handler.records
|
||||||
item.stash[caplog_handler_key] = caplog_handler
|
item.stash[caplog_handler_key] = caplog_handler
|
||||||
|
|
||||||
|
try:
|
||||||
yield
|
yield
|
||||||
|
finally:
|
||||||
log = report_handler.stream.getvalue().strip()
|
log = report_handler.stream.getvalue().strip()
|
||||||
item.add_report_section(when, "log", log)
|
item.add_report_section(when, "log", log)
|
||||||
|
|
||||||
@hookimpl(hookwrapper=True)
|
@hookimpl(wrapper=True)
|
||||||
def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None, None, None]:
|
def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None, None, None]:
|
||||||
self.log_cli_handler.set_when("setup")
|
self.log_cli_handler.set_when("setup")
|
||||||
|
|
||||||
|
@ -802,17 +803,19 @@ class LoggingPlugin:
|
||||||
item.stash[caplog_records_key] = empty
|
item.stash[caplog_records_key] = empty
|
||||||
yield from self._runtest_for(item, "setup")
|
yield from self._runtest_for(item, "setup")
|
||||||
|
|
||||||
@hookimpl(hookwrapper=True)
|
@hookimpl(wrapper=True)
|
||||||
def pytest_runtest_call(self, item: nodes.Item) -> Generator[None, None, None]:
|
def pytest_runtest_call(self, item: nodes.Item) -> Generator[None, None, None]:
|
||||||
self.log_cli_handler.set_when("call")
|
self.log_cli_handler.set_when("call")
|
||||||
|
|
||||||
yield from self._runtest_for(item, "call")
|
yield from self._runtest_for(item, "call")
|
||||||
|
|
||||||
@hookimpl(hookwrapper=True)
|
@hookimpl(wrapper=True)
|
||||||
def pytest_runtest_teardown(self, item: nodes.Item) -> Generator[None, None, None]:
|
def pytest_runtest_teardown(self, item: nodes.Item) -> Generator[None, None, None]:
|
||||||
self.log_cli_handler.set_when("teardown")
|
self.log_cli_handler.set_when("teardown")
|
||||||
|
|
||||||
|
try:
|
||||||
yield from self._runtest_for(item, "teardown")
|
yield from self._runtest_for(item, "teardown")
|
||||||
|
finally:
|
||||||
del item.stash[caplog_records_key]
|
del item.stash[caplog_records_key]
|
||||||
del item.stash[caplog_handler_key]
|
del item.stash[caplog_handler_key]
|
||||||
|
|
||||||
|
@ -820,13 +823,13 @@ class LoggingPlugin:
|
||||||
def pytest_runtest_logfinish(self) -> None:
|
def pytest_runtest_logfinish(self) -> None:
|
||||||
self.log_cli_handler.set_when("finish")
|
self.log_cli_handler.set_when("finish")
|
||||||
|
|
||||||
@hookimpl(hookwrapper=True, tryfirst=True)
|
@hookimpl(wrapper=True, tryfirst=True)
|
||||||
def pytest_sessionfinish(self) -> Generator[None, None, None]:
|
def pytest_sessionfinish(self) -> Generator[None, None, None]:
|
||||||
self.log_cli_handler.set_when("sessionfinish")
|
self.log_cli_handler.set_when("sessionfinish")
|
||||||
|
|
||||||
with catching_logs(self.log_cli_handler, level=self.log_cli_level):
|
with catching_logs(self.log_cli_handler, level=self.log_cli_level):
|
||||||
with catching_logs(self.log_file_handler, level=self.log_file_level):
|
with catching_logs(self.log_file_handler, level=self.log_file_level):
|
||||||
yield
|
return (yield)
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
def pytest_unconfigure(self) -> None:
|
def pytest_unconfigure(self) -> None:
|
||||||
|
|
|
@ -9,21 +9,21 @@ 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 Literal
|
||||||
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
|
||||||
from typing import Type
|
from typing import Type
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
from typing import Union
|
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
|
||||||
|
@ -43,10 +43,6 @@ from _pytest.runner import collect_one_node
|
||||||
from _pytest.runner import SetupState
|
from _pytest.runner import SetupState
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from typing_extensions import Literal
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser: Parser) -> None:
|
def pytest_addoption(parser: Parser) -> None:
|
||||||
parser.addini(
|
parser.addini(
|
||||||
"norecursedirs",
|
"norecursedirs",
|
||||||
|
@ -400,6 +396,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
|
||||||
|
|
||||||
|
|
||||||
|
@ -456,6 +458,11 @@ class _bestrelpath_cache(Dict[Path, str]):
|
||||||
|
|
||||||
@final
|
@final
|
||||||
class Session(nodes.FSCollector):
|
class Session(nodes.FSCollector):
|
||||||
|
"""The root of the collection tree.
|
||||||
|
|
||||||
|
``Session`` collects the initial paths given as arguments to pytest.
|
||||||
|
"""
|
||||||
|
|
||||||
Interrupted = Interrupted
|
Interrupted = Interrupted
|
||||||
Failed = Failed
|
Failed = Failed
|
||||||
# Set on the session by runner.pytest_sessionstart.
|
# Set on the session by runner.pytest_sessionstart.
|
||||||
|
@ -563,9 +570,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(
|
||||||
|
@ -686,8 +690,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))
|
||||||
|
@ -708,7 +712,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.
|
||||||
|
@ -717,7 +721,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
|
||||||
|
|
||||||
|
@ -732,8 +736,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
|
||||||
|
|
||||||
|
@ -750,7 +754,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
|
||||||
|
|
||||||
|
@ -373,7 +374,9 @@ def get_unpacked_marks(
|
||||||
if not consider_mro:
|
if not consider_mro:
|
||||||
mark_lists = [obj.__dict__.get("pytestmark", [])]
|
mark_lists = [obj.__dict__.get("pytestmark", [])]
|
||||||
else:
|
else:
|
||||||
mark_lists = [x.__dict__.get("pytestmark", []) for x in obj.__mro__]
|
mark_lists = [
|
||||||
|
x.__dict__.get("pytestmark", []) for x in reversed(obj.__mro__)
|
||||||
|
]
|
||||||
mark_list = []
|
mark_list = []
|
||||||
for item in mark_lists:
|
for item in mark_lists:
|
||||||
if isinstance(item, list):
|
if isinstance(item, list):
|
||||||
|
@ -412,6 +415,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,6 +5,7 @@ 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 Mapping
|
||||||
|
@ -15,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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
@ -23,7 +24,6 @@ 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._code.code import Traceback
|
||||||
from _pytest.compat import cached_property
|
|
||||||
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
|
||||||
|
@ -157,10 +157,11 @@ class NodeMeta(type):
|
||||||
|
|
||||||
|
|
||||||
class Node(metaclass=NodeMeta):
|
class Node(metaclass=NodeMeta):
|
||||||
"""Base class for Collector and Item, the components of the test
|
r"""Base class of :class:`Collector` and :class:`Item`, the components of
|
||||||
collection tree.
|
the test collection tree.
|
||||||
|
|
||||||
Collector subclasses have children; Items are leaf nodes.
|
``Collector``\'s are the internal nodes of the tree, and ``Item``\'s are the
|
||||||
|
leaf nodes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Implemented in the legacypath plugin.
|
# Implemented in the legacypath plugin.
|
||||||
|
@ -525,15 +526,17 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i
|
||||||
|
|
||||||
|
|
||||||
class Collector(Node):
|
class Collector(Node):
|
||||||
"""Collector instances create children through collect() and thus
|
"""Base class of all collectors.
|
||||||
iteratively build a tree."""
|
|
||||||
|
Collector create children through `collect()` and thus iteratively build
|
||||||
|
the collection tree.
|
||||||
|
"""
|
||||||
|
|
||||||
class CollectError(Exception):
|
class CollectError(Exception):
|
||||||
"""An error during collection, contains a custom message."""
|
"""An error during collection, contains a custom message."""
|
||||||
|
|
||||||
def collect(self) -> Iterable[Union["Item", "Collector"]]:
|
def collect(self) -> Iterable[Union["Item", "Collector"]]:
|
||||||
"""Return a list of children (items and collectors) for this
|
"""Collect children (items and collectors) for this collector."""
|
||||||
collection node."""
|
|
||||||
raise NotImplementedError("abstract")
|
raise NotImplementedError("abstract")
|
||||||
|
|
||||||
# TODO: This omits the style= parameter which breaks Liskov Substitution.
|
# TODO: This omits the style= parameter which breaks Liskov Substitution.
|
||||||
|
@ -577,6 +580,8 @@ def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[
|
||||||
|
|
||||||
|
|
||||||
class FSCollector(Collector):
|
class FSCollector(Collector):
|
||||||
|
"""Base class for filesystem collectors."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
fspath: Optional[LEGACY_PATH] = None,
|
fspath: Optional[LEGACY_PATH] = None,
|
||||||
|
@ -660,7 +665,7 @@ class File(FSCollector):
|
||||||
|
|
||||||
|
|
||||||
class Item(Node):
|
class Item(Node):
|
||||||
"""A basic test invocation item.
|
"""Base class of all test invocation items.
|
||||||
|
|
||||||
Note that for a single function there might be multiple test invocation items.
|
Note that for a single function there might be multiple test invocation items.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -523,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)])
|
||||||
|
@ -633,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:
|
||||||
|
@ -642,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)
|
||||||
|
|
||||||
|
@ -755,21 +769,3 @@ def bestrelpath(directory: Path, dest: Path) -> str:
|
||||||
# Forward from base to dest.
|
# Forward from base to dest.
|
||||||
*reldest.parts,
|
*reldest.parts,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Originates from py. path.local.copy(), with siginficant trims and adjustments.
|
|
||||||
# TODO(py38): Replace with shutil.copytree(..., symlinks=True, dirs_exist_ok=True)
|
|
||||||
def copytree(source: Path, target: Path) -> None:
|
|
||||||
"""Recursively copy a source directory to target."""
|
|
||||||
assert source.is_dir()
|
|
||||||
for entry in visit(source, recurse=lambda entry: not entry.is_symlink()):
|
|
||||||
x = Path(entry)
|
|
||||||
relpath = x.relative_to(source)
|
|
||||||
newx = target / relpath
|
|
||||||
newx.parent.mkdir(exist_ok=True)
|
|
||||||
if x.is_symlink():
|
|
||||||
newx.symlink_to(os.readlink(x))
|
|
||||||
elif x.is_file():
|
|
||||||
shutil.copyfile(x, newx)
|
|
||||||
elif x.is_dir():
|
|
||||||
newx.mkdir(exist_ok=True)
|
|
||||||
|
|
|
@ -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
|
||||||
|
@ -60,18 +63,13 @@ from _pytest.outcomes import fail
|
||||||
from _pytest.outcomes import importorskip
|
from _pytest.outcomes import importorskip
|
||||||
from _pytest.outcomes import skip
|
from _pytest.outcomes import skip
|
||||||
from _pytest.pathlib import bestrelpath
|
from _pytest.pathlib import bestrelpath
|
||||||
from _pytest.pathlib import copytree
|
|
||||||
from _pytest.pathlib import make_numbered_dir
|
from _pytest.pathlib import make_numbered_dir
|
||||||
from _pytest.reports import CollectReport
|
from _pytest.reports import CollectReport
|
||||||
from _pytest.reports import TestReport
|
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 +127,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:
|
||||||
|
@ -161,10 +160,12 @@ class LsofFdLeakChecker:
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@hookimpl(hookwrapper=True, tryfirst=True)
|
@hookimpl(wrapper=True, tryfirst=True)
|
||||||
def pytest_runtest_protocol(self, item: Item) -> Generator[None, None, None]:
|
def pytest_runtest_protocol(self, item: Item) -> Generator[None, object, object]:
|
||||||
lines1 = self.get_open_files()
|
lines1 = self.get_open_files()
|
||||||
yield
|
try:
|
||||||
|
return (yield)
|
||||||
|
finally:
|
||||||
if hasattr(sys, "pypy_version_info"):
|
if hasattr(sys, "pypy_version_info"):
|
||||||
gc.collect()
|
gc.collect()
|
||||||
lines2 = self.get_open_files()
|
lines2 = self.get_open_files()
|
||||||
|
@ -971,7 +972,7 @@ class Pytester:
|
||||||
example_path = example_dir.joinpath(name)
|
example_path = example_dir.joinpath(name)
|
||||||
|
|
||||||
if example_path.is_dir() and not example_path.joinpath("__init__.py").is_file():
|
if example_path.is_dir() and not example_path.joinpath("__init__.py").is_file():
|
||||||
copytree(example_path, self.path)
|
shutil.copytree(example_path, self.path, symlinks=True, dirs_exist_ok=True)
|
||||||
return self.path
|
return self.path
|
||||||
elif example_path.is_file():
|
elif example_path.is_file():
|
||||||
result = self.path.joinpath(example_path.name)
|
result = self.path.joinpath(example_path.name)
|
||||||
|
|
|
@ -15,17 +15,18 @@ 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
|
||||||
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 Pattern
|
from typing import Pattern
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from typing import Set
|
from typing import Set
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import _pytest
|
import _pytest
|
||||||
|
@ -40,7 +41,6 @@ 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
|
||||||
|
@ -75,16 +75,12 @@ from _pytest.pathlib import import_path
|
||||||
from _pytest.pathlib import ImportPathMismatchError
|
from _pytest.pathlib import ImportPathMismatchError
|
||||||
from _pytest.pathlib import parts
|
from _pytest.pathlib import parts
|
||||||
from _pytest.pathlib import visit
|
from _pytest.pathlib import visit
|
||||||
|
from _pytest.scope import _ScopeName
|
||||||
from _pytest.scope import Scope
|
from _pytest.scope import Scope
|
||||||
from _pytest.warning_types import PytestCollectionWarning
|
from _pytest.warning_types import PytestCollectionWarning
|
||||||
from _pytest.warning_types import PytestReturnNotNoneWarning
|
from _pytest.warning_types import PytestReturnNotNoneWarning
|
||||||
from _pytest.warning_types import PytestUnhandledCoroutineWarning
|
from _pytest.warning_types import PytestUnhandledCoroutineWarning
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from typing_extensions import Literal
|
|
||||||
|
|
||||||
from _pytest.scope import _ScopeName
|
|
||||||
|
|
||||||
|
|
||||||
_PYTEST_DIR = Path(_pytest.__file__).parent
|
_PYTEST_DIR = Path(_pytest.__file__).parent
|
||||||
|
|
||||||
|
@ -522,7 +518,7 @@ class PyCollector(PyobjMixin, nodes.Collector):
|
||||||
|
|
||||||
|
|
||||||
class Module(nodes.File, PyCollector):
|
class Module(nodes.File, PyCollector):
|
||||||
"""Collector for test classes and functions."""
|
"""Collector for test classes and functions in a Python module."""
|
||||||
|
|
||||||
def _getobj(self):
|
def _getobj(self):
|
||||||
return self._importtestmodule()
|
return self._importtestmodule()
|
||||||
|
@ -659,6 +655,9 @@ class Module(nodes.File, PyCollector):
|
||||||
|
|
||||||
|
|
||||||
class Package(Module):
|
class Package(Module):
|
||||||
|
"""Collector for files and directories in a Python packages -- directories
|
||||||
|
with an `__init__.py` file."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
fspath: Optional[LEGACY_PATH],
|
fspath: Optional[LEGACY_PATH],
|
||||||
|
@ -706,9 +705,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(
|
||||||
|
@ -739,7 +735,9 @@ class Package(Module):
|
||||||
this_path = self.path.parent
|
this_path = self.path.parent
|
||||||
|
|
||||||
# Always collect the __init__ first.
|
# Always collect the __init__ first.
|
||||||
if path_matches_patterns(self.path, 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=self.path)
|
yield Module.from_parent(self, path=self.path)
|
||||||
|
|
||||||
pkg_prefixes: Set[Path] = set()
|
pkg_prefixes: Set[Path] = set()
|
||||||
|
@ -791,7 +789,7 @@ def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> Optional[o
|
||||||
|
|
||||||
|
|
||||||
class Class(PyCollector):
|
class Class(PyCollector):
|
||||||
"""Collector for test methods."""
|
"""Collector for test methods (and nested classes) in a Python class."""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_parent(cls, parent, *, name, obj=None, **kw):
|
def from_parent(cls, parent, *, name, obj=None, **kw):
|
||||||
|
@ -1234,7 +1232,7 @@ class Metafunc:
|
||||||
ids: Optional[
|
ids: Optional[
|
||||||
Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
|
Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
|
||||||
] = None,
|
] = None,
|
||||||
scope: "Optional[_ScopeName]" = None,
|
scope: Optional[_ScopeName] = None,
|
||||||
*,
|
*,
|
||||||
_param_mark: Optional[Mark] = None,
|
_param_mark: Optional[Mark] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -1676,7 +1674,7 @@ def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None:
|
||||||
|
|
||||||
|
|
||||||
class Function(PyobjMixin, nodes.Item):
|
class Function(PyobjMixin, nodes.Item):
|
||||||
"""An Item responsible for setting up and executing a Python test function.
|
"""Item responsible for setting up and executing a Python test function.
|
||||||
|
|
||||||
:param name:
|
:param name:
|
||||||
The full function name, including any decorations like those
|
The full function name, including any decorations like those
|
||||||
|
@ -1833,10 +1831,8 @@ class Function(PyobjMixin, nodes.Item):
|
||||||
|
|
||||||
|
|
||||||
class FunctionDefinition(Function):
|
class FunctionDefinition(Function):
|
||||||
"""
|
"""This class is a stop gap solution until we evolve to have actual function
|
||||||
This class is a step gap solution until we evolve to have actual function definition nodes
|
definition nodes and manage to get rid of ``metafunc``."""
|
||||||
and manage to get rid of ``metafunc``.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def runtest(self) -> None:
|
def runtest(self) -> None:
|
||||||
raise RuntimeError("function definitions are not supposed to be run as tests")
|
raise RuntimeError("function definitions are not supposed to be run as tests")
|
||||||
|
|
|
@ -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(
|
||||||
|
@ -266,6 +265,7 @@ class ApproxMapping(ApproxBase):
|
||||||
approx_side_as_map.items(), other_side.values()
|
approx_side_as_map.items(), other_side.values()
|
||||||
):
|
):
|
||||||
if approx_value != other_value:
|
if approx_value != other_value:
|
||||||
|
if approx_value.expected is not None and other_value is not None:
|
||||||
max_abs_diff = max(
|
max_abs_diff = max(
|
||||||
max_abs_diff, abs(approx_value.expected - other_value)
|
max_abs_diff, abs(approx_value.expected - other_value)
|
||||||
)
|
)
|
||||||
|
@ -843,6 +843,14 @@ def raises( # noqa: F811
|
||||||
>>> with pytest.raises(ValueError, match=r'must be \d+$'):
|
>>> with pytest.raises(ValueError, match=r'must be \d+$'):
|
||||||
... raise ValueError("value must be 42")
|
... raise ValueError("value must be 42")
|
||||||
|
|
||||||
|
The ``match`` argument searches the formatted exception string, which includes any
|
||||||
|
`PEP-678 <https://peps.python.org/pep-0678/>` ``__notes__``:
|
||||||
|
|
||||||
|
>>> with pytest.raises(ValueError, match=r'had a note added'): # doctest: +SKIP
|
||||||
|
... e = ValueError("value must be 42")
|
||||||
|
... e.add_note("had a note added")
|
||||||
|
... raise e
|
||||||
|
|
||||||
The context manager produces an :class:`ExceptionInfo` object which can be used to inspect the
|
The context manager produces an :class:`ExceptionInfo` object which can be used to inspect the
|
||||||
details of the captured exception::
|
details of the captured exception::
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue