Python: dekorator WYJĄTKÓW. Jak zachować stacktrace

Piszę dekorator do zastosowania do funkcji. Powinien wychwycić dowolny wyjątek, a następnie wywołać Niestandardowy wyjątek na podstawie oryginalnego Komunikatu o wyjątku. (Dzieje się tak dlatego, że suds rzuca ogólny wyjątek WebFault, z którego wiadomości analizuję wyjątek rzucony przez usługę internetową i podnoszę wyjątek Pythona, aby go odbić.)

Jednakże, gdy podniosę Niestandardowy wyjątek w opakowaniu, chcę, aby stacktrace wskazywało na funkcję, która podniosła oryginał Wyjątek WebFault. To co mam do tej pory powoduje powstanie WŁAŚCIWEGO wyjątku (dynamicznie przetwarza wiadomość i tworzy instancję klasy wyjątku). moje pytanie: Jak mogę zachować stacktrace, aby wskazać oryginalną funkcję, która wywołała wyjątek 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)
Author: igniteflow, 2012-01-25

2 answers

W Pythonie 2.x, mało znaną cechą raise jest to, że może być używany z więcej niż jednym argumentem: trójargumentowa forma raise przyjmuje typ wyjątku, instancję wyjątku i traceback. Traceback można uzyskać za pomocą sys.exc_info(), która zwraca (nieprzypadkowo) typ wyjątku, instancję wyjątku i traceback.

(powodem, dla którego traktuje typ wyjątku i instancję wyjątku jako dwa oddzielne argumenty, jest artefakt z dni przed klasy WYJĄTKÓW.)

Więc:

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

W Pythonie 3, to się trochę zmieniło. Tam, tracebacki są dołączone do instancji wyjątku i mają metodę with_traceback:

raise MyError(e).with_traceback(tb)

Z drugiej strony Python 3 ma również wyjątek chaining, co w wielu przypadkach ma większy sens; aby go użyć, wystarczy użyć:

raise MyError(e) from e
 43
Author: Thomas Wouters,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-01-25 16:40:50

Miałem do czynienia z tym problemem z testami, które zostały udekorowane moimi niestandardowymi dekoratorami.

Użyłem następującej konstrukcji w ciele dekoratora, aby zachować oryginalny ślad wydrukowany w wyjściu unittests:

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)
 5
Author: Kee,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-09-11 21:45:49