Search code examples
pythonpython-2.7python-decoratorsfunctools

Applying functools.wraps to nested wrappers


I have a base decorator that takes arguments but that also is built upon by other decorators. I can't seem to figure where to put the functools.wraps in order to preserve the full signature of the decorated function.

import inspect
from functools import wraps

# Base decorator
def _process_arguments(func, *indices):
    """ Apply the pre-processing function to each selected parameter """
    @wraps(func)
    def wrap(f):
        @wraps(f)
        def wrapped_f(*args):
            params = inspect.getargspec(f)[0]

            args_out = list()
            for ind, arg in enumerate(args):
                if ind in indices:
                    args_out.append(func(arg))
                else:
                    args_out.append(arg)

            return f(*args_out)
        return wrapped_f
    return wrap


# Function that will be used to process each parameter
def double(x):
    return x * 2

# Decorator called by end user
def double_selected(*args):
    return _process_arguments(double, *args)

# End-user's function
@double_selected(2, 0)
def say_hello(a1, a2, a3):
    """ doc string for say_hello """
    print('{} {} {}'.format(a1, a2, a3))

say_hello('say', 'hello', 'arguments')

The result of this code should be and is:

saysay hello argumentsarguments

However, running help on say_hello gives me:

say_hello(*args, **kwargs)
    doc string for say_hello

Everything is preserved except the parameter names.

It seems like I just need to add another @wraps() somewhere, but where?


Solution

  • direprobs was correct in that no amount of functools wraps would get me there. bravosierra99 pointed me to somewhat related examples. However, I couldn't find a single example of signature preservation on nested decorators in which the outer decorator takes arguments.

    The comments on Bruce Eckel's post on decorators with arguments gave me the biggest hints in achieving my desired result.

    The key was in removing the middle function from within my _process_arguments function and placing its parameter in the next, nested function. It kind of makes sense to me now...but it works:

    import inspect
    from decorator import decorator
    
    # Base decorator
    def _process_arguments(func, *indices):
        """ Apply the pre-processing function to each selected parameter """
        @decorator
        def wrapped_f(f, *args):
            params = inspect.getargspec(f)[0]
    
            args_out = list()
            for ind, arg in enumerate(args):
                if ind in indices:
                    args_out.append(func(arg))
                else:
                    args_out.append(arg)
    
            return f(*args_out)
        return wrapped_f
    
    
    # Function that will be used to process each parameter
    def double(x):
        return x * 2
    
    # Decorator called by end user
    def double_selected(*args):
        return _process_arguments(double, *args)
    
    # End-user's function
    @double_selected(2, 0)
    def say_hello(a1, a2,a3):
        """ doc string for say_hello """
        print('{} {} {}'.format(a1, a2, a3))
    
    say_hello('say', 'hello', 'arguments')
    print(help(say_hello))
    

    And the result:

    saysay hello argumentsarguments
    Help on function say_hello in module __main__:
    
    say_hello(a1, a2, a3)
        doc string for say_hello