]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
PEP 479: Change StopIteration handling inside generators.
authorYury Selivanov <yselivanov@sprymix.com>
Sat, 9 May 2015 15:44:30 +0000 (11:44 -0400)
committerYury Selivanov <yselivanov@sprymix.com>
Sat, 9 May 2015 15:44:30 +0000 (11:44 -0400)
Closes issue #22906.

14 files changed:
Doc/howto/functional.rst
Doc/library/__future__.rst
Doc/library/exceptions.rst
Doc/reference/expressions.rst
Include/code.h
Include/compile.h
Include/pythonrun.h
Lib/__future__.py
Lib/contextlib.py
Lib/difflib.py
Lib/test/test_contextlib.py
Misc/NEWS
Objects/genobject.c
Python/future.c

index 1969b3210857d297814a2cc002e8926b7e334f90..945a240666cf42b426b7c0a28e19772fdf2e8b46 100644 (file)
@@ -481,10 +481,10 @@ Here's a sample usage of the ``generate_ints()`` generator:
 You could equally write ``for i in generate_ints(5)``, or ``a,b,c =
 generate_ints(3)``.
 
-Inside a generator function, ``return value`` is semantically equivalent to
-``raise StopIteration(value)``.  If no value is returned or the bottom of the
-function is reached, the procession of values ends and the generator cannot
-return any further values.
+Inside a generator function, ``return value`` causes ``StopIteration(value)``
+to be raised from the :meth:`~generator.__next__` method.  Once this happens, or
+the bottom of the function is reached, the procession of values ends and the
+generator cannot yield any further values.
 
 You could achieve the effect of generators manually by writing your own class
 and storing all the local variables of the generator as instance variables.  For
index 72f2963a2cd0d9591606c6941dd59d1d71167ca6..73d8b6b7e8aff025e6cc552fa7dcdc5ea39a744f 100644 (file)
@@ -87,6 +87,9 @@ language using this mechanism:
 | unicode_literals | 2.6.0a2     | 3.0          | :pep:`3112`:                                |
 |                  |             |              | *Bytes literals in Python 3000*             |
 +------------------+-------------+--------------+---------------------------------------------+
+| generator_stop   | 3.5.0b1     | 3.7          | :pep:`479`:                                 |
+|                  |             |              | *StopIteration handling inside generators*  |
++------------------+-------------+--------------+---------------------------------------------+
 
 
 .. seealso::
index bddd0edc6c825a57950ec8792a699038fd639bec..e7768bee1becf0418176c4e6465109cbf6c20330 100644 (file)
@@ -310,10 +310,18 @@ The following exceptions are the exceptions that are usually raised.
    raised, and the value returned by the function is used as the
    :attr:`value` parameter to the constructor of the exception.
 
