Search code examples
pythondecoratortraceback

traceback shows up until decorator


This nice little Python decorator can configurably disabled decorated functions:

enabled = get_bool_from_config()

def run_if_enabled(fn):
    def wrapped(*args, **kwargs):
        try:
            return fn(*args, **kwargs) if enabled else None
        except Exception:
            log.exception('')
            return None
    return wrapped

alas, if an exception is raised within fn() the traceback shows only up to the wrapper:

Traceback (most recent call last):
  File "C:\my_proj\run.py", line 46, in wrapped
    return fn(*args, **kwargs) if enabled else None
  File "C:\my_proj\run.py", line 490, in a_decorated_function
    some_dict['some_value']
KeyError: 'some_value'
  1. Why?
  2. Can I workaround to see the full traceback?

Solution

  • Ah! Ok, now this is an interesting question!

    Here is is the same approximate function, but grabbing the exception directly from sys.exc_info():

    import sys
    import traceback
    
    def save_if_allowed(fn):
        def wrapped(*args, **kwargs):
            try:
                return fn(*args, **kwargs) if enabled else None
            except Exception:
                print "The exception:"
                print "".join(traceback.format_exception(*sys.exc_info()))
                return None
        return wrapped
    
    @save_if_allowed
    def stuff():
        raise Exception("stuff")
    
    
    def foo():
        stuff()
    
    foo()
    

    And it's true: no higher stack frames are included in the traceback that's printed:

    $ python test.py
    The exception:
    Traceback (most recent call last):
      File "x.py", line 21, in wrapped
        return fn(*args, **kwargs) if enabled else None
      File "x.py", line 29, in stuff
        raise Exception("stuff")
    Exception: stuff
    

    Now, to narrow this down a bit, I suspect it's happening because the stack frame only includes stack information up until the most recent try/except block… So we should be able to recreate this without the decorator:

    $ cat test.py
    def inner():
        raise Exception("inner")
    
    def outer():
        try:
            inner()
        except Exception:
            print "".join(traceback.format_exception(*sys.exc_info()))
    
    def caller():
        outer()
    
    caller()
    
    $ python test.py
    Traceback (most recent call last):
      File "x.py", line 42, in outer
        inner()
      File "x.py", line 38, in inner
        raise Exception("inner")
    Exception: inner
    

    Ah ha! Now, on reflection, this does make sense in a certain kind of way: at this point, the exception has only encountered two stack frames: that of inner() and that of outer() — the exception doesn't yet know from whence outer() was called.

    So, to get the complete stack, you'll need to combine the current stack with the exception's stack:

    $ cat test.py
    def inner():
        raise Exception("inner")
    
    def outer():
        try:
            inner()
        except Exception:
            exc_info = sys.exc_info()
            stack = traceback.extract_stack()
            tb = traceback.extract_tb(exc_info[2])
            full_tb = stack[:-1] + tb
            exc_line = traceback.format_exception_only(*exc_info[:2])
            print "Traceback (most recent call last):"
            print "".join(traceback.format_list(full_tb)),
            print "".join(exc_line)
    
    def caller():
        outer()
    
    caller()
    
    $ python test.py
    Traceback (most recent call last):
      File "test.py", line 56, in <module>
        caller()
      File "test.py", line 54, in caller
        outer()
      File "test.py", line 42, in outer
        inner()
      File "test.py", line 38, in inner
        raise Exception("inner")
    Exception: inner
    

    See also: