Skip to content

bpo-32670: Enforce PEP 479. #5327

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 26, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions Doc/library/exceptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -367,17 +367,21 @@ 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).
If a generator code directly or indirectly raises :exc:`StopIteration`,
it is 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.
Introduced the RuntimeError transformation via
``from __future__ import generator_stop``, see :pep:`479`.

.. versionchanged:: 3.7
Enable :pep:`479` for all code by default: a :exc:`StopIteration`
error raised in a generator is transformed into a :exc:`RuntimeError`.

.. exception:: StopAsyncIteration

Expand Down
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.7.rst
Original file line number Diff line number Diff line change
Expand Up @@ -935,6 +935,12 @@ that may require changes to your code.
Changes in Python behavior
--------------------------

* :pep:`479` is enabled for all code in Python 3.7, meaning that
:exc:`StopIteration` exceptions raised directly or indirectly in
coroutines and generators are transformed into :exc:`RuntimeError`
exceptions.
(Contributed by Yury Selivanov in :issue:`32670`.)

* Due to an oversight, earlier Python versions erroneously accepted the
following syntax::

Expand Down
22 changes: 3 additions & 19 deletions Lib/test/test_generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,26 +265,16 @@ def gen():
self.assertEqual(next(g), "done")
self.assertEqual(sys.exc_info(), (None, None, None))

def test_stopiteration_warning(self):
def test_stopiteration_error(self):
# See also PEP 479.

def gen():
raise StopIteration
yield

with self.assertRaises(StopIteration), \
self.assertWarnsRegex(DeprecationWarning, "StopIteration"):

next(gen())

with self.assertRaisesRegex(DeprecationWarning,
"generator .* raised StopIteration"), \
warnings.catch_warnings():

warnings.simplefilter('error')
with self.assertRaisesRegex(RuntimeError, 'raised StopIteration'):
next(gen())


def test_tutorial_stopiteration(self):
# Raise StopIteration" stops the generator too:

Expand All @@ -296,13 +286,7 @@ def f():
g = f()
self.assertEqual(next(g), 1)

with self.assertWarnsRegex(DeprecationWarning, "StopIteration"):
with self.assertRaises(StopIteration):
next(g)

with self.assertRaises(StopIteration):
# This time StopIteration isn't raised from the generator's body,
# hence no warning.
with self.assertRaisesRegex(RuntimeError, 'raised StopIteration'):
next(g)

def test_return_tuple(self):
Expand Down
11 changes: 6 additions & 5 deletions Lib/test/test_with.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,8 +458,8 @@ def shouldThrow():
with cm():
raise StopIteration("from with")

with self.assertWarnsRegex(DeprecationWarning, "StopIteration"):
self.assertRaises(StopIteration, shouldThrow)
with self.assertRaisesRegex(StopIteration, 'from with'):
shouldThrow()

def testRaisedStopIteration2(self):
# From bug 1462485
Expand All @@ -473,7 +473,8 @@ def shouldThrow():
with cm():
raise StopIteration("from with")

self.assertRaises(StopIteration, shouldThrow)
with self.assertRaisesRegex(StopIteration, 'from with'):
shouldThrow()

def testRaisedStopIteration3(self):
# Another variant where the exception hasn't been instantiated
Expand All @@ -486,8 +487,8 @@ def shouldThrow():
with cm():
raise next(iter([]))

with self.assertWarnsRegex(DeprecationWarning, "StopIteration"):
self.assertRaises(StopIteration, shouldThrow)
with self.assertRaises(StopIteration):
shouldThrow()

def testRaisedGeneratorExit1(self):
# From bug 1462485
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Enforce PEP 479 for all code.

This means that manually raising a StopIteration exception from a generator
is prohibited for all code, regardless of whether 'from __future__ import
generator_stop' was used or not.
58 changes: 8 additions & 50 deletions Objects/genobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -248,59 +248,17 @@ gen_send_ex(PyGenObject *gen, PyObject *arg, int exc, int closing)
Py_CLEAR(result);
}
else if (!result && PyErr_ExceptionMatches(PyExc_StopIteration)) {
/* Check for __future__ generator_stop and conditionally turn
* a leaking StopIteration into RuntimeError (with its cause
* set appropriately). */

const int check_stop_iter_error_flags = CO_FUTURE_GENERATOR_STOP |
CO_COROUTINE |
CO_ITERABLE_COROUTINE |
CO_ASYNC_GENERATOR;

if (gen->gi_code != NULL &&
((PyCodeObject *)gen->gi_code)->co_flags &
check_stop_iter_error_flags)
{
/* `gen` is either:
* a generator with CO_FUTURE_GENERATOR_STOP flag;
* a coroutine;
* a generator with CO_ITERABLE_COROUTINE flag
(decorated with types.coroutine decorator);
* an async generator.
*/
const char *msg = "generator raised StopIteration";
if (PyCoro_CheckExact(gen)) {
msg = "coroutine raised StopIteration";
}
else if PyAsyncGen_CheckExact(gen) {
msg = "async generator raised StopIteration";
}
_PyErr_FormatFromCause(PyExc_RuntimeError, "%s", msg);
const char *msg = "generator raised StopIteration";
if (PyCoro_CheckExact(gen)) {
msg = "coroutine raised StopIteration";
}
else {
/* `gen` is an ordinary generator without
CO_FUTURE_GENERATOR_STOP flag.
*/

PyObject *exc, *val, *tb;

/* Pop the exception before issuing a warning. */
PyErr_Fetch(&exc, &val, &tb);

if (PyErr_WarnFormat(PyExc_DeprecationWarning, 1,
"generator '%.50S' raised StopIteration",
gen->gi_qualname)) {
/* Warning was converted to an error. */
Py_XDECREF(exc);
Py_XDECREF(val);
Py_XDECREF(tb);
}
else {
PyErr_Restore(exc, val, tb);
}
else if PyAsyncGen_CheckExact(gen) {
msg = "async generator raised StopIteration";
}
_PyErr_FormatFromCause(PyExc_RuntimeError, "%s", msg);

}
else if (PyAsyncGen_CheckExact(gen) && !result &&
else if (!result && PyAsyncGen_CheckExact(gen) &&
PyErr_ExceptionMatches(PyExc_StopAsyncIteration))
{
/* code in `gen` raised a StopAsyncIteration error:
Expand Down
2 changes: 1 addition & 1 deletion Python/future.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ future_check_features(PyFutureFeatures *ff, stmt_ty s, PyObject *filename)
} 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;
continue;
} else if (strcmp(feature, FUTURE_ANNOTATIONS) == 0) {
ff->ff_features |= CO_FUTURE_ANNOTATIONS;
} else if (strcmp(feature, "braces") == 0) {
Expand Down