Skip to content

Commit 402d734

Browse files
msullivanilevkivskyi
authored andcommitted
Fix missing Optional in DictExpr.item (#5500)
The key part of an entry in DictExpr can be None if one of the entries in a dictionary is '**x'. Fixing the type errors revealed a crash bug such a dict is used to create a TypedDict.
1 parent 1adacd0 commit 402d734

File tree

6 files changed

+20
-10
lines changed

6 files changed

+20
-10
lines changed

mypy/checkexpr.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -385,13 +385,13 @@ def check_typeddict_call(self, callee: TypedDictType,
385385
def check_typeddict_call_with_dict(self, callee: TypedDictType,
386386
kwargs: DictExpr,
387387
context: Context) -> Type:
388-
item_name_exprs = [item[0] for item in kwargs.items]
389388
item_args = [item[1] for item in kwargs.items]
390389

391390
item_names = [] # List[str]
392-
for item_name_expr in item_name_exprs:
391+
for item_name_expr, item_arg in kwargs.items:
393392
if not isinstance(item_name_expr, StrExpr):
394-
self.chk.fail(messages.TYPEDDICT_KEY_MUST_BE_STRING_LITERAL, item_name_expr)
393+
key_context = item_name_expr or item_arg
394+
self.chk.fail(messages.TYPEDDICT_KEY_MUST_BE_STRING_LITERAL, key_context)
395395
return AnyType(TypeOfAny.from_error)
396396
item_names.append(item_name_expr.value)
397397

mypy/literals.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,8 @@ def visit_list_expr(self, e: ListExpr) -> Optional[Key]:
139139

140140
def visit_dict_expr(self, e: DictExpr) -> Optional[Key]:
141141
if all(a and literal(a) == literal(b) == LITERAL_YES for a, b in e.items):
142-
rest = tuple((literal_hash(a), literal_hash(b)) for a, b in e.items) # type: Any
142+
rest = tuple((literal_hash(a) if a else None, literal_hash(b))
143+
for a, b in e.items) # type: Any
143144
return ('Dict',) + rest
144145
return None
145146

mypy/nodes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1694,9 +1694,9 @@ def accept(self, visitor: ExpressionVisitor[T]) -> T:
16941694
class DictExpr(Expression):
16951695
"""Dictionary literal expression {key: value, ...}."""
16961696

1697-
items = None # type: List[Tuple[Expression, Expression]]
1697+
items = None # type: List[Tuple[Optional[Expression], Expression]]
16981698

1699-
def __init__(self, items: List[Tuple[Expression, Expression]]) -> None:
1699+
def __init__(self, items: List[Tuple[Optional[Expression], Expression]]) -> None:
17001700
super().__init__()
17011701
self.items = items
17021702

mypy/semanal_typeddict.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,15 +242,18 @@ def parse_typeddict_args(self, call: CallExpr) -> Tuple[List[str], List[Type], b
242242
assert total is not None
243243
return items, types, total, ok
244244

245-
def parse_typeddict_fields_with_types(self, dict_items: List[Tuple[Expression, Expression]],
246-
context: Context) -> Tuple[List[str], List[Type], bool]:
245+
def parse_typeddict_fields_with_types(
246+
self,
247+
dict_items: List[Tuple[Optional[Expression], Expression]],
248+
context: Context) -> Tuple[List[str], List[Type], bool]:
247249
items = [] # type: List[str]
248250
types = [] # type: List[Type]
249251
for (field_name_expr, field_type_expr) in dict_items:
250252
if isinstance(field_name_expr, (StrExpr, BytesExpr, UnicodeExpr)):
251253
items.append(field_name_expr.value)
252254
else:
253-
self.fail_typeddict_arg("Invalid TypedDict() field name", field_name_expr)
255+
name_context = field_name_expr or field_type_expr
256+
self.fail_typeddict_arg("Invalid TypedDict() field name", name_context)
254257
return [], [], False
255258
try:
256259
type = expr_to_unanalyzed_type(field_type_expr)

mypy/treetransform.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,7 @@ def visit_list_expr(self, node: ListExpr) -> ListExpr:
403403
return ListExpr(self.expressions(node.items))
404404

405405
def visit_dict_expr(self, node: DictExpr) -> DictExpr:
406-
return DictExpr([(self.expr(key), self.expr(value))
406+
return DictExpr([(self.expr(key) if key else None, self.expr(value))
407407
for key, value in node.items])
408408

409409
def visit_tuple_expr(self, node: TupleExpr) -> TupleExpr:

test-data/unit/check-typeddict.test

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,6 +1131,12 @@ from mypy_extensions import TypedDict
11311131
Point = TypedDict('Point', {'x'}) # E: TypedDict() expects a dictionary literal as the second argument
11321132
[builtins fixtures/dict.pyi]
11331133

1134+
[case testCannotCreateTypedDictTypeWithKwargs]
1135+
from mypy_extensions import TypedDict
1136+
d = {'x': int, 'y': int}
1137+
Point = TypedDict('Point', {**d}) # E: Invalid TypedDict() field name
1138+
[builtins fixtures/dict.pyi]
1139+
11341140
-- NOTE: The following code works at runtime but is not yet supported by mypy.
11351141
-- Keyword arguments may potentially be supported in the future.
11361142
[case testCannotCreateTypedDictTypeWithNonpositionalArgs]

0 commit comments

Comments
 (0)