10

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?

Greg Nisbet
  • 6,710
  • 3
  • 25
  • 65
  • 2
    I think in recent Python versions, `functools.wraps` does something to make `inspect.getargspec` and `inspect.signature` report the wrapped function's signature, but it doesn't change the wrapper's real signature. The [`decorator`](https://pypi.python.org/pypi/decorator) third-party library provides real signature-preserving decorator functionality through means I've never looked deeply into, probably involving something like bytecode rewriting. I've never used it. – user2357112 Oct 07 '16 at 22:25
  • Maybe check something like http://stackoverflow.com/questions/3729378/how-can-i-programmatically-change-the-argspec-of-a-function-in-a-python-decorato – Nf4r Oct 07 '16 at 22:27

2 Answers2

14

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)
Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
  • 1
    This is the most useful answer in Python 3 with the clear use case. Thank you to make my life easier! – Timmy Lin Oct 30 '18 at 07:34
3

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)
Guillaume
  • 10,463
  • 1
  • 33
  • 47