From bee66e048914d0b13aea6e6f2f1b617e0d6d931d Mon Sep 17 00:00:00 2001 From: Michael Sullivan Date: Thu, 25 Jan 2018 13:31:37 -0800 Subject: [PATCH 1/3] Handle TypedDict in diff and deps Required performing a patchup in semanal to *actually* replace the TypeInfo with the 'replaced' version. --- mypy/semanal.py | 2 + mypy/server/astdiff.py | 2 +- mypy/server/deps.py | 19 ++++++--- test-data/unit/deps.test | 44 +++++++++++++++++++++ test-data/unit/diff.test | 30 ++++++++++++++ test-data/unit/fine-grained.test | 56 +++++++++++++++++++++++++++ test-data/unit/semanal-typeddict.test | 4 +- 7 files changed, 149 insertions(+), 8 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index a0bca4a315fe..444838ede525 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1325,6 +1325,7 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> bool: fields, types, required_keys = self.check_typeddict_classdef(defn) info = self.build_typeddict_typeinfo(defn.name, fields, types, required_keys) defn.info.replaced = info + defn.info = info node.node = info defn.analyzed = TypedDictExpr(info) defn.analyzed.line = defn.line @@ -1360,6 +1361,7 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> bool: required_keys.update(new_required_keys) info = self.build_typeddict_typeinfo(defn.name, keys, types, required_keys) defn.info.replaced = info + defn.info = info node.node = info defn.analyzed = TypedDictExpr(info) defn.analyzed.line = defn.line diff --git a/mypy/server/astdiff.py b/mypy/server/astdiff.py index 160b4e36f28e..e5efeff2b527 100644 --- a/mypy/server/astdiff.py +++ b/mypy/server/astdiff.py @@ -306,13 +306,13 @@ def snapshot_definition(node: Optional[SymbolNode], # type_vars # bases # _promote - # typeddict_type attrs = (node.is_abstract, node.is_enum, node.fallback_to_any, node.is_named_tuple, node.is_newtype, snapshot_optional_type(node.tuple_type), + snapshot_optional_type(node.typeddict_type), [base.fullname() for base in node.mro]) prefix = node.fullname() symbol_table = snapshot_symbol_table(prefix, node.names) diff --git a/mypy/server/deps.py b/mypy/server/deps.py index f7ad8c531d3f..0e286ba0b50b 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -87,7 +87,7 @@ class 'mod.Cls'. This can also refer to an attribute inherited from a ImportFrom, CallExpr, CastExpr, TypeVarExpr, TypeApplication, IndexExpr, UnaryExpr, OpExpr, ComparisonExpr, GeneratorExpr, DictionaryComprehension, StarExpr, PrintStmt, ForStmt, WithStmt, TupleExpr, ListExpr, OperatorAssignmentStmt, DelStmt, YieldFromExpr, Decorator, Block, - TypeInfo, FuncBase, OverloadedFuncDef, RefExpr, SuperExpr, Var, NamedTupleExpr, + TypeInfo, FuncBase, OverloadedFuncDef, RefExpr, SuperExpr, Var, NamedTupleExpr, TypedDictExpr, LDEF, MDEF, GDEF, op_methods, reverse_op_methods, ops_with_inplace_method, unary_op_methods ) @@ -151,7 +151,6 @@ def __init__(self, # TODO (incomplete): # from m import * # await - # TypedDict # protocols # metaclasses # type aliases @@ -199,6 +198,8 @@ def visit_class_def(self, o: ClassDef) -> None: self.add_type_dependencies(base, target=target) if o.info.tuple_type: self.add_type_dependencies(o.info.tuple_type, target=make_trigger(target)) + if o.info.typeddict_type: + self.add_type_dependencies(o.info.typeddict_type, target=make_trigger(target)) # TODO: Add dependencies based on remaining TypeInfo attributes. super().visit_class_def(o) self.is_class = old_is_class @@ -237,7 +238,6 @@ def visit_block(self, o: Block) -> None: def visit_assignment_stmt(self, o: AssignmentStmt) -> None: # TODO: Implement all assignment special forms, including these: - # TypedDict # Enum # type aliases rvalue = o.rvalue @@ -258,6 +258,12 @@ def visit_assignment_stmt(self, o: AssignmentStmt) -> None: self.add_type_dependencies(typ, target=make_trigger(prefix)) attr_target = make_trigger('%s.%s' % (prefix, name)) self.add_type_dependencies(typ, target=attr_target) + elif isinstance(rvalue, CallExpr) and isinstance(rvalue.analyzed, TypedDictExpr): + # Depend on the underlying typeddict type + info = rvalue.analyzed.info + assert(info.typeddict_type is not None) + prefix = '%s.%s' % (self.scope.current_full_target(), info.name()) + self.add_type_dependencies(info.typeddict_type, target=make_trigger(prefix)) else: # Normal assignment super().visit_assignment_stmt(o) @@ -703,8 +709,11 @@ def visit_type_var(self, typ: TypeVarType) -> List[str]: return triggers def visit_typeddict_type(self, typ: TypedDictType) -> List[str]: - # TODO: implement - return [] + triggers = [] + for item in typ.items.values(): + triggers.extend(get_type_triggers(item)) + triggers.extend(get_type_triggers(typ.fallback)) + return triggers def visit_unbound_type(self, typ: UnboundType) -> List[str]: return [] diff --git a/test-data/unit/deps.test b/test-data/unit/deps.test index 47e1db148452..b38d8d5ce161 100644 --- a/test-data/unit/deps.test +++ b/test-data/unit/deps.test @@ -552,3 +552,47 @@ x = 1 -> pkg.mod -> pkg -> m + +[case testTypedDict] +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int, 'y': int}) +p = Point(dict(x=42, y=1337)) +def foo(x: Point) -> int: + return x['x'] + x['y'] +[builtins fixtures/dict.pyi] +[out] + -> , , m, m.foo + -> m + -> m + +[case testTypedDict2] +from mypy_extensions import TypedDict +class A: pass +Point = TypedDict('Point', {'x': int, 'y': A}) +p = Point(dict(x=42, y=A())) +def foo(x: Point) -> int: + return x['x'] +[builtins fixtures/dict.pyi] +[out] + -> m + -> , , , m, m.A, m.foo + -> , , m, m.foo + -> m + -> m + +[case testTypedDict3] +from mypy_extensions import TypedDict +class A: pass +class Point(TypedDict): + x: int + y: A +p = Point(dict(x=42, y=A())) +def foo(x: Point) -> int: + return x['x'] +[builtins fixtures/dict.pyi] +[out] + -> m + -> , , , m, m.A, m.foo + -> , , m, m.Point, m.foo + -> m + -> m diff --git a/test-data/unit/diff.test b/test-data/unit/diff.test index f1a84dd1e723..fa480602673e 100644 --- a/test-data/unit/diff.test +++ b/test-data/unit/diff.test @@ -562,3 +562,33 @@ B = Enum('B', 'y') [out] __main__.B.x __main__.B.y + +[case testTypedDict] +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int, 'y': int}) +p = Point(dict(x=42, y=1337)) +[file next.py] +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int, 'y': str}) +p = Point(dict(x=42, y='lurr')) +[builtins fixtures/dict.pyi] +[out] +__main__.Point +__main__.p + +[case testTypedDict2] +from mypy_extensions import TypedDict +class Point(TypedDict): + x: int + y: int +p = Point(dict(x=42, y=1337)) +[file next.py] +from mypy_extensions import TypedDict +class Point(TypedDict): + x: int + y: str +p = Point(dict(x=42, y='lurr')) +[builtins fixtures/dict.pyi] +[out] +__main__.Point +__main__.p diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 3b3bce0f2c1e..997dd8ed50a4 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -1720,3 +1720,59 @@ def f(x: a.N) -> None: f(a.x) [out] == + +[case testTypedDictRefresh] +[builtins fixtures/dict.pyi] +import a +[file a.py] +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int, 'y': int}) +p = Point(dict(x=42, y=1337)) +[file a.py.2] +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int, 'y': int}) +p = Point(dict(x=42, y=1337)) +[out] +== + +[case testTypedDictUpdate] +import b +[file a.py] +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int, 'y': int}) +p = Point(dict(x=42, y=1337)) +[file a.py.2] +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int, 'y': str}) +p = Point(dict(x=42, y='lurr')) +[file b.py] +from a import Point +def foo(x: Point) -> int: + return x['x'] + x['y'] +[builtins fixtures/dict.pyi] +[out] +== +b.py:3: error: Unsupported operand types for + ("int" and "str") + +[case testTypedDictUpdate] +import b +[file a.py] +from mypy_extensions import TypedDict +class Point(TypedDict): + x: int + y: int +p = Point(dict(x=42, y=1337)) +[file a.py.2] +from mypy_extensions import TypedDict +class Point(TypedDict): + x: int + y: str +p = Point(dict(x=42, y='lurr')) +[file b.py] +from a import Point +def foo(x: Point) -> int: + return x['x'] + x['y'] +[builtins fixtures/dict.pyi] +[out] +== +b.py:3: error: Unsupported operand types for + ("int" and "str") diff --git a/test-data/unit/semanal-typeddict.test b/test-data/unit/semanal-typeddict.test index 4261c724cb2a..8bd22237bcd0 100644 --- a/test-data/unit/semanal-typeddict.test +++ b/test-data/unit/semanal-typeddict.test @@ -46,8 +46,8 @@ MypyFile:1( ImportFrom:1(mypy_extensions, [TypedDict]) ClassDef:2( A - BaseTypeExpr( - NameExpr(TypedDict [mypy_extensions.TypedDict])) + BaseType( + typing.Mapping[builtins.str, builtins.str]) ExpressionStmt:3( StrExpr(foo)) AssignmentStmt:4( From 6965aa7d629f6cfc19b51086178d63b505d70c31 Mon Sep 17 00:00:00 2001 From: Michael Sullivan Date: Fri, 26 Jan 2018 11:45:43 -0800 Subject: [PATCH 2/3] Handle changes to totality --- mypy/server/astdiff.py | 5 ++++- mypy/server/deps.py | 2 +- test-data/unit/diff.test | 26 ++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/mypy/server/astdiff.py b/mypy/server/astdiff.py index e5efeff2b527..d3139cd82e59 100644 --- a/mypy/server/astdiff.py +++ b/mypy/server/astdiff.py @@ -152,6 +152,8 @@ def visit_typeddict_type(self, left: TypedDictType) -> bool: if isinstance(self.right, TypedDictType): if left.items.keys() != self.right.items.keys(): return False + if left.required_keys != self.right.required_keys: + return False for (_, left_item_type, right_item_type) in left.zip(self.right): if not is_identical_type(left_item_type, right_item_type): return False @@ -407,7 +409,8 @@ def visit_tuple_type(self, typ: TupleType) -> SnapshotItem: def visit_typeddict_type(self, typ: TypedDictType) -> SnapshotItem: items = tuple((key, snapshot_type(item_type)) for key, item_type in typ.items.items()) - return ('TypedDictType', items) + required = tuple(sorted(typ.required_keys)) + return ('TypedDictType', items, required) def visit_union_type(self, typ: UnionType) -> SnapshotItem: # Sort and remove duplicates so that we can use equality to test for diff --git a/mypy/server/deps.py b/mypy/server/deps.py index 0e286ba0b50b..2c1bdccc291c 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -261,7 +261,7 @@ def visit_assignment_stmt(self, o: AssignmentStmt) -> None: elif isinstance(rvalue, CallExpr) and isinstance(rvalue.analyzed, TypedDictExpr): # Depend on the underlying typeddict type info = rvalue.analyzed.info - assert(info.typeddict_type is not None) + assert info.typeddict_type is not None prefix = '%s.%s' % (self.scope.current_full_target(), info.name()) self.add_type_dependencies(info.typeddict_type, target=make_trigger(prefix)) else: diff --git a/test-data/unit/diff.test b/test-data/unit/diff.test index fa480602673e..5e5d81294de9 100644 --- a/test-data/unit/diff.test +++ b/test-data/unit/diff.test @@ -592,3 +592,29 @@ p = Point(dict(x=42, y='lurr')) [out] __main__.Point __main__.p + +[case testTypedDict3] +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int, 'y': int}) +p = Point(dict(x=42, y=1337)) +[file next.py] +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int}) +p = Point(dict(x=42)) +[builtins fixtures/dict.pyi] +[out] +__main__.Point +__main__.p + +[case testTypedDict4] +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int, 'y': int}) +p = Point(dict(x=42, y=1337)) +[file next.py] +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int, 'y': int}, total=False) +p = Point(dict(x=42, y=1337)) +[builtins fixtures/dict.pyi] +[out] +__main__.Point +__main__.p From e653bdbd622f9201bc32f0653d3d04cf17424394 Mon Sep 17 00:00:00 2001 From: Michael Sullivan Date: Fri, 26 Jan 2018 12:23:11 -0800 Subject: [PATCH 3/3] fix test name --- test-data/unit/fine-grained.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 997dd8ed50a4..d0a0933b9c80 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -1754,7 +1754,7 @@ def foo(x: Point) -> int: == b.py:3: error: Unsupported operand types for + ("int" and "str") -[case testTypedDictUpdate] +[case testTypedDictUpdate2] import b [file a.py] from mypy_extensions import TypedDict