Skip to content

Commit 3c8e5ac

Browse files
committed
TypedDict: Recognize declaration of TypedDict('Point', {'x': int, 'y': int}).
Also: * Create mypy.typing module for experimental typing additions.
1 parent 7e2b3b4 commit 3c8e5ac

File tree

9 files changed

+180
-7
lines changed

9 files changed

+180
-7
lines changed

mypy/checker.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
Context, ListComprehension, ConditionalExpr, GeneratorExpr,
2121
Decorator, SetExpr, TypeVarExpr, NewTypeExpr, PrintStmt,
2222
LITERAL_TYPE, BreakStmt, ContinueStmt, ComparisonExpr, StarExpr,
23-
YieldFromExpr, NamedTupleExpr, SetComprehension,
23+
YieldFromExpr, NamedTupleExpr, TypedDictExpr, SetComprehension,
2424
DictionaryComprehension, ComplexExpr, EllipsisExpr, TypeAliasExpr,
2525
RefExpr, YieldExpr, BackquoteExpr, ImportFrom, ImportAll, ImportBase,
2626
AwaitExpr,
@@ -2054,6 +2054,10 @@ def visit_namedtuple_expr(self, e: NamedTupleExpr) -> Type:
20542054
# TODO: Perhaps return a type object type?
20552055
return AnyType()
20562056

2057+
def visit_typeddict_expr(self, e: TypedDictExpr) -> Type:
2058+
# TODO: Perhaps return a type object type?
2059+
return AnyType()
2060+
20572061
def visit_list_expr(self, e: ListExpr) -> Type:
20582062
return self.expr_checker.visit_list_expr(e)
20592063

mypy/nodes.py

+18-2
Original file line numberDiff line numberDiff line change
@@ -1722,7 +1722,7 @@ def accept(self, visitor: NodeVisitor[T]) -> T:
17221722

17231723

17241724
class NamedTupleExpr(Expression):
1725-
"""Named tuple expression namedtuple(...)."""
1725+
"""Named tuple expression namedtuple(...) or NamedTuple(...)."""
17261726

17271727
# The class representation of this named tuple (its tuple_type attribute contains
17281728
# the tuple item types)
@@ -1735,6 +1735,19 @@ def accept(self, visitor: NodeVisitor[T]) -> T:
17351735
return visitor.visit_namedtuple_expr(self)
17361736

17371737

1738+
class TypedDictExpr(Expression):
1739+
"""Typed dict expression TypedDict(...)."""
1740+
1741+
# The class representation of this typed dict
1742+
info = None # type: TypeInfo
1743+
1744+
def __init__(self, info: 'TypeInfo') -> None:
1745+
self.info = info
1746+
1747+
def accept(self, visitor: NodeVisitor[T]) -> T:
1748+
return visitor.visit_typeddict_expr(self)
1749+
1750+
17381751
class PromoteExpr(Expression):
17391752
"""Ducktype class decorator expression _promote(...)."""
17401753

@@ -1857,6 +1870,9 @@ class is generic then it will be a type constructor of higher kind.
18571870
# Is this a named tuple type?
18581871
is_named_tuple = False
18591872

1873+
# Is this a typed dict type?
1874+
is_typed_dict = False
1875+
18601876
# Is this a newtype type?
18611877
is_newtype = False
18621878

@@ -1868,7 +1884,7 @@ class is generic then it will be a type constructor of higher kind.
18681884

18691885
FLAGS = [
18701886
'is_abstract', 'is_enum', 'fallback_to_any', 'is_named_tuple',
1871-
'is_newtype', 'is_dummy'
1887+
'is_typed_dict', 'is_newtype', 'is_dummy'
18721888
]
18731889

18741890
def __init__(self, names: 'SymbolTable', defn: ClassDef, module_name: str) -> None:

mypy/semanal.py

