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 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
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 catches the exception and returns a
value, this value is returned from :meth:`close`. If the generator 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` and has no other effect.

.. versionchanged:: 3.13

If a generator returns a value upon 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 upon being closed, the value is now returned
by :meth:`generator.close`.
11 changes: 8 additions & 3 deletions Objects/genobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -408,11 +408,16 @@ 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 */
assert(PyErr_Occurred());
if (PyErr_ExceptionMatches(PyExc_GeneratorExit)) {
PyErr_Clear(); /* ignore this error */
Py_RETURN_NONE;
}
/* if the generator returned a value while closing, StopIteration was
* raised in gen_send_ex() above; retrieve and return the value here */
if (_PyGen_FetchStopIterationValue(&retval) == 0) {
return retval;
}
return NULL;
}

Expand Down