Skip to content

Commit 1d48bef

Browse files
author
Guido van Rossum
committed
Support reading options from a config file, default mypy.ini.
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. 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 b116b68 commit 1d48bef

File tree

5 files changed

+111
-4
lines changed

5 files changed

+111
-4
lines changed

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: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Mypy type checker command line tool."""
22

33
import argparse
4+
import configparser
45
import os
56
import re
67
import sys
@@ -125,6 +126,7 @@ def process_options(args: List[str],
125126
help_factory = (lambda prog:
126127
argparse.RawDescriptionHelpFormatter(prog=prog, max_help_position=28))
127128
parser = argparse.ArgumentParser(prog='mypy', epilog=FOOTER,
129+
fromfile_prefix_chars='@',
128130
formatter_class=help_factory)
129131

130132
# Unless otherwise specified, arguments will be parsed directly onto an
@@ -172,7 +174,7 @@ def process_options(args: List[str],
172174
help="enable experimental module cache")
173175
parser.add_argument('--cache-dir', action='store', metavar='DIR',
174176
help="store module cache info in the given folder in incremental mode "
175-
"(defaults to '{}')".format(defaults.MYPY_CACHE))
177+
"(defaults to '{}')".format(defaults.CACHE_DIR))
176178
parser.add_argument('--strict-optional', action='store_true',
177179
dest='special-opts:strict_optional',
178180
help="enable experimental strict Optional checks")
@@ -191,6 +193,9 @@ def process_options(args: List[str],
191193
help="use a custom typing module")
192194
parser.add_argument('--scripts-are-modules', action='store_true',
193195
help="Script x becomes module x instead of __main__")
196+
parser.add_argument('--config-file',
197+
help="Configuration file, must have a [mypy] section "
198+
"(defaults to {})".format(defaults.CONFIG_FILE))
194199
# hidden options
195200
# --shadow-file a.py tmp.py will typecheck tmp.py in place of a.py.
196201
# Useful for tools to make transformations to a file to get more
@@ -247,7 +252,18 @@ def process_options(args: List[str],
247252
code_group.add_argument(metavar='files', nargs='*', dest='special-opts:files',
248253
help="type-check given files or directories")
249254

255+
# Parse arguments once into a dummy namespace so we can get the
256+
# filename for the config file.
257+
dummy = argparse.Namespace()
258+
parser.parse_args(args, dummy)
259+
config_file = dummy.config_file or defaults.CONFIG_FILE
260+
261+
# Parse config file first, so command line can override.
250262
options = Options()
263+
if config_file and os.path.exists(config_file):
264+
parse_config_file(options, config_file)
265+
266+
# Parse command line for real, using a split namespace.
251267
special_opts = argparse.Namespace()
252268
parser.parse_args(args, SplitNamespace(options, special_opts, 'special-opts:'))
253269

@@ -416,6 +432,60 @@ def get_init_file(dir: str) -> Optional[str]:
416432
return None
417433

418434

435+
# For most options, the type of the default value set in options.py is
436+
# sufficient, and we don't have to do anything here. This table
437+
# exists to specify types for values initialized to None or container
438+
# types.
439+
config_types = {
440+
# TODO: Check validity of python version
441+
'python_version': lambda s: tuple(map(int, s.split('.'))),
442+
'strict_optional_whitelist': lambda s: s.split(),
443+
'custom_typing_module': str,
444+
}
445+
446+
447+
def parse_config_file(options: Options, filename: str) -> None:
448+
"""Parse an options file.
449+
450+
Errors are written to stderr but are not fatal.
451+
"""
452+
parser = configparser.RawConfigParser()
453+
try:
454+
parser.read(filename)
455+
except configparser.Error as err:
456+
print("%s: %s" % (filename, err), file=sys.stderr)
457+
return
458+
if 'mypy' not in parser:
459+
print("%s: No [mypy] section in config file" % filename, file=sys.stderr)
460+
return
461+
section = parser['mypy']
462+
for key in section:
463+
key = key.replace('-', '_')
464+
if key in config_types:
465+
ct = config_types[key]
466+
else:
467+
dv = getattr(options, key, None)
468+
if dv is None:
469+
print("%s: Unrecognized option: %s = %s" % (filename, key, section[key]),
470+
file=sys.stderr)
471+
continue
472+
ct = type(dv)
473+
v = None # type: Any
474+
try:
475+
if ct is bool:
476+
v = section.getboolean(key) # type: ignore # Until better stub
477+
elif callable(ct):
478+
v = ct(section.get(key))
479+
else:
480+
print("%s: Don't know what type %s should have" % (filename, key), file=sys.stderr)
481+
continue
482+
except ValueError as err:
483+
print("%s: %s: %s" % (filename, key, err), file=sys.stderr)
484+
continue
485+
if v != getattr(options, key, None):
486+
setattr(options, key, v)
487+
488+
419489
def fail(msg: str) -> None:
420490
sys.stderr.write('%s\n' % msg)
421491
sys.exit(1)

mypy/options.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def __init__(self) -> None:
6767
# -- experimental options --
6868
self.fast_parser = False
6969
self.incremental = False
70-
self.cache_dir = defaults.MYPY_CACHE
70+
self.cache_dir = defaults.CACHE_DIR
7171
self.debug_cache = False
7272
self.suppress_error_context = False # Suppress "note: In function "foo":" messages.
7373
self.shadow_file = None # type: Optional[Tuple[str, str]]

mypy/test/testcheck.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def run_case(self, testcase: DataDrivenTestCase) -> None:
100100
self.run_case_once(testcase)
101101

102102
def clear_cache(self) -> None:
103-
dn = defaults.MYPY_CACHE
103+
dn = defaults.CACHE_DIR
104104

105105
if os.path.exists(dn):
106106
shutil.rmtree(dn)

test-data/unit/cmdline.test

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,39 @@ mypy: can't decode file 'a.py': unknown encoding: uft-8
9090
# type: ignore
9191
[out]
9292
two/mod/__init__.py: error: Duplicate module named 'mod'
93+
94+
[case testFlagsFile]
95+
# cmd: mypy @flagsfile
96+
[file flagsfile]
97+
-2
98+
main.py
99+
[file main.py]
100+
def f():
101+
try:
102+
1/0
103+
except ZeroDivisionError, err:
104+
print err
105+
106+
[case testConfigFile]
107+
# cmd: mypy main.py
108+
[file mypy.ini]
109+
[[mypy]
110+
python_version = 2.7
111+
[file main.py]
112+
def f():
113+
try:
114+
1/0
115+
except ZeroDivisionError, err:
116+
print err
117+
118+
[case testAltConfigFile]
119+
# cmd: mypy --config-file config.ini main.py
120+
[file config.ini]
121+
[[mypy]
122+
python_version = 2.7
123+
[file main.py]
124+
def f():
125+
try:
126+
1/0
127+
except ZeroDivisionError, err:
128+
print err

0 commit comments

Comments
 (0)