Initial commit
This commit is contained in:
586
skills/python3-development/references/modern-modules/blinker.md
Normal file
586
skills/python3-development/references/modern-modules/blinker.md
Normal file
@@ -0,0 +1,586 @@
|
||||
---
|
||||
title: "Blinker: Fast Signal/Event Dispatching System"
|
||||
library_name: blinker
|
||||
pypi_package: blinker
|
||||
category: event-system
|
||||
python_compatibility: "3.8+"
|
||||
last_updated: "2025-11-02"
|
||||
official_docs: "https://blinker.readthedocs.io"
|
||||
official_repository: "https://github.com/pallets-eco/blinker"
|
||||
maintenance_status: "active"
|
||||
---
|
||||
|
||||
# Blinker: Fast Signal/Event Dispatching System
|
||||
|
||||
## Official Information
|
||||
|
||||
**Repository:** <https://github.com/pallets-eco/blinker> **PyPI Package:** blinker **Current Version:** 1.9.0 (Released 2024-11-08) **Official Documentation:** <https://blinker.readthedocs.io/> **License:** MIT License **Maintenance Status:** Active (Pallets Community Ecosystem)
|
||||
|
||||
@source <https://github.com/pallets-eco/blinker> @source <https://blinker.readthedocs.io/> @source <https://pypi.org/project/blinker/>
|
||||
|
||||
## Core Purpose
|
||||
|
||||
Blinker provides a fast dispatching system that allows any number of interested parties to subscribe to events or "signals". It implements the Observer pattern with a clean, Pythonic API.
|
||||
|
||||
### Problem Space
|
||||
|
||||
Without blinker, you would need to manually implement:
|
||||
|
||||
- Global event registries for decoupled components
|
||||
- Weak reference management for automatic cleanup
|
||||
- Thread-safe event dispatching
|
||||
- Sender-specific event filtering
|
||||
- Return value collection from multiple handlers
|
||||
|
||||
### When to Use Blinker
|
||||
|
||||
**Use blinker when:**
|
||||
|
||||
- Building plugin systems that need event hooks
|
||||
- Implementing application lifecycle hooks (like Flask)
|
||||
- Creating decoupled components that communicate via events
|
||||
- Building event-driven architectures within a single process
|
||||
- Need multiple independent handlers for the same event
|
||||
- Want automatic cleanup via weak references
|
||||
|
||||
**What you would be "reinventing the wheel" without it:**
|
||||
|
||||
- Observer/subscriber pattern implementation
|
||||
- Named signal registries for plugin communication
|
||||
- Weak reference management for receivers
|
||||
- Thread-safe signal dispatching
|
||||
- Sender filtering and context passing
|
||||
|
||||
## Python Version Compatibility
|
||||
|
||||
**Minimum Python Version:** 3.9+ **Python 3.11:** Fully compatible **Python 3.12:** Fully compatible **Python 3.13:** Fully compatible **Python 3.14:** Expected to be compatible
|
||||
|
||||
@source <https://blinker.readthedocs.io/en/stable/>
|
||||
|
||||
### Thread Safety
|
||||
|
||||
Blinker signals are thread-safe. The library uses weak references for automatic cleanup and properly handles concurrent signal emission and subscription.
|
||||
|
||||
## Integration Patterns
|
||||
|
||||
### Flask Ecosystem Integration
|
||||
|
||||
Flask uses blinker as its signal system foundation. Flask provides built-in signals like:
|
||||
|
||||
- `request_started` - Before request processing begins
|
||||
- `request_finished` - After response is constructed
|
||||
- `template_rendered` - When template is rendered
|
||||
- `request_tearing_down` - During request teardown
|
||||
|
||||
@source <https://flask.palletsprojects.com/en/latest/signals/>
|
||||
|
||||
**Example Flask Signal Usage:**
|
||||
|
||||
```python
|
||||
from flask import template_rendered
|
||||
|
||||
def log_template_renders(sender, template, context, **extra):
|
||||
sender.logger.info(
|
||||
f"Rendered {template.name} with context {context}"
|
||||
)
|
||||
|
||||
template_rendered.connect(log_template_renders, app)
|
||||
```
|
||||
|
||||
### Event-Driven Architecture
|
||||
|
||||
Blinker excels at creating loosely coupled components:
|
||||
|
||||
```python
|
||||
from blinker import Namespace
|
||||
|
||||
# Create isolated namespace for your application
|
||||
app_signals = Namespace()
|
||||
|
||||
# Define signals
|
||||
user_logged_in = app_signals.signal('user-logged-in')
|
||||
data_updated = app_signals.signal('data-updated')
|
||||
|
||||
# Multiple handlers can subscribe
|
||||
@user_logged_in.connect
|
||||
def update_last_login(sender, **kwargs):
|
||||
user_id = kwargs.get('user_id')
|
||||
# Update database
|
||||
|
||||
@user_logged_in.connect
|
||||
def send_login_notification(sender, **kwargs):
|
||||
# Send email notification
|
||||
pass
|
||||
|
||||
# Emit signal
|
||||
user_logged_in.send(app, user_id=123, ip_address='192.168.1.1')
|
||||
```
|
||||
|
||||
### Plugin Systems
|
||||
|
||||
```python
|
||||
from blinker import signal
|
||||
|
||||
# Core application defines hook points
|
||||
plugin_loaded = signal('plugin-loaded')
|
||||
before_process = signal('before-process')
|
||||
after_process = signal('after-process')
|
||||
|
||||
# Plugins subscribe to hooks
|
||||
@before_process.connect
|
||||
def plugin_preprocess(sender, data):
|
||||
# Plugin modifies data before processing
|
||||
return data
|
||||
|
||||
# Application emits signals at hook points
|
||||
results = before_process.send(self, data=input_data)
|
||||
for receiver, result in results:
|
||||
if result is not None:
|
||||
input_data = result
|
||||
```
|
||||
|
||||
## Real-World Examples
|
||||
|
||||
### Example 1: Flask Request Monitoring
|
||||
|
||||
@source <https://github.com/instana/python-sensor> (Flask instrumentation with blinker)
|
||||
|
||||
```python
|
||||
from flask import request_started, request_finished
|
||||
import time
|
||||
|
||||
request_times = {}
|
||||
|
||||
def track_request_start(sender, **extra):
|
||||
request_times[id(extra)] = time.time()
|
||||
|
||||
def track_request_end(sender, response, **extra):
|
||||
duration = time.time() - request_times.pop(id(extra), time.time())
|
||||
sender.logger.info(f"Request took {duration:.2f}s")
|
||||
|
||||
request_started.connect(track_request_start)
|
||||
request_finished.connect(track_request_end)
|
||||
```
|
||||
|
||||
### Example 2: Model Save Hooks
|
||||
|
||||
@source <https://blinker.readthedocs.io/>
|
||||
|
||||
```python
|
||||
from blinker import Namespace
|
||||
|
||||
model_signals = Namespace()
|
||||
model_saved = model_signals.signal('model-saved')
|
||||
|
||||
class Model:
|
||||
def save(self):
|
||||
# Save to database
|
||||
self._persist()
|
||||
# Emit signal for observers
|
||||
model_saved.send(self, model_type=self.__class__.__name__)
|
||||
|
||||
# Cache invalidation handler
|
||||
@model_saved.connect
|
||||
def invalidate_cache(sender, **kwargs):
|
||||
cache.delete(f"model:{kwargs['model_type']}")
|
||||
|
||||
# Audit logging handler
|
||||
@model_saved.connect
|
||||
def log_change(sender, **kwargs):
|
||||
audit_log.write(f"Model saved: {kwargs['model_type']}")
|
||||
```
|
||||
|
||||
### Example 3: Sender-Specific Subscriptions
|
||||
|
||||
@source <https://github.com/pallets-eco/blinker> README
|
||||
|
||||
```python
|
||||
from blinker import signal
|
||||
|
||||
round_started = signal('round-started')
|
||||
|
||||
# General subscriber - receives from all senders
|
||||
@round_started.connect
|
||||
def each_round(sender):
|
||||
print(f"Round {sender}")
|
||||
|
||||
# Sender-specific subscriber - only for sender=2
|
||||
@round_started.connect_via(2)
|
||||
def special_round(sender):
|
||||
print("This is round two!")
|
||||
|
||||
for round_num in range(1, 4):
|
||||
round_started.send(round_num)
|
||||
# Output:
|
||||
# Round 1
|
||||
# Round 2
|
||||
# This is round two!
|
||||
# Round 3
|
||||
```
|
||||
|
||||
### Example 4: Async Signal Handlers
|
||||
|
||||
@source <https://blinker.readthedocs.io/en/stable/>
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from blinker import Signal
|
||||
|
||||
async_signal = Signal()
|
||||
|
||||
# Async receiver
|
||||
async def async_receiver(sender, **kwargs):
|
||||
await asyncio.sleep(1)
|
||||
print("Async handler completed")
|
||||
|
||||
async_signal.connect(async_receiver)
|
||||
|
||||
# Send to async receivers
|
||||
await async_signal.send_async()
|
||||
|
||||
# Mix sync and async receivers
|
||||
def sync_receiver(sender, **kwargs):
|
||||
print("Sync handler")
|
||||
|
||||
async_signal.connect(sync_receiver)
|
||||
|
||||
# Provide wrapper for sync handlers in async context
|
||||
async def sync_wrapper(func):
|
||||
async def inner(*args, **kwargs):
|
||||
func(*args, **kwargs)
|
||||
return inner
|
||||
|
||||
await async_signal.send_async(_sync_wrapper=sync_wrapper)
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Signal Definition and Connection
|
||||
|
||||
```python
|
||||
from blinker import signal
|
||||
|
||||
# Named signals (shared across modules)
|
||||
initialized = signal('initialized')
|
||||
|
||||
# Anonymous signals (class attributes)
|
||||
from blinker import Signal
|
||||
|
||||
class Processor:
|
||||
on_ready = Signal()
|
||||
on_complete = Signal()
|
||||
|
||||
def process(self):
|
||||
self.on_ready.send(self)
|
||||
# Do work
|
||||
self.on_complete.send(self, status='success')
|
||||
|
||||
# Connect receivers
|
||||
@initialized.connect
|
||||
def on_init(sender, **kwargs):
|
||||
print(f"Initialized by {sender}")
|
||||
|
||||
processor = Processor()
|
||||
|
||||
@processor.on_complete.connect
|
||||
def handle_completion(sender, **kwargs):
|
||||
print(f"Status: {kwargs['status']}")
|
||||
```
|
||||
|
||||
### Named Signals for Decoupling
|
||||
|
||||
```python
|
||||
from blinker import signal
|
||||
|
||||
# Module A defines and sends
|
||||
def user_service():
|
||||
user_created = signal('user-created')
|
||||
# Create user
|
||||
user_created.send('user_service', user_id=123, username='john')
|
||||
|
||||
# Module B subscribes (no import of Module A needed!)
|
||||
def notification_service():
|
||||
user_created = signal('user-created') # Same signal instance
|
||||
|
||||
@user_created.connect
|
||||
def send_welcome_email(sender, **kwargs):
|
||||
print(f"Sending email to {kwargs['username']}")
|
||||
```
|
||||
|
||||
### Checking for Receivers Before Expensive Operations
|
||||
|
||||
```python
|
||||
from blinker import signal
|
||||
|
||||
data_changed = signal('data-changed')
|
||||
|
||||
def update_data(new_data):
|
||||
# Only compute expensive stats if someone is listening
|
||||
if data_changed.receivers:
|
||||
stats = compute_expensive_stats(new_data)
|
||||
data_changed.send(self, data=new_data, stats=stats)
|
||||
else:
|
||||
# Skip expensive computation
|
||||
data_changed.send(self, data=new_data)
|
||||
```
|
||||
|
||||
### Temporarily Muting Signals (Testing)
|
||||
|
||||
```python
|
||||
from blinker import signal
|
||||
|
||||
send_email = signal('send-email')
|
||||
|
||||
@send_email.connect
|
||||
def actually_send(sender, **kwargs):
|
||||
# Send real email
|
||||
pass
|
||||
|
||||
def test_user_registration():
|
||||
# Don't send emails during tests
|
||||
with send_email.muted():
|
||||
register_user('test@example.com')
|
||||
# send_email signal is ignored in this context
|
||||
```
|
||||
|
||||
### Collecting Return Values
|
||||
|
||||
```python
|
||||
from blinker import signal
|
||||
|
||||
validate_data = signal('validate-data')
|
||||
|
||||
@validate_data.connect
|
||||
def check_email(sender, **kwargs):
|
||||
email = kwargs['email']
|
||||
if '@' not in email:
|
||||
return False, "Invalid email"
|
||||
return True, None
|
||||
|
||||
@validate_data.connect
|
||||
def check_username(sender, **kwargs):
|
||||
username = kwargs['username']
|
||||
if len(username) < 3:
|
||||
return False, "Username too short"
|
||||
return True, None
|
||||
|
||||
# Collect all validation results
|
||||
results = validate_data.send(
|
||||
None,
|
||||
email='invalid',
|
||||
username='ab'
|
||||
)
|
||||
|
||||
for receiver, (valid, error) in results:
|
||||
if not valid:
|
||||
print(f"Validation failed: {error}")
|
||||
```
|
||||
|
||||
## When NOT to Use Blinker
|
||||
|
||||
### Scenario 1: Simple Callbacks Sufficient
|
||||
|
||||
**Don't use blinker when:**
|
||||
|
||||
- Single callback function is enough
|
||||
- No need for dynamic subscription/unsubscription
|
||||
- Callbacks are tightly coupled to caller
|
||||
|
||||
```python
|
||||
# Overkill - use simple callback
|
||||
from blinker import signal
|
||||
sig = signal('done')
|
||||
sig.connect(on_done)
|
||||
sig.send(self)
|
||||
|
||||
# Better - direct callback
|
||||
def process(callback):
|
||||
# do work
|
||||
callback()
|
||||
|
||||
process(on_done)
|
||||
```
|
||||
|
||||
### Scenario 2: Async Event Systems
|
||||
|
||||
**Don't use blinker when:**
|
||||
|
||||
- Building async-first distributed event system
|
||||
- Need message queuing and persistence
|
||||
- Cross-process or cross-network communication
|
||||
|
||||
```python
|
||||
# Wrong tool - blinker is in-process only
|
||||
from blinker import signal
|
||||
distributed_event = signal('cross-service-event')
|
||||
|
||||
# Better - use async message queue
|
||||
import asyncio
|
||||
from aio_pika import connect, Message
|
||||
|
||||
async def publish_event():
|
||||
connection = await connect("amqp://guest:guest@localhost/")
|
||||
channel = await connection.channel()
|
||||
await channel.default_exchange.publish(
|
||||
Message(b"event data"),
|
||||
routing_key="events"
|
||||
)
|
||||
```
|
||||
|
||||
### Scenario 3: Complex State Machines
|
||||
|
||||
**Don't use blinker when:**
|
||||
|
||||
- Need state transitions with guards and actions
|
||||
- Require hierarchical or concurrent states
|
||||
- Complex workflow orchestration
|
||||
|
||||
```python
|
||||
# Wrong tool - too complex for simple signals
|
||||
from blinker import signal
|
||||
|
||||
# Better - use state machine library
|
||||
from transitions import Machine
|
||||
|
||||
class Order:
|
||||
states = ['pending', 'paid', 'shipped', 'delivered']
|
||||
|
||||
def __init__(self):
|
||||
self.machine = Machine(
|
||||
model=self,
|
||||
states=Order.states,
|
||||
initial='pending'
|
||||
)
|
||||
self.machine.add_transition('pay', 'pending', 'paid')
|
||||
self.machine.add_transition('ship', 'paid', 'shipped')
|
||||
```
|
||||
|
||||
### Scenario 4: Request/Response Patterns
|
||||
|
||||
**Don't use blinker when:**
|
||||
|
||||
- Need bidirectional request/response communication
|
||||
- Require RPC-style method calls
|
||||
- Need return values from specific handlers
|
||||
|
||||
```python
|
||||
# Awkward with signals
|
||||
result = some_signal.send(self, request='data')
|
||||
# Hard to know which handler provided what
|
||||
|
||||
# Better - direct method call or dependency injection
|
||||
class ServiceLocator:
|
||||
def get_service(self, name):
|
||||
return self._services[name]
|
||||
|
||||
service = locator.get_service('data_processor')
|
||||
result = service.process(data)
|
||||
```
|
||||
|
||||
## Decision Guidance Matrix
|
||||
|
||||
| Use Blinker When | Use Callbacks When | Use AsyncIO When | Use Message Queue When |
|
||||
| --- | --- | --- | --- |
|
||||
| Multiple independent handlers needed | Single handler sufficient | Async/await throughout codebase | Cross-process communication needed |
|
||||
| Plugin system with dynamic handlers | Tightly coupled components | I/O-bound async operations | Message persistence required |
|
||||
| Decoupled modules need communication | Callback logic is simple | Event loop already present | Distributed systems |
|
||||
| Framework-level hooks (like Flask) | Direct function call works | Concurrent async tasks | Reliability and retry needed |
|
||||
| Observable events in OOP design | Inline lambda sufficient | Network I/O heavy | Message ordering matters |
|
||||
| Weak reference cleanup needed | Manual lifecycle management OK | WebSockets/long-lived connections | Load balancing across workers |
|
||||
|
||||
### Decision Tree
|
||||
|
||||
```text
|
||||
Need event notifications?
|
||||
├─ Single process only?
|
||||
│ ├─ YES: Continue
|
||||
│ └─ NO: Use message queue (RabbitMQ, Redis, Kafka)
|
||||
│
|
||||
├─ Multiple handlers per event?
|
||||
│ ├─ YES: Continue
|
||||
│ └─ NO: Use simple callback function
|
||||
│
|
||||
├─ Handlers need to be dynamic (plugins)?
|
||||
│ ├─ YES: Use Blinker ✓
|
||||
│ └─ NO: Direct method calls may suffice
|
||||
│
|
||||
├─ Async/await heavy codebase?
|
||||
│ ├─ YES: Consider asyncio event system
|
||||
│ │ (or use Blinker with send_async)
|
||||
│ └─ NO: Use Blinker ✓
|
||||
│
|
||||
└─ Need weak reference cleanup?
|
||||
├─ YES: Use Blinker ✓
|
||||
└─ NO: Simple callbacks OK
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install blinker
|
||||
```
|
||||
|
||||
Current version: 1.9.0 Minimum Python: 3.9+
|
||||
|
||||
@source <https://pypi.org/project/blinker/>
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Global named signal registry:** `signal('name')` returns same instance everywhere
|
||||
- **Anonymous signals:** Create isolated `Signal()` instances
|
||||
- **Sender filtering:** `connect(handler, sender=obj)` for sender-specific subscriptions
|
||||
- **Weak references:** Automatic cleanup when receivers are garbage collected
|
||||
- **Thread safety:** Safe for concurrent use
|
||||
- **Return value collection:** Gather results from all handlers
|
||||
- **Async support:** `send_async()` for coroutine receivers
|
||||
- **Temporary connections:** Context managers for scoped subscriptions
|
||||
- **Signal muting:** Disable signals temporarily (useful for testing)
|
||||
|
||||
@source <https://blinker.readthedocs.io/en/stable/>
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Memory leaks with strong references:**
|
||||
|
||||
```python
|
||||
# Default uses weak references - OK
|
||||
signal.connect(handler)
|
||||
|
||||
# Strong reference - prevents garbage collection
|
||||
signal.connect(handler, weak=False) # Use sparingly!
|
||||
```
|
||||
|
||||
2. **Expecting signals to modify behavior:**
|
||||
- Signals are for observation, not control flow
|
||||
- Don't rely on signal handlers to prevent actions
|
||||
- Use explicit validation/authorization instead
|
||||
|
||||
3. **Forgetting sender parameter:**
|
||||
|
||||
```python
|
||||
@my_signal.connect
|
||||
def handler(sender, **kwargs): # sender is required!
|
||||
print(kwargs['data'])
|
||||
```
|
||||
|
||||
4. **Cross-process communication:**
|
||||
- Blinker is in-process only
|
||||
- Use message queues for distributed systems
|
||||
|
||||
5. **Performance with many handlers:**
|
||||
- Check `signal.receivers` before expensive operations
|
||||
- Consider limiting number of subscribers for hot paths
|
||||
|
||||
## Related Libraries
|
||||
|
||||
- **Django Signals:** Built into Django, similar concept but Django-specific
|
||||
- **PyPubSub:** More complex publish-subscribe system
|
||||
- **asyncio events:** For async-first applications
|
||||
- **RxPY:** Reactive extensions for Python (more powerful, more complex)
|
||||
- **Celery:** For distributed task queues and async workers
|
||||
|
||||
## Summary
|
||||
|
||||
Blinker is the standard solution for in-process event dispatching in Python, particularly within the Pallets ecosystem (Flask). Use it when you need clean, decoupled event notifications between components in the same process. For distributed systems, async-heavy codebases, or simple single-callback scenarios, consider alternatives.
|
||||
|
||||
**TL;DR:** Blinker = Observer pattern done right, with weak references, thread safety, and a clean API. Essential for Flask signals and plugin systems.
|
||||
Reference in New Issue
Block a user