Fast, simple object-to-object and broadcast signaling
npx @tessl/cli install tessl/pypi-blinker@1.9.00
# Blinker
1
2
Fast, simple object-to-object and broadcast signaling system that allows any number of interested parties to subscribe to events, or "signals". Blinker provides a lightweight, thread-safe implementation of the observer pattern for Python applications, enabling decoupled communication between different parts of an application.
3
4
## Package Information
5
6
- **Package Name**: blinker
7
- **Language**: Python
8
- **Installation**: `pip install blinker`
9
10
## Core Imports
11
12
```python
13
import blinker
14
```
15
16
Common usage imports:
17
18
```python
19
from blinker import signal, Signal, ANY
20
```
21
22
Import all public components:
23
24
```python
25
from blinker import signal, Signal, NamedSignal, Namespace, default_namespace, ANY
26
```
27
28
Complete typing imports for advanced usage:
29
30
```python
31
import typing as t
32
import collections.abc as c
33
import weakref
34
from contextlib import contextmanager
35
from functools import cached_property
36
from typing import Any, Callable, ClassVar, Coroutine, Generator, Hashable, TypeVar
37
38
# Type variables and aliases
39
F = t.TypeVar("F", bound=c.Callable[..., t.Any])
40
T = t.TypeVar("T")
41
```
42
43
## Basic Usage
44
45
```python
46
from blinker import signal
47
48
# Create a named signal
49
started = signal('round-started')
50
51
# Connect receivers
52
def each_round(sender, **kwargs):
53
print(f"Round {kwargs.get('round_num', '?')}")
54
55
def round_two_only(sender, **kwargs):
56
print("This is round two.")
57
58
# Connect receivers
59
started.connect(each_round)
60
started.connect(round_two_only, sender=2) # Only for sender=2
61
62
# Send signals
63
for round_num in range(1, 4):
64
started.send(round_num, round_num=round_num)
65
# Output:
66
# Round 1
67
# Round 2
68
# This is round two.
69
# Round 3
70
```
71
72
## Architecture
73
74
Blinker's core components work together to provide flexible signal dispatching:
75
76
- **Signal**: Core signal emitter that manages receivers and sends notifications
77
- **NamedSignal**: Signal with an assigned name for use in namespaces
78
- **Namespace**: Dictionary-like container for organizing related named signals
79
- **Weak References**: Automatic cleanup of receivers when they go out of scope
80
- **Threading**: Thread-safe operations for concurrent applications
81
82
The library supports both anonymous signals (Signal instances) and named signals (managed by Namespace), with automatic cleanup via weak references to prevent memory leaks.
83
84
## Capabilities
85
86
### Signal Creation and Management
87
88
Create and manage signal instances for event dispatching. Supports both anonymous signals and named signals organized in namespaces.
89
90
```python { .api }
91
class Signal:
92
"""A notification emitter."""
93
94
def __init__(self, doc: str | None = None): ...
95
96
def signal(name: str, doc: str | None = None) -> NamedSignal:
97
"""Return a NamedSignal in default_namespace with the given name."""
98
99
class NamedSignal(Signal):
100
"""A named generic notification emitter."""
101
102
def __init__(self, name: str, doc: str | None = None): ...
103
104
name: str # The name of this signal
105
106
class Namespace(dict[str, NamedSignal]):
107
"""A dict mapping names to signals."""
108
109
def signal(self, name: str, doc: str | None = None) -> NamedSignal:
110
"""Return the NamedSignal for the given name, creating it if required."""
111
```
112
113
### Receiver Connection
114
115
Connect callable receivers to signals with flexible sender filtering and automatic cleanup options.
116
117
```python { .api }
118
F = TypeVar("F", bound=Callable[..., Any])
119
120
def connect(self, receiver: F, sender: Any = ANY, weak: bool = True) -> F:
121
"""
122
Connect receiver to be called when the signal is sent by sender.
123
124
Parameters:
125
- receiver: The callable to call when send() is called with the given sender,
126
passing sender as a positional argument along with any extra keyword arguments
127
- sender: Any object or ANY. receiver will only be called when send() is called
128
with this sender. If ANY, the receiver will be called for any sender
129
- weak: Track the receiver with a weakref. The receiver will be automatically
130
disconnected when it is garbage collected. When connecting a receiver defined
131
within a function, set to False, otherwise it will be disconnected when the
132
function scope ends
133
134
Returns:
135
The receiver function (for decorator chaining)
136
"""
137
138
def connect_via(self, sender: Any, weak: bool = False) -> Callable[[F], F]:
139
"""
140
Connect the decorated function to be called when the signal is sent by sender.
141
142
The decorated function will be called when send() is called with the given sender,
143
passing sender as a positional argument along with any extra keyword arguments.
144
145
Parameters:
146
- sender: Any object or ANY. receiver will only be called when send() is called
147
with this sender. If ANY, the receiver will be called for any sender
148
- weak: Track the receiver with a weakref. The receiver will be automatically
149
disconnected when it is garbage collected. When connecting a receiver defined
150
within a function, set to False, otherwise it will be disconnected when the
151
function scope ends
152
153
Returns:
154
Decorator function
155
"""
156
157
@contextmanager
158
def connected_to(self, receiver: Callable[..., Any], sender: Any = ANY) -> Generator[None, None, None]:
159
"""
160
A context manager that temporarily connects receiver to the signal while a with
161
block executes. When the block exits, the receiver is disconnected. Useful for tests.
162
163
Parameters:
164
- receiver: The callable to call when send() is called with the given sender,
165
passing sender as a positional argument along with any extra keyword arguments
166
- sender: Any object or ANY. receiver will only be called when send() is called
167
with this sender. If ANY, the receiver will be called for any sender
168
169
Usage:
170
with signal.connected_to(my_receiver):
171
# receiver is connected
172
signal.send("test")
173
# receiver is automatically disconnected
174
"""
175
```
176
177
### Signal Sending
178
179
Send signals to connected receivers with support for both synchronous and asynchronous execution patterns.
180
181
```python { .api }
182
def send(
183
self,
184
sender: Any | None = None,
185
/,
186
*,
187
_async_wrapper: Callable[
188
[Callable[..., Coroutine[Any, Any, Any]]], Callable[..., Any]
189
] | None = None,
190
**kwargs: Any,
191
) -> list[tuple[Callable[..., Any], Any]]:
192
"""
193
Call all receivers that are connected to the given sender or ANY. Each receiver
194
is called with sender as a positional argument along with any extra keyword
195
arguments. Return a list of (receiver, return value) tuples.
196
197
The order receivers are called is undefined, but can be influenced by setting
198
set_class.
199
200
If a receiver raises an exception, that exception will propagate up. This makes
201
debugging straightforward, with an assumption that correctly implemented receivers
202
will not raise.
203
204
Parameters:
205
- sender: Call receivers connected to this sender, in addition to those connected to ANY
206
- _async_wrapper: Will be called on any receivers that are async coroutines to turn
207
them into sync callables. For example, could run the receiver with an event loop
208
- **kwargs: Extra keyword arguments to pass to each receiver
209
210
Returns:
211
List of (receiver, return_value) tuples
212
"""
213
214
async def send_async(
215
self,
216
sender: Any | None = None,
217
/,
218
*,
219
_sync_wrapper: Callable[
220
[Callable[..., Any]], Callable[..., Coroutine[Any, Any, Any]]
221
] | None = None,
222
**kwargs: Any,
223
) -> list[tuple[Callable[..., Any], Any]]:
224
"""
225
Await all receivers that are connected to the given sender or ANY. Each receiver
226
is called with sender as a positional argument along with any extra keyword
227
arguments. Return a list of (receiver, return value) tuples.
228
229
The order receivers are called is undefined, but can be influenced by setting
230
set_class.
231
232
If a receiver raises an exception, that exception will propagate up. This makes
233
debugging straightforward, with an assumption that correctly implemented receivers
234
will not raise.
235
236
Parameters:
237
- sender: Call receivers connected to this sender, in addition to those connected to ANY
238
- _sync_wrapper: Will be called on any receivers that are sync callables to turn them
239
into async coroutines. For example, could call the receiver in a thread
240
- **kwargs: Extra keyword arguments to pass to each receiver
241
242
Returns:
243
List of (receiver, return_value) tuples
244
"""
245
```
246
247
### Receiver Discovery and Disconnection
248
249
Query and manage connected receivers with support for weak reference cleanup and batch disconnection.
250
251
```python { .api }
252
def has_receivers_for(self, sender: Any) -> bool:
253
"""
254
Check if there is at least one receiver that will be called with the given sender.
255
A receiver connected to ANY will always be called, regardless of sender. Does not
256
check if weakly referenced receivers are still live. See receivers_for for a
257
stronger search.
258
259
Parameters:
260
- sender: Check for receivers connected to this sender, in addition to those
261
connected to ANY
262
263
Returns:
264
True if receivers exist for sender or ANY
265
"""
266
267
def receivers_for(self, sender: Any) -> Generator[Callable[..., Any], None, None]:
268
"""
269
Yield each receiver to be called for sender, in addition to those to be called
270
for ANY. Weakly referenced receivers that are not live will be disconnected and
271
skipped.
272
273
Parameters:
274
- sender: Yield receivers connected to this sender, in addition to those
275
connected to ANY
276
277
Yields:
278
Callable receivers (weak references resolved automatically)
279
"""
280
281
def disconnect(self, receiver: Callable[..., Any], sender: Any = ANY) -> None:
282
"""
283
Disconnect receiver from being called when the signal is sent by sender.
284
285
Parameters:
286
- receiver: A connected receiver callable
287
- sender: Disconnect from only this sender. By default, disconnect from all senders
288
"""
289
```
290
291
### Signal Control and Introspection
292
293
Control signal behavior and inspect signal state for debugging and testing purposes.
294
295
```python { .api }
296
@contextmanager
297
def muted(self) -> Generator[None, None, None]:
298
"""
299
A context manager that temporarily disables the signal. No receivers will be
300
called if the signal is sent, until the with block exits. Useful for tests.
301
302
Usage:
303
with signal.muted():
304
signal.send("test") # No receivers called
305
"""
306
307
# Instance attributes for introspection
308
receivers: dict[Any, weakref.ref[Callable[..., Any]] | Callable[..., Any]]
309
"""The map of connected receivers. Useful to quickly check if any receivers are
310
connected to the signal: if s.receivers:. The structure and data is not part of
311
the public API, but checking its boolean value is."""
312
313
is_muted: bool
314
"""Whether signal is currently muted."""
315
316
@cached_property
317
def receiver_connected(self) -> Signal:
318
"""Emitted at the end of each connect() call.
319
320
The signal sender is the signal instance, and the connect() arguments are passed
321
through: receiver, sender, and weak.
322
"""
323
324
@cached_property
325
def receiver_disconnected(self) -> Signal:
326
"""Emitted at the end of each disconnect() call.
327
328
The sender is the signal instance, and the disconnect() arguments are passed
329
through: receiver and sender.
330
331
This signal is emitted only when disconnect() is called explicitly. This signal
332
cannot be emitted by an automatic disconnect when a weakly referenced receiver or
333
sender goes out of scope, as the instance is no longer be available to be used as
334
the sender for this signal.
335
"""
336
337
# Class attributes for customization
338
set_class: type[set[Any]] = set
339
"""The set class to use for tracking connected receivers and senders. Python's set
340
is unordered. If receivers must be dispatched in the order they were connected, an
341
ordered set implementation can be used."""
342
343
ANY = ANY
344
"""An alias for the ANY sender symbol."""
345
346
# Testing and cleanup methods
347
def _clear_state(self) -> None:
348
"""Disconnect all receivers and senders. Useful for tests."""
349
350
def _cleanup_bookkeeping(self) -> None:
351
"""Prune unused sender/receiver bookkeeping. Not threadsafe.
352
353
Connecting & disconnecting leaves behind a small amount of bookkeeping data.
354
Typical workloads using Blinker, for example in most web apps, Flask, CLI scripts,
355
etc., are not adversely affected by this bookkeeping.
356
357
With a long-running process performing dynamic signal routing with high volume,
358
e.g. connecting to function closures, senders are all unique object instances.
359
Doing all of this over and over may cause memory usage to grow due to extraneous
360
bookkeeping. (An empty set for each stale sender/receiver pair.)
361
362
This method will prune that bookkeeping away, with the caveat that such pruning
363
is not threadsafe. The risk is that cleanup of a fully disconnected receiver/sender
364
pair occurs while another thread is connecting that same pair.
365
"""
366
```
367
368
## Constants and Utilities
369
370
```python { .api }
371
ANY: Symbol
372
"""Symbol for 'any sender' - receivers connected to ANY are called for all senders."""
373
374
default_namespace: Namespace
375
"""Default Namespace instance for creating named signals."""
376
377
class Symbol:
378
"""
379
A constant symbol, nicer than object(). Repeated calls return the same instance.
380
381
Usage:
382
>>> Symbol('foo') is Symbol('foo')
383
True
384
>>> Symbol('foo')
385
foo
386
"""
387
388
symbols: ClassVar[dict[str, Symbol]] = {}
389
390
def __new__(cls, name: str) -> Symbol: ...
391
def __init__(self, name: str) -> None: ...
392
def __repr__(self) -> str: ...
393
def __getnewargs__(self) -> tuple[Any, ...]: ...
394
395
name: str
396
397
def make_id(obj: object) -> Hashable:
398
"""
399
Get a stable identifier for a receiver or sender, to be used as a dict key
400
or in a set.
401
402
For bound methods, uses the id of the unbound function and instance.
403
For strings and ints, returns the value directly (stable hash).
404
For other types, assumes they are not hashable but will be the same instance.
405
"""
406
407
def make_ref(obj: T, callback: Callable[[ref[T]], None] | None = None) -> ref[T]:
408
"""
409
Create a weak reference to obj with optional callback.
410
411
For methods, uses WeakMethod for proper cleanup.
412
For other objects, uses standard weakref.ref.
413
414
Parameters:
415
- obj: Object to create weak reference to
416
- callback: Optional callback when reference is garbage collected
417
418
Returns:
419
Weak reference to the object
420
"""
421
```
422
423
## Usage Examples
424
425
### Named Signals with Namespaces
426
427
```python
428
from blinker import signal, Namespace
429
430
# Using default namespace
431
user_logged_in = signal('user-logged-in')
432
user_logged_out = signal('user-logged-out')
433
434
# Using custom namespace
435
app_signals = Namespace()
436
request_started = app_signals.signal('request-started')
437
request_finished = app_signals.signal('request-finished')
438
439
@request_started.connect
440
def log_request_start(sender, **kwargs):
441
print(f"Request started: {kwargs}")
442
```
443
444
### Weak vs Strong References
445
446
```python
447
from blinker import Signal
448
449
sig = Signal()
450
451
# Weak reference (default) - automatically disconnected when receiver is garbage collected
452
def temp_handler(sender, **kwargs):
453
print("Temporary handler")
454
455
sig.connect(temp_handler, weak=True) # weak=True is default
456
457
# Strong reference - receiver stays connected until explicitly disconnected
458
sig.connect(temp_handler, weak=False)
459
```
460
461
### Advanced Signal Patterns
462
463
```python
464
from blinker import Signal
465
466
# Signal with multiple senders
467
data_changed = Signal()
468
469
class Model:
470
def __init__(self, name):
471
self.name = name
472
473
def update(self, **kwargs):
474
# Send with self as sender
475
data_changed.send(self, model=self.name, **kwargs)
476
477
# Connect receiver for specific sender
478
model1 = Model("users")
479
model2 = Model("products")
480
481
@data_changed.connect_via(model1)
482
def handle_user_changes(sender, **kwargs):
483
print(f"User model updated: {kwargs}")
484
485
@data_changed.connect # Receives from ANY sender
486
def handle_all_changes(sender, **kwargs):
487
print(f"Model {sender.name} updated: {kwargs}")
488
```
489
490
### Async Signal Handling
491
492
```python
493
import asyncio
494
from blinker import Signal
495
496
async_signal = Signal()
497
498
async def async_handler(sender, **kwargs):
499
await asyncio.sleep(0.1)
500
return f"Processed {kwargs}"
501
502
def sync_handler(sender, **kwargs):
503
return f"Sync processed {kwargs}"
504
505
async_signal.connect(async_handler)
506
async_signal.connect(sync_handler)
507
508
# Send to async receivers
509
async def send_async_example():
510
def sync_to_async(func):
511
async def wrapper(*args, **kwargs):
512
return func(*args, **kwargs)
513
return wrapper
514
515
results = await async_signal.send_async(
516
"test_sender",
517
_sync_wrapper=sync_to_async,
518
data="example"
519
)
520
521
for receiver, result in results:
522
print(f"{receiver.__name__}: {result}")
523
524
# asyncio.run(send_async_example())
525
```
526
527
### Testing with Context Managers
528
529
```python
530
from blinker import signal
531
532
# Signal for testing
533
test_signal = signal('test-event')
534
535
def test_receiver(sender, **kwargs):
536
print(f"Test received: {kwargs}")
537
538
# Temporary connection for testing
539
with test_signal.connected_to(test_receiver):
540
test_signal.send("test_sender", message="hello")
541
542
# Muted signal for testing
543
with test_signal.muted():
544
test_signal.send("test_sender", message="ignored") # No output
545
```
546
547
### Error Handling and Edge Cases
548
549
```python
550
from blinker import Signal, signal
551
import asyncio
552
553
# Error propagation in signal sending
554
error_signal = Signal()
555
556
def failing_receiver(sender, **kwargs):
557
raise ValueError("Something went wrong")
558
559
def safe_receiver(sender, **kwargs):
560
print("Safe receiver called")
561
562
error_signal.connect(failing_receiver)
563
error_signal.connect(safe_receiver)
564
565
try:
566
# First receiver will raise, preventing safe_receiver from being called
567
error_signal.send("test")
568
except ValueError as e:
569
print(f"Caught error: {e}")
570
571
# Handle async receiver errors
572
async def failing_async_receiver(sender, **kwargs):
573
raise RuntimeError("Async error")
574
575
async_signal = Signal()
576
async_signal.connect(failing_async_receiver)
577
578
# Mixing sync and async receivers
579
def sync_wrapper(func):
580
async def wrapper(*args, **kwargs):
581
return func(*args, **kwargs)
582
return wrapper
583
584
try:
585
async def test_async_error():
586
await async_signal.send_async("test", _sync_wrapper=sync_wrapper)
587
except RuntimeError as e:
588
print(f"Async error: {e}")
589
590
# Weak reference cleanup
591
def create_temporary_receiver():
592
def temp_receiver(sender, **kwargs):
593
print("Temporary receiver")
594
return temp_receiver
595
596
cleanup_signal = Signal()
597
temp_func = create_temporary_receiver()
598
cleanup_signal.connect(temp_func, weak=True)
599
600
# After temp_func goes out of scope, it will be automatically disconnected
601
del temp_func
602
# Signal will automatically clean up the weak reference
603
```
604
605
### Advanced Signal Routing Patterns
606
607
```python
608
from blinker import Signal, Namespace
609
610
# Event routing with multiple namespaces
611
user_events = Namespace()
612
system_events = Namespace()
613
614
user_login = user_events.signal('login')
615
user_logout = user_events.signal('logout')
616
system_startup = system_events.signal('startup')
617
618
# Global event handler
619
def global_event_handler(sender, **kwargs):
620
print(f"Global: {sender} sent {kwargs}")
621
622
# Connect to multiple signals
623
for sig in [user_login, user_logout, system_startup]:
624
sig.connect(global_event_handler)
625
626
# Conditional receivers
627
class EventFilter:
628
def __init__(self, allowed_senders):
629
self.allowed_senders = set(allowed_senders)
630
631
def filtered_receiver(self, sender, **kwargs):
632
if sender in self.allowed_senders:
633
print(f"Filtered: {sender} -> {kwargs}")
634
# Else ignore
635
636
filter_handler = EventFilter(['admin', 'system'])
637
user_login.connect(filter_handler.filtered_receiver)
638
639
# Send events
640
user_login.send('admin', action='login') # Will be processed
641
user_login.send('guest', action='login') # Will be ignored
642
643
# Signal chaining - one signal triggers another
644
chain_start = Signal()
645
chain_middle = Signal()
646
chain_end = Signal()
647
648
@chain_start.connect
649
def start_to_middle(sender, **kwargs):
650
chain_middle.send(sender, **kwargs)
651
652
@chain_middle.connect
653
def middle_to_end(sender, **kwargs):
654
chain_end.send(sender, **kwargs)
655
656
@chain_end.connect
657
def end_handler(sender, **kwargs):
658
print(f"Chain completed: {kwargs}")
659
660
# Trigger the chain
661
chain_start.send("initiator", data="test")
662
```