Merge branch 'main' into teardown_fixture_order

This commit is contained in:
John Litborn 2024-02-23 12:06:06 +01:00 committed by GitHub
commit fbe15ca8be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 939 additions and 249 deletions

View File

@ -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

View File

@ -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>'

View File

@ -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"]

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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`.

View File

@ -1 +0,0 @@
Correctly handle errors from :func:`getpass.getuser` in Python 3.13.

View File

@ -1 +0,0 @@
Fix an edge case where ``ExceptionInfo._stringify_exception`` could crash :func:`pytest.raises`.

View File

@ -0,0 +1 @@
Fix collection on Windows where initial paths contain the short version of a path (for example ``c:\PROGRA~1\tests``).

View File

@ -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.

View File

@ -1 +0,0 @@
Fix a regression in pytest 8.0.0 whereby items would be collected in reverse order in some circumstances.

View File

@ -0,0 +1 @@
Fix an ``IndexError`` crash raising from ``getstatementrange_ast``.

View File

@ -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``.

View File

@ -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.

View File

@ -0,0 +1 @@
Fixed a regression in 8.0.1 whereby ``setup_module`` xunit-style fixtures are not executed when ``--doctest-modules`` is passed.

View File

@ -0,0 +1 @@
Fix the ``stacklevel`` used when warning about marks used on fixtures.

View File

@ -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 %}

View File

@ -6,6 +6,7 @@ Release announcements
:maxdepth: 2
release-8.0.1
release-8.0.0
release-8.0.0rc2
release-8.0.0rc1

View File

@ -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

View File

@ -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.

View File

@ -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)
=========================

View File

@ -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

View File

@ -22,7 +22,7 @@ Install ``pytest``
.. code-block:: bash
$ pytest --version
pytest 8.0.0
pytest 8.0.1
.. _`simpletest`:

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
)
}

View File

@ -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)):

View File

@ -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

View File

@ -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":

View File

@ -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.

View File

@ -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, {}

View File

@ -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)

View File

@ -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.

View File

@ -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()

View File

@ -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

View File

@ -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.

View File

@ -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)

View File

@ -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)

View File

@ -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__})"
)

View File

@ -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):

View File

@ -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():

View File

@ -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."""

View File

@ -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
"""
)

View File

@ -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}

View File

@ -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(

View File

@ -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"])

View File

@ -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))