Skip to content

Commit 382062a

Browse files
authored
Change flag names for silent modes and add a new mode ("follow silently") (#2513)
There are now two flags: ``` --ignore-missing-imports silently ignore imports of missing modules --follow-imports {normal,silent,skip,error} how to treat imports (default normal) ``` The first is pretty clear. The second works as follows (I still have to update the docs). It only applies to .py modules (i.e. not to .pyi stubs) that are not specified on the command line but are needed to satisfy some import(s) and are found on the module search path (MYPYPATH plus the defaults derived from the command line). The values mean the following: - normal: process such modules normally (parse, analyze etc.) - silent: process normally but silence (ignore) any non-blocker errors in those modules - skip: silently don't process at all; the module is given type `Any` - error: don't process at all and give a single error message for each such module Note that when these flags are used as per-file flags, the pattern should match the imported module, not the importing module.
1 parent 823e1ff commit 382062a

13 files changed

+409
-125
lines changed

mypy/build.py

Lines changed: 40 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ def default_lib_path(data_dir: str,
301301
])
302302
# NOTE: dependencies + suppressed == all reachable imports;
303303
# suppressed contains those reachable imports that were prevented by
304-
# --silent-imports or simply not found.
304+
# silent mode or simply not found.
305305

306306

307307
# Priorities used for imports. (Here, top-level includes inside a class.)
@@ -481,7 +481,7 @@ def module_not_found(self, path: str, line: int, id: str) -> None:
481481
else:
482482
self.errors.report(line, 0, "Cannot find module named '{}'".format(id))
483483
self.errors.report(line, 0, '(Perhaps setting MYPYPATH '
484-
'or using the "--silent-imports" flag would help)',
484+
'or using the "--ignore-missing-imports" flag would help)',
485485
severity='note', only_once=True)
486486

487487
def report_file(self, file: MypyFile, type_map: Dict[Expression, Type]) -> None:
@@ -1101,6 +1101,9 @@ class State:
11011101
# Options, specialized for this file
11021102
options = None # type: Options
11031103

