Skip to content
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

gh-104770: Let generator.close() return value #104771

Merged
merged 9 commits into from
May 23, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
19 changes: 13 additions & 6 deletions Doc/reference/expressions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -595,12 +595,19 @@ 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 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.
paused. If the generator function then returns a value (by catching
:exc:`GeneratorExit`), it is returned by :meth:`close`. If the generator
ntessore marked this conversation as resolved.
Show resolved Hide resolved
function is already closed, or raises :exc:`GeneratorExit` (by not catching
the exception), :meth:`close` returns :const:`None`. If the generator
yields a value, a :exc:`RuntimeError` is raised. If the generator raises
any other exception, it is propagated to the caller. If the generator has
already exited due to an exception or normal exit, :meth:`close` returns
:const:`None`.
ntessore marked this conversation as resolved.
Show resolved Hide resolved

.. versionchanged:: 3.13

If a generator returns a value after being closed, the value is returned
by :meth:`close`.

.. index:: single: yield; examples

Expand Down
82 changes: 82 additions & 0 deletions Lib/test/test_generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,88 @@ def g():
self.assertEqual(cm.exception.value.value, 2)


class GeneratorCloseTest(unittest.TestCase):

def test_close_no_return_value(self):
def f():
yield

gen = f()
gen.send(None)
self.assertIsNone(gen.close())

def test_close_return_value(self):
def f():
try:
yield
# close() raises GeneratorExit here, which is caught
except GeneratorExit:
return 0

gen = f()
gen.send(None)
self.assertEqual(gen.close(), 0)

def test_close_not_catching_exit(self):
def f():
yield
# close() raises GeneratorExit here, which isn't caught and
# therefore propagates -- no return value
return 0

gen = f()
gen.send(None)
self.assertIsNone(gen.close())

def test_close_not_started(self):
def f():
try:
yield
except GeneratorExit:
return 0

gen = f()
self.assertIsNone(gen.close())

def test_close_exhausted(self):
def f():
try:
yield
except GeneratorExit:
return 0

gen = f()
next(gen)
with self.assertRaises(StopIteration):
next(gen)
self.assertIsNone(gen.close())

def test_close_closed(self):
def f():
try:
yield
except GeneratorExit:
return 0

gen = f()
gen.send(None)
self.assertEqual(gen.close(), 0)
self.assertIsNone(gen.close())

def test_close_raises(self):
def f():
try:
yield
except GeneratorExit:
pass
raise RuntimeError

gen = f()
gen.send(None)
with self.assertRaises(RuntimeError):
gen.close()


class GeneratorThrowTest(unittest.TestCase):

def test_exception_context_with_yield(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
If a generator returns a value after being closed, the value is returned
by :meth:`generator.close`.
ntessore marked this conversation as resolved.
Show resolved Hide resolved
25 changes: 22 additions & 3 deletions Objects/genobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -408,9 +408,28 @@ gen_close(PyGenObject *gen, PyObject *args)
PyErr_SetString(PyExc_RuntimeError, msg);
return NULL;
}
if (PyErr_ExceptionMatches(PyExc_StopIteration)
|| PyErr_ExceptionMatches(PyExc_GeneratorExit)) {
PyErr_Clear(); /* ignore these errors */
if (PyErr_ExceptionMatches(PyExc_StopIteration)) {
ntessore marked this conversation as resolved.
Show resolved Hide resolved
/* retrieve the StopIteration exception instance being handled, and
* extract its value */
PyObject *exc, *args, *value;
exc = PyErr_GetRaisedException(); /* clears the error indicator */
ntessore marked this conversation as resolved.
Show resolved Hide resolved
if (exc == NULL || !PyExceptionInstance_Check(exc)) {
Py_XDECREF(exc);
Py_RETURN_NONE;
}
ntessore marked this conversation as resolved.
Show resolved Hide resolved
args = ((PyBaseExceptionObject*)exc)->args;
if (args == NULL || !PyTuple_Check(args)
|| PyTuple_GET_SIZE(args) == 0) {
Py_DECREF(exc);
Py_RETURN_NONE;
}
value = PyTuple_GET_ITEM(args, 0);
Py_INCREF(value);
ntessore marked this conversation as resolved.
Show resolved Hide resolved
Py_DECREF(exc);
ntessore marked this conversation as resolved.
Show resolved Hide resolved
return value;
}
if (PyErr_ExceptionMatches(PyExc_GeneratorExit)) {
PyErr_Clear(); /* ignore this error */
Py_RETURN_NONE;
}
return NULL;
Expand Down