From fca56917014e36e6bbaab60dbb89ef2e3cb2c237 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Tue, 29 Sep 2026 19:17:14 -0700 Subject: [PATCH] gh-158192: expose the Future in FutureIter as fi_future I'd be happy to bikeshed about the name if anybody wants to, but the scheme kind of matches generators and similar. For consistency, I made the Python implementation match, which required a wrapper class. I can drop that if it doesn't seem worth the hassle; the future can be introspected from the frame object of the generator already. --- Lib/asyncio/futures.py | 28 ++++++++++++++++++- Lib/test/test_asyncio/test_futures.py | 4 +++ ...-09-29-19-34-30.gh-issue-158192.6wPjiU.rst | 1 + Modules/_asynciomodule.c | 17 +++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-29-19-34-30.gh-issue-158192.6wPjiU.rst diff --git a/Lib/asyncio/futures.py b/Lib/asyncio/futures.py index 11858a0274a69fd..eece4f3978f24d4 100644 --- a/Lib/asyncio/futures.py +++ b/Lib/asyncio/futures.py @@ -293,7 +293,7 @@ def set_exception(self, exception): self.__schedule_callbacks() self.__log_traceback = True - def __await__(self): + def _await(self): if not self.done(): self._asyncio_future_blocking = True yield self # This tells Task to wait for completion. @@ -301,9 +301,35 @@ def __await__(self): raise RuntimeError("await wasn't used with future") return self.result() # May raise too. + def __await__(self): + return _FutureIter(self) + __iter__ = __await__ # make compatible with 'yield from'. +class _FutureIter: + def __init__(self, fut): + # The whole point of this class is to expose fi_future, like the C + # version + self.fi_future = fut + self.__gen = fut._await() + + def __iter__(self): + return self + + def __next__(self): + return self.__gen.__next__() + + def send(self, v, /): + return self.__gen.send(v) + + def throw(self, *args): + return self.__gen.throw(*args) + + def close(self): + self.__gen.close() + + # Needed for testing purposes. _PyFuture = Future diff --git a/Lib/test/test_asyncio/test_futures.py b/Lib/test/test_asyncio/test_futures.py index a217177e1deb06d..c716b9f14cb13c5 100644 --- a/Lib/test/test_asyncio/test_futures.py +++ b/Lib/test/test_asyncio/test_futures.py @@ -712,6 +712,10 @@ def test_future_stop_iteration_args(self): self.fail('StopIteration was expected') self.assertEqual(result, (1, 2)) + def test_future_iter_fut(self): + fut = self._new_future(loop=self.loop) + self.assertIs(iter(fut).fi_future, fut) + def test_future_iter_throw(self): fut = self._new_future(loop=self.loop) fi = iter(fut) diff --git a/Misc/NEWS.d/next/Library/2026-09-29-19-34-30.gh-issue-158192.6wPjiU.rst b/Misc/NEWS.d/next/Library/2026-09-29-19-34-30.gh-issue-158192.6wPjiU.rst new file mode 100644 index 000000000000000..b0634084c6af8ad --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-29-19-34-30.gh-issue-158192.6wPjiU.rst @@ -0,0 +1 @@ +Expose the future being waited on by a ``FutureIter`` as ``fi_future``. diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index 18e731336d6da1d..b91f819ba84bc73 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -1971,6 +1971,22 @@ static PyMethodDef FutureIter_methods[] = { {NULL, NULL} /* Sentinel */ }; +static PyObject * +FutureIter_get_fi_future(PyObject *op, void *Py_UNUSED(closure)) +{ + futureiterobject *it = (futureiterobject*)op; + PyObject *fut; + Py_BEGIN_CRITICAL_SECTION(op); + fut = it->future ? Py_NewRef(it->future) : Py_None; + Py_END_CRITICAL_SECTION(); + return fut; +} + +static PyGetSetDef FutureIter_getsetlist[] = { + {"fi_future", FutureIter_get_fi_future, NULL, NULL}, + {NULL} /* Sentinel */ +}; + static PyType_Slot FutureIter_slots[] = { {Py_tp_dealloc, FutureIter_dealloc}, {Py_tp_getattro, PyObject_GenericGetAttr}, @@ -1979,6 +1995,7 @@ static PyType_Slot FutureIter_slots[] = { {Py_tp_iter, PyObject_SelfIter}, {Py_tp_iternext, FutureIter_iternext}, {Py_tp_methods, FutureIter_methods}, + {Py_tp_getset, FutureIter_getsetlist}, // async methods {Py_am_send, FutureIter_am_send},