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

Co-authored-by: Irit Katriel <1055913+iritkatriel@users.noreply.github.com>
This commit is contained in:
Nicolas Tessore 2023-05-23 21:51:56 +01:00 committed by GitHub
parent 50fce89d12
commit d56c933992
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 105 additions and 9 deletions

View File

@ -595,12 +595,19 @@ is already executing raises a :exc:`ValueError` exception.
.. method:: generator.close() .. method:: generator.close()
Raises a :exc:`GeneratorExit` at the point where the generator function was Raises a :exc:`GeneratorExit` at the point where the generator function was
paused. If the generator function then exits gracefully, is already closed, paused. If the generator function catches the exception and returns a
or raises :exc:`GeneratorExit` (by not catching the exception), close value, this value is returned from :meth:`close`. If the generator function
returns to its caller. If the generator yields a value, a is already closed, or raises :exc:`GeneratorExit` (by not catching the
:exc:`RuntimeError` is raised. If the generator raises any other exception, exception), :meth:`close` returns :const:`None`. If the generator yields a
it is propagated to the caller. :meth:`close` does nothing if the generator value, a :exc:`RuntimeError` is raised. If the generator raises any other
has already exited due to an exception or normal exit. 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 .. index:: single: yield; examples

View File

@ -451,6 +451,88 @@ class ExceptionTest(unittest.TestCase):
self.assertEqual(cm.exception.value.value, 2) 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): class GeneratorThrowTest(unittest.TestCase):
def test_exception_context_with_yield(self): def test_exception_context_with_yield(self):

View File

@ -0,0 +1,2 @@
If a generator returns a value upon being closed, the value is now returned
by :meth:`generator.close`.

View File

@ -408,11 +408,16 @@ gen_close(PyGenObject *gen, PyObject *args)
PyErr_SetString(PyExc_RuntimeError, msg); PyErr_SetString(PyExc_RuntimeError, msg);
return NULL; return NULL;
} }
if (PyErr_ExceptionMatches(PyExc_StopIteration) assert(PyErr_Occurred());
|| PyErr_ExceptionMatches(PyExc_GeneratorExit)) { if (PyErr_ExceptionMatches(PyExc_GeneratorExit)) {
PyErr_Clear(); /* ignore these errors */ PyErr_Clear(); /* ignore this error */
Py_RETURN_NONE; 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; return NULL;
} }