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
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
# These tags are protected, see:
|
||||
# https://github.com/pytest-dev/pytest/settings/tag_protection
|
||||
- "[0-9]+.[0-9]+.[0-9]+"
|
||||
- "[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Release version'
|
||||
required: true
|
||||
default: '1.2.3'
|
||||
|
||||
|
||||
# Set permissions at the job level.
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
|
||||
deploy:
|
||||
if: github.repository == 'pytest-dev/pytest'
|
||||
|
||||
package:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: write
|
||||
env:
|
||||
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ github.event.inputs.version }}
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
@ -29,7 +26,19 @@ jobs:
|
|||
persist-credentials: false
|
||||
|
||||
- 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
|
||||
uses: actions/download-artifact@v3
|
||||
|
@ -38,14 +47,35 @@ jobs:
|
|||
path: dist
|
||||
|
||||
- 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:
|
||||
password: ${{ secrets.pypi_token }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.7"
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Install tox
|
||||
run: |
|
||||
|
|
|
@ -27,7 +27,19 @@ concurrency:
|
|||
permissions: {}
|
||||
|
||||
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:
|
||||
needs: [package]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 45
|
||||
permissions:
|
||||
|
@ -38,17 +50,17 @@ jobs:
|
|||
matrix:
|
||||
name: [
|
||||
"windows-py37",
|
||||
"windows-py37-pluggy",
|
||||
"windows-py38",
|
||||
"windows-py38-pluggy",
|
||||
"windows-py39",
|
||||
"windows-py310",
|
||||
"windows-py311",
|
||||
"windows-py312",
|
||||
|
||||
"ubuntu-py37",
|
||||
"ubuntu-py37-pluggy",
|
||||
"ubuntu-py37-freeze",
|
||||
"ubuntu-py38",
|
||||
"ubuntu-py38-pluggy",
|
||||
"ubuntu-py39",
|
||||
"ubuntu-py310",
|
||||
"ubuntu-py311",
|
||||
|
@ -60,7 +72,6 @@ jobs:
|
|||
"macos-py310",
|
||||
"macos-py312",
|
||||
|
||||
"docs",
|
||||
"doctesting",
|
||||
"plugins",
|
||||
]
|
||||
|
@ -70,15 +81,15 @@ jobs:
|
|||
python: "3.7"
|
||||
os: windows-latest
|
||||
tox_env: "py37-numpy"
|
||||
- name: "windows-py37-pluggy"
|
||||
python: "3.7"
|
||||
os: windows-latest
|
||||
tox_env: "py37-pluggymain-pylib-xdist"
|
||||
- name: "windows-py38"
|
||||
python: "3.8"
|
||||
os: windows-latest
|
||||
tox_env: "py38-unittestextras"
|
||||
use_coverage: true
|
||||
- name: "windows-py38-pluggy"
|
||||
python: "3.8"
|
||||
os: windows-latest
|
||||
tox_env: "py38-pluggymain-pylib-xdist"
|
||||
- name: "windows-py39"
|
||||
python: "3.9"
|
||||
os: windows-latest
|
||||
|
@ -101,10 +112,6 @@ jobs:
|
|||
os: ubuntu-latest
|
||||
tox_env: "py37-lsof-numpy-pexpect"
|
||||
use_coverage: true
|
||||
- name: "ubuntu-py37-pluggy"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py37-pluggymain-pylib-xdist"
|
||||
- name: "ubuntu-py37-freeze"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
|
@ -113,6 +120,10 @@ jobs:
|
|||
python: "3.8"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py38-xdist"
|
||||
- name: "ubuntu-py38-pluggy"
|
||||
python: "3.8"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py38-pluggymain-pylib-xdist"
|
||||
- name: "ubuntu-py39"
|
||||
python: "3.9"
|
||||
os: ubuntu-latest
|
||||
|
@ -159,10 +170,6 @@ jobs:
|
|||
os: ubuntu-latest
|
||||
tox_env: "plugins"
|
||||
|
||||
- name: "docs"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
tox_env: "docs"
|
||||
- name: "doctesting"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
|
@ -175,6 +182,12 @@ jobs:
|
|||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download Package
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: Packages
|
||||
path: dist
|
||||
|
||||
- name: Set up Python ${{ matrix.python }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
|
@ -188,11 +201,13 @@ jobs:
|
|||
|
||||
- name: Test without 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
|
||||
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
|
||||
if: "matrix.use_coverage"
|
||||
|
@ -206,10 +221,3 @@ jobs:
|
|||
fail_ci_if_error: true
|
||||
files: ./coverage.xml
|
||||
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: .
|
||||
- requirements: doc/en/requirements.txt
|
||||
|
||||
sphinx:
|
||||
configuration: doc/en/conf.py
|
||||
fail_on_warning: true
|
||||
|
||||
build:
|
||||
os: ubuntu-20.04
|
||||
tools:
|
||||
|
|
10
AUTHORS
10
AUTHORS
|
@ -47,6 +47,7 @@ Ariel Pillemer
|
|||
Armin Rigo
|
||||
Aron Coyle
|
||||
Aron Curzon
|
||||
Arthur Richard
|
||||
Ashish Kurmi
|
||||
Aviral Verma
|
||||
Aviv Palivoda
|
||||
|
@ -166,6 +167,8 @@ Ian Bicking
|
|||
Ian Lesperance
|
||||
Ilya Konstantinov
|
||||
Ionuț Turturică
|
||||
Isaac Virshup
|
||||
Israel Fruchter
|
||||
Itxaso Aizpurua
|
||||
Iwan Briquemont
|
||||
Jaap Broekhuizen
|
||||
|
@ -173,6 +176,7 @@ Jake VanderPlas
|
|||
Jakob van Santen
|
||||
Jakub Mitoraj
|
||||
James Bourbeau
|
||||
James Frost
|
||||
Jan Balster
|
||||
Janne Vanhala
|
||||
Jason R. Coombs
|
||||
|
@ -229,6 +233,7 @@ Maho
|
|||
Maik Figura
|
||||
Mandeep Bhutani
|
||||
Manuel Krebber
|
||||
Marc Mueller
|
||||
Marc Schlaich
|
||||
Marcelo Duarte Trevisani
|
||||
Marcin Bachry
|
||||
|
@ -320,6 +325,7 @@ Ronny Pfannschmidt
|
|||
Ross Lawley
|
||||
Ruaridh Williamson
|
||||
Russel Winder
|
||||
Ryan Puddephatt
|
||||
Ryan Wooden
|
||||
Saiprasad Kale
|
||||
Samuel Colvin
|
||||
|
@ -334,11 +340,13 @@ Serhii Mozghovyi
|
|||
Seth Junot
|
||||
Shantanu Jain
|
||||
Shubham Adep
|
||||
Simon Blanchard
|
||||
Simon Gomizelj
|
||||
Simon Holesch
|
||||
Simon Kerr
|
||||
Skylar Downes
|
||||
Srinivas Reddy Thatiparthy
|
||||
Stefaan Lippens
|
||||
Stefan Farmbauer
|
||||
Stefan Scherfke
|
||||
Stefan Zimmermann
|
||||
|
@ -370,7 +378,9 @@ Tomer Keren
|
|||
Tony Narlock
|
||||
Tor Colvin
|
||||
Trevor Bekolay
|
||||
Tushar Sadhwani
|
||||
Tyler Goodlet
|
||||
Tyler Smart
|
||||
Tzu-ping Chung
|
||||
Vasily Kuznetsov
|
||||
Victor Maryama
|
||||
|
|
|
@ -50,7 +50,7 @@ Fix bugs
|
|||
--------
|
||||
|
||||
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.
|
||||
|
||||
: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.
|
||||
|
||||
#. After all tests pass and the PR has been approved, tag the release commit
|
||||
in the ``release-MAJOR.MINOR.PATCH`` branch and push it. This will publish to PyPI::
|
||||
#. After all tests pass and the PR has been approved, trigger the ``deploy`` job
|
||||
in https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml, using the ``release-MAJOR.MINOR.PATCH`` branch
|
||||
as source.
|
||||
|
||||
git fetch upstream
|
||||
git tag MAJOR.MINOR.PATCH upstream/release-MAJOR.MINOR.PATCH
|
||||
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>`_.
|
||||
This job will require approval from ``pytest-dev/core``, after which it will publish to PyPI
|
||||
and tag the repository.
|
||||
|
||||
#. 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:
|
||||
|
||||
* ``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.
|
||||
* ``doc``: documentation improvement, like rewording an entire session or adding missing docs.
|
||||
* ``deprecation``: feature deprecation.
|
||||
|
|
|
@ -6,6 +6,11 @@ Release announcements
|
|||
: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.1
|
||||
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
|
||||
rootdir: /home/sweet/project
|
||||
collected 0 items
|
||||
cache -- .../_pytest/cacheprovider.py:510
|
||||
cache -- .../_pytest/cacheprovider.py:532
|
||||
Return a cache object that can persist state between testing sessions.
|
||||
|
||||
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()
|
||||
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
|
||||
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`.
|
||||
|
||||
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`
|
||||
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
|
||||
|
||||
caplog -- .../_pytest/logging.py:498
|
||||
caplog -- .../_pytest/logging.py:570
|
||||
Access and control log capturing.
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
=========================
|
||||
|
||||
|
|
|
@ -273,6 +273,9 @@ html_show_sourcelink = False
|
|||
# Output file base name for HTML help builder.
|
||||
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 --------------------------------------------------
|
||||
|
||||
|
|
|
@ -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``.
|
||||
|
||||
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``.
|
||||
|
||||
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
|
||||
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
|
||||
-------------------------------------------------------
|
||||
|
|
|
@ -12,7 +12,7 @@ class YamlFile(pytest.File):
|
|||
# We need a yaml parser, e.g. PyYAML.
|
||||
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()):
|
||||
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
|
||||
if rep.when == "call" and rep.failed:
|
||||
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
|
||||
if "tmp_path" in item.fixturenames:
|
||||
extra = " ({})".format(item.funcargs["tmp_path"])
|
||||
|
@ -1088,4 +1088,4 @@ application with standard ``pytest`` command-line options:
|
|||
|
||||
.. 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
|
||||
|
||||
$ pytest --version
|
||||
pytest 7.3.2
|
||||
pytest 7.4.4
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
|
|
|
@ -54,14 +54,13 @@ operators. (See :ref:`tbreportdemo`). This allows you to use the
|
|||
idiomatic python constructs without boilerplate code while not losing
|
||||
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
|
||||
|
||||
assert a % 2 == 0, "value was odd, should be even"
|
||||
|
||||
then no assertion introspection takes places at all and the message
|
||||
will be simply shown in the traceback.
|
||||
it is printed alongside the assertion introspection in the traceback.
|
||||
|
||||
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
|
||||
---------------------------------------------
|
||||
|
||||
When no tests failed in the last run, or when no cached ``lastfailed`` data was
|
||||
found, ``pytest`` can be configured either to run all of the tests or no tests,
|
||||
using the ``--last-failed-no-failures`` option, which takes one of the following values:
|
||||
The ``--lfnf/--last-failed-no-failures`` option governs the behavior of ``--last-failed``.
|
||||
Determines whether to execute tests when there are no previously (known)
|
||||
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
|
||||
|
||||
pytest --last-failed --last-failed-no-failures all # run all tests (default behavior)
|
||||
pytest --last-failed --last-failed-no-failures none # run no tests and exit
|
||||
pytest --last-failed --last-failed-no-failures all # runs the full test suite (default behavior)
|
||||
pytest --last-failed --last-failed-no-failures none # runs no tests and exits successfully
|
||||
|
||||
The new config.cache object
|
||||
--------------------------------
|
||||
|
|
|
@ -1698,7 +1698,7 @@ and declare its use in a test module via a ``usefixtures`` marker:
|
|||
class TestDirectoryInit:
|
||||
def test_cwd_starts_empty(self):
|
||||
assert os.listdir(os.getcwd()) == []
|
||||
with open("myfile", "w") as f:
|
||||
with open("myfile", "w", encoding="utf-8") as f:
|
||||
f.write("hello")
|
||||
|
||||
def test_cwd_again_starts_empty(self):
|
||||
|
|
|
@ -478,7 +478,7 @@ integration servers, use this invocation:
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
pytest --junitxml=path
|
||||
pytest --junit-xml=path
|
||||
|
||||
to create an XML file at ``path``.
|
||||
|
||||
|
|
|
@ -24,8 +24,8 @@ created in the `base temporary directory`_.
|
|||
d = tmp_path / "sub"
|
||||
d.mkdir()
|
||||
p = d / "hello.txt"
|
||||
p.write_text(CONTENT)
|
||||
assert p.read_text() == CONTENT
|
||||
p.write_text(CONTENT, encoding="utf-8")
|
||||
assert p.read_text(encoding="utf-8") == CONTENT
|
||||
assert len(list(tmp_path.iterdir())) == 1
|
||||
assert 0
|
||||
|
||||
|
@ -51,8 +51,8 @@ Running this would result in a passed test except for the last
|
|||
d = tmp_path / "sub"
|
||||
d.mkdir()
|
||||
p = d / "hello.txt"
|
||||
p.write_text(CONTENT)
|
||||
assert p.read_text() == CONTENT
|
||||
p.write_text(CONTENT, encoding="utf-8")
|
||||
assert p.read_text(encoding="utf-8") == CONTENT
|
||||
assert len(list(tmp_path.iterdir())) == 1
|
||||
> assert 0
|
||||
E assert 0
|
||||
|
|
|
@ -207,10 +207,10 @@ creation of a per-test temporary directory:
|
|||
@pytest.fixture(autouse=True)
|
||||
def initdir(self, tmp_path, monkeypatch):
|
||||
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):
|
||||
with open("samplefile.ini") as f:
|
||||
with open("samplefile.ini", encoding="utf-8") as f:
|
||||
s = f.read()
|
||||
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.
|
||||
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
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
: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/>`_, March 5th to 7th 2024 (3 day in-depth training), Leipzig/Remote
|
||||
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_ (3 day in-depth training):
|
||||
* **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>`.
|
||||
|
||||
|
|
|
@ -90,7 +90,7 @@ and can also be used to hold pytest configuration if they have a ``[pytest]`` se
|
|||
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.
|
||||
|
||||
.. code-block:: ini
|
||||
|
|
|
@ -1,14 +1,26 @@
|
|||
|
||||
.. _plugin-list:
|
||||
|
||||
Plugin List
|
||||
===========
|
||||
Pytest Plugin List
|
||||
==================
|
||||
|
||||
PyPI projects that match "pytest-\*" are considered plugins and are listed
|
||||
automatically together with a manually-maintained list in `the source
|
||||
code <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
|
||||
Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
|
||||
It includes PyPI projects whose names begin with "pytest-" and a handful of manually selected projects.
|
||||
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
|
||||
creating a PDF, because otherwise the table gets far too wide for the
|
||||
page.
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
:tocdepth: 3
|
||||
|
||||
.. _`api-reference`:
|
||||
|
||||
API Reference
|
||||
|
@ -77,11 +79,13 @@ pytest.xfail
|
|||
pytest.exit
|
||||
~~~~~~~~~~~
|
||||
|
||||
.. autofunction:: pytest.exit(reason, [returncode=False, msg=None])
|
||||
.. autofunction:: pytest.exit(reason, [returncode=None, msg=None])
|
||||
|
||||
pytest.main
|
||||
~~~~~~~~~~~
|
||||
|
||||
**Tutorial**: :ref:`pytest.main-usage`
|
||||
|
||||
.. autofunction:: pytest.main
|
||||
|
||||
pytest.param
|
||||
|
@ -783,18 +787,66 @@ reporting or interaction with exceptions:
|
|||
.. 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.CallInfo()
|
||||
.. autoclass:: _pytest.nodes.Node()
|
||||
: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
|
||||
~~~~~
|
||||
|
@ -803,13 +855,34 @@ Class
|
|||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Collector
|
||||
~~~~~~~~~
|
||||
Function
|
||||
~~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Collector()
|
||||
.. autoclass:: pytest.Function()
|
||||
:members:
|
||||
: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
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
|
@ -837,13 +910,6 @@ ExitCode
|
|||
.. autoclass:: pytest.ExitCode
|
||||
:members:
|
||||
|
||||
File
|
||||
~~~~
|
||||
|
||||
.. autoclass:: pytest.File()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
FixtureDef
|
||||
~~~~~~~~~~
|
||||
|
@ -852,34 +918,6 @@ FixtureDef
|
|||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
FSCollector
|
||||
~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: _pytest.nodes.FSCollector()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Function
|
||||
~~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Function()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
FunctionDefinition
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: _pytest.python.FunctionDefinition()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Item
|
||||
~~~~
|
||||
|
||||
.. autoclass:: pytest.Item()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
MarkDecorator
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
|
@ -907,19 +945,6 @@ Metafunc
|
|||
.. autoclass:: pytest.Metafunc()
|
||||
:members:
|
||||
|
||||
Module
|
||||
~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Module()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Node
|
||||
~~~~
|
||||
|
||||
.. autoclass:: _pytest.nodes.Node()
|
||||
:members:
|
||||
|
||||
Parser
|
||||
~~~~~~
|
||||
|
||||
|
@ -941,13 +966,6 @@ PytestPluginManager
|
|||
:inherited-members:
|
||||
:show-inheritance:
|
||||
|
||||
Session
|
||||
~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Session()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
TestReport
|
||||
~~~~~~~~~~
|
||||
|
||||
|
@ -962,10 +980,10 @@ TestShortLogReport
|
|||
.. autoclass:: pytest.TestShortLogReport()
|
||||
: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
|
||||
~~~~~
|
||||
|
@ -1871,8 +1889,12 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
|||
tests. Optional argument: glob (default: '*').
|
||||
--cache-clear Remove all cache contents at start of test run
|
||||
--lfnf={all,none}, --last-failed-no-failures={all,none}
|
||||
Which tests to run with no previously (known)
|
||||
failures
|
||||
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.
|
||||
--sw, --stepwise Exit on test failure and continue from last failing
|
||||
test next time
|
||||
--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
|
||||
the configuration file raise errors
|
||||
--strict (Deprecated) alias to --strict-markers
|
||||
-c, --config-file FILE
|
||||
-c FILE, --config-file=FILE
|
||||
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
|
||||
Force test execution even if collection errors occur
|
||||
--rootdir=ROOTDIR Define root directory for tests. Can be relative
|
||||
|
|
|
@ -31,10 +31,22 @@ class InvalidFeatureRelease(Exception):
|
|||
SLUG = "pytest-dev/pytest"
|
||||
|
||||
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
|
||||
can be released by pushing a tag `{version}` to this repository.
|
||||
Once all builds pass and it has been **approved** by one or more maintainers, start the \
|
||||
[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"""
|
||||
.. _plugin-list:
|
||||
|
||||
Plugin List
|
||||
===========
|
||||
Pytest Plugin List
|
||||
==================
|
||||
|
||||
PyPI projects that match "pytest-\*" are considered plugins and are listed
|
||||
automatically together with a manually-maintained list in `the source
|
||||
code <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
|
||||
Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
|
||||
It includes PyPI projects whose names begin with "pytest-" and a handful of manually selected projects.
|
||||
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
|
||||
creating a PDF, because otherwise the table gets far too wide for the
|
||||
page.
|
||||
|
|
|
@ -13,6 +13,7 @@ import struct
|
|||
import sys
|
||||
import tokenize
|
||||
import types
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from pathlib import PurePath
|
||||
from typing import Callable
|
||||
|
@ -56,6 +57,10 @@ else:
|
|||
astNum = ast.Num
|
||||
|
||||
|
||||
class Sentinel:
|
||||
pass
|
||||
|
||||
|
||||
assertstate_key = StashKey["AssertionState"]()
|
||||
|
||||
# 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_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):
|
||||
"""PEP302/PEP451 import hook which rewrites asserts."""
|
||||
|
@ -596,6 +604,13 @@ def _get_assertion_exprs(src: bytes) -> Dict[int, str]:
|
|||
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):
|
||||
"""Assertion rewriting implementation.
|
||||
|
||||
|
@ -645,6 +660,8 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||
.push_format_context() and .pop_format_context() which allows
|
||||
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
|
||||
that change value within an assert. This happens when a variable is
|
||||
reassigned with the walrus operator
|
||||
|
@ -666,7 +683,10 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||
else:
|
||||
self.enable_assertion_pass_hook = False
|
||||
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:
|
||||
"""Find all assert statements in *mod* and rewrite them."""
|
||||
|
@ -687,11 +707,10 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||
expect_docstring
|
||||
and isinstance(item, ast.Expr)
|
||||
and isinstance(item.value, astStr)
|
||||
and isinstance(_get_ast_constant_value(item.value), str)
|
||||
):
|
||||
if sys.version_info >= (3, 8):
|
||||
doc = item.value.value
|
||||
else:
|
||||
doc = item.value.s
|
||||
doc = _get_ast_constant_value(item.value)
|
||||
assert isinstance(doc, str)
|
||||
if self.is_rewrite_disabled(doc):
|
||||
return
|
||||
expect_docstring = False
|
||||
|
@ -732,9 +751,17 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||
mod.body[pos:pos] = imports
|
||||
|
||||
# Collect asserts.
|
||||
nodes: List[ast.AST] = [mod]
|
||||
self.scope = (mod,)
|
||||
nodes: List[Union[ast.AST, Sentinel]] = [mod]
|
||||
while nodes:
|
||||
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):
|
||||
if isinstance(field, list):
|
||||
new: List[ast.AST] = []
|
||||
|
@ -1005,7 +1032,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||
]
|
||||
):
|
||||
pytest_temp = self.variable()
|
||||
self.variables_overwrite[
|
||||
self.variables_overwrite[self.scope][
|
||||
v.left.target.id
|
||||
] = v.left # type:ignore[assignment]
|
||||
v.left.target.id = pytest_temp
|
||||
|
@ -1048,17 +1075,20 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||
new_args = []
|
||||
new_kwargs = []
|
||||
for arg in call.args:
|
||||
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite:
|
||||
arg = self.variables_overwrite[arg.id] # type:ignore[assignment]
|
||||
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get(
|
||||
self.scope, {}
|
||||
):
|
||||
arg = self.variables_overwrite[self.scope][
|
||||
arg.id
|
||||
] # type:ignore[assignment]
|
||||
res, expl = self.visit(arg)
|
||||
arg_expls.append(expl)
|
||||
new_args.append(res)
|
||||
for keyword in call.keywords:
|
||||
if (
|
||||
isinstance(keyword.value, ast.Name)
|
||||
and keyword.value.id in self.variables_overwrite
|
||||
):
|
||||
keyword.value = self.variables_overwrite[
|
||||
if isinstance(
|
||||
keyword.value, ast.Name
|
||||
) and keyword.value.id in self.variables_overwrite.get(self.scope, {}):
|
||||
keyword.value = self.variables_overwrite[self.scope][
|
||||
keyword.value.id
|
||||
] # type:ignore[assignment]
|
||||
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]:
|
||||
self.push_format_context()
|
||||
# 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:
|
||||
comp.left = self.variables_overwrite[
|
||||
if isinstance(
|
||||
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
|
||||
] # type:ignore[assignment]
|
||||
if isinstance(comp.left, namedExpr):
|
||||
self.variables_overwrite[
|
||||
self.variables_overwrite[self.scope][
|
||||
comp.left.target.id
|
||||
] = comp.left # type:ignore[assignment]
|
||||
left_res, left_expl = self.visit(comp.left)
|
||||
|
@ -1119,7 +1151,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||
and next_operand.target.id == left_res.id
|
||||
):
|
||||
next_operand.target.id = self.variable()
|
||||
self.variables_overwrite[
|
||||
self.variables_overwrite[self.scope][
|
||||
left_res.id
|
||||
] = next_operand # type:ignore[assignment]
|
||||
next_res, next_expl = self.visit(next_operand)
|
||||
|
|
|
@ -132,7 +132,7 @@ def isiterable(obj: Any) -> bool:
|
|||
try:
|
||||
iter(obj)
|
||||
return not istext(obj)
|
||||
except TypeError:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
|
|
|
@ -505,7 +505,11 @@ def pytest_addoption(parser: Parser) -> None:
|
|||
dest="last_failed_no_failures",
|
||||
choices=("all", "none"),
|
||||
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:
|
||||
"""Return the current user id, or None if we cannot get it reliably on the current platform."""
|
||||
# win32 does not have a getuid() function.
|
||||
# On Emscripten, getuid() is a stub that always returns 0.
|
||||
if sys.platform in ("win32", "emscripten"):
|
||||
"""Return the current process's real user id or None if it could not be
|
||||
determined.
|
||||
|
||||
: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
|
||||
# getuid shouldn't fail, but cpython defines such a case.
|
||||
# Let's hope for the best.
|
||||
uid = os.getuid()
|
||||
return uid if uid != -1 else None
|
||||
else:
|
||||
# On other platforms, a return value of -1 is assumed to indicate that
|
||||
# the current process's real user id could not be determined.
|
||||
ERROR = -1
|
||||
uid = os.getuid()
|
||||
return uid if uid != ERROR else None
|
||||
|
||||
|
||||
# Perform exhaustiveness checking.
|
||||
|
|
|
@ -57,6 +57,7 @@ from _pytest.pathlib import bestrelpath
|
|||
from _pytest.pathlib import import_path
|
||||
from _pytest.pathlib import ImportMode
|
||||
from _pytest.pathlib import resolve_package_path
|
||||
from _pytest.pathlib import safe_exists
|
||||
from _pytest.stash import Stash
|
||||
from _pytest.warning_types import PytestConfigWarning
|
||||
from _pytest.warning_types import warn_explicit_for
|
||||
|
@ -137,7 +138,9 @@ def main(
|
|||
) -> Union[int, ExitCode]:
|
||||
"""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.
|
||||
|
||||
:returns: An exit code.
|
||||
|
@ -442,10 +445,10 @@ class PytestPluginManager(PluginManager):
|
|||
# so we avoid accessing possibly non-readable attributes
|
||||
# (see issue #1073).
|
||||
if not name.startswith("pytest_"):
|
||||
return
|
||||
return None
|
||||
# Ignore names which can not be hooks.
|
||||
if name == "pytest_plugins":
|
||||
return
|
||||
return None
|
||||
|
||||
opts = super().parse_hookimpl_opts(plugin, name)
|
||||
if opts is not None:
|
||||
|
@ -454,9 +457,9 @@ class PytestPluginManager(PluginManager):
|
|||
method = getattr(plugin, name)
|
||||
# Consider only actual functions for hooks (#3775).
|
||||
if not inspect.isroutine(method):
|
||||
return
|
||||
return None
|
||||
# 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")
|
||||
)
|
||||
|
||||
|
@ -465,7 +468,7 @@ class PytestPluginManager(PluginManager):
|
|||
if opts is None:
|
||||
method = getattr(module_or_class, name)
|
||||
if name.startswith("pytest_"):
|
||||
opts = _get_legacy_hook_marks(
|
||||
opts = _get_legacy_hook_marks( # type: ignore[assignment]
|
||||
method,
|
||||
"spec",
|
||||
("firstresult", "historic"),
|
||||
|
@ -555,12 +558,8 @@ class PytestPluginManager(PluginManager):
|
|||
anchor = absolutepath(current / path)
|
||||
|
||||
# Ensure we do not break if what appears to be an anchor
|
||||
# is in fact a very long option (#10169).
|
||||
try:
|
||||
anchor_exists = anchor.exists()
|
||||
except OSError: # pragma: no cover
|
||||
anchor_exists = False
|
||||
if anchor_exists:
|
||||
# is in fact a very long option (#10169, #11394).
|
||||
if safe_exists(anchor):
|
||||
self._try_load_conftest(anchor, importmode, rootpath)
|
||||
foundanchor = True
|
||||
if not foundanchor:
|
||||
|
@ -1063,9 +1062,10 @@ class Config:
|
|||
fin()
|
||||
|
||||
def get_terminal_writer(self) -> TerminalWriter:
|
||||
terminalreporter: TerminalReporter = self.pluginmanager.get_plugin(
|
||||
terminalreporter: Optional[TerminalReporter] = self.pluginmanager.get_plugin(
|
||||
"terminalreporter"
|
||||
)
|
||||
assert terminalreporter is not None
|
||||
return terminalreporter._tw
|
||||
|
||||
def pytest_cmdline_parse(
|
||||
|
|
|
@ -16,6 +16,7 @@ from .exceptions import UsageError
|
|||
from _pytest.outcomes import fail
|
||||
from _pytest.pathlib import absolutepath
|
||||
from _pytest.pathlib import commonpath
|
||||
from _pytest.pathlib import safe_exists
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import Config
|
||||
|
@ -151,14 +152,6 @@ def get_dirs_from_args(args: Iterable[str]) -> List[Path]:
|
|||
return path
|
||||
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
|
||||
possible_paths = (
|
||||
absolutepath(get_file_part_from_node_id(arg))
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""Discover and run doctests in modules and test files."""
|
||||
import bdb
|
||||
import functools
|
||||
import inspect
|
||||
import os
|
||||
import platform
|
||||
|
@ -536,6 +537,25 @@ class DoctestModule(Module):
|
|||
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":
|
||||
module = self.config.pluginmanager._importconftest(
|
||||
self.path,
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import io
|
||||
import os
|
||||
import sys
|
||||
from typing import Generator
|
||||
|
@ -10,8 +9,8 @@ from _pytest.nodes import Item
|
|||
from _pytest.stash import StashKey
|
||||
|
||||
|
||||
fault_handler_original_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:
|
||||
|
@ -25,8 +24,15 @@ def pytest_addoption(parser: Parser) -> None:
|
|||
def pytest_configure(config: Config) -> None:
|
||||
import faulthandler
|
||||
|
||||
config.stash[fault_handler_stderr_fd_key] = os.dup(get_stderr_fileno())
|
||||
config.stash[fault_handler_originally_enabled_key] = faulthandler.is_enabled()
|
||||
# at teardown we want to restore the original faulthandler fileno
|
||||
# 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])
|
||||
|
||||
|
||||
|
@ -38,9 +44,10 @@ def pytest_unconfigure(config: Config) -> None:
|
|||
if fault_handler_stderr_fd_key in config.stash:
|
||||
os.close(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.
|
||||
faulthandler.enable(file=get_stderr_fileno())
|
||||
# Re-enable the faulthandler if it was originally enabled.
|
||||
if fault_handler_original_stderr_fd_key in config.stash:
|
||||
faulthandler.enable(config.stash[fault_handler_original_stderr_fd_key])
|
||||
del config.stash[fault_handler_original_stderr_fd_key]
|
||||
|
||||
|
||||
def get_stderr_fileno() -> int:
|
||||
|
@ -51,7 +58,7 @@ def get_stderr_fileno() -> int:
|
|||
if fileno == -1:
|
||||
raise AttributeError()
|
||||
return fileno
|
||||
except (AttributeError, io.UnsupportedOperation):
|
||||
except (AttributeError, ValueError):
|
||||
# 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
|
||||
# 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 PrintHelp
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.terminal import TerminalReporter
|
||||
|
||||
|
||||
class HelpAction(Action):
|
||||
|
@ -159,7 +160,10 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
|
|||
def showhelp(config: Config) -> None:
|
||||
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.write(config._parser.optparser.format_help())
|
||||
tw.line()
|
||||
|
|
|
@ -369,7 +369,7 @@ def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object]
|
|||
__tracebackhide__ = True
|
||||
|
||||
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
|
||||
_check_record_param_type("name", name)
|
||||
|
||||
|
@ -502,6 +502,10 @@ class LogXML:
|
|||
# Local hack to handle xdist report order.
|
||||
workernode = getattr(report, "node", None)
|
||||
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:
|
||||
reporter.finalize()
|
||||
|
||||
|
@ -599,9 +603,6 @@ class LogXML:
|
|||
reporter = self._opentestcase(report)
|
||||
reporter.write_captured_output(report)
|
||||
|
||||
for propname, propvalue in report.user_properties:
|
||||
reporter.add_property(propname, str(propvalue))
|
||||
|
||||
self.finalize(report)
|
||||
report_wid = getattr(report, "worker_id", None)
|
||||
report_ii = getattr(report, "item_index", None)
|
||||
|
|
|
@ -660,6 +660,8 @@ class LoggingPlugin:
|
|||
)
|
||||
if self._log_cli_enabled():
|
||||
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")
|
||||
# if capturemanager plugin is disabled, live logging still works.
|
||||
self.log_cli_handler: Union[
|
||||
|
|
|
@ -36,6 +36,7 @@ from _pytest.outcomes import exit
|
|||
from _pytest.pathlib import absolutepath
|
||||
from _pytest.pathlib import bestrelpath
|
||||
from _pytest.pathlib import fnmatch_ex
|
||||
from _pytest.pathlib import safe_exists
|
||||
from _pytest.pathlib import visit
|
||||
from _pytest.reports import CollectReport
|
||||
from _pytest.reports import TestReport
|
||||
|
@ -462,6 +463,11 @@ class _bestrelpath_cache(Dict[Path, str]):
|
|||
|
||||
@final
|
||||
class Session(nodes.FSCollector):
|
||||
"""The root of the collection tree.
|
||||
|
||||
``Session`` collects the initial paths given as arguments to pytest.
|
||||
"""
|
||||
|
||||
Interrupted = Interrupted
|
||||
Failed = Failed
|
||||
# Set on the session by runner.pytest_sessionstart.
|
||||
|
@ -890,7 +896,7 @@ def resolve_collection_argument(
|
|||
strpath = search_pypath(strpath)
|
||||
fspath = invocation_path / strpath
|
||||
fspath = absolutepath(fspath)
|
||||
if not fspath.exists():
|
||||
if not safe_exists(fspath):
|
||||
msg = (
|
||||
"module or package not found: {arg} (missing __init__.py?)"
|
||||
if as_pypath
|
||||
|
|
|
@ -373,7 +373,9 @@ def get_unpacked_marks(
|
|||
if not consider_mro:
|
||||
mark_lists = [obj.__dict__.get("pytestmark", [])]
|
||||
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 = []
|
||||
for item in mark_lists:
|
||||
if isinstance(item, list):
|
||||
|
|
|
@ -157,10 +157,11 @@ class NodeMeta(type):
|
|||
|
||||
|
||||
class Node(metaclass=NodeMeta):
|
||||
"""Base class for Collector and Item, the components of the test
|
||||
collection tree.
|
||||
r"""Base class of :class:`Collector` and :class:`Item`, the components of
|
||||
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.
|
||||
|
@ -525,15 +526,17 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i
|
|||
|
||||
|
||||
class Collector(Node):
|
||||
"""Collector instances create children through collect() and thus
|
||||
iteratively build a tree."""
|
||||
"""Base class of all collectors.
|
||||
|
||||
Collector create children through `collect()` and thus iteratively build
|
||||
the collection tree.
|
||||
"""
|
||||
|
||||
class CollectError(Exception):
|
||||
"""An error during collection, contains a custom message."""
|
||||
|
||||
def collect(self) -> Iterable[Union["Item", "Collector"]]:
|
||||
"""Return a list of children (items and collectors) for this
|
||||
collection node."""
|
||||
"""Collect children (items and collectors) for this collector."""
|
||||
raise NotImplementedError("abstract")
|
||||
|
||||
# TODO: This omits the style= parameter which breaks Liskov Substitution.
|
||||
|
@ -564,7 +567,7 @@ class Collector(Node):
|
|||
ntraceback = traceback.cut(path=self.path)
|
||||
if ntraceback == traceback:
|
||||
ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
|
||||
return excinfo.traceback.filter(excinfo)
|
||||
return ntraceback.filter(excinfo)
|
||||
return excinfo.traceback
|
||||
|
||||
|
||||
|
@ -577,6 +580,8 @@ def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[
|
|||
|
||||
|
||||
class FSCollector(Collector):
|
||||
"""Base class for filesystem collectors."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fspath: Optional[LEGACY_PATH] = None,
|
||||
|
@ -660,7 +665,7 @@ class File(FSCollector):
|
|||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
|
|
@ -123,7 +123,7 @@ def exit(
|
|||
only because `msg` is deprecated.
|
||||
|
||||
: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:
|
||||
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:
|
||||
module_name = module_name_from_path(path, root)
|
||||
with contextlib.suppress(KeyError):
|
||||
return sys.modules[module_name]
|
||||
|
||||
for meta_importer in sys.meta_path:
|
||||
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.
|
||||
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)
|
||||
|
||||
|
||||
|
@ -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__``.
|
||||
"""
|
||||
module_parts = module_name.split(".")
|
||||
child_module: Union[ModuleType, None] = None
|
||||
module: Union[ModuleType, None] = None
|
||||
child_name: str = ""
|
||||
while module_name:
|
||||
if module_name not in modules:
|
||||
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.
|
||||
if not sys.meta_path:
|
||||
raise ModuleNotFoundError
|
||||
importlib.import_module(module_name)
|
||||
module = importlib.import_module(module_name)
|
||||
except ModuleNotFoundError:
|
||||
module = ModuleType(
|
||||
module_name,
|
||||
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
|
||||
# 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_name = ".".join(module_parts)
|
||||
|
||||
|
@ -773,3 +792,13 @@ def copytree(source: Path, target: Path) -> None:
|
|||
shutil.copyfile(x, newx)
|
||||
elif x.is_dir():
|
||||
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:
|
||||
"""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)
|
||||
return reprec
|
||||
|
||||
|
@ -1074,7 +1074,7 @@ class Pytester:
|
|||
return self.inline_run(*values)
|
||||
|
||||
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
|
||||
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):
|
||||
"""Collector for test classes and functions."""
|
||||
"""Collector for test classes and functions in a Python module."""
|
||||
|
||||
def _getobj(self):
|
||||
return self._importtestmodule()
|
||||
|
@ -659,6 +659,9 @@ class Module(nodes.File, PyCollector):
|
|||
|
||||
|
||||
class Package(Module):
|
||||
"""Collector for files and directories in a Python packages -- directories
|
||||
with an `__init__.py` file."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fspath: Optional[LEGACY_PATH],
|
||||
|
@ -788,7 +791,7 @@ def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> Optional[o
|
|||
|
||||
|
||||
class Class(PyCollector):
|
||||
"""Collector for test methods."""
|
||||
"""Collector for test methods (and nested classes) in a Python class."""
|
||||
|
||||
@classmethod
|
||||
def from_parent(cls, parent, *, name, obj=None, **kw):
|
||||
|
@ -1149,7 +1152,7 @@ class CallSpec2:
|
|||
arg2scope = self._arg2scope.copy()
|
||||
for arg, val in zip(argnames, valset):
|
||||
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]
|
||||
if valtype_for_arg == "params":
|
||||
params[arg] = val
|
||||
|
@ -1240,8 +1243,9 @@ class Metafunc:
|
|||
during the collection phase. If you need to setup expensive resources
|
||||
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
|
||||
previous parametrizations, e.g.
|
||||
Can be called multiple times per test function (but only on different
|
||||
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):
|
||||
"""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:
|
||||
The full function name, including any decorations like those
|
||||
|
@ -1830,10 +1834,8 @@ class Function(PyobjMixin, nodes.Item):
|
|||
|
||||
|
||||
class FunctionDefinition(Function):
|
||||
"""
|
||||
This class is a step gap solution until we evolve to have actual function definition nodes
|
||||
and manage to get rid of ``metafunc``.
|
||||
"""
|
||||
"""This class is a stop gap solution until we evolve to have actual function
|
||||
definition nodes and manage to get rid of ``metafunc``."""
|
||||
|
||||
def runtest(self) -> None:
|
||||
raise RuntimeError("function definitions are not supposed to be run as tests")
|
||||
|
|
|
@ -266,19 +266,20 @@ class ApproxMapping(ApproxBase):
|
|||
approx_side_as_map.items(), other_side.values()
|
||||
):
|
||||
if approx_value != 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 is not None and other_value is not None:
|
||||
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
|
||||
),
|
||||
)
|
||||
different_ids.append(approx_key)
|
||||
|
||||
message_data = [
|
||||
|
|
|
@ -868,6 +868,9 @@ class TestLocalPath(CommonFSTests):
|
|||
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):
|
||||
# https://github.com/pytest-dev/py/issues/30
|
||||
with multiprocessing.Pool() as pool:
|
||||
|
|
|
@ -1317,3 +1317,38 @@ def test_function_return_non_none_warning(pytester: Pytester) -> None:
|
|||
)
|
||||
res = pytester.runpytest()
|
||||
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)
|
||||
|
||||
|
||||
@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)
|
||||
def pytest_collection_modifyitems(items):
|
||||
"""Prefer faster tests.
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# mypy: disable-error-code="attr-defined"
|
||||
# mypy: disallow-untyped-defs
|
||||
import logging
|
||||
from typing import Iterator
|
||||
|
||||
import pytest
|
||||
from _pytest.logging import caplog_records_key
|
||||
|
@ -9,8 +11,8 @@ logger = logging.getLogger(__name__)
|
|||
sublogger = logging.getLogger(__name__ + ".baz")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cleanup_disabled_logging():
|
||||
@pytest.fixture(autouse=True)
|
||||
def cleanup_disabled_logging() -> Iterator[None]:
|
||||
"""Simple fixture that ensures that a test doesn't disable 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*"])
|
||||
|
||||
|
||||
def test_change_level(caplog):
|
||||
def test_change_level(caplog: pytest.LogCaptureFixture) -> None:
|
||||
caplog.set_level(logging.INFO)
|
||||
logger.debug("handler DEBUG level")
|
||||
logger.info("handler INFO level")
|
||||
|
@ -42,7 +44,7 @@ def test_change_level(caplog):
|
|||
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)
|
||||
assert logging.root.manager.disable == logging.CRITICAL
|
||||
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*")
|
||||
|
||||
|
||||
def test_change_disabled_level_undo(
|
||||
pytester: Pytester, cleanup_disabled_logging
|
||||
) -> None:
|
||||
def test_change_disabled_level_undo(pytester: Pytester) -> None:
|
||||
"""Ensure that '_force_enable_logging' in 'set_level' is undone after the end of the test.
|
||||
|
||||
Tests the logging output themselves (affected by disabled logging level).
|
||||
|
@ -144,7 +144,7 @@ def test_change_level_undos_handler_level(pytester: Pytester) -> None:
|
|||
result.assert_outcomes(passed=3)
|
||||
|
||||
|
||||
def test_with_statement(caplog):
|
||||
def test_with_statement(caplog: pytest.LogCaptureFixture) -> None:
|
||||
with caplog.at_level(logging.INFO):
|
||||
logger.debug("handler DEBUG level")
|
||||
logger.info("handler INFO level")
|
||||
|
@ -159,7 +159,7 @@ def test_with_statement(caplog):
|
|||
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)
|
||||
assert logging.root.manager.disable == logging.CRITICAL
|
||||
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(
|
||||
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.
|
||||
|
||||
``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
|
||||
|
||||
|
||||
def test_log_access(caplog):
|
||||
def test_log_access(caplog: pytest.LogCaptureFixture) -> None:
|
||||
caplog.set_level(logging.INFO)
|
||||
logger.info("boo %s", "arg")
|
||||
assert caplog.records[0].levelname == "INFO"
|
||||
|
@ -226,7 +226,7 @@ def test_log_access(caplog):
|
|||
assert "boo arg" in caplog.text
|
||||
|
||||
|
||||
def test_messages(caplog):
|
||||
def test_messages(caplog: pytest.LogCaptureFixture) -> None:
|
||||
caplog.set_level(logging.INFO)
|
||||
logger.info("boo %s", "arg")
|
||||
logger.info("bar %s\nbaz %s", "arg1", "arg2")
|
||||
|
@ -247,14 +247,14 @@ def test_messages(caplog):
|
|||
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)
|
||||
logger.info("boo %s", "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)
|
||||
logger.info("bū")
|
||||
assert caplog.records[0].levelname == "INFO"
|
||||
|
@ -262,7 +262,7 @@ def test_unicode(caplog):
|
|||
assert "bū" in caplog.text
|
||||
|
||||
|
||||
def test_clear(caplog):
|
||||
def test_clear(caplog: pytest.LogCaptureFixture) -> None:
|
||||
caplog.set_level(logging.INFO)
|
||||
logger.info("bū")
|
||||
assert len(caplog.records)
|
||||
|
@ -273,7 +273,9 @@ def test_clear(caplog):
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def logging_during_setup_and_teardown(caplog):
|
||||
def logging_during_setup_and_teardown(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> Iterator[None]:
|
||||
caplog.set_level("INFO")
|
||||
logger.info("a_setup_log")
|
||||
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"]
|
||||
|
||||
|
||||
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.get_records("call")
|
||||
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"]
|
||||
|
||||
# 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")
|
||||
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 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()
|
||||
|
||||
assert caplog.get_records("call") == []
|
||||
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")
|
||||
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 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:
|
||||
|
|
|
@ -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(
|
||||
[1.0, 2.0, 3.0, 4.0],
|
||||
[1.0, 3.0, 3.0, 5.0],
|
||||
|
|
|
@ -1493,7 +1493,7 @@ class TestMetafuncFunctional:
|
|||
pass
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest("--collectonly")
|
||||
result = pytester.runpytest("--collect-only")
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"collected 0 items / 1 error",
|
||||
|
|
|
@ -686,6 +686,25 @@ class TestAssertionRewrite:
|
|||
assert msg is not None
|
||||
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 f() -> None:
|
||||
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)"])
|
||||
|
||||
|
||||
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(
|
||||
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)
|
||||
result = pytester.runpytest("-vv")
|
||||
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.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:
|
||||
def test_ignore_collect_path(self, pytester: Pytester) -> None:
|
||||
|
|
|
@ -482,6 +482,27 @@ class TestDoctests:
|
|||
reprec = pytester.inline_run(p, "--doctest-modules")
|
||||
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):
|
||||
p = pytester.mkpydir("hello")
|
||||
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 *"])
|
||||
|
||||
|
||||
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(
|
||||
pytester: Pytester, run_and_parse: RunAndParse
|
||||
) -> None:
|
||||
|
|
|
@ -262,3 +262,34 @@ def test_module_full_path_without_drive(pytester: Pytester) -> None:
|
|||
"* 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)
|
||||
|
||||
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]
|
||||
|
||||
|
||||
# @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:
|
||||
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:
|
||||
encoding = locale.getpreferredencoding(False)
|
||||
try:
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import errno
|
||||
import os.path
|
||||
import pickle
|
||||
import sys
|
||||
|
@ -7,6 +8,7 @@ from textwrap import dedent
|
|||
from types import ModuleType
|
||||
from typing import Any
|
||||
from typing import Generator
|
||||
from typing import Iterator
|
||||
|
||||
import pytest
|
||||
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_lock_path
|
||||
from _pytest.pathlib import import_path
|
||||
from _pytest.pathlib import ImportMode
|
||||
from _pytest.pathlib import ImportPathMismatchError
|
||||
from _pytest.pathlib import insert_missing_modules
|
||||
from _pytest.pathlib import maybe_delete_a_numbered_dir
|
||||
from _pytest.pathlib import module_name_from_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 visit
|
||||
from _pytest.pytester import Pytester
|
||||
from _pytest.tmpdir import TempPathFactory
|
||||
|
||||
|
||||
|
@ -282,29 +287,36 @@ class TestImportPath:
|
|||
import_path(tmp_path / "invalid.py", root=tmp_path)
|
||||
|
||||
@pytest.fixture
|
||||
def simple_module(self, tmp_path: Path) -> Path:
|
||||
fn = tmp_path / "_src/tests/mymod.py"
|
||||
def simple_module(
|
||||
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.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."""
|
||||
module = import_path(simple_module, mode="importlib", root=tmp_path)
|
||||
assert module.foo(2) == 42 # type: ignore[attr-defined]
|
||||
assert str(simple_module.parent) not in sys.path
|
||||
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.tests" in sys.modules
|
||||
|
||||
def test_importmode_twice_is_different_module(
|
||||
def test_remembers_previous_imports(
|
||||
self, simple_module: Path, tmp_path: Path
|
||||
) -> 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)
|
||||
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(
|
||||
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
|
||||
import importlib.util
|
||||
|
||||
# Force module to be re-imported.
|
||||
del sys.modules[module.__name__]
|
||||
|
||||
monkeypatch.setattr(
|
||||
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"))
|
||||
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(
|
||||
self, monkeypatch: MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
|
@ -592,3 +615,100 @@ class TestImportLibMode:
|
|||
modules = {}
|
||||
insert_missing_modules(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.__dict__["pytest_plugins"] = ["pytest_p1", "pytest_p2"]
|
||||
pytestpm.consider_module(mod)
|
||||
assert pytestpm.get_plugin("pytest_p1").__name__ == "pytest_p1"
|
||||
assert pytestpm.get_plugin("pytest_p2").__name__ == "pytest_p2"
|
||||
p1 = pytestpm.get_plugin("pytest_p1")
|
||||
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(
|
||||
self, pytester: Pytester, _config_for_test: Config
|
||||
|
@ -336,6 +340,7 @@ class TestPytestPluginManager:
|
|||
len2 = len(pytestpm.get_plugins())
|
||||
assert len1 == len2
|
||||
plugin1 = pytestpm.get_plugin("pytest_hello")
|
||||
assert plugin1 is not None
|
||||
assert plugin1.__name__.endswith("pytest_hello")
|
||||
plugin2 = pytestpm.get_plugin("pytest_hello")
|
||||
assert plugin2 is plugin1
|
||||
|
@ -351,6 +356,7 @@ class TestPytestPluginManager:
|
|||
pluginname = "pkg.plug"
|
||||
pytestpm.import_plugin(pluginname)
|
||||
mod = pytestpm.get_plugin("pkg.plug")
|
||||
assert mod is not None
|
||||
assert mod.x == 3
|
||||
|
||||
def test_consider_conftest_deps(
|
||||
|
|
Loading…
Reference in New Issue