Skip to content

Commit 55ce1b1

Browse files
committed
Handle TypedDict in diff and deps
Required performing a patchup in semanal to *actually* replace the TypeInfo with the 'replaced' version.
1 parent 47f3fff commit 55ce1b1

File tree

7 files changed

+150
-8
lines changed

7 files changed

+150
-8
lines changed

mypy/semanal.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,6 +1325,7 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> bool:
13251325
fields, types, required_keys = self.check_typeddict_classdef(defn)
13261326
info = self.build_typeddict_typeinfo(defn.name, fields, types, required_keys)
13271327
defn.info.replaced = info
1328+
defn.info = info
13281329
node.node = info
13291330
defn.analyzed = TypedDictExpr(info)
13301331
defn.analyzed.line = defn.line
@@ -1360,6 +1361,7 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> bool:
13601361
required_keys.update(new_required_keys)
13611362
info = self.build_typeddict_typeinfo(defn.name, keys, types, required_keys)
13621363
defn.info.replaced = info
1364+
defn.info = info
13631365
node.node = info
13641366
defn.analyzed = TypedDictExpr(info)
13651367
defn.analyzed.line = defn.line

mypy/server/astdiff.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,13 +306,13 @@ def snapshot_definition(node: Optional[SymbolNode],
306306
# type_vars
307307
# bases
308308
# _promote
309-
# typeddict_type
310309
attrs = (node.is_abstract,
311310
node.is_enum,
312311
node.fallback_to_any,
313312
node.is_named_tuple,
314313
node.is_newtype,
315314
snapshot_optional_type(node.tuple_type),
315+
snapshot_optional_type(node.typeddict_type),
316316
[base.fullname() for base in node.mro])
317317
prefix = node.fullname()
318318
symbol_table = snapshot_symbol_table(prefix, node.names)

mypy/server/deps.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ class 'mod.Cls'. This can also refer to an attribute inherited from a
8787
ImportFrom, CallExpr, CastExpr, TypeVarExpr, TypeApplication, IndexExpr, UnaryExpr, OpExpr,
8888
ComparisonExpr, GeneratorExpr, DictionaryComprehension, StarExpr, PrintStmt, ForStmt, WithStmt,
8989
TupleExpr, ListExpr, OperatorAssignmentStmt, DelStmt, YieldFromExpr, Decorator, Block,
90-
TypeInfo, FuncBase, OverloadedFuncDef, RefExpr, Var, NamedTupleExpr, LDEF, MDEF, GDEF,
90+
TypeInfo, FuncBase, OverloadedFuncDef, RefExpr, Var, NamedTupleExpr, TypedDictExpr,
91+
LDEF, MDEF, GDEF,
9192
op_methods, reverse_op_methods, ops_with_inplace_method, unary_op_methods
9293
)
9394
from mypy.traverser import TraverserVisitor
@@ -150,7 +151,6 @@ def __init__(self,
150151
# TODO (incomplete):
151152
# from m import *
152153
# await
153-
# TypedDict
154154
# protocols
155155
# metaclasses
156156
# type aliases
@@ -199,6 +199,8 @@ def visit_class_def(self, o: ClassDef) -> None:
199199
self.add_type_dependencies(base, target=target)
200200
if o.info.tuple_type:
201201
self.add_type_dependencies(o.info.tuple_type, target=make_trigger(target))
202+
if o.info.typeddict_type:
203+
self.add_type_dependencies(o.info.typeddict_type, target=make_trigger(target))
202204
# TODO: Add dependencies based on remaining TypeInfo attributes.
203205
super().visit_class_def(o)
204206
self.is_class = old_is_class
@@ -237,7 +239,6 @@ def visit_block(self, o: Block) -> None:
237239

238240
def visit_assignment_stmt(self, o: AssignmentStmt) -> None:
239241
# TODO: Implement all assignment special forms, including these:
240-
# TypedDict
241242
# Enum
242243
# type aliases
243244
rvalue = o.rvalue
@@ -258,6 +259,12 @@ def visit_assignment_stmt(self, o: AssignmentStmt) -> None:
258259
self.add_type_dependencies(typ, target=make_trigger(prefix))
259260
attr_target = make_trigger('%s.%s' % (prefix, name))
260261
self.add_type_dependencies(typ, target=attr_target)
262+
elif isinstance(rvalue, CallExpr) and isinstance(rvalue.analyzed, TypedDictExpr):
263+
# Depend on the underlying typeddict type
264+
info = rvalue.analyzed.info
265+
assert(info.typeddict_type is not None)
266+
prefix = '%s.%s' % (self.scope.current_full_target(), info.name())
267+
self.add_type_dependencies(info.typeddict_type, target=make_trigger(prefix))
261268
else:
262269
# Normal assignment
263270
super().visit_assignment_stmt(o)
@@ -697,8 +704,11 @@ def visit_type_var(self, typ: TypeVarType) -> List[str]:
697704
return triggers
698705

699706
def visit_typeddict_type(self, typ: TypedDictType) -> List[str]:
700-
# TODO: implement
701-
return []
707+
triggers = []
708+
for item in typ.items.values():
709+
triggers.extend(get_type_triggers(item))
710+
triggers.extend(get_type_triggers(typ.fallback))
711+
return triggers
702712

703713
def visit_unbound_type(self, typ: UnboundType) -> List[str]:
704714
return []

test-data/unit/deps.test

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,3 +548,47 @@ x = 1
548548
<pkg.a> -> pkg.mod
549549
<pkg.mod> -> pkg
550550
<pkg> -> m
551+
552+
[case testTypedDict]
553+
from mypy_extensions import TypedDict
554+
Point = TypedDict('Point', {'x': int, 'y': int})
555+
p = Point(dict(x=42, y=1337))
556+
def foo(x: Point) -> int:
557+
return x['x'] + x['y']
558+
[builtins fixtures/dict.pyi]
559+
[out]
560+
<m.Point> -> <m.foo>, <m.p>, m, m.foo
561+
<m.p> -> m
562+
<mypy_extensions.TypedDict> -> m
563+
564+
[case testTypedDict2]
565+
from mypy_extensions import TypedDict
566+
class A: pass
567+
Point = TypedDict('Point', {'x': int, 'y': A})
568+
p = Point(dict(x=42, y=A()))
569+
def foo(x: Point) -> int:
570+
return x['x']
571+
[builtins fixtures/dict.pyi]
572+
[out]
573+
<m.A.__init__> -> m
574+
<m.A> -> <m.Point>, <m.foo>, <m.p>, m, m.A, m.foo
575+
<m.Point> -> <m.foo>, <m.p>, m, m.foo
576+
<m.p> -> m
577+
<mypy_extensions.TypedDict> -> m
578+
579+
[case testTypedDict3]
580+
from mypy_extensions import TypedDict
581+
class A: pass
582+
class Point(TypedDict):
583+
x: int
584+
y: A
585+
p = Point(dict(x=42, y=A()))
586+
def foo(x: Point) -> int:
587+
return x['x']
588+
[builtins fixtures/dict.pyi]
589+
[out]
590+
<m.A.__init__> -> m
591+
<m.A> -> <m.Point>, <m.foo>, <m.p>, m, m.A, m.foo
592+
<m.Point> -> <m.foo>, <m.p>, m, m.Point, m.foo
593+
<m.p> -> m
594+
<mypy_extensions.TypedDict> -> m

test-data/unit/diff.test

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,3 +562,33 @@ B = Enum('B', 'y')
562562
[out]
563563
__main__.B.x
564564
__main__.B.y
565+
566+
[case testTypedDict]
567+
from mypy_extensions import TypedDict
568+
Point = TypedDict('Point', {'x': int, 'y': int})
569+
p = Point(dict(x=42, y=1337))
570+
[file next.py]
571+
from mypy_extensions import TypedDict
572+
Point = TypedDict('Point', {'x': int, 'y': str})
573+
p = Point(dict(x=42, y='lurr'))
574+
[builtins fixtures/dict.pyi]
575+
[out]
576+
__main__.Point
577+
__main__.p
578+
579+
[case testTypedDict2]
580+
from mypy_extensions import TypedDict
581+
class Point(TypedDict):
582+
x: int
583+
y: int
584+
p = Point(dict(x=42, y=1337))
585+
[file next.py]
586+
from mypy_extensions import TypedDict
587+
class Point(TypedDict):
588+
x: int
589+
y: str
590+
p = Point(dict(x=42, y='lurr'))
591+
[builtins fixtures/dict.pyi]
592+
[out]
593+
__main__.Point
594+
__main__.p

test-data/unit/fine-grained.test

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1669,3 +1669,59 @@ def f(x: a.N) -> None:
16691669
f(a.x)
16701670
[out]
16711671
==
1672+
1673+
[case testTypedDictRefresh]
1674+
[builtins fixtures/dict.pyi]
1675+
import a
1676+
[file a.py]
1677+
from mypy_extensions import TypedDict
1678+
Point = TypedDict('Point', {'x': int, 'y': int})
1679+
p = Point(dict(x=42, y=1337))
1680+
[file a.py.2]
1681+
from mypy_extensions import TypedDict
1682+
Point = TypedDict('Point', {'x': int, 'y': int})
1683+
p = Point(dict(x=42, y=1337))
1684+
[out]
1685+
==
1686+
1687+
[case testTypedDictUpdate]
1688+
import b
1689+
[file a.py]
1690+
from mypy_extensions import TypedDict
1691+
Point = TypedDict('Point', {'x': int, 'y': int})
1692+
p = Point(dict(x=42, y=1337))
1693+
[file a.py.2]
1694+
from mypy_extensions import TypedDict
1695+
Point = TypedDict('Point', {'x': int, 'y': str})
1696+
p = Point(dict(x=42, y='lurr'))
1697+
[file b.py]
1698+
from a import Point
1699+
def foo(x: Point) -> int:
1700+
return x['x'] + x['y']
1701+
[builtins fixtures/dict.pyi]
1702+
[out]
1703+
==
1704+
b.py:3: error: Unsupported operand types for + ("int" and "str")
1705+
1706+
[case testTypedDictUpdate]
1707+
import b
1708+
[file a.py]
1709+
from mypy_extensions import TypedDict
1710+
class Point(TypedDict):
1711+
x: int
1712+
y: int
1713+
p = Point(dict(x=42, y=1337))
1714+
[file a.py.2]
1715+
from mypy_extensions import TypedDict
1716+
class Point(TypedDict):
1717+
x: int
1718+
y: str
1719+
p = Point(dict(x=42, y='lurr'))
1720+
[file b.py]
1721+
from a import Point
1722+
def foo(x: Point) -> int:
1723+
return x['x'] + x['y']
1724+
[builtins fixtures/dict.pyi]
1725+
[out]
1726+
==
1727+
b.py:3: error: Unsupported operand types for + ("int" and "str")

test-data/unit/semanal-typeddict.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ MypyFile:1(
4646
ImportFrom:1(mypy_extensions, [TypedDict])
4747
ClassDef:2(
4848
A
49-
BaseTypeExpr(
50-
NameExpr(TypedDict [mypy_extensions.TypedDict]))
49+
BaseType(
50+
typing.Mapping[builtins.str, builtins.str])
5151
ExpressionStmt:3(
5252
StrExpr(foo))
5353
AssignmentStmt:4(

0 commit comments

Comments
 (0)