[New-bugs-announce] [issue44292] contextmanager + ExitStack.pop_all()

Luca Mattiello report at bugs.python.org
Wed Jun 2 17:59:52 EDT 2021


New submission from Luca Mattiello <lucae.mattiello at gmail.com>:

Reading the contextlib documentation, one might assume the following to be functionally equivalent, when used in a with statement:

@contextlib.contextmanager
def managed_resource():
  resource = acquire()
  try:
    yield resource
  finally:
    resource.release()

class ManagedResource:
  def __init__(self):
    self.resource = acquire()
  def __enter__(self):
    return self.resource
  def __exit__(self, *args):
    self.resource.release()

However, the first version has a seemingly unexpected behavior when used in conjunction with an ExitStack, and pop_all().

with contextlib.ExitStack() as es:
  r = es.enter_context(managed_resource())
  es.pop_all()
  # Uh-oh, r gets released anyway

with contextlib.ExitStack() as es:
  r = es.enter_context(ManagedResource())
  es.pop_all()
  # Works as expected

I think the reason is https://docs.python.org/3/reference/expressions.html#yield-expressions, in particular

> Yield expressions are allowed anywhere in a try construct.
> If the generator is not resumed before it is finalized (by
> reaching a zero reference count or by being garbage collected),
> the generator-iterator’s close() method will be called,
> allowing any pending finally clauses to execute.

I guess this is working according to the specs, but I found it very counter-intuitive. Could we improve the documentation to point out this subtle difference?

Full repro:

import contextlib

@contextlib.contextmanager
def cm():
  print("acquire cm")
  try:
    yield 1
  finally:
    print("release cm")

class CM:
  def __init__(self):
    print("acquire CM")
  def __enter__(self):
    return 1
  def __exit__(self, *args):
    print("release CM")

def f1():
  with contextlib.ExitStack() as es:
    es.enter_context(cm())
    es.pop_all()

def f2():
  with contextlib.ExitStack() as es:
    es.enter_context(CM())
    es.pop_all()

f1()
f2()

Output:

acquire cm
release cm
acquire CM

----------
assignee: docs at python
components: Documentation, Library (Lib)
messages: 394948
nosy: docs at python, lucae.mattiello
priority: normal
severity: normal
status: open
title: contextmanager + ExitStack.pop_all()
versions: Python 3.6, Python 3.7, Python 3.8, Python 3.9

_______________________________________
Python tracker <report at bugs.python.org>
<https://bugs.python.org/issue44292>
_______________________________________


More information about the New-bugs-announce mailing list