gh-135736: Fix interaction between TaskGroup and aclose() - #154649
gh-135736: Fix interaction between TaskGroup and aclose()#154649msullivan wants to merge 1 commit into
Conversation
0b648b6 to
5b945dd
Compare
Currently if you have a generator like:
```
async def test():
async with asyncio.TaskGroup() as tg:
async for x in whatever():
yield x
```
If aclose() is called on the generator and GeneratorExit is raised,
the TaskGroup's __aexit__ will raise a BaseExceptionGroup, and that
will be raised from the aclose() call instead of it being swallowed.
Fix this by raising GeneratorExit from __aexit__ when:
1. The body of task group raised it
2. No subtasks raised exceptions
This makes GeneratorExit work without swallowing other exceptions
(like it would if we just added it to _is_base_error).
5b945dd to
147f0f4
Compare
|
A slightly different approach that @1st1 and I discussed would be to add a class: and raise that, wrapping the original GeneratorExit, instead of just raising the original GeneratorExit. This has the advantage that the behavior changes less: a BaseExceptionGroup still gets raised, it's just also now a GeneratorExit. I have an implementation of this and it works fine, but the idea of having to documen this exception for end users seemed unfortunate to me, so I didn't do it for this PR. Any thoughts on this? |
Documentation build overview
|
|
This PR seems to do the same as #154538 |
Huh, very funny that the bug sits around for a year and attracts two competing PRs in the same week. Anyway, I think the approaches and behaviors of the two PRs are quite different, though. I think this approach is preferable, because it doesn't drop exceptions on a GeneratorExit, which is not a particularly exceptional case. (Unlike SystemExit and KeyboardInterrupt) |
Yeah, that's quite important. @kumaraditya303 Yeah, this PR has a better approach. Basically,
IMO this PR is fine. We should salvage the logging part of https://github.com/python/cpython/pull/154538/changes in a separate PR though. |
|
@msullivan I've approved the PR but I'll let it sit for some time before hitting the button in case Kumar or others have more comments. |
| with self.assertRaises(BaseExceptionGroup): | ||
| await fn() |
There was a problem hiding this comment.
| with self.assertRaises(BaseExceptionGroup): | |
| await fn() | |
| with self.assertRaises(BaseExceptionGroup) as cm: | |
| await fn() | |
| self.assertEqual(get_error_types(cm.exception), {GeneratorExit}) |
|
|
||
| .. versionchanged:: 3.16 | ||
|
|
||
| Addition of the special case for :exc:`GeneratorExit`. |
There was a problem hiding this comment.
I think we are backporting this, in that case it doesn't make sense to keep a version changed entry for 3.16
Currently if you have a generator like:
If aclose() is called on the generator and GeneratorExit is raised,
the TaskGroup's
__aexit__will raise a BaseExceptionGroup, and thatwill be raised from the aclose() call instead of it being swallowed.
Fix this by raising GeneratorExit from aexit when:
This makes GeneratorExit work without swallowing other exceptions
(like it would if we just added it to _is_base_error).