Python: Exception decorator. How to preserve stacktrace
Asked Answered
A

2

20

I am writing a decorator to apply to a function. It should catch any exception, and then raise a custom exception based on the original exception message. (This is because suds throws a generic WebFault exception, from whose message I parse the exception thrown by the web service and raise a Python exception to mirror it.)

However, when I raise the custom exception in the wrapper, I want the stacktrace to point to the function that raised the original WebFault exception. What I have so far raises the correct exception (it dynamically parses the message and instantiates the exception class). My question: How can I preserve the stacktrace to point to the original function that raised the WebFault exception?

from functools import wraps

def try_except(fn):
        def wrapped(*args, **kwargs):
            try:
                fn(*args, **kwargs)
            except Exception, e:
                parser = exceptions.ExceptionParser()
                raised_exception = parser.get_raised_exception_class_name(e)
                exception = getattr(exceptions, raised_exception)
                raise exception(parser.get_message(e))
        return wraps(fn)(wrapped)
Aquarelle answered 25/1, 2012 at 16:9 Comment(3)
Have you looked at the traceback module? docs.python.org/library/traceback.htmlDissemblance
When wrapping in a decorator use functools.wrapHeptastich
possible duplicate of "Inner exception" (with traceback) in Python?Heptastich
M
43

In Python 2.x, a little-known feature of raise is that it can be used with more than just one argument: the three-argument form of raise takes the exception type, the exception instance and the traceback. You can get at the traceback with sys.exc_info(), which returns (not coincidentally) the exception type, the exception instance and the traceback.

(The reason this treats the exception type and the exception instance as two separate arguments is an artifact from the days before exception classes.)

So:

import sys

class MyError(Exception):
    pass

def try_except(fn):
    def wrapped(*args, **kwargs):
        try:
            return fn(*args, **kwargs)
        except Exception, e:
            et, ei, tb = sys.exc_info()
            raise MyError, MyError(e), tb
    return wrapped

def bottom():
   1 / 0

@try_except
def middle():
   bottom()

def top():
   middle()

>>> top()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "tmp.py", line 24, in top
    middle()
  File "tmp.py", line 10, in wrapped
    return fn(*args, **kwargs)
  File "tmp.py", line 21, in middle
    bottom()
  File "tmp.py", line 17, in bottom
    1 / 0
__main__.MyError: integer division or modulo by zero

In Python 3, this changed a little. There, the tracebacks are attached to the exception instance instead, and they have a with_traceback method:

raise MyError(e).with_traceback(tb)

On the other hand Python 3 also has exception chaining, which makes more sense in many cases; to use that, you would just use:

raise MyError(e) from e
Mycobacterium answered 25/1, 2012 at 16:40 Comment(3)
Great, thanks! This did the job. Only issue was that I had to move the declaration of the decorator into the same file as the function being decorated, otherwise sys.exc_info() returned (None, None, None) - any ideas as to why this would be?Aquarelle
That... doesn't make sense. sys.exc_info() doesn't care where the caller is defined. It returns the currently-being-handled exception. It sounds like the decorator you had in a separate file wasn't doing the right thing, but it's hard to say without seeing the actual code.Mycobacterium
Great mention of the super convenient syntax in Py3! Super useful.Raama
S
5

I've faced this problem with tests that were decorated with my custom decorators.

I used following construct in decorator body to preserve original trace printed in unittests output:

try:
    result = func(self, *args, **kwargs)
except Exception:
    exc_type, exc_instance, exc_traceback = sys.exc_info()
    formatted_traceback = ''.join(traceback.format_tb(
        exc_traceback))
    message = '\n{0}\n{1}:\n{2}'.format(
        formatted_traceback,
        exc_type.__name__,
        exc_instance.message
    )
    raise exc_type(message)
Straggle answered 11/9, 2012 at 20:5 Comment(2)
yeah but what are the arguments to the decorator?Unsaddle
Just to point that the difference with the above code is traceback.format_tb. Thanks.Negotiable

© 2022 - 2024 — McMap. All rights reserved.