diff --git a/AUTHORS b/AUTHORS index e5b863e71f1..302e486514b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -134,6 +134,7 @@ Deysha Rivera Dheeraj C K Dhiren Serai Diego Russo +Dima Gerasimov Dmitry Dygalo Dmitry Pribysh Dominic Mortlock diff --git a/changelog/13445.bugfix.rst b/changelog/13445.bugfix.rst new file mode 100644 index 00000000000..1c601a1045e --- /dev/null +++ b/changelog/13445.bugfix.rst @@ -0,0 +1 @@ +Made the type annotations of :func:`pytest.skip` and friends more spec-complaint to have them work across more type checkers. diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index 68ba0543365..766be95c0f7 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -3,13 +3,10 @@ from __future__ import annotations -from collections.abc import Callable import sys from typing import Any -from typing import cast +from typing import ClassVar from typing import NoReturn -from typing import Protocol -from typing import TypeVar from .warning_types import PytestDeprecationWarning @@ -77,35 +74,11 @@ def __init__( super().__init__(msg) -# We need a callable protocol to add attributes, for discussion see -# https://github.com/python/mypy/issues/2087. - -_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 - - -# Exposed helper methods. +class XFailed(Failed): + """Raised from an explicit call to pytest.xfail().""" -@_with_exception(Exit) -def exit( - reason: str = "", - returncode: int | None = None, -) -> NoReturn: +class _Exit: """Exit testing process. :param reason: @@ -113,21 +86,24 @@ def 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. 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) + Exception: ClassVar[type[Exit]] = Exit -@_with_exception(Skipped) -def skip( - reason: str = "", - *, - allow_module_level: bool = False, -) -> NoReturn: + def __call__(self, reason: str = "", returncode: int | None = None) -> NoReturn: + __tracebackhide__ = True + raise Exit(msg=reason, returncode=returncode) + + +exit: _Exit = _Exit() + + +class _Skip: """Skip an executing test with the given message. This function should be called only during testing (setup, call or teardown) or @@ -155,12 +131,18 @@ def skip( 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) + 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) + + +skip: _Skip = _Skip() -@_with_exception(Failed) -def fail(reason: str = "", pytrace: bool = True) -> NoReturn: + +class _Fail: """Explicitly fail an executing test with the given message. :param reason: @@ -173,16 +155,18 @@ def fail(reason: str = "", pytrace: bool = True) -> NoReturn: :raises pytest.fail.Exception: The exception that is raised. """ - __tracebackhide__ = True - raise Failed(msg=reason, pytrace=pytrace) + Exception: ClassVar[type[Failed]] = Failed -class XFailed(Failed): - """Raised from an explicit call to pytest.xfail().""" + def __call__(self, reason: str = "", pytrace: bool = True) -> NoReturn: + __tracebackhide__ = True + raise Failed(msg=reason, pytrace=pytrace) -@_with_exception(XFailed) -def xfail(reason: str = "") -> NoReturn: +fail: _Fail = _Fail() + + +class _XFail: """Imperatively xfail an executing test or setup function with the given reason. This function should be called only during testing (setup, call or teardown). @@ -201,8 +185,15 @@ def xfail(reason: str = "") -> NoReturn: :raises pytest.xfail.Exception: The exception that is raised. """ - __tracebackhide__ = True - raise XFailed(reason) + + Exception: ClassVar[type[XFailed]] = XFailed + + def __call__(self, reason: str = "") -> NoReturn: + __tracebackhide__ = True + raise XFailed(msg=reason) + + +xfail: _XFail = _XFail() def importorskip( diff --git a/testing/python/collect.py b/testing/python/collect.py index 530f1c340ff..80981ed280f 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -1075,7 +1075,8 @@ class TestTracebackCutting: def test_skip_simple(self): with pytest.raises(pytest.skip.Exception) as excinfo: pytest.skip("xxx") - assert excinfo.traceback[-1].frame.code.name == "skip" + if sys.version_info >= (3, 11): + assert excinfo.traceback[-1].frame.code.raw.co_qualname == "_Skip.__call__" assert excinfo.traceback[-1].ishidden(excinfo) assert excinfo.traceback[-2].frame.code.name == "test_skip_simple" assert not excinfo.traceback[-2].ishidden(excinfo)