I want to replace class’s __enter__
/__exit__
functions with single function decorated as contextlib.contextmanager
, here’s code:
class Test: def __enter__(self): self._cm_obj = self._cm() self._cm_obj.__enter__() def __exit__(self, exc_type, exc_val, exc_tb): try: # Here we first time caught exception, # Pass it to _cm: self._cm_obj.__exit__(exc_type, exc_val, exc_tb) except: # Here we should catch exception reraised by _cm, # but it doesn't happen. raise else: return True @contextmanager def _cm(self): print('enter') try: yield except: # Here we got exception from __exit__ # Reraise it to tell __exit__ it should be raised. raise finally: print('exit') with Test(): raise Exception(123)
When we got exception inside Test’s __exit__
, we pass it to _cm
‘s __exit__
, it works fine, I see exception inside _cm
. But then, when I decide to reraise inside _cm
, it doesn’t happen: after Test’s __exit__
I don’t see exception (and code works wrong, without exception).
Why doesn’t exeprion reraised inside __exit__
?
If it’s normal behavior, could you advice me solution to properly replace __enter__
/__exit__
with contextmanager
function?
Edit:
cm_obj.__exit__
returns None
on exception inside _cm
and True
if exception was suppressed (or not raised at all).
At other side, inside Test.__exit__
we can return None
to propagate current exception or True
to suppress it.
Looks like just returning cm_obj.__exit__
‘s value inside Test.__exit__
do job, this code working as I want:
class Test: def __enter__(self): self._cm_obj = self._cm() self._cm_obj.__enter__() def __exit__(self, exc_type, exc_val, exc_tb): return self._cm_obj.__exit__(exc_type, exc_val, exc_tb) @contextmanager def _cm(self): print('---- enter') try: yield except: raise # comment to suppess exception pass finally: print('---- exit') with Test(): raise Exception(123)
Advertisement
Answer
The exception is not raised in __exit__
, so there is nothing to reraise.
The exception is passed in to the method as arguments, because you can’t raise an exception in another method (other than a generator). You don’t have to raise the exception either, you need to simply return not return a true value for the exception to be propagated. Returning None
should suffice.
From the With Statement Context Managers documentation:
Exit the runtime context related to this object. The parameters describe the exception that caused the context to be exited. If the context was exited without an exception, all three arguments will be
None
.If an exception is supplied, and the method wishes to suppress the exception (i.e., prevent it from being propagated), it should return a true value. Otherwise, the exception will be processed normally upon exit from this method.
Note that
__exit__()
methods should not reraise the passed-in exception; this is the caller’s responsibility.
Bold emphasis mine.
You instead suppress the result of the self._cm_obj.__exit__()
call, and return True
to ensure the exception isn’t raised again:
def __exit__(self, exc_type, exc_val, exc_tb): self._cm_obj.__exit__(exc_type, exc_val, exc_tb) return True
If you didn’t return True
here you’d see the exception re-raised by the with
statement.
You can’t re-raise exceptions that don’t reach the context manager however. If you catch and handle an exception in the code block (inside _cm
for example), then the context manager will never be told about this. Suppressed exceptions stay suppressed.
Note that @contextmanager
can’t change these rules; although it transfers the exception into the generator with generator.throw()
, it also has to catch that same exception if your generator doesn’t handle it. It’ll instead return a false value at that moment, because that is what a contextmanager.__exit__()
method should do.