jesnie / dropstackframe

A python library for dropping stack frames.
MIT License
0 stars 0 forks source link

dropstackframe

A python library for dropping stack frames.

This can be useful for removing decorators from stack traces, when using a framework with a lot of decorators.

Example

Let's us write a small decorator for measuring the time it takes to call a function:

from time import perf_counter

def measure_time(func):
    def wrapper(*args, **kwargs):
        before = perf_counter()
        result = func(*args, **kwargs)
        after = perf_counter()
        print(f"{func.__name__} took {after - before}s.")
        return result

    return wrapper

We can use it like this:

@measure_time
def foo(should_raise):
    assert not should_raise
    return 42

@measure_time
def bar(should_raise):
    return foo(should_raise)

@measure_time
def baz(should_raise):
    return bar(should_raise)

baz(False)

On my computer this prints:

foo took 2.2800122678745538e-07s.
bar took 3.576500057533849e-05s.
baz took 4.227000135870185e-05s.

Great. But what happens if we raise an error?

baz(True)

yields:

Traceback (most recent call last):
  File "example.py", line 32, in <module>
    baz(True)
  File "example.py", line 7, in wrapper
    result = func(*args, **kwargs)
  File "example.py", line 28, in baz
    return bar(should_raise)
  File "example.py", line 7, in wrapper
    result = func(*args, **kwargs)
  File "example.py", line 23, in bar
    return foo(should_raise)
  File "example.py", line 7, in wrapper
    result = func(*args, **kwargs)
  File "example.py", line 17, in foo
    assert not should_raise
AssertionError

Notice how every other line is the wrapper from our decorator? If we have large codebase and it is using a framework with a lot of decorators, this can make the stack traces hard to read, because most of the frames are irrelevant decorators.

We can use the dropstackframe library to rewrite our decorator:

from time import perf_counter
from dropstackframe import drop_stack_frame

def measure_time(func):
    def wrapper(*args, **kwargs):
        before = perf_counter()
        try:
            result = func(*args, **kwargs)
        except Exception:
            drop_stack_frame()
            raise
        after = perf_counter()
        print(f"{func.__name__} took {after - before}s.")
        return result

    return wrapper

Now, if we get an error:

baz(True)

we get:

Traceback (most recent call last):
  File "example2.py", line 37, in <module>
    baz(True)
  File "example2.py", line 33, in baz
    return bar(should_raise)
  File "example2.py", line 28, in bar
    return foo(should_raise)
  File "example2.py", line 22, in foo
    assert not should_raise
AssertionError

and all the annoying wrapper stack frames have been removed.

Disabling dropstackframe

Let's say you have a large codebase that uses dropstackframe and one day you have a bug that is really hard to find. In fact you start suspecting that the bug might be hidden by drop_stack_frame. You can use set_enable_drop_stack_frame to disable drop_stack_frame:

from dropstackframe import set_enable_drop_stack_frame

set_enable_drop_stack_frame(False)
baz(True)

set_enable_drop_stack_frame can also be used as a context manager, if you only want to disable drop_stack_frame in a limited region of your code:

from dropstackframe import set_enable_drop_stack_frame

with set_enable_drop_stack_frame(False):
    baz(True)