Description
Bug report
Bug description:
Consider the following code:
import asyncio async def wait_and_raise(): await asyncio.sleep(0.5) raise RuntimeError(1) async def wait_and_start(tg): try: await asyncio.sleep(1) finally: try: tg.create_task(asyncio.sleep(1)) except RuntimeError as e: print(f"wait_and_start() caught {e!r}") async def main(): try: async with asyncio.TaskGroup() as tg: tg.create_task(wait_and_start(tg)) tg.create_task(wait_and_raise()) except Exception as e: print(f"main() caught {e!r}") try: tg.create_task(asyncio.sleep(1)) except RuntimeError as e: print(f"main() caught {e!r}") asyncio.run(main())
This gives the following output
wait_and_start() caught RuntimeError('TaskGroup <TaskGroup tasks=1 errors=1 cancelling> is shutting down') C:\code\taskgrouptest.py:16: RuntimeWarning: coroutine 'sleep' was never awaited print(f"wait_and_start() caught {e!r}") RuntimeWarning: Enable tracemalloc to get the object allocation traceback main() caught ExceptionGroup('unhandled errors in a TaskGroup', [RuntimeError(1)]) main() caught RuntimeError('TaskGroup <TaskGroup cancelling> is finished') C:\code\taskgrouptest.py:29: RuntimeWarning: coroutine 'sleep' was never awaited print(f"main() caught {e!r}") RuntimeWarning: Enable tracemalloc to get the object allocation traceback
Arguably, when you call tg.create_task()
on a task group that is shutting down or has finished, the calling code "knows" about the error because it gets a RuntimeError
exception (as you can see above), so there is no need to get a warning about a coroutine that was not awaited. So, when a TaskGroup
encounters this situation, it should close the coroutine before raising the error.
The other argument would be that this still represents a design mistake so should still get the warning. I can see both points of view but I'm raising this issue so a conscious decision can be made.
For comparison, when you do this on a Trio Nursery or AnyIO TaskGroup that has already closed, a coroutine never even gets created in the first place, because you use a different syntax (nursery.start_soon(foo, 1, 2)
rather than tg.create_task(foo(1, 2))
), so it's a lot like if asyncio were to close the coroutine. The situation is a bit different for a nursery that is shutting down: then it runs till the first (unshielded) await
and is cancelled at that point, which is possible because they use level-based cancellation rather than edge-based cancellation.
CPython versions tested on:
3.12
Operating systems tested on:
Windows
Linked PRs
Metadata
Metadata
Assignees
Projects
Status