freebie freebie - 2 months ago 15
Python Question

Optional Synchronous Interface to Asynchronous Functions

I'm writing a library which is using Tornado Web's

tornado.httpclient.AsyncHTTPClient
to make requests which gives my code a
async
interface of:

async def my_library_function():
return await ...


I want to make this interface optionally serial if the user provides a kwarg - something like:
serial=True
. Though you can't obviously call a function defined with the
async
keyword from a normal function without
await
. This would be ideal - though almost certain imposible in the language at the moment:

async def here_we_go():
result = await my_library_function()
result = my_library_function(serial=True)


I'm not been able to find anything online where someones come up with a nice solution to this. I don't want to have to reimplement basically the same code without the
awaits
splattered throughout.

Is this something that can be solved or would it need support from the language?




Solution (though use Jesse's instead - explained below)



Jesse's solution below is pretty much what I'm going to go with. I did end up getting the interface I originally wanted by using a decorator. Something like this:

import asyncio
from functools import wraps


def serializable(f):
@wraps(f)
def wrapper(*args, asynchronous=False, **kwargs):
if asynchronous:
return f(*args, **kwargs)
else:
# Get pythons current execution thread and use that
loop = asyncio.get_event_loop()
return loop.run_until_complete(f(*args, **kwargs))
return wrapper


This gives you this interface:

result = await my_library_function(asynchronous=True)
result = my_library_function(asynchronous=False)


I sanity checked this on python's async mailing list and I was lucky enough to have Guido respond and he politely shot it down for this reason:


Code smell -- being able to call the same function both asynchronously
and synchronously is highly surprising. Also it violates the rule of
thumb that the value of an argument shouldn't affect the return type.


Nice to know it's possible though if not considered a great interface. Guido essentially suggested Jesse's answer and introducing the wrapping function as a helper util in the library instead of hiding it in a decorator.

Answer

When you want to call such a function synchronously, use run_until_complete:

asyncio.get_event_loop().run_until_complete(here_we_go())

Of course, if you do this often in your code, you should come up with an abbreviation for this statement, perhaps just:

def sync(fn, *args, **kwargs):
    return asyncio.get_event_loop().run_until_complete(fn(*args, **kwargs))

Then you could do:

result = sync(here_we_go)