Python:异常装饰器。 如何保留堆栈跟踪

Python: Exception decorator. How to preserve stacktrace

我正在写一个装饰器来应用于一个函数。 它应该捕获任何异常,然后根据原始异常消息引发自定义异常。 (这是因为suds抛出一个通用的WebFault异常,我从它的消息中解析Web服务抛出的异常并引发Python异常来镜像它。)

但是,当我在包装器中引发自定义异常时,我希望堆栈跟踪指向引发原始WebFault异常的函数。 到目前为止,我提出了正确的异常(它动态地解析消息并实例化异常类)。 我的问题:如何保持堆栈跟踪指向引发WebFault异常的原始函数?

1
2
3
4
5
6
7
8
9
10
11
12
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)


在Python 2.x中,raise的一个鲜为人知的特性是它可以使用多于一个参数:raise的三参数形式采用异常类型,异常实例和回溯。您可以使用sys.exc_info()获取回溯,它返回(不是巧合)异常类型,异常实例和回溯。

(将异常类型和异常实例视为两个单独的参数的原因是异常类之前几天的工件。)

所以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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

在Python 3中,这改变了一点。在那里,回溯附加到异常实例,它们有一个with_traceback方法:

1
raise MyError(e).with_traceback(tb)

另一方面,Python 3也有异常链接,在许多情况下更有意义;要使用它,你只需使用:

1
raise MyError(e) from e


我用自定义装饰器装饰的测试遇到了这个问题。

我在装饰器主体中使用了以下构造来保留在unittests输出中打印的原始轨迹:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 = '
{0}
{1}:
{2}'
.format(
        formatted_traceback,
        exc_type.__name__,
        exc_instance.message
    )
    raise exc_type(message)