An alternative implementation can be found in this github repo, code taken with minor modifications:
Essentially here the trick is to inherit property
and calling the function with the instance's class instead of the instance.
from typing import Callable, Generic, overload, Any
class classproperty[T_class, T_value](property):
"""
Decorator for a Class-level property.
Credit to Denis Rhyzhkov on Stackoverflow: https://stackoverflow.com/a/13624858
"""
def __init__(self, func: "Callable[..., T_value]", /) -> None:
"""Initialise the classproperty object."""
super().__init__(func)
def __get__(self, owner_self: object, owner_cls: "type | None" = None, /) -> T_value:
"""Retrieve the value of the property."""
if self.fget is None:
BROKEN_OBJECT_MESSAGE = "Broken object 'classproperty'."
raise RuntimeError(BROKEN_OBJECT_MESSAGE)
value: T_value = self.fget(owner_cls)
return value
class Test:
@classproperty
def my_property(cls) -> int:
reveal_type(cls)
return 1
reveal_type(Test.my_property) # int
reveal_type(Test().my_property) # int
Below some further variants that you can use.
Note you can use the Any Trick to manually type cls
as the correct type. Do not decorate it as a classmethod as self.func
with func
a classmethod
is not callable.
Alternative classproperty3
can also remove the type
restriction from the first parameter.
Note, how I did not make C
a generic for the class but only for the function. This also circumvents the Never
problem. I am not yet sure if this is a bug, or if mypy infers that no type can be passed and therefore it is Never
.
class classproperty2[T, C]:
def __init__(self, func: Callable[[type[C]], T]):
# allows usage of @classmethod for typing-reasons
if isinstance(func, classmethod):
func = func.__func__ # cannot call classmethods
self.func = func
def __get__(self, _obj: C | None, owner: type[C]) -> T:
return self.func(owner)
class classproperty3[T]:
def __init__(self, func: Callable[..., T]):
if isinstance(func, classmethod):
func = func.__func__ # cannot call classmethods
self.func = func
def __get__[C](self, _obj: C | None, owner: type[C] | None) -> T:
return self.func(owner)
class Test2:
@classproperty2 # Use the Any Trick
def my_property1(cls: type["Test2"] | Any) -> int:
reveal_type(cls)
return 1
@classproperty2
@classmethod # Only for typing reasons; needs to be stripped!
def my_property2(cls) -> int:
reveal_type(cls)
return 1
@classproperty3
def my_property3(cls) -> int:
reveal_type(cls)
return 1
reveal_type(Test2.my_property1)
reveal_type(Test2().my_property1)
reveal_type(Test2.my_property2)
reveal_type(Test2().my_property2)
reveal_type(Test2.my_property3)
reveal_type(Test2().my_property3)
__get__
being called in a class in their static analysis.mypy
to infer what typecls
has frommy_proprety
itself. Adding an explicit notation liketype['Test']
tocls
would seem to help, but really just changes the error message. I suspect you need to modifymypy
itself (say, via a plugin) for something like this to work properly (just likeclassmethod
itself requires specialmypy
support.)