Skip to content

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

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 9 commits into from
May 23, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
12 changes: 9 additions & 3 deletions Doc/reference/expressions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -595,13 +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
paused. If the generator function then exits gracefully, its return value
is returned by :meth:`close`. If the generator function is already closed,
or raises :exc:`GeneratorExit` (by not catching the exception),
:meth:`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.

.. versionchanged:: 3.13

If a generator exits gracefully, its return value is returned by
:meth:`close`.

.. index:: single: yield; examples

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 @@
Let :meth:`generator.close` forward the return value of a generator function
on graceful exit.
30 changes: 27 additions & 3 deletions Objects/genobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -408,9 +408,33 @@ 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)) {
/* retrieve the StopIteration exception instance being handled, and
* extract its value */
PyObject *exc, *args, *value;
PyThreadState *tstate = _PyThreadState_GET();
if (tstate == NULL) {
PyErr_Clear();
Py_RETURN_NONE;
}
exc = tstate->current_exception;
if (exc == NULL || !PyExceptionInstance_Check(exc)) {
PyErr_Clear();
Py_RETURN_NONE;
}
args = ((PyBaseExceptionObject*)exc)->args;
if (args == NULL || !PyTuple_Check(args)
|| PyTuple_GET_SIZE(args) == 0) {
PyErr_Clear();
Py_RETURN_NONE;
}
value = PyTuple_GET_ITEM(args, 0);
Py_INCREF(value);
PyErr_Clear();
return value;
}
if (PyErr_ExceptionMatches(PyExc_GeneratorExit)) {
PyErr_Clear(); /* ignore this error */
Py_RETURN_NONE;
}
return NULL;
Expand Down