1

I am trying to create a function Bar.bar with signature and docs copied (OP) from foo:

from inspect import signature
import functools
from typing import Callable

def deco_meta_copy_signature(signature_source: Callable):
    def deco(target: Callable):
        @functools.wraps(target)
        def tgt(*args, **kwargs):
            signature(signature_source).bind(*args, **kwargs)
            return target(*args, **kwargs)
        tgt.__signature__ = signature(signature_source)
        tgt.__doc__ = signature_source.__doc__
        print('Signature 1)', signature(tgt))
        return tgt
    return deco

def foo(a, b, c=0, d=1, **kwargs):
    """ foo! """
    pass

class Bar:
    @deco_meta_copy_signature(foo)
    def bar(self):
        pass

b = Bar()
print('Signature 2)', signature(b.bar))

which prints:

Signature 1) (a, b, c=0, d=1, **kwargs)
Signature 2) (b, c=0, d=1, **kwargs)

As you can see in Signature 2 the first parameter is not preserved. I assume this has to do with the target function being a method and receiving self parameter. How can I add self to the signature while having all the other parameters as well?

alex
  • 10,900
  • 15
  • 70
  • 100

1 Answers1

1

The following works but it does so by updating a "private" member, _parameters, of class Signature, so that is my disclaimer for the future.

The decorator looks at the signature of the target and if there is a first argument named self, then if copies the signature of the source and prepends additional arguments to the front:

from inspect import signature, Parameter
import functools
from typing import Callable
from types import MappingProxyType
from collections import OrderedDict
from copy import deepcopy

def deco_meta_copy_signature(signature_source: Callable):
    def deco(target: Callable):
        @functools.wraps(target)
        def tgt(*args, **kwargs):
            signature(signature_source).bind(*args, **kwargs)
            return target(*args, **kwargs)
        sig_s = signature(signature_source)
        sig_t = signature(target)
        parameters = sig_t.parameters
        if len(parameters):
            # get first parameter
            it = iter(parameters)
            param0 = next(it)
            if param0 == 'self':
                #add extra dummy parameter that will be discarded:
                d = OrderedDict()
                d['dummy'] = Parameter('dummy', Parameter.POSITIONAL_OR_KEYWORD)
                d['self'] = Parameter('self', Parameter.POSITIONAL_OR_KEYWORD)
                for k, v in sig_s.parameters.items():
                    d[k] = v
                sig_s = deepcopy(sig_s) # leave actual signature unmodified
                sig_s._parameters = MappingProxyType(d) # accessing "private" member: beware!
        tgt.__signature__ = sig_s
        tgt.__doc__ = signature_source.__doc__
        print('Signature 1)', signature(tgt))
        return tgt
    return deco

def foo(a, b, c=0, d=1, **kwargs):
    """ foo! """
    pass

class Action:
    def doit(self, x=9):
        pass

edit = Action()

class Bar:
    @deco_meta_copy_signature(foo)
    def bar(self):
        pass

    @deco_meta_copy_signature(edit.doit)
    def foo(self):
        pass


@deco_meta_copy_signature(foo)
def bar(x=8):
    pass

@deco_meta_copy_signature(edit.doit)
def foo(x=8):
    pass

b = Bar()
print('Signature 2)', signature(b.bar))
print('Signature 3)', signature(b.foo))
print('Signature 4)', signature(bar))
print('Signature 5)', signature(foo))

Prints:

Signature 1) (dummy, self, a, b, c=0, d=1, **kwargs)
Signature 1) (dummy, self, x=9)
Signature 1) (a, b, c=0, d=1, **kwargs)
Signature 1) (x=9)
Signature 2) (self, a, b, c=0, d=1, **kwargs)
Signature 3) (self, x=9)
Signature 4) (a, b, c=0, d=1, **kwargs)
Signature 5) (x=9)
Booboo
  • 38,656
  • 3
  • 37
  • 60
  • I'm not a huge fan of private access, I wonder if it could be done "the right way"... Anyway, I implemented your solution only changing decorated function params to `self, *args, **kwargs` and it works well, thank you! Could you please comment on what's the purpose of the `dummy` Parameter? – alex Feb 02 '21 at 12:01
  • First, I don't know what the *right* way is. As you and I have discovered, when you show the signature of an instance method, the first argument, which is the instance itself, and only by convention named `self`, is not shown. And that answers your second question: I add `dummy` and `self` knowing that the first one, whatever it is (I arbitrarily use `dummy`), will not be shown leaving the second one (i.e.`self`) as the first one shown. – Booboo Feb 02 '21 at 12:35
  • By adding `self` to the params, you are assuming there is always at least one positional parameter. My solution does not and that is why it works even without instance methods and why I test to see if there are any parameters. Perhaps you do not need such a general ability to copy a signature to *any* type of function. – Booboo Feb 02 '21 at 12:39