Skip to content

Commit 4e6d753

Browse files
Michael0x2ailevkivskyi
authored andcommitted
Remove and refactor old overload selection logic (#5321)
This commit removes a few remnants of the old overload selection algorithm in checkexpr. Specifically, this commit: 1. Modifies `erased_signature_similarity` so it returns a bool instead of an int. 2. Simplifies how `erased_signature_similarity` handles types like `Type[X]`. Note: although this change relaxes and loosens the precision of `erased_signature_similarity`, it will not impact correctness: we now use this method exclusively as a heuristic to help us determine which overloads the user might have meant to select in our error messages. As a consequence, loosening this function actually led to a more helpful error message in one case. 3. Remove the `match_signature_types` method. It didn't appear as if anybody was calling it.
1 parent 1d06efa commit 4e6d753

File tree

3 files changed

+85
-132
lines changed

3 files changed

+85
-132
lines changed

mypy/checkexpr.py

Lines changed: 50 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1575,12 +1575,9 @@ def union_overload_matches(self, types: Sequence[Type]) -> Union[AnyType, Callab
15751575
def erased_signature_similarity(self, arg_types: List[Type], arg_kinds: List[int],
15761576
arg_names: Optional[Sequence[Optional[str]]],
15771577
callee: CallableType,
1578-
context: Context) -> int:
1579-
"""Determine whether arguments could match the signature at runtime.
1580-
1581-
Return similarity level (0 = no match, 1 = can match, 2 = non-promotion match). See
1582-
overload_arg_similarity for a discussion of similarity levels.
1583-
"""
1578+
context: Context) -> bool:
1579+
"""Determine whether arguments could match the signature at runtime, after
1580+
erasing types."""
15841581
formal_to_actual = map_actuals_to_formals(arg_kinds,
15851582
arg_names,
15861583
callee.arg_kinds,
@@ -1590,55 +1587,22 @@ def erased_signature_similarity(self, arg_types: List[Type], arg_kinds: List[int
15901587
if not self.check_argument_count(callee, arg_types, arg_kinds, arg_names,
15911588
formal_to_actual, None, None):
15921589
# Too few or many arguments -> no match.
1593-
return 0
1594-
1595-
similarity = 2
1590+
return False
15961591

15971592
def check_arg(caller_type: Type, original_caller_type: Type, caller_kind: int,
15981593
callee_type: Type, n: int, m: int, callee: CallableType,
15991594
context: Context, messages: MessageBuilder) -> None:
1600-
nonlocal similarity
1601-
similarity = min(similarity,
1602-
overload_arg_similarity(caller_type, callee_type))
1603-
if similarity == 0:
1595+
if not arg_approximate_similarity(caller_type, callee_type):
16041596
# No match -- exit early since none of the remaining work can change
16051597
# the result.
16061598
raise Finished
16071599

16081600
try:
16091601
self.check_argument_types(arg_types, arg_kinds, callee, formal_to_actual,
16101602
context=context, check_arg=check_arg)
1603+
return True
16111604
except Finished:
1612-
pass
1613-
1614-
return similarity
1615-
1616-
def match_signature_types(self, arg_types: List[Type], arg_kinds: List[int],
1617-
arg_names: Optional[Sequence[Optional[str]]], callee: CallableType,
1618-
context: Context) -> bool:
1619-
"""Determine whether arguments types match the signature.
1620-
1621-
Assume that argument counts are compatible.
1622-
1623-
Return True if arguments match.
1624-
"""
1625-
formal_to_actual = map_actuals_to_formals(arg_kinds,
1626-
arg_names,
1627-
callee.arg_kinds,
1628-
callee.arg_names,
1629-
lambda i: arg_types[i])
1630-
ok = True
1631-
1632-
def check_arg(caller_type: Type, original_caller_type: Type, caller_kind: int,
1633-
callee_type: Type, n: int, m: int, callee: CallableType,
1634-
context: Context, messages: MessageBuilder) -> None:
1635-
nonlocal ok
1636-
if not is_subtype(caller_type, callee_type):
1637-
ok = False
1638-
1639-
self.check_argument_types(arg_types, arg_kinds, callee, formal_to_actual,
1640-
context=context, check_arg=check_arg)
1641-
return ok
1605+
return False
16421606

16431607
def apply_generic_arguments(self, callable: CallableType, types: Sequence[Optional[Type]],
16441608
context: Context) -> CallableType:
@@ -3399,101 +3363,68 @@ def visit_uninhabited_type(self, t: UninhabitedType) -> bool:
33993363
return True
34003364

34013365

3402-
def overload_arg_similarity(actual: Type, formal: Type) -> int:
3403-
"""Return if caller argument (actual) is compatible with overloaded signature arg (formal).
3366+
def arg_approximate_similarity(actual: Type, formal: Type) -> bool:
3367+
"""Return if caller argument (actual) is roughly compatible with signature arg (formal).
34043368
3405-
Return a similarity level:
3406-
0: no match
3407-
1: actual is compatible, but only using type promotions (e.g. int vs float)
3408-
2: actual is compatible without type promotions (e.g. int vs object)
3369+
This function is deliberately loose and will report two types are similar
3370+
as long as their "shapes" are plausibly the same.
34093371
3410-
The distinction is important in cases where multiple overload items match. We want
3411-
give priority to higher similarity matches.
3372+
This is useful when we're doing error reporting: for example, if we're trying
3373+
to select an overload alternative and there's no exact match, we can use
3374+
this function to help us identify which alternative the user might have
3375+
*meant* to match.
34123376
"""
3413-
# Replace type variables with their upper bounds. Overloading
3414-
# resolution is based on runtime behavior which erases type
3415-
# parameters, so no need to handle type variables occurring within
3416-
# a type.
3377+
3378+
# Erase typevars: we'll consider them all to have the same "shape".
3379+
34173380
if isinstance(actual, TypeVarType):
34183381
actual = actual.erase_to_union_or_bound()
34193382
if isinstance(formal, TypeVarType):
34203383
formal = formal.erase_to_union_or_bound()
3421-
if (isinstance(actual, UninhabitedType) or isinstance(actual, AnyType) or
3422-
isinstance(formal, AnyType) or
3423-
(isinstance(actual, Instance) and actual.type.fallback_to_any)):
3424-
# These could match anything at runtime.
3425-
return 2
3384+
3385+
# Callable or Type[...]-ish types
3386+
3387+
def is_typetype_like(typ: Type) -> bool:
3388+
return (isinstance(typ, TypeType)
3389+
or (isinstance(typ, FunctionLike) and typ.is_type_obj())
3390+
or (isinstance(typ, Instance) and typ.type.fullname() == "builtins.type"))
3391+
34263392
if isinstance(formal, CallableType):
3427-
if isinstance(actual, (CallableType, Overloaded)):
3428-
# TODO: do more sophisticated callable matching
3429-
return 2
3430-
if isinstance(actual, TypeType):
3431-
return 2 if is_subtype(actual, formal) else 0
3432-
if isinstance(actual, NoneTyp):
3433-
if not experiments.STRICT_OPTIONAL:
3434-
# NoneTyp matches anything if we're not doing strict Optional checking
3435-
return 2
3436-
else:
3437-
# NoneType is a subtype of object
3438-
if isinstance(formal, Instance) and formal.type.fullname() == "builtins.object":
3439-
return 2
3393+
if isinstance(actual, (CallableType, Overloaded, TypeType)):
3394+
return True
3395+
if is_typetype_like(actual) and is_typetype_like(formal):
3396+
return True
3397+
3398+
# Unions
3399+
34403400
if isinstance(actual, UnionType):
3441-
return max(overload_arg_similarity(item, formal)
3442-
for item in actual.relevant_items())
3401+
return any(arg_approximate_similarity(item, formal) for item in actual.relevant_items())
34433402
if isinstance(formal, UnionType):
3444-
return max(overload_arg_similarity(actual, item)
3445-
for item in formal.relevant_items())
3446-
if isinstance(formal, TypeType):
3447-
if isinstance(actual, TypeType):
3448-
# Since Type[T] is covariant, check if actual = Type[A] is
3449-
# a subtype of formal = Type[F].
3450-
return overload_arg_similarity(actual.item, formal.item)
3451-
elif isinstance(actual, FunctionLike) and actual.is_type_obj():
3452-
# Check if the actual is a constructor of some sort.
3453-
# Note that this is this unsound, since we don't check the __init__ signature.
3454-
return overload_arg_similarity(actual.items()[0].ret_type, formal.item)
3455-
else:
3456-
return 0
3403+
return any(arg_approximate_similarity(actual, item) for item in formal.relevant_items())
3404+
3405+
# TypedDicts
3406+
34573407
if isinstance(actual, TypedDictType):
34583408
if isinstance(formal, TypedDictType):
3459-
# Don't support overloading based on the keys or value types of a TypedDict since
3460-
# that would be complicated and probably only marginally useful.
3461-
return 2
3462-
return overload_arg_similarity(actual.fallback, formal)
3409+
return True
3410+
return arg_approximate_similarity(actual.fallback, formal)
3411+
3412+
# Instances
3413+
# For instances, we mostly defer to the existing is_subtype check.
3414+
34633415
if isinstance(formal, Instance):
34643416
if isinstance(actual, CallableType):
34653417
actual = actual.fallback
34663418
if isinstance(actual, Overloaded):
34673419
actual = actual.items()[0].fallback
34683420
if isinstance(actual, TupleType):
34693421
actual = actual.fallback
3470-
if isinstance(actual, Instance):
3471-
# First perform a quick check (as an optimization) and fall back to generic
3472-
# subtyping algorithm if type promotions are possible (e.g., int vs. float).
3473-
if formal.type in actual.type.mro:
3474-
return 2
3475-
elif formal.type.is_protocol and is_subtype(actual, erasetype.erase_type(formal)):
3476-
return 2
3477-
elif actual.type._promote and is_subtype(actual, formal):
3478-
return 1
3479-
else:
3480-
return 0
3481-
elif isinstance(actual, TypeType):
3482-
item = actual.item
3483-
if formal.type.fullname() in {"builtins.object", "builtins.type"}:
3484-
return 2
3485-
elif isinstance(item, Instance) and item.type.metaclass_type:
3486-
# FIX: this does not handle e.g. Union of instances
3487-
return overload_arg_similarity(item.type.metaclass_type, formal)
3488-
else:
3489-
return 0
3490-
else:
3491-
return 0
3492-
if isinstance(actual, UnboundType) or isinstance(formal, UnboundType):
3493-
# Either actual or formal is the result of an error; shut up.
3494-
return 2
3495-
# Fall back to a conservative equality check for the remaining kinds of type.
3496-
return 2 if is_same_type(erasetype.erase_type(actual), erasetype.erase_type(formal)) else 0
3422+
if isinstance(actual, Instance) and formal.type in actual.type.mro:
3423+
# Try performing a quick check as an optimization
3424+
return True
3425+
3426+
# Fall back to a standard subtype check for the remaining kinds of type.
3427+
return is_subtype(erasetype.erase_type(actual), erasetype.erase_type(formal))
34973428

34983429

34993430
def any_causes_overload_ambiguity(items: List[CallableType],

test-data/unit/check-classes.test

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1223,11 +1223,8 @@ class D:
12231223
def __get__(self, inst: Base, own: Type[Base]) -> str: pass
12241224
[builtins fixtures/bool.pyi]
12251225
[out]
1226-
main:5: error: Revealed type is 'Any'
1227-
main:5: error: No overload variant of "__get__" of "D" matches argument types "None", "Type[A]"
1228-
main:5: note: Possible overload variants:
1229-
main:5: note: def __get__(self, inst: None, own: Type[Base]) -> D
1230-
main:5: note: def __get__(self, inst: Base, own: Type[Base]) -> str
1226+
main:5: error: Revealed type is 'd.D'
1227+
main:5: error: Argument 2 to "__get__" of "D" has incompatible type "Type[A]"; expected "Type[Base]"
12311228
main:6: error: Revealed type is 'Any'
12321229
main:6: error: No overload variant of "__get__" of "D" matches argument types "A", "Type[A]"
12331230
main:6: note: Possible overload variants:
@@ -3016,16 +3013,10 @@ def f(a: Type[B]) -> None: pass
30163013
@overload
30173014
def f(a: int) -> None: pass
30183015

3019-
f(A) # E: No overload variant of "f" matches argument type "Type[A]" \
3020-
# N: Possible overload variants: \
3021-
# N: def f(a: Type[B]) -> None \
3022-
# N: def f(a: int) -> None
3016+
f(A) # E: Argument 1 to "f" has incompatible type "Type[A]"; expected "Type[B]"
30233017
f(B)
30243018
f(C)
3025-
f(AType) # E: No overload variant of "f" matches argument type "Type[A]" \
3026-
# N: Possible overload variants: \
3027-
# N: def f(a: Type[B]) -> None \
3028-
# N: def f(a: int) -> None
3019+
f(AType) # E: Argument 1 to "f" has incompatible type "Type[A]"; expected "Type[B]"
30293020
f(BType)
30303021
f(CType)
30313022
[builtins fixtures/classmethod.pyi]

test-data/unit/check-overloading.test

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4374,3 +4374,34 @@ def bar(x: T) -> T: ...
43744374
@overload
43754375
def bar(x: Any) -> int: ...
43764376
[out]
4377+
4378+
[case testBadOverloadProbableMatch]
4379+
from typing import overload, List, Type
4380+
4381+
class Other: pass
4382+
4383+
@overload
4384+
def multiple_plausible(x: int) -> int: ...
4385+
@overload
4386+
def multiple_plausible(x: str) -> str: ...
4387+
def multiple_plausible(x): pass
4388+
4389+
4390+
@overload
4391+
def single_plausible(x: Type[int]) -> int: ...
4392+
@overload
4393+
def single_plausible(x: List[str]) -> str: ...
4394+
def single_plausible(x): pass
4395+
4396+
a = multiple_plausible(Other()) # E: No overload variant of "multiple_plausible" matches argument type "Other" \
4397+
# N: Possible overload variants: \
4398+
# N: def multiple_plausible(x: int) -> int \
4399+
# N: def multiple_plausible(x: str) -> str
4400+
reveal_type(a) # E: Revealed type is 'Any'
4401+
4402+
b = single_plausible(Other) # E: Argument 1 to "single_plausible" has incompatible type "Type[Other]"; expected "Type[int]"
4403+
reveal_type(b) # E: Revealed type is 'builtins.int'
4404+
4405+
c = single_plausible([Other()]) # E: List item 0 has incompatible type "Other"; expected "str"
4406+
reveal_type(c) # E: Revealed type is 'builtins.str'
4407+
[builtins fixtures/list.pyi]

0 commit comments

Comments
 (0)