Search code examples
pythonpython-asynciocontextmanager

Is yielding a future from an contextlib asynccontextmanager unsafe?


I have a problem where I need to check that something's occurred in the log file between two points in execution.

Currently I do this:

print("start")
# do something here
print("end")
res = await check_log(...) # check in the log if something's happened 
                        # between start and end and return the line if so

I'm wondering if I can instead have a contextlib asynccontextmanager that looks like this:

class foo():
    def __init__(self, ...):
    ...

    @asynccontextmanager
    async def bar(self):
        print("start")
        fut = asyncio.Future()
        yield fut
        print("end")
        res = await check_log(...)
        fut.set_result(res)

and which I call like this, where the class contains variables I would otherwise have to pass to check_log:

obj = foo(...)

async with obj.bar() as b:
    # do something here

res = b.result()

Is there anything fundamentally unsafe or wrong about this? If so, is there a better way of doing this? I know with a regular context manager you can get around it by setting an attribute though I'm not sure it's possible with contextlib.


Solution

  • This looks perfectly valid and ok. The fundamental thing to keep in mind is that execution will only proceed in the code containing the with block after the __aexit__ method in the context manager is executed - which means running the part past the yield fut in your method, and therefore, the completion of the the await check_log(...) execution.

    If you want check_log to perform concurrently with the block after the with block, that is equally as easy to do by changing creating check_log as a task, and setting its done callback to a function that will set fut's result - then, in this example, b could be awaited whenever one would like to check that result. Otherwise, it is good as is. (Just, please, put that yield statement and the block after it in a try-finally compound).

    Of course, note that if someone tries to await b inside the with block, your code will be dead-locked. If you have some value that the code post- with can get with no side-effects, you could use a contextvar.ContextVars as a class attribute of foo:

    import contextvars
    
    class foo():
        last_log = contextvars.ContextVar("last_log")
        def __init__(self, ...):
        ...
    
        @asynccontextmanager
        async def bar(self):
            print("start")
            try:
                yield None
            finally:
                print("end")
                self.last_log.set(await check_log(...))
    ...
    
    obj = foo(...)
    
    async with obj.bar():
        # do something here
    
    res = obj.last_log.get()