Detailed documentation¶
Base class¶
The two main classes in aioresult, ResultCapture
and Future
, have almost
identical interfaces: they both allow waiting for and retrieving a value. That interface is
contained in the base class, ResultBase
:
Note
If you are returning a ResultCapture
or Future
from a function then you may
wish to document the return type as just ResultBase
because that has all the methods
relevant for retrieving a result. For example, StartableResultCapture.start_result()
does this.
- class aioresult.ResultBase¶
Base class for
ResultCapture
andFuture
. Has methods for checking if the task is done and fetching its result.- result()¶
Returns the captured result of the task.
- Returns:
The value returned by the task.
- Raises:
TaskNotDoneException – If the task is not done yet; use
is_done()
orwait_done()
to check or wait for completion to avoid this exception.TaskFailedException – If the task failed with an exception; see Exception handling and design rationale.
- exception()¶
Returns the exception raised by the task.
It is usually better design to use the
result()
method and catch the exception it raises. However,exception()
can be useful in some situations e.g. filtering a list ofResultCapture
objects.- Returns:
The exception raised by the task, if it completed by raising an exception. Note that it is the original exception returned, not a
TaskFailedException
as raised byresult()
.If the task completed by returning a value then this method returns
None
.
- Raises:
TaskNotDoneException – If the task is not done yet.
- is_done()¶
Returns
True
if the task is done i.e. the result (or an exception) is captured. ReturnsFalse
otherwise.
- await wait_done()¶
Waits until the task is done.
There are specialised situations where it may be useful to use this method to wait until the task is done, typically where you are writing library code and you want to start a routine in a user supplied nursery but wait for it in some other context.
Typically, though, it is much better design to wait for the task’s nursery to complete. Consider a nursery-based approach before using this method.
ResultBase
makes uses of the following exception class (along with TaskFailedException
,
which is described below).
- exception aioresult.TaskNotDoneException¶
Bases:
Exception
Exception raised when attempting to access the result (using
ResultBase.result()
orResultBase.exception()
) that is not complete yet.It is raised with the capture instance passed as the argument to the exception:
raise TaskNotDoneException(self)
This allows access to the
ResultCapture
orFuture
from the exception using theargs
attribute:- args¶
1-tuple of the
ResultCapture
orFuture
that raised this exception.- Type:
(ResultBase, )
Exception handling and design rationale¶
A key design decision about the ResultCapture
class is that exceptions are allowed to
escape out of the task so they propagate into the task’s nursery.
Some related libraries, such as Outcome and trio-future, consume any exception thrown by the task and reraise it when the result is retrieved. This gives the calling code more control: it can choose at what point to retrieve the result, and therefore at what point the exception is thrown. However, it has some disadvantages:
The calling code must ensure the exception is always retrieved, otherwise the exception is silently lost. That would be particularly problematic if it is an injected exception such as
trio.Cancelled
orKeyboardInterrupt
. This can be difficult to arrange reliably, especially if multiple tasks like this raise exceptions. The whole point of structured concurrency was meant to be that you don’t have to worry about this problem!For many exceptions, it does not make sense to be raised more than once, so the calling code must be careful to retrieve the result only once. For example, Outcome raises an error if it is unwrapped more than once.
The calling code must be careful about whether the exception still makes sense in the context in which the result is retrieved. For example, if the exception is a
trio.Cancelled
then its corresponding nursery must still be live.
If your goal is simply to store a return value from an async function then you probably do not want this control anyway: you just want any exception to be raised in the nursery where the function is running. aioresult does this, which avoids the above complexities: the exception always makes sense in its context (because it’s in the original place it was raised) and you are free to retrieve the result once, many times, or not at all.
If the task throws an exception then any calls to ResultBase.result()
will also throw an
exception, but a TaskFailedException
is thrown instead of the original:
- exception aioresult.TaskFailedException¶
Bases:
Exception
Exception raised when accessing
ResultBase.result()
for a task that raised an exception. This exception is raised as a chained exception:raise TaskFailedException(self) from original_exception
This allows access to the original exception and the relevant
ResultCapture
orFuture
as attributes:- __cause__¶
The original exception that was raised by the task.
- Type:
- args¶
1-tuple of the
ResultCapture
orFuture
that raised this exception.- Type:
(ResultBase, )
Capturing a result¶
The main class of aioresult is the ResultCapture
class. If you are directly awaiting a task
then there is no need to use this class – you can just use the return value:
result1 = await foo(1)
result2 = await foo(2)
print("results:", result1, result2)
If you want to run your tasks in parallel then you would typically use a nursery, but then it’s harder to get hold of the results:
async with trio.open_nursery() as n:
n.start_soon(foo, 1)
n.start_soon(foo, 2)
# At this point the tasks have completed, but the results are lost
print("results: ??")
To get access to the results, the usual advice is to either modify the routines so that they
store their result somewhere rather than returning it or to create a little wrapper function
that stores the return value of the function you actually care about. ResultCapture
is a
simple helper to do this:
async with trio.open_nursery() as n:
result1 = ResultCapture.start_soon(n, foo, 1)
result2 = ResultCapture.start_soon(n, foo, 2)
# At this point the tasks have completed, and results are stashed in ResultCapture objects
print("results", result1.result(), result2.result())
You can get very similar effect to asyncio.gather()
by using a nursery and an array
of ResultCapture
objects:
async with trio.open_nursery() as n:
results = [ResultCapture.start_soon(n, foo, i) for i in range(10)]
print("results:", *[r.result() for r in results])
Unlike asyncio’s gather, you benefit from the safer behaviour of Trio nurseries if one of the tasks
throws an exception. ResultCapture
is also more flexible because you don’t have to use a
list, for example you could use a dictionary:
async with trio.open_nursery() as n:
results = {i: ResultCapture.start_soon(n, foo, i) for i in range(10)}
print("results:", *[f"{i} -> {r.result()}," for i, r in results.items()])
Any exception thrown by the task will propagate out as usual, typically to the enclosing nursery. See Exception handling and design rationale for details.
- class aioresult.ResultCapture(routine, *args)¶
Bases:
ResultBase
Captures the result of a task for later access.
Most usually, an instance is created with the
start_soon()
class method. However, it is possible to instantiate directly, in which case you will need to arrange for therun()
method to be called.- Parameters:
routine – An async callable.
args – Positional arguments for
routine
.
Note
ResultCapture
inherits most of its methods from its base classResultBase
; see the documentation for that class for the inherited methods.- classmethod start_soon(nursery: None, routine, *args)¶
Runs the task in the given nursery and captures its result.
Under the hood, this simply constructs an instance with
ResultCapture(routine, *args)
, starts the routine by callingnursery.start_soon(rc.run)
, then returns the new object. It’s literally three lines long! But it’s the preferred way to create an instance.- Parameters:
nursery – A
trio.Nursery
oranyio.abc.TaskGroup
to run the routine.routine – An async callable.
args – Positional arguments for
routine
. If you want to pass keyword arguments, usefunctools.partial()
.
- Returns:
A new
ResultCapture
instance representing the result of the given routine.
- await run(**kwargs)¶
Runs the routine and captures its result.
This is where the magic of
ResultCapture
happens … except it’s not very magical. It just runs the routine passed to the constructor and stores the result in an internal member variable: effectively likeself._result = await self._routine(*self._args)
. It also wraps that in atry:
…except:
block and stores any exception raised (but then re-raises the exception immediately; see Exception handling and design rationale for details). Either way, it sets an internaltrio.Event
orasyncio.Event
, which is used inResultBase.is_done()
andResultBase.wait_done()
.Typically you would use the
start_soon()
class method, which constructs theResultCapture
and arranges for thisrun()
method to be run in the given nursery. But it is reasonable to manually construct the object and call therun()
method in situations where extra control is needed.- Returns:
Always
None
. To access the return value of the routine, useResultBase.result()
.- Raises:
BaseException – Whatever exception is raised by the routine.
Warning
Ensure this routine is not called more than once. In particular, do not call it at all if the instance was created with
start_soon()
, which already calls the this once.
- routine()¶
Returns the routine whose result will be captured (passed to the constructor or
start_soon()
).
- args()¶
Returns the arguments passed to the routine whose result will be captured (this is the
args
argument passed to the constructor orstart_soon()
).
Wait for a task to finish starting¶
Trio and anyio support waiting until a task has finished starting with trio.Nursery.start()
and anyio.abc.TaskGroup.start()
. For example, a routine that supports this could look like
this:
async def task_with_start_result(i, task_status=trio.TASK_STATUS_IGNORED):
await trio.sleep(i)
task_status.started(i * 2)
await trio.sleep(i)
return i * 3
It could be used as follows:
async with trio.open_nursery() as n:
start_result = await n.start(task_with_start_result, 1)
# At this point task is running in background
# Another 1 second passes before we get here
For example, trio.serve_tcp()
signals that it has finished starting when the port is open
for listening, and it returns which port number it is listenting on (which is useful because the
port can be assigned automatically).
The peculiar-looking default value of trio.TASK_STATUS_IGNORED
is there so that the function can
be called without using it as part of this special dance. In particular, this means you can use
these functions with ResultCapture
as usual:
async with trio.open_nursery() as n:
rc = ResultCapture.start_soon(n, task_with_start_result, 1)
print("Final result:", rc.result())
However, doing this loses the ability to wait until the task has started (but not completed) and
fetch the start return value. StartableResultCapture
is a derived class of
ResultCapture
that allows capturing both the start value and final value:
async with trio.open_nursery() as n:
rc = StartableResultCapture.start_soon(n, task_with_start_result, 1)
await rc.start_result().wait_done()
print("Start result:", rc.start_result().result())
print("Final result:", rc.result())
It is also possible to pass in another nursery where the task will be started. This is useful for waiting until multiple tasks have all finished their startup code:
async with trio.open_nursery as run_nursery:
async with trio.open_nursery as start_nursery:
rcs = [
StartableResultCapture.start_soon(
run_nursery, task_with_start_result, i, start_nursery=start_nursery
) for i in range(10)
]
print("Now all tasks have started:", *[rc.start_result().result() for rc in rcs])
print("Overall results:", *[rc.result() for rc in rcs])
- class aioresult.StartableResultCapture(routine, *args)¶
Bases:
ResultCapture
Captures result of a task and allows waiting until it has finished starting.
As with
ResultCapture
, the methods to wait for and retrieve the overall result can be found in theResultBase
documentation. The startup result is also an instance ofResultBase
, returned from thestart_result()
method.- classmethod start_soon(nursery: None, routine, *args, start_nursery: None = None)¶
Runs the task in the given nursery and captures its start result and overall result.
Optionally allows specifying another nursery where the task will be run until it has finished starting. If not specified, the whole task including startup will run in the one specified nursery.
- Parameters:
nursery – A
trio.Nursery
oranyio.abc.TaskGroup
to run the routine.routine – An async callable.
args – Positional arguments for
routine
. If you want to pass keyword arguments, usefunctools.partial()
.nursery – Optional nursery to run just the startup of the task.
- Returns:
A new
ResultCapture
instance representing the result of the given routine.
- await run(task_status=None)¶
Runs the routine and captures its result.
As with
ResultCapture.run()
, this is called internally bystart_soon()
so is rarely needed directly, but can be useful when some extra control is needed.- Parameters:
task_status – The
.started()
method of this object is called when called by the routine passed to the constructor.
- start_result() ResultBase ¶
Returns the start result i.e. the value passed to
task_status.started()
.This is an instance of
ResultBase
, which allows waiting for the task to start withResultBase.wait_done()
and retrieving the result withResultBase.result()
.Note
Although the interface of
ResultBase
allows fetching a raised exception, in this case the exception is never set. If the task raises an exception during startup then the exception is set on the overallStartableResultCapture
object (and the object returned by this method will haveResultBase.is_done()
still returnFalse
).Note
It is possible for
ResultBase.is_done()
of the object returned to beFalse
even if the overall result hasResultBase.is_done()
equal toTrue
. In other words, a task can completely finish without ever having started! That can happen if the task raises an exception during its startup code, or simply returns without ever having calledtask_status.started()
(in which case the exception is aRuntimeError
).
Future¶
The Future
class allows storing the result of an operation, either a return value or a
raised exception. It differs from ResultCapture
in that you manually specify the result by
calling either Future.set_result()
or Future.set_exception()
rather than the result
automatically being captured from some async function.
This is often useful when you are implementing an API in a library where requests can be sent to some remote server, but multiple requests can be outstanding at a time so the result is set in some separate async routine:
# Public function in the API: Send the request over some connection
def start_request(request_payload) -> ResultBase:
request_id = connection.send_request(request_payload)
result = aioresult.Future()
outstanding_requests[request_id] = result
return result
# Hidden function in the API: In a separate task, wait for responses to any request
async def get_responses():
while True:
request_id, response = await connection.get_next_response()
outstanding_requests[request_id].set_result(response)
del outstanding_requests[request_id]
# Caller code: Use the API and returned Future object
async def make_request():
f = start_request(my_request)
await f.wait_done()
print("result:", f.result())
If you need to wait for several futures to finish, in a similar way to asyncio.gather()
,
then use a nursery and await ResultBase.wait_done()
for them all:
results = [start_request(i) for i in range(10)]
async with trio.open_nursery() as n:
for f in some_futures:
n.start_soon(f.wait_done)
print("results:", *[f.result() for f in results
The above code also warks with ResultCapture
but in that case is usually a mistake – just
pass the nursery to the ResultCapture.start_soon()
function.
- class aioresult.Future¶
Bases:
ResultBase
Stores a result or exception that is explicitly set by the caller.
- set_result(result)¶
Sets the result of the future to the given value.
After calling this method, future calls to
ResultBase.result()
will return the value passed in.- Parameters:
result – The result value to be stored.
- Raises:
FutureSetAgainException – If the result has already been set with
set_result()
orset_exception()
.
- set_exception(exception)¶
Sets the exception of the future to the given value.
After calling this method, future calls to
ResultBase.result()
will throw an exception and calls toResultBase.exception()
will return this exception. The exception raised byResultBase.result()
will be aTaskFailedException
rather than the exception passed to this method, which matches the behaviour ofResultCapture
; see Exception handling and design rationale.- Parameters:
exception – The exception to be stored.
- Raises:
FutureSetAgainException – If the result has already been set with
set_result()
orset_exception()
.
Future
makes use of the following exception class:
- exception aioresult.FutureSetAgainException¶
Bases:
Exception
Raised if
Future.set_result()
orFuture.set_exception()
called more than once.It can also be raised from a
ResultCapture
ifResultCapture.run()
is called more than once.It is raised with the future passed as the argument to the exception:
raise FutureSetAgainException(self)
This allows access to the future from the exception using the
args
attribute:- args¶
1-tuple of the
ResultCapture
orFuture
that raised this exception.- Type:
(ResultBase, )