Skip to content

Commit d6cb14f

Browse files
authored
Fix descriptor overload selection (#18868)
Fixes #15921 I know there were previously concerns about performance of `check_self_arg()`, but note that the code path where I add it only affects descriptors and `__getattr__`/`__setattr__`, so I think it should be OK.
1 parent a10c6f1 commit d6cb14f

File tree

2 files changed

+52
-6
lines changed

2 files changed

+52
-6
lines changed

mypy/checkmember.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
erase_to_bound,
4343
freeze_all_type_vars,
4444
function_type,
45+
get_all_type_vars,
4546
get_type_vars,
4647
make_simplified_union,
4748
supported_self_type,
@@ -604,7 +605,10 @@ def analyze_member_var_access(
604605
setattr_meth = info.get_method("__setattr__")
605606
if setattr_meth and setattr_meth.info.fullname != "builtins.object":
606607
bound_type = analyze_decorator_or_funcbase_access(
607-
defn=setattr_meth, itype=itype, name=name, mx=mx.copy_modified(is_lvalue=False)
608+
defn=setattr_meth,
609+
itype=itype,
610+
name="__setattr__",
611+
mx=mx.copy_modified(is_lvalue=False),
608612
)
609613
typ = map_instance_to_supertype(itype, setattr_meth.info)
610614
setattr_type = get_proper_type(expand_type_by_instance(bound_type, typ))
@@ -1031,7 +1035,16 @@ def f(self: S) -> T: ...
10311035
selfarg = get_proper_type(item.arg_types[0])
10321036
# This level of erasure matches the one in checker.check_func_def(),
10331037
# better keep these two checks consistent.
1034-
if subtypes.is_subtype(dispatched_arg_type, erase_typevars(erase_to_bound(selfarg))):
1038+
if subtypes.is_subtype(
1039+
dispatched_arg_type,
1040+
erase_typevars(erase_to_bound(selfarg)),
1041+
# This is to work around the fact that erased ParamSpec and TypeVarTuple
1042+
# callables are not always compatible with non-erased ones both ways.
1043+
always_covariant=any(
1044+
not isinstance(tv, TypeVarType) for tv in get_all_type_vars(selfarg)
1045+
),
1046+
ignore_pos_arg_names=True,
1047+
):
10351048
new_items.append(item)
10361049
elif isinstance(selfarg, ParamSpecType):
10371050
# TODO: This is not always right. What's the most reasonable thing to do here?
@@ -1164,6 +1177,7 @@ def analyze_class_attribute_access(
11641177
def_vars = set(node.node.info.defn.type_vars)
11651178
if not node.node.is_classvar and node.node.info.self_type:
11661179
def_vars.add(node.node.info.self_type)
1180+
# TODO: should we include ParamSpec etc. here (i.e. use get_all_type_vars)?
11671181
typ_vars = set(get_type_vars(t))
11681182
if def_vars & typ_vars:
11691183
# Exception: access on Type[...], including first argument of class methods is OK.
@@ -1405,6 +1419,6 @@ def analyze_decorator_or_funcbase_access(
14051419
"""
14061420
if isinstance(defn, Decorator):
14071421
return analyze_var(name, defn.var, itype, mx)
1408-
return bind_self(
1409-
function_type(defn, mx.chk.named_type("builtins.function")), original_type=mx.self_type
1410-
)
1422+
typ = function_type(defn, mx.chk.named_type("builtins.function"))
1423+
typ = check_self_arg(typ, mx.self_type, defn.is_class, mx.context, name, mx.msg)
1424+
return bind_self(typ, original_type=mx.self_type, is_classmethod=defn.is_class)

test-data/unit/check-classes.test

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3135,7 +3135,8 @@ from typing import Any
31353135
class Test:
31363136
def __setattr__() -> None: ... # E: Method must have at least one argument. Did you forget the "self" argument? # E: Invalid signature "Callable[[], None]" for "__setattr__"
31373137
t = Test()
3138-
t.crash = 'test' # E: "Test" has no attribute "crash"
3138+
t.crash = 'test' # E: Attribute function "__setattr__" with type "Callable[[], None]" does not accept self argument \
3139+
# E: "Test" has no attribute "crash"
31393140

31403141
class A:
31413142
def __setattr__(self): ... # E: Invalid signature "Callable[[A], Any]" for "__setattr__"
@@ -8648,3 +8649,34 @@ class C(B):
86488649
def meth(self) -> None:
86498650
def cb() -> None:
86508651
self.x: int = 1 # E: Incompatible types in assignment (expression has type "int", base class "B" defined the type as "str")
8652+
8653+
[case testOverloadedDescriptorSelected]
8654+
from typing import Generic, TypeVar, Any, overload
8655+
8656+
T_co = TypeVar("T_co", covariant=True)
8657+
class Field(Generic[T_co]):
8658+
@overload
8659+
def __get__(self: Field[bool], instance: None, owner: Any) -> BoolField: ...
8660+
@overload
8661+
def __get__(self: Field[int], instance: None, owner: Any) -> NumField: ...
8662+
@overload
8663+
def __get__(self: Field[Any], instance: None, owner: Any) -> AnyField[T_co]: ...
8664+
@overload
8665+
def __get__(self, instance: Any, owner: Any) -> T_co: ...
8666+
8667+
def __get__(self, instance: Any, owner: Any) -> Any:
8668+
pass
8669+
8670+
class BoolField(Field[bool]): ...
8671+
class NumField(Field[int]): ...
8672+
class AnyField(Field[T_co]): ...
8673+
class Custom: ...
8674+
8675+
class Fields:
8676+
bool_f: Field[bool]
8677+
int_f: Field[int]
8678+
custom_f: Field[Custom]
8679+
8680+
reveal_type(Fields.bool_f) # N: Revealed type is "__main__.BoolField"
8681+
reveal_type(Fields.int_f) # N: Revealed type is "__main__.NumField"
8682+
reveal_type(Fields.custom_f) # N: Revealed type is "__main__.AnyField[__main__.Custom]"

0 commit comments

Comments
 (0)