From fca56917014e36e6bbaab60dbb89ef2e3cb2c237 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Tue, 29 Sep 2026 19:17:14 -0700 Subject: [PATCH 1/3] 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}, From 38612003d30d109035a4dfef808b57b2e12d441e Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Tue, 6 Oct 2026 23:23:20 -0700 Subject: [PATCH 2/3] clear fi_future in python throw/close --- Lib/asyncio/futures.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/asyncio/futures.py b/Lib/asyncio/futures.py index eece4f3978f24d4..6f192fcee323253 100644 --- a/Lib/asyncio/futures.py +++ b/Lib/asyncio/futures.py @@ -324,9 +324,11 @@ def send(self, v, /): return self.__gen.send(v) def throw(self, *args): + self.fi_future = None return self.__gen.throw(*args) def close(self): + self.fi_future = None self.__gen.close() From 1f400b397bc296f8d056836b5c25c3beebf00a26 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Wed, 7 Oct 2026 15:32:45 -0700 Subject: [PATCH 3/3] make fi_future read-only, use __slots__ --- Lib/asyncio/futures.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Lib/asyncio/futures.py b/Lib/asyncio/futures.py index 6f192fcee323253..76675988c2bfe28 100644 --- a/Lib/asyncio/futures.py +++ b/Lib/asyncio/futures.py @@ -302,18 +302,24 @@ def _await(self): return self.result() # May raise too. def __await__(self): + # Return an object that exposes fi_future in addition to being + # a generator, for compatability with the C version. return _FutureIter(self) __iter__ = __await__ # make compatible with 'yield from'. class _FutureIter: + __slots__ = ('__future', '__gen') + def __init__(self, fut): - # The whole point of this class is to expose fi_future, like the C - # version - self.fi_future = fut + self.__future = fut self.__gen = fut._await() + @property + def fi_future(self): + return self.__future + def __iter__(self): return self @@ -324,11 +330,11 @@ def send(self, v, /): return self.__gen.send(v) def throw(self, *args): - self.fi_future = None + self.__future = None return self.__gen.throw(*args) def close(self): - self.fi_future = None + self.__future = None self.__gen.close()