Удалить Self из Callable Type подпись, чтобы соответствовать метод Instance - PullRequest
0 голосов
/ 05 марта 2020

Мотивация для этого состоит в том, чтобы набирать обработчики событий проверки, чтобы обеспечить соответствие между типами, которые зарегистрированные события ожидают в качестве аргументов, и теми, которые обработчик готов предоставить.

Я пытаюсь отслеживать сигнатуры функций в аннотациях типов для декоратора функций на основе классов. Это просто проект заглушки Mypy: фактическая реализация будет получать тот же результат по-другому.

Итак, у нас есть базовый c скелет декоратора, подобный такому

from typing import Any, Callable, Generic, TypeVar

FuncT = TypeVar("FuncT", bound=Callable)

class decorator(Generic[FuncT]):
    def __init__(self, method: FuncT) -> None:
        ... # Allows mypy to infer the parameter type

    __call__: FuncT

    execute: FuncT

Со следующим примером заглушки

class Widget:
    def bar(self: Any, a: int) -> int:
        ...

    @decorator
    def foo(self: Any, a: int) -> int:
        ...

w = Widget()

reveal_type(Widget.bar)
reveal_type(w.bar)

reveal_type(Widget.foo.__call__)
reveal_type(w.foo.__call__)

Выявлены следующие типы:

Widget.bar (undecorated class method):         'def (self: demo.Widget, a: builtins.int) -> builtins.int'
w.bar (undecorated instance method):           'def (a: builtins.int) -> builtins.int'
Widget.foo.__call__ (decorated class method):  'def (self: demo.Widget, a: builtins.int) -> builtins.int'
w.foo.__call__ (decorated instance method):    'def (self: demo.Widget, a: builtins.int) -> builtins.int'

Следствием этого является то, что если я вызываю w.bar(2), он проходит проверку типов, но если я вызываю w.foo(2) или w.foo.execute(2), тогда mypy жалуется, что не хватает параметров. Между тем все Widget.bar(w, 2) Widget.foo(w, 2) и Widget.foo.execute(w, 2) проходят нормально.

Мне нужен способ аннотировать это, чтобы убедить w.foo.__call__ и w.foo.execute поставить ту же подпись, что и w.bar.

1 Ответ

0 голосов
/ 12 марта 2020

Мне кажется, что это невозможно сделать так чисто, как хотелось бы. Текущая система типов просто не может не иметь возможности связываться с аргументами.

Самое близкое, на что я смог прийти, использует два трюка: большая перегрузка для перечисления (вероятных) типов ввода и определение __get__, чтобы отличить класс guish от доступа к экземпляру.

class DecoratorCallable(Generic[FuncT]):#, ArbitraryCallable[FuncT]):
    __call__ : FuncT
    execute  : FuncT

# FuncT and FuncT2 refer to the method signature with and without self
class DecoratorBase(Generic[FuncT, FuncT2]):
    @overload
    def __get__(self, instance: None, owner: object) -> DecoratorCallable[FuncT]:
        # when a method is accessed directly, instance will be None
        ...
    @overload
    def __get__(self, instance: object, owner: object) -> DecoratorCallable[FuncT2]:
        # when a method is accessed through an instance, instance will be that object
        ...

    def __get__(self, instance: Optional[object], owner: object) -> DecoratorCallable:
        ...

# To support methods with up to 5 parameters, define 5 type vars. 
# For more parameters, just keep counting.
T1 = TypeVar("T1")
T2 = TypeVar("T2")
T3 = TypeVar("T3")
T4 = TypeVar("T4")
T5 = TypeVar("T5")
R = TypeVar("R")

@overload
def decorator(f: Callable[[T1], R]) -> DecoratorBase[Callable[[T1], R], Callable[[], R]] :...
@overload
def decorator(f: Callable[[T1, T2], R]) -> DecoratorBase[Callable[[T1, T2], R], Callable[[T2], R]] :...
@overload
def decorator(f: Callable[[T1, T2, T3], R]) -> DecoratorBase[Callable[[T1, T2, T3], R], Callable[[T2, T3], R]] :...
@overload
def decorator(f: Callable[[T1, T2, T3, T4], R]) -> DecoratorBase[Callable[[T1, T2, T3, T4], R], Callable[[T2, T3, T4], R]] :...
@overload
def decorator(f: Callable[[T1, T2, T3, T4, T5], R]) -> DecoratorBase[Callable[[T1, T2, T3, T4, T5], R], Callable[[T2, T3, T4, T5], R]] :...

def decorator(f: Callable[..., R]) -> DecoratorBase[Callable, Callable]:
    ...

С тем же классом, что и раньше, обнаруженные типы теперь

Widget.bar (undecorated class method):         'def (self: Any, a: builtins.float) -> builtins.bool'
w.bar (undecorated instance method):           'def (a: builtins.float) -> builtins.bool'
Widget.foo.__call__ (decorated class method):  'def (Any, builtins.float*) -> builtins.bool*'
w.foo.__call__ (decorated instance method):    'def (builtins.float*) -> builtins.bool*'

Это по крайней мере означает, что Mypy правильно разрешит Widget.foo(w, 2) и w.foo(2) и будет правильно disallow Widget.foo(w, "A") и w.foo("A")

Однако в процессе он потерял имена параметров, и поэтому w.foo(a=2) получает бесполезную ошибку "Неожиданный аргумент ключевого слова" a "".

Он также падает, если вы пытаетесь украсить метод слишком большим количеством аргументов, и, возможно, в некоторых странных крайних случаях.

...