Merge branch 'main' into teardown_fixture_order
This commit is contained in:
commit
fbe15ca8be
|
@ -205,7 +205,7 @@ jobs:
|
||||||
|
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
if: "matrix.use_coverage"
|
if: "matrix.use_coverage"
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v4
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
|
|
|
@ -46,7 +46,7 @@ jobs:
|
||||||
run: python scripts/update-plugin-list.py
|
run: python scripts/update-plugin-list.py
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38
|
uses: peter-evans/create-pull-request@b1ddad2c994a25fbc81a28b3ec0e368bb2021c50
|
||||||
with:
|
with:
|
||||||
commit-message: '[automated] Update plugin list'
|
commit-message: '[automated] Update plugin list'
|
||||||
author: 'pytest bot <pytestbot@users.noreply.github.com>'
|
author: 'pytest bot <pytestbot@users.noreply.github.com>'
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: "v0.2.1"
|
rev: "v0.2.2"
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args: ["--fix"]
|
args: ["--fix"]
|
||||||
|
|
2
AUTHORS
2
AUTHORS
|
@ -56,6 +56,7 @@ Babak Keyvani
|
||||||
Barney Gale
|
Barney Gale
|
||||||
Ben Brown
|
Ben Brown
|
||||||
Ben Gartner
|
Ben Gartner
|
||||||
|
Ben Leith
|
||||||
Ben Webb
|
Ben Webb
|
||||||
Benjamin Peterson
|
Benjamin Peterson
|
||||||
Benjamin Schubert
|
Benjamin Schubert
|
||||||
|
@ -127,6 +128,7 @@ Edison Gustavo Muenz
|
||||||
Edoardo Batini
|
Edoardo Batini
|
||||||
Edson Tadeu M. Manoel
|
Edson Tadeu M. Manoel
|
||||||
Eduardo Schettino
|
Eduardo Schettino
|
||||||
|
Eero Vaher
|
||||||
Eli Boyarski
|
Eli Boyarski
|
||||||
Elizaveta Shashkova
|
Elizaveta Shashkova
|
||||||
Éloi Rivard
|
Éloi Rivard
|
||||||
|
|
|
@ -23,7 +23,6 @@ members of the `contributors team`_ interested in receiving funding.
|
||||||
|
|
||||||
The current list of contributors receiving funding are:
|
The current list of contributors receiving funding are:
|
||||||
|
|
||||||
* `@asottile`_
|
|
||||||
* `@nicoddemus`_
|
* `@nicoddemus`_
|
||||||
* `@The-Compiler`_
|
* `@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
|
.. _`@pytest-dev/tidelift-admins`: https://github.com/orgs/pytest-dev/teams/tidelift-admins/members
|
||||||
.. _`agreement`: https://tidelift.com/docs/lifting/agreement
|
.. _`agreement`: https://tidelift.com/docs/lifting/agreement
|
||||||
|
|
||||||
.. _`@asottile`: https://github.com/asottile
|
|
||||||
.. _`@nicoddemus`: https://github.com/nicoddemus
|
.. _`@nicoddemus`: https://github.com/nicoddemus
|
||||||
.. _`@The-Compiler`: https://github.com/The-Compiler
|
.. _`@The-Compiler`: https://github.com/The-Compiler
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
:func:`pytest.warns` now validates that warning object's ``message`` is of type `str` -- currently in Python it is possible to pass other types than `str` when creating `Warning` instances, 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.
|
: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.
|
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.
|
||||||
|
|
|
@ -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 @@
|
||||||
Correctly handle errors from :func:`getpass.getuser` in Python 3.13.
|
|
|
@ -1 +0,0 @@
|
||||||
Fix an edge case where ``ExceptionInfo._stringify_exception`` could crash :func:`pytest.raises`.
|
|
|
@ -0,0 +1 @@
|
||||||
|
Fix collection on Windows where initial paths contain the short version of a path (for example ``c:\PROGRA~1\tests``).
|
|
@ -1 +0,0 @@
|
||||||
Fix a regression in pytest 8.0.0 whereby autouse fixtures defined in a module get ignored by the doctests in the module.
|
|
|
@ -1 +0,0 @@
|
||||||
Fix a regression in pytest 8.0.0 whereby items would be collected in reverse order in some circumstances.
|
|
|
@ -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.
|
|
@ -5,11 +5,10 @@
|
||||||
<div id="searchbox" style="display: none" role="search">
|
<div id="searchbox" style="display: none" role="search">
|
||||||
<div class="searchformwrapper">
|
<div class="searchformwrapper">
|
||||||
<form class="search" action="{{ pathto('search') }}" method="get">
|
<form class="search" action="{{ pathto('search') }}" method="get">
|
||||||
<input type="text" name="q" aria-labelledby="searchlabel"
|
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||||
placeholder="Search"/>
|
|
||||||
<input type="submit" value="{{ _('Go') }}" />
|
<input type="submit" value="{{ _('Go') }}" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="text/javascript">$('#searchbox').show(0);</script>
|
<script>document.getElementById('searchbox').style.display = "block"</script>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
|
@ -6,6 +6,7 @@ Release announcements
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
|
||||||
|
release-8.0.1
|
||||||
release-8.0.0
|
release-8.0.0
|
||||||
release-8.0.0rc2
|
release-8.0.0rc2
|
||||||
release-8.0.0rc1
|
release-8.0.0rc1
|
||||||
|
|
|
@ -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
|
cachedir: .pytest_cache
|
||||||
rootdir: /home/sweet/project
|
rootdir: /home/sweet/project
|
||||||
collected 0 items
|
collected 0 items
|
||||||
cache -- .../_pytest/cacheprovider.py:526
|
cache -- .../_pytest/cacheprovider.py:527
|
||||||
Return a cache object that can persist state between testing sessions.
|
Return a cache object that can persist state between testing sessions.
|
||||||
|
|
||||||
cache.get(key, default)
|
cache.get(key, default)
|
||||||
|
@ -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.
|
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``.
|
Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||||
|
|
||||||
The captured output is made available via ``capsysbinary.readouterr()``
|
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>`.
|
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def test_output(capsysbinary):
|
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()
|
captured = capsysbinary.readouterr()
|
||||||
assert captured.out == b"hello\n"
|
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``.
|
Enable text capturing of writes to file descriptors ``1`` and ``2``.
|
||||||
|
|
||||||
The captured output is made available via ``capfd.readouterr()`` method
|
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>`.
|
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def test_system_echo(capfd):
|
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()
|
captured = capfd.readouterr()
|
||||||
assert captured.out == "hello\n"
|
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``.
|
Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
|
||||||
|
|
||||||
The captured output is made available via ``capfd.readouterr()`` method
|
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>`.
|
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def test_system_echo(capfdbinary):
|
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>`.
|
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def test_output(capsys):
|
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()
|
captured = capsys.readouterr()
|
||||||
assert captured.out == "hello\n"
|
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
|
Fixture that returns a :py:class:`dict` that will be injected into the
|
||||||
namespace of doctests.
|
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`.
|
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`
|
Session-scoped fixture that returns the session's :class:`pytest.Config`
|
||||||
object.
|
object.
|
||||||
|
|
||||||
|
@ -129,7 +125,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
if pytestconfig.getoption("verbose") > 0:
|
if pytestconfig.getoption("verbose") > 0:
|
||||||
...
|
...
|
||||||
|
|
||||||
record_property -- .../_pytest/junitxml.py:284
|
record_property -- .../_pytest/junitxml.py:283
|
||||||
Add extra properties to the calling test.
|
Add extra properties to the calling test.
|
||||||
|
|
||||||
User properties become part of the test report and are available to the
|
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):
|
def test_function(record_property):
|
||||||
record_property("example_key", 1)
|
record_property("example_key", 1)
|
||||||
|
|
||||||
record_xml_attribute -- .../_pytest/junitxml.py:307
|
record_xml_attribute -- .../_pytest/junitxml.py:306
|
||||||
Add extra xml attributes to the tag for the calling test.
|
Add extra xml attributes to the tag for the calling test.
|
||||||
|
|
||||||
The fixture is callable with ``name, value``. The value is
|
The fixture is callable with ``name, value``. The value is
|
||||||
automatically XML-encoded.
|
automatically XML-encoded.
|
||||||
|
|
||||||
record_testsuite_property [session scope] -- .../_pytest/junitxml.py:345
|
record_testsuite_property [session scope] -- .../_pytest/junitxml.py:344
|
||||||
Record a new ``<property>`` tag as child of the root ``<testsuite>``.
|
Record a new ``<property>`` tag as child of the root ``<testsuite>``.
|
||||||
|
|
||||||
This is suitable to writing global information regarding the entire test
|
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
|
`pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See
|
||||||
:issue:`7767` for details.
|
: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.
|
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
|
Return a temporary directory path object which is unique to each test
|
||||||
function invocation, created as a sub directory of the base temporary
|
function invocation, created as a sub directory of the base temporary
|
||||||
directory.
|
directory.
|
||||||
|
@ -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.record_tuples -> list of (logger_name, level, message) tuples
|
||||||
* caplog.clear() -> clear captured records and formatted log output string
|
* caplog.clear() -> clear captured records and formatted log output string
|
||||||
|
|
||||||
monkeypatch -- .../_pytest/monkeypatch.py:30
|
monkeypatch -- .../_pytest/monkeypatch.py:32
|
||||||
A convenient fixture for monkey-patching.
|
A convenient fixture for monkey-patching.
|
||||||
|
|
||||||
The fixture provides these methods to modify objects, dictionaries, or
|
The fixture provides these methods to modify objects, dictionaries, or
|
||||||
|
@ -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,
|
To undo modifications done by the fixture in a contained scope,
|
||||||
use :meth:`context() <pytest.MonkeyPatch.context>`.
|
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.
|
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
|
See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information
|
||||||
on warning categories.
|
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.
|
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
|
Return a temporary directory path object which is unique to each test
|
||||||
function invocation, created as a sub directory of the base temporary
|
function invocation, created as a sub directory of the base temporary
|
||||||
directory.
|
directory.
|
||||||
|
|
|
@ -28,6 +28,30 @@ with advance notice in the **Deprecations** section of releases.
|
||||||
|
|
||||||
.. towncrier release notes start
|
.. 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)
|
pytest 8.0.0 (2024-01-27)
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
|
|
|
@ -503,10 +503,10 @@ Running it results in some skips if we don't have all the python interpreters in
|
||||||
.. code-block:: pytest
|
.. code-block:: pytest
|
||||||
|
|
||||||
. $ pytest -rs -q multipython.py
|
. $ pytest -rs -q multipython.py
|
||||||
ssssssssssssssssssssssss... [100%]
|
ssssssssssss...ssssssssssss [100%]
|
||||||
========================= short test summary info ==========================
|
========================= short test summary info ==========================
|
||||||
SKIPPED [12] multipython.py:68: 'python3.9' not found
|
SKIPPED [12] multipython.py:65: 'python3.9' not found
|
||||||
SKIPPED [12] multipython.py:68: 'python3.10' not found
|
SKIPPED [12] multipython.py:65: 'python3.11' not found
|
||||||
3 passed, 24 skipped in 0.12s
|
3 passed, 24 skipped in 0.12s
|
||||||
|
|
||||||
Parametrization of optional implementations/imports
|
Parametrization of optional implementations/imports
|
||||||
|
|
|
@ -22,7 +22,7 @@ Install ``pytest``
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ pytest --version
|
$ pytest --version
|
||||||
pytest 8.0.0
|
pytest 8.0.1
|
||||||
|
|
||||||
.. _`simpletest`:
|
.. _`simpletest`:
|
||||||
|
|
||||||
|
|
|
@ -206,8 +206,9 @@ option names are:
|
||||||
* ``log_cli_date_format``
|
* ``log_cli_date_format``
|
||||||
|
|
||||||
If you need to record the whole test suite logging calls to a file, you can pass
|
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.
|
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
|
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.
|
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:
|
option names are:
|
||||||
|
|
||||||
* ``log_file``
|
* ``log_file``
|
||||||
|
* ``log_file_mode``
|
||||||
* ``log_file_level``
|
* ``log_file_level``
|
||||||
* ``log_file_format``
|
* ``log_file_format``
|
||||||
* ``log_file_date_format``
|
* ``log_file_date_format``
|
||||||
|
|
||||||
You can call ``set_log_path()`` to customize the log_file path dynamically. This functionality
|
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:
|
.. _log_colors:
|
||||||
|
|
||||||
|
|
|
@ -177,13 +177,20 @@ Files will only be matched for configuration if:
|
||||||
* ``tox.ini``: contains a ``[pytest]`` section.
|
* ``tox.ini``: contains a ``[pytest]`` section.
|
||||||
* ``setup.cfg``: contains a ``[tool: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
|
The files are considered in the order above. Options from multiple ``configfiles`` candidates
|
||||||
are never merged - the first match wins.
|
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)
|
The :class:`Config <pytest.Config>` object (accessible via hooks or through the :fixture:`pytestconfig` fixture)
|
||||||
will subsequently carry these attributes:
|
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``
|
- :attr:`config.inipath <pytest.Config.inipath>`: the determined ``configfile``, may be ``None``
|
||||||
(it is named ``inipath`` for historical reasons).
|
(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
|
versions of the older ``config.rootdir`` and ``config.inifile``, which have type
|
||||||
``py.path.local``, and still exist for backward compatibility.
|
``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:
|
Example:
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -2028,7 +2028,7 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||||
failure
|
failure
|
||||||
--doctest-glob=pat Doctests file matching pattern, default: test*.txt
|
--doctest-glob=pat Doctests file matching pattern, default: test*.txt
|
||||||
--doctest-ignore-import-errors
|
--doctest-ignore-import-errors
|
||||||
Ignore doctest ImportErrors
|
Ignore doctest collection errors
|
||||||
--doctest-continue-on-failure
|
--doctest-continue-on-failure
|
||||||
For a given doctest, continue to run after the first
|
For a given doctest, continue to run after the first
|
||||||
failure
|
failure
|
||||||
|
|
|
@ -2,7 +2,7 @@ pallets-sphinx-themes
|
||||||
pluggy>=1.2.0
|
pluggy>=1.2.0
|
||||||
pygments-pytest>=2.3.0
|
pygments-pytest>=2.3.0
|
||||||
sphinx-removed-in>=0.2.0
|
sphinx-removed-in>=0.2.0
|
||||||
sphinx>=5,<8
|
sphinx>=7
|
||||||
sphinxcontrib-trio
|
sphinxcontrib-trio
|
||||||
sphinxcontrib-svg2pdfconverter
|
sphinxcontrib-svg2pdfconverter
|
||||||
# Pin packaging because it no longer handles 'latest' version, which
|
# Pin packaging because it no longer handles 'latest' version, which
|
||||||
|
|
|
@ -29,7 +29,7 @@ Pytest Plugin List
|
||||||
==================
|
==================
|
||||||
|
|
||||||
Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
|
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.
|
Packages classified as inactive are excluded.
|
||||||
|
|
||||||
For detailed insights into how this list is generated,
|
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
|
ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins
|
||||||
"logassert",
|
"logassert",
|
||||||
|
"logot",
|
||||||
"nuts",
|
"nuts",
|
||||||
"flask_fixture",
|
"flask_fixture",
|
||||||
}
|
}
|
||||||
|
@ -109,7 +110,10 @@ def pytest_plugin_projects_from_pypi(session: CachedSession) -> dict[str, int]:
|
||||||
return {
|
return {
|
||||||
name: p["_last-serial"]
|
name: p["_last-serial"]
|
||||||
for p in response.json()["projects"]
|
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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -197,7 +197,9 @@ def getstatementrange_ast(
|
||||||
# by using the BlockFinder helper used which inspect.getsource() uses itself.
|
# by using the BlockFinder helper used which inspect.getsource() uses itself.
|
||||||
block_finder = inspect.BlockFinder()
|
block_finder = inspect.BlockFinder()
|
||||||
# If we start with an indented line, put blockfinder to "started" mode.
|
# 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])
|
it = ((x + "\n") for x in source.lines[start:end])
|
||||||
try:
|
try:
|
||||||
for tok in tokenize.generate_tokens(lambda: next(it)):
|
for tok in tokenize.generate_tokens(lambda: next(it)):
|
||||||
|
|
|
@ -289,7 +289,7 @@ def get_user_id() -> int | None:
|
||||||
# mypy follows the version and platform checking expectation of PEP 484:
|
# 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
|
# 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.
|
# Containment checks are too complex for mypy v1.5.0 and cause failure.
|
||||||
if sys.platform in {"win32", "emscripten"}:
|
if sys.platform == "win32" or sys.platform == "emscripten": # noqa: PLR1714
|
||||||
# win32 does not have a getuid() function.
|
# win32 does not have a getuid() function.
|
||||||
# Emscripten has a return 0 stub.
|
# Emscripten has a return 0 stub.
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -578,12 +578,18 @@ class PytestPluginManager(PluginManager):
|
||||||
self._try_load_conftest(invocation_dir, importmode, rootpath)
|
self._try_load_conftest(invocation_dir, importmode, rootpath)
|
||||||
|
|
||||||
def _is_in_confcutdir(self, path: Path) -> bool:
|
def _is_in_confcutdir(self, path: Path) -> bool:
|
||||||
"""Whether a path is within the confcutdir.
|
"""Whether to consider the given path to load conftests from."""
|
||||||
|
|
||||||
When false, should not load conftest.
|
|
||||||
"""
|
|
||||||
if self._confcutdir is None:
|
if self._confcutdir is None:
|
||||||
return True
|
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
|
return path not in self._confcutdir.parents
|
||||||
|
|
||||||
def _try_load_conftest(
|
def _try_load_conftest(
|
||||||
|
@ -609,9 +615,6 @@ class PytestPluginManager(PluginManager):
|
||||||
if directory in self._dirpath2confmods:
|
if directory in self._dirpath2confmods:
|
||||||
return
|
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 = []
|
clist = []
|
||||||
for parent in reversed((directory, *directory.parents)):
|
for parent in reversed((directory, *directory.parents)):
|
||||||
if self._is_in_confcutdir(parent):
|
if self._is_in_confcutdir(parent):
|
||||||
|
@ -1563,9 +1566,11 @@ class Config:
|
||||||
# in this case, we already have a list ready to use.
|
# in this case, we already have a list ready to use.
|
||||||
#
|
#
|
||||||
if type == "paths":
|
if type == "paths":
|
||||||
# TODO: This assert is probably not valid in all cases.
|
dp = (
|
||||||
assert self.inipath is not None
|
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
|
input_values = shlex.split(value) if isinstance(value, str) else value
|
||||||
return [dp / x for x in input_values]
|
return [dp / x for x in input_values]
|
||||||
elif type == "args":
|
elif type == "args":
|
||||||
|
|
|
@ -198,9 +198,16 @@ class Parser:
|
||||||
* ``paths``: a list of :class:`pathlib.Path`, separated as in a shell
|
* ``paths``: a list of :class:`pathlib.Path`, separated as in a shell
|
||||||
* ``pathlist``: a list of ``py.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
|
.. versionadded:: 7.0
|
||||||
The ``paths`` variable type.
|
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.
|
Defaults to ``string`` if ``None`` or not passed.
|
||||||
:param default:
|
:param default:
|
||||||
Default value if no ini-file option exists but is queried.
|
Default value if no ini-file option exists but is queried.
|
||||||
|
|
|
@ -101,15 +101,20 @@ def locate_config(
|
||||||
args = [x for x in args if not str(x).startswith("-")]
|
args = [x for x in args if not str(x).startswith("-")]
|
||||||
if not args:
|
if not args:
|
||||||
args = [invocation_dir]
|
args = [invocation_dir]
|
||||||
|
found_pyproject_toml: Optional[Path] = None
|
||||||
for arg in args:
|
for arg in args:
|
||||||
argpath = absolutepath(arg)
|
argpath = absolutepath(arg)
|
||||||
for base in (argpath, *argpath.parents):
|
for base in (argpath, *argpath.parents):
|
||||||
for config_name in config_names:
|
for config_name in config_names:
|
||||||
p = base / config_name
|
p = base / config_name
|
||||||
if p.is_file():
|
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)
|
ini_config = load_config_dict_from_file(p)
|
||||||
if ini_config is not None:
|
if ini_config is not None:
|
||||||
return base, p, ini_config
|
return base, p, ini_config
|
||||||
|
if found_pyproject_toml is not None:
|
||||||
|
return found_pyproject_toml.parent, found_pyproject_toml, {}
|
||||||
return None, None, {}
|
return None, None, {}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,7 @@ from _pytest.warning_types import PytestWarning
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import doctest
|
import doctest
|
||||||
|
from typing import Self
|
||||||
|
|
||||||
DOCTEST_REPORT_CHOICE_NONE = "none"
|
DOCTEST_REPORT_CHOICE_NONE = "none"
|
||||||
DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
|
DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
|
||||||
|
@ -133,11 +134,9 @@ def pytest_collect_file(
|
||||||
if config.option.doctestmodules and not any(
|
if config.option.doctestmodules and not any(
|
||||||
(_is_setup_py(file_path), _is_main_py(file_path))
|
(_is_setup_py(file_path), _is_main_py(file_path))
|
||||||
):
|
):
|
||||||
mod: DoctestModule = DoctestModule.from_parent(parent, path=file_path)
|
return DoctestModule.from_parent(parent, path=file_path)
|
||||||
return mod
|
|
||||||
elif _is_doctest(config, file_path, parent):
|
elif _is_doctest(config, file_path, parent):
|
||||||
txt: DoctestTextfile = DoctestTextfile.from_parent(parent, path=file_path)
|
return DoctestTextfile.from_parent(parent, path=file_path)
|
||||||
return txt
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@ -272,14 +271,14 @@ class DoctestItem(Item):
|
||||||
self._initrequest()
|
self._initrequest()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_parent( # type: ignore
|
def from_parent( # type: ignore[override]
|
||||||
cls,
|
cls,
|
||||||
parent: "Union[DoctestTextfile, DoctestModule]",
|
parent: "Union[DoctestTextfile, DoctestModule]",
|
||||||
*,
|
*,
|
||||||
name: str,
|
name: str,
|
||||||
runner: "doctest.DocTestRunner",
|
runner: "doctest.DocTestRunner",
|
||||||
dtest: "doctest.DocTest",
|
dtest: "doctest.DocTest",
|
||||||
):
|
) -> "Self":
|
||||||
# incompatible signature due to imposed limits on subclass
|
# incompatible signature due to imposed limits on subclass
|
||||||
"""The public named constructor."""
|
"""The public named constructor."""
|
||||||
return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
|
return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
|
||||||
|
|
|
@ -169,33 +169,28 @@ def get_parametrized_fixture_keys(
|
||||||
the specified scope."""
|
the specified scope."""
|
||||||
assert scope is not Scope.Function
|
assert scope is not Scope.Function
|
||||||
try:
|
try:
|
||||||
callspec = item.callspec # type: ignore[attr-defined]
|
callspec: CallSpec2 = item.callspec # type: ignore[attr-defined]
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
return
|
||||||
else:
|
for argname in callspec.indices:
|
||||||
cs: CallSpec2 = callspec
|
if callspec._arg2scope[argname] != scope:
|
||||||
# cs.indices is random order of argnames. Need to
|
continue
|
||||||
# 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
|
|
||||||
|
|
||||||
item_cls = None
|
item_cls = None
|
||||||
if scope is Scope.Session:
|
if scope is Scope.Session:
|
||||||
scoped_item_path = None
|
scoped_item_path = None
|
||||||
elif scope is Scope.Package:
|
elif scope is Scope.Package:
|
||||||
scoped_item_path = item.path
|
scoped_item_path = item.path
|
||||||
elif scope is Scope.Module:
|
elif scope is Scope.Module:
|
||||||
scoped_item_path = item.path
|
scoped_item_path = item.path
|
||||||
elif scope is Scope.Class:
|
elif scope is Scope.Class:
|
||||||
scoped_item_path = item.path
|
scoped_item_path = item.path
|
||||||
item_cls = item.cls # type: ignore[attr-defined]
|
item_cls = item.cls # type: ignore[attr-defined]
|
||||||
else:
|
else:
|
||||||
assert_never(scope)
|
assert_never(scope)
|
||||||
|
|
||||||
param_index = cs.indices[argname]
|
param_index = callspec.indices[argname]
|
||||||
yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls)
|
yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls)
|
||||||
|
|
||||||
|
|
||||||
# Algorithm for sorting on a per-parametrized resource setup basis.
|
# Algorithm for sorting on a per-parametrized resource setup basis.
|
||||||
|
|
|
@ -298,6 +298,13 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
default=None,
|
default=None,
|
||||||
help="Path to a file when logging will be written to",
|
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(
|
add_option_ini(
|
||||||
"--log-file-level",
|
"--log-file-level",
|
||||||
dest="log_file_level",
|
dest="log_file_level",
|
||||||
|
@ -669,7 +676,10 @@ class LoggingPlugin:
|
||||||
if not os.path.isdir(directory):
|
if not os.path.isdir(directory):
|
||||||
os.makedirs(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_format = get_option_ini(config, "log_file_format", "log_format")
|
||||||
log_file_date_format = get_option_ini(
|
log_file_date_format = get_option_ini(
|
||||||
config, "log_file_date_format", "log_date_format"
|
config, "log_file_date_format", "log_date_format"
|
||||||
|
@ -746,7 +756,7 @@ class LoggingPlugin:
|
||||||
fpath.parent.mkdir(exist_ok=True, parents=True)
|
fpath.parent.mkdir(exist_ok=True, parents=True)
|
||||||
|
|
||||||
# https://github.com/python/mypy/issues/11193
|
# 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)
|
old_stream = self.log_file_handler.setStream(stream)
|
||||||
if old_stream:
|
if old_stream:
|
||||||
old_stream.close()
|
old_stream.close()
|
||||||
|
|
|
@ -21,6 +21,7 @@ from typing import Optional
|
||||||
from typing import overload
|
from typing import overload
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
from typing import Union
|
from typing import Union
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
@ -49,6 +50,10 @@ from _pytest.runner import SetupState
|
||||||
from _pytest.warning_types import PytestWarning
|
from _pytest.warning_types import PytestWarning
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from typing import Self
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser: Parser) -> None:
|
def pytest_addoption(parser: Parser) -> None:
|
||||||
parser.addini(
|
parser.addini(
|
||||||
"norecursedirs",
|
"norecursedirs",
|
||||||
|
@ -491,16 +496,16 @@ class Dir(nodes.Directory):
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_parent( # type: ignore[override]
|
def from_parent( # type: ignore[override]
|
||||||
cls,
|
cls,
|
||||||
parent: nodes.Collector, # type: ignore[override]
|
parent: nodes.Collector,
|
||||||
*,
|
*,
|
||||||
path: Path,
|
path: Path,
|
||||||
) -> "Dir":
|
) -> "Self":
|
||||||
"""The public constructor.
|
"""The public constructor.
|
||||||
|
|
||||||
:param parent: The parent collector of this Dir.
|
:param parent: The parent collector of this Dir.
|
||||||
:param path: The directory's path.
|
: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]]:
|
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
|
||||||
config = self.config
|
config = self.config
|
||||||
|
@ -901,6 +906,10 @@ class Session(nodes.Collector):
|
||||||
# Path part e.g. `/a/b/` in `/a/b/test_file.py::TestIt::test_it`.
|
# Path part e.g. `/a/b/` in `/a/b/test_file.py::TestIt::test_it`.
|
||||||
if isinstance(matchparts[0], Path):
|
if isinstance(matchparts[0], Path):
|
||||||
is_match = node.path == matchparts[0]
|
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`.
|
# Name part e.g. `TestIt` in `/a/b/test_file.py::TestIt::test_it`.
|
||||||
else:
|
else:
|
||||||
# TODO: Remove parametrized workaround once collection structure contains
|
# TODO: Remove parametrized workaround once collection structure contains
|
||||||
|
|
|
@ -355,7 +355,7 @@ class MarkDecorator:
|
||||||
func = args[0]
|
func = args[0]
|
||||||
is_class = inspect.isclass(func)
|
is_class = inspect.isclass(func)
|
||||||
if len(args) == 1 and (istestfunc(func) or is_class):
|
if len(args) == 1 and (istestfunc(func) or is_class):
|
||||||
store_mark(func, self.mark)
|
store_mark(func, self.mark, stacklevel=3)
|
||||||
return func
|
return func
|
||||||
return self.with_args(*args, **kwargs)
|
return self.with_args(*args, **kwargs)
|
||||||
|
|
||||||
|
@ -410,7 +410,7 @@ def normalize_mark_list(
|
||||||
yield mark_obj
|
yield mark_obj
|
||||||
|
|
||||||
|
|
||||||
def store_mark(obj, mark: Mark) -> None:
|
def store_mark(obj, mark: Mark, *, stacklevel: int = 2) -> None:
|
||||||
"""Store a Mark on an object.
|
"""Store a Mark on an object.
|
||||||
|
|
||||||
This is used to implement the Mark declarations/decorators correctly.
|
This is used to implement the Mark declarations/decorators correctly.
|
||||||
|
@ -420,7 +420,7 @@ def store_mark(obj, mark: Mark) -> None:
|
||||||
from ..fixtures import getfixturemarker
|
from ..fixtures import getfixturemarker
|
||||||
|
|
||||||
if getfixturemarker(obj) is not None:
|
if getfixturemarker(obj) is not None:
|
||||||
warnings.warn(MARKED_FIXTURE, stacklevel=2)
|
warnings.warn(MARKED_FIXTURE, stacklevel=stacklevel)
|
||||||
|
|
||||||
# Always reassign name to avoid updating pytestmark in a reference that
|
# Always reassign name to avoid updating pytestmark in a reference that
|
||||||
# was only borrowed.
|
# was only borrowed.
|
||||||
|
|
|
@ -11,6 +11,7 @@ from typing import Iterable
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import MutableMapping
|
from typing import MutableMapping
|
||||||
|
from typing import NoReturn
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import overload
|
from typing import overload
|
||||||
from typing import Set
|
from typing import Set
|
||||||
|
@ -41,6 +42,8 @@ from _pytest.warning_types import PytestWarning
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from typing import Self
|
||||||
|
|
||||||
# Imported here due to circular import.
|
# Imported here due to circular import.
|
||||||
from _pytest._code.code import _TracebackStyle
|
from _pytest._code.code import _TracebackStyle
|
||||||
from _pytest.main import Session
|
from _pytest.main import Session
|
||||||
|
@ -51,6 +54,7 @@ SEP = "/"
|
||||||
tracebackcutdir = Path(_pytest.__file__).parent
|
tracebackcutdir = Path(_pytest.__file__).parent
|
||||||
|
|
||||||
|
|
||||||
|
_T = TypeVar("_T")
|
||||||
_NodeType = TypeVar("_NodeType", bound="Node")
|
_NodeType = TypeVar("_NodeType", bound="Node")
|
||||||
|
|
||||||
|
|
||||||
|
@ -69,33 +73,33 @@ class NodeMeta(abc.ABCMeta):
|
||||||
progress on detangling the :class:`Node` classes.
|
progress on detangling the :class:`Node` classes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __call__(self, *k, **kw):
|
def __call__(cls, *k, **kw) -> NoReturn:
|
||||||
msg = (
|
msg = (
|
||||||
"Direct construction of {name} has been deprecated, please use {name}.from_parent.\n"
|
"Direct construction of {name} has been deprecated, please use {name}.from_parent.\n"
|
||||||
"See "
|
"See "
|
||||||
"https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent"
|
"https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent"
|
||||||
" for more details."
|
" for more details."
|
||||||
).format(name=f"{self.__module__}.{self.__name__}")
|
).format(name=f"{cls.__module__}.{cls.__name__}")
|
||||||
fail(msg, pytrace=False)
|
fail(msg, pytrace=False)
|
||||||
|
|
||||||
def _create(self, *k, **kw):
|
def _create(cls: Type[_T], *k, **kw) -> _T:
|
||||||
try:
|
try:
|
||||||
return super().__call__(*k, **kw)
|
return super().__call__(*k, **kw) # type: ignore[no-any-return,misc]
|
||||||
except TypeError:
|
except TypeError:
|
||||||
sig = signature(getattr(self, "__init__"))
|
sig = signature(getattr(cls, "__init__"))
|
||||||
known_kw = {k: v for k, v in kw.items() if k in sig.parameters}
|
known_kw = {k: v for k, v in kw.items() if k in sig.parameters}
|
||||||
from .warning_types import PytestDeprecationWarning
|
from .warning_types import PytestDeprecationWarning
|
||||||
|
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
PytestDeprecationWarning(
|
PytestDeprecationWarning(
|
||||||
f"{self} is not using a cooperative constructor and only takes {set(known_kw)}.\n"
|
f"{cls} is not using a cooperative constructor and only takes {set(known_kw)}.\n"
|
||||||
"See https://docs.pytest.org/en/stable/deprecations.html"
|
"See https://docs.pytest.org/en/stable/deprecations.html"
|
||||||
"#constructors-of-custom-pytest-node-subclasses-should-take-kwargs "
|
"#constructors-of-custom-pytest-node-subclasses-should-take-kwargs "
|
||||||
"for more details."
|
"for more details."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().__call__(*k, **known_kw)
|
return super().__call__(*k, **known_kw) # type: ignore[no-any-return,misc]
|
||||||
|
|
||||||
|
|
||||||
class Node(abc.ABC, metaclass=NodeMeta):
|
class Node(abc.ABC, metaclass=NodeMeta):
|
||||||
|
@ -181,7 +185,7 @@ class Node(abc.ABC, metaclass=NodeMeta):
|
||||||
self._store = self.stash
|
self._store = self.stash
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_parent(cls, parent: "Node", **kw):
|
def from_parent(cls, parent: "Node", **kw) -> "Self":
|
||||||
"""Public constructor for Nodes.
|
"""Public constructor for Nodes.
|
||||||
|
|
||||||
This indirection got introduced in order to enable removing
|
This indirection got introduced in order to enable removing
|
||||||
|
@ -583,7 +587,7 @@ class FSCollector(Collector, abc.ABC):
|
||||||
*,
|
*,
|
||||||
path: Optional[Path] = None,
|
path: Optional[Path] = None,
|
||||||
**kw,
|
**kw,
|
||||||
):
|
) -> "Self":
|
||||||
"""The public constructor."""
|
"""The public constructor."""
|
||||||
return super().from_parent(parent=parent, path=path, **kw)
|
return super().from_parent(parent=parent, path=path, **kw)
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ from typing import Pattern
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from typing import Set
|
from typing import Set
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
from typing import Union
|
from typing import Union
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
@ -81,6 +82,10 @@ from _pytest.warning_types import PytestReturnNotNoneWarning
|
||||||
from _pytest.warning_types import PytestUnhandledCoroutineWarning
|
from _pytest.warning_types import PytestUnhandledCoroutineWarning
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from typing import Self
|
||||||
|
|
||||||
|
|
||||||
_PYTEST_DIR = Path(_pytest.__file__).parent
|
_PYTEST_DIR = Path(_pytest.__file__).parent
|
||||||
|
|
||||||
|
|
||||||
|
@ -204,8 +209,7 @@ def pytest_collect_directory(
|
||||||
) -> Optional[nodes.Collector]:
|
) -> Optional[nodes.Collector]:
|
||||||
pkginit = path / "__init__.py"
|
pkginit = path / "__init__.py"
|
||||||
if pkginit.is_file():
|
if pkginit.is_file():
|
||||||
pkg: Package = Package.from_parent(parent, path=path)
|
return Package.from_parent(parent, path=path)
|
||||||
return pkg
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@ -230,8 +234,7 @@ def path_matches_patterns(path: Path, patterns: Iterable[str]) -> bool:
|
||||||
|
|
||||||
|
|
||||||
def pytest_pycollect_makemodule(module_path: Path, parent) -> "Module":
|
def pytest_pycollect_makemodule(module_path: Path, parent) -> "Module":
|
||||||
mod: Module = Module.from_parent(parent, path=module_path)
|
return Module.from_parent(parent, path=module_path)
|
||||||
return mod
|
|
||||||
|
|
||||||
|
|
||||||
@hookimpl(trylast=True)
|
@hookimpl(trylast=True)
|
||||||
|
@ -242,8 +245,7 @@ def pytest_pycollect_makeitem(
|
||||||
# Nothing was collected elsewhere, let's do it here.
|
# Nothing was collected elsewhere, let's do it here.
|
||||||
if safe_isclass(obj):
|
if safe_isclass(obj):
|
||||||
if collector.istestclass(obj, name):
|
if collector.istestclass(obj, name):
|
||||||
klass: Class = Class.from_parent(collector, name=name, obj=obj)
|
return Class.from_parent(collector, name=name, obj=obj)
|
||||||
return klass
|
|
||||||
elif collector.istestfunction(obj, name):
|
elif collector.istestfunction(obj, name):
|
||||||
# mock seems to store unbound methods (issue473), normalize it.
|
# mock seems to store unbound methods (issue473), normalize it.
|
||||||
obj = getattr(obj, "__func__", obj)
|
obj = getattr(obj, "__func__", obj)
|
||||||
|
@ -262,7 +264,7 @@ def pytest_pycollect_makeitem(
|
||||||
)
|
)
|
||||||
elif getattr(obj, "__test__", True):
|
elif getattr(obj, "__test__", True):
|
||||||
if is_generator(obj):
|
if is_generator(obj):
|
||||||
res: Function = Function.from_parent(collector, name=name)
|
res = Function.from_parent(collector, name=name)
|
||||||
reason = (
|
reason = (
|
||||||
f"yield tests were removed in pytest 4.0 - {name} will be ignored"
|
f"yield tests were removed in pytest 4.0 - {name} will be ignored"
|
||||||
)
|
)
|
||||||
|
@ -465,9 +467,7 @@ class PyCollector(PyobjMixin, nodes.Collector, abc.ABC):
|
||||||
clscol = self.getparent(Class)
|
clscol = self.getparent(Class)
|
||||||
cls = clscol and clscol.obj or None
|
cls = clscol and clscol.obj or None
|
||||||
|
|
||||||
definition: FunctionDefinition = FunctionDefinition.from_parent(
|
definition = FunctionDefinition.from_parent(self, name=name, callobj=funcobj)
|
||||||
self, name=name, callobj=funcobj
|
|
||||||
)
|
|
||||||
fixtureinfo = definition._fixtureinfo
|
fixtureinfo = definition._fixtureinfo
|
||||||
|
|
||||||
# pytest_generate_tests impls call metafunc.parametrize() which fills
|
# pytest_generate_tests impls call metafunc.parametrize() which fills
|
||||||
|
@ -751,7 +751,7 @@ class Class(PyCollector):
|
||||||
"""Collector for test methods (and nested classes) in a Python class."""
|
"""Collector for test methods (and nested classes) in a Python class."""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_parent(cls, parent, *, name, obj=None, **kw):
|
def from_parent(cls, parent, *, name, obj=None, **kw) -> "Self": # type: ignore[override]
|
||||||
"""The public constructor."""
|
"""The public constructor."""
|
||||||
return super().from_parent(name=name, parent=parent, **kw)
|
return super().from_parent(name=name, parent=parent, **kw)
|
||||||
|
|
||||||
|
@ -1267,7 +1267,6 @@ class Metafunc:
|
||||||
# Add funcargs as fixturedefs to fixtureinfo.arg2fixturedefs by registering
|
# Add funcargs as fixturedefs to fixtureinfo.arg2fixturedefs by registering
|
||||||
# artificial "pseudo" FixtureDef's so that later at test execution time we can
|
# artificial "pseudo" FixtureDef's so that later at test execution time we can
|
||||||
# rely on a proper FixtureDef to exist for fixture setup.
|
# rely on a proper FixtureDef to exist for fixture setup.
|
||||||
arg2fixturedefs = self._arg2fixturedefs
|
|
||||||
node = None
|
node = None
|
||||||
# If we have a scope that is higher than function, we need
|
# If we have a scope that is higher than function, we need
|
||||||
# to make sure we only ever create an according fixturedef on
|
# to make sure we only ever create an according fixturedef on
|
||||||
|
@ -1281,7 +1280,7 @@ class Metafunc:
|
||||||
# If used class scope and there is no class, use module-level
|
# If used class scope and there is no class, use module-level
|
||||||
# collector (for now).
|
# collector (for now).
|
||||||
if scope_ is Scope.Class:
|
if scope_ is Scope.Class:
|
||||||
assert isinstance(collector, _pytest.python.Module)
|
assert isinstance(collector, Module)
|
||||||
node = collector
|
node = collector
|
||||||
# If used package scope and there is no package, use session
|
# If used package scope and there is no package, use session
|
||||||
# (for now).
|
# (for now).
|
||||||
|
@ -1316,7 +1315,7 @@ class Metafunc:
|
||||||
)
|
)
|
||||||
if name2pseudofixturedef is not None:
|
if name2pseudofixturedef is not None:
|
||||||
name2pseudofixturedef[argname] = fixturedef
|
name2pseudofixturedef[argname] = fixturedef
|
||||||
arg2fixturedefs[argname] = [fixturedef]
|
self._arg2fixturedefs[argname] = [fixturedef]
|
||||||
|
|
||||||
# Create the new calls: if we are parametrize() multiple times (by applying the decorator
|
# Create the new calls: if we are parametrize() multiple times (by applying the decorator
|
||||||
# more than once) then we accumulate those calls generating the cartesian product
|
# more than once) then we accumulate those calls generating the cartesian product
|
||||||
|
@ -1731,8 +1730,9 @@ class Function(PyobjMixin, nodes.Item):
|
||||||
self.fixturenames = fixtureinfo.names_closure
|
self.fixturenames = fixtureinfo.names_closure
|
||||||
self._initrequest()
|
self._initrequest()
|
||||||
|
|
||||||
|
# todo: determine sound type limitations
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_parent(cls, parent, **kw): # todo: determine sound type limitations
|
def from_parent(cls, parent, **kw) -> "Self":
|
||||||
"""The public constructor."""
|
"""The public constructor."""
|
||||||
return super().from_parent(parent=parent, **kw)
|
return super().from_parent(parent=parent, **kw)
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ import warnings
|
||||||
|
|
||||||
from _pytest.deprecated import check_ispytest
|
from _pytest.deprecated import check_ispytest
|
||||||
from _pytest.fixtures import fixture
|
from _pytest.fixtures import fixture
|
||||||
|
from _pytest.outcomes import Exit
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
|
|
||||||
|
|
||||||
|
@ -302,7 +303,18 @@ class WarningsChecker(WarningsRecorder):
|
||||||
|
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
|
|
||||||
def found_str():
|
# BaseExceptions like pytest.{skip,fail,xfail,exit} or Ctrl-C within
|
||||||
|
# pytest.warns should *not* trigger "DID NOT WARN" and get suppressed
|
||||||
|
# when the warning doesn't happen. Control-flow exceptions should always
|
||||||
|
# propagate.
|
||||||
|
if exc_val is not None and (
|
||||||
|
not isinstance(exc_val, Exception)
|
||||||
|
# Exit is an Exception, not a BaseException, for some reason.
|
||||||
|
or isinstance(exc_val, Exit)
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
def found_str() -> str:
|
||||||
return pformat([record.message for record in self], indent=2)
|
return pformat([record.message for record in self], indent=2)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -322,21 +334,37 @@ class WarningsChecker(WarningsRecorder):
|
||||||
for w in self:
|
for w in self:
|
||||||
if not self.matches(w):
|
if not self.matches(w):
|
||||||
warnings.warn_explicit(
|
warnings.warn_explicit(
|
||||||
str(w.message),
|
message=w.message,
|
||||||
w.message.__class__, # type: ignore[arg-type]
|
category=w.category,
|
||||||
w.filename,
|
filename=w.filename,
|
||||||
w.lineno,
|
lineno=w.lineno,
|
||||||
module=w.__module__,
|
module=w.__module__,
|
||||||
source=w.source,
|
source=w.source,
|
||||||
)
|
)
|
||||||
# Check warnings has valid argument type (#10865).
|
|
||||||
wrn: warnings.WarningMessage
|
|
||||||
for wrn in self:
|
|
||||||
self._validate_message(wrn)
|
|
||||||
|
|
||||||
@staticmethod
|
# Currently in Python it is possible to pass other types than an
|
||||||
def _validate_message(wrn: Any) -> None:
|
# `str` message when creating `Warning` instances, however this
|
||||||
if not isinstance(msg := wrn.message.args[0], str):
|
# causes an exception when :func:`warnings.filterwarnings` is used
|
||||||
raise TypeError(
|
# to filter those warnings. See
|
||||||
f"Warning message must be str, got {msg!r} (type {type(msg).__name__})"
|
# https://github.com/python/cpython/issues/103577 for a discussion.
|
||||||
)
|
# While this can be considered a bug in CPython, we put guards in
|
||||||
|
# pytest as the error message produced without this check in place
|
||||||
|
# is confusing (#10865).
|
||||||
|
for w in self:
|
||||||
|
if type(w.message) is not UserWarning:
|
||||||
|
# If the warning was of an incorrect type then `warnings.warn()`
|
||||||
|
# creates a UserWarning. Any other warning must have been specified
|
||||||
|
# explicitly.
|
||||||
|
continue
|
||||||
|
if not w.message.args:
|
||||||
|
# UserWarning() without arguments must have been specified explicitly.
|
||||||
|
continue
|
||||||
|
msg = w.message.args[0]
|
||||||
|
if isinstance(msg, str):
|
||||||
|
continue
|
||||||
|
# It's possible that UserWarning was explicitly specified, and
|
||||||
|
# its first argument was not a string. But that case can't be
|
||||||
|
# distinguished from an invalid type.
|
||||||
|
raise TypeError(
|
||||||
|
f"Warning must be str or Warning, got {msg!r} (type {type(msg).__name__})"
|
||||||
|
)
|
||||||
|
|
|
@ -55,8 +55,7 @@ def pytest_pycollect_makeitem(
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
# Yes, so let's collect it.
|
# Yes, so let's collect it.
|
||||||
item: UnitTestCase = UnitTestCase.from_parent(collector, name=name, obj=obj)
|
return UnitTestCase.from_parent(collector, name=name, obj=obj)
|
||||||
return item
|
|
||||||
|
|
||||||
|
|
||||||
class UnitTestCase(Class):
|
class UnitTestCase(Class):
|
||||||
|
|
|
@ -118,6 +118,8 @@ def test_fixture_disallow_marks_on_fixtures():
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
assert len(record) == 2 # one for each mark decorator
|
assert len(record) == 2 # one for each mark decorator
|
||||||
|
# should point to this file
|
||||||
|
assert all(rec.filename == __file__ for rec in record)
|
||||||
|
|
||||||
|
|
||||||
def test_fixture_disallowed_between_marks():
|
def test_fixture_disallowed_between_marks():
|
||||||
|
|
|
@ -661,6 +661,73 @@ def test_log_file_cli(pytester: Pytester) -> None:
|
||||||
assert "This log message won't be shown" not in contents
|
assert "This log message won't be shown" not in contents
|
||||||
|
|
||||||
|
|
||||||
|
def test_log_file_mode_cli(pytester: Pytester) -> None:
|
||||||
|
# Default log file level
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
import logging
|
||||||
|
def test_log_file(request):
|
||||||
|
plugin = request.config.pluginmanager.getplugin('logging-plugin')
|
||||||
|
assert plugin.log_file_handler.level == logging.WARNING
|
||||||
|
logging.getLogger('catchlog').info("This log message won't be shown")
|
||||||
|
logging.getLogger('catchlog').warning("This log message will be shown")
|
||||||
|
print('PASSED')
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
log_file = str(pytester.path.joinpath("pytest.log"))
|
||||||
|
|
||||||
|
with open(log_file, mode="w", encoding="utf-8") as wfh:
|
||||||
|
wfh.write("A custom header\n")
|
||||||
|
|
||||||
|
result = pytester.runpytest(
|
||||||
|
"-s",
|
||||||
|
f"--log-file={log_file}",
|
||||||
|
"--log-file-mode=a",
|
||||||
|
"--log-file-level=WARNING",
|
||||||
|
)
|
||||||
|
|
||||||
|
# fnmatch_lines does an assertion internally
|
||||||
|
result.stdout.fnmatch_lines(["test_log_file_mode_cli.py PASSED"])
|
||||||
|
|
||||||
|
# make sure that we get a '0' exit code for the testsuite
|
||||||
|
assert result.ret == 0
|
||||||
|
assert os.path.isfile(log_file)
|
||||||
|
with open(log_file, encoding="utf-8") as rfh:
|
||||||
|
contents = rfh.read()
|
||||||
|
assert "A custom header" in contents
|
||||||
|
assert "This log message will be shown" in contents
|
||||||
|
assert "This log message won't be shown" not in contents
|
||||||
|
|
||||||
|
|
||||||
|
def test_log_file_mode_cli_invalid(pytester: Pytester) -> None:
|
||||||
|
# Default log file level
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
import logging
|
||||||
|
def test_log_file(request):
|
||||||
|
plugin = request.config.pluginmanager.getplugin('logging-plugin')
|
||||||
|
assert plugin.log_file_handler.level == logging.WARNING
|
||||||
|
logging.getLogger('catchlog').info("This log message won't be shown")
|
||||||
|
logging.getLogger('catchlog').warning("This log message will be shown")
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
log_file = str(pytester.path.joinpath("pytest.log"))
|
||||||
|
|
||||||
|
result = pytester.runpytest(
|
||||||
|
"-s",
|
||||||
|
f"--log-file={log_file}",
|
||||||
|
"--log-file-mode=b",
|
||||||
|
"--log-file-level=WARNING",
|
||||||
|
)
|
||||||
|
|
||||||
|
# make sure that we get a '4' exit code for the testsuite
|
||||||
|
assert result.ret == ExitCode.USAGE_ERROR
|
||||||
|
|
||||||
|
|
||||||
def test_log_file_cli_level(pytester: Pytester) -> None:
|
def test_log_file_cli_level(pytester: Pytester) -> None:
|
||||||
# Default log file level
|
# Default log file level
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
|
@ -741,6 +808,47 @@ def test_log_file_ini(pytester: Pytester) -> None:
|
||||||
assert "This log message won't be shown" not in contents
|
assert "This log message won't be shown" not in contents
|
||||||
|
|
||||||
|
|
||||||
|
def test_log_file_mode_ini(pytester: Pytester) -> None:
|
||||||
|
log_file = str(pytester.path.joinpath("pytest.log"))
|
||||||
|
|
||||||
|
pytester.makeini(
|
||||||
|
f"""
|
||||||
|
[pytest]
|
||||||
|
log_file={log_file}
|
||||||
|
log_file_mode=a
|
||||||
|
log_file_level=WARNING
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
import logging
|
||||||
|
def test_log_file(request):
|
||||||
|
plugin = request.config.pluginmanager.getplugin('logging-plugin')
|
||||||
|
assert plugin.log_file_handler.level == logging.WARNING
|
||||||
|
logging.getLogger('catchlog').info("This log message won't be shown")
|
||||||
|
logging.getLogger('catchlog').warning("This log message will be shown")
|
||||||
|
print('PASSED')
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(log_file, mode="w", encoding="utf-8") as wfh:
|
||||||
|
wfh.write("A custom header\n")
|
||||||
|
|
||||||
|
result = pytester.runpytest("-s")
|
||||||
|
|
||||||
|
# fnmatch_lines does an assertion internally
|
||||||
|
result.stdout.fnmatch_lines(["test_log_file_mode_ini.py PASSED"])
|
||||||
|
|
||||||
|
assert result.ret == ExitCode.OK
|
||||||
|
assert os.path.isfile(log_file)
|
||||||
|
with open(log_file, encoding="utf-8") as rfh:
|
||||||
|
contents = rfh.read()
|
||||||
|
assert "A custom header" in contents
|
||||||
|
assert "This log message will be shown" in contents
|
||||||
|
assert "This log message won't be shown" not in contents
|
||||||
|
|
||||||
|
|
||||||
def test_log_file_ini_level(pytester: Pytester) -> None:
|
def test_log_file_ini_level(pytester: Pytester) -> None:
|
||||||
log_file = str(pytester.path.joinpath("pytest.log"))
|
log_file = str(pytester.path.joinpath("pytest.log"))
|
||||||
|
|
||||||
|
@ -1060,6 +1168,66 @@ def test_log_set_path(pytester: Pytester) -> None:
|
||||||
assert "message from test 2" in content
|
assert "message from test 2" in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_log_set_path_with_log_file_mode(pytester: Pytester) -> None:
|
||||||
|
report_dir_base = str(pytester.path)
|
||||||
|
|
||||||
|
pytester.makeini(
|
||||||
|
"""
|
||||||
|
[pytest]
|
||||||
|
log_file_level = DEBUG
|
||||||
|
log_cli=true
|
||||||
|
log_file_mode=a
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
pytester.makeconftest(
|
||||||
|
f"""
|
||||||
|
import os
|
||||||
|
import pytest
|
||||||
|
@pytest.hookimpl(wrapper=True, tryfirst=True)
|
||||||
|
def pytest_runtest_setup(item):
|
||||||
|
config = item.config
|
||||||
|
logging_plugin = config.pluginmanager.get_plugin("logging-plugin")
|
||||||
|
report_file = os.path.join({report_dir_base!r}, item._request.node.name)
|
||||||
|
logging_plugin.set_log_path(report_file)
|
||||||
|
return (yield)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger("testcase-logger")
|
||||||
|
def test_first():
|
||||||
|
logger.info("message from test 1")
|
||||||
|
assert True
|
||||||
|
|
||||||
|
def test_second():
|
||||||
|
logger.debug("message from test 2")
|
||||||
|
assert True
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
test_first_log_file = os.path.join(report_dir_base, "test_first")
|
||||||
|
test_second_log_file = os.path.join(report_dir_base, "test_second")
|
||||||
|
with open(test_first_log_file, mode="w", encoding="utf-8") as wfh:
|
||||||
|
wfh.write("A custom header for test 1\n")
|
||||||
|
|
||||||
|
with open(test_second_log_file, mode="w", encoding="utf-8") as wfh:
|
||||||
|
wfh.write("A custom header for test 2\n")
|
||||||
|
|
||||||
|
result = pytester.runpytest()
|
||||||
|
assert result.ret == ExitCode.OK
|
||||||
|
|
||||||
|
with open(test_first_log_file, encoding="utf-8") as rfh:
|
||||||
|
content = rfh.read()
|
||||||
|
assert "A custom header for test 1" in content
|
||||||
|
assert "message from test 1" in content
|
||||||
|
|
||||||
|
with open(test_second_log_file, encoding="utf-8") as rfh:
|
||||||
|
content = rfh.read()
|
||||||
|
assert "A custom header for test 2" in content
|
||||||
|
assert "message from test 2" in content
|
||||||
|
|
||||||
|
|
||||||
def test_colored_captured_log(pytester: Pytester) -> None:
|
def test_colored_captured_log(pytester: Pytester) -> None:
|
||||||
"""Test that the level names of captured log messages of a failing test
|
"""Test that the level names of captured log messages of a failing test
|
||||||
are colored."""
|
are colored."""
|
||||||
|
|
|
@ -2730,12 +2730,12 @@ class TestFixtureMarker:
|
||||||
"""
|
"""
|
||||||
test_dynamic_parametrized_ordering.py::test[flavor1-vxlan] PASSED
|
test_dynamic_parametrized_ordering.py::test[flavor1-vxlan] PASSED
|
||||||
test_dynamic_parametrized_ordering.py::test2[flavor1-vxlan] PASSED
|
test_dynamic_parametrized_ordering.py::test2[flavor1-vxlan] PASSED
|
||||||
test_dynamic_parametrized_ordering.py::test[flavor2-vxlan] PASSED
|
|
||||||
test_dynamic_parametrized_ordering.py::test2[flavor2-vxlan] PASSED
|
|
||||||
test_dynamic_parametrized_ordering.py::test[flavor2-vlan] PASSED
|
|
||||||
test_dynamic_parametrized_ordering.py::test2[flavor2-vlan] PASSED
|
|
||||||
test_dynamic_parametrized_ordering.py::test[flavor1-vlan] PASSED
|
test_dynamic_parametrized_ordering.py::test[flavor1-vlan] PASSED
|
||||||
test_dynamic_parametrized_ordering.py::test2[flavor1-vlan] PASSED
|
test_dynamic_parametrized_ordering.py::test2[flavor1-vlan] PASSED
|
||||||
|
test_dynamic_parametrized_ordering.py::test[flavor2-vlan] PASSED
|
||||||
|
test_dynamic_parametrized_ordering.py::test2[flavor2-vlan] PASSED
|
||||||
|
test_dynamic_parametrized_ordering.py::test[flavor2-vxlan] PASSED
|
||||||
|
test_dynamic_parametrized_ordering.py::test2[flavor2-vxlan] PASSED
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,11 @@ from pathlib import Path
|
||||||
import pprint
|
import pprint
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
import textwrap
|
import textwrap
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
from _pytest.assertion.util import running_on_ci
|
||||||
from _pytest.config import ExitCode
|
from _pytest.config import ExitCode
|
||||||
from _pytest.fixtures import FixtureRequest
|
from _pytest.fixtures import FixtureRequest
|
||||||
from _pytest.main import _in_venv
|
from _pytest.main import _in_venv
|
||||||
|
@ -1613,7 +1615,7 @@ def test_fscollector_from_parent(pytester: Pytester, request: FixtureRequest) ->
|
||||||
assert collector.x == 10
|
assert collector.x == 10
|
||||||
|
|
||||||
|
|
||||||
def test_class_from_parent(pytester: Pytester, request: FixtureRequest) -> None:
|
def test_class_from_parent(request: FixtureRequest) -> None:
|
||||||
"""Ensure Class.from_parent can forward custom arguments to the constructor."""
|
"""Ensure Class.from_parent can forward custom arguments to the constructor."""
|
||||||
|
|
||||||
class MyCollector(pytest.Class):
|
class MyCollector(pytest.Class):
|
||||||
|
@ -1759,3 +1761,29 @@ def test_does_not_crash_on_recursive_symlink(pytester: Pytester) -> None:
|
||||||
|
|
||||||
assert result.ret == ExitCode.OK
|
assert result.ret == ExitCode.OK
|
||||||
assert result.parseoutcomes() == {"passed": 1}
|
assert result.parseoutcomes() == {"passed": 1}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not sys.platform.startswith("win"), reason="Windows only")
|
||||||
|
def test_collect_short_file_windows(pytester: Pytester) -> None:
|
||||||
|
"""Reproducer for #11895: short paths not colleced on Windows."""
|
||||||
|
short_path = tempfile.mkdtemp()
|
||||||
|
if "~" not in short_path: # pragma: no cover
|
||||||
|
if running_on_ci():
|
||||||
|
# On CI, we are expecting that under the current GitHub actions configuration,
|
||||||
|
# tempfile.mkdtemp() is producing short paths, so we want to fail to prevent
|
||||||
|
# this from silently changing without us noticing.
|
||||||
|
pytest.fail(
|
||||||
|
f"tempfile.mkdtemp() failed to produce a short path on CI: {short_path}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# We want to skip failing this test locally in this situation because
|
||||||
|
# depending on the local configuration tempfile.mkdtemp() might not produce a short path:
|
||||||
|
# For example, user might have configured %TEMP% exactly to avoid generating short paths.
|
||||||
|
pytest.skip(
|
||||||
|
f"tempfile.mkdtemp() failed to produce a short path: {short_path}, skipping"
|
||||||
|
)
|
||||||
|
|
||||||
|
test_file = Path(short_path).joinpath("test_collect_short_file_windows.py")
|
||||||
|
test_file.write_text("def test(): pass", encoding="UTF-8")
|
||||||
|
result = pytester.runpytest(short_path)
|
||||||
|
assert result.parseoutcomes() == {"passed": 1}
|
||||||
|
|
|
@ -135,15 +135,45 @@ class TestParseIni:
|
||||||
assert config.getini("minversion") == "3.36"
|
assert config.getini("minversion") == "3.36"
|
||||||
|
|
||||||
def test_pyproject_toml(self, pytester: Pytester) -> None:
|
def test_pyproject_toml(self, pytester: Pytester) -> None:
|
||||||
pytester.makepyprojecttoml(
|
pyproject_toml = pytester.makepyprojecttoml(
|
||||||
"""
|
"""
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
minversion = "1.0"
|
minversion = "1.0"
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
config = pytester.parseconfig()
|
config = pytester.parseconfig()
|
||||||
|
assert config.inipath == pyproject_toml
|
||||||
assert config.getini("minversion") == "1.0"
|
assert config.getini("minversion") == "1.0"
|
||||||
|
|
||||||
|
def test_empty_pyproject_toml(self, pytester: Pytester) -> None:
|
||||||
|
"""An empty pyproject.toml is considered as config if no other option is found."""
|
||||||
|
pyproject_toml = pytester.makepyprojecttoml("")
|
||||||
|
config = pytester.parseconfig()
|
||||||
|
assert config.inipath == pyproject_toml
|
||||||
|
|
||||||
|
def test_empty_pyproject_toml_found_many(self, pytester: Pytester) -> None:
|
||||||
|
"""
|
||||||
|
In case we find multiple pyproject.toml files in our search, without a [tool.pytest.ini_options]
|
||||||
|
table and without finding other candidates, the closest to where we started wins.
|
||||||
|
"""
|
||||||
|
pytester.makefile(
|
||||||
|
".toml",
|
||||||
|
**{
|
||||||
|
"pyproject": "",
|
||||||
|
"foo/pyproject": "",
|
||||||
|
"foo/bar/pyproject": "",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
config = pytester.parseconfig(pytester.path / "foo/bar")
|
||||||
|
assert config.inipath == pytester.path / "foo/bar/pyproject.toml"
|
||||||
|
|
||||||
|
def test_pytest_ini_trumps_pyproject_toml(self, pytester: Pytester) -> None:
|
||||||
|
"""A pytest.ini always take precedence over a pyproject.toml file."""
|
||||||
|
pytester.makepyprojecttoml("[tool.pytest.ini_options]")
|
||||||
|
pytest_ini = pytester.makefile(".ini", pytest="")
|
||||||
|
config = pytester.parseconfig()
|
||||||
|
assert config.inipath == pytest_ini
|
||||||
|
|
||||||
def test_toxini_before_lower_pytestini(self, pytester: Pytester) -> None:
|
def test_toxini_before_lower_pytestini(self, pytester: Pytester) -> None:
|
||||||
sub = pytester.mkdir("sub")
|
sub = pytester.mkdir("sub")
|
||||||
sub.joinpath("tox.ini").write_text(
|
sub.joinpath("tox.ini").write_text(
|
||||||
|
@ -1874,6 +1904,18 @@ class TestOverrideIniArgs:
|
||||||
assert "ERROR:" not in result.stderr.str()
|
assert "ERROR:" not in result.stderr.str()
|
||||||
result.stdout.fnmatch_lines(["collected 1 item", "*= 1 passed in *="])
|
result.stdout.fnmatch_lines(["collected 1 item", "*= 1 passed in *="])
|
||||||
|
|
||||||
|
def test_override_ini_without_config_file(self, pytester: Pytester) -> None:
|
||||||
|
pytester.makepyfile(**{"src/override_ini_without_config_file.py": ""})
|
||||||
|
pytester.makepyfile(
|
||||||
|
**{
|
||||||
|
"tests/test_override_ini_without_config_file.py": (
|
||||||
|
"import override_ini_without_config_file\ndef test(): pass"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = pytester.runpytest("--override-ini", "pythonpath=src")
|
||||||
|
assert result.parseoutcomes() == {"passed": 1}
|
||||||
|
|
||||||
|
|
||||||
def test_help_via_addopts(pytester: Pytester) -> None:
|
def test_help_via_addopts(pytester: Pytester) -> None:
|
||||||
pytester.makeini(
|
pytester.makeini(
|
||||||
|
|
|
@ -878,6 +878,25 @@ class TestDoctests:
|
||||||
result = pytester.runpytest(p, "--doctest-modules")
|
result = pytester.runpytest(p, "--doctest-modules")
|
||||||
result.stdout.fnmatch_lines(["*collected 1 item*"])
|
result.stdout.fnmatch_lines(["*collected 1 item*"])
|
||||||
|
|
||||||
|
def test_setup_module(self, pytester: Pytester) -> None:
|
||||||
|
"""Regression test for #12011 - setup_module not executed when running
|
||||||
|
with `--doctest-modules`."""
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
CONSTANT = 0
|
||||||
|
|
||||||
|
def setup_module():
|
||||||
|
global CONSTANT
|
||||||
|
CONSTANT = 1
|
||||||
|
|
||||||
|
def test():
|
||||||
|
assert CONSTANT == 1
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest("--doctest-modules")
|
||||||
|
assert result.ret == 0
|
||||||
|
result.assert_outcomes(passed=1)
|
||||||
|
|
||||||
|
|
||||||
class TestLiterals:
|
class TestLiterals:
|
||||||
@pytest.mark.parametrize("config_mode", ["ini", "comment"])
|
@pytest.mark.parametrize("config_mode", ["ini", "comment"])
|
||||||
|
|
|
@ -3,11 +3,13 @@ import sys
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
from typing import Union
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from _pytest.pytester import Pytester
|
|
||||||
from _pytest.recwarn import WarningsRecorder
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from pytest import ExitCode
|
||||||
|
from pytest import Pytester
|
||||||
|
from pytest import WarningsRecorder
|
||||||
|
|
||||||
|
|
||||||
def test_recwarn_stacklevel(recwarn: WarningsRecorder) -> None:
|
def test_recwarn_stacklevel(recwarn: WarningsRecorder) -> None:
|
||||||
|
@ -479,28 +481,117 @@ class TestWarns:
|
||||||
warnings.warn("some warning", category=FutureWarning)
|
warnings.warn("some warning", category=FutureWarning)
|
||||||
raise ValueError("some exception")
|
raise ValueError("some exception")
|
||||||
|
|
||||||
|
def test_skip_within_warns(self, pytester: Pytester) -> None:
|
||||||
|
"""Regression test for #11907."""
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
def test_raise_type_error_on_non_string_warning() -> None:
|
def test_it():
|
||||||
"""Check pytest.warns validates warning messages are strings (#10865)."""
|
with pytest.warns(Warning):
|
||||||
with pytest.raises(TypeError, match="Warning message must be str"):
|
pytest.skip("this is OK")
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = pytester.runpytest()
|
||||||
|
assert result.ret == ExitCode.OK
|
||||||
|
result.assert_outcomes(skipped=1)
|
||||||
|
|
||||||
|
def test_fail_within_warns(self, pytester: Pytester) -> None:
|
||||||
|
"""Regression test for #11907."""
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
def test_it():
|
||||||
|
with pytest.warns(Warning):
|
||||||
|
pytest.fail("BOOM")
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = pytester.runpytest()
|
||||||
|
assert result.ret == ExitCode.TESTS_FAILED
|
||||||
|
result.assert_outcomes(failed=1)
|
||||||
|
assert "DID NOT WARN" not in str(result.stdout)
|
||||||
|
|
||||||
|
def test_exit_within_warns(self, pytester: Pytester) -> None:
|
||||||
|
"""Regression test for #11907."""
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
def test_it():
|
||||||
|
with pytest.warns(Warning):
|
||||||
|
pytest.exit()
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = pytester.runpytest()
|
||||||
|
assert result.ret == ExitCode.INTERRUPTED
|
||||||
|
result.assert_outcomes()
|
||||||
|
|
||||||
|
def test_keyboard_interrupt_within_warns(self, pytester: Pytester) -> None:
|
||||||
|
"""Regression test for #11907."""
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
def test_it():
|
||||||
|
with pytest.warns(Warning):
|
||||||
|
raise KeyboardInterrupt()
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = pytester.runpytest_subprocess()
|
||||||
|
assert result.ret == ExitCode.INTERRUPTED
|
||||||
|
result.assert_outcomes()
|
||||||
|
|
||||||
|
|
||||||
|
def test_raise_type_error_on_invalid_warning() -> None:
|
||||||
|
"""Check pytest.warns validates warning messages are strings (#10865) or
|
||||||
|
Warning instances (#11959)."""
|
||||||
|
with pytest.raises(TypeError, match="Warning must be str or Warning"):
|
||||||
with pytest.warns(UserWarning):
|
with pytest.warns(UserWarning):
|
||||||
warnings.warn(1) # type: ignore
|
warnings.warn(1) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def test_no_raise_type_error_on_string_warning() -> None:
|
@pytest.mark.parametrize(
|
||||||
"""Check pytest.warns validates warning messages are strings (#10865)."""
|
"message",
|
||||||
with pytest.warns(UserWarning):
|
[
|
||||||
warnings.warn("Warning")
|
pytest.param("Warning", id="str"),
|
||||||
|
pytest.param(UserWarning(), id="UserWarning"),
|
||||||
|
pytest.param(Warning(), id="Warning"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_no_raise_type_error_on_valid_warning(message: Union[str, Warning]) -> None:
|
||||||
|
"""Check pytest.warns validates warning messages are strings (#10865) or
|
||||||
|
Warning instances (#11959)."""
|
||||||
|
with pytest.warns(Warning):
|
||||||
|
warnings.warn(message)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
hasattr(sys, "pypy_version_info"),
|
hasattr(sys, "pypy_version_info"),
|
||||||
reason="Not for pypy",
|
reason="Not for pypy",
|
||||||
)
|
)
|
||||||
def test_raise_type_error_on_non_string_warning_cpython() -> None:
|
def test_raise_type_error_on_invalid_warning_message_cpython() -> None:
|
||||||
# Check that we get the same behavior with the stdlib, at least if filtering
|
# Check that we get the same behavior with the stdlib, at least if filtering
|
||||||
# (see https://github.com/python/cpython/issues/103577 for details)
|
# (see https://github.com/python/cpython/issues/103577 for details)
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
warnings.filterwarnings("ignore", "test")
|
warnings.filterwarnings("ignore", "test")
|
||||||
warnings.warn(1) # type: ignore
|
warnings.warn(1) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiple_arg_custom_warning() -> None:
|
||||||
|
"""Test for issue #11906."""
|
||||||
|
|
||||||
|
class CustomWarning(UserWarning):
|
||||||
|
def __init__(self, a, b):
|
||||||
|
pass
|
||||||
|
|
||||||
|
with pytest.warns(CustomWarning):
|
||||||
|
with pytest.raises(pytest.fail.Exception, match="DID NOT WARN"):
|
||||||
|
with pytest.warns(CustomWarning, match="not gonna match"):
|
||||||
|
a, b = 1, 2
|
||||||
|
warnings.warn(CustomWarning(a, b))
|
||||||
|
|
Loading…
Reference in New Issue