Python: decorador de excepciones. Cómo conservar stacktrace

Estoy escribiendo un decorador para aplicar a una función. Debería detectar cualquier excepción y luego generar una excepción personalizada basada en el mensaje de excepción original. (Esto se debe a que suds lanza una excepción genérica de WebFault, desde cuyo mensaje analizo la excepción lanzada por el servicio web y levanto una excepción de Python para reflejarla).

Sin embargo, cuando subo la excepción personalizada en el contenedor, quiero que el seguimiento de stack apunte a la función que generó la excepción original de WebFault. Lo que tengo hasta ahora plantea la excepción correcta (analiza dinámicamente el mensaje y crea una instancia de la clase de excepción). Mi pregunta: ¿Cómo puedo conservar el seguimiento de stack para señalar la función original que generó la excepción de WebFault?

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) 

En Python 2.x, una característica poco conocida de raise es que se puede usar con más de un solo argumento: la forma de raise de tres argumentos toma el tipo de excepción, la instancia de excepción y el rastreo. Puede obtener en el sys.exc_info() con sys.exc_info() , que devuelve (no por casualidad) el tipo de excepción, la instancia de excepción y el rastreo.

(La razón por la que esto trata el tipo de excepción y la instancia de excepción como dos argumentos separados es un artefacto de los días anteriores a las clases de excepción).

Asi que:

 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 "", line 1, in  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 

En Python 3, esto cambió un poco. Allí, las trazas de retorno se adjuntan a la instancia de excepción en su lugar, y tienen un método with_traceback :

 raise MyError(e).with_traceback(tb) 

Por otro lado, Python 3 también tiene un encadenamiento de excepciones, lo que tiene más sentido en muchos casos; para usar eso, solo usarías:

 raise MyError(e) from e 

He enfrentado este problema con pruebas que fueron decoradas con mis decoradores personalizados.

Utilicé la siguiente construcción en el cuerpo del decorador para conservar el trazo original impreso en la salida de pruebas de unidad:

 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)