From 1dc60ecb82685ad06e0368309987a2bf2afbb325 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 19 Jan 2018 22:15:29 -0800 Subject: [PATCH 01/14] Don't error on re-exported imports in cycles. Fixes #4049. --- mypy/semanal.py | 8 ++++++-- test-data/unit/semanal-modules.test | 27 +++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index f94b836d783c..37851cd2a0fa 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1471,14 +1471,17 @@ def visit_import_from(self, imp: ImportFrom) -> None: for id, as_id in imp.names: node = module.names.get(id) if module else None missing = False + is_same_module = import_id == self.cur_mod_id + is_unbound = node and node.kind == UNBOUND_IMPORTED possible_module_id = import_id + '.' + id # If the module does not contain a symbol with the name 'id', # try checking if it's a module instead. - if not node or node.kind == UNBOUND_IMPORTED: + if not node or is_unbound: mod = self.modules.get(possible_module_id) if mod is not None: node = SymbolTableNode(MODULE_REF, mod) + is_same_module = False self.add_submodules_to_parent_modules(possible_module_id, True) elif possible_module_id in self.missing_modules: missing = True @@ -1499,7 +1502,8 @@ def visit_import_from(self, imp: ImportFrom) -> None: symbol = SymbolTableNode(GDEF, ast_node) self.add_symbol(name, symbol, imp) return - if node and node.kind != UNBOUND_IMPORTED and not node.module_hidden: + + if node and not node.module_hidden and not (is_unbound and is_same_module): node = self.normalize_type_alias(node, imp) if not node: return diff --git a/test-data/unit/semanal-modules.test b/test-data/unit/semanal-modules.test index 7a00e665b969..c0a909a0f939 100644 --- a/test-data/unit/semanal-modules.test +++ b/test-data/unit/semanal-modules.test @@ -875,3 +875,30 @@ MypyFile:1( AssignmentStmt:1( NameExpr(a* [x.a]) IntExpr(1))) + +[case testImportCycle] +from m import One +[file m/__init__.py] +from .one import One +from .two import Two +[file m/one.py] +class One: + pass +[file m/two.py] +from m import One +class Two: + pass +[out] +MypyFile:1( + ImportFrom:1(m, [One])) +MypyFile:1( + tmp/m/one.py + ClassDef:1( + One + PassStmt:2())) +MypyFile:1( + tmp/m/two.py + ImportFrom:1(m, [One]) + ClassDef:2( + Two + PassStmt:3())) From 02c401b5b020000ac67e8d54a6ef7d83e7c912bc Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 19 Jan 2018 22:28:32 -0800 Subject: [PATCH 02/14] Move test to check-modules and assert on type of imported obj. --- test-data/unit/check-modules.test | 17 +++++++++++++++++ test-data/unit/semanal-modules.test | 27 --------------------------- 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index d8492dad0436..a7e308bde997 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -1943,3 +1943,20 @@ main:2: error: Name 'name' is not defined main:3: error: Revealed type is 'Any' [builtins fixtures/module.pyi] + + +[case testImportCycle] +from m import One +reveal_type(One) +[file m/__init__.py] +from .one import One +from .two import Two +[file m/one.py] +class One: + pass +[file m/two.py] +from m import One +class Two: + pass +[out] +main:2: error: Revealed type is 'def () -> m.one.One' diff --git a/test-data/unit/semanal-modules.test b/test-data/unit/semanal-modules.test index c0a909a0f939..7a00e665b969 100644 --- a/test-data/unit/semanal-modules.test +++ b/test-data/unit/semanal-modules.test @@ -875,30 +875,3 @@ MypyFile:1( AssignmentStmt:1( NameExpr(a* [x.a]) IntExpr(1))) - -[case testImportCycle] -from m import One -[file m/__init__.py] -from .one import One -from .two import Two -[file m/one.py] -class One: - pass -[file m/two.py] -from m import One -class Two: - pass -[out] -MypyFile:1( - ImportFrom:1(m, [One])) -MypyFile:1( - tmp/m/one.py - ClassDef:1( - One - PassStmt:2())) -MypyFile:1( - tmp/m/two.py - ImportFrom:1(m, [One]) - ClassDef:2( - Two - PassStmt:3())) From 5edf2af51fec88cf0ec0e15df2c2ff6a6deefb40 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 19 Jan 2018 22:34:07 -0800 Subject: [PATCH 03/14] Small simplification of the test. --- test-data/unit/check-modules.test | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index a7e308bde997..fc8854dee63b 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -1947,7 +1947,7 @@ main:3: error: Revealed type is 'Any' [case testImportCycle] from m import One -reveal_type(One) +reveal_type(One) # E: Revealed type is 'def () -> m.one.One' [file m/__init__.py] from .one import One from .two import Two @@ -1958,5 +1958,3 @@ class One: from m import One class Two: pass -[out] -main:2: error: Revealed type is 'def () -> m.one.One' From 9903110abcc4cca97af57d7c7a281a73789e8e7f Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 19 Jan 2018 22:40:40 -0800 Subject: [PATCH 04/14] Add failing assertion. --- test-data/unit/check-modules.test | 1 + 1 file changed, 1 insertion(+) diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index fc8854dee63b..f271c4eeb33f 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -1956,5 +1956,6 @@ class One: pass [file m/two.py] from m import One +reveal_type(One) # E: Revealed type is 'def () -> m.one.One' class Two: pass From 9f36fecbaef24576708641dcedd43f684d83d01d Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 19 Jan 2018 23:13:43 -0800 Subject: [PATCH 05/14] First attempt to correct type of cyclic import; not working. --- mypy/semanal_pass3.py | 18 +++++++++++++++++- test-data/unit/check-modules.test | 1 + 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/mypy/semanal_pass3.py b/mypy/semanal_pass3.py index 01c5a6627deb..f1ba01092ec4 100644 --- a/mypy/semanal_pass3.py +++ b/mypy/semanal_pass3.py @@ -17,7 +17,8 @@ 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, ImportFrom, + UNBOUND_IMPORTED ) from mypy.types import ( Type, Instance, AnyType, TypeOfAny, CallableType, TupleType, TypeVarType, TypedDictType, @@ -259,6 +260,21 @@ def visit_type_application(self, e: TypeApplication) -> None: self.analyze(type, e) super().visit_type_application(e) + def visit_import_from(self, imp: ImportFrom) -> None: + import_id = self.sem.correct_relative_import(imp) + module = self.modules.get(import_id) + if not module: + return + for id, as_id in imp.names: + my_node = self.sem.globals.get(as_id or id) + src_node = module.names.get(id) + if my_node and src_node and my_node.kind == UNBOUND_IMPORTED: + my_node.kind = src_node.kind + my_node.node = src_node.node + my_node.type_override = src_node.type_override + my_node.normalized = src_node.normalized + my_node.alias_tvars = src_node.alias_tvars + # Helpers def perform_transform(self, node: Union[Node, SymbolTableNode], diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index f271c4eeb33f..2cc6d5473073 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -1951,6 +1951,7 @@ reveal_type(One) # E: Revealed type is 'def () -> m.one.One' [file m/__init__.py] from .one import One from .two import Two +reveal_type(One) # E: Revealed type is 'def () -> m.one.One' [file m/one.py] class One: pass From 3cc7e2eb2836071937a29021d1a266e323c2bad2 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 22 Jan 2018 15:21:34 -0800 Subject: [PATCH 06/14] Also update UNBOUND_IMPORTED Name expressions. --- mypy/semanal_pass3.py | 10 ++++++++++ test-data/unit/check-modules.test | 10 +++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/mypy/semanal_pass3.py b/mypy/semanal_pass3.py index f1ba01092ec4..e7335f4bcdd3 100644 --- a/mypy/semanal_pass3.py +++ b/mypy/semanal_pass3.py @@ -268,6 +268,7 @@ def visit_import_from(self, imp: ImportFrom) -> None: for id, as_id in imp.names: my_node = self.sem.globals.get(as_id or id) src_node = module.names.get(id) + # Fixup remaining UNBOUND_IMPORTED nodes from import cycles if my_node and src_node and my_node.kind == UNBOUND_IMPORTED: my_node.kind = src_node.kind my_node.node = src_node.node @@ -275,6 +276,15 @@ def visit_import_from(self, imp: ImportFrom) -> None: my_node.normalized = src_node.normalized my_node.alias_tvars = src_node.alias_tvars + def visit_name_expr(self, expr: NameExpr) -> None: + # Fixup remaining UNBOUND_IMPORTED nodes from import cycles + if expr.kind == UNBOUND_IMPORTED: + n = self.sem.lookup(expr.name, expr) + if n: + expr.kind = n.kind + expr.node = n.node + expr.fullname = n.fullname + # Helpers def perform_transform(self, node: Union[Node, SymbolTableNode], diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index 2cc6d5473073..6b1763afa3df 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -1947,16 +1947,20 @@ main:3: error: Revealed type is 'Any' [case testImportCycle] from m import One -reveal_type(One) # E: Revealed type is 'def () -> m.one.One' +reveal_type(One) [file m/__init__.py] from .one import One from .two import Two -reveal_type(One) # E: Revealed type is 'def () -> m.one.One' +reveal_type(One) [file m/one.py] class One: pass [file m/two.py] from m import One -reveal_type(One) # E: Revealed type is 'def () -> m.one.One' +reveal_type(One) class Two: pass +[out] +tmp/m/two.py:2: error: Revealed type is 'def () -> m.one.One' +tmp/m/__init__.py:3: error: Revealed type is 'def () -> m.one.One' +main:2: error: Revealed type is 'def () -> m.one.One' From 2255e1cea09cbd33ff5d65c3cc90a9658c00e39a Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 22 Jan 2018 22:32:51 -0800 Subject: [PATCH 07/14] Suppress errors in followup lookup. --- mypy/semanal_pass3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/semanal_pass3.py b/mypy/semanal_pass3.py index e7335f4bcdd3..9712cb3e79c4 100644 --- a/mypy/semanal_pass3.py +++ b/mypy/semanal_pass3.py @@ -279,7 +279,7 @@ def visit_import_from(self, imp: ImportFrom) -> None: def visit_name_expr(self, expr: NameExpr) -> None: # Fixup remaining UNBOUND_IMPORTED nodes from import cycles if expr.kind == UNBOUND_IMPORTED: - n = self.sem.lookup(expr.name, expr) + n = self.sem.lookup(expr.name, expr, suppress_errors=True) if n: expr.kind = n.kind expr.node = n.node From f78ae950bb4f7951b08d2d634c18d6b96a9451db Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 9 Feb 2018 15:56:51 -0800 Subject: [PATCH 08/14] Also fix up member expressions. --- mypy/semanal.py | 104 ++++++++++++++++-------------- mypy/semanal_pass3.py | 20 +++++- test-data/unit/check-modules.test | 23 ++++++- 3 files changed, 96 insertions(+), 51 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index ab72bf868452..54c454d423ca 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1474,12 +1474,11 @@ def visit_import_from(self, imp: ImportFrom) -> None: node = module.names.get(id) if module else None missing = False is_same_module = import_id == self.cur_mod_id - is_unbound = node and node.kind == UNBOUND_IMPORTED possible_module_id = import_id + '.' + id # If the module does not contain a symbol with the name 'id', # try checking if it's a module instead. - if not node or is_unbound: + if not node or node.kind == UNBOUND_IMPORTED: mod = self.modules.get(possible_module_id) if mod is not None: node = SymbolTableNode(MODULE_REF, mod) @@ -1505,7 +1504,11 @@ def visit_import_from(self, imp: ImportFrom) -> None: self.add_symbol(name, symbol, imp) return - if node and not node.module_hidden and not (is_unbound and is_same_module): + # If we found the node being imported, and it's not in a hidden + # module, nor a recursive self-import (which should be an import + # error), go ahead and update our local symbol table. + if node and not node.module_hidden and not ( + node.kind == UNBOUND_IMPORTED and is_same_module): node = self.normalize_type_alias(node, imp) if not node: return @@ -3334,51 +3337,7 @@ def visit_member_expr(self, expr: MemberExpr) -> None: base.accept(self) # Bind references to module attributes. if isinstance(base, RefExpr) and base.kind == MODULE_REF: - # This branch handles the case foo.bar where foo is a module. - # In this case base.node is the module's MypyFile and we look up - # bar in its namespace. This must be done for all types of bar. - file = cast(Optional[MypyFile], base.node) # can't use isinstance due to issue #2999 - # TODO: Should we actually use this? Not sure if this makes a difference. - # if file.fullname() == self.cur_mod_id: - # names = self.globals - # else: - # names = file.names - n = file.names.get(expr.name, None) if file is not None else None - if n and not n.module_hidden: - n = self.normalize_type_alias(n, expr) - if not n: - return - n = self.rebind_symbol_table_node(n) - if n: - # TODO: What if None? - expr.kind = n.kind - expr.fullname = n.fullname - expr.node = n.node - elif file is not None and file.is_stub and '__getattr__' in file.names: - # If there is a module-level __getattr__, then any attribute on the module is valid - # per PEP 484. - getattr_defn = file.names['__getattr__'] - if isinstance(getattr_defn.node, FuncDef): - if isinstance(getattr_defn.node.type, CallableType): - typ = getattr_defn.node.type.ret_type - else: - typ = AnyType(TypeOfAny.special_form) - expr.kind = MDEF - expr.fullname = '{}.{}'.format(file.fullname(), expr.name) - expr.node = Var(expr.name, type=typ) - else: - # We only catch some errors here; the rest will be - # caught during type checking. - # - # This way we can report a larger number of errors in - # one type checker run. If we reported errors here, - # the build would terminate after semantic analysis - # and we wouldn't be able to report any type errors. - full_name = '%s.%s' % (file.fullname() if file is not None else None, expr.name) - mod_name = " '%s'" % file.fullname() if file is not None else '' - if full_name in obsolete_name_mapping: - self.fail("Module%s has no attribute %r (it's now called %r)" % ( - mod_name, expr.name, obsolete_name_mapping[full_name]), expr) + self.bind_module_attribute_reference(expr) elif isinstance(base, RefExpr): # This branch handles the case C.bar (or cls.bar or self.bar inside # a classmethod/method), where C is a class and bar is a type @@ -3408,6 +3367,55 @@ def visit_member_expr(self, expr: MemberExpr) -> None: expr.fullname = n.fullname expr.node = n.node + def bind_module_attribute_reference(self, expr: MemberExpr) -> None: + base = expr.expr + # This method handles the case foo.bar where foo is a module. + # In this case base.node is the module's MypyFile and we look up + # bar in its namespace. This must be done for all types of bar. + file = cast(Optional[MypyFile], base.node) # can't use isinstance due to issue #2999 + # TODO: Should we actually use this? Not sure if this makes a difference. + # if file.fullname() == self.cur_mod_id: + # names = self.globals + # else: + # names = file.names + n = file.names.get(expr.name, None) if file is not None else None + if n and not n.module_hidden: + n = self.normalize_type_alias(n, expr) + if not n: + return + n = self.rebind_symbol_table_node(n) + if n: + # TODO: What if None? + expr.kind = n.kind + expr.fullname = n.fullname + expr.node = n.node + elif file is not None and file.is_stub and '__getattr__' in file.names: + # If there is a module-level __getattr__, then any attribute on the module is valid + # per PEP 484. + getattr_defn = file.names['__getattr__'] + if isinstance(getattr_defn.node, FuncDef): + if isinstance(getattr_defn.node.type, CallableType): + typ = getattr_defn.node.type.ret_type + else: + typ = AnyType(TypeOfAny.special_form) + expr.kind = MDEF + expr.fullname = '{}.{}'.format(file.fullname(), expr.name) + expr.node = Var(expr.name, type=typ) + else: + # We only catch some errors here; the rest will be + # caught during type checking. + # + # This way we can report a larger number of errors in + # one type checker run. If we reported errors here, + # the build would terminate after semantic analysis + # and we wouldn't be able to report any type errors. + full_name = '%s.%s' % (file.fullname() if file is not None else None, expr.name) + mod_name = " '%s'" % file.fullname() if file is not None else '' + if full_name in obsolete_name_mapping: + self.fail("Module%s has no attribute %r (it's now called %r)" % ( + mod_name, expr.name, obsolete_name_mapping[full_name]), expr) + + def visit_op_expr(self, expr: OpExpr) -> None: expr.left.accept(self) diff --git a/mypy/semanal_pass3.py b/mypy/semanal_pass3.py index 9712cb3e79c4..4fe7e05fba48 100644 --- a/mypy/semanal_pass3.py +++ b/mypy/semanal_pass3.py @@ -18,7 +18,7 @@ Block, TypedDictExpr, NamedTupleExpr, AssignmentStmt, IndexExpr, TypeAliasExpr, NameExpr, CallExpr, NewTypeExpr, ForStmt, WithStmt, CastExpr, TypeVarExpr, TypeApplication, Lvalue, TupleExpr, RevealTypeExpr, SymbolTableNode, Var, ARG_POS, OverloadedFuncDef, ImportFrom, - UNBOUND_IMPORTED + MemberExpr, UNBOUND_IMPORTED, MODULE_REF ) from mypy.types import ( Type, Instance, AnyType, TypeOfAny, CallableType, TupleType, TypeVarType, TypedDictType, @@ -285,6 +285,24 @@ def visit_name_expr(self, expr: NameExpr) -> None: expr.node = n.node expr.fullname = n.fullname + def visit_member_expr(self, expr: MemberExpr) -> None: + # Fixup remaining UNBOUND_IMPORTED from import cycles + base = expr.expr + base.accept(self) + if isinstance(base, RefExpr) and base.kind == MODULE_REF: + if expr.kind == UNBOUND_IMPORTED: + module_id = expr.expr.name + module = self.modules.get(module_id) + node = module and module.names.get(expr.name) + if node: + expr.kind = node.kind + expr.node = node.node + expr.fullname = node.fullname + elif expr.kind is None: + self.sem.bind_module_attribute_reference(expr) + super().visit_member_expr(expr) + + # Helpers def perform_transform(self, node: Union[Node, SymbolTableNode], diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index 6b1763afa3df..ff2211d72231 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -1944,8 +1944,7 @@ main:3: error: Revealed type is 'Any' [builtins fixtures/module.pyi] - -[case testImportCycle] +[case testImportFromReExportInCycle] from m import One reveal_type(One) [file m/__init__.py] @@ -1964,3 +1963,23 @@ class Two: tmp/m/two.py:2: error: Revealed type is 'def () -> m.one.One' tmp/m/__init__.py:3: error: Revealed type is 'def () -> m.one.One' main:2: error: Revealed type is 'def () -> m.one.One' + +[case testImportReExportInCycle] +from m import One +reveal_type(One) +[file m/__init__.py] +from .one import One +from .two import Two +reveal_type(One) +[file m/one.py] +class One: + pass +[file m/two.py] +import m +reveal_type(m.One) +class Two: + pass +[out] +tmp/m/two.py:2: error: Revealed type is 'def () -> m.one.One' +tmp/m/__init__.py:3: error: Revealed type is 'def () -> m.one.One' +main:2: error: Revealed type is 'def () -> m.one.One' From b5bf41d13fa3cfbc59a9ee4d91af48846be689bd Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 13 Feb 2018 21:18:20 -0800 Subject: [PATCH 09/14] Clean up lint and typecheck. --- mypy/semanal.py | 6 ++---- mypy/semanal_pass3.py | 7 +++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 54c454d423ca..b8a04a5b7b67 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -3337,7 +3337,7 @@ def visit_member_expr(self, expr: MemberExpr) -> None: base.accept(self) # Bind references to module attributes. if isinstance(base, RefExpr) and base.kind == MODULE_REF: - self.bind_module_attribute_reference(expr) + self.bind_module_attribute_reference(base, expr) elif isinstance(base, RefExpr): # This branch handles the case C.bar (or cls.bar or self.bar inside # a classmethod/method), where C is a class and bar is a type @@ -3367,8 +3367,7 @@ def visit_member_expr(self, expr: MemberExpr) -> None: expr.fullname = n.fullname expr.node = n.node - def bind_module_attribute_reference(self, expr: MemberExpr) -> None: - base = expr.expr + def bind_module_attribute_reference(self, base: RefExpr, expr: MemberExpr) -> None: # This method handles the case foo.bar where foo is a module. # In this case base.node is the module's MypyFile and we look up # bar in its namespace. This must be done for all types of bar. @@ -3415,7 +3414,6 @@ def bind_module_attribute_reference(self, expr: MemberExpr) -> None: self.fail("Module%s has no attribute %r (it's now called %r)" % ( mod_name, expr.name, obsolete_name_mapping[full_name]), expr) - def visit_op_expr(self, expr: OpExpr) -> None: expr.left.accept(self) diff --git a/mypy/semanal_pass3.py b/mypy/semanal_pass3.py index 4fe7e05fba48..aebd7309cc2f 100644 --- a/mypy/semanal_pass3.py +++ b/mypy/semanal_pass3.py @@ -290,8 +290,8 @@ def visit_member_expr(self, expr: MemberExpr) -> None: base = expr.expr base.accept(self) if isinstance(base, RefExpr) and base.kind == MODULE_REF: - if expr.kind == UNBOUND_IMPORTED: - module_id = expr.expr.name + if isinstance(base, NameExpr) and expr.kind == UNBOUND_IMPORTED: + module_id = base.name module = self.modules.get(module_id) node = module and module.names.get(expr.name) if node: @@ -299,10 +299,9 @@ def visit_member_expr(self, expr: MemberExpr) -> None: expr.node = node.node expr.fullname = node.fullname elif expr.kind is None: - self.sem.bind_module_attribute_reference(expr) + self.sem.bind_module_attribute_reference(base, expr) super().visit_member_expr(expr) - # Helpers def perform_transform(self, node: Union[Node, SymbolTableNode], From 06dbcc6553624c8f61b09d071deef3c59cca1f12 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 6 Mar 2018 01:22:20 -0700 Subject: [PATCH 10/14] Adjust for addition of cur_mod_node attribute. --- mypy/semanal_pass3.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/semanal_pass3.py b/mypy/semanal_pass3.py index 747343c818d4..884f6c46763b 100644 --- a/mypy/semanal_pass3.py +++ b/mypy/semanal_pass3.py @@ -63,13 +63,14 @@ 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.sem.cur_mod_node = self.cur_mod_node = file_node 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) self.scope.leave() del self.cur_mod_node + del self.sem.cur_mod_node self.patches = [] def refresh_partial(self, node: Union[MypyFile, FuncItem, OverloadedFuncDef], From 2efc322889debe714434881239fded4d66ce36b6 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 6 Mar 2018 02:00:14 -0700 Subject: [PATCH 11/14] Add support for type annotation. --- mypy/semanal_pass3.py | 5 +++++ mypy/typeanal.py | 2 +- mypy/types.py | 2 ++ test-data/unit/check-modules.test | 6 ++++++ 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/mypy/semanal_pass3.py b/mypy/semanal_pass3.py index 884f6c46763b..dcf356b966a9 100644 --- a/mypy/semanal_pass3.py +++ b/mypy/semanal_pass3.py @@ -221,6 +221,11 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: resulted from this assignment (if any). Currently this includes NewType, TypedDict, NamedTuple, and TypeVar. """ + if isinstance(s.type, AnyType) and s.type.type_of_any == TypeOfAny.from_unbound_import: + node = self.sem.globals.get(s.lvalues[0].name) + if node and node.type == s.type: + s.type = self.sem.anal_type(s.unanalyzed_type, third_pass=True) + node.node.type = s.type self.analyze(s.type, s) if isinstance(s.rvalue, IndexExpr) and isinstance(s.rvalue.analyzed, TypeAliasExpr): self.analyze(s.rvalue.analyzed.type, s.rvalue.analyzed, warn=True) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 337854d05709..4bef847bbd9b 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -190,7 +190,7 @@ def visit_unbound_type(self, t: UnboundType) -> Type: # UNBOUND_IMPORTED can happen if an unknown name was imported. if sym.kind != UNBOUND_IMPORTED: self.fail('Internal error (node is None, kind={})'.format(sym.kind), t) - return AnyType(TypeOfAny.special_form) + return AnyType(TypeOfAny.from_unbound_import) fullname = sym.node.fullname() hook = self.plugin.get_type_analyze_hook(fullname) if hook: diff --git a/mypy/types.py b/mypy/types.py index ef3b092d3a42..f634e4e5632d 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -284,6 +284,8 @@ class TypeOfAny(Enum): special_form = 'special_form' # Does this Any come from interaction with another Any? from_another_any = 'from_another_any' + # Does this Any come from an unresolved unbound import (e.g. from import cycle)? + from_unbound_import = 'from_unbound_import' class AnyType(Type): diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index 1edbded321bc..1010c9a1d8ff 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -1969,10 +1969,13 @@ class One: [file m/two.py] from m import One reveal_type(One) +x: One +reveal_type(x) class Two: pass [out] tmp/m/two.py:2: error: Revealed type is 'def () -> m.one.One' +tmp/m/two.py:4: error: Revealed type is 'm.one.One' tmp/m/__init__.py:3: error: Revealed type is 'def () -> m.one.One' main:2: error: Revealed type is 'def () -> m.one.One' @@ -1989,9 +1992,12 @@ class One: [file m/two.py] import m reveal_type(m.One) +x: m.One +reveal_type(x) class Two: pass [out] tmp/m/two.py:2: error: Revealed type is 'def () -> m.one.One' +tmp/m/two.py:4: error: Revealed type is 'm.one.One' tmp/m/__init__.py:3: error: Revealed type is 'def () -> m.one.One' main:2: error: Revealed type is 'def () -> m.one.One' From f7c3d43ce289264e0f3c14897a145be1f335f881 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 6 Mar 2018 02:24:02 -0700 Subject: [PATCH 12/14] Typecheck fixes and TODO comments. --- mypy/semanal_pass3.py | 9 +++++++-- test-data/unit/check-modules.test | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/mypy/semanal_pass3.py b/mypy/semanal_pass3.py index dcf356b966a9..74fd7dad7c5b 100644 --- a/mypy/semanal_pass3.py +++ b/mypy/semanal_pass3.py @@ -221,9 +221,12 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: resulted from this assignment (if any). Currently this includes NewType, TypedDict, NamedTuple, and TypeVar. """ - if isinstance(s.type, AnyType) and s.type.type_of_any == TypeOfAny.from_unbound_import: + if (isinstance(s.type, AnyType) and + s.type.type_of_any == TypeOfAny.from_unbound_import and + len(s.lvalues) == 1 and + isinstance(s.lvalues[0], NameExpr)): node = self.sem.globals.get(s.lvalues[0].name) - if node and node.type == s.type: + if node and isinstance(node.node, Var) and s.unanalyzed_type: s.type = self.sem.anal_type(s.unanalyzed_type, third_pass=True) node.node.type = s.type self.analyze(s.type, s) @@ -302,6 +305,8 @@ def visit_import_from(self, imp: ImportFrom) -> None: def visit_name_expr(self, expr: NameExpr) -> None: # Fixup remaining UNBOUND_IMPORTED nodes from import cycles if expr.kind == UNBOUND_IMPORTED: + # TODO: this works only for module-level attributes, because not all + # the namespace context is set up correctly in pass 3. n = self.sem.lookup(expr.name, expr, suppress_errors=True) if n: expr.kind = n.kind diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index 1010c9a1d8ff..934d383db4ef 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -1971,6 +1971,7 @@ from m import One reveal_type(One) x: One reveal_type(x) +# TODO also resolve One correctly when used as base class (currently resolves as Any) class Two: pass [out] From 5adf42b55cd9e23f27afdc8af882e0afb4ec8e0c Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 6 Mar 2018 02:42:43 -0700 Subject: [PATCH 13/14] Add tests for namedtuple and type alias. --- test-data/unit/check-modules.test | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index 934d383db4ef..f4834d8e73d5 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -2002,3 +2002,38 @@ tmp/m/two.py:2: error: Revealed type is 'def () -> m.one.One' tmp/m/two.py:4: error: Revealed type is 'm.one.One' tmp/m/__init__.py:3: error: Revealed type is 'def () -> m.one.One' main:2: error: Revealed type is 'def () -> m.one.One' + +[case testImportReExportedNamedTupleInCycle] +from m import One +[file m/__init__.py] +from .one import One +from .two import Two +[file m/one.py] +from typing import NamedTuple +class One(NamedTuple): + name: str +[file m/two.py] +import m +x = m.One(name="Foo") +reveal_type(x.name) +class Two: + pass +[out] +tmp/m/two.py:3: error: Revealed type is 'builtins.str' + +[case testImportReExportedTypeAliasInCycle] +from m import One +[file m/__init__.py] +from .one import One +from .two import Two +[file m/one.py] +from typing import Union +One = Union[int, str] +[file m/two.py] +import m +x: m.One +reveal_type(x) +class Two: + pass +[out] +tmp/m/two.py:3: error: Revealed type is 'Union[builtins.int, builtins.str]' From f6757069f89549bc33b240f02c7332ab67ec40f6 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 6 Mar 2018 02:48:51 -0700 Subject: [PATCH 14/14] Add test for incremental mode. --- test-data/unit/check-incremental.test | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 03582bad4e0e..729a098707e6 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -4127,3 +4127,26 @@ class Foo: x = Foo() [out] [out2] + +[case testImportReExportInCycle] +from m import One +[file m/__init__.py] +from .one import One +from .two import Two +[file m/one.py] +class One: + pass +[file m/two.py] +import m +class Two: + pass +[file m/one.py.2] +class One: + name: str +[file m/two.py.2] +import m +reveal_type(m.One.name) +class Two: + pass +[out2] +tmp/m/two.py:2: error: Revealed type is 'builtins.str'