Skip to content

Commit 80bcb3e

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 b470aba commit 80bcb3e

12 files changed

+380
-43
lines changed

mypy/checkexpr.py

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

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

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

mypy/messages.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@
7272
'Keyword argument only valid with "str" key type in call to "dict"'
7373
ALL_MUST_BE_SEQ_STR = 'Type of __all__ must be {}, not {}'
7474
INVALID_TYPEDDICT_ARGS = \
75-
'Expected keyword arguments or dictionary literal in TypedDict constructor'
76-
TYPEDDICT_KEY_MUST_BE_STRING_LITERAL = \
77-
'Expected key in TypedDict constructor to be string literal'
75+
'Expected keyword arguments, {...}, or dict(...) in TypedDict constructor'
76+
TYPEDDICT_ITEM_NAME_MUST_BE_STRING_LITERAL = \
77+
'Expected TypedDict item name to be string literal'
7878

7979

8080
class MessageBuilder:

mypy/semanal.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1717,7 +1717,7 @@ def parse_namedtuple_args(self, call: CallExpr,
17171717
types = [AnyType() for _ in items]
17181718
underscore = [item for item in items if item.startswith('_')]
17191719
if underscore:
1720-
self.fail("namedtuple() Field names cannot start with an underscore: "
1720+
self.fail("namedtuple() field names cannot start with an underscore: "
17211721
+ ', '.join(underscore), call)
17221722
return items, types, ok
17231723

@@ -1905,6 +1905,10 @@ def parse_typeddict_args(self, call: CallExpr,
19051905
"TypedDict() expects a dictionary literal as the second argument", call)
19061906
dictexpr = args[1]
19071907
items, types, ok = self.parse_typeddict_fields_with_types(dictexpr.items, call)
1908+
underscore = [item for item in items if item.startswith('_')]
1909+
if underscore:
1910+
self.fail("TypedDict() item names cannot start with an underscore: "
1911+
+ ', '.join(underscore), call)
19081912
return items, types, ok
19091913

19101914
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
@@ -189,14 +193,16 @@ def visit_tuple_type(self, left: TupleType) -> bool:
189193
def visit_typeddict_type(self, left: TypedDictType) -> bool:
190194
right = self.right
191195
if isinstance(right, Instance):
196+
if right.type.typeddict_type is not None:
197+
return is_subtype(left, right.type.typeddict_type, self.check_type_parameter)
192198
return is_subtype(left.fallback, right, self.check_type_parameter)
193199
elif isinstance(right, TypedDictType):
194200
if not left.names_are_wider_than(right):
195201
return False
196202
for (_, l, r) in left.zip(right):
197-
if not is_subtype(l, r, self.check_type_parameter):
203+
if not is_subtype(r, l, self.check_type_parameter):
198204
return False
199-
if not is_subtype(left.fallback, right.fallback, self.check_type_parameter):
205+
if not is_subtype(right.fallback, left.fallback, self.check_type_parameter):
200206
return False
201207
return True
202208
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
@@ -1366,7 +1366,10 @@ def visit_tuple_type(self, t: TupleType) -> str:
13661366
def visit_typeddict_type(self, t: TypedDictType) -> str:
13671367
s = self.keywords_str(t.items.items())
13681368
if t.fallback and t.fallback.type:
1369-
return 'TypedDict({}, _fallback={})'.format(s, t.fallback.accept(self))
1369+
if s == '':
1370+
return 'TypedDict(_fallback={})'.format(t.fallback.accept(self))
1371+
else:
1372+
return 'TypedDict({}, _fallback={})'.format(s, t.fallback.accept(self))
13701373
return 'TypedDict({})'.format(s)
13711374

13721375
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)