Compare commits
66 Commits
Author | SHA1 | Date |
---|---|---|
|
40d58e0ce7 | |
|
549190651e | |
|
cf72d1a40e | |
|
18dcd9d38d | |
|
33f694f4b3 | |
|
76c107c463 | |
|
531d76daa4 | |
|
a0f58fa9e7 | |
|
b1f3387d42 | |
|
2cdd619bf4 | |
|
d06c05bd23 | |
|
5582bfcddf | |
|
13024efd7a | |
|
a40dacf657 | |
|
3550906ead | |
|
bd068705b1 | |
|
ec5bd27cc7 | |
|
a32feda04c | |
|
f8070ffb9b | |
|
53df6164b4 | |
|
2390610696 | |
|
a0714aa007 | |
|
44ad1c9811 | |
|
5dc77253d4 | |
|
a517827318 | |
|
21fe071d79 | |
|
f8bb8572fe | |
|
1944dc06d3 | |
|
946634c84c | |
|
d849a3ed64 | |
|
721a0881fb | |
|
5341b9cd67 | |
|
c39bdf6190 | |
|
b0c4775a28 | |
|
45f34dfb8d | |
|
e4f022f0d8 | |
|
63b0c6f75f | |
|
884b911a9c | |
|
6e49a74089 | |
|
79c2012d40 | |
|
de69883e3a | |
|
1de00e9830 | |
|
7f5d9b9df4 | |
|
82eb86f707 | |
|
0319a0d4fd | |
|
7855a72d2c | |
|
7a0a0e8b08 | |
|
fbcfd3a52e | |
|
b170081788 | |
|
7a5f2feefb | |
|
69140717d4 | |
|
5c7c3f6329 | |
|
ba40975bb7 | |
|
e3fe7286f8 | |
|
34c73944e1 | |
|
350122abb2 | |
|
06ff7ca13b | |
|
6dfe498c77 | |
|
a566b78730 | |
|
511adf85be | |
|
c71b5df734 | |
|
d53951836d | |
|
a4d7254d18 | |
|
b6c55787fe | |
|
fb03d1388b | |
|
d9bf9dbec1 |
|
@ -1,26 +1,23 @@
|
||||||
name: deploy
|
name: deploy
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_dispatch:
|
||||||
tags:
|
inputs:
|
||||||
# These tags are protected, see:
|
version:
|
||||||
# https://github.com/pytest-dev/pytest/settings/tag_protection
|
description: 'Release version'
|
||||||
- "[0-9]+.[0-9]+.[0-9]+"
|
required: true
|
||||||
- "[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
|
default: '1.2.3'
|
||||||
|
|
||||||
|
|
||||||
# Set permissions at the job level.
|
# Set permissions at the job level.
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
package:
|
||||||
deploy:
|
|
||||||
if: github.repository == 'pytest-dev/pytest'
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 30
|
env:
|
||||||
permissions:
|
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ github.event.inputs.version }}
|
||||||
contents: write
|
timeout-minutes: 10
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
@ -29,7 +26,19 @@ jobs:
|
||||||
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.3
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
if: github.repository == 'pytest-dev/pytest'
|
||||||
|
needs: [package]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: deploy
|
||||||
|
timeout-minutes: 30
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Download Package
|
- name: Download Package
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
|
@ -38,14 +47,35 @@ jobs:
|
||||||
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.5
|
||||||
|
|
||||||
|
- name: Push tag
|
||||||
|
run: |
|
||||||
|
git config user.name "pytest bot"
|
||||||
|
git config user.email "pytestbot@gmail.com"
|
||||||
|
git tag --annotate --message=v${{ github.event.inputs.version }} ${{ github.event.inputs.version }} ${{ github.sha }}
|
||||||
|
git push origin ${{ github.event.inputs.version }}
|
||||||
|
|
||||||
|
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:
|
with:
|
||||||
password: ${{ secrets.pypi_token }}
|
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.10"
|
||||||
|
|
||||||
- name: Install tox
|
- name: Install tox
|
||||||
run: |
|
run: |
|
||||||
|
|
|
@ -27,7 +27,19 @@ concurrency:
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
package:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Build and Check Package
|
||||||
|
uses: hynek/build-and-inspect-python-package@v1.5.3
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
needs: [package]
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 45
|
timeout-minutes: 45
|
||||||
permissions:
|
permissions:
|
||||||
|
@ -38,17 +50,17 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
name: [
|
name: [
|
||||||
"windows-py37",
|
"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",
|
"windows-py312",
|
||||||
|
|
||||||
"ubuntu-py37",
|
"ubuntu-py37",
|
||||||
"ubuntu-py37-pluggy",
|
|
||||||
"ubuntu-py37-freeze",
|
"ubuntu-py37-freeze",
|
||||||
"ubuntu-py38",
|
"ubuntu-py38",
|
||||||
|
"ubuntu-py38-pluggy",
|
||||||
"ubuntu-py39",
|
"ubuntu-py39",
|
||||||
"ubuntu-py310",
|
"ubuntu-py310",
|
||||||
"ubuntu-py311",
|
"ubuntu-py311",
|
||||||
|
@ -60,7 +72,6 @@ jobs:
|
||||||
"macos-py310",
|
"macos-py310",
|
||||||
"macos-py312",
|
"macos-py312",
|
||||||
|
|
||||||
"docs",
|
|
||||||
"doctesting",
|
"doctesting",
|
||||||
"plugins",
|
"plugins",
|
||||||
]
|
]
|
||||||
|
@ -70,15 +81,15 @@ jobs:
|
||||||
python: "3.7"
|
python: "3.7"
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
tox_env: "py37-numpy"
|
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
|
||||||
|
@ -101,10 +112,6 @@ jobs:
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
tox_env: "py37-lsof-numpy-pexpect"
|
tox_env: "py37-lsof-numpy-pexpect"
|
||||||
use_coverage: true
|
use_coverage: true
|
||||||
- name: "ubuntu-py37-pluggy"
|
|
||||||
python: "3.7"
|
|
||||||
os: ubuntu-latest
|
|
||||||
tox_env: "py37-pluggymain-pylib-xdist"
|
|
||||||
- name: "ubuntu-py37-freeze"
|
- name: "ubuntu-py37-freeze"
|
||||||
python: "3.7"
|
python: "3.7"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
|
@ -113,6 +120,10 @@ jobs:
|
||||||
python: "3.8"
|
python: "3.8"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
tox_env: "py38-xdist"
|
tox_env: "py38-xdist"
|
||||||
|
- name: "ubuntu-py38-pluggy"
|
||||||
|
python: "3.8"
|
||||||
|
os: ubuntu-latest
|
||||||
|
tox_env: "py38-pluggymain-pylib-xdist"
|
||||||
- name: "ubuntu-py39"
|
- name: "ubuntu-py39"
|
||||||
python: "3.9"
|
python: "3.9"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
|
@ -159,10 +170,6 @@ jobs:
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
tox_env: "plugins"
|
tox_env: "plugins"
|
||||||
|
|
||||||
- name: "docs"
|
|
||||||
python: "3.7"
|
|
||||||
os: ubuntu-latest
|
|
||||||
tox_env: "docs"
|
|
||||||
- name: "doctesting"
|
- name: "doctesting"
|
||||||
python: "3.7"
|
python: "3.7"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
|
@ -175,6 +182,12 @@ jobs:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Download Package
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: Packages
|
||||||
|
path: dist
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python }}
|
- name: Set up Python ${{ matrix.python }}
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
|
@ -188,11 +201,13 @@ jobs:
|
||||||
|
|
||||||
- name: Test without coverage
|
- name: Test without coverage
|
||||||
if: "! matrix.use_coverage"
|
if: "! matrix.use_coverage"
|
||||||
run: "tox -e ${{ matrix.tox_env }}"
|
shell: bash
|
||||||
|
run: tox run -e ${{ matrix.tox_env }} --installpkg `find dist/*.tar.gz`
|
||||||
|
|
||||||
- name: Test with coverage
|
- name: Test with coverage
|
||||||
if: "matrix.use_coverage"
|
if: "matrix.use_coverage"
|
||||||
run: "tox -e ${{ matrix.tox_env }}-coverage"
|
shell: bash
|
||||||
|
run: tox run -e ${{ matrix.tox_env }}-coverage --installpkg `find dist/*.tar.gz`
|
||||||
|
|
||||||
- name: Generate coverage report
|
- name: Generate coverage report
|
||||||
if: "matrix.use_coverage"
|
if: "matrix.use_coverage"
|
||||||
|
@ -206,10 +221,3 @@ jobs:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
files: ./coverage.xml
|
files: ./coverage.xml
|
||||||
verbose: true
|
verbose: true
|
||||||
|
|
||||||
check-package:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Build and Check Package
|
|
||||||
uses: hynek/build-and-inspect-python-package@v1.5
|
|
||||||
|
|
|
@ -9,6 +9,10 @@ python:
|
||||||
path: .
|
path: .
|
||||||
- requirements: doc/en/requirements.txt
|
- requirements: doc/en/requirements.txt
|
||||||
|
|
||||||
|
sphinx:
|
||||||
|
configuration: doc/en/conf.py
|
||||||
|
fail_on_warning: true
|
||||||
|
|
||||||
build:
|
build:
|
||||||
os: ubuntu-20.04
|
os: ubuntu-20.04
|
||||||
tools:
|
tools:
|
||||||
|
|
10
AUTHORS
10
AUTHORS
|
@ -47,6 +47,7 @@ Ariel Pillemer
|
||||||
Armin Rigo
|
Armin Rigo
|
||||||
Aron Coyle
|
Aron Coyle
|
||||||
Aron Curzon
|
Aron Curzon
|
||||||
|
Arthur Richard
|
||||||
Ashish Kurmi
|
Ashish Kurmi
|
||||||
Aviral Verma
|
Aviral Verma
|
||||||
Aviv Palivoda
|
Aviv Palivoda
|
||||||
|
@ -166,6 +167,8 @@ Ian Bicking
|
||||||
Ian Lesperance
|
Ian Lesperance
|
||||||
Ilya Konstantinov
|
Ilya Konstantinov
|
||||||
Ionuț Turturică
|
Ionuț Turturică
|
||||||
|
Isaac Virshup
|
||||||
|
Israel Fruchter
|
||||||
Itxaso Aizpurua
|
Itxaso Aizpurua
|
||||||
Iwan Briquemont
|
Iwan Briquemont
|
||||||
Jaap Broekhuizen
|
Jaap Broekhuizen
|
||||||
|
@ -173,6 +176,7 @@ Jake VanderPlas
|
||||||
Jakob van Santen
|
Jakob van Santen
|
||||||
Jakub Mitoraj
|
Jakub Mitoraj
|
||||||
James Bourbeau
|
James Bourbeau
|
||||||
|
James Frost
|
||||||
Jan Balster
|
Jan Balster
|
||||||
Janne Vanhala
|
Janne Vanhala
|
||||||
Jason R. Coombs
|
Jason R. Coombs
|
||||||
|
@ -229,6 +233,7 @@ Maho
|
||||||
Maik Figura
|
Maik Figura
|
||||||
Mandeep Bhutani
|
Mandeep Bhutani
|
||||||
Manuel Krebber
|
Manuel Krebber
|
||||||
|
Marc Mueller
|
||||||
Marc Schlaich
|
Marc Schlaich
|
||||||
Marcelo Duarte Trevisani
|
Marcelo Duarte Trevisani
|
||||||
Marcin Bachry
|
Marcin Bachry
|
||||||
|
@ -320,6 +325,7 @@ Ronny Pfannschmidt
|
||||||
Ross Lawley
|
Ross Lawley
|
||||||
Ruaridh Williamson
|
Ruaridh Williamson
|
||||||
Russel Winder
|
Russel Winder
|
||||||
|
Ryan Puddephatt
|
||||||
Ryan Wooden
|
Ryan Wooden
|
||||||
Saiprasad Kale
|
Saiprasad Kale
|
||||||
Samuel Colvin
|
Samuel Colvin
|
||||||
|
@ -334,11 +340,13 @@ Serhii Mozghovyi
|
||||||
Seth Junot
|
Seth Junot
|
||||||
Shantanu Jain
|
Shantanu Jain
|
||||||
Shubham Adep
|
Shubham Adep
|
||||||
|
Simon Blanchard
|
||||||
Simon Gomizelj
|
Simon Gomizelj
|
||||||
Simon Holesch
|
Simon Holesch
|
||||||
Simon Kerr
|
Simon Kerr
|
||||||
Skylar Downes
|
Skylar Downes
|
||||||
Srinivas Reddy Thatiparthy
|
Srinivas Reddy Thatiparthy
|
||||||
|
Stefaan Lippens
|
||||||
Stefan Farmbauer
|
Stefan Farmbauer
|
||||||
Stefan Scherfke
|
Stefan Scherfke
|
||||||
Stefan Zimmermann
|
Stefan Zimmermann
|
||||||
|
@ -370,7 +378,9 @@ Tomer Keren
|
||||||
Tony Narlock
|
Tony Narlock
|
||||||
Tor Colvin
|
Tor Colvin
|
||||||
Trevor Bekolay
|
Trevor Bekolay
|
||||||
|
Tushar Sadhwani
|
||||||
Tyler Goodlet
|
Tyler Goodlet
|
||||||
|
Tyler Smart
|
||||||
Tzu-ping Chung
|
Tzu-ping Chung
|
||||||
Vasily Kuznetsov
|
Vasily Kuznetsov
|
||||||
Victor Maryama
|
Victor Maryama
|
||||||
|
|
|
@ -50,7 +50,7 @@ Fix bugs
|
||||||
--------
|
--------
|
||||||
|
|
||||||
Look through the `GitHub issues for bugs <https://github.com/pytest-dev/pytest/labels/type:%20bug>`_.
|
Look through the `GitHub issues for bugs <https://github.com/pytest-dev/pytest/labels/type:%20bug>`_.
|
||||||
See also the `"status: easy" issues <https://github.com/pytest-dev/pytest/labels/status%3A%20easy>`_
|
See also the `"good first issue" issues <https://github.com/pytest-dev/pytest/labels/good%20first%20issue>`_
|
||||||
that are friendly to new contributors.
|
that are friendly to new contributors.
|
||||||
|
|
||||||
:ref:`Talk <contact>` to developers to find out how you can fix specific bugs. To indicate that you are going
|
:ref:`Talk <contact>` to developers to find out how you can fix specific bugs. To indicate that you are going
|
||||||
|
|
|
@ -133,14 +133,12 @@ Releasing
|
||||||
|
|
||||||
Both automatic and manual processes described above follow the same steps from this point onward.
|
Both automatic and manual processes described above follow the same steps from this point onward.
|
||||||
|
|
||||||
#. After all tests pass and the PR has been approved, tag the release commit
|
#. After all tests pass and the PR has been approved, trigger the ``deploy`` job
|
||||||
in the ``release-MAJOR.MINOR.PATCH`` branch and push it. This will publish to PyPI::
|
in https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml, using the ``release-MAJOR.MINOR.PATCH`` branch
|
||||||
|
as source.
|
||||||
|
|
||||||
git fetch upstream
|
This job will require approval from ``pytest-dev/core``, after which it will publish to PyPI
|
||||||
git tag MAJOR.MINOR.PATCH upstream/release-MAJOR.MINOR.PATCH
|
and tag the repository.
|
||||||
git push upstream MAJOR.MINOR.PATCH
|
|
||||||
|
|
||||||
Wait for the deploy to complete, then make sure it is `available on PyPI <https://pypi.org/project/pytest>`_.
|
|
||||||
|
|
||||||
#. Merge the PR. **Make sure it's not squash-merged**, so that the tagged commit ends up in the main branch.
|
#. Merge the PR. **Make sure it's not squash-merged**, so that the tagged commit ends up in the main branch.
|
||||||
|
|
||||||
|
|
|
@ -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 @@
|
||||||
Added handling of ``%f`` directive to print microseconds in log format options, such as ``log-date-format``.
|
|
|
@ -1 +0,0 @@
|
||||||
Added underlying exception to cache provider path creation and write warning messages.
|
|
|
@ -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 @@
|
||||||
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 the ``--last-failed`` whole-file skipping functionality ("skipped N files") for :ref:`non-python test files <non-python tests>`.
|
|
|
@ -1,7 +0,0 @@
|
||||||
The :confval:`norecursedir` 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 `norecursedir` 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.
|
|
|
@ -1,3 +0,0 @@
|
||||||
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.
|
|
|
@ -0,0 +1 @@
|
||||||
|
The documentation webpages now links to a canonical version to reduce outdated documentation in search engine results.
|
|
@ -1 +0,0 @@
|
||||||
Fixed traceback entries hidden with ``__tracebackhide__ = True`` still being shown for chained exceptions (parts after "... the above exception ..." message).
|
|
|
@ -1 +0,0 @@
|
||||||
Fix writing non-encodable text to log file when using ``--debug``.
|
|
|
@ -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)``.
|
|
|
@ -1 +0,0 @@
|
||||||
Improve Documentation for `caplog.set_level`.
|
|
|
@ -14,7 +14,7 @@ Each file should be named like ``<ISSUE>.<TYPE>.rst``, where
|
||||||
``<ISSUE>`` is an issue number, and ``<TYPE>`` is one of:
|
``<ISSUE>`` is an issue number, and ``<TYPE>`` is one of:
|
||||||
|
|
||||||
* ``feature``: new user facing features, like new command-line options and new behavior.
|
* ``feature``: new user facing features, like new command-line options and new behavior.
|
||||||
* ``improvement``: improvement of existing functionality, usually without requiring user intervention (for example, new fields being written in ``--junitxml``, improved colors in terminal, etc).
|
* ``improvement``: improvement of existing functionality, usually without requiring user intervention (for example, new fields being written in ``--junit-xml``, improved colors in terminal, etc).
|
||||||
* ``bugfix``: fixes a bug.
|
* ``bugfix``: fixes a bug.
|
||||||
* ``doc``: documentation improvement, like rewording an entire session or adding missing docs.
|
* ``doc``: documentation improvement, like rewording an entire session or adding missing docs.
|
||||||
* ``deprecation``: feature deprecation.
|
* ``deprecation``: feature deprecation.
|
||||||
|
|
|
@ -6,6 +6,11 @@ Release announcements
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
|
||||||
|
release-7.4.4
|
||||||
|
release-7.4.3
|
||||||
|
release-7.4.2
|
||||||
|
release-7.4.1
|
||||||
|
release-7.4.0
|
||||||
release-7.3.2
|
release-7.3.2
|
||||||
release-7.3.1
|
release-7.3.1
|
||||||
release-7.3.0
|
release-7.3.0
|
||||||
|
|
|
@ -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
|
|
@ -0,0 +1,20 @@
|
||||||
|
pytest-7.4.1
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 7.4.1 has just been released to PyPI.
|
||||||
|
|
||||||
|
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||||
|
|
||||||
|
pip install --upgrade pytest
|
||||||
|
|
||||||
|
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||||
|
|
||||||
|
Thanks to all of the contributors to this release:
|
||||||
|
|
||||||
|
* Bruno Oliveira
|
||||||
|
* Florian Bruhin
|
||||||
|
* Ran Benita
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -0,0 +1,18 @@
|
||||||
|
pytest-7.4.2
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 7.4.2 has just been released to PyPI.
|
||||||
|
|
||||||
|
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||||
|
|
||||||
|
pip install --upgrade pytest
|
||||||
|
|
||||||
|
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||||
|
|
||||||
|
Thanks to all of the contributors to this release:
|
||||||
|
|
||||||
|
* Bruno Oliveira
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -0,0 +1,19 @@
|
||||||
|
pytest-7.4.3
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 7.4.3 has just been released to PyPI.
|
||||||
|
|
||||||
|
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||||
|
|
||||||
|
pip install --upgrade pytest
|
||||||
|
|
||||||
|
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||||
|
|
||||||
|
Thanks to all of the contributors to this release:
|
||||||
|
|
||||||
|
* Bruno Oliveira
|
||||||
|
* Marc Mueller
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -0,0 +1,20 @@
|
||||||
|
pytest-7.4.4
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 7.4.4 has just been released to PyPI.
|
||||||
|
|
||||||
|
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||||
|
|
||||||
|
pip install --upgrade pytest
|
||||||
|
|
||||||
|
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||||
|
|
||||||
|
Thanks to all of the contributors to this release:
|
||||||
|
|
||||||
|
* Bruno Oliveira
|
||||||
|
* Ran Benita
|
||||||
|
* Zac Hatfield-Dodds
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -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:532
|
||||||
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)
|
||||||
|
@ -105,7 +105,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert captured.out == "hello\n"
|
assert captured.out == "hello\n"
|
||||||
|
|
||||||
doctest_namespace [session scope] -- .../_pytest/doctest.py:737
|
doctest_namespace [session scope] -- .../_pytest/doctest.py:757
|
||||||
Fixture that returns a :py:class:`dict` that will be injected into the
|
Fixture that returns a :py:class:`dict` that will be injected into the
|
||||||
namespace of doctests.
|
namespace of doctests.
|
||||||
|
|
||||||
|
@ -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::
|
||||||
|
|
|
@ -28,6 +28,180 @@ with advance notice in the **Deprecations** section of releases.
|
||||||
|
|
||||||
.. towncrier release notes start
|
.. towncrier release notes start
|
||||||
|
|
||||||
|
pytest 7.4.4 (2023-12-31)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#11140 <https://github.com/pytest-dev/pytest/issues/11140>`_: Fix non-string constants at the top of file being detected as docstrings on Python>=3.8.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11572 <https://github.com/pytest-dev/pytest/issues/11572>`_: Handle an edge case where :data:`sys.stderr` and :data:`sys.__stderr__` might already be closed when :ref:`faulthandler` is tearing down.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11710 <https://github.com/pytest-dev/pytest/issues/11710>`_: Fixed tracebacks from collection errors not getting pruned.
|
||||||
|
|
||||||
|
|
||||||
|
- `#7966 <https://github.com/pytest-dev/pytest/issues/7966>`_: Removed unhelpful error message from assertion rewrite mechanism when exceptions are raised in ``__iter__`` methods. Now they are treated un-iterable instead.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Improved Documentation
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
- `#11091 <https://github.com/pytest-dev/pytest/issues/11091>`_: Updated documentation to refer to hyphenated options: replaced ``--junitxml`` with ``--junit-xml`` and ``--collectonly`` with ``--collect-only``.
|
||||||
|
|
||||||
|
|
||||||
|
pytest 7.4.3 (2023-10-24)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#10447 <https://github.com/pytest-dev/pytest/issues/10447>`_: Markers are now considered in the reverse mro order to ensure base class markers are considered first -- this resolves a regression.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11239 <https://github.com/pytest-dev/pytest/issues/11239>`_: Fixed ``:=`` in asserts impacting unrelated test cases.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11439 <https://github.com/pytest-dev/pytest/issues/11439>`_: Handled an edge case where :data:`sys.stderr` might already be closed when :ref:`faulthandler` is tearing down.
|
||||||
|
|
||||||
|
|
||||||
|
pytest 7.4.2 (2023-09-07)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#11237 <https://github.com/pytest-dev/pytest/issues/11237>`_: Fix doctest collection of `functools.cached_property` objects.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11306 <https://github.com/pytest-dev/pytest/issues/11306>`_: Fixed bug using ``--importmode=importlib`` which would cause package ``__init__.py`` files to be imported more than once in some cases.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11367 <https://github.com/pytest-dev/pytest/issues/11367>`_: Fixed bug where `user_properties` where not being saved in the JUnit XML file if a fixture failed during teardown.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11394 <https://github.com/pytest-dev/pytest/issues/11394>`_: Fixed crash when parsing long command line arguments that might be interpreted as files.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Improved Documentation
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
- `#11391 <https://github.com/pytest-dev/pytest/issues/11391>`_: Improved disclaimer on pytest plugin reference page to better indicate this is an automated, non-curated listing.
|
||||||
|
|
||||||
|
|
||||||
|
pytest 7.4.1 (2023-09-02)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#10337 <https://github.com/pytest-dev/pytest/issues/10337>`_: Fixed bug where fake intermediate modules generated by ``--import-mode=importlib`` would not include the
|
||||||
|
child modules as attributes of the parent modules.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10702 <https://github.com/pytest-dev/pytest/issues/10702>`_: Fixed error assertion handling in :func:`pytest.approx` when ``None`` is an expected or received value when comparing dictionaries.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10811 <https://github.com/pytest-dev/pytest/issues/10811>`_: 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.
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
pytest 7.3.2 (2023-06-10)
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
|
|
|
@ -273,6 +273,9 @@ html_show_sourcelink = False
|
||||||
# Output file base name for HTML help builder.
|
# Output file base name for HTML help builder.
|
||||||
htmlhelp_basename = "pytestdoc"
|
htmlhelp_basename = "pytestdoc"
|
||||||
|
|
||||||
|
# The base URL which points to the root of the HTML documentation. It is used
|
||||||
|
# to indicate the location of document using the canonical link relation (#12363).
|
||||||
|
html_baseurl = "https://docs.pytest.org/en/stable/"
|
||||||
|
|
||||||
# -- Options for LaTeX output --------------------------------------------------
|
# -- Options for LaTeX output --------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -596,7 +596,7 @@ By using ``legacy`` you will keep using the legacy/xunit1 format when upgrading
|
||||||
pytest 6.0, where the default format will be ``xunit2``.
|
pytest 6.0, where the default format will be ``xunit2``.
|
||||||
|
|
||||||
In order to let users know about the transition, pytest will issue a warning in case
|
In order to let users know about the transition, pytest will issue a warning in case
|
||||||
the ``--junitxml`` option is given in the command line but ``junit_family`` is not explicitly
|
the ``--junit-xml`` option is given in the command line but ``junit_family`` is not explicitly
|
||||||
configured in ``pytest.ini``.
|
configured in ``pytest.ini``.
|
||||||
|
|
||||||
Services known to support the ``xunit2`` format:
|
Services known to support the ``xunit2`` format:
|
||||||
|
|
|
@ -136,7 +136,7 @@ Or select multiple nodes:
|
||||||
|
|
||||||
Node IDs for failing tests are displayed in the test summary info
|
Node IDs for failing tests are displayed in the test summary info
|
||||||
when running pytest with the ``-rf`` option. You can also
|
when running pytest with the ``-rf`` option. You can also
|
||||||
construct Node IDs from the output of ``pytest --collectonly``.
|
construct Node IDs from the output of ``pytest --collect-only``.
|
||||||
|
|
||||||
Using ``-k expr`` to select tests based on their name
|
Using ``-k expr`` to select tests based on their name
|
||||||
-------------------------------------------------------
|
-------------------------------------------------------
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -817,7 +817,7 @@ case we just write some information out to a ``failures`` file:
|
||||||
# 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"])
|
||||||
|
@ -1088,4 +1088,4 @@ application with standard ``pytest`` command-line options:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
./app_main --pytest --verbose --tb=long --junitxml=results.xml test-suite/
|
./app_main --pytest --verbose --tb=long --junit=xml=results.xml test-suite/
|
||||||
|
|
|
@ -22,7 +22,7 @@ Install ``pytest``
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ pytest --version
|
$ pytest --version
|
||||||
pytest 7.3.2
|
pytest 7.4.4
|
||||||
|
|
||||||
.. _`simpletest`:
|
.. _`simpletest`:
|
||||||
|
|
||||||
|
|
|
@ -54,14 +54,13 @@ operators. (See :ref:`tbreportdemo`). This allows you to use the
|
||||||
idiomatic python constructs without boilerplate code while not losing
|
idiomatic python constructs without boilerplate code while not losing
|
||||||
introspection information.
|
introspection information.
|
||||||
|
|
||||||
However, if you specify a message with the assertion like this:
|
If a message is specified with the assertion like this:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
assert a % 2 == 0, "value was odd, should be even"
|
assert a % 2 == 0, "value was odd, should be even"
|
||||||
|
|
||||||
then no assertion introspection takes places at all and the message
|
it is printed alongside the assertion introspection in the traceback.
|
||||||
will be simply shown in the traceback.
|
|
||||||
|
|
||||||
See :ref:`assert-details` for more information on assertion introspection.
|
See :ref:`assert-details` for more information on assertion introspection.
|
||||||
|
|
||||||
|
|
|
@ -176,14 +176,21 @@ with more recent files coming first.
|
||||||
Behavior when no tests failed in the last run
|
Behavior when no tests failed in the last run
|
||||||
---------------------------------------------
|
---------------------------------------------
|
||||||
|
|
||||||
When no tests failed in the last run, or when no cached ``lastfailed`` data was
|
The ``--lfnf/--last-failed-no-failures`` option governs the behavior of ``--last-failed``.
|
||||||
found, ``pytest`` can be configured either to run all of the tests or no tests,
|
Determines whether to execute tests when there are no previously (known)
|
||||||
using the ``--last-failed-no-failures`` option, which takes one of the following values:
|
failures or when no cached ``lastfailed`` data was found.
|
||||||
|
|
||||||
|
There are two options:
|
||||||
|
|
||||||
|
* ``all``: when there are no known test failures, runs all tests (the full test suite). This is the default.
|
||||||
|
* ``none``: when there are no known test failures, just emits a message stating this and exit successfully.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
pytest --last-failed --last-failed-no-failures all # run all tests (default behavior)
|
pytest --last-failed --last-failed-no-failures all # runs the full test suite (default behavior)
|
||||||
pytest --last-failed --last-failed-no-failures none # run no tests and exit
|
pytest --last-failed --last-failed-no-failures none # runs no tests and exits successfully
|
||||||
|
|
||||||
The new config.cache object
|
The new config.cache object
|
||||||
--------------------------------
|
--------------------------------
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -478,7 +478,7 @@ integration servers, use this invocation:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
pytest --junitxml=path
|
pytest --junit-xml=path
|
||||||
|
|
||||||
to create an XML file at ``path``.
|
to create an XML file at ``path``.
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
@ -51,8 +51,8 @@ Running this would result in a passed test except for the last
|
||||||
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
|
||||||
E assert 0
|
E 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
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
:orphan:
|
:orphan:
|
||||||
|
|
||||||
.. sidebar:: Next Open Trainings
|
.. sidebar:: Next Open Trainings and Events
|
||||||
|
|
||||||
- `pytest tips and tricks for a better testsuite <https://ep2023.europython.eu/session/pytest-tips-and-tricks-for-a-better-testsuite>`_, at `Europython 2023 <https://ep2023.europython.eu/>`_, July 18th (3h), Prague/Remote
|
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_ (3 day in-depth training):
|
||||||
- `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
|
* **June 11th to 13th 2024**, Remote
|
||||||
|
* **March 4th to 6th 2025**, Leipzig, Germany / Remote
|
||||||
|
- `pytest development sprint <https://github.com/pytest-dev/pytest/discussions/11655>`_, June 2024 (`date poll <https://nuudel.digitalcourage.de/2tEsEpRcwMNcAXVO>`_)
|
||||||
|
|
||||||
Also see :doc:`previous talks and blogposts <talks>`.
|
Also see :doc:`previous talks and blogposts <talks>`.
|
||||||
|
|
||||||
|
|
|
@ -90,7 +90,7 @@ and can also be used to hold pytest configuration if they have a ``[pytest]`` se
|
||||||
setup.cfg
|
setup.cfg
|
||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
|
|
||||||
``setup.cfg`` files are general purpose configuration files, used originally by :doc:`distutils <python:distutils/configfile>`, and can also be used to hold pytest configuration
|
``setup.cfg`` files are general purpose configuration files, used originally by ``distutils`` (now deprecated) and `setuptools <https://setuptools.pypa.io/en/latest/userguide/declarative_config.html>`__, and can also be used to hold pytest configuration
|
||||||
if they have a ``[tool:pytest]`` section.
|
if they have a ``[tool:pytest]`` section.
|
||||||
|
|
||||||
.. code-block:: ini
|
.. code-block:: ini
|
||||||
|
|
|
@ -1,14 +1,26 @@
|
||||||
|
|
||||||
.. _plugin-list:
|
.. _plugin-list:
|
||||||
|
|
||||||
Plugin List
|
Pytest Plugin List
|
||||||
===========
|
==================
|
||||||
|
|
||||||
PyPI projects that match "pytest-\*" are considered plugins and are listed
|
Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
|
||||||
automatically together with a manually-maintained list in `the source
|
It includes PyPI projects whose names begin with "pytest-" and a handful of manually selected projects.
|
||||||
code <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
|
|
||||||
Packages classified as inactive are excluded.
|
Packages classified as inactive are excluded.
|
||||||
|
|
||||||
|
For detailed insights into how this list is generated,
|
||||||
|
please refer to `the update script <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
Please be aware that this list is not a curated collection of projects
|
||||||
|
and does not undergo a systematic review process.
|
||||||
|
It serves purely as an informational resource to aid in the discovery of ``pytest`` plugins.
|
||||||
|
|
||||||
|
Do not presume any endorsement from the ``pytest`` project or its developers,
|
||||||
|
and always conduct your own quality assessment before incorporating any of these plugins into your own projects.
|
||||||
|
|
||||||
|
|
||||||
.. 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
|
||||||
page.
|
page.
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
:tocdepth: 3
|
||||||
|
|
||||||
.. _`api-reference`:
|
.. _`api-reference`:
|
||||||
|
|
||||||
API Reference
|
API Reference
|
||||||
|
@ -77,11 +79,13 @@ pytest.xfail
|
||||||
pytest.exit
|
pytest.exit
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
|
|
||||||
.. autofunction:: pytest.exit(reason, [returncode=False, msg=None])
|
.. autofunction:: pytest.exit(reason, [returncode=None, msg=None])
|
||||||
|
|
||||||
pytest.main
|
pytest.main
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
**Tutorial**: :ref:`pytest.main-usage`
|
||||||
|
|
||||||
.. autofunction:: pytest.main
|
.. autofunction:: pytest.main
|
||||||
|
|
||||||
pytest.param
|
pytest.param
|
||||||
|
@ -783,18 +787,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 +855,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,13 +910,6 @@ ExitCode
|
||||||
.. autoclass:: pytest.ExitCode
|
.. autoclass:: pytest.ExitCode
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
File
|
|
||||||
~~~~
|
|
||||||
|
|
||||||
.. autoclass:: pytest.File()
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
|
|
||||||
FixtureDef
|
FixtureDef
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
@ -852,34 +918,6 @@ FixtureDef
|
||||||
:members:
|
:members:
|
||||||
:show-inheritance:
|
: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:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
MarkDecorator
|
MarkDecorator
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -907,19 +945,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 +966,6 @@ PytestPluginManager
|
||||||
:inherited-members:
|
:inherited-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
Session
|
|
||||||
~~~~~~~
|
|
||||||
|
|
||||||
.. autoclass:: pytest.Session()
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
TestReport
|
TestReport
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -962,10 +980,10 @@ TestShortLogReport
|
||||||
.. autoclass:: pytest.TestShortLogReport()
|
.. autoclass:: pytest.TestShortLogReport()
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
_Result
|
Result
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
Result object used within :ref:`hook wrappers <hookwrapper>`, see :py:class:`_Result in the pluggy documentation <pluggy._callers._Result>` for more information.
|
Result object used within :ref:`hook wrappers <hookwrapper>`, see :py:class:`Result in the pluggy documentation <pluggy.Result>` for more information.
|
||||||
|
|
||||||
Stash
|
Stash
|
||||||
~~~~~
|
~~~~~
|
||||||
|
@ -1871,8 +1889,12 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||||
tests. Optional argument: glob (default: '*').
|
tests. Optional argument: glob (default: '*').
|
||||||
--cache-clear Remove all cache contents at start of test run
|
--cache-clear Remove all cache contents at start of test run
|
||||||
--lfnf={all,none}, --last-failed-no-failures={all,none}
|
--lfnf={all,none}, --last-failed-no-failures={all,none}
|
||||||
Which tests to run with no previously (known)
|
With ``--lf``, determines whether to execute tests
|
||||||
failures
|
when there are no previously (known) failures or
|
||||||
|
when no cached ``lastfailed`` data was found.
|
||||||
|
``all`` (the default) runs the full test suite
|
||||||
|
again. ``none`` just emits a message about no known
|
||||||
|
failures and exits successfully.
|
||||||
--sw, --stepwise Exit on test failure and continue from last failing
|
--sw, --stepwise Exit on test failure and continue from last failing
|
||||||
test next time
|
test next time
|
||||||
--sw-skip, --stepwise-skip
|
--sw-skip, --stepwise-skip
|
||||||
|
@ -1923,9 +1945,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
|
||||||
|
|
|
@ -31,10 +31,22 @@ class InvalidFeatureRelease(Exception):
|
||||||
SLUG = "pytest-dev/pytest"
|
SLUG = "pytest-dev/pytest"
|
||||||
|
|
||||||
PR_BODY = """\
|
PR_BODY = """\
|
||||||
Created automatically from manual trigger.
|
Created by the [prepare release pr]\
|
||||||
|
(https://github.com/pytest-dev/pytest/actions/workflows/prepare-release-pr.yml) workflow.
|
||||||
|
|
||||||
Once all builds pass and it has been **approved** by one or more maintainers, the build
|
Once all builds pass and it has been **approved** by one or more maintainers, start the \
|
||||||
can be released by pushing a tag `{version}` to this repository.
|
[deploy](https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml) workflow, using these parameters:
|
||||||
|
|
||||||
|
* `Use workflow from`: `release-{version}`.
|
||||||
|
* `Release version`: `{version}`.
|
||||||
|
|
||||||
|
Or execute on the command line:
|
||||||
|
|
||||||
|
```console
|
||||||
|
gh workflow run deploy.yml -r release-{version} -f version={version}
|
||||||
|
```
|
||||||
|
|
||||||
|
After the workflow has been approved by a core maintainer, the package will be uploaded to PyPI automatically.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -13,14 +13,26 @@ from tqdm import tqdm
|
||||||
FILE_HEAD = r"""
|
FILE_HEAD = r"""
|
||||||
.. _plugin-list:
|
.. _plugin-list:
|
||||||
|
|
||||||
Plugin List
|
Pytest Plugin List
|
||||||
===========
|
==================
|
||||||
|
|
||||||
PyPI projects that match "pytest-\*" are considered plugins and are listed
|
Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
|
||||||
automatically together with a manually-maintained list in `the source
|
It includes PyPI projects whose names begin with "pytest-" and a handful of manually selected projects.
|
||||||
code <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
|
|
||||||
Packages classified as inactive are excluded.
|
Packages classified as inactive are excluded.
|
||||||
|
|
||||||
|
For detailed insights into how this list is generated,
|
||||||
|
please refer to `the update script <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
Please be aware that this list is not a curated collection of projects
|
||||||
|
and does not undergo a systematic review process.
|
||||||
|
It serves purely as an informational resource to aid in the discovery of ``pytest`` plugins.
|
||||||
|
|
||||||
|
Do not presume any endorsement from the ``pytest`` project or its developers,
|
||||||
|
and always conduct your own quality assessment before incorporating any of these plugins into your own projects.
|
||||||
|
|
||||||
|
|
||||||
.. 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
|
||||||
page.
|
page.
|
||||||
|
|
|
@ -13,6 +13,7 @@ import struct
|
||||||
import sys
|
import sys
|
||||||
import tokenize
|
import tokenize
|
||||||
import types
|
import types
|
||||||
|
from collections import defaultdict
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from pathlib import PurePath
|
from pathlib import PurePath
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
@ -56,6 +57,10 @@ else:
|
||||||
astNum = ast.Num
|
astNum = ast.Num
|
||||||
|
|
||||||
|
|
||||||
|
class Sentinel:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
assertstate_key = StashKey["AssertionState"]()
|
assertstate_key = StashKey["AssertionState"]()
|
||||||
|
|
||||||
# pytest caches rewritten pycs in pycache dirs
|
# pytest caches rewritten pycs in pycache dirs
|
||||||
|
@ -63,6 +68,9 @@ PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}"
|
||||||
PYC_EXT = ".py" + (__debug__ and "c" or "o")
|
PYC_EXT = ".py" + (__debug__ and "c" or "o")
|
||||||
PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
|
PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
|
||||||
|
|
||||||
|
# Special marker that denotes we have just left a scope definition
|
||||||
|
_SCOPE_END_MARKER = Sentinel()
|
||||||
|
|
||||||
|
|
||||||
class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader):
|
class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader):
|
||||||
"""PEP302/PEP451 import hook which rewrites asserts."""
|
"""PEP302/PEP451 import hook which rewrites asserts."""
|
||||||
|
@ -596,6 +604,13 @@ def _get_assertion_exprs(src: bytes) -> Dict[int, str]:
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def _get_ast_constant_value(value: astStr) -> object:
|
||||||
|
if sys.version_info >= (3, 8):
|
||||||
|
return value.value
|
||||||
|
else:
|
||||||
|
return value.s
|
||||||
|
|
||||||
|
|
||||||
class AssertionRewriter(ast.NodeVisitor):
|
class AssertionRewriter(ast.NodeVisitor):
|
||||||
"""Assertion rewriting implementation.
|
"""Assertion rewriting implementation.
|
||||||
|
|
||||||
|
@ -645,6 +660,8 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
.push_format_context() and .pop_format_context() which allows
|
.push_format_context() and .pop_format_context() which allows
|
||||||
to build another %-formatted string while already building one.
|
to build another %-formatted string while already building one.
|
||||||
|
|
||||||
|
:scope: A tuple containing the current scope used for variables_overwrite.
|
||||||
|
|
||||||
:variables_overwrite: A dict filled with references to variables
|
:variables_overwrite: A dict filled with references to variables
|
||||||
that change value within an assert. This happens when a variable is
|
that change value within an assert. This happens when a variable is
|
||||||
reassigned with the walrus operator
|
reassigned with the walrus operator
|
||||||
|
@ -666,7 +683,10 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
else:
|
else:
|
||||||
self.enable_assertion_pass_hook = False
|
self.enable_assertion_pass_hook = False
|
||||||
self.source = source
|
self.source = source
|
||||||
self.variables_overwrite: Dict[str, str] = {}
|
self.scope: tuple[ast.AST, ...] = ()
|
||||||
|
self.variables_overwrite: defaultdict[
|
||||||
|
tuple[ast.AST, ...], Dict[str, str]
|
||||||
|
] = defaultdict(dict)
|
||||||
|
|
||||||
def run(self, mod: ast.Module) -> None:
|
def run(self, mod: ast.Module) -> None:
|
||||||
"""Find all assert statements in *mod* and rewrite them."""
|
"""Find all assert statements in *mod* and rewrite them."""
|
||||||
|
@ -687,11 +707,10 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
expect_docstring
|
expect_docstring
|
||||||
and isinstance(item, ast.Expr)
|
and isinstance(item, ast.Expr)
|
||||||
and isinstance(item.value, astStr)
|
and isinstance(item.value, astStr)
|
||||||
|
and isinstance(_get_ast_constant_value(item.value), str)
|
||||||
):
|
):
|
||||||
if sys.version_info >= (3, 8):
|
doc = _get_ast_constant_value(item.value)
|
||||||
doc = item.value.value
|
assert isinstance(doc, str)
|
||||||
else:
|
|
||||||
doc = item.value.s
|
|
||||||
if self.is_rewrite_disabled(doc):
|
if self.is_rewrite_disabled(doc):
|
||||||
return
|
return
|
||||||
expect_docstring = False
|
expect_docstring = False
|
||||||
|
@ -732,9 +751,17 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
mod.body[pos:pos] = imports
|
mod.body[pos:pos] = imports
|
||||||
|
|
||||||
# Collect asserts.
|
# Collect asserts.
|
||||||
nodes: List[ast.AST] = [mod]
|
self.scope = (mod,)
|
||||||
|
nodes: List[Union[ast.AST, Sentinel]] = [mod]
|
||||||
while nodes:
|
while nodes:
|
||||||
node = nodes.pop()
|
node = nodes.pop()
|
||||||
|
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
||||||
|
self.scope = tuple((*self.scope, node))
|
||||||
|
nodes.append(_SCOPE_END_MARKER)
|
||||||
|
if node == _SCOPE_END_MARKER:
|
||||||
|
self.scope = self.scope[:-1]
|
||||||
|
continue
|
||||||
|
assert isinstance(node, ast.AST)
|
||||||
for name, field in ast.iter_fields(node):
|
for name, field in ast.iter_fields(node):
|
||||||
if isinstance(field, list):
|
if isinstance(field, list):
|
||||||
new: List[ast.AST] = []
|
new: List[ast.AST] = []
|
||||||
|
@ -1005,7 +1032,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
]
|
]
|
||||||
):
|
):
|
||||||
pytest_temp = self.variable()
|
pytest_temp = self.variable()
|
||||||
self.variables_overwrite[
|
self.variables_overwrite[self.scope][
|
||||||
v.left.target.id
|
v.left.target.id
|
||||||
] = v.left # type:ignore[assignment]
|
] = v.left # type:ignore[assignment]
|
||||||
v.left.target.id = pytest_temp
|
v.left.target.id = pytest_temp
|
||||||
|
@ -1048,17 +1075,20 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
new_args = []
|
new_args = []
|
||||||
new_kwargs = []
|
new_kwargs = []
|
||||||
for arg in call.args:
|
for arg in call.args:
|
||||||
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite:
|
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get(
|
||||||
arg = self.variables_overwrite[arg.id] # type:ignore[assignment]
|
self.scope, {}
|
||||||
|
):
|
||||||
|
arg = self.variables_overwrite[self.scope][
|
||||||
|
arg.id
|
||||||
|
] # type:ignore[assignment]
|
||||||
res, expl = self.visit(arg)
|
res, expl = self.visit(arg)
|
||||||
arg_expls.append(expl)
|
arg_expls.append(expl)
|
||||||
new_args.append(res)
|
new_args.append(res)
|
||||||
for keyword in call.keywords:
|
for keyword in call.keywords:
|
||||||
if (
|
if isinstance(
|
||||||
isinstance(keyword.value, ast.Name)
|
keyword.value, ast.Name
|
||||||
and keyword.value.id in self.variables_overwrite
|
) and keyword.value.id in self.variables_overwrite.get(self.scope, {}):
|
||||||
):
|
keyword.value = self.variables_overwrite[self.scope][
|
||||||
keyword.value = self.variables_overwrite[
|
|
||||||
keyword.value.id
|
keyword.value.id
|
||||||
] # type:ignore[assignment]
|
] # type:ignore[assignment]
|
||||||
res, expl = self.visit(keyword.value)
|
res, expl = self.visit(keyword.value)
|
||||||
|
@ -1094,12 +1124,14 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]:
|
def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]:
|
||||||
self.push_format_context()
|
self.push_format_context()
|
||||||
# We first check if we have overwritten a variable in the previous assert
|
# We first check if we have overwritten a variable in the previous assert
|
||||||
if isinstance(comp.left, ast.Name) and comp.left.id in self.variables_overwrite:
|
if isinstance(
|
||||||
comp.left = self.variables_overwrite[
|
comp.left, ast.Name
|
||||||
|
) and comp.left.id in self.variables_overwrite.get(self.scope, {}):
|
||||||
|
comp.left = self.variables_overwrite[self.scope][
|
||||||
comp.left.id
|
comp.left.id
|
||||||
] # type:ignore[assignment]
|
] # type:ignore[assignment]
|
||||||
if isinstance(comp.left, namedExpr):
|
if isinstance(comp.left, namedExpr):
|
||||||
self.variables_overwrite[
|
self.variables_overwrite[self.scope][
|
||||||
comp.left.target.id
|
comp.left.target.id
|
||||||
] = comp.left # type:ignore[assignment]
|
] = comp.left # type:ignore[assignment]
|
||||||
left_res, left_expl = self.visit(comp.left)
|
left_res, left_expl = self.visit(comp.left)
|
||||||
|
@ -1119,7 +1151,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
and next_operand.target.id == left_res.id
|
and next_operand.target.id == left_res.id
|
||||||
):
|
):
|
||||||
next_operand.target.id = self.variable()
|
next_operand.target.id = self.variable()
|
||||||
self.variables_overwrite[
|
self.variables_overwrite[self.scope][
|
||||||
left_res.id
|
left_res.id
|
||||||
] = next_operand # type:ignore[assignment]
|
] = next_operand # type:ignore[assignment]
|
||||||
next_res, next_expl = self.visit(next_operand)
|
next_res, next_expl = self.visit(next_operand)
|
||||||
|
|
|
@ -132,7 +132,7 @@ def isiterable(obj: Any) -> bool:
|
||||||
try:
|
try:
|
||||||
iter(obj)
|
iter(obj)
|
||||||
return not istext(obj)
|
return not istext(obj)
|
||||||
except TypeError:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -505,7 +505,11 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
dest="last_failed_no_failures",
|
dest="last_failed_no_failures",
|
||||||
choices=("all", "none"),
|
choices=("all", "none"),
|
||||||
default="all",
|
default="all",
|
||||||
help="Which tests to run with no previously (known) failures",
|
help="With ``--lf``, determines whether to execute tests when there "
|
||||||
|
"are no previously (known) failures or when no "
|
||||||
|
"cached ``lastfailed`` data was found. "
|
||||||
|
"``all`` (the default) runs the full test suite again. "
|
||||||
|
"``none`` just emits a message about no known failures and exits successfully.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -380,15 +380,24 @@ else:
|
||||||
|
|
||||||
|
|
||||||
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 process's real user id or None if it could not be
|
||||||
# win32 does not have a getuid() function.
|
determined.
|
||||||
# On Emscripten, getuid() is a stub that always returns 0.
|
|
||||||
if sys.platform in ("win32", "emscripten"):
|
:return: The user id or None if it could not be determined.
|
||||||
|
"""
|
||||||
|
# mypy follows the version and platform checking expectation of PEP 484:
|
||||||
|
# https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks
|
||||||
|
# Containment checks are too complex for mypy v1.5.0 and cause failure.
|
||||||
|
if sys.platform == "win32" or sys.platform == "emscripten":
|
||||||
|
# win32 does not have a getuid() function.
|
||||||
|
# Emscripten has a return 0 stub.
|
||||||
return None
|
return None
|
||||||
# getuid shouldn't fail, but cpython defines such a case.
|
else:
|
||||||
# Let's hope for the best.
|
# On other platforms, a return value of -1 is assumed to indicate that
|
||||||
uid = os.getuid()
|
# the current process's real user id could not be determined.
|
||||||
return uid if uid != -1 else None
|
ERROR = -1
|
||||||
|
uid = os.getuid()
|
||||||
|
return uid if uid != ERROR else None
|
||||||
|
|
||||||
|
|
||||||
# Perform exhaustiveness checking.
|
# Perform exhaustiveness checking.
|
||||||
|
|
|
@ -57,6 +57,7 @@ from _pytest.pathlib import bestrelpath
|
||||||
from _pytest.pathlib import import_path
|
from _pytest.pathlib import import_path
|
||||||
from _pytest.pathlib import ImportMode
|
from _pytest.pathlib import ImportMode
|
||||||
from _pytest.pathlib import resolve_package_path
|
from _pytest.pathlib import resolve_package_path
|
||||||
|
from _pytest.pathlib import safe_exists
|
||||||
from _pytest.stash import Stash
|
from _pytest.stash import Stash
|
||||||
from _pytest.warning_types import PytestConfigWarning
|
from _pytest.warning_types import PytestConfigWarning
|
||||||
from _pytest.warning_types import warn_explicit_for
|
from _pytest.warning_types import warn_explicit_for
|
||||||
|
@ -137,7 +138,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.
|
||||||
|
@ -442,10 +445,10 @@ class PytestPluginManager(PluginManager):
|
||||||
# so we avoid accessing possibly non-readable attributes
|
# so we avoid accessing possibly non-readable attributes
|
||||||
# (see issue #1073).
|
# (see issue #1073).
|
||||||
if not name.startswith("pytest_"):
|
if not name.startswith("pytest_"):
|
||||||
return
|
return None
|
||||||
# Ignore names which can not be hooks.
|
# Ignore names which can not be hooks.
|
||||||
if name == "pytest_plugins":
|
if name == "pytest_plugins":
|
||||||
return
|
return None
|
||||||
|
|
||||||
opts = super().parse_hookimpl_opts(plugin, name)
|
opts = super().parse_hookimpl_opts(plugin, name)
|
||||||
if opts is not None:
|
if opts is not None:
|
||||||
|
@ -454,9 +457,9 @@ class PytestPluginManager(PluginManager):
|
||||||
method = getattr(plugin, name)
|
method = getattr(plugin, name)
|
||||||
# Consider only actual functions for hooks (#3775).
|
# Consider only actual functions for hooks (#3775).
|
||||||
if not inspect.isroutine(method):
|
if not inspect.isroutine(method):
|
||||||
return
|
return None
|
||||||
# Collect unmarked hooks as long as they have the `pytest_' prefix.
|
# Collect unmarked hooks as long as they have the `pytest_' prefix.
|
||||||
return _get_legacy_hook_marks(
|
return _get_legacy_hook_marks( # type: ignore[return-value]
|
||||||
method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper")
|
method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -465,7 +468,7 @@ class PytestPluginManager(PluginManager):
|
||||||
if opts is None:
|
if opts is None:
|
||||||
method = getattr(module_or_class, name)
|
method = getattr(module_or_class, name)
|
||||||
if name.startswith("pytest_"):
|
if name.startswith("pytest_"):
|
||||||
opts = _get_legacy_hook_marks(
|
opts = _get_legacy_hook_marks( # type: ignore[assignment]
|
||||||
method,
|
method,
|
||||||
"spec",
|
"spec",
|
||||||
("firstresult", "historic"),
|
("firstresult", "historic"),
|
||||||
|
@ -555,12 +558,8 @@ class PytestPluginManager(PluginManager):
|
||||||
anchor = absolutepath(current / path)
|
anchor = absolutepath(current / path)
|
||||||
|
|
||||||
# Ensure we do not break if what appears to be an anchor
|
# Ensure we do not break if what appears to be an anchor
|
||||||
# is in fact a very long option (#10169).
|
# is in fact a very long option (#10169, #11394).
|
||||||
try:
|
if safe_exists(anchor):
|
||||||
anchor_exists = anchor.exists()
|
|
||||||
except OSError: # pragma: no cover
|
|
||||||
anchor_exists = False
|
|
||||||
if anchor_exists:
|
|
||||||
self._try_load_conftest(anchor, importmode, rootpath)
|
self._try_load_conftest(anchor, importmode, rootpath)
|
||||||
foundanchor = True
|
foundanchor = True
|
||||||
if not foundanchor:
|
if not foundanchor:
|
||||||
|
@ -1063,9 +1062,10 @@ class Config:
|
||||||
fin()
|
fin()
|
||||||
|
|
||||||
def get_terminal_writer(self) -> TerminalWriter:
|
def get_terminal_writer(self) -> TerminalWriter:
|
||||||
terminalreporter: TerminalReporter = self.pluginmanager.get_plugin(
|
terminalreporter: Optional[TerminalReporter] = self.pluginmanager.get_plugin(
|
||||||
"terminalreporter"
|
"terminalreporter"
|
||||||
)
|
)
|
||||||
|
assert terminalreporter is not None
|
||||||
return terminalreporter._tw
|
return terminalreporter._tw
|
||||||
|
|
||||||
def pytest_cmdline_parse(
|
def pytest_cmdline_parse(
|
||||||
|
|
|
@ -16,6 +16,7 @@ from .exceptions import UsageError
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
from _pytest.pathlib import absolutepath
|
from _pytest.pathlib import absolutepath
|
||||||
from _pytest.pathlib import commonpath
|
from _pytest.pathlib import commonpath
|
||||||
|
from _pytest.pathlib import safe_exists
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from . import Config
|
from . import Config
|
||||||
|
@ -151,14 +152,6 @@ def get_dirs_from_args(args: Iterable[str]) -> List[Path]:
|
||||||
return path
|
return path
|
||||||
return path.parent
|
return path.parent
|
||||||
|
|
||||||
def safe_exists(path: Path) -> bool:
|
|
||||||
# This can throw on paths that contain characters unrepresentable at the OS level,
|
|
||||||
# or with invalid syntax on Windows (https://bugs.python.org/issue35306)
|
|
||||||
try:
|
|
||||||
return path.exists()
|
|
||||||
except OSError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# These look like paths but may not exist
|
# These look like paths but may not exist
|
||||||
possible_paths = (
|
possible_paths = (
|
||||||
absolutepath(get_file_part_from_node_id(arg))
|
absolutepath(get_file_part_from_node_id(arg))
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
"""Discover and run doctests in modules and test files."""
|
"""Discover and run doctests in modules and test files."""
|
||||||
import bdb
|
import bdb
|
||||||
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
@ -536,6 +537,25 @@ class DoctestModule(Module):
|
||||||
tests, obj, name, module, source_lines, globs, seen
|
tests, obj, name, module, source_lines, globs, seen
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if sys.version_info < (3, 13):
|
||||||
|
|
||||||
|
def _from_module(self, module, object):
|
||||||
|
"""`cached_property` objects are never considered a part
|
||||||
|
of the 'current module'. As such they are skipped by doctest.
|
||||||
|
Here we override `_from_module` to check the underlying
|
||||||
|
function instead. https://github.com/python/cpython/issues/107995
|
||||||
|
"""
|
||||||
|
if hasattr(functools, "cached_property") and isinstance(
|
||||||
|
object, functools.cached_property
|
||||||
|
):
|
||||||
|
object = object.func
|
||||||
|
|
||||||
|
# Type ignored because this is a private function.
|
||||||
|
return super()._from_module(module, object) # type: ignore[misc]
|
||||||
|
|
||||||
|
else: # pragma: no cover
|
||||||
|
pass
|
||||||
|
|
||||||
if self.path.name == "conftest.py":
|
if self.path.name == "conftest.py":
|
||||||
module = self.config.pluginmanager._importconftest(
|
module = self.config.pluginmanager._importconftest(
|
||||||
self.path,
|
self.path,
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import io
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
|
@ -10,8 +9,8 @@ from _pytest.nodes import Item
|
||||||
from _pytest.stash import StashKey
|
from _pytest.stash import StashKey
|
||||||
|
|
||||||
|
|
||||||
|
fault_handler_original_stderr_fd_key = StashKey[int]()
|
||||||
fault_handler_stderr_fd_key = StashKey[int]()
|
fault_handler_stderr_fd_key = StashKey[int]()
|
||||||
fault_handler_originally_enabled_key = StashKey[bool]()
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser: Parser) -> None:
|
def pytest_addoption(parser: Parser) -> None:
|
||||||
|
@ -25,8 +24,15 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
def pytest_configure(config: Config) -> None:
|
def pytest_configure(config: Config) -> None:
|
||||||
import faulthandler
|
import faulthandler
|
||||||
|
|
||||||
config.stash[fault_handler_stderr_fd_key] = os.dup(get_stderr_fileno())
|
# at teardown we want to restore the original faulthandler fileno
|
||||||
config.stash[fault_handler_originally_enabled_key] = faulthandler.is_enabled()
|
# but faulthandler has no api to return the original fileno
|
||||||
|
# so here we stash the stderr fileno to be used at teardown
|
||||||
|
# sys.stderr and sys.__stderr__ may be closed or patched during the session
|
||||||
|
# so we can't rely on their values being good at that point (#11572).
|
||||||
|
stderr_fileno = get_stderr_fileno()
|
||||||
|
if faulthandler.is_enabled():
|
||||||
|
config.stash[fault_handler_original_stderr_fd_key] = stderr_fileno
|
||||||
|
config.stash[fault_handler_stderr_fd_key] = os.dup(stderr_fileno)
|
||||||
faulthandler.enable(file=config.stash[fault_handler_stderr_fd_key])
|
faulthandler.enable(file=config.stash[fault_handler_stderr_fd_key])
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,9 +44,10 @@ def pytest_unconfigure(config: Config) -> None:
|
||||||
if fault_handler_stderr_fd_key in config.stash:
|
if fault_handler_stderr_fd_key in config.stash:
|
||||||
os.close(config.stash[fault_handler_stderr_fd_key])
|
os.close(config.stash[fault_handler_stderr_fd_key])
|
||||||
del config.stash[fault_handler_stderr_fd_key]
|
del config.stash[fault_handler_stderr_fd_key]
|
||||||
if config.stash.get(fault_handler_originally_enabled_key, False):
|
# Re-enable the faulthandler if it was originally enabled.
|
||||||
# Re-enable the faulthandler if it was originally enabled.
|
if fault_handler_original_stderr_fd_key in config.stash:
|
||||||
faulthandler.enable(file=get_stderr_fileno())
|
faulthandler.enable(config.stash[fault_handler_original_stderr_fd_key])
|
||||||
|
del config.stash[fault_handler_original_stderr_fd_key]
|
||||||
|
|
||||||
|
|
||||||
def get_stderr_fileno() -> int:
|
def get_stderr_fileno() -> int:
|
||||||
|
@ -51,7 +58,7 @@ def get_stderr_fileno() -> int:
|
||||||
if fileno == -1:
|
if fileno == -1:
|
||||||
raise AttributeError()
|
raise AttributeError()
|
||||||
return fileno
|
return fileno
|
||||||
except (AttributeError, io.UnsupportedOperation):
|
except (AttributeError, ValueError):
|
||||||
# pytest-xdist monkeypatches sys.stderr with an object that is not an actual file.
|
# pytest-xdist monkeypatches sys.stderr with an object that is not an actual file.
|
||||||
# https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors
|
# https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors
|
||||||
# This is potentially dangerous, but the best we can do.
|
# This is potentially dangerous, but the best we can do.
|
||||||
|
|
|
@ -11,6 +11,7 @@ from _pytest.config import Config
|
||||||
from _pytest.config import ExitCode
|
from _pytest.config import ExitCode
|
||||||
from _pytest.config import PrintHelp
|
from _pytest.config import PrintHelp
|
||||||
from _pytest.config.argparsing import Parser
|
from _pytest.config.argparsing import Parser
|
||||||
|
from _pytest.terminal import TerminalReporter
|
||||||
|
|
||||||
|
|
||||||
class HelpAction(Action):
|
class HelpAction(Action):
|
||||||
|
@ -159,7 +160,10 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
|
||||||
def showhelp(config: Config) -> None:
|
def showhelp(config: Config) -> None:
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
reporter = config.pluginmanager.get_plugin("terminalreporter")
|
reporter: Optional[TerminalReporter] = config.pluginmanager.get_plugin(
|
||||||
|
"terminalreporter"
|
||||||
|
)
|
||||||
|
assert reporter is not None
|
||||||
tw = reporter._tw
|
tw = reporter._tw
|
||||||
tw.write(config._parser.optparser.format_help())
|
tw.write(config._parser.optparser.format_help())
|
||||||
tw.line()
|
tw.line()
|
||||||
|
|
|
@ -369,7 +369,7 @@ def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object]
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
|
|
||||||
def record_func(name: str, value: object) -> None:
|
def record_func(name: str, value: object) -> None:
|
||||||
"""No-op function in case --junitxml was not passed in the command-line."""
|
"""No-op function in case --junit-xml was not passed in the command-line."""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
_check_record_param_type("name", name)
|
_check_record_param_type("name", name)
|
||||||
|
|
||||||
|
@ -502,6 +502,10 @@ class LogXML:
|
||||||
# Local hack to handle xdist report order.
|
# Local hack to handle xdist report order.
|
||||||
workernode = getattr(report, "node", None)
|
workernode = getattr(report, "node", None)
|
||||||
reporter = self.node_reporters.pop((nodeid, workernode))
|
reporter = self.node_reporters.pop((nodeid, workernode))
|
||||||
|
|
||||||
|
for propname, propvalue in report.user_properties:
|
||||||
|
reporter.add_property(propname, str(propvalue))
|
||||||
|
|
||||||
if reporter is not None:
|
if reporter is not None:
|
||||||
reporter.finalize()
|
reporter.finalize()
|
||||||
|
|
||||||
|
@ -599,9 +603,6 @@ class LogXML:
|
||||||
reporter = self._opentestcase(report)
|
reporter = self._opentestcase(report)
|
||||||
reporter.write_captured_output(report)
|
reporter.write_captured_output(report)
|
||||||
|
|
||||||
for propname, propvalue in report.user_properties:
|
|
||||||
reporter.add_property(propname, str(propvalue))
|
|
||||||
|
|
||||||
self.finalize(report)
|
self.finalize(report)
|
||||||
report_wid = getattr(report, "worker_id", None)
|
report_wid = getattr(report, "worker_id", None)
|
||||||
report_ii = getattr(report, "item_index", None)
|
report_ii = getattr(report, "item_index", None)
|
||||||
|
|
|
@ -660,6 +660,8 @@ class LoggingPlugin:
|
||||||
)
|
)
|
||||||
if self._log_cli_enabled():
|
if self._log_cli_enabled():
|
||||||
terminal_reporter = config.pluginmanager.get_plugin("terminalreporter")
|
terminal_reporter = config.pluginmanager.get_plugin("terminalreporter")
|
||||||
|
# Guaranteed by `_log_cli_enabled()`.
|
||||||
|
assert terminal_reporter is not None
|
||||||
capture_manager = config.pluginmanager.get_plugin("capturemanager")
|
capture_manager = config.pluginmanager.get_plugin("capturemanager")
|
||||||
# if capturemanager plugin is disabled, live logging still works.
|
# if capturemanager plugin is disabled, live logging still works.
|
||||||
self.log_cli_handler: Union[
|
self.log_cli_handler: Union[
|
||||||
|
|
|
@ -36,6 +36,7 @@ from _pytest.outcomes import exit
|
||||||
from _pytest.pathlib import absolutepath
|
from _pytest.pathlib import absolutepath
|
||||||
from _pytest.pathlib import bestrelpath
|
from _pytest.pathlib import bestrelpath
|
||||||
from _pytest.pathlib import fnmatch_ex
|
from _pytest.pathlib import fnmatch_ex
|
||||||
|
from _pytest.pathlib import safe_exists
|
||||||
from _pytest.pathlib import visit
|
from _pytest.pathlib import visit
|
||||||
from _pytest.reports import CollectReport
|
from _pytest.reports import CollectReport
|
||||||
from _pytest.reports import TestReport
|
from _pytest.reports import TestReport
|
||||||
|
@ -462,6 +463,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.
|
||||||
|
@ -890,7 +896,7 @@ def resolve_collection_argument(
|
||||||
strpath = search_pypath(strpath)
|
strpath = search_pypath(strpath)
|
||||||
fspath = invocation_path / strpath
|
fspath = invocation_path / strpath
|
||||||
fspath = absolutepath(fspath)
|
fspath = absolutepath(fspath)
|
||||||
if not fspath.exists():
|
if not safe_exists(fspath):
|
||||||
msg = (
|
msg = (
|
||||||
"module or package not found: {arg} (missing __init__.py?)"
|
"module or package not found: {arg} (missing __init__.py?)"
|
||||||
if as_pypath
|
if as_pypath
|
||||||
|
|
|
@ -373,7 +373,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):
|
||||||
|
|
|
@ -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.
|
||||||
|
@ -564,7 +567,7 @@ class Collector(Node):
|
||||||
ntraceback = traceback.cut(path=self.path)
|
ntraceback = traceback.cut(path=self.path)
|
||||||
if ntraceback == traceback:
|
if ntraceback == traceback:
|
||||||
ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
|
ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
|
||||||
return excinfo.traceback.filter(excinfo)
|
return ntraceback.filter(excinfo)
|
||||||
return excinfo.traceback
|
return excinfo.traceback
|
||||||
|
|
||||||
|
|
||||||
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -123,7 +123,7 @@ def exit(
|
||||||
only because `msg` is deprecated.
|
only because `msg` is deprecated.
|
||||||
|
|
||||||
:param returncode:
|
:param returncode:
|
||||||
Return code to be used when exiting pytest.
|
Return code to be used when exiting pytest. None means the same as ``0`` (no error), same as :func:`sys.exit`.
|
||||||
|
|
||||||
:param msg:
|
:param msg:
|
||||||
Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
|
Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
|
||||||
|
|
|
@ -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)])
|
||||||
|
@ -621,6 +623,11 @@ def module_name_from_path(path: Path, root: Path) -> str:
|
||||||
# Use the parts for the relative path to the root path.
|
# Use the parts for the relative path to the root path.
|
||||||
path_parts = relative_path.parts
|
path_parts = relative_path.parts
|
||||||
|
|
||||||
|
# Module name for packages do not contain the __init__ file, unless
|
||||||
|
# the `__init__.py` file is at the root.
|
||||||
|
if len(path_parts) >= 2 and path_parts[-1] == "__init__":
|
||||||
|
path_parts = path_parts[:-1]
|
||||||
|
|
||||||
return ".".join(path_parts)
|
return ".".join(path_parts)
|
||||||
|
|
||||||
|
|
||||||
|
@ -633,6 +640,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 +652,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)
|
||||||
|
|
||||||
|
@ -773,3 +792,13 @@ def copytree(source: Path, target: Path) -> None:
|
||||||
shutil.copyfile(x, newx)
|
shutil.copyfile(x, newx)
|
||||||
elif x.is_dir():
|
elif x.is_dir():
|
||||||
newx.mkdir(exist_ok=True)
|
newx.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def safe_exists(p: Path) -> bool:
|
||||||
|
"""Like Path.exists(), but account for input arguments that might be too long (#11394)."""
|
||||||
|
try:
|
||||||
|
return p.exists()
|
||||||
|
except (ValueError, OSError):
|
||||||
|
# ValueError: stat: path too long for Windows
|
||||||
|
# OSError: [WinError 123] The filename, directory name, or volume label syntax is incorrect
|
||||||
|
return False
|
||||||
|
|
|
@ -752,7 +752,7 @@ class Pytester:
|
||||||
|
|
||||||
def make_hook_recorder(self, pluginmanager: PytestPluginManager) -> HookRecorder:
|
def make_hook_recorder(self, pluginmanager: PytestPluginManager) -> HookRecorder:
|
||||||
"""Create a new :class:`HookRecorder` for a :class:`PytestPluginManager`."""
|
"""Create a new :class:`HookRecorder` for a :class:`PytestPluginManager`."""
|
||||||
pluginmanager.reprec = reprec = HookRecorder(pluginmanager, _ispytest=True)
|
pluginmanager.reprec = reprec = HookRecorder(pluginmanager, _ispytest=True) # type: ignore[attr-defined]
|
||||||
self._request.addfinalizer(reprec.finish_recording)
|
self._request.addfinalizer(reprec.finish_recording)
|
||||||
return reprec
|
return reprec
|
||||||
|
|
||||||
|
@ -1074,7 +1074,7 @@ class Pytester:
|
||||||
return self.inline_run(*values)
|
return self.inline_run(*values)
|
||||||
|
|
||||||
def inline_genitems(self, *args) -> Tuple[List[Item], HookRecorder]:
|
def inline_genitems(self, *args) -> Tuple[List[Item], HookRecorder]:
|
||||||
"""Run ``pytest.main(['--collectonly'])`` in-process.
|
"""Run ``pytest.main(['--collect-only'])`` in-process.
|
||||||
|
|
||||||
Runs the :py:func:`pytest.main` function to run all of pytest inside
|
Runs the :py:func:`pytest.main` function to run all of pytest inside
|
||||||
the test process itself like :py:meth:`inline_run`, but returns a
|
the test process itself like :py:meth:`inline_run`, but returns a
|
||||||
|
|
|
@ -522,7 +522,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 +659,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],
|
||||||
|
@ -788,7 +791,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):
|
||||||
|
@ -1149,7 +1152,7 @@ class CallSpec2:
|
||||||
arg2scope = self._arg2scope.copy()
|
arg2scope = self._arg2scope.copy()
|
||||||
for arg, val in zip(argnames, valset):
|
for arg, val in zip(argnames, valset):
|
||||||
if arg in params or arg in funcargs:
|
if arg in params or arg in funcargs:
|
||||||
raise ValueError(f"duplicate {arg!r}")
|
raise ValueError(f"duplicate parametrization of {arg!r}")
|
||||||
valtype_for_arg = valtypes[arg]
|
valtype_for_arg = valtypes[arg]
|
||||||
if valtype_for_arg == "params":
|
if valtype_for_arg == "params":
|
||||||
params[arg] = val
|
params[arg] = val
|
||||||
|
@ -1240,8 +1243,9 @@ class Metafunc:
|
||||||
during the collection phase. If you need to setup expensive resources
|
during the collection phase. If you need to setup expensive resources
|
||||||
see about setting indirect to do it rather than at test setup time.
|
see about setting indirect to do it rather than at test setup time.
|
||||||
|
|
||||||
Can be called multiple times, in which case each call parametrizes all
|
Can be called multiple times per test function (but only on different
|
||||||
previous parametrizations, e.g.
|
argument names), in which case each call parametrizes all previous
|
||||||
|
parametrizations, e.g.
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
|
@ -1673,7 +1677,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
|
||||||
|
@ -1830,10 +1834,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")
|
||||||
|
|
|
@ -266,19 +266,20 @@ 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:
|
||||||
max_abs_diff = max(
|
if approx_value.expected is not None and other_value is not None:
|
||||||
max_abs_diff, abs(approx_value.expected - other_value)
|
max_abs_diff = max(
|
||||||
)
|
max_abs_diff, abs(approx_value.expected - other_value)
|
||||||
if approx_value.expected == 0.0:
|
|
||||||
max_rel_diff = math.inf
|
|
||||||
else:
|
|
||||||
max_rel_diff = max(
|
|
||||||
max_rel_diff,
|
|
||||||
abs(
|
|
||||||
(approx_value.expected - other_value)
|
|
||||||
/ approx_value.expected
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
if approx_value.expected == 0.0:
|
||||||
|
max_rel_diff = math.inf
|
||||||
|
else:
|
||||||
|
max_rel_diff = max(
|
||||||
|
max_rel_diff,
|
||||||
|
abs(
|
||||||
|
(approx_value.expected - other_value)
|
||||||
|
/ approx_value.expected
|
||||||
|
),
|
||||||
|
)
|
||||||
different_ids.append(approx_key)
|
different_ids.append(approx_key)
|
||||||
|
|
||||||
message_data = [
|
message_data = [
|
||||||
|
|
|
@ -868,6 +868,9 @@ class TestLocalPath(CommonFSTests):
|
||||||
py_path.strpath, str_path
|
py_path.strpath, str_path
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@pytest.mark.xfail(
|
||||||
|
reason="#11603", raises=(error.EEXIST, error.ENOENT), strict=False
|
||||||
|
)
|
||||||
def test_make_numbered_dir_multiprocess_safe(self, tmpdir):
|
def test_make_numbered_dir_multiprocess_safe(self, tmpdir):
|
||||||
# https://github.com/pytest-dev/py/issues/30
|
# https://github.com/pytest-dev/py/issues/30
|
||||||
with multiprocessing.Pool() as pool:
|
with multiprocessing.Pool() as pool:
|
||||||
|
|
|
@ -1317,3 +1317,38 @@ def test_function_return_non_none_warning(pytester: Pytester) -> None:
|
||||||
)
|
)
|
||||||
res = pytester.runpytest()
|
res = pytester.runpytest()
|
||||||
res.stdout.fnmatch_lines(["*Did you mean to use `assert` instead of `return`?*"])
|
res.stdout.fnmatch_lines(["*Did you mean to use `assert` instead of `return`?*"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_doctest_and_normal_imports_with_importlib(pytester: Pytester) -> None:
|
||||||
|
"""
|
||||||
|
Regression test for #10811: previously import_path with ImportMode.importlib would
|
||||||
|
not return a module if already in sys.modules, resulting in modules being imported
|
||||||
|
multiple times, which causes problems with modules that have import side effects.
|
||||||
|
"""
|
||||||
|
# Uses the exact reproducer form #10811, given it is very minimal
|
||||||
|
# and illustrates the problem well.
|
||||||
|
pytester.makepyfile(
|
||||||
|
**{
|
||||||
|
"pmxbot/commands.py": "from . import logging",
|
||||||
|
"pmxbot/logging.py": "",
|
||||||
|
"tests/__init__.py": "",
|
||||||
|
"tests/test_commands.py": """
|
||||||
|
import importlib
|
||||||
|
from pmxbot import logging
|
||||||
|
|
||||||
|
class TestCommands:
|
||||||
|
def test_boo(self):
|
||||||
|
assert importlib.import_module('pmxbot.logging') is logging
|
||||||
|
""",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
pytester.makeini(
|
||||||
|
"""
|
||||||
|
[pytest]
|
||||||
|
addopts=
|
||||||
|
--doctest-modules
|
||||||
|
--import-mode importlib
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest_subprocess()
|
||||||
|
result.stdout.fnmatch_lines("*1 passed*")
|
||||||
|
|
|
@ -21,6 +21,15 @@ if sys.gettrace():
|
||||||
sys.settrace(orig_trace)
|
sys.settrace(orig_trace)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def set_column_width(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""
|
||||||
|
Force terminal width to 80: some tests check the formatting of --help, which is sensible
|
||||||
|
to terminal width.
|
||||||
|
"""
|
||||||
|
monkeypatch.setenv("COLUMNS", "80")
|
||||||
|
|
||||||
|
|
||||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||||
def pytest_collection_modifyitems(items):
|
def pytest_collection_modifyitems(items):
|
||||||
"""Prefer faster tests.
|
"""Prefer faster tests.
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
# mypy: disable-error-code="attr-defined"
|
# mypy: disable-error-code="attr-defined"
|
||||||
|
# mypy: disallow-untyped-defs
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Iterator
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest.logging import caplog_records_key
|
from _pytest.logging import caplog_records_key
|
||||||
|
@ -9,8 +11,8 @@ logger = logging.getLogger(__name__)
|
||||||
sublogger = logging.getLogger(__name__ + ".baz")
|
sublogger = logging.getLogger(__name__ + ".baz")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture(autouse=True)
|
||||||
def cleanup_disabled_logging():
|
def cleanup_disabled_logging() -> Iterator[None]:
|
||||||
"""Simple fixture that ensures that a test doesn't disable logging.
|
"""Simple fixture that ensures that a test doesn't disable logging.
|
||||||
|
|
||||||
This is necessary because ``logging.disable()`` is global, so a test disabling logging
|
This is necessary because ``logging.disable()`` is global, so a test disabling logging
|
||||||
|
@ -27,7 +29,7 @@ def test_fixture_help(pytester: Pytester) -> None:
|
||||||
result.stdout.fnmatch_lines(["*caplog*"])
|
result.stdout.fnmatch_lines(["*caplog*"])
|
||||||
|
|
||||||
|
|
||||||
def test_change_level(caplog):
|
def test_change_level(caplog: pytest.LogCaptureFixture) -> None:
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
logger.debug("handler DEBUG level")
|
logger.debug("handler DEBUG level")
|
||||||
logger.info("handler INFO level")
|
logger.info("handler INFO level")
|
||||||
|
@ -42,7 +44,7 @@ def test_change_level(caplog):
|
||||||
assert "CRITICAL" in caplog.text
|
assert "CRITICAL" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
def test_change_level_logging_disabled(caplog, cleanup_disabled_logging):
|
def test_change_level_logging_disabled(caplog: pytest.LogCaptureFixture) -> None:
|
||||||
logging.disable(logging.CRITICAL)
|
logging.disable(logging.CRITICAL)
|
||||||
assert logging.root.manager.disable == logging.CRITICAL
|
assert logging.root.manager.disable == logging.CRITICAL
|
||||||
caplog.set_level(logging.WARNING)
|
caplog.set_level(logging.WARNING)
|
||||||
|
@ -85,9 +87,7 @@ def test_change_level_undo(pytester: Pytester) -> None:
|
||||||
result.stdout.no_fnmatch_line("*log from test2*")
|
result.stdout.no_fnmatch_line("*log from test2*")
|
||||||
|
|
||||||
|
|
||||||
def test_change_disabled_level_undo(
|
def test_change_disabled_level_undo(pytester: Pytester) -> None:
|
||||||
pytester: Pytester, cleanup_disabled_logging
|
|
||||||
) -> None:
|
|
||||||
"""Ensure that '_force_enable_logging' in 'set_level' is undone after the end of the test.
|
"""Ensure that '_force_enable_logging' in 'set_level' is undone after the end of the test.
|
||||||
|
|
||||||
Tests the logging output themselves (affected by disabled logging level).
|
Tests the logging output themselves (affected by disabled logging level).
|
||||||
|
@ -144,7 +144,7 @@ def test_change_level_undos_handler_level(pytester: Pytester) -> None:
|
||||||
result.assert_outcomes(passed=3)
|
result.assert_outcomes(passed=3)
|
||||||
|
|
||||||
|
|
||||||
def test_with_statement(caplog):
|
def test_with_statement(caplog: pytest.LogCaptureFixture) -> None:
|
||||||
with caplog.at_level(logging.INFO):
|
with caplog.at_level(logging.INFO):
|
||||||
logger.debug("handler DEBUG level")
|
logger.debug("handler DEBUG level")
|
||||||
logger.info("handler INFO level")
|
logger.info("handler INFO level")
|
||||||
|
@ -159,7 +159,7 @@ def test_with_statement(caplog):
|
||||||
assert "CRITICAL" in caplog.text
|
assert "CRITICAL" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
def test_with_statement_logging_disabled(caplog, cleanup_disabled_logging):
|
def test_with_statement_logging_disabled(caplog: pytest.LogCaptureFixture) -> None:
|
||||||
logging.disable(logging.CRITICAL)
|
logging.disable(logging.CRITICAL)
|
||||||
assert logging.root.manager.disable == logging.CRITICAL
|
assert logging.root.manager.disable == logging.CRITICAL
|
||||||
with caplog.at_level(logging.WARNING):
|
with caplog.at_level(logging.WARNING):
|
||||||
|
@ -198,8 +198,8 @@ def test_with_statement_logging_disabled(caplog, cleanup_disabled_logging):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_force_enable_logging_level_string(
|
def test_force_enable_logging_level_string(
|
||||||
caplog, cleanup_disabled_logging, level_str, expected_disable_level
|
caplog: pytest.LogCaptureFixture, level_str: str, expected_disable_level: int
|
||||||
):
|
) -> None:
|
||||||
"""Test _force_enable_logging using a level string.
|
"""Test _force_enable_logging using a level string.
|
||||||
|
|
||||||
``expected_disable_level`` is one level below ``level_str`` because the disabled log level
|
``expected_disable_level`` is one level below ``level_str`` because the disabled log level
|
||||||
|
@ -218,7 +218,7 @@ def test_force_enable_logging_level_string(
|
||||||
assert test_logger.manager.disable == expected_disable_level
|
assert test_logger.manager.disable == expected_disable_level
|
||||||
|
|
||||||
|
|
||||||
def test_log_access(caplog):
|
def test_log_access(caplog: pytest.LogCaptureFixture) -> None:
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
logger.info("boo %s", "arg")
|
logger.info("boo %s", "arg")
|
||||||
assert caplog.records[0].levelname == "INFO"
|
assert caplog.records[0].levelname == "INFO"
|
||||||
|
@ -226,7 +226,7 @@ def test_log_access(caplog):
|
||||||
assert "boo arg" in caplog.text
|
assert "boo arg" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
def test_messages(caplog):
|
def test_messages(caplog: pytest.LogCaptureFixture) -> None:
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
logger.info("boo %s", "arg")
|
logger.info("boo %s", "arg")
|
||||||
logger.info("bar %s\nbaz %s", "arg1", "arg2")
|
logger.info("bar %s\nbaz %s", "arg1", "arg2")
|
||||||
|
@ -247,14 +247,14 @@ def test_messages(caplog):
|
||||||
assert "Exception" not in caplog.messages[-1]
|
assert "Exception" not in caplog.messages[-1]
|
||||||
|
|
||||||
|
|
||||||
def test_record_tuples(caplog):
|
def test_record_tuples(caplog: pytest.LogCaptureFixture) -> None:
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
logger.info("boo %s", "arg")
|
logger.info("boo %s", "arg")
|
||||||
|
|
||||||
assert caplog.record_tuples == [(__name__, logging.INFO, "boo arg")]
|
assert caplog.record_tuples == [(__name__, logging.INFO, "boo arg")]
|
||||||
|
|
||||||
|
|
||||||
def test_unicode(caplog):
|
def test_unicode(caplog: pytest.LogCaptureFixture) -> None:
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
logger.info("bū")
|
logger.info("bū")
|
||||||
assert caplog.records[0].levelname == "INFO"
|
assert caplog.records[0].levelname == "INFO"
|
||||||
|
@ -262,7 +262,7 @@ def test_unicode(caplog):
|
||||||
assert "bū" in caplog.text
|
assert "bū" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
def test_clear(caplog):
|
def test_clear(caplog: pytest.LogCaptureFixture) -> None:
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
logger.info("bū")
|
logger.info("bū")
|
||||||
assert len(caplog.records)
|
assert len(caplog.records)
|
||||||
|
@ -273,7 +273,9 @@ def test_clear(caplog):
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def logging_during_setup_and_teardown(caplog):
|
def logging_during_setup_and_teardown(
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> Iterator[None]:
|
||||||
caplog.set_level("INFO")
|
caplog.set_level("INFO")
|
||||||
logger.info("a_setup_log")
|
logger.info("a_setup_log")
|
||||||
yield
|
yield
|
||||||
|
@ -281,7 +283,9 @@ def logging_during_setup_and_teardown(caplog):
|
||||||
assert [x.message for x in caplog.get_records("teardown")] == ["a_teardown_log"]
|
assert [x.message for x in caplog.get_records("teardown")] == ["a_teardown_log"]
|
||||||
|
|
||||||
|
|
||||||
def test_caplog_captures_for_all_stages(caplog, logging_during_setup_and_teardown):
|
def test_caplog_captures_for_all_stages(
|
||||||
|
caplog: pytest.LogCaptureFixture, logging_during_setup_and_teardown: None
|
||||||
|
) -> None:
|
||||||
assert not caplog.records
|
assert not caplog.records
|
||||||
assert not caplog.get_records("call")
|
assert not caplog.get_records("call")
|
||||||
logger.info("a_call_log")
|
logger.info("a_call_log")
|
||||||
|
@ -290,25 +294,31 @@ def test_caplog_captures_for_all_stages(caplog, logging_during_setup_and_teardow
|
||||||
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
|
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
|
||||||
|
|
||||||
# This reaches into private API, don't use this type of thing in real tests!
|
# This reaches into private API, don't use this type of thing in real tests!
|
||||||
assert set(caplog._item.stash[caplog_records_key]) == {"setup", "call"}
|
caplog_records = caplog._item.stash[caplog_records_key]
|
||||||
|
assert set(caplog_records) == {"setup", "call"}
|
||||||
|
|
||||||
|
|
||||||
def test_clear_for_call_stage(caplog, logging_during_setup_and_teardown):
|
def test_clear_for_call_stage(
|
||||||
|
caplog: pytest.LogCaptureFixture, logging_during_setup_and_teardown: None
|
||||||
|
) -> None:
|
||||||
logger.info("a_call_log")
|
logger.info("a_call_log")
|
||||||
assert [x.message for x in caplog.get_records("call")] == ["a_call_log"]
|
assert [x.message for x in caplog.get_records("call")] == ["a_call_log"]
|
||||||
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
|
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
|
||||||
assert set(caplog._item.stash[caplog_records_key]) == {"setup", "call"}
|
caplog_records = caplog._item.stash[caplog_records_key]
|
||||||
|
assert set(caplog_records) == {"setup", "call"}
|
||||||
|
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
assert caplog.get_records("call") == []
|
assert caplog.get_records("call") == []
|
||||||
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
|
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
|
||||||
assert set(caplog._item.stash[caplog_records_key]) == {"setup", "call"}
|
caplog_records = caplog._item.stash[caplog_records_key]
|
||||||
|
assert set(caplog_records) == {"setup", "call"}
|
||||||
|
|
||||||
logging.info("a_call_log_after_clear")
|
logging.info("a_call_log_after_clear")
|
||||||
assert [x.message for x in caplog.get_records("call")] == ["a_call_log_after_clear"]
|
assert [x.message for x in caplog.get_records("call")] == ["a_call_log_after_clear"]
|
||||||
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
|
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
|
||||||
assert set(caplog._item.stash[caplog_records_key]) == {"setup", "call"}
|
caplog_records = caplog._item.stash[caplog_records_key]
|
||||||
|
assert set(caplog_records) == {"setup", "call"}
|
||||||
|
|
||||||
|
|
||||||
def test_ini_controls_global_log_level(pytester: Pytester) -> None:
|
def test_ini_controls_global_log_level(pytester: Pytester) -> None:
|
||||||
|
|
|
@ -122,6 +122,23 @@ class TestApprox:
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assert_approx_raises_regex(
|
||||||
|
{"a": 1.0, "b": None, "c": None},
|
||||||
|
{
|
||||||
|
"a": None,
|
||||||
|
"b": 1000.0,
|
||||||
|
"c": None,
|
||||||
|
},
|
||||||
|
[
|
||||||
|
r" comparison failed. Mismatched elements: 2 / 3:",
|
||||||
|
r" Max absolute difference: -inf",
|
||||||
|
r" Max relative difference: -inf",
|
||||||
|
r" Index \| Obtained\s+\| Expected\s+",
|
||||||
|
rf" a \| {SOME_FLOAT} \| None",
|
||||||
|
rf" b \| None\s+\| {SOME_FLOAT} ± {SOME_FLOAT}",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
assert_approx_raises_regex(
|
assert_approx_raises_regex(
|
||||||
[1.0, 2.0, 3.0, 4.0],
|
[1.0, 2.0, 3.0, 4.0],
|
||||||
[1.0, 3.0, 3.0, 5.0],
|
[1.0, 3.0, 3.0, 5.0],
|
||||||
|
|
|
@ -1493,7 +1493,7 @@ class TestMetafuncFunctional:
|
||||||
pass
|
pass
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = pytester.runpytest("--collectonly")
|
result = pytester.runpytest("--collect-only")
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
[
|
[
|
||||||
"collected 0 items / 1 error",
|
"collected 0 items / 1 error",
|
||||||
|
|
|
@ -686,6 +686,25 @@ class TestAssertionRewrite:
|
||||||
assert msg is not None
|
assert msg is not None
|
||||||
assert "<MY42 object> < 0" in msg
|
assert "<MY42 object> < 0" in msg
|
||||||
|
|
||||||
|
def test_assert_handling_raise_in__iter__(self, pytester: Pytester) -> None:
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""\
|
||||||
|
class A:
|
||||||
|
def __iter__(self):
|
||||||
|
raise ValueError()
|
||||||
|
|
||||||
|
def __eq__(self, o: object) -> bool:
|
||||||
|
return self is o
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<A object>"
|
||||||
|
|
||||||
|
assert A() == A()
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest()
|
||||||
|
result.stdout.fnmatch_lines(["*E*assert <A object> == <A object>"])
|
||||||
|
|
||||||
def test_formatchar(self) -> None:
|
def test_formatchar(self) -> None:
|
||||||
def f() -> None:
|
def f() -> None:
|
||||||
assert "%test" == "test" # type: ignore[comparison-overlap]
|
assert "%test" == "test" # type: ignore[comparison-overlap]
|
||||||
|
@ -1531,6 +1550,28 @@ class TestIssue11028:
|
||||||
result.stdout.fnmatch_lines(["*assert 4 > 5", "*where 5 = add_one(4)"])
|
result.stdout.fnmatch_lines(["*assert 4 > 5", "*where 5 = add_one(4)"])
|
||||||
|
|
||||||
|
|
||||||
|
class TestIssue11239:
|
||||||
|
@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason="Only Python 3.8+")
|
||||||
|
def test_assertion_walrus_different_test_cases(self, pytester: Pytester) -> None:
|
||||||
|
"""Regression for (#11239)
|
||||||
|
|
||||||
|
Walrus operator rewriting would leak to separate test cases if they used the same variables.
|
||||||
|
"""
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
def test_1():
|
||||||
|
state = {"x": 2}.get("x")
|
||||||
|
assert state is not None
|
||||||
|
|
||||||
|
def test_2():
|
||||||
|
db = {"x": 2}
|
||||||
|
assert (state := db.get("x")) is not None
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest()
|
||||||
|
assert result.ret == 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
sys.maxsize <= (2**31 - 1), reason="Causes OverflowError on 32bit systems"
|
sys.maxsize <= (2**31 - 1), reason="Causes OverflowError on 32bit systems"
|
||||||
)
|
)
|
||||||
|
@ -2055,3 +2096,17 @@ class TestReprSizeVerbosity:
|
||||||
self.create_test_file(pytester, DEFAULT_REPR_MAX_SIZE * 10)
|
self.create_test_file(pytester, DEFAULT_REPR_MAX_SIZE * 10)
|
||||||
result = pytester.runpytest("-vv")
|
result = pytester.runpytest("-vv")
|
||||||
result.stdout.no_fnmatch_line("*xxx...xxx*")
|
result.stdout.no_fnmatch_line("*xxx...xxx*")
|
||||||
|
|
||||||
|
|
||||||
|
class TestIssue11140:
|
||||||
|
def test_constant_not_picked_as_module_docstring(self, pytester: Pytester) -> None:
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""\
|
||||||
|
0
|
||||||
|
|
||||||
|
def test_foo():
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest()
|
||||||
|
assert result.ret == 0
|
||||||
|
|
|
@ -345,6 +345,29 @@ class TestPrunetraceback:
|
||||||
result = pytester.runpytest(p)
|
result = pytester.runpytest(p)
|
||||||
result.stdout.fnmatch_lines(["*ERROR collecting*", "*header1*"])
|
result.stdout.fnmatch_lines(["*ERROR collecting*", "*header1*"])
|
||||||
|
|
||||||
|
def test_collection_error_traceback_is_clean(self, pytester: Pytester) -> None:
|
||||||
|
"""When a collection error occurs, the report traceback doesn't contain
|
||||||
|
internal pytest stack entries.
|
||||||
|
|
||||||
|
Issue #11710.
|
||||||
|
"""
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
raise Exception("LOUSY")
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest()
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
[
|
||||||
|
"*ERROR collecting*",
|
||||||
|
"test_*.py:1: in <module>",
|
||||||
|
' raise Exception("LOUSY")',
|
||||||
|
"E Exception: LOUSY",
|
||||||
|
"*= short test summary info =*",
|
||||||
|
],
|
||||||
|
consecutive=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestCustomConftests:
|
class TestCustomConftests:
|
||||||
def test_ignore_collect_path(self, pytester: Pytester) -> None:
|
def test_ignore_collect_path(self, pytester: Pytester) -> None:
|
||||||
|
|
|
@ -482,6 +482,27 @@ class TestDoctests:
|
||||||
reprec = pytester.inline_run(p, "--doctest-modules")
|
reprec = pytester.inline_run(p, "--doctest-modules")
|
||||||
reprec.assertoutcome(failed=1)
|
reprec.assertoutcome(failed=1)
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
sys.version_info[:2] <= (3, 7), reason="Only Python 3.7 or less"
|
||||||
|
)
|
||||||
|
def test_doctest_cached_property(self, pytester: Pytester):
|
||||||
|
p = pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import functools
|
||||||
|
|
||||||
|
class Foo:
|
||||||
|
@functools.cached_property
|
||||||
|
def foo(self):
|
||||||
|
'''
|
||||||
|
>>> assert False, "Tacos!"
|
||||||
|
'''
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest(p, "--doctest-modules")
|
||||||
|
result.assert_outcomes(failed=1)
|
||||||
|
assert "Tacos!" in result.stdout.str()
|
||||||
|
|
||||||
def test_doctestmodule_external_and_issue116(self, pytester: Pytester):
|
def test_doctestmodule_external_and_issue116(self, pytester: Pytester):
|
||||||
p = pytester.mkpydir("hello")
|
p = pytester.mkpydir("hello")
|
||||||
p.joinpath("__init__.py").write_text(
|
p.joinpath("__init__.py").write_text(
|
||||||
|
|
|
@ -1228,6 +1228,36 @@ def test_record_property(pytester: Pytester, run_and_parse: RunAndParse) -> None
|
||||||
result.stdout.fnmatch_lines(["*= 1 passed in *"])
|
result.stdout.fnmatch_lines(["*= 1 passed in *"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_record_property_on_test_and_teardown_failure(
|
||||||
|
pytester: Pytester, run_and_parse: RunAndParse
|
||||||
|
) -> None:
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def other(record_property):
|
||||||
|
record_property("bar", 1)
|
||||||
|
yield
|
||||||
|
assert 0
|
||||||
|
|
||||||
|
def test_record(record_property, other):
|
||||||
|
record_property("foo", "<1")
|
||||||
|
assert 0
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result, dom = run_and_parse()
|
||||||
|
node = dom.find_first_by_tag("testsuite")
|
||||||
|
tnodes = node.find_by_tag("testcase")
|
||||||
|
for tnode in tnodes:
|
||||||
|
psnode = tnode.find_first_by_tag("properties")
|
||||||
|
assert psnode, f"testcase didn't had expected properties:\n{tnode}"
|
||||||
|
pnodes = psnode.find_by_tag("property")
|
||||||
|
pnodes[0].assert_attr(name="bar", value="1")
|
||||||
|
pnodes[1].assert_attr(name="foo", value="<1")
|
||||||
|
result.stdout.fnmatch_lines(["*= 1 failed, 1 error *"])
|
||||||
|
|
||||||
|
|
||||||
def test_record_property_same_name(
|
def test_record_property_same_name(
|
||||||
pytester: Pytester, run_and_parse: RunAndParse
|
pytester: Pytester, run_and_parse: RunAndParse
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
@ -262,3 +262,34 @@ def test_module_full_path_without_drive(pytester: Pytester) -> None:
|
||||||
"* 1 passed in *",
|
"* 1 passed in *",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_very_long_cmdline_arg(pytester: Pytester) -> None:
|
||||||
|
"""
|
||||||
|
Regression test for #11394.
|
||||||
|
|
||||||
|
Note: we could not manage to actually reproduce the error with this code, we suspect
|
||||||
|
GitHub runners are configured to support very long paths, however decided to leave
|
||||||
|
the test in place in case this ever regresses in the future.
|
||||||
|
"""
|
||||||
|
pytester.makeconftest(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
def pytest_addoption(parser):
|
||||||
|
parser.addoption("--long-list", dest="long_list", action="store", default="all", help="List of things")
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def specified_feeds(request):
|
||||||
|
list_string = request.config.getoption("--long-list")
|
||||||
|
return list_string.split(',')
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
def test_foo(specified_feeds):
|
||||||
|
assert len(specified_feeds) == 100_000
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest("--long-list", ",".join(["helloworld"] * 100_000))
|
||||||
|
result.stdout.fnmatch_lines("* 1 passed *")
|
||||||
|
|
|
@ -1130,6 +1130,41 @@ def test_mark_mro() -> None:
|
||||||
|
|
||||||
all_marks = get_unpacked_marks(C)
|
all_marks = get_unpacked_marks(C)
|
||||||
|
|
||||||
assert all_marks == [xfail("c").mark, xfail("a").mark, xfail("b").mark]
|
assert all_marks == [xfail("b").mark, xfail("a").mark, xfail("c").mark]
|
||||||
|
|
||||||
assert get_unpacked_marks(C, consider_mro=False) == [xfail("c").mark]
|
assert get_unpacked_marks(C, consider_mro=False) == [xfail("c").mark]
|
||||||
|
|
||||||
|
|
||||||
|
# @pytest.mark.issue("https://github.com/pytest-dev/pytest/issues/10447")
|
||||||
|
def test_mark_fixture_order_mro(pytester: Pytester):
|
||||||
|
"""This ensures we walk marks of the mro starting with the base classes
|
||||||
|
the action at a distance fixtures are taken as minimal example from a real project
|
||||||
|
|
||||||
|
"""
|
||||||
|
foo = pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def add_attr1(request):
|
||||||
|
request.instance.attr1 = object()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def add_attr2(request):
|
||||||
|
request.instance.attr2 = request.instance.attr1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures('add_attr1')
|
||||||
|
class Parent:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures('add_attr2')
|
||||||
|
class TestThings(Parent):
|
||||||
|
def test_attrs(self):
|
||||||
|
assert self.attr1 == self.attr2
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest(foo)
|
||||||
|
result.assert_outcomes(passed=1)
|
||||||
|
|
|
@ -291,7 +291,8 @@ class TestParser:
|
||||||
|
|
||||||
def test_argcomplete(pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
|
def test_argcomplete(pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
|
||||||
try:
|
try:
|
||||||
encoding = locale.getencoding() # New in Python 3.11, ignores utf-8 mode
|
# New in Python 3.11, ignores utf-8 mode
|
||||||
|
encoding = locale.getencoding() # type: ignore[attr-defined]
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
encoding = locale.getpreferredencoding(False)
|
encoding = locale.getpreferredencoding(False)
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import errno
|
||||||
import os.path
|
import os.path
|
||||||
import pickle
|
import pickle
|
||||||
import sys
|
import sys
|
||||||
|
@ -7,6 +8,7 @@ from textwrap import dedent
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
|
from typing import Iterator
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest.monkeypatch import MonkeyPatch
|
from _pytest.monkeypatch import MonkeyPatch
|
||||||
|
@ -17,13 +19,16 @@ from _pytest.pathlib import fnmatch_ex
|
||||||
from _pytest.pathlib import get_extended_length_path_str
|
from _pytest.pathlib import get_extended_length_path_str
|
||||||
from _pytest.pathlib import get_lock_path
|
from _pytest.pathlib import get_lock_path
|
||||||
from _pytest.pathlib import import_path
|
from _pytest.pathlib import import_path
|
||||||
|
from _pytest.pathlib import ImportMode
|
||||||
from _pytest.pathlib import ImportPathMismatchError
|
from _pytest.pathlib import ImportPathMismatchError
|
||||||
from _pytest.pathlib import insert_missing_modules
|
from _pytest.pathlib import insert_missing_modules
|
||||||
from _pytest.pathlib import maybe_delete_a_numbered_dir
|
from _pytest.pathlib import maybe_delete_a_numbered_dir
|
||||||
from _pytest.pathlib import module_name_from_path
|
from _pytest.pathlib import module_name_from_path
|
||||||
from _pytest.pathlib import resolve_package_path
|
from _pytest.pathlib import resolve_package_path
|
||||||
|
from _pytest.pathlib import safe_exists
|
||||||
from _pytest.pathlib import symlink_or_skip
|
from _pytest.pathlib import symlink_or_skip
|
||||||
from _pytest.pathlib import visit
|
from _pytest.pathlib import visit
|
||||||
|
from _pytest.pytester import Pytester
|
||||||
from _pytest.tmpdir import TempPathFactory
|
from _pytest.tmpdir import TempPathFactory
|
||||||
|
|
||||||
|
|
||||||
|
@ -282,29 +287,36 @@ class TestImportPath:
|
||||||
import_path(tmp_path / "invalid.py", root=tmp_path)
|
import_path(tmp_path / "invalid.py", root=tmp_path)
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def simple_module(self, tmp_path: Path) -> Path:
|
def simple_module(
|
||||||
fn = tmp_path / "_src/tests/mymod.py"
|
self, tmp_path: Path, request: pytest.FixtureRequest
|
||||||
|
) -> Iterator[Path]:
|
||||||
|
name = f"mymod_{request.node.name}"
|
||||||
|
fn = tmp_path / f"_src/tests/{name}.py"
|
||||||
fn.parent.mkdir(parents=True)
|
fn.parent.mkdir(parents=True)
|
||||||
fn.write_text("def foo(x): return 40 + x", encoding="utf-8")
|
fn.write_text("def foo(x): return 40 + x", encoding="utf-8")
|
||||||
return fn
|
module_name = module_name_from_path(fn, root=tmp_path)
|
||||||
|
yield fn
|
||||||
|
sys.modules.pop(module_name, None)
|
||||||
|
|
||||||
def test_importmode_importlib(self, simple_module: Path, tmp_path: Path) -> None:
|
def test_importmode_importlib(
|
||||||
|
self, simple_module: Path, tmp_path: Path, request: pytest.FixtureRequest
|
||||||
|
) -> None:
|
||||||
"""`importlib` mode does not change sys.path."""
|
"""`importlib` mode does not change sys.path."""
|
||||||
module = import_path(simple_module, mode="importlib", root=tmp_path)
|
module = import_path(simple_module, mode="importlib", root=tmp_path)
|
||||||
assert module.foo(2) == 42 # type: ignore[attr-defined]
|
assert module.foo(2) == 42 # type: ignore[attr-defined]
|
||||||
assert str(simple_module.parent) not in sys.path
|
assert str(simple_module.parent) not in sys.path
|
||||||
assert module.__name__ in sys.modules
|
assert module.__name__ in sys.modules
|
||||||
assert module.__name__ == "_src.tests.mymod"
|
assert module.__name__ == f"_src.tests.mymod_{request.node.name}"
|
||||||
assert "_src" in sys.modules
|
assert "_src" in sys.modules
|
||||||
assert "_src.tests" in sys.modules
|
assert "_src.tests" in sys.modules
|
||||||
|
|
||||||
def test_importmode_twice_is_different_module(
|
def test_remembers_previous_imports(
|
||||||
self, simple_module: Path, tmp_path: Path
|
self, simple_module: Path, tmp_path: Path
|
||||||
) -> None:
|
) -> None:
|
||||||
"""`importlib` mode always returns a new module."""
|
"""`importlib` mode called remembers previous module (#10341, #10811)."""
|
||||||
module1 = import_path(simple_module, mode="importlib", root=tmp_path)
|
module1 = import_path(simple_module, mode="importlib", root=tmp_path)
|
||||||
module2 = import_path(simple_module, mode="importlib", root=tmp_path)
|
module2 = import_path(simple_module, mode="importlib", root=tmp_path)
|
||||||
assert module1 is not module2
|
assert module1 is module2
|
||||||
|
|
||||||
def test_no_meta_path_found(
|
def test_no_meta_path_found(
|
||||||
self, simple_module: Path, monkeypatch: MonkeyPatch, tmp_path: Path
|
self, simple_module: Path, monkeypatch: MonkeyPatch, tmp_path: Path
|
||||||
|
@ -317,6 +329,9 @@ class TestImportPath:
|
||||||
# mode='importlib' fails if no spec is found to load the module
|
# mode='importlib' fails if no spec is found to load the module
|
||||||
import importlib.util
|
import importlib.util
|
||||||
|
|
||||||
|
# Force module to be re-imported.
|
||||||
|
del sys.modules[module.__name__]
|
||||||
|
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
importlib.util, "spec_from_file_location", lambda *args: None
|
importlib.util, "spec_from_file_location", lambda *args: None
|
||||||
)
|
)
|
||||||
|
@ -574,6 +589,14 @@ class TestImportLibMode:
|
||||||
result = module_name_from_path(Path("/home/foo/test_foo.py"), Path("/bar"))
|
result = module_name_from_path(Path("/home/foo/test_foo.py"), Path("/bar"))
|
||||||
assert result == "home.foo.test_foo"
|
assert result == "home.foo.test_foo"
|
||||||
|
|
||||||
|
# Importing __init__.py files should return the package as module name.
|
||||||
|
result = module_name_from_path(tmp_path / "src/app/__init__.py", tmp_path)
|
||||||
|
assert result == "src.app"
|
||||||
|
|
||||||
|
# Unless __init__.py file is at the root, in which case we cannot have an empty module name.
|
||||||
|
result = module_name_from_path(tmp_path / "__init__.py", tmp_path)
|
||||||
|
assert result == "__init__"
|
||||||
|
|
||||||
def test_insert_missing_modules(
|
def test_insert_missing_modules(
|
||||||
self, monkeypatch: MonkeyPatch, tmp_path: Path
|
self, monkeypatch: MonkeyPatch, tmp_path: Path
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -592,3 +615,100 @@ class TestImportLibMode:
|
||||||
modules = {}
|
modules = {}
|
||||||
insert_missing_modules(modules, "")
|
insert_missing_modules(modules, "")
|
||||||
assert modules == {}
|
assert modules == {}
|
||||||
|
|
||||||
|
def test_parent_contains_child_module_attribute(
|
||||||
|
self, monkeypatch: MonkeyPatch, tmp_path: Path
|
||||||
|
):
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
# Use 'xxx' and 'xxy' as parent names as they are unlikely to exist and
|
||||||
|
# don't end up being imported.
|
||||||
|
modules = {"xxx.tests.foo": ModuleType("xxx.tests.foo")}
|
||||||
|
insert_missing_modules(modules, "xxx.tests.foo")
|
||||||
|
assert sorted(modules) == ["xxx", "xxx.tests", "xxx.tests.foo"]
|
||||||
|
assert modules["xxx"].tests is modules["xxx.tests"]
|
||||||
|
assert modules["xxx.tests"].foo is modules["xxx.tests.foo"]
|
||||||
|
|
||||||
|
def test_importlib_package(self, monkeypatch: MonkeyPatch, tmp_path: Path):
|
||||||
|
"""
|
||||||
|
Importing a package using --importmode=importlib should not import the
|
||||||
|
package's __init__.py file more than once (#11306).
|
||||||
|
"""
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
monkeypatch.syspath_prepend(tmp_path)
|
||||||
|
|
||||||
|
package_name = "importlib_import_package"
|
||||||
|
tmp_path.joinpath(package_name).mkdir()
|
||||||
|
init = tmp_path.joinpath(f"{package_name}/__init__.py")
|
||||||
|
init.write_text(
|
||||||
|
dedent(
|
||||||
|
"""
|
||||||
|
from .singleton import Singleton
|
||||||
|
|
||||||
|
instance = Singleton()
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
encoding="ascii",
|
||||||
|
)
|
||||||
|
singleton = tmp_path.joinpath(f"{package_name}/singleton.py")
|
||||||
|
singleton.write_text(
|
||||||
|
dedent(
|
||||||
|
"""
|
||||||
|
class Singleton:
|
||||||
|
INSTANCES = []
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.INSTANCES.append(self)
|
||||||
|
if len(self.INSTANCES) > 1:
|
||||||
|
raise RuntimeError("Already initialized")
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
encoding="ascii",
|
||||||
|
)
|
||||||
|
|
||||||
|
mod = import_path(init, root=tmp_path, mode=ImportMode.importlib)
|
||||||
|
assert len(mod.instance.INSTANCES) == 1
|
||||||
|
|
||||||
|
def test_importlib_root_is_package(self, pytester: Pytester) -> None:
|
||||||
|
"""
|
||||||
|
Regression for importing a `__init__`.py file that is at the root
|
||||||
|
(#11417).
|
||||||
|
"""
|
||||||
|
pytester.makepyfile(__init__="")
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
def test_my_test():
|
||||||
|
assert True
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
result = pytester.runpytest("--import-mode=importlib")
|
||||||
|
result.stdout.fnmatch_lines("* 1 passed *")
|
||||||
|
|
||||||
|
|
||||||
|
def test_safe_exists(tmp_path: Path) -> None:
|
||||||
|
d = tmp_path.joinpath("some_dir")
|
||||||
|
d.mkdir()
|
||||||
|
assert safe_exists(d) is True
|
||||||
|
|
||||||
|
f = tmp_path.joinpath("some_file")
|
||||||
|
f.touch()
|
||||||
|
assert safe_exists(f) is True
|
||||||
|
|
||||||
|
# Use unittest.mock() as a context manager to have a very narrow
|
||||||
|
# patch lifetime.
|
||||||
|
p = tmp_path.joinpath("some long filename" * 100)
|
||||||
|
with unittest.mock.patch.object(
|
||||||
|
Path,
|
||||||
|
"exists",
|
||||||
|
autospec=True,
|
||||||
|
side_effect=OSError(errno.ENAMETOOLONG, "name too long"),
|
||||||
|
):
|
||||||
|
assert safe_exists(p) is False
|
||||||
|
|
||||||
|
with unittest.mock.patch.object(
|
||||||
|
Path,
|
||||||
|
"exists",
|
||||||
|
autospec=True,
|
||||||
|
side_effect=ValueError("name too long"),
|
||||||
|
):
|
||||||
|
assert safe_exists(p) is False
|
||||||
|
|
|
@ -242,8 +242,12 @@ class TestPytestPluginManager:
|
||||||
mod = types.ModuleType("temp")
|
mod = types.ModuleType("temp")
|
||||||
mod.__dict__["pytest_plugins"] = ["pytest_p1", "pytest_p2"]
|
mod.__dict__["pytest_plugins"] = ["pytest_p1", "pytest_p2"]
|
||||||
pytestpm.consider_module(mod)
|
pytestpm.consider_module(mod)
|
||||||
assert pytestpm.get_plugin("pytest_p1").__name__ == "pytest_p1"
|
p1 = pytestpm.get_plugin("pytest_p1")
|
||||||
assert pytestpm.get_plugin("pytest_p2").__name__ == "pytest_p2"
|
assert p1 is not None
|
||||||
|
assert p1.__name__ == "pytest_p1"
|
||||||
|
p2 = pytestpm.get_plugin("pytest_p2")
|
||||||
|
assert p2 is not None
|
||||||
|
assert p2.__name__ == "pytest_p2"
|
||||||
|
|
||||||
def test_consider_module_import_module(
|
def test_consider_module_import_module(
|
||||||
self, pytester: Pytester, _config_for_test: Config
|
self, pytester: Pytester, _config_for_test: Config
|
||||||
|
@ -336,6 +340,7 @@ class TestPytestPluginManager:
|
||||||
len2 = len(pytestpm.get_plugins())
|
len2 = len(pytestpm.get_plugins())
|
||||||
assert len1 == len2
|
assert len1 == len2
|
||||||
plugin1 = pytestpm.get_plugin("pytest_hello")
|
plugin1 = pytestpm.get_plugin("pytest_hello")
|
||||||
|
assert plugin1 is not None
|
||||||
assert plugin1.__name__.endswith("pytest_hello")
|
assert plugin1.__name__.endswith("pytest_hello")
|
||||||
plugin2 = pytestpm.get_plugin("pytest_hello")
|
plugin2 = pytestpm.get_plugin("pytest_hello")
|
||||||
assert plugin2 is plugin1
|
assert plugin2 is plugin1
|
||||||
|
@ -351,6 +356,7 @@ class TestPytestPluginManager:
|
||||||
pluginname = "pkg.plug"
|
pluginname = "pkg.plug"
|
||||||
pytestpm.import_plugin(pluginname)
|
pytestpm.import_plugin(pluginname)
|
||||||
mod = pytestpm.get_plugin("pkg.plug")
|
mod = pytestpm.get_plugin("pkg.plug")
|
||||||
|
assert mod is not None
|
||||||
assert mod.x == 3
|
assert mod.x == 3
|
||||||
|
|
||||||
def test_consider_conftest_deps(
|
def test_consider_conftest_deps(
|
||||||
|
|
Loading…
Reference in New Issue