Duet Async Library
Duet is a simple future-based async library for Python, version 0.2.9. It takes inspiration from the `trio` library's structured concurrency but primarily relies on the standard `concurrent.futures.Future` interface for parallelism, allowing `async/await` coroutines to interact with these futures. A distinctive feature of Duet is its re-entrancy, permitting multiple calls to `duet.run()` within a single asynchronous context, which can simplify the incremental refactoring of synchronous codebases to asynchronous ones. The library is actively maintained with a steady release cadence for bug fixes and minor improvements.
Common errors
-
TypeError: object Future can't be awaited
cause Attempting to `await` a `concurrent.futures.Future` object directly without wrapping it in `duet.AwaitableFuture`.fixImport `AwaitableFuture` from `duet` and wrap the future: `await duet.AwaitableFuture(your_future)`. -
NameError: name 'run' is not defined
cause The `run` function, or other Duet symbols, were used without being properly imported from the `duet` library.fixEnsure you have imported the necessary components, e.g., `from duet import run` or `from duet import AwaitableFuture`.
Warnings
- gotcha When working with `concurrent.futures.Future` objects, they do not inherently support the `__await__` protocol. You must wrap them using `duet.AwaitableFuture` before you can `await` them directly within a Duet asynchronous function.
- gotcha Duet explicitly supports re-entrancy of its event loop, meaning you can call `duet.run()` from within an already running `duet.run()` context. This differs significantly from other popular async libraries like `asyncio` or `trio`, which would typically raise a `RuntimeError` in such a scenario. While this simplifies certain refactoring efforts, be aware of this unique behavior if you're accustomed to strict single-event-loop execution patterns.
Install
-
pip install duet
Imports
- run
from duet import run
- AwaitableFuture
from duet import AwaitableFuture
Quickstart
import concurrent.futures
import time
import duet
def long_running_sync_task(value):
"""A blocking synchronous task."""
time.sleep(0.1) # Simulate work
return f"Processed {value}"
async def async_wrapper(future: concurrent.futures.Future):
"""Wraps a concurrent.futures.Future to be awaitable in Duet."""
return await duet.AwaitableFuture(future)
async def main():
print("Starting Duet example...")
# Example 1: Running a synchronous task in a thread pool executor
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(long_running_sync_task, "data_item_A")
result_a = await async_wrapper(future)
print(f"Result 1: {result_a}")
# Example 2: Demonstrate re-entrancy (calling duet.run within an async function)
# In most async libraries (e.g., asyncio, trio), this would raise a RuntimeError
# Duet explicitly allows it for easier refactoring of mixed sync/async code.
async def nested_task():
print(" Running nested_task synchronously via duet.run...")
nested_future = executor.submit(long_running_sync_task, "data_item_B")
nested_result = await async_wrapper(nested_future)
print(f" Nested task result: {nested_result}")
return "Nested task completed"
nested_call_result = duet.run(nested_task())
print(f"Result 2: {nested_call_result}")
print("Duet example finished.")
if __name__ == "__main__":
duet.run(main())