updates
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user