Skip to content

← Concurrency with Threads and asyncio step 22 of 56

Medium Primitives

Resolve Values, Awaitables and Factories in Order

A settings loader gets a list of sources. Some are plain numbers. Some are awaitables, such as a coroutine that looks a value up. Some are factories, functions with no arguments that create the awaitable only when you call them. Write resolve_all, which turns each source into its value, one source at a time and in order.

async def double(n: int) -> int:
    return n * 2

await resolve_all([1, double(2), lambda: double(3)])  # [1, 4, 6]

What to write

Fill in resolve_all, and keep the rest of the starter as it is. Go through sources in order, and finish each source before you call or await the next:

  1. If the source is callable, call it once with no arguments and take the result. Otherwise take the source itself.
  2. If what you took is an Awaitable from collections.abc, await it once. Otherwise use it as it is.
  3. Append the value to the list you return.

Also follow these rules:

  • Check callable first. An object that is both callable and awaitable gets called.
  • Start no tasks of your own, and don't use asyncio.gather. The report checks that each factory runs only when resolve_all reaches it.
  • If a factory or an await raises, let the exception propagate, and call no later factories.
  • Keep the generic signature. Your code must pass mypy --strict without Any, casts or # type: ignore.

An empty list gives [].

Tasks in the input

Some sources are tasks. As asyncio from Zero showed, asyncio.create_task hands a coroutine to the event loop to run on its own. The task starts as soon as your code pauses at an await and lets the loop run. That can happen before resolve_all reaches it. Await it like any other awaitable. Its result still goes in its own place in the output, even if it finishes before the sources ahead of it.

What the driver reports

solve takes three lists of the same length, kinds, values and hops. Each kind is "value", "coro", "factory" or "task", and the driver builds that kind of source from the matching value. Each coroutine it builds logs its value when its body starts, waits hops loop turns with asyncio.sleep(0), and returns value * 10 + hops. Each factory logs its value when you call it.

  • values: the list resolve_all returned.
  • started: the values whose coroutines started, in the order they started.
  • calls: the factory values, in the order you called them.
  • pending: other tasks still unfinished at the end. It must be 0.

With kinds=["value", "coro", "factory", "task"], values=[1, 2, 3, 4] and hops=[0, 3, 1, 2], the report has values=[1, 23, 31, 42], started=[2, 4, 3] and calls=[3]. The task for 4 starts while you await the coroutine for 2.