Package: greenback
Python 3.5 introducedasync/awaitsyntax for defining
functions that can run concurrently in a cooperative multitasking
framework such asasyncioorTrio. Such frameworks have a number of advantages
over previous approaches to concurrency: they scale better than threads and areclearer about control flowthan the implicit cooperative multitasking provided bygevent. They’re also being
actively developed to explore somenew ideas about concurrent programming.Porting an existing codebase toasync/awaitsyntax can be
challenging, though, since it’s somewhat “viral”: only an async
function can call another async function. That means you don’t just have
to modify the functions that actually perform I/O; you also need to
(trivially) modify every function that directly or indirectly calls a
function that performs I/O. While the results are generally an improvement
(“explicit is better than implicit”), getting there in one big step is not
always feasible, especially if some of these layers are in libraries that
you don’t control.greenbackis a small library that attempts to bridge this gap. It
allows you tocall back into async code from a syntactically
synchronous function, as long as the synchronous function was
originally called from an async task (running in an asyncio or Trio
event loop) that set up agreenback“portal” as explained
below. This is potentially useful in a number of different situations:You can interoperate with some existing libraries that are notasync/awaitaware, without pushing their work off into
another thread.You can migrate an existing program toasync/awaitsyntax one layer at a time, instead of all at once.You can (cautiously) design async APIs that block in places where
you can’t writeawait, such as on attribute accesses.greenbackrequires Python 3.8 or later and an implementation that
supports thegreenletlibrary. Either CPython or PyPy should work.
There are no known OS dependencies.QuickstartCallawait greenback.ensure_portal()at least once in each task that will be
usinggreenback. (Additional calls in the same task do nothing.) You can think
of this as creating a portal that will be used by future calls togreenback.await_()in the same task.Later, usegreenback.await_(foo())as a replacement forawait foo()in places where you can’t writeawait.If all of the places where you want to usegreenback.await_()are indirectly within a single function, you can
eschew theawait greenback.ensure_portal()and instead write a wrapper
around calls to that function:awaitgreenback.with_portal_run(...)for an async function, orawaitgreenback.with_portal_run_sync(...)for a synchronous function. These have the advantage of cleaning up the
portal (and its associated minor performance impact) as soon as the
function returns, rather than leaving it open until the task terminates.For more details and additional helper methods, see thedocumentation.ExampleSuppose you start with this async-unaware program:importsubprocessdefmain():print_fact(10)defprint_fact(n,mult=1):"""Print the value of *n* factorial times *mult*."""ifn<=1:print_value(mult)else:print_fact(n-1,mult*n)defprint_value(n):"""Print the value *n* in an unreasonably convoluted way."""assertisinstance(n,int)subprocess.run(f"echo{n}",shell=True)if__name__=="__main__":main()Usinggreenback, you can change it to run in a Trio event loop by
changing only the top and bottom layers, with no change toprint_fact().importtrioimportgreenbackasyncdefmain():awaitgreenback.ensure_portal()print_fact(10)defprint_fact(n,mult=1):"""Print the value of *n* factorial times *mult*."""ifn<=1:print_value(mult)else:print_fact(n-1,mult*n)defprint_value(n):"""Print the value *n* in an unreasonably convoluted way."""assertisinstance(n,int)greenback.await_(trio.run_process(f"echo{n}",shell=True))if__name__=="__main__":trio.run(main)FAQWhy is it called “greenback”?It uses thegreenletlibrary to get youbackto an enclosing async context. Also, maybe it saves youmoney(engineering time) or
something.How does it work?After you runawait greenback.ensure_portal()in a certain task, that task will run inside a greenlet.
(This is achieved by interposing a “shim” coroutine in between the event
loop and the coroutine for your task; see the source code for details.)
Calls togreenback.await_()are then able to switch from that greenlet
back to the parent greenlet, which can easily perform the necessaryawaitsince it has direct access to the async environment. The
task greenlet is then resumed with the value or exception
produced by theawait.Should I trust this in production?Maybe; try it and see. The
technique is rather low-level, and has some minorperformance implications(any task in which you callawait greenback.ensure_portal()will run a bit slower), but we’re in
good company: SQLAlchemy’s async ORM support is implemented in much
the same way.greenbackitself is a fairly small amount of
pure-Python code on top ofgreenlet. (There is one small usage ofctypesto work around a knob that’s not exposed by the asyncio
acceleration extension module on CPython.)greenletis a C module full of platform-specific arcana, but
it’s been around for a very long time and popular production-quality
concurrency systems such asgeventrely heavily on it.What won’t work?A few things:Greenlet switching works by moving parts of the C stack to different
memory addresses, relying on the assumption that Python objects are
fully heap-allocated and don’t contain any pointers into the C
stack. Poorly-behaved C extension modules might violate this
assumption and are likely to crash if used withgreenback.
Such extension modules are buggy and could be made to crash withoutgreenbacktoo, but perhaps only under an obscure or unlikely
series of operations.Callinggreenback.await_()inside a finalizer (__del__method), signal handler, or weakref callback is unsupported. It
might work most of the time, or even all the time, but the
environment in which such methods run is weird enough that the
author isn’t prepared to make any guarantees.  (Not that you have
any guarantees about the rest of it, just some better theoretical
grounding.)Licensegreenbackis licensed under your choice of the MIT or Apache 2.0 license.
SeeLICENSEfor details.