Skip to content

Commit 48758ee

Browse files
committed
Implement tests.
Also: * Fix support for using dict(...) in TypedDict instance constructor. * Allow instantiation of empty TypedDict. * Disallow underscore prefix on TypedDict item names. * Fix TypedDict subtyping.
1 parent 0ba684d commit 48758ee

12 files changed

+380
-43
lines changed

mypy/checkexpr.py

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -193,38 +193,56 @@ def check_typeddict_call(self, callee: TypedDictType,
193193
item_args = args
194194
return self.check_typeddict_call_with_kwargs(
195195
callee, OrderedDict(zip(item_names, item_args)), context)
196-
elif len(args) == 1 and arg_kinds[0] == ARG_POS and isinstance(args[0], DictExpr):
197-
# ex: Point({'x': 42, 'y': 1337})
198-
# ex: Point(dict(x=42, y=1337))
199-
kwargs = args[0]
200-
item_name_exprs = [item[0] for item in kwargs.items]
201-
item_args = [item[1] for item in kwargs.items]
202-
203-
item_names = [] # List[str]
204-
for item_name_expr in item_name_exprs:
205-
if not isinstance(item_name_expr, StrExpr):
206-
self.chk.fail(messages.TYPEDDICT_KEY_MUST_BE_STRING_LITERAL, item_name_expr)
207-
return AnyType()
208-
item_names.append(item_name_expr.value)
209196

197+
if len(args) == 1 and arg_kinds[0] == ARG_POS:
198+
unique_arg = args[0]
199+
if isinstance(unique_arg, DictExpr):
200+
# ex: Point({'x': 42, 'y': 1337})
201+
return self.check_typeddict_call_with_dict(callee, unique_arg, context)
202+
if isinstance(unique_arg, CallExpr) and isinstance(unique_arg.analyzed, DictExpr):
203+
# ex: Point(dict(x=42, y=1337))
204+
return self.check_typeddict_call_with_dict(callee, unique_arg.analyzed, context)
205+
206+
if len(args) == 0:
207+
# ex: EmptyDict()
210208
return self.check_typeddict_call_with_kwargs(
211-
callee, OrderedDict(zip(item_names, item_args)), context)
212-
else:
213-
self.chk.fail(messages.INVALID_TYPEDDICT_ARGS, context)
214-
return AnyType()
209+
callee, OrderedDict(), context)
210+
211+
self.chk.fail(messages.INVALID_TYPEDDICT_ARGS, context)
212+
return AnyType()
213+
214+
def check_typeddict_call_with_dict(self, callee: TypedDictType,
215+
kwargs: DictExpr,
216+
context: Context) -> Type:
217+
item_name_exprs = [item[0] for item in kwargs.items]
218+
item_args = [item[1] for item in kwargs.items]
219+
220+
item_names = [] # List[str]
221+
for item_name_expr in item_name_exprs:
222+
if not isinstance(item_name_expr, StrExpr):
223+
self.chk.fail(messages.TYPEDDICT_ITEM_NAME_MUST_BE_STRING_LITERAL, item_name_expr)
224+
return AnyType()
225+
item_names.append(item_name_expr.value)
226+
227+
return self.check_typeddict_call_with_kwargs(
228+
callee, OrderedDict(zip(item_names, item_args)), context)
215229

216230
def check_typeddict_call_with_kwargs(self, callee: TypedDictType,
217231
kwargs: 'OrderedDict[str, Expression]',
218232
context: Context) -> Type:
219233
if callee.items.keys() != kwargs.keys():
234+
callee_item_names = callee.items.keys()
235+
kwargs_item_names = kwargs.keys()
236+
missing_item_names = [k for k in callee_item_names if k not in kwargs_item_names]
237+
extra_item_names = [k for k in kwargs_item_names if k not in callee_item_names]
238+
220239
self.chk.fail(
221-
'Expected keys {} but found {}. Missing {}. Extra {}.'.format(
222-
list(callee.items.keys()),
223-
list(kwargs.keys()),
224-
# TODO: Ensure order is preserved
225-
callee.items.keys() - kwargs.keys(),
226-
# TODO: Ensure order is preserved
227-
kwargs.keys() - callee.items.keys()),
240+
'Expected items {} but found {}. Missing {}. Extra {}.'.format(
241+
list(callee_item_names),
242+
list(kwargs_item_names),
243+
missing_item_names,
244+
extra_item_names
245+
),
228246
context)
229247
return AnyType()
230248

