'remove decorator from stack trace

I want to write a function decorator that removes itself from the stack trace when an exception occurs (outside the logic specific to the decorator itself), for example when:

  • the caller uses arguments that don't match the function signature, or
  • the decorated function itself raises an exception.

Consider the following example:

import functools

def foo(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # ... (decorator functionality before calling func)
        result = func(*args, **kwargs)
        # ... (decorator functionality after calling func)
        return result
    return wrapper

@foo
def f(x):
    return 1 / x

Unfortunately:

>>> f()
TypeError                                 Traceback (most recent call last)
Input In [2], in <cell line: 1>()
----> 1 f()

Input In [1], in foo.<locals>.wrapper(*args, **kwargs)
      4 @functools.wraps(func)
      5 def wrapper(*args, **kwargs):
      6     # ... (decorator functionality before calling func)
----> 7     result = func(*args, **kwargs)
      8     # ... (decorator functionality after calling func)
      9     return result

TypeError: f() missing 1 required positional argument: 'x'

Likewise:

>>> f(0)
ZeroDivisionError                         Traceback (most recent call last)
Input In [3], in <cell line: 1>()
----> 1 f(0)

Input In [1], in foo.<locals>.wrapper(*args, **kwargs)
      4 @functools.wraps(func)
      5 def wrapper(*args, **kwargs):
      6     # ... (decorator functionality before calling func)
----> 7     result = func(*args, **kwargs)
      8     # ... (decorator functionality after calling func)
      9     return result

Input In [1], in f(x)
     12 @foo
     13 def f(x):
---> 14     return 1 / x

ZeroDivisionError: division by zero

This leads to "polluted" stack traces that include the decorator code context, file, lineno etc. The problem is compounded when we have nested decorated functions.

By contrast, observe how e.g. lru_cache keeps the traceback clean:

@functools.lru_cache(maxsize=4)
def f(x):
    return 1 / x

>>> f()
TypeError                                 Traceback (most recent call last)
Input In [5], in <cell line: 1>()
----> 1 f()

TypeError: f() missing 1 required positional argument: 'x'

>>> f(0)
ZeroDivisionError                         Traceback (most recent call last)
Input In [6], in <cell line: 1>()
----> 1 f(0)

Input In [4], in f(x)
      1 @functools.lru_cache(maxsize=4)
      2 def f(x):
----> 3     return 1 / x

ZeroDivisionError: division by zero

How to achieve similar cleanliness in custom decorators?



Solution 1:[1]

The trick that functools.lru_cache uses is to retrieve the unwrapped function using the .__wrapped__ property of the wrapped function as demonstrated here.

You can look at the CPython implementation of lru_cache where the specific technique of using .__wrapped__ done in the called update_wrapper.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Cory Kramer