Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Contextvars + async generator + break + finally #118944

Closed
sylvainmouquet opened this issue May 11, 2024 · 3 comments
Closed

Contextvars + async generator + break + finally #118944

sylvainmouquet opened this issue May 11, 2024 · 3 comments
Labels
type-bug An unexpected behavior, bug, or error

Comments

@sylvainmouquet
Copy link

sylvainmouquet commented May 11, 2024

Bug report

Bug description:

import asyncio
import contextvars

async def gen():
    var = contextvars.ContextVar('one')

    token = var.set(1)

    ctx = contextvars.copy_context()
    print(list(ctx.items()))

    try:
        yield 1
        yield 2
    finally:
        ctx = contextvars.copy_context()
        print(list(ctx.items()))

        try:
            var.reset(token)
        except Exception as e:
            print(e.args)

async def main():
    async for i in gen():
        print(i)
        break

asyncio.run(main())

result :

[(<ContextVar name='one' at 0x107312930>, 1)]
1
[(<ContextVar name='one' at 0x107312930>, 1)]
("<Token var=<ContextVar name='one' at 0x107312930> at 0x107303480> was created in a different Context",)

In finally block it's not possible to manually reset the contextvars

CPython versions tested on:

3.12

Operating systems tested on:

macOS

@sylvainmouquet sylvainmouquet added the type-bug An unexpected behavior, bug, or error label May 11, 2024
@graingert
Copy link
Contributor

graingert commented May 11, 2024

Yes this is expected, because the async generator finalization is triggered by the garbage collector, where asyncio uses sys.set_asyncgen_hooks() to aclose the async generator in a new task. See https://peps.python.org/pep-0525/#finalization

If you want to ensure finalization in the same context use

async def main():
    async with contextlib.aclosing(gen()) as agen:
        async for i in agen:
            print(i)
            break()

@graingert
Copy link
Contributor

graingert commented May 11, 2024

See also https://docs.python.org/3/reference/expressions.html#asynchronous-generator-functions

If an asynchronous generator happens to exit early by break, the caller task being cancelled, or other exceptions, the generator’s async cleanup code will run and possibly raise exceptions or access context variables in an unexpected context–perhaps after the lifetime of tasks it depends, or during the event loop shutdown when the async-generator garbage collection hook is called. To prevent this, the caller must explicitly close the async generator by calling aclose() method to finalize the generator and ultimately detach it from the event loop.

And https://docs.python.org/3/library/contextlib.html#contextlib.aclosing

This pattern ensures that the generator’s async exit code is executed in the same context as its iterations (so that exceptions and context variables work as expected, and the exit code isn’t run after the lifetime of some task it depends on).

@graingert
Copy link
Contributor

I'm going to close this, as working as documented

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

2 participants