Skip to content

Commit 2fbb724

Browse files
authored
Support reading options from a config file, default mypy.ini. (#2148)
Also support reading command line flags using `mypy @flagsfile`. Addresses #1249 (but does not completely fix it). The mypy.ini file has the format: ``` [mypy] silent_imports = True python_version = 2.7 ``` Errors in this config file are non-fatal. Comments and blank lines are supported. There are also sections with glob patterns for per-file options, e.g. `[mypy-dir1/*,dir2/*]` (I'll document those later). The `@flagsfile` option reads additional argparse-style flags, including filenames, from `flagsfile`, one per line. This is typically used for passing in a list of files, but it can also be used for passing flags: ``` --silent-imports --py2 mypy ``` This format does not allow comments or blank lines. Each option must appear on a line by itself. Errors are fatal. The mypy.ini file serves as a set of defaults that can be overridden (or in some cases extended) by command line flags. An alternate config file may be specified using a command line flag: `--config-file anywhere.ini`. (There's a trick involved in making this work, read the source. :-)
1 parent 1555ed6 commit 2fbb724

9 files changed

Lines changed: 344 additions & 47 deletions

File tree

‎mypy/build.py‎

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -762,7 +762,7 @@ def find_cache_meta(id: str, path: str, manager: BuildManager) -> Optional[Cache
762762

763763
# Ignore cache if (relevant) options aren't the same.
764764
cached_options = m.options
765-
current_options = select_options_affecting_cache(manager.options)
765+
current_options = manager.options.select_options_affecting_cache()
766766
if cached_options != current_options:
767767
manager.trace('Metadata abandoned for {}: options differ'.format(id))
768768
return None
@@ -790,20 +790,6 @@ def is_meta_fresh(meta: CacheMeta, id: str, path: str, manager: BuildManager) ->
790790
return True
791791

792792

793-
def select_options_affecting_cache(options: Options) -> Mapping[str, bool]:
794-
return {opt: getattr(options, opt) for opt in OPTIONS_AFFECTING_CACHE}
795-
796-
797-
OPTIONS_AFFECTING_CACHE = [
798-
"silent_imports",
799-
"almost_silent",
800-
"disallow_untyped_calls",
801-
"disallow_untyped_defs",
802-
"check_untyped_defs",
803-
"debug_cache",
804-
]
805-
806-
807793
def random_string() -> str:
808794
return binascii.hexlify(os.urandom(8)).decode('ascii')
809795

@@ -877,6 +863,7 @@ def write_cache(id: str, path: str, tree: MypyFile,
877863
st = manager.get_stat(path) # TODO: Handle errors
878864
mtime = st.st_mtime
879865
size = st.st_size
866+
options = manager.options.clone_for_file(path)
880867
meta = {'id': id,
881868
'path': path,
882869
'mtime': mtime,
@@ -885,7 +872,7 @@ def write_cache(id: str, path: str, tree: MypyFile,
885872
'dependencies': dependencies,
886873
'suppressed': suppressed,
887874
'child_modules': child_modules,
888-
'options': select_options_affecting_cache(manager.options),
875+
'options': options.select_options_affecting_cache(),
889876
'dep_prios': dep_prios,
890877
'interface_hash': interface_hash,
891878
'version_id': manager.version_id,

‎mypy/checker.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ class TypeChecker(NodeVisitor[Type]):
120120
# Is this file a typeshed stub?
121121
is_typeshed_stub = False
122122
# Should strict Optional-related errors be suppressed in this file?
123-
suppress_none_errors = False
123+
suppress_none_errors = False # TODO: Get it from options instead
124124
options = None # type: Options
125125

126126
# The set of all dependencies (suppressed or not) that this module accesses, either
@@ -153,6 +153,8 @@ def __init__(self, errors: Errors, modules: Dict[str, MypyFile], options: Option
153153

154154
def visit_file(self, file_node: MypyFile, path: str) -> None:
155155
"""Type check a mypy file with the given path."""
156+
save_options = self.options
157+
self.options = self.options.clone_for_file(path)
156158
self.pass_num = 0
157159
self.is_stub = file_node.is_stub
158160
self.errors.set_file(path)
@@ -163,7 +165,7 @@ def visit_file(self, file_node: MypyFile, path: str) -> None:
163165
self.module_type_map = {}
164166
self.module_refs = set()
165167
if self.options.strict_optional_whitelist is None:
166-
self.suppress_none_errors = False
168+
self.suppress_none_errors = not self.options.show_none_errors
167169
else:
168170
self.suppress_none_errors = not any(fnmatch.fnmatch(path, pattern)
169171
for pattern
@@ -188,6 +190,8 @@ def visit_file(self, file_node: MypyFile, path: str) -> None:
188190
self.fail(messages.ALL_MUST_BE_SEQ_STR.format(str_seq_s, all_s),
189191
all_.node)
190192

193+
self.options = save_options
194+
191195
def check_second_pass(self) -> None:
192196
"""Run second pass of type checking which goes through deferred nodes."""
193197
self.pass_num = 1

‎mypy/defaults.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
PYTHON2_VERSION = (2, 7)
22
PYTHON3_VERSION = (3, 5)
3-
MYPY_CACHE = '.mypy_cache'
3+
CACHE_DIR = '.mypy_cache'
4+
CONFIG_FILE = 'mypy.ini'

‎mypy/main.py‎

Lines changed: 127 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
"""Mypy type checker command line tool."""
22

33
import argparse
4+
import configparser
45
import os
56
import re
67
import sys
78

8-
from typing import Any, Dict, List, Optional, Set, Tuple
9+
from typing import Any, Dict, List, Mapping, Optional, Set, Tuple
910

1011
from mypy import build
1112
from mypy import defaults
@@ -14,6 +15,7 @@
1415
from mypy.build import BuildSource, BuildResult, PYTHON_EXTENSIONS
1516
from mypy.errors import CompileError, set_drop_into_pdb, set_show_tb
1617
from mypy.options import Options, BuildType
18+
from mypy.report import reporter_classes
1719

1820
from mypy.version import __version__
1921

@@ -125,6 +127,7 @@ def process_options(args: List[str],
125127
help_factory = (lambda prog:
126128
argparse.RawDescriptionHelpFormatter(prog=prog, max_help_position=28))
127129
parser = argparse.ArgumentParser(prog='mypy', epilog=FOOTER,
130+
fromfile_prefix_chars='@',
128131
formatter_class=help_factory)
129132

130133
# Unless otherwise specified, arguments will be parsed directly onto an
@@ -172,9 +175,9 @@ def process_options(args: List[str],
172175
help="enable experimental module cache")
173176
parser.add_argument('--cache-dir', action='store', metavar='DIR',
174177
help="store module cache info in the given folder in incremental mode "
175-
"(defaults to '{}')".format(defaults.MYPY_CACHE))
178+
"(defaults to '{}')".format(defaults.CACHE_DIR))
176179
parser.add_argument('--strict-optional', action='store_true',
177-
dest='special-opts:strict_optional',
180+
dest='strict_optional',
178181
help="enable experimental strict Optional checks")
179182
parser.add_argument('--strict-optional-whitelist', metavar='GLOB', nargs='*',
180183
help="suppress strict Optional errors in all but the provided files "
@@ -191,6 +194,9 @@ def process_options(args: List[str],
191194
help="use a custom typing module")
192195
parser.add_argument('--scripts-are-modules', action='store_true',
193196
help="Script x becomes module x instead of __main__")
197+
parser.add_argument('--config-file',
198+
help="Configuration file, must have a [mypy] section "
199+
"(defaults to {})".format(defaults.CONFIG_FILE))
194200
# hidden options
195201
# --shadow-file a.py tmp.py will typecheck tmp.py in place of a.py.
196202
# Useful for tools to make transformations to a file to get more
@@ -215,30 +221,18 @@ def process_options(args: List[str],
215221
report_group = parser.add_argument_group(
216222
title='report generation',
217223
description='Generate a report in the specified format.')
218-
report_group.add_argument('--html-report', metavar='DIR',
219-
dest='special-opts:html_report')
220-
report_group.add_argument('--old-html-report', metavar='DIR',
221-
dest='special-opts:old_html_report')
222-
report_group.add_argument('--xslt-html-report', metavar='DIR',
223-
dest='special-opts:xslt_html_report')
224-
report_group.add_argument('--xml-report', metavar='DIR',
225-
dest='special-opts:xml_report')
226-
report_group.add_argument('--txt-report', metavar='DIR',
227-
dest='special-opts:txt_report')
228-
report_group.add_argument('--xslt-txt-report', metavar='DIR',
229-
dest='special-opts:xslt_txt_report')
230-
report_group.add_argument('--linecount-report', metavar='DIR',
231-
dest='special-opts:linecount_report')
232-
report_group.add_argument('--linecoverage-report', metavar='DIR',
233-
dest='special-opts:linecoverage_report')
224+
for report_type in reporter_classes:
225+
report_group.add_argument('--%s-report' % report_type.replace('_', '-'),
226+
metavar='DIR',
227+
dest='special-opts:%s_report' % report_type)
234228

235229
code_group = parser.add_argument_group(title='How to specify the code to type check')
236230
code_group.add_argument('-m', '--module', action='append', metavar='MODULE',
237231
dest='special-opts:modules',
238232
help="type-check module; can repeat for more modules")
239-
# TODO: `mypy -c A -c B` and `mypy -p A -p B` currently silently
240-
# ignore A (last option wins). Perhaps -c, -m and -p could just
241-
# be command-line flags that modify how we interpret self.files?
233+
# TODO: `mypy -p A -p B` currently silently ignores ignores A
234+
# (last option wins). Perhaps -c, -m and -p could just be
235+
# command-line flags that modify how we interpret self.files?
242236
code_group.add_argument('-c', '--command', action='append', metavar='PROGRAM_TEXT',
243237
dest='special-opts:command',
244238
help="type-check program passed in as string")
@@ -247,7 +241,18 @@ def process_options(args: List[str],
247241
code_group.add_argument(metavar='files', nargs='*', dest='special-opts:files',
248242
help="type-check given files or directories")
249243

244+
# Parse arguments once into a dummy namespace so we can get the
245+
# filename for the config file.
246+
dummy = argparse.Namespace()
247+
parser.parse_args(args, dummy)
248+
config_file = dummy.config_file or defaults.CONFIG_FILE
249+
250+
# Parse config file first, so command line can override.
250251
options = Options()
252+
if config_file and os.path.exists(config_file):
253+
parse_config_file(options, config_file)
254+
255+
# Parse command line for real, using a split namespace.
251256
special_opts = argparse.Namespace()
252257
parser.parse_args(args, SplitNamespace(options, special_opts, 'special-opts:'))
253258

@@ -281,7 +286,10 @@ def process_options(args: List[str],
281286
parser.error("May only specify one of: module, package, files, or command.")
282287

283288
# Set build flags.
284-
if special_opts.strict_optional or options.strict_optional_whitelist is not None:
289+
if options.strict_optional_whitelist is not None:
290+
# TODO: Deprecate, then kill this flag
291+
options.strict_optional = True
292+
if options.strict_optional:
285293
experiments.STRICT_OPTIONAL = True
286294

287295
# Set reports.
@@ -416,6 +424,102 @@ def get_init_file(dir: str) -> Optional[str]:
416424
return None
417425

418426

427+
# For most options, the type of the default value set in options.py is
428+
# sufficient, and we don't have to do anything here. This table
429+
# exists to specify types for values initialized to None or container
430+
# types.
431+
config_types = {
432+
# TODO: Check validity of python version
433+
'python_version': lambda s: tuple(map(int, s.split('.'))),
434+
'strict_optional_whitelist': lambda s: s.split(),
435+
'custom_typing_module': str,
436+
}
437+
438+
439+
def parse_config_file(options: Options, filename: str) -> None:
440+
"""Parse a config file into an Options object.
441+
442+
Errors are written to stderr but are not fatal.
443+
"""
444+
parser = configparser.RawConfigParser()
445+
try:
446+
parser.read(filename)
447+
except configparser.Error as err:
448+
print("%s: %s" % (filename, err), file=sys.stderr)
449+
return
450+
if 'mypy' not in parser:
451+
print("%s: No [mypy] section in config file" % filename, file=sys.stderr)
452+
return
453+
454+
section = parser['mypy']
455+
prefix = '%s: [%s]' % (filename, 'mypy')
456+
updates, report_dirs = parse_section(prefix, options, section)
457+
for k, v in updates.items():
458+
setattr(options, k, v)
459+
options.report_dirs.update(report_dirs)
460+
461+
for name, section in parser.items():
462+
if name.startswith('mypy-'):
463+
prefix = '%s: [%s]' % (filename, name)
464+
updates, report_dirs = parse_section(prefix, options, section)
465+
# TODO: Limit updates to flags that can be per-file.
466+
if report_dirs:
467+
print("%s: Per-file sections should not specify reports (%s)" %
468+
(prefix, ', '.join(s + '_report' for s in sorted(report_dirs))),
469+
file=sys.stderr)
470+
if set(updates) - Options.PER_FILE_OPTIONS:
471+
print("%s: Per-file sections should only specify per-file flags (%s)" %
472+
(prefix, ', '.join(sorted(set(updates) - Options.PER_FILE_OPTIONS))),
473+
file=sys.stderr)
474+
updates = {k: v for k, v in updates.items() if k in Options.PER_FILE_OPTIONS}
475+
globs = name[5:]
476+
for glob in globs.split(','):
477+
options.per_file_options[glob] = updates
478+
479+
480+
def parse_section(prefix: str, template: Options,
481+
section: Mapping[str, str]) -> Tuple[Dict[str, object], Dict[str, str]]:
482+
"""Parse one section of a config file.
483+
484+
Returns a dict of option values encountered, and a dict of report directories.
485+
"""
486+
results = {}
487+
report_dirs = {} # type: Dict[str, str]
488+
for key in section:
489+
key = key.replace('-', '_')
490+
if key in config_types:
491+
ct = config_types[key]
492+
else:
493+
dv = getattr(template, key, None)
494+
if dv is None:
495+
if key.endswith('_report'):
496+
report_type = key[:-7].replace('_', '-')
497+
if report_type in reporter_classes:
498+
report_dirs[report_type] = section.get(key)
499+
else:
500+
print("%s: Unrecognized report type: %s" % (prefix, key),
501+
file=sys.stderr)
502+
continue
503+
print("%s: Unrecognized option: %s = %s" % (prefix, key, section[key]),
504+
file=sys.stderr)
505+
continue
506+
ct = type(dv)
507+
v = None # type: Any
508+
try:
509+
if ct is bool:
510+
v = section.getboolean(key) # type: ignore # Until better stub
511+
elif callable(ct):
512+
v = ct(section.get(key))
513+
else:
514+
print("%s: Don't know what type %s should have" % (prefix, key), file=sys.stderr)
515+
continue
516+
except ValueError as err:
517+
print("%s: %s: %s" % (prefix, key, err), file=sys.stderr)
518+
continue
519+
results[key] = v
520+
return results, report_dirs
521+
522+
419523
def fail(msg: str) -> None:
420524
sys.stderr.write('%s\n' % msg)
421525
sys.exit(1)

‎mypy/messages.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ def not_callable(self, typ: Type, context: Context) -> Type:
417417

418418
def untyped_function_call(self, callee: CallableType, context: Context) -> Type:
419419
name = callee.name if callee.name is not None else '(unknown)'
420-
self.fail('call to untyped function {} in typed context'.format(name), context)
420+
self.fail('Call to untyped function {} in typed context'.format(name), context)
421421
return AnyType()
422422

423423
def incompatible_argument(self, n: int, m: int, callee: CallableType, arg_type: Type,

0 commit comments

Comments
 (0)