typeshed icon indicating copy to clipboard operation
typeshed copied to clipboard

"class property" should be Generic[T]

Open Conchylicultor opened this issue 5 years ago • 6 comments

Currently property.__get__ returns Any, which loose information:

https://github.com/python/typeshed/blob/c58a93b928b07a5123fa8ceebe6087fa262fe238/stdlib/3/builtins.pyi#L877

This would be more correct for property.__get__ to be generic and return T. Similar to cached_property:

https://github.com/python/typeshed/blob/1dd1b701c97e4bff9b8e05fb58ce8d3dd27aa213/stdlib/3/functools.pyi#L113

class A:

  @cached_property
  def f(self) -> int:  # f is `cached_property[int]`, f.__get__() -> int
    ...

  @property
  def g(self) -> int:  # f is `property[Any]`, g.__get__() -> Any
    ...

Conchylicultor avatar Oct 30 '20 09:10 Conchylicultor

This makes sense to me. It might even make sense to use two type variables for setting and getting. On the other hand, I don't know what the impact on type checkers is, since I think they usually hard-code properties.

srittau avatar Nov 03 '20 09:11 srittau

I was going to open a new issue in response to https://github.com/python/typing/issues/758, but found this one already opened :)

jp-larose avatar May 18 '21 00:05 jp-larose

I seriously doubt this is idea is supported, but I'll ask anyway.... Is it possible to @overload a class like you can for a function/method? The reason I thought of that is it would allow property with one or two TypeVars. I suspect that the vast majority of uses is for the getter and setter of property to use the same type. However, for those cases where they use different types, there needs to be some way to express that.

@overload
class property(Generic[_T]):
    fget: Optional[Callable[[Any], _T]]
    fset: Optional[Callable[[Any, _T], None]]
    fdel: Optional[Callable[[Any], None]]
    .... # The remaining signatures


@overload
class property(Generic[_T_contra, _T_co]):
    fget: Optional[Callable[[Any], _T_co]]
    fset: Optional[Callable[[Any, _T_contra], None]]
    fdel: Optional[Callable[[Any], None]]
    .... # The remaining signatures

jp-larose avatar May 18 '21 00:05 jp-larose

You can overloads its __init__ or __new__ method.

JelleZijlstra avatar May 18 '21 00:05 JelleZijlstra

Though I guess that wouldn't give you a way to vary the number of type parameters.

JelleZijlstra avatar May 18 '21 00:05 JelleZijlstra

How about something like this?

_C = TypeVar("_C", contravariant=True)
_R = TypeVar("_R", covariant=True)
_R2 = TypeVar("_R2", covariant=True)
_V = TypeVar("_V", contravariant=True)
_V2 = TypeVar("_V2", contravariant=True)

class property(Generic[_C, _R, _V]):
    fget: Callable[[_C], _R] | None
    fset: Callable[[_C, _V], None] | None
    fdel: Callable[[_C], None] | None
    __isabstractmethod__: bool
    def __init__(
        self,
        fget: Callable[[_C], _R] | None = ...,
        fset: Callable[[_C, _V], None] | None = ...,
        fdel: Callable[[_C], None] | None = ...,
        doc: str | None = ...,
    ) -> None: ...
    def getter(self, __fget: Callable[[_C], _R2]) -> property[_C, _R2, _V]: ...
    def setter(self, __fset: Callable[[_C, _V2], None]) -> property[_C, _R, _V2]: ...
    def deleter(self, __fdel: Callable[[_C], None]) -> property: ...
    def __get__(self, __obj: _C | None, __type: Type[_C] | None = ...) -> Any: ...
    def __set__(self, __obj: _C, __value: _V) -> None: ...
    def __delete__(self, __obj: _C) -> None: ...

Since the undefined type vars default to Never when they are not provided, this should work fine.

I'm happy to create a pull request if this is something that would potentially be accepted. What do you guys think?

Edit: After writing this, I realized that @erictraut has written something similar

davidhalter avatar Mar 09 '23 20:03 davidhalter