A coroutine-based signal implementation for asyncio projects providing asynchronous callback management
npx @tessl/cli install tessl/pypi-aiosignal@1.4.0A coroutine-based signal implementation for asyncio projects. aiosignal provides a Signal class that acts as a list of registered asynchronous callbacks with a two-stage lifecycle: first, callbacks can be registered using standard list operations, then the signal is frozen to prevent further modifications and enable callback execution.
pip install aiosignalfrom aiosignal import SignalVersion information:
from aiosignal import __version__
print(__version__) # "1.4.0"import asyncio
from aiosignal import Signal
# Create a signal with an owner object
class MyApp:
def __init__(self):
self.on_startup = Signal(self)
app = MyApp()
# Register callbacks using list operations
async def init_database():
print("Database initialized")
async def setup_logging():
print("Logging configured")
app.on_startup.append(init_database)
app.on_startup.append(setup_logging)
# Or register using decorator syntax
@app.on_startup
async def load_config():
print("Configuration loaded")
# Freeze the signal to enable execution
app.on_startup.freeze()
# Send the signal to execute all callbacks
async def main():
await app.on_startup.send()
# Run the example
asyncio.run(main())The Signal class inherits from FrozenList (from the frozenlist package), providing a two-stage lifecycle:
freeze(), the signal becomes immutable and can execute callbacks via send()This design ensures thread-safe callback execution while preventing accidental modifications during execution.
Create and configure signal instances for managing asynchronous callbacks.
class Signal(FrozenList[Callable[[Unpack[_Ts]], Awaitable[object]]]):
def __init__(self, owner: object):
"""
Initialize a signal with an owner object.
Args:
owner: The object that owns this signal (for identification)
"""
def __repr__(self) -> str:
"""
Return string representation showing owner, frozen state, and callbacks.
Returns:
str: Formatted representation
"""Register asynchronous callbacks that will be executed when the signal is sent.
def __call__(
self, func: Callable[[Unpack[_Ts]], Awaitable[_T]]
) -> Callable[[Unpack[_Ts]], Awaitable[_T]]:
"""
Decorator to add a function to this signal.
Args:
func: Asynchronous function to register as callback
Returns:
The same function (for use as decorator)
"""All standard list operations are available for callback management:
append(callback) - Add callback to signalinsert(index, callback) - Insert callback at specific positionremove(callback) - Remove specific callbackpop(index) - Remove and return callback at indexclear() - Remove all callbacks__getitem__(index) - Get callback at index__setitem__(index, callback) - Replace callback at index__delitem__(index) - Delete callback at index__len__() - Get number of callbacks__iter__() - Iterate over callbacksExecute all registered callbacks by sending the signal with optional arguments.
async def send(self, *args: Unpack[_Ts], **kwargs: Any) -> None:
"""
Send data to all registered receivers.
The signal must be frozen before sending. All registered callbacks
will be executed in registration order.
Args:
*args: Positional arguments to pass to each callback
**kwargs: Keyword arguments to pass to each callback
Raises:
RuntimeError: If signal is not frozen
TypeError: If any callback is not a coroutine function
"""Control the signal's lifecycle and state transitions.
def freeze(self) -> None:
"""
Freeze the signal to prevent further modifications.
After freezing, callbacks cannot be added, removed, or modified.
The signal can then be used to send data to callbacks.
Raises:
RuntimeError: If attempting to modify after freezing
"""
@property
def frozen(self) -> bool:
"""
Check if signal is frozen.
Returns:
bool: True if signal is frozen, False otherwise
"""from typing import Any, Awaitable, Callable, TypeVar
from typing_extensions import Unpack, TypeVarTuple
_T = TypeVar("_T")
_Ts = TypeVarTuple("_Ts", default=Unpack[tuple[()]])
__version__: str = "1.4.0"
class Signal(FrozenList[Callable[[Unpack[_Ts]], Awaitable[object]]]):
"""
Generic signal class that can be parameterized with callback argument types.
Examples:
Signal[str, int] - callbacks take (str, int) arguments
Signal[dict] - callbacks take dict argument
Signal - callbacks take any arguments
"""
_owner: objectaiosignal raises the following exceptions:
# Example error handling
import asyncio
from aiosignal import Signal
signal = Signal(owner=None)
# This raises RuntimeError - signal not frozen
try:
await signal.send()
except RuntimeError as e:
print(f"Cannot send non-frozen signal: {e}")
# Register non-coroutine callback
def sync_callback():
pass
signal.append(sync_callback)
signal.freeze()
# This raises TypeError - callback not a coroutine
try:
await signal.send()
except TypeError as e:
print(f"Callback must be coroutine: {e}")aiosignal requires the following dependencies:
The package is designed for Python 3.9+ with full asyncio compatibility.