|
| 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