diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 1633eaf52983..61c3f5c7cd14 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -890,13 +890,27 @@ def analyze_var( call_type: ProperType | None = None if var.is_initialized_in_class and (not is_instance_var(var) or mx.is_operator): typ = get_proper_type(typ) - if isinstance(typ, FunctionLike) and not typ.is_type_obj(): - call_type = typ - elif var.is_property: + if var.is_property and (not isinstance(typ, FunctionLike) or typ.is_type_obj()): deco_mx = mx.copy_modified(original_type=typ, self_type=typ, is_lvalue=False) call_type = get_proper_type(_analyze_member_access("__call__", typ, deco_mx)) else: call_type = typ + if ( + isinstance(call_type, Instance) + and call_type.type.is_protocol + and call_type.type.get_method("__call__") + ): + # This is ugly, but it reflects the reality that Python treats + # real functions and callable objects differently in class bodies. + # We want to make callback protocols behave like the former. + proto_mx = mx.copy_modified(original_type=typ, self_type=typ, is_lvalue=False) + call_type = get_proper_type(_analyze_member_access("__call__", typ, proto_mx)) + if isinstance(call_type, CallableType): + call_type = call_type.copy_modified(is_bound=False) + elif isinstance(call_type, Overloaded): + call_type = Overloaded( + [it.copy_modified(is_bound=False) for it in call_type.items] + ) # Bound variables with callable types are treated like methods # (these are usually method aliases like __rmul__ = __mul__). diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index c6c2c5f8da98..8276d51cb4b3 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -4136,6 +4136,44 @@ class P(Protocol): class C(P): ... C(0) # OK +[case testCallbackProtocolMethod] +from typing import Callable, Protocol + +class CallbackProtocol(Protocol): + def __call__(self, __x: int) -> int: ... + +class CallableObject: + attribute: str + def __call__(self, __x: int) -> int: + return 3 + +def make_method_protocol() -> CallbackProtocol: ... +def make_method_callable() -> Callable[[int], int]: ... +def make_method_object() -> CallableObject: ... + +class ClsProto: + meth = make_method_protocol() + +class ClsCall: + meth = make_method_callable() + +class ClsObject: + meth = make_method_object() + +reveal_type(ClsProto.meth) # N: Revealed type is "__main__.CallbackProtocol" +reveal_type(ClsCall.meth) # N: Revealed type is "def (builtins.int) -> builtins.int" +reveal_type(ClsObject.meth) # N: Revealed type is "__main__.CallableObject" + +def takes(p: ClsProto, c: ClsCall, o: ClsObject) -> None: + reveal_type(p.meth(0)) # E: Invalid self argument "ClsProto" to attribute function "meth" with type "Callable[[int], int]" \ + # E: Too many arguments for "__call__" of "CallbackProtocol" \ + # N: Revealed type is "builtins.int" + reveal_type(c.meth(0)) # E: Invalid self argument "ClsCall" to attribute function "meth" with type "Callable[[int], int]" \ + # E: Too many arguments \ + # N: Revealed type is "builtins.int" + reveal_type(o.meth(0)) # N: Revealed type is "builtins.int" + reveal_type(o.meth.attribute) # N: Revealed type is "builtins.str" + [case testTypeVarValueConstraintAgainstGenericProtocol] from typing import TypeVar, Generic, Protocol, overload