(original) (raw)



On Wed, Feb 25, 2009 at 04:24, Nick Coghlan <ncoghlan@gmail.com> wrote:

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?

No, but I do wonder how useful this truly is.

Are there any backwards compatibility

problems that I'm missing?

As long as the exception inherits from BaseException, no.

Should I write a PEP or just add the feature

to the with statement in 2.7/3.1?

Sounds PEPpy to me since you are proposing changing the semantics for a syntactic construct.