У меня есть декоратор, который:
funtools.wraps
, чтобы сохранить остальную информацию.Есть ли способ добиться этого, не mypy
жалуясь?
Минимальный рабочий пример будет выглядеть так:
from functools import wraps
from typing import Callable, TypeVar
B = TypeVar('B', bound=str)
def str_as_int_wrapper(func: Callable[[int], int]) -> Callable[[B], B]:
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',)
WRAPPER_UPDATES = ('__dict__', '__annotations__')
@wraps(func, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)
def _wrapped_func(val: B) -> B:
num = int(val)
result = func(num)
return val.__class__(result)
return _wrapped_func
@str_as_int_wrapper
def add_one(val: int) -> int:
return val + 1
Вроде всё работает, но mypy
(версия 1.10.0) это не нравится. Вместо этого он жалуется на
test.py:17: error: Incompatible return value type (got "_Wrapped[[int], int, [Never], Never]", expected "Callable[[B], B]") [return-value]
test.py:17: note: "_Wrapped[[int], int, [Never], Never].__call__" has type "Callable[[Arg(Never, 'val')], Never]"
Если я удалю декоратор @wraps
или заменю аннотации типа B
на str
, ошибка исчезнет.
Я что-то пропустил? Это какая-то ошибка или ограничение, о которых уже сообщалось от mypy
(ничего не удалось найти)? Стоит ли об этом сообщать?
Спасибо!
В каком-то смысле это регресс, хотя это скорее ограничение. Ваш код должен был работать как есть, и Mypy 1.9 передает ваш код, что означает, что изменение поведения было добавлено в версии 1.10.
Согласно подобной проблеме (о которой сообщалось как об ошибке три месяца назад, но она не была отсортирована), причиной является этот PR, в котором определения wraps
и связанных с ним символов в копии typeshed
Mypy были изменены с (упрощены):
class IdentityFunction(Protocol):
def __call__(self, x: _T, /) -> _T: ...
_AnyCallable: TypeAlias = Callable[..., object]
def wraps(
wrapped: _AnyCallable, # ...
) -> IdentityFunction: ...
...to (также упрощенно):
class _Wrapped(Generic[_PWrapped, _RWrapped, _PWrapper, _RWrapper]):
__wrapped__: Callable[_PWrapped, _RWrapped]
def __call__(self, *args: _PWrapper.args, **kwargs: _PWrapper.kwargs) -> _RWrapper: ...
class _Wrapper(Generic[_PWrapped, _RWrapped]):
def __call__(self, f: Callable[_PWrapper, _RWrapper]) -> _Wrapped[_PWrapped, _RWrapped, _PWrapper, _RWrapper]: ...
def wraps(
wrapped: Callable[_PWrapped, _RWrapped], # ...
) -> _Wrapper[_PWrapped, _RWrapped]: ...
Судя по всему, это было сделано в попытке исправить эту набранную проблему.
Если вас не интересует объяснение, примените эти общие решения, и все готово:
return _wrapped_func # type: ignore[return-value]
return cast(Callable[[B], B], _wrapped_func)
if TYPE_CHECKING:
_T = TypeVar('_T')
class IdentityFunction(Protocol):
def __call__(self, x: _T, /) -> _T: ...
_AnyCallable = Callable[..., object]
def wraps(
wrapped: _AnyCallable,
# Default values omitted here for brevity
assigned: Sequence[str] = (...),
updated: Sequence[str] = (...),
) -> IdentityFunction: ...
Первоначально wraps(func)
(где тип func
совершенно не имеет значения) возвращал IdentityFunction
, необобщенный Protocol
, __call__
которого является универсальным над _T
:
class IdentityFunction(Protocol):
def __call__(self, x: _T, /) -> _T: ...
Таким образом, в следующем (неявном) вызове wraps(func)(_wrapped_func)
, _wrapped_func
сохранили свой исходный тип: универсальную функцию. Mypy понял это правильно, и так оно и есть до сих пор.
def str_as_int_wrapper(func: Callable[[int], int]) -> Callable[[B], B]:
@wraps(func, ...)
def _wrapped_func(val: B) -> B:
...
reveal_type(_wrapped_func) # def [B <: builtins.str] (val: B`-1) -> B`-1
return _wrapped_func
После изменения wraps(func)
теперь возвращает _Wrapper[<P_of_func>, <R_of_func>]
, чья __call__
возвращает _Wrapped[<P_of_func>, <R_of_func>, <P_of_wrapped_func>, <R_of_wrapped_func>]
, тип, отличный от исходного типа _wrapped_func
.
Здесь Mypy сделал неправильный шаг, как это было бы до версии 1.10, если бы копия typeshed
не была изменена. Как заметил С.Терляков:
Проблема возникает из-за попытки решить
B
слишком рано: она привязана к общему аргументу_Wrapped
[s] и решена,B
после этого не сохраняется (следовательно,Never
в выводе).
Это имеет смысл! Спасибо за подробное объяснение!
Это
mypy
ограничение, но я подозреваю, что о подобном уже сообщалось. Проблема возникает из-за слишком ранней попытки решитьB
: она связана с_Wrapped
(см. набранный) общим аргументом и решена,B
после этого не сохраняется (следовательно,Never
в выводе). Просто оставьте комментарий в игноре и вперед.Pyright
не сообщает о проблемах с вашим кодом.