Skip to content

Commit 6077dc8

Browse files
authored
Improve ambiguous **kwarg checking (#9573)
Fixes #4708 Allows for multiple ambiguous **kwarg unpacking in a call -- all ambiguous **kwargs will map to all formal args that do not have a certain actual arg. Fixes #9395 Defers ambiguous **kwarg mapping until all other unambiguous formal args have been mapped -- order of **kwarg unpacking no longer affects the arg map.
1 parent 941a414 commit 6077dc8

File tree

5 files changed

+86
-22
lines changed

5 files changed

+86
-22
lines changed

mypy/argmap.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def map_actuals_to_formals(actual_kinds: List[int],
2424
"""
2525
nformals = len(formal_kinds)
2626
formal_to_actual = [[] for i in range(nformals)] # type: List[List[int]]
27+
ambiguous_actual_kwargs = [] # type: List[int]
2728
fi = 0
2829
for ai, actual_kind in enumerate(actual_kinds):
2930
if actual_kind == nodes.ARG_POS:
@@ -76,18 +77,25 @@ def map_actuals_to_formals(actual_kinds: List[int],
7677
formal_to_actual[formal_kinds.index(nodes.ARG_STAR2)].append(ai)
7778
else:
7879
# We don't exactly know which **kwargs are provided by the
79-
# caller. Assume that they will fill the remaining arguments.
80-
for fi in range(nformals):
81-
# TODO: If there are also tuple varargs, we might be missing some potential
82-
# matches if the tuple was short enough to not match everything.
83-
no_certain_match = (
84-
not formal_to_actual[fi]
85-
or actual_kinds[formal_to_actual[fi][0]] == nodes.ARG_STAR)
86-
if ((formal_names[fi]
87-
and no_certain_match
88-
and formal_kinds[fi] != nodes.ARG_STAR) or
89-
formal_kinds[fi] == nodes.ARG_STAR2):
90-
formal_to_actual[fi].append(ai)
80+
# caller, so we'll defer until all the other unambiguous
81+
# actuals have been processed
82+
ambiguous_actual_kwargs.append(ai)
83+
84+
if ambiguous_actual_kwargs:
85+
# Assume the ambiguous kwargs will fill the remaining arguments.
86+
#
87+
# TODO: If there are also tuple varargs, we might be missing some potential
88+
# matches if the tuple was short enough to not match everything.
89+
unmatched_formals = [fi for fi in range(nformals)
90+
if (formal_names[fi]
91+
and (not formal_to_actual[fi]
92+
or actual_kinds[formal_to_actual[fi][0]] == nodes.ARG_STAR)
93+
and formal_kinds[fi] != nodes.ARG_STAR)
94+
or formal_kinds[fi] == nodes.ARG_STAR2]
95+
for ai in ambiguous_actual_kwargs:
96+
for fi in unmatched_formals:
97+
formal_to_actual[fi].append(ai)
98+
9199
return formal_to_actual
92100

93101

mypy/checkexpr.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1336,7 +1336,7 @@ def check_argument_count(self,
13361336
ok = False
13371337
elif kind in [nodes.ARG_POS, nodes.ARG_OPT,
13381338
nodes.ARG_NAMED, nodes.ARG_NAMED_OPT] and is_duplicate_mapping(
1339-
formal_to_actual[i], actual_kinds):
1339+
formal_to_actual[i], actual_types, actual_kinds):
13401340
if (self.chk.in_checked_function() or
13411341
isinstance(get_proper_type(actual_types[formal_to_actual[i][0]]),
13421342
TupleType)):
@@ -4112,15 +4112,25 @@ def is_non_empty_tuple(t: Type) -> bool:
41124112
return isinstance(t, TupleType) and bool(t.items)
41134113

41144114

4115-
def is_duplicate_mapping(mapping: List[int], actual_kinds: List[int]) -> bool:
4116-
# Multiple actuals can map to the same formal only if they both come from
4117-
# varargs (*args and **kwargs); in this case at runtime it is possible that
4118-
# there are no duplicates. We need to allow this, as the convention
4119-
# f(..., *args, **kwargs) is common enough.
4120-
return len(mapping) > 1 and not (
4121-
len(mapping) == 2 and
4122-
actual_kinds[mapping[0]] == nodes.ARG_STAR and
4123-
actual_kinds[mapping[1]] == nodes.ARG_STAR2)
4115+
def is_duplicate_mapping(mapping: List[int],
4116+
actual_types: List[Type],
4117+
actual_kinds: List[int]) -> bool:
4118+
return (
4119+
len(mapping) > 1
4120+
# Multiple actuals can map to the same formal if they both come from
4121+
# varargs (*args and **kwargs); in this case at runtime it is possible
4122+
# that here are no duplicates. We need to allow this, as the convention
4123+
# f(..., *args, **kwargs) is common enough.
4124+
and not (len(mapping) == 2
4125+
and actual_kinds[mapping[0]] == nodes.ARG_STAR
4126+
and actual_kinds[mapping[1]] == nodes.ARG_STAR2)
4127+
# Multiple actuals can map to the same formal if there are multiple
4128+
# **kwargs which cannot be mapped with certainty (non-TypedDict
4129+
# **kwargs).
4130+
and not all(actual_kinds[m] == nodes.ARG_STAR2 and
4131+
not isinstance(get_proper_type(actual_types[m]), TypedDictType)
4132+
for m in mapping)
4133+
)
41244134

41254135

41264136
def replace_callable_return_type(c: CallableType, new_ret_type: Type) -> CallableType:

test-data/unit/check-kwargs.test

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,29 @@ f(*l, **d)
377377
class A: pass
378378
[builtins fixtures/dict.pyi]
379379

380+
[case testPassingMultipleKeywordVarArgs]
381+
from typing import Any, Dict
382+
def f1(a: 'A', b: 'A') -> None: pass
383+
def f2(a: 'A') -> None: pass
384+
def f3(a: 'A', **kwargs: 'A') -> None: pass
385+
def f4(**kwargs: 'A') -> None: pass
386+
d = None # type: Dict[Any, Any]
387+
d2 = None # type: Dict[Any, Any]
388+
f1(**d, **d2)
389+
f2(**d, **d2)
390+
f3(**d, **d2)
391+
f4(**d, **d2)
392+
class A: pass
393+
[builtins fixtures/dict.pyi]
394+
395+
[case testPassingKeywordVarArgsToVarArgsOnlyFunction]
396+
from typing import Any, Dict
397+
def f(*args: 'A') -> None: pass
398+
d = None # type: Dict[Any, Any]
399+
f(**d) # E: Too many arguments for "f"
400+
class A: pass
401+
[builtins fixtures/dict.pyi]
402+
380403
[case testKeywordArgumentAndCommentSignature]
381404
import typing
382405
def f(x): # type: (int) -> str # N: "f" defined here

test-data/unit/check-python38.test

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,11 +145,16 @@ f(arg=1) # E: Unexpected keyword argument "arg" for "f"
145145
f(arg="ERROR") # E: Unexpected keyword argument "arg" for "f"
146146

147147
[case testPEP570Calls]
148+
from typing import Any, Dict
148149
def f(p, /, p_or_kw, *, kw) -> None: ... # N: "f" defined here
150+
d = None # type: Dict[Any, Any]
149151
f(0, 0, 0) # E: Too many positional arguments for "f"
150152
f(0, 0, kw=0)
151153
f(0, p_or_kw=0, kw=0)
152154
f(p=0, p_or_kw=0, kw=0) # E: Unexpected keyword argument "p" for "f"
155+
f(0, **d)
156+
f(**d) # E: Too few arguments for "f"
157+
[builtins fixtures/dict.pyi]
153158

154159
[case testPEP570Signatures1]
155160
def f(p1: bytes, p2: float, /, p_or_kw: int, *, kw: str) -> None:

test-data/unit/check-typeddict.test

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1570,6 +1570,24 @@ f1(**c, **a) # E: "f1" gets multiple values for keyword argument "x" \
15701570
# E: Argument "x" to "f1" has incompatible type "str"; expected "int"
15711571
[builtins fixtures/tuple.pyi]
15721572

1573+
[case testTypedDictAsStarStarAndDictAsStarStar]
1574+
from mypy_extensions import TypedDict
1575+
from typing import Any, Dict
1576+
1577+
TD = TypedDict('TD', {'x': int, 'y': str})
1578+
1579+
def f1(x: int, y: str, z: bytes) -> None: ...
1580+
def f2(x: int, y: str) -> None: ...
1581+
1582+
td: TD
1583+
d = None # type: Dict[Any, Any]
1584+
1585+
f1(**td, **d)
1586+
f1(**d, **td)
1587+
f2(**td, **d) # E: Too many arguments for "f2"
1588+
f2(**d, **td) # E: Too many arguments for "f2"
1589+
[builtins fixtures/dict.pyi]
1590+
15731591
[case testTypedDictNonMappingMethods]
15741592
from typing import List
15751593
from mypy_extensions import TypedDict

0 commit comments

Comments
 (0)