Description
I believe this is a bug (explanation below).
I expected to be able to statically check a case by checking whether self
matches a given protocol (with the check_implements
below).
This seems to work, but if the method is decorated (with the implements
as shown in the code below) it doesn't validate properly that a method return type is wrong.
i.e.: in the code below I expected that the _: IFoo = check_implements(self)
line gives an error because the protocol declaration expects a bool
return, whereas the method implementation says it returns a str
.
I think this is a bug because the same type-check later on says that the implementation doesn't conform to the protocol if done outside of the class -- as _: IFoo = check_implements(Foo())
, so, it does something on __init__
and a different thing if out of the __init__
. Besides, it works if the function is not decorated.
Note: even if I do _: IFoo = check_implements(Foo())
in the __init__
it doesn't validate properly (so, it doesn't seem related to self
-- and it works as expected if the function is not decorated).
from typing import TypeVar, Protocol, Any, Callable
import functools
F = TypeVar('F', bound=Callable[..., Any])
def implements(method: Any) -> Callable[[F], F]:
"""
Meant to be used as
class B:
@implements(IExpectedProtocol.method)
def method(self):
pass
Useful for knowing whether a name still exists in the interface and document
what's being implemented.
In runtime validates that the name passed is the same name implemented.
"""
@functools.wraps(method)
def wrapper(func):
if func.__name__ != method.__name__:
msg = "Wrong @implements: %r expected, but implementing %r."
msg = msg % (func.__name__, method.__name__)
raise AssertionError(msg)
return func
return wrapper
T = TypeVar("T")
def check_implements(x: T) -> T:
"""
This is to be used in the constructor of a class which is expected to
implement some protocol. It can be used as:
def __init__(self):
_: IExpectedProtocol = check_implements(self)
Mypy should complain if `self` is not implementing the IExpectedProtocol.
"""
return x
class IFoo(Protocol):
def some_method(self) -> bool:
pass
class Foo(object):
def __init__(self) -> None: # give type
_: IFoo = check_implements(self) # Issue: Mypy does not report error here
@implements(IFoo.some_method) # Note: if this implements is removed it reports the error at the __init__!
def some_method(self) -> str: # Note: return value does not match protocol!
pass
_: IFoo = check_implements(Foo()) # Mypy properly reports error here (with or without the implements decorator).