Skip to content

Commit 6077dc8

Browse files
authored
Improve ambiguous **kwarg checking (python#9573)
Fixes python#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 python#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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy