From 5bcc3d5795c887462fecf08b1fdb2d9e8d9b9320 Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 27 Feb 2024 21:52:57 +0000 Subject: [PATCH] gh-115957: Close coroutine if the TaskGroup is inactive --- Doc/library/asyncio-task.rst | 4 ++++ Doc/whatsnew/3.13.rst | 5 +++++ Lib/asyncio/taskgroups.py | 3 +++ Lib/test/test_asyncio/test_taskgroups.py | 21 +++++++++++---------- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/Doc/library/asyncio-task.rst b/Doc/library/asyncio-task.rst index 24bd36e6431b4f..db92e45adb2f54 100644 --- a/Doc/library/asyncio-task.rst +++ b/Doc/library/asyncio-task.rst @@ -335,6 +335,10 @@ and reliable way to wait for all tasks in the group to finish. Create a task in this task group. The signature matches that of :func:`asyncio.create_task`. + .. versionchanged:: 3.13 + + Close the given coroutine if the task group is not active. + Example:: async def main(): diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 96c8aee5da075a..350a22ffba21ef 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -185,6 +185,11 @@ Other Language Changes (Contributed by Sebastian Pipping in :gh:`115623`.) +* When :func:`asyncio.TaskGroup.create_task` is called on an inactive + :class:`asyncio.TaskGroup`, the given coroutine will be closed (which + prevents a :exc:`RuntimeWarning`). + + (Contributed by Arthur Tacca and Jason Zhang in :gh:`115957`.) New Modules =========== diff --git a/Lib/asyncio/taskgroups.py b/Lib/asyncio/taskgroups.py index f322b1f6653f6a..57f01230159319 100644 --- a/Lib/asyncio/taskgroups.py +++ b/Lib/asyncio/taskgroups.py @@ -154,10 +154,13 @@ def create_task(self, coro, *, name=None, context=None): Similar to `asyncio.create_task`. """ if not self._entered: + coro.close() raise RuntimeError(f"TaskGroup {self!r} has not been entered") if self._exiting and not self._tasks: + coro.close() raise RuntimeError(f"TaskGroup {self!r} is finished") if self._aborting: + coro.close() raise RuntimeError(f"TaskGroup {self!r} is shutting down") if context is None: task = self._loop.create_task(coro, name=name) diff --git a/Lib/test/test_asyncio/test_taskgroups.py b/Lib/test/test_asyncio/test_taskgroups.py index 7a18362b54e469..91959394d23e2c 100644 --- a/Lib/test/test_asyncio/test_taskgroups.py +++ b/Lib/test/test_asyncio/test_taskgroups.py @@ -738,10 +738,7 @@ async def coro2(g): await asyncio.sleep(1) except asyncio.CancelledError: with self.assertRaises(RuntimeError): - g.create_task(c1 := coro1()) - # We still have to await c1 to avoid a warning - with self.assertRaises(ZeroDivisionError): - await c1 + g.create_task(coro1()) with self.assertRaises(ExceptionGroup) as cm: async with taskgroups.TaskGroup() as g: @@ -803,16 +800,12 @@ async def test_taskgroup_finished(self): coro = asyncio.sleep(0) with self.assertRaisesRegex(RuntimeError, "is finished"): tg.create_task(coro) - # We still have to await coro to avoid a warning - await coro async def test_taskgroup_not_entered(self): tg = taskgroups.TaskGroup() coro = asyncio.sleep(0) with self.assertRaisesRegex(RuntimeError, "has not been entered"): tg.create_task(coro) - # We still have to await coro to avoid a warning - await coro async def test_taskgroup_without_parent_task(self): tg = taskgroups.TaskGroup() @@ -821,8 +814,16 @@ async def test_taskgroup_without_parent_task(self): coro = asyncio.sleep(0) with self.assertRaisesRegex(RuntimeError, "has not been entered"): tg.create_task(coro) - # We still have to await coro to avoid a warning - await coro + + def test_coro_closed_when_tg_closed(self): + async def run_coro_after_tg_closes(): + async with taskgroups.TaskGroup() as tg: + pass + coro = asyncio.sleep(0) + with self.assertRaisesRegex(RuntimeError, "is finished"): + tg.create_task(coro) + loop = asyncio.get_event_loop() + loop.run_until_complete(run_coro_after_tg_closes()) if __name__ == "__main__":