GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Improve `contextlib.ContextDecorator` support for generators and async functions · Issue #125862 · python/cpython

Improve contextlib.ContextDecorator support for generators and async functions #125862

New issue
Copy link
New issue
Copy link
Closed
Closed
Improve contextlib.ContextDecorator support for generators and async functions#125862
Copy link
Assignees
gpshead
Labels
3.15bugs and security fixesbugs and security fixesstdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytype-featureA feature request or enhancementA feature request or enhancement

Description

@Zac-HD
Zac-HD
opened on Oct 23, 2024
Issue body actions

Suppose we define a context manager, in the usual way:

@contextmanager
def ctx():
    print("enter")
    yield
    print("exit")

def classic():
    with ctx():
        print("body")

classic()  # enter, body, exit

We can also use it as a decorator, thanks to the convenient ContextDecorator class used by @contextmanager:

@ctx()
def fn():
    print("body")

fn()  # enter, body, exit

...but if we naively do the same thing to a generator or an async function, the equivalence breaks down:

@ctx()
def gen():
    print("body")
    yield

for _ in gen(): ...  # enter, exit, body!

@ctx()
async def afn():
    print("body")

await afn()  # enter, exit, body!

This seems pretty obviously undesirable, so I think we'll want to change ContextDecorator.__call__. Possibilities include:

  1. branch on iscoroutinefunction / isgeneratorfunction / isasyncgenfunction, creating alternative inner functions to preserve the invariant that @ctx() is just like writing with ctx(): as the first line of the function body. In a quick survey, this is the expected behavior, but also a change from the current effect of the decorator.

  2. instead of branching, warn (and after a few years raise) when decorating a generator or async function.

    • alternative implementation: inspect the return value inside inner, to detect sync wrappers of async functions. I think the increased accuracy is unlikely to be worth the performance cost.
    • we could retain the convenience of using a decorator by defining ContextDecorator.gen(), .async_(), and .agen() methods which explicitly support wrapping their corresponding kind of function.

We'll also want applying an AsyncContextDecorator to an async generator function to match whatever we decide for ContextDecorator on a sync generator.

Linked PRs

  • gh-125862: Context decorator support for generators and async functions #136212
Reacted by dgpb, blhsing and craig citro
Reacted by Gregory P. Smith

Activity

  1. encukou
    added
    type-featureA feature request or enhancementA feature request or enhancement
    stdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directory
    on Jan 24, 2025
  2. agronholm commented on May 13, 2025

    @agronholm
    agronholm
    on May 13, 2025
    Contributor
    More actions

    This seems to work for me:

    class ContextDecorator(object):
        "A base class or mixin that enables context managers to work as decorators."
    
        def _recreate_cm(self):
            """Return a recreated instance of self.
    
            Allows an otherwise one-shot context manager like
            _GeneratorContextManager to support use as
            a decorator via implicit recreation.
    
            This is a private interface just for _GeneratorContextManager.
            See issue #11647 for details.
            """
            return self
    
        def __call__(self, func):
            def inner(*args, **kwds):
                with self._recreate_cm():
                    return func(*args, **kwds)
    
            def gen_inner(*args, **kwds):
                with self._recreate_cm():
                    yield from func(*args, **kwds)
    
            async def async_inner(*args, **kwds):
                with self._recreate_cm():
                    return await func(*args, **kwds)
    
            async def asyncgen_inner(*args, **kwds):
                with self._recreate_cm():
                    async for value in func(*args, **kwds):
                        yield value
    
            wrapper = wraps(func)
            if isasyncgenfunction(func):
                return wrapper(asyncgen_inner)
            elif iscoroutinefunction(func):
                return wrapper(async_inner)
            elif isgeneratorfunction(func):
                return wrapper(gen_inner)
            else:
                return wrapper(inner)

    Thoughts?

    Reacted by Zac Hatfield-Dodds and Denis Otkidach
  3. Zac-HD commented on May 22, 2025

    @Zac-HD
    Zac-HD
    on May 22, 2025
    ContributorAuthor
    More actions

    #114033 made a similar observation; my impression is that we can open a PR with @agronholm's proposal above and just fix this surprising behavior in Python 3.15 👍

  4. agronholm commented on May 24, 2025

    @agronholm
    agronholm
    on May 24, 2025
    Contributor
    More actions

    The above changes may not work properly when context managers are being stacked and it is wrapping an already wrapped function, in which case the function type detection won't work as we'd like.

  5. Zac-HD
    closed this as completedon May 24, 2025
  6. Zac-HD commented on May 24, 2025

    @Zac-HD
    Zac-HD
    on May 24, 2025
    ContributorAuthor
    More actions

    Gah, misclick and I don't have permission to reopen. Can a triager help out?

    Reacted by Zac Hatfield-Dodds and Gregory P. Smith
  7. picnixz
    reopened this on May 24, 2025
  8. Zac-HD commented on May 24, 2025

    @Zac-HD
    Zac-HD
    on May 24, 2025
    ContributorAuthor
    More actions

    The above changes may not work properly when context managers are being stacked and it is wrapping an already wrapped function, in which case the function type detection won't work as we'd like.

    Sounds like the first decorator should apply @inspect.markcoroutinefunction, no? I think stacking ContextDecorator will work correctly, since we're defining sync or async functions appropriately.

    We could warn in most mistaken cases by replacing return func(*args, **kwds) with value = ...; if isawaitable(value): <issue warning> ; return value, but I could go either way on whether the performance overhead is worth it.

  9. agronholm commented on May 30, 2025

    @agronholm
    agronholm
    on May 30, 2025
    Contributor
    More actions

    Sounds like the first decorator should apply @inspect.markcoroutinefunction, no? I think stacking ContextDecorator will work correctly, since we're defining sync or async functions appropriately.

    But if our ContextDecorator is wrapping a decorator that wraps an async function, how can we possibly select the correct wrapper function?

  10. Zac-HD commented on May 31, 2025

    @Zac-HD
    Zac-HD
    on May 31, 2025
    ContributorAuthor
    More actions

    istm that it's the responsibility of each decorator to return an appropriate function type, so if this doesn't work then I'd open the equivalent of this very issue against the decorator in question.

  11. agronholm commented on Jul 2, 2025

    @agronholm
    agronholm
    on Jul 2, 2025
    Contributor
    More actions

    I've made the appropriate modifications to ContextDecorator and AsyncContextDecorator both, and their respective test suites still pass. Now I'm trying to come up with pathological cases to test against.

  12. gpshead
    self-assigned this
    on Apr 28, 2026
  13. gpshead
    added
    3.15bugs and security fixesbugs and security fixes
    on Apr 28, 2026
  14. gpshead
    added a commit that references this issue on Apr 28, 2026

    gh-125862: Keep ContextDecorator open across generator/coroutine exec…

    2925139
  15. Zac-HD commented on Apr 28, 2026

    @Zac-HD
    Zac-HD
    on Apr 28, 2026
    ContributorAuthor
    More actions

    fixed by #136212 - thanks, agronholm and gps!

    Reacted by Gregory P. Smith
  16. Zac-HD
    closed this as completedon Apr 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

  • @gpshead
    gpshead

Labels

3.15bugs and security fixesbugs and security fixesstdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytype-featureA feature request or enhancementA feature request or enhancement

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    You are visitor 01402503 — GeezerVison does not store the pages you look up.
    Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.