mypy/messages.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,9 @@
8989
'Keyword argument only valid with "str" key type in call to "dict"'
9090
ALL_MUST_BE_SEQ_STR = 'Type of __all__ must be {}, not {}'
9191
INVALID_TYPEDDICT_ARGS = \
92-
'Expected keyword arguments or dictionary literal in TypedDict constructor'
93-
TYPEDDICT_KEY_MUST_BE_STRING_LITERAL = \
94-
'Expected key in TypedDict constructor to be string literal'
92+
'Expected keyword arguments, {...}, or dict(...) in TypedDict constructor'
93+
TYPEDDICT_ITEM_NAME_MUST_BE_STRING_LITERAL = \
94+
'Expected TypedDict item name to be string literal'
9595

9696

9797
class MessageBuilder:

mypy/semanal.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1654,7 +1654,7 @@ def parse_namedtuple_args(self, call: CallExpr,
16541654
types = [AnyType() for _ in items]
16551655
underscore = [item for item in items if item.startswith('_')]
16561656
if underscore:
1657-
self.fail("namedtuple() Field names cannot start with an underscore: "
1657+
self.fail("namedtuple() field names cannot start with an underscore: "
16581658
+ ', '.join(underscore), call)
16591659
return items, types, ok
16601660

@@ -1842,6 +1842,10 @@ def parse_typeddict_args(self, call: CallExpr,
18421842
"TypedDict() expects a dictionary literal as the second argument", call)
18431843
dictexpr = args[1]
18441844
items, types, ok = self.parse_typeddict_fields_with_types(dictexpr.items, call)
1845+
underscore = [item for item in items if item.startswith('_')]
1846+
if underscore:
1847+
self.fail("TypedDict() item names cannot start with an underscore: "
1848+
+ ', '.join(underscore), call)
18451849
return items, types, ok
18461850

18471851
def parse_typeddict_fields_with_types(self, dict_items: List[Tuple[Expression, Expression]],

mypy/subtypes.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ def visit_instance(self, left: Instance) -> bool:
125125
self.right,
126126
self.check_type_parameter):
127127
return True
128+
if left.type.typeddict_type is not None and is_subtype(left.type.typeddict_type,
129+
self.right,
130+
self.check_type_parameter):
131+
return True
128132
rname = right.type.fullname()
129133
if not left.type.has_base(rname) and rname != 'builtins.object':
130134
return False
@@ -190,14 +194,16 @@ def visit_tuple_type(self, left: TupleType) -> bool:
190194
def visit_typeddict_type(self, left: TypedDictType) -> bool:
191195
right = self.right
192196
if isinstance(right, Instance):
197+
if right.type.typeddict_type is not None:
198+
return is_subtype(left, right.type.typeddict_type, self.check_type_parameter)
193199
return is_subtype(left.fallback, right, self.check_type_parameter)
194200
elif isinstance(right, TypedDictType):
195201
if not left.names_are_wider_than(right):
196202
return False
197203
for (_, l, r) in left.zip(right):
198-
if not is_subtype(l, r, self.check_type_parameter):
204+
if not is_subtype(r, l, self.check_type_parameter):
199205
return False
200-
if not is_subtype(left.fallback, right.fallback, self.check_type_parameter):
206+
if not is_subtype(right.fallback, left.fallback, self.check_type_parameter):
201207
return False
202208
return True
203209
else:

mypy/test/testcheck.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
'check-isinstance.test',
5555
'check-lists.test',
5656
'check-namedtuple.test',
57+
'check-typeddict.test',
5758
'check-type-aliases.test',
5859
'check-ignore.test',
5960
'check-type-promotion.test',

mypy/types.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1365,7 +1365,10 @@ def visit_tuple_type(self, t: TupleType) -> str:
13651365
def visit_typeddict_type(self, t: TypedDictType) -> str:
13661366
s = self.keywords_str(t.items.items())
13671367
if t.fallback and t.fallback.type:
1368-
return 'TypedDict({}, _fallback={})'.format(s, t.fallback.accept(self))
1368+
if s == '':
1369+
return 'TypedDict(_fallback={})'.format(t.fallback.accept(self))
1370+
else:
1371+
return 'TypedDict({}, _fallback={})'.format(s, t.fallback.accept(self))
13691372
return 'TypedDict({})'.format(s)
13701373

13711374
def visit_star_type(self, t: StarType) -> str:

test-data/unit/check-namedtuple.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ x[2] # E: Tuple index out of range
2323
[case testNamedTupleNoUnderscoreFields]
2424
from collections import namedtuple
2525

26-
X = namedtuple('X', 'x, _y, _z') # E: namedtuple() Field names cannot start with an underscore: _y, _z
26+
X = namedtuple('X', 'x, _y, _z') # E: namedtuple() field names cannot start with an underscore: _y, _z
2727

2828
[case testNamedTupleAccessingAttributes]
2929
from collections import namedtuple

0 commit comments

Comments
 (0)