Skip to content

Commit 9f82042

Browse files
committed
Use the config-file format for per-file configuration overrides
1 parent 48c9a24 commit 9f82042

File tree

3 files changed

+158
-119
lines changed

3 files changed

+158
-119
lines changed

mypy/main.py

Lines changed: 1 addition & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from mypy import util
1717
from mypy.build import BuildSource, BuildResult, PYTHON_EXTENSIONS
1818
from mypy.errors import CompileError
19-
from mypy.options import Options, BuildType
19+
from mypy.options import Options, BuildType, parse_section, parse_version
2020
from mypy.report import reporter_classes
2121

2222
from mypy.version import __version__
@@ -119,26 +119,6 @@ def __getattr__(self, name: str) -> Any:
119119
return getattr(self._standard_namespace, name)
120120

121121

122-
def parse_version(v: str) -> Tuple[int, int]:
123-
m = re.match(r'\A(\d)\.(\d+)\Z', v)
124-
if not m:
125-
raise argparse.ArgumentTypeError(
126-
"Invalid python version '{}' (expected format: 'x.y')".format(v))
127-
major, minor = int(m.group(1)), int(m.group(2))
128-
if major == 2:
129-
if minor != 7:
130-
raise argparse.ArgumentTypeError(
131-
"Python 2.{} is not supported (must be 2.7)".format(minor))
132-
elif major == 3:
133-
if minor <= 2:
134-
raise argparse.ArgumentTypeError(
135-
"Python 3.{} is not supported (must be 3.3 or higher)".format(minor))
136-
else:
137-
raise argparse.ArgumentTypeError(
138-
"Python major version '{}' out of range (must be 2 or 3)".format(major))
139-
return major, minor
140-
141-
142122
# Make the help output a little less jarring.
143123
class AugmentedHelpFormatter(argparse.HelpFormatter):
144124
def __init__(self, prog: Optional[str]) -> None:
@@ -559,22 +539,6 @@ def get_init_file(dir: str) -> Optional[str]:
559539
return None
560540

561541

562-
# For most options, the type of the default value set in options.py is
563-
# sufficient, and we don't have to do anything here. This table
564-
# exists to specify types for values initialized to None or container
565-
# types.
566-
config_types = {
567-
'python_version': parse_version,
568-
'strict_optional_whitelist': lambda s: s.split(),
569-
'custom_typing_module': str,
570-
'custom_typeshed_dir': str,
571-
'mypy_path': lambda s: [p.strip() for p in re.split('[,:]', s)],
572-
'junit_xml': str,
573-
# These two are for backwards compatibility
574-
'silent_imports': bool,
575-
'almost_silent': bool,
576-
}
577-
578542
SHARED_CONFIG_FILES = ('setup.cfg',)
579543

580544

@@ -641,67 +605,6 @@ def parse_config_file(options: Options, filename: Optional[str]) -> None:
641605
options.per_module_options[pattern] = updates
642606

643607

644-
def parse_section(prefix: str, template: Options,
645-
section: Mapping[str, str]) -> Tuple[Dict[str, object], Dict[str, str]]:
646-
"""Parse one section of a config file.
647-
648-
Returns a dict of option values encountered, and a dict of report directories.
649-
"""
650-
results = {} # type: Dict[str, object]
651-
report_dirs = {} # type: Dict[str, str]
652-
for key in section:
653-
key = key.replace('-', '_')
654-
if key in config_types:
655-
ct = config_types[key]
656-
else:
657-
dv = getattr(template, key, None)
658-
if dv is None:
659-
if key.endswith('_report'):
660-
report_type = key[:-7].replace('_', '-')
661-
if report_type in reporter_classes:
662-
report_dirs[report_type] = section.get(key)
663-
else:
664-
print("%s: Unrecognized report type: %s" % (prefix, key),
665-
file=sys.stderr)
666-
continue
667-
print("%s: Unrecognized option: %s = %s" % (prefix, key, section[key]),
668-
file=sys.stderr)
669-
continue
670-
ct = type(dv)
671-
v = None # type: Any
672-
try:
673-
if ct is bool:
674-
v = section.getboolean(key) # type: ignore # Until better stub
675-
elif callable(ct):
676-
try:
677-
v = ct(section.get(key))
678-
except argparse.ArgumentTypeError as err:
679-
print("%s: %s: %s" % (prefix, key, err), file=sys.stderr)
680-
continue
681-
else:
682-
print("%s: Don't know what type %s should have" % (prefix, key), file=sys.stderr)
683-
continue
684-
except ValueError as err:
685-
print("%s: %s: %s" % (prefix, key, err), file=sys.stderr)
686-
continue
687-
if key == 'silent_imports':
688-
print("%s: silent_imports has been replaced by "
689-
"ignore_missing_imports=True; follow_imports=skip" % prefix, file=sys.stderr)
690-
if v:
691-
if 'ignore_missing_imports' not in results:
692-
results['ignore_missing_imports'] = True
693-
if 'follow_imports' not in results:
694-
results['follow_imports'] = 'skip'
695-
if key == 'almost_silent':
696-
print("%s: almost_silent has been replaced by "
697-
"follow_imports=error" % prefix, file=sys.stderr)
698-
if v:
699-
if 'follow_imports' not in results:
700-
results['follow_imports'] = 'error'
701-
results[key] = v
702-
return results, report_dirs
703-
704-
705608
def fail(msg: str) -> None:
706609
sys.stderr.write('%s\n' % msg)
707610
sys.exit(1)

mypy/options.py

Lines changed: 138 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
import argparse
2+
from configparser import RawConfigParser
13
import fnmatch
4+
import os
25
import pprint
36
import re
47
import sys
58

69
from typing import Any, Mapping, Optional, Tuple, List, Pattern, Dict
710

811
from mypy import defaults
12+
from mypy.report import reporter_classes
913

1014

1115
class BuildType:
@@ -145,23 +149,49 @@ def clone_for_module(self, module: str, path: Optional[str]) -> 'Options':
145149
if self.module_matches_pattern(module, pattern):
146150
updates.update(self.per_module_options[pattern])
147151

148-
if path:
152+
new_options = Options()
153+
154+
if path and os.path.exists(path):
155+
options_section = []
156+
found_options = False
149157
with open(path) as file_contents:
150158
for line in file_contents:
151159
if not re.match('\s*#', line):
152160
break
153-
match = re.match('\s*#\s*mypy-options:(.*)', line)
154-
if match is not None:
155-
for flag in match.group(1).split(','):
156-
flag = flag.strip()
157-
if flag in self.PER_MODULE_OPTIONS:
158-
updates[flag] = True
159-
else:
160-
print("Warning: {!r} in {} is not a valid per-module option".format(flag, path))
161+
162+
if re.match('\s*#\s*\[mypy\]', line):
163+
options_section.append(line.strip().strip('#'))
164+
found_options = True
165+
continue
166+
167+
if found_options:
168+
options_section.append(line.strip().strip('#'))
169+
170+
if found_options:
171+
parser = RawConfigParser()
172+
parser.read_string("\n".join(options_section))
173+
updates, report_dirs = parse_section(
174+
"%s [mypy]" % path,
175+
new_options,
176+
parser['mypy']
177+
)
178+
if report_dirs:
179+
print("Warning: can't specify new mypy reports "
180+
"in a per-file override (from {})".format(path))
181+
182+
for option, file_override in updates.items():
183+
if file_override == getattr(new_options, option):
184+
# Skip options that are set to the defaults
185+
continue
186+
187+
if option not in self.PER_MODULE_OPTIONS:
188+
print("Warning: {!r} in {} is not a valid "
189+
"per-module option".format(option, path))
190+
else:
191+
updates[option] = file_override
161192

162193
if not updates:
163194
return self
164-
new_options = Options()
165195
new_options.__dict__.update(self.__dict__)
166196
new_options.__dict__.update(updates)
167197
return new_options
@@ -174,3 +204,101 @@ def module_matches_pattern(self, module: str, pattern: Pattern[str]) -> bool:
174204

175205
def select_options_affecting_cache(self) -> Mapping[str, bool]:
176206
return {opt: getattr(self, opt) for opt in self.OPTIONS_AFFECTING_CACHE}
207+
208+
209+
def parse_version(v: str) -> Tuple[int, int]:
210+
m = re.match(r'\A(\d)\.(\d+)\Z', v)
211+
if not m:
212+
raise argparse.ArgumentTypeError(
213+
"Invalid python version '{}' (expected format: 'x.y')".format(v))
214+
major, minor = int(m.group(1)), int(m.group(2))
215+
if major == 2:
216+
if minor != 7:
217+
raise argparse.ArgumentTypeError(
218+
"Python 2.{} is not supported (must be 2.7)".format(minor))
219+
elif major == 3:
220+
if minor <= 2:
221+
raise argparse.ArgumentTypeError(
222+
"Python 3.{} is not supported (must be 3.3 or higher)".format(minor))
223+
else:
224+
raise argparse.ArgumentTypeError(
225+
"Python major version '{}' out of range (must be 2 or 3)".format(major))
226+
return major, minor
227+
228+
229+
# For most options, the type of the default value set in options.py is
230+
# sufficient, and we don't have to do anything here. This table
231+
# exists to specify types for values initialized to None or container
232+
# types.
233+
config_types = {
234+
'python_version': parse_version,
235+
'strict_optional_whitelist': lambda s: s.split(),
236+
'custom_typing_module': str,
237+
'custom_typeshed_dir': str,
238+
'mypy_path': lambda s: [p.strip() for p in re.split('[,:]', s)],
239+
'junit_xml': str,
240+
# These two are for backwards compatibility
241+
'silent_imports': bool,
242+
'almost_silent': bool,
243+
}
244+
245+
246+
def parse_section(prefix: str, template: Options,
247+
section: Mapping[str, str]) -> Tuple[Dict[str, object], Dict[str, str]]:
248+
"""Parse one section of a config file.
249+
250+
Returns a dict of option values encountered, and a dict of report directories.
251+
"""
252+
results = {} # type: Dict[str, object]
253+
report_dirs = {} # type: Dict[str, str]
254+
for key in section:
255+
key = key.replace('-', '_')
256+
if key in config_types:
257+
ct = config_types[key]
258+
else:
259+
dv = getattr(template, key, None)
260+
if dv is None:
261+
if key.endswith('_report'):
262+
report_type = key[:-7].replace('_', '-')
263+
if report_type in reporter_classes:
264+
report_dirs[report_type] = section.get(key)
265+
else:
266+
print("%s: Unrecognized report type: %s" % (prefix, key),
267+
file=sys.stderr)
268+
continue
269+
print("%s: Unrecognized option: %s = %s" % (prefix, key, section[key]),
270+
file=sys.stderr)
271+
continue
272+
ct = type(dv)
273+
v = None # type: Any
274+
try:
275+
if ct is bool:
276+
v = section.getboolean(key) # type: ignore # Until better stub
277+
elif callable(ct):
278+
try:
279+
v = ct(section.get(key))
280+
except argparse.ArgumentTypeError as err:
281+
print("%s: %s: %s" % (prefix, key, err), file=sys.stderr)
282+
continue
283+
else:
284+
print("%s: Don't know what type %s should have" % (prefix, key), file=sys.stderr)
285+
continue
286+
except ValueError as err:
287+
print("%s: %s: %s" % (prefix, key, err), file=sys.stderr)
288+
continue
289+
if key == 'silent_imports':
290+
print("%s: silent_imports has been replaced by "
291+
"ignore_missing_imports=True; follow_imports=skip" % prefix, file=sys.stderr)
292+
if v:
293+
if 'ignore_missing_imports' not in results:
294+
results['ignore_missing_imports'] = True
295+
if 'follow_imports' not in results:
296+
results['follow_imports'] = 'skip'
297+
if key == 'almost_silent':
298+
print("%s: almost_silent has been replaced by "
299+
"follow_imports=error" % prefix, file=sys.stderr)
300+
if v:
301+
if 'follow_imports' not in results:
302+
results['follow_imports'] = 'error'
303+
results[key] = v
304+
return results, report_dirs

0 commit comments

Comments
 (0)