Let's say I define a simple protocol using typing.Protocol, and a class that implements it:
from typing import Protocol
class I(Protocol):
def m(self, x: int) -> int:
...
class A(I):
def m(self, x: int) -> int:
return x + 1
So far, no problem, and mypy doesn't report any errors. (I inherited explicitly from the protocol so that mypy would tell me if my implementation works or not.)
Now, let's say I want to implement my protocol with some classes that work similarly to how Python's built-in functions/bound methods work, that is accessing the attribute on the class gives an "unbound` instance, and accessing it on the instance gives me a "bound" instance that holds on to a reference to the instance, something like this:
from typing import (
Any,
Callable,
Generic,
Optional,
Type,
TypeVar,
Union,
overload,
)
from typing_extensions import Concatenate, ParamSpec
P = ParamSpec("P")
R = TypeVar("R")
class BoundField(Generic[P, R]):
def __init__(
self, func: Callable[Concatenate[Any, P], R], instance: Any
) -> None:
self.func = func
self.instance = instance
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
return self.func(self.instance, *args, **kwargs)
class UnboundField(Generic[P, R]):
def __init__(self, func: Callable[Concatenate[Any, P], R]) -> None:
self.func = func
@overload
def __get__(
self, instance: None, owner: Optional[Type]
) -> "UnboundField[P, R]":
...
@overload
def __get__(
self, instance: object, owner: Optional[Type]
) -> BoundField[P, R]:
...
def __get__(
self, instance: Optional[object], owner: Optional[Type] = None
) -> Union["UnboundField[P, R]", BoundField[P, R]]:
if instance is None:
return self
return BoundField(func=self.func, instance=instance)
def __call__(self, instance: Any, *args: P.args, **kwargs: P.kwargs) -> R:
return self.func(instance, *args, **kwargs)
def decorator(func: Callable[Concatenate[Any, P], R]) -> UnboundField[P, R]:
return UnboundField(func)
class B(I):
@decorator
def m(self, x: int) -> int:
return x * 2
reveal_type(A.m)
reveal_type(A().m)
reveal_type(B.m)
reveal_type(B().m)
In this case, although the B.m object is not a function or a bound method, it implements all the behavior of a function or a bound method, so there's no reason B shouldn't be considered to implement the I protocol. Unfortunately, mypy complains:
$ mypy --version
mypy 0.961 (compiled: yes)
$ mypy protodesc.py
protodesc.py:77: error: Signature of "m" incompatible with supertype "I"
protodesc.py:81: note: Revealed type is "def (self: tmp.protodesc.A, x: builtins.int) -> builtins.int"
protodesc.py:82: note: Revealed type is "def (x: builtins.int) -> builtins.int"
protodesc.py:83: note: Revealed type is "tmp.protodesc.UnboundField[[x: builtins.int], builtins.int]"
protodesc.py:84: note: Revealed type is "tmp.protodesc.BoundField[[x: builtins.int], builtins.int]"
Found 1 error in 1 file (checked 1 source file)
Of course, I want to do more with my UnboundField and BoundField than just define the __get__ and __call__ methods, which is why I can't do something like cast(Callable[Concatenate[Any, P], R], UnboundField(func)) inside the decorator (because I want to add other methods to UnboundField and BoundField and use them elsewhere, which mypy will complain about if it thinks that they are just simple Callables without extra methods).
Note that mypy does understand descriptors in general, for example:
from typing import Any, Generic, Optional, Type, TypeVar, Union, overload
T = TypeVar("T")
class B:
def __init__(self, arg: int = 42):
self.arg = arg
class D(Generic[T]):
"""Descriptor class."""
@overload
def __get__(self, instance: None, type: Type[T]) -> "D":
...
@overload
def __get__(self, instance: T, type: Optional[Type[T]]) -> B:
...
def __get__(
self, instance: Optional[T], type: Optional[Type[T]] = None
) -> Union[B, "D"]:
return B() if instance is not None else self
class A:
b: D = D()
reveal_type(A.b)
reveal_type(A().b)
reveal_type(A().b.arg)
print(A().b.arg)
$ mypy descriptor.py
descriptor.py:33: note: Revealed type is "tmp.descriptor.D[Any]"
descriptor.py:34: note: Revealed type is "tmp.descriptor.B"
descriptor.py:35: note: Revealed type is "builtins.int"
Success: no issues found in 1 source file
Is there some reason that I'm missing that this doesn't work, or is it a deficiency in mypy?