signal - Callback system based on Signals and Slots, and “Delphi events”¶
Overview¶
This module provides two callback mechanisms: one based on signals and slots similar to Qt signal/slot, and second based on optional method delegation similar to events in Delphi.
In both cases, the callback callables could be functions, instance or class methods,
partials and lambda functions. The inspect
module is used to define the signature for
callbacks, and to validate that only compatible callables are assigned.
Important
All type annotations in signatures are significant, so callbacks must have exactly the same annotations as signatures used by signals or events. The sole exception are excess keyword arguments with default values defined on connected callable.
Tip
You may use functools.partial
to adapt callable with different signatures. However,
you can “mask” only keyword arguments (without default) and leading positional arguments
(as any positional argument binded by name will not mask-out parameter from signature
introspection).
Signals and Slots¶
Signals and slots are suitable for 1:N notification schemes. The Signal
works as a point
to which one or more Slots could be connected. When Signal
is “emitted”, all connected
slots are called (executed). It’s possible to pass parameters to slot callables, but any
value returned by slot callable is ignored. The Signal contructor takes inspect.Signature
argument that defines the required signature that callables (slots) must have to connect
to this signal.
This mechanism is provided in two forms:
The
Signal
class to create signal instances for direct use.The
signal
decorator to define signals on classes. This decorator works like builtinproperty
(without setter and deleter), where the ‘getter’ method is used only to define the signature required for slots.
Example:
class Emitor:
def __init__(self, name: str):
self.name = name
def showtime(self):
self.signal_a(self, 'They Live!', 42)
@signal
def signal_a(self, source: Emitor, msg: str, value: int) -> None:
"Documentation for signal"
class Receptor:
def __init__(self, name: str):
self.name = name
def on_signal_a(self, source: Emitor, msg: str, value: int) -> None:
print(f"{self.name} received signal from {source.name} ({msg=}, {value=})")
@classmethod
def cls_on_signal_a(cls, source: Emitor, msg: str, value: int) -> None:
print(f"{cls.__name__} received signal from {source.name} ({msg=}, {value=})")
def on_signal_a(source: Emitor, msg: str, value: int):
print(f"Function 'on_signal_a' received signal from {source.name} ({msg=}, {value=})")
e1 = Emitor('e1')
e2 = Emitor('e2')
r1 = Receptor('r1')
r2 = Receptor('r2')
#
e1.signal_a.connect(r1.on_signal_a)
e1.signal_a.connect(r2.on_signal_a)
e1.signal_a.connect(r1.cls_on_signal_a)
e2.signal_a.connect(on_signal_a)
e2.signal_a.connect(r2.on_signal_a)
#
e1.showtime()
e2.showtime()
Output from sample code:
r1 received signal from e1 (msg='They Live!', value=42)
r2 received signal from e1 (msg='They Live!', value=42)
Receptor received signal from e1 (msg='They Live!', value=42)
Function 'on_signal_a' received signal from e2 (msg='They Live!', value=42)
r2 received signal from e2 (msg='They Live!', value=42)
Events¶
Events are suitable for optional callbacks that delegate some functionality to other class or function.
The ‘event’ works as a point to which one ‘slot’ could be connected. The event itself
acts as callable, that executes the connected slot (if assigned). Events may have parameters
and return values. Events could be defined only on classes using eventsocket
decorator,
that works like builtin property
(without deleter), where the ‘getter’ method is used only
to define the signature required for slot, and ‘setter’ is used to assign the callable.
To disconnect the callable from event, simply assign None to the event.
Example:
class Component:
def __init__(self, name: str):
self.name = name
@eventsocket
def on_init(self, source: Component, arg: str) -> bool:
"Documentation for event"
@eventsocket
def on_exit(self, source: Component) -> None:
"Documentation for event"
def showtime(self) -> None:
print(f"{self.name}.on_init handler is {'SET' if self.on_init.is_set() else 'NOT SET'}")
print(f"{self.name}.on_exit handler is {'SET' if self.on_exit.is_set() else 'NOT SET'}")
print("Event handler returned", self.on_init(self, 'argument'))
print(f"{self.name} does something...")
self.on_exit(self)
class Container:
def __init__(self):
self.c1 = Component('C1')
self.c1.on_init = self.event_init
self.c2 = Component('C2')
self.c2.on_init = self.event_init
self.c2.on_exit = self.event_exit
self.c3 = Component('C3')
self.c3.on_exit = self.event_exit
def event_init(self, source: Component, arg: str) -> bool:
print(f"Handlig {source.name}.on_init({arg=})")
return source is self.c2
def event_exit(self, source: Component) -> None:
print(f"Handlig {source.name}.on_exit()")
def showtime(self) -> None:
self.c1.showtime()
self.c2.showtime()
self.c3.showtime()
cn = Container()
cn.showtime()
Output from sample code:
C1.on_init handler is SET
C1.on_exit handler is NOT SET
Handlig C1.on_init(arg='argument')
Event handler returned False
C1 does something...
C2.on_init handler is SET
C2.on_exit handler is SET
Handlig C2.on_init(arg='argument')
Event handler returned True
C2 does something...
Handlig C2.on_exit()
C3.on_init handler is NOT SET
C3.on_exit handler is SET
Event handler returned None
C3 does something...
Handlig C3.on_exit()
Classes¶
- class firebird.base.signal.Signal(signature: Signature)[source]¶
Bases:
object
The Signal is the core object that handles connection with slots and emission.
Slots are callables that are called when signal is emitted (the return value is ignored). They could be functions, instance or class methods, partials and lambda functions.
- Parameters:
signature (Signature) –
- __init__(signature: Signature)[source]¶
- Parameters:
signature (Signature) – Signature for slots.
Important
Only slots that match the signature could be connected to signal. The check is performed only on parameters, and not on return value type (as signals does not have/ignore return values).
The match must be exact, including type annotations, parameter names, order, parameter type etc. The sole exception to this rule are excess slot keyword arguments with default values.
Note
Signal functions with signatures different from signal could be adapted using
functools.partial
. However, you can “mask” only keyword arguments (without default) and leading positional arguments (as any positional argument binded by name will not mask-out parameter from signature introspection).
- connect(slot: Callable) None [source]¶
Connects the signal to callable that will receive the signal when emitted.
- Parameters:
slot (Callable) – Callable with signature that match the signature defined for signal.
- Raises:
ValueError – When callable signature does not match the signature of signal.
- Return type:
None
Decorators¶
- class firebird.base.signal.signal(fget, doc=None)[source]¶
Bases:
object
Decorator that defines signal as read-only property. The decorated function/method is used to define the signature required for slots to successfuly register to signal, and does not need to have a body as it’s never executed.
The usage is similar to builtin
property
, except that it does not support custom setter and deleter.
- class firebird.base.signal.eventsocket(fget, doc=None)[source]¶
Bases:
object
The
eventsocket
is like read/write property that handles connection and call delegation to single slot. It basically works like Delphi event.The Slot could be function, instance or class method, partial and lambda function.
Important
Only slot that match the signature could be connected to eventsocket. The check is performed on parameters and return value type (as events may have return values).
The match must be exact, including type annotations, parameter names, order, parameter type etc. The sole exception to this rule are excess slot keyword arguments with default values.
Note
Eventsocket functions with signatures different from event could be adapted using
functools.partial
. However, you can “mask” only keyword arguments (without default) and leading positional arguments (as any positional argument binded by name will not mask-out parameter from signature introspection).To call the event, simply call the eventsocket property with required parameters. To check whether slot is assigned to eventsocket, use
is_set()
bool function defined on property.