Exception information isn't passed through to context.with_resource managed resources
When writing a simple tool with a bunch of commands updating a database, I was using a context manager in each command that would begin a transaction, and then commit it on success, or rollback on error.
Initially, I thought I'd be able to avoid some boilerplate on every subcommand by adding the session as a managed resource via ctx.with_resource(context_manager). However, while this works in the success case, it will also commit the transaction if an unexpected exception occurs, since the exception information doesn't seem to be being passed through to the context manager.
Eg. given the below context manager:
class TestContextManager:
def __enter__(self):
print("ENTER")
def __exit__(self, exc_type, exc_val, traceback):
print("EXIT", exc_type, exc_val, traceback)
Using it in a regular context manager via:
with TestContextManager():
raise Exception()
will correctly print the exc_type, exc_val and traceback parameters when an exception occurs. But using it via:
@click.group()
@click.pass_context
def cli(ctx):
ctx.obj = ctx.with_resource(TestContextManager())
These will always be None, and so any context manager that needs to act differently on the occurrence of an exception vs success will always treat this as success. (And likewise, try/except blocks will never trigger in function-style ones using the @contextmanager decorator.)
Would it be possible for the exception information to be passed through to managed resources so that context managers that treat success/failure cases differently operate correctly?
Environment:
- Python version: 3.10.9
- Click version: 8.1.3
I came across this issue too and asked about it on StackOverflow here (in my case, I'd like to print exception tracebacks to a file, and prevent them from printing to stderr). The solution offered was to override Context.__exit__ so it forwards on the exception information:
class MyContext(click.Context):
def __exit__(self, exc_type, exc_value, tb):
self._depth -= 1
if self._depth == 0:
self._exit_stack.__exit__(exc_type, exc_value, tb)
self._exit_stack = contextlib.ExitStack()
click.core.pop_context()
cli.context_class = MyContext
For reference, the current implementation of Context.__exit__ is essentially:
def __exit__(self, exc_type, exc_value, tb):
self._depth -= 1
if self._depth == 0:
self._exit_stack.close() # equivalent to: `self._exit_stack.__exit__(None, None, None)`
self._exit_stack = ExitStack()
pop_context()
So it seems this would be a minor change to fix; although I have no idea if it would have undesirable side effects.