From 8b3d25aa27d3569a495f6bc55dd751a2ef87e3a3 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 22 Aug 2023 01:27:12 -0700 Subject: [PATCH 1/5] Fix inference for properties with __call__ --- mypy/checkmember.py | 74 ++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 2b0717f181a9..8e06cbd65f69 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -779,44 +779,44 @@ def analyze_var( if ( var.is_initialized_in_class and (not is_instance_var(var) or mx.is_operator) - and isinstance(typ, FunctionLike) - and not typ.is_type_obj() ): - if mx.is_lvalue: - if var.is_property: - if not var.is_settable_property: - mx.msg.read_only_property(name, itype.type, mx.context) - else: - mx.msg.cant_assign_to_method(mx.context) - - if not var.is_staticmethod: - # Class-level function objects and classmethods become bound methods: - # the former to the instance, the latter to the class. - functype = typ - # Use meet to narrow original_type to the dispatched type. - # For example, assume - # * A.f: Callable[[A1], None] where A1 <: A (maybe A1 == A) - # * B.f: Callable[[B1], None] where B1 <: B (maybe B1 == B) - # * x: Union[A1, B1] - # In `x.f`, when checking `x` against A1 we assume x is compatible with A - # and similarly for B1 when checking against B - dispatched_type = meet.meet_types(mx.original_type, itype) - signature = freshen_all_functions_type_vars(functype) - bound = get_proper_type(expand_self_type(var, signature, mx.original_type)) - assert isinstance(bound, FunctionLike) - signature = bound - signature = check_self_arg( - signature, dispatched_type, var.is_classmethod, mx.context, name, mx.msg - ) - signature = bind_self(signature, mx.self_type, var.is_classmethod) - expanded_signature = expand_type_by_instance(signature, itype) - freeze_all_type_vars(expanded_signature) - if var.is_property: - # A property cannot have an overloaded type => the cast is fine. - assert isinstance(expanded_signature, CallableType) - result = expanded_signature.ret_type - else: - result = expanded_signature + call_type = _analyze_member_access("__call__", typ, mx) + if isinstance(call_type, FunctionLike) and not call_type.is_type_obj(): + if mx.is_lvalue: + if var.is_property: + if not var.is_settable_property: + mx.msg.read_only_property(name, itype.type, mx.context) + else: + mx.msg.cant_assign_to_method(mx.context) + + if not var.is_staticmethod: + # Class-level function objects and classmethods become bound methods: + # the former to the instance, the latter to the class. + functype = call_type + # Use meet to narrow original_type to the dispatched type. + # For example, assume + # * A.f: Callable[[A1], None] where A1 <: A (maybe A1 == A) + # * B.f: Callable[[B1], None] where B1 <: B (maybe B1 == B) + # * x: Union[A1, B1] + # In `x.f`, when checking `x` against A1 we assume x is compatible with A + # and similarly for B1 when checking against B + dispatched_type = meet.meet_types(mx.original_type, itype) + signature = freshen_all_functions_type_vars(functype) + bound = get_proper_type(expand_self_type(var, signature, mx.original_type)) + assert isinstance(bound, FunctionLike) + signature = bound + signature = check_self_arg( + signature, dispatched_type, var.is_classmethod, mx.context, name, mx.msg + ) + signature = bind_self(signature, mx.self_type, var.is_classmethod) + expanded_signature = expand_type_by_instance(signature, itype) + freeze_all_type_vars(expanded_signature) + if var.is_property: + # A property cannot have an overloaded type => the cast is fine. + assert isinstance(expanded_signature, CallableType) + result = expanded_signature.ret_type + else: + result = expanded_signature else: if not var.is_ready and not mx.no_deferral: mx.not_ready_callback(var.name, mx.context) From 68bcd7aff9d9ac67ba077982f149bc0baba634d5 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 22 Aug 2023 01:54:45 -0700 Subject: [PATCH 2/5] . --- mypy/checkmember.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 8e06cbd65f69..846897de8fb1 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -780,7 +780,13 @@ def analyze_var( var.is_initialized_in_class and (not is_instance_var(var) or mx.is_operator) ): - call_type = _analyze_member_access("__call__", typ, mx) + call_type: ProperType + if isinstance(typ, FunctionLike) and not typ.is_type_obj(): + call_type = typ + elif var.is_property: + call_type = get_proper_type(_analyze_member_access("__call__", typ, mx)) + else: + call_type = typ if isinstance(call_type, FunctionLike) and not call_type.is_type_obj(): if mx.is_lvalue: if var.is_property: From c6f570742da3e5f16ad84112b9934402c39582e8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 09:00:49 +0000 Subject: [PATCH 3/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checkmember.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 846897de8fb1..31a742045d37 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -776,10 +776,7 @@ def analyze_var( freeze_all_type_vars(t) result: Type = t typ = get_proper_type(typ) - if ( - var.is_initialized_in_class - and (not is_instance_var(var) or mx.is_operator) - ): + if var.is_initialized_in_class and (not is_instance_var(var) or mx.is_operator): call_type: ProperType if isinstance(typ, FunctionLike) and not typ.is_type_obj(): call_type = typ From 187f01a2278a5ea3d20339d27b21236fa7c85ba6 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Wed, 23 Aug 2023 00:47:33 -0700 Subject: [PATCH 4/5] test --- test-data/unit/check-functions.test | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index f49541420cc0..4cc523a595d1 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -3158,3 +3158,20 @@ class C(A, B): class D(A, B): def f(self, z: int) -> str: pass # E: Method "f" is not using @override but is overriding a method in class "__main__.A" [typing fixtures/typing-override.pyi] + +[case testCallableProperty] +from typing import Callable + +class something_callable: + def __call__(self, fn) -> str: ... + +def decorator(fn: Callable[..., int]) -> something_callable: ... + +class A: + @property + @decorator + def f(self) -> int: ... + +reveal_type(A.f) # N: Revealed type is "__main__.something_callable" +reveal_type(A().f) # N: Revealed type is "builtins.str" +[builtins fixtures/property.pyi] From 01700405bf64eb7cb77c9ed89c337f2c48a6d749 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 29 Aug 2023 18:22:28 -0700 Subject: [PATCH 5/5] reduce nesting --- mypy/checkmember.py | 78 +++++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 31a742045d37..0d26f7da30f2 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Callable, Sequence, cast +from typing import TYPE_CHECKING, Callable, Optional, Sequence, cast from mypy import meet, message_registry, subtypes from mypy.erasetype import erase_typevars @@ -776,50 +776,52 @@ def analyze_var( freeze_all_type_vars(t) result: Type = t typ = get_proper_type(typ) + + call_type: Optional[ProperType] = None if var.is_initialized_in_class and (not is_instance_var(var) or mx.is_operator): - call_type: ProperType if isinstance(typ, FunctionLike) and not typ.is_type_obj(): call_type = typ elif var.is_property: call_type = get_proper_type(_analyze_member_access("__call__", typ, mx)) else: call_type = typ - if isinstance(call_type, FunctionLike) and not call_type.is_type_obj(): - if mx.is_lvalue: - if var.is_property: - if not var.is_settable_property: - mx.msg.read_only_property(name, itype.type, mx.context) - else: - mx.msg.cant_assign_to_method(mx.context) - - if not var.is_staticmethod: - # Class-level function objects and classmethods become bound methods: - # the former to the instance, the latter to the class. - functype = call_type - # Use meet to narrow original_type to the dispatched type. - # For example, assume - # * A.f: Callable[[A1], None] where A1 <: A (maybe A1 == A) - # * B.f: Callable[[B1], None] where B1 <: B (maybe B1 == B) - # * x: Union[A1, B1] - # In `x.f`, when checking `x` against A1 we assume x is compatible with A - # and similarly for B1 when checking against B - dispatched_type = meet.meet_types(mx.original_type, itype) - signature = freshen_all_functions_type_vars(functype) - bound = get_proper_type(expand_self_type(var, signature, mx.original_type)) - assert isinstance(bound, FunctionLike) - signature = bound - signature = check_self_arg( - signature, dispatched_type, var.is_classmethod, mx.context, name, mx.msg - ) - signature = bind_self(signature, mx.self_type, var.is_classmethod) - expanded_signature = expand_type_by_instance(signature, itype) - freeze_all_type_vars(expanded_signature) - if var.is_property: - # A property cannot have an overloaded type => the cast is fine. - assert isinstance(expanded_signature, CallableType) - result = expanded_signature.ret_type - else: - result = expanded_signature + + if isinstance(call_type, FunctionLike) and not call_type.is_type_obj(): + if mx.is_lvalue: + if var.is_property: + if not var.is_settable_property: + mx.msg.read_only_property(name, itype.type, mx.context) + else: + mx.msg.cant_assign_to_method(mx.context) + + if not var.is_staticmethod: + # Class-level function objects and classmethods become bound methods: + # the former to the instance, the latter to the class. + functype: FunctionLike = call_type + # Use meet to narrow original_type to the dispatched type. + # For example, assume + # * A.f: Callable[[A1], None] where A1 <: A (maybe A1 == A) + # * B.f: Callable[[B1], None] where B1 <: B (maybe B1 == B) + # * x: Union[A1, B1] + # In `x.f`, when checking `x` against A1 we assume x is compatible with A + # and similarly for B1 when checking against B + dispatched_type = meet.meet_types(mx.original_type, itype) + signature = freshen_all_functions_type_vars(functype) + bound = get_proper_type(expand_self_type(var, signature, mx.original_type)) + assert isinstance(bound, FunctionLike) + signature = bound + signature = check_self_arg( + signature, dispatched_type, var.is_classmethod, mx.context, name, mx.msg + ) + signature = bind_self(signature, mx.self_type, var.is_classmethod) + expanded_signature = expand_type_by_instance(signature, itype) + freeze_all_type_vars(expanded_signature) + if var.is_property: + # A property cannot have an overloaded type => the cast is fine. + assert isinstance(expanded_signature, CallableType) + result = expanded_signature.ret_type + else: + result = expanded_signature else: if not var.is_ready and not mx.no_deferral: mx.not_ready_callback(var.name, mx.context)