Skip to content
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

Ambiguous **kwargs only map to unmapped arguments that have a matching type. #9705

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
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
42 changes: 34 additions & 8 deletions mypy/argmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from typing import List, Optional, Sequence, Callable, Set

from mypy.types import (
Type, Instance, TupleType, AnyType, TypeOfAny, TypedDictType, get_proper_type
Type, Instance, TupleType, AnyType, TypeOfAny, TypedDictType, get_proper_type,
TypeVarType,
)
from mypy import nodes

Expand All @@ -12,8 +13,8 @@ def map_actuals_to_formals(actual_kinds: List[int],
actual_names: Optional[Sequence[Optional[str]]],
formal_kinds: List[int],
formal_names: Sequence[Optional[str]],
actual_arg_type: Callable[[int],
Type]) -> List[List[int]]:
actual_arg_type: Callable[[int], Type],
formal_types: List[Type]) -> List[List[int]]:
"""Calculate mapping between actual (caller) args and formals.

The result contains a list of caller argument indexes mapping to each
Expand Down Expand Up @@ -93,24 +94,49 @@ def map_actuals_to_formals(actual_kinds: List[int],
and formal_kinds[fi] != nodes.ARG_STAR)
or formal_kinds[fi] == nodes.ARG_STAR2]
for ai in ambiguous_actual_kwargs:
for fi in unmatched_formals:
for fi in map_kwargs_to_formals(get_proper_type(actual_arg_type(ai)),
actual_kinds[ai], unmatched_formals,
formal_types, formal_names, formal_kinds):
formal_to_actual[fi].append(ai)

return formal_to_actual


def map_kwargs_to_formals(actual_type: Type, actual_kind: int,
formals: List[int],
formal_types: List[Type],
formal_names: Sequence[Optional[str]],
formal_kinds: List[int]) -> List[int]:
"""Generate the mapping between the actual **kwargs and formal parameters. Any given **kwarg
will only map to a parameter which it is type compatible with.
"""
from mypy.subtypes import is_subtype
mapped_formals = [] # type: List[int]
for fi in formals:
if formal_kinds[fi] == nodes.ARG_STAR:
continue
mapper = ArgTypeExpander()
expanded_actual_type = mapper.expand_actual_type(actual_type, actual_kind,
formal_names[fi],
formal_kinds[fi])
formal_type = formal_types[fi]
if is_subtype(expanded_actual_type, formal_type) or isinstance(formal_type, TypeVarType):
mapped_formals.append(fi)
return mapped_formals


def map_formals_to_actuals(actual_kinds: List[int],
actual_names: Optional[Sequence[Optional[str]]],
formal_kinds: List[int],
formal_names: List[Optional[str]],
actual_arg_type: Callable[[int],
Type]) -> List[List[int]]:
actual_arg_type: Callable[[int], Type],
formal_types: List[Type]) -> List[List[int]]:
"""Calculate the reverse mapping of map_actuals_to_formals."""
formal_to_actual = map_actuals_to_formals(actual_kinds,
actual_names,
formal_kinds,
formal_names,
actual_arg_type)
actual_arg_type,
formal_types)
# Now reverse the mapping.
actual_to_formal = [[] for _ in actual_kinds] # type: List[List[int]]
for formal, actuals in enumerate(formal_to_actual):
Expand Down
57 changes: 48 additions & 9 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@
from mypy import applytype
from mypy import erasetype
from mypy.checkmember import analyze_member_access, type_object_type
from mypy.argmap import ArgTypeExpander, map_actuals_to_formals, map_formals_to_actuals
from mypy.argmap import (
ArgTypeExpander, map_actuals_to_formals, map_formals_to_actuals, map_kwargs_to_formals
)
from mypy.checkstrformat import StringFormatterChecker
from mypy.expandtype import expand_type, expand_type_by_instance, freshen_function_type_vars
from mypy.util import split_module_names
Expand Down Expand Up @@ -309,8 +311,8 @@ def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) ->
formal_to_actual = map_actuals_to_formals(
e.arg_kinds, e.arg_names,
e.callee.arg_kinds, e.callee.arg_names,
lambda i: self.accept(e.args[i]))

lambda i: self.accept(e.args[i]),
[AnyType(TypeOfAny.special_form)] * len(e.callee.arg_names))
arg_types = [join.join_type_list([self.accept(e.args[j]) for j in formal_to_actual[i]])
for i in range(len(e.callee.arg_kinds))]
type_context = CallableType(arg_types, e.callee.arg_kinds, e.callee.arg_names,
Expand Down Expand Up @@ -748,7 +750,8 @@ def apply_signature_hook(
formal_to_actual = map_actuals_to_formals(
arg_kinds, arg_names,
callee.arg_kinds, callee.arg_names,
lambda i: self.accept(args[i]))
lambda i: self.accept(args[i]),
callee.arg_types)
formal_arg_exprs = [[] for _ in range(num_formals)] # type: List[List[Expression]]
for formal, actuals in enumerate(formal_to_actual):
for actual in actuals:
Expand Down Expand Up @@ -990,7 +993,8 @@ def check_callable_call(self,
formal_to_actual = map_actuals_to_formals(
arg_kinds, arg_names,
callee.arg_kinds, callee.arg_names,
lambda i: self.accept(args[i]))
lambda i: self.accept(args[i]),
callee.arg_types)

if callee.is_generic():
callee = freshen_function_type_vars(callee)
Expand Down Expand Up @@ -1383,6 +1387,32 @@ def check_argument_count(self,
ok = False
return ok

def check_for_incompatible_kwargs(self,
callee: CallableType,
actual_types: List[Type],
actual_kinds: List[int],
context: Context,
messages: MessageBuilder,
formal_to_actual: List[List[int]]) -> None:
"""Each **kwarg supplied to a callable should map to at least one formal
parameter.
"""
ambiguous_kwargs = [(i, actualt, actualk)
for i, (actualt, actualk) in enumerate(zip(actual_types, actual_kinds))
if actualk == nodes.ARG_STAR2
and not isinstance(get_proper_type(actualt), TypedDictType)]
for i, actualt, actualk in ambiguous_kwargs:
potential_formals = [] # type: List[int]
actual_formals = [] # type: List[int]
for fi in map_kwargs_to_formals(actualt, actualk, list(range(len(callee.arg_types))),
callee.arg_types, callee.arg_names, callee.arg_kinds):
potential_formals.append(fi)
if i in formal_to_actual[fi]:
actual_formals.append(fi)
if not actual_formals:
messages.kwargs_has_no_compatible_parameter(callee, context, i, actualt,
potential_formals)

def check_for_extra_actual_arguments(self,
callee: CallableType,
actual_types: List[Type],
Expand Down Expand Up @@ -1454,6 +1484,8 @@ def check_argument_types(self,
"""
messages = messages or self.msg
check_arg = check_arg or self.check_arg
self.check_for_incompatible_kwargs(callee, arg_types, arg_kinds,
context, messages, formal_to_actual)
# Keep track of consumed tuple *arg items.
mapper = ArgTypeExpander()
for i, actuals in enumerate(formal_to_actual):
Expand Down Expand Up @@ -1502,7 +1534,9 @@ def check_arg(self,
isinstance(callee_type.item, Instance) and
(callee_type.item.type.is_abstract or callee_type.item.type.is_protocol)):
self.msg.concrete_only_call(callee_type, context)
elif not is_subtype(caller_type, callee_type):
elif not is_subtype(caller_type, callee_type) and (caller_kind != nodes.ARG_STAR2
or isinstance(original_caller_type,
TypedDictType)):
if self.chk.should_suppress_optional_error([caller_type, callee_type]):
return
code = messages.incompatible_argument(n,
Expand Down Expand Up @@ -1657,7 +1691,8 @@ def has_shape(typ: Type) -> bool:
for typ in overload.items():
formal_to_actual = map_actuals_to_formals(arg_kinds, arg_names,
typ.arg_kinds, typ.arg_names,
lambda i: arg_types[i])
lambda i: arg_types[i],
typ.arg_types)

