Skip to content

Commit 41090b7

Browse files
[3.12] pythongh-112182: Replace StopIteration with RuntimeError for future (pythonGH-113220) (pythonGH-123033)
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__` (cherry picked from commit 4826d52) Co-authored-by: Jamie Phan <[email protected]>
1 parent 9f153a2 commit 41090b7

File tree

4 files changed

+49
-12
lines changed

4 files changed

+49
-12
lines changed

Lib/asyncio/futures.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -272,9 +272,13 @@ def set_exception(self, exception):
272272
raise exceptions.InvalidStateError(f'{self._state}: {self!r}')
273273
if isinstance(exception, type):
274274
exception = exception()
275-
if type(exception) is StopIteration:
276-
raise TypeError("StopIteration interacts badly with generators "
277-
"and cannot be raised into a Future")
275+
if isinstance(exception, StopIteration):
276+
new_exc = RuntimeError("StopIteration interacts badly with "
277+
"generators and cannot be raised into a "
278+
"Future")
279+
new_exc.__cause__ = exception
280+
new_exc.__context__ = exception
281+
exception = new_exc
278282
self._exception = exception
279283
self._exception_tb = exception.__traceback__
280284
self._state = _FINISHED

Lib/test/test_asyncio/test_futures.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -270,10 +270,6 @@ def test_exception(self):
270270
f = self._new_future(loop=self.loop)
271271
self.assertRaises(asyncio.InvalidStateError, f.exception)
272272

273-
# StopIteration cannot be raised into a Future - CPython issue26221
274-
self.assertRaisesRegex(TypeError, "StopIteration .* cannot be raised",
275-
f.set_exception, StopIteration)
276-
277273
f.set_exception(exc)
278274
self.assertFalse(f.cancelled())
279275
self.assertTrue(f.done())
@@ -283,6 +279,25 @@ def test_exception(self):
283279
self.assertRaises(asyncio.InvalidStateError, f.set_exception, None)
284280
self.assertFalse(f.cancel())
285281

282+
def test_stop_iteration_exception(self, stop_iteration_class=StopIteration):
283+
exc = stop_iteration_class()
284+
f = self._new_future(loop=self.loop)
285+
f.set_exception(exc)
286+
self.assertFalse(f.cancelled())
287+
self.assertTrue(f.done())
288+
self.assertRaises(RuntimeError, f.result)
289+
exc = f.exception()
290+
cause = exc.__cause__
291+
self.assertIsInstance(exc, RuntimeError)
292+
self.assertRegex(str(exc), 'StopIteration .* cannot be raised')
293+
self.assertIsInstance(cause, stop_iteration_class)
294+
295+
def test_stop_iteration_subclass_exception(self):
296+
class MyStopIteration(StopIteration):
297+
pass
298+
299+
self.test_stop_iteration_exception(MyStopIteration)
300+
286301
def test_exception_class(self):
287302
f = self._new_future(loop=self.loop)
288303
f.set_exception(RuntimeError)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:meth:`!asyncio.futures.Future.set_exception()` now transforms :exc:`StopIteration`
2+
into :exc:`RuntimeError` instead of hanging or other misbehavior. Patch
3+
contributed by Jamie Phan.

Modules/_asynciomodule.c

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -594,12 +594,27 @@ future_set_exception(asyncio_state *state, FutureObj *fut, PyObject *exc)
594594
PyErr_SetString(PyExc_TypeError, "invalid exception object");
595595
return NULL;
596596
}
597-
if (Py_IS_TYPE(exc_val, (PyTypeObject *)PyExc_StopIteration)) {
597+
if (PyErr_GivenExceptionMatches(exc_val, PyExc_StopIteration)) {
598+
const char *msg = "StopIteration interacts badly with "
599+
"generators and cannot be raised into a "
600+
"Future";
601+
PyObject *message = PyUnicode_FromString(msg);
602+
if (message == NULL) {
603+
Py_DECREF(exc_val);
604+
return NULL;
605+
}
606+
PyObject *err = PyObject_CallOneArg(PyExc_RuntimeError, message);
607+
Py_DECREF(message);
608+
if (err == NULL) {
609+
Py_DECREF(exc_val);
610+
return NULL;
611+
}
612+
assert(PyExceptionInstance_Check(err));
613+
614+
PyException_SetCause(err, Py_NewRef(exc_val));
615+
PyException_SetContext(err, Py_NewRef(exc_val));
598616
Py_DECREF(exc_val);
599-
PyErr_SetString(PyExc_TypeError,
600-
"StopIteration interacts badly with generators "
601-
"and cannot be raised into a Future");
602-
return NULL;
617+
exc_val = err;
603618
}
604619

605620
assert(!fut->fut_exception);

0 commit comments

Comments
 (0)