Search code examples
python-3.xinheritancemultiple-inheritance

Python class design for inheritance with varied number of inherited methods


I am building a Python API wrapper for a SOAP API. I need to design a set of API classes which contain a varied number of verb actions (add, get, list, remove etc.). Additionally, because of the nature of the objects in the SOAP API, these may also contain additional methods from a subset of (sync, options, apply, restart and reset). Internals of each verb means that very few cases require overriding and can easily be inherited. My problem is that some endpoints are singletons, or for whatever reason may only support a subset of these methods. Meaning that:

EndpointA

  • get only

EndpointB

  • add, get, list, remove
  • apply and restart

EndpointC

  • add and get only

EndpointD

  • add, get, list, remove
  • apply, restart, reset

I have more than 100 endpoints. Most fit a common theme of:

  • add, get, list, remove

There are, however, many exceptions.

In my current design, all endpoints instantiate with Client attribute that controls the SOAP connection, requests and responses on the wire. I am looking for an approach to flexibly create a class design allowing me to drop in methods without needing to duplicate code or inadvertently inherit methods that an API endpoint doesn't support.

A flat inheritance is problematic as I don't have enough flexibility for all the permutations for methods.

BaseAPI(object):

    def __init___(self, client):
        self.client = client


ChildAPI(BaseAPI):

    def __init___(self, client):
        super().__init__(client)

    def get(self, **kwargs):
        soap_method = methodcaller("".join(["get", self.__class__.__name__]), **kwargs)
        resp = soap_method(self.client.service)
        return resp

    def list(self, **kwargs):
        soap_method = methodcaller("".join(["list", self.__class__.__name__]), **kwargs)
        resp = soap_method(self.client.service)
        return stuff

    # same for add and remove...


EndpointA(BaseAPI):

    def __init___(self, client):
        super().__init__(client)

    # now i have a problem as i only wanted the get method...


EndpointD(BaseAPI):

    def __init___(self, client):
        super().__init__(client)

    # I have all the methods I wanted...

I was thinking about mixins, but as you can see, the verb methods are dependent on the common client. From my understanding, mixins should only inherit from object.

Can anyone suggest how to lay out my class design to promote re-use as far as possible, and avoid a lack of flexibility?


Solution

  • Do I understand it right that the method's implementation, if the Endpoints support it, are all the same for all Endpoints? In this case, you could solve it with a little list that holds the supported methods names and gets overwritten by every Endpoint subclass:

    import functools
    
    class BaseAPI:
        def __init___(self, client):
            self.client = client
    
    class ChildAPI(BaseAPI):
        supported_methods = ["add", "get", "list", "remove", "apply", "restart", "reset"]
    
        @classmethod
        def assert_supported(cls, func):
            """Decorator looks up func's name in self.supported_methods."""
            @functools.wraps(func)
            def wrapper(self, *args, **kwargs):
                if func.__name__ not in self.supported_methods:
                    raise AttributeError("This Endpoint does not support this method.")
                return func(self, *args, **kwargs)
            return wrapper
    
        # implementation of methods here, each decorated with @assert_supported
        @assert_supported
        def get(self, **kwargs):
            soap_method = methodcaller("".join(["get", self.__class__.__name__]), **kwargs)
            resp = soap_method(self.client.service)
            return resp
    
        @assert_supported
        def list(self, **kwargs):
            soap_method = methodcaller("".join(["list", self.__class__.__name__]), **kwargs)
            resp = soap_method(self.client.service)
            return stuff
    
    class EndpointA(BaseAPI):
        supported_methods = ["get"]
    
    class EndpointB(BaseAPI):
        supported_methods = ["add", "get", "list", "remove", "apply", "restart"]
    
    class EndpointC(BaseAPI):
        supported_methods = ["add", "get"]
    
    class EndpointD(BaseAPI):
        pass   # no change to supported_methods => all of them
    

    This reduces the effort for the adaption of the base methods to one single line per class, and still gives you the flexibility to add whatever code necessary.