[Python-checkins] r70237 - peps/trunk/pep-0377.txt

nick.coghlan python-checkins at python.org
Sun Mar 8 04:52:42 CET 2009


Author: nick.coghlan
Date: Sun Mar  8 04:52:41 2009
New Revision: 70237

Log:
New PEP to cover problems with being to implement contextlib.nested() properly

Added:
   peps/trunk/pep-0377.txt   (contents, props changed)

Added: peps/trunk/pep-0377.txt
==============================================================================
--- (empty file)
+++ peps/trunk/pep-0377.txt	Sun Mar  8 04:52:41 2009
@@ -0,0 +1,199 @@
+PEP: 377
+Title: Allow __enter__() methods to skip the statement body
+Version: $Revision$
+Last-Modified: $Date$
+Author: Nick Coghlan <ncoghlan at gmail.com>
+Status: Draft
+Type: Standards Track
+Content-Type: text/x-rst
+Created: 8-Mar-2009
+Python-Version: 2.7, 3.1
+Post-History: 8-Mar-2009
+
+
+Abstract
+========
+
+This PEP proposes a backwards compatible mechanism that allows ``__enter__()``
+methods to skip the body of the associated ``with`` statment. The lack of
+this ability currently means the ``contextlib.nested`` context manager
+is unable to fulfil its specification of being equivalent to writing out
+multiple nested ``with`` statements [1].
+
+The proposed change is to introduce a new flow control exception
+``SkipStatement``, and skip the execution of the ``with``
+statement body if ``__enter__()`` raises this exception.
+
+
+Proposed Change
+===============
+
+The semantics of the ``with`` statement will be changed to include a
+new ``try``/``except``/``else`` block around the call to ``__enter__()``.
+If ``SkipStatement`` is raised by the ``__enter__()`` method, then
+the main section of the ``with`` statement (now located in the ``else``
+clause) will not be executed. To avoid leaving the names in any ``as``
+clause unbound in this case, a new ``StatementSkipped`` singleton
+(similar to the existing ``NotImplemented`` singleton) will be
+assigned to all names that appear in the ``as`` clause.
+
+The components of the ``with`` statement remain as described in PEP 343 [2]::
+
+    with EXPR as VAR:
+        BLOCK
+
+After the modification, the ``with`` statement semantics would
+be as follows::
+
+    mgr = (EXPR)
+    exit = mgr.__exit__  # Not calling it yet
+    try:
+        value = mgr.__enter__()
+    except SkipStatement:
+        VAR = StatementSkipped
+        # Only if "as VAR" is present and
+        # VAR is a single name
+        # If VAR is a tuple of names, then StatementSkipped
+        # will be assigned to each name in the tuple
+    else:
+        exc = True
+        try:
+            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)
+
+With the above change in place for the ``with`` statement semantics,
+``contextlib.contextmanager()`` will then be modified to raise
+``SkipStatement`` instead of ``RuntimeError`` when the underlying
+generator doesn't yield.
+
+Rationale for Change
+====================
+
+Currently, some apparently innocuous context managers may raise
+``RuntimeError`` when executed. This occurs when the context
+manager's ``__enter__()`` method encounters a situation where
+the written out version of the code corresponding to the
+context manager would skip the code that is now the body
+of the ``with`` statement. Since the ``__enter__()`` method
+has no mechanism available to signal this to the interpreter,
+it is instead forced to raise an exception that not only
+skips the body of the ``with`` statement, but also jumps over
+all code until the nearest exception handler. This goes against
+one of the design goals of the ``with`` statement, which was to
+be able to factor out arbitrary common exception handling code
+into a single context manager by putting into a generator
+function and replacing the variant part of the code with a
+``yield`` statement.
+
+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 a RuntimeError complaining that the context
+  # manager's underlying generator didn't yield
+
+  with contextlib.nested(cmA(), cmB()):
+    do_stuff()
+  # This will raise the same RuntimeError as the contextmanager()
+  # example (unsurprising, given that the nested() implementation
+  # uses contextmanager())
+
+  # The following class based version shows that the issue isn't
+  # specific to contextlib.contextmanager() (it also shows how
+  # much simpler it is to write context managers as generators
+  # instead of as classes!)
+  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
+
+
+Reference Implementation
+========================
+
+In work.
+
+
+Acknowledgements
+================
+
+James William Pye both raised the issue and suggested the solution
+described in this PEP.
+
+References
+==========
+
+.. [1] Issue 5251: contextlib.nested inconsistent with nested with statements
+   (http://bugs.python.org/issue5251)
+
+.. [2] PEP 343: The "with" Statement
+   (http://www.python.org/dev/peps/pep-0343/)
+
+Copyright
+=========
+
+This document has been placed in the public domain.
+
+..
+   Local Variables:
+   mode: indented-text
+   indent-tabs-mode: nil
+   sentence-end-double-space: t
+   fill-column: 70
+   End:


More information about the Python-checkins mailing list