Skip to content

Remove and refactor old overload selection logic #5321

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
169 changes: 50 additions & 119 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -1575,12 +1575,9 @@ def union_overload_matches(self, types: Sequence[Type]) -> Union[AnyType, Callab
def erased_signature_similarity(self, arg_types: List[Type], arg_kinds: List[int],
arg_names: Optional[Sequence[Optional[str]]],
callee: CallableType,
context: Context) -> int:
"""Determine whether arguments could match the signature at runtime.

Return similarity level (0 = no match, 1 = can match, 2 = non-promotion match). See
overload_arg_similarity for a discussion of similarity levels.
"""
context: Context) -> bool:
"""Determine whether arguments could match the signature at runtime, after
erasing types."""
formal_to_actual = map_actuals_to_formals(arg_kinds,
arg_names,
callee.arg_kinds,
Expand All @@ -1590,55 +1587,22 @@ def erased_signature_similarity(self, arg_types: List[Type], arg_kinds: List[int
if not self.check_argument_count(callee, arg_types, arg_kinds, arg_names,
formal_to_actual, None, None):
# Too few or many arguments -> no match.
return 0

similarity = 2
return False

def check_arg(caller_type: Type, original_caller_type: Type, caller_kind: int,
callee_type: Type, n: int, m: int, callee: CallableType,
context: Context, messages: MessageBuilder) -> None:
nonlocal similarity
similarity = min(similarity,
overload_arg_similarity(caller_type, callee_type))
if similarity == 0:
if not arg_approximate_similarity(caller_type, callee_type):
# No match -- exit early since none of the remaining work can change
# the result.
raise Finished

try:
self.check_argument_types(arg_types, arg_kinds, callee, formal_to_actual,
context=context, check_arg=check_arg)
return True
except Finished:
pass

return similarity

def match_signature_types(self, arg_types: List[Type], arg_kinds: List[int],
arg_names: Optional[Sequence[Optional[str]]], callee: CallableType,
context: Context) -> bool:
"""Determine whether arguments types match the signature.

Assume that argument counts are compatible.

Return True if arguments match.
"""
formal_to_actual = map_actuals_to_formals(arg_kinds,
arg_names,
callee.arg_kinds,
callee.arg_names,
lambda i: arg_types[i])
ok = True

def check_arg(caller_type: Type, original_caller_type: Type, caller_kind: int,
callee_type: Type, n: int, m: int, callee: CallableType,
context: Context, messages: MessageBuilder) -> None:
nonlocal ok
if not is_subtype(caller_type, callee_type):
ok = False

self.check_argument_types(arg_types, arg_kinds, callee, formal_to_actual,
context=context, check_arg=check_arg)
return ok
return False

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


def overload_arg_similarity(actual: Type, formal: Type) -> int:
"""Return if caller argument (actual) is compatible with overloaded signature arg (formal).
def arg_approximate_similarity(actual: Type, formal: Type) -> bool:
"""Return if caller argument (actual) is roughly compatible with signature arg (formal).

Return a similarity level:
0: no match
1: actual is compatible, but only using type promotions (e.g. int vs float)
2: actual is compatible without type promotions (e.g. int vs object)
This function is deliberately loose and will report two types are similar
as long as their "shapes" are plausibly the same.

The distinction is important in cases where multiple overload items match. We want
give priority to higher similarity matches.
This is useful when we're doing error reporting: for example, if we're trying
to select an overload alternative and there's no exact match, we can use
this function to help us identify which alternative the user might have
*meant* to match.
"""
# Replace type variables with their upper bounds. Overloading
# resolution is based on runtime behavior which erases type
# parameters, so no need to handle type variables occurring within
# a type.

# Erase typevars: we'll consider them all to have the same "shape".

if isinstance(actual, TypeVarType):
actual = actual.erase_to_union_or_bound()
if isinstance(formal, TypeVarType):
formal = formal.erase_to_union_or_bound()
if (isinstance(actual, UninhabitedType) or isinstance(actual, AnyType) or
isinstance(formal, AnyType) or
(isinstance(actual, Instance) and actual.type.fallback_to_any)):
# These could match anything at runtime.
return 2

# Callable or Type[...]-ish types

def is_typetype_like(typ: Type) -> bool:
return (isinstance(typ, TypeType)
or (isinstance(typ, FunctionLike) and typ.is_type_obj())
or (isinstance(typ, Instance) and typ.type.fullname() == "builtins.type"))

if isinstance(formal, CallableType):
if isinstance(actual, (CallableType, Overloaded)):
# TODO: do more sophisticated callable matching
return 2
if isinstance(actual, TypeType):
return 2 if is_subtype(actual, formal) else 0
if isinstance(actual, NoneTyp):
if not experiments.STRICT_OPTIONAL:
# NoneTyp matches anything if we're not doing strict Optional checking
return 2
else:
# NoneType is a subtype of object
if isinstance(formal, Instance) and formal.type.fullname() == "builtins.object":
return 2
if isinstance(actual, (CallableType, Overloaded, TypeType)):
return True
if is_typetype_like(actual) and is_typetype_like(formal):
return True

# Unions

if isinstance(actual, UnionType):
return max(overload_arg_similarity(item, formal)
for item in actual.relevant_items())
return any(arg_approximate_similarity(item, formal) for item in actual.relevant_items())
if isinstance(formal, UnionType):
return max(overload_arg_similarity(actual, item)
for item in formal.relevant_items())
if isinstance(formal, TypeType):
if isinstance(actual, TypeType):
# Since Type[T] is covariant, check if actual = Type[A] is
# a subtype of formal = Type[F].
return overload_arg_similarity(actual.item, formal.item)
elif isinstance(actual, FunctionLike) and actual.is_type_obj():
# Check if the actual is a constructor of some sort.
# Note that this is this unsound, since we don't check the __init__ signature.
return overload_arg_similarity(actual.items()[0].ret_type, formal.item)
else:
return 0
return any(arg_approximate_similarity(actual, item) for item in formal.relevant_items())

# TypedDicts

if isinstance(actual, TypedDictType):
if isinstance(formal, TypedDictType):
# Don't support overloading based on the keys or value types of a TypedDict since
# that would be complicated and probably only marginally useful.
return 2
return overload_arg_similarity(actual.fallback, formal)
return True
return arg_approximate_similarity(actual.fallback, formal)

# Instances
# For instances, we mostly defer to the existing is_subtype check.

if isinstance(formal, Instance):
if isinstance(actual, CallableType):
actual = actual.fallback
if isinstance(actual, Overloaded):
actual = actual.items()[0].fallback
if isinstance(actual, TupleType):
actual = actual.fallback
if isinstance(actual, Instance):
# First perform a quick check (as an optimization) and fall back to generic
# subtyping algorithm if type promotions are possible (e.g., int vs. float).
if formal.type in actual.type.mro:
return 2
elif formal.type.is_protocol and is_subtype(actual, erasetype.erase_type(formal)):
return 2
elif actual.type._promote and is_subtype(actual, formal):
return 1
else:
return 0
elif isinstance(actual, TypeType):
item = actual.item
if formal.type.fullname() in {"builtins.object", "builtins.type"}:
return 2
elif isinstance(item, Instance) and item.type.metaclass_type:
# FIX: this does not handle e.g. Union of instances
return overload_arg_similarity(item.type.metaclass_type, formal)
else:
return 0
else:
return 0
if isinstance(actual, UnboundType) or isinstance(formal, UnboundType):
# Either actual or formal is the result of an error; shut up.
return 2
# Fall back to a conservative equality check for the remaining kinds of type.
return 2 if is_same_type(erasetype.erase_type(actual), erasetype.erase_type(formal)) else 0
if isinstance(actual, Instance) and formal.type in actual.type.mro:
# Try performing a quick check as an optimization
return True

# Fall back to a standard subtype check for the remaining kinds of type.
return is_subtype(erasetype.erase_type(actual), erasetype.erase_type(formal))


def any_causes_overload_ambiguity(items: List[CallableType],
Expand Down
17 changes: 4 additions & 13 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -1223,11 +1223,8 @@ class D:
def __get__(self, inst: Base, own: Type[Base]) -> str: pass
[builtins fixtures/bool.pyi]
[out]
main:5: error: Revealed type is 'Any'
main:5: error: No overload variant of "__get__" of "D" matches argument types "None", "Type[A]"
main:5: note: Possible overload variants:
main:5: note: def __get__(self, inst: None, own: Type[Base]) -> D
main:5: note: def __get__(self, inst: Base, own: Type[Base]) -> str
main:5: error: Revealed type is 'd.D'
Copy link
Member

Choose a reason for hiding this comment

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

I am not sure this is a good idea to have d.D instead of Any. In case of errors we typically return Any to avoid other (potentially bogus) errors.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think in this case, what's happening is that there's actually only one plausible matching overload -- when we do A.f and f is a descriptor, we always end up calling the overload def __get__(self, inst: None, own: Type[Base]) -> D.

So, since only one variant is a plausible match, we just infer that variant's return type. (And regarding your comment below -- this is also why we don't bother showing the possible overload variants here).

I think this behavior is consistent with how we handle bad calls to regular functions. E.g. mypy will infer the return type instead of Any in this program:

def foo(x: int) -> int: 
    return x

a = foo("asdf")   # Error
reveal_type(a)  # Revealed type is 'builtins.int', not Any

Granted, I don't think it's obvious that only one variant will be selected here, so I added another test case for this behavior.

Copy link
Member

Choose a reason for hiding this comment

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

OK.

main:5: error: Argument 2 to "__get__" of "D" has incompatible type "Type[A]"; expected "Type[Base]"
Copy link
Member

Choose a reason for hiding this comment

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

Why don't we have the same nicer error for the case right below? (This is very low priority.)

main:6: error: Revealed type is 'Any'
main:6: error: No overload variant of "__get__" of "D" matches argument types "A", "Type[A]"
main:6: note: Possible overload variants:
Expand Down Expand Up @@ -3016,16 +3013,10 @@ def f(a: Type[B]) -> None: pass
@overload
def f(a: int) -> None: pass

f(A) # E: No overload variant of "f" matches argument type "Type[A]" \
# N: Possible overload variants: \
# N: def f(a: Type[B]) -> None \
# N: def f(a: int) -> None
f(A) # E: Argument 1 to "f" has incompatible type "Type[A]"; expected "Type[B]"
f(B)
f(C)
f(AType) # E: No overload variant of "f" matches argument type "Type[A]" \
# N: Possible overload variants: \
# N: def f(a: Type[B]) -> None \
# N: def f(a: int) -> None
f(AType) # E: Argument 1 to "f" has incompatible type "Type[A]"; expected "Type[B]"
f(BType)
f(CType)
[builtins fixtures/classmethod.pyi]
Expand Down
31 changes: 31 additions & 0 deletions test-data/unit/check-overloading.test
Original file line number Diff line number Diff line change
Expand Up @@ -4374,3 +4374,34 @@ def bar(x: T) -> T: ...
@overload
def bar(x: Any) -> int: ...
[out]

[case testBadOverloadProbableMatch]
from typing import overload, List, Type

class Other: pass

@overload
def multiple_plausible(x: int) -> int: ...
@overload
def multiple_plausible(x: str) -> str: ...
def multiple_plausible(x): pass


@overload
def single_plausible(x: Type[int]) -> int: ...
@overload
def single_plausible(x: List[str]) -> str: ...
def single_plausible(x): pass

a = multiple_plausible(Other()) # E: No overload variant of "multiple_plausible" matches argument type "Other" \
# N: Possible overload variants: \
# N: def multiple_plausible(x: int) -> int \
# N: def multiple_plausible(x: str) -> str
reveal_type(a) # E: Revealed type is 'Any'

b = single_plausible(Other) # E: Argument 1 to "single_plausible" has incompatible type "Type[Other]"; expected "Type[int]"
reveal_type(b) # E: Revealed type is 'builtins.int'

c = single_plausible([Other()]) # E: List item 0 has incompatible type "Other"; expected "str"
reveal_type(c) # E: Revealed type is 'builtins.str'
[builtins fixtures/list.pyi]