From 0824ce81f7e08633622339023ee9c6764743d3cc Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 1 Apr 2025 13:25:45 +0100 Subject: [PATCH 1/2] Fix descriptor overload selection --- mypy/checkmember.py | 15 +++++++++----- test-data/unit/check-classes.test | 34 ++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 44a20341807b..eb3ea9cc09d2 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -604,7 +604,10 @@ def analyze_member_var_access( setattr_meth = info.get_method("__setattr__") if setattr_meth and setattr_meth.info.fullname != "builtins.object": bound_type = analyze_decorator_or_funcbase_access( - defn=setattr_meth, itype=itype, name=name, mx=mx.copy_modified(is_lvalue=False) + defn=setattr_meth, + itype=itype, + name="__setattr__", + mx=mx.copy_modified(is_lvalue=False), ) typ = map_instance_to_supertype(itype, setattr_meth.info) setattr_type = get_proper_type(expand_type_by_instance(bound_type, typ)) @@ -1016,7 +1019,9 @@ def f(self: S) -> T: ... selfarg = get_proper_type(item.arg_types[0]) # This level of erasure matches the one in checker.check_func_def(), # better keep these two checks consistent. - if subtypes.is_subtype(dispatched_arg_type, erase_typevars(erase_to_bound(selfarg))): + if subtypes.is_subtype( + dispatched_arg_type, erase_typevars(erase_to_bound(selfarg)), always_covariant=True + ): new_items.append(item) elif isinstance(selfarg, ParamSpecType): # TODO: This is not always right. What's the most reasonable thing to do here? @@ -1390,6 +1395,6 @@ def analyze_decorator_or_funcbase_access( """ if isinstance(defn, Decorator): return analyze_var(name, defn.var, itype, mx) - return bind_self( - function_type(defn, mx.chk.named_type("builtins.function")), original_type=mx.self_type - ) + typ = function_type(defn, mx.chk.named_type("builtins.function")) + typ = check_self_arg(typ, mx.self_type, defn.is_class, mx.context, name, mx.msg) + return bind_self(typ, original_type=mx.self_type, is_classmethod=defn.is_class) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index e9667db3086e..559088f34a31 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -3135,7 +3135,8 @@ from typing import Any class Test: def __setattr__() -> None: ... # E: Method must have at least one argument. Did you forget the "self" argument? # E: Invalid signature "Callable[[], None]" for "__setattr__" t = Test() -t.crash = 'test' # E: "Test" has no attribute "crash" +t.crash = 'test' # E: Attribute function "__setattr__" with type "Callable[[], None]" does not accept self argument \ + # E: "Test" has no attribute "crash" class A: def __setattr__(self): ... # E: Invalid signature "Callable[[A], Any]" for "__setattr__" @@ -8648,3 +8649,34 @@ class C(B): def meth(self) -> None: def cb() -> None: self.x: int = 1 # E: Incompatible types in assignment (expression has type "int", base class "B" defined the type as "str") + +[case testOverloadedDescriptorSelected] +from typing import Generic, TypeVar, Any, overload + +T_co = TypeVar("T_co", covariant=True) +class Field(Generic[T_co]): + @overload + def __get__(self: Field[bool], instance: None, owner: Any) -> BoolField: ... + @overload + def __get__(self: Field[int], instance: None, owner: Any) -> NumField: ... + @overload + def __get__(self: Field[Any], instance: None, owner: Any) -> AnyField[T_co]: ... + @overload + def __get__(self, instance: Any, owner: Any) -> T_co: ... + + def __get__(self, instance: Any, owner: Any) -> Any: + pass + +class BoolField(Field[bool]): ... +class NumField(Field[int]): ... +class AnyField(Field[T_co]): ... +class Custom: ... + +class Fields: + bool_f: Field[bool] + int_f: Field[int] + custom_f: Field[Custom] + +reveal_type(Fields.bool_f) # N: Revealed type is "__main__.BoolField" +reveal_type(Fields.int_f) # N: Revealed type is "__main__.NumField" +reveal_type(Fields.custom_f) # N: Revealed type is "__main__.AnyField[__main__.Custom]" From ad7eaf3f439b5ea5e55ac6e7460be23025c9cdef Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 1 Apr 2025 15:37:10 +0100 Subject: [PATCH 2/2] Try dedicated hack for ParamSpec corner case --- mypy/checkmember.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index eb3ea9cc09d2..b01aa1b9d2f8 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -42,6 +42,7 @@ erase_to_bound, freeze_all_type_vars, function_type, + get_all_type_vars, get_type_vars, make_simplified_union, supported_self_type, @@ -1020,7 +1021,14 @@ def f(self: S) -> T: ... # This level of erasure matches the one in checker.check_func_def(), # better keep these two checks consistent. if subtypes.is_subtype( - dispatched_arg_type, erase_typevars(erase_to_bound(selfarg)), always_covariant=True + dispatched_arg_type, + erase_typevars(erase_to_bound(selfarg)), + # This is to work around the fact that erased ParamSpec and TypeVarTuple + # callables are not always compatible with non-erased ones both ways. + always_covariant=any( + not isinstance(tv, TypeVarType) for tv in get_all_type_vars(selfarg) + ), + ignore_pos_arg_names=True, ): new_items.append(item) elif isinstance(selfarg, ParamSpecType): @@ -1154,6 +1162,7 @@ def analyze_class_attribute_access( def_vars = set(node.node.info.defn.type_vars) if not node.node.is_classvar and node.node.info.self_type: def_vars.add(node.node.info.self_type) + # TODO: should we include ParamSpec etc. here (i.e. use get_all_type_vars)? typ_vars = set(get_type_vars(t)) if def_vars & typ_vars: # Exception: access on Type[...], including first argument of class methods is OK.