10

I have something like the following:

from typing import TypeVar, Callable, Generic, Type, Union, Optional

T = TypeVar("T")
V = TypeVar("V")

class DescClass(Generic[T, V]):
    """A descriptor."""
    def __init__(self, func: Callable[[T], V]) -> None:
        self.func = func

    def __get__(self, instance: Optional[T], owner: Type[T]) -> Callable[[], V]:
        return self.func.__get__(instance, owner)

class C:
    @DescClass
    def f(self): ...

...for which Mypy will return this error:

test.py:12: error: "Callable[[T], Any]" has no attribute "__get__"

What is the canonical way to specify the type for func, so that Mypy understands it is a descriptor (and thus always has a __get__)?

Update: it's a bit humorous that "descriptor" has no hits when searching the Mypy help.

bad_coder
  • 11,289
  • 20
  • 44
  • 72
Rick
  • 43,029
  • 15
  • 76
  • 119
  • Related, but [not the same Q](https://stackoverflow.com/questions/54413434/type-hinting-with-descriptors). – Rick Apr 30 '19 at 17:34
  • Well, `Callable` in general does *not* have a `__get__` – juanpa.arrivillaga Apr 30 '19 at 17:34
  • 1
    @juanpa.arrivillaga right. `class MyCallable: def __call__(self): ...` etc etc. so.... – Rick Apr 30 '19 at 17:35
  • But why would you need to hint this? Every function is a non-data descriptor. – Filip Dimitrovski Apr 30 '19 at 17:36
  • @FilipDimitrovski Yes, but functions aren't the only callables that exist in python. So the type hint has to specify "something that can be called *and* is a descriptor". – Aran-Fey Apr 30 '19 at 17:39
  • 2
    Perhaps a good use-case for [protocols/structural types](https://mypy.readthedocs.io/en/latest/protocols.html) – juanpa.arrivillaga Apr 30 '19 at 17:40
  • 1
    @FilipDimitrovski because future (read: stupid, forgetful, careless....) me may try to monkey patch using a functional syntax, and some callable non-function object: `C.g = DescClass(callable_obj)` and it will not work. – Rick Apr 30 '19 at 17:40
  • @juanpa.arrivillaga last i heard the protocol thing was still in development hell. but that's probably teh answer. a protocol-like type `Descriptor`- a sub type of `Callable` which adds on the desc protocol requirements. – Rick Apr 30 '19 at 17:47
  • 1
    @RickTeachey I had to abandon my own attemp to incorporate a protocol, although it was rather complex (requiring classmethds etc) and I'm not sure the protocol was the source of the problems exactly. I ended up just going with an `abc.ABC` . Read the answer/comments in [this question](https://stackoverflow.com/questions/53126193/classmethods-in-generic-protocols-with-self-types-mypy-type-checking-failure). Your use-case may be simple enough that it "just works", but it's definitely not a feature ready for production, at least that was my impression. – juanpa.arrivillaga Apr 30 '19 at 17:49
  • @juanpa.arrivillaga that looks really helpful- thanks! – Rick Apr 30 '19 at 17:50
  • 1
    It's possible to *define* the type of a descriptor as ``class Descriptor(Protocol[T, V]): def __get__(self, instance: Optional[T], owner: Optional[Type[T]]) -> V: pass`` (add ``__set__``/``__del__`` if a data descriptor is desired). This then works correctly when ``DescClass`` is set to take a ``func: Descriptor[T, Callable[[], V]]``. However, as of MyPy 0.812 the ultimate problem is that the ``def`` "creates" just a ``Callable[[C], Any]"`` without any information that it is a descriptor as well. – MisterMiyagi Mar 24 '21 at 13:48

1 Answers1

3

This appears to work fine on modern Python and modern mypy:

from typing import (
    TypeVar,
    Callable,
    Generic, 
    Type, 
    Optional,
    cast,
    Protocol
)

T_contra = TypeVar("T_contra", contravariant=True)
V = TypeVar("V")
P_co = TypeVar("P_co", covariant=True)


class DescriptorProto(Protocol[P_co, T_contra]):
    def __get__(
        self, 
        instance: Optional[T_contra], 
        owner: Type[T_contra]
    ) -> P_co:
        ...


FuncType = DescriptorProto[Callable[[], V], T_contra]


class DescClass(Generic[T_contra, V]):
    """A descriptor."""

    def __init__(self, func: Callable[[T_contra], V]) -> None:
        self.func = cast(FuncType[V, T_contra], func)

    def __get__(
        self, 
        instance: Optional[T_contra], 
        owner: Type[T_contra]
    ) -> Callable[[], V]:
        return self.func.__get__(instance, owner)


class C:
    @DescClass
    def f(self) -> None: ...
bad_coder
  • 11,289
  • 20
  • 44
  • 72
Alex Waygood
  • 6,304
  • 3
  • 24
  • 46
  • 1
    NICE! I guess this has been fixed (I haven't tested your code but it looks fine to me!). – Rick Aug 19 '21 at 18:25