8000 TypedDict: Recognize declaration of TypedDict('Point', {'x': int, 'y'… · python/mypy@76a9a62 · GitHub
[go: up one dir, main page]

Skip to content

Commit 76a9a62

Browse files
davidfstrgvanrossum
authored andcommitted
TypedDict: Recognize declaration of TypedDict('Point', {'x': int, 'y': int}). (#2206)
This is really just the first phase, doing the syntactic analysis; type checking will follow as a separate PR, as will improvements like keyword args.
1 parent 1a1179e commit 76a9a62

11 files changed

+227
-16
lines changed

extensions/mypy_extensions.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,16 @@
77

88
# NOTE: This module must support Python 2.7 in addition to Python 3.x
99

10-
# (TODO: Declare TypedDict and other extensions here)
10+
11+
def TypedDict(typename, fields):
12+
"""TypedDict creates a dictionary type that expects all of its
13+
instances to have a certain 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
21+
new_dict.__supertype__ = dict
22+
return new_dict

mypy/checker.py

Lines changed: 5 additions & 1 deletion
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, PassStmt, 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,
@@ -2082,6 +2082,10 @@ def visit_namedtuple_expr(self, e: NamedTupleExpr) -> Type:
20822082
# TODO: Perhaps return a type object type?
20832083
return AnyType()
20842084

2085+
def visit_typeddict_expr(self, e: TypedDictExpr) -> Type:
2086+
# TODO: Perhaps return a type object type?
2087+
return AnyType()
2088+
20852089
def visit_list_expr(self, e: ListExpr) -> Type:
20862090
return self.expr_checker.visit_list_expr(e)
20872091

mypy/nodes.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1747,7 +1747,7 @@ def accept(self, visitor: NodeVisitor[T]) -> T:
17471747

17481748

17491749
class NamedTupleExpr(Expression):
1750-
"""Named tuple expression namedtuple(...)."""
1750+
"""Named tuple expression namedtuple(...) or NamedTuple(...)."""
17511751

17521752
# The class representation of this named tuple (its tuple_type attribute contains
17531753
# the tuple item types)
@@ -1760,6 +1760,19 @@ def accept(self, visitor: NodeVisitor[T]) -> T:
17601760
return visitor.visit_namedtuple_expr(self)
17611761

17621762

1763+
class TypedDictExpr(Expression):
1764+
"""Typed dict expression TypedDict(...)."""
1765+
1766+
# The class representation of this typed dict
1767+
info = None # type: TypeInfo
1768+
1769+
def __init__(self, info: 'TypeInfo') -> None:
1770+
self.info = info
1771+
1772+
def accept(self, visitor: NodeVisitor[T]) -> T:
1773+
return visitor.visit_typeddict_expr(self)
1774+
1775+
17631776
class PromoteExpr(Expression):
17641777
"""Ducktype class decorator expression _promote(...)."""
17651778

@@ -1882,6 +1895,9 @@ class is generic then it will be a type constructor of higher kind.
18821895
# Is this a named tuple type?
18831896
is_named_tuple = False
18841897

1898+
# Is this a typed dict type?
1899+
is_typed_dict = False
1900+
18851901
# Is this a newtype type?
18861902
is_newtype = False
18871903

@@ -1893,7 +1909,7 @@ class is generic then it will be a type constructor of higher kind.
18931909

18941910
FLAGS = [
18951911
'is_abstract', 'is_enum', 'fallback_to_any', 'is_named_tuple',
1896-
'is_newtype', 'is_dummy'
1912+
'is_typed_dict', 'is_newtype', 'is_dummy'
18971913
]
18981914

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

mypy/semanal.py

Lines changed: 117 additions & 9 deletions
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,
@@ -1127,6 +1127,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
11271127
self.process_newtype_declaration(s)
11281128
self.process_typevar_declaration(s)
11291129
self.process_namedtuple_definition(s)
1130+
self.process_typeddict_definition(s)
11301131

11311132
if (len(s.lvalues) == 1 and isinstance(s.lvalues[0], NameExpr) and
11321133
s.lvalues[0].name == '__all__' and s.lvalues[0].kind == GDEF and
@@ -1498,9 +1499,9 @@ def get_typevar_declaration(self, s: AssignmentStmt) -> Optional[CallExpr]:
14981499
if not isinstance(s.rvalue, CallExpr):
14991500
return None
15001501
call = s.rvalue
1501-
if not isinstance(call.callee, RefExpr):
1502-
return None
15031502
callee = call.callee
1503+
if not isinstance(callee, RefExpr):
1504+
return None
15041505
if callee.fullname != 'typing.TypeVar':
15051506
return None
15061507
return call
@@ -1579,10 +1580,9 @@ def process_namedtuple_definition(self, s: AssignmentStmt) -> None:
15791580
# Yes, it's a valid namedtuple definition. Add it to the symbol table.
15801581
node = self.lookup(name, s)
15811582
node.kind = GDEF # TODO locally defined namedtuple
1582-
# TODO call.analyzed
15831583
node.node = named_tuple
15841584

1585-
def check_namedtuple(self, node: Expression, var_name: str = None) -> TypeInfo:
1585+
def check_namedtuple(self, node: Expression, var_name: str = None) -> Optional[TypeInfo]:
15861586
"""Check if a call defines a namedtuple.
15871587
15881588
The optional var_name argument is the name of the variable to
@@ -1596,9 +1596,9 @@ def check_namedtuple(self, node: Expression, var_name: str = None) -> TypeInfo:
15961596
if not isinstance(node, CallExpr):
15971597
return None
15981598
call = node
1599-
if not isinstance(call.callee, RefExpr):
1600-
return None
16011599
callee = call.callee
1600+
if not isinstance(callee, RefExpr):
1601+
return None
16021602
fullname = callee.fullname
16031603
if fullname not in ('collections.namedtuple', 'typing.NamedTuple'):
16041604
return None
@@ -1607,9 +1607,9 @@ def check_namedtuple(self, node: Expression, var_name: str = None) -> TypeInfo:
16071607
# Error. Construct dummy return value.
16081608
return self.build_namedtuple_typeinfo('namedtuple', [], [])
16091609
else:
1610-
# Give it a unique name derived from the line number.
16111610
name = cast(StrExpr, call.args[0]).value
16121611
if name != var_name:
1612+
# Give it a unique name derived from the line number.
16131613
name += '@' + str(call.line)
16141614
info = self.build_namedtuple_typeinfo(name, items, types)
16151615
# Store it as a global just in case it would remain anonymous.
@@ -1620,7 +1620,7 @@ def check_namedtuple(self, node: Expression, var_name: str = None) -> TypeInfo:
16201620

16211621
def parse_namedtuple_args(self, call: CallExpr,
16221622
fullname: str) -> Tuple[List[str], List[Type], bool]:
1623-
# TODO Share code with check_argument_count in checkexpr.py?
1623+
# TODO: Share code with check_argument_count in checkexpr.py?
16241624
args = call.args
16251625
if len(args) < 2:
16261626
return self.fail_namedtuple_arg("Too few arguments for namedtuple()", call)
@@ -1777,6 +1777,114 @@ def analyze_types(self, items: List[Expression]) -> List[Type]:
17771777
result.append(AnyType())
17781778
return result
17791779

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

mypy/strconv.py

Lines changed: 4 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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+
'mypy_extensions.pyi',
8183
'abc.pyi',
8284
'collections.pyi'))
8385
and not os.path.basename(f.path).startswith('_')

mypy/treetransform.py

Lines changed: 4 additions & 1 deletion
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/visitor.py

Lines changed: 3 additions & 0 deletions
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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from typing import Dict, Type, TypeVar
2+
3+
T = TypeVar('T')
4+
5+
6+
def TypedDict(typename: str, fields: Dict[str, Type[T]]) -> Type[dict]: ...

test-data/unit/semanal-namedtuple.test

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,11 @@ N = namedtuple('N', 1) # E: List or tuple literal expected as the second argumen
154154
from collections import namedtuple
155155
N = namedtuple('N', ['x', 1]) # E: String literal expected as namedtuple() item
156156

157-
[case testNamedTupleWithInvalidArgs]
157+
-- NOTE: The following code works at runtime but is not yet supported by mypy.
158+
-- Keyword arguments may potentially be supported in the future.
159+
[case testNamedTupleWithNonpositionalArgs]
158160
from collections import namedtuple
159-
N = namedtuple('N', x=['x']) # E: Unexpected arguments to namedtuple()
161+
N = namedtuple(typename='N', field_names=['x']) # E: Unexpected arguments to namedtuple()
160162

161163
[case testInvalidNamedTupleBaseClass]
162164
from typing import NamedTuple

0 commit comments

Comments
 (0)
0