summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJamie Phan <jamie@ordinarylab.dev>2024-01-10 05:21:00 (GMT)
committerGitHub <noreply@github.com>2024-01-10 05:21:00 (GMT)
commit4826d52338396758b2d6790a498c2a06eec19a86 (patch)
treee1dc94c361fbc8ffb62d108bc8587b7072cd522a
parent5d8a3e74b51a59752f24cb869e7daa065b673f83 (diff)
downloadcpython-4826d52338396758b2d6790a498c2a06eec19a86.zip
cpython-4826d52338396758b2d6790a498c2a06eec19a86.tar.gz
cpython-4826d52338396758b2d6790a498c2a06eec19a86.tar.bz2
gh-112182: Replace StopIteration with RuntimeError for future (#113220)
When an `StopIteration` raises into `asyncio.Future`, this will cause a thread to hang. This commit address this by not raising an exception and silently transforming the `StopIteration` with a `RuntimeError`, which the caller can reconstruct from `fut.exception().__cause__`
-rw-r--r--Lib/asyncio/futures.py10
-rw-r--r--Lib/test/test_asyncio/test_futures.py23
-rw-r--r--Misc/NEWS.d/next/Library/2023-12-17-10-22-55.gh-issue-112182.jLWGlr.rst3
-rw-r--r--Modules/_asynciomodule.c25
4 files changed, 49 insertions, 12 deletions
diff --git a/Lib/asyncio/futures.py b/Lib/asyncio/futures.py
index d19e5d8..5d35321 100644
--- a/Lib/asyncio/futures.py
+++ b/Lib/asyncio/futures.py
@@ -269,9 +269,13 @@ class Future:
raise exceptions.InvalidStateError(f'{self._state}: {self!r}')
if isinstance(exception, type):
exception = exception()
- if type(exception) is StopIteration:
- raise TypeError("StopIteration interacts badly with generators "
- "and cannot be raised into a Future")
+ if isinstance(exception, StopIteration):
+ new_exc = RuntimeError("StopIteration interacts badly with "
+ "generators and cannot be raised into a "
+ "Future")
+ new_exc.__cause__ = exception
+ new_exc.__context__ = exception
+ exception = new_exc
self._exception = exception
self._exception_tb = exception.__traceback__
self._state = _FINISHED
diff --git a/Lib/test/test_asyncio/test_futures.py b/Lib/test/test_asyncio/test_futures.py
index 2184b20..d3e8efe 100644
--- a/Lib/test/test_asyncio/test_futures.py
+++ b/Lib/test/test_asyncio/test_futures.py
@@ -270,10 +270,6 @@ class BaseFutureTests:
f = self._new_future(loop=self.loop)
self.assertRaises(asyncio.InvalidStateError, f.exception)
- # StopIteration cannot be raised into a Future - CPython issue26221
- self.assertRaisesRegex(TypeError, "StopIteration .* cannot be raised",
- f.set_exception, StopIteration)
-
f.set_exception(exc)
self.assertFalse(f.cancelled())
self.assertTrue(f.done())
@@ -283,6 +279,25 @@ class BaseFutureTests:
self.assertRaises(asyncio.InvalidStateError, f.set_exception, None)
self.assertFalse(f.cancel())
+ def test_stop_iteration_exception(self, stop_iteration_class=StopIteration):
+ exc = stop_iteration_class()
+ f = self._new_future(loop=self.loop)
+ f.set_exception(exc)
+ self.assertFalse(f.cancelled())
+ self.assertTrue(f.done())
+ self.assertRaises(RuntimeError, f.result)
+ exc = f.exception()
+ cause = exc.__cause__
+ self.assertIsInstance(exc, RuntimeError)
+ self.assertRegex(str(exc), 'StopIteration .* cannot be raised')
+ self.assertIsInstance(cause, stop_iteration_class)
+
+ def test_stop_iteration_subclass_exception(self):
+ class MyStopIteration(StopIteration):
+ pass
+
+ self.test_stop_iteration_exception(MyStopIteration)
+
def test_exception_class(self):
f = self._new_future(loop=self.loop)
f.set_exception(RuntimeError)
diff --git a/Misc/NEWS.d/next/Library/2023-12-17-10-22-55.gh-issue-112182.jLWGlr.rst b/Misc/NEWS.d/next/Library/2023-12-17-10-22-55.gh-issue-112182.jLWGlr.rst
new file mode 100644
index 0000000..dc5bb69
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-12-17-10-22-55.gh-issue-112182.jLWGlr.rst
@@ -0,0 +1,3 @@
+:meth:`asyncio.futures.Future.set_exception()` now transforms :exc:`StopIteration`
+into :exc:`RuntimeError` instead of hanging or other misbehavior. Patch
+contributed by Jamie Phan.
diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c
index b929e6d..c1aa849 100644
--- a/Modules/_asynciomodule.c
+++ b/Modules/_asynciomodule.c
@@ -597,12 +597,27 @@ future_set_exception(asyncio_state *state, FutureObj *fut, PyObject *exc)
PyErr_SetString(PyExc_TypeError, "invalid exception object");
return NULL;
}
- if (Py_IS_TYPE(exc_val, (PyTypeObject *)PyExc_StopIteration)) {
+ if (PyErr_GivenExceptionMatches(exc_val, PyExc_StopIteration)) {
+ const char *msg = "StopIteration interacts badly with "
+ "generators and cannot be raised into a "
+ "Future";
+ PyObject *message = PyUnicode_FromString(msg);
+ if (message == NULL) {
+ Py_DECREF(exc_val);
+ return NULL;
+ }
+ PyObject *err = PyObject_CallOneArg(PyExc_RuntimeError, message);
+ Py_DECREF(message);
+ if (err == NULL) {
+ Py_DECREF(exc_val);
+ return NULL;
+ }
+ assert(PyExceptionInstance_Check(err));
+
+ PyException_SetCause(err, Py_NewRef(exc_val));
+ PyException_SetContext(err, Py_NewRef(exc_val));
Py_DECREF(exc_val);
- PyErr_SetString(PyExc_TypeError,
- "StopIteration interacts badly with generators "
- "and cannot be raised into a Future");
- return NULL;
+ exc_val = err;
}
assert(!fut->fut_exception);