pythonsingletonpython-typing

Type hinting for generic singleton?


I have a base service, which provides common methods for all services. Moreover, this BaseService serves as a service registry:

class BaseService:
    instances = {}

    @classmethod
    def get_instance(cls) -> 'BaseService':
        if cls.instances.get(cls) is None:
            cls.instances[cls] = cls()
        return cls.instances[cls]

class Service1(BaseService):
    pass

class Service2(BaseService):
    pass

Service1.get_instance()
Service2.get_instance()
Service1.get_instance()

The get_instance() method is returning the children class instance and I feel that the current annotation -> 'BaseService' is incorect. How should I annotate this method properly?


Solution

  • As I said in my comments, doing this for the classmethod of a baseclass is problematic because by definition, that method is going to shared with any subclasses. This is especially true for a singleton.

    The workaround is to give each subclass its own similarly named method with the proper return value annotation. While this can be done with a class decorator, as illustrated in the earlier version of my answer, using a metaclass seems like a cleaner approach, so I've updated my answer accordingly:

    class BaseServiceMeta(type):
        """ Metaclass that properly annotates the return value of the get_instance() method of
            any subclasses of the BaseService class.
        """
        def __new__(metaclass, classname, bases, classdict):
            cls = super(metaclass, metaclass).__new__(metaclass, classname, bases, classdict)
            if classname != 'BaseService':  # subclass?
    
                # define function with the correct return value annotation
                def get_instance() -> classname:
                    return super(cls, cls).get_instance()  # call superclass classmethod
    
                setattr(cls, 'get_instance', get_instance)  # override inherited method
    
            return cls
    
    class BaseService(metaclass=BaseServiceMeta):  # metaclass added
        instances = {}
    
        @classmethod
        def get_instance(cls) -> 'BaseService':
            if cls.instances.get(cls) is None:
                cls.instances[cls] = cls()
            return cls.instances[cls]
    
    class Service1(BaseService):
        pass
    
    class Service2(BaseService):
        pass
    
    # show that the methods have the correct return annotation
    print(repr(BaseService.get_instance.__annotations__['return']))  # -> 'BaseService'
    print(repr(   Service1.get_instance.__annotations__['return']))  # -> 'Service1'
    print(repr(   Service2.get_instance.__annotations__['return']))  # -> 'Service2'
    
    # call subclass methods to show they return the correct singleton instance of each type
    print(Service1.get_instance())  # -> <__main__.Service1 object at 0x004A07D0>
    print(Service2.get_instance())  # -> <__main__.Service2 object at 0x004A07F0>
    print(Service1.get_instance())  # -> <__main__.Service1 object at 0x004A07D0>