Home > OS >  Why __aexit__ is not fully executed when it has await inside?
Why __aexit__ is not fully executed when it has await inside?

Time:07-03

This is the simplified version of my code:

main is a coroutine which stops after the second iteration.
get_numbers is an async generator which yields numbers but within an async context manager.

import asyncio


class MyContextManager:
    async def __aenter__(self):
        print("Enter to the Context Manager...")
        return self

    async def __aexit__(self, exc_type, exc_value, exc_tb):
        print(exc_type)
        print("Exit from the Context Manager...")
        await asyncio.sleep(1)
        print("This line is not executed")  # <-------------------
        await asyncio.sleep(1)


async def get_numbers():
    async with MyContextManager():
        for i in range(30):
            yield i


async def main():
    async for i in get_numbers():
        print(i)
        if i == 1:
            break


asyncio.run(main())

And the output is:

Enter to the Context Manager...
0
1
<class 'asyncio.exceptions.CancelledError'>
Exit from the Context Manager...

I have two questions actually:

  1. From my understanding, AsyncIO schedules a Task to be called soon in the next cycle of the event loop and gives __aexit__ a chance to execute. But the line print("This line is not executed") is not executed. Why is that? Is it correct to assume that if we have an await statement inside the __aexit__, the codes after that line is not going to execute at all and we shouldn't rely on that for cleaning?

  1. Output of the help() on async generators shows that:
 |  aclose(...)
 |      aclose() -> raise GeneratorExit inside generator.

so why I get <class 'asyncio.exceptions.CancelledError'> exception inside the __aexit__ ?

* I'm using Python 3.10.4

CodePudding user response:

I'm not sure what's happening, but posting what I found in case it proves useful to others who decide to investigate. The output changes to expected when we store reference to get_numbers() outside main(). I would say that it seems get_numbers() is garbage collected to early, but disabling gc doesn't help, so my guess may be wrong.

import asyncio


test = None


class MyContextManager:
    async def __aenter__(self):
        print("Enter to the Context Manager...")
        return self

    async def __aexit__(self, exc_type, exc_value, exc_tb):
        print(exc_type)
        print("Exit from the Context Manager...")
        await asyncio.sleep(1)
        print("This line is not executed")  # <-- Executed now
        await asyncio.sleep(1)


async def get_numbers():
    async with MyContextManager():
        for i in range(30):
            yield i


async def main():
    global test
    test = get_numbers()

    async for i in test:
        print(i)
        if i == 1:
            break


asyncio.run(main())

CodePudding user response:

To answer the first question:

Is it correct to assume that if we have an await statement inside the __aexit__, the codes after that line is not going to execute at all?

I would say no it's always the case. As long as the main has enough time and can again pass the control back to event loop, the code inside the __aexit__ may execute. I tried this:

async def main():
    async for i in get_numbers():
        print(i)
        if i == 1:
            break
    await asyncio.sleep(4)   #  <---- New

.run() only cares about the coroutine which is passed to it and runs that to the end, not the other coroutines including __aexit__. So if it doesn't have enough time or doesn't pass the control to event loop, I can't rely on the next lines after the first await statement.

CodePudding user response:

the answer is simple: the interpreter will continue executing __aexit__ after one second, but the main function is finished and there is no pointer to the context manager.

first obvious solution which is mentioned by yourself is to wait long enough after the main function:

async def main():
    async for i in get_numbers():
        print(i)
        if i == 1:
            break
    await asyncio.sleep(4)   #  <---- New

another way is to use try/finally:

    async def __aexit__(self, exc_type, exc_value, exc_tb):
        try:
            pass
            print(exc_type)
            print("Exit from the Context Manager...")
            await asyncio.sleep(1)
        finally:
            print("This line is not executed")  # <-------------------
  • Related