Skip to content

Commit dd1c5d0

Browse files
authored
Type check typed dict as caller **kwargs (#5925)
Expand and type check TypedDict types when used as **kwargs in calls. Also refactored the implementation of checking function arguments and removed some apparently useless code. Fixes #5198 and another related issue: type checking calls with multiple *args arguments.
1 parent 4f6db54 commit dd1c5d0

File tree

6 files changed

+322
-121
lines changed

6 files changed

+322
-121
lines changed

mypy/argmap.py

+98-35
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""Utilities for mapping between actual and formal arguments (and their types)."""
22

3-
from typing import List, Optional, Sequence, Callable
3+
from typing import List, Optional, Sequence, Callable, Set
44

5-
from mypy.types import Type, Instance, TupleType, AnyType, TypeOfAny
5+
from mypy.types import Type, Instance, TupleType, AnyType, TypeOfAny, TypedDictType
66
from mypy import nodes
77

88

@@ -65,13 +65,24 @@ def map_actuals_to_formals(caller_kinds: List[int],
6565
map[callee_kinds.index(nodes.ARG_STAR2)].append(i)
6666
else:
6767
assert kind == nodes.ARG_STAR2
68-
for j in range(ncallee):
69-
# TODO tuple varargs complicate this
70-
no_certain_match = (
71-
not map[j] or caller_kinds[map[j][0]] == nodes.ARG_STAR)
72-
if ((callee_names[j] and no_certain_match)
73-
or callee_kinds[j] == nodes.ARG_STAR2):
74-
map[j].append(i)
68+
argt = caller_arg_type(i)
69+
if isinstance(argt, TypedDictType):
70+
for name, value in argt.items.items():
71+
if name in callee_names:
72+
map[callee_names.index(name)].append(i)
73+
elif nodes.ARG_STAR2 in callee_kinds:
74+
map[callee_kinds.index(nodes.ARG_STAR2)].append(i)
75+
else:
76+
# We don't exactly know which **kwargs are provided by the
77+
# caller. Assume that they will fill the remaining arguments.
78+
for j in range(ncallee):
79+
# TODO: If there are also tuple varargs, we might be missing some potential
80+
# matches if the tuple was short enough to not match everything.
81+
no_certain_match = (
82+
not map[j] or caller_kinds[map[j][0]] == nodes.ARG_STAR)
83+
if ((callee_names[j] and no_certain_match)
84+
or callee_kinds[j] == nodes.ARG_STAR2):
85+
map[j].append(i)
7586
return map
7687

7788

@@ -95,35 +106,87 @@ def map_formals_to_actuals(caller_kinds: List[int],
95106
return actual_to_formal
96107

97108

98-
def get_actual_type(arg_type: Type, kind: int,
99-
tuple_counter: List[int]) -> Type:
100-
"""Return the type of an actual argument with the given kind.
109+
class ArgTypeExpander:
110+
"""Utility class for mapping actual argument types to formal arguments.
111+
112+
One of the main responsibilities is to expand caller tuple *args and TypedDict
113+
**kwargs, and to keep track of which tuple/TypedDict items have already been
114+
consumed.
115+
116+
Example:
117+
118+
def f(x: int, *args: str) -> None: ...
119+
f(*(1, 'x', 1.1))
120+
121+
We'd call expand_actual_type three times:
101122
102-
If the argument is a *arg, return the individual argument item.
123+
1. The first call would provide 'int' as the actual type of 'x' (from '1').
124+
2. The second call would provide 'str' as one of the actual types for '*args'.
125+
2. The third call would provide 'float' as one of the actual types for '*args'.
126+
127+
A single instance can process all the arguments for a single call. Each call
128+
needs a separate instance since instances have per-call state.
103129
"""
104130

105-
if kind == nodes.ARG_STAR:
106-
if isinstance(arg_type, Instance):
107-
if arg_type.type.fullname() == 'builtins.list':
108-
# List *arg.
109-
return arg_type.args[0]
110-
elif arg_type.args:
111-
# TODO try to map type arguments to Iterable
112-
return arg_type.args[0]
131+
def __init__(self) -> None:
132+
# Next tuple *args index to use.
133+
self.tuple_index = 0
134+
# Keyword arguments in TypedDict **kwargs used.
135+
self.kwargs_used = set() # type: Set[str]
136+
137+
def expand_actual_type(self,
138+
actual_type: Type,
139+
actual_kind: int,
140+
formal_name: Optional[str],
141+
formal_kind: int) -> Type:
142+
"""Return the actual (caller) type(s) of a formal argument with the given kinds.
143+
144+
If the actual argument is a tuple *args, return the next individual tuple item that
145+
maps to the formal arg.
146+
147+
If the actual argument is a TypedDict **kwargs, return the next matching typed dict
148+
value type based on formal argument name and kind.
149+
150+
This is supposed to be called for each formal, in order. Call multiple times per
151+
formal if multiple actuals map to a formal.
152+
"""
153+
if actual_kind == nodes.ARG_STAR:
154+
if isinstance(actual_type, Instance):
155+
if actual_type.type.fullname() == 'builtins.list':
156+
# List *arg.
157+
return actual_type.args[0]
158+
elif actual_type.args:
159+
# TODO: Try to map type arguments to Iterable
160+
return actual_type.args[0]
161+
else:
162+
return AnyType(TypeOfAny.from_error)
163+
elif isinstance(actual_type, TupleType):
164+
# Get the next tuple item of a tuple *arg.
165+
if self.tuple_index >= len(actual_type.items):
166+
# Exhausted a tuple -- continue to the next *args.
167+
self.tuple_index = 1
168+
else:
169+
self.tuple_index += 1
170+
return actual_type.items[self.tuple_index - 1]
171+
else:
172+
return AnyType(TypeOfAny.from_error)
173+
elif actual_kind == nodes.ARG_STAR2:
174+
if isinstance(actual_type, TypedDictType):
175+
if formal_kind != nodes.ARG_STAR2 and formal_name in actual_type.items:
176+
# Lookup type based on keyword argument name.
177+
assert formal_name is not None
178+
else:
179+
# Pick an arbitrary item if no specified keyword is expected.
180+
formal_name = (set(actual_type.items.keys()) - self.kwargs_used).pop()
181+
self.kwargs_used.add(formal_name)
182+
return actual_type.items[formal_name]
183+
elif (isinstance(actual_type, Instance)
184+
and (actual_type.type.fullname() == 'builtins.dict')):
185+
# Dict **arg.
186+
# TODO: Handle arbitrary Mapping
187+
return actual_type.args[1]
113188
else:
114189
return AnyType(TypeOfAny.from_error)
115-
elif isinstance(arg_type, TupleType):
116-
# Get the next tuple item of a tuple *arg.
117-
tuple_counter[0] += 1
118-
return arg_type.items[tuple_counter[0] - 1]
119-
else:
120-
return AnyType(TypeOfAny.from_error)
121-
elif kind == nodes.ARG_STAR2:
122-
if isinstance(arg_type, Instance) and (arg_type.type.fullname() == 'builtins.dict'):
123-
# Dict **arg. TODO more general (Mapping)
124-
return arg_type.args[1]
125190
else:
126-
return AnyType(TypeOfAny.from_error)
127-
else:
128-
# No translation for other kinds.
129-
return arg_type
191+
# No translation for other kinds -- 1:1 mapping.
192+
return actual_type

0 commit comments

Comments
 (0)