Evan
Evan

Reputation: 345

Cancelling an async context manager

It's easy enough to get a handle to a task for cancellation:

task = loop.create_task(coro_fn())

# later
task.cancel()

Is it possible to do the same for async context managers?

async with foo() as bar:
   # is it possible to cancel before getting here,
   # while waiting to enter the context manager?
   await bar.baz()

# later
# How do I get a handle to the context manager for cancellation?

Is there any way to do this? Or does the context manager code need to run in its own task?

Upvotes: 2

Views: 1593

Answers (1)

user4815162342
user4815162342

Reputation: 155356

How do I get a handle to the context manager for cancellation?

You don't, at least not directly - a context manager is just a convenient packaging for methods to acquire and release a resource. The async with foo() as bar: ... from your question desugars to something roughly like:

_cm = foo()
bar = await _cm.__aenter__()
try:
    await bar.baz()
finally:
    await _cm.__aexit__(*sys.exc_info())

Both __aenter__ and __aexit__ are normal awaitables, whose execution can be cancelled, just like the execution of await bar.baz() inside the async with body.

So if by "canceling a context manager" you mean cancel the operation that the async with block is currently stuck in, you can do it the same as for any other cancellation. For example, you could extract the async with into a coroutine that runs in its own task and cancel() that. Note that you'd need to obtain a task anyway to even reach the "later" section without the async context manager getting closed in the process.

Here is an example:

async def run_foo():
    async with foo() as bar:
       # is it possible to cancel before getting here,
       # while waiting to enter the context manager?
       await bar.baz()

task = asyncio.create_task(run_foo())
# "task" is the handle you can await or cancel at your leisure

Upvotes: 4

Related Questions