signal - Callback system based on Signals and Slots, and “Delphi events”¶
Overview¶
This module provides two callback mechanisms:
Signals and Slots (
Signal,signaldecorator): Inspired by Qt, a signal can be connected to multiple slots (callbacks). When the signal is emitted, all connected slots are called. Return values from slots are ignored.Eventsockets (
eventsocketdecorator): Similar to Delphi events, an eventsocket holds a reference to a single slot (callback). Assigning a new slot replaces the previous one. Calling the eventsocket delegates the call directly to the connected slot. Return values are passed back from the slot.
In both cases, slots can be functions, instance/class methods, functools.partial
objects, or lambda functions. The inspect module is used to enforce signature
matching between the signal/eventsocket definition and the connected slots.
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
Signalclass to create signal instances for direct use.The
signaldecorator 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:
objectHandles connections between a signal and multiple slots (callbacks).
When the signal is emitted, all connected slots are called with the provided arguments. Return values from slots are ignored.
- Parameters:
signature (Signature) – The
inspect.Signatureobject defining the expected parameters for connected 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).- _kw_test(sig: Signature) bool[source]¶
Internal helper to check if the only difference between
sigandself._sigis the presence of extra keyword arguments with default values insig.
- connect(slot: Callable) None[source]¶
Connect a callable slot to this signal.
The slot will be called whenever the signal is emitted.
- Parameters:
slot (Callable) – The callable (function, method, lambda, partial) to connect. Its signature must match the signal’s signature (see class docs).
- Raises:
ValueError – If
slotis not callable or if its signature does not match the signal’s signature (parameters and their types/names/kinds, excluding return type and allowing extra keyword args with defaults).- Return type:
None
Storage Note:
Regular functions are stored using
weakref.refto avoid preventing garbage collection if the signal outlives the function’s scope.Instance methods are stored using a
WeakKeyDictionarymapping the instance (weakly) to the unbound function.Lambdas and
functools.partialobjects are stored directly, as weak references to them are often problematic.
- disconnect(slot: Callable) None[source]¶
Disconnect a previously connected slot from the signal.
Attempts to remove the specified slot. Does nothing if the slot is not currently connected or not callable.
- emit(*args, **kwargs) None[source]¶
Emit the signal, calling all connected slots with the given arguments.
Does nothing if
self.blockis True. Handles different storage types (functions, methods, lambdas, partials) correctly.- Parameters:
*args – Positional arguments to pass to the slots.
**kwargs – Keyword arguments to pass to the slots.
- Return type:
None
Decorators¶
- class firebird.base.signal.signal(fget, doc=None)[source]¶
Bases:
objectDecorator to define a
Signalinstance as a read-only property on a class.The decorated function’s signature (excluding ‘self’) defines the required signature for slots connecting to this signal. The body of the decorated function is never executed.
A unique
Signalinstance is lazily created for each object instance the first time the signal property is accessed.Example:
class MyClass: @signal def value_changed(self, new_value: int): # This signature dictates slots must accept (new_value: int) pass # Body is ignored instance = MyClass() instance.value_changed.connect(my_slot_function) instance.value_changed.emit(10)
- class firebird.base.signal.eventsocket(fget: Callable, doc: str | None = None)[source]¶
Bases:
objectDecorator defining a property that holds a single callable slot (like a Delphi event).
Assigning a callable (function, method, lambda, partial) to the property connects it as the event handler. Assigning
Nonedisconnects the current handler. Calling the property like a method invokes the currently connected handler, passing through arguments and returning its result.The decorated function’s signature (excluding ‘self’ but including the return type annotation) defines the required signature for the assigned slot.
Use the
is_set()method on the property access to check if a handler is assigned.Example:
class MyComponent: @eventsocket def on_update(self, data: dict) -> None: # Slots must match (data: dict) -> None pass def do_update(self): data = {'value': 1} if self.on_update.is_set(): self.on_update(data) # Call the assigned handler def my_handler(data: dict): print(f"Handler received: {data}") comp = MyComponent() comp.on_update = my_handler # Connect handler comp.do_update() # Calls my_handler comp.on_update = None # Disconnect handler
Important
Signature matching includes parameter names, types, kinds, order, and the return type annotation. The only exception is that the assigned slot may have extra keyword arguments if they have default values.
- Storage Note:
Similar to
Signal, functions and methods are stored using weak references where appropriate to prevent memory leaks. Lambdas/partials are stored directly.
- Parameters:
fget (Callable) –
doc (str | None) –