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: contextmanager + ExitStack.pop_all()
Type: Stage:
Components: Documentation, Library (Lib) Versions: Python 3.9, Python 3.8, Python 3.7, Python 3.6
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: docs@python Nosy List: docs@python, lucae.mattiello
Priority: normal Keywords:

Created on 2021-06-02 21:59 by lucae.mattiello, last changed 2022-04-11 14:59 by admin.

Messages (1)
msg394948 - (view) Author: Luca Mattiello (lucae.mattiello) Date: 2021-06-02 21:59
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
History
Date User Action Args
2022-04-11 14:59:46adminsetgithub: 88458
2021-06-02 21:59:52lucae.mattiellocreate