3

Is this decorator typed correctly, given the current limits of mypy? I include example usage below:

import functools
from typing import TypeVar, Type, Any, cast

C = TypeVar('C', bound=Type[Any])


def singleton(cls: C) -> C:
    """Transforms a class into a Singleton (only one instance can exist)."""

    @functools.wraps(cls)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        if not wrapper.instance:  # type: ignore  # https://github.com/python/mypy/issues/2087
            wrapper.instance = cls(*args, **kwargs)  # type: ignore  # https://github.com/python/mypy/issues/2087
        return wrapper.instance  # type: ignore  # https://github.com/python/mypy/issues/2087

    wrapper.instance = None  # type: ignore  # https://github.com/python/mypy/issues/2087
    return cast(C, wrapper)


@singleton
class Test:
    pass


if __name__ == '__main__':
    a = Test()
    b = Test()
    print(a is b)

I had to add type: ignore on the lines where the instance attribute appears because otherwise mypy would flag these errors:

error: "Callable[..., Any]" has no attribute "instance"

  • I didn't see any errors, the code runs perfectly. May I know the python version you are running? – not 0x12 Mar 09 '19 at 22:02
  • it runs well, but I'm curious if the typing done on the code is correct. That's the question. – Laurențiu Andronache Mar 09 '19 at 23:38
  • As recently explained [here](https://stackoverflow.com/a/55157875/4134674), the `singleton` decorator does not return a class, but a function (namely `wrapper`) that returns instances of the decorated class when called. Hence the correct way to decorate `singleton` imho rather looks like `def singleton(cls: Type[C]) -> Callable[[Tuple[Any], Dict[str, Any]], C]:`. Still, doesn't solve the issue with extra attributes on callables. – shmee Mar 19 '19 at 12:52

1 Answers1

2

Your function takes an argument of type C and returns a result of the same type. Therefore, according to mypy a and b will have the same type. You can check it with reveal_type.

reveal_type(a) # Revealed type is 'test.Test'
reveal_type(b) # Revealed type is 'test.Test'

Anyway both cast and # type: ignore should be used with caution, because they are telling mypy to trust you (the developer) that the types are correct even if it cannot confirm it.

The potential problem I see with your code is that you are substituting a class (i.e. Test) with a function, and this could break some code. For example:

>>> Test
<function Test at 0x7f257dd2bae8>
>>> Test.mro()
AttributeError: 'function' object has no attribute 'mro'

An other approach you could try is to substitute the __new__ method of the decorated class:

def singleton(cls: C) -> C:
    """Transforms a class into a Singleton (only one instance can exist)."""

    new = cls.__new__

    def _singleton_new(cls, *args, **kwds):
        try:
            inst = cls._instance
        except AttributeError:
            cls._instance = inst = new(cls, *args, **kwds)
        return inst

    cls.__new__ = _singleton_new
    return cls

In this case you are not replacing the whole class, and therefore you are less likely to break other code using the class:

>>> Test                                                                                                                                                   
test.Test

>>> Test.mro()                                                                                                                                                           
[test.Test, object]

Note that the above code is just an example to show the limitations of your current approach. Therefore, you should probably not use it as is, but look for a more solid solution.

tyrion
  • 1,984
  • 1
  • 19
  • 25