4

I have a few decorators which wrap many functions. When one of the functions throws an error my traceback is polluted with many decorator lines. Is there a way to modify a python decorator so it doesn't print itself in an exception traceback?

Here is some example code:

import functools


def dec(func):
    @functools.wraps(func)
    def wrap(*args, **kwargs):
        return func(*args, **kwargs)
    return wrap


@dec
def spam():
    print('I sure hope nobody throws an Exception')
    eggs()


@dec
def eggs():
    raise Exception('Spanish Inquisition')


if __name__ == '__main__':
    spam()

When I run this I get:

Traceback (most recent call last):
  File "tmp.py", line 23, in <module>
    spam()
  File "tmp.py", line 7, in wrap
    return func(*args, **kwargs)
  File "tmp.py", line 14, in spam
    eggs()
  File "tmp.py", line 7, in wrap
    return func(*args, **kwargs)
  File "tmp.py", line 19, in eggs
    raise Exception('Spanish Inquisition')
Exception: Spanish Inquisition

But what I really want is this:

Traceback (most recent call last):
  File "tmp.py", line 23, in <module>
    spam()
  File "tmp.py", line 14, in spam
    eggs()
  File "tmp.py", line 19, in eggs
    raise Exception('Spanish Inquisition')
Exception: Spanish Inquisition

How might I go about this?

Erotemic
  • 4,806
  • 4
  • 39
  • 80
  • Think about it for a moment: decorators are essentially functions that take a function and put it in a wrapper function, which is then "reassigned" to the function name that it was declared. Of course that will be called and hence it will be in your traceback. If your code is going to handle the exception then it shouldn't leave tracebacks on stderr or the like. If you are logging exceptions then write your own traceback printer, see https://docs.python.org/2/library/traceback.html#traceback.extract_tb – metatoaster Apr 03 '14 at 01:10
  • yeah, I was afraid that was the answer. – Erotemic Apr 03 '14 at 13:19
  • 1
    just to share: I finally found an [answer](https://stackoverflow.com/a/68908998/758174) that really works at removing the decorator itself from the stack trace (although, in my tests, removing just one frame is what I want, keeping the wrapped function in the trace, i.e. exactly as if the function had not been decorated). – Pierre D Aug 19 '22 at 14:15

1 Answers1

1

So, I made a decorator ignores_exc_tb which seems to work for what I want. I'll post the code here along with the real code I'm using it with:

import sys
from functools import wraps
from .util_iter import isiterable
from .util_print import Indenter


IGNORE_EXC_TB = not '--noignore-exctb' in sys.argv


def ignores_exc_tb(func):
    """ decorator that removes other decorators from traceback """
    if IGNORE_EXC_TB:
        @wraps(func)
        def wrapper_ignore_exctb(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except Exception:
                # Code to remove this decorator from traceback
                exc_type, exc_value, exc_traceback = sys.exc_info()
                # Remove two levels to remove this one as well
                raise exc_type, exc_value, exc_traceback.tb_next.tb_next
        return wrapper_ignore_exctb
    else:
        return func


def indent_decor(lbl):
    def indent_decor_outer_wrapper(func):
        @ignores_exc_tb
        @wraps(func)
        def indent_decor_inner_wrapper(*args, **kwargs):
            with Indenter(lbl):
                return func(*args, **kwargs)
        return indent_decor_inner_wrapper
    return indent_decor_outer_wrapper


def indent_func(func):
    @wraps(func)
    @indent_decor('[' + func.func_name + ']')
    @ignores_exc_tb
    def wrapper_indent_func(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper_indent_func


def accepts_scalar_input(func):
    '''
    accepts_scalar_input is a decorator which expects to be used on class methods.
    It lets the user pass either a vector or a scalar to a function, as long as
    the function treats everything like a vector. Input and output is sanatized
    to the user expected format on return.
    '''
    @ignores_exc_tb
    @wraps(func)
    def wrapper_scalar_input(self, input_, *args, **kwargs):
        is_scalar = not isiterable(input_)
        if is_scalar:
            iter_input = (input_,)
        else:
            iter_input = input_
        result = func(self, iter_input, *args, **kwargs)
        if is_scalar:
            result = result[0]
        return result
    return wrapper_scalar_input


def accepts_scalar_input_vector_output(func):
    '''
    accepts_scalar_input is a decorator which expects to be used on class
    methods.  It lets the user pass either a vector or a scalar to a function,
    as long as the function treats everything like a vector. Input and output is
    sanatized to the user expected format on return.
    '''
    @ignores_exc_tb
    @wraps(func)
    def wrapper_vec_output(self, input_, *args, **kwargs):
        is_scalar = not isiterable(input_)
        if is_scalar:
            iter_input = (input_,)
        else:
            iter_input = input_
        result = func(self, iter_input, *args, **kwargs)
        if is_scalar:
            if len(result) != 0:
                result = result[0]
        return result
    return wrapper_vec_output

EDIT: New python 3 syntax for reraising the exeception:

exc_type, exc_value, exc_traceback = sys.exc_info() try: exc_traceback = exc_traceback.tb_next exc_traceback = exc_traceback.tb_next except Exception: pass ex = exc_type(exc_value) ex.__traceback__ = exc_traceback raise ex

Erotemic
  • 4,806
  • 4
  • 39
  • 80
  • Nice! Though probably horribly confusing if something goes wrong. – Peter Mar 16 '15 at 09:11
  • @Peter, less confusing than you would think. If your decorators are simple and non-intrusive then this helps because when something goes wrong only the lines where something actually did go wrong are printed. However, if you've got super complicated decorators I agree that it would be confusing. But decorators should ideally never be super complicated. – Erotemic Mar 16 '15 at 13:41
  • This doesn't work with python 3, which drops the three-argument version of raise. – David Roundy May 06 '16 at 18:39
  • It can be modified to use python 3, although it is tricky to write a function that works in both languages due to syntax errors. What I do is I write a python 2 version and a python 3 version in standalone tiles and import them into the main module depending on which version is in use. I've added a section to show the new python 3 syntax. – Erotemic May 09 '16 at 20:16
  • 1
    In python 3.8 to avoid "During handling of the above exception, another exception occurred" and doubled traceback you can use current exception object, eg. except Exception as exc: raise exc.with_traceback(exc.__traceback__.tb_next.tb_next) But it only removes the "try:" traceback part of ignores_exc_tb decorator while rising exception adds its own traceback part after all. – Karolius Jan 19 '21 at 14:53