Description
Bug report
Bug description:
The issue
I would expect that handling exceptions within a contextlib.contextmanager
-created function would work in the same way as other functions and clear the sys.exception()
after an error is handled.
import contextlib
import sys
def p(msg):
print(msg, repr(sys.exception()), sep=": ")
def ctx_gen():
p("before yield")
try:
yield
except:
p("during handling")
p("after handling")
ctx = contextlib.contextmanager(ctx_gen)
with ctx():
1/0
The above example prints:
before yield: None
during handling: ZeroDivisionError('division by zero')
after handling: ZeroDivisionError('division by zero')
Whereas since the error was handled by the except:
block, my expectation was:
before yield: None
during handling: ZeroDivisionError('division by zero')
after handling: None
Just working as designed?
From doing some digging, it seems like this is happening because the exception is still being handled by the _GeneratorContextManager.__exit__
function (added by the @contextlib.contextmanager
decorator) that's driving the ctx_gen
generator, even after the ctg_gen
has handled it.
The following is a very rough approximation of how @contextlib.contextmanager
drives ctx_gen
:
c = ctx_gen()
next(c) # __enter__()
try:
# code inside the with block
1/0
except Exception as e: # __exit__(typ, exc, tb) for e
# throw exception into generator and expect to run to end
try:
c.throw(e)
except StopIteration:
pass
else: # __exit__(None, None, None)
# expect to run to end
try:
next(e)
except StopIteration:
pass
Running the above (including the definitions from the first code block) also prints:
before yield: None
during handling: ZeroDivisionError('division by zero')
after handling: ZeroDivisionError('division by zero')
In the above code, it's more clear that the exception is still being handled by the except Exception as e:
block until c.throw()
returns/raises, which only happens after the generator exits. Therefore the exception is still being handled the entire time ctx_gen
is running all the code after the first yield
.
The fix?
Even though this behavior looks to be technically correct, it still seems unexpected and a bit of an abstraction leak.
Is this something that can be/should be fixed? Or should the behavior just be documented?
CPython versions tested on:
3.8, 3.9, 3.10, 3.11, CPython main branch
Operating systems tested on:
macOS