+   If a generator function defined in the presence of a ``from __future__
+   import generator_stop`` directive raises :exc:`StopIteration`, it will be
+   converted into a :exc:`RuntimeError` (retaining the :exc:`StopIteration`
+   as the new exception's cause).
+
    .. versionchanged:: 3.3
       Added ``value`` attribute and the ability for generator functions to
       use it to return a value.
 
+   .. versionchanged:: 3.5
+      Introduced the RuntimeError transformation.
+
 .. exception:: SyntaxError
 
    Raised when the parser encounters a syntax error.  This may occur in an
index efe4d0b521992f815efe34a01fe2dd6cbc8d7fd2..b252e2643368faffaf2c0a81e22a840e1488f2de 100644 (file)
@@ -443,12 +443,12 @@ is already executing raises a :exc:`ValueError` exception.
 .. method:: generator.close()
 
    Raises a :exc:`GeneratorExit` at the point where the generator function was
-   paused.  If the generator function then raises :exc:`StopIteration` (by
-   exiting normally, or due to already being closed) or :exc:`GeneratorExit` (by
-   not catching the exception), close returns to its caller.  If the generator
-   yields a value, a :exc:`RuntimeError` is raised.  If the generator raises any
-   other exception, it is propagated to the caller.  :meth:`close` does nothing
-   if the generator has already exited due to an exception or normal exit.
+   paused.  If the generator function then exits gracefully, is already closed,
+   or raises :exc:`GeneratorExit` (by not catching the exception), close
+   returns to its caller.  If the generator yields a value, a
+   :exc:`RuntimeError` is raised.  If the generator raises any other exception,
+   it is propagated to the caller.  :meth:`close` does nothing if the generator
+   has already exited due to an exception or normal exit.
 
 .. index:: single: yield; examples
 
index ff2b97efe9270444bc0bfb9ddd2b80109185d344..131de501dccfcf9579972cca9afcb4ddae06b555 100644 (file)
@@ -62,6 +62,7 @@ typedef struct {
 #define CO_FUTURE_UNICODE_LITERALS 0x20000
 
 #define CO_FUTURE_BARRY_AS_BDFL  0x40000
+#define CO_FUTURE_GENERATOR_STOP  0x80000
 
 /* This value is found in the co_cell2arg array when the associated cell
    variable does not correspond to an argument. The maximum number of
index c6650d7f776821ba157ad453ef380f05967816c7..ecd8dc1d104263f8833a5ddbaf21ec367a80a920 100644 (file)
@@ -27,6 +27,7 @@ typedef struct {
 #define FUTURE_PRINT_FUNCTION "print_function"
 #define FUTURE_UNICODE_LITERALS "unicode_literals"
 #define FUTURE_BARRY_AS_BDFL "barry_as_FLUFL"
+#define FUTURE_GENERATOR_STOP "generator_stop"
 
 struct _mod; /* Declare the existence of this type */
 #define PyAST_Compile(mod, s, f, ar) PyAST_CompileEx(mod, s, f, -1, ar)
index 1d9b71beaa97d5fc99f1b44b498c88e159c8798c..f92148da0707c209295eacf87112049960c91ec7 100644 (file)
@@ -9,7 +9,8 @@ extern "C" {
 
 #define PyCF_MASK (CO_FUTURE_DIVISION | CO_FUTURE_ABSOLUTE_IMPORT | \
                    CO_FUTURE_WITH_STATEMENT | CO_FUTURE_PRINT_FUNCTION | \
-                   CO_FUTURE_UNICODE_LITERALS | CO_FUTURE_BARRY_AS_BDFL)
+                   CO_FUTURE_UNICODE_LITERALS | CO_FUTURE_BARRY_AS_BDFL | \
+                   CO_FUTURE_GENERATOR_STOP)
 #define PyCF_MASK_OBSOLETE (CO_NESTED)
 #define PyCF_SOURCE_IS_UTF8  0x0100
 #define PyCF_DONT_IMPLY_DEDENT 0x0200
index 3b2d5ecb92abb11cd1cbe63ceb1081637ae746ec..63b2be3524fc7c4aeaed33445591dbcfaa19014a 100644 (file)
@@ -56,6 +56,7 @@ all_feature_names = [
     "print_function",
     "unicode_literals",
     "barry_as_FLUFL",
+    "generator_stop",
 ]
 
 __all__ = ["all_feature_names"] + all_feature_names
@@ -72,6 +73,7 @@ CO_FUTURE_WITH_STATEMENT  = 0x8000   # with statement
 CO_FUTURE_PRINT_FUNCTION  = 0x10000   # print function
 CO_FUTURE_UNICODE_LITERALS = 0x20000 # unicode string literals
 CO_FUTURE_BARRY_AS_BDFL = 0x40000
+CO_FUTURE_GENERATOR_STOP  = 0x80000 # StopIteration becomes RuntimeError in generators
 
 class _Feature:
     def __init__(self, optionalRelease, mandatoryRelease, compiler_flag):
@@ -132,3 +134,7 @@ unicode_literals = _Feature((2, 6, 0, "alpha", 2),
 barry_as_FLUFL = _Feature((3, 1, 0, "alpha", 2),
                          (3, 9, 0, "alpha", 0),
                          CO_FUTURE_BARRY_AS_BDFL)
+
+generator_stop = _Feature((3, 5, 0, "beta", 1),
+                         (3, 7, 0, "alpha", 0),
+                         CO_FUTURE_GENERATOR_STOP)
index 2fbc90cdc8cad0cd7bea4770d9d4dc0e95399421..379c2515d50b60ec31e3ea6f6beb95c2aa6e672a 100644 (file)
@@ -77,10 +77,17 @@ class _GeneratorContextManager(ContextDecorator):
                 self.gen.throw(type, value, traceback)
                 raise RuntimeError("generator didn't stop after throw()")
             except StopIteration as exc:
-                # Suppress the exception *unless* it's the same exception that
+                # Suppress StopIteration *unless* it's the same exception that
                 # was passed to throw().  This prevents a StopIteration
-                # raised inside the "with" statement from being suppressed
+                # raised inside the "with" statement from being suppressed.
                 return exc is not value
+            except RuntimeError as exc:
+                # Likewise, avoid suppressing if a StopIteration exception
+                # was passed to throw() and later wrapped into a RuntimeError
+                # (see PEP 479).
+                if exc.__cause__ is value:
+                    return False
+                raise
             except:
                 # only re-raise if it's *not* the exception that was
                 # passed to throw(), because __exit__() must not raise
index 96fd9ab37cfff4c203988cdb3a131e0f3b098db4..aa98436ce38073a5eb6e0dc152e72b382d2d2d27 100644 (file)
@@ -1596,8 +1596,7 @@ def _mdiff(fromlines, tolines, context=None, linejunk=None,
     # them up without doing anything else with them.
     line_pair_iterator = _line_pair_iterator()
     if context is None:
-        while True:
-            yield next(line_pair_iterator)
+        yield from line_pair_iterator
     # Handle case where user wants context differencing.  We must do some
     # storage of lines until we know for sure that they are to be yielded.
     else:
index c52066be3cc8098d9be3c3c613baf4d682334e3c..a5d68a99f86fefe61727ac647b1bd10aa8a19393 100644 (file)
@@ -83,6 +83,40 @@ class ContextManagerTestCase(unittest.TestCase):
             raise ZeroDivisionError(999)
         self.assertEqual(state, [1, 42, 999])
 
+    def test_contextmanager_except_stopiter(self):
+        stop_exc = StopIteration('spam')
+        @contextmanager
+        def woohoo():
+            yield
+        try:
+            with woohoo():
+                raise stop_exc
+        except Exception as ex:
+            self.assertIs(ex, stop_exc)
+        else:
+            self.fail('StopIteration was suppressed')
+
+    def test_contextmanager_except_pep479(self):
+        code = """\
+from __future__ import generator_stop
+from contextlib import contextmanager
+@contextmanager
+def woohoo():
+    yield
+"""
+        locals = {}
+        exec(code, locals, locals)
+        woohoo = locals['woohoo']
+
+        stop_exc = StopIteration('spam')
+        try:
+            with woohoo():
+                raise stop_exc
+        except Exception as ex:
+            self.assertIs(ex, stop_exc)
+        else:
+            self.fail('StopIteration was suppressed')
+
     def _create_contextmanager_attribs(self):
         def attribs(**kw):
             def decorate(func):
index ac065f73085d248e9e4bfc1f291c5ce4f2e2ed3f..8c40764cc782ac4f4acd7389d62a9f87e16cfcee 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -33,6 +33,8 @@ Core and Builtins
 
 - Issue #9951: Added a hex() method to bytes, bytearray, and memoryview.
 
+- Issue #22906: PEP 479: Change StopIteration handling inside generators.
+
 Library
 -------
 
index 87060f36dda61b30d34dcc32920743b4128026fe..5ae8512f540418646f5fbb459cd59133ea4215fd 100644 (file)
@@ -130,6 +130,30 @@ gen_send_ex(PyGenObject *gen, PyObject *arg, int exc)
         }
         Py_CLEAR(result);
     }
+    else if (!result) {
+        /* Check for __future__ generator_stop and conditionally turn
+         * a leaking StopIteration into RuntimeError (with its cause
+         * set appropriately). */
+        if ((((PyCodeObject *)gen->gi_code)->co_flags &
+                                                CO_FUTURE_GENERATOR_STOP)
+            && PyErr_ExceptionMatches(PyExc_StopIteration))
+        {
+            PyObject *exc, *val, *val2, *tb;
+            PyErr_Fetch(&exc, &val, &tb);
+            PyErr_NormalizeException(&exc, &val, &tb);
+            if (tb != NULL)
+                PyException_SetTraceback(val, tb);
+            Py_DECREF(exc);
+            Py_XDECREF(tb);
+            PyErr_SetString(PyExc_RuntimeError,
+                "generator raised StopIteration");
+            PyErr_Fetch(&exc, &val2, &tb);
+            PyErr_NormalizeException(&exc, &val2, &tb);
+            PyException_SetCause(val2, val);
+            PyException_SetContext(val2, val);
+            PyErr_Restore(exc, val2, tb);
+        }
+    }
 
     if (!result || f->f_stacktop == NULL) {
         /* generator can't be rerun, so release the frame */
index 81eab54dd62fa33ce4ffafa20515e3e96c59014d..163f87f673da5d5c31868849c9b9e45a7c74d024 100644 (file)
@@ -40,6 +40,8 @@ future_check_features(PyFutureFeatures *ff, stmt_ty s, PyObject *filename)
             continue;
         } else if (strcmp(feature, FUTURE_BARRY_AS_BDFL) == 0) {
             ff->ff_features |= CO_FUTURE_BARRY_AS_BDFL;
+        } else if (strcmp(feature, FUTURE_GENERATOR_STOP) == 0) {
+            ff->ff_features |= CO_FUTURE_GENERATOR_STOP;
         } else if (strcmp(feature, "braces") == 0) {
             PyErr_SetString(PyExc_SyntaxError,
                             "not a chance");