This commit is contained in:
Iliyan Angelov
2025-12-06 03:27:35 +02:00
parent 7667eb5eda
commit 5a8ca3c475
2211 changed files with 28086 additions and 37066 deletions

View File

@@ -1,21 +1,35 @@
"""Exception classes and constants handling test outcomes as well as
functions creating them."""
from __future__ import annotations
import sys
import warnings
from typing import Any
from typing import ClassVar
from typing import Callable
from typing import cast
from typing import NoReturn
from typing import Optional
from typing import Type
from typing import TypeVar
from .warning_types import PytestDeprecationWarning
from _pytest.deprecated import KEYWORD_MSG_ARG
TYPE_CHECKING = False # Avoid circular import through compat.
if TYPE_CHECKING:
from typing_extensions import Protocol
else:
# typing.Protocol is only available starting from Python 3.8. It is also
# available from typing_extensions, but we don't want a runtime dependency
# on that. So use a dummy runtime implementation.
from typing import Generic
Protocol = Generic
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:
def __init__(self, msg: Optional[str] = None, pytrace: bool = True) -> None:
if msg is not None and not isinstance(msg, str):
error_msg = ( # type: ignore[unreachable]
"{} expected string as 'msg' parameter, got '{}' instead.\n"
@@ -44,7 +58,7 @@ class Skipped(OutcomeException):
def __init__(
self,
msg: str | None = None,
msg: Optional[str] = None,
pytrace: bool = True,
allow_module_level: bool = False,
*,
@@ -67,18 +81,41 @@ class Exit(Exception):
"""Raised for immediate program exits (no tracebacks/summaries)."""
def __init__(
self, msg: str = "unknown reason", returncode: int | None = None
self, msg: str = "unknown reason", returncode: Optional[int] = None
) -> None:
self.msg = msg
self.returncode = returncode
super().__init__(msg)
class XFailed(Failed):
"""Raised from an explicit call to pytest.xfail()."""
# Elaborate hack to work around https://github.com/python/mypy/issues/2087.
# Ideally would just be `exit.Exception = Exit` etc.
_F = TypeVar("_F", bound=Callable[..., object])
_ET = TypeVar("_ET", bound=Type[BaseException])
class _Exit:
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
# Exposed helper methods.
@_with_exception(Exit)
def exit(
reason: str = "", returncode: Optional[int] = None, *, msg: Optional[str] = None
) -> NoReturn:
"""Exit testing process.
:param reason:
@@ -86,24 +123,30 @@ class _Exit:
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`.
Return code to be used when exiting pytest.
:raises pytest.exit.Exception:
The exception that is raised.
:param msg:
Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
"""
__tracebackhide__ = True
from _pytest.config import UsageError
Exception: ClassVar[type[Exit]] = Exit
def __call__(self, reason: str = "", returncode: int | None = None) -> NoReturn:
__tracebackhide__ = True
raise Exit(msg=reason, returncode=returncode)
if reason and msg:
raise UsageError(
"cannot pass reason and msg to exit(), `msg` is deprecated, use `reason`."
)
if not reason:
if msg is None:
raise UsageError("exit() requires a reason argument")
warnings.warn(KEYWORD_MSG_ARG.format(func="exit"), stacklevel=2)
reason = msg
raise Exit(reason, returncode)
exit: _Exit = _Exit()
class _Skip:
@_with_exception(Skipped)
def skip(
reason: str = "", *, allow_module_level: bool = False, msg: Optional[str] = None
) -> NoReturn:
"""Skip an executing test with the given message.
This function should be called only during testing (setup, call or teardown) or
@@ -121,8 +164,8 @@ class _Skip:
Defaults to False.
:raises pytest.skip.Exception:
The exception that is raised.
:param msg:
Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
.. note::
It is better to use the :ref:`pytest.mark.skipif ref` marker when
@@ -131,18 +174,13 @@ class _Skip:
Similarly, use the ``# doctest: +SKIP`` directive (see :py:data:`doctest.SKIP`)
to skip a doctest statically.
"""
Exception: ClassVar[type[Skipped]] = Skipped
def __call__(self, reason: str = "", allow_module_level: bool = False) -> NoReturn:
__tracebackhide__ = True
raise Skipped(msg=reason, allow_module_level=allow_module_level)
__tracebackhide__ = True
reason = _resolve_msg_to_reason("skip", reason, msg)
raise Skipped(msg=reason, allow_module_level=allow_module_level)
skip: _Skip = _Skip()
class _Fail:
@_with_exception(Failed)
def fail(reason: str = "", pytrace: bool = True, msg: Optional[str] = None) -> NoReturn:
"""Explicitly fail an executing test with the given message.
:param reason:
@@ -152,28 +190,60 @@ class _Fail:
If False, msg represents the full failure information and no
python traceback will be reported.
:raises pytest.fail.Exception:
The exception that is raised.
:param msg:
Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
"""
Exception: ClassVar[type[Failed]] = Failed
def __call__(self, reason: str = "", pytrace: bool = True) -> NoReturn:
__tracebackhide__ = True
raise Failed(msg=reason, pytrace=pytrace)
__tracebackhide__ = True
reason = _resolve_msg_to_reason("fail", reason, msg)
raise Failed(msg=reason, pytrace=pytrace)
fail: _Fail = _Fail()
def _resolve_msg_to_reason(
func_name: str, reason: str, msg: Optional[str] = None
) -> str:
"""
Handles converting the deprecated msg parameter if provided into
reason, raising a deprecation warning. This function will be removed
when the optional msg argument is removed from here in future.
:param str func_name:
The name of the offending function, this is formatted into the deprecation message.
:param str reason:
The reason= passed into either pytest.fail() or pytest.skip()
:param str msg:
The msg= passed into either pytest.fail() or pytest.skip(). This will
be converted into reason if it is provided to allow pytest.skip(msg=) or
pytest.fail(msg=) to continue working in the interim period.
:returns:
The value to use as reason.
"""
__tracebackhide__ = True
if msg is not None:
if reason:
from pytest import UsageError
raise UsageError(
f"Passing both ``reason`` and ``msg`` to pytest.{func_name}(...) is not permitted."
)
warnings.warn(KEYWORD_MSG_ARG.format(func=func_name), stacklevel=3)
reason = msg
return reason
class _XFail:
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.
@@ -181,27 +251,13 @@ class _XFail:
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.
"""
Exception: ClassVar[type[XFailed]] = XFailed
def __call__(self, reason: str = "") -> NoReturn:
__tracebackhide__ = True
raise XFailed(msg=reason)
xfail: _XFail = _XFail()
__tracebackhide__ = True
raise XFailed(reason)
def importorskip(
modname: str,
minversion: str | None = None,
reason: str | None = None,
*,
exc_type: type[ImportError] | None = None,
modname: str, minversion: Optional[str] = None, reason: Optional[str] = None
) -> Any:
"""Import and return the requested module ``modname``, or skip the
current test if the module cannot be imported.
@@ -214,84 +270,30 @@ 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.
: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") # 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: Skipped | None = None
warning: Warning | None = 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 exc_type as exc:
# Do not raise or issue warnings inside the catch_warnings() block.
except ImportError 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
raise Skipped(reason, allow_module_level=True) from None
mod = sys.modules[modname]
if minversion is None:
return mod
@@ -302,7 +304,8 @@ def importorskip(
if verattr is None or Version(verattr) < Version(minversion):
raise Skipped(
f"module {modname!r} has __version__ {verattr!r}, required is: {minversion!r}",
"module %r has __version__ %r, required is: %r"
% (modname, verattr, minversion),
allow_module_level=True,
)
return mod