Skip to content

Commit ce0ae1d

Browse files
authored
gh-115957: Close coroutine if TaskGroup.create_task() raises an error (#116009)
1 parent 7114cf2 commit ce0ae1d

File tree

5 files changed

+42
-16
lines changed

5 files changed

+42
-16
lines changed

Doc/library/asyncio-task.rst

+7
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,13 @@ and reliable way to wait for all tasks in the group to finish.
334334

335335
Create a task in this task group.
336336
The signature matches that of :func:`asyncio.create_task`.
337+
If the task group is inactive (e.g. not yet entered,
338+
already finished, or in the process of shutting down),
339+
we will close the given ``coro``.
340+
341+
.. versionchanged:: 3.13
342+
343+
Close the given coroutine if the task group is not active.
337344

338345
Example::
339346

Doc/whatsnew/3.13.rst

+6
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,12 @@ Other Language Changes
185185

186186
(Contributed by Sebastian Pipping in :gh:`115623`.)
187187

188+
* When :func:`asyncio.TaskGroup.create_task` is called on an inactive
189+
:class:`asyncio.TaskGroup`, the given coroutine will be closed (which
190+
prevents a :exc:`RuntimeWarning` about the given coroutine being
191+
never awaited).
192+
193+
(Contributed by Arthur Tacca and Jason Zhang in :gh:`115957`.)
188194

189195
New Modules
190196
===========

Lib/asyncio/taskgroups.py

+3
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,13 @@ def create_task(self, coro, *, name=None, context=None):
154154
Similar to `asyncio.create_task`.
155155
"""
156156
if not self._entered:
157+
coro.close()
157158
raise RuntimeError(f"TaskGroup {self!r} has not been entered")
158159
if self._exiting and not self._tasks:
160+
coro.close()
159161
raise RuntimeError(f"TaskGroup {self!r} is finished")
160162
if self._aborting:
163+
coro.close()
161164
raise RuntimeError(f"TaskGroup {self!r} is shutting down")
162165
if context is None:
163166
task = self._loop.create_task(coro, name=name)

Lib/test/test_asyncio/test_taskgroups.py

+25-16
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import contextlib
88
from asyncio import taskgroups
99
import unittest
10+
import warnings
1011

1112
from test.test_asyncio.utils import await_without_task
1213

@@ -738,10 +739,7 @@ async def coro2(g):
738739
await asyncio.sleep(1)
739740
except asyncio.CancelledError:
740741
with self.assertRaises(RuntimeError):
741-
g.create_task(c1 := coro1())
742-
# We still have to await c1 to avoid a warning
743-
with self.assertRaises(ZeroDivisionError):
744-
await c1
742+
g.create_task(coro1())
745743

746744
with self.assertRaises(ExceptionGroup) as cm:
747745
async with taskgroups.TaskGroup() as g:
@@ -797,22 +795,25 @@ async def test_taskgroup_double_enter(self):
797795
pass
798796

799797
async def test_taskgroup_finished(self):
800-
tg = taskgroups.TaskGroup()
801-
async with tg:
802-
pass
803-
coro = asyncio.sleep(0)
804-
with self.assertRaisesRegex(RuntimeError, "is finished"):
805-
tg.create_task(coro)
806-
# We still have to await coro to avoid a warning
807-
await coro
798+
async def create_task_after_tg_finish():
799+
tg = taskgroups.TaskGroup()
800+
async with tg:
801+
pass
802+
coro = asyncio.sleep(0)
803+
with self.assertRaisesRegex(RuntimeError, "is finished"):
804+
tg.create_task(coro)
805+
806+
# Make sure the coroutine was closed when submitted to the inactive tg
807+
# (if not closed, a RuntimeWarning should have been raised)
808+
with warnings.catch_warnings(record=True) as w:
809+
await create_task_after_tg_finish()
810+
self.assertEqual(len(w), 0)
808811

809812
async def test_taskgroup_not_entered(self):
810813
tg = taskgroups.TaskGroup()
811814
coro = asyncio.sleep(0)
812815
with self.assertRaisesRegex(RuntimeError, "has not been entered"):
813816
tg.create_task(coro)
814-
# We still have to await coro to avoid a warning
815-
await coro
816817

817818
async def test_taskgroup_without_parent_task(self):
818819
tg = taskgroups.TaskGroup()
@@ -821,8 +822,16 @@ async def test_taskgroup_without_parent_task(self):
821822
coro = asyncio.sleep(0)
822823
with self.assertRaisesRegex(RuntimeError, "has not been entered"):
823824
tg.create_task(coro)
824-
# We still have to await coro to avoid a warning
825-
await coro
825+
826+
def test_coro_closed_when_tg_closed(self):
827+
async def run_coro_after_tg_closes():
828+
async with taskgroups.TaskGroup() as tg:
829+
pass
830+
coro = asyncio.sleep(0)
831+
with self.assertRaisesRegex(RuntimeError, "is finished"):
832+
tg.create_task(coro)
833+
loop = asyncio.get_event_loop()
834+
loop.run_until_complete(run_coro_after_tg_closes())
826835

827836

828837
if __name__ == "__main__":
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
When ``asyncio.TaskGroup.create_task`` is called on an inactive ``asyncio.TaskGroup``, the given coroutine will be closed (which prevents a ``RuntimeWarning``).

0 commit comments

Comments
 (0)