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

Nick Coghlan ncoghlan at gmail.com
Wed Feb 25 13:24:33 CET 2009


An interesting discrepancy [1] has been noted when comparing
contextlib.nested (and contextlib.contextmanager) with the equivalent
nested with statements.

Specifically, the following examples behave differently if
cmB().__enter__() raises an exception which cmA().__exit__() then
handles (and suppresses):

  with cmA():
    with cmB():
      do_stuff()
  # This will resume here without executing "Do stuff"

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

  with combined():
    do_stuff()
  # This will raise RuntimeError complaining that the underlying
  # generator didn't yield

  with contextlib.nested(cmA(), cmB()):
    do_stuff()
  # This will raise the same RuntimeError as the contextmanager
  # example (unsurprising, given the way nested() is implemented)

The problem arises any time it is possible to skip over the yield
statement in a contextlib.contextmanager based context manager without
raising an exception that can be seen by the code calling __enter__().

I think the right way to fix this (as suggested by the original poster
of the bug report) is to introduce a new flow control exception along
the lines of GeneratorExit (e.g. SkipContext) and tweak the expansion of
the with statement [2] to skip the body of the statement if __enter__()
throws that specific exception:

  mgr = (EXPR)
  exit = mgr.__exit__  # Not calling it yet
  try:
    value = mgr.__enter__()
  except SkipContext:
    pass # This exception handler is the new part...
  else:
    exc = True
    try:
      VAR = value  # Only if "as VAR" is present
      BLOCK
    except:
      # The exceptional case is handled here
      exc = False
      if not exit(*sys.exc_info()):
        raise
      # The exception is swallowed if exit() returns true
    finally:
      # The normal and non-local-goto cases are handled here
      if exc:
        exit(None, None, None)

Naturally, contextlib.contextmanager would then be modified to raise
SkipContext instead of RuntimeError if the generator doesn't yield. The
latter two examples would then correctly resume execution at the first
statement after the with block.

I don't see any other way to comprehensively fix the problem - without
it, there will always be some snippets of code which cannot correctly be
converted into context managers, and those snippets won't always be
obvious (e.g. the fact that combined() is potentially a broken context
manager implementation would surprise most people - it certainly
surprised me).

Thoughts? Do people hate the idea? Are there any backwards compatibility
problems that I'm missing? Should I write a PEP or just add the feature
to the with statement in 2.7/3.1?

Cheers,
Nick.

[1] http://bugs.python.org/issue5251

[2] http://www.python.org/dev/peps/pep-0343/
-- 
Nick Coghlan   |   ncoghlan at gmail.com   |   Brisbane, Australia
---------------------------------------------------------------


More information about the Python-Dev mailing list