if self.check_argument_count(typ, arg_types, arg_kinds, arg_names,
formal_to_actual, None, None):
Expand Down Expand Up @@ -1959,7 +1994,8 @@ def erased_signature_similarity(self,
arg_names,
callee.arg_kinds,
callee.arg_names,
lambda i: arg_types[i])
lambda i: arg_types[i],
callee.arg_types)

if not self.check_argument_count(callee, arg_types, arg_kinds, arg_names,
formal_to_actual, None, None):
Expand Down Expand Up @@ -4381,7 +4417,10 @@ def any_causes_overload_ambiguity(items: List[CallableType],

actual_to_formal = [
map_formals_to_actuals(
arg_kinds, arg_names, item.arg_kinds, item.arg_names, lambda i: arg_types[i])
arg_kinds, arg_names,
item.arg_kinds, item.arg_names,
lambda i: arg_types[i],
item.arg_types)
for item in items
]

Expand Down
23 changes: 23 additions & 0 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,29 @@ def too_many_arguments(self, callee: CallableType, context: Context) -> None:
msg = 'Too many arguments' + for_function(callee)
self.fail(msg, context, code=codes.CALL_ARG)

def kwargs_has_no_compatible_parameter(self, callee: CallableType, context: Context,
kwargs_index: int, kwargs_type: Type,
formals: List[int]) -> None:
callee_name = callable_name(callee)
kwargs_type_str = format_type(kwargs_type)
argument_number = kwargs_index + 1
if formals:
formal_names = ', '.join(('"' + name + '"')
if name
else 'parameter {}'.format(fi)
for fi, name in enumerate(callee.arg_names[fi]
for fi in formals))
msg = ('Argument {} in call to {} with type {} cannot unpack into any expected '
'parameters; {} in use'.
format(argument_number, callee_name, kwargs_type_str, formal_names))
else:
msg = ('Argument {} in call to {} cannot unpack into any parameters; '
'no parameter accepts type {}'.
format(argument_number,
callee_name,
kwargs_type_str))
self.fail(msg, context, code=codes.CALL_ARG)

