-
-
Notifications
You must be signed in to change notification settings - Fork 30k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
bpo-44594: fix (Async)ExitStack handling of __context__ #27089
Conversation
Make enter_context(foo()) / enter_async_context(foo()) equivalent to `[async] with foo()` regarding __context__ when an exception is raised. Previously exceptions would be caught and re-raised with the wrong context when explicitly overriding __context__ with None. TODO: unit tests
00cd48f
to
6b94522
Compare
if exc_context is old_exc: | ||
if exc_context is None or exc_context is old_exc: | ||
# Context is already set correctly (see issue 20317) | ||
return | ||
if exc_context is None or exc_context is frame_exc: | ||
if exc_context is frame_exc: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that an exception raised with __context__
of None
will already have __context__
populated before ExitStack receives it. Therefore this existing handling of None
was not necessary and not handling the intended case.
When __context__
is None
here, it means that it was explicitly set this way at the raise
site (via a finally block). So we should not override it to old_exc
. This is in line with the behavior of actual nested with
statements.
@ncoghlan I noticed you've stewarded the exception chaining in ExitStack, would you have time to review this PR? noting:
|
comments from graingert:
|
This has apparently been causing a lot of pain for some trio users: python-trio/trio#2001 This is some very tricky code, but having finally found the spoons to read it over carefully, I think @belm0's analysis is right. To summarize: However, the cleanup functions are actually all called directly from So the job of However, if |
BTW, this whole strategy of fixing up the exception metadata after the fact is inherently heuristic, and has some very subtle corner cases. For example, consider this cleanup function: def cleanup():
try:
raise OSError # the original exception
except OSError as exc:
raise MyLibError from exc # a wrapper exception, whose __cause__ points to the original exception Here, we want to get a display like: ...Except this case actually works! The reason is that the interpreter will also set OTOH, if we slightly tweak the cleanup function, in a way that doesn't look like it should affect anything...
...then now It might be better to trick the interpreter into using the correct saved_ctx = pending_raise.__context__
saved_tb = pending_raise.__traceback__
try:
raise pending_raise
except type(pending_raise):
pending_raise.__context__ = saved_ctx
pending_raise.__traceback__ = saved_tb
# invoke the next __exit__ callback
if cb(*exc_details):
suppressed_exc = True
pending_raise = False
exc_details = (None, None, None) ...And also a separate branch to handle the case where .......ANYWAY though, none of that actually affects this PR, it's just musing on further possible improvements while I happen to have this swapped into my brain. |
Filed https://bugs.python.org/issue45361 about a possible improvement to fix this once and for all |
Sorry, @belm0 and @njsmith, I could not cleanly backport this to |
Sorry @belm0 and @njsmith, I had trouble checking out the |
…ngh-27089) * bpo-44594: fix (Async)ExitStack handling of __context__ Make enter_context(foo()) / enter_async_context(foo()) equivalent to `[async] with foo()` regarding __context__ when an exception is raised. Previously exceptions would be caught and re-raised with the wrong context when explicitly overriding __context__ with None.. (cherry picked from commit e6d1aa1) Co-authored-by: John Belmonte <john@neggie.net>
…gh-27089) * bpo-44594: fix (Async)ExitStack handling of __context__ Make enter_context(foo()) / enter_async_context(foo()) equivalent to `[async] with foo()` regarding __context__ when an exception is raised. Previously exceptions would be caught and re-raised with the wrong context when explicitly overriding __context__ with None.. (cherry picked from commit e6d1aa1) Co-authored-by: John Belmonte <john@neggie.net>
) (GH-28730) Make enter_context(foo()) / enter_async_context(foo()) equivalent to `[async] with foo()` regarding __context__ when an exception is raised. Previously exceptions would be caught and re-raised with the wrong context when explicitly overriding __context__ with None.. (cherry picked from commit e6d1aa1) Co-authored-by: John Belmonte <john@neggie.net> Automerge-Triggered-By: GH:njsmith
…) (GH-28731) Make enter_context(foo()) / enter_async_context(foo()) equivalent to `[async] with foo()` regarding __context__ when an exception is raised. Previously exceptions would be caught and re-raised with the wrong context when explicitly overriding __context__ with None.. (cherry picked from commit e6d1aa1) Co-authored-by: John Belmonte <john@neggie.net> Automerge-Triggered-By: GH:njsmith
Make
enter_[async_]context(foo())
equivalent to[async] with foo()
regarding__context__
when an exception is raised.Previously, exceptions would be caught and re-raised with the wrong context when explicitly overriding
__context__
withNone
. In other words, in usage like the following where we explicitly try to clear the context toNone
on an exception in flight, (Async)ExitStack will overwrite that with the context of the old exception, while the real (async)with
will not:This bug seems to have existed from the first version of (Async)ExitStack. Namely, the
_fix_exception_context()
code was trying to substitute a suitable value for__context__
when the exception was raised with a context ofNone
. But this handling was not needed, since raising with__context__
ofNone
will be replaced with a suitable context by Python's machinery before (Async)ExitStack accesses the exception viasys.exc_info()
. Since this handling in (Async)ExitStack had no purpose and interferes with the valid use case of clearing__context__
on an in-flight exception, this PR removes it.Included are unit tests verifying that (Async)ExitStack now match the behavior of (async)
with
when clearing the context of an in-flight exception toNone
.TODO:
https://bugs.python.org/issue44594