Skip to content

Commit 039af9d

Browse files
committed
Move ini config parsing to its own module
This is essentially in preparation for a proof-of-concept implementation of #2938 I am working on, which will want to call into this code from build, and a build -> main dependency seems wrong.
1 parent faebf3c commit 039af9d

File tree

3 files changed

+228
-211
lines changed

3 files changed

+228
-211
lines changed

mypy/config_parser.py

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import argparse
2+
import configparser
3+
import glob as fileglob
4+
import os
5+
import re
6+
import sys
7+
8+
from mypy import defaults
9+
from mypy.options import Options, PER_MODULE_OPTIONS
10+
11+
from typing import Any, Dict, List, Mapping, Optional, Tuple, TextIO
12+
13+
14+
MYPY = False
15+
if MYPY:
16+
from typing_extensions import Final
17+
18+
19+
def parse_version(v: str) -> Tuple[int, int]:
20+
m = re.match(r'\A(\d)\.(\d+)\Z', v)
21+
if not m:
22+
raise argparse.ArgumentTypeError(
23+
"Invalid python version '{}' (expected format: 'x.y')".format(v))
24+
major, minor = int(m.group(1)), int(m.group(2))
25+
if major == 2:
26+
if minor != 7:
27+
raise argparse.ArgumentTypeError(
28+
"Python 2.{} is not supported (must be 2.7)".format(minor))
29+
elif major == 3:
30+
if minor < defaults.PYTHON3_VERSION_MIN[1]:
31+
raise argparse.ArgumentTypeError(
32+
"Python 3.{0} is not supported (must be {1}.{2} or higher)".format(minor,
33+
*defaults.PYTHON3_VERSION_MIN))
34+
else:
35+
raise argparse.ArgumentTypeError(
36+
"Python major version '{}' out of range (must be 2 or 3)".format(major))
37+
return major, minor
38+
39+
40+
def split_and_match_files(paths: str) -> List[str]:
41+
"""Take a string representing a list of files/directories (with support for globbing
42+
through the glob library).
43+
44+
Where a path/glob matches no file, we still include the raw path in the resulting list.
45+
46+
Returns a list of file paths
47+
"""
48+
expanded_paths = []
49+
50+
for path in paths.split(','):
51+
path = path.strip()
52+
globbed_files = fileglob.glob(path, recursive=True)
53+
if globbed_files:
54+
expanded_paths.extend(globbed_files)
55+
else:
56+
expanded_paths.append(path)
57+
58+
return expanded_paths
59+
60+
61+
# For most options, the type of the default value set in options.py is
62+
# sufficient, and we don't have to do anything here. This table
63+
# exists to specify types for values initialized to None or container
64+
# types.
65+
config_types = {
66+
'python_version': parse_version,
67+
'strict_optional_whitelist': lambda s: s.split(),
68+
'custom_typing_module': str,
69+
'custom_typeshed_dir': str,
70+
'mypy_path': lambda s: [p.strip() for p in re.split('[,:]', s)],
71+
'files': split_and_match_files,
72+
'quickstart_file': str,
73+
'junit_xml': str,
74+
# These two are for backwards compatibility
75+
'silent_imports': bool,
76+
'almost_silent': bool,
77+
'plugins': lambda s: [p.strip() for p in s.split(',')],
78+
'always_true': lambda s: [p.strip() for p in s.split(',')],
79+
'always_false': lambda s: [p.strip() for p in s.split(',')],
80+
'package_root': lambda s: [p.strip() for p in s.split(',')],
81+
} # type: Final
82+
83+
84+
def parse_config_file(options: Options, filename: Optional[str],
85+
stdout: TextIO = sys.stdout,
86+
stderr: TextIO = sys.stderr) -> None:
87+
"""Parse a config file into an Options object.
88+
89+
Errors are written to stderr but are not fatal.
90+
91+
If filename is None, fall back to default config files.
92+
"""
93+
if filename is not None:
94+
config_files = (filename,) # type: Tuple[str, ...]
95+
else:
96+
config_files = tuple(map(os.path.expanduser, defaults.CONFIG_FILES))
97+
98+
parser = configparser.RawConfigParser()
99+
100+
for config_file in config_files:
101+
if not os.path.exists(config_file):
102+
continue
103+
try:
104+
parser.read(config_file)
105+
except configparser.Error as err:
106+
print("%s: %s" % (config_file, err), file=stderr)
107+
else:
108+
file_read = config_file
109+
options.config_file = file_read
110+
break
111+
else:
112+
return
113+
114+
if 'mypy' not in parser:
115+
if filename or file_read not in defaults.SHARED_CONFIG_FILES:
116+
print("%s: No [mypy] section in config file" % file_read, file=stderr)
117+
else:
118+
section = parser['mypy']
119+
prefix = '%s: [%s]' % (file_read, 'mypy')
120+
updates, report_dirs = parse_section(prefix, options, section,
121+
stdout, stderr)
122+
for k, v in updates.items():
123+
setattr(options, k, v)
124+
options.report_dirs.update(report_dirs)
125+
126+
for name, section in parser.items():
127+
if name.startswith('mypy-'):
128+
prefix = '%s: [%s]' % (file_read, name)
129+
updates, report_dirs = parse_section(prefix, options, section,
130+
stdout, stderr)
131+
if report_dirs:
132+
print("%s: Per-module sections should not specify reports (%s)" %
133+
(prefix, ', '.join(s + '_report' for s in sorted(report_dirs))),
134+
file=stderr)
135+
if set(updates) - PER_MODULE_OPTIONS:
136+
print("%s: Per-module sections should only specify per-module flags (%s)" %
137+
(prefix, ', '.join(sorted(set(updates) - PER_MODULE_OPTIONS))),
138+
file=stderr)
139+
updates = {k: v for k, v in updates.items() if k in PER_MODULE_OPTIONS}
140+
globs = name[5:]
141+
for glob in globs.split(','):
142+
# For backwards compatibility, replace (back)slashes with dots.
143+
glob = glob.replace(os.sep, '.')
144+
if os.altsep:
145+
glob = glob.replace(os.altsep, '.')
146+
147+
if (any(c in glob for c in '?[]!') or
148+
any('*' in x and x != '*' for x in glob.split('.'))):
149+
print("%s: Patterns must be fully-qualified module names, optionally "
150+
"with '*' in some components (e.g spam.*.eggs.*)"
151+
% prefix,
152+
file=stderr)
153+
else:
154+
options.per_module_options[glob] = updates
155+
156+
157+
def parse_section(prefix: str, template: Options,
158+
section: Mapping[str, str],
159+
stdout: TextIO = sys.stdout,
160+
stderr: TextIO = sys.stderr
161+
) -> Tuple[Dict[str, object], Dict[str, str]]:
162+
"""Parse one section of a config file.
163+
164+
Returns a dict of option values encountered, and a dict of report directories.
165+
"""
166+
results = {} # type: Dict[str, object]
167+
report_dirs = {} # type: Dict[str, str]
168+
for key in section:
169+
if key in config_types:
170+
ct = config_types[key]
171+
else:
172+
dv = getattr(template, key, None)
173+
if dv is None:
174+
if key.endswith('_report'):
175+
report_type = key[:-7].replace('_', '-')
176+
if report_type in defaults.REPORTER_NAMES:
177+
report_dirs[report_type] = section[key]
178+
else:
179+
print("%s: Unrecognized report type: %s" % (prefix, key),
180+
file=stderr)
181+
continue
182+
if key.startswith('x_'):
183+
continue # Don't complain about `x_blah` flags
184+
elif key == 'strict':
185+
print("%s: Strict mode is not supported in configuration files: specify "
186+
"individual flags instead (see 'mypy -h' for the list of flags enabled "
187+
"in strict mode)" % prefix, file=stderr)
188+
else:
189+
print("%s: Unrecognized option: %s = %s" % (prefix, key, section[key]),
190+
file=stderr)
191+
continue
192+
ct = type(dv)
193+
v = None # type: Any
194+
try:
195+
if ct is bool:
196+
v = section.getboolean(key) # type: ignore # Until better stub
197+
elif callable(ct):
198+
try:
199+
v = ct(section.get(key))
200+
except argparse.ArgumentTypeError as err:
201+
print("%s: %s: %s" % (prefix, key, err), file=stderr)
202+
continue
203+
else:
204+
print("%s: Don't know what type %s should have" % (prefix, key), file=stderr)
205+
continue
206+
except ValueError as err:
207+
print("%s: %s: %s" % (prefix, key, err), file=stderr)
208+
continue
209+
if key == 'cache_dir':
210+
v = os.path.expanduser(v)
211+
if key == 'silent_imports':
212+
print("%s: silent_imports has been replaced by "
213+
"ignore_missing_imports=True; follow_imports=skip" % prefix, file=stderr)
214+
if v:
215+
if 'ignore_missing_imports' not in results:
216+
results['ignore_missing_imports'] = True
217+
if 'follow_imports' not in results:
218+
results['follow_imports'] = 'skip'
219+
if key == 'almost_silent':
220+
print("%s: almost_silent has been replaced by "
221+
"follow_imports=error" % prefix, file=stderr)
222+
if v:
223+
if 'follow_imports' not in results:
224+
results['follow_imports'] = 'error'
225+
results[key] = v
226+
return results, report_dirs

0 commit comments

Comments
 (0)