Greg Nisbet
Greg Nisbet

Reputation: 6994

Python create decorator preserving function arguments

I'm trying to write a decorator that preserves the arguments of the functions it decorates. The motivation for doing this is to write a decorator that interacts nicely with pytest.fixtures.

Suppose we have a function foo. It takes a single argument a.

def foo(a):
    pass

If we get the argument spec of foo

>>> inspect.getargspec(foo)
ArgSpec(args=['a'], varargs=None, keywords=None, defaults=None)

We frequently want to create a decorator where the wrapper function passes all of its arguments verbatim to the wrapped function. The most obvious way to do this uses *args and **kwargs.

def identity_decorator(wrapped):
    def wrapper(*args, **kwargs):
        return wrapped(*args, **kwargs)
    return wrapper

@identity_decorator
def foo(a):
    pass

This, not surprisingly, produces a function with an argument spec reflecting the *args and **kwargs.

>>> inspect.getargspec(foo)
ArgSpec(args=[], varargs='args', keywords='kwargs', defaults=None)

Is there a way to either change the argument spec to match the wrapped function or create the function with the right argument spec initially?

Upvotes: 11

Views: 1453

Answers (2)

Serge Ballesta
Serge Ballesta

Reputation: 148880

AFAIK, it is possible only since Python 3.3 with the Signature object:

def identity_decorator(wrapped):
    def wrapper(*args, **kwargs):
        return wrapped(*args, **kwargs)
    wrapper.__signature__ = inspect.signature(wrapped)  # the magic is here!
    return wrapper

Then, you can do:

@identity_decorator
def foo(a):
    pass

and finally:

>>> inspect.getargspec(foo)
ArgSpec(args=['a'], varargs=None, keywords=None, defaults=None)

Upvotes: 16

Guillaume
Guillaume

Reputation: 10961

As suggested in comments, you can use the decorator module or you can use eval evil powers to create a lambda function with correct signature:

import inspect

def identity_decorator(wrapped):
    argspec = inspect.getargspec(wrapped)
    args = inspect.formatargspec(*argspec)

    def wrapper(*args, **kwargs):

        return wrapped(*args, **kwargs)

    func = eval('lambda %s: wrapper%s' % (args.strip('()'), args), locals())

    return func

@identity_decorator
def foo(a):
    pass

This is kinda hackish, but it preserves function arguments:

>>> inspect.getargspec(foo)
ArgSpec(args=['a'], varargs=None, keywords=None, defaults=None)

Upvotes: 3

Related Questions