Is it possible to add/overwrite a type hint in case of the following example? The example is just to get an idea of what I mean, by no means is this something that I would use in this way.
from dataclasses import dataclass
def wrapper(f):
def deco(instance):
if not instance.user:
instance.user = data(name="test")
return f(instance)
return deco
@dataclass
class data:
name: str
class test_class:
def __init__(self):
self.user: None | data = None
@wrapper
def test(self):
print(self.user.name)
x = test_class()
x.test()
The issue is that type hinting does not understand that the decorated method's user attribute is not None, thus showing a linting error that name is not a known member of none
.
Of course this code could be altered so that instead of using a decorator it would just do something like this:
def test(self):
if not self.user:
...
print(self.user.name)
But that is not the point. I just want to know if it is possible to let the type hinter know that the attribute is not None. I could also just suppress the warning but that is not what I am looking for.
I would use the good ol' assert
and be done with it:
...
@wrapper
def test(self):
assert isinstance(self.user, data)
print(self.user.name)
I realize this is a crude way as opposed to some annotation magic you might have expected for the decorator, but in my opinion this is the most practical approach.
There are countless other situations that can be constructed, where the type of some instance attribute may be altered externally. In those cases the use of such a simple assertion is not only for the benefit of the static type checker, but can also save you from shooting yourself in the foot, if you decide to alter that external behavior.
Another possibility is to make the user
attribute private and add a function (or property) to get it, which ensures that it is not None
. Here is a working example:
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import TypeVar
T = TypeVar("T")
@dataclass
class Data:
name: str
def wrapper(f: Callable[[TestClass], T]) -> Callable[[TestClass], T]:
def deco(self: TestClass) -> T:
try:
_ = self.user
except RuntimeError:
self.user = Data(name="test")
return f(self)
return deco
class TestClass:
def __init__(self) -> None:
self._user: None | Data = None
@property
def user(self) -> Data:
if self._user is None:
raise RuntimeError
return self._user
@user.setter
def user(self, data: Data) -> None:
self._user = data
@wrapper
def test(self) -> None:
print(self.user.name)
if __name__ == '__main__':
x = TestClass()
x.test()
Depending on the use case, this might actually be preferred because otherwise, user
being a public attribute, all outside code that wants to use TestClass
will face the same problem of never being sure if user
is None
or not, thus being forced to do the same checks again and again.