0

The issue

I would like to be able to re-use methods by implementing them with a decorator, while preserving my IDE's ability to type-hint the methods added, such that:

@methods(methods=[implement_foo, implement_bar])
class K:
    pass

# OR

@method(methods[Foo, Bar])
class K:
    pass

k = K()

#### THE ISSUE
k. <- #  IDE should recognize the methods .foo() or bar(), but does not.

My issue is much like How to create a class decorator that can add multiple methods to a class?, but as mentioned, while preserving the type-hint and only use one decorator.


What I have tried

I can make it work with one decorator, but not with multiple.

Example with one decorator called implement_method

def implement_method(cls):

    class Inner(cls):

        def __init__(self, *args, **kargs):
            super(Inner, self).__init__(*args, **kargs)
        def method(self):
            pass

    return Inner

@implement_method
class K:
    pass

And type hint works for a new instance of K:

enter image description here

I imagine that one of the issues is using a loop, but I am unable to come up with a different solution. The following is my best attempt:

def methods(methods):
    def wrapper(cls):
        for method in methods:
            cls = method(cls)
        return cls
    return wrapper

class Bar:
    def bar(self):
        pass

@methods(methods=[Bar])
class K:
    pass

k = K()
k. # <- not finding bar()
Tredecies
  • 139
  • 4
  • Hi. I updated the question. I added 2 sections, updated the first paragraph and highlighted the most important code in the first code-snippet. I hope that helps. – Tredecies Feb 07 '23 at 15:37
  • You have to type-hint the *decorator* to indicate what it actually does. It doesn't just return a class, it returns a class that follows a specific *protocol* that you can define. (Also, there's no reason to define a new subclass; just attach the new methods to the incoming class and return it.) – chepner Feb 07 '23 at 16:19

1 Answers1

0

Since your question is a 2 part one: I have an answer for your first part and I am quite stuck on the second. You can modify signatures of functions using the inspect module, but I have not found anything similar for classes and I am not sure if it is possible. So for my answer I will focus on your first part:

One decorator for multiple functions:

Let's look at the decorator first:

def add_methods(*methods):
    def wrapper(cls):
        for method in methods:
            setattr(cls, method.__name__, staticmethod(method))
        return cls
    return wrapper

We use *methods as a parameter so that we can add as many methods as we want as arguments.

Then we define a wrapper for the class and in it iterate over all methods we want to add using setattr to add the method to the class. Notice the staticmethod wrapping the original method. You can leave this out if you want the methods to receive the argument self.

Then we return from the wrapper returning the class and return from the decorator returning the wrapper.

Let's write some simple methods next:

def method_a():
    print("I am a banana!")


def method_b():
    print("I am an apple!")

Now we create a simple class using our decorator:

@add_methods(method_a, method_b)
class MyClass:
    def i_was_here_before(self):
        print("Hah!")

And finally test it:

my_instance = MyClass()
my_instance.i_was_here_before()
my_instance.method_a()
my_instance.method_b()

Our output:

Hah!
I am a banana!
I am an apple!

A word of caution

Ususally it is not advised to change the signature of functions or classes without a good reason (and sometimes even with a good reason).

Alternate Solution

Given that you will need to apply the decorator to each class anyway, you could also just use a superclass like this:

class Parent:
    @staticmethod
    def method_a():
        print("I am a banana!")

    @staticmethod
    def method_b():
        print("I am an apple!")


class MyClass(Parent):
    def i_was_here_before(self):
        print("Hah!")


my_instance = MyClass()
my_instance.i_was_here_before()
my_instance.method_a()
my_instance.method_b()

Since python supports multiple inheritance this should work better and it also gives you the correct hints.

Complete working example:

def add_methods(*methods):
    def wrapper(cls):
        for method in methods:
            setattr(cls, method.__name__, staticmethod(method))
        return cls
    return wrapper


def method_a():
    print("I am a banana!")


def method_b():
    print("I am an apple!")


@add_methods(method_a, method_b)
class MyClass:
    def i_was_here_before(self):
        print("Hah!")


my_instance = MyClass()
my_instance.i_was_here_before()
my_instance.method_a()
my_instance.method_b()
Juhuja
  • 147
  • 5