Mypy 1.10 сообщает об ошибке, когда functools.wraps() используется для общей функции

TLDR;

У меня есть декоратор, который:

  • меняет сигнатуру функции
  • обернутая функция использует некоторые аргументы общего типа
  • Кроме подписи, я хотел бы использовать 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 ограничение, но я подозреваю, что о подобном уже сообщалось. Проблема возникает из-за слишком ранней попытки решить B: она связана с _Wrapped (см. набранный) общим аргументом и решена, B после этого не сохраняется (следовательно, Never в выводе). Просто оставьте комментарий в игноре и вперед. Pyright не сообщает о проблемах с вашим кодом.

STerliakov 29.07.2024 17:54
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
2
1
50
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В каком-то смысле это регресс, хотя это скорее ограничение. Ваш код должен был работать как есть, и 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 понял это правильно, и так оно и есть до сих пор.

(playgrounds: 1.9 , 1.11)

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 в выводе).

Это имеет смысл! Спасибо за подробное объяснение!

mgab 31.07.2024 17:26

Другие вопросы по теме