Skip to content

Commit a0735e1

Browse files
committed
use ParamSpec for pytest.skip instead of __call__ Protocol attribute
This is a more canonical way of typing generic callbacks/decorators (see https://mypy.readthedocs.io/en/stable/protocols.html#callback-protocols) This helps with potential issues/ambiguity with `__call__` semanics in type checkers -- according to python spec, `__call__` needs to be present on the class, rather than as an instance attribute to be considered callable. This worked in mypy because it didn't handle the spec 100% correctly (in this case this has no negative consequences) However, `ty` type checker is stricter/more correct about it and every `pytest.skip` usage results in: `error[call-non-callable]: Object of type `_WithException[Unknown, <class 'Skipped'>]` is not callable` For more context, see: - astral-sh/ruff#17832 (comment) - https://discuss.python.org/t/when-should-we-assume-callable-types-are-method-descriptors/92938 Testing: Tested with running mypy against the following snippet: ``` import pytest reveal_type(pytest.skip) reveal_type(pytest.skip.Exception) reveal_type(pytest.skip(reason="whatever")) ``` Before the change: ``` test_pytest_skip.py:2: note: Revealed type is "_pytest.outcomes._WithException[def (reason: builtins.str =, *, allow_module_level: builtins.bool =) -> Never, def (msg: Union[builtins.str, None] =, pytrace: builtins.bool =, allow_module_level: builtins.bool =, *, _use_item_location: builtins.bool =) -> _pytest.outcomes.Skipped]" test_pytest_skip.py:3: note: Revealed type is "def (msg: Union[builtins.str, None] =, pytrace: builtins.bool =, allow_module_level: builtins.bool =, *, _use_item_location: builtins.bool =) -> _pytest.outcomes.Skipped" test_pytest_skip.py:4: note: Revealed type is "Never" ``` After the change: ``` test_pytest_skip.py:2: note: Revealed type is "_pytest.outcomes._WithException[[reason: builtins.str =, *, allow_module_level: builtins.bool =], Never, def (msg: Union[builtins.str, None] =, pytrace: builtins.bool =, allow_module_level: builtins.bool =, *, _use_item_location: builtins.bool =) -> _pytest.outcomes.Skipped]" test_pytest_skip.py:3: note: Revealed type is "def (msg: Union[builtins.str, None] =, pytrace: builtins.bool =, allow_module_level: builtins.bool =, *, _use_item_location: builtins.bool =) -> _pytest.outcomes.Skipped" test_pytest_skip.py:4: note: Revealed type is "Never" ``` All types are matching and propagated correctly.
1 parent 83536b4 commit a0735e1

File tree

2 files changed

+13
-6
lines changed

2 files changed

+13
-6
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ Deysha Rivera
134134
Dheeraj C K
135135
Dhiren Serai
136136
Diego Russo
137+
Dima Gerasimov
137138
Dmitry Dygalo
138139
Dmitry Pribysh
139140
Dominic Mortlock

src/_pytest/outcomes.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from typing import Protocol
1212
from typing import TypeVar
1313

14+
from typing_extensions import ParamSpec
15+
1416
from .warning_types import PytestDeprecationWarning
1517

1618

@@ -80,18 +82,22 @@ def __init__(
8082
# We need a callable protocol to add attributes, for discussion see
8183
# https://github.com/python/mypy/issues/2087.
8284

83-
_F = TypeVar("_F", bound=Callable[..., object])
85+
_P = ParamSpec("_P")
86+
_R = TypeVar("_R", covariant=True)
8487
_ET = TypeVar("_ET", bound=type[BaseException])
8588

8689

87-
class _WithException(Protocol[_F, _ET]):
90+
class _WithException(Protocol[_P, _R, _ET]):
8891
Exception: _ET
89-
__call__: _F
92+
93+
def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R: ...
9094

9195

92-
def _with_exception(exception_type: _ET) -> Callable[[_F], _WithException[_F, _ET]]:
93-
def decorate(func: _F) -> _WithException[_F, _ET]:
94-
func_with_exception = cast(_WithException[_F, _ET], func)
96+
def _with_exception(
97+
exception_type: _ET,
98+
) -> Callable[[Callable[_P, _R]], _WithException[_P, _R, _ET]]:
99+
def decorate(func: Callable[_P, _R]) -> _WithException[_P, _R, _ET]:
100+
func_with_exception = cast(_WithException[_P, _R, _ET], func)
95101
func_with_exception.Exception = exception_type
96102
return func_with_exception
97103

0 commit comments

Comments
 (0)