+110-3
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
FuncExpr, MDEF, FuncBase, Decorator, SetExpr, TypeVarExpr, NewTypeExpr,
6161
StrExpr, BytesExpr, PrintStmt, ConditionalExpr, PromoteExpr,
6262
ComparisonExpr, StarExpr, ARG_POS, ARG_NAMED, MroError, type_aliases,
63-
YieldFromExpr, NamedTupleExpr, NonlocalDecl, SymbolNode,
63+
YieldFromExpr, NamedTupleExpr, TypedDictExpr, NonlocalDecl, SymbolNode,
6464
SetComprehension, DictionaryComprehension, TYPE_ALIAS, TypeAliasExpr,
6565
YieldExpr, ExecStmt, Argument, BackquoteExpr, ImportBase, AwaitExpr,
6666
IntExpr, FloatExpr, UnicodeExpr, EllipsisExpr,
@@ -1126,6 +1126,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
11261126
self.process_newtype_declaration(s)
11271127
self.process_typevar_declaration(s)
11281128
self.process_namedtuple_definition(s)
1129+
self.process_typeddict_definition(s)
11291130

11301131
if (len(s.lvalues) == 1 and isinstance(s.lvalues[0], NameExpr) and
11311132
s.lvalues[0].name == '__all__' and s.lvalues[0].kind == GDEF and
@@ -1578,10 +1579,9 @@ def process_namedtuple_definition(self, s: AssignmentStmt) -> None:
15781579
# Yes, it's a valid namedtuple definition. Add it to the symbol table.
15791580
node = self.lookup(name, s)
15801581
node.kind = GDEF # TODO locally defined namedtuple
1581-
# TODO call.analyzed
15821582
node.node = named_tuple
15831583

1584-
def check_namedtuple(self, node: Expression, var_name: str = None) -> TypeInfo:
1584+
def check_namedtuple(self, node: Expression, var_name: str = None) -> Optional[TypeInfo]:
15851585
"""Check if a call defines a namedtuple.
15861586
15871587
The optional var_name argument is the name of the variable to
@@ -1776,6 +1776,113 @@ def analyze_types(self, items: List[Expression]) -> List[Type]:
17761776
result.append(AnyType())
17771777
return result
17781778

1779+
def process_typeddict_definition(self, s: AssignmentStmt) -> None:
1780+
"""Check if s defines a TypedDict; if yes, store the definition in symbol table."""
1781+
if len(s.lvalues) != 1 or not isinstance(s.lvalues[0], NameExpr):
1782+
return
1783+
lvalue = s.lvalues[0]
1784+
name = lvalue.name
1785+
typed_dict = self.check_typeddict(s.rvalue, name)
1786+
if typed_dict is None:
1787+
return
1788+
# Yes, it's a valid TypedDict definition. Add it to the symbol table.
1789+
node = self.lookup(name, s)
1790+
node.kind = GDEF # TODO locally defined TypedDict
1791+
node.node = typed_dict
1792+
1793+
def check_typeddict(self, node: Expression, var_name: str = None) -> Optional[TypeInfo]:
1794+
"""Check if a call defines a TypedDict.
1795+
1796+
The optional var_name argument is the name of the variable to
1797+
which this is assigned, if any.
1798+
1799+
If it does, return the corresponding TypeInfo. Return None otherwise.
1800+
1801+
If the definition is invalid but looks like a TypedDict,
1802+
report errors but return (some) TypeInfo.
1803+
"""
1804+
if not isinstance(node, CallExpr):
1805+
return None
1806+
call = node
1807+
if not isinstance(call.callee, RefExpr):
1808+
return None
1809+
callee = call.callee
1810+
fullname = callee.fullname
1811+
if fullname != 'mypy.typing.TypedDict':
1812+
return None
1813+
items, types, ok = self.parse_typeddict_args(call, fullname)
1814+
if not ok:
1815+
# Error. Construct dummy return value.
1816+
return self.build_typeddict_typeinfo('TypedDict', [], [])
1817+
else:
1818+
# Give it a unique name derived from the line number.
1819+
name = cast(StrExpr, call.args[0]).value
1820+
if name != var_name:
1821+
name += '@' + str(call.line)
1822+
info = self.build_typeddict_typeinfo(name, items, types)
1823+
# Store it as a global just in case it would remain anonymous.
1824+
self.globals[name] = SymbolTableNode(GDEF, info, self.cur_mod_id)
1825+
call.analyzed = TypedDictExpr(info)
1826+
call.analyzed.set_line(call.line, call.column)
1827+
return info
1828+
1829+
def parse_typeddict_args(self, call: CallExpr,
1830+
fullname: str) -> Tuple[List[str], List[Type], bool]:
1831+
# TODO Share code with check_argument_count in checkexpr.py?
1832+
args = call.args
1833+
if len(args) < 2:
1834+
return self.fail_typeddict_arg("Too few arguments for TypedDict()", call)
1835+
if len(args) > 2:
1836+
return self.fail_typeddict_arg("Too many arguments for TypedDict()", call)
1837+
if call.arg_kinds != [ARG_POS, ARG_POS]:
1838+
return self.fail_typeddict_arg("Unexpected arguments to TypedDict()", call)
1839+
if not isinstance(args[0], (StrExpr, BytesExpr, UnicodeExpr)):
1840+
return self.fail_typeddict_arg(
1841+
"TypedDict() expects a string literal as the first argument", call)
1842+
if not isinstance(args[1], DictExpr):
1843+
return self.fail_typeddict_arg(
1844+
"TypedDict() expects a dictionary literal as the second argument", call)
1845+
dictexpr = args[1]
1846+
items, types, ok = self.parse_typeddict_fields_with_types(dictexpr.items, call)
1847+
return items, types, ok
1848+
1849+
def parse_typeddict_fields_with_types(self, dict_items: List[Tuple[Expression, Expression]],
1850+
context: Context) -> Tuple[List[str], List[Type], bool]:
1851+
items = [] # type: List[str]
1852+
types = [] # type: List[Type]
1853+
for (field_name_expr, field_type_expr) in dict_items:
1854+
if isinstance(field_name_expr, (StrExpr, BytesExpr, UnicodeExpr)):
1855+
items.append(field_name_expr.value)
1856+
else:
1857+
return self.fail_typeddict_arg("Invalid TypedDict() field name", field_name_expr)
1858+
try:
1859+
type = expr_to_unanalyzed_type(field_type_expr)
1860+
except TypeTranslationError:
1861+
return self.fail_typeddict_arg('Invalid field type', field_type_expr)
1862+
types.append(self.anal_type(type))
1863+
return items, types, True
1864+
1865+
def fail_typeddict_arg(self, message: str,
1866+
context: Context) -> Tuple[List[str], List[Type], bool]:
1867+
self.fail(message, context)
1868+
return [], [], False
1869+
1870+
def build_typeddict_typeinfo(self, name: str, items: List[str],
1871+
types: List[Type]) -> TypeInfo:
1872+
strtype = self.named_type('__builtins__.str') # type: Type
1873+
dictype = (self.named_type_or_none('builtins.dict', [strtype, AnyType()])
1874+
or self.object_type())
1875+
fallback = dictype
1876+
1877+
info = self.basic_new_typeinfo(name, fallback)
1878+
info.is_typed_dict = True
1879+
1880+
# (TODO: Store {items, types} inside "info" somewhere for use later.
1881+
# Probably inside a new "info.keys" field which
1882+
# would be analogous to "info.names".)
1883+
1884+
return info
1885+
17791886
def visit_decorator(self, dec: Decorator) -> None:
17801887
for d in dec.decorators:
17811888
d.accept(self)

mypy/strconv.py

+4
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,10 @@ def visit_namedtuple_expr(self, o: 'mypy.nodes.NamedTupleExpr') -> str:
422422
o.info.name(),
423423
o.info.tuple_type)
424424

425+
def visit_typeddict_expr(self, o: 'mypy.nodes.TypedDictExpr') -> str:
426+
return 'TypedDictExpr:{}({})'.format(o.line,
427+
o.info.name())
428+
425429
def visit__promote_expr(self, o: 'mypy.nodes.PromoteExpr') -> str:
426430
return 'PromoteExpr:{}({})'.format(o.line, o.type)
427431

mypy/test/testsemanal.py

+2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
'semanal-statements.test',
3030
'semanal-abstractclasses.test',
3131
'semanal-namedtuple.test',
32+
'semanal-typeddict.test',
3233
'semanal-python2.test']
3334

3435

@@ -78,6 +79,7 @@ def test_semanal(testcase):
7879
# TODO the test is not reliable
7980
if (not f.path.endswith((os.sep + 'builtins.pyi',
8081
'typing.pyi',
82+
'typing.py',
8183
'abc.pyi',
8284
'collections.pyi'))
8385
and not os.path.basename(f.path).startswith('_')

mypy/treetransform.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
SliceExpr, OpExpr, UnaryExpr, FuncExpr, TypeApplication, PrintStmt,
1818
SymbolTable, RefExpr, TypeVarExpr, NewTypeExpr, PromoteExpr,
1919
ComparisonExpr, TempNode, StarExpr, Statement, Expression,
20-
YieldFromExpr, NamedTupleExpr, NonlocalDecl, SetComprehension,
20+
YieldFromExpr, NamedTupleExpr, TypedDictExpr, NonlocalDecl, SetComprehension,
2121
DictionaryComprehension, ComplexExpr, TypeAliasExpr, EllipsisExpr,
2222
YieldExpr, ExecStmt, Argument, BackquoteExpr, AwaitExpr,
2323
)
@@ -492,6 +492,9 @@ def visit_newtype_expr(self, node: NewTypeExpr) -> NewTypeExpr:
492492
def visit_namedtuple_expr(self, node: NamedTupleExpr) -> NamedTupleExpr:
493493
return NamedTupleExpr(node.info)
494494

495+
def visit_typeddict_expr(self, node: TypedDictExpr) -> Node:
496+
return TypedDictExpr(node.info)
497+
495498
def visit__promote_expr(self, node: PromoteExpr) -> PromoteExpr:
496499
return PromoteExpr(node.type)
497500

mypy/typing.py

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""The "mypy.typing" module defines experimental extensions to the standard
2+
"typing" module that is supported by the mypy typechecker.
3+
"""
4+
5+
from typing import cast, Dict, Type, TypeVar
6+
7+
8+
_T = TypeVar('_T')
9+
10+
11+
def TypedDict(typename: str, fields: Dict[str, Type[_T]]) -> Type[dict]:
12+
"""TypedDict creates a dictionary type that expects all of its
13+
instances to have a certain common set of keys, with each key
14+
associated with a value of a consistent type. This expectation
15+
is not checked at runtime but is only enforced by typecheckers.
16+
"""
17+
def new_dict(*args, **kwargs):
18+
return dict(*args, **kwargs)
19+
20+
new_dict.__name__ = typename # type: ignore # https://github.com/python/mypy/issues/708
21+
new_dict.__supertype__ = dict # type: ignore # https://github.com/python/mypy/issues/708
22+
return cast(Type[dict], new_dict)

mypy/visitor.py

+3
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,9 @@ def visit_type_alias_expr(self, o: 'mypy.nodes.TypeAliasExpr') -> T:
225225
def visit_namedtuple_expr(self, o: 'mypy.nodes.NamedTupleExpr') -> T:
226226
pass
227227

228+
def visit_typeddict_expr(self, o: 'mypy.nodes.TypedDictExpr') -> T:
229+
pass
230+
228231
def visit_newtype_expr(self, o: 'mypy.nodes.NewTypeExpr') -> T:
229232
pass
230233

test-data/unit/semanal-typeddict.test

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
-- Semantic analysis of typed dicts
2+
3+
[case testCanDefineTypedDictType]
4+
from mypy.typing import TypedDict
5+
Point = TypedDict('Point', {'x': int, 'y': int})
6+
[builtins fixtures/dict.pyi]
7+
[out]
8+
MypyFile:1(
9+
ImportFrom:1(mypy.typing, [TypedDict])
10+
AssignmentStmt:2(
11+
NameExpr(Point* [__main__.Point])
12+
TypedDictExpr:2(Point)))

0 commit comments

Comments
 (0)