def too_many_arguments_from_typed_dict(self,
callee: CallableType,
arg_type: TypedDictType,
Expand Down
3 changes: 2 additions & 1 deletion mypy/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,8 @@ def record_callable_target_precision(self, o: CallExpr, callee: CallableType) ->
o.arg_names,
callee.arg_kinds,
callee.arg_names,
lambda n: typemap[o.args[n]])
lambda n: typemap[o.args[n]],
callee.arg_types)
for formals in actual_to_formal:
for n in formals:
formal = get_proper_type(callee.arg_types[n])
Expand Down
3 changes: 2 additions & 1 deletion mypy/suggestions.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,8 @@ def visit_call_expr(self, o: CallExpr) -> None:

formal_to_actual = map_actuals_to_formals(
o.arg_kinds, o.arg_names, typ.arg_kinds, typ.arg_names,
lambda n: AnyType(TypeOfAny.special_form))
lambda n: AnyType(TypeOfAny.special_form),
typ.arg_types)

for i, args in enumerate(formal_to_actual):
for arg_idx in args:
Expand Down
6 changes: 4 additions & 2 deletions mypy/test/testinfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,8 @@ def assert_map(self,
caller_names,
callee_kinds,
callee_names,
lambda i: AnyType(TypeOfAny.special_form))
lambda i: AnyType(TypeOfAny.special_form),
[AnyType(TypeOfAny.special_form)] * len(callee_kinds))
assert_equal(result, expected)

def assert_vararg_map(self,
Expand All @@ -195,7 +196,8 @@ def assert_vararg_map(self,
[],
callee_kinds,
[],
lambda i: vararg_type)
lambda i: vararg_type,
[AnyType(TypeOfAny.special_form)] * len(callee_kinds))
assert_equal(result, expected)


Expand Down
6 changes: 4 additions & 2 deletions mypyc/irbuild/ll_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
)

from mypy.nodes import ARG_POS, ARG_NAMED, ARG_STAR, ARG_STAR2, op_methods
from mypy.types import AnyType, TypeOfAny
from mypy.types import AnyType, TypeOfAny, Type
from mypy.checkexpr import map_actuals_to_formals

from mypyc.ir.ops import (
Expand Down Expand Up @@ -333,11 +333,13 @@ def native_args_to_positional(self,

sig_arg_kinds = [arg.kind for arg in sig.args]
sig_arg_names = [arg.name for arg in sig.args]
sig_arg_types = [AnyType(TypeOfAny.special_form)] * len(sig_arg_kinds) # type: List[Type]
formal_to_actual = map_actuals_to_formals(arg_kinds,
arg_names,
sig_arg_kinds,
sig_arg_names,
lambda n: AnyType(TypeOfAny.special_form))
lambda n: AnyType(TypeOfAny.special_form),
sig_arg_types)

# Flatten out the arguments, loading error values for default
# arguments, constructing tuples/dicts for star args, and
Expand Down
4 changes: 3 additions & 1 deletion test-data/unit/check-columns.test
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ def g(**x: int) -> None: pass
a = ['']
f(*a) # E:4: Argument 1 to "f" has incompatible type "*List[str]"; expected "int"
b = {'x': 'y'}
g(**b) # E:5: Argument 1 to "g" has incompatible type "**Dict[str, str]"; expected "int"
# TODO: "too many arguments" error will not appear after #9629 is merged
g(**b) # E:1: Too many arguments for "g" \
# E:1: Argument 1 in call to "g" cannot unpack into any parameters; no parameter accepts type "Dict[str, str]"
[builtins fixtures/dict.pyi]

[case testColumnsMultipleStatementsPerLine]
Expand Down
3 changes: 2 additions & 1 deletion test-data/unit/check-ctypes.test
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ import ctypes
intarr4 = ctypes.c_int * 4

x = {"a": 1, "b": 2}
intarr4(**x) # E: Too many arguments for "Array"
intarr4(**x) # E: Too many arguments for "Array" \
# E: Argument 1 in call to "Array" cannot unpack into any parameters; no parameter accepts type "Dict[str, int]"

[builtins fixtures/floatdict.pyi]
3 changes: 2 additions & 1 deletion test-data/unit/check-expressions.test
Original file line number Diff line number Diff line change
Expand Up @@ -2217,7 +2217,8 @@ kw2 = {'x': ''}
d2 = dict(it, **kw2)
d2() # E: "Dict[str, object]" not callable

d3 = dict(it, **kw2) # type: Dict[str, int] # E: Argument 2 to "dict" has incompatible type "**Dict[str, str]"; expected "int"
d3 = dict(it, **kw2) # type: Dict[str, int] \
# E: Argument 2 in call to "dict" cannot unpack into any parameters; no parameter accepts type "Dict[str, str]"
[builtins fixtures/dict.pyi]

[case testDictFromIterableAndStarStarArgs2]
Expand Down
Loading