Merge branch 'pytest-dev:main' into main
This commit is contained in:
commit
0d699188ac
1
AUTHORS
1
AUTHORS
|
@ -101,6 +101,7 @@ Cyrus Maden
|
|||
Damian Skrzypczak
|
||||
Daniel Grana
|
||||
Daniel Hahler
|
||||
Daniel Miller
|
||||
Daniel Nuri
|
||||
Daniel Sánchez Castelló
|
||||
Daniel Valenzuela Zenteno
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
:func:`pytest.importorskip` will now issue a warning if the module could be found, but raised :class:`ImportError` instead of :class:`ModuleNotFoundError`.
|
||||
|
||||
The warning can be suppressed by passing ``exc_type=ImportError`` to :func:`pytest.importorskip`.
|
||||
|
||||
See :ref:`import-or-skip-import-error` for details.
|
|
@ -0,0 +1 @@
|
|||
For ``unittest``-based tests, exceptions during class cleanup (as raised by functions registered with :meth:`TestCase.addClassCleanup <unittest.TestCase.addClassCleanup>`) are now reported instead of silently failing.
|
|
@ -1 +0,0 @@
|
|||
Fixed attribute error in pytest.approx for types implicitly convertible to numpy arrays by converting other_side to a numpy array so that np_array_shape != other_side.shape can be properly checked.
|
|
@ -6,6 +6,7 @@ Release announcements
|
|||
:maxdepth: 2
|
||||
|
||||
|
||||
release-8.1.2
|
||||
release-8.1.1
|
||||
release-8.1.0
|
||||
release-8.0.2
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
pytest-8.1.2
|
||||
=======================================
|
||||
|
||||
pytest 8.1.2 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Bruno Oliveira
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
|
@ -28,6 +28,15 @@ with advance notice in the **Deprecations** section of releases.
|
|||
|
||||
.. towncrier release notes start
|
||||
|
||||
pytest 8.1.2 (2024-04-26)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#12114 <https://github.com/pytest-dev/pytest/issues/12114>`_: Fixed error in :func:`pytest.approx` when used with `numpy` arrays and comparing with other types.
|
||||
|
||||
|
||||
pytest 8.1.1 (2024-03-08)
|
||||
=========================
|
||||
|
||||
|
|
|
@ -19,6 +19,41 @@ Below is a complete list of all pytest features which are considered deprecated.
|
|||
:class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
|
||||
|
||||
|
||||
.. _import-or-skip-import-error:
|
||||
|
||||
``pytest.importorskip`` default behavior regarding :class:`ImportError`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 8.2
|
||||
|
||||
Traditionally :func:`pytest.importorskip` will capture :class:`ImportError`, with the original intent being to skip
|
||||
tests where a dependent module is not installed, for example testing with different dependencies.
|
||||
|
||||
However some packages might be installed in the system, but are not importable due to
|
||||
some other issue, for example, a compilation error or a broken installation. In those cases :func:`pytest.importorskip`
|
||||
would still silently skip the test, but more often than not users would like to see the unexpected
|
||||
error so the underlying issue can be fixed.
|
||||
|
||||
In ``8.2`` the ``exc_type`` parameter has been added, giving users the ability of passing :class:`ModuleNotFoundError`
|
||||
to skip tests only if the module cannot really be found, and not because of some other error.
|
||||
|
||||
Catching only :class:`ModuleNotFoundError` by default (and letting other errors propagate) would be the best solution,
|
||||
however for backward compatibility, pytest will keep the existing behavior but raise an warning if:
|
||||
|
||||
1. The captured exception is of type :class:`ImportError`, and:
|
||||
2. The user does not pass ``exc_type`` explicitly.
|
||||
|
||||
If the import attempt raises :class:`ModuleNotFoundError` (the usual case), then the module is skipped and no
|
||||
warning is emitted.
|
||||
|
||||
This way, the usual cases will keep working the same way, while unexpected errors will now issue a warning, with
|
||||
users being able to supress the warning by passing ``exc_type=ImportError`` explicitly.
|
||||
|
||||
In ``9.0``, the warning will turn into an error, and in ``9.1`` :func:`pytest.importorskip` will only capture
|
||||
:class:`ModuleNotFoundError` by default and no warnings will be issued anymore -- but users can still capture
|
||||
:class:`ImportError` by passing it to ``exc_type``.
|
||||
|
||||
|
||||
.. _node-ctor-fspath-deprecation:
|
||||
|
||||
``fspath`` argument for Node constructors replaced with ``pathlib.Path``
|
||||
|
|
|
@ -162,7 +162,7 @@ objects, they are still using the default pytest representation:
|
|||
rootdir: /home/sweet/project
|
||||
collected 8 items
|
||||
|
||||
<Dir parametrize.rst-196>
|
||||
<Dir parametrize.rst-197>
|
||||
<Module test_time.py>
|
||||
<Function test_timedistance_v0[a0-b0-expected0]>
|
||||
<Function test_timedistance_v0[a1-b1-expected1]>
|
||||
|
@ -239,7 +239,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia
|
|||
rootdir: /home/sweet/project
|
||||
collected 4 items
|
||||
|
||||
<Dir parametrize.rst-196>
|
||||
<Dir parametrize.rst-197>
|
||||
<Module test_scenarios.py>
|
||||
<Class TestSampleWithScenarios>
|
||||
<Function test_demo1[basic]>
|
||||
|
@ -318,7 +318,7 @@ Let's first see how it looks like at collection time:
|
|||
rootdir: /home/sweet/project
|
||||
collected 2 items
|
||||
|
||||
<Dir parametrize.rst-196>
|
||||
<Dir parametrize.rst-197>
|
||||
<Module test_backends.py>
|
||||
<Function test_db_initialized[d1]>
|
||||
<Function test_db_initialized[d2]>
|
||||
|
|
|
@ -152,7 +152,7 @@ The test collection would look like this:
|
|||
configfile: pytest.ini
|
||||
collected 2 items
|
||||
|
||||
<Dir pythoncollection.rst-197>
|
||||
<Dir pythoncollection.rst-198>
|
||||
<Module check_myapp.py>
|
||||
<Class CheckMyApp>
|
||||
<Function simple_check>
|
||||
|
@ -215,7 +215,7 @@ You can always peek at the collection tree without running tests like this:
|
|||
configfile: pytest.ini
|
||||
collected 3 items
|
||||
|
||||
<Dir pythoncollection.rst-197>
|
||||
<Dir pythoncollection.rst-198>
|
||||
<Dir CWD>
|
||||
<Module pythoncollection.py>
|
||||
<Function test_function>
|
||||
|
|
|
@ -22,7 +22,7 @@ Install ``pytest``
|
|||
.. code-block:: bash
|
||||
|
||||
$ pytest --version
|
||||
pytest 8.1.1
|
||||
pytest 8.1.2
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
|
|
|
@ -1418,7 +1418,7 @@ Running the above tests results in the following test IDs being used:
|
|||
rootdir: /home/sweet/project
|
||||
collected 12 items
|
||||
|
||||
<Dir fixtures.rst-215>
|
||||
<Dir fixtures.rst-216>
|
||||
<Module test_anothersmtp.py>
|
||||
<Function test_showhelo[smtp.gmail.com]>
|
||||
<Function test_showhelo[mail.python.org]>
|
||||
|
|
|
@ -52,7 +52,7 @@ from _pytest.pathlib import absolutepath
|
|||
from _pytest.pathlib import bestrelpath
|
||||
|
||||
|
||||
if sys.version_info[:2] < (3, 11):
|
||||
if sys.version_info < (3, 11):
|
||||
from exceptiongroup import BaseExceptionGroup
|
||||
|
||||
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
|
||||
|
@ -703,7 +703,7 @@ class ExceptionInfo(Generic[E]):
|
|||
# Workaround for https://github.com/python/cpython/issues/98778 on
|
||||
# Python <= 3.9, and some 3.10 and 3.11 patch versions.
|
||||
HTTPError = getattr(sys.modules.get("urllib.error", None), "HTTPError", ())
|
||||
if sys.version_info[:2] <= (3, 11) and isinstance(exc, HTTPError):
|
||||
if sys.version_info < (3, 12) and isinstance(exc, HTTPError):
|
||||
notes = []
|
||||
else:
|
||||
raise
|
||||
|
|
|
@ -448,7 +448,7 @@ class MyOptionParser(argparse.ArgumentParser):
|
|||
getattr(parsed, FILE_OR_DIR).extend(unrecognized)
|
||||
return parsed
|
||||
|
||||
if sys.version_info[:2] < (3, 9): # pragma: no cover
|
||||
if sys.version_info < (3, 9): # pragma: no cover
|
||||
# Backport of https://github.com/python/cpython/pull/14316 so we can
|
||||
# disable long --argument abbreviations without breaking short flags.
|
||||
def _parse_optional(
|
||||
|
|
|
@ -35,6 +35,7 @@ import warnings
|
|||
import _pytest
|
||||
from _pytest import nodes
|
||||
from _pytest._code import getfslineno
|
||||
from _pytest._code import Source
|
||||
from _pytest._code.code import FormattedExcinfo
|
||||
from _pytest._code.code import TerminalRepr
|
||||
from _pytest._io import TerminalWriter
|
||||
|
@ -69,7 +70,7 @@ from _pytest.scope import HIGH_SCOPES
|
|||
from _pytest.scope import Scope
|
||||
|
||||
|
||||
if sys.version_info[:2] < (3, 11):
|
||||
if sys.version_info < (3, 11):
|
||||
from exceptiongroup import BaseExceptionGroup
|
||||
|
||||
|
||||
|
@ -410,41 +411,6 @@ class FixtureRequest(abc.ABC):
|
|||
"""Underlying collection node (depends on current request scope)."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
|
||||
fixturedefs = self._arg2fixturedefs.get(argname, None)
|
||||
if fixturedefs is None:
|
||||
# We arrive here because of a dynamic call to
|
||||
# getfixturevalue(argname) usage which was naturally
|
||||
# not known at parsing/collection time.
|
||||
fixturedefs = self._fixturemanager.getfixturedefs(argname, self._pyfuncitem)
|
||||
if fixturedefs is not None:
|
||||
self._arg2fixturedefs[argname] = fixturedefs
|
||||
# No fixtures defined with this name.
|
||||
if fixturedefs is None:
|
||||
raise FixtureLookupError(argname, self)
|
||||
# The are no fixtures with this name applicable for the function.
|
||||
if not fixturedefs:
|
||||
raise FixtureLookupError(argname, self)
|
||||
|
||||
# A fixture may override another fixture with the same name, e.g. a
|
||||
# fixture in a module can override a fixture in a conftest, a fixture in
|
||||
# a class can override a fixture in the module, and so on.
|
||||
# An overriding fixture can request its own name (possibly indirectly);
|
||||
# in this case it gets the value of the fixture it overrides, one level
|
||||
# up.
|
||||
# Check how many `argname`s deep we are, and take the next one.
|
||||
# `fixturedefs` is sorted from furthest to closest, so use negative
|
||||
# indexing to go in reverse.
|
||||
index = -1
|
||||
for request in self._iter_chain():
|
||||
if request.fixturename == argname:
|
||||
index -= 1
|
||||
# If already consumed all of the available levels, fail.
|
||||
if -index > len(fixturedefs):
|
||||
raise FixtureLookupError(argname, self)
|
||||
|
||||
return fixturedefs[index]
|
||||
|
||||
@property
|
||||
def config(self) -> Config:
|
||||
"""The pytest config object associated with this request."""
|
||||
|
@ -569,39 +535,53 @@ class FixtureRequest(abc.ABC):
|
|||
def _get_active_fixturedef(
|
||||
self, argname: str
|
||||
) -> Union["FixtureDef[object]", PseudoFixtureDef[object]]:
|
||||
if argname == "request":
|
||||
cached_result = (self, [0], None)
|
||||
return PseudoFixtureDef(cached_result, Scope.Function)
|
||||
|
||||
# If we already finished computing a fixture by this name in this item,
|
||||
# return it.
|
||||
fixturedef = self._fixture_defs.get(argname)
|
||||
if fixturedef is None:
|
||||
try:
|
||||
fixturedef = self._getnextfixturedef(argname)
|
||||
except FixtureLookupError:
|
||||
if argname == "request":
|
||||
cached_result = (self, [0], None)
|
||||
return PseudoFixtureDef(cached_result, Scope.Function)
|
||||
raise
|
||||
self._compute_fixture_value(fixturedef)
|
||||
self._fixture_defs[argname] = fixturedef
|
||||
else:
|
||||
if fixturedef is not None:
|
||||
self._check_scope(fixturedef, fixturedef._scope)
|
||||
return fixturedef
|
||||
return fixturedef
|
||||
|
||||
def _get_fixturestack(self) -> List["FixtureDef[Any]"]:
|
||||
values = [request._fixturedef for request in self._iter_chain()]
|
||||
values.reverse()
|
||||
return values
|
||||
# Find the appropriate fixturedef.
|
||||
fixturedefs = self._arg2fixturedefs.get(argname, None)
|
||||
if fixturedefs is None:
|
||||
# We arrive here because of a dynamic call to
|
||||
# getfixturevalue(argname) which was naturally
|
||||
# not known at parsing/collection time.
|
||||
fixturedefs = self._fixturemanager.getfixturedefs(argname, self._pyfuncitem)
|
||||
if fixturedefs is not None:
|
||||
self._arg2fixturedefs[argname] = fixturedefs
|
||||
# No fixtures defined with this name.
|
||||
if fixturedefs is None:
|
||||
raise FixtureLookupError(argname, self)
|
||||
# The are no fixtures with this name applicable for the function.
|
||||
if not fixturedefs:
|
||||
raise FixtureLookupError(argname, self)
|
||||
# A fixture may override another fixture with the same name, e.g. a
|
||||
# fixture in a module can override a fixture in a conftest, a fixture in
|
||||
# a class can override a fixture in the module, and so on.
|
||||
# An overriding fixture can request its own name (possibly indirectly);
|
||||
# in this case it gets the value of the fixture it overrides, one level
|
||||
# up.
|
||||
# Check how many `argname`s deep we are, and take the next one.
|
||||
# `fixturedefs` is sorted from furthest to closest, so use negative
|
||||
# indexing to go in reverse.
|
||||
index = -1
|
||||
for request in self._iter_chain():
|
||||
if request.fixturename == argname:
|
||||
index -= 1
|
||||
# If already consumed all of the available levels, fail.
|
||||
if -index > len(fixturedefs):
|
||||
raise FixtureLookupError(argname, self)
|
||||
fixturedef = fixturedefs[index]
|
||||
|
||||
def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None:
|
||||
"""Create a SubRequest based on "self" and call the execute method
|
||||
of the given FixtureDef object.
|
||||
|
||||
If the FixtureDef has cached the result it will do nothing, otherwise it will
|
||||
setup and run the fixture, cache the value, and schedule a finalizer for it.
|
||||
"""
|
||||
# prepare a subrequest object before calling fixture function
|
||||
# (latter managed by fixturedef)
|
||||
argname = fixturedef.argname
|
||||
funcitem = self._pyfuncitem
|
||||
# Prepare a SubRequest object for calling the fixture.
|
||||
try:
|
||||
callspec = funcitem.callspec
|
||||
callspec = self._pyfuncitem.callspec
|
||||
except AttributeError:
|
||||
callspec = None
|
||||
if callspec is not None and argname in callspec.params:
|
||||
|
@ -613,41 +593,8 @@ class FixtureRequest(abc.ABC):
|
|||
param = NOTSET
|
||||
param_index = 0
|
||||
scope = fixturedef._scope
|
||||
|
||||
has_params = fixturedef.params is not None
|
||||
fixtures_not_supported = getattr(funcitem, "nofuncargs", False)
|
||||
if has_params and fixtures_not_supported:
|
||||
msg = (
|
||||
f"{funcitem.name} does not support fixtures, maybe unittest.TestCase subclass?\n"
|
||||
f"Node id: {funcitem.nodeid}\n"
|
||||
f"Function type: {type(funcitem).__name__}"
|
||||
)
|
||||
fail(msg, pytrace=False)
|
||||
if has_params:
|
||||
frame = inspect.stack()[3]
|
||||
frameinfo = inspect.getframeinfo(frame[0])
|
||||
source_path = absolutepath(frameinfo.filename)
|
||||
source_lineno = frameinfo.lineno
|
||||
try:
|
||||
source_path_str = str(
|
||||
source_path.relative_to(funcitem.config.rootpath)
|
||||
)
|
||||
except ValueError:
|
||||
source_path_str = str(source_path)
|
||||
location = getlocation(fixturedef.func, funcitem.config.rootpath)
|
||||
msg = (
|
||||
"The requested fixture has no parameter defined for test:\n"
|
||||
f" {funcitem.nodeid}\n\n"
|
||||
f"Requested fixture '{fixturedef.argname}' defined in:\n"
|
||||
f"{location}\n\n"
|
||||
f"Requested here:\n"
|
||||
f"{source_path_str}:{source_lineno}"
|
||||
)
|
||||
fail(msg, pytrace=False)
|
||||
|
||||
# Check if a higher-level scoped fixture accesses a lower level one.
|
||||
self._check_fixturedef_without_param(fixturedef)
|
||||
self._check_scope(fixturedef, scope)
|
||||
|
||||
subrequest = SubRequest(
|
||||
self, scope, param, param_index, fixturedef, _ispytest=True
|
||||
)
|
||||
|
@ -655,6 +602,47 @@ class FixtureRequest(abc.ABC):
|
|||
# Make sure the fixture value is cached, running it if it isn't
|
||||
fixturedef.execute(request=subrequest)
|
||||
|
||||
self._fixture_defs[argname] = fixturedef
|
||||
return fixturedef
|
||||
|
||||
def _check_fixturedef_without_param(self, fixturedef: "FixtureDef[object]") -> None:
|
||||
"""Check that this request is allowed to execute this fixturedef without
|
||||
a param."""
|
||||
funcitem = self._pyfuncitem
|
||||
has_params = fixturedef.params is not None
|
||||
fixtures_not_supported = getattr(funcitem, "nofuncargs", False)
|
||||
if has_params and fixtures_not_supported:
|
||||
msg = (
|
||||
f"{funcitem.name} does not support fixtures, maybe unittest.TestCase subclass?\n"
|
||||
f"Node id: {funcitem.nodeid}\n"
|
||||
f"Function type: {type(funcitem).__name__}"
|
||||
)
|
||||
fail(msg, pytrace=False)
|
||||
if has_params:
|
||||
frame = inspect.stack()[3]
|
||||
frameinfo = inspect.getframeinfo(frame[0])
|
||||
source_path = absolutepath(frameinfo.filename)
|
||||
source_lineno = frameinfo.lineno
|
||||
try:
|
||||
source_path_str = str(source_path.relative_to(funcitem.config.rootpath))
|
||||
except ValueError:
|
||||
source_path_str = str(source_path)
|
||||
location = getlocation(fixturedef.func, funcitem.config.rootpath)
|
||||
msg = (
|
||||
"The requested fixture has no parameter defined for test:\n"
|
||||
f" {funcitem.nodeid}\n\n"
|
||||
f"Requested fixture '{fixturedef.argname}' defined in:\n"
|
||||
f"{location}\n\n"
|
||||
f"Requested here:\n"
|
||||
f"{source_path_str}:{source_lineno}"
|
||||
)
|
||||
fail(msg, pytrace=False)
|
||||
|
||||
def _get_fixturestack(self) -> List["FixtureDef[Any]"]:
|
||||
values = [request._fixturedef for request in self._iter_chain()]
|
||||
values.reverse()
|
||||
return values
|
||||
|
||||
|
||||
@final
|
||||
class TopRequest(FixtureRequest):
|
||||
|
@ -877,13 +865,6 @@ class FixtureLookupErrorRepr(TerminalRepr):
|
|||
tw.line("%s:%d" % (os.fspath(self.filename), self.firstlineno + 1))
|
||||
|
||||
|
||||
def fail_fixturefunc(fixturefunc, msg: str) -> NoReturn:
|
||||
fs, lineno = getfslineno(fixturefunc)
|
||||
location = f"{fs}:{lineno + 1}"
|
||||
source = _pytest._code.Source(fixturefunc)
|
||||
fail(msg + ":\n\n" + str(source.indent()) + "\n" + location, pytrace=False)
|
||||
|
||||
|
||||
def call_fixture_func(
|
||||
fixturefunc: "_FixtureFunc[FixtureValue]", request: FixtureRequest, kwargs
|
||||
) -> FixtureValue:
|
||||
|
@ -913,7 +894,13 @@ def _teardown_yield_fixture(fixturefunc, it) -> None:
|
|||
except StopIteration:
|
||||
pass
|
||||
else:
|
||||
fail_fixturefunc(fixturefunc, "fixture function has more than one 'yield'")
|
||||
fs, lineno = getfslineno(fixturefunc)
|
||||
fail(
|
||||
f"fixture function has more than one 'yield':\n\n"
|
||||
f"{Source(fixturefunc).indent()}\n"
|
||||
f"{fs}:{lineno + 1}",
|
||||
pytrace=False,
|
||||
)
|
||||
|
||||
|
||||
def _eval_scope_callable(
|
||||
|
|
|
@ -765,7 +765,7 @@ class Item(Node, abc.ABC):
|
|||
and lineno is a 0-based line number.
|
||||
"""
|
||||
location = self.reportinfo()
|
||||
path = absolutepath(os.fspath(location[0]))
|
||||
path = absolutepath(location[0])
|
||||
relfspath = self.session._node_location_to_relpath(path)
|
||||
assert type(location[2]) is str
|
||||
return (relfspath, location[1], location[2])
|
||||
|
|
|
@ -11,6 +11,8 @@ from typing import Protocol
|
|||
from typing import Type
|
||||
from typing import TypeVar
|
||||
|
||||
from .warning_types import PytestDeprecationWarning
|
||||
|
||||
|
||||
class OutcomeException(BaseException):
|
||||
"""OutcomeException and its subclass instances indicate and contain info
|
||||
|
@ -192,7 +194,11 @@ def xfail(reason: str = "") -> NoReturn:
|
|||
|
||||
|
||||
def importorskip(
|
||||
modname: str, minversion: Optional[str] = None, reason: Optional[str] = None
|
||||
modname: str,
|
||||
minversion: Optional[str] = None,
|
||||
reason: Optional[str] = None,
|
||||
*,
|
||||
exc_type: Optional[Type[ImportError]] = None,
|
||||
) -> Any:
|
||||
"""Import and return the requested module ``modname``, or skip the
|
||||
current test if the module cannot be imported.
|
||||
|
@ -205,6 +211,18 @@ def importorskip(
|
|||
:param reason:
|
||||
If given, this reason is shown as the message when the module cannot
|
||||
be imported.
|
||||
:param exc_type:
|
||||
The exception that should be captured in order to skip modules.
|
||||
Must be :py:class:`ImportError` or a subclass.
|
||||
|
||||
If the module can be imported but raises :class:`ImportError`, pytest will
|
||||
issue a warning to the user, as often users expect the module not to be
|
||||
found (which would raise :class:`ModuleNotFoundError` instead).
|
||||
|
||||
This warning can be suppressed by passing ``exc_type=ImportError`` explicitly.
|
||||
|
||||
See :ref:`import-or-skip-import-error` for details.
|
||||
|
||||
|
||||
:returns:
|
||||
The imported module. This should be assigned to its canonical name.
|
||||
|
@ -212,23 +230,62 @@ def importorskip(
|
|||
Example::
|
||||
|
||||
docutils = pytest.importorskip("docutils")
|
||||
|
||||
.. versionadded:: 8.2
|
||||
|
||||
The ``exc_type`` parameter.
|
||||
"""
|
||||
import warnings
|
||||
|
||||
__tracebackhide__ = True
|
||||
compile(modname, "", "eval") # to catch syntaxerrors
|
||||
|
||||
# Until pytest 9.1, we will warn the user if we catch ImportError (instead of ModuleNotFoundError),
|
||||
# as this might be hiding an installation/environment problem, which is not usually what is intended
|
||||
# when using importorskip() (#11523).
|
||||
# In 9.1, to keep the function signature compatible, we just change the code below to:
|
||||
# 1. Use `exc_type = ModuleNotFoundError` if `exc_type` is not given.
|
||||
# 2. Remove `warn_on_import` and the warning handling.
|
||||
if exc_type is None:
|
||||
exc_type = ImportError
|
||||
warn_on_import_error = True
|
||||
else:
|
||||
warn_on_import_error = False
|
||||
|
||||
skipped: Optional[Skipped] = None
|
||||
warning: Optional[Warning] = None
|
||||
|
||||
with warnings.catch_warnings():
|
||||
# Make sure to ignore ImportWarnings that might happen because
|
||||
# of existing directories with the same name we're trying to
|
||||
# import but without a __init__.py file.
|
||||
warnings.simplefilter("ignore")
|
||||
|
||||
try:
|
||||
__import__(modname)
|
||||
except ImportError as exc:
|
||||
except exc_type as exc:
|
||||
# Do not raise or issue warnings inside the catch_warnings() block.
|
||||
if reason is None:
|
||||
reason = f"could not import {modname!r}: {exc}"
|
||||
raise Skipped(reason, allow_module_level=True) from None
|
||||
skipped = Skipped(reason, allow_module_level=True)
|
||||
|
||||
if warn_on_import_error and not isinstance(exc, ModuleNotFoundError):
|
||||
lines = [
|
||||
"",
|
||||
f"Module '{modname}' was found, but when imported by pytest it raised:",
|
||||
f" {exc!r}",
|
||||
"In pytest 9.1 this warning will become an error by default.",
|
||||
"You can fix the underlying problem, or alternatively overwrite this behavior and silence this "
|
||||
"warning by passing exc_type=ImportError explicitly.",
|
||||
"See https://docs.pytest.org/en/stable/deprecations.html#pytest-importorskip-default-behavior-regarding-importerror",
|
||||
]
|
||||
warning = PytestDeprecationWarning("\n".join(lines))
|
||||
|
||||
if warning:
|
||||
warnings.warn(warning, stacklevel=2)
|
||||
if skipped:
|
||||
raise skipped
|
||||
|
||||
mod = sys.modules[modname]
|
||||
if minversion is None:
|
||||
return mod
|
||||
|
|
|
@ -924,13 +924,13 @@ def visit(
|
|||
yield from visit(entry.path, recurse)
|
||||
|
||||
|
||||
def absolutepath(path: Union[Path, str]) -> Path:
|
||||
def absolutepath(path: "Union[str, os.PathLike[str]]") -> Path:
|
||||
"""Convert a path to an absolute path using os.path.abspath.
|
||||
|
||||
Prefer this over Path.resolve() (see #6523).
|
||||
Prefer this over Path.absolute() (not public, doesn't normalize).
|
||||
"""
|
||||
return Path(os.path.abspath(str(path)))
|
||||
return Path(os.path.abspath(path))
|
||||
|
||||
|
||||
def commonpath(path1: Path, path2: Path) -> Optional[Path]:
|
||||
|
|
|
@ -39,7 +39,7 @@ from _pytest.outcomes import Skipped
|
|||
from _pytest.outcomes import TEST_OUTCOME
|
||||
|
||||
|
||||
if sys.version_info[:2] < (3, 11):
|
||||
if sys.version_info < (3, 11):
|
||||
from exceptiongroup import BaseExceptionGroup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
|
@ -32,6 +32,9 @@ from _pytest.runner import CallInfo
|
|||
import pytest
|
||||
|
||||
|
||||
if sys.version_info[:2] < (3, 11):
|
||||
from exceptiongroup import ExceptionGroup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import unittest
|
||||
|
||||
|
@ -111,6 +114,20 @@ class UnitTestCase(Class):
|
|||
return None
|
||||
cleanup = getattr(cls, "doClassCleanups", lambda: None)
|
||||
|
||||
def process_teardown_exceptions() -> None:
|
||||
# tearDown_exceptions is a list set in the class containing exc_infos for errors during
|
||||
# teardown for the class.
|
||||
exc_infos = getattr(cls, "tearDown_exceptions", None)
|
||||
if not exc_infos:
|
||||
return
|
||||
exceptions = [exc for (_, exc, _) in exc_infos]
|
||||
# If a single exception, raise it directly as this provides a more readable
|
||||
# error (hopefully this will improve in #12255).
|
||||
if len(exceptions) == 1:
|
||||
raise exceptions[0]
|
||||
else:
|
||||
raise ExceptionGroup("Unittest class cleanup errors", exceptions)
|
||||
|
||||
def unittest_setup_class_fixture(
|
||||
request: FixtureRequest,
|
||||
) -> Generator[None, None, None]:
|
||||
|
@ -125,6 +142,7 @@ class UnitTestCase(Class):
|
|||
# follow this here.
|
||||
except Exception:
|
||||
cleanup()
|
||||
process_teardown_exceptions()
|
||||
raise
|
||||
yield
|
||||
try:
|
||||
|
@ -132,6 +150,7 @@ class UnitTestCase(Class):
|
|||
teardown()
|
||||
finally:
|
||||
cleanup()
|
||||
process_teardown_exceptions()
|
||||
|
||||
self.session._fixturemanager._register_fixture(
|
||||
# Use a unique name to speed up lookup.
|
||||
|
|
|
@ -28,7 +28,7 @@ import pytest
|
|||
if TYPE_CHECKING:
|
||||
from _pytest._code.code import _TracebackStyle
|
||||
|
||||
if sys.version_info[:2] < (3, 11):
|
||||
if sys.version_info < (3, 11):
|
||||
from exceptiongroup import ExceptionGroup
|
||||
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ from typing import Dict
|
|||
from typing import List
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
import warnings
|
||||
|
||||
from _pytest import outcomes
|
||||
from _pytest import reports
|
||||
|
@ -22,7 +23,7 @@ from _pytest.pytester import Pytester
|
|||
import pytest
|
||||
|
||||
|
||||
if sys.version_info[:2] < (3, 11):
|
||||
if sys.version_info < (3, 11):
|
||||
from exceptiongroup import ExceptionGroup
|
||||
|
||||
|
||||
|
@ -762,6 +763,73 @@ def test_importorskip_imports_last_module_part() -> None:
|
|||
assert os.path == ospath
|
||||
|
||||
|
||||
class TestImportOrSkipExcType:
|
||||
"""Tests for #11523."""
|
||||
|
||||
def test_no_warning(self) -> None:
|
||||
# An attempt on a module which does not exist will raise ModuleNotFoundError, so it will
|
||||
# be skipped normally and no warning will be issued.
|
||||
with warnings.catch_warnings(record=True) as captured:
|
||||
warnings.simplefilter("always")
|
||||
|
||||
with pytest.raises(pytest.skip.Exception):
|
||||
pytest.importorskip("TestImportOrSkipExcType_test_no_warning")
|
||||
|
||||
assert captured == []
|
||||
|
||||
def test_import_error_with_warning(self, pytester: Pytester) -> None:
|
||||
# Create a module which exists and can be imported, however it raises
|
||||
# ImportError due to some other problem. In this case we will issue a warning
|
||||
# about the future behavior change.
|
||||
fn = pytester.makepyfile("raise ImportError('some specific problem')")
|
||||
pytester.syspathinsert()
|
||||
|
||||
with warnings.catch_warnings(record=True) as captured:
|
||||
warnings.simplefilter("always")
|
||||
|
||||
with pytest.raises(pytest.skip.Exception):
|
||||
pytest.importorskip(fn.stem)
|
||||
|
||||
[warning] = captured
|
||||
assert warning.category is pytest.PytestDeprecationWarning
|
||||
|
||||
def test_import_error_suppress_warning(self, pytester: Pytester) -> None:
|
||||
# Same as test_import_error_with_warning, but we can suppress the warning
|
||||
# by passing ImportError as exc_type.
|
||||
fn = pytester.makepyfile("raise ImportError('some specific problem')")
|
||||
pytester.syspathinsert()
|
||||
|
||||
with warnings.catch_warnings(record=True) as captured:
|
||||
warnings.simplefilter("always")
|
||||
|
||||
with pytest.raises(pytest.skip.Exception):
|
||||
pytest.importorskip(fn.stem, exc_type=ImportError)
|
||||
|
||||
assert captured == []
|
||||
|
||||
def test_warning_integration(self, pytester: Pytester) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
import pytest
|
||||
def test_foo():
|
||||
pytest.importorskip("warning_integration_module")
|
||||
"""
|
||||
)
|
||||
pytester.makepyfile(
|
||||
warning_integration_module="""
|
||||
raise ImportError("required library foobar not compiled properly")
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*Module 'warning_integration_module' was found, but when imported by pytest it raised:",
|
||||
"* ImportError('required library foobar not compiled properly')",
|
||||
"*1 skipped, 1 warning*",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_importorskip_dev_module(monkeypatch) -> None:
|
||||
try:
|
||||
mod = types.ModuleType("mockmodule")
|
||||
|
|
|
@ -1146,7 +1146,7 @@ def test_errors_in_xfail_skip_expressions(pytester: Pytester) -> None:
|
|||
if pypy_version_info is not None and pypy_version_info < (6,):
|
||||
markline = markline[1:]
|
||||
|
||||
if sys.version_info[:2] >= (3, 10):
|
||||
if sys.version_info >= (3, 10):
|
||||
expected = [
|
||||
"*ERROR*test_nameerror*",
|
||||
"*asd*",
|
||||
|
|
|
@ -1500,6 +1500,95 @@ def test_do_cleanups_on_teardown_failure(pytester: Pytester) -> None:
|
|||
assert passed == 1
|
||||
|
||||
|
||||
class TestClassCleanupErrors:
|
||||
"""
|
||||
Make sure to show exceptions raised during class cleanup function (those registered
|
||||
via addClassCleanup()).
|
||||
|
||||
See #11728.
|
||||
"""
|
||||
|
||||
def test_class_cleanups_failure_in_setup(self, pytester: Pytester) -> None:
|
||||
testpath = pytester.makepyfile(
|
||||
"""
|
||||
import unittest
|
||||
class MyTestCase(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
def cleanup(n):
|
||||
raise Exception(f"fail {n}")
|
||||
cls.addClassCleanup(cleanup, 2)
|
||||
cls.addClassCleanup(cleanup, 1)
|
||||
raise Exception("fail 0")
|
||||
def test(self):
|
||||
pass
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest("-s", testpath)
|
||||
result.assert_outcomes(passed=0, errors=1)
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*Unittest class cleanup errors *2 sub-exceptions*",
|
||||
"*Exception: fail 1",
|
||||
"*Exception: fail 2",
|
||||
]
|
||||
)
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"* ERROR at setup of MyTestCase.test *",
|
||||
"E * Exception: fail 0",
|
||||
]
|
||||
)
|
||||
|
||||
def test_class_cleanups_failure_in_teardown(self, pytester: Pytester) -> None:
|
||||
testpath = pytester.makepyfile(
|
||||
"""
|
||||
import unittest
|
||||
class MyTestCase(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
def cleanup(n):
|
||||
raise Exception(f"fail {n}")
|
||||
cls.addClassCleanup(cleanup, 2)
|
||||
cls.addClassCleanup(cleanup, 1)
|
||||
def test(self):
|
||||
pass
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest("-s", testpath)
|
||||
result.assert_outcomes(passed=1, errors=1)
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*Unittest class cleanup errors *2 sub-exceptions*",
|
||||
"*Exception: fail 1",
|
||||
"*Exception: fail 2",
|
||||
]
|
||||
)
|
||||
|
||||
def test_class_cleanup_1_failure_in_teardown(self, pytester: Pytester) -> None:
|
||||
testpath = pytester.makepyfile(
|
||||
"""
|
||||
import unittest
|
||||
class MyTestCase(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
def cleanup(n):
|
||||
raise Exception(f"fail {n}")
|
||||
cls.addClassCleanup(cleanup, 1)
|
||||
def test(self):
|
||||
pass
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest("-s", testpath)
|
||||
result.assert_outcomes(passed=1, errors=1)
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*ERROR at teardown of MyTestCase.test*",
|
||||
"*Exception: fail 1",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_traceback_pruning(pytester: Pytester) -> None:
|
||||
"""Regression test for #9610 - doesn't crash during traceback pruning."""
|
||||
pytester.makepyfile(
|
||||
|
|
Loading…
Reference in New Issue