contextmanager-generator-missing-cleanup / W0135ΒΆ

Message emitted:

The context used in function %r will not be exited.

Description:

Used when a contextmanager is used inside a generator function and the cleanup is not handled.

Problematic code:

import contextlib


@contextlib.contextmanager
def cm():
    contextvar = "acquired context"
    print("cm enter")
    yield contextvar
    print("cm exit")


def genfunc_with_cm():
    with cm() as context:  # [contextmanager-generator-missing-cleanup]
        yield context * 2

Correct code:

import contextlib


@contextlib.contextmanager
def good_cm_except():
    contextvar = "acquired context"
    print("good cm enter")
    try:
        yield contextvar
    except GeneratorExit:
        print("good cm exit")


def genfunc_with_cm():
    with good_cm_except() as context:
        yield context * 2


def genfunc_with_discard():
    with good_cm_except():
        yield "discarded"


@contextlib.contextmanager
def good_cm_yield_none():
    print("good cm enter")
    yield
    print("good cm exit")


def genfunc_with_none_yield():
    with good_cm_yield_none() as var:
        print(var)
        yield "constant yield"


@contextlib.contextmanager
def good_cm_finally():
    contextvar = "acquired context"
    print("good cm enter")
    try:
        yield contextvar
    finally:
        print("good cm exit")


def good_cm_finally_genfunc():
    with good_cm_finally() as context:
        yield context * 2


@contextlib.contextmanager
def good_cm_no_cleanup():
    contextvar = "acquired context"
    print("cm enter")
    yield contextvar


def good_cm_no_cleanup_genfunc():
    with good_cm_no_cleanup() as context:
        yield context * 2

Additional details:

Instantiating and using a contextmanager inside a generator function can result in unexpected behavior if there is an expectation that the context is only available for the generator function. In the case that the generator is not closed or destroyed then the context manager is held suspended as is.

This message warns on the generator function instead of the contextmanager function because the ways to use a contextmanager are many. A contextmanager can be used as a decorator (which immediately has __enter__/__exit__ applied) and the use of as ... or discard of the return value also implies whether the context needs cleanup or not. So for this message, warning the invoker of the contextmanager is important.

The check can create false positives if yield is used inside an if-else block without custom cleanup. Use pylint: disable for these.

from contextlib import contextmanager

@contextmanager
def good_cm_no_cleanup():
    contextvar = "acquired context"
    print("cm enter")
    if some_condition:
        yield contextvar
    else:
        yield contextvar


def good_cm_no_cleanup_genfunc():
    # pylint: disable-next=contextmanager-generator-missing-cleanup
    with good_cm_no_cleanup() as context:
        yield context * 2

Related links:

Created by the basic checker.