diff --git a/panda/src/event/asyncFuture_ext.cxx b/panda/src/event/asyncFuture_ext.cxx index 525757d618..28753dd924 100644 --- a/panda/src/event/asyncFuture_ext.cxx +++ b/panda/src/event/asyncFuture_ext.cxx @@ -167,8 +167,27 @@ static PyObject *gen_next(PyObject *self) { } else { PyObject *result = get_done_result(future); if (result != nullptr) { - Py_INCREF(PyExc_StopIteration); - PyErr_Restore(PyExc_StopIteration, result, nullptr); + // See python/cpython#101578 - PyErr_SetObject has a special case where + // it interprets a tuple specially, so we bypass that by creating the + // exception directly. +#if PY_VERSION_HEX >= 0x030C0000 // 3.12 + PyObject *exc = PyObject_CallOneArg(PyExc_StopIteration, result); + if (LIKELY(exc != nullptr)) { + // This function steals a reference to exc. + PyErr_SetRaisedException(exc); + } +#else + if (PyTuple_Check(result)) { + PyObject *exc = PyObject_CallOneArg(PyExc_StopIteration, result); + if (LIKELY(exc != nullptr)) { + PyErr_SetObject(PyExc_StopIteration, exc); + Py_DECREF(exc); + } + } else { + Py_INCREF(PyExc_StopIteration); + PyErr_Restore(PyExc_StopIteration, result, nullptr); + } +#endif } return nullptr; } diff --git a/tests/event/test_futures.py b/tests/event/test_futures.py index 2334549eef..db016ae8d3 100644 --- a/tests/event/test_futures.py +++ b/tests/event/test_futures.py @@ -10,6 +10,21 @@ else: CancelledError = Exception +def check_result(fut, expected): + """Asserts the result of the future is the expected value.""" + + if fut.result() != expected: + return False + + # Make sure that await also returns the values properly + with pytest.raises(StopIteration) as e: + next(fut.__await__()) + if e.value.value != expected: + return False + + return True + + def test_future_cancelled(): fut = core.AsyncFuture() @@ -205,15 +220,20 @@ def test_future_result(): ep = core.EventParameter(0.5) fut = core.AsyncFuture() fut.set_result(ep) - assert fut.result() == 0.5 - assert fut.result() == 0.5 + assert check_result(fut, 0.5) + assert check_result(fut, 0.5) # Store TypedObject dg = core.Datagram(b"test") fut = core.AsyncFuture() fut.set_result(dg) - assert fut.result() == dg - assert fut.result() == dg + assert check_result(fut, dg) + assert check_result(fut, dg) + + # Store tuple + fut = core.AsyncFuture() + fut.set_result((1, 2)) + assert check_result(fut, (1, 2)) # Store arbitrary Python object obj = object() @@ -250,7 +270,7 @@ def test_future_gather(): assert gather.done() assert not gather.cancelled() - assert tuple(gather.result()) == (1, 2) + assert check_result(gather, (1, 2)) def test_future_gather_cancel_inner():