Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I elide a function wrapper from the traceback in Python-3?

The issue

The Phantom Menace

Say i wrote a function decorator which takes the function, and wraps it in another function like so:

# File example-1.py
from functools import wraps

def decorator(func):
    # Do something
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Do something
        return func(*args, **kwargs)
        # Do something
    # Do something
    return wrapper

Now lets suppose the function I'm decorating raises an exception:

@decorator
def foo():
    raise Exception('test')

The result of running foo() will print out the following traceback (In any Python version):

Traceback (most recent call last):
  File "./example-1.py", line 20, in <module>
    foo()
  File "./example-1.py", line 11, in wrapper
    return func(*args, **kwargs)
  File "./example-1.py", line 18, in foo
    raise Exception('test')
Exception: test

Attack of the Clones

OK, now i look at my traceback and i see it goes through the wrapper function. What if I wrapped the function multiple times(presumably with a slightly more sophisticated decorator object which receives arguments in its constructor)? What if I use this decorator often in my code(I use it for logging, or profiling, or whatever)?

Traceback (most recent call last):
  File "./example-1.py", line 20, in <module>
    foo()
  File "./example-1.py", line 11, in wrapper
    return func(*args, **kwargs)
  File "./example-1.py", line 11, in wrapper
    return func(*args, **kwargs)
  File "./example-1.py", line 11, in wrapper
    return func(*args, **kwargs)
  File "./example-1.py", line 11, in wrapper
    return func(*args, **kwargs)
  File "./example-1.py", line 18, in foo
    raise Exception('test')
Exception: test

I don't want it "polluting" my traceback when i know from the function definition that the wrapper is there, and i don't want it showing up multiple times when the code snippet it displays is the unhelpful return func(*args, **kwargs)

Python 2

Revenge of the Sith

In Python-2, as this answer to a different question points out, the following trick does the job:

# In file example-2.py

def decorator(func):
    # Do something
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Do something
        info = None
        try:
            return func(*args, **kwargs)
        except:
            info = sys.exc_info()
            raise info[0], info[1], info[2].tb_next
        finally:
            # Break the cyclical reference created by the traceback object
            del info
        # Do something
    # Do something
    return wrapper

By directly wrapping the call to the wrapped function with this idiom in the same block as the function I want to elide from the traceback, I effectively remove the current layer from the traceback and let the exception keep propagating. Every time the stack unwinding goes through this function, it removes itself from the traceback so this solution works perfectly:

Traceback (most recent call last):
  File "./example-2.py", line 28, in <module>
    foo()
  File "./example-2.py", line 26, in foo
    raise Exception('test')
Exception: test

(Note however that you can not encapsulate this idiom in another function, since as soon the stack will unwind from that function back into wrapper, it will still be added to the traceback)

Python 3

A New Hope

Now that we have this covered, lets move along to Python-3. Python-3 introduced this new syntax:

raise_stmt ::=  "raise" [expression ["from" expression]]

which allows chaining exceptions using the __cause__ attribute of the new exception. This feature is uninteresting to us, since it modifies the exception, not the traceback. Our goal is to be a completely transparent wrapper, as far as visibility goes, so this won't do.

Alternatively, we can try the following syntax, which promises to do what we want (code example taken from the python documentation):

raise Exception("foo occurred").with_traceback(tracebackobj)

Using this syntax we may try something like this:

# In file example-3
def decorator(func):
    # Do something
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Do something
        info = None
        try:
            return func(*args, **kwargs)
        except:
            info = sys.exc_info()
            raise info[1].with_traceback(info[2].tb_next)
        finally:
            # Break the cyclical reference created by the traceback object
            del info
        # Do something
    # Do something
    return wrapper

The Empire Strikes Back

But, unfortunately, this does not do what we want:

Traceback (most recent call last):
  File "./example-3.py", line 29, in <module>
    foo()
  File "./example-3.py", line 17, in wrapper
    raise info[1].with_traceback(info[2].tb_next)
  File "./example-3.py", line 27, in foo
    raise Exception('test')
Exception: test

As you can see, the line executing the raise statement shows up in the traceback. This seems to come from the fact that while the Python-2 syntax sets the traceback from the third argument to raise as the function is being unwound, and thus it is not added to the traceback chain(as explained in the docs under Data Model), the Python-3 syntax on the other hand changes the traceback on the Exception object as an expression inside the functions context, and then passes it to the raise statement which adds the new location in code to the traceback chain (the explanation of this is very similar in Python-3).

A workaround that comes to mind is avoiding the "raise" [ expression ] form of the statement, and instead use the clean raise statement to let the exception propagate as usual but modify the exception objects __traceback__ attribute manually:

# File example-4
def decorator(func):
    # Do something
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Do something
        info = None
        try:
            return func(*args, **kwargs)
        except:
            info = sys.exc_info()
            info[1].__traceback__ = info[2].tb_next
            raise
        finally:
            # Break the cyclical reference created by the traceback object
            del info
        # Do something
    # Do something
    return wrapper

But this doesn't work at all!

Traceback (most recent call last):
  File "./example-4.py", line 30, in <module>
    foo()
  File "./example-4.py", line 14, in wrapper
    return func(*args, **kwargs)
  File "./example-4.py", line 28, in foo
    raise Exception('test')
Exception: test

Return of the Jedi(?)

So, what else can i do? It seems like using the "traditional" way of doing this just won't work because of the change in syntax, and I wouldn't want to start messing with the traceback printing mechanism (using the traceback module) at the project level. This is because it'll be hard if not impossible to implement in an extensible which won't be disruptive to any other package that tries to change the traceback, print the traceback in a custom format at the top level, or otherwise do anything else related to the issue.

Also, can someone explain why in fact the last technique fails completely?

(I tried these examples on python 2.6, 2.7, 3.4, 3.6)

EDIT: After thinking about it for a while, in my opinion the python 3 behavior makes more sense, to the point that the python 2 behavior almost looks like a design bug, but I still think that there should be a way to do this kinda stuff.

like image 497
Re.po Avatar asked Jun 28 '17 22:06

Re.po


People also ask

How do I fix traceback error in Python?

The traceback error also shows the type of error and information about that error. The above case is IndexError: list index out of range . You can fix it using the valid index number to retrieve an item from a list.

What is traceback Print_exc ()?

If type(value) is SyntaxError and value has the appropriate format, it prints the line where the syntax error occurred with a caret indicating the approximate position of the error. traceback. print_exc(limit = None, file = None, chain = True) : This is a shorthand for print_exception(*sys.

What is stack trace in Python?

The Python stack trace is a valuable piece of information that you can use to debug your code. It contains information about the call stack and points out where things have gone wrong. At the end of a stack trace, you can always find the exact exception type and a detailed message of what's gone wrong.


1 Answers

The simple answer is that you shouldn't do that. Hiding things from the traceback is dangerous. You may think you don't want to show that line because it's trivial or "just a wrapper", but in general you wouldn't write the wrapper function if it didn't do something. Next thing you know there is a bug in the wrapper function which is now unfindable because the wrapper function has erased itself from the traceback.

Just deal with the extra lines in the traceback, or, if you really want, override sys.excepthook and filter them out at the top level. If you're worried about someone else overriding sys.excepthook too, then wrap all your code in a top-level function that does the exception printing itself. It isn't and shouldn't be easy to hide levels from the traceback.

like image 166
BrenBarn Avatar answered Oct 19 '22 06:10

BrenBarn