What is the proper way to give type hint for dynamic attributes of decorated classes? #1917
-
Let's think about following code, a decorator that adds a method to the target class; import typing
P = typing.ParamSpec("P")
S = typing.TypeVar("S")
Ret = typing.TypeVar("Ret")
def backup_parameters(
fn: typing.Callable[typing.Concatenate[S, P], Ret],
) -> typing.Callable[typing.Concatenate[S, P], Ret]:
"""
Backup the parameters of the decorated constructor.
You should only use this decorator on the constructor(`__init__`).
"""
if fn.__name__ != "__init__":
raise ValueError("This decorator should only be used on the constructor")
def inner_init(self: S, *args: P.args, **kwargs: P.kwargs) -> Ret:
self._parameters_backup = (args, kwargs)
return fn(self, *args, **kwargs)
return inner_init
def fresh_copy(cls: type[S]) -> type[S]:
"""
Decorate a class to make it fresh copyable.
"""
if hasattr(cls, "fresh_copy"):
raise ValueError(f"This class {cls} already has a `fresh_copy` method")
cls.__init__ = backup_parameters(cls.__init__)
def fresh_copy_inner(self: S) -> S:
return cls(*self._parameters_backup[0], **self._parameters_backup[1])
cls.fresh_copy = fresh_copy_inner
return cls If you decorate some class by I made this method because some of my legacy classes does not store their original arguments in constructor, and also it is not appropriate to directly deepcopy the internal attributes on cloning objects due to several restrictions like lack of "internal state reset method" and internal caches. However, the intellisense will fail to catch type information of @fresh_copy
class TestContainer:
def __init__(self, a: int, b: int):
self.ab_sum: list[int] = [1 for _ in range(a)] + [2 for _ in range(b)]
if __name__ == "__main__":
test = TestContainer(a=1, b=2)
print(test.ab_sum)
test2 = test.fresh_copy()
print(test2.ab_sum)
typing.reveal_type(test2.ab_sum) The Currently the only way I have to resolve this is to provide some class FreshCopyable(typing.Protocol):
"""Protocol for classes that can be fresh copied."""
def fresh_copy(self) -> "FreshCopyable": ...
_parameters_backup: tuple[tuple, dict]
FC = typing.TypeVar("FC", bound=FreshCopyable)
@fresh_copy
class TestContainer(FreshCopyable):
... Is there any possible alternative solution for this? Thanks in advance. |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 1 reply
-
There isn’t a way to define this right now. (intersections could do it but that’s a massive new feature.), One solution would be to make Have you considered just defining |
Beta Was this translation helpful? Give feedback.
There isn’t a way to define this right now. (intersections could do it but that’s a massive new feature.), One solution would be to make
fresh_copy
a function, not a method. You’d have to type-ignore inside the function, but callers would type correctly. Except that it’d be possible to pass undecorated classes, who would fail at runtime.Have you considered just defining
__copy__
methods, and then usingcopy.copy
? You’d have no issues then.