Skip to content

Commit

Permalink
pythongh-112182: Replace StopIteration with RuntimeError for future (p…
Browse files Browse the repository at this point in the history
…ython#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__`
  • Loading branch information
ordinary-jamie authored and aisk committed Feb 11, 2024
1 parent 666e4dd commit 4faa2df
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 12 deletions.
10 changes: 7 additions & 3 deletions Lib/asyncio/futures.py
Expand Up @@ -269,9 +269,13 @@ def set_exception(self, exception):
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
Expand Down
23 changes: 19 additions & 4 deletions Lib/test/test_asyncio/test_futures.py
Expand Up @@ -270,10 +270,6 @@ def test_exception(self):
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())
Expand All @@ -283,6 +279,25 @@ def test_exception(self):
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)
Expand Down
@@ -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.
25 changes: 20 additions & 5 deletions Modules/_asynciomodule.c
Expand Up @@ -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);
Expand Down

0 comments on commit 4faa2df

Please sign in to comment.