mirror of
https://github.com/python/cpython
synced 2024-10-14 10:13:37 +00:00
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__`
This commit is contained in:
parent
5d8a3e74b5
commit
4826d52338
|
@ -269,9 +269,13 @@ def set_exception(self, exception):
|
||||||
raise exceptions.InvalidStateError(f'{self._state}: {self!r}')
|
raise exceptions.InvalidStateError(f'{self._state}: {self!r}')
|
||||||
if isinstance(exception, type):
|
if isinstance(exception, type):
|
||||||
exception = exception()
|
exception = exception()
|
||||||
if type(exception) is StopIteration:
|
if isinstance(exception, StopIteration):
|
||||||
raise TypeError("StopIteration interacts badly with generators "
|
new_exc = RuntimeError("StopIteration interacts badly with "
|
||||||
"and cannot be raised into a Future")
|
"generators and cannot be raised into a "
|
||||||
|
"Future")
|
||||||
|
new_exc.__cause__ = exception
|
||||||
|
new_exc.__context__ = exception
|
||||||
|
exception = new_exc
|
||||||
self._exception = exception
|
self._exception = exception
|
||||||
self._exception_tb = exception.__traceback__
|
self._exception_tb = exception.__traceback__
|
||||||
self._state = _FINISHED
|
self._state = _FINISHED
|
||||||
|
|
|
@ -270,10 +270,6 @@ def test_exception(self):
|
||||||
f = self._new_future(loop=self.loop)
|
f = self._new_future(loop=self.loop)
|
||||||
self.assertRaises(asyncio.InvalidStateError, f.exception)
|
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)
|
f.set_exception(exc)
|
||||||
self.assertFalse(f.cancelled())
|
self.assertFalse(f.cancelled())
|
||||||
self.assertTrue(f.done())
|
self.assertTrue(f.done())
|
||||||
|
@ -283,6 +279,25 @@ def test_exception(self):
|
||||||
self.assertRaises(asyncio.InvalidStateError, f.set_exception, None)
|
self.assertRaises(asyncio.InvalidStateError, f.set_exception, None)
|
||||||
self.assertFalse(f.cancel())
|
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):
|
def test_exception_class(self):
|
||||||
f = self._new_future(loop=self.loop)
|
f = self._new_future(loop=self.loop)
|
||||||
f.set_exception(RuntimeError)
|
f.set_exception(RuntimeError)
|
||||||
|
|
|
@ -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.
|
|
@ -597,13 +597,28 @@ future_set_exception(asyncio_state *state, FutureObj *fut, PyObject *exc)
|
||||||
PyErr_SetString(PyExc_TypeError, "invalid exception object");
|
PyErr_SetString(PyExc_TypeError, "invalid exception object");
|
||||||
return NULL;
|
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);
|
Py_DECREF(exc_val);
|
||||||
PyErr_SetString(PyExc_TypeError,
|
|
||||||
"StopIteration interacts badly with generators "
|
|
||||||
"and cannot be raised into a Future");
|
|
||||||
return NULL;
|
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);
|
||||||
|
exc_val = err;
|
||||||
|
}
|
||||||
|
|
||||||
assert(!fut->fut_exception);
|
assert(!fut->fut_exception);
|
||||||
assert(!fut->fut_exception_tb);
|
assert(!fut->fut_exception_tb);
|
||||||
|
|
Loading…
Reference in a new issue