11"""Mypy type checker command line tool."""
22
33import argparse
4+ import configparser
45import os
56import re
67import sys
78
8- from typing import Any , Dict , List , Optional , Set , Tuple
9+ from typing import Any , Dict , List , Mapping , Optional , Set , Tuple
910
1011from mypy import build
1112from mypy import defaults
1415from mypy .build import BuildSource , BuildResult , PYTHON_EXTENSIONS
1516from mypy .errors import CompileError , set_drop_into_pdb , set_show_tb
1617from mypy .options import Options , BuildType
18+ from mypy .report import reporter_classes
1719
1820from mypy .version import __version__
1921
@@ -125,6 +127,7 @@ def process_options(args: List[str],
125127 help_factory = (lambda prog :
126128 argparse .RawDescriptionHelpFormatter (prog = prog , max_help_position = 28 ))
127129 parser = argparse .ArgumentParser (prog = 'mypy' , epilog = FOOTER ,
130+ fromfile_prefix_chars = '@' ,
128131 formatter_class = help_factory )
129132
130133 # Unless otherwise specified, arguments will be parsed directly onto an
@@ -172,9 +175,9 @@ def process_options(args: List[str],
172175 help = "enable experimental module cache" )
173176 parser .add_argument ('--cache-dir' , action = 'store' , metavar = 'DIR' ,
174177 help = "store module cache info in the given folder in incremental mode "
175- "(defaults to '{}')" .format (defaults .MYPY_CACHE ))
178+ "(defaults to '{}')" .format (defaults .CACHE_DIR ))
176179 parser .add_argument ('--strict-optional' , action = 'store_true' ,
177- dest = 'special-opts: strict_optional' ,
180+ dest = 'strict_optional' ,
178181 help = "enable experimental strict Optional checks" )
179182 parser .add_argument ('--strict-optional-whitelist' , metavar = 'GLOB' , nargs = '*' ,
180183 help = "suppress strict Optional errors in all but the provided files "
@@ -191,6 +194,9 @@ def process_options(args: List[str],
191194 help = "use a custom typing module" )
192195 parser .add_argument ('--scripts-are-modules' , action = 'store_true' ,
193196 help = "Script x becomes module x instead of __main__" )
197+ parser .add_argument ('--config-file' ,
198+ help = "Configuration file, must have a [mypy] section "
199+ "(defaults to {})" .format (defaults .CONFIG_FILE ))
194200 # hidden options
195201 # --shadow-file a.py tmp.py will typecheck tmp.py in place of a.py.
196202 # Useful for tools to make transformations to a file to get more
@@ -215,30 +221,18 @@ def process_options(args: List[str],
215221 report_group = parser .add_argument_group (
216222 title = 'report generation' ,
217223 description = 'Generate a report in the specified format.' )
218- report_group .add_argument ('--html-report' , metavar = 'DIR' ,
219- dest = 'special-opts:html_report' )
220- report_group .add_argument ('--old-html-report' , metavar = 'DIR' ,
221- dest = 'special-opts:old_html_report' )
222- report_group .add_argument ('--xslt-html-report' , metavar = 'DIR' ,
223- dest = 'special-opts:xslt_html_report' )
224- report_group .add_argument ('--xml-report' , metavar = 'DIR' ,
225- dest = 'special-opts:xml_report' )
226- report_group .add_argument ('--txt-report' , metavar = 'DIR' ,
227- dest = 'special-opts:txt_report' )
228- report_group .add_argument ('--xslt-txt-report' , metavar = 'DIR' ,
229- dest = 'special-opts:xslt_txt_report' )
230- report_group .add_argument ('--linecount-report' , metavar = 'DIR' ,
231- dest = 'special-opts:linecount_report' )
232- report_group .add_argument ('--linecoverage-report' , metavar = 'DIR' ,
233- dest = 'special-opts:linecoverage_report' )
224+ for report_type in reporter_classes :
225+ report_group .add_argument ('--%s-report' % report_type .replace ('_' , '-' ),
226+ metavar = 'DIR' ,
227+ dest = 'special-opts:%s_report' % report_type )
234228
235229 code_group = parser .add_argument_group (title = 'How to specify the code to type check' )
236230 code_group .add_argument ('-m' , '--module' , action = 'append' , metavar = 'MODULE' ,
237231 dest = 'special-opts:modules' ,
238232 help = "type-check module; can repeat for more modules" )
239- # TODO: `mypy -c A -c B` and `mypy - p A -p B` currently silently
240- # ignore A (last option wins). Perhaps -c, -m and -p could just
241- # be command-line flags that modify how we interpret self.files?
233+ # TODO: `mypy -p A -p B` currently silently ignores ignores A
234+ # (last option wins). Perhaps -c, -m and -p could just be
235+ # command-line flags that modify how we interpret self.files?
242236 code_group .add_argument ('-c' , '--command' , action = 'append' , metavar = 'PROGRAM_TEXT' ,
243237 dest = 'special-opts:command' ,
244238 help = "type-check program passed in as string" )
@@ -247,7 +241,18 @@ def process_options(args: List[str],
247241 code_group .add_argument (metavar = 'files' , nargs = '*' , dest = 'special-opts:files' ,
248242 help = "type-check given files or directories" )
249243
244+ # Parse arguments once into a dummy namespace so we can get the
245+ # filename for the config file.
246+ dummy = argparse .Namespace ()
247+ parser .parse_args (args , dummy )
248+ config_file = dummy .config_file or defaults .CONFIG_FILE
249+
250+ # Parse config file first, so command line can override.
250251 options = Options ()
252+ if config_file and os .path .exists (config_file ):
253+ parse_config_file (options , config_file )
254+
255+ # Parse command line for real, using a split namespace.
251256 special_opts = argparse .Namespace ()
252257 parser .parse_args (args , SplitNamespace (options , special_opts , 'special-opts:' ))
253258
@@ -281,7 +286,10 @@ def process_options(args: List[str],
281286 parser .error ("May only specify one of: module, package, files, or command." )
282287
283288 # Set build flags.
284- if special_opts .strict_optional or options .strict_optional_whitelist is not None :
289+ if options .strict_optional_whitelist is not None :
290+ # TODO: Deprecate, then kill this flag
291+ options .strict_optional = True
292+ if options .strict_optional :
285293 experiments .STRICT_OPTIONAL = True
286294
287295 # Set reports.
@@ -416,6 +424,102 @@ def get_init_file(dir: str) -> Optional[str]:
416424 return None
417425
418426
427+ # For most options, the type of the default value set in options.py is
428+ # sufficient, and we don't have to do anything here. This table
429+ # exists to specify types for values initialized to None or container
430+ # types.
431+ config_types = {
432+ # TODO: Check validity of python version
433+ 'python_version' : lambda s : tuple (map (int , s .split ('.' ))),
434+ 'strict_optional_whitelist' : lambda s : s .split (),
435+ 'custom_typing_module' : str ,
436+ }
437+
438+
439+ def parse_config_file (options : Options , filename : str ) -> None :
440+ """Parse a config file into an Options object.
441+
442+ Errors are written to stderr but are not fatal.
443+ """
444+ parser = configparser .RawConfigParser ()
445+ try :
446+ parser .read (filename )
447+ except configparser .Error as err :
448+ print ("%s: %s" % (filename , err ), file = sys .stderr )
449+ return
450+ if 'mypy' not in parser :
451+ print ("%s: No [mypy] section in config file" % filename , file = sys .stderr )
452+ return
453+
454+ section = parser ['mypy' ]
455+ prefix = '%s: [%s]' % (filename , 'mypy' )
456+ updates , report_dirs = parse_section (prefix , options , section )
457+ for k , v in updates .items ():
458+ setattr (options , k , v )
459+ options .report_dirs .update (report_dirs )
460+
461+ for name , section in parser .items ():
462+ if name .startswith ('mypy-' ):
463+ prefix = '%s: [%s]' % (filename , name )
464+ updates , report_dirs = parse_section (prefix , options , section )
465+ # TODO: Limit updates to flags that can be per-file.
466+ if report_dirs :
467+ print ("%s: Per-file sections should not specify reports (%s)" %
468+ (prefix , ', ' .join (s + '_report' for s in sorted (report_dirs ))),
469+ file = sys .stderr )
470+ if set (updates ) - Options .PER_FILE_OPTIONS :
471+ print ("%s: Per-file sections should only specify per-file flags (%s)" %
472+ (prefix , ', ' .join (sorted (set (updates ) - Options .PER_FILE_OPTIONS ))),
473+ file = sys .stderr )
474+ updates = {k : v for k , v in updates .items () if k in Options .PER_FILE_OPTIONS }
475+ globs = name [5 :]
476+ for glob in globs .split (',' ):
477+ options .per_file_options [glob ] = updates
478+
479+
480+ def parse_section (prefix : str , template : Options ,
481+ section : Mapping [str , str ]) -> Tuple [Dict [str , object ], Dict [str , str ]]:
482+ """Parse one section of a config file.
483+
484+ Returns a dict of option values encountered, and a dict of report directories.
485+ """
486+ results = {}
487+ report_dirs = {} # type: Dict[str, str]
488+ for key in section :
489+ key = key .replace ('-' , '_' )
490+ if key in config_types :
491+ ct = config_types [key ]
492+ else :
493+ dv = getattr (template , key , None )
494+ if dv is None :
495+ if key .endswith ('_report' ):
496+ report_type = key [:- 7 ].replace ('_' , '-' )
497+ if report_type in reporter_classes :
498+ report_dirs [report_type ] = section .get (key )
499+ else :
500+ print ("%s: Unrecognized report type: %s" % (prefix , key ),
501+ file = sys .stderr )
502+ continue
503+ print ("%s: Unrecognized option: %s = %s" % (prefix , key , section [key ]),
504+ file = sys .stderr )
505+ continue
506+ ct = type (dv )
507+ v = None # type: Any
508+ try :
509+ if ct is bool :
510+ v = section .getboolean (key ) # type: ignore # Until better stub
511+ elif callable (ct ):
512+ v = ct (section .get (key ))
513+ else :
514+ print ("%s: Don't know what type %s should have" % (prefix , key ), file = sys .stderr )
515+ continue
516+ except ValueError as err :
517+ print ("%s: %s: %s" % (prefix , key , err ), file = sys .stderr )
518+ continue
519+ results [key ] = v
520+ return results , report_dirs
521+
522+
419523def fail (msg : str ) -> None :
420524 sys .stderr .write ('%s\n ' % msg )
421525 sys .exit (1 )
0 commit comments