diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 44a20341807bd..5071709613c9a 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -870,15 +870,13 @@ def analyze_var( mx.msg.read_only_property(name, itype.type, mx.context) if var.is_classvar: mx.msg.cant_assign_to_classvar(name, mx.context) - t = freshen_all_functions_type_vars(typ) - t = expand_self_type_if_needed(t, mx, var, original_itype) - t = expand_type_by_instance(t, itype) - freeze_all_type_vars(t) - result = t - typ = get_proper_type(typ) + # This is the most common case for variables, so start with this. + result = expand_without_binding(typ, var, itype, original_itype, mx) + # A non-None value indicates that we should actually bind self for this variable. 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: @@ -888,37 +886,23 @@ def analyze_var( else: call_type = typ + # Bound variables with callable types are treated like methods + # (these are usually method aliases like __rmul__ = __mul__). if isinstance(call_type, FunctionLike) and not call_type.is_type_obj(): - if mx.is_lvalue and not mx.suppress_errors: - if var.is_property and not var.is_settable_property: - mx.msg.read_only_property(name, itype.type, mx.context) - elif not var.is_property: - 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 - 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, mx.self_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) - if var.is_settable_property and mx.is_lvalue and var.setter_type is not None: - # TODO: use check_call() to infer better type, same as for __set__(). - result = expanded_signature.arg_types[0] - else: - result = expanded_signature.ret_type + if mx.is_lvalue and not var.is_property and not mx.suppress_errors: + mx.msg.cant_assign_to_method(mx.context) + + # Bind the self type for each callable component (when needed). + if call_type and not var.is_staticmethod: + bound_items = [] + for ct in call_type.items if isinstance(call_type, UnionType) else [call_type]: + p_ct = get_proper_type(ct) + if isinstance(p_ct, FunctionLike) and not p_ct.is_type_obj(): + item = expand_and_bind_callable(p_ct, var, itype, name, mx) else: - result = expanded_signature + item = expand_without_binding(ct, var, itype, original_itype, mx) + bound_items.append(item) + result = UnionType.make_union(bound_items) else: if not var.is_ready and not mx.no_deferral: mx.not_ready_callback(var.name, mx.context) @@ -937,6 +921,37 @@ def analyze_var( return result +def expand_without_binding( + typ: Type, var: Var, itype: Instance, original_itype: Instance, mx: MemberContext +) -> Type: + typ = freshen_all_functions_type_vars(typ) + typ = expand_self_type_if_needed(typ, mx, var, original_itype) + expanded = expand_type_by_instance(typ, itype) + freeze_all_type_vars(expanded) + return expanded + + +def expand_and_bind_callable( + functype: FunctionLike, var: Var, itype: Instance, name: str, mx: MemberContext +) -> Type: + functype = freshen_all_functions_type_vars(functype) + typ = get_proper_type(expand_self_type(var, functype, mx.original_type)) + assert isinstance(typ, FunctionLike) + typ = check_self_arg(typ, mx.self_type, var.is_classmethod, mx.context, name, mx.msg) + typ = bind_self(typ, mx.self_type, var.is_classmethod) + expanded = expand_type_by_instance(typ, itype) + freeze_all_type_vars(expanded) + if not var.is_property: + return expanded + # TODO: a decorated property can result in Overloaded here. + assert isinstance(expanded, CallableType) + if var.is_settable_property and mx.is_lvalue and var.setter_type is not None: + # TODO: use check_call() to infer better type, same as for __set__(). + return expanded.arg_types[0] + else: + return expanded.ret_type + + def expand_self_type_if_needed( t: Type, mx: MemberContext, var: Var, itype: Instance, is_class: bool = False ) -> Type: diff --git a/test-data/unit/check-classvar.test b/test-data/unit/check-classvar.test index 918926627bfd2..63bbd7471bc8b 100644 --- a/test-data/unit/check-classvar.test +++ b/test-data/unit/check-classvar.test @@ -334,3 +334,12 @@ class C: c:C c.foo() # E: Too few arguments \ # N: "foo" is considered instance variable, to make it class variable use ClassVar[...] + +[case testClassVarUnionBoundOnInstance] +from typing import Union, Callable, ClassVar + +class C: + def f(self) -> int: ... + g: ClassVar[Union[Callable[[C], int], int]] = f + +reveal_type(C().g) # N: Revealed type is "Union[def () -> builtins.int, builtins.int]"