|
|
"""Exception classes and constants handling test outcomes as well as |
|
|
functions creating them.""" |
|
|
|
|
|
from __future__ import annotations |
|
|
|
|
|
import sys |
|
|
from typing import Any |
|
|
from typing import Callable |
|
|
from typing import cast |
|
|
from typing import NoReturn |
|
|
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 |
|
|
about test and collection outcomes.""" |
|
|
|
|
|
def __init__(self, msg: str | None = None, pytrace: bool = True) -> None: |
|
|
if msg is not None and not isinstance(msg, str): |
|
|
error_msg = ( |
|
|
"{} expected string as 'msg' parameter, got '{}' instead.\n" |
|
|
"Perhaps you meant to use a mark?" |
|
|
) |
|
|
raise TypeError(error_msg.format(type(self).__name__, type(msg).__name__)) |
|
|
super().__init__(msg) |
|
|
self.msg = msg |
|
|
self.pytrace = pytrace |
|
|
|
|
|
def __repr__(self) -> str: |
|
|
if self.msg is not None: |
|
|
return self.msg |
|
|
return f"<{self.__class__.__name__} instance>" |
|
|
|
|
|
__str__ = __repr__ |
|
|
|
|
|
|
|
|
TEST_OUTCOME = (OutcomeException, Exception) |
|
|
|
|
|
|
|
|
class Skipped(OutcomeException): |
|
|
|
|
|
|
|
|
__module__ = "builtins" |
|
|
|
|
|
def __init__( |
|
|
self, |
|
|
msg: str | None = None, |
|
|
pytrace: bool = True, |
|
|
allow_module_level: bool = False, |
|
|
*, |
|
|
_use_item_location: bool = False, |
|
|
) -> None: |
|
|
super().__init__(msg=msg, pytrace=pytrace) |
|
|
self.allow_module_level = allow_module_level |
|
|
|
|
|
|
|
|
self._use_item_location = _use_item_location |
|
|
|
|
|
|
|
|
class Failed(OutcomeException): |
|
|
"""Raised from an explicit call to pytest.fail().""" |
|
|
|
|
|
__module__ = "builtins" |
|
|
|
|
|
|
|
|
class Exit(Exception): |
|
|
"""Raised for immediate program exits (no tracebacks/summaries).""" |
|
|
|
|
|
def __init__( |
|
|
self, msg: str = "unknown reason", returncode: int | None = None |
|
|
) -> None: |
|
|
self.msg = msg |
|
|
self.returncode = returncode |
|
|
super().__init__(msg) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_F = TypeVar("_F", bound=Callable[..., object]) |
|
|
_ET = TypeVar("_ET", bound=Type[BaseException]) |
|
|
|
|
|
|
|
|
class _WithException(Protocol[_F, _ET]): |
|
|
Exception: _ET |
|
|
__call__: _F |
|
|
|
|
|
|
|
|
def _with_exception(exception_type: _ET) -> Callable[[_F], _WithException[_F, _ET]]: |
|
|
def decorate(func: _F) -> _WithException[_F, _ET]: |
|
|
func_with_exception = cast(_WithException[_F, _ET], func) |
|
|
func_with_exception.Exception = exception_type |
|
|
return func_with_exception |
|
|
|
|
|
return decorate |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@_with_exception(Exit) |
|
|
def exit( |
|
|
reason: str = "", |
|
|
returncode: int | None = None, |
|
|
) -> NoReturn: |
|
|
"""Exit testing process. |
|
|
|
|
|
:param reason: |
|
|
The message to show as the reason for exiting pytest. reason has a default value |
|
|
only because `msg` is deprecated. |
|
|
|
|
|
:param returncode: |
|
|
Return code to be used when exiting pytest. None means the same as ``0`` (no error), same as :func:`sys.exit`. |
|
|
|
|
|
:raises pytest.exit.Exception: |
|
|
The exception that is raised. |
|
|
""" |
|
|
__tracebackhide__ = True |
|
|
raise Exit(reason, returncode) |
|
|
|
|
|
|
|
|
@_with_exception(Skipped) |
|
|
def skip( |
|
|
reason: str = "", |
|
|
*, |
|
|
allow_module_level: bool = False, |
|
|
) -> NoReturn: |
|
|
"""Skip an executing test with the given message. |
|
|
|
|
|
This function should be called only during testing (setup, call or teardown) or |
|
|
during collection by using the ``allow_module_level`` flag. This function can |
|
|
be called in doctests as well. |
|
|
|
|
|
:param reason: |
|
|
The message to show the user as reason for the skip. |
|
|
|
|
|
:param allow_module_level: |
|
|
Allows this function to be called at module level. |
|
|
Raising the skip exception at module level will stop |
|
|
the execution of the module and prevent the collection of all tests in the module, |
|
|
even those defined before the `skip` call. |
|
|
|
|
|
Defaults to False. |
|
|
|
|
|
:raises pytest.skip.Exception: |
|
|
The exception that is raised. |
|
|
|
|
|
.. note:: |
|
|
It is better to use the :ref:`pytest.mark.skipif ref` marker when |
|
|
possible to declare a test to be skipped under certain conditions |
|
|
like mismatching platforms or dependencies. |
|
|
Similarly, use the ``# doctest: +SKIP`` directive (see :py:data:`doctest.SKIP`) |
|
|
to skip a doctest statically. |
|
|
""" |
|
|
__tracebackhide__ = True |
|
|
raise Skipped(msg=reason, allow_module_level=allow_module_level) |
|
|
|
|
|
|
|
|
@_with_exception(Failed) |
|
|
def fail(reason: str = "", pytrace: bool = True) -> NoReturn: |
|
|
"""Explicitly fail an executing test with the given message. |
|
|
|
|
|
:param reason: |
|
|
The message to show the user as reason for the failure. |
|
|
|
|
|
:param pytrace: |
|
|
If False, msg represents the full failure information and no |
|
|
python traceback will be reported. |
|
|
|
|
|
:raises pytest.fail.Exception: |
|
|
The exception that is raised. |
|
|
""" |
|
|
__tracebackhide__ = True |
|
|
raise Failed(msg=reason, pytrace=pytrace) |
|
|
|
|
|
|
|
|
class XFailed(Failed): |
|
|
"""Raised from an explicit call to pytest.xfail().""" |
|
|
|
|
|
|
|
|
@_with_exception(XFailed) |
|
|
def xfail(reason: str = "") -> NoReturn: |
|
|
"""Imperatively xfail an executing test or setup function with the given reason. |
|
|
|
|
|
This function should be called only during testing (setup, call or teardown). |
|
|
|
|
|
No other code is executed after using ``xfail()`` (it is implemented |
|
|
internally by raising an exception). |
|
|
|
|
|
:param reason: |
|
|
The message to show the user as reason for the xfail. |
|
|
|
|
|
.. note:: |
|
|
It is better to use the :ref:`pytest.mark.xfail ref` marker when |
|
|
possible to declare a test to be xfailed under certain conditions |
|
|
like known bugs or missing features. |
|
|
|
|
|
:raises pytest.xfail.Exception: |
|
|
The exception that is raised. |
|
|
""" |
|
|
__tracebackhide__ = True |
|
|
raise XFailed(reason) |
|
|
|
|
|
|
|
|
def importorskip( |
|
|
modname: str, |
|
|
minversion: str | None = None, |
|
|
reason: str | None = None, |
|
|
*, |
|
|
exc_type: type[ImportError] | None = None, |
|
|
) -> Any: |
|
|
"""Import and return the requested module ``modname``, or skip the |
|
|
current test if the module cannot be imported. |
|
|
|
|
|
:param modname: |
|
|
The name of the module to import. |
|
|
:param minversion: |
|
|
If given, the imported module's ``__version__`` attribute must be at |
|
|
least this minimal version, otherwise the test is still skipped. |
|
|
: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. |
|
|
|
|
|
:raises pytest.skip.Exception: |
|
|
If the module cannot be imported. |
|
|
|
|
|
Example:: |
|
|
|
|
|
docutils = pytest.importorskip("docutils") |
|
|
|
|
|
.. versionadded:: 8.2 |
|
|
|
|
|
The ``exc_type`` parameter. |
|
|
""" |
|
|
import warnings |
|
|
|
|
|
__tracebackhide__ = True |
|
|
compile(modname, "", "eval") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if exc_type is None: |
|
|
exc_type = ImportError |
|
|
warn_on_import_error = True |
|
|
else: |
|
|
warn_on_import_error = False |
|
|
|
|
|
skipped: Skipped | None = None |
|
|
warning: Warning | None = None |
|
|
|
|
|
with warnings.catch_warnings(): |
|
|
|
|
|
|
|
|
|
|
|
warnings.simplefilter("ignore") |
|
|
|
|
|
try: |
|
|
__import__(modname) |
|
|
except exc_type as exc: |
|
|
|
|
|
if reason is None: |
|
|
reason = f"could not import {modname!r}: {exc}" |
|
|
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 |
|
|
verattr = getattr(mod, "__version__", None) |
|
|
if minversion is not None: |
|
|
|
|
|
from packaging.version import Version |
|
|
|
|
|
if verattr is None or Version(verattr) < Version(minversion): |
|
|
raise Skipped( |
|
|
f"module {modname!r} has __version__ {verattr!r}, required is: {minversion!r}", |
|
|
allow_module_level=True, |
|
|
) |
|
|
return mod |
|
|
|