pythongeneratorpython-asyncio

Translating async generator into sync one


Imagine we have an original API that returns a generator (it really is a mechanisms that brings pages/chunks of results from a server while the providing a simple generator to the user, and lets him iterate over these results one by one. For simplicity:

# Original sync generator
def get_results():
     # fetch from server
     yield 1
     yield 2
     # fetch next page
     yield 3
     yield 4
     # ....

Now there is a need to implement an asyncio version of the API, however we need to keep the old API operational as well. This is where things get complicated, we kind of want to translate an async generator into sync one, but I can't find an elegant way to do that. The best I could make work so far is "fetch all result into a list first, then provide a fake sync generator on that list". Which kind of defeats the purpose:

# Async generator
async def get_results_async():
     # await fetch from server
     yield 1
     yield 2
     # await fetch next page
     yield 3
     yield 4
     # ....


# Backward compatible sync generator
def get_results():

    async def gather_all_results():
        res = []
        async for i in get_results_async():
            res.append(i)
        return res

    res = asyncio.run(gather_all_results())
    for i in res:
        yield i

Is there a better, more elegant way to do that without fetching all the results before returning them?

Thanks


Solution

  • For the reason that asyncio is contagious, it's hard to write elegant code to integrate asyncio code into the old codes. For the scenario above, the flowing code is a little better, but I don't think it's elegant enough.

    async def get_results_async():
        # await fetch from server
        yield 1
        yield 2
        # await fetch next page
        yield 3
        yield 4
        # ....
    
    
    # Backward compatible sync generator
    def get_results():
        gen = get_results_async()
        while True:
            try:
                yield asyncio.run(gen.__anext__())
            except StopAsyncIteration:
                break 
    

    And you can re-use your event loop and not to create a new one.

    async def get_results_async():
        # await fetch from server
        yield 1
        yield 2
        # await fetch next page
        yield 3
        yield 4
        # ....
    
    # loop that you save in somewhere.
    loop = asyncio.get_event_loop()
    
    # Backward compatible sync generator
    def get_results():
        gen = get_results_async()
        while True:
            try:
                yield loop.run_until_complete(gen.__anext__())
            except StopAsyncIteration:
                break