From 975fbe56e57cdf89b99cd62b98e043fd0d57e4b7 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 29 Aug 2023 18:58:20 -0700 Subject: [PATCH 1/6] Fix difference between callables and callback protocols Fixes https://github.com/python/mypy/pull/15926#discussion_r1303520040 --- mypy/checkmember.py | 6 +++--- test-data/unit/check-protocols.test | 27 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index f7d002f17eb95..18977ed39cb89 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -779,12 +779,12 @@ def analyze_var( call_type: Optional[ProperType] = None if var.is_initialized_in_class and (not is_instance_var(var) or mx.is_operator): - 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()): call_type = get_proper_type(_analyze_member_access("__call__", typ, mx)) else: call_type = typ + if isinstance(call_type, Instance) and call_type.type.get_method("__call__"): + call_type = get_proper_type(_analyze_member_access("__call__", typ, mx)) if isinstance(call_type, FunctionLike) and not call_type.is_type_obj(): if mx.is_lvalue: diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index dba01be50fee1..c0c1b3c326329 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -4127,3 +4127,30 @@ 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: ... + +def make_method_protocol() -> CallbackProtocol: ... +def make_method_callable() -> Callable[[int], int]: ... + +class ClsProto: + meth = make_method_protocol() + +class ClsCall: + meth = make_method_callable() + +reveal_type(ClsProto.meth) # N: Revealed type is "__main__.CallbackProtocol" +reveal_type(ClsCall.meth) # N: Revealed type is "def (builtins.int) -> builtins.int" + +def takes(p: ClsProto, c: ClsCall) -> 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" From cc99a6ca8b0b9f72f98f2297ea7ca8090275d742 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 29 Aug 2023 19:53:57 -0700 Subject: [PATCH 2/6] protocol --- mypy/checkmember.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 18977ed39cb89..fb11d90bb259f 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -783,7 +783,11 @@ def analyze_var( call_type = get_proper_type(_analyze_member_access("__call__", typ, mx)) else: call_type = typ - if isinstance(call_type, Instance) and call_type.type.get_method("__call__"): + if ( + isinstance(call_type, Instance) + and call_type.type.is_protocol + and call_type.type.get_method("__call__") + ): call_type = get_proper_type(_analyze_member_access("__call__", typ, mx)) if isinstance(call_type, FunctionLike) and not call_type.is_type_obj(): From ab94945f119ba968814630fa3252c24b6450ef6a Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 29 Aug 2023 19:57:19 -0700 Subject: [PATCH 3/6] more test --- test-data/unit/check-protocols.test | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index c0c1b3c326329..fdc1cf2127ad5 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -4135,8 +4135,14 @@ 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() @@ -4144,13 +4150,19 @@ class ClsProto: 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) -> None: +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" From c3112fdb7a9647f5ef43349032b2ef349f427f57 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 15 Jun 2025 12:04:54 +0100 Subject: [PATCH 4/6] Try modenizing PR implementation --- mypy/checkmember.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 295cc4243fd45..73969e2bd7f61 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -900,7 +900,17 @@ def analyze_var( and call_type.type.is_protocol and call_type.type.get_method("__call__") ): - call_type = get_proper_type(_analyze_member_access("__call__", typ, mx)) + # 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__). From 658a2083fdc9d23832bb65b14c84b2f1c696c4d5 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 15 Jun 2025 12:09:34 +0100 Subject: [PATCH 5/6] Fix typo --- mypy/checkmember.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 73969e2bd7f61..06e690d02e4f9 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -907,7 +907,7 @@ def analyze_var( 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) + elif isinstance(call_type, Overloaded): call_type = Overloaded( [it.copy_modified(is_bound=False) for it in call_type.items()] ) From 5c0e009d1d6b77acc4f8639112061bda58c6ef44 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 15 Jun 2025 12:21:40 +0100 Subject: [PATCH 6/6] Fix property --- mypy/checkmember.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 06e690d02e4f9..61c3f5c7cd14a 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -909,7 +909,7 @@ def analyze_var( 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()] + [it.copy_modified(is_bound=False) for it in call_type.items] ) # Bound variables with callable types are treated like methods