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
nis a mutable cell holding an awaitable, in this case returned by a coroutine function.nextis a computed cell with a coroutine as its computation function that awaits the awaitable held in n and adds1to it.The value of
nextis 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
PendingAsyncValueErrorexception 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:
PendingAsyncValueErroris 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