Reputation: 39546
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)
Upvotes: 4
Views: 1773
Reputation: 1121644
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.
Upvotes: 3
Reputation: 280426
When a context manager's __exit__
method needs to indicate that an exception should be propagated, it does not do so by propagating the exception out of __exit__
. It does so by returning a falsy value.
@contextmanager
provides a different API to you, but it still needs to convert that to the normal context manager API to show to Python. When the exception passed to a @contextmanager
context manager's __exit__
is propagated out of the wrapped function uncaught, @contextmanager
catches the exception and returns a falsy value from __exit__
. That means that this call:
self._cm_obj.__exit__(exc_type, exc_val, exc_tb)
does not raise an exception, because it's caught by @contextmanager
.
You can see the code where @contextmanager
catches the exception in Lib/contextlib.py
try:
self.gen.throw(type, value, traceback)
raise RuntimeError("generator didn't stop after throw()")
except StopIteration, exc:
# Suppress the exception *unless* it's the same exception that
# was passed to throw(). This prevents a StopIteration
# raised inside the "with" statement from being suppressed
return exc is not value
except:
# only re-raise if it's *not* the exception that was
# passed to throw(), because __exit__() must not raise
# an exception unless __exit__() itself failed. But throw()
# has to raise the exception to signal propagation, so this
# fixes the impedance mismatch between the throw() protocol
# and the __exit__() protocol.
#
if sys.exc_info()[1] is not value:
raise
Upvotes: 2