7

I have two functions in a class, plot() and show(). show(), as convenience method, does nothing else than to add two lines to the code of plot() like

def plot(
        self,
        show_this=True,
        show_that=True,
        color='k',
        boundary_color=None,
        other_color=[0.8, 0.8, 0.8],
        show_axes=True
        ):
    # lots of code
    return

def show(
        self,
        show_this=True,
        show_that=True,
        color='k',
        boundary_color=None,
        other_color=[0.8, 0.8, 0.8],
        show_axes=True
        ):
    from matplotlib import pyplot as plt
    self.plot(
        show_this=show_this,
        show_that=show_that,
        color=color,
        boundary_color=boundary_color,
        other_color=other_color,
        show_axes=show_axes
        )
    plt.show()
    return

This all works.

The issue I have is that this seems way too much code in the show() wrapper. What I really want: Let show() have the same signature and default arguments as plot(), and forward all arguments to it.

Any hints?

Nico Schlömer
  • 53,797
  • 27
  • 201
  • 249

4 Answers4

16

Python 3 offers the ability to actually copy the signature of the wrapped function with the inspect module:

def show(self, *args, **kwargs):
    from matplotlib import pyplot as plt
    self.plot(*args, **kwargs)
    plt.show()
show.__signature__ = inspect.signature(plot)

Now if you use show in a shell that provides autocompletion like IDLE, you will see the correct parameters for show instead of the cryptics *args, **kwargs

Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
  • I found a problem with this approach. For some reason the signature is not enforced on `show`. You can call `show(dummy=1)` and it won't complain unlike when you call `plot`. Do you know why this happens? – Ark-kun Sep 14 '18 at 10:22
  • The why is rather simple, signature is just an attribute of the function object. It doesn't change the behavior of calling the function, it only provides additional information for introspection. You can however check, if the arguments match the signature by calling `inspect.signature(show).bind(*args, **kwargs)`. – DerWeh Nov 23 '19 at 13:06
  • I had a similar use case and went for the proposed solution. However it does not seem to work as well as expected. As an analogy, if I print the signature of show, I do see the same that the plot signature. However as I code, I do not have the correct auto-completion in VSCode using Pylance and IntelliSense. Any idea what this could be ? – vianmixt Sep 24 '22 at 11:46
  • @vianmixt: This should not be a comment, but a new question giving this one as reference and explaining why its answers do not work in your use case. Anyway, as I do not use VSCode, I cannot really help you here... – Serge Ballesta Sep 25 '22 at 12:33
6

Expanding on Serge's answer I would propose the following:

from inspect import signature

# create a decorator to copy signatures
def copy_signature(source_fct): 
    def copy(target_fct): 
        target_fct.__signature__ = signature(source_fct) 
        return target_fct 
    return copy 

# create a short test function as example
def test(a, /, b, c=2, *, d, e): 
    print(a, b, c, d, e)

# now wrap it
@copy_signature(test)
def forwards(*args, **kwds):
    # assert that arguments are correct
    signature(forwards).bind(*args, **kwds) 
    # now do potentially complicated and obscuring tasks
    print('Just wrapping') 
    test(*args, **kwds)

Calling signature(forwards).bind(*args, **kwds) ensures that the function is called with the proper arguments. Checking the arguments before calling the function makes errors clearer if you have complicated functions.

I purposely didn't include the check in the decorator. Else we would need another function call which makes debugging more complicated.

MiniQuark
  • 46,633
  • 36
  • 147
  • 183
DerWeh
  • 1,721
  • 1
  • 15
  • 26
  • Pretty nice complementation, I'll use it! – artu-hnrq May 03 '20 at 05:20
  • 1
    This works very nice. Is there any way to make the linter (in vscode) to use the copied signature? – Olindholm Sep 03 '22 at 16:47
  • @Olindholm, I have the same issue. The proposed solutions modify the signature at runtime and therefore while you code VSCode and the static analyzers are not aware of the new signature. – vianmixt Sep 24 '22 at 12:04
3

You can make use of argument packing/unpacking:

def show(self, *args, **kwargs):
    from matplotlib import pyplot as plt
    self.plot(*args, **kwargs)
    plt.show()
Aran-Fey
  • 39,665
  • 11
  • 104
  • 149
3

Based on DerWeh's answer (in this page):

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)
        return tgt
    return deco
mo-han
  • 397
  • 5
  • 5