@@ -370,18 +370,104 @@ def infer_python_executable(options: Options, special_opts: argparse.Namespace)
370370 Define MYPY_CACHE_DIR to override configuration cache_dir path."""
371371
372372
373+ def is_terminal_punctuation (char : str ) -> bool :
374+ return char in ("." , "?" , "!" )
375+
376+
377+ class ArgumentGroup (argparse ._ArgumentGroup ):
378+ """A wrapper for argparse's ArgumentGroup class that lets us enforce capitalization
379+ on the added arguments."""
380+
381+ def __init__ (self , argument_group : argparse ._ArgumentGroup ) -> None :
382+ self .argument_group = argument_group
383+
384+ def add_argument (
385+ self , * name_or_flags : str , help : str | None = None , ** kwargs : Any
386+ ) -> argparse .Action :
387+ if self .argument_group .title == "Report generation" :
388+ if help and help != argparse .SUPPRESS :
389+ ValueError (
390+ "Mypy-internal CLI documentation style error: help description for the Report generation flag"
391+ + f" { name_or_flags } was unexpectedly provided. (Currently, '{ help } '.)"
392+ + " This check is in the code because we assume there's nothing helpful to say about the report flags."
393+ + " If you're improving that situation, feel free to remove this check."
394+ )
395+ else :
396+ if not help :
397+ raise ValueError (
398+ f"Mypy-internal CLI documentation style error: flag help description for { name_or_flags } "
399+ + f" must be provided. (Currently, '{ help } '.)"
400+ )
401+ if help [0 ] != help [0 ].upper ():
402+ raise ValueError (
403+ f"Mypy-internal CLI documentation style error: flag help description for { name_or_flags } "
404+ + f" must start with a capital letter (or unicameral symbol). (Currently, '{ help } '.)"
405+ )
406+ if help [- 1 ] == "." :
407+ raise ValueError (
408+ f"Mypy-internal CLI documentation style error: flag help description for { name_or_flags } "
409+ + f" must NOT end with a period. (Currently, '{ help } '.)"
410+ )
411+ return self .argument_group .add_argument (* name_or_flags , help = help , ** kwargs )
412+
413+ def _add_action (self , action : Any ) -> Any :
414+ """This is used by the internal argparse machinery so we have to provide it."""
415+ return self .argument_group ._add_action (action )
416+
417+
373418class CapturableArgumentParser (argparse .ArgumentParser ):
374419 """Override ArgumentParser methods that use sys.stdout/sys.stderr directly.
375420
376421 This is needed because hijacking sys.std* is not thread-safe,
377422 yet output must be captured to properly support mypy.api.run.
423+
424+ Also enforces our style guides for groups and flags (ie, capitalization).
378425 """
379426
380427 def __init__ (self , * args : Any , ** kwargs : Any ) -> None :
381428 self .stdout = kwargs .pop ("stdout" , sys .stdout )
382429 self .stderr = kwargs .pop ("stderr" , sys .stderr )
383430 super ().__init__ (* args , ** kwargs )
384431
432+ # =====================
433+ # Enforce style guide
434+ # =====================
435+ # We just hard fail on these, as CI will ensure the runtime errors never get to users.
436+ def add_argument_group (
437+ self , title : str | None = None , description : str | None = None , ** kwargs : str | Any
438+ ) -> ArgumentGroup :
439+ if title is None :
440+ raise ValueError (
441+ "CLI documentation style error: all argument groups must have titles,"
442+ + " and at least one currently does not."
443+ )
444+ if title not in [
445+ "positional arguments" ,
446+ "options" ,
447+ "optional arguments" , # name in python 3.9
448+ ]: # These are built-in names, ignore them.
449+ if not title [0 ].isupper ():
450+ raise ValueError (
451+ f"CLI documentation style error: Title of group { title } "
452+ + f" must start with a capital letter. (Currently, '{ title [0 ]} '.)"
453+ )
454+ if description and not description [0 ].isupper ():
455+ raise ValueError (
456+ f"CLI documentation style error: Description of group { title } "
457+ + f" must start with a capital letter. (Currently, '{ description [0 ]} '.)"
458+ )
459+ if is_terminal_punctuation (title [- 1 ]):
460+ raise ValueError (
461+ f"CLI documentation style error: Title of group { title } "
462+ + f" must NOT end with terminal punction. (Currently, '{ title [- 1 ]} '.)"
463+ )
464+ if description and not is_terminal_punctuation (description [- 1 ]):
465+ raise ValueError (
466+ f"CLI documentation style error: Description of group { title } "
467+ + f" must end with terminal punction. (Currently, '{ description [- 1 ]} '.)"
468+ )
469+ return ArgumentGroup (super ().add_argument_group (title , description , ** kwargs ))
470+
385471 # =====================
386472 # Help-printing methods
387473 # =====================
@@ -469,7 +555,8 @@ def define_options(
469555 stderr : TextIO = sys .stderr ,
470556 server_options : bool = False ,
471557) -> tuple [CapturableArgumentParser , list [str ], list [tuple [str , bool ]]]:
472- """Define the options in the parser (by calling a bunch of methods that express/build our desired command-line flags).
558+ """Define the options in the parser
559+ (by calling a bunch of methods that express/build our desired command-line flags).
473560 Returns a tuple of:
474561 a parser object, that can parse command line arguments to mypy (expected consumer: main's process_options),
475562 a list of what flags are strict (expected consumer: docs' html_builder's _add_strict_list),
@@ -544,10 +631,6 @@ def add_invertible_flag(
544631 # Feel free to add subsequent sentences that add additional details.
545632 # 3. If you cannot think of a meaningful description for a new group, omit it entirely.
546633 # (E.g. see the "miscellaneous" sections).
547- # 4. The group description should end with a period (unless the last line is a link). If you
548- # do end the group description with a link, omit the 'http://' prefix. (Some links are too
549- # long and will break up into multiple lines if we include that prefix, so for consistency
550- # we omit the prefix on all links.)
551634
552635 general_group = parser .add_argument_group (title = "Optional arguments" )
553636 general_group .add_argument (
@@ -777,7 +860,7 @@ def add_invertible_flag(
777860 title = "None and Optional handling" ,
778861 description = "Adjust how values of type 'None' are handled. For more context on "
779862 "how mypy handles values of type 'None', see: "
780- "https://mypy.readthedocs.io/en/stable/kinds_of_types.html#optional-types-and-the-none-type" ,
863+ "https://mypy.readthedocs.io/en/stable/kinds_of_types.html#optional-types-and-the-none-type. " ,
781864 )
782865 add_invertible_flag (
783866 "--implicit-optional" ,
@@ -1034,7 +1117,7 @@ def add_invertible_flag(
10341117 "Mypy caches type information about modules into a cache to "
10351118 "let you speed up future invocations of mypy. Also see "
10361119 "mypy's daemon mode: "
1037- "mypy.readthedocs.io/en/stable/mypy_daemon.html#mypy-daemon" ,
1120+ "https:// mypy.readthedocs.io/en/stable/mypy_daemon.html#mypy-daemon. " ,
10381121 )
10391122 incremental_group .add_argument (
10401123 "-i" , "--incremental" , action = "store_true" , help = argparse .SUPPRESS
@@ -1128,7 +1211,7 @@ def add_invertible_flag(
11281211 dest = "shadow_file" ,
11291212 action = "append" ,
11301213 help = "When encountering SOURCE_FILE, read and type check "
1131- "the contents of SHADOW_FILE instead. " ,
1214+ "the contents of SHADOW_FILE instead" ,
11321215 )
11331216 internals_group .add_argument ("--fast-exit" , action = "store_true" , help = argparse .SUPPRESS )
11341217 internals_group .add_argument (
@@ -1278,7 +1361,7 @@ def add_invertible_flag(
12781361 code_group = parser .add_argument_group (
12791362 title = "Running code" ,
12801363 description = "Specify the code you want to type check. For more details, see "
1281- "mypy.readthedocs.io/en/stable/running_mypy.html#running-mypy" ,
1364+ "https:// mypy.readthedocs.io/en/stable/running_mypy.html#running-mypy. " ,
12821365 )
12831366 add_invertible_flag (
12841367 "--explicit-package-bases" ,
0 commit comments