Usage with async/await#
Psygnal can be made to work with asynchronous functions (those defined with async def) by setting an async backend. The pattern is slightly different depending on the async framework you are using, but the general idea is the same.
We currently support:
Premise#
Assume you have a class that emits signals, and an async function that you want to use as a callback:
from psygnal import Signal
class MyObj:
value_changed = Signal(str)
def set_value(self, value: str) -> None:
self.value_changed.emit(value)
async def on_value_changed(new_value: str) -> None:
"""Callback function that will be called when the value changes."""
print(f"The new value is {new_value!r}")
Connecting Async Callbacks#
To connect the value_changed signal to the on_value_changed async function, we need to:
- Set up the async backend using
set_async_backend(), inside an async context. - Wait for the backend to be ready.
- Connect the async function to the signal.
Then whenever set_value() is called, the on_value_changed async function will be called asynchronously.
Order matters!
Failure to call set_async_backend() before connecting an async callback will result in RuntimeError.
Failure to wait for the backend to be ready before connecting an async callback will result in a RuntimeWarning, and the callback will not be called.
import asyncio
from psygnal import set_async_backend
async def main() -> None:
backend = set_async_backend("asyncio") # (1)!
# Set up the async backend and wait for it to be ready
await backend.running.wait() # (2)!
# Create an instance of MyObj and connect the async callback
obj = MyObj()
obj.value_changed.connect(on_value_changed) # (3)!
# Set a value to trigger the callback
obj.set_value("hello!")
# Give the callback time to execute
await asyncio.sleep(0.01)
if __name__ == "__main__":
asyncio.run(main())
- Call
psygnal.set_async_backend("asyncio"). This immediately creates a task to process the queues. - Wait for the backend to be ready before connecting the signal.
- Connect the signal to the async callback function.
import anyio
from psygnal import set_async_backend
async def main() -> None:
backend = set_async_backend("anyio") # (1)!
async with anyio.create_task_group() as tg:
# Set up the async backend and wait for it to be ready before connecting
tg.start_soon(backend.run) # (2)!
await backend.running.wait() # (3)!
# Create an instance of MyObj and connect the async callback
obj = MyObj()
obj.value_changed.connect(on_value_changed) # (4)!
# Set a value to trigger the callback
obj.set_value("hello!")
# Give the callback time to execute
await anyio.sleep(0.01)
tg.cancel_scope.cancel()
if __name__ == "__main__":
anyio.run(main)
- Call
psygnal.set_async_backend("anyio")to create send/receive queues. - Start watching the queues in the background using
backend.run(). - Wait for the backend to be ready before connecting the signal.
- Connect the signal to the async callback function.
import trio
from psygnal import set_async_backend
async def main() -> None:
backend = set_async_backend("trio") # (1)!
async with trio.open_nursery() as nursery:
# Set up the async backend and wait for it to be ready before connecting
nursery.start_soon(backend.run) # (2)!
await backend.running.wait() # (3)!
# Create an instance of MyObj and connect the async callback
obj = MyObj()
obj.value_changed.connect(on_value_changed) # (4)!
# Set a value to trigger the callback
obj.set_value("hello!")
# Give the callback time to execute
await trio.sleep(0.01)
nursery.cancel_scope.cancel()
if __name__ == "__main__":
trio.run(main)
- Call
psygnal.set_async_backend("trio")to create send/receive channels. - Start watching the channels in the background using
backend.run(). - Wait for the backend to be ready before connecting the signal.
- Connect the signal to the async callback function.