Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 44 additions & 41 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -776,47 +776,50 @@ 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)
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
if var.is_initialized_in_class and (not is_instance_var(var) or mx.is_operator):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to avoid extra nesting level by computing something before the first if? If yes, I would prefer that. This function is already hard to read.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

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
else:
if not var.is_ready and not mx.no_deferral:
mx.not_ready_callback(var.name, mx.context)
Expand Down
17 changes: 17 additions & 0 deletions test-data/unit/check-functions.test
Original file line number Diff line number Diff line change
Expand Up @@ -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: ...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

People recently complained (don't remember where) that this use case:

def make_method() -> SomeCallbackProtocol: ...
class A:
    meth = make_method()

is not handled the same way as

def make_method_callable() -> Callable[[int], int]: ...
class B:
    meth = make_method_callable()

(because certain special-casing only applies to callable types). I am 95% sure this PR should fix that as well (or maybe with very few modifications), if yes, could you please add such test as well?

Copy link
Collaborator Author

@hauntsaninja hauntsaninja Aug 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for mentioning this! It's not fixed by this change, but should be doable fix. I will make a follow up PR


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]