Reputation:
I want to create a decorator that return a property
of the decorated function, i.e:
from typing import TYPE_CHECKING
def make_prop(param):
def wrapper(func) -> 'property(func)':
return property(func)
return wrapper
class A:
@make_prop('foo')
def a(self) -> str:
return "hello"
a = A()
assert a.a == "hello"
if TYPE_CHECKING:
reveal_type(a.a)
This is what reveal_type
prints
note: Revealed type is "Any"
While the code above runs correctly and the type should be str
Upvotes: 2
Views: 323
Reputation: 106891
You can type the returning value of make_prop
as a wrapper callable that takes a callable that returns a type variable that is the same as what the wrapper callable returns:
from collections.abc import Callable
from typing import TypeVar, cast
T = TypeVar('T')
def make_prop(param) -> Callable[[Callable[..., T]], T]:
def wrapper(func):
return property(func)
return wrapper
Or to type the inner wrapper function as well:
def make_prop(param) -> Callable[[Callable[..., T]], T]:
def wrapper(func: Callable[..., T]) -> T:
return cast(T, property(func))
return wrapper
Demo: https://mypy-play.net/?mypy=latest&python=3.11&gist=afcdaafe99dbdc075a3be650d8582252
Upvotes: 0
Reputation: 3608
You can't do this with the property
from Python's builtins, because their type isn't generic (see the typeshed stubs).
As with most things in a class body, you'll have to understand Python's descriptor protocol to come up with a typing construct that does this properly. To start off, implement your own generic version of property
:
from __future__ import annotations
import collections.abc as cx
import typing as t
R = t.TypeVar("R")
class property_(property, t.Generic[R]):
fget: cx.Callable[[t.Any], R]
fset: cx.Callable[[t.Any, R], None] | None
fdel: cx.Callable[[t.Any], None] | None
if t.TYPE_CHECKING:
def __new__(
cls,
fget: cx.Callable[[t.Any], R],
fset: cx.Callable[[t.Any, R], None] | None = ...,
fdel: cx.Callable[[t.Any], None] | None = ...,
) -> property_[R]: ...
@t.overload
def __get__(self, obj: None, type_: type | None = ...) -> property_[R]: ...
@t.overload
def __get__(self, obj: object, type_: type | None = ...) -> R: ...
def __get__(self, obj: object, type_: type | None = None) -> property_[R] | R: pass
def __set__(self, obj: t.Any, value: R) -> None: ...
Then, your make_prop
can be simplified to
def make_prop(func: cx.Callable[[t.Any], R]) -> property_[R]:
return property_(func)
Finally,
class A:
@make_prop
def a(self) -> str:
return "hello"
a: A = A()
assert a.a == "hello"
if t.TYPE_CHECKING:
reveal_type(a.a) # mypy: Revealed type is "builtins.str"
reveal_type(A.a) # mypy: Revealed type is "property_[builtins.str]"
Note that A.a != "hello"
; this is what the first overload def __get__(self, obj: None, type_: type | None = ...) -> property_[R]: ...
handles.
As the question has been edited to set make_prop
as a decorator factory instead, the minor change to make_prop
would be
def make_prop(param: t.Any) -> cx.Callable[[cx.Callable[[t.Any], R]], property_[R]]:
def wrapper(func: cx.Callable[[t.Any], R]) -> property_[R]:
return property_(func)
return wrapper
class A:
@make_prop("foo")
def a(self) -> str:
return "hello"
a: A = A()
assert a.a == "hello"
if t.TYPE_CHECKING:
reveal_type(a.a) # mypy: Revealed type is "builtins.str"
reveal_type(A.a) # mypy: Revealed type is "property_[builtins.str]"
Upvotes: 2