From ee57713829cc7b4081bed3d193fcf78b46e15093 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 18 Jun 2018 02:39:24 +0100 Subject: [PATCH 01/11] Add basic support --- mypy/build.py | 11 ++++++- mypy/nodes.py | 1 + mypy/semanal_pass1.py | 3 ++ test-data/unit/check-modules.test | 48 +++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 1 deletion(-) diff --git a/mypy/build.py b/mypy/build.py index 6a31f1f89b4e5..3d2d955645c29 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -2305,7 +2305,7 @@ def find_module_and_diagnose(manager: BuildManager, # misspelled module name, missing stub, module not in # search path or the module has not been installed. if caller_state: - if not options.ignore_missing_imports: + if not (options.ignore_missing_imports or in_partial_package(id, manager)): module_not_found(manager, caller_line, caller_state, id) raise ModuleNotFound else: @@ -2315,6 +2315,15 @@ def find_module_and_diagnose(manager: BuildManager, raise CompileError(["mypy: can't find module '%s'" % id]) +def in_partial_package(id: str, manager: BuildManager) -> bool: + while '.' in id: + parent, _ = id.rsplit('.', 1) + if parent in manager.modules and manager.modules[parent].is_partial_stub_package: + return True + id = parent + return False + + def module_not_found(manager: BuildManager, line: int, caller_state: State, target: str) -> None: errors = manager.errors diff --git a/mypy/nodes.py b/mypy/nodes.py index 4f6ef124e3112..119eb7c9adb6c 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -219,6 +219,7 @@ class MypyFile(SymbolNode): is_stub = False # Is this loaded from the cache and thus missing the actual body of the file? is_cache_skeleton = False + is_partial_stub_package = False def __init__(self, defs: List[Statement], diff --git a/mypy/semanal_pass1.py b/mypy/semanal_pass1.py index 12da48e92813d..c2e43d1bdb332 100644 --- a/mypy/semanal_pass1.py +++ b/mypy/semanal_pass1.py @@ -154,6 +154,9 @@ def visit_func_def(self, func: FuncDef) -> None: func.is_conditional = sem.block_depth[-1] > 0 func._fullname = sem.qualified_name(func.name()) at_module = sem.is_module_scope() + if (at_module and func.name() == '__getattr__' and + '__init__.pyi' in self.sem.cur_mod_node.path): + self.sem.cur_mod_node.is_partial_stub_package = True if at_module and func.name() in sem.globals: # Already defined in this module. original_sym = sem.globals[func.name()] diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index 3e28bd5e9c6ed..315d2b5d653a0 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -2173,6 +2173,54 @@ from c import x x = str() y = int() +[case testModuleGetattrInit1] +from a import b + +x = b.f() +[file a/__init__.pyi] +from typing import Any +def __getattr__(attr: str) -> Any: pass +[out] + +[case testModuleGetattrInit2] +import a.b + +x = a.b.f() +[file a/__init__.pyi] +from typing import Any +def __getattr__(attr: str) -> Any: pass +[out] + +[case testModuleGetattrInit3] +import a.b + +x = a.b.f() +[file a/__init__.py] +from typing import Any +def __getattr__(attr: str) -> Any: pass +[out] +main:1: error: Cannot find module named 'a.b' +main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help) +main:3: error: "object" has no attribute "b" + +[case testModuleGetattrInit4] +import a.b.c + +x = a.b.c.f() +[file a/__init__.pyi] +from typing import Any +def __getattr__(attr: str) -> Any: pass +[out] + +[case testModuleGetattrInit5] +from a.b import f + +x = f() +[file a/__init__.pyi] +from typing import Any +def __getattr__(attr: str) -> Any: pass +[out] + [case testIndirectFromImportWithinCycleUsedAsBaseClass-skip] -- TODO: Fails because of missing ImportedName handling in mypy.typeanal import a From c72a65c49b6924b85846e14dbbccb2ff6f4ef38d Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 18 Jun 2018 03:47:54 +0100 Subject: [PATCH 02/11] Better support --- mypy/build.py | 32 ++++++++++++++++++++++++++------ mypy/nodes.py | 2 ++ mypy/semanal_pass1.py | 2 +- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 3d2d955645c29..b33002c2221cd 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -1738,6 +1738,7 @@ def __init__(self, caller_line: int = 0, ancestor_for: 'Optional[State]' = None, root_source: bool = False, + temporary: bool = False, ) -> None: assert id or path or source is not None, "Neither id, path nor source given" self.manager = manager @@ -1759,9 +1760,10 @@ def __init__(self, try: path, follow_imports = find_module_and_diagnose( manager, id, self.options, caller_state, caller_line, - ancestor_for, root_source) + ancestor_for, root_source, temporary) except ModuleNotFound: - manager.missing_modules.add(id) + if not temporary: + manager.missing_modules.add(id) raise if follow_imports == 'silent': self.ignore_all = True @@ -2242,7 +2244,8 @@ def find_module_and_diagnose(manager: BuildManager, caller_state: 'Optional[State]' = None, caller_line: int = 0, ancestor_for: 'Optional[State]' = None, - root_source: bool = False) -> Tuple[str, str]: + root_source: bool = False, + quick: bool = False) -> Tuple[str, str]: """Find a module by name, respecting follow_imports and producing diagnostics. Args: @@ -2283,7 +2286,8 @@ def find_module_and_diagnose(manager: BuildManager, and not options.follow_imports_for_stubs) # except when they aren't or id == 'builtins'): # Builtins is always normal follow_imports = 'normal' - + if quick: + return (path, follow_imports) if follow_imports == 'silent': # Still import it, but silence non-blocker errors. manager.log("Silencing %s (%s)" % (path, id)) @@ -2304,6 +2308,8 @@ def find_module_and_diagnose(manager: BuildManager, # Could not find a module. Typically the reason is a # misspelled module name, missing stub, module not in # search path or the module has not been installed. + if quick: + raise ModuleNotFound if caller_state: if not (options.ignore_missing_imports or in_partial_package(id, manager)): module_not_found(manager, caller_line, caller_state, id) @@ -2318,8 +2324,22 @@ def find_module_and_diagnose(manager: BuildManager, def in_partial_package(id: str, manager: BuildManager) -> bool: while '.' in id: parent, _ = id.rsplit('.', 1) - if parent in manager.modules and manager.modules[parent].is_partial_stub_package: - return True + parent_mod = None + if parent in manager.modules: + parent_mod = manager.modules[parent] + else: + try: + parent_st = State(id=parent, path=None, source=None, manager=manager, + temporary=True) + except (ModuleNotFound, CompileError): + pass + else: + parent_mod = parent_st.tree + if parent_mod: + if parent_mod.is_partial_stub_package: + return True + if not parent_mod.is_stub: + return False id = parent return False diff --git a/mypy/nodes.py b/mypy/nodes.py index 119eb7c9adb6c..0f417f3f1c387 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -257,6 +257,7 @@ def serialize(self) -> JsonDict: 'names': self.names.serialize(self._fullname), 'is_stub': self.is_stub, 'path': self.path, + 'is_partial_stub_package': self.is_partial_stub_package } @classmethod @@ -268,6 +269,7 @@ def deserialize(cls, data: JsonDict) -> 'MypyFile': tree.names = SymbolTable.deserialize(data['names']) tree.is_stub = data['is_stub'] tree.path = data['path'] + tree.is_partial_stub_package = data['is_partial_stub_package'] tree.is_cache_skeleton = True return tree diff --git a/mypy/semanal_pass1.py b/mypy/semanal_pass1.py index c2e43d1bdb332..c70113f2291dd 100644 --- a/mypy/semanal_pass1.py +++ b/mypy/semanal_pass1.py @@ -155,7 +155,7 @@ def visit_func_def(self, func: FuncDef) -> None: func._fullname = sem.qualified_name(func.name()) at_module = sem.is_module_scope() if (at_module and func.name() == '__getattr__' and - '__init__.pyi' in self.sem.cur_mod_node.path): + self.sem.cur_mod_node.is_package_init_file() and self.sem.cur_mod_node.is_stub): self.sem.cur_mod_node.is_partial_stub_package = True if at_module and func.name() in sem.globals: # Already defined in this module. From 1debe2fd69b256d4b67b67c47ce49af2ec26a034 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 18 Jun 2018 12:29:28 +0100 Subject: [PATCH 03/11] Add some comments and docstrings --- mypy/build.py | 12 ++++++++++++ mypy/nodes.py | 3 +++ 2 files changed, 15 insertions(+) diff --git a/mypy/build.py b/mypy/build.py index b33002c2221cd..58626a7b6b86d 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -1738,6 +1738,10 @@ def __init__(self, caller_line: int = 0, ancestor_for: 'Optional[State]' = None, root_source: bool = False, + # If `temporary` is True, this State is beeing created to just + # quickly parse/load the tree, without an intention to further + # process it. With this flag, any changes to external state as well + # as error reprting should be avoided. temporary: bool = False, ) -> None: assert id or path or source is not None, "Neither id, path nor source given" @@ -2255,6 +2259,7 @@ def find_module_and_diagnose(manager: BuildManager, caller_line: the line number of the import ancestor_for: the child module this is an ancestor of, if applicable root_source: whether this source was specified on the command line + quick: skip any error diagnosis and reporting The specified value of follow_imports for a module can be overridden if the module is specified on the command line or if it is a stub, @@ -2322,12 +2327,18 @@ def find_module_and_diagnose(manager: BuildManager, def in_partial_package(id: str, manager: BuildManager) -> bool: + """Check if a missing module can potentially be a part of a package. + + This checks if there is any existing parent __init__.pyi stub that + defines a module-level __getattr__ (a.k.a. partial stub package). + """ while '.' in id: parent, _ = id.rsplit('.', 1) parent_mod = None if parent in manager.modules: parent_mod = manager.modules[parent] else: + # Parent is not in build, try quickly if we can find it. try: parent_st = State(id=parent, path=None, source=None, manager=manager, temporary=True) @@ -2339,6 +2350,7 @@ def in_partial_package(id: str, manager: BuildManager) -> bool: if parent_mod.is_partial_stub_package: return True if not parent_mod.is_stub: + # Bail out soon, only stub package can be partial, not runtime one. return False id = parent return False diff --git a/mypy/nodes.py b/mypy/nodes.py index 0f417f3f1c387..1e1b691d0dc77 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -219,6 +219,9 @@ class MypyFile(SymbolNode): is_stub = False # Is this loaded from the cache and thus missing the actual body of the file? is_cache_skeleton = False + # Is this representns an __init__.pyi stub with a module __getattr__ + # (i.e. a partial stub package), for such packages we suppress any missing + # module errors in addition to missing attribute errors. is_partial_stub_package = False def __init__(self, From aa4a9d473d79256e5f3c162a27312a319f89c8eb Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 18 Jun 2018 13:10:38 +0100 Subject: [PATCH 04/11] Be more strict with return type --- mypy/semanal_pass1.py | 11 +++++++++-- test-data/unit/check-modules.test | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/mypy/semanal_pass1.py b/mypy/semanal_pass1.py index c70113f2291dd..f0f00fb919ab9 100644 --- a/mypy/semanal_pass1.py +++ b/mypy/semanal_pass1.py @@ -26,7 +26,7 @@ TryStmt, OverloadedFuncDef, Lvalue, Context, ImportedName, LDEF, GDEF, MDEF, UNBOUND_IMPORTED, MODULE_REF, implicit_module_attrs ) -from mypy.types import Type, UnboundType, UnionType, AnyType, TypeOfAny, NoneTyp +from mypy.types import Type, UnboundType, UnionType, AnyType, TypeOfAny, NoneTyp, CallableType from mypy.semanal import SemanticAnalyzerPass2, infer_reachability_of_if_statement from mypy.semanal_shared import create_indirect_imported_name from mypy.options import Options @@ -156,7 +156,14 @@ def visit_func_def(self, func: FuncDef) -> None: at_module = sem.is_module_scope() if (at_module and func.name() == '__getattr__' and self.sem.cur_mod_node.is_package_init_file() and self.sem.cur_mod_node.is_stub): - self.sem.cur_mod_node.is_partial_stub_package = True + if isinstance(func.type, CallableType): + ret = func.type.ret_type + if isinstance(ret, UnboundType) and not ret.args: + sym = self.sem.lookup_qualified(ret.name, func, suppress_errors=True) + # We only interpred package as partial if __getattr__ return type + # is either types.ModuleType of Any. + if sym and sym.node and sym.node.fullname() in ('types.ModuleType', 'typing.Any'): + self.sem.cur_mod_node.is_partial_stub_package = True if at_module and func.name() in sem.globals: # Already defined in this module. original_sym = sem.globals[func.name()] diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index 315d2b5d653a0..065947289f132 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -2221,6 +2221,26 @@ from typing import Any def __getattr__(attr: str) -> Any: pass [out] +[case testModuleGetattrInit5a] +from a.b import f + +x = f() +[file a/__init__.pyi] +from types import ModuleType +def __getattr__(attr: str) -> ModuleType: pass +[out] + + +[case testModuleGetattrInit5b] +from a.b import f + +x = f() +[file a/__init__.pyi] +def __getattr__(attr: str) -> int: pass +[out] +main:1: error: Cannot find module named 'a.b' +main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help) + [case testIndirectFromImportWithinCycleUsedAsBaseClass-skip] -- TODO: Fails because of missing ImportedName handling in mypy.typeanal import a From 1ac99a9805efbb7b3690ffc48b8208ee4daa9469 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 18 Jun 2018 13:16:01 +0100 Subject: [PATCH 05/11] Fix lint --- mypy/semanal_pass1.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/semanal_pass1.py b/mypy/semanal_pass1.py index f0f00fb919ab9..e027ccd84f93c 100644 --- a/mypy/semanal_pass1.py +++ b/mypy/semanal_pass1.py @@ -162,7 +162,8 @@ def visit_func_def(self, func: FuncDef) -> None: sym = self.sem.lookup_qualified(ret.name, func, suppress_errors=True) # We only interpred package as partial if __getattr__ return type # is either types.ModuleType of Any. - if sym and sym.node and sym.node.fullname() in ('types.ModuleType', 'typing.Any'): + if sym and sym.node and sym.node.fullname() in ('types.ModuleType', + 'typing.Any'): self.sem.cur_mod_node.is_partial_stub_package = True if at_module and func.name() in sem.globals: # Already defined in this module. From e6d458e2ffd3b92c0f55137dd3c27e2e4ced9a6b Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 18 Jun 2018 13:49:32 +0100 Subject: [PATCH 06/11] Tighted subpackages --- mypy/build.py | 4 ++-- test-data/unit/check-modules.test | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 58626a7b6b86d..854d202380054 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -2349,8 +2349,8 @@ def in_partial_package(id: str, manager: BuildManager) -> bool: if parent_mod: if parent_mod.is_partial_stub_package: return True - if not parent_mod.is_stub: - # Bail out soon, only stub package can be partial, not runtime one. + else: + # Bail out soon, complete subpackage found return False id = parent return False diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index 065947289f132..2d8512f98f44e 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -2241,6 +2241,22 @@ def __getattr__(attr: str) -> int: pass main:1: error: Cannot find module named 'a.b' main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help) +[case testModuleGetattrInit8] +import a.b.c.d + +x = a.b.c.d.f() +[file a/__init__.pyi] +from typing import Any +def __getattr__(attr: str) -> Any: pass +[file a/b/__init__.pyi] +# empty (i.e. complete subpackage) +[builtins fixtures/module.pyi] +[out] +main:1: error: Cannot find module named 'a.b.c' +main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help) +main:1: error: Cannot find module named 'a.b.c.d' +main:3: error: Module has no attribute "c" + [case testIndirectFromImportWithinCycleUsedAsBaseClass-skip] -- TODO: Fails because of missing ImportedName handling in mypy.typeanal import a From e3185d8397ddf90e403fb28947a50f509beba9b3 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 18 Jun 2018 13:59:52 +0100 Subject: [PATCH 07/11] Add one more test --- test-data/unit/check-modules.test | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index 2d8512f98f44e..fe68e35a14d33 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -2257,6 +2257,19 @@ main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" main:1: error: Cannot find module named 'a.b.c.d' main:3: error: Module has no attribute "c" +[case testModuleGetattrInit8a] +import a.b.c # Error +import a.d # OK +[file a/__init__.pyi] +from typing import Any +def __getattr__(attr: str) -> Any: pass +[file a/b/__init__.pyi] +# empty (i.e. complete subpackage) +[builtins fixtures/module.pyi] +[out] +main:1: error: Cannot find module named 'a.b.c' +main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help) + [case testIndirectFromImportWithinCycleUsedAsBaseClass-skip] -- TODO: Fails because of missing ImportedName handling in mypy.typeanal import a From 1fb3b1d984e3e24c8a5b949082f0b02607d7f2b9 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 19 Jun 2018 12:35:12 +0100 Subject: [PATCH 08/11] Address CR first part --- mypy/build.py | 2 +- mypy/nodes.py | 2 +- mypy/semanal_pass1.py | 2 +- test-data/unit/check-modules.test | 27 +++++++++++++++++---------- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 854d202380054..530493825beb5 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -1741,7 +1741,7 @@ def __init__(self, # If `temporary` is True, this State is beeing created to just # quickly parse/load the tree, without an intention to further # process it. With this flag, any changes to external state as well - # as error reprting should be avoided. + # as error reporting should be avoided. temporary: bool = False, ) -> None: assert id or path or source is not None, "Neither id, path nor source given" diff --git a/mypy/nodes.py b/mypy/nodes.py index 1e1b691d0dc77..36690d4ff7eac 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -219,7 +219,7 @@ class MypyFile(SymbolNode): is_stub = False # Is this loaded from the cache and thus missing the actual body of the file? is_cache_skeleton = False - # Is this representns an __init__.pyi stub with a module __getattr__ + # Does this represent an __init__.pyi stub with a module __getattr__ # (i.e. a partial stub package), for such packages we suppress any missing # module errors in addition to missing attribute errors. is_partial_stub_package = False diff --git a/mypy/semanal_pass1.py b/mypy/semanal_pass1.py index e027ccd84f93c..87e728f69e5e6 100644 --- a/mypy/semanal_pass1.py +++ b/mypy/semanal_pass1.py @@ -160,7 +160,7 @@ def visit_func_def(self, func: FuncDef) -> None: ret = func.type.ret_type if isinstance(ret, UnboundType) and not ret.args: sym = self.sem.lookup_qualified(ret.name, func, suppress_errors=True) - # We only interpred package as partial if __getattr__ return type + # We only interpret a package as partial if the __getattr__ return type # is either types.ModuleType of Any. if sym and sym.node and sym.node.fullname() in ('types.ModuleType', 'typing.Any'): diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index fe68e35a14d33..e719a8df169d4 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -2179,7 +2179,8 @@ from a import b x = b.f() [file a/__init__.pyi] from typing import Any -def __getattr__(attr: str) -> Any: pass +def __getattr__(attr: str) -> Any: ... +[builtins fixtures/module.pyi] [out] [case testModuleGetattrInit2] @@ -2188,7 +2189,8 @@ import a.b x = a.b.f() [file a/__init__.pyi] from typing import Any -def __getattr__(attr: str) -> Any: pass +def __getattr__(attr: str) -> Any: ... +[builtins fixtures/module.pyi] [out] [case testModuleGetattrInit3] @@ -2197,11 +2199,12 @@ import a.b x = a.b.f() [file a/__init__.py] from typing import Any -def __getattr__(attr: str) -> Any: pass +def __getattr__(attr: str) -> Any: ... +[builtins fixtures/module.pyi] [out] main:1: error: Cannot find module named 'a.b' main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help) -main:3: error: "object" has no attribute "b" +main:3: error: Module has no attribute "b" [case testModuleGetattrInit4] import a.b.c @@ -2209,7 +2212,8 @@ import a.b.c x = a.b.c.f() [file a/__init__.pyi] from typing import Any -def __getattr__(attr: str) -> Any: pass +def __getattr__(attr: str) -> Any: ... +[builtins fixtures/module.pyi] [out] [case testModuleGetattrInit5] @@ -2218,7 +2222,8 @@ from a.b import f x = f() [file a/__init__.pyi] from typing import Any -def __getattr__(attr: str) -> Any: pass +def __getattr__(attr: str) -> Any: ... +[builtins fixtures/module.pyi] [out] [case testModuleGetattrInit5a] @@ -2227,7 +2232,8 @@ from a.b import f x = f() [file a/__init__.pyi] from types import ModuleType -def __getattr__(attr: str) -> ModuleType: pass +def __getattr__(attr: str) -> ModuleType: ... +[builtins fixtures/module.pyi] [out] @@ -2236,7 +2242,8 @@ from a.b import f x = f() [file a/__init__.pyi] -def __getattr__(attr: str) -> int: pass +def __getattr__(attr: str) -> int: ... +[builtins fixtures/module.pyi] [out] main:1: error: Cannot find module named 'a.b' main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help) @@ -2247,7 +2254,7 @@ import a.b.c.d x = a.b.c.d.f() [file a/__init__.pyi] from typing import Any -def __getattr__(attr: str) -> Any: pass +def __getattr__(attr: str) -> Any: ... [file a/b/__init__.pyi] # empty (i.e. complete subpackage) [builtins fixtures/module.pyi] @@ -2262,7 +2269,7 @@ import a.b.c # Error import a.d # OK [file a/__init__.pyi] from typing import Any -def __getattr__(attr: str) -> Any: pass +def __getattr__(attr: str) -> Any: ... [file a/b/__init__.pyi] # empty (i.e. complete subpackage) [builtins fixtures/module.pyi] From c0bf5481f4d2c1f9c98bcee5fcc97e52aeca1d93 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 19 Jun 2018 12:53:10 +0100 Subject: [PATCH 09/11] Address CR: second part --- mypy/build.py | 23 ++++++++++++----------- mypy/nodes.py | 2 +- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 530493825beb5..c46d7b501b558 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -1738,7 +1738,7 @@ def __init__(self, caller_line: int = 0, ancestor_for: 'Optional[State]' = None, root_source: bool = False, - # If `temporary` is True, this State is beeing created to just + # If `temporary` is True, this State is being created to just # quickly parse/load the tree, without an intention to further # process it. With this flag, any changes to external state as well # as error reporting should be avoided. @@ -1764,7 +1764,7 @@ def __init__(self, try: path, follow_imports = find_module_and_diagnose( manager, id, self.options, caller_state, caller_line, - ancestor_for, root_source, temporary) + ancestor_for, root_source, skip_diagnose=temporary) except ModuleNotFound: if not temporary: manager.missing_modules.add(id) @@ -2249,9 +2249,10 @@ def find_module_and_diagnose(manager: BuildManager, caller_line: int = 0, ancestor_for: 'Optional[State]' = None, root_source: bool = False, - quick: bool = False) -> Tuple[str, str]: + skip_diagnose: bool = False) -> Tuple[str, str]: """Find a module by name, respecting follow_imports and producing diagnostics. + If the module is not found, then the ModuleNotFound exception is raised. Args: id: module to find options: the options for the module being loaded @@ -2259,7 +2260,8 @@ def find_module_and_diagnose(manager: BuildManager, caller_line: the line number of the import ancestor_for: the child module this is an ancestor of, if applicable root_source: whether this source was specified on the command line - quick: skip any error diagnosis and reporting + skip_diagnose: skip any error diagnosis and reporting (but ModuleNotFound is + still raised if the module is missing) The specified value of follow_imports for a module can be overridden if the module is specified on the command line or if it is a stub, @@ -2291,9 +2293,9 @@ def find_module_and_diagnose(manager: BuildManager, and not options.follow_imports_for_stubs) # except when they aren't or id == 'builtins'): # Builtins is always normal follow_imports = 'normal' - if quick: - return (path, follow_imports) - if follow_imports == 'silent': + if skip_diagnose: + pass + elif follow_imports == 'silent': # Still import it, but silence non-blocker errors. manager.log("Silencing %s (%s)" % (path, id)) elif follow_imports == 'skip' or follow_imports == 'error': @@ -2313,7 +2315,7 @@ def find_module_and_diagnose(manager: BuildManager, # Could not find a module. Typically the reason is a # misspelled module name, missing stub, module not in # search path or the module has not been installed. - if quick: + if skip_diagnose: raise ModuleNotFound if caller_state: if not (options.ignore_missing_imports or in_partial_package(id, manager)): @@ -2334,7 +2336,6 @@ def in_partial_package(id: str, manager: BuildManager) -> bool: """ while '.' in id: parent, _ = id.rsplit('.', 1) - parent_mod = None if parent in manager.modules: parent_mod = manager.modules[parent] else: @@ -2343,10 +2344,10 @@ def in_partial_package(id: str, manager: BuildManager) -> bool: parent_st = State(id=parent, path=None, source=None, manager=manager, temporary=True) except (ModuleNotFound, CompileError): - pass + parent_mod = None else: parent_mod = parent_st.tree - if parent_mod: + if parent_mod is not None: if parent_mod.is_partial_stub_package: return True else: diff --git a/mypy/nodes.py b/mypy/nodes.py index 36690d4ff7eac..fd68a83ae12f5 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -260,7 +260,7 @@ def serialize(self) -> JsonDict: 'names': self.names.serialize(self._fullname), 'is_stub': self.is_stub, 'path': self.path, - 'is_partial_stub_package': self.is_partial_stub_package + 'is_partial_stub_package': self.is_partial_stub_package, } @classmethod From 700391e84abbead39f42a193553f2fbebb3179cf Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 25 Jun 2018 17:53:13 +0100 Subject: [PATCH 10/11] Fix self-check; add one test case --- mypy/build.py | 2 +- test-data/unit/check-modules.test | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/mypy/build.py b/mypy/build.py index c46d7b501b558..a34610ff4b3dd 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -2337,7 +2337,7 @@ def in_partial_package(id: str, manager: BuildManager) -> bool: while '.' in id: parent, _ = id.rsplit('.', 1) if parent in manager.modules: - parent_mod = manager.modules[parent] + parent_mod = manager.modules[parent] # type: Optional[MypyFile] else: # Parent is not in build, try quickly if we can find it. try: diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index e719a8df169d4..2ba097e40d25a 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -2277,6 +2277,26 @@ def __getattr__(attr: str) -> Any: ... main:1: error: Cannot find module named 'a.b.c' main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help) +[case testModuleGetattrInit10] +# flags: --config-file tmp/mypy.ini +import a.b.c # silenced +import a.b.d # error + +[file a/__init__.pyi] +from typing import Any +def __getattr__(attr: str) -> Any: ... +[file a/b/__init__.pyi] +# empty (i.e. complete subpackage) + +[file mypy.ini] +[[mypy] +[[mypy-a.b.c] +ignore_missing_imports = True +[builtins fixtures/module.pyi] +[out] +main:3: error: Cannot find module named 'a.b.d' +main:3: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help) + [case testIndirectFromImportWithinCycleUsedAsBaseClass-skip] -- TODO: Fails because of missing ImportedName handling in mypy.typeanal import a From daede0ff6f17e31ca46d1ade5b167682751d127c Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 25 Jun 2018 17:57:41 +0100 Subject: [PATCH 11/11] Fix docstring formatting; update tests --- mypy/build.py | 1 + test-data/unit/check-modules.test | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 8f42615f7e640..5a0a8c617d972 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -2276,6 +2276,7 @@ def find_module_and_diagnose(manager: BuildManager, """Find a module by name, respecting follow_imports and producing diagnostics. If the module is not found, then the ModuleNotFound exception is raised. + Args: id: module to find options: the options for the module being loaded diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index d2a859a144e19..8e96a228d107d 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -2204,7 +2204,6 @@ def __getattr__(attr: str) -> Any: ... [out] main:1: error: Cannot find module named 'a.b' main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help) -main:3: error: Module has no attribute "b" [case testModuleGetattrInit4] import a.b.c @@ -2262,7 +2261,6 @@ def __getattr__(attr: str) -> Any: ... main:1: error: Cannot find module named 'a.b.c' main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help) main:1: error: Cannot find module named 'a.b.c.d' -main:3: error: Module has no attribute "c" [case testModuleGetattrInit8a] import a.b.c # Error