Skip to content

Commit

Permalink
pythongh-104770: Let generator.close() return value (python#104771)
Browse files Browse the repository at this point in the history
Co-authored-by: Irit Katriel <1055913+iritkatriel@users.noreply.github.com>
  • Loading branch information
ntessore and iritkatriel authored May 23, 2023
1 parent 50fce89 commit d56c933
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 9 deletions.
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

0 comments on commit d56c933

Please # to comment.