How to annotate a decorator that returns a `property`

Question:

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

Asked By: ניר

||

Answers:

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

Answered By: blhsing

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]"
Answered By: dROOOze
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.