Я пытаюсь обернуть класс сигнала блинкера классом, который обеспечивает ввод текста, чтобы аргументы send
и connect
проверялись по типам для каждого конкретного сигнала.
например, если у меня есть сигнал user_update
, который ожидает, что sender
будет экземпляром User
и будет иметь ровно два kwargs
: time: int, audit: str
, я могу подкласс Signal
, чтобы обеспечить это следующим образом:
class UserUpdateSignal(Signal):
class Receiver(Protocol):
def __call__(sender: User, /, time: int, audit: str):
...
def send(sender: User, /, time: int, audit: str):
# super call
def connect(receiver: Receiver):
# super call
что приводит к желаемому поведению при проверке типов:
user_update.send(user, time=34, audit = "user_initiated") # OK
@user_update.connect # OK
def receiver(sender: User, /, time: int, audit: str):
...
user_update.send("sender") # typing error - signature mismatch
@user_update.connect # typing error - signature mismatch
def receiver(sender: str):
...
Проблемы с этим подходом заключаются в следующем:
send
с типом подписи connect
- их можно обновлять независимо, проверка типов пройдет, но код будет аварийно завершать работу при запускеИдеальный подход заключался бы в применении подписи, определенной один раз, как к send
, так и к connect
— возможно, с помощью дженериков. На данный момент я попробовал несколько подходов:
Я могу добиться желаемого поведения, используя только
class TypedSignal(Generic[P], Signal):
def send(self, *args: P.args, **kwargs: P.kwargs):
super().send(*args, **kwargs)
def connect(self, receiver: Callable[P, None]):
return super().connect(receiver=receiver)
user_update = TypedSignal[[User, str]]()
Этот тип правильно проверяет позиционные аргументы, но не поддерживает kwargs
из-за ограничений Callable
. Мне нужна kwargs
поддержка, поскольку blinker
использует kwargs
для каждого прошлого аргумента sender
.
Я могу добиться подсказки типа для sender
arg довольно просто, используя дженерики:
T = TypeVar("T")
class TypedSignal(Generic[T], Signal):
def send(self, sender: Type[T], **kwargs):
super(TypedSignal, self).send(sender)
def connect(self, receiver: Callable[[Type[T], ...], None]) -> Callable:
return super(TypedSignal, self).connect(receiver)
# used as
my_signal = TypedSignal[MyClass]()
сложнее всего, когда я хочу добавить проверку типов для kwargs
. Подход, который я пытаюсь реализовать, заключается в использовании вариативного обобщения и Unpack
вот так:
T = TypeVar("T")
KW = TypeVarTuple("KW")
class TypedSignal(Generic[T, Unpack[KW]], Signal):
def send(self, sender: Type[T], **kwargs: Unpack[Type[KW]]):
super(TypedSignal, self).send(sender)
def connect(self, receiver: Callable[[Type[T], Unpack[Type[KW]]], None]) -> Callable:
return super(TypedSignal, self).connect(receiver)
но mypy жалуется: error: Unpack item in ** argument must be a TypedDict
, что кажется странным, потому что эта ошибка выдается даже без использования обобщенного кода, не говоря уже о том, когда передается TypedDict
.
P = ParamSpec("P")
class TypedSignal(Generic[P], Signal):
def send(self, *args: P.args, **kwargs: P.kwargs) -> None:
super().send(*args, **kwargs)
def connect(self, receiver: Callable[P, None]):
return super().connect(receiver=receiver)
class Receiver(Protocol):
def __call__(self, sender: MyClass) -> None:
pass
update = TypedSignal[Receiver]()
@update.connect
def my_func(sender: MyClass) -> None:
pass
update.send(MyClass())
но mypy, похоже, оборачивает протокол, поэтому ожидает функцию, которая принимает протокол, и выдает следующие ошибки:
error: Argument 1 to "connect" of "TypedSignal" has incompatible type "Callable[[MyClass], None]"; expected "Callable[[Receiver], None]" [arg-type]
error: Argument 1 to "send" of "TypedSignal" has incompatible type "MyClass"; expected "Receiver" [arg-type]
Есть ли более простой способ сделать это? Возможно ли это при текущей типизации Python?
версия mypy — 1.9.0 — пробовал с более ранними версиями, и она полностью разбилась.
Ваши примеры, кажется, намекают на то, что вы хотите (1), но ваш TypedSignal.send
, похоже, не передает **kwargs
в super()
, поэтому - что касается вашей текущей реализации - вообще нет смысла проверять тип **kwargs
- вы можете также используйте typing.Any
или _typeshed.Unused
в качестве аннотации.
Я хочу создать подписи для каждого набираемого сигнала. например, у меня был бы сигнал user_update
, где отправителем всегда должен быть экземпляр User
, и у него должен быть определенный набор kwargs
, которые проверяются по типу - я добавлю подробности к вопросу
В вашем обновленном примере вы разрешаете time=34, audit = "user_initiated"
в качестве аргумента ключевого слова для send
. Как вы выбрали эти два ключевых аргумента, соответствующие send
? Например, где определена исходная подпись, чтобы send
совместимо с этой исходной подписью? Или вы ищете API (что-то вроде TypedSignal[Receiver, <KWArgs>]
), который позволит вам генерировать подтип Signal
, который автоматически проверяет, соответствует ли send
<KWArgs>
?
Да, как и второй! Я обновлю вопрос более описательным ответом
Я не думаю, что это возможно.
Вы можете комментировать **kwargs
тремя способами:
**kwargs: Foo
-> kwargs
имеет тип dict[str, Foo]
**kwargs: Unpack[TD]
где TD
— TypedDict -> kwargs
имеет тип TD
*args: P.args, **kwargs: P.kwargs
где P
— это ParamSpec -> kwargs
является общим для P
Таким образом, единственный способ использовать общий kwargs
— это использовать 1. с обычным TypeVar
, что означает, что все аргументы ключевых слов должны иметь совместимые типы, или использовать 3., но в настоящее время , который требует, чтобы у вас также были *args , что вы не имею.
Спасибо! Я также пробовал использовать ParamSpec
, преобразуя первый аргумент в *args
, но это не работает, потому что я не могу найти способ определить с его помощью kwargs
(аналогично этой проблеме: github.com/python/typing/issues/ 1405)
Добавлен еще один подход с использованием ParamSpec
и Protocol
— любопытно, стоит ли что-нибудь попробовать в этом направлении. Есть ли более простой способ использовать ParamSpec
здесь?
Я не уверен. Частично сложность заключается в том, что mypy имеет тенденцию отставать от новых функций набора текста, что иногда означает, что трудно отличить «использование неправильного набора текста», «это невозможно ввести в этой версии Python» и «это невозможно ввести в этой версии mypy». .
После долгих попыток найти ошибку я нашел относительно простое решение, хотя оно зависит от mypy_extensions
, которое устарело, поэтому оно может быть не совсем перспективным, хотя оно все еще работает в последней mypy
версии.
По сути, использование NamedArg mypy позволяет определить kwargs
в Callable
, что позволяет нам просто использовать ParamSpec
для решения этой проблемы:
class TypedSignal(Generic[P], Signal):
def send(self, *args: P.args, **kwargs: P.kwargs):
super().send(*args, **kwargs)
def connect(self, receiver: Callable[P, None]):
return super().connect(receiver=receiver)
user_update = TypedSignal[[User, NamedArg(str, "metadata")]]()
Это правильно проверяет типы вызовов, так что все, кроме приведенного ниже:
@user_update.connect
def my_func(sender: User, metadata: str) -> None:
pass
user_update.send(User(), metadata = "metadata")
выдаст ошибку.
Непонятно, что вы пытаетесь набрать статически. Я бы посоветовал вам переписать вопрос с точки зрения ожидаемых ошибок, которые mypy должен выдать, если вы ответите неправильно
**kwargs
. Основываясь на исходном коде блинкера, я вижу две возможные вещи, которые вы можете захотеть ввести check при передаче**kwargs
: (1) соответствие подписиblinker.base.Signal.send
и.connect
или (2) соответствие вызываемым объектам в.receivers: dict[IdentityType, t.Callable | annotatable_weakref]
, которые получат**kwargs
перешел вsend
.