19

I'm writing a library that I'd like end-users to be able to optionally use as if its methods and functions were not coroutines.

For example, given this function:

@asyncio.coroutine
def blah_getter():
    return (yield from http_client.get('http://blahblahblah'))

An end user who doesn't care to use any asynchronous features in their own code, still has to import asyncio and run this:

>>> response = asyncio.get_event_loop().run_until_complete(blah_getter())

It would be cool if I could, inside of blah_getter determine if I was being called as a coroutine or not and react accordingly.

So something like:

@asyncio.coroutine
def blah_getter():
    if magically_determine_if_being_yielded_from():
        return (yield from http_client.get('http://blahblahblah'))
    else:
        el = asyncio.get_event_loop()
        return el.run_until_complete(http_client.get('http://blahblahblah'))
Andrew Svetlov
  • 16,730
  • 8
  • 66
  • 69
Dustin Wyatt
  • 4,046
  • 5
  • 31
  • 60

3 Answers3

30

You need two functions -- asynchronous coroutine and synchronous regular function:

@asyncio.coroutine
def async_gettter():
    return (yield from http_client.get('http://example.com'))

def sync_getter()
    return asyncio.get_event_loop().run_until_complete(async_getter())

magically_determine_if_being_yielded_from() is actually event_loop.is_running() but I strongly don't recommend to mix sync and async code in the same function.

Andrew Svetlov
  • 16,730
  • 8
  • 66
  • 69
  • 7
    I agree with this answer: mixing the two is a bad idea and may lead to confusion and unexpected results. "Explicit is better than implicit." – Andrea Corbellini May 11 '15 at 09:14
  • 2
    After thinking about it some more, I think you're right. Explicit is better than implicit! – Dustin Wyatt May 11 '15 at 15:31
  • There is related question here: https://stackoverflow.com/questions/45213133/async-sync-async-calls-in-one-python-event-loop – dzmitry Jul 20 '17 at 11:17
  • Will the approach works everywhere in Python 3 even we do not use **asyncio** in other parts of code? For instance, when we want a library which supports blocking/non-blocking functions. One example: I want a library which manages bots from usual (without async in case of one bot) function and from async (many bots) functions. – sergzach Nov 10 '17 at 11:10
  • In general -- yes, but your question is quite broad. – Andrew Svetlov Nov 10 '17 at 20:01
17

I agree with Andrew's answer, I just want to add that if you're dealing with objects, rather than top-level functions, you can use a metaclass to add synchronous versions of your asynchronous methods automatically. See this example:

import asyncio
import aiohttp

class SyncAdder(type):
    """ A metaclass which adds synchronous version of coroutines.

    This metaclass finds all coroutine functions defined on a class
    and adds a synchronous version with a '_s' suffix appended to the
    original function name.

    """
    def __new__(cls, clsname, bases, dct, **kwargs):
        new_dct = {}
        for name,val in dct.items():
            # Make a sync version of all coroutine functions
            if asyncio.iscoroutinefunction(val):
                meth = cls.sync_maker(name)
                syncname = '{}_s'.format(name)
                meth.__name__ = syncname
                meth.__qualname__ = '{}.{}'.format(clsname, syncname)
                new_dct[syncname] = meth
        dct.update(new_dct)
        return super().__new__(cls, clsname, bases, dct)

    @staticmethod
    def sync_maker(func):
        def sync_func(self, *args, **kwargs):
            meth = getattr(self, func)
            return asyncio.get_event_loop().run_until_complete(meth(*args, **kwargs))
        return sync_func

class Stuff(metaclass=SyncAdder):
    @asyncio.coroutine
    def getter(self, url):
        return (yield from aiohttp.request('GET', url))

Usage:

