joerick/pyinstrument

Extra level in report when using decorator factories

sebastian-correa opened this issue · 2 comments

I really like pyinstrument, it works like a charm! However, I'm facing an issue with the report when running decorated functions.

For example, the following code

import time
from functools import wraps


def timeit(message):
    def decorator(function):
        @wraps(function)
        def wrapper(*args, **kwargs):
            t1 = time.perf_counter()
            result = function(*args, **kwargs)
            t2 = time.perf_counter()
            print(f"{message} {t2 - t1}s")
            return result

        return wrapper

    return decorator


@timeit("You were done in")
def f(a, b, c):
    """TEST DOCSTRING"""
    time.sleep(1)
    return a + b + c

print(f.__name__)
print(f.__doc__)

for a, b, c in zip(range(10), range(10, 20), range(20, 30)):
    f(a, b, c)

has the following profile
image
were there's a wrapped level that is unexpected. print(f.__name__) prints f and print(f.__doc__) prints TEST DOCSTRING, which is fine.

If the decorator is changed to this

def timeit(message):
    @wraps
    def decorator(function):
        def wrapper(*args, **kwargs):
            t1 = time.perf_counter()
            result = function(*args, **kwargs)
            t2 = time.perf_counter()
            print(f"{message} {t2 - t1}s")
            return result

        return wrapper

    return decorator

the report looks good,
image
but print(f.__name__) prints decorator and print(f.__doc__) prints None, which is wrong.

Maybe this is an issue with my code, but these are the only 2 ways I imagine using wraps, as not using it also results in undesired behavior.

Edit: Running with

pyinstrument --renderer=html test.py

Edit 2: pyinstrument 4.4.0, on Python 3.10.8.

The first looks right, to me, though I'm a little fuzzy on how exactly wraps is working in the second output.

In the first, pyinstrument is accurately representing what is happening in the call stack - E.g. if wrapper was slow you'd want to know about it.

If you'd prefer to hide this frame, there's a new feature in recent versions of pyinstrument called 'traceback hide' where frames can be hidden. Check the changelog for details.

I guess that knowing if the decorator itself is slow could be useful in certain cases, yes. The __tracebackhide__ = True worked, I hadn't seen it documented.

Ended up being:

def timeit(message):
    def decorator(function):
        @wraps(function)
        def wrapper(*args, **kwargs):
            __tracebackhide__ = True
            t1 = time.perf_counter()
            result = function(*args, **kwargs)
            t2 = time.perf_counter()
            print(f"{message} {t2 - t1}s")
            return result

        return wrapper

    return decorator

Edit: you can now close the issue.