diff --git a/mypy/main.py b/mypy/main.py index 7d5721851c3d..f6abd0199078 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -379,11 +379,58 @@ def infer_python_executable(options: Options, special_opts: argparse.Namespace) Define MYPY_CACHE_DIR to override configuration cache_dir path.""" +def is_terminal_punctuation(char: str) -> bool: + return char in (".", "?", "!") + + +class ArgumentGroup(argparse._ArgumentGroup): + """A wrapper for argparse's ArgumentGroup class that lets us enforce capitalization + on the added arguments.""" + + def __init__(self, argument_group: argparse._ArgumentGroup) -> None: + self.argument_group = argument_group + + def add_argument( + self, *name_or_flags: str, help: str | None = None, **kwargs: Any + ) -> argparse.Action: + if self.argument_group.title == "Report generation": + if help and help != argparse.SUPPRESS: + ValueError( + "Mypy-internal CLI documentation style error: help description for the Report generation flag" + + f" {name_or_flags} was unexpectedly provided. (Currently, '{help}'.)" + + " This check is in the code because we assume there's nothing helpful to say about the report flags." + + " If you're improving that situation, feel free to remove this check." + ) + else: + if not help: + raise ValueError( + f"Mypy-internal CLI documentation style error: flag help description for {name_or_flags}" + + f" must be provided. (Currently, '{help}'.)" + ) + if help[0] != help[0].upper(): + raise ValueError( + f"Mypy-internal CLI documentation style error: flag help description for {name_or_flags}" + + f" must start with a capital letter (or unicameral symbol). (Currently, '{help}'.)" + ) + if help[-1] == ".": + raise ValueError( + f"Mypy-internal CLI documentation style error: flag help description for {name_or_flags}" + + f" must NOT end with a period. (Currently, '{help}'.)" + ) + return self.argument_group.add_argument(*name_or_flags, help=help, **kwargs) + + def _add_action(self, action: Any) -> Any: + """This is used by the internal argparse machinery so we have to provide it.""" + return self.argument_group._add_action(action) + + class CapturableArgumentParser(argparse.ArgumentParser): """Override ArgumentParser methods that use sys.stdout/sys.stderr directly. This is needed because hijacking sys.std* is not thread-safe, yet output must be captured to properly support mypy.api.run. + + Also enforces our style guides for groups and flags (ie, capitalization). """ def __init__(self, *args: Any, **kwargs: Any) -> None: @@ -391,6 +438,45 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.stderr = kwargs.pop("stderr", sys.stderr) super().__init__(*args, **kwargs) + # ===================== + # Enforce style guide + # ===================== + # We just hard fail on these, as CI will ensure the runtime errors never get to users. + def add_argument_group( + self, title: str | None = None, description: str | None = None, **kwargs: str | Any + ) -> ArgumentGroup: + if title is None: + raise ValueError( + "CLI documentation style error: all argument groups must have titles," + + " and at least one currently does not." + ) + if title not in [ + "positional arguments", + "options", + "optional arguments", # name in python 3.9 + ]: # These are built-in names, ignore them. + if not title[0].isupper(): + raise ValueError( + f"CLI documentation style error: Title of group {title}" + + f" must start with a capital letter. (Currently, '{title[0]}'.)" + ) + if description and not description[0].isupper(): + raise ValueError( + f"CLI documentation style error: Description of group {title}" + + f" must start with a capital letter. (Currently, '{description[0]}'.)" + ) + if is_terminal_punctuation(title[-1]): + raise ValueError( + f"CLI documentation style error: Title of group {title}" + + f" must NOT end with terminal punction. (Currently, '{title[-1]}'.)" + ) + if description and not is_terminal_punctuation(description[-1]): + raise ValueError( + f"CLI documentation style error: Description of group {title}" + + f" must end with terminal punction. (Currently, '{description[-1]}'.)" + ) + return ArgumentGroup(super().add_argument_group(title, description, **kwargs)) + # ===================== # Help-printing methods # ===================== @@ -478,7 +564,8 @@ def define_options( stderr: TextIO = sys.stderr, server_options: bool = False, ) -> tuple[CapturableArgumentParser, list[str], list[tuple[str, bool]]]: - """Define the options in the parser (by calling a bunch of methods that express/build our desired command-line flags). + """Define the options in the parser + (by calling a bunch of methods that express/build our desired command-line flags). Returns a tuple of: a parser object, that can parse command line arguments to mypy (expected consumer: main's process_options), a list of what flags are strict (expected consumer: docs' html_builder's _add_strict_list), @@ -791,7 +878,7 @@ def add_invertible_flag( title="None and Optional handling", description="Adjust how values of type 'None' are handled. For more context on " "how mypy handles values of type 'None', see: " - "https://mypy.readthedocs.io/en/stable/kinds_of_types.html#optional-types-and-the-none-type", + "https://mypy.readthedocs.io/en/stable/kinds_of_types.html#optional-types-and-the-none-type.", ) add_invertible_flag( "--implicit-optional", @@ -1138,7 +1225,7 @@ def add_invertible_flag( dest="shadow_file", action="append", help="When encountering SOURCE_FILE, read and type check " - "the contents of SHADOW_FILE instead.", + "the contents of SHADOW_FILE instead", ) internals_group.add_argument("--fast-exit", action="store_true", help=argparse.SUPPRESS) internals_group.add_argument(