or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

index.md

index.mddocs/

0

# 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

```