Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make classproperty descriptor work #2563

Open
JukkaL opened this issue Dec 13, 2016 · 4 comments
Open

Make classproperty descriptor work #2563

JukkaL opened this issue Dec 13, 2016 · 4 comments
Labels
bug mypy got something wrong false-positive mypy gave an error on correct code priority-1-normal topic-descriptors Properties, class vs. instance attributes

Comments

@JukkaL
Copy link
Collaborator

JukkaL commented Dec 13, 2016

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*]'
@tyrion
Copy link
Contributor

tyrion commented Aug 22, 2019

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.

@oakkitten
Copy link

oakkitten commented May 20, 2020

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?

@sirosen
Copy link

sirosen commented Nov 5, 2021

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.

@AlexWaygood AlexWaygood added the topic-descriptors Properties, class vs. instance attributes label Mar 24, 2022
@JacobHayes
Copy link

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
Labels
bug mypy got something wrong false-positive mypy gave an error on correct code priority-1-normal topic-descriptors Properties, class vs. instance attributes
Projects
None yet
Development

No branches or pull requests

6 participants