Skip to content

Commit d56c933

Browse files
gh-104770: Let generator.close() return value (#104771)
Co-authored-by: Irit Katriel <[email protected]>
1 parent 50fce89 commit d56c933

File tree

4 files changed

+105
-9
lines changed

4 files changed

+105
-9
lines changed

Doc/reference/expressions.rst

+13-6
Original file line numberDiff line numberDiff line change
@@ -595,12 +595,19 @@ is already executing raises a :exc:`ValueError` exception.
595595
.. method:: generator.close()
596596

597597
Raises a :exc:`GeneratorExit` at the point where the generator function was
598-
paused. If the generator function then exits gracefully, is already closed,
599-
or raises :exc:`GeneratorExit` (by not catching the exception), close
600-
returns to its caller. If the generator yields a value, a
601-
:exc:`RuntimeError` is raised. If the generator raises any other exception,
602-
it is propagated to the caller. :meth:`close` does nothing if the generator
603-
has already exited due to an exception or normal exit.
598+
paused. If the generator function catches the exception and returns a
599+
value, this value is returned from :meth:`close`. If the generator function
600+
is already closed, or raises :exc:`GeneratorExit` (by not catching the
601+
exception), :meth:`close` returns :const:`None`. If the generator yields a
602+
value, a :exc:`RuntimeError` is raised. If the generator raises any other
603+
exception, it is propagated to the caller. If the generator has already
604+
exited due to an exception or normal exit, :meth:`close` returns
605+
:const:`None` and has no other effect.
606+
607+
.. versionchanged:: 3.13
608+
609+
If a generator returns a value upon being closed, the value is returned
610+
by :meth:`close`.
604611

605612
.. index:: single: yield; examples
606613

Lib/test/test_generators.py

+82
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,88 @@ def g():
451451
self.assertEqual(cm.exception.value.value, 2)
452452

453453

454+
class GeneratorCloseTest(unittest.TestCase):
455+
456+
def test_close_no_return_value(self):
457+
def f():
458+
yield
459+
460+
gen = f()
461+
gen.send(None)
462+
self.assertIsNone(gen.close())
463+
464+
def test_close_return_value(self):
465+
def f():
466+
try:
467+
yield
468+
# close() raises GeneratorExit here, which is caught
469+
except GeneratorExit:
470+
return 0
471+
472+
gen = f()
473+
gen.send(None)
474+
self.assertEqual(gen.close(), 0)
475+
476+
def test_close_not_catching_exit(self):
477+
def f():
478+
yield
479+
# close() raises GeneratorExit here, which isn't caught and
480+
# therefore propagates -- no return value
481+
return 0
482+
483+
gen = f()
484+
gen.send(None)
485+
self.assertIsNone(gen.close())
486+
487+
def test_close_not_started(self):
488+
def f():
489+
try:
490+
yield
491+
except GeneratorExit:
492+
return 0
493+
494+
gen = f()
495+
self.assertIsNone(gen.close())
496+
497+
def test_close_exhausted(self):
498+
def f():
499+
try:
500+
yield
501+
except GeneratorExit:
502+
return 0
503+
504+
gen = f()
505+
next(gen)
506+
with self.assertRaises(StopIteration):
507+
next(gen)
508+
self.assertIsNone(gen.close())
509+
510+
def test_close_closed(self):
511+
def f():
512+
try:
513+
yield
514+
except GeneratorExit:
515+
return 0
516+
517+
gen = f()
518+
gen.send(None)
519+
self.assertEqual(gen.close(), 0)
520+
self.assertIsNone(gen.close())
521+
522+
def test_close_raises(self):
523+
def f():
524+
try:
525+
yield
526+
except GeneratorExit:
527+
pass
528+
raise RuntimeError
529+
530+
gen = f()
531+
gen.send(None)
532+
with self.assertRaises(RuntimeError):
533+
gen.close()
534+
535+
454536
class GeneratorThrowTest(unittest.TestCase):
455537

456538
def test_exception_context_with_yield(self):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
If a generator returns a value upon being closed, the value is now returned
2+
by :meth:`generator.close`.

Objects/genobject.c

+8-3
Original file line numberDiff line numberDiff line change
@@ -408,11 +408,16 @@ gen_close(PyGenObject *gen, PyObject *args)
408408
PyErr_SetString(PyExc_RuntimeError, msg);
409409
return NULL;
410410
}
411-
if (PyErr_ExceptionMatches(PyExc_StopIteration)
412-
|| PyErr_ExceptionMatches(PyExc_GeneratorExit)) {
413-
PyErr_Clear(); /* ignore these errors */
411+
assert(PyErr_Occurred());
412+
if (PyErr_ExceptionMatches(PyExc_GeneratorExit)) {
413+
PyErr_Clear(); /* ignore this error */
414414
Py_RETURN_NONE;
415415
}
416+
/* if the generator returned a value while closing, StopIteration was
417+
* raised in gen_send_ex() above; retrieve and return the value here */
418+
if (_PyGen_FetchStopIterationValue(&retval) == 0) {
419+
return retval;
420+
}
416421
return NULL;
417422
}
418423

0 commit comments

Comments
 (0)