From 3eed2a514ce41d54fb4300042e1d0822f6cd961c Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 30 Jan 2018 22:15:11 +0000 Subject: [PATCH 01/42] Basic implementation --- mypy/build.py | 1 + mypy/nodes.py | 12 +++++++-- mypy/semanal.py | 42 ++++++++++++++++++++++++++------ mypy/semanal_pass3.py | 11 +++++++++ mypy/server/deps.py | 15 ++++++++++-- mypy/typeanal.py | 16 +++++++++--- test-data/unit/fine-grained.test | 37 ++++++++++++++++++++++++++++ 7 files changed, 119 insertions(+), 15 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 301c18cef9f9..4de76c7211f1 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -2517,6 +2517,7 @@ def process_stale_scc(graph: Graph, scc: List[str], manager: BuildManager) -> No graph[id].semantic_analysis() for id in stale: graph[id].semantic_analysis_pass_three() + # print(graph[id].tree.alias_deps) for id in fresh: graph[id].calculate_mros() for id in stale: diff --git a/mypy/nodes.py b/mypy/nodes.py index 6ea5c91a5017..4f75e4863c47 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -2,9 +2,9 @@ import os from abc import abstractmethod -from collections import OrderedDict +from collections import OrderedDict, defaultdict from typing import ( - Any, TypeVar, List, Tuple, cast, Set, Dict, Union, Optional, Callable, Sequence, + Any, TypeVar, List, Tuple, cast, Set, Dict, Union, Optional, Callable, Sequence, DefaultDict ) import mypy.strconv @@ -194,6 +194,8 @@ class MypyFile(SymbolNode): path = '' # Top-level definitions and statements defs = None # type: List[Statement] + # Type alias dependencies as mapping from node to set of alias full names + alias_deps = None # type: DefaultDict[Union[MypyFile, FuncItem, ClassDef], Set[str]] # Is there a UTF-8 BOM at the start? is_bom = False names = None # type: SymbolTable @@ -213,6 +215,7 @@ def __init__(self, self.line = 1 # Dummy line number self.imports = imports self.is_bom = is_bom + self.alias_deps = defaultdict(set) if ignored_lines: self.ignored_lines = ignored_lines else: @@ -2331,6 +2334,10 @@ class SymbolTableNode: normalized = False # type: bool # Was this defined by assignment to self attribute? implicit = False # type: bool + # Is this node refers to other node via node aliasing? + # (This is currently used for simple aliases like `A = int` instead of .type_override) + is_aliasing = False # type: bool + alias_depends_on = None # type: Set[str] def __init__(self, kind: int, @@ -2349,6 +2356,7 @@ def __init__(self, self.alias_tvars = alias_tvars self.implicit = implicit self.module_hidden = module_hidden + self.alias_depends_on = set() @property def fullname(self) -> Optional[str]: diff --git a/mypy/semanal.py b/mypy/semanal.py index 444838ede525..7f191bdcb370 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -597,9 +597,13 @@ def analyze_function(self, defn: FuncItem) -> None: assert isinstance(defn.type, CallableType) # Signature must be analyzed in the surrounding scope so that # class-level imported names and type variables are in scope. - defn.type = self.type_analyzer().visit_callable_type(defn.type, nested=False) + analyzer = self.type_analyzer() + defn.type = analyzer.visit_callable_type(defn.type, nested=False) + if analyzer.aliases_used: + self.cur_mod_node.alias_deps[defn].update(analyzer.aliases_used) self.check_function_signature(defn) if isinstance(defn, FuncDef): + assert isinstance(defn.type, CallableType) defn.type = set_callable_name(defn.type, defn) for arg in defn.arguments: if arg.initializer: @@ -1679,7 +1683,16 @@ def anal_type(self, t: Type, *, aliasing=aliasing, allow_tuple_literal=allow_tuple_literal, third_pass=third_pass) - return t.accept(a) + tp = t.accept(a) + if a.aliases_used: + if self.is_class_scope(): + assert self.type is not None, "Type not set at class scope" + self.cur_mod_node.alias_deps[self.type.defn].update(a.aliases_used) + elif self.is_func_scope(): + self.cur_mod_node.alias_deps[self.function_stack[-1]].update(a.aliases_used) + else: + self.cur_mod_node.alias_deps[self.cur_mod_node].update(a.aliases_used) + return tp def visit_assignment_stmt(self, s: AssignmentStmt) -> None: for lval in s.lvalues: @@ -1755,7 +1768,7 @@ def alias_fallback(self, tp: Type) -> Instance: return Instance(fb_info, []) def analyze_alias(self, rvalue: Expression, - warn_bound_tvar: bool = False) -> Tuple[Optional[Type], List[str]]: + warn_bound_tvar: bool = False) -> Tuple[Optional[Type], List[str], Set[str]]: """Check if 'rvalue' represents a valid type allowed for aliasing (e.g. not a type variable). If yes, return the corresponding type and a list of qualified type variable names for generic aliases. @@ -1776,11 +1789,14 @@ def analyze_alias(self, rvalue: Expression, global_scope=global_scope, warn_bound_tvar=warn_bound_tvar) if res: + tp, depends_on = res alias_tvars = [name for (name, _) in - res.accept(TypeVariableQuery(self.lookup_qualified, self.tvar_scope))] + tp.accept(TypeVariableQuery(self.lookup_qualified, self.tvar_scope))] else: + tp = None alias_tvars = [] - return res, alias_tvars + depends_on = set() + return tp, alias_tvars, depends_on def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None: """Check if assignment creates a type alias and set it up as needed. @@ -1809,11 +1825,14 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None: # annotations (see the second rule). return rvalue = s.rvalue - res, alias_tvars = self.analyze_alias(rvalue, warn_bound_tvar=True) + res, alias_tvars, depends_on = self.analyze_alias(rvalue, warn_bound_tvar=True) if not res: return node = self.lookup(lvalue.name, lvalue) assert node is not None + node.alias_depends_on = depends_on.copy() + node.alias_depends_on.add(lvalue.fullname) # To avoid extra attributes on SymbolTableNode + # we add the fullname of alias to what it depends on. if not lvalue.is_inferred_def: # Type aliases can't be re-defined. if node and (node.kind == TYPE_ALIAS or isinstance(node.node, TypeInfo)): @@ -1830,6 +1849,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None: # For simple (on-generic) aliases we use aliasing TypeInfo's # to allow using them in runtime context where it makes sense. node.node = res.type + node.is_aliasing = True if isinstance(rvalue, RefExpr): sym = self.lookup_type_node(rvalue) if sym: @@ -3439,12 +3459,20 @@ def visit_index_expr(self, expr: IndexExpr) -> None: elif isinstance(expr.base, RefExpr) and expr.base.kind == TYPE_ALIAS: # Special form -- subscripting a generic type alias. # Perform the type substitution and create a new alias. - res, alias_tvars = self.analyze_alias(expr) + res, alias_tvars, depends_on = self.analyze_alias(expr) assert res is not None, "Failed analyzing already defined alias" expr.analyzed = TypeAliasExpr(res, alias_tvars, fallback=self.alias_fallback(res), in_runtime=True) expr.analyzed.line = expr.line expr.analyzed.column = expr.column + if depends_on: # for situations like `L = LongGeneric; x = L[int]()` + if self.is_class_scope(): + assert self.type is not None, "Type not set at class scope" + self.cur_mod_node.alias_deps[self.type.defn].update(depends_on) + elif self.is_func_scope(): + self.cur_mod_node.alias_deps[self.function_stack[-1]].update(depends_on) + else: + self.cur_mod_node.alias_deps[self.cur_mod_node].update(depends_on) elif refers_to_class_or_function(expr.base): # Special form -- type application. # Translate index to an unanalyzed type. diff --git a/mypy/semanal_pass3.py b/mypy/semanal_pass3.py index 01c5a6627deb..1222937c2a2a 100644 --- a/mypy/semanal_pass3.py +++ b/mypy/semanal_pass3.py @@ -59,6 +59,8 @@ def visit_file(self, file_node: MypyFile, fnam: str, options: Options, self.patches = patches self.is_typeshed_file = self.errors.is_typeshed_file(fnam) self.sem.cur_mod_id = file_node.fullname() + self.cur_mod_node = file_node + self.cur_node = file_node # type: Union[MypyFile, FuncItem, ClassDef] self.sem.globals = file_node.names with experiments.strict_optional_set(options.strict_optional): self.accept(file_node) @@ -92,8 +94,11 @@ def visit_func_def(self, fdef: FuncDef) -> None: if not self.recurse_into_functions: return self.errors.push_function(fdef.name()) + old_node = self.cur_node + self.cur_node = fdef self.analyze(fdef.type, fdef) super().visit_func_def(fdef) + self.cur_node = old_node self.errors.pop_function() def visit_overloaded_func_def(self, fdef: OverloadedFuncDef) -> None: @@ -134,7 +139,10 @@ def visit_class_def(self, tdef: ClassDef) -> None: elif isinstance(tdef.analyzed, NamedTupleExpr): self.analyze(tdef.analyzed.info.tuple_type, tdef.analyzed, warn=True) self.analyze_info(tdef.analyzed.info) + old_node = self.cur_node + self.cur_node = tdef super().visit_class_def(tdef) + self.cur_node = old_node def visit_decorator(self, dec: Decorator) -> None: """Try to infer the type of the decorated function. @@ -353,6 +361,7 @@ def analyze(self, type: Optional[Type], node: Union[Node, SymbolTableNode], type.accept(analyzer) self.check_for_omitted_generics(type) self.generate_type_patches(node, indicator, warn) + self.cur_mod_node.alias_deps[self.cur_node].update(analyzer.aliases_used) def analyze_types(self, types: List[Type], node: Node) -> None: # Similar to above but for nodes with multiple types. @@ -361,6 +370,8 @@ def analyze_types(self, types: List[Type], node: Node) -> None: analyzer = self.make_type_analyzer(indicator) type.accept(analyzer) self.check_for_omitted_generics(type) + if analyzer.aliases_used: + self.cur_mod_node.alias_deps[self.cur_node].update(analyzer.aliases_used) self.generate_type_patches(node, indicator, warn=False) def generate_type_patches(self, diff --git a/mypy/server/deps.py b/mypy/server/deps.py index 2c1bdccc291c..89abd06dfbde 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -79,7 +79,7 @@ class 'mod.Cls'. This can also refer to an attribute inherited from a Test cases for this module live in 'test-data/unit/deps*.test'. """ -from typing import Dict, List, Set, Optional, Tuple, Union +from typing import Dict, List, Set, Optional, Tuple, Union, DefaultDict from mypy.checkmember import bind_self from mypy.nodes import ( @@ -88,7 +88,7 @@ class 'mod.Cls'. This can also refer to an attribute inherited from a ComparisonExpr, GeneratorExpr, DictionaryComprehension, StarExpr, PrintStmt, ForStmt, WithStmt, TupleExpr, ListExpr, OperatorAssignmentStmt, DelStmt, YieldFromExpr, Decorator, Block, TypeInfo, FuncBase, OverloadedFuncDef, RefExpr, SuperExpr, Var, NamedTupleExpr, TypedDictExpr, - LDEF, MDEF, GDEF, + LDEF, MDEF, GDEF, FuncItem, op_methods, reverse_op_methods, ops_with_inplace_method, unary_op_methods ) from mypy.traverser import TraverserVisitor @@ -106,6 +106,7 @@ def get_dependencies(target: MypyFile, python_version: Tuple[int, int]) -> Dict[str, Set[str]]: """Get all dependencies of a node, recursively.""" visitor = DependencyVisitor(type_map, python_version) + visitor.alias_deps = target.alias_deps target.accept(visitor) return visitor.map @@ -138,6 +139,7 @@ def get_dependencies_of_target(module_id: str, class DependencyVisitor(TraverserVisitor): + alias_deps = None # type: Optional[DefaultDict[Union[MypyFile, FuncItem, ClassDef], Set[str]]] def __init__(self, type_map: Dict[Expression, Type], python_version: Tuple[int, int]) -> None: @@ -160,6 +162,9 @@ def __init__(self, def visit_mypy_file(self, o: MypyFile) -> None: self.scope.enter_file(o.fullname()) self.is_package_init_file = o.is_package_init_file() + if o in self.alias_deps: + for alias in self.alias_deps[o]: + self.add_dependency(make_trigger(alias)) super().visit_mypy_file(o) self.scope.leave() @@ -177,6 +182,9 @@ def visit_func_def(self, o: FuncDef) -> None: if o.info: for base in non_trivial_bases(o.info): self.add_dependency(make_trigger(base.fullname() + '.' + o.name())) + if self.alias_deps and o in self.alias_deps: + for alias in self.alias_deps[o]: + self.add_dependency(make_trigger(alias)) super().visit_func_def(o) self.scope.leave() @@ -202,6 +210,9 @@ def visit_class_def(self, o: ClassDef) -> None: 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) + if self.alias_deps and o in self.alias_deps: + for alias in self.alias_deps[o]: + self.add_dependency(make_trigger(alias)) self.is_class = old_is_class info = o.info for name, node in info.names.items(): diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 2eb5366d1639..824c07b60108 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -63,7 +63,7 @@ def analyze_type_alias(node: Expression, allow_unnormalized: bool = False, in_dynamic_func: bool = False, global_scope: bool = True, - warn_bound_tvar: bool = False) -> Optional[Type]: + warn_bound_tvar: bool = False) -> Optional[Tuple[Type, Set[str]]]: """Return type if node is valid as a type alias rvalue. Return None otherwise. 'node' must have been semantically analyzed. @@ -103,7 +103,7 @@ def analyze_type_alias(node: Expression, arg = lookup_func(node.args[0].name, node.args[0]) if (call is not None and call.node and call.node.fullname() == 'builtins.type' and arg is not None and arg.node and arg.node.fullname() == 'builtins.None'): - return NoneTyp() + return NoneTyp(), set() return None return None else: @@ -120,7 +120,8 @@ def analyze_type_alias(node: Expression, allow_unnormalized=allow_unnormalized, warn_bound_tvar=warn_bound_tvar) analyzer.in_dynamic_func = in_dynamic_func analyzer.global_scope = global_scope - return type.accept(analyzer) + res = type.accept(analyzer) + return res, analyzer.aliases_used def no_subscript_builtin_alias(name: str, propose_alt: bool = True) -> str: @@ -171,6 +172,7 @@ def __init__(self, self.is_typeshed_stub = is_typeshed_stub self.warn_bound_tvar = warn_bound_tvar self.third_pass = third_pass + self.aliases_used = set() # type: Set[str] def visit_unbound_type(self, t: UnboundType) -> Type: if t.optional: @@ -259,6 +261,7 @@ def visit_unbound_type(self, t: UnboundType) -> Type: elif fullname in ('mypy_extensions.NoReturn', 'typing.NoReturn'): return UninhabitedType(is_noreturn=True) elif sym.kind == TYPE_ALIAS: + self.aliases_used.update(sym.alias_depends_on) override = sym.type_override all_vars = sym.alias_tvars assert override is not None @@ -307,6 +310,8 @@ def visit_unbound_type(self, t: UnboundType) -> Type: self.note_func("Forward references to type variables are prohibited", t) return t info = sym.node # type: TypeInfo + if sym.is_aliasing: + self.aliases_used.update(sym.alias_depends_on) if len(t.args) > 0 and info.fullname() == 'builtins.tuple': fallback = Instance(info, [AnyType(TypeOfAny.special_form)], t.line) return TupleType(self.anal_array(t.args), fallback, t.line) @@ -666,6 +671,7 @@ def __init__(self, self.is_typeshed_stub = is_typeshed_stub self.indicator = indicator self.patches = patches + self.aliases_used = set() # type: Set[str] def visit_instance(self, t: Instance) -> None: info = t.type @@ -796,7 +802,9 @@ def anal_type(self, tp: UnboundType) -> Type: self.options, self.is_typeshed_stub, third_pass=True) - return tp.accept(tpan) + res = tp.accept(tpan) + self.aliases_used = tpan.aliases_used + return res TypeVarList = List[Tuple[str, TypeVarExpr]] diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index d0a0933b9c80..be074f64580b 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -1776,3 +1776,40 @@ def foo(x: Point) -> int: [out] == b.py:3: error: Unsupported operand types for + ("int" and "str") + +[case testBasicAliasUpdate] +import b +[file a.py] +N = int +x = 1 +[file a.py.2] +N = str +x = 'hi' +[file b.py] +import a +def f(x: a.N) -> None: + pass +f(a.x) +[out] +== + +[case testBasicAliasUpdateGeneric] +import b +[file a.py] +from typing import Dict, TypeVar +T = TypeVar('T') +D = Dict[int, T] +x = {1: 1} +[file a.py.2] +from typing import Dict, TypeVar +T = TypeVar('T') +D = Dict[str, T] +x = {'hi': 1} +[file b.py] +import a +def f(x: a.D[int]) -> None: + pass +f(a.x) +[builtins fixtures/dict.pyi] +[out] +== From 3e895d606c128382e28dc70ef07f75d99b24c8c3 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 31 Jan 2018 19:33:51 +0000 Subject: [PATCH 02/42] Fix processing of aliases in base classes and of chained aliases; add tests --- mypy/build.py | 1 - mypy/semanal.py | 21 +++- mypy/semanal_pass3.py | 3 +- mypy/server/deps.py | 3 +- mypy/server/update.py | 2 +- test-data/unit/deps-types.test | 147 ++++++++++++++++++++++ test-data/unit/fine-grained.test | 202 +++++++++++++++++++++++++++++++ 7 files changed, 371 insertions(+), 8 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 4de76c7211f1..301c18cef9f9 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -2517,7 +2517,6 @@ def process_stale_scc(graph: Graph, scc: List[str], manager: BuildManager) -> No graph[id].semantic_analysis() for id in stale: graph[id].semantic_analysis_pass_three() - # print(graph[id].tree.alias_deps) for id in fresh: graph[id].calculate_mros() for id in stale: diff --git a/mypy/semanal.py b/mypy/semanal.py index 7f191bdcb370..81952ac13cab 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1070,7 +1070,7 @@ def analyze_base_classes(self, defn: ClassDef) -> None: for base_expr in defn.base_type_exprs: try: - base = self.expr_to_analyzed_type(base_expr) + base = self.expr_to_analyzed_type(base_expr, from_bases=defn) except TypeTranslationError: self.fail('Invalid base class', base_expr) info.fallback_to_any = True @@ -1182,7 +1182,8 @@ def update_metaclass(self, defn: ClassDef) -> None: return defn.metaclass = metas.pop() - def expr_to_analyzed_type(self, expr: Expression) -> Type: + def expr_to_analyzed_type(self, expr: Expression, *, + from_bases: Optional[ClassDef] = None) -> Type: if isinstance(expr, CallExpr): expr.accept(self) info = self.check_namedtuple(expr) @@ -1194,7 +1195,7 @@ def expr_to_analyzed_type(self, expr: Expression) -> Type: fallback = Instance(info, []) return TupleType(info.tuple_type.items, fallback=fallback) typ = expr_to_unanalyzed_type(expr) - return self.anal_type(typ) + return self.anal_type(typ, from_bases=from_bases) def verify_base_classes(self, defn: ClassDef) -> bool: info = defn.info @@ -1678,13 +1679,17 @@ def anal_type(self, t: Type, *, tvar_scope: Optional[TypeVarScope] = None, allow_tuple_literal: bool = False, aliasing: bool = False, - third_pass: bool = False) -> Type: + third_pass: bool = False, + from_bases: Optional[ClassDef] = None) -> Type: a = self.type_analyzer(tvar_scope=tvar_scope, aliasing=aliasing, allow_tuple_literal=allow_tuple_literal, third_pass=third_pass) tp = t.accept(a) if a.aliases_used: + if from_bases is not None: + self.cur_mod_node.alias_deps[from_bases].update(a.aliases_used) + return tp if self.is_class_scope(): assert self.type is not None, "Type not set at class scope" self.cur_mod_node.alias_deps[self.type.defn].update(a.aliases_used) @@ -1833,6 +1838,14 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None: node.alias_depends_on = depends_on.copy() node.alias_depends_on.add(lvalue.fullname) # To avoid extra attributes on SymbolTableNode # we add the fullname of alias to what it depends on. + if depends_on: + if self.is_class_scope(): + assert self.type is not None, "Type not set at class scope" + self.cur_mod_node.alias_deps[self.type.defn].update(depends_on) + elif self.is_func_scope(): + self.cur_mod_node.alias_deps[self.function_stack[-1]].update(depends_on) + else: + self.cur_mod_node.alias_deps[self.cur_mod_node].update(depends_on) if not lvalue.is_inferred_def: # Type aliases can't be re-defined. if node and (node.kind == TYPE_ALIAS or isinstance(node.node, TypeInfo)): diff --git a/mypy/semanal_pass3.py b/mypy/semanal_pass3.py index 1222937c2a2a..05e3a4842ad7 100644 --- a/mypy/semanal_pass3.py +++ b/mypy/semanal_pass3.py @@ -361,7 +361,8 @@ def analyze(self, type: Optional[Type], node: Union[Node, SymbolTableNode], type.accept(analyzer) self.check_for_omitted_generics(type) self.generate_type_patches(node, indicator, warn) - self.cur_mod_node.alias_deps[self.cur_node].update(analyzer.aliases_used) + if analyzer.aliases_used: + self.cur_mod_node.alias_deps[self.cur_node].update(analyzer.aliases_used) def analyze_types(self, types: List[Type], node: Node) -> None: # Similar to above but for nodes with multiple types. diff --git a/mypy/server/deps.py b/mypy/server/deps.py index 89abd06dfbde..b226e1511bcd 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -112,12 +112,14 @@ def get_dependencies(target: MypyFile, def get_dependencies_of_target(module_id: str, + module_tree: MypyFile, target: Node, type_map: Dict[Expression, Type], python_version: Tuple[int, int]) -> Dict[str, Set[str]]: """Get dependencies of a target -- don't recursive into nested targets.""" # TODO: Add tests for this function. visitor = DependencyVisitor(type_map, python_version) + visitor.alias_deps = module_tree.alias_deps visitor.scope.enter_file(module_id) if isinstance(target, MypyFile): # Only get dependencies of the top-level of the module. Don't recurse into @@ -155,7 +157,6 @@ def __init__(self, # await # protocols # metaclasses - # type aliases # functional enum # type variable with value restriction diff --git a/mypy/server/update.py b/mypy/server/update.py index e322b7bef315..6918645e6ada 100644 --- a/mypy/server/update.py +++ b/mypy/server/update.py @@ -945,7 +945,7 @@ def update_deps(module_id: str, for deferred in nodes: node = deferred.node type_map = graph[module_id].type_map() - new_deps = get_dependencies_of_target(module_id, node, type_map, options.python_version) + new_deps = get_dependencies_of_target(module_id, graph[module_id].tree, node, type_map, options.python_version) for trigger, targets in new_deps.items(): deps.setdefault(trigger, set()).update(targets) diff --git a/test-data/unit/deps-types.test b/test-data/unit/deps-types.test index 4334bff6cbfe..7db1d5188e39 100644 --- a/test-data/unit/deps-types.test +++ b/test-data/unit/deps-types.test @@ -151,3 +151,150 @@ def h() -> None: -> m, m.h -> m, m.h -> m.f + +[case testAliasDepsNormalMod] +A = int +x: A +[out] + -> m + -> m + +[case testAliasDepsNormalFunc] +A = int +def f(x: A) -> None: + pass +[out] + -> m, m.f + +[case testAliasDepsNormalClass] +from a import A +class C: + x: A +[file a.py] +A = int +[out] + -> m.C + -> m + +[case testAliasDepsNormalClassBases] +from a import A +class C(A): + pass +[file a.py] +A = int +[out] + -> m.C + -> m + +[case testAliasDepsGenericMod] +from typing import Dict +A = Dict[int, str] +x: A +[builtins fixtures/dict.pyi] +[out] + -> m + -> m + +[case testAliasDepsGenericFunc] +from typing import Dict +A = Dict[str, int] +def f(x: A) -> None: + pass +[builtins fixtures/dict.pyi] +[out] + -> m, m.f + +[case testAliasDepsGenericClass] +from typing import Dict, TypeVar +T = TypeVar('T') +A = Dict[str, T] +class C: + x: A[int] +[builtins fixtures/dict.pyi] +[out] + -> m + -> m.C + +[case testAliasDepsForwardMod] +x: A +A = int +[out] + -> m + -> m + +[case testAliasDepsForwardFunc] +def f(x: A) -> None: + pass +A = int +[out] + -> m, m.f + +[case testAliasDepsForwardClass] +class C: + x: A +A = int +[out] + -> m + -> m.C + +[case testAliasDepsChainedMod] +A = int +B = A +x: B +[out] + -> m + -> m + -> m + +[case testAliasDepsChainedFunc] +A = int +B = A +def f(x: B) -> None: + pass +[out] + -> m, m.f + -> m, m.f + +[case testAliasDepsChainedClass] +A = int +B = A +class C(B): + pass +[out] + -> m + -> m + -> m.C + +[case testAliasDepsNestedMod] +from typing import Dict +A = Dict[str, int] +B = Dict[str, A] +x: B +[builtins fixtures/dict.pyi] +[out] + -> m + -> m + -> m + +[case testAliasDepsNestedFunc] +from typing import Dict +A = Dict[str, int] +B = Dict[str, A] +def f(x: B) -> None: + pass +[builtins fixtures/dict.pyi] +[out] + -> m, m.f + -> m, m.f + +[case testAliasDepsNestedClass] +from typing import Dict +A = Dict[str, int] +B = Dict[str, A] +class C: + x: B +[builtins fixtures/dict.pyi] +[out] + -> m + -> m + -> m.C diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index be074f64580b..eaf7dbf0fa47 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -1813,3 +1813,205 @@ f(a.x) [builtins fixtures/dict.pyi] [out] == + +[case testAliasFineNormalMod] +import b +[file a.py] +A = int +[file a.py.2] +A = str +[file b.py] +import a +x: a.A = int() +[out] +== +b.py:2: error: Incompatible types in assignment (expression has type "int", variable has type "str") + +[case testAliasFineNormalFunc] +import b +[file a.py] +A = int +[file a.py.2] +A = str +[file b.py] +import a +def f(x: a.A): + x = int() +[out] +== +b.py:3: error: Incompatible types in assignment (expression has type "int", variable has type "str") + +[case testAliasFineNormalClass] +import b +[file a.py] +A = int +[file a.py.2] +A = str +[file b.py] +import a +class C: + x: a.A +c = C() +c.x = int() +[out] +== +b.py:5: error: Incompatible types in assignment (expression has type "int", variable has type "str") + +[case testAliasFineNormalClassBases] +import b +[file a.py] +import c +A = c.BaseI +[file a.py.2] +import c +A = c.BaseS +[file b.py] +import a +class C(a.A): + x = int() +[file c.py] +class BaseI: + x: int +class BaseS: + x: str +[out] +== +b.py:3: error: Incompatible types in assignment (expression has type "int", base class "BaseS" defined the type as "str") + +[case testAliasFineGenericMod] +import b +[file a.py] +from typing import Dict +A = Dict[str, int] +[file a.py.2] +from typing import Dict +A = Dict[str, str] +[file b.py] +import a +x: a.A = {str(): int()} +[builtins fixtures/dict.pyi] +[out] +== +b.py:2: error: Dict entry 0 has incompatible type "str": "int"; expected "str": "str" + +[case testAliasFineGenericFunc] +import b +[file a.py] +from typing import Dict +A = Dict[str, int] +[file a.py.2] +from typing import Dict +A = Dict[str, str] +[file b.py] +import a +def f(x: a.A): + pass +reveal_type(f) +[builtins fixtures/dict.pyi] +[out] +b.py:4: error: Revealed type is 'def (x: builtins.dict[builtins.str, builtins.int]) -> Any' +== +b.py:4: error: Revealed type is 'def (x: builtins.dict[builtins.str, builtins.str]) -> Any' + +[case testAliasFineForwardMod] +import b +[file b.py] +x: A = int() +A = int +[file b.py.2] +x: A = int() +A = str +[out] +== +b.py:1: error: Incompatible types in assignment (expression has type "int", variable has type "str") + +[case testAliasFineForwardFunc] +import b +[file b.py] +def f(x: A): + x = int() +A = int +[file b.py.2] +def f(x: A): + x = int() +A = str +[out] +== +b.py:2: error: Incompatible types in assignment (expression has type "int", variable has type "str") + +[case testAliasFineChainedFunc] +import b +[file a.py] +A = int +[file a.py.2] +A = str +[file aa.py] +import a +B = a.A +[file b.py] +import aa +def f(x: aa.B): + x = int() +[out] +== +b.py:3: error: Incompatible types in assignment (expression has type "int", variable has type "str") + +[case testAliasFineChainedClass] +import b +[file a.py] +A = int +[file a.py.2] +A = str +[file aa.py] +import a +B = a.A +[file b.py] +import aa +class C: + x: aa.B +c = C() +c.x = int() +[out] +== +b.py:5: error: Incompatible types in assignment (expression has type "int", variable has type "str") + +[case testAliasFineNestedMod] +import b +[file a.py] +from typing import Dict +A = Dict[str, int] +[file a.py.2] +from typing import Dict +A = Dict[str, str] +[file aa.py] +from typing import Dict +import a +B = Dict[str, a.A] +[file b.py] +import aa +x: aa.B = {'first': {str(): int()}} +[builtins fixtures/dict.pyi] +[out] +== +b.py:2: error: Dict entry 0 has incompatible type "str": "int"; expected "str": "str" + +[case testAliasFineNestedFunc] +import b +[file a.py] +from typing import Dict +A = Dict[str, int] +[file a.py.2] +from typing import Dict +A = Dict[str, str] +[file aa.py] +from typing import Dict +import a +B = Dict[str, a.A] +[file b.py] +import aa +def f(x: aa.B): + x = {'first': {str(): int()}} +[builtins fixtures/dict.pyi] +[out] +== +b.py:3: error: Dict entry 0 has incompatible type "str": "int"; expected "str": "str" From 8d42ca783da64b2715f4073cd1bc092915bc22e3 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 31 Jan 2018 19:34:19 +0000 Subject: [PATCH 03/42] Fix processing of aliases in base classes and of chained aliases; add tests --- mypy/server/deps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/server/deps.py b/mypy/server/deps.py index b226e1511bcd..75425d72c3cc 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -106,7 +106,7 @@ def get_dependencies(target: MypyFile, python_version: Tuple[int, int]) -> Dict[str, Set[str]]: """Get all dependencies of a node, recursively.""" visitor = DependencyVisitor(type_map, python_version) - visitor.alias_deps = target.alias_deps + visitor.alias_deps = set() #target.alias_deps target.accept(visitor) return visitor.map @@ -119,7 +119,7 @@ def get_dependencies_of_target(module_id: str, """Get dependencies of a target -- don't recursive into nested targets.""" # TODO: Add tests for this function. visitor = DependencyVisitor(type_map, python_version) - visitor.alias_deps = module_tree.alias_deps + visitor.alias_deps = set() #module_tree.alias_deps visitor.scope.enter_file(module_id) if isinstance(target, MypyFile): # Only get dependencies of the top-level of the module. Don't recurse into From c0a6ed77106eddd425548fd40421e69e9cf31e6c Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 31 Jan 2018 19:34:42 +0000 Subject: [PATCH 04/42] Revert "Fix processing of aliases in base classes and of chained aliases; add tests" This reverts commit 8d42ca783da64b2715f4073cd1bc092915bc22e3. --- mypy/server/deps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/server/deps.py b/mypy/server/deps.py index 75425d72c3cc..b226e1511bcd 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -106,7 +106,7 @@ def get_dependencies(target: MypyFile, python_version: Tuple[int, int]) -> Dict[str, Set[str]]: """Get all dependencies of a node, recursively.""" visitor = DependencyVisitor(type_map, python_version) - visitor.alias_deps = set() #target.alias_deps + visitor.alias_deps = target.alias_deps target.accept(visitor) return visitor.map @@ -119,7 +119,7 @@ def get_dependencies_of_target(module_id: str, """Get dependencies of a target -- don't recursive into nested targets.""" # TODO: Add tests for this function. visitor = DependencyVisitor(type_map, python_version) - visitor.alias_deps = set() #module_tree.alias_deps + visitor.alias_deps = module_tree.alias_deps visitor.scope.enter_file(module_id) if isinstance(target, MypyFile): # Only get dependencies of the top-level of the module. Don't recurse into From 54289f0fd50c711fe4c4f2869a6591b0d1c140d6 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 31 Jan 2018 19:50:33 +0000 Subject: [PATCH 05/42] Skip two tests; minor fixes --- mypy/semanal.py | 8 +++++--- mypy/server/deps.py | 7 ++++--- mypy/server/update.py | 5 ++++- test-data/unit/fine-grained.test | 4 ++-- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 81952ac13cab..45c9cac35732 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1793,12 +1793,12 @@ def analyze_alias(self, rvalue: Expression, in_dynamic_func=dynamic, global_scope=global_scope, warn_bound_tvar=warn_bound_tvar) + tp = None # type: Optional[Type] if res: tp, depends_on = res alias_tvars = [name for (name, _) in tp.accept(TypeVariableQuery(self.lookup_qualified, self.tvar_scope))] else: - tp = None alias_tvars = [] depends_on = set() return tp, alias_tvars, depends_on @@ -1836,8 +1836,10 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None: node = self.lookup(lvalue.name, lvalue) assert node is not None node.alias_depends_on = depends_on.copy() - node.alias_depends_on.add(lvalue.fullname) # To avoid extra attributes on SymbolTableNode - # we add the fullname of alias to what it depends on. + if lvalue.fullname is not None: + # To avoid extra attributes on SymbolTableNode we add the fullname + # of alias to what it depends on. + node.alias_depends_on.add(lvalue.fullname) if depends_on: if self.is_class_scope(): assert self.type is not None, "Type not set at class scope" diff --git a/mypy/server/deps.py b/mypy/server/deps.py index b226e1511bcd..6d944f47ff35 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -141,7 +141,8 @@ def get_dependencies_of_target(module_id: str, class DependencyVisitor(TraverserVisitor): - alias_deps = None # type: Optional[DefaultDict[Union[MypyFile, FuncItem, ClassDef], Set[str]]] + alias_deps = None # type: DefaultDict[Union[MypyFile, FuncItem, ClassDef], Set[str]] + def __init__(self, type_map: Dict[Expression, Type], python_version: Tuple[int, int]) -> None: @@ -183,7 +184,7 @@ def visit_func_def(self, o: FuncDef) -> None: if o.info: for base in non_trivial_bases(o.info): self.add_dependency(make_trigger(base.fullname() + '.' + o.name())) - if self.alias_deps and o in self.alias_deps: + if o in self.alias_deps: for alias in self.alias_deps[o]: self.add_dependency(make_trigger(alias)) super().visit_func_def(o) @@ -211,7 +212,7 @@ def visit_class_def(self, o: ClassDef) -> None: 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) - if self.alias_deps and o in self.alias_deps: + if o in self.alias_deps: for alias in self.alias_deps[o]: self.add_dependency(make_trigger(alias)) self.is_class = old_is_class diff --git a/mypy/server/update.py b/mypy/server/update.py index 6918645e6ada..b23a8e0c137f 100644 --- a/mypy/server/update.py +++ b/mypy/server/update.py @@ -945,7 +945,10 @@ def update_deps(module_id: str, for deferred in nodes: node = deferred.node type_map = graph[module_id].type_map() - new_deps = get_dependencies_of_target(module_id, graph[module_id].tree, node, type_map, options.python_version) + tree = graph[module_id].tree + assert tree is not None, "Tree must be processed at this stage" + new_deps = get_dependencies_of_target(module_id, tree, node, type_map, + options.python_version) for trigger, targets in new_deps.items(): deps.setdefault(trigger, set()).update(targets) diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index eaf7dbf0fa47..cd49ea2da16d 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -1975,7 +1975,7 @@ c.x = int() == b.py:5: error: Incompatible types in assignment (expression has type "int", variable has type "str") -[case testAliasFineNestedMod] +[case testAliasFineNestedMod-skip] import b [file a.py] from typing import Dict @@ -1995,7 +1995,7 @@ x: aa.B = {'first': {str(): int()}} == b.py:2: error: Dict entry 0 has incompatible type "str": "int"; expected "str": "str" -[case testAliasFineNestedFunc] +[case testAliasFineNestedFunc-skip] import b [file a.py] from typing import Dict From 0e365e8d071152fab9be371829aefc0bafadb433 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 31 Jan 2018 20:12:07 +0000 Subject: [PATCH 06/42] Refsactor dependency update in a method --- mypy/semanal.py | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 45c9cac35732..1fa4d44bf4e3 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1690,15 +1690,22 @@ def anal_type(self, t: Type, *, if from_bases is not None: self.cur_mod_node.alias_deps[from_bases].update(a.aliases_used) return tp - if self.is_class_scope(): - assert self.type is not None, "Type not set at class scope" - self.cur_mod_node.alias_deps[self.type.defn].update(a.aliases_used) - elif self.is_func_scope(): - self.cur_mod_node.alias_deps[self.function_stack[-1]].update(a.aliases_used) - else: - self.cur_mod_node.alias_deps[self.cur_mod_node].update(a.aliases_used) + self.add_alias_deps(a.aliases_used) return tp + def add_alias_deps(self, aliases_used: Set[str]) -> None: + """Add full names of type aliases on which the current node depends. + + This is used by fine-grained incremental mode to re-chek the corresponding nodes. + """ + if self.is_class_scope(): + assert self.type is not None, "Type not set at class scope" + self.cur_mod_node.alias_deps[self.type.defn].update(aliases_used) + elif self.is_func_scope(): + self.cur_mod_node.alias_deps[self.function_stack[-1]].update(aliases_used) + else: + self.cur_mod_node.alias_deps[self.cur_mod_node].update(aliases_used) + def visit_assignment_stmt(self, s: AssignmentStmt) -> None: for lval in s.lvalues: self.analyze_lvalue(lval, explicit_type=s.type is not None) @@ -1841,13 +1848,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None: # of alias to what it depends on. node.alias_depends_on.add(lvalue.fullname) if depends_on: - if self.is_class_scope(): - assert self.type is not None, "Type not set at class scope" - self.cur_mod_node.alias_deps[self.type.defn].update(depends_on) - elif self.is_func_scope(): - self.cur_mod_node.alias_deps[self.function_stack[-1]].update(depends_on) - else: - self.cur_mod_node.alias_deps[self.cur_mod_node].update(depends_on) + self.add_alias_deps(depends_on) if not lvalue.is_inferred_def: # Type aliases can't be re-defined. if node and (node.kind == TYPE_ALIAS or isinstance(node.node, TypeInfo)): @@ -3481,13 +3482,7 @@ def visit_index_expr(self, expr: IndexExpr) -> None: expr.analyzed.line = expr.line expr.analyzed.column = expr.column if depends_on: # for situations like `L = LongGeneric; x = L[int]()` - if self.is_class_scope(): - assert self.type is not None, "Type not set at class scope" - self.cur_mod_node.alias_deps[self.type.defn].update(depends_on) - elif self.is_func_scope(): - self.cur_mod_node.alias_deps[self.function_stack[-1]].update(depends_on) - else: - self.cur_mod_node.alias_deps[self.cur_mod_node].update(depends_on) + self.add_alias_deps(depends_on) elif refers_to_class_or_function(expr.base): # Special form -- type application. # Translate index to an unanalyzed type. From 8d88890230c39612067bb7649daccfd318c6dcdf Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 31 Jan 2018 20:30:28 +0000 Subject: [PATCH 07/42] 3.5.1 --- mypy/nodes.py | 6 +++++- mypy/server/deps.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/mypy/nodes.py b/mypy/nodes.py index 4f75e4863c47..227a34dda013 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -4,9 +4,13 @@ from abc import abstractmethod from collections import OrderedDict, defaultdict from typing import ( - Any, TypeVar, List, Tuple, cast, Set, Dict, Union, Optional, Callable, Sequence, DefaultDict + Any, TypeVar, List, Tuple, cast, Set, Dict, Union, Optional, Callable, Sequence ) +MYPY = False +if MYPY: + from typing import DefaultDict + import mypy.strconv from mypy.util import short_type from mypy.visitor import NodeVisitor, StatementVisitor, ExpressionVisitor diff --git a/mypy/server/deps.py b/mypy/server/deps.py index 6d944f47ff35..151eb6c13568 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -79,7 +79,11 @@ class 'mod.Cls'. This can also refer to an attribute inherited from a Test cases for this module live in 'test-data/unit/deps*.test'. """ -from typing import Dict, List, Set, Optional, Tuple, Union, DefaultDict +from typing import Dict, List, Set, Optional, Tuple, Union + +MYPY = False +if MYPY: + from typing import DefaultDict from mypy.checkmember import bind_self from mypy.nodes import ( From e43821f43d38c5d32c876def5490868b4270668e Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 1 Feb 2018 15:23:39 +0000 Subject: [PATCH 08/42] Review comments 1/3 --- mypy/semanal.py | 60 +++++++++++++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 1fa4d44bf4e3..bc57b06b4730 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1070,7 +1070,7 @@ def analyze_base_classes(self, defn: ClassDef) -> None: for base_expr in defn.base_type_exprs: try: - base = self.expr_to_analyzed_type(base_expr, from_bases=defn) + base = self.expr_to_analyzed_type(base_expr, source_classdef=defn) except TypeTranslationError: self.fail('Invalid base class', base_expr) info.fallback_to_any = True @@ -1183,7 +1183,10 @@ def update_metaclass(self, defn: ClassDef) -> None: defn.metaclass = metas.pop() def expr_to_analyzed_type(self, expr: Expression, *, - from_bases: Optional[ClassDef] = None) -> Type: + source_classdef: Optional[ClassDef] = None) -> Type: + # If this function is called to analyze a base in class definition, then + # the corresponding ClassDef, should ber passed in `source_classdef`. + # This is used to determine correct target for a fine-grained dependency. if isinstance(expr, CallExpr): expr.accept(self) info = self.check_namedtuple(expr) @@ -1195,7 +1198,7 @@ def expr_to_analyzed_type(self, expr: Expression, *, fallback = Instance(info, []) return TupleType(info.tuple_type.items, fallback=fallback) typ = expr_to_unanalyzed_type(expr) - return self.anal_type(typ, from_bases=from_bases) + return self.anal_type(typ, source_classdef=source_classdef) def verify_base_classes(self, defn: ClassDef) -> bool: info = defn.info @@ -1680,24 +1683,29 @@ def anal_type(self, t: Type, *, allow_tuple_literal: bool = False, aliasing: bool = False, third_pass: bool = False, - from_bases: Optional[ClassDef] = None) -> Type: + source_classdef: Optional[ClassDef] = None) -> Type: a = self.type_analyzer(tvar_scope=tvar_scope, aliasing=aliasing, allow_tuple_literal=allow_tuple_literal, third_pass=third_pass) - tp = t.accept(a) - if a.aliases_used: - if from_bases is not None: - self.cur_mod_node.alias_deps[from_bases].update(a.aliases_used) - return tp - self.add_alias_deps(a.aliases_used) - return tp - - def add_alias_deps(self, aliases_used: Set[str]) -> None: + typ = t.accept(a) + self.add_type_alias_deps(a.aliases_used, source_classdef) + return typ + + def add_type_alias_deps(self, aliases_used: Set[str], + node_override: Optional[ClassDef] = None) -> None: """Add full names of type aliases on which the current node depends. - This is used by fine-grained incremental mode to re-chek the corresponding nodes. + This is used by fine-grained incremental mode to re-check the corresponding nodes. + If `node_override` is None, then the target node used will be the current scope. """ + if not aliases_used: + # A basic optimization to avoid adding targets with no dependencies to + # the `alias_deps` dict. + return + if node_override: + self.cur_mod_node.alias_deps[node_override].update(aliases_used) + return if self.is_class_scope(): assert self.type is not None, "Type not set at class scope" self.cur_mod_node.alias_deps[self.type.defn].update(aliases_used) @@ -1782,8 +1790,12 @@ def alias_fallback(self, tp: Type) -> Instance: def analyze_alias(self, rvalue: Expression, warn_bound_tvar: bool = False) -> Tuple[Optional[Type], List[str], Set[str]]: """Check if 'rvalue' represents a valid type allowed for aliasing - (e.g. not a type variable). If yes, return the corresponding type and a list of - qualified type variable names for generic aliases. + (e.g. not a type variable). If yes, return the corresponding type, a list of + qualified type variable names for generic aliases, and a set of names the alias depends on. + An schematic example for the latter: + A = int + B = str + analyze_alias(Dict[A, B])[2] == {'__main__.A', '__main__.B'} """ dynamic = bool(self.function_stack and self.function_stack[-1].is_dynamic()) global_scope = not self.type and not self.function_stack @@ -1800,15 +1812,15 @@ def analyze_alias(self, rvalue: Expression, in_dynamic_func=dynamic, global_scope=global_scope, warn_bound_tvar=warn_bound_tvar) - tp = None # type: Optional[Type] + typ = None # type: Optional[Type] if res: - tp, depends_on = res + typ, depends_on = res alias_tvars = [name for (name, _) in - tp.accept(TypeVariableQuery(self.lookup_qualified, self.tvar_scope))] + typ.accept(TypeVariableQuery(self.lookup_qualified, self.tvar_scope))] else: alias_tvars = [] depends_on = set() - return tp, alias_tvars, depends_on + return typ, alias_tvars, depends_on def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None: """Check if assignment creates a type alias and set it up as needed. @@ -1847,8 +1859,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None: # To avoid extra attributes on SymbolTableNode we add the fullname # of alias to what it depends on. node.alias_depends_on.add(lvalue.fullname) - if depends_on: - self.add_alias_deps(depends_on) + self.add_type_alias_deps(depends_on) if not lvalue.is_inferred_def: # Type aliases can't be re-defined. if node and (node.kind == TYPE_ALIAS or isinstance(node.node, TypeInfo)): @@ -3481,8 +3492,9 @@ def visit_index_expr(self, expr: IndexExpr) -> None: in_runtime=True) expr.analyzed.line = expr.line expr.analyzed.column = expr.column - if depends_on: # for situations like `L = LongGeneric; x = L[int]()` - self.add_alias_deps(depends_on) + # We also store fine-grained dependencies to correctly re-process nodes + # with situations like `L = LongGeneric; x = L[int]()`. + self.add_type_alias_deps(depends_on) elif refers_to_class_or_function(expr.base): # Special form -- type application. # Translate index to an unanalyzed type. From 2b25e911ca23b13e862da598df09786f07fa64ec Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 1 Feb 2018 17:55:54 +0000 Subject: [PATCH 09/42] Review comments 2/3 --- mypy/semanal_pass3.py | 27 ++++++++++++++++++--------- mypy/server/deps.py | 39 +++++++++++++++++++++++---------------- mypy/typeanal.py | 5 ++++- 3 files changed, 45 insertions(+), 26 deletions(-) diff --git a/mypy/semanal_pass3.py b/mypy/semanal_pass3.py index 05e3a4842ad7..6e930d6d1783 100644 --- a/mypy/semanal_pass3.py +++ b/mypy/semanal_pass3.py @@ -10,6 +10,7 @@ """ from collections import OrderedDict +from contextlib import contextmanager from typing import Dict, List, Callable, Optional, Union, Set, cast, Tuple from mypy import messages, experiments @@ -60,11 +61,23 @@ def visit_file(self, file_node: MypyFile, fnam: str, options: Options, self.is_typeshed_file = self.errors.is_typeshed_file(fnam) self.sem.cur_mod_id = file_node.fullname() self.cur_mod_node = file_node + # This variable tracks the current scope node: a module, a class definition, + # or a function. It is used to, e.g., correctly determine target of a fine-grained + # dependency. self.cur_node = file_node # type: Union[MypyFile, FuncItem, ClassDef] self.sem.globals = file_node.names with experiments.strict_optional_set(options.strict_optional): self.accept(file_node) + @contextmanager + def enter_scope(self, node: Union[MypyFile, ClassDef, FuncItem]) -> None: + old_node = self.cur_node + self.cur_node = node + try: + yield + finally: + self.cur_node = old_node + def refresh_partial(self, node: Union[MypyFile, FuncItem, OverloadedFuncDef]) -> None: """Refresh a stale target in fine-grained incremental mode.""" if isinstance(node, MypyFile): @@ -94,11 +107,9 @@ def visit_func_def(self, fdef: FuncDef) -> None: if not self.recurse_into_functions: return self.errors.push_function(fdef.name()) - old_node = self.cur_node - self.cur_node = fdef - self.analyze(fdef.type, fdef) - super().visit_func_def(fdef) - self.cur_node = old_node + with self.enter_scope(fdef): + self.analyze(fdef.type, fdef) + super().visit_func_def(fdef) self.errors.pop_function() def visit_overloaded_func_def(self, fdef: OverloadedFuncDef) -> None: @@ -139,10 +150,8 @@ def visit_class_def(self, tdef: ClassDef) -> None: elif isinstance(tdef.analyzed, NamedTupleExpr): self.analyze(tdef.analyzed.info.tuple_type, tdef.analyzed, warn=True) self.analyze_info(tdef.analyzed.info) - old_node = self.cur_node - self.cur_node = tdef - super().visit_class_def(tdef) - self.cur_node = old_node + with self.enter_scope(tdef): + super().visit_class_def(tdef) def visit_decorator(self, dec: Decorator) -> None: """Try to infer the type of the decorated function. diff --git a/mypy/server/deps.py b/mypy/server/deps.py index 151eb6c13568..792e75084f4b 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -109,8 +109,7 @@ def get_dependencies(target: MypyFile, type_map: Dict[Expression, Type], python_version: Tuple[int, int]) -> Dict[str, Set[str]]: """Get all dependencies of a node, recursively.""" - visitor = DependencyVisitor(type_map, python_version) - visitor.alias_deps = target.alias_deps + visitor = DependencyVisitor(type_map, python_version, target.alias_deps) target.accept(visitor) return visitor.map @@ -122,8 +121,7 @@ def get_dependencies_of_target(module_id: str, python_version: Tuple[int, int]) -> Dict[str, Set[str]]: """Get dependencies of a target -- don't recursive into nested targets.""" # TODO: Add tests for this function. - visitor = DependencyVisitor(type_map, python_version) - visitor.alias_deps = module_tree.alias_deps + visitor = DependencyVisitor(type_map, python_version, module_tree.alias_deps) visitor.scope.enter_file(module_id) if isinstance(target, MypyFile): # Only get dependencies of the top-level of the module. Don't recurse into @@ -145,14 +143,23 @@ def get_dependencies_of_target(module_id: str, class DependencyVisitor(TraverserVisitor): - alias_deps = None # type: DefaultDict[Union[MypyFile, FuncItem, ClassDef], Set[str]] - def __init__(self, type_map: Dict[Expression, Type], - python_version: Tuple[int, int]) -> None: + python_version: Tuple[int, int], + alias_deps: DefaultDict[Union[MypyFile, FuncItem, ClassDef], Set[str]]) -> None: self.scope = Scope() self.type_map = type_map self.python2 = python_version[0] == 2 + # This attribute holds a mapping from target to names of type aliases + # it depends on. These need to be processed specially, since they are + # only present in expanded form in symbol tables. For example, after: + # A = List[int] + # x: A + # The module symbol table will just have a Var `x` with type `List[int]`, + # and the dependency of `x` on `A` is lost. Therefore the alias dependencies + # are preserved at alias expansion points in `semanal.py`, stored as an attribute + # on MypyFile, and then passed here. + self.alias_deps = alias_deps self.map = {} # type: Dict[str, Set[str]] self.is_class = False self.is_package_init_file = False @@ -168,9 +175,7 @@ def __init__(self, def visit_mypy_file(self, o: MypyFile) -> None: self.scope.enter_file(o.fullname()) self.is_package_init_file = o.is_package_init_file() - if o in self.alias_deps: - for alias in self.alias_deps[o]: - self.add_dependency(make_trigger(alias)) + self.add_type_alias_deps(o) super().visit_mypy_file(o) self.scope.leave() @@ -188,9 +193,7 @@ def visit_func_def(self, o: FuncDef) -> None: if o.info: for base in non_trivial_bases(o.info): self.add_dependency(make_trigger(base.fullname() + '.' + o.name())) - if o in self.alias_deps: - for alias in self.alias_deps[o]: - self.add_dependency(make_trigger(alias)) + self.add_type_alias_deps(o) super().visit_func_def(o) self.scope.leave() @@ -216,9 +219,7 @@ def visit_class_def(self, o: ClassDef) -> None: 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) - if o in self.alias_deps: - for alias in self.alias_deps[o]: - self.add_dependency(make_trigger(alias)) + self.add_type_alias_deps(o) self.is_class = old_is_class info = o.info for name, node in info.names.items(): @@ -523,6 +524,12 @@ def visit_yield_from_expr(self, e: YieldFromExpr) -> None: # Helpers + def add_type_alias_deps(self, o: Union[MypyFile, FuncItem, ClassDef]) -> None: + """Add dependencies from type aliases to the current target.""" + if o in self.alias_deps: + for alias in self.alias_deps[o]: + self.add_dependency(make_trigger(alias)) + def add_dependency(self, trigger: str, target: Optional[str] = None) -> None: """Add dependency from trigger to a target. diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 824c07b60108..ad1123f966cd 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -64,7 +64,8 @@ def analyze_type_alias(node: Expression, in_dynamic_func: bool = False, global_scope: bool = True, warn_bound_tvar: bool = False) -> Optional[Tuple[Type, Set[str]]]: - """Return type if node is valid as a type alias rvalue. + """If `node` is valid as a type alias rvalue, return the resulting type and a set of + full names of type aliases it depends on (directly or indirectly). Return None otherwise. 'node' must have been semantically analyzed. """ @@ -172,6 +173,8 @@ def __init__(self, self.is_typeshed_stub = is_typeshed_stub self.warn_bound_tvar = warn_bound_tvar self.third_pass = third_pass + # Names of type aliases encountered while analysing a type will be collected here. + # (This also recursively includes the names of aliases they depend on.) self.aliases_used = set() # type: Set[str] def visit_unbound_type(self, t: UnboundType) -> Type: From d5306f2352095804a14b93c4631f6f53605c0daf Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 1 Feb 2018 18:07:15 +0000 Subject: [PATCH 10/42] Minor fixes, not done yet with review comments. --- mypy/semanal_pass3.py | 4 ++-- mypy/server/deps.py | 2 +- mypy/typeanal.py | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mypy/semanal_pass3.py b/mypy/semanal_pass3.py index 6e930d6d1783..5a814b01b198 100644 --- a/mypy/semanal_pass3.py +++ b/mypy/semanal_pass3.py @@ -11,7 +11,7 @@ from collections import OrderedDict from contextlib import contextmanager -from typing import Dict, List, Callable, Optional, Union, Set, cast, Tuple +from typing import Dict, List, Callable, Optional, Union, Set, cast, Tuple, Iterator from mypy import messages, experiments from mypy.nodes import ( @@ -70,7 +70,7 @@ def visit_file(self, file_node: MypyFile, fnam: str, options: Options, self.accept(file_node) @contextmanager - def enter_scope(self, node: Union[MypyFile, ClassDef, FuncItem]) -> None: + def enter_scope(self, node: Union[MypyFile, ClassDef, FuncItem]) -> Iterator[None]: old_node = self.cur_node self.cur_node = node try: diff --git a/mypy/server/deps.py b/mypy/server/deps.py index 792e75084f4b..6f5c72159300 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -146,7 +146,7 @@ class DependencyVisitor(TraverserVisitor): def __init__(self, type_map: Dict[Expression, Type], python_version: Tuple[int, int], - alias_deps: DefaultDict[Union[MypyFile, FuncItem, ClassDef], Set[str]]) -> None: + alias_deps: 'DefaultDict[Union[MypyFile, FuncItem, ClassDef], Set[str]]') -> None: self.scope = Scope() self.type_map = type_map self.python2 = python_version[0] == 2 diff --git a/mypy/typeanal.py b/mypy/typeanal.py index ad1123f966cd..2df3f7c892fb 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -2,6 +2,7 @@ from collections import OrderedDict from typing import Callable, List, Optional, Set, Tuple, Iterator, TypeVar, Iterable, Dict, Union + from itertools import chain from contextlib import contextmanager From fa6d426d704cbb661f5e1172ad6aab8b8581ec56 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 2 Feb 2018 00:53:30 +0000 Subject: [PATCH 11/42] Update some tests as requested. More refactoring needed to record TypeVar fullname. Also maybe record exact location of type alias in semanal to fire attribute triggers. --- mypy/server/deps.py | 14 ++- test-data/unit/deps-types.test | 172 ++++++++++++++++++++++++++------- 2 files changed, 151 insertions(+), 35 deletions(-) diff --git a/mypy/server/deps.py b/mypy/server/deps.py index 6f5c72159300..b03ce8161c60 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -92,7 +92,7 @@ class 'mod.Cls'. This can also refer to an attribute inherited from a ComparisonExpr, GeneratorExpr, DictionaryComprehension, StarExpr, PrintStmt, ForStmt, WithStmt, TupleExpr, ListExpr, OperatorAssignmentStmt, DelStmt, YieldFromExpr, Decorator, Block, TypeInfo, FuncBase, OverloadedFuncDef, RefExpr, SuperExpr, Var, NamedTupleExpr, TypedDictExpr, - LDEF, MDEF, GDEF, FuncItem, + LDEF, MDEF, GDEF, FuncItem, TypeAliasExpr, op_methods, reverse_op_methods, ops_with_inplace_method, unary_op_methods ) from mypy.traverser import TraverserVisitor @@ -257,7 +257,6 @@ def visit_block(self, o: Block) -> None: def visit_assignment_stmt(self, o: AssignmentStmt) -> None: # TODO: Implement all assignment special forms, including these: # Enum - # type aliases rvalue = o.rvalue if isinstance(rvalue, CallExpr) and isinstance(rvalue.analyzed, TypeVarExpr): # TODO: Support type variable value restriction @@ -282,6 +281,17 @@ def visit_assignment_stmt(self, o: AssignmentStmt) -> 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)) + elif isinstance(rvalue, IndexExpr) and isinstance(rvalue.analyzed, TypeAliasExpr): + assert len(o.lvalues) == 1 + lvalue = o.lvalues[0] + assert isinstance(lvalue, NameExpr) + attr_trigger = make_trigger('%s.%s' % (self.scope.current_full_target(), + lvalue.name)) + self.add_type_dependencies(rvalue.analyzed.type, + attr_trigger) + for tvar in rvalue.analyzed.tvars: + self.add_dependency(make_trigger(tvar), target=attr_trigger) + self.add_dependency(attr_trigger) else: # Normal assignment super().visit_assignment_stmt(o) diff --git a/test-data/unit/deps-types.test b/test-data/unit/deps-types.test index 7db1d5188e39..6704b6f583aa 100644 --- a/test-data/unit/deps-types.test +++ b/test-data/unit/deps-types.test @@ -152,149 +152,255 @@ def h() -> None: -> m, m.h -> m.f +-- Type aliases + [case testAliasDepsNormalMod] -A = int +from mod import I +A = I x: A +[file mod.py] +class I: pass [out] -> m -> m + -> m + -> , , m [case testAliasDepsNormalFunc] -A = int +from mod import I +A = I def f(x: A) -> None: pass +[file mod.py] +class I: pass [out] -> m, m.f + -> m + -> , , m, m.f [case testAliasDepsNormalClass] from a import A class C: x: A [file a.py] -A = int +from mod import I +A = I +[file mod.py] +class I: pass [out] -> m.C -> m + -> , m [case testAliasDepsNormalClassBases] from a import A class C(A): pass [file a.py] -A = int +from mod import I +A = I +[file mod.py] +class I: pass [out] -> m.C -> m + -> + -> m, m.C [case testAliasDepsGenericMod] -from typing import Dict -A = Dict[int, str] +from mod import I, S, D +A = D[I, S] x: A -[builtins fixtures/dict.pyi] +[file mod.py] +from typing import TypeVar, Generic +T = TypeVar('T') +U = TypeVar('U') +class D(Generic[T, U]): pass +class I: pass +class S: pass [out] -> m -> m + -> , , m + -> , , m + -> , , m [case testAliasDepsGenericFunc] -from typing import Dict -A = Dict[str, int] +from mod import I, S, D +A = D[S, I] def f(x: A) -> None: pass -[builtins fixtures/dict.pyi] +[file mod.py] +from typing import TypeVar, Generic +T = TypeVar('T') +U = TypeVar('U') +class D(Generic[T, U]): pass +class I: pass +class S: pass [out] -> m, m.f + -> , , m, m.f + -> , , m, m.f + -> , , m, m.f [case testAliasDepsGenericClass] -from typing import Dict, TypeVar -T = TypeVar('T') -A = Dict[str, T] +from mod import I, D, S, T +A = D[S, T] class C: - x: A[int] -[builtins fixtures/dict.pyi] + x: A[I] +[file mod.py] +from typing import TypeVar, Generic +T = TypeVar('T') +U = TypeVar('U') +class D(Generic[T, U]): pass +class I: pass +class S: pass [out] -> m -> m.C + -> , , m + -> , m + -> , , m + -> , m [case testAliasDepsForwardMod] +from mod import I x: A -A = int +A = I +[file mod.py] +from typing import TypeVar, Generic +class I: pass [out] -> m -> m + -> m + -> , , m [case testAliasDepsForwardFunc] +from mod import I def f(x: A) -> None: pass -A = int +A = I +[file mod.py] +class I: pass [out] -> m, m.f + -> m + -> , , m, m.f [case testAliasDepsForwardClass] +from mod import I class C: x: A -A = int +A = I +[file mod.py] +class I: pass [out] -> m -> m.C + -> m + -> , , m [case testAliasDepsChainedMod] -A = int +from mod import I +A = I B = A x: B +[file mod.py] +class I: pass [out] -> m -> m -> m + -> m + -> , , , m [case testAliasDepsChainedFunc] -A = int +from mod import I +A = I B = A def f(x: B) -> None: pass +[file mod.py] +class I: pass [out] -> m, m.f -> m, m.f + -> m + -> , , , m, m.f [case testAliasDepsChainedClass] -A = int +from mod import I +A = I B = A class C(B): pass +[file mod.py] +class I: pass [out] -> m -> m -> m.C + -> , m + -> , , m, m.C [case testAliasDepsNestedMod] -from typing import Dict -A = Dict[str, int] -B = Dict[str, A] +from mod import I, S, D +A = D[S, I] +B = D[S, A] x: B -[builtins fixtures/dict.pyi] +[file mod.py] +from typing import TypeVar, Generic +T = TypeVar('T') +U = TypeVar('U') +class D(Generic[T, U]): pass +class I: pass +class S: pass [out] -> m -> m -> m + -> , , , m + -> , , , m + -> , , , m [case testAliasDepsNestedFunc] -from typing import Dict -A = Dict[str, int] -B = Dict[str, A] +from mod import I, S, D +A = D[S, I] +B = D[S, A] def f(x: B) -> None: pass -[builtins fixtures/dict.pyi] +[file mod.py] +from typing import TypeVar, Generic +T = TypeVar('T') +U = TypeVar('U') +class D(Generic[T, U]): pass +class I: pass +class S: pass [out] -> m, m.f -> m, m.f + -> , , , m, m.f + -> , , , m, m.f + -> , , , m, m.f [case testAliasDepsNestedClass] -from typing import Dict -A = Dict[str, int] -B = Dict[str, A] +from mod import I, S, D +A = D[S, I] +B = D[S, A] class C: x: B -[builtins fixtures/dict.pyi] +[file mod.py] +from typing import TypeVar, Generic +T = TypeVar('T') +U = TypeVar('U') +class D(Generic[T, U]): pass +class I: pass +class S: pass [out] -> m -> m -> m.C + -> , , , m + -> , , , m + -> , , , m From d9b397ef8676a87dfe83740daebd44efb1712a41 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 8 Feb 2018 12:46:34 +0000 Subject: [PATCH 12/42] Add more tests --- test-data/unit/deps-types.test | 70 ++++++++++++++++++++++- test-data/unit/fine-grained.test | 98 ++++++++++++++++++++++++++++++-- 2 files changed, 163 insertions(+), 5 deletions(-) diff --git a/test-data/unit/deps-types.test b/test-data/unit/deps-types.test index 6704b6f583aa..f12dd8eb3dfd 100644 --- a/test-data/unit/deps-types.test +++ b/test-data/unit/deps-types.test @@ -243,7 +243,7 @@ class S: pass -> , , m, m.f -> , , m, m.f -[case testAliasDepsGenericClass] +[case testAliasDepsGenericClass-skip] from mod import I, D, S, T A = D[S, T] class C: @@ -404,3 +404,71 @@ class S: pass -> , , , m -> , , , m -> , , , m + +[case testAliasDepsCast] +from typing import cast +from mod import I +A = I +def fun() -> None: + x = cast(A, 42) +[file mod.py] +from typing import TypeVar, Generic +T = TypeVar('T') +U = TypeVar('U') +class D(Generic[T, U]): pass +class I: pass +class S: pass +[out] + -> m, m.fun + -> m + -> , m, m.fun + +[case testAliasDepsRuntime] +from mod import I, S, D +A = I +x = D[S, A]() +[file mod.py] +from typing import TypeVar, Generic +T = TypeVar('T') +U = TypeVar('U') +class D(Generic[T, U]): pass +class I: pass +class S: pass +[out] + -> m + -> m + -> m + -> , m + -> m + -> , , m + -> , m + +[case testAliasDepsNamedTuple] +from typing import NamedTuple +from mod import I +A = I +class P(NamedTuple): + x: A +[file mod.py] +class I: pass +[out] + -> m + -> m.P + -> m + -> , , , m, m.P + +[case testAliasDepsTypedDict] +from mypy_extensions import TypedDict +from mod import I +A = I +class P(TypedDict): + x: A +[file mod.py] +class I: pass +[builtins fixtures/dict.pyi] +[out] + -> m + -> m.P + -> m + -> , , m, m.P + -> m diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index cd49ea2da16d..3cbacd7cfca3 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -1894,7 +1894,7 @@ x: a.A = {str(): int()} == b.py:2: error: Dict entry 0 has incompatible type "str": "int"; expected "str": "str" -[case testAliasFineGenericFunc] +[case testAliasFineGenericFunc-skip] import b [file a.py] from typing import Dict @@ -1906,12 +1906,11 @@ A = Dict[str, str] import a def f(x: a.A): pass -reveal_type(f) +f({str(): int()}) [builtins fixtures/dict.pyi] [out] -b.py:4: error: Revealed type is 'def (x: builtins.dict[builtins.str, builtins.int]) -> Any' == -b.py:4: error: Revealed type is 'def (x: builtins.dict[builtins.str, builtins.str]) -> Any' +b.py:4: error: Dict entry 0 has incompatible type "str": "int"; expected "str": "str" [case testAliasFineForwardMod] import b @@ -2015,3 +2014,94 @@ def f(x: aa.B): [out] == b.py:3: error: Dict entry 0 has incompatible type "str": "int"; expected "str": "str" + +[case testAliasFineNonGenericToGeneric] +import b +[file a.py] +from typing import Dict, TypeVar +T = TypeVar('T') +A = Dict[T, int] +[file a.py.2] +A = str +[file b.py] +import a +def f(x: a.A[str]): + pass +[builtins fixtures/dict.pyi] +[out] +== +b.py:2: error: "str" expects no type arguments, but 1 given + +[case testAliasFineGenericToNonGeneric] +import b +[file a.py] +A = str +[file a.py.2] +from typing import Dict, TypeVar +T = TypeVar('T') +A = Dict[T, int] +[file b.py] +import a +def f(x: a.A): + pass +reveal_type(f) +[builtins fixtures/dict.pyi] +[out] +b.py:4: error: Revealed type is 'def (x: builtins.str) -> Any' +== +b.py:4: error: Revealed type is 'def (x: builtins.dict[Any, builtins.int]) -> Any' + +[case testAliasFineChangedNumberOfTypeVars] +import b +[file a.py] +from typing import Dict, TypeVar +T = TypeVar('T') +A = Dict[T, int] +[file a.py.2] +from typing import Dict, TypeVar +T = TypeVar('T') +S = TypeVar('S') +A = Dict[T, S] +[file b.py] +import a +def f(x: a.A[str]): + pass +[builtins fixtures/dict.pyi] +[out] +== +b.py:2: error: Bad number of arguments for type alias, expected: 2, given: 1 + +[case testAliasFineComponentDeleted-skip] +import b +[file a.py] +class B: pass +[file a.py.2] +x = 1 +[file b.py] +import a +from typing import Dict, TypeVar +T = TypeVar('T') +A = Dict[T, a.B] +def f(x: A[int]): + pass +[builtins fixtures/dict.pyi] +[out] +== +b.py:4: error: Name 'a.B' is not defined + +[case testAliasFineTargetDeleted] +import c +[file a.py] +A = int +[file b.py] +import a +B = a.A +[file b.py.2] +x = 1 +[file c.py] +import b +def f(x: b.B): + pass +[out] +== +c.py:2: error: Name 'b.B' is not defined \ No newline at end of file From d7adb15ae5740f4d484553f1691b10516c704fc7 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 8 Feb 2018 17:38:11 +0000 Subject: [PATCH 13/42] Add type variables to type aliases dependencies --- mypy/nodes.py | 8 +++++++- mypy/semanal.py | 8 ++++++-- mypy/semanal_pass3.py | 4 ++-- mypy/server/deps.py | 14 ++++++-------- test-data/unit/deps-types.test | 12 ++++++------ 5 files changed, 27 insertions(+), 19 deletions(-) diff --git a/mypy/nodes.py b/mypy/nodes.py index 227a34dda013..7ec4c35f5e4e 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -187,6 +187,9 @@ def deserialize(cls, data: JsonDict) -> 'SymbolNode': raise NotImplementedError('unexpected .class {}'.format(classname)) +DepNode = Union['MypyFile', 'FuncItem', 'ClassDef', 'AssignmentStmt'] + + class MypyFile(SymbolNode): """The abstract syntax tree of a single source file.""" @@ -199,7 +202,7 @@ class MypyFile(SymbolNode): # Top-level definitions and statements defs = None # type: List[Statement] # Type alias dependencies as mapping from node to set of alias full names - alias_deps = None # type: DefaultDict[Union[MypyFile, FuncItem, ClassDef], Set[str]] + alias_deps = None # type: DefaultDict[DepNodde, Set[str]] # Is there a UTF-8 BOM at the start? is_bom = False names = None # type: SymbolTable @@ -2341,6 +2344,9 @@ class SymbolTableNode: # Is this node refers to other node via node aliasing? # (This is currently used for simple aliases like `A = int` instead of .type_override) is_aliasing = False # type: bool + # This includes full names of aliases used in this alias, and full names of + # type variables used to define it, if it is generic. + # TODO: refactor to move type variables together with alias_tvars (currently unqualified). alias_depends_on = None # type: Set[str] def __init__(self, diff --git a/mypy/semanal.py b/mypy/semanal.py index bc57b06b4730..d21e56c12fd7 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1693,7 +1693,7 @@ def anal_type(self, t: Type, *, return typ def add_type_alias_deps(self, aliases_used: Set[str], - node_override: Optional[ClassDef] = None) -> None: + node_override: Optional[Union[ClassDef, AssertStmt]] = None) -> None: """Add full names of type aliases on which the current node depends. This is used by fine-grained incremental mode to re-check the corresponding nodes. @@ -1815,8 +1815,11 @@ def analyze_alias(self, rvalue: Expression, typ = None # type: Optional[Type] if res: typ, depends_on = res - alias_tvars = [name for (name, _) in + alias_tvars = [name for (name, node) in typ.accept(TypeVariableQuery(self.lookup_qualified, self.tvar_scope))] + qualified_tvars = [node.fullname() for (name, node) in + typ.accept(TypeVariableQuery(self.lookup_qualified, self.tvar_scope))] + depends_on.update(qualified_tvars) else: alias_tvars = [] depends_on = set() @@ -1860,6 +1863,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None: # of alias to what it depends on. node.alias_depends_on.add(lvalue.fullname) self.add_type_alias_deps(depends_on) + self.add_type_alias_deps(depends_on, node_override=s) if not lvalue.is_inferred_def: # Type aliases can't be re-defined. if node and (node.kind == TYPE_ALIAS or isinstance(node.node, TypeInfo)): diff --git a/mypy/semanal_pass3.py b/mypy/semanal_pass3.py index 5a814b01b198..ded81482e06e 100644 --- a/mypy/semanal_pass3.py +++ b/mypy/semanal_pass3.py @@ -18,7 +18,7 @@ Node, Expression, MypyFile, FuncDef, FuncItem, Decorator, RefExpr, Context, TypeInfo, ClassDef, Block, TypedDictExpr, NamedTupleExpr, AssignmentStmt, IndexExpr, TypeAliasExpr, NameExpr, CallExpr, NewTypeExpr, ForStmt, WithStmt, CastExpr, TypeVarExpr, TypeApplication, Lvalue, - TupleExpr, RevealTypeExpr, SymbolTableNode, Var, ARG_POS, OverloadedFuncDef + TupleExpr, RevealTypeExpr, SymbolTableNode, Var, ARG_POS, OverloadedFuncDef, DepNode ) from mypy.types import ( Type, Instance, AnyType, TypeOfAny, CallableType, TupleType, TypeVarType, TypedDictType, @@ -64,7 +64,7 @@ def visit_file(self, file_node: MypyFile, fnam: str, options: Options, # This variable tracks the current scope node: a module, a class definition, # or a function. It is used to, e.g., correctly determine target of a fine-grained # dependency. - self.cur_node = file_node # type: Union[MypyFile, FuncItem, ClassDef] + self.cur_node = file_node # type: DepNode self.sem.globals = file_node.names with experiments.strict_optional_set(options.strict_optional): self.accept(file_node) diff --git a/mypy/server/deps.py b/mypy/server/deps.py index b03ce8161c60..2d5ac52715c7 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -92,7 +92,7 @@ class 'mod.Cls'. This can also refer to an attribute inherited from a ComparisonExpr, GeneratorExpr, DictionaryComprehension, StarExpr, PrintStmt, ForStmt, WithStmt, TupleExpr, ListExpr, OperatorAssignmentStmt, DelStmt, YieldFromExpr, Decorator, Block, TypeInfo, FuncBase, OverloadedFuncDef, RefExpr, SuperExpr, Var, NamedTupleExpr, TypedDictExpr, - LDEF, MDEF, GDEF, FuncItem, TypeAliasExpr, + LDEF, MDEF, GDEF, FuncItem, TypeAliasExpr, DepNode, op_methods, reverse_op_methods, ops_with_inplace_method, unary_op_methods ) from mypy.traverser import TraverserVisitor @@ -146,7 +146,7 @@ class DependencyVisitor(TraverserVisitor): def __init__(self, type_map: Dict[Expression, Type], python_version: Tuple[int, int], - alias_deps: 'DefaultDict[Union[MypyFile, FuncItem, ClassDef], Set[str]]') -> None: + alias_deps: 'DefaultDict[DepNode, Set[str]]') -> None: self.scope = Scope() self.type_map = type_map self.python2 = python_version[0] == 2 @@ -287,11 +287,9 @@ def visit_assignment_stmt(self, o: AssignmentStmt) -> None: assert isinstance(lvalue, NameExpr) attr_trigger = make_trigger('%s.%s' % (self.scope.current_full_target(), lvalue.name)) - self.add_type_dependencies(rvalue.analyzed.type, - attr_trigger) - for tvar in rvalue.analyzed.tvars: - self.add_dependency(make_trigger(tvar), target=attr_trigger) - self.add_dependency(attr_trigger) + self.add_type_dependencies(rvalue.analyzed.type, attr_trigger) + for dep in self.alias_deps[o]: + self.add_dependency(make_trigger(dep), target=attr_trigger) else: # Normal assignment super().visit_assignment_stmt(o) @@ -534,7 +532,7 @@ def visit_yield_from_expr(self, e: YieldFromExpr) -> None: # Helpers - def add_type_alias_deps(self, o: Union[MypyFile, FuncItem, ClassDef]) -> None: + def add_type_alias_deps(self, o: DepNode) -> None: """Add dependencies from type aliases to the current target.""" if o in self.alias_deps: for alias in self.alias_deps[o]: diff --git a/test-data/unit/deps-types.test b/test-data/unit/deps-types.test index f12dd8eb3dfd..4458afca3b03 100644 --- a/test-data/unit/deps-types.test +++ b/test-data/unit/deps-types.test @@ -238,12 +238,12 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> m, m.f + -> m.f -> , , m, m.f -> , , m, m.f -> , , m, m.f -[case testAliasDepsGenericClass-skip] +[case testAliasDepsGenericClass] from mod import I, D, S, T A = D[S, T] class C: @@ -357,7 +357,7 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> m + -> , m -> m -> m -> , , , m @@ -378,8 +378,8 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> m, m.f - -> m, m.f + -> , m, m.f + -> m.f -> , , , m, m.f -> , , , m, m.f -> , , , m, m.f @@ -398,7 +398,7 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> m + -> , m -> m -> m.C -> , , , m From 3a4b12b3874f6164b4885697b1625421490cd4a6 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 8 Feb 2018 17:41:21 +0000 Subject: [PATCH 14/42] Fix typos --- mypy/nodes.py | 2 +- mypy/semanal.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/nodes.py b/mypy/nodes.py index 7ec4c35f5e4e..ae949c104301 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -202,7 +202,7 @@ class MypyFile(SymbolNode): # Top-level definitions and statements defs = None # type: List[Statement] # Type alias dependencies as mapping from node to set of alias full names - alias_deps = None # type: DefaultDict[DepNodde, Set[str]] + alias_deps = None # type: DefaultDict[DepNode, Set[str]] # Is there a UTF-8 BOM at the start? is_bom = False names = None # type: SymbolTable diff --git a/mypy/semanal.py b/mypy/semanal.py index d21e56c12fd7..d82bc5f27df5 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1693,7 +1693,7 @@ def anal_type(self, t: Type, *, return typ def add_type_alias_deps(self, aliases_used: Set[str], - node_override: Optional[Union[ClassDef, AssertStmt]] = None) -> None: + node_override: Optional[Union[ClassDef, AssignmentStmt]] = None) -> None: """Add full names of type aliases on which the current node depends. This is used by fine-grained incremental mode to re-check the corresponding nodes. From afa49673dbf6c70b699c10fa2f29da0c2082efe9 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 8 Feb 2018 18:00:05 +0000 Subject: [PATCH 15/42] Add few more dependencies --- mypy/server/deps.py | 4 ++++ test-data/unit/deps-types.test | 30 +++++++++++++++--------------- test-data/unit/fine-grained.test | 2 +- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/mypy/server/deps.py b/mypy/server/deps.py index 2d5ac52715c7..be074e2aa99a 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -537,6 +537,10 @@ def add_type_alias_deps(self, o: DepNode) -> None: if o in self.alias_deps: for alias in self.alias_deps[o]: self.add_dependency(make_trigger(alias)) + if isinstance(o, FuncItem): + self.add_dependency(make_trigger(alias), make_trigger(o.fullname())) + elif isinstance(o, ClassDef): + self.add_dependency(make_trigger(alias), make_trigger(o.fullname)) def add_dependency(self, trigger: str, target: Optional[str] = None) -> None: """Add dependency from trigger to a target. diff --git a/test-data/unit/deps-types.test b/test-data/unit/deps-types.test index 4458afca3b03..24b13f90f778 100644 --- a/test-data/unit/deps-types.test +++ b/test-data/unit/deps-types.test @@ -174,7 +174,7 @@ def f(x: A) -> None: [file mod.py] class I: pass [out] - -> m, m.f + -> , m, m.f -> m -> , , m, m.f @@ -238,7 +238,7 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> m.f + -> , m.f -> , , m, m.f -> , , m, m.f -> , , m, m.f @@ -256,12 +256,12 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> m + -> , m -> m.C -> , , m -> , m -> , , m - -> , m + -> , , m [case testAliasDepsForwardMod] from mod import I @@ -284,7 +284,7 @@ A = I [file mod.py] class I: pass [out] - -> m, m.f + -> , m, m.f -> m -> , , m, m.f @@ -296,7 +296,7 @@ A = I [file mod.py] class I: pass [out] - -> m + -> , m -> m.C -> m -> , , m @@ -324,8 +324,8 @@ def f(x: B) -> None: [file mod.py] class I: pass [out] - -> m, m.f - -> m, m.f + -> , m, m.f + -> , m, m.f -> m -> , , , m, m.f @@ -338,8 +338,8 @@ class C(B): [file mod.py] class I: pass [out] - -> m - -> m + -> , m + -> , m -> m.C -> , m -> , , m, m.C @@ -378,8 +378,8 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> , m, m.f - -> m.f + -> , , m, m.f + -> , m.f -> , , , m, m.f -> , , , m, m.f -> , , , m, m.f @@ -398,8 +398,8 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> , m - -> m + -> , , m + -> , m -> m.C -> , , , m -> , , , m @@ -419,7 +419,7 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> m, m.fun + -> , m, m.fun -> m -> , m, m.fun diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 3cbacd7cfca3..07dfe35ccf27 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -1894,7 +1894,7 @@ x: a.A = {str(): int()} == b.py:2: error: Dict entry 0 has incompatible type "str": "int"; expected "str": "str" -[case testAliasFineGenericFunc-skip] +[case testAliasFineGenericFunc] import b [file a.py] from typing import Dict From d19455a7b47360093c6f5136e2a5dbb08c602e6b Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 8 Feb 2018 19:53:56 +0000 Subject: [PATCH 16/42] Fix aststrip --- mypy/server/aststrip.py | 3 +++ test-data/unit/fine-grained.test | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/mypy/server/aststrip.py b/mypy/server/aststrip.py index e2c2055cdbf2..5ecb7540a4fe 100644 --- a/mypy/server/aststrip.py +++ b/mypy/server/aststrip.py @@ -217,6 +217,9 @@ def is_duplicate_attribute_def(self, node: MemberExpr) -> bool: return any(info.get(node.name) is not None for info in self.type.mro[1:]) def strip_ref_expr(self, node: RefExpr) -> None: + if node.fullname and (node.fullname.startswith('typing') or + node.fullname.startswith('builtins')): + return node.kind = None node.node = None node.fullname = None diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 07dfe35ccf27..834819703a78 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -1974,7 +1974,7 @@ c.x = int() == b.py:5: error: Incompatible types in assignment (expression has type "int", variable has type "str") -[case testAliasFineNestedMod-skip] +[case testAliasFineNestedMod] import b [file a.py] from typing import Dict @@ -1994,7 +1994,7 @@ x: aa.B = {'first': {str(): int()}} == b.py:2: error: Dict entry 0 has incompatible type "str": "int"; expected "str": "str" -[case testAliasFineNestedFunc-skip] +[case testAliasFineNestedFunc] import b [file a.py] from typing import Dict From 398446496d001200b952551c63b1fb75a42806b4 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 8 Feb 2018 20:13:45 +0000 Subject: [PATCH 17/42] Fire module trigger if module symbol table changed --- mypy/server/update.py | 5 ++++- test-data/unit/fine-grained.test | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/mypy/server/update.py b/mypy/server/update.py index b23a8e0c137f..2c9fd5c36c3f 100644 --- a/mypy/server/update.py +++ b/mypy/server/update.py @@ -685,7 +685,10 @@ def calculate_active_triggers(manager: BuildManager, names.add(id) else: snapshot2 = snapshot_symbol_table(id, new.names) - names |= compare_symbol_table_snapshots(id, snapshot1, snapshot2) + diff = compare_symbol_table_snapshots(id, snapshot1, snapshot2) + names |= diff + if diff: + names.add(id) return {make_trigger(name) for name in names} diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 834819703a78..22d70a22fccb 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -2071,7 +2071,7 @@ def f(x: a.A[str]): == b.py:2: error: Bad number of arguments for type alias, expected: 2, given: 1 -[case testAliasFineComponentDeleted-skip] +[case testAliasFineComponentDeleted] import b [file a.py] class B: pass From 224d02d34290a1bae857c4b284b6ae88db701118 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 8 Feb 2018 20:17:30 +0000 Subject: [PATCH 18/42] Fix lint --- mypy/server/aststrip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/server/aststrip.py b/mypy/server/aststrip.py index 5ecb7540a4fe..7730ff89b8ca 100644 --- a/mypy/server/aststrip.py +++ b/mypy/server/aststrip.py @@ -218,7 +218,7 @@ def is_duplicate_attribute_def(self, node: MemberExpr) -> bool: def strip_ref_expr(self, node: RefExpr) -> None: if node.fullname and (node.fullname.startswith('typing') or - node.fullname.startswith('builtins')): + node.fullname.startswith('builtins')): return node.kind = None node.node = None From 108a40dc92fd45dfdc2b836a2286bfc284a46739 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 8 Feb 2018 20:31:34 +0000 Subject: [PATCH 19/42] Add extra fired triger to the test --- 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 22d70a22fccb..5655ef58752c 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -1310,7 +1310,7 @@ class A: def g(self): pass [triggered] -2: , +2: , , [out] == From b3a0488c49068ff206d8de6a26f6c19a60fbcee4 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 8 Feb 2018 20:38:22 +0000 Subject: [PATCH 20/42] Change the order of messages --- test-data/unit/fine-grained-modules.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/fine-grained-modules.test b/test-data/unit/fine-grained-modules.test index 91c7589e6362..7e2bea8a74f4 100644 --- a/test-data/unit/fine-grained-modules.test +++ b/test-data/unit/fine-grained-modules.test @@ -38,8 +38,8 @@ def f(x: int) -> None: pass == a.py:2: error: Incompatible return value type (got "int", expected "str") == -b.py:2: error: Too many arguments for "f" a.py:2: error: Incompatible return value type (got "int", expected "str") +b.py:2: error: Too many arguments for "f" == [case testAddFileFixesError] From ee1fdc9b45cc4358fcbee860d41eb373c3b1390c Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 8 Feb 2018 20:54:11 +0000 Subject: [PATCH 21/42] Revert "Change the order of messages" This reverts commit b3a0488c49068ff206d8de6a26f6c19a60fbcee4. --- test-data/unit/fine-grained-modules.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/fine-grained-modules.test b/test-data/unit/fine-grained-modules.test index 7e2bea8a74f4..91c7589e6362 100644 --- a/test-data/unit/fine-grained-modules.test +++ b/test-data/unit/fine-grained-modules.test @@ -38,8 +38,8 @@ def f(x: int) -> None: pass == a.py:2: error: Incompatible return value type (got "int", expected "str") == -a.py:2: error: Incompatible return value type (got "int", expected "str") b.py:2: error: Too many arguments for "f" +a.py:2: error: Incompatible return value type (got "int", expected "str") == [case testAddFileFixesError] From f31e6af06d6df0a790ebd311866dc8aab6365107 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 9 Feb 2018 14:12:49 +0000 Subject: [PATCH 22/42] Temporary skip a test --- test-data/unit/fine-grained-modules.test | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test-data/unit/fine-grained-modules.test b/test-data/unit/fine-grained-modules.test index 43f13657a6b3..27066af267eb 100644 --- a/test-data/unit/fine-grained-modules.test +++ b/test-data/unit/fine-grained-modules.test @@ -23,7 +23,8 @@ a.f(1) == b.py:2: error: Too many arguments for "f" -[case testAddFileWithErrors] +[case testAddFileWithErrors-skip] +# TODO: fix the order of error message to be the same with and without cache import b [file b.py] [file a.py.2] From edabb050d27d8769659786ce399be73b50bc2a2a Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 9 Feb 2018 14:42:06 +0000 Subject: [PATCH 23/42] Add one more TODO item --- mypy/typeanal.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 2df3f7c892fb..2b37530d7f66 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -176,6 +176,8 @@ def __init__(self, self.third_pass = third_pass # Names of type aliases encountered while analysing a type will be collected here. # (This also recursively includes the names of aliases they depend on.) + # TODO: adding the sub-dependencies shouldn't be necessary since the logic + # of fine-grained should propagate triggered dependencies. self.aliases_used = set() # type: Set[str] def visit_unbound_type(self, t: UnboundType) -> Type: From b3db8e0c3b63fc5b7455607c21c07b20107ef5fd Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 9 Feb 2018 18:25:09 +0000 Subject: [PATCH 24/42] Smaller review comments, still need to move the Scope --- mypy/semanal.py | 7 +++---- mypy/server/deps.py | 2 ++ mypy/server/update.py | 5 +---- mypy/typeanal.py | 5 +++-- test-data/unit/fine-grained-modules.test | 3 +-- test-data/unit/fine-grained.test | 4 ++-- 6 files changed, 12 insertions(+), 14 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index d82bc5f27df5..8ad77fcd32a2 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1815,10 +1815,9 @@ def analyze_alias(self, rvalue: Expression, typ = None # type: Optional[Type] if res: typ, depends_on = res - alias_tvars = [name for (name, node) in - typ.accept(TypeVariableQuery(self.lookup_qualified, self.tvar_scope))] - qualified_tvars = [node.fullname() for (name, node) in - typ.accept(TypeVariableQuery(self.lookup_qualified, self.tvar_scope))] + found_type_vars = typ.accept(TypeVariableQuery(self.lookup_qualified, self.tvar_scope)) + alias_tvars = [name for (name, node) in found_type_vars] + qualified_tvars = [node.fullname() for (name, node) in found_type_vars] depends_on.update(qualified_tvars) else: alias_tvars = [] diff --git a/mypy/server/deps.py b/mypy/server/deps.py index be074e2aa99a..a3fe0811e912 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -287,6 +287,8 @@ def visit_assignment_stmt(self, o: AssignmentStmt) -> None: assert isinstance(lvalue, NameExpr) attr_trigger = make_trigger('%s.%s' % (self.scope.current_full_target(), lvalue.name)) + # We need to re-process the target where alias is defined to "refresh" the alias. + self.add_type_dependencies(rvalue.analyzed.type) self.add_type_dependencies(rvalue.analyzed.type, attr_trigger) for dep in self.alias_deps[o]: self.add_dependency(make_trigger(dep), target=attr_trigger) diff --git a/mypy/server/update.py b/mypy/server/update.py index 13f63f014a47..47caffca2587 100644 --- a/mypy/server/update.py +++ b/mypy/server/update.py @@ -677,10 +677,7 @@ def calculate_active_triggers(manager: BuildManager, names.add(id) else: snapshot2 = snapshot_symbol_table(id, new.names) - diff = compare_symbol_table_snapshots(id, snapshot1, snapshot2) - names |= diff - if diff: - names.add(id) + names |= compare_symbol_table_snapshots(id, snapshot1, snapshot2) return {make_trigger(name) for name in names} diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 2b37530d7f66..38f2e2c1dca7 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -65,9 +65,10 @@ def analyze_type_alias(node: Expression, in_dynamic_func: bool = False, global_scope: bool = True, warn_bound_tvar: bool = False) -> Optional[Tuple[Type, Set[str]]]: - """If `node` is valid as a type alias rvalue, return the resulting type and a set of - full names of type aliases it depends on (directly or indirectly). + """Analyze r.h.s. of a (potential) type alias definition. + If `node` is valid as a type alias rvalue, return the resulting type and a set of + full names of type aliases it depends on (directly or indirectly). Return None otherwise. 'node' must have been semantically analyzed. """ # Quickly return None if the expression doesn't look like a type. Note diff --git a/test-data/unit/fine-grained-modules.test b/test-data/unit/fine-grained-modules.test index 27066af267eb..43f13657a6b3 100644 --- a/test-data/unit/fine-grained-modules.test +++ b/test-data/unit/fine-grained-modules.test @@ -23,8 +23,7 @@ a.f(1) == b.py:2: error: Too many arguments for "f" -[case testAddFileWithErrors-skip] -# TODO: fix the order of error message to be the same with and without cache +[case testAddFileWithErrors] import b [file b.py] [file a.py.2] diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index e2bb9333f793..529873342dcc 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -1330,7 +1330,7 @@ class A: def g(self): pass [triggered] -2: , , +2: , [out] == @@ -2124,4 +2124,4 @@ def f(x: b.B): pass [out] == -c.py:2: error: Name 'b.B' is not defined \ No newline at end of file +c.py:2: error: Name 'b.B' is not defined From 7b2a13cebb5a9d677f51a81d13148678a2117d93 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 9 Feb 2018 20:34:14 +0000 Subject: [PATCH 25/42] Move the Scope; some clean-up needed --- mypy/nodes.py | 7 +--- mypy/scope.py | 79 ++++++++++++++++++++++++++++++++++++ mypy/semanal.py | 30 +++++++------- mypy/semanal_pass3.py | 37 ++++++++--------- mypy/server/deps.py | 94 +++++-------------------------------------- 5 files changed, 120 insertions(+), 127 deletions(-) create mode 100644 mypy/scope.py diff --git a/mypy/nodes.py b/mypy/nodes.py index 51c8a4447ca3..3e1320ba8e8e 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -187,9 +187,6 @@ def deserialize(cls, data: JsonDict) -> 'SymbolNode': raise NotImplementedError('unexpected .class {}'.format(classname)) -DepNode = Union['MypyFile', 'FuncItem', 'ClassDef', 'AssignmentStmt'] - - class MypyFile(SymbolNode): """The abstract syntax tree of a single source file.""" @@ -201,8 +198,8 @@ class MypyFile(SymbolNode): path = '' # Top-level definitions and statements defs = None # type: List[Statement] - # Type alias dependencies as mapping from node to set of alias full names - alias_deps = None # type: DefaultDict[DepNode, Set[str]] + # Type alias dependencies as mapping from target to set of alias full names + alias_deps = None # type: DefaultDict[str, Set[str]] # Is there a UTF-8 BOM at the start? is_bom = False names = None # type: SymbolTable diff --git a/mypy/scope.py b/mypy/scope.py new file mode 100644 index 000000000000..17819be5e095 --- /dev/null +++ b/mypy/scope.py @@ -0,0 +1,79 @@ +"""Track current scope to easily calculate the corresponding fine-grained target. + +This is currently only used by mypy.semanal and mypy.server.deps. +""" + +from typing import List, Optional + +from mypy.nodes import TypeInfo, FuncItem + +class Scope: + """Track which target we are processing at any given time.""" + + def __init__(self) -> None: + self.module = None # type: Optional[str] + self.classes = [] # type: List[TypeInfo] + self.function = None # type: Optional[FuncItem] + # Number of nested scopes ignored (that don't get their own separate targets) + self.ignored = 0 + + def current_module_id(self) -> str: + assert self.module + return self.module + + def current_target(self) -> str: + """Return the current target (non-class; for a class return enclosing module).""" + assert self.module + target = self.module + if self.function: + if self.classes: + target += '.' + '.'.join(c.name() for c in self.classes) + target += '.' + self.function.name() + return target + + def current_full_target(self) -> str: + """Return the current target (may be a class).""" + assert self.module + target = self.module + if self.classes: + target += '.' + '.'.join(c.name() for c in self.classes) + if self.function: + target += '.' + self.function.name() + return target + + def enter_file(self, prefix: str) -> None: + self.module = prefix + self.classes = [] + self.function = None + self.ignored = 0 + + def enter_function(self, fdef: FuncItem) -> None: + if not self.function: + self.function = fdef + else: + # Nested functions are part of the topmost function target. + self.ignored += 1 + + def enter_class(self, info: TypeInfo) -> None: + """Enter a class target scope.""" + if not self.function: + self.classes.append(info) + else: + # Classes within functions are part of the enclosing function target. + self.ignored += 1 + + def leave(self) -> None: + """Leave the innermost scope (can be any kind of scope).""" + if self.ignored: + # Leave a scope that's included in the enclosing target. + self.ignored -= 1 + elif self.function: + # Function is always the innermost target. + self.function = None + elif self.classes: + # Leave the innermost class. + self.classes.pop() + else: + # Leave module. + assert self.module + self.module = None diff --git a/mypy/semanal.py b/mypy/semanal.py index 8ad77fcd32a2..1fc2d01d792c 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -85,6 +85,7 @@ from mypy import join from mypy.util import get_prefix, correct_relative_import from mypy.semanal_shared import PRIORITY_FALLBACKS +from mypy.scope import Scope T = TypeVar('T') @@ -255,6 +256,7 @@ def __init__(self, # If True, process function definitions. If False, don't. This is used # for processing module top levels in fine-grained incremental mode. self.recurse_into_functions = True + self.scope = Scope() def visit_file(self, file_node: MypyFile, fnam: str, options: Options, patches: List[Tuple[int, Callable[[], None]]]) -> None: @@ -287,8 +289,10 @@ def visit_file(self, file_node: MypyFile, fnam: str, options: Options, v.is_ready = True defs = file_node.defs + self.scope.enter_file(file_node.fullname()) for d in defs: self.accept(d) + self.scope.leave() if self.cur_mod_id == 'builtins': remove_imported_names_from_symtable(self.globals, 'builtins') @@ -305,11 +309,13 @@ def visit_file(self, file_node: MypyFile, fnam: str, options: Options, def refresh_partial(self, node: Union[MypyFile, FuncItem, OverloadedFuncDef]) -> None: """Refresh a stale target in fine-grained incremental mode.""" + self.scope.enter_file(self.cur_mod_id) if isinstance(node, MypyFile): self.refresh_top_level(node) else: self.recurse_into_functions = True self.accept(node) + self.scope.leave() def refresh_top_level(self, file_node: MypyFile) -> None: """Reanalyze a stale module top-level in fine-grained incremental mode.""" @@ -591,6 +597,7 @@ def analyze_property_with_multi_part_definition(self, defn: OverloadedFuncDef) - def analyze_function(self, defn: FuncItem) -> None: is_method = self.is_class_scope() + self.scope.enter_function(defn) with self.tvar_scope_frame(self.tvar_scope.method_frame()): if defn.type: self.check_classvar_in_signature(defn.type) @@ -599,8 +606,7 @@ def analyze_function(self, defn: FuncItem) -> None: # class-level imported names and type variables are in scope. analyzer = self.type_analyzer() defn.type = analyzer.visit_callable_type(defn.type, nested=False) - if analyzer.aliases_used: - self.cur_mod_node.alias_deps[defn].update(analyzer.aliases_used) + self.add_type_alias_deps(analyzer.aliases_used) self.check_function_signature(defn) if isinstance(defn, FuncDef): assert isinstance(defn.type, CallableType) @@ -637,6 +643,7 @@ def analyze_function(self, defn: FuncItem) -> None: self.leave() self.function_stack.pop() + self.scope.leave() def check_classvar_in_signature(self, typ: Type) -> None: if isinstance(typ, Overloaded): @@ -664,10 +671,12 @@ def check_function_signature(self, fdef: FuncItem) -> None: self.fail('Type signature has too many arguments', fdef, blocker=True) def visit_class_def(self, defn: ClassDef) -> None: + self.scope.enter_class(defn.info) with self.analyze_class_body(defn) as should_continue: if should_continue: # Analyze class body. defn.defs.accept(self) + self.scope.leave() @contextmanager def analyze_class_body(self, defn: ClassDef) -> Iterator[bool]: @@ -1689,11 +1698,10 @@ def anal_type(self, t: Type, *, allow_tuple_literal=allow_tuple_literal, third_pass=third_pass) typ = t.accept(a) - self.add_type_alias_deps(a.aliases_used, source_classdef) + self.add_type_alias_deps(a.aliases_used) return typ - def add_type_alias_deps(self, aliases_used: Set[str], - node_override: Optional[Union[ClassDef, AssignmentStmt]] = None) -> None: + def add_type_alias_deps(self, aliases_used: Set[str]) -> None: """Add full names of type aliases on which the current node depends. This is used by fine-grained incremental mode to re-check the corresponding nodes. @@ -1703,16 +1711,7 @@ def add_type_alias_deps(self, aliases_used: Set[str], # A basic optimization to avoid adding targets with no dependencies to # the `alias_deps` dict. return - if node_override: - self.cur_mod_node.alias_deps[node_override].update(aliases_used) - return - if self.is_class_scope(): - assert self.type is not None, "Type not set at class scope" - self.cur_mod_node.alias_deps[self.type.defn].update(aliases_used) - elif self.is_func_scope(): - self.cur_mod_node.alias_deps[self.function_stack[-1]].update(aliases_used) - else: - self.cur_mod_node.alias_deps[self.cur_mod_node].update(aliases_used) + self.cur_mod_node.alias_deps[self.scope.current_full_target()].update(aliases_used) def visit_assignment_stmt(self, s: AssignmentStmt) -> None: for lval in s.lvalues: @@ -1862,7 +1861,6 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None: # of alias to what it depends on. node.alias_depends_on.add(lvalue.fullname) self.add_type_alias_deps(depends_on) - self.add_type_alias_deps(depends_on, node_override=s) if not lvalue.is_inferred_def: # Type aliases can't be re-defined. if node and (node.kind == TYPE_ALIAS or isinstance(node.node, TypeInfo)): diff --git a/mypy/semanal_pass3.py b/mypy/semanal_pass3.py index ded81482e06e..8f940e08777a 100644 --- a/mypy/semanal_pass3.py +++ b/mypy/semanal_pass3.py @@ -18,7 +18,7 @@ Node, Expression, MypyFile, FuncDef, FuncItem, Decorator, RefExpr, Context, TypeInfo, ClassDef, Block, TypedDictExpr, NamedTupleExpr, AssignmentStmt, IndexExpr, TypeAliasExpr, NameExpr, CallExpr, NewTypeExpr, ForStmt, WithStmt, CastExpr, TypeVarExpr, TypeApplication, Lvalue, - TupleExpr, RevealTypeExpr, SymbolTableNode, Var, ARG_POS, OverloadedFuncDef, DepNode + TupleExpr, RevealTypeExpr, SymbolTableNode, Var, ARG_POS, OverloadedFuncDef ) from mypy.types import ( Type, Instance, AnyType, TypeOfAny, CallableType, TupleType, TypeVarType, TypedDictType, @@ -32,6 +32,7 @@ from mypy.semanal_shared import PRIORITY_FORWARD_REF, PRIORITY_TYPEVAR_VALUES from mypy.subtypes import is_subtype from mypy.sametypes import is_same_type +from mypy.scope import Scope import mypy.semanal @@ -47,6 +48,7 @@ def __init__(self, modules: Dict[str, MypyFile], errors: Errors, self.modules = modules self.errors = errors self.sem = sem + self.scope = Scope() # If True, process function definitions. If False, don't. This is used # for processing module top levels in fine-grained incremental mode. self.recurse_into_functions = True @@ -61,31 +63,22 @@ def visit_file(self, file_node: MypyFile, fnam: str, options: Options, self.is_typeshed_file = self.errors.is_typeshed_file(fnam) self.sem.cur_mod_id = file_node.fullname() self.cur_mod_node = file_node - # This variable tracks the current scope node: a module, a class definition, - # or a function. It is used to, e.g., correctly determine target of a fine-grained - # dependency. - self.cur_node = file_node # type: DepNode self.sem.globals = file_node.names with experiments.strict_optional_set(options.strict_optional): + self.scope.enter_file(file_node.fullname()) self.accept(file_node) - - @contextmanager - def enter_scope(self, node: Union[MypyFile, ClassDef, FuncItem]) -> Iterator[None]: - old_node = self.cur_node - self.cur_node = node - try: - yield - finally: - self.cur_node = old_node + self.scope.leave() def refresh_partial(self, node: Union[MypyFile, FuncItem, OverloadedFuncDef]) -> None: """Refresh a stale target in fine-grained incremental mode.""" + self.scope.enter_file(self.sem.cur_mod_id) if isinstance(node, MypyFile): self.recurse_into_functions = False self.refresh_top_level(node) else: self.recurse_into_functions = True self.accept(node) + self.scope.leave() def refresh_top_level(self, file_node: MypyFile) -> None: """Reanalyze a stale module top-level in fine-grained incremental mode.""" @@ -106,11 +99,12 @@ def visit_block(self, b: Block) -> None: def visit_func_def(self, fdef: FuncDef) -> None: if not self.recurse_into_functions: return + self.scope.enter_function(fdef) self.errors.push_function(fdef.name()) - with self.enter_scope(fdef): - self.analyze(fdef.type, fdef) - super().visit_func_def(fdef) + self.analyze(fdef.type, fdef) + super().visit_func_def(fdef) self.errors.pop_function() + self.scope.leave() def visit_overloaded_func_def(self, fdef: OverloadedFuncDef) -> None: if not self.recurse_into_functions: @@ -121,6 +115,7 @@ def visit_overloaded_func_def(self, fdef: OverloadedFuncDef) -> None: def visit_class_def(self, tdef: ClassDef) -> None: # NamedTuple base classes are validated in check_namedtuple_classdef; we don't have to # check them again here. + self.scope.enter_class(tdef.info) if not tdef.info.is_named_tuple: types = list(tdef.info.bases) # type: List[Type] for tvar in tdef.type_vars: @@ -150,8 +145,8 @@ def visit_class_def(self, tdef: ClassDef) -> None: elif isinstance(tdef.analyzed, NamedTupleExpr): self.analyze(tdef.analyzed.info.tuple_type, tdef.analyzed, warn=True) self.analyze_info(tdef.analyzed.info) - with self.enter_scope(tdef): - super().visit_class_def(tdef) + super().visit_class_def(tdef) + self.scope.leave() def visit_decorator(self, dec: Decorator) -> None: """Try to infer the type of the decorated function. @@ -371,7 +366,7 @@ def analyze(self, type: Optional[Type], node: Union[Node, SymbolTableNode], self.check_for_omitted_generics(type) self.generate_type_patches(node, indicator, warn) if analyzer.aliases_used: - self.cur_mod_node.alias_deps[self.cur_node].update(analyzer.aliases_used) + self.cur_mod_node.alias_deps[self.scope.current_full_target()].update(analyzer.aliases_used) def analyze_types(self, types: List[Type], node: Node) -> None: # Similar to above but for nodes with multiple types. @@ -381,7 +376,7 @@ def analyze_types(self, types: List[Type], node: Node) -> None: type.accept(analyzer) self.check_for_omitted_generics(type) if analyzer.aliases_used: - self.cur_mod_node.alias_deps[self.cur_node].update(analyzer.aliases_used) + self.cur_mod_node.alias_deps[self.scope.current_full_target()].update(analyzer.aliases_used) self.generate_type_patches(node, indicator, warn=False) def generate_type_patches(self, diff --git a/mypy/server/deps.py b/mypy/server/deps.py index a3fe0811e912..2502c82324a7 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -92,7 +92,7 @@ class 'mod.Cls'. This can also refer to an attribute inherited from a ComparisonExpr, GeneratorExpr, DictionaryComprehension, StarExpr, PrintStmt, ForStmt, WithStmt, TupleExpr, ListExpr, OperatorAssignmentStmt, DelStmt, YieldFromExpr, Decorator, Block, TypeInfo, FuncBase, OverloadedFuncDef, RefExpr, SuperExpr, Var, NamedTupleExpr, TypedDictExpr, - LDEF, MDEF, GDEF, FuncItem, TypeAliasExpr, DepNode, + LDEF, MDEF, GDEF, FuncItem, TypeAliasExpr, op_methods, reverse_op_methods, ops_with_inplace_method, unary_op_methods ) from mypy.traverser import TraverserVisitor @@ -103,6 +103,7 @@ class 'mod.Cls'. This can also refer to an attribute inherited from a ) from mypy.server.trigger import make_trigger from mypy.util import correct_relative_import +from mypy.scope import Scope def get_dependencies(target: MypyFile, @@ -146,7 +147,7 @@ class DependencyVisitor(TraverserVisitor): def __init__(self, type_map: Dict[Expression, Type], python_version: Tuple[int, int], - alias_deps: 'DefaultDict[DepNode, Set[str]]') -> None: + alias_deps: 'DefaultDict[str, Set[str]]') -> None: self.scope = Scope() self.type_map = type_map self.python2 = python_version[0] == 2 @@ -175,7 +176,7 @@ def __init__(self, def visit_mypy_file(self, o: MypyFile) -> None: self.scope.enter_file(o.fullname()) self.is_package_init_file = o.is_package_init_file() - self.add_type_alias_deps(o) + self.add_type_alias_deps(self.scope.current_full_target()) super().visit_mypy_file(o) self.scope.leave() @@ -193,7 +194,7 @@ def visit_func_def(self, o: FuncDef) -> None: if o.info: for base in non_trivial_bases(o.info): self.add_dependency(make_trigger(base.fullname() + '.' + o.name())) - self.add_type_alias_deps(o) + self.add_type_alias_deps(self.scope.current_full_target()) super().visit_func_def(o) self.scope.leave() @@ -219,7 +220,7 @@ def visit_class_def(self, o: ClassDef) -> None: 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.add_type_alias_deps(o) + self.add_type_alias_deps(self.scope.current_full_target()) self.is_class = old_is_class info = o.info for name, node in info.names.items(): @@ -534,15 +535,10 @@ def visit_yield_from_expr(self, e: YieldFromExpr) -> None: # Helpers - def add_type_alias_deps(self, o: DepNode) -> None: - """Add dependencies from type aliases to the current target.""" - if o in self.alias_deps: - for alias in self.alias_deps[o]: + def add_type_alias_deps(self, target): + if target in self.alias_deps: + for alias in self.alias_deps[target]: self.add_dependency(make_trigger(alias)) - if isinstance(o, FuncItem): - self.add_dependency(make_trigger(alias), make_trigger(o.fullname())) - elif isinstance(o, ClassDef): - self.add_dependency(make_trigger(alias), make_trigger(o.fullname)) def add_dependency(self, trigger: str, target: Optional[str] = None) -> None: """Add dependency from trigger to a target. @@ -611,78 +607,6 @@ def add_iter_dependency(self, node: Expression) -> None: self.add_attribute_dependency(typ, '__iter__') -class Scope: - """Track which target we are processing at any given time.""" - - def __init__(self) -> None: - self.module = None # type: Optional[str] - self.classes = [] # type: List[TypeInfo] - self.function = None # type: Optional[FuncDef] - # Number of nested scopes ignored (that don't get their own separate targets) - self.ignored = 0 - - def current_module_id(self) -> str: - assert self.module - return self.module - - def current_target(self) -> str: - """Return the current target (non-class; for a class return enclosing module).""" - assert self.module - target = self.module - if self.function: - if self.classes: - target += '.' + '.'.join(c.name() for c in self.classes) - target += '.' + self.function.name() - return target - - def current_full_target(self) -> str: - """Return the current target (may be a class).""" - assert self.module - target = self.module - if self.classes: - target += '.' + '.'.join(c.name() for c in self.classes) - if self.function: - target += '.' + self.function.name() - return target - - def enter_file(self, prefix: str) -> None: - self.module = prefix - self.classes = [] - self.function = None - self.ignored = 0 - - def enter_function(self, fdef: FuncDef) -> None: - if not self.function: - self.function = fdef - else: - # Nested functions are part of the topmost function target. - self.ignored += 1 - - def enter_class(self, info: TypeInfo) -> None: - """Enter a class target scope.""" - if not self.function: - self.classes.append(info) - else: - # Classes within functions are part of the enclosing function target. - self.ignored += 1 - - def leave(self) -> None: - """Leave the innermost scope (can be any kind of scope).""" - if self.ignored: - # Leave a scope that's included in the enclosing target. - self.ignored -= 1 - elif self.function: - # Function is always the innermost target. - self.function = None - elif self.classes: - # Leave the innermost class. - self.classes.pop() - else: - # Leave module. - assert self.module - self.module = None - - def get_type_triggers(typ: Type) -> List[str]: """Return all triggers that correspond to a type becoming stale.""" return typ.accept(TypeTriggersVisitor()) From d1e0d07d671166d8eecd1c64f9e7eefd81f61c81 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 9 Feb 2018 22:12:26 +0000 Subject: [PATCH 26/42] Cleanup some logic to match normal (non-aliases) types --- mypy/semanal.py | 31 +++++++++++++++---------------- mypy/server/deps.py | 12 ++++++++---- test-data/unit/deps-types.test | 18 +++++++++--------- 3 files changed, 32 insertions(+), 29 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 1fc2d01d792c..633d2d1fa442 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1079,7 +1079,7 @@ def analyze_base_classes(self, defn: ClassDef) -> None: for base_expr in defn.base_type_exprs: try: - base = self.expr_to_analyzed_type(base_expr, source_classdef=defn) + base = self.expr_to_analyzed_type(base_expr) except TypeTranslationError: self.fail('Invalid base class', base_expr) info.fallback_to_any = True @@ -1191,11 +1191,7 @@ def update_metaclass(self, defn: ClassDef) -> None: return defn.metaclass = metas.pop() - def expr_to_analyzed_type(self, expr: Expression, *, - source_classdef: Optional[ClassDef] = None) -> Type: - # If this function is called to analyze a base in class definition, then - # the corresponding ClassDef, should ber passed in `source_classdef`. - # This is used to determine correct target for a fine-grained dependency. + def expr_to_analyzed_type(self, expr: Expression) -> Type: if isinstance(expr, CallExpr): expr.accept(self) info = self.check_namedtuple(expr) @@ -1207,7 +1203,7 @@ def expr_to_analyzed_type(self, expr: Expression, *, fallback = Instance(info, []) return TupleType(info.tuple_type.items, fallback=fallback) typ = expr_to_unanalyzed_type(expr) - return self.anal_type(typ, source_classdef=source_classdef) + return self.anal_type(typ) def verify_base_classes(self, defn: ClassDef) -> bool: info = defn.info @@ -1691,8 +1687,7 @@ def anal_type(self, t: Type, *, tvar_scope: Optional[TypeVarScope] = None, allow_tuple_literal: bool = False, aliasing: bool = False, - third_pass: bool = False, - source_classdef: Optional[ClassDef] = None) -> Type: + third_pass: bool = False) -> Type: a = self.type_analyzer(tvar_scope=tvar_scope, aliasing=aliasing, allow_tuple_literal=allow_tuple_literal, @@ -1701,7 +1696,7 @@ def anal_type(self, t: Type, *, self.add_type_alias_deps(a.aliases_used) return typ - def add_type_alias_deps(self, aliases_used: Set[str]) -> None: + def add_type_alias_deps(self, aliases_used: Set[str], target: Optional[str] = None) -> None: """Add full names of type aliases on which the current node depends. This is used by fine-grained incremental mode to re-check the corresponding nodes. @@ -1711,7 +1706,9 @@ def add_type_alias_deps(self, aliases_used: Set[str]) -> None: # A basic optimization to avoid adding targets with no dependencies to # the `alias_deps` dict. return - self.cur_mod_node.alias_deps[self.scope.current_full_target()].update(aliases_used) + if target is None: + target = self.scope.current_full_target() + self.cur_mod_node.alias_deps[target].update(aliases_used) def visit_assignment_stmt(self, s: AssignmentStmt) -> None: for lval in s.lvalues: @@ -1787,7 +1784,8 @@ def alias_fallback(self, tp: Type) -> Instance: return Instance(fb_info, []) def analyze_alias(self, rvalue: Expression, - warn_bound_tvar: bool = False) -> Tuple[Optional[Type], List[str], Set[str]]: + warn_bound_tvar: bool = False) -> Tuple[Optional[Type], List[str], + Set[str], List[str]]: """Check if 'rvalue' represents a valid type allowed for aliasing (e.g. not a type variable). If yes, return the corresponding type, a list of qualified type variable names for generic aliases, and a set of names the alias depends on. @@ -1817,11 +1815,11 @@ def analyze_alias(self, rvalue: Expression, found_type_vars = typ.accept(TypeVariableQuery(self.lookup_qualified, self.tvar_scope)) alias_tvars = [name for (name, node) in found_type_vars] qualified_tvars = [node.fullname() for (name, node) in found_type_vars] - depends_on.update(qualified_tvars) else: alias_tvars = [] depends_on = set() - return typ, alias_tvars, depends_on + qualified_tvars = [] + return typ, alias_tvars, depends_on, qualified_tvars def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None: """Check if assignment creates a type alias and set it up as needed. @@ -1850,7 +1848,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None: # annotations (see the second rule). return rvalue = s.rvalue - res, alias_tvars, depends_on = self.analyze_alias(rvalue, warn_bound_tvar=True) + res, alias_tvars, depends_on, qualified_tvars = self.analyze_alias(rvalue, warn_bound_tvar=True) if not res: return node = self.lookup(lvalue.name, lvalue) @@ -1861,6 +1859,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None: # of alias to what it depends on. node.alias_depends_on.add(lvalue.fullname) self.add_type_alias_deps(depends_on) + self.add_type_alias_deps(qualified_tvars, target='<%s>' % lvalue.fullname) if not lvalue.is_inferred_def: # Type aliases can't be re-defined. if node and (node.kind == TYPE_ALIAS or isinstance(node.node, TypeInfo)): @@ -3487,7 +3486,7 @@ def visit_index_expr(self, expr: IndexExpr) -> None: elif isinstance(expr.base, RefExpr) and expr.base.kind == TYPE_ALIAS: # Special form -- subscripting a generic type alias. # Perform the type substitution and create a new alias. - res, alias_tvars, depends_on = self.analyze_alias(expr) + res, alias_tvars, depends_on, _ = self.analyze_alias(expr) assert res is not None, "Failed analyzing already defined alias" expr.analyzed = TypeAliasExpr(res, alias_tvars, fallback=self.alias_fallback(res), in_runtime=True) diff --git a/mypy/server/deps.py b/mypy/server/deps.py index 2502c82324a7..3c29136e54ec 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -189,12 +189,13 @@ def visit_func_def(self, o: FuncDef) -> None: else: signature = o.type for trigger in get_type_triggers(signature): + # TODO: avoid adding both `mod.func` and `` here and below for aliases self.add_dependency(trigger) self.add_dependency(trigger, target=make_trigger(target)) if o.info: for base in non_trivial_bases(o.info): self.add_dependency(make_trigger(base.fullname() + '.' + o.name())) - self.add_type_alias_deps(self.scope.current_full_target()) + self.add_type_alias_deps(self.scope.current_full_target(), add_trigger=True) super().visit_func_def(o) self.scope.leave() @@ -291,8 +292,9 @@ def visit_assignment_stmt(self, o: AssignmentStmt) -> None: # We need to re-process the target where alias is defined to "refresh" the alias. self.add_type_dependencies(rvalue.analyzed.type) self.add_type_dependencies(rvalue.analyzed.type, attr_trigger) - for dep in self.alias_deps[o]: - self.add_dependency(make_trigger(dep), target=attr_trigger) + if attr_trigger in self.alias_deps: + for dep in self.alias_deps[attr_trigger]: + self.add_dependency(make_trigger(dep), target=attr_trigger) else: # Normal assignment super().visit_assignment_stmt(o) @@ -535,10 +537,12 @@ def visit_yield_from_expr(self, e: YieldFromExpr) -> None: # Helpers - def add_type_alias_deps(self, target): + def add_type_alias_deps(self, target: str, add_trigger: bool = False): if target in self.alias_deps: for alias in self.alias_deps[target]: self.add_dependency(make_trigger(alias)) + if add_trigger: + self.add_dependency(make_trigger(alias), make_trigger(target)) def add_dependency(self, trigger: str, target: Optional[str] = None) -> None: """Add dependency from trigger to a target. diff --git a/test-data/unit/deps-types.test b/test-data/unit/deps-types.test index 24b13f90f778..870032b38f94 100644 --- a/test-data/unit/deps-types.test +++ b/test-data/unit/deps-types.test @@ -256,12 +256,12 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> , m + -> m -> m.C -> , , m -> , m -> , , m - -> , , m + -> , m [case testAliasDepsForwardMod] from mod import I @@ -296,7 +296,7 @@ A = I [file mod.py] class I: pass [out] - -> , m + -> m -> m.C -> m -> , , m @@ -338,8 +338,8 @@ class C(B): [file mod.py] class I: pass [out] - -> , m - -> , m + -> m + -> m -> m.C -> , m -> , , m, m.C @@ -357,7 +357,7 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> , m + -> m -> m -> m -> , , , m @@ -378,7 +378,7 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> , , m, m.f + -> , m, m.f -> , m.f -> , , , m, m.f -> , , , m, m.f @@ -398,8 +398,8 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> , , m - -> , m + -> m + -> m -> m.C -> , , , m -> , , , m From 2803764192e0eb73aa72747269a82046b757abfc Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 9 Feb 2018 22:32:26 +0000 Subject: [PATCH 27/42] More clean-up: docstrings, comments. Also added test for class in function --- mypy/nodes.py | 4 +--- mypy/semanal.py | 9 +++++---- test-data/unit/deps-types.test | 14 ++++++++++++++ test-data/unit/fine-grained.test | 15 +++++++++++++++ 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/mypy/nodes.py b/mypy/nodes.py index 3e1320ba8e8e..f1851c2fd921 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -2344,9 +2344,7 @@ class SymbolTableNode: # Is this node refers to other node via node aliasing? # (This is currently used for simple aliases like `A = int` instead of .type_override) is_aliasing = False # type: bool - # This includes full names of aliases used in this alias, and full names of - # type variables used to define it, if it is generic. - # TODO: refactor to move type variables together with alias_tvars (currently unqualified). + # This includes full names of aliases used in this alias. alias_depends_on = None # type: Set[str] def __init__(self, diff --git a/mypy/semanal.py b/mypy/semanal.py index 633d2d1fa442..19895323614b 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1696,11 +1696,11 @@ def anal_type(self, t: Type, *, self.add_type_alias_deps(a.aliases_used) return typ - def add_type_alias_deps(self, aliases_used: Set[str], target: Optional[str] = None) -> None: + def add_type_alias_deps(self, aliases_used: Iterable[str], target: Optional[str] = None) -> None: """Add full names of type aliases on which the current node depends. This is used by fine-grained incremental mode to re-check the corresponding nodes. - If `node_override` is None, then the target node used will be the current scope. + If `target` is None, then the target node used will be the current scope. """ if not aliases_used: # A basic optimization to avoid adding targets with no dependencies to @@ -1788,8 +1788,9 @@ def analyze_alias(self, rvalue: Expression, Set[str], List[str]]: """Check if 'rvalue' represents a valid type allowed for aliasing (e.g. not a type variable). If yes, return the corresponding type, a list of - qualified type variable names for generic aliases, and a set of names the alias depends on. - An schematic example for the latter: + qualified type variable names for generic aliases, a set of names the alias depends on, + and a list of type variables if the alias is generic. + An schematic example for the dependencies: A = int B = str analyze_alias(Dict[A, B])[2] == {'__main__.A', '__main__.B'} diff --git a/test-data/unit/deps-types.test b/test-data/unit/deps-types.test index 870032b38f94..e7dd70ff77e1 100644 --- a/test-data/unit/deps-types.test +++ b/test-data/unit/deps-types.test @@ -472,3 +472,17 @@ class I: pass -> m -> , , m, m.P -> m + +[case testAliasDepsClassInFunction] +from mod import I +A = I +def f() -> None: + class C: + x: A +[file mod.py] +class I: pass +[out] + -> , m, m.f + -> m.f + -> m + -> , , m, m.f diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 529873342dcc..345a69c69ac4 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -2125,3 +2125,18 @@ def f(x: b.B): [out] == c.py:2: error: Name 'b.B' is not defined + +[case testAliasFineClassInFunction] +import b +[file a.py] +A = int +[file a.py.2] +A = str +[file b.py] +import a +def f() -> None: + class C: + x: a.A = int() +[out] +== +b.py:4: error: Incompatible types in assignment (expression has type "int", variable has type "str") From 9ad014cd7f0cbccf1bb629f1654e79a8d573f8d1 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 9 Feb 2018 22:47:07 +0000 Subject: [PATCH 28/42] Fix lint --- mypy/scope.py | 1 + mypy/semanal.py | 6 ++++-- mypy/semanal_pass3.py | 6 ++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/mypy/scope.py b/mypy/scope.py index 17819be5e095..48fabcb2fc60 100644 --- a/mypy/scope.py +++ b/mypy/scope.py @@ -7,6 +7,7 @@ from mypy.nodes import TypeInfo, FuncItem + class Scope: """Track which target we are processing at any given time.""" diff --git a/mypy/semanal.py b/mypy/semanal.py index 19895323614b..6a050ae714ed 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1696,7 +1696,8 @@ def anal_type(self, t: Type, *, self.add_type_alias_deps(a.aliases_used) return typ - def add_type_alias_deps(self, aliases_used: Iterable[str], target: Optional[str] = None) -> None: + def add_type_alias_deps(self, aliases_used: Iterable[str], + target: Optional[str] = None) -> None: """Add full names of type aliases on which the current node depends. This is used by fine-grained incremental mode to re-check the corresponding nodes. @@ -1849,7 +1850,8 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None: # annotations (see the second rule). return rvalue = s.rvalue - res, alias_tvars, depends_on, qualified_tvars = self.analyze_alias(rvalue, warn_bound_tvar=True) + res, alias_tvars, depends_on, qualified_tvars = self.analyze_alias(rvalue, + warn_bound_tvar=True) if not res: return node = self.lookup(lvalue.name, lvalue) diff --git a/mypy/semanal_pass3.py b/mypy/semanal_pass3.py index 8f940e08777a..ac333737c0b9 100644 --- a/mypy/semanal_pass3.py +++ b/mypy/semanal_pass3.py @@ -366,7 +366,8 @@ def analyze(self, type: Optional[Type], node: Union[Node, SymbolTableNode], self.check_for_omitted_generics(type) self.generate_type_patches(node, indicator, warn) if analyzer.aliases_used: - self.cur_mod_node.alias_deps[self.scope.current_full_target()].update(analyzer.aliases_used) + target = self.scope.current_full_target() + self.cur_mod_node.alias_deps[target].update(analyzer.aliases_used) def analyze_types(self, types: List[Type], node: Node) -> None: # Similar to above but for nodes with multiple types. @@ -376,7 +377,8 @@ def analyze_types(self, types: List[Type], node: Node) -> None: type.accept(analyzer) self.check_for_omitted_generics(type) if analyzer.aliases_used: - self.cur_mod_node.alias_deps[self.scope.current_full_target()].update(analyzer.aliases_used) + target = self.scope.current_full_target() + self.cur_mod_node.alias_deps[target].update(analyzer.aliases_used) self.generate_type_patches(node, indicator, warn=False) def generate_type_patches(self, From 6e1cc7af0de39d4d2c5c98df26053bad521c5792 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 9 Feb 2018 23:01:11 +0000 Subject: [PATCH 29/42] Add missing annotation --- mypy/server/deps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/server/deps.py b/mypy/server/deps.py index 3c29136e54ec..63f39a404f6c 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -537,7 +537,7 @@ def visit_yield_from_expr(self, e: YieldFromExpr) -> None: # Helpers - def add_type_alias_deps(self, target: str, add_trigger: bool = False): + def add_type_alias_deps(self, target: str, add_trigger: bool = False) -> None: if target in self.alias_deps: for alias in self.alias_deps[target]: self.add_dependency(make_trigger(alias)) From 3b9b19ecdd43cc14dda8938bed4ff0e1708e5ef5 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 19 Feb 2018 23:02:49 +0000 Subject: [PATCH 30/42] Avoid long-range (indirect) dependencies for aliases --- mypy/nodes.py | 6 +++--- mypy/scope.py | 2 +- mypy/semanal.py | 24 +++++++++++++++++------- mypy/server/deps.py | 26 ++++++++++++++++---------- mypy/typeanal.py | 9 ++++----- test-data/unit/deps-types.test | 30 +++++++++--------------------- 6 files changed, 50 insertions(+), 47 deletions(-) diff --git a/mypy/nodes.py b/mypy/nodes.py index f1851c2fd921..4e17111bdfeb 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -804,6 +804,8 @@ class AssignmentStmt(Statement): unanalyzed_type = None # type: Optional[mypy.types.Type] # This indicates usage of PEP 526 type annotation syntax in assignment. new_syntax = False # type: bool + # Does this assignment define a type alias? + is_alias_def = False def __init__(self, lvalues: List[Lvalue], rvalue: Expression, type: 'Optional[mypy.types.Type]' = None, new_syntax: bool = False) -> None: @@ -2344,8 +2346,7 @@ class SymbolTableNode: # Is this node refers to other node via node aliasing? # (This is currently used for simple aliases like `A = int` instead of .type_override) is_aliasing = False # type: bool - # This includes full names of aliases used in this alias. - alias_depends_on = None # type: Set[str] + alias_name = None # type: Optional[str] def __init__(self, kind: int, @@ -2364,7 +2365,6 @@ def __init__(self, self.alias_tvars = alias_tvars self.implicit = implicit self.module_hidden = module_hidden - self.alias_depends_on = set() @property def fullname(self) -> Optional[str]: diff --git a/mypy/scope.py b/mypy/scope.py index 48fabcb2fc60..0242f06a621d 100644 --- a/mypy/scope.py +++ b/mypy/scope.py @@ -1,6 +1,6 @@ """Track current scope to easily calculate the corresponding fine-grained target. -This is currently only used by mypy.semanal and mypy.server.deps. +TODO: Use everywhere where we track targets, including in mypy.errors. """ from typing import List, Optional diff --git a/mypy/semanal.py b/mypy/semanal.py index 6a050ae714ed..c1426f4fd9b2 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1787,8 +1787,9 @@ def alias_fallback(self, tp: Type) -> Instance: def analyze_alias(self, rvalue: Expression, warn_bound_tvar: bool = False) -> Tuple[Optional[Type], List[str], Set[str], List[str]]: - """Check if 'rvalue' represents a valid type allowed for aliasing - (e.g. not a type variable). If yes, return the corresponding type, a list of + """Check if 'rvalue' is a valid type allowed for aliasing (e.g. not a type variable). + + If yes, return the corresponding type, a list of qualified type variable names for generic aliases, a set of names the alias depends on, and a list of type variables if the alias is generic. An schematic example for the dependencies: @@ -1825,6 +1826,7 @@ def analyze_alias(self, rvalue: Expression, def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None: """Check if assignment creates a type alias and set it up as needed. + For simple aliases like L = List we use a simpler mechanism, just copying TypeInfo. For subscripted (including generic) aliases the resulting types are stored in rvalue.analyzed. @@ -1854,15 +1856,17 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None: warn_bound_tvar=True) if not res: return + s.is_alias_def = True node = self.lookup(lvalue.name, lvalue) assert node is not None - node.alias_depends_on = depends_on.copy() if lvalue.fullname is not None: - # To avoid extra attributes on SymbolTableNode we add the fullname - # of alias to what it depends on. - node.alias_depends_on.add(lvalue.fullname) - self.add_type_alias_deps(depends_on) + node.alias_name = lvalue.fullname + self.add_type_alias_deps({lvalue.fullname}) + self.add_type_alias_deps(depends_on, target='<%s>' % lvalue.fullname) self.add_type_alias_deps(qualified_tvars, target='<%s>' % lvalue.fullname) + # The above are only direct deps on other aliases. + # For subscripted aliases, type deps from expansion are added in deps.py + # (because the type is stored) if not lvalue.is_inferred_def: # Type aliases can't be re-defined. if node and (node.kind == TYPE_ALIAS or isinstance(node.node, TypeInfo)): @@ -1881,6 +1885,12 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None: node.node = res.type node.is_aliasing = True if isinstance(rvalue, RefExpr): + # For non-subscripted aliases we add type deps right here + # (because the node is stored, not type) + # TODO: currently subscripted and unsubscripted aliases are processed differently + # This leads to duplication of most of the logic with small variations. + # Fix this. + self.add_type_alias_deps({node.node.fullname()}, target='<%s>' % lvalue.fullname) sym = self.lookup_type_node(rvalue) if sym: node.normalized = sym.normalized diff --git a/mypy/server/deps.py b/mypy/server/deps.py index 63f39a404f6c..0255d06be510 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -176,7 +176,7 @@ def __init__(self, def visit_mypy_file(self, o: MypyFile) -> None: self.scope.enter_file(o.fullname()) self.is_package_init_file = o.is_package_init_file() - self.add_type_alias_deps(self.scope.current_full_target()) + self.add_type_alias_deps(self.scope.current_target()) super().visit_mypy_file(o) self.scope.leave() @@ -195,7 +195,8 @@ def visit_func_def(self, o: FuncDef) -> None: if o.info: for base in non_trivial_bases(o.info): self.add_dependency(make_trigger(base.fullname() + '.' + o.name())) - self.add_type_alias_deps(self.scope.current_full_target(), add_trigger=True) + # TODO: avoid adding both `mod.func` and `` here and above for normal types + self.add_type_alias_deps(self.scope.current_target(), trigger_recursively=True) super().visit_func_def(o) self.scope.leave() @@ -283,16 +284,19 @@ def visit_assignment_stmt(self, o: AssignmentStmt) -> 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)) - elif isinstance(rvalue, IndexExpr) and isinstance(rvalue.analyzed, TypeAliasExpr): + elif o.is_alias_def: assert len(o.lvalues) == 1 lvalue = o.lvalues[0] assert isinstance(lvalue, NameExpr) - attr_trigger = make_trigger('%s.%s' % (self.scope.current_full_target(), - lvalue.name)) - # We need to re-process the target where alias is defined to "refresh" the alias. - self.add_type_dependencies(rvalue.analyzed.type) - self.add_type_dependencies(rvalue.analyzed.type, attr_trigger) + # TODO: Do we need `self.process_global_ref_expr(lvalue)` to get deps from .__init__? + # I think no, if an alias is used at runtime corresponding deps will be generated + if lvalue.fullname: + attr_trigger = make_trigger(lvalue.fullname) + if isinstance(rvalue, IndexExpr) and isinstance(rvalue.analyzed, TypeAliasExpr): + self.add_type_dependencies(rvalue.analyzed.type, attr_trigger) if attr_trigger in self.alias_deps: + # Alias that depends on other alias, like A = Dict[int, B] + # We need to add -> for dep in self.alias_deps[attr_trigger]: self.add_dependency(make_trigger(dep), target=attr_trigger) else: @@ -537,11 +541,13 @@ def visit_yield_from_expr(self, e: YieldFromExpr) -> None: # Helpers - def add_type_alias_deps(self, target: str, add_trigger: bool = False) -> None: + def add_type_alias_deps(self, target: str, trigger_recursively: bool = False) -> None: + # Type aliases are special, because some of the dependencies are calculated + # in semanal.py, before they are expanded. if target in self.alias_deps: for alias in self.alias_deps[target]: self.add_dependency(make_trigger(alias)) - if add_trigger: + if trigger_recursively: self.add_dependency(make_trigger(alias), make_trigger(target)) def add_dependency(self, trigger: str, target: Optional[str] = None) -> None: diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 38f2e2c1dca7..337854d05709 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -176,9 +176,6 @@ def __init__(self, self.warn_bound_tvar = warn_bound_tvar self.third_pass = third_pass # Names of type aliases encountered while analysing a type will be collected here. - # (This also recursively includes the names of aliases they depend on.) - # TODO: adding the sub-dependencies shouldn't be necessary since the logic - # of fine-grained should propagate triggered dependencies. self.aliases_used = set() # type: Set[str] def visit_unbound_type(self, t: UnboundType) -> Type: @@ -268,7 +265,8 @@ def visit_unbound_type(self, t: UnboundType) -> Type: elif fullname in ('mypy_extensions.NoReturn', 'typing.NoReturn'): return UninhabitedType(is_noreturn=True) elif sym.kind == TYPE_ALIAS: - self.aliases_used.update(sym.alias_depends_on) + if sym.alias_name is not None: + self.aliases_used.add(sym.alias_name) override = sym.type_override all_vars = sym.alias_tvars assert override is not None @@ -318,7 +316,8 @@ def visit_unbound_type(self, t: UnboundType) -> Type: return t info = sym.node # type: TypeInfo if sym.is_aliasing: - self.aliases_used.update(sym.alias_depends_on) + if sym.alias_name is not None: + self.aliases_used.add(sym.alias_name) if len(t.args) > 0 and info.fullname() == 'builtins.tuple': fallback = Instance(info, [AnyType(TypeOfAny.special_form)], t.line) return TupleType(self.anal_array(t.args), fallback, t.line) diff --git a/test-data/unit/deps-types.test b/test-data/unit/deps-types.test index e7dd70ff77e1..6134a9c2521b 100644 --- a/test-data/unit/deps-types.test +++ b/test-data/unit/deps-types.test @@ -163,7 +163,6 @@ class I: pass [out] -> m -> m - -> m -> , , m [case testAliasDepsNormalFunc] @@ -175,7 +174,6 @@ def f(x: A) -> None: class I: pass [out] -> , m, m.f - -> m -> , , m, m.f [case testAliasDepsNormalClass] @@ -238,7 +236,7 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> , m.f + -> , m, m.f -> , , m, m.f -> , , m, m.f -> , , m, m.f @@ -273,7 +271,6 @@ class I: pass [out] -> m -> m - -> m -> , , m [case testAliasDepsForwardFunc] @@ -285,7 +282,6 @@ A = I class I: pass [out] -> , m, m.f - -> m -> , , m, m.f [case testAliasDepsForwardClass] @@ -298,7 +294,6 @@ class I: pass [out] -> m -> m.C - -> m -> , , m [case testAliasDepsChainedMod] @@ -309,10 +304,9 @@ x: B [file mod.py] class I: pass [out] - -> m + -> , m -> m -> m - -> m -> , , , m [case testAliasDepsChainedFunc] @@ -324,9 +318,8 @@ def f(x: B) -> None: [file mod.py] class I: pass [out] - -> , m, m.f + -> , m -> , m, m.f - -> m -> , , , m, m.f [case testAliasDepsChainedClass] @@ -338,10 +331,10 @@ class C(B): [file mod.py] class I: pass [out] - -> m + -> , m -> m -> m.C - -> , m + -> -> , , m, m.C [case testAliasDepsNestedMod] @@ -357,7 +350,7 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> m + -> , m -> m -> m -> , , , m @@ -378,8 +371,8 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> , m, m.f - -> , m.f + -> , m + -> , m, m.f -> , , , m, m.f -> , , , m, m.f -> , , , m, m.f @@ -398,7 +391,7 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> m + -> , m -> m -> m.C -> , , , m @@ -420,7 +413,6 @@ class I: pass class S: pass [out] -> , m, m.fun - -> m -> , m, m.fun [case testAliasDepsRuntime] @@ -439,7 +431,6 @@ class S: pass -> m -> m -> , m - -> m -> , , m -> , m @@ -454,7 +445,6 @@ class I: pass [out] -> m -> m.P - -> m -> , , , m, m.P [case testAliasDepsTypedDict] @@ -469,7 +459,6 @@ class I: pass [out] -> m -> m.P - -> m -> , , m, m.P -> m @@ -484,5 +473,4 @@ class I: pass [out] -> , m, m.f -> m.f - -> m -> , , m, m.f From e5ab3eb49cec14590c84baee6980504669192c7f Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 20 Feb 2018 13:32:08 +0000 Subject: [PATCH 31/42] Add some more deps tests with three modules --- test-data/unit/deps-types.test | 65 ++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/test-data/unit/deps-types.test b/test-data/unit/deps-types.test index 6134a9c2521b..e3659e37448f 100644 --- a/test-data/unit/deps-types.test +++ b/test-data/unit/deps-types.test @@ -165,6 +165,20 @@ class I: pass -> m -> , , m +[case testAliasDepsNormalModExtended] +import a +x: a.A +[file a.py] +from mod import I +A = I +[file mod.py] +class I: pass +[out] + -> m + -> m + -> m + -> , m + [case testAliasDepsNormalFunc] from mod import I A = I @@ -176,6 +190,20 @@ class I: pass -> , m, m.f -> , , m, m.f +[case testAliasDepsNormalFuncExtended] +import a +def f(x: a.A) -> None: + pass +[file a.py] +from mod import I +A = I +[file mod.py] +class I: pass +[out] + -> , m.f + -> m + -> , m.f + [case testAliasDepsNormalClass] from a import A class C: @@ -241,6 +269,27 @@ class S: pass -> , , m, m.f -> , , m, m.f +[case testAliasDepsGenericFuncExtended] +import a +def f(x: a.A) -> None: + pass +[file a.py] +from mod import I, S, D +A = D[S, I] +[file mod.py] +from typing import TypeVar, Generic +T = TypeVar('T') +U = TypeVar('U') +class D(Generic[T, U]): pass +class I: pass +class S: pass +[out] + -> , m.f + -> m + -> , m.f + -> , m.f + -> , m.f + [case testAliasDepsGenericClass] from mod import I, D, S, T A = D[S, T] @@ -322,6 +371,22 @@ class I: pass -> , m, m.f -> , , , m, m.f +[case testAliasDepsChainedFuncExtended] +import a +B = a.A +def f(x: B) -> None: + pass +[file a.py] +from mod import I +A = I +[file mod.py] +class I: pass +[out] + -> , m, m.f + -> + -> m + -> , , m.f + [case testAliasDepsChainedClass] from mod import I A = I From 4832e37bf8605fdbd0d249aaac2cc995cfb1324e Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 20 Feb 2018 13:42:11 +0000 Subject: [PATCH 32/42] Add AST diff tests for aliases --- test-data/unit/diff.test | 50 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/test-data/unit/diff.test b/test-data/unit/diff.test index 5e5d81294de9..0f786927b718 100644 --- a/test-data/unit/diff.test +++ b/test-data/unit/diff.test @@ -618,3 +618,53 @@ p = Point(dict(x=42, y=1337)) [out] __main__.Point __main__.p + +[case testTypeAliasSimple] +A = int +[file next.py] +A = str +[out] +__main__.A + +[case testTypeAliasGeneric] +from typing import List +A = List[int] +[file next.py] +from typing import List +A = List[str] +[builtins fixtures/list.pyi] +[out] +__main__.A + +[case testTypeAliasGenToNonGen] +from typing import List +A = List[str] +[file next.py] +from typing import List +A = List +[builtins fixtures/list.pyi] +[out] +__main__.A + +[case testTypeAliasNonGenToGen] +from typing import List +A = List +[file next.py] +from typing import List +A = List[str] +[builtins fixtures/list.pyi] +[out] +__main__.A + +[case testTypeAliasGenericTypeVar] +from typing import TypeVar, Dict +T = TypeVar('T') +A = Dict[str, T] +[file next.py] +from typing import TypeVar, Dict +class T: pass +A = Dict[str, T] +[builtins fixtures/dict.pyi] +[out] +__main__.A +__main__.T From b08aef8026f5fb0085c472b06370ee3d5f5d354f Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 20 Feb 2018 17:16:15 +0000 Subject: [PATCH 33/42] Remove intermediate type alias deps --- mypy/semanal.py | 7 +-- mypy/semanal_pass3.py | 4 +- mypy/server/deps.py | 9 +-- test-data/unit/deps-types.test | 105 +++++++++++++++++-------------- test-data/unit/fine-grained.test | 5 +- 5 files changed, 67 insertions(+), 63 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index c1426f4fd9b2..1d2451fc16ff 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1861,9 +1861,8 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None: assert node is not None if lvalue.fullname is not None: node.alias_name = lvalue.fullname - self.add_type_alias_deps({lvalue.fullname}) - self.add_type_alias_deps(depends_on, target='<%s>' % lvalue.fullname) - self.add_type_alias_deps(qualified_tvars, target='<%s>' % lvalue.fullname) + self.add_type_alias_deps(depends_on) + self.add_type_alias_deps(qualified_tvars) # The above are only direct deps on other aliases. # For subscripted aliases, type deps from expansion are added in deps.py # (because the type is stored) @@ -1890,7 +1889,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None: # TODO: currently subscripted and unsubscripted aliases are processed differently # This leads to duplication of most of the logic with small variations. # Fix this. - self.add_type_alias_deps({node.node.fullname()}, target='<%s>' % lvalue.fullname) + self.add_type_alias_deps({node.node.fullname()}) sym = self.lookup_type_node(rvalue) if sym: node.normalized = sym.normalized diff --git a/mypy/semanal_pass3.py b/mypy/semanal_pass3.py index ac333737c0b9..783c2754a9d2 100644 --- a/mypy/semanal_pass3.py +++ b/mypy/semanal_pass3.py @@ -366,7 +366,7 @@ def analyze(self, type: Optional[Type], node: Union[Node, SymbolTableNode], self.check_for_omitted_generics(type) self.generate_type_patches(node, indicator, warn) if analyzer.aliases_used: - target = self.scope.current_full_target() + target = self.scope.current_target() self.cur_mod_node.alias_deps[target].update(analyzer.aliases_used) def analyze_types(self, types: List[Type], node: Node) -> None: @@ -377,7 +377,7 @@ def analyze_types(self, types: List[Type], node: Node) -> None: type.accept(analyzer) self.check_for_omitted_generics(type) if analyzer.aliases_used: - target = self.scope.current_full_target() + target = self.scope.current_target() self.cur_mod_node.alias_deps[target].update(analyzer.aliases_used) self.generate_type_patches(node, indicator, warn=False) diff --git a/mypy/server/deps.py b/mypy/server/deps.py index 0255d06be510..706fc3ca2972 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -290,15 +290,8 @@ def visit_assignment_stmt(self, o: AssignmentStmt) -> None: assert isinstance(lvalue, NameExpr) # TODO: Do we need `self.process_global_ref_expr(lvalue)` to get deps from .__init__? # I think no, if an alias is used at runtime corresponding deps will be generated - if lvalue.fullname: - attr_trigger = make_trigger(lvalue.fullname) if isinstance(rvalue, IndexExpr) and isinstance(rvalue.analyzed, TypeAliasExpr): - self.add_type_dependencies(rvalue.analyzed.type, attr_trigger) - if attr_trigger in self.alias_deps: - # Alias that depends on other alias, like A = Dict[int, B] - # We need to add -> - for dep in self.alias_deps[attr_trigger]: - self.add_dependency(make_trigger(dep), target=attr_trigger) + self.add_type_dependencies(rvalue.analyzed.type) else: # Normal assignment super().visit_assignment_stmt(o) diff --git a/test-data/unit/deps-types.test b/test-data/unit/deps-types.test index e3659e37448f..8bba7e96555d 100644 --- a/test-data/unit/deps-types.test +++ b/test-data/unit/deps-types.test @@ -163,7 +163,7 @@ class I: pass [out] -> m -> m - -> , , m + -> , m [case testAliasDepsNormalModExtended] import a @@ -187,8 +187,8 @@ def f(x: A) -> None: [file mod.py] class I: pass [out] - -> , m, m.f - -> , , m, m.f + -> , m.f + -> , m, m.f [case testAliasDepsNormalFuncExtended] import a @@ -247,9 +247,9 @@ class S: pass [out] -> m -> m - -> , , m - -> , , m - -> , , m + -> , m + -> , m + -> , m [case testAliasDepsGenericFunc] from mod import I, S, D @@ -264,10 +264,10 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> , m, m.f - -> , , m, m.f - -> , , m, m.f - -> , , m, m.f + -> , m.f + -> , m, m.f + -> , m, m.f + -> , m, m.f [case testAliasDepsGenericFuncExtended] import a @@ -305,10 +305,10 @@ class S: pass [out] -> m -> m.C - -> , , m + -> , m -> , m - -> , , m - -> , m + -> , m + -> m [case testAliasDepsForwardMod] from mod import I @@ -320,7 +320,7 @@ class I: pass [out] -> m -> m - -> , , m + -> , m [case testAliasDepsForwardFunc] from mod import I @@ -330,8 +330,8 @@ A = I [file mod.py] class I: pass [out] - -> , m, m.f - -> , , m, m.f + -> , m.f + -> , m, m.f [case testAliasDepsForwardClass] from mod import I @@ -343,7 +343,7 @@ class I: pass [out] -> m -> m.C - -> , , m + -> , m [case testAliasDepsChainedMod] from mod import I @@ -353,10 +353,10 @@ x: B [file mod.py] class I: pass [out] - -> , m + -> m -> m -> m - -> , , , m + -> , m [case testAliasDepsChainedFunc] from mod import I @@ -367,9 +367,9 @@ def f(x: B) -> None: [file mod.py] class I: pass [out] - -> , m - -> , m, m.f - -> , , , m, m.f + -> m + -> , m.f + -> , m, m.f [case testAliasDepsChainedFuncExtended] import a @@ -382,10 +382,10 @@ A = I [file mod.py] class I: pass [out] - -> , m, m.f - -> + -> , m.f + -> m -> m - -> , , m.f + -> , m, m.f [case testAliasDepsChainedClass] from mod import I @@ -396,11 +396,11 @@ class C(B): [file mod.py] class I: pass [out] - -> , m + -> m -> m -> m.C -> - -> , , m, m.C + -> m, m.C [case testAliasDepsNestedMod] from mod import I, S, D @@ -415,12 +415,12 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> , m + -> m -> m -> m - -> , , , m - -> , , , m - -> , , , m + -> , m + -> , m + -> , m [case testAliasDepsNestedFunc] from mod import I, S, D @@ -436,11 +436,11 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> , m - -> , m, m.f - -> , , , m, m.f - -> , , , m, m.f - -> , , , m, m.f + -> m + -> , m.f + -> , m, m.f + -> , m, m.f + -> , m, m.f [case testAliasDepsNestedClass] from mod import I, S, D @@ -456,12 +456,12 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> , m + -> m -> m -> m.C - -> , , , m - -> , , , m - -> , , , m + -> , m + -> , m + -> , m [case testAliasDepsCast] from typing import cast @@ -477,8 +477,8 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> , m, m.fun - -> , m, m.fun + -> , m.fun + -> m, m.fun [case testAliasDepsRuntime] from mod import I, S, D @@ -496,7 +496,7 @@ class S: pass -> m -> m -> , m - -> , , m + -> , m -> , m [case testAliasDepsNamedTuple] @@ -510,7 +510,7 @@ class I: pass [out] -> m -> m.P - -> , , , m, m.P + -> , , m, m.P [case testAliasDepsTypedDict] from mypy_extensions import TypedDict @@ -524,7 +524,7 @@ class I: pass [out] -> m -> m.P - -> , , m, m.P + -> , m, m.P -> m [case testAliasDepsClassInFunction] @@ -536,6 +536,17 @@ def f() -> None: [file mod.py] class I: pass [out] - -> , m, m.f + -> , m.f -> m.f - -> , , m, m.f + -> , m, m.f + +[case testAliasDepsChainedFuncTricky] +import aa +def f(x: aa.B): + x = int() +[file a.py] +A = int +[file aa.py] +import a +B = a.A +[out] diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 345a69c69ac4..040bdd7e6cd3 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -1958,7 +1958,8 @@ A = str == b.py:2: error: Incompatible types in assignment (expression has type "int", variable has type "str") -[case testAliasFineChainedFunc] +[case testAliasFineChainedFunc-skip-nocache] +# This and next don't work without -> intermediate deps import b [file a.py] A = int @@ -1975,7 +1976,7 @@ def f(x: aa.B): == b.py:3: error: Incompatible types in assignment (expression has type "int", variable has type "str") -[case testAliasFineChainedClass] +[case testAliasFineChainedClass-skip-nocache] import b [file a.py] A = int From 35f43800408fc9dfec47177cbae2a8a275ae4d2f Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 20 Feb 2018 17:30:08 +0000 Subject: [PATCH 34/42] Replace full_target with target in alias deps --- mypy/semanal.py | 2 +- mypy/server/deps.py | 2 +- test-data/unit/deps-types.test | 11 ----------- test-data/unit/fine-grained.test | 4 ++-- 4 files changed, 4 insertions(+), 15 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 1d2451fc16ff..b7763c73e07f 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1708,7 +1708,7 @@ def add_type_alias_deps(self, aliases_used: Iterable[str], # the `alias_deps` dict. return if target is None: - target = self.scope.current_full_target() + target = self.scope.current_target() self.cur_mod_node.alias_deps[target].update(aliases_used) def visit_assignment_stmt(self, s: AssignmentStmt) -> None: diff --git a/mypy/server/deps.py b/mypy/server/deps.py index 706fc3ca2972..e8bc7d64787c 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -222,7 +222,7 @@ def visit_class_def(self, o: ClassDef) -> None: 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.add_type_alias_deps(self.scope.current_full_target()) + self.add_type_alias_deps(self.scope.current_target()) self.is_class = old_is_class info = o.info for name, node in info.names.items(): diff --git a/test-data/unit/deps-types.test b/test-data/unit/deps-types.test index 8bba7e96555d..208130812743 100644 --- a/test-data/unit/deps-types.test +++ b/test-data/unit/deps-types.test @@ -539,14 +539,3 @@ class I: pass -> , m.f -> m.f -> , m, m.f - -[case testAliasDepsChainedFuncTricky] -import aa -def f(x: aa.B): - x = int() -[file a.py] -A = int -[file aa.py] -import a -B = a.A -[out] diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 040bdd7e6cd3..5b774c261c97 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -1958,7 +1958,7 @@ A = str == b.py:2: error: Incompatible types in assignment (expression has type "int", variable has type "str") -[case testAliasFineChainedFunc-skip-nocache] +[case testAliasFineChainedFunc] # This and next don't work without -> intermediate deps import b [file a.py] @@ -1976,7 +1976,7 @@ def f(x: aa.B): == b.py:3: error: Incompatible types in assignment (expression has type "int", variable has type "str") -[case testAliasFineChainedClass-skip-nocache] +[case testAliasFineChainedClass] import b [file a.py] A = int From 8f3cb3274c377a7fb7db7c90c15c0d0ef05490c9 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 20 Feb 2018 21:26:05 +0000 Subject: [PATCH 35/42] Don't add deps like -> , only -> fun --- mypy/server/deps.py | 8 ++------ test-data/unit/deps-types.test | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/mypy/server/deps.py b/mypy/server/deps.py index e8bc7d64787c..1565ba130679 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -189,14 +189,12 @@ def visit_func_def(self, o: FuncDef) -> None: else: signature = o.type for trigger in get_type_triggers(signature): - # TODO: avoid adding both `mod.func` and `` here and below for aliases self.add_dependency(trigger) self.add_dependency(trigger, target=make_trigger(target)) if o.info: for base in non_trivial_bases(o.info): self.add_dependency(make_trigger(base.fullname() + '.' + o.name())) - # TODO: avoid adding both `mod.func` and `` here and above for normal types - self.add_type_alias_deps(self.scope.current_target(), trigger_recursively=True) + self.add_type_alias_deps(self.scope.current_target()) super().visit_func_def(o) self.scope.leave() @@ -534,14 +532,12 @@ def visit_yield_from_expr(self, e: YieldFromExpr) -> None: # Helpers - def add_type_alias_deps(self, target: str, trigger_recursively: bool = False) -> None: + def add_type_alias_deps(self, target: str) -> None: # Type aliases are special, because some of the dependencies are calculated # in semanal.py, before they are expanded. if target in self.alias_deps: for alias in self.alias_deps[target]: self.add_dependency(make_trigger(alias)) - if trigger_recursively: - self.add_dependency(make_trigger(alias), make_trigger(target)) def add_dependency(self, trigger: str, target: Optional[str] = None) -> None: """Add dependency from trigger to a target. diff --git a/test-data/unit/deps-types.test b/test-data/unit/deps-types.test index 208130812743..62ae34c5b0e0 100644 --- a/test-data/unit/deps-types.test +++ b/test-data/unit/deps-types.test @@ -187,7 +187,7 @@ def f(x: A) -> None: [file mod.py] class I: pass [out] - -> , m.f + -> m.f -> , m, m.f [case testAliasDepsNormalFuncExtended] @@ -200,7 +200,7 @@ A = I [file mod.py] class I: pass [out] - -> , m.f + -> m.f -> m -> , m.f @@ -264,7 +264,7 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> , m.f + -> m.f -> , m, m.f -> , m, m.f -> , m, m.f @@ -284,7 +284,7 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> , m.f + -> m.f -> m -> , m.f -> , m.f @@ -330,7 +330,7 @@ A = I [file mod.py] class I: pass [out] - -> , m.f + -> m.f -> , m, m.f [case testAliasDepsForwardClass] @@ -368,7 +368,7 @@ def f(x: B) -> None: class I: pass [out] -> m - -> , m.f + -> m.f -> , m, m.f [case testAliasDepsChainedFuncExtended] @@ -382,7 +382,7 @@ A = I [file mod.py] class I: pass [out] - -> , m.f + -> m.f -> m -> m -> , m, m.f @@ -437,7 +437,7 @@ class I: pass class S: pass [out] -> m - -> , m.f + -> m.f -> , m, m.f -> , m, m.f -> , m, m.f @@ -477,7 +477,7 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> , m.fun + -> m.fun -> m, m.fun [case testAliasDepsRuntime] @@ -536,6 +536,6 @@ def f() -> None: [file mod.py] class I: pass [out] - -> , m.f + -> m.f -> m.f -> , m, m.f From e8e01c96592aa30435775b6eae88cc9f579d543f Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 20 Feb 2018 21:33:39 +0000 Subject: [PATCH 36/42] Add two more tests --- test-data/unit/deps-types.test | 20 ++++++++++++++++++++ test-data/unit/fine-grained.test | 19 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/test-data/unit/deps-types.test b/test-data/unit/deps-types.test index 62ae34c5b0e0..850e019e01aa 100644 --- a/test-data/unit/deps-types.test +++ b/test-data/unit/deps-types.test @@ -442,6 +442,26 @@ class S: pass -> , m, m.f -> , m, m.f +[case testAliasDepsNestedFuncDirect] +from mod import I, S, D +A = D[S, I] +E = D +def f(x: E[S, A]) -> None: + pass +[file mod.py] +from typing import TypeVar, Generic +T = TypeVar('T') +U = TypeVar('U') +class D(Generic[T, U]): pass +class I: pass +class S: pass +[out] + -> m.f + -> m.f + -> , m, m.f + -> , m, m.f + -> , m, m.f + [case testAliasDepsNestedClass] from mod import I, S, D A = D[S, I] diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 7a31d0b12dba..0e7abb7d745d 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -2036,6 +2036,25 @@ def f(x: aa.B): == b.py:3: error: Dict entry 0 has incompatible type "str": "int"; expected "str": "str" +[case testAliasDepsNestedFuncDirect] +import b +[file a.py] +from typing import Dict +A = Dict[str, int] +[file a.py.2] +from typing import Dict +A = Dict[str, str] +[file aa.py] +from typing import Dict +import a +E = Dict +[file b.py] +import aa +def f(x: aa.E[str, aa.a.A]): + x = {'first': {str(): int()}} +[builtins fixtures/dict.pyi] +[out] + [case testAliasFineNonGenericToGeneric] import b [file a.py] From b259da4a2f6b92821134ab689ab39c0136744b6d Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 20 Feb 2018 21:39:33 +0000 Subject: [PATCH 37/42] Fix test name and output; prepare for builtins problem investigation --- mypy/server/aststrip.py | 3 --- test-data/unit/fine-grained.test | 4 +++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/mypy/server/aststrip.py b/mypy/server/aststrip.py index 7730ff89b8ca..e2c2055cdbf2 100644 --- a/mypy/server/aststrip.py +++ b/mypy/server/aststrip.py @@ -217,9 +217,6 @@ def is_duplicate_attribute_def(self, node: MemberExpr) -> bool: return any(info.get(node.name) is not None for info in self.type.mro[1:]) def strip_ref_expr(self, node: RefExpr) -> None: - if node.fullname and (node.fullname.startswith('typing') or - node.fullname.startswith('builtins')): - return node.kind = None node.node = None node.fullname = None diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 0e7abb7d745d..02081ab40368 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -2036,7 +2036,7 @@ def f(x: aa.B): == b.py:3: error: Dict entry 0 has incompatible type "str": "int"; expected "str": "str" -[case testAliasDepsNestedFuncDirect] +[case testAliasFineNestedFuncDirect] import b [file a.py] from typing import Dict @@ -2054,6 +2054,8 @@ def f(x: aa.E[str, aa.a.A]): x = {'first': {str(): int()}} [builtins fixtures/dict.pyi] [out] +== +b.py:3: error: Dict entry 0 has incompatible type "str": "int"; expected "str": "str" [case testAliasFineNonGenericToGeneric] import b From 6817ba515c580fad5ea847332996d2fceb9925a0 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 20 Feb 2018 23:41:46 +0000 Subject: [PATCH 38/42] Strip IndexExpr --- mypy/server/aststrip.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mypy/server/aststrip.py b/mypy/server/aststrip.py index e2c2055cdbf2..d9fdf7b2da8d 100644 --- a/mypy/server/aststrip.py +++ b/mypy/server/aststrip.py @@ -43,7 +43,7 @@ from mypy.nodes import ( Node, FuncDef, NameExpr, MemberExpr, RefExpr, MypyFile, FuncItem, ClassDef, AssignmentStmt, ImportFrom, Import, TypeInfo, SymbolTable, Var, CallExpr, Decorator, OverloadedFuncDef, - SuperExpr, UNBOUND_IMPORTED, GDEF, MDEF + SuperExpr, UNBOUND_IMPORTED, GDEF, MDEF, IndexExpr ) from mypy.traverser import TraverserVisitor @@ -204,6 +204,10 @@ def visit_member_expr(self, node: MemberExpr) -> None: node.def_var = None super().visit_member_expr(node) + def visit_index_expr(self, node: IndexExpr) -> None: + node.analyzed = None # was a type alias + super().visit_index_expr(node) + def strip_class_attr(self, name: str) -> None: if self.type is not None: del self.type.names[name] From b8893f037c64db2ad245ff450e3f387381adce0e Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 21 Feb 2018 15:40:27 +0000 Subject: [PATCH 39/42] Add extra dep from __init__ to alias definition scope and corresponding tests --- mypy/server/deps.py | 7 +- test-data/unit/deps-types.test | 22 ++++- test-data/unit/fine-grained.test | 141 +++++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+), 3 deletions(-) diff --git a/mypy/server/deps.py b/mypy/server/deps.py index 1565ba130679..c349793a7128 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -286,8 +286,11 @@ def visit_assignment_stmt(self, o: AssignmentStmt) -> None: assert len(o.lvalues) == 1 lvalue = o.lvalues[0] assert isinstance(lvalue, NameExpr) - # TODO: Do we need `self.process_global_ref_expr(lvalue)` to get deps from .__init__? - # I think no, if an alias is used at runtime corresponding deps will be generated + # TODO: get rid of this extra dependency from __init__ to alias definition scope + typ = self.type_map.get(lvalue) + if isinstance(typ, FunctionLike) and typ.is_type_obj(): + class_name = typ.type_object().fullname() + self.add_dependency(make_trigger(class_name + '.__init__')) if isinstance(rvalue, IndexExpr) and isinstance(rvalue.analyzed, TypeAliasExpr): self.add_type_dependencies(rvalue.analyzed.type) else: diff --git a/test-data/unit/deps-types.test b/test-data/unit/deps-types.test index 850e019e01aa..89db07676e74 100644 --- a/test-data/unit/deps-types.test +++ b/test-data/unit/deps-types.test @@ -163,6 +163,7 @@ class I: pass [out] -> m -> m + -> m -> , m [case testAliasDepsNormalModExtended] @@ -188,6 +189,7 @@ def f(x: A) -> None: class I: pass [out] -> m.f + -> m -> , m, m.f [case testAliasDepsNormalFuncExtended] @@ -247,6 +249,7 @@ class S: pass [out] -> m -> m + -> m -> , m -> , m -> , m @@ -265,6 +268,7 @@ class I: pass class S: pass [out] -> m.f + -> m -> , m, m.f -> , m, m.f -> , m, m.f @@ -305,6 +309,7 @@ class S: pass [out] -> m -> m.C + -> m -> , m -> , m -> , m @@ -320,6 +325,7 @@ class I: pass [out] -> m -> m + -> m -> , m [case testAliasDepsForwardFunc] @@ -331,6 +337,7 @@ A = I class I: pass [out] -> m.f + -> m -> , m, m.f [case testAliasDepsForwardClass] @@ -343,6 +350,7 @@ class I: pass [out] -> m -> m.C + -> m -> , m [case testAliasDepsChainedMod] @@ -356,6 +364,7 @@ class I: pass -> m -> m -> m + -> m -> , m [case testAliasDepsChainedFunc] @@ -369,6 +378,7 @@ class I: pass [out] -> m -> m.f + -> m -> , m, m.f [case testAliasDepsChainedFuncExtended] @@ -385,6 +395,7 @@ class I: pass -> m.f -> m -> m + -> m -> , m, m.f [case testAliasDepsChainedClass] @@ -399,7 +410,7 @@ class I: pass -> m -> m -> m.C - -> + -> , m -> m, m.C [case testAliasDepsNestedMod] @@ -418,6 +429,7 @@ class S: pass -> m -> m -> m + -> m -> , m -> , m -> , m @@ -438,6 +450,7 @@ class S: pass [out] -> m -> m.f + -> m -> , m, m.f -> , m, m.f -> , m, m.f @@ -458,6 +471,7 @@ class S: pass [out] -> m.f -> m.f + -> m -> , m, m.f -> , m, m.f -> , m, m.f @@ -479,6 +493,7 @@ class S: pass -> m -> m -> m.C + -> m -> , m -> , m -> , m @@ -498,6 +513,7 @@ class I: pass class S: pass [out] -> m.fun + -> m -> m, m.fun [case testAliasDepsRuntime] @@ -516,6 +532,7 @@ class S: pass -> m -> m -> , m + -> m -> , m -> , m @@ -530,6 +547,7 @@ class I: pass [out] -> m -> m.P + -> m -> , , m, m.P [case testAliasDepsTypedDict] @@ -544,6 +562,7 @@ class I: pass [out] -> m -> m.P + -> m -> , m, m.P -> m @@ -558,4 +577,5 @@ class I: pass [out] -> m.f -> m.f + -> m -> , m, m.f diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 02081ab40368..6d113bcb1974 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -2163,6 +2163,147 @@ def f() -> None: == b.py:4: error: Incompatible types in assignment (expression has type "int", variable has type "str") +[case testAliasFineInitNormalMod] +import c +[file a.py] +class A: + def __init__(self, x: int) -> None: + pass +[file a.py.2] +class A: + def __init__(self, x: str) -> None: + pass +[file b.py] +import a +B = a.A +[file c.py] +from b import B +B(int()) +[out] +== +c.py:2: error: Argument 1 to "A" has incompatible type "int"; expected "str" + +[case testAliasFineInitNormalFunc] +import c +[file a.py] +class A: + def __init__(self, x: int) -> None: + pass +[file a.py.2] +class A: + def __init__(self, x: str) -> None: + pass +[file b.py] +import a +B = a.A +[file c.py] +from b import B +def f() -> None: + B(int()) +[out] +== +c.py:3: error: Argument 1 to "A" has incompatible type "int"; expected "str" + +[case testAliasFineInitGenericMod] +import c +[file a.py] +from typing import Generic, TypeVar +T = TypeVar('T') +S = TypeVar('S') +class A(Generic[T, S]): + def __init__(self, x: T) -> None: + pass +[file a.py.2] +from typing import Generic, TypeVar +T = TypeVar('T') +S = TypeVar('S') +class A(Generic[T, S]): + def __init__(self, x: S) -> None: + pass +[file b.py] +import a +B = a.A[int, str] +[file c.py] +from b import B +B(int()) +[out] +== +c.py:2: error: Argument 1 has incompatible type "int"; expected "str" + +[case testAliasFineInitGenericFunc] +import c +[file a.py] +from typing import Generic, TypeVar +T = TypeVar('T') +S = TypeVar('S') +class A(Generic[T, S]): + def __init__(self, x: T) -> None: + pass +[file a.py.2] +from typing import Generic, TypeVar +T = TypeVar('T') +S = TypeVar('S') +class A(Generic[T, S]): + def __init__(self, x: S) -> None: + pass +[file b.py] +import a +B = a.A[int, str] +[file c.py] +from b import B +def f() -> None: + B(str()) +[out] +c.py:3: error: Argument 1 has incompatible type "str"; expected "int" +== + +[case testAliasFineInitChainedMod] +import d +[file a.py] +class A: + def __init__(self, x: int) -> None: + pass +[file a.py.2] +class A: + def __init__(self, x: str) -> None: + pass +[file b.py] +import a +B = a.A +[file c.py] +import b +C = b.B +[file d.py] +from c import C +C(int()) +[out] +== +d.py:2: error: Argument 1 to "A" has incompatible type "int"; expected "str" + +[case testAliasFineInitChainedFunc] +import d +[file a.py] +class A: + def __init__(self, x: int) -> None: + pass +[file a.py.2] +class A: + def __init__(self, x: str) -> None: + pass +[file b.py] +import a +B = a.A +[file c.py] +import b +C = b.B +[file d.py] +from c import C +def f() -> None: + C(str()) +[out] +d.py:3: error: Argument 1 to "A" has incompatible type "str"; expected "int" +== + [case testNonePartialType] import a a.y From ef7f3b9227cb03d568cf64c8b3ea87f2d1924c11 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 21 Feb 2018 16:31:35 +0000 Subject: [PATCH 40/42] More deps tests; allow showing all deps --- mypy/test/testdeps.py | 11 +-- test-data/unit/deps-types.test | 117 ++++++++++++++++++++++++++++++- test-data/unit/diff.test | 12 ++++ test-data/unit/fine-grained.test | 1 - 4 files changed, 134 insertions(+), 7 deletions(-) diff --git a/mypy/test/testdeps.py b/mypy/test/testdeps.py index 63d27b29471a..19eaecbe8b45 100644 --- a/mypy/test/testdeps.py +++ b/mypy/test/testdeps.py @@ -2,6 +2,7 @@ import os from typing import List, Tuple, Dict, Optional +from collections import defaultdict from mypy import build, defaults from mypy.build import BuildSource @@ -33,6 +34,7 @@ class GetDependenciesSuite(DataSuite): def run_case(self, testcase: DataDrivenTestCase) -> None: src = '\n'.join(testcase.input) + dump_all = '# __dump_all__' in src if testcase.name.endswith('python2'): python_version = defaults.PYTHON2_VERSION else: @@ -43,11 +45,12 @@ def run_case(self, testcase: DataDrivenTestCase) -> None: if not a: a = ['Unknown compile error (likely syntax error in test case or fixture)'] else: - deps = {} - for module in dumped_modules: - if module in files: + deps = defaultdict(set) + for module in files: + if module in dumped_modules or dump_all and module not in ('abc', 'typing'): new_deps = get_dependencies(files[module], type_map, python_version) - deps.update(new_deps) + for source in new_deps: + deps[source].update(new_deps[source]) for source, targets in sorted(deps.items()): line = '%s -> %s' % (source, ', '.join(sorted(targets))) diff --git a/test-data/unit/deps-types.test b/test-data/unit/deps-types.test index 89db07676e74..51b19b47489e 100644 --- a/test-data/unit/deps-types.test +++ b/test-data/unit/deps-types.test @@ -167,6 +167,7 @@ class I: pass -> , m [case testAliasDepsNormalModExtended] +# __dump_all__ import a x: a.A [file a.py] @@ -178,7 +179,8 @@ class I: pass -> m -> m -> m - -> , m + -> a + -> , m, a, mod.I [case testAliasDepsNormalFunc] from mod import I @@ -193,6 +195,7 @@ class I: pass -> , m, m.f [case testAliasDepsNormalFuncExtended] +# __dump_all__ import a def f(x: a.A) -> None: pass @@ -204,7 +207,8 @@ class I: pass [out] -> m.f -> m - -> , m.f + -> a + -> , m.f, a, mod.I [case testAliasDepsNormalClass] from a import A @@ -434,6 +438,34 @@ class S: pass -> , m -> , m +[case testAliasDepsNestedModExtended] +# __dump_all__ +from mod import S, D +import a +B = D[S, a.A] +x: B +[file a.py] +from mod import I, S, D +A = D[S, I] +[file mod.py] +from typing import TypeVar, Generic +T = TypeVar('T') +U = TypeVar('U') +class D(Generic[T, U]): pass +class I: pass +class S: pass +[out] + -> m + -> m + -> m + -> m + -> m, a + -> , m, a, mod.D + -> , m, a, mod.I + -> , m, a, mod.S + -> mod.D + -> mod.D + [case testAliasDepsNestedFunc] from mod import I, S, D A = D[S, I] @@ -455,6 +487,34 @@ class S: pass -> , m, m.f -> , m, m.f +[case testAliasDepsNestedFuncExtended] +# __dump_all__ +from mod import S, D +import a +B = D[S, a.A] +def f(x: B) -> None: + pass +[file a.py] +from mod import I, S, D +A = D[S, I] +[file mod.py] +from typing import TypeVar, Generic +T = TypeVar('T') +U = TypeVar('U') +class D(Generic[T, U]): pass +class I: pass +class S: pass +[out] + -> m.f + -> m + -> m + -> m, a + -> , m, m.f, a, mod.D + -> , m, m.f, a, mod.I + -> , m, m.f, a, mod.S + -> mod.D + -> mod.D + [case testAliasDepsNestedFuncDirect] from mod import I, S, D A = D[S, I] @@ -536,6 +596,33 @@ class S: pass -> , m -> , m +[case testAliasDepsRuntimeExtended] +# __dump_all__ +from mod import I, S, D +import a +x = D[S, a.A]() +[file a.py] +from mod import I +A = I +[file mod.py] +from typing import TypeVar, Generic +T = TypeVar('T') +U = TypeVar('U') +class D(Generic[T, U]): pass +class I: pass +class S: pass +[out] + -> m + -> m + -> m + -> m + -> , m, mod.D + -> a + -> , m, a, mod.I + -> , m, mod.S + -> mod.D + -> mod.D + [case testAliasDepsNamedTuple] from typing import NamedTuple from mod import I @@ -550,6 +637,18 @@ class I: pass -> m -> , , m, m.P +[case testAliasDepsNamedTupleFunctional] +from typing import NamedTuple +from mod import I +A = I +P = NamedTuple('P', [('x', A)]) +[file mod.py] +class I: pass +[out] + -> m + -> m + -> , , m + [case testAliasDepsTypedDict] from mypy_extensions import TypedDict from mod import I @@ -566,6 +665,20 @@ class I: pass -> , m, m.P -> m +[case testAliasDepsTypedDictFunctional] +from mypy_extensions import TypedDict +from mod import I +A = I +P = TypedDict('P', {'x': A}) +[file mod.py] +class I: pass +[builtins fixtures/dict.pyi] +[out] + -> m + -> m + -> , m + -> m + [case testAliasDepsClassInFunction] from mod import I A = I diff --git a/test-data/unit/diff.test b/test-data/unit/diff.test index 0f786927b718..841b5648c077 100644 --- a/test-data/unit/diff.test +++ b/test-data/unit/diff.test @@ -621,17 +621,21 @@ __main__.p [case testTypeAliasSimple] A = int +B = int [file next.py] A = str +B = int [out] __main__.A [case testTypeAliasGeneric] from typing import List A = List[int] +B = List[int] [file next.py] from typing import List A = List[str] +B = List[int] [builtins fixtures/list.pyi] [out] __main__.A @@ -639,9 +643,11 @@ __main__.A [case testTypeAliasGenToNonGen] from typing import List A = List[str] +B = List [file next.py] from typing import List A = List +B = List [builtins fixtures/list.pyi] [out] __main__.A @@ -649,9 +655,11 @@ __main__.A [case testTypeAliasNonGenToGen] from typing import List A = List +B = List [file next.py] from typing import List A = List[str] +B = List [builtins fixtures/list.pyi] [out] __main__.A @@ -659,11 +667,15 @@ __main__.A [case testTypeAliasGenericTypeVar] from typing import TypeVar, Dict T = TypeVar('T') +S = TypeVar('S') A = Dict[str, T] +B = Dict[str, S] [file next.py] from typing import TypeVar, Dict class T: pass +S = TypeVar('S') A = Dict[str, T] +B = Dict[str, S] [builtins fixtures/dict.pyi] [out] __main__.A diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 6d113bcb1974..7044bc93d47b 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -1959,7 +1959,6 @@ A = str b.py:2: error: Incompatible types in assignment (expression has type "int", variable has type "str") [case testAliasFineChainedFunc] -# This and next don't work without -> intermediate deps import b [file a.py] A = int From 86b1bfdcda40f18953b8687a6c05ff40752b1bc0 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 21 Feb 2018 16:43:56 +0000 Subject: [PATCH 41/42] Add remaining required tests --- mypy/test/testdeps.py | 7 +++-- test-data/unit/fine-grained.test | 52 ++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/mypy/test/testdeps.py b/mypy/test/testdeps.py index 19eaecbe8b45..2e3973550c14 100644 --- a/mypy/test/testdeps.py +++ b/mypy/test/testdeps.py @@ -1,7 +1,10 @@ """Test cases for generating node-level dependencies (for fine-grained incremental checking)""" import os -from typing import List, Tuple, Dict, Optional +from typing import List, Tuple, Dict, Optional, Set +MYPY = False +if MYPY: + from typing import DefaultDict from collections import defaultdict from mypy import build, defaults @@ -45,7 +48,7 @@ def run_case(self, testcase: DataDrivenTestCase) -> None: if not a: a = ['Unknown compile error (likely syntax error in test case or fixture)'] else: - deps = defaultdict(set) + deps = defaultdict(set) # type: DefaultDict[str, Set[str]] for module in files: if module in dumped_modules or dump_all and module not in ('abc', 'typing'): new_deps = get_dependencies(files[module], type_map, python_version) diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 7044bc93d47b..e1597e9859ef 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -2112,6 +2112,58 @@ def f(x: a.A[str]): == b.py:2: error: Bad number of arguments for type alias, expected: 2, given: 1 +[case testAliasFineAdded] +import b +[file a.py] +[file a.py.2] +A = int +[file b.py] +import a +x: a.A +[out] +b.py:2: error: Name 'a.A' is not defined +== + +[case testAliasFineDeleted] +import b +[file a.py] +A = int +[file a.py.2] +[file b.py] +import a +x: a.A +[out] +== +b.py:2: error: Name 'a.A' is not defined + +[case testAliasFineClassToAlias] +import b +[file a.py] +class A: pass +[file a.py.2] +A = int +[file b.py] +import a +x: a.A +x = 1 +[out] +b.py:3: error: Incompatible types in assignment (expression has type "int", variable has type "A") +== + +[case testAliasFineAliasToClass] +import b +[file a.py] +A = int +[file a.py.2] +class A: pass +[file b.py] +import a +x: a.A +x = 1 +[out] +== +b.py:3: error: Incompatible types in assignment (expression has type "int", variable has type "A") + [case testAliasFineComponentDeleted] import b [file a.py] From 0a4a23e99f2573d237d4c122e0f19f8dc54ac074 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 21 Feb 2018 17:37:15 +0000 Subject: [PATCH 42/42] Improve two tests --- mypy/test/testdeps.py | 3 ++- test-data/unit/deps-types.test | 24 ++++++++++++++++-------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/mypy/test/testdeps.py b/mypy/test/testdeps.py index 2e3973550c14..84c5ec2f66c9 100644 --- a/mypy/test/testdeps.py +++ b/mypy/test/testdeps.py @@ -50,7 +50,8 @@ def run_case(self, testcase: DataDrivenTestCase) -> None: else: deps = defaultdict(set) # type: DefaultDict[str, Set[str]] for module in files: - if module in dumped_modules or dump_all and module not in ('abc', 'typing'): + if module in dumped_modules or dump_all and module not in ('abc', 'typing', + 'mypy_extensions'): new_deps = get_dependencies(files[module], type_map, python_version) for source in new_deps: deps[source].update(new_deps[source]) diff --git a/test-data/unit/deps-types.test b/test-data/unit/deps-types.test index 51b19b47489e..4361defa3960 100644 --- a/test-data/unit/deps-types.test +++ b/test-data/unit/deps-types.test @@ -638,16 +638,20 @@ class I: pass -> , , m, m.P [case testAliasDepsNamedTupleFunctional] +# __dump_all__ from typing import NamedTuple +import a +P = NamedTuple('P', [('x', a.A)]) +[file a.py] from mod import I A = I -P = NamedTuple('P', [('x', A)]) [file mod.py] class I: pass [out] - -> m - -> m - -> , , m + -> m + -> m + -> a + -> , , m, a, mod.I [case testAliasDepsTypedDict] from mypy_extensions import TypedDict @@ -666,17 +670,21 @@ class I: pass -> m [case testAliasDepsTypedDictFunctional] +# __dump_all__ from mypy_extensions import TypedDict +import a +P = TypedDict('P', {'x': a.A}) +[file a.py] from mod import I A = I -P = TypedDict('P', {'x': A}) [file mod.py] class I: pass [builtins fixtures/dict.pyi] [out] - -> m - -> m - -> , m + -> m + -> m + -> a + -> , a, mod.I -> m [case testAliasDepsClassInFunction]