1
+ import argparse
2
+ from configparser import RawConfigParser
1
3
import fnmatch
4
+ import os
2
5
import pprint
3
6
import re
4
7
import sys
5
8
6
9
from typing import Any , Mapping , Optional , Tuple , List , Pattern , Dict
7
10
8
11
from mypy import defaults
12
+ from mypy .report import reporter_classes
9
13
10
14
11
15
class BuildType :
@@ -145,23 +149,49 @@ def clone_for_module(self, module: str, path: Optional[str]) -> 'Options':
145
149
if self .module_matches_pattern (module , pattern ):
146
150
updates .update (self .per_module_options [pattern ])
147
151
148
- if path :
152
+ new_options = Options ()
153
+
154
+ if path and os .path .exists (path ):
155
+ options_section = []
156
+ found_options = False
149
157
with open (path ) as file_contents :
150
158
for line in file_contents :
151
159
if not re .match ('\s*#' , line ):
152
160
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
161
192
162
193
if not updates :
163
194
return self
164
- new_options = Options ()
165
195
new_options .__dict__ .update (self .__dict__ )
166
196
new_options .__dict__ .update (updates )
167
197
return new_options
@@ -174,3 +204,101 @@ def module_matches_pattern(self, module: str, pattern: Pattern[str]) -> bool:
174
204
175
205
def select_options_affecting_cache (self ) -> Mapping [str , bool ]:
176
206
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