Skip to content

Commit 2a0c16f

Browse files
programmatically enforce help style rules
1 parent d27b43b commit 2a0c16f

File tree

1 file changed

+92
-9
lines changed

1 file changed

+92
-9
lines changed

mypy/main.py

Lines changed: 92 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
373418
class 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

Comments
 (0)