Re-raise Python exception and preserve stack trace - python

I'm trying to catch an exception in a thread and re-raise it in the main thread:
import threading
import sys
class FailingThread(threading.Thread):
def run(self):
try:
raise ValueError('x')
except ValueError:
self.exc_info = sys.exc_info()
failingThread = FailingThread()
failingThread.start()
failingThread.join()
print failingThread.exc_info
raise failingThread.exc_info[1]
This basically works and yields the following output:
(<type 'exceptions.ValueError'>, ValueError('x',), <traceback object at 0x1004cc320>)
Traceback (most recent call last):
File "test.py", line 16, in <module>
raise failingThread.exc_info[1]
However, the source of the exception points to line 16, where the re-raise occurred. The original exception comes from line 7. How do I have to modify the main thread so that the output reads:
Traceback (most recent call last):
File "test.py", line 7, in <module>

In Python 2 you need to use all three arguments to raise:
raise failingThread.exc_info[0], failingThread.exc_info[1], failingThread.exc_info[2]
passing the traceback object in as the third argument preserves the stack.
From help('raise'):
If a third object is present and not None, it must be a traceback
object (see section The standard type hierarchy), and it is
substituted instead of the current location as the place where the
exception occurred. If the third object is present and not a
traceback object or None, a TypeError exception is raised. The
three-expression form of raise is useful to re-raise an exception
transparently in an except clause, but raise with no expressions
should be preferred if the exception to be re-raised was the most
recently active exception in the current scope.
In this particular case you cannot use the no expression version.
For Python 3 (as per the comments):
raise failingThread.exc_info[1].with_traceback(failingThread.exc_info[2])
or you can simply chain the exceptions using raise ... from ... but that raises a chained exception with the original context attached in the cause attribute and that may or may not be what you want.

This code snippet works in both python 2 & 3:
1 try:
----> 2 raise KeyError('Default key error message')
3 except KeyError as e:
4 e.args = ('Custom message when get re-raised',) #The comma is not a typo, it's there to indicate that we're replacing the tuple that e.args pointing to with another tuple that contain the custom message.
5 raise

Related

Get only last traceback without using raise from None?

I have a common situation in my projects, where I use custom Exceptions to wrap built in exception scenarios, like:
# meaningless here but in other situations its useful...
try:
5/0
except ZeroDivisionError as e:
raise MyCustomException(msg=str(e))
along with a universal exception handler that looks like this:
#app.errorhandler(Exception) # this decorator is flask related - ignore it
def handle_error(error):
if isinstance(error, MyCustomException):
# some code
# I only want MyCustomException traceback
else:
# some other code
exc_stack = traceback.format_exc(limit=5)
The known issue here is that I get both exception tracebacks, whereas in if-case I want only the last one.
There are two solutions in this problem as far as I know.
First workaround (use from None):
try:
5/0
except ZeroDivisionError as e:
raise MyCustomException(msg=str(e)) from None # python3
Second workaround (call the traceback before raising the second exception)
try:
5/0
except ZeroDivisionError as e:
tracb = traceback.format_exc(limit=5)
raise MyCustomException(msg=str(e), tracb_msg=tracb)
No need to call traceback.format_exc() in exception handler, just use the tracb_msg passed to the instance. Obviously first workaround is simpler.
My Problem:
Both of these approaches re-appear (repeating code/trick) dozens of times inside the code, every time I raise MyCustomException. Has anyone come up with a trick to handle this once inside the handler function?
Use the __suppress_context__ attribute to disable context printing.
According to the docs, using raise MyCustomException(foo) from bar sets __cause__ to bar, and __context__ to the original exception (the implicitly chained exception).
An implicitly chained exception in __context__ is shown only if __cause__ is None and __suppress_context__ is false.
Here's an example:
# Declare an exception that never shows context.
class MyCustomException(Exception):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__suppress_context__ = True
try:
1/0
except ZeroDivisionError as e:
raise MyCustomException(str(e))
Here's the output I get:
Traceback (most recent call last):
File "/home/don/workspace/scratch/scratch.py", line 12, in <module>
raise MyCustomException(str(e))
MyCustomException: division by zero
Here's the output if I set __suppress_context__ to False:
Traceback (most recent call last):
File "/home/don/workspace/scratch/scratch.py", line 10, in <module>
1/0
ZeroDivisionError: division by zero
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/home/don/workspace/scratch/scratch.py", line 12, in <module>
raise MyCustomException(str(e))
MyCustomException: division by zero

