Merge branch 'main' into teardown_fixture_order
This commit is contained in:
commit
fbe15ca8be
|
@ -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
|
||||
|
|
|
@ -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,6 +1,6 @@
|
|||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: "v0.2.1"
|
||||
rev: "v0.2.2"
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: ["--fix"]
|
||||
|
|
2
AUTHORS
2
AUTHORS
|
@ -56,6 +56,7 @@ Babak Keyvani
|
|||
Barney Gale
|
||||
Ben Brown
|
||||
Ben Gartner
|
||||
Ben Leith
|
||||
Ben Webb
|
||||
Benjamin Peterson
|
||||
Benjamin Schubert
|
||||
|
@ -127,6 +128,7 @@ Edison Gustavo Muenz
|
|||
Edoardo Batini
|
||||
Edson Tadeu M. Manoel
|
||||
Eduardo Schettino
|
||||
Eero Vaher
|
||||
Eli Boyarski
|
||||
Elizaveta Shashkova
|
||||
Éloi Rivard
|
||||
|
|
|
@ -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,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.
|
||||
|
|
|
@ -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 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,7 @@ Release announcements
|
|||
:maxdepth: 2
|
||||
|
||||
|
||||
release-8.0.1
|
||||
release-8.0.0
|
||||
release-8.0.0rc2
|
||||
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
|
||||
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:284
|
||||
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:307
|
||||
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:345
|
||||
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.
|
||||
|
@ -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,30 @@ 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)
|
||||
=========================
|
||||
|
||||
|
|
|
@ -503,10 +503,10 @@ Running it results in some skips if we don't have all the python interpreters in
|
|||
.. code-block:: pytest
|
||||
|
||||
. $ pytest -rs -q multipython.py
|
||||
ssssssssssssssssssssssss... [100%]
|
||||
ssssssssssss...ssssssssssss [100%]
|
||||
========================= short test summary info ==========================
|
||||
SKIPPED [12] multipython.py:68: 'python3.9' not found
|
||||
SKIPPED [12] multipython.py:68: 'python3.10' 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
|
||||
|
|
|
@ -22,7 +22,7 @@ Install ``pytest``
|
|||
.. code-block:: bash
|
||||
|
||||
$ pytest --version
|
||||
pytest 8.0.0
|
||||
pytest 8.0.1
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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
|
@ -2028,7 +2028,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
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
|
@ -109,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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -197,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)):
|
||||
|
|
|
@ -289,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 in {"win32", "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
|
||||
|
|
|
@ -578,12 +578,18 @@ class PytestPluginManager(PluginManager):
|
|||
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(
|
||||
|
@ -609,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):
|
||||
|
@ -1563,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":
|
||||
|
|
|
@ -198,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.
|
||||
|
|
|
@ -101,15 +101,20 @@ def locate_config(
|
|||
args = [x for x in args if not str(x).startswith("-")]
|
||||
if not args:
|
||||
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, {}
|
||||
|
||||
|
||||
|
|
|
@ -47,6 +47,7 @@ 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"
|
||||
|
@ -133,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
|
||||
|
||||
|
||||
|
@ -272,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)
|
||||
|
|
|
@ -169,33 +169,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.
|
||||
|
|
|
@ -298,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",
|
||||
|
@ -669,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"
|
||||
|
@ -746,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()
|
||||
|
|
|
@ -21,6 +21,7 @@ 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
|
||||
|
||||
|
@ -49,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",
|
||||
|
@ -491,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
|
||||
|
@ -901,6 +906,10 @@ class Session(nodes.Collector):
|
|||
# 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
|
||||
|
|
|
@ -355,7 +355,7 @@ class MarkDecorator:
|
|||
func = args[0]
|
||||
is_class = inspect.isclass(func)
|
||||
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 self.with_args(*args, **kwargs)
|
||||
|
||||
|
@ -410,7 +410,7 @@ def normalize_mark_list(
|
|||
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.
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
# was only borrowed.
|
||||
|
|
|
@ -11,6 +11,7 @@ from typing import Iterable
|
|||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import MutableMapping
|
||||
from typing import NoReturn
|
||||
from typing import Optional
|
||||
from typing import overload
|
||||
from typing import Set
|
||||
|
@ -41,6 +42,8 @@ from _pytest.warning_types import PytestWarning
|
|||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Self
|
||||
|
||||
# Imported here due to circular import.
|
||||
from _pytest._code.code import _TracebackStyle
|
||||
from _pytest.main import Session
|
||||
|
@ -51,6 +54,7 @@ SEP = "/"
|
|||
tracebackcutdir = Path(_pytest.__file__).parent
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_NodeType = TypeVar("_NodeType", bound="Node")
|
||||
|
||||
|
||||
|
@ -69,33 +73,33 @@ class NodeMeta(abc.ABCMeta):
|
|||
progress on detangling the :class:`Node` classes.
|
||||
"""
|
||||
|
||||
def __call__(self, *k, **kw):
|
||||
def __call__(cls, *k, **kw) -> NoReturn:
|
||||
msg = (
|
||||
"Direct construction of {name} has been deprecated, please use {name}.from_parent.\n"
|
||||
"See "
|
||||
"https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent"
|
||||
" for more details."
|
||||
).format(name=f"{self.__module__}.{self.__name__}")
|
||||
).format(name=f"{cls.__module__}.{cls.__name__}")
|
||||
fail(msg, pytrace=False)
|
||||
|
||||
def _create(self, *k, **kw):
|
||||
def _create(cls: Type[_T], *k, **kw) -> _T:
|
||||
try:
|
||||
return super().__call__(*k, **kw)
|
||||
return super().__call__(*k, **kw) # type: ignore[no-any-return,misc]
|
||||
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}
|
||||
from .warning_types import PytestDeprecationWarning
|
||||
|
||||
warnings.warn(
|
||||
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"
|
||||
"#constructors-of-custom-pytest-node-subclasses-should-take-kwargs "
|
||||
"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):
|
||||
|
@ -181,7 +185,7 @@ class Node(abc.ABC, metaclass=NodeMeta):
|
|||
self._store = self.stash
|
||||
|
||||
@classmethod
|
||||
def from_parent(cls, parent: "Node", **kw):
|
||||
def from_parent(cls, parent: "Node", **kw) -> "Self":
|
||||
"""Public constructor for Nodes.
|
||||
|
||||
This indirection got introduced in order to enable removing
|
||||
|
@ -583,7 +587,7 @@ class FSCollector(Collector, abc.ABC):
|
|||
*,
|
||||
path: Optional[Path] = None,
|
||||
**kw,
|
||||
):
|
||||
) -> "Self":
|
||||
"""The public constructor."""
|
||||
return super().from_parent(parent=parent, path=path, **kw)
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ from typing import Pattern
|
|||
from typing import Sequence
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
import warnings
|
||||
|
||||
|
@ -81,6 +82,10 @@ from _pytest.warning_types import PytestReturnNotNoneWarning
|
|||
from _pytest.warning_types import PytestUnhandledCoroutineWarning
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Self
|
||||
|
||||
|
||||
_PYTEST_DIR = Path(_pytest.__file__).parent
|
||||
|
||||
|
||||
|
@ -204,8 +209,7 @@ def pytest_collect_directory(
|
|||
) -> Optional[nodes.Collector]:
|
||||
pkginit = path / "__init__.py"
|
||||
if pkginit.is_file():
|
||||
pkg: Package = Package.from_parent(parent, path=path)
|
||||
return pkg
|
||||
return Package.from_parent(parent, path=path)
|
||||
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":
|
||||
mod: Module = Module.from_parent(parent, path=module_path)
|
||||
return mod
|
||||
return Module.from_parent(parent, path=module_path)
|
||||
|
||||
|
||||
@hookimpl(trylast=True)
|
||||
|
@ -242,8 +245,7 @@ def pytest_pycollect_makeitem(
|
|||
# Nothing was collected elsewhere, let's do it here.
|
||||
if safe_isclass(obj):
|
||||
if collector.istestclass(obj, name):
|
||||
klass: Class = Class.from_parent(collector, name=name, obj=obj)
|
||||
return klass
|
||||
return Class.from_parent(collector, name=name, obj=obj)
|
||||
elif collector.istestfunction(obj, name):
|
||||
# mock seems to store unbound methods (issue473), normalize it.
|
||||
obj = getattr(obj, "__func__", obj)
|
||||
|
@ -262,7 +264,7 @@ def pytest_pycollect_makeitem(
|
|||
)
|
||||
elif getattr(obj, "__test__", True):
|
||||
if is_generator(obj):
|
||||
res: Function = Function.from_parent(collector, name=name)
|
||||
res = Function.from_parent(collector, name=name)
|
||||
reason = (
|
||||
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)
|
||||
cls = clscol and clscol.obj or None
|
||||
|
||||
definition: FunctionDefinition = FunctionDefinition.from_parent(
|
||||
self, name=name, callobj=funcobj
|
||||
)
|
||||
definition = FunctionDefinition.from_parent(self, name=name, callobj=funcobj)
|
||||
fixtureinfo = definition._fixtureinfo
|
||||
|
||||
# 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."""
|
||||
|
||||
@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."""
|
||||
return super().from_parent(name=name, parent=parent, **kw)
|
||||
|
||||
|
@ -1267,7 +1267,6 @@ class Metafunc:
|
|||
# Add funcargs as fixturedefs to fixtureinfo.arg2fixturedefs by registering
|
||||
# artificial "pseudo" FixtureDef's so that later at test execution time we can
|
||||
# rely on a proper FixtureDef to exist for fixture setup.
|
||||
arg2fixturedefs = self._arg2fixturedefs
|
||||
node = None
|
||||
# If we have a scope that is higher than function, we need
|
||||
# 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
|
||||
# collector (for now).
|
||||
if scope_ is Scope.Class:
|
||||
assert isinstance(collector, _pytest.python.Module)
|
||||
assert isinstance(collector, Module)
|
||||
node = collector
|
||||
# If used package scope and there is no package, use session
|
||||
# (for now).
|
||||
|
@ -1316,7 +1315,7 @@ class Metafunc:
|
|||
)
|
||||
if name2pseudofixturedef is not None:
|
||||
name2pseudofixturedef[argname] = fixturedef
|
||||
arg2fixturedefs[argname] = [fixturedef]
|
||||
self._arg2fixturedefs[argname] = [fixturedef]
|
||||
|
||||
# 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
|
||||
|
@ -1731,8 +1730,9 @@ class Function(PyobjMixin, nodes.Item):
|
|||
self.fixturenames = fixtureinfo.names_closure
|
||||
self._initrequest()
|
||||
|
||||
# todo: determine sound type limitations
|
||||
@classmethod
|
||||
def from_parent(cls, parent, **kw): # todo: determine sound type limitations
|
||||
def from_parent(cls, parent, **kw) -> "Self":
|
||||
"""The public constructor."""
|
||||
return super().from_parent(parent=parent, **kw)
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import warnings
|
|||
|
||||
from _pytest.deprecated import check_ispytest
|
||||
from _pytest.fixtures import fixture
|
||||
from _pytest.outcomes import Exit
|
||||
from _pytest.outcomes import fail
|
||||
|
||||
|
||||
|
@ -302,7 +303,18 @@ class WarningsChecker(WarningsRecorder):
|
|||
|
||||
__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)
|
||||
|
||||
try:
|
||||
|
@ -322,21 +334,37 @@ class WarningsChecker(WarningsRecorder):
|
|||
for w in self:
|
||||
if not self.matches(w):
|
||||
warnings.warn_explicit(
|
||||
str(w.message),
|
||||
w.message.__class__, # type: ignore[arg-type]
|
||||
w.filename,
|
||||
w.lineno,
|
||||
message=w.message,
|
||||
category=w.category,
|
||||
filename=w.filename,
|
||||
lineno=w.lineno,
|
||||
module=w.__module__,
|
||||
source=w.source,
|
||||
)
|
||||
# Check warnings has valid argument type (#10865).
|
||||
wrn: warnings.WarningMessage
|
||||
for wrn in self:
|
||||
self._validate_message(wrn)
|
||||
|
||||
@staticmethod
|
||||
def _validate_message(wrn: Any) -> None:
|
||||
if not isinstance(msg := wrn.message.args[0], str):
|
||||
raise TypeError(
|
||||
f"Warning message must be str, got {msg!r} (type {type(msg).__name__})"
|
||||
)
|
||||
# Currently in Python it is possible to pass other types than an
|
||||
# `str` message when creating `Warning` instances, however this
|
||||
# causes an exception when :func:`warnings.filterwarnings` is used
|
||||
# to filter those warnings. See
|
||||
# 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:
|
||||
return None
|
||||
# Yes, so let's collect it.
|
||||
item: UnitTestCase = UnitTestCase.from_parent(collector, name=name, obj=obj)
|
||||
return item
|
||||
return UnitTestCase.from_parent(collector, name=name, obj=obj)
|
||||
|
||||
|
||||
class UnitTestCase(Class):
|
||||
|
|
|
@ -118,6 +118,8 @@ def test_fixture_disallow_marks_on_fixtures():
|
|||
raise NotImplementedError()
|
||||
|
||||
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():
|
||||
|
|
|
@ -661,6 +661,73 @@ def test_log_file_cli(pytester: Pytester) -> None:
|
|||
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:
|
||||
# Default log file level
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
"""Test that the level names of captured log messages of a failing test
|
||||
are colored."""
|
||||
|
|
|
@ -2730,12 +2730,12 @@ class TestFixtureMarker:
|
|||
"""
|
||||
test_dynamic_parametrized_ordering.py::test[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::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 shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import textwrap
|
||||
from typing import List
|
||||
|
||||
from _pytest.assertion.util import running_on_ci
|
||||
from _pytest.config import ExitCode
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
from _pytest.main import _in_venv
|
||||
|
@ -1613,7 +1615,7 @@ def test_fscollector_from_parent(pytester: Pytester, request: FixtureRequest) ->
|
|||
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."""
|
||||
|
||||
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.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"
|
||||
|
||||
def test_pyproject_toml(self, pytester: Pytester) -> None:
|
||||
pytester.makepyprojecttoml(
|
||||
pyproject_toml = pytester.makepyprojecttoml(
|
||||
"""
|
||||
[tool.pytest.ini_options]
|
||||
minversion = "1.0"
|
||||
"""
|
||||
)
|
||||
config = pytester.parseconfig()
|
||||
assert config.inipath == pyproject_toml
|
||||
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:
|
||||
sub = pytester.mkdir("sub")
|
||||
sub.joinpath("tox.ini").write_text(
|
||||
|
@ -1874,6 +1904,18 @@ class TestOverrideIniArgs:
|
|||
assert "ERROR:" not in result.stderr.str()
|
||||
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:
|
||||
pytester.makeini(
|
||||
|
|
|
@ -878,6 +878,25 @@ class TestDoctests:
|
|||
result = pytester.runpytest(p, "--doctest-modules")
|
||||
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:
|
||||
@pytest.mark.parametrize("config_mode", ["ini", "comment"])
|
||||
|
|
|
@ -3,11 +3,13 @@ import sys
|
|||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Type
|
||||
from typing import Union
|
||||
import warnings
|
||||
|
||||
from _pytest.pytester import Pytester
|
||||
from _pytest.recwarn import WarningsRecorder
|
||||
import pytest
|
||||
from pytest import ExitCode
|
||||
from pytest import Pytester
|
||||
from pytest import WarningsRecorder
|
||||
|
||||
|
||||
def test_recwarn_stacklevel(recwarn: WarningsRecorder) -> None:
|
||||
|
@ -479,28 +481,117 @@ class TestWarns:
|
|||
warnings.warn("some warning", category=FutureWarning)
|
||||
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:
|
||||
"""Check pytest.warns validates warning messages are strings (#10865)."""
|
||||
with pytest.raises(TypeError, match="Warning message must be str"):
|
||||
def test_it():
|
||||
with pytest.warns(Warning):
|
||||
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):
|
||||
warnings.warn(1) # type: ignore
|
||||
|
||||
|
||||
def test_no_raise_type_error_on_string_warning() -> None:
|
||||
"""Check pytest.warns validates warning messages are strings (#10865)."""
|
||||
with pytest.warns(UserWarning):
|
||||
warnings.warn("Warning")
|
||||
@pytest.mark.parametrize(
|
||||
"message",
|
||||
[
|
||||
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(
|
||||
hasattr(sys, "pypy_version_info"),
|
||||
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
|
||||
# (see https://github.com/python/cpython/issues/103577 for details)
|
||||
with pytest.raises(TypeError):
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore", "test")
|
||||
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