1104+
# Whether to ignore all errors
1105+
ignore_all = False
1106+
11041107
def __init__(self,
11051108
id: Optional[str],
11061109
path: Optional[str],
@@ -1137,16 +1140,25 @@ def __init__(self,
11371140
file_id = '__builtin__'
11381141
path = find_module(file_id, manager.lib_path)
11391142
if path:
1140-
# In silent mode, don't import .py files, except from stubs.
1141-
if (self.options.silent_imports and
1142-
path.endswith('.py') and (caller_state or ancestor_for)):
1143-
# (Never silence builtins, even if it's a .py file;
1144-
# this can happen in tests!)
1145-
if (id != 'builtins' and
1146-
not ((caller_state and
1147-
caller_state.tree and
1148-
caller_state.tree.is_stub))):
1149-
if self.options.almost_silent:
1143+
# For non-stubs, look at options.follow_imports:
1144+
# - normal (default) -> fully analyze
1145+
# - silent -> analyze but silence errors
1146+
# - skip -> don't analyze, make the type Any
1147+
follow_imports = self.options.follow_imports
1148+
if (follow_imports != 'normal'
1149+
and path.endswith('.py') # Stubs are always normal
1150+
and id != 'builtins' # Builtins is always normal
1151+
and not (caller_state and
1152+
caller_state.tree and
1153+
caller_state.tree.is_stub)):
1154+
if follow_imports == 'silent':
1155+
# Still import it, but silence non-blocker errors.
1156+
manager.log("Silencing %s (%s)" % (path, id))
1157+
self.ignore_all = True
1158+
else:
1159+
# In 'error' mode, produce special error messages.
1160+
manager.log("Skipping %s (%s)" % (path, id))
1161+
if follow_imports == 'error':
11501162
if ancestor_for:
11511163
self.skipping_ancestor(id, path, ancestor_for)
11521164
else:
@@ -1159,9 +1171,7 @@ def __init__(self,
11591171
# misspelled module name, missing stub, module not in
11601172
# search path or the module has not been installed.
11611173
if caller_state:
1162-
suppress_message = (self.options.silent_imports
1163-
and not self.options.almost_silent)
1164-
if not suppress_message:
1174+
if not self.options.ignore_missing_imports:
11651175
save_import_context = manager.errors.import_context()
11661176
manager.errors.set_import_context(caller_state.import_context)
11671177
manager.module_not_found(caller_state.xpath, caller_line, id)
@@ -1207,11 +1217,10 @@ def skipping_ancestor(self, id: str, path: str, ancestor_for: 'State') -> None:
12071217
manager = self.manager
12081218
manager.errors.set_import_context([])
12091219
manager.errors.set_file(ancestor_for.xpath)
1210-
manager.errors.report(-1, -1, "Ancestor package '%s' silently ignored" % (id,),
1220+
manager.errors.report(-1, -1, "Ancestor package '%s' ignored" % (id,),
12111221
severity='note', only_once=True)
1212-
manager.errors.report(-1, -1, "(Using --silent-imports, submodule passed on command line)",
1213-
severity='note', only_once=True)
1214-
manager.errors.report(-1, -1, "(This note brought to you by --almost-silent)",
1222+
manager.errors.report(-1, -1,
1223+
"(Using --follow-imports=error, submodule passed on command line)",
12151224
severity='note', only_once=True)
12161225

12171226
def skipping_module(self, id: str, path: str) -> None:
@@ -1222,12 +1231,10 @@ def skipping_module(self, id: str, path: str) -> None:
12221231
manager.errors.set_file(self.caller_state.xpath)
12231232
line = self.caller_line
12241233
manager.errors.report(line, 0,
1225-
"Import of '%s' silently ignored" % (id,),
1234+
"Import of '%s' ignored" % (id,),
12261235
severity='note')
12271236
manager.errors.report(line, 0,
1228-
"(Using --silent-imports, module not passed on command line)",
1229-
severity='note', only_once=True)
1230-
manager.errors.report(line, 0, "(This note courtesy of --almost-silent)",
1237+
"(Using --follow-imports=error, module not passed on command line)",
12311238
severity='note', only_once=True)
12321239
manager.errors.set_import_context(save_import_context)
12331240

@@ -1244,7 +1251,7 @@ def is_fresh(self) -> bool:
12441251
"""Return whether the cache data for this file is fresh."""
12451252
# NOTE: self.dependencies may differ from
12461253
# self.meta.dependencies when a dependency is dropped due to
1247-
# suppression by --silent-imports. However when a suppressed
1254+
# suppression by silent mode. However when a suppressed
12481255
# dependency is added back we find out later in the process.
12491256
return (self.meta is not None
12501257
and self.is_interface_fresh()
@@ -1303,24 +1310,25 @@ def calculate_mros(self) -> None:
13031310
fixup_module_pass_two(self.tree, self.manager.modules)
13041311

13051312
def fix_suppressed_dependencies(self, graph: Graph) -> None:
1306-
"""Corrects whether dependencies are considered stale or not when using silent_imports.
1313+
"""Corrects whether dependencies are considered stale in silent mode.
13071314
1308-
This method is a hack to correct imports in silent_imports + incremental mode.
1315+
This method is a hack to correct imports in silent mode + incremental mode.
13091316
In particular, the problem is that when running mypy with a cold cache, the
13101317
`parse_file(...)` function is called *at the start* of the `load_graph(...)` function.
13111318
Note that load_graph will mark some dependencies as suppressed if they weren't specified
1312-
on the command line in silent_imports mode.
1319+
on the command line in silent mode.
13131320
13141321
However, if the interface for a module is changed, parse_file will be called within
13151322
`process_stale_scc` -- *after* load_graph is finished, wiping out the changes load_graph
13161323
previously made.
13171324
13181325
This method is meant to be run after parse_file finishes in process_stale_scc and will
1319-
recompute what modules should be considered suppressed in silent_import mode.
1326+
recompute what modules should be considered suppressed in silent mode.
13201327
"""
13211328
# TODO: See if it's possible to move this check directly into parse_file in some way.
13221329
# TODO: Find a way to write a test case for this fix.
1323-
silent_mode = self.options.silent_imports or self.options.almost_silent
1330+
silent_mode = (self.options.ignore_missing_imports or
1331+
self.options.follow_imports == 'skip')
13241332
if not silent_mode:
13251333
return
13261334

@@ -1360,7 +1368,8 @@ def parse_file(self) -> None:
13601368
except (UnicodeDecodeError, DecodeError) as decodeerr:
13611369
raise CompileError([
13621370
"mypy: can't decode file '{}': {}".format(self.path, str(decodeerr))])
1363-
self.tree = manager.parse_file(self.id, self.xpath, source, self.options.ignore_errors)
1371+
self.tree = manager.parse_file(self.id, self.xpath, source,
1372+
self.ignore_all or self.options.ignore_errors)
13641373

13651374
modules[self.id] = self.tree
13661375

@@ -1413,7 +1422,7 @@ def parse_file(self) -> None:
14131422
# NOTE: What to do about race conditions (like editing the
14141423
# file while mypy runs)? A previous version of this code
14151424
# explicitly checked for this, but ran afoul of other reasons
1416-
# for differences (e.g. --silent-imports).
1425+
# for differences (e.g. silent mode).
14171426
self.dependencies = dependencies
14181427
self.suppressed = suppressed
14191428
self.priorities = priorities

mypy/errors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ def report(self, line: int, column: int, message: str, blocker: bool = False,
204204

205205
def add_error_info(self, info: ErrorInfo) -> None:
206206
(file, line) = info.origin
207-
if not info.blocker:
207+
if not info.blocker: # Blockers cannot be ignored
208208
if file in self.ignored_lines and line in self.ignored_lines[file]:
209209
# Annotation requests us to ignore all errors on this line.
210210
self.used_ignored_lines[file].add(line)

mypy/main.py

Lines changed: 53 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,10 @@ def process_options(args: List[str],
148148
"(defaults to sys.platform).")
149149
parser.add_argument('-2', '--py2', dest='python_version', action='store_const',
150150
const=defaults.PYTHON2_VERSION, help="use Python 2 mode")
151-
parser.add_argument('-s', '--silent-imports', action='store_true',
152-
help="don't follow imports to .py files")
153-
parser.add_argument('--almost-silent', action='store_true',
154-
help="like --silent-imports but reports the imports as errors")
151+
parser.add_argument('--ignore-missing-imports', action='store_true',
152+
help="silently ignore imports of missing modules")
153+
parser.add_argument('--follow-imports', choices=['normal', 'silent', 'skip', 'error'],
154+
default='normal', help="how to treat imports (default normal)")
155155
parser.add_argument('--disallow-untyped-calls', action='store_true',
156156
help="disallow calling functions without type annotations"
157157
" from functions with type annotations")
@@ -175,7 +175,7 @@ def process_options(args: List[str],
175175
dest='hide_error_context',
176176
help="Hide context notes before errors")
177177
parser.add_argument('--fast-parser', action='store_true',
178-
help="enable experimental fast parser")
178+
help="enable fast parser (recommended except on Windows)")
179179
parser.add_argument('-i', '--incremental', action='store_true',
180180
help="enable experimental module cache")
181181
parser.add_argument('--cache-dir', action='store', metavar='DIR',
@@ -223,14 +223,18 @@ def process_options(args: List[str],
223223
# is easier to debug).
224224
parser.add_argument('--debug-cache', action='store_true', help=argparse.SUPPRESS)
225225
# deprecated options
226-
parser.add_argument('--silent', action='store_true', dest='special-opts:silent',
227-
help=argparse.SUPPRESS)
228226
parser.add_argument('-f', '--dirty-stubs', action='store_true',
229227
dest='special-opts:dirty_stubs',
230228
help=argparse.SUPPRESS)
231229
parser.add_argument('--use-python-path', action='store_true',
232230
dest='special-opts:use_python_path',
233231
help=argparse.SUPPRESS)
232+
parser.add_argument('-s', '--silent-imports', action='store_true',
233+
dest='special-opts:silent_imports',
234+
help=argparse.SUPPRESS)
235+
parser.add_argument('--almost-silent', action='store_true',
236+
dest='special-opts:almost_silent',
237+
help=argparse.SUPPRESS)
234238

235239
report_group = parser.add_argument_group(
236240
title='report generation',
@@ -259,7 +263,11 @@ def process_options(args: List[str],
259263
# filename for the config file.
260264
dummy = argparse.Namespace()
261265
parser.parse_args(args, dummy)
262-
config_file = dummy.config_file or defaults.CONFIG_FILE
266+
config_file = defaults.CONFIG_FILE
267+
if dummy.config_file:
268+
config_file = dummy.config_file
269+
if not os.path.exists(config_file):
270+
parser.error("Cannot file config file '%s'" % config_file)
263271

264272
# Parse config file first, so command line can override.
265273
options = Options()
@@ -278,11 +286,18 @@ def process_options(args: List[str],
278286
"See https://github.com/python/mypy/issues/1411 for more discussion."
279287
)
280288

281-
# warn about deprecated options
282-
if special_opts.silent:
283-
print("Warning: --silent is deprecated; use --silent-imports",
284-
file=sys.stderr)
285-
options.silent_imports = True
289+
# Process deprecated options
290+
if special_opts.almost_silent:
291+
print("Warning: --almost-silent has been replaced by "
292+
"--follow=imports=errors", file=sys.stderr)
293+
if options.follow_imports == 'normal':
294+
options.follow_imports = 'errors'
295+
elif special_opts.silent_imports:
296+
print("Warning: --silent-imports has been replaced by "
297+
"--ignore-missing-imports --follow=imports=skip", file=sys.stderr)
298+
options.ignore_missing_imports = True
299+
if options.follow_imports == 'normal':
300+
options.follow_imports = 'skip'
286301
if special_opts.dirty_stubs:
287302
print("Warning: -f/--dirty-stubs is deprecated and no longer necessary. Mypy no longer "
288303
"checks the git status of stubs.",
@@ -456,6 +471,9 @@ def get_init_file(dir: str) -> Optional[str]:
456471
'custom_typeshed_dir': str,
457472
'mypy_path': lambda s: [p.strip() for p in re.split('[,:]', s)],
458473
'junit_xml': str,
474+
# These two are for backwards compatibility
475+
'silent_imports': bool,
476+
'almost_silent': bool,
459477
}
460478

461479

@@ -472,14 +490,13 @@ def parse_config_file(options: Options, filename: str) -> None:
472490
return
473491
if 'mypy' not in parser:
474492
print("%s: No [mypy] section in config file" % filename, file=sys.stderr)
475-
return
476-
477-
section = parser['mypy']
478-
prefix = '%s: [%s]' % (filename, 'mypy')
479-
updates, report_dirs = parse_section(prefix, options, section)
480-
for k, v in updates.items():
481-
setattr(options, k, v)
482-
options.report_dirs.update(report_dirs)
493+
else:
494+
section = parser['mypy']
495+
prefix = '%s: [%s]' % (filename, 'mypy')
496+
updates, report_dirs = parse_section(prefix, options, section)
497+
for k, v in updates.items():
498+
setattr(options, k, v)
499+
options.report_dirs.update(report_dirs)
483500

484501
for name, section in parser.items():
485502
if name.startswith('mypy-'):
@@ -509,7 +526,7 @@ def parse_section(prefix: str, template: Options,
509526
510527
Returns a dict of option values encountered, and a dict of report directories.
511528
"""
512-
results = {}
529+
results = {} # type: Dict[str, object]
513530
report_dirs = {} # type: Dict[str, str]
514531
for key in section:
515532
key = key.replace('-', '_')
@@ -542,6 +559,20 @@ def parse_section(prefix: str, template: Options,
542559
except ValueError as err:
543560
print("%s: %s: %s" % (prefix, key, err), file=sys.stderr)
544561
continue
562+
if key == 'silent_imports':
563+
print("%s: silent_imports has been replaced by "
564+
"ignore_missing_imports=True; follow_imports=skip" % prefix, file=sys.stderr)
565+
if v:
566+
if 'ignore_missing_imports' not in results:
567+
results['ignore_missing_imports'] = True
568+
if 'follow_imports' not in results:
569+
results['follow_imports'] = 'skip'
570+
if key == 'almost_silent':
571+
print("%s: almost_silent has been replaced by "
572+
"follow_imports=error" % prefix, file=sys.stderr)
573+
if v:
574+
if 'follow_imports' not in results:
575+
results['follow_imports'] = 'error'
545576
results[key] = v
546577
return results, report_dirs
547578

mypy/options.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ class Options:
1717
"""Options collected from flags."""
1818

1919
PER_MODULE_OPTIONS = {
20-
"silent_imports",
21-
"almost_silent",
20+
"ignore_missing_imports",
21+
"follow_imports",
2222
"disallow_untyped_calls",
2323
"disallow_untyped_defs",
2424
"check_untyped_defs",
@@ -40,8 +40,8 @@ def __init__(self) -> None:
4040
self.custom_typeshed_dir = None # type: Optional[str]
4141
self.mypy_path = [] # type: List[str]
4242
self.report_dirs = {} # type: Dict[str, str]
43-
self.silent_imports = False
44-
self.almost_silent = False
43+
self.ignore_missing_imports = False
44+
self.follow_imports = 'normal' # normal|silent|skip|error
4545

4646
# Disallow calling untyped functions from typed ones
4747
self.disallow_untyped_calls = False

0 commit comments

Comments
 (0)