Styvane Styvane - 1 month ago 8
Python Question

Return a future result using single event loop with decorated coroutine

I have a decorator that decorate a coroutine function and assign the value returned by the coroutine to a future instance.

import asyncio
import functools


def ensure_prepare(future):
async def decorator(asyncfunc):
@functools.wraps(asyncfunc)
async def wrapper(*args, **kwargs):
future.set_result(await asyncfunc(*args, **kwargs))
return future
return wrapper
return decorator


Demo:

>>> future = asyncio.Future()
>>>
>>> @ensure_prepare(future)
... async def check_sanity():
... return 9
...
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(check_sanity)
<function check_sanity at 0x7f935300a158>
>>> _()
<coroutine object check_sanity at 0x7f934f78a728>
>>> loop.run_until_complete(_)
<Future finished result=9>
>>> _.result()
9


As you can see I need to run two times the event loop in order to get the future result. Is there any way to make the event loop return the value after the first run? I don't want to await in my code and assign the result(function) to a name.

Answer

in your code you have made your decorator wrapper to be async which is not what you wanted, it means that any time you use your wrapper it gives back a coroutine object that will generate the wrapped function:

>>> future = asyncio.Future()
>>> @ensure_prepare(future)
async def chech_sanity():
    return 9

>>> check_sanity
<coroutine object ensure_prepare.<locals>.decorator at 0x10572f4c0>

>>> check_sanity.send(None) #advance coroutine
Traceback (most recent call last):
  File "<pyshell#7>", line 1, in <module>
    check_sanity.send(None) #advance coroutine
StopIteration: <function check_sanity at 0x105096a60>

               # ^ the function is the result of the coroutine

so just remove the async in the line

async def decorator(asyncfunc):

And your problem will be solved:

def ensure_prepare(future):
    def decorator(asyncfunc):
        @functools.wraps(asyncfunc)
        async def wrapper(*args, **kwargs):
            future.set_result(await asyncfunc(*args, **kwargs))
            return future
        return wrapper
    return decorator


>>> future = asyncio.Future()
>>> @ensure_prepare(future)
async def check_sanity():
    return 9

>>> chech_sanity
<function check_sanity at 0x105784a60>
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(check_sanity()) #remember to call check_sanity!
<Future finished result=9>