Merge branch 'pytest-dev:main' into main

This commit is contained in:
HFFuture 2024-04-27 10:07:56 -04:00 committed by GitHub
commit 0d699188ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 416 additions and 127 deletions

View File

@ -101,6 +101,7 @@ Cyrus Maden
Damian Skrzypczak
Daniel Grana
Daniel Hahler
Daniel Miller
Daniel Nuri
Daniel Sánchez Castelló
Daniel Valenzuela Zenteno

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ Release announcements
:maxdepth: 2
release-8.1.2
release-8.1.1
release-8.1.0
release-8.0.2

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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*",

View File

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