Skip to content

Commit 4826d52

Browse files
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__`
1 parent 5d8a3e7 commit 4826d52

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
@@ -269,9 +269,13 @@ def set_exception(self, exception):
269269
raise exceptions.InvalidStateError(f'{self._state}: {self!r}')
270270
if isinstance(exception, type):
271271
exception = exception()
272-
if type(exception) is StopIteration:
273-
raise TypeError("StopIteration interacts badly with generators "
274-
"and cannot be raised into a Future")
272+
if isinstance(exception, StopIteration):
273+
new_exc = RuntimeError("StopIteration interacts badly with "
274+
"generators and cannot be raised into a "
275+
"Future")
276+
new_exc.__cause__ = exception
277+
new_exc.__context__ = exception
278+
exception = new_exc
275279
self._exception = exception
276280
self._exception_tb = exception.__traceback__
277281
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
@@ -597,12 +597,27 @@ future_set_exception(asyncio_state *state, FutureObj *fut, PyObject *exc)
597597
PyErr_SetString(PyExc_TypeError, "invalid exception object");
598598
return NULL;
599599
}
600-
if (Py_IS_TYPE(exc_val, (PyTypeObject *)PyExc_StopIteration)) {
600+
if (PyErr_GivenExceptionMatches(exc_val, PyExc_StopIteration)) {
601+
const char *msg = "StopIteration interacts badly with "
602+
"generators and cannot be raised into a "
603+
"Future";
604+
PyObject *message = PyUnicode_FromString(msg);
605+
if (message == NULL) {
606+
Py_DECREF(exc_val);
607+
return NULL;
608+
}
609+
PyObject *err = PyObject_CallOneArg(PyExc_RuntimeError, message);
610+
Py_DECREF(message);
611+
if (err == NULL) {
612+
Py_DECREF(exc_val);
613+
return NULL;
614+
}
615+
assert(PyExceptionInstance_Check(err));
616+
617+
PyException_SetCause(err, Py_NewRef(exc_val));
618+
PyException_SetContext(err, Py_NewRef(exc_val));
601619
Py_DECREF(exc_val);
602-
PyErr_SetString(PyExc_TypeError,
603-
"StopIteration interacts badly with generators "
604-
"and cannot be raised into a Future");
605-
return NULL;
620+
exc_val = err;
606621
}
607622

608623
assert(!fut->fut_exception);

0 commit comments

Comments
 (0)