Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I programmatically change the argspec of a function *not* in a python decorator?

Tags:

python

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.

like image 681
keflavich Avatar asked Sep 04 '13 23:09

keflavich


1 Answers

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
like image 88
Ethan Furman Avatar answered Sep 29 '22 13:09

Ethan Furman