diff --git a/mypy/build.py b/mypy/build.py index 0e612a2ebc63..cf0423bcca24 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -57,6 +57,7 @@ DUMP_TYPE_STATS = 'dump-type-stats' DUMP_INFER_STATS = 'dump-infer-stats' SILENT_IMPORTS = 'silent-imports' # Silence imports of .py files +ALMOST_SILENT = 'almost-silent' # If SILENT_IMPORTS: report silenced imports as errors INCREMENTAL = 'incremental' # Incremental mode: use the cache FAST_PARSER = 'fast-parser' # Use experimental fast parser # Disallow calling untyped functions from typed ones @@ -941,7 +942,7 @@ def __init__(self, manager: BuildManager, caller_state: 'State' = None, caller_line: int = 0, - is_ancestor: bool = False, + ancestor_for: 'State' = None, ) -> None: assert id or path or source is not None, "Neither id, path nor source given" self.manager = manager @@ -971,13 +972,18 @@ def __init__(self, if path: # In silent mode, don't import .py files, except from stubs. if (SILENT_IMPORTS in manager.flags and - path.endswith('.py') and (caller_state or is_ancestor)): + path.endswith('.py') and (caller_state or ancestor_for)): # (Never silence builtins, even if it's a .py file; # this can happen in tests!) if (id != 'builtins' and not ((caller_state and caller_state.tree and caller_state.tree.is_stub))): + if ALMOST_SILENT in manager.flags: + if ancestor_for: + self.skipping_ancestor(id, path, ancestor_for) + else: + self.skipping_module(id, path) path = None manager.missing_modules.add(id) raise ModuleNotFound @@ -986,10 +992,12 @@ def __init__(self, # misspelled module name, missing stub, module not in # search path or the module has not been installed. if caller_state: - if not (SILENT_IMPORTS in manager.flags or - (caller_state.tree is not None and - (caller_line in caller_state.tree.ignored_lines or - 'import' in caller_state.tree.weak_opts))): + suppress_message = ((SILENT_IMPORTS in manager.flags and + ALMOST_SILENT not in manager.flags) or + (caller_state.tree is not None and + (caller_line in caller_state.tree.ignored_lines or + 'import' in caller_state.tree.weak_opts))) + if not suppress_message: save_import_context = manager.errors.import_context() manager.errors.set_import_context(caller_state.import_context) manager.module_not_found(caller_state.xpath, caller_line, id) @@ -1015,6 +1023,36 @@ def __init__(self, # Parse the file (and then some) to get the dependencies. self.parse_file() + def skipping_ancestor(self, id: str, path: str, ancestor_for: 'State') -> None: + # TODO: Read the path (the __init__.py file) and return + # immediately if it's empty or only contains comments. + # But beware, some package may be the ancestor of many modules, + # so we'd need to cache the decision. + manager = self.manager + manager.errors.set_import_context([]) + manager.errors.set_file(ancestor_for.xpath) + manager.errors.report(-1, "Ancestor package '%s' silently ignored" % (id,), + severity='note', only_once=True) + manager.errors.report(-1, "(Using --silent-imports, submodule passed on command line)", + severity='note', only_once=True) + manager.errors.report(-1, "(This note brought to you by --almost-silent)", + severity='note', only_once=True) + + def skipping_module(self, id: str, path: str) -> None: + assert self.caller_state, (id, path) + manager = self.manager + save_import_context = manager.errors.import_context() + manager.errors.set_import_context(self.caller_state.import_context) + manager.errors.set_file(self.caller_state.xpath) + line = self.caller_line + manager.errors.report(line, "Import of '%s' silently ignored" % (id,), + severity='note') + manager.errors.report(line, "(Using --silent-imports, module not passed on command line)", + severity='note', only_once=True) + manager.errors.report(line, "(This note courtesy of --almost-silent)", + severity='note', only_once=True) + manager.errors.set_import_context(save_import_context) + def add_ancestors(self) -> None: # All parent packages are new ancestors. ancestors = [] @@ -1215,7 +1253,7 @@ def load_graph(sources: List[BuildSource], manager: BuildManager) -> Graph: # TODO: Why not 'if dep not in st.dependencies' ? # Ancestors don't have import context. newst = State(id=dep, path=None, source=None, manager=manager, - is_ancestor=True) + ancestor_for=st) else: newst = State(id=dep, path=None, source=None, manager=manager, caller_state=st, caller_line=st.dep_line_map.get(dep, 1)) diff --git a/mypy/main.py b/mypy/main.py index d8b18b1b617b..1fb54612b1c1 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -130,8 +130,12 @@ def parse_version(v): help='use Python x.y') parser.add_argument('--py2', dest='python_version', action='store_const', const=defaults.PYTHON2_VERSION, help="use Python 2 mode") - parser.add_argument('-s', '--silent-imports', '--silent', action='store_true', + parser.add_argument('-s', '--silent-imports', action='store_true', help="don't follow imports to .py files") + parser.add_argument('--silent', action='store_true', + help="deprecated name for --silent-imports") + parser.add_argument('--almost-silent', action='store_true', + help="like --silent-imports but reports the imports as errors") parser.add_argument('--disallow-untyped-calls', action='store_true', help="disallow calling functions without type annotations" " from functions with type annotations") @@ -208,8 +212,11 @@ def parse_version(v): if args.inferstats: options.build_flags.append(build.DUMP_INFER_STATS) - if args.silent_imports: + if args.silent_imports or args.silent: options.build_flags.append(build.SILENT_IMPORTS) + if args.almost_silent: + options.build_flags.append(build.SILENT_IMPORTS) + options.build_flags.append(build.ALMOST_SILENT) if args.disallow_untyped_calls: options.build_flags.append(build.DISALLOW_UNTYPED_CALLS) diff --git a/mypy/test/data/check-modules.test b/mypy/test/data/check-modules.test index 574e6de780eb..5665219f7ea2 100644 --- a/mypy/test/data/check-modules.test +++ b/mypy/test/data/check-modules.test @@ -699,6 +699,29 @@ class C(B): [out] tmp/bar.py:1: error: Module has no attribute 'B' +[case testImportSuppressedWhileAlmostSilent] +# cmd: mypy -m main +[file main.py] +# flags: silent-imports almost-silent +import mod +[file mod.py] +[builtins fixtures/module.py] +[out] +tmp/main.py:2: note: Import of 'mod' silently ignored +tmp/main.py:2: note: (Using --silent-imports, module not passed on command line) +tmp/main.py:2: note: (This note courtesy of --almost-silent) + +[case testAncestorSuppressedWhileAlmostSilent] +# cmd: mypy -m foo.bar +[file foo/bar.py] +# flags: silent-imports almost-silent +[file foo/__init__.py] +[builtins fixtures/module.py] +[out] +tmp/foo/bar.py: note: Ancestor package 'foo' silently ignored +tmp/foo/bar.py: note: (Using --silent-imports, submodule passed on command line) +tmp/foo/bar.py: note: (This note brought to you by --almost-silent) + [case testStubImportNonStubWhileSilent] # cmd: mypy -m main [file main.py]