|
| 1 | +.. module:: firebird.base.signal |
| 2 | + :synopsis: Callback system based on Signals and Slots, and "Delphi events" |
| 3 | + |
| 4 | +######################################################################## |
| 5 | +signal - Callback system based on Signals and Slots, and "Delphi events" |
| 6 | +######################################################################## |
| 7 | + |
| 8 | +Overview |
| 9 | +======== |
| 10 | + |
| 11 | +This module provides two callback mechanisms: one based on signals and slots similar to Qt |
| 12 | +signal/slot, and second based on optional method delegation similar to events in Delphi. |
| 13 | + |
| 14 | +In both cases, the callback callables could be functions, instance or class methods, |
| 15 | +partials and lambda functions. The `inspect` module is used to define the signature for |
| 16 | +callbacks, and to validate that only compatible callables are assigned. |
| 17 | + |
| 18 | +.. important:: |
| 19 | + |
| 20 | + All type annotations in signatures are significant, so callbacks must have exactly the |
| 21 | + same annotations as signatures used by signals or events. The sole exception are excess |
| 22 | + keyword arguments with default values defined on connected callable. |
| 23 | + |
| 24 | +.. tip:: |
| 25 | + |
| 26 | + You may use `functools.partial` to adapt callable with different signatures. However, |
| 27 | + you can "mask" only keyword arguments (without default) and leading positional arguments |
| 28 | + (as any positional argument binded by name will not mask-out parameter from signature |
| 29 | + introspection). |
| 30 | + |
| 31 | +Signals and Slots |
| 32 | +----------------- |
| 33 | + |
| 34 | +Signals and slots are suitable for 1:N notification schemes. The `Signal` works as a point |
| 35 | +to which one or more Slots could be connected. When `Signal` is "emitted", all connected |
| 36 | +slots are called (executed). It's possible to pass parameters to slot callables, but any |
| 37 | +value returned by slot callable is ignored. The Signal contructor takes `inspect.Signature` |
| 38 | +argument that defines the required signature that callables (slots) must have to connect |
| 39 | +to this signal. |
| 40 | + |
| 41 | +This mechanism is provided in two forms: |
| 42 | + |
| 43 | +- The `Signal` class to create signal instances for direct use. |
| 44 | +- The `signal` decorator to define signals on classes. This decorator works like builtin |
| 45 | + `property` (without setter and deleter), where the 'getter' method is used only to define |
| 46 | + the signature required for slots. |
| 47 | + |
| 48 | +**Example:** |
| 49 | + |
| 50 | +.. code-block:: |
| 51 | + |
| 52 | + class Emitor: |
| 53 | + def __init__(self, name: str): |
| 54 | + self.name = name |
| 55 | + def showtime(self): |
| 56 | + self.signal_a(self, 'They Live!', 42) |
| 57 | + @signal |
| 58 | + def signal_a(self, source: Emitor, msg: str, value: int) -> None: |
| 59 | + "Documentation for signal" |
| 60 | + |
| 61 | + class Receptor: |
| 62 | + def __init__(self, name: str): |
| 63 | + self.name = name |
| 64 | + def on_signal_a(self, source: Emitor, msg: str, value: int) -> None: |
| 65 | + print(f"{self.name} received signal from {source.name} ({msg=}, {value=})") |
| 66 | + @classmethod |
| 67 | + def cls_on_signal_a(cls, source: Emitor, msg: str, value: int) -> None: |
| 68 | + print(f"{cls.__name__} received signal from {source.name} ({msg=}, {value=})") |
| 69 | + |
| 70 | + def on_signal_a(source: Emitor, msg: str, value: int): |
| 71 | + print(f"Function 'on_signal_a' received signal from {source.name} ({msg=}, {value=})") |
| 72 | + |
| 73 | + e1 = Emitor('e1') |
| 74 | + e2 = Emitor('e2') |
| 75 | + r1 = Receptor('r1') |
| 76 | + r2 = Receptor('r2') |
| 77 | + # |
| 78 | + e1.signal_a.connect(r1.on_signal_a) |
| 79 | + e1.signal_a.connect(r2.on_signal_a) |
| 80 | + e1.signal_a.connect(r1.cls_on_signal_a) |
| 81 | + e2.signal_a.connect(on_signal_a) |
| 82 | + e2.signal_a.connect(r2.on_signal_a) |
| 83 | + # |
| 84 | + e1.showtime() |
| 85 | + e2.showtime() |
| 86 | + |
| 87 | +| |
| 88 | + |
| 89 | +**Output from sample code**:: |
| 90 | + |
| 91 | + r1 received signal from e1 (msg='They Live!', value=42) |
| 92 | + r2 received signal from e1 (msg='They Live!', value=42) |
| 93 | + Receptor received signal from e1 (msg='They Live!', value=42) |
| 94 | + Function 'on_signal_a' received signal from e2 (msg='They Live!', value=42) |
| 95 | + r2 received signal from e2 (msg='They Live!', value=42) |
| 96 | + |
| 97 | + |
| 98 | +Events |
| 99 | +------ |
| 100 | + |
| 101 | +Events are suitable for optional callbacks that delegate some functionality to other class |
| 102 | +or function. |
| 103 | + |
| 104 | +The 'event' works as a point to which one 'slot' could be connected. The event itself |
| 105 | +acts as callable, that executes the connected slot (if assigned). Events may have parameters |
| 106 | +and return values. Events could be defined only on classes using `eventsocket` decorator, |
| 107 | +that works like builtin `property` (without deleter), where the 'getter' method is used only |
| 108 | +to define the signature required for slot, and 'setter' is used to assign the callable. |
| 109 | +To disconnect the callable from event, simply assign None to the event. |
| 110 | + |
| 111 | +**Example:** |
| 112 | + |
| 113 | +.. code-block:: |
| 114 | + |
| 115 | + class Component: |
| 116 | + def __init__(self, name: str): |
| 117 | + self.name = name |
| 118 | + @eventsocket |
| 119 | + def on_init(self, source: Component, arg: str) -> bool: |
| 120 | + "Documentation for event" |
| 121 | + @eventsocket |
| 122 | + def on_exit(self, source: Component) -> None: |
| 123 | + "Documentation for event" |
| 124 | + def showtime(self) -> None: |
| 125 | + print(f"{self.name}.on_init handler is {'SET' if self.on_init.is_set() else 'NOT SET'}") |
| 126 | + print(f"{self.name}.on_exit handler is {'SET' if self.on_exit.is_set() else 'NOT SET'}") |
| 127 | + print("Event handler returned", self.on_init(self, 'argument')) |
| 128 | + print(f"{self.name} does something...") |
| 129 | + self.on_exit(self) |
| 130 | + |
| 131 | + class Container: |
| 132 | + def __init__(self): |
| 133 | + self.c1 = Component('C1') |
| 134 | + self.c1.on_init = self.event_init |
| 135 | + self.c2 = Component('C2') |
| 136 | + self.c2.on_init = self.event_init |
| 137 | + self.c2.on_exit = self.event_exit |
| 138 | + self.c3 = Component('C3') |
| 139 | + self.c3.on_exit = self.event_exit |
| 140 | + def event_init(self, source: Component, arg: str) -> bool: |
| 141 | + print(f"Handlig {source.name}.on_init({arg=})") |
| 142 | + return source is self.c2 |
| 143 | + def event_exit(self, source: Component) -> None: |
| 144 | + print(f"Handlig {source.name}.on_exit()") |
| 145 | + def showtime(self) -> None: |
| 146 | + self.c1.showtime() |
| 147 | + self.c2.showtime() |
| 148 | + self.c3.showtime() |
| 149 | + |
| 150 | + cn = Container() |
| 151 | + cn.showtime() |
| 152 | + |
| 153 | +| |
| 154 | + |
| 155 | +**Output from sample code**:: |
| 156 | + |
| 157 | + C1.on_init handler is SET |
| 158 | + C1.on_exit handler is NOT SET |
| 159 | + Handlig C1.on_init(arg='argument') |
| 160 | + Event handler returned False |
| 161 | + C1 does something... |
| 162 | + C2.on_init handler is SET |
| 163 | + C2.on_exit handler is SET |
| 164 | + Handlig C2.on_init(arg='argument') |
| 165 | + Event handler returned True |
| 166 | + C2 does something... |
| 167 | + Handlig C2.on_exit() |
| 168 | + C3.on_init handler is NOT SET |
| 169 | + C3.on_exit handler is SET |
| 170 | + Event handler returned None |
| 171 | + C3 does something... |
| 172 | + Handlig C3.on_exit() |
| 173 | + |
| 174 | +Classes |
| 175 | +======= |
| 176 | + |
| 177 | +Signal |
| 178 | +------ |
| 179 | +.. autoclass:: Signal |
| 180 | + |
| 181 | +_EventSocket |
| 182 | +------------ |
| 183 | +.. autoclass:: _EventSocket |
| 184 | + |
| 185 | +Decorators |
| 186 | +========== |
| 187 | + |
| 188 | +signal |
| 189 | +------ |
| 190 | +.. autoclass:: signal |
| 191 | + |
| 192 | +eventsocket |
| 193 | +----------- |
| 194 | +.. autoclass:: eventsocket |
0 commit comments