Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 858
freebie Avatar asked Sep 29 '16 19:09

freebie


1 Answers

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)
like image 181
A. Jesse Jiryu Davis Avatar answered Oct 04 '22 21:10

A. Jesse Jiryu Davis