>>> import aio, asyncio
>>> aio.Stuff.getter_s
<function Stuff.getter_s at 0x7f90459c2bf8>
>>> aio.Stuff.getter
<function Stuff.getter at 0x7f90459c2b70>
>>> s = aio.Stuff()
>>> s.getter_s('http://example.com')
<ClientResponse(http://example.com) [200 OK]>
<CIMultiDictProxy {'ACCEPT-RANGES': 'bytes', 'CACHE-CONTROL': 'max-age=604800', 'DATE': 'Mon, 11 May 2015 15:13:21 GMT', 'ETAG': '"359670651"', 'EXPIRES': 'Mon, 18 May 2015 15:13:21 GMT', 'SERVER': 'ECS (ewr/15BD)', 'X-CACHE': 'HIT', 'X-EC-CUSTOM-ERROR': '1', 'CONTENT-LENGTH': '1270', 'CONTENT-TYPE': 'text/html', 'LAST-MODIFIED': 'Fri, 09 Aug 2013 23:54:35 GMT', 'VIA': '1.1 xyz.com:80', 'CONNECTION': 'keep-alive'}>
>>> asyncio.get_event_loop().run_until_complete(s.getter('http://example.com'))
<ClientResponse(http://example.com) [200 OK]>
<CIMultiDictProxy {'ACCEPT-RANGES': 'bytes', 'CACHE-CONTROL': 'max-age=604800', 'DATE': 'Mon, 11 May 2015 15:25:09 GMT', 'ETAG': '"359670651"', 'EXPIRES': 'Mon, 18 May 2015 15:25:09 GMT', 'SERVER': 'ECS (ewr/15BD)', 'X-CACHE': 'HIT', 'X-EC-CUSTOM-ERROR': '1', 'CONTENT-LENGTH': '1270', 'CONTENT-TYPE': 'text/html', 'LAST-MODIFIED': 'Fri, 09 Aug 2013 23:54:35 GMT', 'VIA': '1.1 xys.com:80', 'CONNECTION': 'keep-alive'}>
dano
  • 91,354
  • 19
  • 222
  • 219
0

Also you can create a simple decorator which makes your function synchronious. This approach can be applied to global functions and to methods.

An example.

# the decorator
def sync(f):
    ASYNC_KEY = 'async'

    def f_in(*args, **kwargs):
        if ASYNC_KEY in kwargs:
            async = kwargs.get(ASYNC_KEY)
            del kwargs[ASYNC_KEY]
        else:
            async = True

        if async:
            return f(*args, **kwargs)           
        else:
            return asyncio.get_event_loop().run_until_complete(f())

    return f_in

# below: the usage    
@sync
async def test():
    print('In sleep...')
    await asyncio.sleep(1)
    print('After sleep')    


# below: or asyncio.get_event_loop().create_task(test())
asyncio.get_event_loop().run_until_complete(test()) 
# and here is your syncronious version
test(async=False)

Moreover: it probably have sense to create special wrapper class not to pass async to every method call. The example is below.

class SyncCallerWrapper(object):
    def __init__(self, obj, is_async=True):
        self._obj = obj 
        self._is_async = is_async


    def __getattr__(self, name):
        def sync_wrapper(obj_attr):
            def f(*args, **kwargs):
                return asyncio.get_event_loop().run_until_complete(obj_attr(*args, **kwargs))

            return f

        obj_attr = getattr(self._obj, name)

        if not self._is_async and asyncio.iscoroutinefunction(obj_attr):
            return sync_wrapper(obj_attr)           

        return obj_attr


class C(object):
    async def sleep1(self):
        print('In sleep1...')
        await asyncio.sleep(1)
        print('After sleep1')


    async def sleep2(self):
        print('In sleep2...')
        await asyncio.sleep(1)
        print('After sleep2')       


# you don't want any concurrency in your code
c_sync = SyncCallerWrapper(C(), is_async=False)
c_sync.sleep1()
c_sync.sleep2()

# here you want concurrency: class methods are coroutines
c_async = SyncCallerWrapper(C(), is_async=True)
asyncio.get_event_loop().run_until_complete(c_async.sleep1())
asyncio.get_event_loop().run_until_complete(c_async.sleep2())

To be more elegant you can replace your class with a function (global constructor). Then a user could create class C passing is_async parameter and have desired behaviour: methods will act as regular (is_async=False) or as async functions (is_async=True).

def C(*args, **kwargs):
    KEY_ISASYNC = 'is_async'
    if KEY_ISASYNC in kwargs:
        is_async = kwargs.get(KEY_ISASYNC)
        del kwargs[KEY_ISASYNC]
    else:
        is_async = False
    return SyncCallerWrapper(_C(*args, **kwargs), is_async=is_async)

# you don't want any concurrency in your code
c_sync = C(is_async=False)
c_sync.sleep1()
c_sync.sleep2()

# here you want concurrency: class methods are coroutines
c_async = C(is_async=True)
asyncio.get_event_loop().run_until_complete(c_async.sleep1())
asyncio.get_event_loop().run_until_complete(c_async.sleep2())
sergzach
  • 6,578
  • 7
  • 46
  • 84
  • in later versions of python async is a keyword that is invalid in function signatures, so you'll need to change the ASYNC_KEY to something other than async. – Erik Aronesty Feb 26 '19 at 14:52