Package: traio
A simple asyncio wrapper attempting to look like TrioWhen going deeper and deeper with asyncio, and managing a lot of tasks
in parallel, you notice that on top of having a lot to deal with
to keep an eye on all your task, you also end up always doing the
same kind of boiler plate, and the code can become easily twisted and
unreadable.Triois there to make asynchronous
programming easy and more "for humans".Traio (as kind of Trio on top of Asyncio) let you use asyncio with a little of
the philosophy of Trio, mostly the Nursery concept (called Scope here).
It also synthesize the most common pattern we are using for handling async
tasks in a sane way.DisclaimerThis isnota replacement for Trio: if you do a full Trio-like project,
just use Trio! It's just truly awesome, but in some cases you get stuck
with asyncio, but still want to have a code you can read and manage...Because we run on top of asyncio, we are quite limited in how we can handle
cancellation, scopes, and coroutines. This isjuston top of asyncio, this
is not an alternative! But on the good side, you can mix this with regular
asyncio code, which can look appealing.ExamplesSimple ScopeThe main way to use the Scope is (like in Trio) as a context managerimportasynciofromtraioimportScopeasyncdeffetch_url(x):# Do something longawaitasyncio.sleep(3)asyncdefmain():asyncwithScope(timeout=10)asscope:foriinrange(10):scope.spawn(fetch_url(i))TheScope.spawnmethod, called on an awaitable thing, will spawn a task and
register it on the scope. An equivalent exist using the<<operator (see next
example).When reaching the end of the context block, the code will block until:all tasks are doneor the timeout is overor the scope get's cancelled.You can also use the Scope without context manager:importasynciofromtraioimportScopeasyncdeffetch_url(x):# Do something longawaitasyncio.sleep(3)asyncdefmain():# Equivalent to previous examplescope=Scope(timeout=10)foriinrange(10):scope<<fetch_url(i)scope.finalize()awaitscopeAwaiting a scope will block until the scope is fully complete: all active tasks
have finished or the scope was cancelled. But unlessscope.finalize()is called,
a scope will note stop on the last task being complete, only on cancellation.Thefinalizemethod is called automatically when used as a context manager.Names and loggerIf you went deep enough in asyncio mysteries, you know that tracing code is
(for now) kind of a nightmare... For that reason, Scope as well as tasks can
be instantiated with a name. The Scope can also take a logger which will be
used for tracing most of the calls and task life cycle, mostly with debug level.Special tasksScope.spawncan be called with different parameters with interesting
effects:Thebubbleboolean parameter controls task error bubbling. A task will
bubble by default. This means that an error in the task will cause the task
to stop (of course), but the scope will be cancelled as well and raise
the given error. This is the default behavior.
But it can be useful in some cases not to do that, and just ignore a task.
Not that if you await manually a task, or add a done callback, this cancels
bubbling automatically: if you take the pain of waiting for a task,
it's not to get all the rest cancelled!importasynciofromtraioimportScopeasyncdeffetch_url():# Do something longawaitasyncio.sleep(10)asyncdeftrivial():awaitasyncio.sleep(0.01)raiseValueError('not interesting')asyncdefmain():asyncwithScope(timeout=0.5)asn:# This will try to run for 10 secondsn<<fetch_url()# This will run a bit then raise (but not 'bubble')n.spawn(trivial(),bubble=False)# Eventually after 0.5 seconds the Scope times out and# gives a TImeoutErrorA task can be marked asmaster, in that case the scope will die with the
task when done. This is typically useful when you have one main task to be
performed and other background ones, which have no meaning if the main one stops.importasynciofromtraioimportScopeasyncdeffetch_url():# Do something longawaitasyncio.sleep(10)asyncdeftrivial():awaitasyncio.sleep(0.01)asyncdefmain():asyncwithScope(timeout=0.5)asn:# This will try to run for 10 secondsn<<fetch_url()# This will run a bit then scope gets cancelled when it endsn.spawn(trivial(),master=True)A task by default isawaited, which means the scope will wait
for it to finish during finalisation stage, before exiting.
It is possible to mark some tasks as notawaitedif you want a task running,
but not so essential that it should prevent cancellation.
Typically, a background job which has no meaning alone.importasynciofromtraioimportScopeasyncdefbackground():whileTrue:# Do something periodic and sleepawaitasyncio.sleep(1)asyncdefjob():# Do the real workawaitasyncio.sleep(10)asyncdefmain():asyncwithScope()asn:# Spawn a background jobn.spawn(background(),awaited=False)# Do what you have to do.n<<job()# At this point, job is done and background task was cancelledNote you can combine all flags; If bubble is False and master is True,
the scope will exit silently when the task is done, even with an exception,
for example.Nested ScopeThat's one of the most exciting ones: you can spawn a sub-scope from the
original one, which will follow the same rules as any Scope but will as well
die if the parent is cancelled!importasynciofromtraioimportScopeasyncdeffetch_url():# Do something longawaitasyncio.sleep(10)asyncdefmain():asyncwithScope(timeout=0.2)asparent:asyncwithparent.fork(timeout=0.5)asinner:# This will try to run for 10 secondsinner<<fetch_url()# Here the outer Scope will timeout first and will cancel the inner one!Running synchronous code with executorsIt is possible to run synchronous code from a scope using theloop.run_in_executormethod; this would return a coroutine which
you can spawn as usual. But beware of using that method! The same way
interrupts handling is a mess in asyncio, using thread from asyncio is tricky.
Most OS don't support cancelling running threads, so does Python.
As such, once your executor code is running, the scope has no way to
actually stop it; if you cancel the scope, the corresponding future will be
cancelled but the task will continue running if not yet finished, resulting in
various "fun" situations if that task has side effects...So: yes, you can use this, but at your own risks!importasyncioimporttimefromtraioimportScopedeffetch_url():# Do something long, sync!time.sleep(10)asyncdefmain():asyncwithScope(timeout=0.2)asscope:scope<<asyncio.get_event_loop().run_in_executor(None,fetch_url)Getting current ScopeUsing the awesomecontextvarsand
the current backport of itaiocontextvars,
we can keep track of the current active scope without necessarily passing the
scope variable all along the call chain.asyncdefhandler(client):# This runs in the scope `main_scope`# Create a subscope of the current oneasyncwithScope.get_current().fork()ass:# Here we are in the scope of `s`, as well as spawned taskss<<do_something(client)s<<do_something_else(client)asyncdefserver(main_scope):# This runs in the scope `main_scope`client=awaitnew_connection()main_scope.spawn(handler(client))asyncdefmain():asyncwithScope()asmain_scope:main_scope<<server(main_scope)main_scope<<do_something_else()StatusThis is beta. We are not going to change the API (much) anymore.TODOSwrite more examplesextend the API:new kinds of tasks to investigate, for example executors (although we have no way to stop
a thread executor...)play more with the real Trio and get a better feeling of itget some user feedback if possible!Similar projectsYou may like as wellAyo, discovered in parallel of writting this lib!