Async signals implementation for Python with type-safe callbacks
Project description
Async Signals
A type-safe asynchronous signal/slot implementation for Python 3.13+ with modern type hints and zero dependencies.
Features
- 🧬 Type-safe callbacks: Generic type parameters ensure callback arguments match signal emissions
- ⚡ Async-first: Built for modern async/await patterns with asyncio integration
- 🔒 Thread-safe: Uses asyncio locks for safe concurrent operations
- 🚀 Concurrent execution: Callbacks run concurrently using asyncio.gather()
- 🔄 Duplicate prevention: Prevents connecting the same callback multiple times
- 📏 Clean API: Simple, intuitive methods with clear semantics
- 🔍 Type hints: Full support for Python 3.13+ type system including *A type variables
- 📦 Zero dependencies: Pure Python, nothing else required
Requirements
- Python 3.13 or higher
- No external dependencies
License
MIT License - see LICENSE file for details.
Installation
pip install asignals
Or install from source:
git clone https://github.com/xmarlon30x2/asignals.git
cd asignals
pip install -e .
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass
- Submit a Pull Request
Support
If you find a bug or have a feature request, please open an issue.
Quick Start
import asyncio
from asignals import Signal
# Create a signal with specific argument types
data_signal = Signal[str, int]()
# Define async callbacks
async def log_data(name: str, value: int):
print(f"📊 Data received: {name} = {value}")
async def process_data(name: str, value: int):
await asyncio.sleep(0.1) # Simulate some work
print(f"⚙️ Processing: {name.upper()}")
# Connect callbacks
async def main():
await data_signal.connect(log_data)
await data_signal.connect(process_data)
# Emit signal (triggers all callbacks concurrently)
await data_signal.emit("temperature", 25)
# Check connections
print(f"Connected callbacks: {len(data_signal)}")
# Disconnect a callback
await data_signal.disconnect(log_data)
# Check if callback is still connected
if log_data not in data_signal:
print("Logger disconnected successfully")
asyncio.run(main())
API Reference
Signal[*A]
Main signal class with generic type parameters for callback arguments.
Methods:
async connect(callback: Callback[*A]) -> None
Connect an async callback to the signal.
- callback: Async function to call when signal is emitted
- Duplicate prevention: If the same callback is already connected, it won't be added again
async disconnect(callback: Callback[*A]) -> None
Disconnect a specific callback from the signal.
- callback: Async function to disconnect
- Safe: If callback isn't connected, does nothing
async disconnect_all() -> None
Disconnect all callbacks from the signal.
- Cleanup: Useful for resetting signal state or cleanup operations
async emit(*args: *A) -> None
Emit the signal with given arguments.
- args: Arguments passed to all connected callbacks
- Concurrent execution: All callbacks run concurrently via asyncio.gather()
- Exception propagation: If any callback raises an exception, it propagates
Special Methods:
len() -> int
Returns the number of connected callbacks.
count = len(signal)
bool() -> bool
Returns True if any callbacks are connected, False otherwise.
if signal:
print("Signal has callbacks")
contains(callback: Callback[*A]) -> bool
Checks if a specific callback is connected.
if my_handler in signal:
print("Handler is connected")
Type Alias
Callback[*A]
Type alias for async callback functions: Callable[[*A], CoroutineType]
Advanced Examples
Multiple Signal Types
from asignals import Signal
# Signal with no arguments
click_signal = Signal()
# Signal with one argument
message_signal = Signal[str]()
# Signal with multiple arguments
data_signal = Signal[str, int, float]()
# Signal with complex types
user_signal = Signal[dict, list[str]]()
Complete Workflow
import asyncio
from asignals import Signal
class EventManager:
def __init__(self):
self.message_received = Signal[str, str]() # (user, message)
self.user_joined = Signal[str]() # username
self.user_left = Signal[str, str]() # (username, reason)
self._setup_handlers()
async def _setup_handlers(self):
await self.message_received.connect(self._log_message)
await self.message_received.connect(self._notify_admins)
await self.user_joined.connect(self._welcome_user)
async def _log_message(self, user: str, message: str):
print(f"[LOG] {user}: {message}")
async def _notify_admins(self, user: str, message: str):
if "urgent" in message.lower():
print(f"[ADMIN] Urgent message from {user}")
async def _welcome_user(self, username: str):
print(f"Welcome, {username}!")
async def broadcast_message(self, user: str, message: str):
await self.message_received.emit(user, message)
async def main():
manager = EventManager()
# Simulate events
await manager.broadcast_message("alice", "Hello everyone!")
await manager.broadcast_message("bob", "URGENT: Server restart needed")
# User joins
await manager.user_joined.emit("charlie")
# Check signal status
print(f"Message signal has {len(manager.message_received)} callbacks")
# Cleanup
await manager.message_received.disconnect_all()
print(f"After cleanup: {len(manager.message_received)} callbacks")
asyncio.run(main())
Error Handling
import asyncio
from asignals import Signal
async def safe_emit(signal: Signal, *args):
"""Safely emit a signal, catching exceptions from callbacks."""
try:
await signal.emit(*args)
except Exception as e:
print(f"Error in signal callbacks: {e}")
# Optionally re-raise or handle differently
async def main():
signal = Signal[str]()
async def bad_callback(msg: str):
raise ValueError(f"Failed to process: {msg}")
async def good_callback(msg: str):
print(f"Success: {msg}")
await signal.connect(bad_callback)
await signal.connect(good_callback)
# This will raise ValueError
try:
await signal.emit("test")
except ValueError as e:
print(f"Caught expected error: {e}")
# Or use safe wrapper
await safe_emit(signal, "another test")
asyncio.run(main())
Testing Signals
The package includes comprehensive tests. To run them:
# Run tests
python -m unittest -v -s ./tests -p test_*.py
Best Practices
- Type Hints
Always specify signal types for better type checking:
# Good
signal = Signal[str, int]()
# Avoid
signal = Signal() # Less type safety
- Callback Design
Keep callbacks focused and independent:
# Good - focused callback
async def save_to_database(data: str):
await db.save(data)
# Avoid - doing too much
async def handle_data(data: str):
await db.save(data)
await notify_users(data)
await log_activity(data)
# Better to split into separate callbacks
- Cleanup
Always disconnect callbacks when they're no longer needed:
async def setup():
signal = Signal[str]()
await signal.connect(handler)
try:
# Use signal...
await signal.emit("data")
finally:
await signal.disconnect(handler) # Clean up
- Exception Handling
Consider how to handle callback exceptions:
# Option 1: Let exceptions propagate
try:
await signal.emit(data)
except Exception as e:
# Handle error from any callback
handle_error(e)
# Option 2: Wrap each callback
async def safe_callback(*args):
try:
await original_callback(*args)
except Exception as e:
logger.error(f"Callback failed: {e}")
Async Signals - Simple, type-safe async event handling for Python 3.13+
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file asignals-1.0.2.tar.gz.
File metadata
- Download URL: asignals-1.0.2.tar.gz
- Upload date:
- Size: 9.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1a1e8d8e8fd2cfb2a145814aa48f9976439e9b7e6487a4e3032258970348663a
|
|
| MD5 |
7da4e1abdfd77a137a2e1c5219148697
|
|
| BLAKE2b-256 |
d345828035cc1e256e85ead35129ad324ac93cd3c344c6632d4d50e252fed804
|
File details
Details for the file asignals-1.0.2-py3-none-any.whl.
File metadata
- Download URL: asignals-1.0.2-py3-none-any.whl
- Upload date:
- Size: 8.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bc40018ab157780b0265e2ca5390446e5e693430d29f23fcaa361817f8f2a0a8
|
|
| MD5 |
f52da2f0d70b7ef16b8d2bf32537d72e
|
|
| BLAKE2b-256 |
e976e6caa6240d3de6c7979714eeb18ee1afc02eaf780af363d325d9b4892d51
|