Merge branch 'main' into test-case-verbosity
This commit is contained in:
commit
6bae32378e
|
@ -23,6 +23,11 @@ afc607cfd81458d4e4f3b1f3cf8cc931b933907e
|
|||
5f95dce95602921a70bfbc7d8de2f7712c5e4505
|
||||
# ran pyupgrade-docs again
|
||||
75d0b899bbb56d6849e9d69d83a9426ed3f43f8b
|
||||
|
||||
# move argument parser to own file
|
||||
c9df77cbd6a365dcb73c39618e4842711817e871
|
||||
# Replace reorder-python-imports by isort due to black incompatibility (#11896)
|
||||
8b54596639f41dfac070030ef20394b9001fe63c
|
||||
# Run blacken-docs with black's 2024's style
|
||||
4546d5445aaefe6a03957db028c263521dfb5c4b
|
||||
# Migration to ruff / ruff format
|
||||
4588653b2497ed25976b7aaff225b889fb476756
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
persist-credentials: false
|
||||
|
||||
- name: Build and Check Package
|
||||
uses: hynek/build-and-inspect-python-package@v2.0.0
|
||||
uses: hynek/build-and-inspect-python-package@v2.0.1
|
||||
|
||||
deploy:
|
||||
if: github.repository == 'pytest-dev/pytest'
|
||||
|
@ -72,6 +72,12 @@ jobs:
|
|||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download Package
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: Packages
|
||||
path: dist
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
|
|
|
@ -35,7 +35,7 @@ jobs:
|
|||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Build and Check Package
|
||||
uses: hynek/build-and-inspect-python-package@v2.0.0
|
||||
uses: hynek/build-and-inspect-python-package@v2.0.1
|
||||
|
||||
build:
|
||||
needs: [package]
|
||||
|
@ -205,7 +205,7 @@ jobs:
|
|||
|
||||
- name: Upload coverage to Codecov
|
||||
if: "matrix.use_coverage"
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
|
|
|
@ -30,7 +30,7 @@ jobs:
|
|||
python-version: "3.11"
|
||||
cache: pip
|
||||
- name: requests-cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pytest-plugin-list/
|
||||
key: plugins-http-cache-${{ github.run_id }} # Can use time based key as well
|
||||
|
@ -46,7 +46,7 @@ jobs:
|
|||
run: python scripts/update-plugin-list.py
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38
|
||||
uses: peter-evans/create-pull-request@b1ddad2c994a25fbc81a28b3ec0e368bb2021c50
|
||||
with:
|
||||
commit-message: '[automated] Update plugin list'
|
||||
author: 'pytest bot <pytestbot@users.noreply.github.com>'
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.12.1
|
||||
hooks:
|
||||
- id: black
|
||||
args: [--safe, --quiet]
|
||||
- repo: https://github.com/asottile/blacken-docs
|
||||
rev: 1.16.0
|
||||
hooks:
|
||||
- id: blacken-docs
|
||||
additional_dependencies: [black==23.7.0]
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: "v0.2.2"
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: ["--fix"]
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
hooks:
|
||||
|
@ -20,37 +16,11 @@ repos:
|
|||
- id: debug-statements
|
||||
exclude: _pytest/(debugging|hookspec).py
|
||||
language_version: python3
|
||||
- repo: https://github.com/PyCQA/autoflake
|
||||
rev: v2.2.1
|
||||
- repo: https://github.com/adamchainz/blacken-docs
|
||||
rev: 1.16.0
|
||||
hooks:
|
||||
- id: autoflake
|
||||
name: autoflake
|
||||
args: ["--in-place", "--remove-unused-variables", "--remove-all-unused-imports"]
|
||||
language: python
|
||||
files: \.py$
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 7.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
language_version: python3
|
||||
additional_dependencies:
|
||||
- flake8-typing-imports==1.12.0
|
||||
- flake8-docstrings==1.5.0
|
||||
- repo: https://github.com/asottile/reorder-python-imports
|
||||
rev: v3.12.0
|
||||
hooks:
|
||||
- id: reorder-python-imports
|
||||
args: ['--application-directories=.:src', --py38-plus]
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.15.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py38-plus]
|
||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||
rev: v2.5.0
|
||||
hooks:
|
||||
- id: setup-cfg-fmt
|
||||
args: ["--max-py-version=3.12", "--include-version-classifiers"]
|
||||
- id: blacken-docs
|
||||
additional_dependencies: [black==24.1.1]
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: v1.10.0
|
||||
hooks:
|
||||
|
@ -64,6 +34,7 @@ repos:
|
|||
additional_dependencies:
|
||||
- iniconfig>=1.1.0
|
||||
- attrs>=19.2.0
|
||||
- pluggy>=1.4.0
|
||||
- packaging
|
||||
- tomli
|
||||
- types-pkg_resources
|
||||
|
@ -71,6 +42,12 @@ repos:
|
|||
# for mypy running on python>=3.11 since exceptiongroup is only a dependency
|
||||
# on <3.11
|
||||
- exceptiongroup>=1.0.0rc8
|
||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||
rev: "1.7.0"
|
||||
hooks:
|
||||
- id: pyproject-fmt
|
||||
# https://pyproject-fmt.readthedocs.io/en/latest/#calculating-max-supported-python-version
|
||||
additional_dependencies: ["tox>=4.9"]
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: rst
|
||||
|
|
7
AUTHORS
7
AUTHORS
|
@ -56,6 +56,7 @@ Babak Keyvani
|
|||
Barney Gale
|
||||
Ben Brown
|
||||
Ben Gartner
|
||||
Ben Leith
|
||||
Ben Webb
|
||||
Benjamin Peterson
|
||||
Benjamin Schubert
|
||||
|
@ -93,6 +94,7 @@ Christopher Dignam
|
|||
Christopher Gilling
|
||||
Claire Cecil
|
||||
Claudio Madotto
|
||||
Clément M.T. Robert
|
||||
CrazyMerlyn
|
||||
Cristian Vera
|
||||
Cyrus Maden
|
||||
|
@ -126,6 +128,8 @@ Edison Gustavo Muenz
|
|||
Edoardo Batini
|
||||
Edson Tadeu M. Manoel
|
||||
Eduardo Schettino
|
||||
Edward Haigh
|
||||
Eero Vaher
|
||||
Eli Boyarski
|
||||
Elizaveta Shashkova
|
||||
Éloi Rivard
|
||||
|
@ -141,6 +145,7 @@ Evgeny Seliverstov
|
|||
Fabian Sturm
|
||||
Fabien Zarifian
|
||||
Fabio Zadrozny
|
||||
faph
|
||||
Felix Hofstätter
|
||||
Felix Nieuwenhuizen
|
||||
Feng Ma
|
||||
|
@ -339,6 +344,7 @@ Ronny Pfannschmidt
|
|||
Ross Lawley
|
||||
Ruaridh Williamson
|
||||
Russel Winder
|
||||
Russell Martin
|
||||
Ryan Puddephatt
|
||||
Ryan Wooden
|
||||
Sadra Barikbin
|
||||
|
@ -413,6 +419,7 @@ Vivaan Verma
|
|||
Vlad Dragos
|
||||
Vlad Radziuk
|
||||
Vladyslav Rachek
|
||||
Volodymyr Kochetkov
|
||||
Volodymyr Piskun
|
||||
Wei Lin
|
||||
Wil Cooley
|
||||
|
|
|
@ -27,9 +27,6 @@
|
|||
:target: https://results.pre-commit.ci/latest/github/pytest-dev/pytest/main
|
||||
:alt: pre-commit.ci status
|
||||
|
||||
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
||||
:target: https://github.com/psf/black
|
||||
|
||||
.. image:: https://www.codetriage.com/pytest-dev/pytest/badges/users.svg
|
||||
:target: https://www.codetriage.com/pytest-dev/pytest
|
||||
|
||||
|
|
|
@ -23,7 +23,6 @@ members of the `contributors team`_ interested in receiving funding.
|
|||
|
||||
The current list of contributors receiving funding are:
|
||||
|
||||
* `@asottile`_
|
||||
* `@nicoddemus`_
|
||||
* `@The-Compiler`_
|
||||
|
||||
|
@ -55,6 +54,5 @@ funds. Just drop a line to one of the `@pytest-dev/tidelift-admins`_ or use the
|
|||
.. _`@pytest-dev/tidelift-admins`: https://github.com/orgs/pytest-dev/teams/tidelift-admins/members
|
||||
.. _`agreement`: https://tidelift.com/docs/lifting/agreement
|
||||
|
||||
.. _`@asottile`: https://github.com/asottile
|
||||
.. _`@nicoddemus`: https://github.com/nicoddemus
|
||||
.. _`@The-Compiler`: https://github.com/The-Compiler
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import sys
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import cProfile
|
||||
import pytest # NOQA
|
||||
import pstats
|
||||
|
||||
import pytest # noqa: F401
|
||||
|
||||
script = sys.argv[1:] if len(sys.argv) > 1 else ["empty.py"]
|
||||
cProfile.run("pytest.cmdline.main(%r)" % script, "prof")
|
||||
p = pstats.Stats("prof")
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
# FastFilesCompleter 0.7383 1.0760
|
||||
import timeit
|
||||
|
||||
|
||||
imports = [
|
||||
"from argcomplete.completers import FilesCompleter as completer",
|
||||
"from _pytest._argcomplete import FastFilesCompleter as completer",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import pytest
|
||||
|
||||
|
||||
SKIP = True
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from unittest import TestCase # noqa: F401
|
||||
|
||||
|
||||
for i in range(15000):
|
||||
exec(
|
||||
f"""
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
:func:`pytest.warns` now validates that :func:`warnings.warn` was called with a `str` or a `Warning`.
|
||||
Currently in Python it is possible to use other types, however this causes an exception when :func:`warnings.filterwarnings` is used to filter those warnings (see `CPython #103577 <https://github.com/python/cpython/issues/103577>`__ for a discussion).
|
||||
While this can be considered a bug in CPython, we decided to put guards in pytest as the error message produced without this check in place is confusing.
|
|
@ -1,5 +0,0 @@
|
|||
Improvements to how ``-r`` for xfailures and xpasses:
|
||||
|
||||
* Report tracebacks for xfailures when ``-rx`` is set.
|
||||
* Report captured output for xpasses when ``-rX`` is set.
|
||||
* For xpasses, add ``-`` in summary between test name and reason, to match how xfail is displayed.
|
|
@ -0,0 +1,4 @@
|
|||
When using ``--override-ini`` for paths in invocations without a configuration file defined, the current working directory is used
|
||||
as the relative directory.
|
||||
|
||||
Previoulsy this would raise an :class:`AssertionError`.
|
|
@ -1 +0,0 @@
|
|||
Fix reporting of teardown errors in higher-scoped fixtures when using `--maxfail` or `--stepwise`.
|
|
@ -1,2 +0,0 @@
|
|||
Fixed ``IndexError: string index out of range`` crash in ``if highlighted[-1] == "\n" and source[-1] != "\n"``.
|
||||
This bug was introduced in pytest 8.0.0rc1.
|
|
@ -0,0 +1 @@
|
|||
Documented the retention of temporary directories created using the ``tmp_path`` fixture in more detail.
|
|
@ -1,2 +1,2 @@
|
|||
Added the :func:`iterparents() <_pytest.nodes.Node.iterparents>` helper method on nodes.
|
||||
Added the :func:`iter_parents() <_pytest.nodes.Node.iter_parents>` helper method on nodes.
|
||||
It is similar to :func:`listchain <_pytest.nodes.Node.listchain>`, but goes from bottom to top, and returns an iterator, not a list.
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Added support for :data:`sys.last_exc` for post-mortem debugging on Python>=3.12.
|
|
@ -0,0 +1 @@
|
|||
Fix collection on Windows where initial paths contain the short version of a path (for example ``c:\PROGRA~1\tests``).
|
|
@ -0,0 +1 @@
|
|||
Fix an ``IndexError`` crash raising from ``getstatementrange_ast``.
|
|
@ -0,0 +1 @@
|
|||
In case no other suitable candidates for configuration file are found, a ``pyproject.toml`` (even without a ``[tool.pytest.ini_options]`` table) will be considered as the configuration file and define the ``rootdir``.
|
|
@ -0,0 +1,3 @@
|
|||
Add ``--log-file-mode`` option to the logging plugin, enabling appending to log-files. This option accepts either ``"w"`` or ``"a"`` and defaults to ``"w"``.
|
||||
|
||||
Previously, the mode was hard-coded to be ``"w"`` which truncates the file before logging.
|
|
@ -0,0 +1 @@
|
|||
Fixed a regression in 8.0.1 whereby ``setup_module`` xunit-style fixtures are not executed when ``--doctest-modules`` is passed.
|
|
@ -0,0 +1 @@
|
|||
Fix the ``stacklevel`` used when warning about marks used on fixtures.
|
|
@ -0,0 +1 @@
|
|||
Reverted a fix to `--maxfail` handling in pytest 8.0.0 because it caused a regression in pytest-xdist whereby session fixture teardowns may get executed multiple times when the max-fails is reached.
|
|
@ -5,11 +5,10 @@
|
|||
<div id="searchbox" style="display: none" role="search">
|
||||
<div class="searchformwrapper">
|
||||
<form class="search" action="{{ pathto('search') }}" method="get">
|
||||
<input type="text" name="q" aria-labelledby="searchlabel"
|
||||
placeholder="Search"/>
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<input type="submit" value="{{ _('Go') }}" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">$('#searchbox').show(0);</script>
|
||||
<script>document.getElementById('searchbox').style.display = "block"</script>
|
||||
{%- endif %}
|
||||
|
|
|
@ -6,6 +6,9 @@ Release announcements
|
|||
:maxdepth: 2
|
||||
|
||||
|
||||
release-8.0.1
|
||||
release-8.0.0
|
||||
release-8.0.0rc2
|
||||
release-8.0.0rc1
|
||||
release-7.4.4
|
||||
release-7.4.3
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
pytest-8.0.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 8.0.0 release!
|
||||
|
||||
This release contains new features, improvements, bug fixes, and breaking changes, so users
|
||||
are encouraged to take a look at the CHANGELOG carefully:
|
||||
|
||||
https://docs.pytest.org/en/stable/changelog.html
|
||||
|
||||
For complete documentation, please visit:
|
||||
|
||||
https://docs.pytest.org/en/stable/
|
||||
|
||||
As usual, you can upgrade from PyPI via:
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Bruno Oliveira
|
||||
* Ran Benita
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
|
@ -0,0 +1,32 @@
|
|||
pytest-8.0.0rc2
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 8.0.0rc2 prerelease!
|
||||
|
||||
This is a prerelease, not intended for production use, but to test the upcoming features and improvements
|
||||
in order to catch any major problems before the final version is released to the major public.
|
||||
|
||||
We appreciate your help testing this out before the final release, making sure to report any
|
||||
regressions to our issue tracker:
|
||||
|
||||
https://github.com/pytest-dev/pytest/issues
|
||||
|
||||
When doing so, please include the string ``[prerelease]`` in the title.
|
||||
|
||||
You can upgrade from PyPI via:
|
||||
|
||||
pip install pytest==8.0.0rc2
|
||||
|
||||
Users are encouraged to take a look at the CHANGELOG carefully:
|
||||
|
||||
https://docs.pytest.org/en/release-8.0.0rc2/changelog.html
|
||||
|
||||
Thanks to all the contributors to this release:
|
||||
|
||||
* Ben Brown
|
||||
* Bruno Oliveira
|
||||
* Ran Benita
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
|
@ -0,0 +1,21 @@
|
|||
pytest-8.0.1
|
||||
=======================================
|
||||
|
||||
pytest 8.0.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
|
||||
* Clément Robert
|
||||
* Pierre Sassoulas
|
||||
* Ran Benita
|
||||
|
||||
|
||||
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:526
|
||||
cache -- .../_pytest/cacheprovider.py:527
|
||||
Return a cache object that can persist state between testing sessions.
|
||||
|
||||
cache.get(key, default)
|
||||
|
@ -33,7 +33,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
|||
|
||||
Values can be any object handled by the json stdlib module.
|
||||
|
||||
capsysbinary -- .../_pytest/capture.py:1008
|
||||
capsysbinary -- .../_pytest/capture.py:1007
|
||||
Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||
|
||||
The captured output is made available via ``capsysbinary.readouterr()``
|
||||
|
@ -43,7 +43,6 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
|||
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_output(capsysbinary):
|
||||
|
@ -51,7 +50,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
|||
captured = capsysbinary.readouterr()
|
||||
assert captured.out == b"hello\n"
|
||||
|
||||
capfd -- .../_pytest/capture.py:1036
|
||||
capfd -- .../_pytest/capture.py:1034
|
||||
Enable text capturing of writes to file descriptors ``1`` and ``2``.
|
||||
|
||||
The captured output is made available via ``capfd.readouterr()`` method
|
||||
|
@ -61,7 +60,6 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
|||
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_system_echo(capfd):
|
||||
|
@ -69,7 +67,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
|||
captured = capfd.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
|
||||
capfdbinary -- .../_pytest/capture.py:1064
|
||||
capfdbinary -- .../_pytest/capture.py:1061
|
||||
Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
|
||||
|
||||
The captured output is made available via ``capfd.readouterr()`` method
|
||||
|
@ -79,7 +77,6 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
|||
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_system_echo(capfdbinary):
|
||||
|
@ -97,7 +94,6 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
|||
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_output(capsys):
|
||||
|
@ -105,7 +101,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:743
|
||||
doctest_namespace [session scope] -- .../_pytest/doctest.py:745
|
||||
Fixture that returns a :py:class:`dict` that will be injected into the
|
||||
namespace of doctests.
|
||||
|
||||
|
@ -119,7 +115,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:1365
|
||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1354
|
||||
Session-scoped fixture that returns the session's :class:`pytest.Config`
|
||||
object.
|
||||
|
||||
|
@ -129,7 +125,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
|||
if pytestconfig.getoption("verbose") > 0:
|
||||
...
|
||||
|
||||
record_property -- .../_pytest/junitxml.py:282
|
||||
record_property -- .../_pytest/junitxml.py:283
|
||||
Add extra properties to the calling test.
|
||||
|
||||
User properties become part of the test report and are available to the
|
||||
|
@ -143,13 +139,13 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
|||
def test_function(record_property):
|
||||
record_property("example_key", 1)
|
||||
|
||||
record_xml_attribute -- .../_pytest/junitxml.py:305
|
||||
record_xml_attribute -- .../_pytest/junitxml.py:306
|
||||
Add extra xml attributes to the tag for the calling test.
|
||||
|
||||
The fixture is callable with ``name, value``. The value is
|
||||
automatically XML-encoded.
|
||||
|
||||
record_testsuite_property [session scope] -- .../_pytest/junitxml.py:343
|
||||
record_testsuite_property [session scope] -- .../_pytest/junitxml.py:344
|
||||
Record a new ``<property>`` tag as child of the root ``<testsuite>``.
|
||||
|
||||
This is suitable to writing global information regarding the entire test
|
||||
|
@ -174,10 +170,10 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
|||
`pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See
|
||||
:issue:`7767` for details.
|
||||
|
||||
tmpdir_factory [session scope] -- .../_pytest/legacypath.py:300
|
||||
tmpdir_factory [session scope] -- .../_pytest/legacypath.py:302
|
||||
Return a :class:`pytest.TempdirFactory` instance for the test session.
|
||||
|
||||
tmpdir -- .../_pytest/legacypath.py:307
|
||||
tmpdir -- .../_pytest/legacypath.py:309
|
||||
Return a temporary directory path object which is unique to each test
|
||||
function invocation, created as a sub directory of the base temporary
|
||||
directory.
|
||||
|
@ -196,7 +192,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:593
|
||||
caplog -- .../_pytest/logging.py:594
|
||||
Access and control log capturing.
|
||||
|
||||
Captured logs are available through the following properties/methods::
|
||||
|
@ -207,7 +203,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
|||
* caplog.record_tuples -> list of (logger_name, level, message) tuples
|
||||
* caplog.clear() -> clear captured records and formatted log output string
|
||||
|
||||
monkeypatch -- .../_pytest/monkeypatch.py:30
|
||||
monkeypatch -- .../_pytest/monkeypatch.py:32
|
||||
A convenient fixture for monkey-patching.
|
||||
|
||||
The fixture provides these methods to modify objects, dictionaries, or
|
||||
|
@ -231,16 +227,16 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
|||
To undo modifications done by the fixture in a contained scope,
|
||||
use :meth:`context() <pytest.MonkeyPatch.context>`.
|
||||
|
||||
recwarn -- .../_pytest/recwarn.py:30
|
||||
recwarn -- .../_pytest/recwarn.py:32
|
||||
Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions.
|
||||
|
||||
See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information
|
||||
on warning categories.
|
||||
|
||||
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:239
|
||||
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:241
|
||||
Return a :class:`pytest.TempPathFactory` instance for the test session.
|
||||
|
||||
tmp_path -- .../_pytest/tmpdir.py:254
|
||||
tmp_path -- .../_pytest/tmpdir.py:256
|
||||
Return a temporary directory path object which is unique to each test
|
||||
function invocation, created as a sub directory of the base temporary
|
||||
directory.
|
||||
|
|
|
@ -28,6 +28,75 @@ with advance notice in the **Deprecations** section of releases.
|
|||
|
||||
.. towncrier release notes start
|
||||
|
||||
pytest 8.0.1 (2024-02-16)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#11875 <https://github.com/pytest-dev/pytest/issues/11875>`_: Correctly handle errors from :func:`getpass.getuser` in Python 3.13.
|
||||
|
||||
|
||||
- `#11879 <https://github.com/pytest-dev/pytest/issues/11879>`_: Fix an edge case where ``ExceptionInfo._stringify_exception`` could crash :func:`pytest.raises`.
|
||||
|
||||
|
||||
- `#11906 <https://github.com/pytest-dev/pytest/issues/11906>`_: Fix regression with :func:`pytest.warns` using custom warning subclasses which have more than one parameter in their `__init__`.
|
||||
|
||||
|
||||
- `#11907 <https://github.com/pytest-dev/pytest/issues/11907>`_: Fix a regression in pytest 8.0.0 whereby calling :func:`pytest.skip` and similar control-flow exceptions within a :func:`pytest.warns()` block would get suppressed instead of propagating.
|
||||
|
||||
|
||||
- `#11929 <https://github.com/pytest-dev/pytest/issues/11929>`_: Fix a regression in pytest 8.0.0 whereby autouse fixtures defined in a module get ignored by the doctests in the module.
|
||||
|
||||
|
||||
- `#11937 <https://github.com/pytest-dev/pytest/issues/11937>`_: Fix a regression in pytest 8.0.0 whereby items would be collected in reverse order in some circumstances.
|
||||
|
||||
|
||||
pytest 8.0.0 (2024-01-27)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#11842 <https://github.com/pytest-dev/pytest/issues/11842>`_: Properly escape the ``reason`` of a :ref:`skip <pytest.mark.skip ref>` mark when writing JUnit XML files.
|
||||
|
||||
|
||||
- `#11861 <https://github.com/pytest-dev/pytest/issues/11861>`_: Avoid microsecond exceeds ``1_000_000`` when using ``log-date-format`` with ``%f`` specifier, which might cause the test suite to crash.
|
||||
|
||||
|
||||
pytest 8.0.0rc2 (2024-01-17)
|
||||
============================
|
||||
|
||||
|
||||
Improvements
|
||||
------------
|
||||
|
||||
- `#11233 <https://github.com/pytest-dev/pytest/issues/11233>`_: Improvements to ``-r`` for xfailures and xpasses:
|
||||
|
||||
* Report tracebacks for xfailures when ``-rx`` is set.
|
||||
* Report captured output for xpasses when ``-rX`` is set.
|
||||
* For xpasses, add ``-`` in summary between test name and reason, to match how xfail is displayed.
|
||||
|
||||
- `#11825 <https://github.com/pytest-dev/pytest/issues/11825>`_: The :hook:`pytest_plugin_registered` hook has a new ``plugin_name`` parameter containing the name by which ``plugin`` is registered.
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#11706 <https://github.com/pytest-dev/pytest/issues/11706>`_: Fix reporting of teardown errors in higher-scoped fixtures when using `--maxfail` or `--stepwise`.
|
||||
|
||||
NOTE: This change was reverted in pytest 8.0.2 to fix a `regression <https://github.com/pytest-dev/pytest-xdist/issues/1024>`_ it caused in pytest-xdist.
|
||||
|
||||
|
||||
- `#11758 <https://github.com/pytest-dev/pytest/issues/11758>`_: Fixed ``IndexError: string index out of range`` crash in ``if highlighted[-1] == "\n" and source[-1] != "\n"``.
|
||||
This bug was introduced in pytest 8.0.0rc1.
|
||||
|
||||
|
||||
- `#9765 <https://github.com/pytest-dev/pytest/issues/9765>`_, `#11816 <https://github.com/pytest-dev/pytest/issues/11816>`_: Fixed a frustrating bug that afflicted some users with the only error being ``assert mod not in mods``. The issue was caused by the fact that ``str(Path(mod))`` and ``mod.__file__`` don't necessarily produce the same string, and was being erroneously used interchangably in some places in the code.
|
||||
|
||||
This fix also broke the internal API of ``PytestPluginManager.consider_conftest`` by introducing a new parameter -- we mention this in case it is being used by external code, even if marked as *private*.
|
||||
|
||||
|
||||
pytest 8.0.0rc1 (2023-12-30)
|
||||
============================
|
||||
|
||||
|
@ -223,6 +292,10 @@ These are breaking changes where deprecation was not possible.
|
|||
therefore fail on the newly-re-emitted warnings.
|
||||
|
||||
|
||||
- The internal ``FixtureManager.getfixtureclosure`` method has changed. Plugins which use this method or
|
||||
which subclass ``FixtureManager`` and overwrite that method will need to adapt to the change.
|
||||
|
||||
|
||||
|
||||
Deprecations
|
||||
------------
|
||||
|
|
|
@ -23,6 +23,7 @@ from typing import TYPE_CHECKING
|
|||
|
||||
from _pytest import __version__ as version
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import sphinx.application
|
||||
|
||||
|
@ -440,9 +441,10 @@ intersphinx_mapping = {
|
|||
|
||||
def configure_logging(app: "sphinx.application.Sphinx") -> None:
|
||||
"""Configure Sphinx's WarningHandler to handle (expected) missing include."""
|
||||
import sphinx.util.logging
|
||||
import logging
|
||||
|
||||
import sphinx.util.logging
|
||||
|
||||
class WarnLogFilter(logging.Filter):
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
"""Ignore warnings about missing include with "only" directive.
|
||||
|
|
|
@ -33,13 +33,11 @@ have been available since years and should be used instead.
|
|||
.. code-block:: python
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
def pytest_runtest_call():
|
||||
...
|
||||
def pytest_runtest_call(): ...
|
||||
|
||||
|
||||
# or
|
||||
def pytest_runtest_call():
|
||||
...
|
||||
def pytest_runtest_call(): ...
|
||||
|
||||
|
||||
pytest_runtest_call.tryfirst = True
|
||||
|
@ -49,8 +47,7 @@ should be changed to:
|
|||
.. code-block:: python
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_runtest_call():
|
||||
...
|
||||
def pytest_runtest_call(): ...
|
||||
|
||||
Changed ``hookimpl`` attributes:
|
||||
|
||||
|
@ -146,8 +143,7 @@ Applying a mark to a fixture function never had any effect, but it is a common u
|
|||
|
||||
@pytest.mark.usefixtures("clean_database")
|
||||
@pytest.fixture
|
||||
def user() -> User:
|
||||
...
|
||||
def user() -> User: ...
|
||||
|
||||
Users expected in this case that the ``usefixtures`` mark would have its intended effect of using the ``clean_database`` fixture when ``user`` was invoked, when in fact it has no effect at all.
|
||||
|
||||
|
@ -308,11 +304,9 @@ they are in fact part of the ``nose`` support.
|
|||
def teardown(self):
|
||||
self.resource.close()
|
||||
|
||||
def test_foo(self):
|
||||
...
|
||||
def test_foo(self): ...
|
||||
|
||||
def test_bar(self):
|
||||
...
|
||||
def test_bar(self): ...
|
||||
|
||||
|
||||
|
||||
|
@ -327,11 +321,9 @@ Native pytest support uses ``setup_method`` and ``teardown_method`` (see :ref:`x
|
|||
def teardown_method(self):
|
||||
self.resource.close()
|
||||
|
||||
def test_foo(self):
|
||||
...
|
||||
def test_foo(self): ...
|
||||
|
||||
def test_bar(self):
|
||||
...
|
||||
def test_bar(self): ...
|
||||
|
||||
|
||||
This is easy to do in an entire code base by doing a simple find/replace.
|
||||
|
@ -346,17 +338,14 @@ Code using `@with_setup <with-setup-nose>`_ such as this:
|
|||
from nose.tools import with_setup
|
||||
|
||||
|
||||
def setup_some_resource():
|
||||
...
|
||||
def setup_some_resource(): ...
|
||||
|
||||
|
||||
def teardown_some_resource():
|
||||
...
|
||||
def teardown_some_resource(): ...
|
||||
|
||||
|
||||
@with_setup(setup_some_resource, teardown_some_resource)
|
||||
def test_foo():
|
||||
...
|
||||
def test_foo(): ...
|
||||
|
||||
Will also need to be ported to a supported pytest style. One way to do it is using a fixture:
|
||||
|
||||
|
@ -365,12 +354,10 @@ Will also need to be ported to a supported pytest style. One way to do it is usi
|
|||
import pytest
|
||||
|
||||
|
||||
def setup_some_resource():
|
||||
...
|
||||
def setup_some_resource(): ...
|
||||
|
||||
|
||||
def teardown_some_resource():
|
||||
...
|
||||
def teardown_some_resource(): ...
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -380,8 +367,7 @@ Will also need to be ported to a supported pytest style. One way to do it is usi
|
|||
teardown_some_resource()
|
||||
|
||||
|
||||
def test_foo(some_resource):
|
||||
...
|
||||
def test_foo(some_resource): ...
|
||||
|
||||
|
||||
.. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup
|
||||
|
@ -500,8 +486,7 @@ Implement the :hook:`pytest_load_initial_conftests` hook instead.
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
def pytest_cmdline_preparse(config: Config, args: List[str]) -> None:
|
||||
...
|
||||
def pytest_cmdline_preparse(config: Config, args: List[str]) -> None: ...
|
||||
|
||||
|
||||
# becomes:
|
||||
|
@ -509,8 +494,7 @@ Implement the :hook:`pytest_load_initial_conftests` hook instead.
|
|||
|
||||
def pytest_load_initial_conftests(
|
||||
early_config: Config, parser: Parser, args: List[str]
|
||||
) -> None:
|
||||
...
|
||||
) -> None: ...
|
||||
|
||||
|
||||
Collection changes in pytest 8
|
||||
|
@ -925,8 +909,7 @@ Applying marks to values of a ``pytest.mark.parametrize`` call is now deprecated
|
|||
(50, 500),
|
||||
],
|
||||
)
|
||||
def test_foo(a, b):
|
||||
...
|
||||
def test_foo(a, b): ...
|
||||
|
||||
This code applies the ``pytest.mark.xfail(reason="flaky")`` mark to the ``(6, 36)`` value of the above parametrization
|
||||
call.
|
||||
|
@ -949,8 +932,7 @@ To update the code, use ``pytest.param``:
|
|||
(50, 500),
|
||||
],
|
||||
)
|
||||
def test_foo(a, b):
|
||||
...
|
||||
def test_foo(a, b): ...
|
||||
|
||||
|
||||
.. _pytest_funcarg__ prefix deprecated:
|
||||
|
@ -1101,15 +1083,13 @@ This is just a matter of renaming the fixture as the API is the same:
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
def test_foo(record_xml_property):
|
||||
...
|
||||
def test_foo(record_xml_property): ...
|
||||
|
||||
Change to:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_foo(record_property):
|
||||
...
|
||||
def test_foo(record_property): ...
|
||||
|
||||
|
||||
.. _passing command-line string to pytest.main deprecated:
|
||||
|
@ -1271,8 +1251,7 @@ Example of usage:
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
class MySymbol:
|
||||
...
|
||||
class MySymbol: ...
|
||||
|
||||
|
||||
def pytest_namespace():
|
||||
|
|
|
@ -172,7 +172,7 @@ class TestRaises:
|
|||
raise ValueError("demo error")
|
||||
|
||||
def test_tupleerror(self):
|
||||
a, b = [1] # NOQA
|
||||
a, b = [1] # noqa: F841
|
||||
|
||||
def test_reinterpret_fails_with_print_for_the_fun_of_it(self):
|
||||
items = [1, 2, 3]
|
||||
|
@ -180,7 +180,7 @@ class TestRaises:
|
|||
a, b = items.pop()
|
||||
|
||||
def test_some_error(self):
|
||||
if namenotexi: # NOQA
|
||||
if namenotexi: # noqa: F821
|
||||
pass
|
||||
|
||||
def func1(self):
|
||||
|
|
|
@ -2,6 +2,7 @@ import os.path
|
|||
|
||||
import pytest
|
||||
|
||||
|
||||
mydir = os.path.dirname(__file__)
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import os.path
|
||||
import shutil
|
||||
|
||||
|
||||
failure_demo = os.path.join(os.path.dirname(__file__), "failure_demo.py")
|
||||
pytest_plugins = ("pytester",)
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""Module containing a parametrized tests testing cross-python serialization
|
||||
via the pickle module."""
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import textwrap
|
||||
|
@ -32,14 +33,12 @@ class Python:
|
|||
dumpfile = self.picklefile.with_name("dump.py")
|
||||
dumpfile.write_text(
|
||||
textwrap.dedent(
|
||||
r"""
|
||||
rf"""
|
||||
import pickle
|
||||
f = open({!r}, 'wb')
|
||||
s = pickle.dump({!r}, f, protocol=2)
|
||||
f = open({str(self.picklefile)!r}, 'wb')
|
||||
s = pickle.dump({obj!r}, f, protocol=2)
|
||||
f.close()
|
||||
""".format(
|
||||
str(self.picklefile), obj
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
subprocess.run((self.pythonpath, str(dumpfile)), check=True)
|
||||
|
@ -48,17 +47,15 @@ class Python:
|
|||
loadfile = self.picklefile.with_name("load.py")
|
||||
loadfile.write_text(
|
||||
textwrap.dedent(
|
||||
r"""
|
||||
rf"""
|
||||
import pickle
|
||||
f = open({!r}, 'rb')
|
||||
f = open({str(self.picklefile)!r}, 'rb')
|
||||
obj = pickle.load(f)
|
||||
f.close()
|
||||
res = eval({!r})
|
||||
res = eval({expression!r})
|
||||
if not res:
|
||||
raise SystemExit(1)
|
||||
""".format(
|
||||
str(self.picklefile), expression
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
print(loadfile)
|
||||
|
|
|
@ -162,7 +162,7 @@ objects, they are still using the default pytest representation:
|
|||
rootdir: /home/sweet/project
|
||||
collected 8 items
|
||||
|
||||
<Dir parametrize.rst-189>
|
||||
<Dir parametrize.rst-193>
|
||||
<Module test_time.py>
|
||||
<Function test_timedistance_v0[a0-b0-expected0]>
|
||||
<Function test_timedistance_v0[a1-b1-expected1]>
|
||||
|
@ -239,7 +239,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia
|
|||
rootdir: /home/sweet/project
|
||||
collected 4 items
|
||||
|
||||
<Dir parametrize.rst-189>
|
||||
<Dir parametrize.rst-193>
|
||||
<Module test_scenarios.py>
|
||||
<Class TestSampleWithScenarios>
|
||||
<Function test_demo1[basic]>
|
||||
|
@ -318,7 +318,7 @@ Let's first see how it looks like at collection time:
|
|||
rootdir: /home/sweet/project
|
||||
collected 2 items
|
||||
|
||||
<Dir parametrize.rst-189>
|
||||
<Dir parametrize.rst-193>
|
||||
<Module test_backends.py>
|
||||
<Function test_db_initialized[d1]>
|
||||
<Function test_db_initialized[d2]>
|
||||
|
@ -505,8 +505,8 @@ Running it results in some skips if we don't have all the python interpreters in
|
|||
. $ pytest -rs -q multipython.py
|
||||
ssssssssssss...ssssssssssss [100%]
|
||||
========================= short test summary info ==========================
|
||||
SKIPPED [12] multipython.py:68: 'python3.9' not found
|
||||
SKIPPED [12] multipython.py:68: 'python3.11' not found
|
||||
SKIPPED [12] multipython.py:65: 'python3.9' not found
|
||||
SKIPPED [12] multipython.py:65: 'python3.11' not found
|
||||
3 passed, 24 skipped in 0.12s
|
||||
|
||||
Parametrization of optional implementations/imports
|
||||
|
|
|
@ -152,7 +152,7 @@ The test collection would look like this:
|
|||
configfile: pytest.ini
|
||||
collected 2 items
|
||||
|
||||
<Dir pythoncollection.rst-190>
|
||||
<Dir pythoncollection.rst-194>
|
||||
<Module check_myapp.py>
|
||||
<Class CheckMyApp>
|
||||
<Function simple_check>
|
||||
|
@ -215,7 +215,7 @@ You can always peek at the collection tree without running tests like this:
|
|||
configfile: pytest.ini
|
||||
collected 3 items
|
||||
|
||||
<Dir pythoncollection.rst-190>
|
||||
<Dir pythoncollection.rst-194>
|
||||
<Dir CWD>
|
||||
<Module pythoncollection.py>
|
||||
<Function test_function>
|
||||
|
|
|
@ -660,6 +660,31 @@ If we run this:
|
|||
E assert 0
|
||||
|
||||
test_step.py:11: AssertionError
|
||||
================================ XFAILURES =================================
|
||||
______________________ TestUserHandling.test_deletion ______________________
|
||||
|
||||
item = <Function test_deletion>
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
if "incremental" in item.keywords:
|
||||
# retrieve the class name of the test
|
||||
cls_name = str(item.cls)
|
||||
# check if a previous test has failed for this class
|
||||
if cls_name in _test_failed_incremental:
|
||||
# retrieve the index of the test (if parametrize is used in combination with incremental)
|
||||
parametrize_index = (
|
||||
tuple(item.callspec.indices.values())
|
||||
if hasattr(item, "callspec")
|
||||
else ()
|
||||
)
|
||||
# retrieve the name of the first test function to fail for this class name and index
|
||||
test_name = _test_failed_incremental[cls_name].get(parametrize_index, None)
|
||||
# if name found, test has failed for the combination of class name & test name
|
||||
if test_name is not None:
|
||||
> pytest.xfail(f"previous test failed ({test_name})")
|
||||
E _pytest.outcomes.XFailed: previous test failed (test_modification)
|
||||
|
||||
conftest.py:47: XFailed
|
||||
========================= short test summary info ==========================
|
||||
XFAIL test_step.py::TestUserHandling::test_deletion - reason: previous test failed (test_modification)
|
||||
================== 1 failed, 2 passed, 1 xfailed in 0.12s ==================
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import pytest
|
||||
|
||||
|
||||
xfail = pytest.mark.xfail
|
||||
|
||||
|
||||
|
|
|
@ -99,8 +99,7 @@ sets. pytest-2.3 introduces a decorator for use on the factory itself:
|
|||
.. code-block:: python
|
||||
|
||||
@pytest.fixture(params=["mysql", "pg"])
|
||||
def db(request):
|
||||
... # use request.param
|
||||
def db(request): ... # use request.param
|
||||
|
||||
Here the factory will be invoked twice (with the respective "mysql"
|
||||
and "pg" values set as ``request.param`` attributes) and all of
|
||||
|
@ -141,8 +140,7 @@ argument:
|
|||
.. code-block:: python
|
||||
|
||||
@pytest.fixture()
|
||||
def db(request):
|
||||
...
|
||||
def db(request): ...
|
||||
|
||||
The name under which the funcarg resource can be requested is ``db``.
|
||||
|
||||
|
@ -151,8 +149,7 @@ aka:
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
def pytest_funcarg__db(request):
|
||||
...
|
||||
def pytest_funcarg__db(request): ...
|
||||
|
||||
|
||||
But it is then not possible to define scoping and parametrization.
|
||||
|
|
|
@ -22,7 +22,7 @@ Install ``pytest``
|
|||
.. code-block:: bash
|
||||
|
||||
$ pytest --version
|
||||
pytest 8.0.0rc1
|
||||
pytest 8.0.1
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
|
|
|
@ -227,8 +227,7 @@ to use strings:
|
|||
|
||||
|
||||
@pytest.mark.skipif("sys.version_info >= (3,3)")
|
||||
def test_function():
|
||||
...
|
||||
def test_function(): ...
|
||||
|
||||
During test function setup the skipif condition is evaluated by calling
|
||||
``eval('sys.version_info >= (3,0)', namespace)``. The namespace contains
|
||||
|
@ -262,8 +261,7 @@ configuration value which you might have added:
|
|||
.. code-block:: python
|
||||
|
||||
@pytest.mark.skipif("not config.getvalue('db')")
|
||||
def test_function():
|
||||
...
|
||||
def test_function(): ...
|
||||
|
||||
The equivalent with "boolean conditions" is:
|
||||
|
||||
|
|
|
@ -154,7 +154,7 @@ method to test for exceptions returned as part of an :class:`ExceptionGroup`:
|
|||
.. code-block:: python
|
||||
|
||||
def test_exception_in_group():
|
||||
with pytest.raises(RuntimeError) as excinfo:
|
||||
with pytest.raises(ExceptionGroup) as excinfo:
|
||||
raise ExceptionGroup(
|
||||
"Group message",
|
||||
[
|
||||
|
@ -176,7 +176,7 @@ exception at a specific level; exceptions contained directly in the top
|
|||
.. code-block:: python
|
||||
|
||||
def test_exception_in_group_at_given_depth():
|
||||
with pytest.raises(RuntimeError) as excinfo:
|
||||
with pytest.raises(ExceptionGroup) as excinfo:
|
||||
raise ExceptionGroup(
|
||||
"Group message",
|
||||
[
|
||||
|
|
|
@ -1418,7 +1418,7 @@ Running the above tests results in the following test IDs being used:
|
|||
rootdir: /home/sweet/project
|
||||
collected 12 items
|
||||
|
||||
<Dir fixtures.rst-208>
|
||||
<Dir fixtures.rst-212>
|
||||
<Module test_anothersmtp.py>
|
||||
<Function test_showhelo[smtp.gmail.com]>
|
||||
<Function test_showhelo[mail.python.org]>
|
||||
|
@ -1721,8 +1721,7 @@ You can specify multiple fixtures like this:
|
|||
.. code-block:: python
|
||||
|
||||
@pytest.mark.usefixtures("cleandir", "anotherfixture")
|
||||
def test():
|
||||
...
|
||||
def test(): ...
|
||||
|
||||
and you may specify fixture usage at the test module level using :globalvar:`pytestmark`:
|
||||
|
||||
|
@ -1750,8 +1749,7 @@ into an ini-file:
|
|||
|
||||
@pytest.mark.usefixtures("my_other_fixture")
|
||||
@pytest.fixture
|
||||
def my_fixture_that_sadly_wont_use_my_other_fixture():
|
||||
...
|
||||
def my_fixture_that_sadly_wont_use_my_other_fixture(): ...
|
||||
|
||||
This generates a deprecation warning, and will become an error in Pytest 8.
|
||||
|
||||
|
|
|
@ -206,8 +206,9 @@ option names are:
|
|||
* ``log_cli_date_format``
|
||||
|
||||
If you need to record the whole test suite logging calls to a file, you can pass
|
||||
``--log-file=/path/to/log/file``. This log file is opened in write mode which
|
||||
``--log-file=/path/to/log/file``. This log file is opened in write mode by default which
|
||||
means that it will be overwritten at each run tests session.
|
||||
If you'd like the file opened in append mode instead, then you can pass ``--log-file-mode=a``.
|
||||
Note that relative paths for the log-file location, whether passed on the CLI or declared in a
|
||||
config file, are always resolved relative to the current working directory.
|
||||
|
||||
|
@ -223,12 +224,13 @@ All of the log file options can also be set in the configuration INI file. The
|
|||
option names are:
|
||||
|
||||
* ``log_file``
|
||||
* ``log_file_mode``
|
||||
* ``log_file_level``
|
||||
* ``log_file_format``
|
||||
* ``log_file_date_format``
|
||||
|
||||
You can call ``set_log_path()`` to customize the log_file path dynamically. This functionality
|
||||
is considered **experimental**.
|
||||
is considered **experimental**. Note that ``set_log_path()`` respects the ``log_file_mode`` option.
|
||||
|
||||
.. _log_colors:
|
||||
|
||||
|
|
|
@ -406,10 +406,19 @@ Example:
|
|||
E assert 0
|
||||
|
||||
test_example.py:14: AssertionError
|
||||
================================ XFAILURES =================================
|
||||
________________________________ test_xfail ________________________________
|
||||
|
||||
def test_xfail():
|
||||
> pytest.xfail("xfailing this test")
|
||||
E _pytest.outcomes.XFailed: xfailing this test
|
||||
|
||||
test_example.py:26: XFailed
|
||||
================================= XPASSES ==================================
|
||||
========================= short test summary info ==========================
|
||||
SKIPPED [1] test_example.py:22: skipping this test
|
||||
XFAIL test_example.py::test_xfail - reason: xfailing this test
|
||||
XPASS test_example.py::test_xpass always xfail
|
||||
XPASS test_example.py::test_xpass - always xfail
|
||||
ERROR test_example.py::test_error - assert 0
|
||||
FAILED test_example.py::test_fail - assert 0
|
||||
== 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s ===
|
||||
|
|
|
@ -47,8 +47,7 @@ which may be passed an optional ``reason``:
|
|||
.. code-block:: python
|
||||
|
||||
@pytest.mark.skip(reason="no way of currently testing this")
|
||||
def test_the_unknown():
|
||||
...
|
||||
def test_the_unknown(): ...
|
||||
|
||||
|
||||
Alternatively, it is also possible to skip imperatively during test execution or setup
|
||||
|
@ -93,8 +92,7 @@ when run on an interpreter earlier than Python3.10:
|
|||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires python3.10 or higher")
|
||||
def test_function():
|
||||
...
|
||||
def test_function(): ...
|
||||
|
||||
If the condition evaluates to ``True`` during collection, the test function will be skipped,
|
||||
with the specified reason appearing in the summary when using ``-rs``.
|
||||
|
@ -112,8 +110,7 @@ You can share ``skipif`` markers between modules. Consider this test module:
|
|||
|
||||
|
||||
@minversion
|
||||
def test_function():
|
||||
...
|
||||
def test_function(): ...
|
||||
|
||||
You can import the marker and reuse it in another test module:
|
||||
|
||||
|
@ -124,8 +121,7 @@ You can import the marker and reuse it in another test module:
|
|||
|
||||
|
||||
@minversion
|
||||
def test_anotherfunction():
|
||||
...
|
||||
def test_anotherfunction(): ...
|
||||
|
||||
For larger test suites it's usually a good idea to have one file
|
||||
where you define the markers which you then consistently apply
|
||||
|
@ -232,8 +228,7 @@ expect a test to fail:
|
|||
.. code-block:: python
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_function():
|
||||
...
|
||||
def test_function(): ...
|
||||
|
||||
This test will run but no traceback will be reported when it fails. Instead, terminal
|
||||
reporting will list it in the "expected to fail" (``XFAIL``) or "unexpectedly
|
||||
|
@ -275,8 +270,7 @@ that condition as the first parameter:
|
|||
.. code-block:: python
|
||||
|
||||
@pytest.mark.xfail(sys.platform == "win32", reason="bug in a 3rd party library")
|
||||
def test_function():
|
||||
...
|
||||
def test_function(): ...
|
||||
|
||||
Note that you have to pass a reason as well (see the parameter description at
|
||||
:ref:`pytest.mark.xfail ref`).
|
||||
|
@ -289,8 +283,7 @@ You can specify the motive of an expected failure with the ``reason`` parameter:
|
|||
.. code-block:: python
|
||||
|
||||
@pytest.mark.xfail(reason="known parser issue")
|
||||
def test_function():
|
||||
...
|
||||
def test_function(): ...
|
||||
|
||||
|
||||
``raises`` parameter
|
||||
|
@ -302,8 +295,7 @@ a single exception, or a tuple of exceptions, in the ``raises`` argument.
|
|||
.. code-block:: python
|
||||
|
||||
@pytest.mark.xfail(raises=RuntimeError)
|
||||
def test_function():
|
||||
...
|
||||
def test_function(): ...
|
||||
|
||||
Then the test will be reported as a regular failure if it fails with an
|
||||
exception not mentioned in ``raises``.
|
||||
|
@ -317,8 +309,7 @@ even executed, use the ``run`` parameter as ``False``:
|
|||
.. code-block:: python
|
||||
|
||||
@pytest.mark.xfail(run=False)
|
||||
def test_function():
|
||||
...
|
||||
def test_function(): ...
|
||||
|
||||
This is specially useful for xfailing tests that are crashing the interpreter and should be
|
||||
investigated later.
|
||||
|
@ -334,8 +325,7 @@ You can change this by setting the ``strict`` keyword-only parameter to ``True``
|
|||
.. code-block:: python
|
||||
|
||||
@pytest.mark.xfail(strict=True)
|
||||
def test_function():
|
||||
...
|
||||
def test_function(): ...
|
||||
|
||||
|
||||
This will make ``XPASS`` ("unexpectedly passing") results from this test to fail the test suite.
|
||||
|
|
|
@ -8,9 +8,8 @@ How to use temporary directories and files in tests
|
|||
The ``tmp_path`` fixture
|
||||
------------------------
|
||||
|
||||
You can use the ``tmp_path`` fixture which will
|
||||
provide a temporary directory unique to the current test,
|
||||
created in the `base temporary directory`_.
|
||||
You can use the ``tmp_path`` fixture which will provide a temporary directory
|
||||
unique to each test function.
|
||||
|
||||
``tmp_path`` is a :class:`pathlib.Path` object. Here is an example test usage:
|
||||
|
||||
|
@ -62,6 +61,11 @@ Running this would result in a passed test except for the last
|
|||
FAILED test_tmp_path.py::test_create_file - assert 0
|
||||
============================ 1 failed in 0.12s =============================
|
||||
|
||||
By default, ``pytest`` retains the temporary directory for the last 3 ``pytest``
|
||||
invocations. Concurrent invocations of the same test function are supported by
|
||||
configuring the base temporary directory to be unique for each concurrent
|
||||
run. See `temporary directory location and retention`_ for details.
|
||||
|
||||
.. _`tmp_path_factory example`:
|
||||
|
||||
The ``tmp_path_factory`` fixture
|
||||
|
@ -100,7 +104,7 @@ See :ref:`tmp_path_factory API <tmp_path_factory factory api>` for details.
|
|||
.. _tmpdir:
|
||||
|
||||
The ``tmpdir`` and ``tmpdir_factory`` fixtures
|
||||
---------------------------------------------------
|
||||
----------------------------------------------
|
||||
|
||||
The ``tmpdir`` and ``tmpdir_factory`` fixtures are similar to ``tmp_path``
|
||||
and ``tmp_path_factory``, but use/return legacy `py.path.local`_ objects
|
||||
|
@ -124,10 +128,10 @@ See :fixture:`tmpdir <tmpdir>` :fixture:`tmpdir_factory <tmpdir_factory>`
|
|||
API for details.
|
||||
|
||||
|
||||
.. _`base temporary directory`:
|
||||
.. _`temporary directory location and retention`:
|
||||
|
||||
The default base temporary directory
|
||||
-----------------------------------------------
|
||||
Temporary directory location and retention
|
||||
------------------------------------------
|
||||
|
||||
Temporary directories are by default created as sub-directories of
|
||||
the system temporary directory. The base name will be ``pytest-NUM`` where
|
||||
|
@ -152,7 +156,7 @@ You can override the default temporary directory setting like this:
|
|||
for that purpose only.
|
||||
|
||||
When distributing tests on the local machine using ``pytest-xdist``, care is taken to
|
||||
automatically configure a basetemp directory for the sub processes such that all temporary
|
||||
data lands below a single per-test run basetemp directory.
|
||||
automatically configure a `basetemp` directory for the sub processes such that all temporary
|
||||
data lands below a single per-test run temporary directory.
|
||||
|
||||
.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
|
||||
|
|
|
@ -46,24 +46,18 @@ Plugin discovery order at tool startup
|
|||
|
||||
5. by loading all plugins specified through the :envvar:`PYTEST_PLUGINS` environment variable.
|
||||
|
||||
6. by loading all :file:`conftest.py` files as inferred by the command line
|
||||
invocation:
|
||||
6. by loading all "initial ":file:`conftest.py` files:
|
||||
|
||||
- if no test paths are specified, use the current dir as a test path
|
||||
- if exists, load ``conftest.py`` and ``test*/conftest.py`` relative
|
||||
to the directory part of the first test path. After the ``conftest.py``
|
||||
file is loaded, load all plugins specified in its
|
||||
:globalvar:`pytest_plugins` variable if present.
|
||||
|
||||
Note that pytest does not find ``conftest.py`` files in deeper nested
|
||||
sub directories at tool startup. It is usually a good idea to keep
|
||||
your ``conftest.py`` file in the top level test or project root directory.
|
||||
|
||||
7. by recursively loading all plugins specified by the
|
||||
:globalvar:`pytest_plugins` variable in ``conftest.py`` files.
|
||||
- determine the test paths: specified on the command line, otherwise in
|
||||
:confval:`testpaths` if defined and running from the rootdir, otherwise the
|
||||
current dir
|
||||
- for each test path, load ``conftest.py`` and ``test*/conftest.py`` relative
|
||||
to the directory part of the test path, if exist. Before a ``conftest.py``
|
||||
file is loaded, load ``conftest.py`` files in all of its parent directories.
|
||||
After a ``conftest.py`` file is loaded, recursively load all plugins specified
|
||||
in its :globalvar:`pytest_plugins` variable if present.
|
||||
|
||||
|
||||
.. _`pytest/plugin`: http://bitbucket.org/pytest-dev/pytest/src/tip/pytest/plugin/
|
||||
.. _`conftest.py plugins`:
|
||||
.. _`localplugin`:
|
||||
.. _`local conftest plugins`:
|
||||
|
@ -108,9 +102,9 @@ Here is how you might run it::
|
|||
See also: :ref:`pythonpath`.
|
||||
|
||||
.. note::
|
||||
Some hooks should be implemented only in plugins or conftest.py files situated at the
|
||||
tests root directory due to how pytest discovers plugins during startup,
|
||||
see the documentation of each hook for details.
|
||||
Some hooks cannot be implemented in conftest.py files which are not
|
||||
:ref:`initial <pluginorder>` due to how pytest discovers plugins during
|
||||
startup. See the documentation of each hook for details.
|
||||
|
||||
Writing your own plugin
|
||||
-----------------------
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
:orphan:
|
||||
|
||||
.. sidebar:: Next Open Trainings
|
||||
.. sidebar:: Next Open Trainings and Events
|
||||
|
||||
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, **March 5th to 7th 2024** (3 day in-depth training), **Leipzig, Germany / Remote**
|
||||
- `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>`.
|
||||
|
||||
|
|
|
@ -177,13 +177,20 @@ Files will only be matched for configuration if:
|
|||
* ``tox.ini``: contains a ``[pytest]`` section.
|
||||
* ``setup.cfg``: contains a ``[tool:pytest]`` section.
|
||||
|
||||
Finally, a ``pyproject.toml`` file will be considered the ``configfile`` if no other match was found, in this case
|
||||
even if it does not contain a ``[tool.pytest.ini_options]`` table (this was added in ``8.1``).
|
||||
|
||||
The files are considered in the order above. Options from multiple ``configfiles`` candidates
|
||||
are never merged - the first match wins.
|
||||
|
||||
The configuration file also determines the value of the ``rootpath``.
|
||||
|
||||
The :class:`Config <pytest.Config>` object (accessible via hooks or through the :fixture:`pytestconfig` fixture)
|
||||
will subsequently carry these attributes:
|
||||
|
||||
- :attr:`config.rootpath <pytest.Config.rootpath>`: the determined root directory, guaranteed to exist.
|
||||
- :attr:`config.rootpath <pytest.Config.rootpath>`: the determined root directory, guaranteed to exist. It is used as
|
||||
a reference directory for constructing test addresses ("nodeids") and can be used also by plugins for storing
|
||||
per-testrun information.
|
||||
|
||||
- :attr:`config.inipath <pytest.Config.inipath>`: the determined ``configfile``, may be ``None``
|
||||
(it is named ``inipath`` for historical reasons).
|
||||
|
@ -193,9 +200,7 @@ will subsequently carry these attributes:
|
|||
versions of the older ``config.rootdir`` and ``config.inifile``, which have type
|
||||
``py.path.local``, and still exist for backward compatibility.
|
||||
|
||||
The ``rootdir`` is used as a reference directory for constructing test
|
||||
addresses ("nodeids") and can be used also by plugins for storing
|
||||
per-testrun information.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -164,8 +164,7 @@ Add warning filters to marked test items.
|
|||
.. code-block:: python
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:.*usage will be deprecated.*:DeprecationWarning")
|
||||
def test_foo():
|
||||
...
|
||||
def test_foo(): ...
|
||||
|
||||
|
||||
.. _`pytest.mark.parametrize ref`:
|
||||
|
@ -276,8 +275,7 @@ For example:
|
|||
.. code-block:: python
|
||||
|
||||
@pytest.mark.timeout(10, "slow", method="thread")
|
||||
def test_function():
|
||||
...
|
||||
def test_function(): ...
|
||||
|
||||
Will create and attach a :class:`Mark <pytest.Mark>` object to the collected
|
||||
:class:`Item <pytest.Item>`, which can then be accessed by fixtures or hooks with
|
||||
|
@ -294,8 +292,7 @@ Example for using multiple custom markers:
|
|||
|
||||
@pytest.mark.timeout(10, "slow", method="thread")
|
||||
@pytest.mark.slow
|
||||
def test_function():
|
||||
...
|
||||
def test_function(): ...
|
||||
|
||||
When :meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>` or :meth:`Node.iter_markers_with_node <_pytest.nodes.Node.iter_markers_with_node>` is used with multiple markers, the marker closest to the function will be iterated over first. The above example will result in ``@pytest.mark.slow`` followed by ``@pytest.mark.timeout(...)``.
|
||||
|
||||
|
@ -2044,7 +2041,7 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
|||
failure
|
||||
--doctest-glob=pat Doctests file matching pattern, default: test*.txt
|
||||
--doctest-ignore-import-errors
|
||||
Ignore doctest ImportErrors
|
||||
Ignore doctest collection errors
|
||||
--doctest-continue-on-failure
|
||||
For a given doctest, continue to run after the first
|
||||
failure
|
||||
|
|
|
@ -2,7 +2,7 @@ pallets-sphinx-themes
|
|||
pluggy>=1.2.0
|
||||
pygments-pytest>=2.3.0
|
||||
sphinx-removed-in>=0.2.0
|
||||
sphinx>=5,<8
|
||||
sphinx>=7
|
||||
sphinxcontrib-trio
|
||||
sphinxcontrib-svg2pdfconverter
|
||||
# Pin packaging because it no longer handles 'latest' version, which
|
||||
|
|
|
@ -3,6 +3,7 @@ from pathlib import Path
|
|||
|
||||
import requests
|
||||
|
||||
|
||||
issues_url = "https://api.github.com/repos/pytest-dev/pytest/issues"
|
||||
|
||||
|
||||
|
|
194
pyproject.toml
194
pyproject.toml
|
@ -1,13 +1,178 @@
|
|||
[build-system]
|
||||
requires = [
|
||||
"setuptools>=45.0",
|
||||
"setuptools-scm[toml]>=6.2.3",
|
||||
[project]
|
||||
name = "pytest"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
readme = "README.rst"
|
||||
keywords = [
|
||||
"test",
|
||||
"unittest",
|
||||
]
|
||||
license = {text = "MIT"}
|
||||
authors = [
|
||||
{name = "Holger Krekel"},
|
||||
{name = "Bruno Oliveira"},
|
||||
{name = "Ronny Pfannschmidt"},
|
||||
{name = "Floris Bruynooghe"},
|
||||
{name = "Brianna Laugher"},
|
||||
{name = "Florian Bruhin"},
|
||||
{name = "Others (See AUTHORS)"},
|
||||
]
|
||||
requires-python = ">=3.8"
|
||||
classifiers = [
|
||||
"Development Status :: 6 - Mature",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: MacOS",
|
||||
"Operating System :: Microsoft :: Windows",
|
||||
"Operating System :: POSIX",
|
||||
"Operating System :: Unix",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Software Development :: Libraries",
|
||||
"Topic :: Software Development :: Testing",
|
||||
"Topic :: Utilities",
|
||||
]
|
||||
dynamic = [
|
||||
"version",
|
||||
]
|
||||
dependencies = [
|
||||
'colorama; sys_platform == "win32"',
|
||||
'exceptiongroup>=1.0.0rc8; python_version < "3.11"',
|
||||
"iniconfig",
|
||||
"packaging",
|
||||
"pluggy<2.0,>=1.4",
|
||||
'tomli>=1; python_version < "3.11"',
|
||||
]
|
||||
[project.optional-dependencies]
|
||||
testing = [
|
||||
"argcomplete",
|
||||
"attrs>=19.2",
|
||||
"hypothesis>=3.56",
|
||||
"mock",
|
||||
"pygments>=2.7.2",
|
||||
"requests",
|
||||
"setuptools",
|
||||
"xmlschema",
|
||||
]
|
||||
[project.urls]
|
||||
Changelog = "https://docs.pytest.org/en/stable/changelog.html"
|
||||
Homepage = "https://docs.pytest.org/en/latest/"
|
||||
Source = "https://github.com/pytest-dev/pytest"
|
||||
Tracker = "https://github.com/pytest-dev/pytest/issues"
|
||||
Twitter = "https://twitter.com/pytestdotorg"
|
||||
[project.scripts]
|
||||
"py.test" = "pytest:console_main"
|
||||
pytest = "pytest:console_main"
|
||||
|
||||
[build-system]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = [
|
||||
"setuptools>=61",
|
||||
"setuptools-scm[toml]>=6.2.3",
|
||||
]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
"_pytest" = ["py.typed"]
|
||||
"pytest" = ["py.typed"]
|
||||
|
||||
[tool.setuptools_scm]
|
||||
write_to = "src/_pytest/_version.py"
|
||||
|
||||
[tool.black]
|
||||
target-version = ['py38']
|
||||
|
||||
[tool.ruff]
|
||||
src = ["src"]
|
||||
line-length = 88
|
||||
|
||||
[tool.ruff.format]
|
||||
docstring-code-format = true
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"B", # bugbear
|
||||
"D", # pydocstyle
|
||||
"E", # pycodestyle
|
||||
"F", # pyflakes
|
||||
"I", # isort
|
||||
"PYI", # flake8-pyi
|
||||
"UP", # pyupgrade
|
||||
"RUF", # ruff
|
||||
"W", # pycodestyle
|
||||
"PIE", # flake8-pie
|
||||
"PGH004", # pygrep-hooks - Use specific rule codes when using noqa
|
||||
"PLE", # pylint error
|
||||
"PLW", # pylint warning
|
||||
"PLR1714", # Consider merging multiple comparisons
|
||||
]
|
||||
ignore = [
|
||||
# bugbear ignore
|
||||
"B004", # Using `hasattr(x, "__call__")` to test if x is callable is unreliable.
|
||||
"B007", # Loop control variable `i` not used within loop body
|
||||
"B009", # Do not call `getattr` with a constant attribute value
|
||||
"B010", # [*] Do not call `setattr` with a constant attribute value.
|
||||
"B011", # Do not `assert False` (`python -O` removes these calls)
|
||||
"B028", # No explicit `stacklevel` keyword argument found
|
||||
# pycodestyle ignore
|
||||
# pytest can do weird low-level things, and we usually know
|
||||
# what we're doing when we use type(..) is ...
|
||||
"E721", # Do not compare types, use `isinstance()`
|
||||
# pydocstyle ignore
|
||||
"D100", # Missing docstring in public module
|
||||
"D101", # Missing docstring in public class
|
||||
"D102", # Missing docstring in public method
|
||||
"D103", # Missing docstring in public function
|
||||
"D104", # Missing docstring in public package
|
||||
"D105", # Missing docstring in magic method
|
||||
"D106", # Missing docstring in public nested class
|
||||
"D107", # Missing docstring in `__init__`
|
||||
"D209", # [*] Multi-line docstring closing quotes should be on a separate line
|
||||
"D205", # 1 blank line required between summary line and description
|
||||
"D400", # First line should end with a period
|
||||
"D401", # First line of docstring should be in imperative mood
|
||||
"D402", # First line should not be the function's signature
|
||||
"D404", # First word of the docstring should not be "This"
|
||||
"D415", # First line should end with a period, question mark, or exclamation point
|
||||
# ruff ignore
|
||||
"RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`
|
||||
# pylint ignore
|
||||
"PLW0603", # Using the global statement
|
||||
"PLW0120", # remove the else and dedent its contents
|
||||
"PLW2901", # for loop variable overwritten by assignment target
|
||||
"PLR5501", # Use `elif` instead of `else` then `if`
|
||||
]
|
||||
|
||||
[tool.ruff.lint.pycodestyle]
|
||||
# In order to be able to format for 88 char in ruff format
|
||||
max-line-length = 120
|
||||
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "pep257"
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
force-single-line = true
|
||||
combine-as-imports = true
|
||||
force-sort-within-sections = true
|
||||
order-by-type = false
|
||||
known-local-folder = ["pytest", "_pytest"]
|
||||
lines-after-imports = 2
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"src/_pytest/_py/**/*.py" = ["B", "PYI"]
|
||||
"src/_pytest/_version.py" = ["I001"]
|
||||
"testing/python/approx.py" = ["B015"]
|
||||
|
||||
[tool.check-wheel-contents]
|
||||
# check-wheel-contents is executed by the build-and-inspect-python-package action.
|
||||
# W009: Wheel contains multiple toplevel library entries
|
||||
ignore = "W009"
|
||||
|
||||
[tool.pyproject-fmt]
|
||||
indent = 4
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
minversion = "2.0"
|
||||
addopts = "-rfEX -p pytester --strict-markers"
|
||||
|
@ -67,7 +232,6 @@ markers = [
|
|||
"uses_pexpect",
|
||||
]
|
||||
|
||||
|
||||
[tool.towncrier]
|
||||
package = "pytest"
|
||||
package_dir = "src"
|
||||
|
@ -116,10 +280,16 @@ template = "changelog/_template.rst"
|
|||
name = "Trivial/Internal Changes"
|
||||
showcontent = true
|
||||
|
||||
[tool.black]
|
||||
target-version = ['py38']
|
||||
|
||||
# check-wheel-contents is executed by the build-and-inspect-python-package action.
|
||||
[tool.check-wheel-contents]
|
||||
# W009: Wheel contains multiple toplevel library entries
|
||||
ignore = "W009"
|
||||
[tool.mypy]
|
||||
mypy_path = ["src"]
|
||||
check_untyped_defs = true
|
||||
disallow_any_generics = true
|
||||
disallow_untyped_defs = true
|
||||
ignore_missing_imports = true
|
||||
show_error_codes = true
|
||||
strict_equality = true
|
||||
warn_redundant_casts = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
warn_unused_configs = true
|
||||
no_implicit_reexport = true
|
||||
|
|
|
@ -8,9 +8,9 @@ our CHANGELOG) into Markdown (which is required by GitHub Releases).
|
|||
|
||||
Requires Python3.6+.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Sequence
|
||||
|
||||
import pypandoc
|
||||
|
|
|
@ -14,8 +14,8 @@ After that, it will create a release using the `release` tox environment, and pu
|
|||
`pytest bot <pytestbot@gmail.com>` commit author.
|
||||
"""
|
||||
import argparse
|
||||
import re
|
||||
from pathlib import Path
|
||||
import re
|
||||
from subprocess import check_call
|
||||
from subprocess import check_output
|
||||
from subprocess import run
|
||||
|
@ -79,7 +79,7 @@ def prepare_release_pr(
|
|||
)
|
||||
except InvalidFeatureRelease as e:
|
||||
print(f"{Fore.RED}{e}")
|
||||
raise SystemExit(1)
|
||||
raise SystemExit(1) from None
|
||||
|
||||
print(f"Version: {Fore.CYAN}{version}")
|
||||
|
||||
|
|
|
@ -107,7 +107,7 @@ def pre_release(
|
|||
|
||||
def changelog(version: str, write_out: bool = False) -> None:
|
||||
addopts = [] if write_out else ["--draft"]
|
||||
check_call(["towncrier", "--yes", "--version", version] + addopts)
|
||||
check_call(["towncrier", "--yes", "--version", version, *addopts])
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# mypy: disallow-untyped-defs
|
||||
import sys
|
||||
from subprocess import call
|
||||
import sys
|
||||
|
||||
|
||||
def main() -> int:
|
||||
|
|
|
@ -11,13 +11,13 @@ from typing import TypedDict
|
|||
|
||||
import packaging.version
|
||||
import platformdirs
|
||||
import tabulate
|
||||
import wcwidth
|
||||
from requests_cache import CachedResponse
|
||||
from requests_cache import CachedSession
|
||||
from requests_cache import OriginalResponse
|
||||
from requests_cache import SQLiteCache
|
||||
import tabulate
|
||||
from tqdm import tqdm
|
||||
import wcwidth
|
||||
|
||||
|
||||
FILE_HEAD = r"""
|
||||
|
@ -29,7 +29,7 @@ Pytest Plugin List
|
|||
==================
|
||||
|
||||
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.
|
||||
It includes PyPI projects whose names begin with "pytest-" or "pytest_" and a handful of manually selected projects.
|
||||
Packages classified as inactive are excluded.
|
||||
|
||||
For detailed insights into how this list is generated,
|
||||
|
@ -61,6 +61,7 @@ DEVELOPMENT_STATUS_CLASSIFIERS = (
|
|||
)
|
||||
ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins
|
||||
"logassert",
|
||||
"logot",
|
||||
"nuts",
|
||||
"flask_fixture",
|
||||
}
|
||||
|
@ -86,7 +87,6 @@ def project_response_with_refresh(
|
|||
|
||||
force refresh in case of last serial mismatch
|
||||
"""
|
||||
|
||||
response = session.get(f"https://pypi.org/pypi/{name}/json")
|
||||
if int(response.headers.get("X-PyPI-Last-Serial", -1)) != last_serial:
|
||||
response = session.get(f"https://pypi.org/pypi/{name}/json", refresh=True)
|
||||
|
@ -110,7 +110,10 @@ def pytest_plugin_projects_from_pypi(session: CachedSession) -> dict[str, int]:
|
|||
return {
|
||||
name: p["_last-serial"]
|
||||
for p in response.json()["projects"]
|
||||
if (name := p["name"]).startswith("pytest-") or name in ADDITIONAL_PROJECTS
|
||||
if (
|
||||
(name := p["name"]).startswith(("pytest-", "pytest_"))
|
||||
or name in ADDITIONAL_PROJECTS
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
@ -185,7 +188,6 @@ def iter_plugins() -> Iterator[PluginInfo]:
|
|||
|
||||
def plugin_definitions(plugins: Iterable[PluginInfo]) -> Iterator[str]:
|
||||
"""Return RST for the plugin list that fits better on a vertical page."""
|
||||
|
||||
for plugin in plugins:
|
||||
yield dedent(
|
||||
f"""
|
||||
|
@ -210,7 +212,7 @@ def main() -> None:
|
|||
f.write(f"This list contains {len(plugins)} plugins.\n\n")
|
||||
f.write(".. only:: not latex\n\n")
|
||||
|
||||
wcwidth # reference library that must exist for tabulate to work
|
||||
_ = wcwidth # reference library that must exist for tabulate to work
|
||||
plugin_table = tabulate.tabulate(plugins, headers="keys", tablefmt="rst")
|
||||
f.write(indent(plugin_table, " "))
|
||||
f.write("\n\n")
|
||||
|
|
104
setup.cfg
104
setup.cfg
|
@ -1,104 +0,0 @@
|
|||
[metadata]
|
||||
name = pytest
|
||||
description = pytest: simple powerful testing with Python
|
||||
long_description = file: README.rst
|
||||
long_description_content_type = text/x-rst
|
||||
url = https://docs.pytest.org/en/latest/
|
||||
author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others
|
||||
license = MIT
|
||||
license_files = LICENSE
|
||||
platforms = unix, linux, osx, cygwin, win32
|
||||
classifiers =
|
||||
Development Status :: 6 - Mature
|
||||
Intended Audience :: Developers
|
||||
License :: OSI Approved :: MIT License
|
||||
Operating System :: MacOS :: MacOS X
|
||||
Operating System :: Microsoft :: Windows
|
||||
Operating System :: POSIX
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3 :: Only
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: 3.10
|
||||
Programming Language :: Python :: 3.11
|
||||
Programming Language :: Python :: 3.12
|
||||
Topic :: Software Development :: Libraries
|
||||
Topic :: Software Development :: Testing
|
||||
Topic :: Utilities
|
||||
keywords = test, unittest
|
||||
project_urls =
|
||||
Changelog=https://docs.pytest.org/en/stable/changelog.html
|
||||
Twitter=https://twitter.com/pytestdotorg
|
||||
Source=https://github.com/pytest-dev/pytest
|
||||
Tracker=https://github.com/pytest-dev/pytest/issues
|
||||
|
||||
[options]
|
||||
packages =
|
||||
_pytest
|
||||
_pytest._code
|
||||
_pytest._io
|
||||
_pytest._py
|
||||
_pytest.assertion
|
||||
_pytest.config
|
||||
_pytest.mark
|
||||
pytest
|
||||
py_modules = py
|
||||
install_requires =
|
||||
iniconfig
|
||||
packaging
|
||||
pluggy>=1.3.0,<2.0
|
||||
colorama;sys_platform=="win32"
|
||||
exceptiongroup>=1.0.0rc8;python_version<"3.11"
|
||||
tomli>=1.0.0;python_version<"3.11"
|
||||
python_requires = >=3.8
|
||||
package_dir =
|
||||
=src
|
||||
setup_requires =
|
||||
setuptools
|
||||
setuptools-scm>=6.0
|
||||
zip_safe = no
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
pytest=pytest:console_main
|
||||
py.test=pytest:console_main
|
||||
|
||||
[options.extras_require]
|
||||
testing =
|
||||
argcomplete
|
||||
attrs>=19.2.0
|
||||
hypothesis>=3.56
|
||||
mock
|
||||
pygments>=2.7.2
|
||||
requests
|
||||
setuptools
|
||||
xmlschema
|
||||
|
||||
[options.package_data]
|
||||
_pytest = py.typed
|
||||
pytest = py.typed
|
||||
|
||||
[build_sphinx]
|
||||
source_dir = doc/en/
|
||||
build_dir = doc/build
|
||||
all_files = 1
|
||||
|
||||
[check-manifest]
|
||||
ignore =
|
||||
src/_pytest/_version.py
|
||||
|
||||
[devpi:upload]
|
||||
formats = sdist.tgz,bdist_wheel
|
||||
|
||||
[mypy]
|
||||
mypy_path = src
|
||||
check_untyped_defs = True
|
||||
disallow_any_generics = True
|
||||
ignore_missing_imports = True
|
||||
show_error_codes = True
|
||||
strict_equality = True
|
||||
warn_redundant_casts = True
|
||||
warn_return_any = True
|
||||
warn_unreachable = True
|
||||
warn_unused_configs = True
|
||||
no_implicit_reexport = True
|
|
@ -1,7 +1,8 @@
|
|||
__all__ = ["__version__", "version_tuple"]
|
||||
|
||||
try:
|
||||
from ._version import version as __version__, version_tuple
|
||||
from ._version import version as __version__
|
||||
from ._version import version_tuple
|
||||
except ImportError: # pragma: no cover
|
||||
# broken installation, we don't even try
|
||||
# unknown only works because we do poor mans version compare
|
||||
|
|
|
@ -61,10 +61,11 @@ If things do not work right away:
|
|||
which should throw a KeyError: 'COMPLINE' (which is properly set by the
|
||||
global argcomplete script).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from glob import glob
|
||||
import os
|
||||
import sys
|
||||
from glob import glob
|
||||
from typing import Any
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Python inspection/code generation API."""
|
||||
|
||||
from .code import Code
|
||||
from .code import ExceptionInfo
|
||||
from .code import filter_traceback
|
||||
|
@ -9,6 +10,7 @@ from .code import TracebackEntry
|
|||
from .source import getrawcode
|
||||
from .source import Source
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Code",
|
||||
"ExceptionInfo",
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
# mypy: allow-untyped-defs
|
||||
import ast
|
||||
import dataclasses
|
||||
import inspect
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
from inspect import CO_VARARGS
|
||||
from inspect import CO_VARKEYWORDS
|
||||
from io import StringIO
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
from traceback import format_exception_only
|
||||
from types import CodeType
|
||||
from types import FrameType
|
||||
|
@ -50,6 +51,7 @@ from _pytest.deprecated import check_ispytest
|
|||
from _pytest.pathlib import absolutepath
|
||||
from _pytest.pathlib import bestrelpath
|
||||
|
||||
|
||||
if sys.version_info[:2] < (3, 11):
|
||||
from exceptiongroup import BaseExceptionGroup
|
||||
|
||||
|
@ -486,9 +488,10 @@ class ExceptionInfo(Generic[E]):
|
|||
|
||||
.. versionadded:: 7.4
|
||||
"""
|
||||
assert (
|
||||
exception.__traceback__
|
||||
), "Exceptions passed to ExcInfo.from_exception(...) must have a non-None __traceback__."
|
||||
assert exception.__traceback__, (
|
||||
"Exceptions passed to ExcInfo.from_exception(...)"
|
||||
" must have a non-None __traceback__."
|
||||
)
|
||||
exc_info = (type(exception), exception, exception.__traceback__)
|
||||
return cls.from_exc_info(exc_info, exprinfo)
|
||||
|
||||
|
@ -587,9 +590,7 @@ class ExceptionInfo(Generic[E]):
|
|||
def __repr__(self) -> str:
|
||||
if self._excinfo is None:
|
||||
return "<ExceptionInfo for raises contextmanager>"
|
||||
return "<{} {} tblen={}>".format(
|
||||
self.__class__.__name__, saferepr(self._excinfo[1]), len(self.traceback)
|
||||
)
|
||||
return f"<{self.__class__.__name__} {saferepr(self._excinfo[1])} tblen={len(self.traceback)}>"
|
||||
|
||||
def exconly(self, tryshort: bool = False) -> str:
|
||||
"""Return the exception as a string.
|
||||
|
@ -698,10 +699,21 @@ class ExceptionInfo(Generic[E]):
|
|||
return fmt.repr_excinfo(self)
|
||||
|
||||
def _stringify_exception(self, exc: BaseException) -> str:
|
||||
try:
|
||||
notes = getattr(exc, "__notes__", [])
|
||||
except KeyError:
|
||||
# Workaround for https://github.com/python/cpython/issues/98778 on
|
||||
# Python <= 3.9, and some 3.10 and 3.11 patch versions.
|
||||
HTTPError = getattr(sys.modules.get("urllib.error", None), "HTTPError", ())
|
||||
if sys.version_info[:2] <= (3, 11) and isinstance(exc, HTTPError):
|
||||
notes = []
|
||||
else:
|
||||
raise
|
||||
|
||||
return "\n".join(
|
||||
[
|
||||
str(exc),
|
||||
*getattr(exc, "__notes__", []),
|
||||
*notes,
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -1006,13 +1018,8 @@ class FormattedExcinfo:
|
|||
extraline: Optional[str] = (
|
||||
"!!! Recursion error detected, but an error occurred locating the origin of recursion.\n"
|
||||
" The following exception happened when comparing locals in the stack frame:\n"
|
||||
" {exc_type}: {exc_msg}\n"
|
||||
" Displaying first and last {max_frames} stack frames out of {total}."
|
||||
).format(
|
||||
exc_type=type(e).__name__,
|
||||
exc_msg=str(e),
|
||||
max_frames=max_frames,
|
||||
total=len(traceback),
|
||||
f" {type(e).__name__}: {e!s}\n"
|
||||
f" Displaying first and last {max_frames} stack frames out of {len(traceback)}."
|
||||
)
|
||||
# Type ignored because adding two instances of a List subtype
|
||||
# currently incorrectly has type List instead of the subtype.
|
||||
|
@ -1219,7 +1226,6 @@ class ReprEntry(TerminalRepr):
|
|||
the "E" prefix) using syntax highlighting, taking care to not highlighting the ">"
|
||||
character, as doing so might break line continuations.
|
||||
"""
|
||||
|
||||
if not self.lines:
|
||||
return
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
# mypy: allow-untyped-defs
|
||||
import ast
|
||||
from bisect import bisect_right
|
||||
import inspect
|
||||
import textwrap
|
||||
import tokenize
|
||||
import types
|
||||
import warnings
|
||||
from bisect import bisect_right
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
|
@ -12,6 +12,7 @@ from typing import Optional
|
|||
from typing import overload
|
||||
from typing import Tuple
|
||||
from typing import Union
|
||||
import warnings
|
||||
|
||||
|
||||
class Source:
|
||||
|
@ -196,7 +197,9 @@ def getstatementrange_ast(
|
|||
# by using the BlockFinder helper used which inspect.getsource() uses itself.
|
||||
block_finder = inspect.BlockFinder()
|
||||
# If we start with an indented line, put blockfinder to "started" mode.
|
||||
block_finder.started = source.lines[start][0].isspace()
|
||||
block_finder.started = (
|
||||
bool(source.lines[start]) and source.lines[start][0].isspace()
|
||||
)
|
||||
it = ((x + "\n") for x in source.lines[start:end])
|
||||
try:
|
||||
for tok in tokenize.generate_tokens(lambda: next(it)):
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# mypy: allow-untyped-defs
|
||||
# This module was imported from the cpython standard library
|
||||
# (https://github.com/python/cpython/) at commit
|
||||
# c5140945c723ae6c4b7ee81ff720ac8ea4b52cfd (python3.12).
|
||||
|
@ -14,9 +15,9 @@
|
|||
# useful, thank small children who sleep at night.
|
||||
import collections as _collections
|
||||
import dataclasses as _dataclasses
|
||||
from io import StringIO as _StringIO
|
||||
import re
|
||||
import types as _types
|
||||
from io import StringIO as _StringIO
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
|
|
|
@ -19,8 +19,8 @@ def _format_repr_exception(exc: BaseException, obj: object) -> str:
|
|||
raise
|
||||
except BaseException as exc:
|
||||
exc_info = f"unpresentable exception ({_try_repr_or_str(exc)})"
|
||||
return "<[{} raised in repr()] {} object at 0x{:x}>".format(
|
||||
exc_info, type(obj).__name__, id(obj)
|
||||
return (
|
||||
f"<[{exc_info} raised in repr()] {type(obj).__name__} object at 0x{id(obj):x}>"
|
||||
)
|
||||
|
||||
|
||||
|
@ -108,7 +108,6 @@ def saferepr(
|
|||
This function is a wrapper around the Repr/reprlib functionality of the
|
||||
stdlib.
|
||||
"""
|
||||
|
||||
return SafeRepr(maxsize, use_ascii).repr(obj)
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Helper functions for writing to terminals and files."""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
@ -183,9 +184,7 @@ class TerminalWriter:
|
|||
"""
|
||||
if indents and len(indents) != len(lines):
|
||||
raise ValueError(
|
||||
"indents size ({}) should have same size as lines ({})".format(
|
||||
len(indents), len(lines)
|
||||
)
|
||||
f"indents size ({len(indents)}) should have same size as lines ({len(lines)})"
|
||||
)
|
||||
if not indents:
|
||||
indents = [""] * len(lines)
|
||||
|
@ -233,17 +232,17 @@ class TerminalWriter:
|
|||
# which may lead to the previous color being propagated to the
|
||||
# start of the expression, so reset first.
|
||||
return "\x1b[0m" + highlighted
|
||||
except pygments.util.ClassNotFound:
|
||||
except pygments.util.ClassNotFound as e:
|
||||
raise UsageError(
|
||||
"PYTEST_THEME environment variable had an invalid value: '{}'. "
|
||||
"Only valid pygment styles are allowed.".format(
|
||||
os.getenv("PYTEST_THEME")
|
||||
)
|
||||
)
|
||||
except pygments.util.OptionError:
|
||||
) from e
|
||||
except pygments.util.OptionError as e:
|
||||
raise UsageError(
|
||||
"PYTEST_THEME_MODE environment variable had an invalid value: '{}'. "
|
||||
"The only allowed values are 'dark' and 'light'.".format(
|
||||
os.getenv("PYTEST_THEME_MODE")
|
||||
)
|
||||
)
|
||||
) from e
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import unicodedata
|
||||
from functools import lru_cache
|
||||
import unicodedata
|
||||
|
||||
|
||||
@lru_cache(100)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""create errno-specific classes for IO or os calls."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import errno
|
||||
|
@ -8,6 +9,7 @@ from typing import Callable
|
|||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
# mypy: allow-untyped-defs
|
||||
"""local path implementation."""
|
||||
from __future__ import annotations
|
||||
|
||||
import atexit
|
||||
from contextlib import contextmanager
|
||||
import fnmatch
|
||||
import importlib.util
|
||||
import io
|
||||
import os
|
||||
import posixpath
|
||||
import sys
|
||||
import uuid
|
||||
import warnings
|
||||
from contextlib import contextmanager
|
||||
from os.path import abspath
|
||||
from os.path import dirname
|
||||
from os.path import exists
|
||||
|
@ -19,18 +16,23 @@ from os.path import isdir
|
|||
from os.path import isfile
|
||||
from os.path import islink
|
||||
from os.path import normpath
|
||||
import posixpath
|
||||
from stat import S_ISDIR
|
||||
from stat import S_ISLNK
|
||||
from stat import S_ISREG
|
||||
import sys
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Literal
|
||||
from typing import overload
|
||||
from typing import TYPE_CHECKING
|
||||
import uuid
|
||||
import warnings
|
||||
|
||||
from . import error
|
||||
|
||||
|
||||
# Moved from local.py.
|
||||
iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt")
|
||||
|
||||
|
@ -450,7 +452,7 @@ class LocalPath:
|
|||
|
||||
def ensure_dir(self, *args):
|
||||
"""Ensure the path joined with args is a directory."""
|
||||
return self.ensure(*args, **{"dir": True})
|
||||
return self.ensure(*args, dir=True)
|
||||
|
||||
def bestrelpath(self, dest):
|
||||
"""Return a string which is a relative path from self
|
||||
|
@ -675,7 +677,7 @@ class LocalPath:
|
|||
else:
|
||||
kw.setdefault("dirname", dirname)
|
||||
kw.setdefault("sep", self.sep)
|
||||
obj.strpath = normpath("%(dirname)s%(sep)s%(basename)s" % kw)
|
||||
obj.strpath = normpath("{dirname}{sep}{basename}".format(**kw))
|
||||
return obj
|
||||
|
||||
def _getbyspec(self, spec: str) -> list[str]:
|
||||
|
@ -760,7 +762,10 @@ class LocalPath:
|
|||
# expected "Callable[[str, Any, Any], TextIOWrapper]" [arg-type]
|
||||
# Which seems incorrect, given io.open supports the given argument types.
|
||||
return error.checked_call(
|
||||
io.open, self.strpath, mode, encoding=encoding # type:ignore[arg-type]
|
||||
io.open,
|
||||
self.strpath,
|
||||
mode,
|
||||
encoding=encoding, # type:ignore[arg-type]
|
||||
)
|
||||
return error.checked_call(open, self.strpath, mode)
|
||||
|
||||
|
@ -779,11 +784,11 @@ class LocalPath:
|
|||
|
||||
valid checkers::
|
||||
|
||||
file=1 # is a file
|
||||
file=0 # is not a file (may not even exist)
|
||||
dir=1 # is a dir
|
||||
link=1 # is a link
|
||||
exists=1 # exists
|
||||
file = 1 # is a file
|
||||
file = 0 # is not a file (may not even exist)
|
||||
dir = 1 # is a dir
|
||||
link = 1 # is a link
|
||||
exists = 1 # exists
|
||||
|
||||
You can specify multiple checker definitions, for example::
|
||||
|
||||
|
@ -1100,9 +1105,7 @@ class LocalPath:
|
|||
modname = self.purebasename
|
||||
spec = importlib.util.spec_from_file_location(modname, str(self))
|
||||
if spec is None or spec.loader is None:
|
||||
raise ImportError(
|
||||
f"Can't find module {modname} at location {str(self)}"
|
||||
)
|
||||
raise ImportError(f"Can't find module {modname} at location {self!s}")
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
@ -1167,7 +1170,8 @@ class LocalPath:
|
|||
where the 'self' path points to executable.
|
||||
The process is directly invoked and not through a system shell.
|
||||
"""
|
||||
from subprocess import Popen, PIPE
|
||||
from subprocess import PIPE
|
||||
from subprocess import Popen
|
||||
|
||||
popen_opts.pop("stdout", None)
|
||||
popen_opts.pop("stderr", None)
|
||||
|
@ -1277,7 +1281,8 @@ class LocalPath:
|
|||
# error: Argument 1 has incompatible type overloaded function; expected "Callable[[str], str]" [arg-type]
|
||||
# Which seems incorrect, given tempfile.mkdtemp supports the given argument types.
|
||||
path = error.checked_call(
|
||||
tempfile.mkdtemp, dir=str(rootdir) # type:ignore[arg-type]
|
||||
tempfile.mkdtemp,
|
||||
dir=str(rootdir), # type:ignore[arg-type]
|
||||
)
|
||||
return cls(path)
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# mypy: allow-untyped-defs
|
||||
"""Support for presenting detailed information in failing assertions."""
|
||||
import sys
|
||||
from typing import Any
|
||||
|
@ -15,6 +16,7 @@ from _pytest.config import hookimpl
|
|||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.nodes import Item
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.main import Session
|
||||
|
||||
|
@ -128,7 +130,6 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
|
|||
reporting via the pytest_assertrepr_compare hook. This sets up this custom
|
||||
comparison for the test.
|
||||
"""
|
||||
|
||||
ihook = item.ihook
|
||||
|
||||
def callbinrepr(op, left: object, right: object) -> Optional[str]:
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
"""Rewrite assertion AST to produce nice error messages."""
|
||||
|
||||
import ast
|
||||
from collections import defaultdict
|
||||
import errno
|
||||
import functools
|
||||
import importlib.abc
|
||||
|
@ -9,13 +11,12 @@ import io
|
|||
import itertools
|
||||
import marshal
|
||||
import os
|
||||
from pathlib import Path
|
||||
from pathlib import PurePath
|
||||
import struct
|
||||
import sys
|
||||
import tokenize
|
||||
import types
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from pathlib import PurePath
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import IO
|
||||
|
@ -33,15 +34,17 @@ from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
|
|||
from _pytest._io.saferepr import saferepr
|
||||
from _pytest._version import version
|
||||
from _pytest.assertion import util
|
||||
from _pytest.assertion.util import ( # noqa: F401
|
||||
format_explanation as _format_explanation,
|
||||
)
|
||||
from _pytest.config import Config
|
||||
from _pytest.main import Session
|
||||
from _pytest.pathlib import absolutepath
|
||||
from _pytest.pathlib import fnmatch_ex
|
||||
from _pytest.stash import StashKey
|
||||
|
||||
|
||||
# fmt: off
|
||||
from _pytest.assertion.util import format_explanation as _format_explanation # noqa:F401, isort:skip
|
||||
# fmt:on
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.assertion import AssertionState
|
||||
|
||||
|
@ -858,9 +861,10 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||
the expression is false.
|
||||
"""
|
||||
if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1:
|
||||
from _pytest.warning_types import PytestAssertRewriteWarning
|
||||
import warnings
|
||||
|
||||
from _pytest.warning_types import PytestAssertRewriteWarning
|
||||
|
||||
# TODO: This assert should not be needed.
|
||||
assert self.module_path is not None
|
||||
warnings.warn_explicit(
|
||||
|
@ -921,7 +925,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||
# If any hooks implement assert_pass hook
|
||||
hook_impl_test = ast.If(
|
||||
self.helper("_check_if_assertion_pass_impl"),
|
||||
self.expl_stmts + [hook_call_pass],
|
||||
[*self.expl_stmts, hook_call_pass],
|
||||
[],
|
||||
)
|
||||
statements_pass = [hook_impl_test]
|
||||
|
@ -1002,7 +1006,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||
if i:
|
||||
fail_inner: List[ast.stmt] = []
|
||||
# cond is set in a prior loop iteration below
|
||||
self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa
|
||||
self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa: F821
|
||||
self.expl_stmts = fail_inner
|
||||
# Check if the left operand is a ast.NamedExpr and the value has already been visited
|
||||
if (
|
||||
|
@ -1016,9 +1020,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||
]
|
||||
):
|
||||
pytest_temp = self.variable()
|
||||
self.variables_overwrite[self.scope][
|
||||
v.left.target.id
|
||||
] = v.left # type:ignore[assignment]
|
||||
self.variables_overwrite[self.scope][v.left.target.id] = v.left # type:ignore[assignment]
|
||||
v.left.target.id = pytest_temp
|
||||
self.push_format_context()
|
||||
res, expl = self.visit(v)
|
||||
|
@ -1062,9 +1064,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||
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]
|
||||
arg = self.variables_overwrite[self.scope][arg.id] # type:ignore[assignment]
|
||||
res, expl = self.visit(arg)
|
||||
arg_expls.append(expl)
|
||||
new_args.append(res)
|
||||
|
@ -1072,9 +1072,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||
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]
|
||||
keyword.value = self.variables_overwrite[self.scope][keyword.value.id] # type:ignore[assignment]
|
||||
res, expl = self.visit(keyword.value)
|
||||
new_kwargs.append(ast.keyword(keyword.arg, res))
|
||||
if keyword.arg:
|
||||
|
@ -1111,13 +1109,9 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||
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]
|
||||
comp.left = self.variables_overwrite[self.scope][comp.left.id] # type:ignore[assignment]
|
||||
if isinstance(comp.left, ast.NamedExpr):
|
||||
self.variables_overwrite[self.scope][
|
||||
comp.left.target.id
|
||||
] = comp.left # type:ignore[assignment]
|
||||
self.variables_overwrite[self.scope][comp.left.target.id] = comp.left # type:ignore[assignment]
|
||||
left_res, left_expl = self.visit(comp.left)
|
||||
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
|
||||
left_expl = f"({left_expl})"
|
||||
|
@ -1135,9 +1129,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
|||
and next_operand.target.id == left_res.id
|
||||
):
|
||||
next_operand.target.id = self.variable()
|
||||
self.variables_overwrite[self.scope][
|
||||
left_res.id
|
||||
] = next_operand # type:ignore[assignment]
|
||||
self.variables_overwrite[self.scope][left_res.id] = next_operand # type:ignore[assignment]
|
||||
next_res, next_expl = self.visit(next_operand)
|
||||
if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
|
||||
next_expl = f"({next_expl})"
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
Current default behaviour is to truncate assertion explanations at
|
||||
terminal lines, unless running with an assertions verbosity level of at least 2 or running on CI.
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
|
@ -91,7 +92,8 @@ def _truncate_explanation(
|
|||
else:
|
||||
# Add proper ellipsis when we were able to fit a full line exactly
|
||||
truncated_explanation[-1] = "..."
|
||||
return truncated_explanation + [
|
||||
return [
|
||||
*truncated_explanation,
|
||||
"",
|
||||
f"...Full output truncated ({truncated_line_count} line"
|
||||
f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}",
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# mypy: allow-untyped-defs
|
||||
"""Utilities for assertion debugging."""
|
||||
import collections.abc
|
||||
import os
|
||||
|
@ -14,13 +15,14 @@ from typing import Protocol
|
|||
from typing import Sequence
|
||||
from unicodedata import normalize
|
||||
|
||||
import _pytest._code
|
||||
from _pytest import outcomes
|
||||
import _pytest._code
|
||||
from _pytest._io.pprint import PrettyPrinter
|
||||
from _pytest._io.saferepr import saferepr
|
||||
from _pytest._io.saferepr import saferepr_unlimited
|
||||
from _pytest.config import Config
|
||||
|
||||
|
||||
# The _reprcompare attribute on the util module is used by the new assertion
|
||||
# interpretation code and assertion rewriter to detect this plugin was
|
||||
# loaded and in turn call the hooks defined here as part of the
|
||||
|
@ -231,8 +233,8 @@ def assertrepr_compare(
|
|||
return None
|
||||
|
||||
if explanation[0] != "":
|
||||
explanation = [""] + explanation
|
||||
return [summary] + explanation
|
||||
explanation = ["", *explanation]
|
||||
return [summary, *explanation]
|
||||
|
||||
|
||||
def _compare_eq_any(
|
||||
|
@ -301,8 +303,8 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
|
|||
if i > 42:
|
||||
i -= 10 # Provide some context
|
||||
explanation += [
|
||||
"Skipping {} identical trailing "
|
||||
"characters in diff, use -v to show".format(i)
|
||||
f"Skipping {i} identical trailing "
|
||||
"characters in diff, use -v to show"
|
||||
]
|
||||
left = left[:-i]
|
||||
right = right[:-i]
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# mypy: allow-untyped-defs
|
||||
"""Implementation of the cache provider."""
|
||||
# This plugin was not named "cache" to avoid conflicts with the external
|
||||
# pytest-cache version.
|
||||
|
@ -31,6 +32,7 @@ from _pytest.nodes import Directory
|
|||
from _pytest.nodes import File
|
||||
from _pytest.reports import TestReport
|
||||
|
||||
|
||||
README_CONTENT = """\
|
||||
# pytest cache directory #
|
||||
|
||||
|
@ -111,6 +113,7 @@ class Cache:
|
|||
"""
|
||||
check_ispytest(_ispytest)
|
||||
import warnings
|
||||
|
||||
from _pytest.warning_types import PytestCacheWarning
|
||||
|
||||
warnings.warn(
|
||||
|
@ -366,15 +369,13 @@ class LFPlugin:
|
|||
|
||||
noun = "failure" if self._previously_failed_count == 1 else "failures"
|
||||
suffix = " first" if self.config.getoption("failedfirst") else ""
|
||||
self._report_status = "rerun previous {count} {noun}{suffix}".format(
|
||||
count=self._previously_failed_count, suffix=suffix, noun=noun
|
||||
self._report_status = (
|
||||
f"rerun previous {self._previously_failed_count} {noun}{suffix}"
|
||||
)
|
||||
|
||||
if self._skipped_files > 0:
|
||||
files_noun = "file" if self._skipped_files == 1 else "files"
|
||||
self._report_status += " (skipped {files} {files_noun})".format(
|
||||
files=self._skipped_files, files_noun=files_noun
|
||||
)
|
||||
self._report_status += f" (skipped {self._skipped_files} {files_noun})"
|
||||
else:
|
||||
self._report_status = "no previously failed tests, "
|
||||
if self.config.getoption("last_failed_no_failures") == "none":
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
# mypy: allow-untyped-defs
|
||||
"""Per-test stdout/stderr capturing mechanism."""
|
||||
import abc
|
||||
import collections
|
||||
import contextlib
|
||||
import io
|
||||
from io import UnsupportedOperation
|
||||
import os
|
||||
import sys
|
||||
from io import UnsupportedOperation
|
||||
from tempfile import TemporaryFile
|
||||
from types import TracebackType
|
||||
from typing import Any
|
||||
|
@ -38,6 +39,7 @@ from _pytest.nodes import File
|
|||
from _pytest.nodes import Item
|
||||
from _pytest.reports import CollectReport
|
||||
|
||||
|
||||
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
|
||||
|
||||
|
||||
|
@ -596,7 +598,8 @@ if sys.version_info >= (3, 11) or TYPE_CHECKING:
|
|||
else:
|
||||
|
||||
class CaptureResult(
|
||||
collections.namedtuple("CaptureResult", ["out", "err"]), Generic[AnyStr]
|
||||
collections.namedtuple("CaptureResult", ["out", "err"]), # noqa: PYI024
|
||||
Generic[AnyStr],
|
||||
):
|
||||
"""The result of :method:`caplog.readouterr() <pytest.CaptureFixture.readouterr>`."""
|
||||
|
||||
|
@ -789,9 +792,7 @@ class CaptureManager:
|
|||
current_fixture = self._capture_fixture.request.fixturename
|
||||
requested_fixture = capture_fixture.request.fixturename
|
||||
capture_fixture.request.raiseerror(
|
||||
"cannot use {} and {} at the same time".format(
|
||||
requested_fixture, current_fixture
|
||||
)
|
||||
f"cannot use {requested_fixture} and {current_fixture} at the same time"
|
||||
)
|
||||
self._capture_fixture = capture_fixture
|
||||
|
||||
|
@ -987,7 +988,6 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
|
|||
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_output(capsys):
|
||||
|
@ -1015,7 +1015,6 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None,
|
|||
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_output(capsysbinary):
|
||||
|
@ -1043,7 +1042,6 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
|
|||
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_system_echo(capfd):
|
||||
|
@ -1071,7 +1069,6 @@ def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, N
|
|||
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_system_echo(capfdbinary):
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# mypy: allow-untyped-defs
|
||||
"""Python version compatibility code."""
|
||||
from __future__ import annotations
|
||||
|
||||
|
@ -5,20 +6,15 @@ import dataclasses
|
|||
import enum
|
||||
import functools
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
from inspect import Parameter
|
||||
from inspect import signature
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Final
|
||||
from typing import NoReturn
|
||||
from typing import TypeVar
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_S = TypeVar("_S")
|
||||
|
||||
|
||||
# fmt: off
|
||||
|
@ -26,7 +22,7 @@ _S = TypeVar("_S")
|
|||
# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
|
||||
class NotSetType(enum.Enum):
|
||||
token = 0
|
||||
NOTSET: Final = NotSetType.token # noqa: E305
|
||||
NOTSET: Final = NotSetType.token
|
||||
# fmt: on
|
||||
|
||||
|
||||
|
@ -176,25 +172,13 @@ _non_printable_ascii_translate_table.update(
|
|||
)
|
||||
|
||||
|
||||
def _translate_non_printable(s: str) -> str:
|
||||
return s.translate(_non_printable_ascii_translate_table)
|
||||
|
||||
|
||||
STRING_TYPES = bytes, str
|
||||
|
||||
|
||||
def _bytes_to_ascii(val: bytes) -> str:
|
||||
return val.decode("ascii", "backslashreplace")
|
||||
|
||||
|
||||
def ascii_escaped(val: bytes | str) -> str:
|
||||
r"""If val is pure ASCII, return it as an str, otherwise, escape
|
||||
bytes objects into a sequence of escaped bytes:
|
||||
|
||||
b'\xc3\xb4\xc5\xd6' -> r'\xc3\xb4\xc5\xd6'
|
||||
|
||||
and escapes unicode objects into a sequence of escaped unicode
|
||||
ids, e.g.:
|
||||
and escapes strings into a sequence of escaped unicode ids, e.g.:
|
||||
|
||||
r'4\nV\U00043efa\x0eMXWB\x1e\u3028\u15fd\xcd\U0007d944'
|
||||
|
||||
|
@ -205,10 +189,10 @@ def ascii_escaped(val: bytes | str) -> str:
|
|||
a UTF-8 string.
|
||||
"""
|
||||
if isinstance(val, bytes):
|
||||
ret = _bytes_to_ascii(val)
|
||||
ret = val.decode("ascii", "backslashreplace")
|
||||
else:
|
||||
ret = val.encode("unicode_escape").decode("ascii")
|
||||
return _translate_non_printable(ret)
|
||||
return ret.translate(_non_printable_ascii_translate_table)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
|
@ -243,9 +227,7 @@ def get_real_func(obj):
|
|||
from _pytest._io.saferepr import saferepr
|
||||
|
||||
raise ValueError(
|
||||
("could not find real function of {start}\nstopped at {current}").format(
|
||||
start=saferepr(start_obj), current=saferepr(obj)
|
||||
)
|
||||
f"could not find real function of {saferepr(start_obj)}\nstopped at {saferepr(obj)}"
|
||||
)
|
||||
if isinstance(obj, functools.partial):
|
||||
obj = obj.func
|
||||
|
@ -307,7 +289,7 @@ def get_user_id() -> int | None:
|
|||
# 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":
|
||||
if sys.platform == "win32" or sys.platform == "emscripten": # noqa: PLR1714
|
||||
# win32 does not have a getuid() function.
|
||||
# Emscripten has a return 0 stub.
|
||||
return None
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
# mypy: allow-untyped-defs
|
||||
"""Command line options, ini-file and conftest.py processing."""
|
||||
import argparse
|
||||
import collections.abc
|
||||
import copy
|
||||
import dataclasses
|
||||
import enum
|
||||
from functools import lru_cache
|
||||
import glob
|
||||
import importlib.metadata
|
||||
import inspect
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shlex
|
||||
import sys
|
||||
import types
|
||||
import warnings
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
import types
|
||||
from types import FunctionType
|
||||
from types import TracebackType
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
|
@ -37,6 +36,7 @@ from typing import Tuple
|
|||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
import warnings
|
||||
|
||||
from pluggy import HookimplMarker
|
||||
from pluggy import HookimplOpts
|
||||
|
@ -44,15 +44,15 @@ from pluggy import HookspecMarker
|
|||
from pluggy import HookspecOpts
|
||||
from pluggy import PluginManager
|
||||
|
||||
import _pytest._code
|
||||
import _pytest.deprecated
|
||||
import _pytest.hookspec
|
||||
from .exceptions import PrintHelp as PrintHelp
|
||||
from .exceptions import UsageError as UsageError
|
||||
from .findpaths import determine_setup
|
||||
import _pytest._code
|
||||
from _pytest._code import ExceptionInfo
|
||||
from _pytest._code import filter_traceback
|
||||
from _pytest._io import TerminalWriter
|
||||
import _pytest.deprecated
|
||||
import _pytest.hookspec
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.outcomes import Skipped
|
||||
from _pytest.pathlib import absolutepath
|
||||
|
@ -65,10 +65,12 @@ from _pytest.stash import Stash
|
|||
from _pytest.warning_types import PytestConfigWarning
|
||||
from _pytest.warning_types import warn_explicit_for
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .argparsing import Argument
|
||||
from .argparsing import Parser
|
||||
from _pytest._code.code import _TracebackStyle
|
||||
from _pytest.terminal import TerminalReporter
|
||||
from .argparsing import Argument, Parser
|
||||
|
||||
|
||||
_PluggyPlugin = object
|
||||
|
@ -112,16 +114,14 @@ class ConftestImportFailure(Exception):
|
|||
def __init__(
|
||||
self,
|
||||
path: Path,
|
||||
excinfo: Tuple[Type[Exception], Exception, TracebackType],
|
||||
*,
|
||||
cause: Exception,
|
||||
) -> None:
|
||||
super().__init__(path, excinfo)
|
||||
self.path = path
|
||||
self.excinfo = excinfo
|
||||
self.cause = cause
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "{}: {} (from {})".format(
|
||||
self.excinfo[0].__name__, self.excinfo[1], self.path
|
||||
)
|
||||
return f"{type(self.cause).__name__}: {self.cause} (from {self.path})"
|
||||
|
||||
|
||||
def filter_traceback_for_conftest_import_failure(
|
||||
|
@ -152,7 +152,7 @@ def main(
|
|||
try:
|
||||
config = _prepareconfig(args, plugins)
|
||||
except ConftestImportFailure as e:
|
||||
exc_info = ExceptionInfo.from_exc_info(e.excinfo)
|
||||
exc_info = ExceptionInfo.from_exception(e.cause)
|
||||
tw = TerminalWriter(sys.stderr)
|
||||
tw.line(f"ImportError while loading conftest '{e.path}'.", red=True)
|
||||
exc_info.traceback = exc_info.traceback.filter(
|
||||
|
@ -238,7 +238,8 @@ essential_plugins = (
|
|||
"helpconfig", # Provides -p.
|
||||
)
|
||||
|
||||
default_plugins = essential_plugins + (
|
||||
default_plugins = (
|
||||
*essential_plugins,
|
||||
"python",
|
||||
"terminal",
|
||||
"debugging",
|
||||
|
@ -493,15 +494,19 @@ class PytestPluginManager(PluginManager):
|
|||
)
|
||||
)
|
||||
return None
|
||||
ret: Optional[str] = super().register(plugin, name)
|
||||
if ret:
|
||||
plugin_name = super().register(plugin, name)
|
||||
if plugin_name is not None:
|
||||
self.hook.pytest_plugin_registered.call_historic(
|
||||
kwargs=dict(plugin=plugin, manager=self)
|
||||
kwargs=dict(
|
||||
plugin=plugin,
|
||||
plugin_name=plugin_name,
|
||||
manager=self,
|
||||
)
|
||||
)
|
||||
|
||||
if isinstance(plugin, types.ModuleType):
|
||||
self.consider_module(plugin)
|
||||
return ret
|
||||
return plugin_name
|
||||
|
||||
def getplugin(self, name: str):
|
||||
# Support deprecated naming because plugins (xdist e.g.) use it.
|
||||
|
@ -540,6 +545,7 @@ class PytestPluginManager(PluginManager):
|
|||
noconftest: bool,
|
||||
rootpath: Path,
|
||||
confcutdir: Optional[Path],
|
||||
invocation_dir: Path,
|
||||
importmode: Union[ImportMode, str],
|
||||
) -> None:
|
||||
"""Load initial conftest files given a preparsed "namespace".
|
||||
|
@ -549,8 +555,9 @@ class PytestPluginManager(PluginManager):
|
|||
All builtin and 3rd party plugins will have been loaded, however, so
|
||||
common options will not confuse our logic here.
|
||||
"""
|
||||
current = Path.cwd()
|
||||
self._confcutdir = absolutepath(current / confcutdir) if confcutdir else None
|
||||
self._confcutdir = (
|
||||
absolutepath(invocation_dir / confcutdir) if confcutdir else None
|
||||
)
|
||||
self._noconftest = noconftest
|
||||
self._using_pyargs = pyargs
|
||||
foundanchor = False
|
||||
|
@ -560,7 +567,7 @@ class PytestPluginManager(PluginManager):
|
|||
i = path.find("::")
|
||||
if i != -1:
|
||||
path = path[:i]
|
||||
anchor = absolutepath(current / path)
|
||||
anchor = absolutepath(invocation_dir / path)
|
||||
|
||||
# Ensure we do not break if what appears to be an anchor
|
||||
# is in fact a very long option (#10169, #11394).
|
||||
|
@ -568,15 +575,21 @@ class PytestPluginManager(PluginManager):
|
|||
self._try_load_conftest(anchor, importmode, rootpath)
|
||||
foundanchor = True
|
||||
if not foundanchor:
|
||||
self._try_load_conftest(current, importmode, rootpath)
|
||||
self._try_load_conftest(invocation_dir, importmode, rootpath)
|
||||
|
||||
def _is_in_confcutdir(self, path: Path) -> bool:
|
||||
"""Whether a path is within the confcutdir.
|
||||
|
||||
When false, should not load conftest.
|
||||
"""
|
||||
"""Whether to consider the given path to load conftests from."""
|
||||
if self._confcutdir is None:
|
||||
return True
|
||||
# The semantics here are literally:
|
||||
# Do not load a conftest if it is found upwards from confcut dir.
|
||||
# But this is *not* the same as:
|
||||
# Load only conftests from confcutdir or below.
|
||||
# At first glance they might seem the same thing, however we do support use cases where
|
||||
# we want to load conftests that are not found in confcutdir or below, but are found
|
||||
# in completely different directory hierarchies like packages installed
|
||||
# in out-of-source trees.
|
||||
# (see #9767 for a regression where the logic was inverted).
|
||||
return path not in self._confcutdir.parents
|
||||
|
||||
def _try_load_conftest(
|
||||
|
@ -602,9 +615,6 @@ class PytestPluginManager(PluginManager):
|
|||
if directory in self._dirpath2confmods:
|
||||
return
|
||||
|
||||
# XXX these days we may rather want to use config.rootpath
|
||||
# and allow users to opt into looking into the rootdir parent
|
||||
# directories instead of requiring to specify confcutdir.
|
||||
clist = []
|
||||
for parent in reversed((directory, *directory.parents)):
|
||||
if self._is_in_confcutdir(parent):
|
||||
|
@ -634,7 +644,8 @@ class PytestPluginManager(PluginManager):
|
|||
def _importconftest(
|
||||
self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path
|
||||
) -> types.ModuleType:
|
||||
existing = self.get_plugin(str(conftestpath))
|
||||
conftestpath_plugin_name = str(conftestpath)
|
||||
existing = self.get_plugin(conftestpath_plugin_name)
|
||||
if existing is not None:
|
||||
return cast(types.ModuleType, existing)
|
||||
|
||||
|
@ -653,8 +664,7 @@ class PytestPluginManager(PluginManager):
|
|||
mod = import_path(conftestpath, mode=importmode, root=rootpath)
|
||||
except Exception as e:
|
||||
assert e.__traceback__ is not None
|
||||
exc_info = (type(e), e, e.__traceback__)
|
||||
raise ConftestImportFailure(conftestpath, exc_info) from e
|
||||
raise ConftestImportFailure(conftestpath, cause=e) from e
|
||||
|
||||
self._check_non_top_pytest_plugins(mod, conftestpath)
|
||||
|
||||
|
@ -663,10 +673,15 @@ class PytestPluginManager(PluginManager):
|
|||
if dirpath in self._dirpath2confmods:
|
||||
for path, mods in self._dirpath2confmods.items():
|
||||
if dirpath in path.parents or path == dirpath:
|
||||
assert mod not in mods
|
||||
if mod in mods:
|
||||
raise AssertionError(
|
||||
f"While trying to load conftest path {conftestpath!s}, "
|
||||
f"found that the module {mod} is already loaded with path {mod.__file__}. "
|
||||
"This is not supposed to happen. Please report this issue to pytest."
|
||||
)
|
||||
mods.append(mod)
|
||||
self.trace(f"loading conftestmodule {mod!r}")
|
||||
self.consider_conftest(mod)
|
||||
self.consider_conftest(mod, registration_name=conftestpath_plugin_name)
|
||||
return mod
|
||||
|
||||
def _check_non_top_pytest_plugins(
|
||||
|
@ -737,18 +752,17 @@ class PytestPluginManager(PluginManager):
|
|||
self.set_blocked("pytest_" + name)
|
||||
else:
|
||||
name = arg
|
||||
# Unblock the plugin. None indicates that it has been blocked.
|
||||
# There is no interface with pluggy for this.
|
||||
if self._name2plugin.get(name, -1) is None:
|
||||
del self._name2plugin[name]
|
||||
# Unblock the plugin.
|
||||
self.unblock(name)
|
||||
if not name.startswith("pytest_"):
|
||||
if self._name2plugin.get("pytest_" + name, -1) is None:
|
||||
del self._name2plugin["pytest_" + name]
|
||||
self.unblock("pytest_" + name)
|
||||
self.import_plugin(arg, consider_entry_points=True)
|
||||
|
||||
def consider_conftest(self, conftestmodule: types.ModuleType) -> None:
|
||||
def consider_conftest(
|
||||
self, conftestmodule: types.ModuleType, registration_name: str
|
||||
) -> None:
|
||||
""":meta private:"""
|
||||
self.register(conftestmodule, name=conftestmodule.__file__)
|
||||
self.register(conftestmodule, name=registration_name)
|
||||
|
||||
def consider_env(self) -> None:
|
||||
""":meta private:"""
|
||||
|
@ -804,7 +818,7 @@ class PytestPluginManager(PluginManager):
|
|||
|
||||
|
||||
def _get_plugin_specs_as_list(
|
||||
specs: Union[None, types.ModuleType, str, Sequence[str]]
|
||||
specs: Union[None, types.ModuleType, str, Sequence[str]],
|
||||
) -> List[str]:
|
||||
"""Parse a plugins specification into a list of plugin names."""
|
||||
# None means empty.
|
||||
|
@ -965,7 +979,8 @@ class Config:
|
|||
*,
|
||||
invocation_params: Optional[InvocationParams] = None,
|
||||
) -> None:
|
||||
from .argparsing import Parser, FILE_OR_DIR
|
||||
from .argparsing import FILE_OR_DIR
|
||||
from .argparsing import Parser
|
||||
|
||||
if invocation_params is None:
|
||||
invocation_params = self.InvocationParams(
|
||||
|
@ -1160,6 +1175,7 @@ class Config:
|
|||
noconftest=early_config.known_args_namespace.noconftest,
|
||||
rootpath=early_config.rootpath,
|
||||
confcutdir=early_config.known_args_namespace.confcutdir,
|
||||
invocation_dir=early_config.invocation_params.dir,
|
||||
importmode=early_config.known_args_namespace.importmode,
|
||||
)
|
||||
|
||||
|
@ -1168,8 +1184,8 @@ class Config:
|
|||
args, namespace=copy.copy(self.option)
|
||||
)
|
||||
rootpath, inipath, inicfg = determine_setup(
|
||||
ns.inifilename,
|
||||
ns.file_or_dir + unknown_args,
|
||||
inifile=ns.inifilename,
|
||||
args=ns.file_or_dir + unknown_args,
|
||||
rootdir_cmd_arg=ns.rootdir or None,
|
||||
invocation_dir=self.invocation_params.dir,
|
||||
)
|
||||
|
@ -1253,6 +1269,8 @@ class Config:
|
|||
"""Decide the args (initial paths/nodeids) to use given the relevant inputs.
|
||||
|
||||
:param warn: Whether can issue warnings.
|
||||
|
||||
:returns: The args and the args source. Guaranteed to be non-empty.
|
||||
"""
|
||||
if args:
|
||||
source = Config.ArgsSource.ARGS
|
||||
|
@ -1361,12 +1379,7 @@ class Config:
|
|||
|
||||
if Version(minver) > Version(pytest.__version__):
|
||||
raise pytest.UsageError(
|
||||
"%s: 'minversion' requires pytest-%s, actual pytest-%s'"
|
||||
% (
|
||||
self.inipath,
|
||||
minver,
|
||||
pytest.__version__,
|
||||
)
|
||||
f"{self.inipath}: 'minversion' requires pytest-{minver}, actual pytest-{pytest.__version__}'"
|
||||
)
|
||||
|
||||
def _validate_config_options(self) -> None:
|
||||
|
@ -1379,8 +1392,9 @@ class Config:
|
|||
return
|
||||
|
||||
# Imported lazily to improve start-up time.
|
||||
from packaging.requirements import InvalidRequirement
|
||||
from packaging.requirements import Requirement
|
||||
from packaging.version import Version
|
||||
from packaging.requirements import InvalidRequirement, Requirement
|
||||
|
||||
plugin_info = self.pluginmanager.list_plugin_distinfo()
|
||||
plugin_dist_info = {dist.project_name: dist.version for _, dist in plugin_info}
|
||||
|
@ -1552,9 +1566,11 @@ class Config:
|
|||
# in this case, we already have a list ready to use.
|
||||
#
|
||||
if type == "paths":
|
||||
# TODO: This assert is probably not valid in all cases.
|
||||
assert self.inipath is not None
|
||||
dp = self.inipath.parent
|
||||
dp = (
|
||||
self.inipath.parent
|
||||
if self.inipath is not None
|
||||
else self.invocation_params.dir
|
||||
)
|
||||
input_values = shlex.split(value) if isinstance(value, str) else value
|
||||
return [dp / x for x in input_values]
|
||||
elif type == "args":
|
||||
|
@ -1600,9 +1616,7 @@ class Config:
|
|||
key, user_ini_value = ini_config.split("=", 1)
|
||||
except ValueError as e:
|
||||
raise UsageError(
|
||||
"-o/--override-ini expects option=value style (got: {!r}).".format(
|
||||
ini_config
|
||||
)
|
||||
f"-o/--override-ini expects option=value style (got: {ini_config!r})."
|
||||
) from e
|
||||
else:
|
||||
if key == name:
|
||||
|
@ -1662,7 +1676,6 @@ class Config:
|
|||
can be used to explicitly use the global verbosity level.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
# content of pytest.ini
|
||||
|
@ -1842,13 +1855,13 @@ def parse_warning_filter(
|
|||
try:
|
||||
action: "warnings._ActionKind" = warnings._getaction(action_) # type: ignore[attr-defined]
|
||||
except warnings._OptionError as e:
|
||||
raise UsageError(error_template.format(error=str(e)))
|
||||
raise UsageError(error_template.format(error=str(e))) from None
|
||||
try:
|
||||
category: Type[Warning] = _resolve_warning_category(category_)
|
||||
except Exception:
|
||||
exc_info = ExceptionInfo.from_current()
|
||||
exception_text = exc_info.getrepr(style="native")
|
||||
raise UsageError(error_template.format(error=exception_text))
|
||||
raise UsageError(error_template.format(error=exception_text)) from None
|
||||
if message and escape:
|
||||
message = re.escape(message)
|
||||
if module and escape:
|
||||
|
@ -1861,7 +1874,7 @@ def parse_warning_filter(
|
|||
except ValueError as e:
|
||||
raise UsageError(
|
||||
error_template.format(error=f"invalid lineno {lineno_!r}: {e}")
|
||||
)
|
||||
) from None
|
||||
else:
|
||||
lineno = 0
|
||||
return action, message, category, module, lineno
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
# mypy: allow-untyped-defs
|
||||
import argparse
|
||||
from gettext import gettext
|
||||
import os
|
||||
import sys
|
||||
from gettext import gettext
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
|
@ -20,6 +21,7 @@ import _pytest._io
|
|||
from _pytest.config.exceptions import UsageError
|
||||
from _pytest.deprecated import check_ispytest
|
||||
|
||||
|
||||
FILE_OR_DIR = "file_or_dir"
|
||||
|
||||
|
||||
|
@ -120,7 +122,7 @@ class Parser:
|
|||
from _pytest._argcomplete import filescompleter
|
||||
|
||||
optparser = MyOptionParser(self, self.extra_info, prog=self.prog)
|
||||
groups = self._groups + [self._anonymous]
|
||||
groups = [*self._groups, self._anonymous]
|
||||
for group in groups:
|
||||
if group.options:
|
||||
desc = group.description or group.name
|
||||
|
@ -196,9 +198,16 @@ class Parser:
|
|||
* ``paths``: a list of :class:`pathlib.Path`, separated as in a shell
|
||||
* ``pathlist``: a list of ``py.path``, separated as in a shell
|
||||
|
||||
For ``paths`` and ``pathlist`` types, they are considered relative to the ini-file.
|
||||
In case the execution is happening without an ini-file defined,
|
||||
they will be considered relative to the current working directory (for example with ``--override-ini``).
|
||||
|
||||
.. versionadded:: 7.0
|
||||
The ``paths`` variable type.
|
||||
|
||||
.. versionadded:: 8.1
|
||||
Use the current working directory to resolve ``paths`` and ``pathlist`` in the absence of an ini-file.
|
||||
|
||||
Defaults to ``string`` if ``None`` or not passed.
|
||||
:param default:
|
||||
Default value if no ini-file option exists but is queried.
|
||||
|
@ -215,7 +224,7 @@ class Parser:
|
|||
|
||||
|
||||
def get_ini_default_for_type(
|
||||
type: Optional[Literal["string", "paths", "pathlist", "args", "linelist", "bool"]]
|
||||
type: Optional[Literal["string", "paths", "pathlist", "args", "linelist", "bool"]],
|
||||
) -> Any:
|
||||
"""
|
||||
Used by addini to get the default value for a given ini-option type, when
|
||||
|
@ -447,7 +456,7 @@ class MyOptionParser(argparse.ArgumentParser):
|
|||
) -> Optional[Tuple[Optional[argparse.Action], str, Optional[str]]]:
|
||||
if not arg_string:
|
||||
return None
|
||||
if not arg_string[0] in self.prefix_chars:
|
||||
if arg_string[0] not in self.prefix_chars:
|
||||
return None
|
||||
if arg_string in self._option_string_actions:
|
||||
action = self._option_string_actions[arg_string]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
|
@ -37,7 +37,6 @@ def load_config_dict_from_file(
|
|||
|
||||
Return None if the file does not contain valid pytest configuration.
|
||||
"""
|
||||
|
||||
# Configuration from ini files are obtained from the [pytest] section, if present.
|
||||
if filepath.suffix == ".ini":
|
||||
iniconfig = _parse_ini_config(filepath)
|
||||
|
@ -87,6 +86,7 @@ def load_config_dict_from_file(
|
|||
|
||||
|
||||
def locate_config(
|
||||
invocation_dir: Path,
|
||||
args: Iterable[Path],
|
||||
) -> Tuple[Optional[Path], Optional[Path], Dict[str, Union[str, List[str]]]]:
|
||||
"""Search in the list of arguments for a valid ini-file for pytest,
|
||||
|
@ -100,20 +100,28 @@ def locate_config(
|
|||
]
|
||||
args = [x for x in args if not str(x).startswith("-")]
|
||||
if not args:
|
||||
args = [Path.cwd()]
|
||||
args = [invocation_dir]
|
||||
found_pyproject_toml: Optional[Path] = None
|
||||
for arg in args:
|
||||
argpath = absolutepath(arg)
|
||||
for base in (argpath, *argpath.parents):
|
||||
for config_name in config_names:
|
||||
p = base / config_name
|
||||
if p.is_file():
|
||||
if p.name == "pyproject.toml" and found_pyproject_toml is None:
|
||||
found_pyproject_toml = p
|
||||
ini_config = load_config_dict_from_file(p)
|
||||
if ini_config is not None:
|
||||
return base, p, ini_config
|
||||
if found_pyproject_toml is not None:
|
||||
return found_pyproject_toml.parent, found_pyproject_toml, {}
|
||||
return None, None, {}
|
||||
|
||||
|
||||
def get_common_ancestor(paths: Iterable[Path]) -> Path:
|
||||
def get_common_ancestor(
|
||||
invocation_dir: Path,
|
||||
paths: Iterable[Path],
|
||||
) -> Path:
|
||||
common_ancestor: Optional[Path] = None
|
||||
for path in paths:
|
||||
if not path.exists():
|
||||
|
@ -130,7 +138,7 @@ def get_common_ancestor(paths: Iterable[Path]) -> Path:
|
|||
if shared is not None:
|
||||
common_ancestor = shared
|
||||
if common_ancestor is None:
|
||||
common_ancestor = Path.cwd()
|
||||
common_ancestor = invocation_dir
|
||||
elif common_ancestor.is_file():
|
||||
common_ancestor = common_ancestor.parent
|
||||
return common_ancestor
|
||||
|
@ -162,10 +170,11 @@ CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supporte
|
|||
|
||||
|
||||
def determine_setup(
|
||||
*,
|
||||
inifile: Optional[str],
|
||||
args: Sequence[str],
|
||||
rootdir_cmd_arg: Optional[str] = None,
|
||||
invocation_dir: Optional[Path] = None,
|
||||
rootdir_cmd_arg: Optional[str],
|
||||
invocation_dir: Path,
|
||||
) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]:
|
||||
"""Determine the rootdir, inifile and ini configuration values from the
|
||||
command line arguments.
|
||||
|
@ -177,8 +186,7 @@ def determine_setup(
|
|||
:param rootdir_cmd_arg:
|
||||
The `--rootdir` command line argument, if given.
|
||||
:param invocation_dir:
|
||||
The working directory when pytest was invoked, if known.
|
||||
If not known, the current working directory is used.
|
||||
The working directory when pytest was invoked.
|
||||
"""
|
||||
rootdir = None
|
||||
dirs = get_dirs_from_args(args)
|
||||
|
@ -189,8 +197,8 @@ def determine_setup(
|
|||
if rootdir_cmd_arg is None:
|
||||
rootdir = inipath_.parent
|
||||
else:
|
||||
ancestor = get_common_ancestor(dirs)
|
||||
rootdir, inipath, inicfg = locate_config([ancestor])
|
||||
ancestor = get_common_ancestor(invocation_dir, dirs)
|
||||
rootdir, inipath, inicfg = locate_config(invocation_dir, [ancestor])
|
||||
if rootdir is None and rootdir_cmd_arg is None:
|
||||
for possible_rootdir in (ancestor, *ancestor.parents):
|
||||
if (possible_rootdir / "setup.py").is_file():
|
||||
|
@ -198,22 +206,18 @@ def determine_setup(
|
|||
break
|
||||
else:
|
||||
if dirs != [ancestor]:
|
||||
rootdir, inipath, inicfg = locate_config(dirs)
|
||||
rootdir, inipath, inicfg = locate_config(invocation_dir, dirs)
|
||||
if rootdir is None:
|
||||
if invocation_dir is not None:
|
||||
cwd = invocation_dir
|
||||
else:
|
||||
cwd = Path.cwd()
|
||||
rootdir = get_common_ancestor([cwd, ancestor])
|
||||
rootdir = get_common_ancestor(
|
||||
invocation_dir, [invocation_dir, ancestor]
|
||||
)
|
||||
if is_fs_root(rootdir):
|
||||
rootdir = ancestor
|
||||
if rootdir_cmd_arg:
|
||||
rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg))
|
||||
if not rootdir.is_dir():
|
||||
raise UsageError(
|
||||
"Directory '{}' not found. Check your '--rootdir' option.".format(
|
||||
rootdir
|
||||
)
|
||||
f"Directory '{rootdir}' not found. Check your '--rootdir' option."
|
||||
)
|
||||
assert rootdir is not None
|
||||
return rootdir, inipath, inicfg or {}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
# mypy: allow-untyped-defs
|
||||
"""Interactive debugging with PDB, the Python Debugger."""
|
||||
import argparse
|
||||
import functools
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Generator
|
||||
|
@ -13,6 +13,7 @@ from typing import Tuple
|
|||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
import unittest
|
||||
|
||||
from _pytest import outcomes
|
||||
from _pytest._code import ExceptionInfo
|
||||
|
@ -25,6 +26,7 @@ from _pytest.config.exceptions import UsageError
|
|||
from _pytest.nodes import Node
|
||||
from _pytest.reports import BaseReport
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.capture import CaptureManager
|
||||
from _pytest.runner import CallInfo
|
||||
|
@ -263,8 +265,7 @@ class pytestPDB:
|
|||
elif capturing:
|
||||
tw.sep(
|
||||
">",
|
||||
"PDB %s (IO-capturing turned off for %s)"
|
||||
% (method, capturing),
|
||||
f"PDB {method} (IO-capturing turned off for {capturing})",
|
||||
)
|
||||
else:
|
||||
tw.sep(">", f"PDB {method}")
|
||||
|
@ -377,7 +378,8 @@ def _postmortem_traceback(excinfo: ExceptionInfo[BaseException]) -> types.Traceb
|
|||
elif isinstance(excinfo.value, ConftestImportFailure):
|
||||
# A config.ConftestImportFailure is not useful for post_mortem.
|
||||
# Use the underlying exception instead:
|
||||
return excinfo.value.excinfo[2]
|
||||
assert excinfo.value.cause.__traceback__ is not None
|
||||
return excinfo.value.cause.__traceback__
|
||||
else:
|
||||
assert excinfo._excinfo is not None
|
||||
return excinfo._excinfo[2]
|
||||
|
|
|
@ -8,12 +8,14 @@ All constants defined in this module should be either instances of
|
|||
:class:`PytestWarning`, or :class:`UnformattedWarning`
|
||||
in case of warnings which need to format their messages.
|
||||
"""
|
||||
|
||||
from warnings import warn
|
||||
|
||||
from _pytest.warning_types import PytestDeprecationWarning
|
||||
from _pytest.warning_types import PytestRemovedIn9Warning
|
||||
from _pytest.warning_types import UnformattedWarning
|
||||
|
||||
|
||||
# set of plugins which have been integrated into the core; we use this list to ignore
|
||||
# them during registration to avoid conflicts
|
||||
DEPRECATED_EXTERNAL_PLUGINS = {
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
# mypy: allow-untyped-defs
|
||||
"""Discover and run doctests in modules and test files."""
|
||||
import bdb
|
||||
from contextlib import contextmanager
|
||||
import functools
|
||||
import inspect
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import sys
|
||||
import traceback
|
||||
import types
|
||||
import warnings
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
|
@ -23,6 +23,7 @@ from typing import Tuple
|
|||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
import warnings
|
||||
|
||||
from _pytest import outcomes
|
||||
from _pytest._code.code import ExceptionInfo
|
||||
|
@ -39,13 +40,14 @@ from _pytest.nodes import Item
|
|||
from _pytest.outcomes import OutcomeException
|
||||
from _pytest.outcomes import skip
|
||||
from _pytest.pathlib import fnmatch_ex
|
||||
from _pytest.pathlib import import_path
|
||||
from _pytest.python import Module
|
||||
from _pytest.python_api import approx
|
||||
from _pytest.warning_types import PytestWarning
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import doctest
|
||||
from typing import Self
|
||||
|
||||
DOCTEST_REPORT_CHOICE_NONE = "none"
|
||||
DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
|
||||
|
@ -105,7 +107,7 @@ def pytest_addoption(parser: Parser) -> None:
|
|||
"--doctest-ignore-import-errors",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Ignore doctest ImportErrors",
|
||||
help="Ignore doctest collection errors",
|
||||
dest="doctest_ignore_import_errors",
|
||||
)
|
||||
group.addoption(
|
||||
|
@ -132,11 +134,9 @@ def pytest_collect_file(
|
|||
if config.option.doctestmodules and not any(
|
||||
(_is_setup_py(file_path), _is_main_py(file_path))
|
||||
):
|
||||
mod: DoctestModule = DoctestModule.from_parent(parent, path=file_path)
|
||||
return mod
|
||||
return DoctestModule.from_parent(parent, path=file_path)
|
||||
elif _is_doctest(config, file_path, parent):
|
||||
txt: DoctestTextfile = DoctestTextfile.from_parent(parent, path=file_path)
|
||||
return txt
|
||||
return DoctestTextfile.from_parent(parent, path=file_path)
|
||||
return None
|
||||
|
||||
|
||||
|
@ -271,14 +271,14 @@ class DoctestItem(Item):
|
|||
self._initrequest()
|
||||
|
||||
@classmethod
|
||||
def from_parent( # type: ignore
|
||||
def from_parent( # type: ignore[override]
|
||||
cls,
|
||||
parent: "Union[DoctestTextfile, DoctestModule]",
|
||||
*,
|
||||
name: str,
|
||||
runner: "doctest.DocTestRunner",
|
||||
dtest: "doctest.DocTest",
|
||||
):
|
||||
) -> "Self":
|
||||
# incompatible signature due to imposed limits on subclass
|
||||
"""The public named constructor."""
|
||||
return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
|
||||
|
@ -485,9 +485,9 @@ def _patch_unwrap_mock_aware() -> Generator[None, None, None]:
|
|||
return real_unwrap(func, stop=lambda obj: _is_mocked(obj) or _stop(func))
|
||||
except Exception as e:
|
||||
warnings.warn(
|
||||
"Got %r when unwrapping %r. This is usually caused "
|
||||
f"Got {e!r} when unwrapping {func!r}. This is usually caused "
|
||||
"by a violation of Python's object protocol; see e.g. "
|
||||
"https://github.com/pytest-dev/pytest/issues/5080" % (e, func),
|
||||
"https://github.com/pytest-dev/pytest/issues/5080",
|
||||
PytestWarning,
|
||||
)
|
||||
raise
|
||||
|
@ -558,24 +558,18 @@ class DoctestModule(Module):
|
|||
else: # pragma: no cover
|
||||
pass
|
||||
|
||||
if self.path.name == "conftest.py":
|
||||
module = self.config.pluginmanager._importconftest(
|
||||
self.path,
|
||||
self.config.getoption("importmode"),
|
||||
rootpath=self.config.rootpath,
|
||||
)
|
||||
else:
|
||||
try:
|
||||
module = import_path(
|
||||
self.path,
|
||||
root=self.config.rootpath,
|
||||
mode=self.config.getoption("importmode"),
|
||||
)
|
||||
except ImportError:
|
||||
if self.config.getvalue("doctest_ignore_import_errors"):
|
||||
skip("unable to import module %r" % self.path)
|
||||
else:
|
||||
raise
|
||||
try:
|
||||
module = self.obj
|
||||
except Collector.CollectError:
|
||||
if self.config.getvalue("doctest_ignore_import_errors"):
|
||||
skip("unable to import module %r" % self.path)
|
||||
else:
|
||||
raise
|
||||
|
||||
# While doctests currently don't support fixtures directly, we still
|
||||
# need to pick up autouse fixtures.
|
||||
self.session._fixturemanager.parsefactories(self)
|
||||
|
||||
# Uses internal doctest module parsing mechanism.
|
||||
finder = MockAwareDocTestFinder()
|
||||
optionflags = get_optionflags(self.config)
|
||||
|
|
|
@ -2,11 +2,11 @@ import os
|
|||
import sys
|
||||
from typing import Generator
|
||||
|
||||
import pytest
|
||||
from _pytest.config import Config
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.nodes import Item
|
||||
from _pytest.stash import StashKey
|
||||
import pytest
|
||||
|
||||
|
||||
fault_handler_original_stderr_fd_key = StashKey[int]()
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
# mypy: allow-untyped-defs
|
||||
import abc
|
||||
from collections import defaultdict
|
||||
from collections import deque
|
||||
import dataclasses
|
||||
import functools
|
||||
import inspect
|
||||
import os
|
||||
import warnings
|
||||
from collections import defaultdict
|
||||
from collections import deque
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
from typing import AbstractSet
|
||||
from typing import Any
|
||||
|
@ -30,6 +29,7 @@ from typing import Tuple
|
|||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
import warnings
|
||||
|
||||
import _pytest
|
||||
from _pytest import nodes
|
||||
|
@ -119,7 +119,7 @@ def get_scope_package(
|
|||
) -> Optional[nodes.Node]:
|
||||
from _pytest.python import Package
|
||||
|
||||
for parent in node.iterparents():
|
||||
for parent in node.iter_parents():
|
||||
if isinstance(parent, Package) and parent.nodeid == fixturedef.baseid:
|
||||
return parent
|
||||
return node.session
|
||||
|
@ -168,33 +168,28 @@ def get_parametrized_fixture_keys(
|
|||
the specified scope."""
|
||||
assert scope is not Scope.Function
|
||||
try:
|
||||
callspec = item.callspec # type: ignore[attr-defined]
|
||||
callspec: CallSpec2 = item.callspec # type: ignore[attr-defined]
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
cs: CallSpec2 = callspec
|
||||
# cs.indices is random order of argnames. Need to
|
||||
# sort this so that different calls to
|
||||
# get_parametrized_fixture_keys will be deterministic.
|
||||
for argname in sorted(cs.indices):
|
||||
if cs._arg2scope[argname] != scope:
|
||||
continue
|
||||
return
|
||||
for argname in callspec.indices:
|
||||
if callspec._arg2scope[argname] != scope:
|
||||
continue
|
||||
|
||||
item_cls = None
|
||||
if scope is Scope.Session:
|
||||
scoped_item_path = None
|
||||
elif scope is Scope.Package:
|
||||
scoped_item_path = item.path
|
||||
elif scope is Scope.Module:
|
||||
scoped_item_path = item.path
|
||||
elif scope is Scope.Class:
|
||||
scoped_item_path = item.path
|
||||
item_cls = item.cls # type: ignore[attr-defined]
|
||||
else:
|
||||
assert_never(scope)
|
||||
item_cls = None
|
||||
if scope is Scope.Session:
|
||||
scoped_item_path = None
|
||||
elif scope is Scope.Package:
|
||||
scoped_item_path = item.path
|
||||
elif scope is Scope.Module:
|
||||
scoped_item_path = item.path
|
||||
elif scope is Scope.Class:
|
||||
scoped_item_path = item.path
|
||||
item_cls = item.cls # type: ignore[attr-defined]
|
||||
else:
|
||||
assert_never(scope)
|
||||
|
||||
param_index = cs.indices[argname]
|
||||
yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls)
|
||||
param_index = callspec.indices[argname]
|
||||
yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls)
|
||||
|
||||
|
||||
# Algorithm for sorting on a per-parametrized resource setup basis.
|
||||
|
@ -582,7 +577,6 @@ class FixtureRequest(abc.ABC):
|
|||
# (latter managed by fixturedef)
|
||||
argname = fixturedef.argname
|
||||
funcitem = self._pyfuncitem
|
||||
scope = fixturedef._scope
|
||||
try:
|
||||
callspec = funcitem.callspec
|
||||
except AttributeError:
|
||||
|
@ -590,24 +584,20 @@ class FixtureRequest(abc.ABC):
|
|||
if callspec is not None and argname in callspec.params:
|
||||
param = callspec.params[argname]
|
||||
param_index = callspec.indices[argname]
|
||||
# If a parametrize invocation set a scope it will override
|
||||
# the static scope defined with the fixture function.
|
||||
with suppress(KeyError):
|
||||
scope = callspec._arg2scope[argname]
|
||||
# The parametrize invocation scope overrides the fixture's scope.
|
||||
scope = callspec._arg2scope[argname]
|
||||
else:
|
||||
param = NOTSET
|
||||
param_index = 0
|
||||
scope = fixturedef._scope
|
||||
|
||||
has_params = fixturedef.params is not None
|
||||
fixtures_not_supported = getattr(funcitem, "nofuncargs", False)
|
||||
if has_params and fixtures_not_supported:
|
||||
msg = (
|
||||
"{name} does not support fixtures, maybe unittest.TestCase subclass?\n"
|
||||
"Node id: {nodeid}\n"
|
||||
"Function type: {typename}"
|
||||
).format(
|
||||
name=funcitem.name,
|
||||
nodeid=funcitem.nodeid,
|
||||
typename=type(funcitem).__name__,
|
||||
f"{funcitem.name} does not support fixtures, maybe unittest.TestCase subclass?\n"
|
||||
f"Node id: {funcitem.nodeid}\n"
|
||||
f"Function type: {type(funcitem).__name__}"
|
||||
)
|
||||
fail(msg, pytrace=False)
|
||||
if has_params:
|
||||
|
@ -740,9 +730,7 @@ class SubRequest(FixtureRequest):
|
|||
if node is None and scope is Scope.Class:
|
||||
# Fallback to function item itself.
|
||||
node = self._pyfuncitem
|
||||
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
|
||||
scope, self._pyfuncitem
|
||||
)
|
||||
assert node, f'Could not obtain a node for scope "{scope}" for function {self._pyfuncitem!r}'
|
||||
return node
|
||||
|
||||
def _check_scope(
|
||||
|
@ -845,8 +833,8 @@ class FixtureLookupError(LookupError):
|
|||
if faclist:
|
||||
available.add(name)
|
||||
if self.argname in available:
|
||||
msg = " recursive dependency involving fixture '{}' detected".format(
|
||||
self.argname
|
||||
msg = (
|
||||
f" recursive dependency involving fixture '{self.argname}' detected"
|
||||
)
|
||||
else:
|
||||
msg = f"fixture '{self.argname}' not found"
|
||||
|
@ -940,15 +928,13 @@ def _eval_scope_callable(
|
|||
result = scope_callable(fixture_name=fixture_name, config=config) # type: ignore[call-arg]
|
||||
except Exception as e:
|
||||
raise TypeError(
|
||||
"Error evaluating {} while defining fixture '{}'.\n"
|
||||
"Expected a function with the signature (*, fixture_name, config)".format(
|
||||
scope_callable, fixture_name
|
||||
)
|
||||
f"Error evaluating {scope_callable} while defining fixture '{fixture_name}'.\n"
|
||||
"Expected a function with the signature (*, fixture_name, config)"
|
||||
) from e
|
||||
if not isinstance(result, str):
|
||||
fail(
|
||||
"Expected {} to return a 'str' while defining fixture '{}', but it returned:\n"
|
||||
"{!r}".format(scope_callable, fixture_name, result),
|
||||
f"Expected {scope_callable} to return a 'str' while defining fixture '{fixture_name}', but it returned:\n"
|
||||
f"{result!r}",
|
||||
pytrace=False,
|
||||
)
|
||||
return result
|
||||
|
@ -1090,9 +1076,7 @@ class FixtureDef(Generic[FixtureValue]):
|
|||
return request.param_index if not hasattr(request, "param") else request.param
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<FixtureDef argname={!r} scope={!r} baseid={!r}>".format(
|
||||
self.argname, self.scope, self.baseid
|
||||
)
|
||||
return f"<FixtureDef argname={self.argname!r} scope={self.scope!r} baseid={self.baseid!r}>"
|
||||
|
||||
|
||||
def resolve_fixture_function(
|
||||
|
@ -1113,7 +1097,8 @@ def resolve_fixture_function(
|
|||
# Handle the case where fixture is defined not in a test class, but some other class
|
||||
# (for example a plugin class with a fixture), see #2270.
|
||||
if hasattr(fixturefunc, "__self__") and not isinstance(
|
||||
request.instance, fixturefunc.__self__.__class__ # type: ignore[union-attr]
|
||||
request.instance,
|
||||
fixturefunc.__self__.__class__, # type: ignore[union-attr]
|
||||
):
|
||||
return fixturefunc
|
||||
fixturefunc = getimfunc(fixturedef.func)
|
||||
|
@ -1196,7 +1181,7 @@ class FixtureFunctionMarker:
|
|||
|
||||
if getattr(function, "_pytestfixturefunction", False):
|
||||
raise ValueError(
|
||||
"fixture is being applied more than once to the same function"
|
||||
f"@pytest.fixture is being applied more than once to the same function {function.__name__!r}"
|
||||
)
|
||||
|
||||
if hasattr(function, "pytestmark"):
|
||||
|
@ -1208,9 +1193,7 @@ class FixtureFunctionMarker:
|
|||
if name == "request":
|
||||
location = getlocation(function)
|
||||
fail(
|
||||
"'request' is a reserved word for fixtures, use another name:\n {}".format(
|
||||
location
|
||||
),
|
||||
f"'request' is a reserved word for fixtures, use another name:\n {location}",
|
||||
pytrace=False,
|
||||
)
|
||||
|
||||
|
@ -1235,7 +1218,7 @@ def fixture(
|
|||
|
||||
|
||||
@overload
|
||||
def fixture( # noqa: F811
|
||||
def fixture(
|
||||
fixture_function: None = ...,
|
||||
*,
|
||||
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ...,
|
||||
|
@ -1249,7 +1232,7 @@ def fixture( # noqa: F811
|
|||
...
|
||||
|
||||
|
||||
def fixture( # noqa: F811
|
||||
def fixture(
|
||||
fixture_function: Optional[FixtureFunction] = None,
|
||||
*,
|
||||
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = "function",
|
||||
|
@ -1483,25 +1466,27 @@ class FixtureManager:
|
|||
|
||||
return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
|
||||
|
||||
def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
|
||||
nodeid = None
|
||||
try:
|
||||
p = absolutepath(plugin.__file__) # type: ignore[attr-defined]
|
||||
except AttributeError:
|
||||
pass
|
||||
def pytest_plugin_registered(self, plugin: _PluggyPlugin, plugin_name: str) -> None:
|
||||
# Fixtures defined in conftest plugins are only visible to within the
|
||||
# conftest's directory. This is unlike fixtures in non-conftest plugins
|
||||
# which have global visibility. So for conftests, construct the base
|
||||
# nodeid from the plugin name (which is the conftest path).
|
||||
if plugin_name and plugin_name.endswith("conftest.py"):
|
||||
# Note: we explicitly do *not* use `plugin.__file__` here -- The
|
||||
# difference is that plugin_name has the correct capitalization on
|
||||
# case-insensitive systems (Windows) and other normalization issues
|
||||
# (issue #11816).
|
||||
conftestpath = absolutepath(plugin_name)
|
||||
try:
|
||||
nodeid = str(conftestpath.parent.relative_to(self.config.rootpath))
|
||||
except ValueError:
|
||||
nodeid = ""
|
||||
if nodeid == ".":
|
||||
nodeid = ""
|
||||
if os.sep != nodes.SEP:
|
||||
nodeid = nodeid.replace(os.sep, nodes.SEP)
|
||||
else:
|
||||
# Construct the base nodeid which is later used to check
|
||||
# what fixtures are visible for particular tests (as denoted
|
||||
# by their test id).
|
||||
if p.name == "conftest.py":
|
||||
try:
|
||||
nodeid = str(p.parent.relative_to(self.config.rootpath))
|
||||
except ValueError:
|
||||
nodeid = ""
|
||||
if nodeid == ".":
|
||||
nodeid = ""
|
||||
if os.sep != nodes.SEP:
|
||||
nodeid = nodeid.replace(os.sep, nodes.SEP)
|
||||
nodeid = None
|
||||
|
||||
self.parsefactories(plugin, nodeid)
|
||||
|
||||
|
@ -1681,7 +1666,7 @@ class FixtureManager:
|
|||
raise NotImplementedError()
|
||||
|
||||
@overload
|
||||
def parsefactories( # noqa: F811
|
||||
def parsefactories(
|
||||
self,
|
||||
node_or_obj: object,
|
||||
nodeid: Optional[str],
|
||||
|
@ -1690,7 +1675,7 @@ class FixtureManager:
|
|||
) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def parsefactories( # noqa: F811
|
||||
def parsefactories(
|
||||
self,
|
||||
node_or_obj: Union[nodes.Node, object],
|
||||
nodeid: Union[str, NotSetType, None] = NOTSET,
|
||||
|
@ -1775,7 +1760,7 @@ class FixtureManager:
|
|||
def _matchfactories(
|
||||
self, fixturedefs: Iterable[FixtureDef[Any]], node: nodes.Node
|
||||
) -> Iterator[FixtureDef[Any]]:
|
||||
parentnodeids = {n.nodeid for n in node.iterparents()}
|
||||
parentnodeids = {n.nodeid for n in node.iter_parents()}
|
||||
for fixturedef in fixturedefs:
|
||||
if fixturedef.baseid in parentnodeids:
|
||||
yield fixturedef
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""Provides a function to report all internal modules for using freezing
|
||||
tools."""
|
||||
|
||||
import types
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
# mypy: allow-untyped-defs
|
||||
"""Version info, help messages, tracing configuration."""
|
||||
from argparse import Action
|
||||
import os
|
||||
import sys
|
||||
from argparse import Action
|
||||
from typing import Generator
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Union
|
||||
|
||||
import pytest
|
||||
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
|
||||
import pytest
|
||||
|
||||
|
||||
class HelpAction(Action):
|
||||
|
@ -108,11 +109,11 @@ def pytest_cmdline_parse() -> Generator[None, Config, Config]:
|
|||
path = config.option.debug
|
||||
debugfile = open(path, "w", encoding="utf-8")
|
||||
debugfile.write(
|
||||
"versions pytest-%s, "
|
||||
"python-%s\ncwd=%s\nargs=%s\n\n"
|
||||
% (
|
||||
"versions pytest-{}, "
|
||||
"python-{}\ninvocation_dir={}\ncwd={}\nargs={}\n\n".format(
|
||||
pytest.__version__,
|
||||
".".join(map(str, sys.version_info)),
|
||||
config.invocation_params.dir,
|
||||
os.getcwd(),
|
||||
config.invocation_params.args,
|
||||
)
|
||||
|
@ -135,9 +136,7 @@ def pytest_cmdline_parse() -> Generator[None, Config, Config]:
|
|||
def showversion(config: Config) -> None:
|
||||
if config.option.version > 1:
|
||||
sys.stdout.write(
|
||||
"This is pytest version {}, imported from {}\n".format(
|
||||
pytest.__version__, pytest.__file__
|
||||
)
|
||||
f"This is pytest version {pytest.__version__}, imported from {pytest.__file__}\n"
|
||||
)
|
||||
plugininfo = getpluginversioninfo(config)
|
||||
if plugininfo:
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# mypy: allow-untyped-defs
|
||||
"""Hook specifications for pytest plugins which are invoked by pytest itself
|
||||
and by builtin plugins."""
|
||||
from pathlib import Path
|
||||
|
@ -13,17 +14,18 @@ from typing import Union
|
|||
|
||||
from pluggy import HookspecMarker
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import pdb
|
||||
import warnings
|
||||
from typing import Literal
|
||||
import warnings
|
||||
|
||||
from _pytest._code.code import ExceptionRepr
|
||||
from _pytest._code.code import ExceptionInfo
|
||||
from _pytest._code.code import ExceptionRepr
|
||||
from _pytest.config import _PluggyPlugin
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ExitCode
|
||||
from _pytest.config import PytestPluginManager
|
||||
from _pytest.config import _PluggyPlugin
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.fixtures import FixtureDef
|
||||
from _pytest.fixtures import SubRequest
|
||||
|
@ -54,24 +56,41 @@ def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None:
|
|||
"""Called at plugin registration time to allow adding new hooks via a call to
|
||||
:func:`pluginmanager.add_hookspecs(module_or_class, prefix) <pytest.PytestPluginManager.add_hookspecs>`.
|
||||
|
||||
:param pytest.PytestPluginManager pluginmanager: The pytest plugin manager.
|
||||
:param pluginmanager: The pytest plugin manager.
|
||||
|
||||
.. note::
|
||||
This hook is incompatible with hook wrappers.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
If a conftest plugin implements this hook, it will be called immediately
|
||||
when the conftest is registered.
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(historic=True)
|
||||
def pytest_plugin_registered(
|
||||
plugin: "_PluggyPlugin", manager: "PytestPluginManager"
|
||||
plugin: "_PluggyPlugin",
|
||||
plugin_name: str,
|
||||
manager: "PytestPluginManager",
|
||||
) -> None:
|
||||
"""A new pytest plugin got registered.
|
||||
|
||||
:param plugin: The plugin module or instance.
|
||||
:param pytest.PytestPluginManager manager: pytest plugin manager.
|
||||
:param plugin_name: The name by which the plugin is registered.
|
||||
:param manager: The pytest plugin manager.
|
||||
|
||||
.. note::
|
||||
This hook is incompatible with hook wrappers.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
If a conftest plugin implements this hook, it will be called immediately
|
||||
when the conftest is registered, once for each plugin registered thus far
|
||||
(including itself!), and for all plugins thereafter when they are
|
||||
registered.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -80,19 +99,13 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
|
|||
"""Register argparse-style options and ini-style config values,
|
||||
called once at the beginning of a test run.
|
||||
|
||||
.. note::
|
||||
|
||||
This function should be implemented only in plugins or ``conftest.py``
|
||||
files situated at the tests root directory due to how pytest
|
||||
:ref:`discovers plugins during startup <pluginorder>`.
|
||||
|
||||
:param pytest.Parser parser:
|
||||
:param parser:
|
||||
To add command line options, call
|
||||
:py:func:`parser.addoption(...) <pytest.Parser.addoption>`.
|
||||
To add ini-file values call :py:func:`parser.addini(...)
|
||||
<pytest.Parser.addini>`.
|
||||
|
||||
:param pytest.PytestPluginManager pluginmanager:
|
||||
:param pluginmanager:
|
||||
The pytest plugin manager, which can be used to install :py:func:`~pytest.hookspec`'s
|
||||
or :py:func:`~pytest.hookimpl`'s and allow one plugin to call another plugin's hooks
|
||||
to change how command line options are added.
|
||||
|
@ -111,6 +124,14 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
|
|||
|
||||
.. note::
|
||||
This hook is incompatible with hook wrappers.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
If a conftest plugin implements this hook, it will be called immediately
|
||||
when the conftest is registered.
|
||||
|
||||
This hook is only called for :ref:`initial conftests <pluginorder>`.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -118,16 +139,17 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
|
|||
def pytest_configure(config: "Config") -> None:
|
||||
"""Allow plugins and conftest files to perform initial configuration.
|
||||
|
||||
This hook is called for every plugin and initial conftest file
|
||||
after command line options have been parsed.
|
||||
|
||||
After that, the hook is called for other conftest files as they are
|
||||
imported.
|
||||
|
||||
.. note::
|
||||
This hook is incompatible with hook wrappers.
|
||||
|
||||
:param pytest.Config config: The pytest config object.
|
||||
:param config: The pytest config object.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
This hook is called for every :ref:`initial conftest <pluginorder>` file
|
||||
after command line options have been parsed. After that, the hook is called
|
||||
for other conftest files as they are registered.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -146,40 +168,54 @@ def pytest_cmdline_parse(
|
|||
Stops at first non-None result, see :ref:`firstresult`.
|
||||
|
||||
.. note::
|
||||
This hook will only be called for plugin classes passed to the
|
||||
This hook is only called for plugin classes passed to the
|
||||
``plugins`` arg when using `pytest.main`_ to perform an in-process
|
||||
test run.
|
||||
|
||||
:param pluginmanager: The pytest plugin manager.
|
||||
:param args: List of arguments passed on the command line.
|
||||
:returns: A pytest config object.
|
||||
"""
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]:
|
||||
"""Called for performing the main command line action. The default
|
||||
implementation will invoke the configure hooks and runtest_mainloop.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult`.
|
||||
|
||||
:param config: The pytest config object.
|
||||
:returns: The exit code.
|
||||
This hook is not called for conftest files.
|
||||
"""
|
||||
|
||||
|
||||
def pytest_load_initial_conftests(
|
||||
early_config: "Config", parser: "Parser", args: List[str]
|
||||
) -> None:
|
||||
"""Called to implement the loading of initial conftest files ahead
|
||||
of command line option parsing.
|
||||
|
||||
.. note::
|
||||
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
|
||||
"""Called to implement the loading of :ref:`initial conftest files
|
||||
<pluginorder>` ahead of command line option parsing.
|
||||
|
||||
:param early_config: The pytest config object.
|
||||
:param args: Arguments passed on the command line.
|
||||
:param parser: To add command line options.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
This hook is not called for conftest files.
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]:
|
||||
"""Called for performing the main command line action.
|
||||
|
||||
The default implementation will invoke the configure hooks and
|
||||
:hook:`pytest_runtestloop`.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult`.
|
||||
|
||||
:param config: The pytest config object.
|
||||
:returns: The exit code.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
This hook is only called for :ref:`initial conftests <pluginorder>`.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -222,6 +258,11 @@ def pytest_collection(session: "Session") -> Optional[object]:
|
|||
counter (and returns `None`).
|
||||
|
||||
:param session: The pytest session object.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
This hook is only called for :ref:`initial conftests <pluginorder>`.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -234,6 +275,11 @@ def pytest_collection_modifyitems(
|
|||
:param session: The pytest session object.
|
||||
:param config: The pytest config object.
|
||||
:param items: List of item objects.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest plugin can implement this hook.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -241,6 +287,11 @@ def pytest_collection_finish(session: "Session") -> None:
|
|||
"""Called after collection has been performed and modified.
|
||||
|
||||
:param session: The pytest session object.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest plugin can implement this hook.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -263,6 +314,14 @@ def pytest_ignore_collect(collection_path: Path, config: "Config") -> Optional[b
|
|||
|
||||
.. versionchanged:: 8.0.0
|
||||
The ``path`` parameter has been removed.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given collection path, only
|
||||
conftest files in parent directories of the collection path are consulted
|
||||
(if the path is a directory, its own conftest file is *not* consulted - a
|
||||
directory cannot ignore itself!).
|
||||
"""
|
||||
|
||||
|
||||
|
@ -284,6 +343,14 @@ def pytest_collect_directory(path: Path, parent: "Collector") -> "Optional[Colle
|
|||
|
||||
See :ref:`custom directory collectors` for a simple example of use of this
|
||||
hook.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given collection path, only
|
||||
conftest files in parent directories of the collection path are consulted
|
||||
(if the path is a directory, its own conftest file is *not* consulted - a
|
||||
directory cannot collect itself!).
|
||||
"""
|
||||
|
||||
|
||||
|
@ -304,6 +371,12 @@ def pytest_collect_file(file_path: Path, parent: "Collector") -> "Optional[Colle
|
|||
|
||||
.. versionchanged:: 8.0.0
|
||||
The ``path`` parameter was removed.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given file path, only
|
||||
conftest files in parent directories of the file path are consulted.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -315,6 +388,13 @@ def pytest_collectstart(collector: "Collector") -> None:
|
|||
|
||||
:param collector:
|
||||
The collector.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given collector, only
|
||||
conftest files in the collector's directory and its parent directories are
|
||||
consulted.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -323,6 +403,12 @@ def pytest_itemcollected(item: "Item") -> None:
|
|||
|
||||
:param item:
|
||||
The item.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given item, only conftest
|
||||
files in the item's directory and its parent directories are consulted.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -331,6 +417,13 @@ def pytest_collectreport(report: "CollectReport") -> None:
|
|||
|
||||
:param report:
|
||||
The collect report.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given collector, only
|
||||
conftest files in the collector's directory and its parent directories are
|
||||
consulted.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -341,6 +434,11 @@ def pytest_deselected(items: Sequence["Item"]) -> None:
|
|||
|
||||
:param items:
|
||||
The items.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -353,6 +451,13 @@ def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectRepor
|
|||
|
||||
:param collector:
|
||||
The collector.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given collector, only
|
||||
conftest files in the collector's directory and its parent directories are
|
||||
consulted.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -380,6 +485,13 @@ def pytest_pycollect_makemodule(module_path: Path, parent) -> Optional["Module"]
|
|||
|
||||
.. versionchanged:: 8.0.0
|
||||
The ``path`` parameter has been removed in favor of ``module_path``.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given parent collector,
|
||||
only conftest files in the collector's directory and its parent directories
|
||||
are consulted.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -399,6 +511,13 @@ def pytest_pycollect_makeitem(
|
|||
The object.
|
||||
:returns:
|
||||
The created items/collectors.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given collector, only
|
||||
conftest files in the collector's directory and its parent directories
|
||||
are consulted.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -410,6 +529,13 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
|
|||
|
||||
:param pyfuncitem:
|
||||
The function item.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given item, only
|
||||
conftest files in the item's directory and its parent directories
|
||||
are consulted.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -418,6 +544,13 @@ def pytest_generate_tests(metafunc: "Metafunc") -> None:
|
|||
|
||||
:param metafunc:
|
||||
The :class:`~pytest.Metafunc` helper for the test function.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given function definition,
|
||||
only conftest files in the functions's directory and its parent directories
|
||||
are consulted.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -435,7 +568,12 @@ def pytest_make_parametrize_id(
|
|||
|
||||
:param config: The pytest config object.
|
||||
:param val: The parametrized value.
|
||||
:param str argname: The automatic parameter name produced by pytest.
|
||||
:param argname: The automatic parameter name produced by pytest.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -462,6 +600,11 @@ def pytest_runtestloop(session: "Session") -> Optional[object]:
|
|||
|
||||
Stops at first non-None result, see :ref:`firstresult`.
|
||||
The return value is not used, but only stops further processing.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -500,6 +643,11 @@ def pytest_runtest_protocol(
|
|||
|
||||
Stops at first non-None result, see :ref:`firstresult`.
|
||||
The return value is not used, but only stops further processing.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -514,6 +662,12 @@ def pytest_runtest_logstart(
|
|||
:param location: A tuple of ``(filename, lineno, testname)``
|
||||
where ``filename`` is a file path relative to ``config.rootpath``
|
||||
and ``lineno`` is 0-based.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given item, only conftest
|
||||
files in the item's directory and its parent directories are consulted.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -528,6 +682,12 @@ def pytest_runtest_logfinish(
|
|||
:param location: A tuple of ``(filename, lineno, testname)``
|
||||
where ``filename`` is a file path relative to ``config.rootpath``
|
||||
and ``lineno`` is 0-based.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given item, only conftest
|
||||
files in the item's directory and its parent directories are consulted.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -541,6 +701,12 @@ def pytest_runtest_setup(item: "Item") -> None:
|
|||
|
||||
:param item:
|
||||
The item.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given item, only conftest
|
||||
files in the item's directory and its parent directories are consulted.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -551,6 +717,12 @@ def pytest_runtest_call(item: "Item") -> None:
|
|||
|
||||
:param item:
|
||||
The item.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given item, only conftest
|
||||
files in the item's directory and its parent directories are consulted.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -569,6 +741,12 @@ def pytest_runtest_teardown(item: "Item", nextitem: Optional["Item"]) -> None:
|
|||
scheduled). This argument is used to perform exact teardowns, i.e.
|
||||
calling just enough finalizers so that nextitem only needs to call
|
||||
setup functions.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given item, only conftest
|
||||
files in the item's directory and its parent directories are consulted.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -585,6 +763,12 @@ def pytest_runtest_makereport(
|
|||
:param call: The :class:`~pytest.CallInfo` for the phase.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult`.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given item, only conftest
|
||||
files in the item's directory and its parent directories are consulted.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -593,6 +777,12 @@ def pytest_runtest_logreport(report: "TestReport") -> None:
|
|||
of the setup, call and teardown runtest phases of an item.
|
||||
|
||||
See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given item, only conftest
|
||||
files in the item's directory and its parent directories are consulted.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -606,6 +796,12 @@ def pytest_report_to_serializable(
|
|||
|
||||
:param config: The pytest config object.
|
||||
:param report: The report.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. The exact details may depend
|
||||
on the plugin which calls the hook.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -618,6 +814,12 @@ def pytest_report_from_serializable(
|
|||
:hook:`pytest_report_to_serializable`.
|
||||
|
||||
:param config: The pytest config object.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. The exact details may depend
|
||||
on the plugin which calls the hook.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -645,6 +847,13 @@ def pytest_fixture_setup(
|
|||
If the fixture function returns None, other implementations of
|
||||
this hook function will continue to be called, according to the
|
||||
behavior of the :ref:`firstresult` option.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given fixture, only
|
||||
conftest files in the fixture scope's directory and its parent directories
|
||||
are consulted.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -659,6 +868,13 @@ def pytest_fixture_post_finalizer(
|
|||
The fixture definition object.
|
||||
:param request:
|
||||
The fixture request object.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given fixture, only
|
||||
conftest files in the fixture scope's directory and its parent directories
|
||||
are consulted.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -672,6 +888,11 @@ def pytest_sessionstart(session: "Session") -> None:
|
|||
and entering the run test loop.
|
||||
|
||||
:param session: The pytest session object.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
This hook is only called for :ref:`initial conftests <pluginorder>`.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -683,6 +904,11 @@ def pytest_sessionfinish(
|
|||
|
||||
:param session: The pytest session object.
|
||||
:param exitstatus: The status which pytest will return to the system.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -690,6 +916,11 @@ def pytest_unconfigure(config: "Config") -> None:
|
|||
"""Called before test process is exited.
|
||||
|
||||
:param config: The pytest config object.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -712,6 +943,12 @@ def pytest_assertrepr_compare(
|
|||
:param op: The operator, e.g. `"=="`, `"!="`, `"not in"`.
|
||||
:param left: The left operand.
|
||||
:param right: The right operand.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given item, only conftest
|
||||
files in the item's directory and its parent directories are consulted.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -740,6 +977,12 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No
|
|||
:param lineno: Line number of the assert statement.
|
||||
:param orig: String with the original assertion.
|
||||
:param expl: String with the assert explanation.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given item, only conftest
|
||||
files in the item's directory and its parent directories are consulted.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -764,18 +1007,17 @@ def pytest_report_header( # type:ignore[empty-body]
|
|||
If you want to have your line(s) displayed first, use
|
||||
:ref:`trylast=True <plugin-hookorder>`.
|
||||
|
||||
.. note::
|
||||
|
||||
This function should be implemented only in plugins or ``conftest.py``
|
||||
files situated at the tests root directory due to how pytest
|
||||
:ref:`discovers plugins during startup <pluginorder>`.
|
||||
|
||||
.. versionchanged:: 7.0.0
|
||||
The ``start_path`` parameter was added as a :class:`pathlib.Path`
|
||||
equivalent of the ``startdir`` parameter.
|
||||
|
||||
.. versionchanged:: 8.0.0
|
||||
The ``startdir`` parameter has been removed.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
This hook is only called for :ref:`initial conftests <pluginorder>`.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -809,6 +1051,11 @@ def pytest_report_collectionfinish( # type:ignore[empty-body]
|
|||
|
||||
.. versionchanged:: 8.0.0
|
||||
The ``startdir`` parameter has been removed.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest plugin can implement this hook.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -837,6 +1084,11 @@ def pytest_report_teststatus( # type:ignore[empty-body]
|
|||
:returns: The test status.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult`.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest plugin can implement this hook.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -853,6 +1105,11 @@ def pytest_terminal_summary(
|
|||
|
||||
.. versionadded:: 4.2
|
||||
The ``config`` parameter.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest plugin can implement this hook.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -877,7 +1134,8 @@ def pytest_warning_recorded(
|
|||
* ``"runtest"``: during test execution.
|
||||
|
||||
:param nodeid:
|
||||
Full id of the item.
|
||||
Full id of the item. Empty string for warnings that are not specific to
|
||||
a particular node.
|
||||
|
||||
:param location:
|
||||
When available, holds information about the execution context of the captured
|
||||
|
@ -885,6 +1143,13 @@ def pytest_warning_recorded(
|
|||
when the execution context is at the module level.
|
||||
|
||||
.. versionadded:: 6.0
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. If the warning is specific to a
|
||||
particular node, only conftest files in parent directories of the node are
|
||||
consulted.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -908,6 +1173,12 @@ def pytest_markeval_namespace( # type:ignore[empty-body]
|
|||
|
||||
:param config: The pytest config object.
|
||||
:returns: A dictionary of additional globals to add.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given item, only conftest
|
||||
files in parent directories of the item are consulted.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -927,6 +1198,11 @@ def pytest_internalerror(
|
|||
|
||||
:param excrepr: The exception repr object.
|
||||
:param excinfo: The exception info.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest plugin can implement this hook.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -936,6 +1212,11 @@ def pytest_keyboard_interrupt(
|
|||
"""Called for keyboard interrupt.
|
||||
|
||||
:param excinfo: The exception info.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest plugin can implement this hook.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -962,6 +1243,12 @@ def pytest_exception_interact(
|
|||
The call information. Contains the exception.
|
||||
:param report:
|
||||
The collection or test report.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given node, only conftest
|
||||
files in parent directories of the node are consulted.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -973,6 +1260,11 @@ def pytest_enter_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
|
|||
|
||||
:param config: The pytest config object.
|
||||
:param pdb: The Pdb instance.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest plugin can implement this hook.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -984,4 +1276,9 @@ def pytest_leave_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
|
|||
|
||||
:param config: The pytest config object.
|
||||
:param pdb: The Pdb instance.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest plugin can implement this hook.
|
||||
"""
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# mypy: allow-untyped-defs
|
||||
"""Report test results in JUnit-XML format, for use with Jenkins and build
|
||||
integration servers.
|
||||
|
||||
|
@ -6,12 +7,11 @@ Based on initial code from Ross Lawley.
|
|||
Output conforms to
|
||||
https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
|
||||
"""
|
||||
from datetime import datetime
|
||||
import functools
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
from datetime import datetime
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
|
@ -19,8 +19,8 @@ from typing import Match
|
|||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Union
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import pytest
|
||||
from _pytest import nodes
|
||||
from _pytest import timing
|
||||
from _pytest._code.code import ExceptionRepr
|
||||
|
@ -32,6 +32,7 @@ from _pytest.fixtures import FixtureRequest
|
|||
from _pytest.reports import TestReport
|
||||
from _pytest.stash import StashKey
|
||||
from _pytest.terminal import TerminalReporter
|
||||
import pytest
|
||||
|
||||
|
||||
xml_key = StashKey["LogXML"]()
|
||||
|
@ -248,7 +249,9 @@ class _NodeReporter:
|
|||
skipreason = skipreason[9:]
|
||||
details = f"{filename}:{lineno}: {skipreason}"
|
||||
|
||||
skipped = ET.Element("skipped", type="pytest.skip", message=skipreason)
|
||||
skipped = ET.Element(
|
||||
"skipped", type="pytest.skip", message=bin_xml_escape(skipreason)
|
||||
)
|
||||
skipped.text = bin_xml_escape(details)
|
||||
self.append(skipped)
|
||||
self.write_captured_output(report)
|
||||
|
@ -271,9 +274,7 @@ def _warn_incompatibility_with_xunit2(
|
|||
if xml is not None and xml.family not in ("xunit1", "legacy"):
|
||||
request.node.warn(
|
||||
PytestWarning(
|
||||
"{fixture_name} is incompatible with junit_family '{family}' (use 'legacy' or 'xunit1')".format(
|
||||
fixture_name=fixture_name, family=xml.family
|
||||
)
|
||||
f"{fixture_name} is incompatible with junit_family '{xml.family}' (use 'legacy' or 'xunit1')"
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -365,7 +366,6 @@ def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object]
|
|||
`pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See
|
||||
:issue:`7767` for details.
|
||||
"""
|
||||
|
||||
__tracebackhide__ = True
|
||||
|
||||
def record_func(name: str, value: object) -> None:
|
||||
|
@ -375,7 +375,7 @@ def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object]
|
|||
|
||||
xml = request.config.stash.get(xml_key, None)
|
||||
if xml is not None:
|
||||
record_func = xml.add_global_property # noqa
|
||||
record_func = xml.add_global_property
|
||||
return record_func
|
||||
|
||||
|
||||
|
@ -624,7 +624,7 @@ class LogXML:
|
|||
def update_testcase_duration(self, report: TestReport) -> None:
|
||||
"""Accumulate total duration for nodeid from given report and update
|
||||
the Junit.testcase with the new total if already created."""
|
||||
if self.report_duration == "total" or report.when == self.report_duration:
|
||||
if self.report_duration in {"total", report.when}:
|
||||
reporter = self.node_reporter(report)
|
||||
reporter.duration += getattr(report, "duration", 0.0)
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
# mypy: allow-untyped-defs
|
||||
"""Add backward compatibility support for the legacy py path type."""
|
||||
import dataclasses
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
from typing import final
|
||||
from typing import List
|
||||
|
@ -14,6 +15,7 @@ from typing import Union
|
|||
from iniconfig import SectionWrapper
|
||||
|
||||
import py
|
||||
|
||||
from _pytest.cacheprovider import Cache
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import hookimpl
|
||||
|
@ -32,6 +34,7 @@ from _pytest.pytester import RunResult
|
|||
from _pytest.terminal import TerminalReporter
|
||||
from _pytest.tmpdir import TempPathFactory
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import pexpect
|
||||
|
||||
|
@ -326,8 +329,8 @@ class LegacyTmpdirPlugin:
|
|||
|
||||
By default, a new base temporary directory is created each test session,
|
||||
and old bases are removed after 3 sessions, to aid in debugging. If
|
||||
``--basetemp`` is used then it is cleared each session. See :ref:`base
|
||||
temporary directory`.
|
||||
``--basetemp`` is used then it is cleared each session. See
|
||||
:ref:`temporary directory location and retention`.
|
||||
|
||||
The returned object is a `legacy_path`_ object.
|
||||
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
# mypy: allow-untyped-defs
|
||||
"""Access and control log capturing."""
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from contextlib import contextmanager
|
||||
from contextlib import nullcontext
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from datetime import timezone
|
||||
import io
|
||||
from io import StringIO
|
||||
import logging
|
||||
from logging import LogRecord
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
from types import TracebackType
|
||||
from typing import AbstractSet
|
||||
from typing import Dict
|
||||
|
@ -43,6 +44,7 @@ from _pytest.main import Session
|
|||
from _pytest.stash import StashKey
|
||||
from _pytest.terminal import TerminalReporter
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
logging_StreamHandler = logging.StreamHandler[StringIO]
|
||||
else:
|
||||
|
@ -71,7 +73,8 @@ class DatetimeFormatter(logging.Formatter):
|
|||
tz = timezone(timedelta(seconds=ct.tm_gmtoff), ct.tm_zone)
|
||||
# Construct `datetime.datetime` object from `struct_time`
|
||||
# and msecs information from `record`
|
||||
dt = datetime(*ct[0:6], microsecond=round(record.msecs * 1000), tzinfo=tz)
|
||||
# Using int() instead of round() to avoid it exceeding 1_000_000 and causing a ValueError (#11861).
|
||||
dt = datetime(*ct[0:6], microsecond=int(record.msecs * 1000), tzinfo=tz)
|
||||
return dt.strftime(datefmt)
|
||||
# Use `logging.Formatter` for non-microsecond formats
|
||||
return super().formatTime(record, datefmt)
|
||||
|
@ -114,7 +117,6 @@ class ColoredLevelFormatter(DatetimeFormatter):
|
|||
.. warning::
|
||||
This is an experimental API.
|
||||
"""
|
||||
|
||||
assert self._fmt is not None
|
||||
levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt)
|
||||
if not levelname_fmt_match:
|
||||
|
@ -181,7 +183,6 @@ class PercentStyleMultiline(logging.PercentStyle):
|
|||
0 (auto-indent turned off) or
|
||||
>0 (explicitly set indentation position).
|
||||
"""
|
||||
|
||||
if auto_indent_option is None:
|
||||
return 0
|
||||
elif isinstance(auto_indent_option, bool):
|
||||
|
@ -297,6 +298,13 @@ def pytest_addoption(parser: Parser) -> None:
|
|||
default=None,
|
||||
help="Path to a file when logging will be written to",
|
||||
)
|
||||
add_option_ini(
|
||||
"--log-file-mode",
|
||||
dest="log_file_mode",
|
||||
default="w",
|
||||
choices=["w", "a"],
|
||||
help="Log file open mode",
|
||||
)
|
||||
add_option_ini(
|
||||
"--log-file-level",
|
||||
dest="log_file_level",
|
||||
|
@ -623,9 +631,9 @@ def get_log_level_for_setting(config: Config, *setting_names: str) -> Optional[i
|
|||
except ValueError as e:
|
||||
# Python logging does not recognise this as a logging level
|
||||
raise UsageError(
|
||||
"'{}' is not recognized as a logging level name for "
|
||||
"'{}'. Please consider passing the "
|
||||
"logging level num instead.".format(log_level, setting_name)
|
||||
f"'{log_level}' is not recognized as a logging level name for "
|
||||
f"'{setting_name}'. Please consider passing the "
|
||||
"logging level num instead."
|
||||
) from e
|
||||
|
||||
|
||||
|
@ -668,7 +676,10 @@ class LoggingPlugin:
|
|||
if not os.path.isdir(directory):
|
||||
os.makedirs(directory)
|
||||
|
||||
self.log_file_handler = _FileHandler(log_file, mode="w", encoding="UTF-8")
|
||||
self.log_file_mode = get_option_ini(config, "log_file_mode") or "w"
|
||||
self.log_file_handler = _FileHandler(
|
||||
log_file, mode=self.log_file_mode, encoding="UTF-8"
|
||||
)
|
||||
log_file_format = get_option_ini(config, "log_file_format", "log_format")
|
||||
log_file_date_format = get_option_ini(
|
||||
config, "log_file_date_format", "log_date_format"
|
||||
|
@ -745,7 +756,7 @@ class LoggingPlugin:
|
|||
fpath.parent.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
# https://github.com/python/mypy/issues/11193
|
||||
stream: io.TextIOWrapper = fpath.open(mode="w", encoding="UTF-8") # type: ignore[assignment]
|
||||
stream: io.TextIOWrapper = fpath.open(mode=self.log_file_mode, encoding="UTF-8") # type: ignore[assignment]
|
||||
old_stream = self.log_file_handler.setStream(stream)
|
||||
if old_stream:
|
||||
old_stream.close()
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
"""Core implementation of the testing process: init, session, runtest loop."""
|
||||
|
||||
import argparse
|
||||
import dataclasses
|
||||
import fnmatch
|
||||
import functools
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import AbstractSet
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
|
@ -21,12 +21,14 @@ from typing import Optional
|
|||
from typing import overload
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
import warnings
|
||||
|
||||
import pluggy
|
||||
|
||||
import _pytest._code
|
||||
from _pytest import nodes
|
||||
import _pytest._code
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import directory_arg
|
||||
from _pytest.config import ExitCode
|
||||
|
@ -48,6 +50,10 @@ from _pytest.runner import SetupState
|
|||
from _pytest.warning_types import PytestWarning
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Self
|
||||
|
||||
|
||||
def pytest_addoption(parser: Parser) -> None:
|
||||
parser.addini(
|
||||
"norecursedirs",
|
||||
|
@ -376,6 +382,9 @@ def _in_venv(path: Path) -> bool:
|
|||
|
||||
|
||||
def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[bool]:
|
||||
if collection_path.name == "__pycache__":
|
||||
return True
|
||||
|
||||
ignore_paths = config._getconftest_pathlist(
|
||||
"collect_ignore", path=collection_path.parent
|
||||
)
|
||||
|
@ -487,16 +496,16 @@ class Dir(nodes.Directory):
|
|||
@classmethod
|
||||
def from_parent( # type: ignore[override]
|
||||
cls,
|
||||
parent: nodes.Collector, # type: ignore[override]
|
||||
parent: nodes.Collector,
|
||||
*,
|
||||
path: Path,
|
||||
) -> "Dir":
|
||||
) -> "Self":
|
||||
"""The public constructor.
|
||||
|
||||
:param parent: The parent collector of this Dir.
|
||||
:param path: The directory's path.
|
||||
"""
|
||||
return super().from_parent(parent=parent, path=path) # type: ignore[no-any-return]
|
||||
return super().from_parent(parent=parent, path=path)
|
||||
|
||||
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
|
||||
config = self.config
|
||||
|
@ -505,8 +514,6 @@ class Dir(nodes.Directory):
|
|||
ihook = self.ihook
|
||||
for direntry in scandir(self.path):
|
||||
if direntry.is_dir():
|
||||
if direntry.name == "__pycache__":
|
||||
continue
|
||||
path = Path(direntry.path)
|
||||
if not self.session.isinitpath(path, with_parents=True):
|
||||
if ihook.pytest_ignore_collect(collection_path=path, config=config):
|
||||
|
@ -724,12 +731,12 @@ class Session(nodes.Collector):
|
|||
...
|
||||
|
||||
@overload
|
||||
def perform_collect( # noqa: F811
|
||||
def perform_collect(
|
||||
self, args: Optional[Sequence[str]] = ..., genitems: bool = ...
|
||||
) -> Sequence[Union[nodes.Item, nodes.Collector]]:
|
||||
...
|
||||
|
||||
def perform_collect( # noqa: F811
|
||||
def perform_collect(
|
||||
self, args: Optional[Sequence[str]] = None, genitems: bool = True
|
||||
) -> Sequence[Union[nodes.Item, nodes.Collector]]:
|
||||
"""Perform the collection phase for this session.
|
||||
|
@ -895,10 +902,14 @@ class Session(nodes.Collector):
|
|||
|
||||
# Prune this level.
|
||||
any_matched_in_collector = False
|
||||
for node in subnodes:
|
||||
for node in reversed(subnodes):
|
||||
# Path part e.g. `/a/b/` in `/a/b/test_file.py::TestIt::test_it`.
|
||||
if isinstance(matchparts[0], Path):
|
||||
is_match = node.path == matchparts[0]
|
||||
if sys.platform == "win32" and not is_match:
|
||||
# In case the file paths do not match, fallback to samefile() to
|
||||
# account for short-paths on Windows (#11895).
|
||||
is_match = os.path.samefile(node.path, matchparts[0])
|
||||
# Name part e.g. `TestIt` in `/a/b/test_file.py::TestIt::test_it`.
|
||||
else:
|
||||
# TODO: Remove parametrized workaround once collection structure contains
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue