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