This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: How should contextmanager/ContextDecorator work with generators?
Type: Stage:
Components: Documentation Versions: Python 3.8
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: ncoghlan
Priority: normal Keywords:

Created on 2019-08-01 23:33 by Antony.Lee, last changed 2022-04-11 14:59 by admin.

Messages (2)
msg348878 - (view) Author: Antony Lee (Antony.Lee) * Date: 2019-08-01 23:33
The docs for ContextDecorator (of which contextmanager is a case) describe its semantics as:

    ... for any construct of the following form:

    def f():
        with cm():
            # Do stuff

    ContextDecorator lets you instead write:

    @cm()
    def f():
        # Do stuff

However, when decorating a generator, the equivalence is broken:

    from contextlib import contextmanager

    @contextmanager
    def cm():
        print("start")
        yield
        print("stop")

    def gen_using_with():
        with cm():
            yield from map(print, range(2))

    @cm()
    def gen_using_decorator():
        yield from map(print, range(2))

    print("using with")
    list(gen_using_with())
    print("==========")
    print("using decorator")
    list(gen_using_decorator())

results in

    using with
    start
    0
    1
    stop
    ==========
    using decorator
    start
    stop
    0
    1

i.e., when used as a decorator, the entire contextmanager is executed first before iterating over the generator (which is unsurprising given the implementation of ContextDecorator: ContextDecorator returns a function that executes the context manager and returns the generator, which is only iterated over later).

Should this be considered as a bug in ContextDecorator, and should ContextDecorator instead detect when it is used to decorate a generator (e.g. with inspect.isgeneratorfunction), and switch its implementation accordingly in that case?
msg351074 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2019-09-03 08:25
This is a documentation bug, as the current behaviour is as intended, but the documented equivalence doesn't hold for generator functions: for a generator function, the CM will only be applied when the generator is instantiated, whereas the inline context manager version will be held open until the generator is closed or destroyed.

That said, an approach similar to the one discussed in #37398 could also be applied, here, with a separate "ContextDecorator.generator()" class method added to give the "wrapped yield from" behaviour. If anyone is interested in pursuing that, it can be filed as a separate enhancement issue (leaving this bug to cover the fact that the existing documentation is only accurate for regular synchronous functions)
History
Date User Action Args
2022-04-11 14:59:18adminsetgithub: 81924
2019-09-03 10:21:10Antony.Leesetnosy: - Antony.Lee
2019-09-03 08:25:36ncoghlansetassignee: ncoghlan ->
messages: + msg351074
components: + Documentation, - Library (Lib)
2019-08-02 06:15:52rhettingersetassignee: ncoghlan
2019-08-02 01:41:37xtreaksetnosy: + ncoghlan
2019-08-01 23:33:04Antony.Leecreate