2

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?

Jules G.M.
  • 3,624
  • 1
  • 21
  • 35
Tom
  • 1,196
  • 2
  • 12
  • 29

2 Answers2

0

I experimented with this:

>>> from functools import wraps
>>> def x(): print(1)
...
>>> @wraps(x)
... def xyz(a,b,c): return x


>>> xyz.__name__
'x'
>>> help(xyz)
Help on function x in module __main__:

x(a, b, c)

AFAIK, this has nothing to do with wraps itself, but an issue related to help. Indeed, because help inspects your objects to provide the information, including __doc__ and other attributes, this is why you get this behavior, although your wrapped function has different argument list. Though, wraps doesn't update that automatically (the argument list) what it really updates is this tuple and the __dict__ which is technically the objects namespace:

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
                       '__annotations__')
WRAPPER_UPDATES = ('__dict__',)

If you aren't sure about how wraps work, probably it'll help if your read the the source code from the standard library: functools.py.

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

No, you don't need to add another wraps in your code, help as I stated above works that way by inspecting your objects. The function's arguments are associated with code objects (__code__) because your function's arguments are stored/represented in that object, wraps has no way to update the argument of the wrapper to be like the wrapped function (continuing with the above example):

>>> xyz.__code__.co_varnames

>>> xyz.__code__.co_varnames = x.__code__.co_varnames
AttributeError: readonly attribute

If help displayed that function xyz has this argument list () instead of (a, b, c) then this is clearly wrong! And the same applies for wraps, to change the argument list of the wrapper to the wrapped, would be cumbersome! So this should not be a concern at all.

>>> @wraps(x, ("__code__",))
... def xyz(a,b,c): pass
...

>>> help(xyz)
Help on function xyz in module __main__:

xyz()

But xyz() returns x():

>>> xyz()
1

For other references take a look at this question or the Python Documentation

What does functools.wraps do?

Community
  • 1
  • 1
GIZ
  • 4,409
  • 1
  • 24
  • 43
0

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
Tom
  • 1,196
  • 2
  • 12
  • 29