8

Very closely related to: How can I programmatically change the argspec of a function in a python decorator?

The decorator module provides the means to make a decorator function that preserves the argspec of the decorated function.

If I define a function that is not used as a decorator, is there a way to copy another function's argspec?

Example use case:

class Blah(object):
    def foo(self, *args, **kwargs): 
        """ a docstr """
        result = bar(*args, **kwargs)
        result = result**2 # just so it's clear we're doing something extra here...
        return result

def bar(x, y, z=1, q=2):
    """ a more useful docstr, saying what x,y,z,q do """
    return x+y*z+q

I would like to have foo's argspec look like bar's, but the source to stay unchanged (i.e., inspect.getsource(foo) would still show the result junk). The main purpose for this is to get sphinx docs and ipython's interactive help to show the appropriate arguments.

As the answers to the other question said, the decorator package shows a way to do this, but I got lost within the meat of that code. It seems that the decorator package is recompiling the source, or something like that. I had hoped a simpler approach, e.g. something like foo.argspec = bar.argspec, would be possible.

Community
  • 1
  • 1
keflavich
  • 18,278
  • 20
  • 86
  • 118
  • Yeah that's some hairy source code in that decorator function. You're right, it's rebuilding the function - I'd suggesting trying to reuse the FunctionMaker.create classmethod and see how you go. – Hamish Sep 05 '13 at 00:09
  • Thinking about it though, it seems like it's going to be more work to do that, than to just improve the original signature, or add an appropriate docblock. – Hamish Sep 05 '13 at 00:22
  • `foo` is actually a method, so the first argument will _always_ be the instantiated object, unless you want `foo` to be a `staticmethod`. Otherwise, it would be confusing to check the help on `Blah.foo` and not see `self` as the first argument in the signature. Not to say it isn't do-able, just that you might need to remember to prefix the `bar` signature with `self`. – Matthew Trevor Sep 05 '13 at 01:05
  • @Hamish - I could just copy & paste the args from bar into foo, but then it's twice as much code to maintain, which ideally I'd like to avoid. Is that what you meant by "improve the original signature"? @MatthewTrevor - sorry, there was an error in the code, it should be `foo(self, *args, **kwargs)` (fixed now) – keflavich Sep 05 '13 at 01:07

1 Answers1

6

A decorator is simply a function that does something with another function. So, technically, you could put the required code directly underneath the foo method and then, technically, you would be changing foo without using a decorator, but it would be a horrible mess.

The easiest way to do what you want is going to be to make a decorator that takes a second function (bar in this case) as an argument so it knows which signature to copy. The class code would then look something like:

class Blah(object):
    @copy_argspec(bar)
    def foo(self, *args, **kwargs): 
        """ a docstr """
        result = bar(*args, **kwargs)
        result = result**2 # just so it's clear we're doing something extra here...
        return result

You'll have to have bar defined before instead of after the class.

.
.
.
. . . time passes . . . .
.
.

Okay, luckily I found an old decorator I could adapt.

help(Blah.foo) looks like this before decoration:

Help on method foo in module __main__:

foo(self, *args, **kwargs) unbound __main__.Blah method
    a docstr

and after decoration it looks like this:

Help on method foo in module __main__:

foo(self, x, y, z=1, q=2) unbound __main__.Blah method
    a more useful docstr, saying what x,y,z,q do

Here's the decorator I used:

import inspect

class copy_argspec(object):
    """
    copy_argspec is a signature modifying decorator.  Specifically, it copies
    the signature from `source_func` to the wrapper, and the wrapper will call
    the original function (which should be using *args, **kwds).  The argspec,
    docstring, and default values are copied from src_func, and __module__ and
    __dict__ from tgt_func.
    """
    def __init__(self, src_func):
        self.argspec = inspect.getargspec(src_func)
        self.src_doc = src_func.__doc__
        self.src_defaults = src_func.func_defaults

    def __call__(self, tgt_func):
        tgt_argspec = inspect.getargspec(tgt_func)
        need_self = False
        if tgt_argspec[0][0] == 'self':
            need_self = True

        name = tgt_func.__name__
        argspec = self.argspec
        if argspec[0][0] == 'self':
            need_self = False
        if need_self:
            newargspec = (['self'] + argspec[0],) + argspec[1:]
        else:
            newargspec = argspec
        signature = inspect.formatargspec(
                formatvalue=lambda val: "",
                *newargspec
                )[1:-1]
        new_func = (
                'def _wrapper_(%(signature)s):\n' 
                '    return %(tgt_func)s(%(signature)s)' % 
                {'signature':signature, 'tgt_func':'tgt_func'}
                   )
        evaldict = {'tgt_func' : tgt_func}
        exec new_func in evaldict
        wrapped = evaldict['_wrapper_']
        wrapped.__name__ = name
        wrapped.__doc__ = self.src_doc
        wrapped.func_defaults = self.src_defaults
        wrapped.__module__ = tgt_func.__module__
        wrapped.__dict__ = tgt_func.__dict__
        return wrapped
Ethan Furman
  • 63,992
  • 20
  • 159
  • 237
  • So you're suggesting `copy_argspec` would be very nearly the same as decorator, but would use `bar` as the source of the argspec instead of `foo`? I like the idea and will try... – keflavich Sep 05 '13 at 05:36
  • 1
    @keflavich: If you get stuck, I added the decorator. :) – Ethan Furman Sep 05 '13 at 06:20