Skip to content

Make classproperty descriptor work #2563

Open

Description

@JukkaL

As discussed in #2266, this apparently doesn't work yet:

# See http://stackoverflow.com/questions/3203286/how-to-create-a-read-only-class-property-in-python

from typing import *

T = TypeVar('T')
V = TypeVar('V')

class classproperty(Generic[T, V]):

    def __init__(self, getter: Callable[[Type[T]], V]) -> None:
        self.getter = getter

    def __get__(self, instance: Any, owner: Type[T]) -> V:
        # instance is really None, but we don't care
        return self.getter(owner)

class C:
    @classproperty    # <-- error here
    def foo(cls) -> int:
        return 42

reveal_type(C.foo)

@gvanrossum reported the following output from mypy:

classprop.py: note: In class "C":
classprop.py:20: error: Argument 1 to "classproperty" has incompatible type Callable[[C], int]; expected Callable[[Type[None]], int]
classprop.py: note: At top level:
classprop.py:25: error: Revealed type is 'classprop.classproperty[builtins.None, builtins.int*]'

Activity

  1. tyrion commented on Aug 22, 2019

    @tyrion
    Contributor

    It seems to work in this way:

    from typing import *
    
    T = TypeVar("T")
    V = TypeVar("V")
    
    class classproperty(Generic[T, V]):
        def __init__(self, getter: Callable[[Type[T]], V]) -> None:
            self.getter = getattr(getter, "__func__", getter)
    
        def __get__(self, instance: Any, owner: Type[T]) -> V:
            return self.getter(owner)
    
    class C:
        @classproperty
        @classmethod
        def foo(cls) -> int:
            return 42
    
    reveal_type(C.foo)

    I don't think it is ideal to have to use both classproperty and classmethod but I couldn't find an other way to make it work.

  2. oakkitten commented on May 20, 2020

    @oakkitten

    the last example doesn't work with TypeVars:

    from typing import *
    
    T = TypeVar("T")
    V = TypeVar("V")
    
    class classproperty(Generic[T, V]):
        def __init__(self, getter: Callable[[Type[T]], V]) -> None:
            self.getter = getattr(getter, "__func__", getter)
    
        def __get__(self, instance: Any, owner: Type[T]) -> V:
            return self.getter(owner)
    
    F = TypeVar("F", bound='Foo')
    
    class Foo:
        @classproperty
        @classmethod
        def foo(cls: Type[F]) -> F:
            return cls()
    
    class Bar(Foo):
        pass
    
    reveal_type(Bar.foo)

    yields

    main.py:24: error: Argument 2 to "__get__" of "classproperty" has incompatible type "Type[Bar]"; expected "Type[F]"
    main.py:24: note: Revealed type is 'F`-1'
    Found 1 error in 1 file (checked 1 source file)
    

    seems to work if you remove T, although it feels like a wrong thing to do?

  3. sirosen commented on Nov 5, 2021

    @sirosen

    Under 3.9, the docs are now recommending the use of classmethod(property(...)) to handle class properties. This is reinforced by sphinx having special-cased support for this case -- if you want autodoc to produce class property docs, it must be done by wrapping a property in the classmethod descriptor.

    If you try to do this and run mypy, however, the class property type is deduced as () -> T instead of T.

    e.g.

    class A:
        @classmethod
        @property
        def foo(cls) -> int:
            return 1
    
    reveal_type(A.foo)
    

    shows Revealed type is "def () -> builtins.int"

    Would it be possible to special-case classmethod(property(...)) to make that case behave correctly? That would satisfy at least some cases and harmonize with 3.9+ docs and sphinx behavior.

    Right now, trying to declare a read-only class property which satisfies the trifecta of

    • runtime behavior
    • sphinx autodoc extension
    • mypy

    is quite tricky.

  4. JacobHayes commented on May 9, 2022

    @JacobHayes

    I've been using a TypeVar and wrapping func to hack this together:

    from collections.abc import Callable
    from typing import TypeVar
    
    PropReturn = TypeVar("PropReturn")
    
    
    def classproperty(meth: Callable[..., PropReturn]) -> PropReturn:
        """Access a @classmethod like a @property."""
        # mypy doesn't understand class properties yet: https://github.com/python/mypy/issues/2563
        return classmethod(property(meth))  # type: ignore
    
    
    class A:
        @classproperty
        def x(cls) -> int:
            return 5
    
    
    print(A.x)  # 5
    reveal_type(A.x)  # note: Revealed type is "builtins.int"
    print(A().x)  # 5
    reveal_type(A().x)  # note: Revealed type is "builtins.int"

    Rather than muck with casts (since they'd be lying anyway 😁), the # type: ignore inside classproperty works around Argument 1 to "classmethod" has incompatible type "property"; expected "Callable[..., Any]" and Incompatible return value type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions