Skip to content

Commit

Permalink
Support reading options from a config file, default mypy.ini.
Browse files Browse the repository at this point in the history
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. :-)
  • Loading branch information
Guido van Rossum committed Sep 16, 2016
1 parent b116b68 commit 1d48bef
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 4 deletions.
3 changes: 2 additions & 1 deletion mypy/defaults.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
PYTHON2_VERSION = (2, 7)
PYTHON3_VERSION = (3, 5)
MYPY_CACHE = '.mypy_cache'
CACHE_DIR = '.mypy_cache'
CONFIG_FILE = 'mypy.ini'
72 changes: 71 additions & 1 deletion mypy/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Mypy type checker command line tool."""

import argparse
import configparser
import os
import re
import sys
Expand Down Expand Up @@ -125,6 +126,7 @@ def process_options(args: List[str],
help_factory = (lambda prog:
argparse.RawDescriptionHelpFormatter(prog=prog, max_help_position=28))
parser = argparse.ArgumentParser(prog='mypy', epilog=FOOTER,
fromfile_prefix_chars='@',
formatter_class=help_factory)

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

# Parse arguments once into a dummy namespace so we can get the
# filename for the config file.
dummy = argparse.Namespace()
parser.parse_args(args, dummy)
config_file = dummy.config_file or defaults.CONFIG_FILE

# Parse config file first, so command line can override.
options = Options()
if config_file and os.path.exists(config_file):
parse_config_file(options, config_file)

# Parse command line for real, using a split namespace.
special_opts = argparse.Namespace()
parser.parse_args(args, SplitNamespace(options, special_opts, 'special-opts:'))

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


# For most options, the type of the default value set in options.py is
# sufficient, and we don't have to do anything here. This table
# exists to specify types for values initialized to None or container
# types.
config_types = {
# TODO: Check validity of python version
'python_version': lambda s: tuple(map(int, s.split('.'))),
'strict_optional_whitelist': lambda s: s.split(),
'custom_typing_module': str,
}


def parse_config_file(options: Options, filename: str) -> None:
"""Parse an options file.
Errors are written to stderr but are not fatal.
"""
parser = configparser.RawConfigParser()
try:
parser.read(filename)
except configparser.Error as err:
print("%s: %s" % (filename, err), file=sys.stderr)
return
if 'mypy' not in parser:
print("%s: No [mypy] section in config file" % filename, file=sys.stderr)
return
section = parser['mypy']
for key in section:
key = key.replace('-', '_')
if key in config_types:
ct = config_types[key]
else:
dv = getattr(options, key, None)
if dv is None:
print("%s: Unrecognized option: %s = %s" % (filename, key, section[key]),
file=sys.stderr)
continue
ct = type(dv)
v = None # type: Any
try:
if ct is bool:
v = section.getboolean(key) # type: ignore # Until better stub
elif callable(ct):
v = ct(section.get(key))
else:
print("%s: Don't know what type %s should have" % (filename, key), file=sys.stderr)
continue
except ValueError as err:
print("%s: %s: %s" % (filename, key, err), file=sys.stderr)
continue
if v != getattr(options, key, None):
setattr(options, key, v)


def fail(msg: str) -> None:
sys.stderr.write('%s\n' % msg)
sys.exit(1)
2 changes: 1 addition & 1 deletion mypy/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def __init__(self) -> None:
# -- experimental options --
self.fast_parser = False
self.incremental = False
self.cache_dir = defaults.MYPY_CACHE
self.cache_dir = defaults.CACHE_DIR
self.debug_cache = False
self.suppress_error_context = False # Suppress "note: In function "foo":" messages.
self.shadow_file = None # type: Optional[Tuple[str, str]]
Expand Down
2 changes: 1 addition & 1 deletion mypy/test/testcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def run_case(self, testcase: DataDrivenTestCase) -> None:
self.run_case_once(testcase)

def clear_cache(self) -> None:
dn = defaults.MYPY_CACHE
dn = defaults.CACHE_DIR

if os.path.exists(dn):
shutil.rmtree(dn)
Expand Down
36 changes: 36 additions & 0 deletions test-data/unit/cmdline.test
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,39 @@ mypy: can't decode file 'a.py': unknown encoding: uft-8
# type: ignore
[out]
two/mod/__init__.py: error: Duplicate module named 'mod'

[case testFlagsFile]
# cmd: mypy @flagsfile
[file flagsfile]
-2
main.py
[file main.py]
def f():
try:
1/0
except ZeroDivisionError, err:
print err

[case testConfigFile]
# cmd: mypy main.py
[file mypy.ini]
[[mypy]
python_version = 2.7
[file main.py]
def f():
try:
1/0
except ZeroDivisionError, err:
print err

[case testAltConfigFile]
# cmd: mypy --config-file config.ini main.py
[file config.ini]
[[mypy]
python_version = 2.7
[file main.py]
def f():
try:
1/0
except ZeroDivisionError, err:
print err

0 comments on commit 1d48bef

Please sign in to comment.