signal - Callback system based on Signals and Slots, and “Delphi events”

Overview

This module provides two callback mechanisms:

  1. Signals and Slots (Signal, signal decorator): 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.

  2. Eventsockets (eventsocket decorator): 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 Signal class to create signal instances for direct use.

  • The signal decorator to define signals on classes. This decorator works like builtin property (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

Handles 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.Signature object 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).

__call__(*args, **kwargs)[source]

Shortcut for emit(*args, **kwargs).

_kw_test(sig: Signature) bool[source]

Internal helper to check if the only difference between sig and self._sig is the presence of extra keyword arguments with default values in sig.

Parameters:

sig (Signature) –

Return type:

bool

clear() None[source]

Clears the signal of all connected slots.

Return type:

None

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 slot is 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.ref to avoid preventing garbage collection if the signal outlives the function’s scope.

  • Instance methods are stored using a WeakKeyDictionary mapping the instance (weakly) to the unbound function.

  • Lambdas and functools.partial objects 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.

Parameters:

slot (Callable) – The callable that was previously passed to connect().

Return type:

None

emit(*args, **kwargs) None[source]

Emit the signal, calling all connected slots with the given arguments.

Does nothing if self.block is 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

block: bool

Toggle to block / unblock signal transmission

class firebird.base.signal._EventSocket(slot: Callable | None = None)[source]

Bases: object

Internal EventSocket handler.

Parameters:

slot (Callable | None) –

is_set() bool[source]

Returns True if slot is assigned to eventsocket.

Return type:

bool

Decorators

class firebird.base.signal.signal(fget, doc=None)[source]

Bases: object

Decorator to define a Signal instance 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 Signal instance 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: object

Decorator 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 None disconnects 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) –