Raise an exception from a higher level, a la warnings

In the module warnings (https://docs.python.org/3.5/library/warnings.html) there is the ability to raise a warning that appears to come from somewhere earlier in the stack:
warnings.warn('This is a test', stacklevel=2)
Is there an equivalent for raising errors? I know I can raise an error with an alternative traceback, but I can't create that traceback within the module since it needs to come from earlier. I imagine something like:
tb = magic_create_traceback_right_here()
raise ValueError('This is a test').with_traceback(tb.tb_next)
The reason is that I am developing a module that has a function module.check_raise that I want to raise an error that appears to originate from where the function is called. If I raise an error within the module.check_raise function, it appears to originate from within module.check_raise, which is undesired.
Also, I've tried tricks like raising a dummy exception, catching it, and passing the traceback along, but somehow the tb_next becomes None. I'm out of ideas.
Edit:
I would like the output of this minimal example (called tb2.py):
import check_raise
check_raise.raise_if_string_is_true('True')
to be only this:
Traceback (most recent call last):
File "tb2.py", line 10, in <module>
check_raise.raise_if_string_is_true(string)
RuntimeError: An exception was raised.
I can't believe I am posting this
By doing this you are going against the zen.
Special cases aren't special enough to break the rules.
But if you insist here is your magical code.
check_raise.py
import sys
import traceback
def raise_if_string_is_true(string):
if string == 'true':
#the frame that called this one
f = sys._getframe().f_back
#the most USELESS error message ever
e = RuntimeError("An exception was raised.")
#the first line of an error message
print('Traceback (most recent call last):',file=sys.stderr)
#the stack information, from f and above
traceback.print_stack(f)
#the last line of the error
print(*traceback.format_exception_only(type(e),e),
file=sys.stderr, sep="",end="")
#exit the program
#if something catches this you will cause so much confusion
raise SystemExit(1)
# SystemExit is the only exception that doesn't trigger an error message by default.
This is pure python, does not interfere with sys.excepthook and even in a try block it is not caught with except Exception: although it is caught with except:
test.py
import check_raise
check_raise.raise_if_string_is_true("true")
print("this should never be printed")
will give you the (horribly uninformative and extremely forged) traceback message you desire.
Tadhgs-MacBook-Pro:Documents Tadhg$ python3 test.py
Traceback (most recent call last):
File "test.py", line 3, in <module>
check_raise.raise_if_string_is_true("true")
RuntimeError: An exception was raised.
Tadhgs-MacBook-Pro:Documents Tadhg$
If I understand correctly, you would like the output of this minimal example:
def check_raise(function):
try:
return function()
except Exception:
raise RuntimeError('An exception was raised.')
def function():
1/0
check_raise(function)
to be only this:
Traceback (most recent call last):
File "tb2.py", line 10, in <module>
check_raise(function)
RuntimeError: An exception was raised.
In fact, it's a lot more output; there is exception chaining, which could be dealt with by handling the RuntimeError immediately, removing its __context__, and re-raising it, and there is another line of traceback for the RuntimeError itself:
File "tb2.py", line 5, in check_raise
raise RuntimeError('An exception was raised.')
As far as I can tell, it is not possible for pure Python code to substitute the traceback of an exception after it was raised; the interpreter has control of adding to it but it only exposes the current traceback whenever the exception is handled. There is no API (not even when using tracing functions) for passing your own traceback to the interpreter, and traceback objects are immutable (this is what's tackled by that Jinja hack involving C-level stuff).
So further assuming that you're interested in the shortened traceback not for further programmatic use but only for user-friendly output, your best bet will be an excepthook that controls how the traceback is printed to the console. For determining where to stop printing, a special local variable could be used (this is a bit more robust than limiting the traceback to its length minus 1 or such). This example requires Python 3.5 (for traceback.walk_tb):
import sys
import traceback
def check_raise(function):
__exclude_from_traceback_from_here__ = True
try:
return function()
except Exception:
raise RuntimeError('An exception was raised.')
def print_traceback(exc_type, exc_value, tb):
for i, (frame, lineno) in enumerate(traceback.walk_tb(tb)):
if '__exclude_from_traceback_from_here__' in frame.f_code.co_varnames:
limit = i
break
else:
limit = None
traceback.print_exception(
exc_type, exc_value, tb, limit=limit, chain=False)
sys.excepthook = print_traceback
def function():
1/0
check_raise(function)
This is the output now:
Traceback (most recent call last):
File "tb2.py", line 26, in <module>
check_raise(function)
RuntimeError: An exception was raised.
EDIT: The previous version did not provide quotes or explanations.
I suggest referring to PEP 3134 which states in the Motivation:
Sometimes it can be useful for an exception handler to intentionally
re-raise an exception, either to provide extra information or to
translate an exception to another type. The __cause__ attribute
provides an explicit way to record the direct cause of an exception.
When an Exception is raised with a __cause__ attribute the traceback message takes the form of:
Traceback (most recent call last):
<CAUSE TRACEBACK>
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
<MAIN TRACEBACK>
To my understanding this is exactly what you are trying to accomplish; clearly indicate that the reason for the error is not your module but somewhere else. If you are instead trying to omit information to the traceback like your edit suggests then the rest of this answer won't do you any good.
Just a note on syntax:
The __cause__ attribute on exception objects is always initialized
to None. It is set by a new form of the 'raise' statement:
raise EXCEPTION from CAUSE
which is equivalent to:
exc = EXCEPTION
exc.__cause__ = CAUSE
raise exc
so the bare minimum example would be something like this:
def function():
int("fail")
def check_raise(function):
try:
function()
except Exception as original_error:
err = RuntimeError("An exception was raised.")
raise err from original_error
check_raise(function)
which gives an error message like this:
Traceback (most recent call last):
File "/PATH/test.py", line 7, in check_raise
function()
File "/PATH/test.py", line 3, in function
int("fail")
ValueError: invalid literal for int() with base 10: 'fail'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/PATH/test.py", line 12, in <module>
check_raise(function)
File "/PATH/test.py", line 10, in check_raise
raise err from original_error
RuntimeError: An exception was raised.
However the first line of the cause is the statement in the try block of check_raise:
File "/PATH/test.py", line 7, in check_raise
function()
so before raising err it may (or may not) be desirable to remove the outer most traceback frame from original_error:
except Exception as original_error:
err = RuntimeError("An exception was raised.")
original_error.__traceback__ = original_error.__traceback__.tb_next
raise err from original_error
This way the only line in the traceback that appears to come from check_raise is the very last raise statement which cannot be omitted with pure python code although depending on how informative the message is you can make it very clear that your module was not the cause of the problem:
err = RuntimeError("""{0.__qualname__} encountered an error during call to {1.__module__}.{1.__name__}
the traceback for the error is shown above.""".format(function,check_raise))
The advantage to raising exception like this is that the original Traceback message is not lost when the new error is raised, which means that a very complex series of exceptions can be raised and python will still display all the relevant information correctly:
def check_raise(function):
try:
function()
except Exception as original_error:
err = RuntimeError("""{0.__qualname__} encountered an error during call to {1.__module__}.{1.__name__}
the traceback for the error is shown above.""".format(function,check_raise))
original_error.__traceback__ = original_error.__traceback__.tb_next
raise err from original_error
def test_chain():
check_raise(test)
def test():
raise ValueError
check_raise(test_chain)
gives me the following error message:
Traceback (most recent call last):
File "/Users/Tadhg/Documents/test.py", line 16, in test
raise ValueError
ValueError
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/Users/Tadhg/Documents/test.py", line 13, in test_chain
check_raise(test)
File "/Users/Tadhg/Documents/test.py", line 10, in check_raise
raise err from original_error
RuntimeError: test encountered an error during call to __main__.check_raise
the traceback for the error is shown above.
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/Users/Tadhg/Documents/test.py", line 18, in <module>
check_raise(test_chain)
File "/Users/Tadhg/Documents/test.py", line 10, in check_raise
raise err from original_error
RuntimeError: test_chain encountered an error during call to __main__.check_raise
the traceback for the error is shown above.
Yes it is long but it is significantly more informative then:
Traceback (most recent call last):
File "/Users/Tadhg/Documents/test.py", line 18, in <module>
check_raise(test_chain)
RuntimeError: An exception was raised.
not to mention that the original error is still usable even if the program doesn't end:
import traceback
def check_raise(function):
...
def fail():
raise ValueError
try:
check_raise(fail)
except RuntimeError as e:
cause = e.__cause__
print("check_raise failed because of this error:")
traceback.print_exception(type(cause), cause, cause.__traceback__)
print("and the program continues...")
I understand 'Don't do this'. On the other hand, there may be some special use cases i believe. I'm generating own errors (just deleting some defined frames...) this way
def get_traceback_with_removed_frames_by_line_string(lines):
"""In traceback call stack, it is possible to remove particular level defined by some line content.
Args:
lines (list): Line in call stack that we want to hide.
Returns:
string: String traceback ready to be printed.
"""
exc = trcbck.TracebackException(*sys.exc_info())
for i in exc.stack[:]:
if i.line in lines:
exc.stack.remove(i)
return "".join(exc.format())
I return just string.
If there is concrete function that is raising, you can add it to ignored frames.
Though have in mind, that if you hide something, somebody may not understand why is something happening...
My use case was to hide only first level - decorator from my library that was decorating all user functions in framework, so error from user side was on level 1.

exceptions must be old-style classes or derived from BaseException, not NoneType

On executing below code I get below error if it fails to get firefox profile/webdriver for some reason:
exceptions must be old-style classes or derived from BaseException, not NoneType
I want to understand why this error is displayed in this case:
self.error = 0
self.profile, profileErrStatus = self.GetFireFoxProfile(path)
if self.profile:
self.driver, driverErrStatus = self.GetFireFoxWebDriver(self.profile)
if self.driver:
else:
print('Failed to get Firefox Webdriver:%s'%(str(sys.exc_info()[0])))
raise
else:
print('Failed to get Firefox Profile:%s'%(str(sys.exc_info()[0])))
raise
This is because you are using raise without providing an exception type or instance.
According to the documentation:
The sole argument to raise indicates the exception to be raised. This
must be either an exception instance or an exception class (a class
that derives from Exception).
Demo:
>>> raise
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: exceptions must be old-style classes or derived from BaseException, not NoneType
>>> raise ValueError('Failed to get Firefox Webdriver')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: Failed to get Firefox Webdriver
Note that raise without arguments can be used inside an except block to re-raise an exception.
FYI, on python3, it would raise a RuntimeError instead:
>>> raise
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
RuntimeError: No active exception to reraise
Note that raise without an argument is allowed if you are in a catch block with an exception currently handled:
If you need to determine whether an exception was raised but don’t intend to handle it, a simpler form of the raise statement allows you to re-raise the exception:
>>> try:
... raise NameError('HiThere')
... except NameError:
... print 'An exception flew by!'
... raise
...
An exception flew by!
Traceback (most recent call last):
File "<stdin>", line 2, in ?
NameError: HiThere
(From Raising Exceptions in the documentation.)
Beware, though, that if a method called in the expect block clears the exception info, raise without an argument will result in the exceptions must be… exception again. So explicitly assigning the exception to a variable with except … as is safer:
try:
raise NameError('HiThere')
except NameError as e:
log_and_clear_exception_info('An exception flew by!')
raise e

"raise" at the end of a python function outside "try" or "except" block

What does raise do, if it's not inside a try or except clause, but simply as the last statement in the function?
def foo(self):
try:
# some code that raises an exception
except Exception as e:
pass
# notice that the "raise" is outside
raise
This example prints 1 but not 2 so it must be that the last raise statement simply raises the last thrown exception.
def foo():
try:
raise Exception()
except Exception as e:
pass
print 1
raise
print 2
if __name__ == '__main__':
foo()
Any official documentation for this type of usage pattern?
As Russell said,
A bare raise statement re-raises the last caught exception.
It doesn't matter whether this is happening in a try-except block or not. If there has been a caught exception, then calling raise will re-raise that exception. Otherwise, Python will complain that the previously caught exception is None and raise a TypeError because None is not something that can actually be raised.
As tdelaney said, it doesn't seem to make sense to do this except in an error-handling function. Personally I'd say that it doesn't even belong in an error-handling function, as the raise should still be in the except clause. Someone could use this in an attempt to execute code whether or not an error occurs, but a finally clause is the proper way to do that. Another possibility would be using this as a way to determine if an error occurred while executing the function, but there are much better ways to do that (such as returning an extra value that indicates if/where an error occurred).
A Bare raise reraises the current exception. This usually makes no sense at the end of a function, unless the function is called in an exception:
By itself, the raise is invalid and python throws its own exception
>>> def x():
... raise
>>> x()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in x
TypeError: exceptions must be old-style classes or derived from BaseException, not NoneType
But if called within an exception block, it acts sanely
>>> try:
... int('a')
... except:
... x()
...
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
File "<stdin>", line 2, in <module>
ValueError: invalid literal for int() with base 10: 'a'
>>>
EDIT
This might be a perfectly reasonable thing to do if the function is attempting some sort of recovery. The function could fix what's broken, log a message, trigger the fire extinguishers, etc... and raise if it still thinks the system is in error.
A bare raise statement re-raises the last caught exception. https://docs.python.org/2/tutorial/errors.html#raising-exceptions
From this documentation we can read:
If no expressions are present, raise re-raises the last exception that
was active in the current scope. If no exception is active in the
current scope, a TypeError exception is raised indicating that this is
an error (if running under IDLE, a Queue.Empty exception is raised
instead).
This means that, in the case of your code, if no exception occurs within the try ... except block, then you are forcing the program to raise a TypeError exception to happen.
I had a problem like this where I needed to raise a previously caught exception outside the try/except block if my function didn't return a value. I did a bit of looking around in the sys and traceback modules, but couldn't find a good method to do this, so I just ended up storing the exception outside the block.
def foo():
caught = None
try:
raise Exception
except Exception as e:
caught = e
pass
raise caught
f = foo()
Output
Traceback (most recent call last):
line 13, in <module>
line 10, in foo
line 5, in foo
Exception
Clearly this isn't useful in the above example, but it's pretty useful if you need to try something quite a few times in a loop and re-raise. My specific need was for an HTTP request retry mechanism.
import time
def foo(key):
caught = None
for i in [1, 2, 3, 4, 5]:
try:
return d[key]
except KeyError as e:
caught = e
print(i)
time.sleep(i)
continue
raise caught
d = {"bar": "baz"}
f = foo(key="baz")
Output
1
2
3
4
5
Traceback (most recent call last):
line 19, in <module>
line 15, in foo
line 8, in foo
KeyError: 'baz'

Python "raise from" usage

What's the difference between raise and raise from in Python?
try:
raise ValueError
except Exception as e:
raise IndexError
which yields
Traceback (most recent call last):
File "tmp.py", line 2, in <module>
raise ValueError
ValueError
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "tmp.py", line 4, in <module>
raise IndexError
IndexError
and
try:
raise ValueError
except Exception as e:
raise IndexError from e
which yields
Traceback (most recent call last):
File "tmp.py", line 2, in <module>
raise ValueError
ValueError
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "tmp.py", line 4, in <module>
raise IndexError from e
IndexError
The difference is that when you use from, the __cause__ attribute is set and the message states that the exception was directly caused by. If you omit the from then no __cause__ is set, but the __context__ attribute may be set as well, and the traceback then shows the context as during handling something else happened.
Setting the __context__ happens if you used raise in an exception handler; if you used raise anywhere else no __context__ is set either.
If a __cause__ is set, a __suppress_context__ = True flag is also set on the exception; when __suppress_context__ is set to True, the __context__ is ignored when printing a traceback.
When raising from a exception handler where you don't want to show the context (don't want a during handling another exception happened message), then use raise ... from None to set __suppress_context__ to True.
In other words, Python sets a context on exceptions so you can introspect where an exception was raised, letting you see if another exception was replaced by it. You can also add a cause to an exception, making the traceback explicit about the other exception (use different wording), and the context is ignored (but can still be introspected when debugging). Using raise ... from None lets you suppress the context being printed.
See the raise statement documenation:
The from clause is used for exception chaining: if given, the second expression must be another exception class or instance, which will then be attached to the raised exception as the __cause__ attribute (which is writable). If the raised exception is not handled, both exceptions will be printed:
>>> try:
... print(1 / 0)
... except Exception as exc:
... raise RuntimeError("Something bad happened") from exc
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
ZeroDivisionError: int division or modulo by zero
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
RuntimeError: Something bad happened
A similar mechanism works implicitly if an exception is raised inside an exception handler or a finally clause: the previous exception is then attached as the new exception’s __context__ attribute:
>>> try:
... print(1 / 0)
... except:
... raise RuntimeError("Something bad happened")
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
ZeroDivisionError: int division or modulo by zero
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
RuntimeError: Something bad happened
Also see the Built-in Exceptions documentation for details on the context and cause information attached to exceptions.
PEP 3134, Exception Chaining and Embedded Tracebacks introduced chaining of exceptions (implicitly chained with explicit raise EXCEPTION or implicit raise, and explicitly chained with explicit raise EXCEPTION from CAUSE). Here are the relevant paragraphs to understand their usage:
Motivation
During the handling of one exception (exception A), it is possible that another exception (exception B) may occur. In today’s Python (version 2.4), if this happens, exception B is propagated outward and exception A is lost. In order to debug the problem, it is useful to know about both exceptions. The __context__ attribute retains this information automatically.
Sometimes it can be useful for an exception handler to intentionally re-raise an exception, either to provide extra information or to translate an exception to another type. The __cause__ attribute provides an explicit way to record the direct cause of an exception.
[…]
Implicit Exception Chaining
Here is an example to illustrate the __context__ attribute:
def compute(a, b):
try:
a/b
except Exception, exc:
log(exc)
def log(exc):
file = open('logfile.txt') # oops, forgot the 'w'
print >>file, exc
file.close()
Calling compute(0, 0) causes a ZeroDivisionError. The compute() function catches this exception and calls log(exc), but the log() function also raises an exception when it tries to write to a file that wasn’t opened for writing.
In today’s Python, the caller of compute() gets thrown an IOError. The ZeroDivisionError is lost. With the proposed change, the instance of IOError has an additional __context__ attribute that retains the ZeroDivisionError.
[…]
Explicit Exception Chaining
The __cause__ attribute on exception objects is always initialized to None. It is set by a new form of the raise statement:
raise EXCEPTION from CAUSE
which is equivalent to:
exc = EXCEPTION
exc.__cause__ = CAUSE
raise exc
In the following example, a database provides implementations for a few different kinds of storage, with file storage as one kind. The database designer wants errors to propagate as DatabaseError objects so that the client doesn’t have to be aware of the storage-specific details, but doesn’t want to lose the underlying error information.
class DatabaseError(Exception):
pass
class FileDatabase(Database):
def __init__(self, filename):
try:
self.file = open(filename)
except IOError, exc:
raise DatabaseError('failed to open') from exc
If the call to open() raises an exception, the problem will be reported as a DatabaseError, with a __cause__ attribute that reveals the IOError as the original cause.
Enhanced Reporting
The default exception handler will be modified to report chained exceptions. The chain of exceptions is traversed by following the __cause__ and __context__ attributes, with __cause__ taking priority. In keeping with the chronological order of tracebacks, the most recently raised exception is displayed last; that is, the display begins with the description of the innermost exception and backs up the chain to the outermost exception. The tracebacks are formatted as usual, with one of the lines:
The above exception was the direct cause of the following exception:
or
During handling of the above exception, another exception occurred:
between tracebacks, depending whether they are linked by __cause__ or __context__ respectively. Here is a sketch of the procedure:
def print_chain(exc):
if exc.__cause__:
print_chain(exc.__cause__)
print '\nThe above exception was the direct cause...'
elif exc.__context__:
print_chain(exc.__context__)
print '\nDuring handling of the above exception, ...'
print_exc(exc)
[…]
PEP 415, Implement Context Suppression with Exception Attributes then introduced suppression of exception contexts (with explicit raise EXCEPTION from None). Here is the relevant paragraph to understand its usage:
Proposal
A new attribute on BaseException, __suppress_context__, will be introduced. Whenever __cause__ is set, __suppress_context__ will be set to True. In particular, raise exc from cause syntax will set exc.__suppress_context__ to True. Exception printing code will check for that attribute to determine whether context and cause will be printed. __cause__ will return to its original purpose and values.
There is precedence for __suppress_context__ with the print_line_and_file exception attribute.
To summarize, raise exc from cause will be equivalent to:
exc.__cause__ = cause
raise exc
where exc.__cause__ = cause implicitly sets exc.__suppress_context__.
So in PEP 415, the sketch of the procedure given in PEP 3134 becomes the following:
def print_chain(exc):
if exc.__cause__:
print_chain(exc.__cause__)
print '\nThe above exception was the direct cause...'
elif exc.__context__ and not exc.__suppress_context__:
print_chain(exc.__context__)
print '\nDuring handling of the above exception, ...'
print_exc(exc)
The shortest answer. PEP-3134 says it all. raise Exception from e sets the __cause__ filed of the new exception.
A longer answer from the same PEP:
__context__ field would be set implicitly to the original error inside except: block unless told not to with __suppress_context__ = True.
__cause__ is just like context but has to be set explicitly by using from syntax
traceback will always chain when you call raise inside an except block. You can get rid of traceback by a) swallowing an exception except: pass or by messing with sys.exc_info() directly.
The long answer
import traceback
import sys
class CustomError(Exception):
def __init__(self):
super().__init__("custom")
def print_exception(func):
print(f"\n\n\nEXECURTING FUNCTION '{func.__name__}' \n")
try:
func()
except Exception as e:
"Here is result of our actions:"
print(f"\tException type: '{type(e)}'")
print(f"\tException message: '{e}'")
print(f"\tException context: '{e.__context__}'")
print(f"\tContext type: '{type(e.__context__)}'")
print(f"\tException cause: '{e.__cause__}'")
print(f"\tCause type: '{type(e.__cause__)}'")
print("\nTRACEBACKSTART>>>")
traceback.print_exc()
print("<<<TRACEBACKEND")
def original_error_emitter():
x = {}
print(x.does_not_exist)
def vanilla_catch_swallow():
"""Nothing is expected to happen"""
try:
original_error_emitter()
except Exception as e:
pass
def vanilla_catch_reraise():
"""Nothing is expected to happen"""
try:
original_error_emitter()
except Exception as e:
raise e
def catch_replace():
"""Nothing is expected to happen"""
try:
original_error_emitter()
except Exception as e:
raise CustomError()
def catch_replace_with_from():
"""Nothing is expected to happen"""
try:
original_error_emitter()
except Exception as e:
raise CustomError() from e
def catch_reset_trace():
saw_an_error = False
try:
original_error_emitter()
except Exception as e:
saw_an_error = True
if saw_an_error:
raise CustomError()
print("Note: This will print nothing")
print_exception(vanilla_catch_swallow)
print("Note: This will print AttributeError and 1 stack trace")
print_exception(vanilla_catch_reraise)
print("Note: This will print CustomError with no context but 2 stack traces")
print_exception(catch_replace)
print("Note: This will print CustomError with AttributeError context and 2 stack traces")
print_exception(catch_replace_with_from)
print("Note: This will brake traceback chain")
print_exception(catch_reset_trace)
Will result in the following output:
Note: This will print nothing
EXECURTING FUNCTION 'vanilla_catch_swallow'
Note: This will print AttributeError and 1 stack trace
EXECURTING FUNCTION 'vanilla_catch_reraise'
Exception type: '<class 'AttributeError'>'
Exception message: ''dict' object has no attribute 'does_not_exist''
Exception context: 'None'
Context type: '<class 'NoneType'>'
Exception cause: 'None'
Cause type: '<class 'NoneType'>'
TRACEBACKSTART>>>
Traceback (most recent call last):
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 11, in print_exception
func()
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 41, in vanilla_catch_reraise
raise e
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 39, in vanilla_catch_reraise
original_error_emitter()
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 27, in original_error_emitter
print(x.does_not_exist)
AttributeError: 'dict' object has no attribute 'does_not_exist'
<<<TRACEBACKEND
Note: This will print CustomError with no context but 2 stack traces
EXECURTING FUNCTION 'catch_replace'
Exception type: '<class '__main__.CustomError'>'
Exception message: 'custom'
Exception context: ''dict' object has no attribute 'does_not_exist''
Context type: '<class 'AttributeError'>'
Exception cause: 'None'
Cause type: '<class 'NoneType'>'
TRACEBACKSTART>>>
Traceback (most recent call last):
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 46, in catch_replace
original_error_emitter()
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 27, in original_error_emitter
print(x.does_not_exist)
AttributeError: 'dict' object has no attribute 'does_not_exist'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 11, in print_exception
func()
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 48, in catch_replace
raise CustomError()
CustomError: custom
<<<TRACEBACKEND
Note: This will print CustomError with AttributeError context and 2 stack traces
EXECURTING FUNCTION 'catch_replace_with_from'
Exception type: '<class '__main__.CustomError'>'
Exception message: 'custom'
Exception context: ''dict' object has no attribute 'does_not_exist''
Context type: '<class 'AttributeError'>'
Exception cause: ''dict' object has no attribute 'does_not_exist''
Cause type: '<class 'AttributeError'>'
TRACEBACKSTART>>>
Traceback (most recent call last):
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 53, in catch_replace_with_from
original_error_emitter()
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 27, in original_error_emitter
print(x.does_not_exist)
AttributeError: 'dict' object has no attribute 'does_not_exist'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 11, in print_exception
func()
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 55, in catch_replace_with_from
raise CustomError() from e
CustomError: custom
<<<TRACEBACKEND
Note: This will brake traceback chain
EXECURTING FUNCTION 'catch_reset_trace'
Exception type: '<class '__main__.CustomError'>'
Exception message: 'custom'
Exception context: 'None'
Context type: '<class 'NoneType'>'
Exception cause: 'None'
Cause type: '<class 'NoneType'>'
TRACEBACKSTART>>>
Traceback (most recent call last):
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 11, in print_exception
func()
File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 64, in catch_reset_trace
raise CustomError()
CustomError: custom
<<<TRACEBACKEND

Categories

Resources