Package: quattro
quattro: task control for asyncioquattrois an Apache 2 licensed library, written in Python, for task control in asyncio applications.quattrois influenced by structured concurrency concepts from theTrio framework.quattrosupports Python versions 3.9 - 3.11, including PyPy.InstallationTo installquattro, simply:$pipinstallquattroquattro.gatherquattrocomes with an independent, simple implementation ofasyncio.gatherbased on Task Groups.
Thequattroversion is safer, and uses a task group under the hood to not leak tasks in cases of errors in child tasks.fromquattroimportgatherasyncdefmy_handler():res_1,res_2=awaitgather(long_query_1(),long_query_2())Thereturn_exceptionsargument can be used to makegather()catch and return exceptions as responses instead of letting them bubble out.fromquattroimportgatherasyncdefmy_handler():res_1,res_2=awaitgather(long_query_1(),long_query_2(),return_exceptions=True,)# res_1 and res_2 may be instances of exceptions.The differences toasyncio.gather()are:If a child task fails other unfinished tasks will be cancelled, just like in a TaskGroup.quattro.gather()only accepts coroutines and not futures and generators, just like a TaskGroup.Whenreturn_exceptionsis false (the default), an exception in a child task will cause an ExceptionGroup to bubble out of the top-levelgather()call, just like in a TaskGroup.Results are returned as a tuple, not a list.Cancel Scopesquattrocontains an independent, asyncio implementation ofTrio CancelScopes.
Due to fundamental differences between asyncio and Trio the actual runtime behavior isn't exactly the same, but close.fromquattroimportmove_on_afterasyncdefmy_handler():withmove_on_after(1.0)ascancel_scope:awaitlong_query()# 1 second later, the function continues runningquattrocontains the following helpers:move_on_aftermove_on_atfail_afterfail_atAll helpers produce instances ofquattro.CancelScope, which is largely similar to the Trio variant.CancelScopeshave the following attributes:cancel()- a method through which the scope can be cancelled manually.cancel()can be called before the scope is entered; entering the scope will cancel it at the first opportunitydeadline- read/write, an optional deadline for the scope, at which the scope will be cancelledcancelled_caught- a readonly bool property, whether the scope finished via cancellationquattroalso supports retrieving the current effective deadline in a task usingquattro.current_effective_deadline.
The current effective deadline is a float value, withfloat('inf')standing in for no deadline.Python versions 3.11 and higher containsimilar helpers,asyncio.timeoutandasyncio.timeout_at.
Thequattrofail_afterandfail_athelpers are effectively equivalent to the asyncio timeouts, and pass the test suite for them.The differences are:Thequattroversions are normal context managers (used with justwith), asyncio versions are async context managers (usingasync with).
Neither version needs to be async since nothing is awaited;quattrochooses to be non-async to signal there are no suspension points being hit, match Trio and be a little more readable.quattroadditionally contains themove_on_atandmove_on_afterhelpers.Thequattroversions support getting the current effective deadline.Thequattroversions can be cancelled manually usingscope.cancel(), and precancelled before they are enteredThequattroversions are available on all supported Python versions, not just 3.11+.asyncio and Trio differencesfail_afterandfail_atraiseasyncio.Timeoutinstead oftrio.Cancelledexceptions when they fail.asyncio has edge-triggered cancellation semantics, while Trio has level-triggered cancellation semantics.
The following example will behave differently inquattroand Trio:withtrio.move_on_after(TIMEOUT):conn=make_connection()try:awaitconn.send_hello_msg()finally:awaitconn.send_goodbye_msg()In Trio, if theTIMEOUTexpires while awaitingsend_hello_msg(),send_goodbye_msg()will also be cancelled.
Inquattro,send_goodbye_msg()will run (and potentially block) anyway.
This is a limitation of the underlying framework.Inquattro, cancellation scopes cannot be shielded.Task GroupsOn Python 3.11 and later, thestandard library TaskGroupimplementation is used instead.
The TaskGroup implementation here can be considered a backport for older Python versions.quattrocontains a TaskGroup implementation. TaskGroups are inspired byTrio nurseries.fromquattroimportTaskGroupasyncdefmy_handler():# We want to spawn some tasks, and ensure they are all handled before we return.asyncdeftask_1():...asyncdeftask_2():...asyncwithTaskGroup()astg:t1=tg.create_task(task_1)t2=tg.create_task(task_2)# The end of the `async with` block awaits the tasks, ensuring they are handled.TaskGroups are essential building blocks for achieving the concept ofstructured concurrency.
In simple terms, structured concurrency means your code does not leak tasks - when a coroutine
finishes, all tasks spawned by that coroutine and all its children are also finished.
(In fancy terms, the execution flow becomes a directed acyclic graph.)Structured concurrency can be achieved by using TaskGroups instead ofasyncio.create_taskto start background tasks.
TaskGroups essentially do two things:when exiting from a TaskGroupasync withblock, the TaskGroup awaits all of its children, ensuring they are finished when it exitswhen a TaskGroup child task raises an exception, all other children and the task inside the context manager are cancelledThe implementation has been borrowed from the EdgeDB project.Changelog23.1.0 (2023-11-29)Introducequattro.gather.
(#5)Add support for Python 3.12.Switch toPDM.22.2.0 (2022-12-27)More robust nested cancellation on 3.11.Better typing support forfail_afterandfail_at.Improve effective deadline handling for pre-cancelled scopes.TaskGroups now support custom ContextVar contexts when creating tasks, just like the standard library implementation.22.1.0 (2022-12-19)Restore TaskGroup copyright notice.TaskGroups now raise ExceptionGroups (using the PyPI backport when necessary) on child errors.Add support for Python 3.11, drop 3.8.TaskGroups no longer have anameand therepris slightly different, to harmonize with the Python 3.11 standard library implementation.TaskGroups no longer swallow child exceptions when aborting, to harmonize with the Python 3.11 standard library implementation.Switch to CalVer.0.3.0 (2022-01-08)Addpy.typedto enable typing information.Flesh out type annotations for TaskGroups.0.2.0 (2021-12-27)Addquattro.current_effective_deadline.0.1.0 (2021-12-08)Initial release, containing task groups and cancellation scopes.CreditsThe initial TaskGroup implementation has been taken from theEdgeDBproject.
The CancelScope implementation was heavily influenced byTrio, and inspired by theasync_timeoutpackage.