run_sync
def run_sync(
coro
):Run coroutine coro to completion from sync code and return its result
run_sync, iter_sync, ctx_sync, athreaded, maybe_await, and then, plus Debounce for coalescing bursts of calls
Async libraries return coroutines, which normally force every caller up the stack to become async too. run_sync (coroutines), iter_sync (async generators), and ctx_sync (async context managers) let ordinary sync code drive them instead, sharing a single event loop that runs on a background daemon thread, created on first use; maybe_await(o) awaits o only if it’s awaitable, else returns it as-is.
Run coroutine coro to completion from sync code and return its result
Exceptions raised inside the coroutine propagate to the caller, and Ctrl-C cancels it rather than leaving it running on the loop thread. Calling run_sync from a coroutine it is already running would deadlock, so that raises RuntimeError instead.
async def _double(x):
await asyncio.sleep(0.01)
return x*2
test_eq(run_sync(_double(3)), 6)
async def _boom(): raise ValueError('boom')
with expect_fail(Exception, 'boom'): run_sync(_boom())
async def _nested(): return run_sync(_double(1))
with expect_fail(Exception, 'its own event loop'): run_sync(_nested())Because the loop lives on its own thread, run_sync also works where the calling thread already has a running loop, such as a Jupyter cell (where asyncio.run raises RuntimeError). A test cell like this one runs on exactly such a loop:
iter_sync pulls each item by running __anext__ on the shared loop, and closes agen when iteration ends, including when the consumer stops early:
__aenter__ and __aexit__ run on the shared loop, and exception suppression behaves as it would under async with:
athreaded is the same bridge crossed in the other direction: it wraps a blocking function as an honest async def, running the original body in a worker thread so the event loop stays free. Use it to give async color to APIs that only exist in blocking form.
Run f in a worker thread, awaitably; use as @athreaded or @athreaded(executor=...)
The wrapper is a genuine coroutine function, so asyncio.iscoroutinefunction (and is_async_callable, below) reports it as async, and it binds as a method like any plain function. The body runs on the awaiting loop’s default executor, contextvars propagate into the thread (as asyncio.to_thread does), and an exception raises in the awaiting task with its real traceback. Because each call occupies a thread rather than the loop, calls overlap under asyncio.gather:
Pass executor= for a dedicated pool, so long-blocking calls don’t queue behind whatever else the loop’s shared default pool is running:
Pipe x through each of fs, awaiting values as needed; result is awaitable only if x or a step result is
then lets one function body serve sync and async callers alike. It applies each of fs in turn, awaiting anything awaitable along the way (the starting value or any step’s result), and the caller gets back a plain value if the whole chain was sync, or an awaitable otherwise. This is what a convenience method on a client offering both sync and async modes needs: written as return then(self.gists.get(gid), ~Self.files.values(), first), a sync client returns the file directly while an async client returns something to await, with no duplicated method. Because step results are awaited as part of the chain, steps may themselves be async calls; the flip side is that a step cannot return an awaitable as its value.
async def _f(): return dict(a=1,b=2)
test_eq(then(dict(a=1,b=2), ~Self.values(), first), 1) # all sync: plain result
test_eq(await then(_f(), ~Self.values(), first), 1) # awaitable start
async def _double(x): return x*2
test_eq(await then(3, _double, mul(10)), 60) # awaitable appears mid-chain
test_eq(then(3), 3)
test_eq(await then(_f()), dict(a=1,b=2)) # no steps: pure passthroughThis is like functools.cache but for async functions, e.g:
Wraps the result of an asynchronous function into an object which can be awaited more than once
CachedCoro and reawaitable are partly based on python issue tracker code from Serhiy Storchaka. They allow an awaitable to be called multiple times.
data
data
Check if obj is an async callable, handling partial wrappers and callable instances
is_async_callable detects whether an object can be called asynchronously. It goes beyond asyncio.iscoroutinefunction by also handling functools.partial-wrapped async functions (unwrapping through any number of layers) and callable objects whose __call__ method is a coroutine. The implementation is thanks to Starlette.
Async map; apply f (sync or async) to items (sync or async iter) concurrently via gather
async def _double(x): return x*2
test_eq(await mapa(mul(2), [1,2,3]), [2,4,6]) # sync f, sync items
test_eq(await mapa(_double, [1,2,3]), [2,4,6]) # async f, sync items
test_eq(await mapa(mul(2), _agen()), [2,4,6]) # sync f, async items
test_eq(await mapa(_double, _agen()), [2,4,6]) # async f, async itemsEvery “wait for X” helper is the same loop: call a probe, sleep, give up at a deadline. wait_until is that loop, taking a sync or async probe and returning its first truthy result:
Call probe until truthy, returning its first truthy result
On timeout it raises TimeoutError naming what, or the probe itself when what is omitted:
Debounce coalesces a burst of calls into one invocation of f: wait secs after calls stop arriving, or max_wait secs after the burst began. A typical use is auto-saving shortly after edits stop, rather than on every edit. Calls carry no arguments: Debounce is a trigger, and whatever state the fire should consume (pending edits, queued items) lives with the caller, which accumulates it before each call and drains it inside f.
Coalesce a burst of calls into one run of f, wait secs after calls stop (or max_wait secs after they start)
Calling a Debounce is fire-and-forget: the call returns at once, and f runs later in a worker task on the event loop. The instance binds to the loop it was constructed on, or else the first loop it is called from, and keeps that loop for life. f can be sync or async, but a blocking f will stall the loop, so wrap slow sync work with athreaded or similar. Each call resets the wait timer, so a steady stream of calls fires nothing until they pause:
The reset also holds for spaced calls: gaps shorter than wait keep postponing the fire, so only the pause at the end lets it through:
With max_wait set, a burst cannot postpone the fire forever: it comes no later than max_wait after the burst began. max_wait=wait gives throttle timing, at most one fire per wait secs. The stream below never pauses long enough for a plain debounce to fire, but the cap forces fires mid-stream, and the count shows how few runs the burst cost:
Because calls carry no payload, “latest args win” and “keep every item” are both caller idioms, not modes. Accumulate into whatever structure suits, and drain it in f:
If f raises, by default the exception ends that burst’s worker task (asyncio logs it as an unretrieved task exception), and any call that arrived during the failing fire is lost with it. Pass on_error to handle the exception instead: the worker survives, so pending calls still fire, and f may retrigger itself to retry:
flush runs any pending fire immediately, which is handy at shutdown, and propagates exceptions to its caller rather than to on_error. With nothing pending it is a no-op. Use it, like cancel, from the owner loop only:
cancel discards the pending fire:
Calls are safe from threads with no loop, and from threads running a different loop: both are forwarded to the owner loop with call_soon_threadsafe, and the instance never migrates between loops:
An async f never overlaps itself: calls that arrive while it is still running wait for it to finish, then trigger one further fire:
Releasing the stalled fire lets the one coalesced follow-up run:
IPython magics are ordinary functions, so a magic defined async def hands its caller an unawaited coroutine. enable_async_magics fixes that per shell: it wraps the shell’s input transformation so a top-level magic call in the transformed source is awaited through a per-shell _amagic hook, which maybe_awaits the result, so sync magics are unaffected. The rewrite must see get_ipython().run_line_magic(...) form. Line magics only reach that form in the token transform phase, after every line_transforms entry has run, so this wraps transform_cell itself rather than joining line_transforms. Only unindented magic lines are wrapped: a magic inside a function body stays sync, since await would be a syntax error there. The _amagic attribute doubles as the idempotence sentinel, so shells enabled twice (say, by two extensions sharing this machinery) get exactly one wrap. It lives on the shell rather than user_ns, so %reset can’t break it.
Undo enable_async_magics on ip
Let line and cell magics on ip be async: coroutine results are awaited. Idempotent per shell.
ip = InteractiveShell()
async def _amag(line): return f'async:{line}'
async def _acell(line, cell): return f'{line}:{cell.strip()}'
ip.register_magic_function(_amag, 'line', 'amag')
ip.register_magic_function(lambda line: f'sync:{line}', 'line', 'smag')
ip.register_magic_function(_acell, 'cell', 'acell')
enable_async_magics(ip)
test_eq((await run_cell(ip, '%amag hi')).result, 'async:hi')
test_eq((await run_cell(ip, '%%acell top\nbody')).result, 'top:body')Out[0]: 'async:hi'
Out[0]: 'sync:hi'
Out[0]: 'top:body'
The wrap only awaits what needs awaiting, so a sync magic runs exactly as before, and an assignment target on the magic line survives the rewrite:
Enabling twice wraps exactly once, and function bodies stay sync:
fmt post-processes each awaited magic result, and disable_async_magics unwraps the shell:
Out[0]: 'ASYNC:HI'