I have the following type parametrized signal:
from __future__ import annotations
from typing_extensions import Callable, TypeVarTuple, Generic, Unpack, List, TypeVar
VarArgs = TypeVarTuple('VarArgs')
class Signal(Generic[Unpack[VarArgs]]):
def __init__(self):
self.functions: List[Callable[..., None]] = []
"""
Simple mechanism that allows abstracted invocation of callbacks. Multiple callbacks can be attached to a signal
so that they are all called when the signal is emitted.
"""
def connect(self, function: Callable[..., None]):
"""
Add a callback to this Signal
:param function: callback to call when emited
"""
self.functions.append(function)
def emit(self, *args: Unpack[VarArgs]):
"""
Call all callbacks with the arguments passed
:param args: arguments for the signal, must be the same type as type parameter
"""
for function in self.functions:
if args:
function(*args)
else:
function()
def main():
def signalEmptyCall():
print("Hello!")
signal: Signal[()] = Signal[()]()
signal.connect(signalEmptyCall)
signal.emit()
if __name__ == '__main__':
main()
The problem is that I cannot have empty argument list in the Callable:
signal: Signal[()] = Signal[()]()
In Python 3.10 I get this error:
Traceback (most recent call last):
File ".../main.py", line 48, in <module>
main()
File ".../main.py", line 40, in main
signal: Signal[()] = Signal[()]()
File "/usr/lib/python3.10/typing.py", line 312, in inner
return func(*args, **kwds)
File "/usr/lib/python3.10/typing.py", line 1328, in __class_getitem__
raise TypeError(
TypeError: Parameter list to Signal[...] cannot be empty
Reading the code in typing.py I find that Tuple is a special case (if not params and cls is not Tuple:
where it doesn't crash for Tuple[()]
), but TypeVarTuple has no special case, but is semantically a very similar thing.
Doing signal: Signal = Signal()
works, but it seems like I am breaking something because PyCharm warns that Expected type '(Any) -> None', got '() -> None' instead
.
How can I fix this?
Thanks for your time.
As @MikeWilliamson pointed out this seems to be a problem in python3.10 because of an edge case of how types are implemented. And as @InSync pointed out ParamSpec
should be used for Callback
parameters.
The simplest solution here seems to be to have two Signals, one generic, and the other one to deal with this special case.
General case:
from __future__ import annotations
from typing_extensions import Callable, Generic, List, ParamSpec
VarArgs = ParamSpec('VarArgs')
class Signal(Generic[VarArgs]):
"""
Simple mechanism that allows abstracted invocation of callbacks. Multiple callbacks can be attached to a signal
so that they are all called when the signal is emitted.
"""
def __init__(self) -> None:
self.functions: List[Callable[VarArgs, None]] = []
"""
Simple mechanism that allows abstracted invocation of callbacks. Multiple callbacks can be attached to a signal
so that they are all called when the signal is emitted.
"""
def connect(self, function: Callable[VarArgs, None]) -> None:
"""
Add a callback to this Signal
:param function: callback to call when emited
"""
self.functions.append(function)
def emit(self, *args: VarArgs.args, **kwargs: VarArgs.kwargs) -> None:
"""
Call all callbacks with the arguments passed
:param args: arguments for the signal, must be the same type as type parameter
"""
for function in self.functions:
function(*args, **kwargs)
Special case:
from __future__ import annotations
from typing_extensions import List, Callable
class SimpleSignal:
"""
HACK for signals with no parameters. See Signal for a generic version.
"""
def __init__(self) -> None:
self.functions: List[Callable[[], None]] = []
"""
Simple mechanism that allows abstracted invocation of callbacks. Multiple callbacks can be attached to a signal
so that they are all called when the signal is emitted.
"""
def connect(self, function: Callable[[], None]) -> None:
"""
Add a callback to this SimpleSignal
:param function: callback to call when emited
"""
self.functions.append(function)
def emit(self) -> None:
"""
Call all callbacks
"""
for function in self.functions:
function()