Integration with Asyncio
There are a number of tools for integrating cells with asyncio awaitables and coroutines.
Asynchronous Cells
An asynchronous cell is a cell with an awaitable value. To define a
cell that performs a computation on an asynchronously computed value,
define a computed cell with an async
computation function.
import asyncio
import live_cells as lc
# Helper for creating awaitables
async def coro1(n):
await asyncio.sleep(5)
return n
n = lc.mutable(coro1(1))
# An asynchronous computed cell
@lc.computed
async def next():
return await n() + 1
n
is a mutable cell holding an awaitable, in this case returned by a coroutine function.next
is a computed cell with a coroutine as its computation function that awaits the awaitable held in n and adds1
to it.The value of
next
is an awaitable holding the result returned by its value computation function.
To retrieve the result computed by the next
cell, await
its
value:
print(await next()) # Prints 2
When a new awaitable is assigned to n
, the value of next
is
recomputed.
n.value = coro(5)
print(await next()) # Prints 6
Important
The values of asynchronous computed cells, which are awaitables, are still updated synchronously whenever the values of their arguments change just like any other computed cell. It’s only the computation functions themselves that are run asynchronously.
Multiple Arguments
An asynchronous computed cell can reference multiple argument cells, just like an ordinary computed cell.
import asyncio
import live_cells as lc
async def delayed(value, *, delay=1):
await asyncio.sleep(delay)
return value
a = lc.mutable(delayed(1, delay=2))
b = lc.mutable(delayed(2, delay=5))
@lc.computed
async def c():
return await a() + await b()
Wait Cells
A wait cell is a cell that retrieves the result of an awaitable that is held in another cell. When the awaitable completes with a value or error, the value of the wait cell is updated to the result of the awaitable. This allows other cells and watch functions to retrieve the result of an awaitable without having to use the await keyword.
Wait cells are created by calling the waited
method on an
asynchronous cell, which holds an awaitable value:
import asyncio
import live_cells as lc
async def delayed(value, *, delay=1):
await asyncio.sleep(delay)
return value
n = lc.mutable(delayed(1))
wait_n = n.waited()
@lc.computed
def next():
return wait_n() + 1
In this example, next
is not an asynchronous computed cell and its
computation function is not a coroutine. Instead, it retrieves the
result of the awaitable held in n
through a wait cell created
with waited
.
Attention
A wait cell must have at least one observer in order to wait for asynchronous cells. If the wait cell has no observers, it neither tracks the completion of the awaitables in the asynchronous cell nor updates its own value.
waited
can be used on any cell that holds an awaitable, regardless
of whether it’s a constant, mutable or computed cell. If the awaitable
completes with an exception, it is raised when accessing the value of
the wait cell. Similarly, exceptions raised by the cell holding the
awaitable itself, are raised when the value of the wait cell is
accessed.
The wait
method creates a wait cell and references its value
in one go. This can be used to simplify the definition of next
:
@lc.computed
def next():
return n.wait() + 1
Note
n.wait()
is equivalent to n.waited()()
With the default options waited
/wait
creates a cell that has
the following behaviour:
Accessing the value of the cell before the awaitable has completed, results in a
PendingAsyncValueError
exception being raised.The value of the cell is updated to the result of the awaitable, when it completes with a value or an error.
When the value of the asynchronous cell, which holds the awaitable, changes, the wait cell is reset, which means:
PendingAsyncValueError
is raised if its value is accessed before the new awaitable has completed.The previous awaitable is ignored, and no value updates are emitted for it by the wait cell if it completes after the value of the asynchronous cell changes.
Reset Option
The reset
argument controls whether the wait cell is reset when
the value of the asynchronous cell changes. By default, reset
is
True
if not given, which means the wait cell is reset.
If reset
is False
the wait cell is not reset when the value
of the asynchronous cell changes. This means that instead of raising
PendingAsyncValueError
, the completed value or error of the
previous awaitable is retained until the new awaitable completes.
import asyncio
import live_cells as lc
async def delay(value, *, delay=1):
await asyncio.sleep(delay)
return value
n = lc.mutable(delay(1))
@lc.watch
def watch_n():
try:
print(f'N = {n.wait()}')
except PendingAsyncValueError:
print(f'PendingAsyncValueError')
The watch function defined above, watch_n, access the value of the
awaitable held in n
through a wait cell defined with
reset=True
, which is the default if no reset
argument is
given.
When a new awaitable is assigned to n
:
# Give the coroutine a chance to execute
await n.value
n.value = delay(2)
the following is printed to standard output:
PendingAsyncValueError
N = 1
PendingAsyncValueError
N = 2
If the wait cell is created with reset=False
instead:
@lc.watch
def watch_n():
try:
print(f'N = {n.wait(reset=False)}')
except PendingAsyncValueError:
print(f'PendingAsyncValueError')
the following is printed to standard output:
PendingAsyncValueError
N = 1
N = 2
Queue Option
Even with reset=False
the wait cell still only waits for the
last awaitable to complete.
When the value of the asynchronous cell changes multiple times before the awaitables held in the cell have completed, a value update is only emitted when the last awaitable completes.
The following:
n.value = delay(3)
n.value = delay(4)
n.value = delay(5)
only results in N = 5
being printed to standard output, since the
previous two awaitables did not have a chance to complete.
The queue
argument controls whether the wait cell waits for
every awaitable to complete or only the last awaitable. By default
queue
is False
, which means the wait cell only waits for the
last awaitable to complete.
If queue
is True
the wait cell waits for all awaitables to
complete.
Attention
queue=True
only has an effect if reset=False
is also given.
By creating the wait cell from the previous example with
queue=True
:
@lc.watch
def watch_n():
try:
value = n.wait(reset=False, queue=True)
print(f'N = {value}')
except PendingAsyncValueError:
print(f'PendingAsyncValueError')
the following assignments to the cell holding the awaitable n
:
n.value = delay(3)
n.value = delay(4)
n.value = delay(5)
result in the following being printed to standard output:
N = 3
N = 4
N = 5
The wait cell waits for the awaitables in the same order as they
are assigned to the asynchronous cell, n
, which is not necessarily the
same as the order of completion of the awaitables.
For example the following:
n.value = delay(3, delay=10)
n.value = delay(4, delay=1)
n.value = delay(5, delay=3)
always results in the following being printed to standard output, regardless of the actual order of completion of the awaitables:
N = 3
N = 4
N = 5
Caution
If an asynchronous cell evaluates to an awaitable that never
completes, all wait cells created with queue=True
will be
stuck with the completed value of the last awaitable, if
any. Therefore it’s best to only use queue=True
if you’re
certain that all awaitables will complete.
Multiple Arguments
The live_cells.waited
and live_cells.wait
functions can
be used to create a wait cell that waits for multiple asynchronous
cells simultaneously.
live_cells.waited
takes a variable number of asynchronous
cells as arguments and returns a single wait cell that evaluates to
a list holding the completed values of the awaitables held in the
asynchronous cells. If an asynchronous cell raises an exception,
or an awaitable completes with an error, it is raised by the wait
cell.
live_cells.wait
takes the same arguments as
live_cells.waited
but creates the wait cell and
references its value in one go much like the wait
method.
import asyncio
import live_cells as lc
async def delayed(value, *, delay=1):
await asyncio.sleep(delay)
return value
a = lc.mutable(delayed(1))
b = lc.mutable(delayed(2))
@lc.computed
def c():
x,y = lc.wait(a, b)
return x + y
@lc.watch
def watch_sum():
print(f'A + B = {c()}')
The cell c
computes the sum of the completed values of the
awaitables held in the asynchronous cells a
and b
. The
values of the awaitables are accessed through a wait cell that
waits for the both the awaitable held in a
and the awaitable
held in b
, simultaneously.
Note
live_cells.waited
and live_cells.wait
accept the same
keyword arguments as waited
and wait
:
x,y = lc.wait(a, b, reset=False)
Preventing Glitches
This form should be used as opposed to multiple individual wait
calls, since the latter may result in glitches if the asynchronous
cells share a common ancestor or are updated simultaneously in a batch
update. This becomes more apparent if the reset=False
option is
used.
For example if the computed cell c
is defined with multiple
individual calls to wait
:
@lc.computed
def c():
return a.wait(reset=False) + b.wait(reset=False)
and the values of a
and b
are updated simultaneously in a
batch:
with lc.batch():
a.value = delayed(10, delay=1)
b.value = delayed(15, delay=2)
the following is printed to standard output:
A + B = 12
A + B = 25
If a single call to live_cells.wait
is used:
@lc.watch
def c():
x, y = lc.wait(a, b, reset=False)
return x + y
only the following is printed after the batch update:
A + B = 25