[Python-Dev] Allow __enter__() methods to skip the with statement body?

Nick Coghlan ncoghlan at gmail.com
Wed Feb 25 22:28:58 CET 2009


Steven Bethard wrote:
> If the problem is just the yield, can't this just be fixed by
> implementing contextlib.nested() as a class rather than as a
> @contextmanager decorated generator? Or is this a problem with class
> based context managers too?

It's a problem for class-based context managers as well. Setting aside
the difficulties of actually maintaining nested()'s state on a class
rather than in a frame (it's definitely possible, but also somewhat
painful), you still end up in the situation where nested() knows that
cmB().__enter__() threw an exception that was then handled by
cmA().__exit__() and hence the body of the with statement should be
skipped but no exception should occur from the point of view of the
surrounding code. However, indicating that is not currently an option
available to nested().__enter__(): it can either raise an exception
(thus skipping the body of the with statement, but also propagating the
exception into the surrounding code), or it can return a value (which
would lead to the execution of the body of the with statement).

Returning a value would definitely be wrong, but raising the exception
isn't really right either.

contextmanager is just a special case - the "skipped yield" inside the
generator reflects the body of the with statement being skipped in the
original non-context manager code.

As to Brett's question of whether or not this is necessary/useful... the
problem I really have with the status quo is that it is currently
impossible to look at the following code snippets and say whether or not
the created CM's are valid:

  cm = contextlib.nested(cmA(), cmB())

  @contextlib.contextmanager
  def cm():
    with cmA():
      with cmB():
        yield

  # Not tested, probably have the class version wrong
  # This should illustrate why nested() wasn't written
  # as a class-based CM though - this one only nests
  # two specifically named CMs and look how tricky it gets!
  class CM(object):
    def __init__(self):
        self.cmA = None
        self.cmB = None
    def __enter__(self):
        if self.cmA is not None:
           raise RuntimeError("Can't re-use this CM")
        self.cmA = cmA()
        self.cmA.__enter__()
        try:
          self.cmB = cmB()
          self.cmB.__enter__()
        except:
          self.cmA.__exit__(*sys.exc_info())
          # Can't suppress in __enter__(), so must raise
          raise
    def __exit__(self, *args):
        suppress = False
        try:
          if self.cmB is not None:
            suppress = self.cmB.__exit__(*args)
        except:
          suppress = self.cmA.__exit__(*sys.exc_info()):
          if not suppress:
            # Exception has changed, so reraise explicitly
            raise
        else:
          if suppress:
             # cmB already suppressed the exception,
             # so don't pass it to cmA
            suppress = self.cmA.__exit__(None, None, None):
          else:
            suppress = self.cmA.__exit__(*args):
        return suppress

With the current with statement semantics, those CM's may raise
exceptions where the original multiple with statement code would work fine.

Cheers,
Nick.

-- 
Nick Coghlan   |   ncoghlan at gmail.com   |   Brisbane, Australia
---------------------------------------------------------------


More information about the Python-Dev mailing list