Skip to content
4 changes: 3 additions & 1 deletion mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -2814,7 +2814,9 @@ def check_overload_call(
code = None
else:
code = codes.OPERATOR
self.msg.no_variant_matches_arguments(callee, arg_types, context, code=code)
self.msg.no_variant_matches_arguments(
callee, arg_types, context, arg_names=arg_names, arg_kinds=arg_kinds, code=code
)

result = self.check_call(
target,
Expand Down
95 changes: 95 additions & 0 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -1091,14 +1091,109 @@ def no_variant_matches_arguments(
arg_types: list[Type],
context: Context,
*,
arg_names: Sequence[str | None] | None,
arg_kinds: list[ArgKind] | None = None,
code: ErrorCode | None = None,
) -> None:
code = code or codes.CALL_OVERLOAD
name = callable_name(overload)
if name:
name_str = f" of {name}"
for_func = f" for overloaded function {name}"
else:
name_str = ""
for_func = ""

# For keyword argument errors
unexpected_kwargs: list[tuple[str, Type]] = []
if arg_names is not None and arg_kinds is not None:
all_valid_kwargs: set[str] = set()
for item in overload.items:
for i, arg_name in enumerate(item.arg_names):
if arg_name is not None and item.arg_kinds[i] != ARG_STAR:
all_valid_kwargs.add(arg_name)
if item.is_kw_arg:
all_valid_kwargs.clear()
break

if all_valid_kwargs:
for i, (arg_name, arg_kind) in enumerate(zip(arg_names, arg_kinds)):
if arg_kind == ARG_NAMED and arg_name is not None:
if arg_name not in all_valid_kwargs:
unexpected_kwargs.append((arg_name, arg_types[i]))

if unexpected_kwargs:
all_kwargs_confident = True
kwargs_with_suggestions: list[tuple[str, list[str]]] = []
kwargs_without_suggestions: list[str] = []

# Find suggestions for each unexpected kwarg, prioritizing type-matching args
for kwarg_name, kwarg_type in unexpected_kwargs:
matching_type_args: list[str] = []
not_matching_type_args: list[str] = []
has_matching_variant = False

for item in overload.items:
item_has_type_match = False
for i, formal_type in enumerate(item.arg_types):
formal_name = item.arg_names[i]
if formal_name is not None and item.arg_kinds[i] != ARG_STAR:
if is_subtype(kwarg_type, formal_type):
if formal_name not in matching_type_args:
matching_type_args.append(formal_name)
item_has_type_match = True
elif formal_name not in not_matching_type_args:
not_matching_type_args.append(formal_name)
if item_has_type_match:
has_matching_variant = True

matches = best_matches(kwarg_name, matching_type_args, n=3)
if not matches:
matches = best_matches(kwarg_name, not_matching_type_args, n=3)

if matches:
kwargs_with_suggestions.append((kwarg_name, matches))
else:
kwargs_without_suggestions.append(kwarg_name)

if not has_matching_variant:
all_kwargs_confident = False

for kwarg_name, matches in kwargs_with_suggestions:
self.fail(
f'Unexpected keyword argument "{kwarg_name}"'
f"{for_func}; did you mean {pretty_seq(matches, 'or')}?",
context,
code=code,
)

if kwargs_without_suggestions:
if len(kwargs_without_suggestions) == 1:
self.fail(
f'Unexpected keyword argument "{kwargs_without_suggestions[0]}"{for_func}',
context,
code=code,
)
else:
quoted_names = ", ".join(f'"{n}"' for n in kwargs_without_suggestions)
self.fail(
f"Unexpected keyword arguments {quoted_names}{for_func}",
context,
code=code,
)

if not all_kwargs_confident:
self.note(
f"Possible overload variant{plural_s(len(overload.items))}:",
context,
code=code,
)
for item in overload.items:
self.note(pretty_callable(item, self.options), context, offset=4, code=code)

if all_kwargs_confident and len(unexpected_kwargs) == len(arg_types):
return

arg_types_str = ", ".join(format_type(arg, self.options) for arg in arg_types)
num_args = len(arg_types)
if num_args == 0:
Expand Down
139 changes: 139 additions & 0 deletions test-data/unit/check-expressions.test
Original file line number Diff line number Diff line change
Expand Up @@ -2582,3 +2582,142 @@ def last_known_value() -> None:
x, y, z = xy # E: Unpacking a string is disallowed
reveal_type(z) # N: Revealed type is "builtins.str"
[builtins fixtures/primitives.pyi]

[case testOverloadUnexpectedKeywordArgWithTypoSuggestion]
from typing import overload, Union

@overload
def f(foobar: int) -> None: ...

@overload
def f(foobar: str) -> None: ...

def f(foobar: Union[int, str]) -> None: pass

f(fobar=1) # E: Unexpected keyword argument "fobar" for overloaded function "f"; did you mean "foobar"?
[builtins fixtures/list.pyi]

[case testOverloadUnexpectedKeywordArgNoMatch]
from typing import overload, Union

@overload
def f(foobar: int) -> None: ...

@overload
def f(foobar: str) -> None: ...

def f(foobar: Union[int, str]) -> None: pass

f(random=[1,2,3]) # E: Unexpected keyword argument "random" for overloaded function "f" \
Copy link
Collaborator

Choose a reason for hiding this comment

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

Additional test ideas:

  • Test multiple invalid keyword arguments
  • Test both invalid keyword argument and incompatible positional argument
  • Test both valid an invalid keyword arguments in the same call

# N: Possible overload variants: \
# N: def f(foobar: int) -> None \
# N: def f(foobar: str) -> None \
# E: No overload variant of "f" matches argument type "list[int]"
[builtins fixtures/list.pyi]

[case testOverloadMultipleUnexpectedKeywordArgs]
from typing import overload, Union

@overload
def f(foobar: int) -> None: ...

@overload
def f(foobar: str) -> None: ...

def f(foobar: Union[int, str]) -> None: pass

f(fobar=1, baz=2) # E: Unexpected keyword argument "fobar" for overloaded function "f"; did you mean "foobar"? \
# E: Unexpected keyword argument "baz" for overloaded function "f"
[builtins fixtures/list.pyi]

[case testOverloadManyUnexpectedKeywordArgs]
from typing import overload, Union

@overload
def f(foobar: int) -> None: ...

@overload
def f(foobar: str) -> None: ...

def f(foobar: Union[int, str]) -> None: pass

f(foobar=1, a=2, b=3, c=4, d=5, e=6) # E: Unexpected keyword arguments "a", "b", "c", "d", "e" for overloaded function "f" \
# E: No overload variant of "f" matches argument types "int", "int", "int", "int", "int", "int" \
# N: Possible overload variants: \
# N: def f(foobar: int) -> None \
# N: def f(foobar: str) -> None
[builtins fixtures/list.pyi]

[case testOverloadUnexpectedKeywordArgsWithTypeMismatch]
from typing import overload, Union

@overload
def f(foobar: int) -> None: ...

@overload
def f(foobar: str) -> None: ...

def f(foobar: Union[int, str]) -> None: pass

f(fobar=1, other=[1,2,3]) # E: Unexpected keyword argument "fobar" for overloaded function "f"; did you mean "foobar"? \
# E: Unexpected keyword argument "other" for overloaded function "f" \
# N: Possible overload variants: \
# N: def f(foobar: int) -> None \
# N: def f(foobar: str) -> None \
# E: No overload variant of "f" matches argument types "int", "list[int]"
[builtins fixtures/list.pyi]

[case testOverloadPositionalArgTypeMismatch]
from typing import overload, Union

@overload
def g(x: int, y: int) -> int: ...

@overload
def g(x: str, y: str) -> str: ...

def g(x: Union[int, str], y: Union[int, str]) -> Union[int, str]:
return x

g([1, 2], 3) # E: No overload variant of "g" matches argument types "list[int]", "int" \
# N: Possible overload variants: \
# N: def g(x: int, y: int) -> int \
# N: def g(x: str, y: str) -> str
[builtins fixtures/list.pyi]

[case testOverloadUnexpectedKeywordWithPositionalMismatch]
from typing import overload, Union

@overload
def g(x: int, y: int) -> int: ...

@overload
def g(x: str, y: str) -> str: ...

def g(x: Union[int, str], y: Union[int, str]) -> Union[int, str]:
return x

g([1, 2], z=3) # E: Unexpected keyword argument "z" for overloaded function "g" \
# E: No overload variant of "g" matches argument types "list[int]", "int" \
# N: Possible overload variants: \
# N: def g(x: int, y: int) -> int \
# N: def g(x: str, y: str) -> str
[builtins fixtures/list.pyi]

[case testOverloadNamedArgTypeMismatch]
from typing import overload, Union

@overload
def g(x: int, y: int) -> int: ...

@overload
def g(x: str, y: str) -> str: ...

def g(x: Union[int, str], y: Union[int, str]) -> Union[int, str]:
return x

g(x="hello", y=1) # E: No overload variant of "g" matches argument types "str", "int" \
# N: Possible overload variants: \
# N: def g(x: int, y: int) -> int \
# N: def g(x: str, y: str) -> str
[builtins fixtures/list.pyi]