]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-104770: Let generator.close() return value (#104771)
authorNicolas Tessore <n.tessore@ucl.ac.uk>
Tue, 23 May 2023 20:51:56 +0000 (21:51 +0100)
committerGitHub <noreply@github.com>
Tue, 23 May 2023 20:51:56 +0000 (13:51 -0700)
Co-authored-by: Irit Katriel <1055913+iritkatriel@users.noreply.github.com>
Doc/reference/expressions.rst
Lib/test/test_generators.py
Misc/NEWS.d/next/Core and Builtins/2023-05-23-00-36-02.gh-issue-104770.poSkyY.rst [new file with mode: 0644]
Objects/genobject.c

index b97a08f25d92a249e0b82ca75939dab543ef2023..0c700f908d6878c84b3cb3e537bb004872cd3162 100644 (file)
@@ -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
 
index 31680b5a92e0f3ca7fae166b296d2136c2af01fa..a8a344ab8de48dfd8da5ffa592081d48450b51e3 100644 (file)
@@ -451,6 +451,88 @@ class ExceptionTest(unittest.TestCase):
         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):
diff --git a/Misc/NEWS.d/next/Core and Builtins/2023-05-23-00-36-02.gh-issue-104770.poSkyY.rst b/Misc/NEWS.d/next/Core and Builtins/2023-05-23-00-36-02.gh-issue-104770.poSkyY.rst
new file mode 100644 (file)
index 0000000..2103fb7
--- /dev/null
@@ -0,0 +1,2 @@
+If a generator returns a value upon being closed, the value is now returned
+by :meth:`generator.close`.
index 9252c65493456573a0e4020a9cd78376a924adb0..1abfc83ab678efcfb8b462cc4955b64594bdbc77 100644 (file)
@@ -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;
 }