Reputation: 584
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"
Upvotes: 3
Views: 1889
Reputation: 2303
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.
Upvotes: 2