11

I am writing a function decorator that will apply a conversion to the first argument of the function. It works fine if I only decorate my functions once but if I decorate them twice I get an error. Below is some code that demonstrates the problem, it is a simplified version of the code I'm working on. I have excluded the code that does the conversion so as to not distract from the problem

from inspect import getargspec
from functools import wraps

def dec(id):
    def _dec(fn):
        @wraps(fn)
        def __dec(*args, **kwargs):
            if len(args):
                return fn(args[0], *args[1:], **kwargs)
            else:
                first_arg = getargspec(fn).args[0]
                new_kwargs = kwargs.copy()
                del new_kwargs[first_arg]
                return fn(kwargs[first_arg], **new_kwargs)
        return __dec
    return _dec

@dec(1)
def functionWithOneDecorator(a, b, c):
    print "functionWithOneDecorator(a = %s, b = %s, c = %s)" % (a, b, c)

@dec(1)
@dec(2)
def functionWithTwoDecorators(a, b, c):
    print "functionWithTwoDecorators(a = %s, b = %s, c = %s)" % (a, b, c)

functionWithOneDecorator(1, 2, 3)
functionWithOneDecorator(1, b=2, c=3)
functionWithOneDecorator(a=1, b=2, c=3)
functionWithOneDecorator(c=3, b=2, a=1)

functionWithTwoDecorators(1, 2, 3)
functionWithTwoDecorators(1, b=2, c=3)
functionWithTwoDecorators(a=1, b=2, c=3)
functionWithTwoDecorators(c=3, b=2, a=1)

When I run the above code I get the following output:

functionWithOneDecorator(a = 1, b = 2, c = 3)
functionWithOneDecorator(a = 1, b = 2, c = 3)
functionWithOneDecorator(a = 1, b = 2, c = 3)
functionWithOneDecorator(a = 1, b = 2, c = 3)
functionWithTwoDecorators(a = 1, b = 2, c = 3)
functionWithTwoDecorators(a = 1, b = 2, c = 3)
IndexError: list index out of range

This is because when the second decorator inspects the function it is decorating to find the argument names and fails because it is decorating a decorator and that only takes *args and **kwargs.

I can think of ways around the problem which would work in the code above but would still break if a function was decorated with my decorator and another from a 3rd party. Is there a general way to fix this? or is there a better way to achieve the same result?

Update: Thanks to @Hernan for pointing out the decorator module. It solves this problem exactly. Now my code looks like this:

from decorator import decorator

def dec(id):
    @decorator
    def _dec(fn, *args, **kwargs):
        return fn(args[0], *args[1:], **kwargs)
    return _dec

@dec(1)
def functionWithOneDecorator(a, b, c):
    print "functionWithOneDecorator(a = %s, b = %s, c = %s)" % (a, b, c)

@dec(1)
@dec(2)
def functionWithTwoDecorators(a, b, c):
    print "functionWithTwoDecorators(a = %s, b = %s, c = %s)" % (a, b, c)

functionWithOneDecorator(1, 2, 3)
functionWithOneDecorator(1, b=2, c=3)
functionWithOneDecorator(a=1, b=2, c=3)
functionWithOneDecorator(c=3, b=2, a=1)

functionWithTwoDecorators(1, 2, 3)
functionWithTwoDecorators(1, b=2, c=3)
functionWithTwoDecorators(a=1, b=2, c=3)
functionWithTwoDecorators(c=3, b=2, a=1)    

Much cleaner, and it works!

Andrew Burrows
  • 261
  • 2
  • 7
  • 1
    Why `args[0], *args[1:]`, it is the same as `*args`? – Jochen Ritzel Oct 09 '11 at 21:58
  • What problem are you trying to solve with this decorator? As far as I can tell it's main goal seems to be to make sure that the first given argument - keyword/optional or otherwise - is alway passed to the wrapped function as it's "first" argument. Also, what is the intended significance of the `id` argument to the decorator? It's not used anywhere. – Mark Gemmill Oct 09 '11 at 22:16
  • I want to apply a conversion to the first argument. In the code supplied above I excluded the code that does the conversion so as to not distract from the problem. – Andrew Burrows Oct 09 '11 at 22:20
  • In the real code I want to be able to define a number of different decorators that each does a different conversion on the first argument of the function. I want to be able to apply more than one of these decorators to a given function as well as possibly other 3rd party decorators. The id is just there to model having a number of these decorators - if you like the id is the id of the conversion to be applied. – Andrew Burrows Oct 09 '11 at 22:30

1 Answers1

5

The problem is that the signature of your decorated function is not the signature (getargspec) from the original. It is really well explained in the help of the decorator module which you can to solve your problem. Basically, you should use a signature-preserving decorators, so that the second decorator sees the same signature as the first.

Hernan
  • 5,811
  • 10
  • 51
  • 86