Decorating functions without losing their signatures

Rotwang sg552 at hotmail.co.uk
Tue Apr 2 21:05:31 EDT 2013


Hi all,

Here's a Python problem I've come up against and my crappy solution. 
Hopefully someone here can suggest something better. I want to decorate 
a bunch of functions with different signatures; for example, I might 
want to add some keyword-only arguments to all functions that return 
instances of a particular class so that the caller can create instances 
with additional attributes. So I do something like this:

import functools

def mydecorator(f):
     @functools.wraps(f)
     def wrapped(*args, attribute = None, **kwargs):
         result = f(*args, **kwargs)
         result.attribute = attribute
         return result
     return wrapped

@mydecorator
def f(x, y = 1, *a, z = 2, **k):
	return something

The problem with this is, when I subsequently type 'f(' in IDLE, the 
signature prompt that appears is not very useful; it looks like this:

(*args, attribute=None, **kwargs)

whereas I'd like it to look like this:

(x, y=1, *a, z=2, attribute=None, **k)


After thinking about it for a while I've come up with the following 
abomination:

import inspect

def sigwrapper(sig):
   if not isinstance(sig, inspect.Signature):
     sig = inspect.signature(sig)
   def wrapper(f):
     ps = 'args = []\n\t\t'
     ks = 'kwargs = {}\n\t\t'
     for p in sig.parameters.values():
       if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD):
         ps = '%sargs.append(%s)\n\t\t' % (ps, p.name)
       elif p.kind == p.VAR_POSITIONAL:
         ps = '%sargs.extend(%s)\n\t\t' % (ps, p.name)
       elif p.kind == p.KEYWORD_ONLY:
         ks = '%skwargs[%r] = %s\n\t\t' % (ks, p.name, p.name)
       elif p.kind == p.VAR_KEYWORD:
         ks = '%skwargs.update(%s)\n\t\t' % (ks, p.name)
     loc = {'wrapped': f}
     defstring = ('def wrapouter(wrapped = wrapped):'
                  '\n\tdef wrapinner%s:'
                  '\n\t\t%s%sreturn wrapped(*args, **kwargs)'
                  '\n\treturn wrapinner' % (sig, ps, ks))
     exec(defstring, f.__globals__, loc)
     return loc['wrapouter']()
   return wrapper

The function sigwrapper() may be passed an inspect.Signature object sig 
(or function, if that function has the right signature) and returns a 
decorator that gives any function the signature sig. I can then replace 
my original decorator with something like

def mydecorator(f):
     sig = inspect.signature(f)
     sig = do_something(sig) # add an additional kw-only argument to sig
     @functools.wraps(f)
     @sigwrapper(sig)
     def wrapped(*args, attribute = None, **kwargs):
         result = f(*args, **kwargs)
         result.attribute = attribute
         return result
     return wrapped

It seems to work, but I don't like it. Does anyone know of a better way 
of doing the same thing?



More information about the Python-list mailing list