[Python-Dev] PEP 377 - allow __enter__() methods to skip the statement body

Guido van Rossum guido at python.org
Mon Mar 16 22:57:40 CET 2009


On Mon, Mar 16, 2009 at 2:37 PM, Nick Coghlan <ncoghlan at gmail.com> wrote:
> Guido van Rossum wrote:
>> Yeah, it really seems pretty much limited to contextlib.nested(). I'd
>> be happy to sacrifice the possibility to *exactly* emulate two nested
>> with-statements.
>
> Then I really haven't explained the problem well at all. One of the
> premises of PEP 343 was "Got a frequently recurring block of code that
> only has one variant sequence of statements somewhere in the middle?
> Well, now you can factor that out by putting it in a generator,
> replacing the part that varies with a yield statement and decorating the
> generator with contextlib.contextmanager."
>
> It turns out that there's a caveat that needs to go on the end of that
> though: "Be very, very sure that the yield statement can *never* be
> skipped or your context manager based version will raise a RuntimeError
> in cases where the original code would have just skipped over the
> variant section of code and resumed execution afterwards."

Well, I don't think you can take that premise (promise? :-) literally
anyways, since you cannot turn a loop into a with-statement either. So
I would be fine with just adding the condition that the variant
sequence should be executed exactly once.

> Nested context managers (whether through contextlib.nested or through
> syntactic support) just turns out to be a common case where you *don't
> necessarily know* just by looking at the code whether it can skip over
> the body of the code or not.
>
> Suppose you have 3 context managers that are regularly used together
> (call them cmA(), cmB(), cmC() for now).
>
> Writing that as:
>
>  with cmA():
>    with cmB():
>      with cmC():
>        do_something()
>
> Or the tentatively proposed:
>
>  with cmA(), cmB(), cmC():
>        do_something()
>
> is definitely OK, regardless of the details of the context managers.
>
> However, whether or not you can bundle that up into a *new* context
> manager (regardless of nesting syntax) depends on whether or not an
> outer context manager can suppress an exception raised by an inner one.
>
>  @contextmanager
>  def cmABC():
>    with cmA():
>      with cmB():
>        with cmC():
>          yield
>
>  with cmABC():
>    do_something()

While all this may make sense to the original inventor of context
managers (== you ;-), I personally find this example quite perverse.
Do you have an example taken from real life?

> The above is broken if cmB().__enter__() or cmC.__enter__() can raise an
> exception that cmA().__exit__() suppresses, or cmB.__enter__() raises an
> exception that cmB().__exit__() suppresses. So whereas the inline
> versions were clearly correct, the correctness of the second version
> currently depends on details of the context managers themselves.
> Changing the syntax to allow the three context managers to be written on
> one line does nothing to fix that semantic discrepancy between the
> original inline code and the factored out version.

I think I understand that, I just don't see a use case so important as
to warrant introducing a brand new exception deriving from
BaseException.

> PEP 377 is about changing the with statement semantics and the
> @contextmanager implementation so that the semantics of the factored out
> version actually matches that of the original inline code.
>
> You can get yourself into similar trouble without nesting context
> managers - all it takes is some way of skipping the variant code in a
> context manager that wouldn't have raised an exception if the code was
> written out inline instead of being factored out into the context manager.

Yeah, see above -- you can't write a context manager that implements a
loop either.

> Suppose for instance you wanted to use a context manager as a different
> way of running tests:
>
>  @contextmanager
>  def inline_test(self, *setup_args):
>    try:
>      self.setup(*setup_args)
>    except:
>      # Setup exception occurred, trap it and log it
>      return
>    try:
>      yield
>    except:
>      # Test exception occurred, trap it and log it
>    finally:
>      try:
>        self.teardown()
>      except:
>        # Teardown exception occurred, trap it and log it
>
>  with inline_test(setup1):
>    test_one()
>  with inline_test(setup2):
>    test_two()
>  with inline_test(setup3):
>    test_three()
>
> That approach isn't actually valid

(but your proposal would make it valid right?)

> - a context manager is not permitted
> to decide in it's __enter__() method that executing the body of the with
> statement would be a bad idea.

A setup failure sounds like a catastrophic error to me.

> The early return in the above makes it obvious that that CM is broken
> under the current semantics, but what about the following one:
>
>  @contextmanager
>  def broken_cm(self):
>    try:
>      call_user_setup()
>      try:
>        yield
>      finally:
>        call_user_teardown()
>    except UserCancel:
>      show_message("Operation aborted by user")
>
> That CM will raise RuntimeError if the user attempts to cancel an
> operation during the execution of the "call_user_setup()" method.
> Without SkipStatement or something like it, that can't be fixed.

Well pretty much anything that tries to catch asynchronous exceptions
is doomed to be 100% correct. Again the example is too abstract to be
convincing.

> Hell, I largely wrote PEP 377 to try to get out of having to document
> these semantic problems with the with statement - if I'm having trouble
> getting *python-dev* to grasp the problem, what hope do other users of
> Python have?

Hell, if you can't come up with a real use case, why bother? :-)

Perhaps you could address my worry about introducing an obscure
BaseException subclass that will forever add to the weight of the list
of built-in exceptions in all documentation?

-- 
--Guido van Rossum (home page: http://www.python.org/~guido/)


More information about the Python-Dev mailing list