views:

45

answers:

3

I'm decorating a function as such:

def some_abstract_decorator(func):
    @another_lower_level_decorator
    def wrapper(*args, **kwargs):
        # ... details omitted
        return func(*args, **kwargs)
    return wrapper

This does what you'd expect (applies a low level decorator and then does some more stuff. My problem is that I now want to use functools.wraps and I don't know where to put it. This is my guess, but I don't know if it'll have unintended consequences.

def some_abstract_decorator(func):
    @wraps(func)
    @another_lower_level_decorator
    def wrapper(*args, **kwargs):
        # ... details omitted
        return func(*args, **kwargs)
    return wrapper

(I of course apply wraps inside of another_lower_level_decorator as well)

+1  A: 

Yes, that looks right to me. @another_lower_level_decorator will return a function, which @wraps will wrap so that it has the same name as func.

Daniel Stutzbach
+2  A: 

That's right. The way this works is

  • wrapper is defined. It calls func with its arguments.
  • another_lower_level_decorator is called, with wrapper as its argument. The function it returns becomes the new value of wrapper.
  • wraps(func) is called to create a wrapper that will apply the name/docstring/etc. of func to whatever function it's called on.
  • The return value of wraps(func), i.e. the produced wrapper function, is passed the current value of wrapper. This, remember, was the return value from another_lower_level_decorator.
  • wraps(func)(wrapper) becomes the new value of wrapper.
  • That value is returned by some_abstract_decorator, making that function suitable for use as a decorator.

Or that's effectively it, anyway. I think in practice wrapper is only reassigned to once.

intuited
+2  A: 

Try it out:

from functools import wraps    

def another_lower_level_decorator(func):
    @wraps( func )
    def wrapped(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapped

def some_abstract_decorator(func):
    @wraps(func)
    @another_lower_level_decorator
    def wrapper(*args, **kwargs):
        # ... details omitted
        return func(*args, **kwargs)
    return wrapper


@some_abstract_decorator
def test():
    """ This is a docstring that should be on the decorated function """
    pass

help(test)

Prints:

Help on function test in module __main__:

test(*args, **kwargs)
    This is a docstring that should be on the decorated function

As you can see it works! The docstring is there and the name assigned.

But this works just the same:

def some_abstract_decorator(func):
    @another_lower_level_decorator
    @wraps(func)
    def wrapper(*args, **kwargs):
        # ... details omitted
        return func(*args, **kwargs)
    return wrapper

wraps just fixes the docstrings/names. As long as all the decorators use wraps, the order in which you apply it doesn't matter

Btw, there is a much cooler decorator library:

from decorator import decorator

@decorator
def another_decorator(func, *args, **kwargs):
    return func(*args, **kwargs)

@decorator
@another_decorator
def some_abstract_decorator(func, *args, **kwargs):
    # ... details omitted
    return func(*args, **kwargs)


@some_abstract_decorator
def test(x):
    """ this is a docstring that should be on the decorated function """
    pass
THC4k
@THC4k - Thanks. I think your first way it the correct way, after reading through this. I realized that if I use `@wraps(func)` after the inner decorator is applied, I'm assuming that the inner decorator also uses `wraps(func)`. By applying it to the decorated `wrapper` function I'm simply applying `wraps` functionality to my resulting function, thus making things more explicit (the low level decorator might be from a 3rd party, etc).
orokusaki