diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index db2407e17df8..70db1cc50374 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -85,8 +85,9 @@ for full details, see :ref:`running-mypy`. This flag will add everything that matches ``.gitignore`` file(s) to :option:`--exclude`. +.. _optional-arguments: -Optional arguments +Utility arguments ****************** .. option:: -h, --help @@ -810,7 +811,10 @@ of the above sections. :option:`mypy --help` output. Note: the exact list of flags enabled by running :option:`--strict` may change - over time. + over time. For this version of mypy, the list is: + + .. include:: strict_list.rst + .. include:: strict_list.rst .. @@ -1082,6 +1086,70 @@ in developing or debugging mypy internals. cause mypy to type check the contents of ``temp.py`` instead of ``original.py``, but error messages will still reference ``original.py``. +.. _enabling-incomplete-experimental-features: + +Experimental features +***************************************** + +.. option:: --enable-incomplete-feature {PreciseTupleTypes, InlineTypedDict} + + Some features may require several mypy releases to implement, for example + due to their complexity, potential for backwards incompatibility, or + ambiguous semantics that would benefit from feedback from the community. + You can enable such features for early preview using this flag. Note that + it is not guaranteed that all features will be ultimately enabled by + default. In *rare cases* we may decide to not go ahead with certain + features. + + List of currently incomplete/experimental features: + + * ``PreciseTupleTypes``: this feature will infer more precise tuple types in + various scenarios. Before variadic types were added to the Python type system + by :pep:`646`, it was impossible to express a type like "a tuple with + at least two integers". The best type available was ``tuple[int, ...]``. + Therefore, mypy applied very lenient checking for variable-length tuples. + Now this type can be expressed as ``tuple[int, int, *tuple[int, ...]]``. + For such more precise types (when explicitly *defined* by a user) mypy, + for example, warns about unsafe index access, and generally handles them + in a type-safe manner. However, to avoid problems in existing code, mypy + does not *infer* these precise types when it technically can. Here are + notable examples where ``PreciseTupleTypes`` infers more precise types: + + .. code-block:: python + + numbers: tuple[int, ...] + + more_numbers = (1, *numbers, 1) + reveal_type(more_numbers) + # Without PreciseTupleTypes: tuple[int, ...] + # With PreciseTupleTypes: tuple[int, *tuple[int, ...], int] + + other_numbers = (1, 1) + numbers + reveal_type(other_numbers) + # Without PreciseTupleTypes: tuple[int, ...] + # With PreciseTupleTypes: tuple[int, int, *tuple[int, ...]] + + if len(numbers) > 2: + reveal_type(numbers) + # Without PreciseTupleTypes: tuple[int, ...] + # With PreciseTupleTypes: tuple[int, int, int, *tuple[int, ...]] + else: + reveal_type(numbers) + # Without PreciseTupleTypes: tuple[int, ...] + # With PreciseTupleTypes: tuple[()] | tuple[int] | tuple[int, int] + + * ``InlineTypedDict``: this feature enables non-standard syntax for inline + :ref:`TypedDicts `, for example: + + .. code-block:: python + + def test_values() -> {"foo": int, "bar": str}: + return {"foo": 42, "bar": "test"} + +.. option:: --find-occurrences CLASS.MEMBER + + This flag will make mypy print out all usages of a class member + based on static type information. This feature is experimental. Report generation ***************** @@ -1143,65 +1211,6 @@ format into the specified directory. ``mypy[reports]``. -Enabling incomplete/experimental features -***************************************** - -.. option:: --enable-incomplete-feature {PreciseTupleTypes, InlineTypedDict} - - Some features may require several mypy releases to implement, for example - due to their complexity, potential for backwards incompatibility, or - ambiguous semantics that would benefit from feedback from the community. - You can enable such features for early preview using this flag. Note that - it is not guaranteed that all features will be ultimately enabled by - default. In *rare cases* we may decide to not go ahead with certain - features. - -List of currently incomplete/experimental features: - -* ``PreciseTupleTypes``: this feature will infer more precise tuple types in - various scenarios. Before variadic types were added to the Python type system - by :pep:`646`, it was impossible to express a type like "a tuple with - at least two integers". The best type available was ``tuple[int, ...]``. - Therefore, mypy applied very lenient checking for variable-length tuples. - Now this type can be expressed as ``tuple[int, int, *tuple[int, ...]]``. - For such more precise types (when explicitly *defined* by a user) mypy, - for example, warns about unsafe index access, and generally handles them - in a type-safe manner. However, to avoid problems in existing code, mypy - does not *infer* these precise types when it technically can. Here are - notable examples where ``PreciseTupleTypes`` infers more precise types: - - .. code-block:: python - - numbers: tuple[int, ...] - - more_numbers = (1, *numbers, 1) - reveal_type(more_numbers) - # Without PreciseTupleTypes: tuple[int, ...] - # With PreciseTupleTypes: tuple[int, *tuple[int, ...], int] - - other_numbers = (1, 1) + numbers - reveal_type(other_numbers) - # Without PreciseTupleTypes: tuple[int, ...] - # With PreciseTupleTypes: tuple[int, int, *tuple[int, ...]] - - if len(numbers) > 2: - reveal_type(numbers) - # Without PreciseTupleTypes: tuple[int, ...] - # With PreciseTupleTypes: tuple[int, int, int, *tuple[int, ...]] - else: - reveal_type(numbers) - # Without PreciseTupleTypes: tuple[int, ...] - # With PreciseTupleTypes: tuple[()] | tuple[int] | tuple[int, int] - -* ``InlineTypedDict``: this feature enables non-standard syntax for inline - :ref:`TypedDicts `, for example: - - .. code-block:: python - - def test_values() -> {"int": int, "str": str}: - return {"int": 42, "str": "test"} - - Miscellaneous ************* @@ -1248,11 +1257,6 @@ Miscellaneous type checking results. This can make it easier to integrate mypy with continuous integration (CI) tools. -.. option:: --find-occurrences CLASS.MEMBER - - This flag will make mypy print out all usages of a class member - based on static type information. This feature is experimental. - .. option:: --scripts-are-modules This flag will give command line arguments that appear to be diff --git a/docs/source/html_builder.py b/docs/source/html_builder.py index 387f7f13b4c2..76537b57c7a2 100644 --- a/docs/source/html_builder.py +++ b/docs/source/html_builder.py @@ -39,7 +39,7 @@ def _add_strict_list(self) -> None: ): raise ValueError(f"{strict_part=}, which doesn't look right (by a simple heuristic).") self.strict_file.write_text( - "For this version of mypy, the list of flags enabled by strict is: " + strict_part + "For this version of mypy, the flags enabled by strict are: " + strict_part ) def _verify_error_codes(self) -> None: diff --git a/mypy/main.py b/mypy/main.py index 2fbb9671e721..b102311eb1f7 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -370,11 +370,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 help 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: @@ -382,6 +429,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 # ===================== @@ -469,7 +555,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), @@ -546,12 +633,8 @@ def add_invertible_flag( # Feel free to add subsequent sentences that add additional details. # 3. If you cannot think of a meaningful description for a new group, omit it entirely. # (E.g. see the "miscellaneous" sections). - # 4. The group description should end with a period (unless the last line is a link). If you - # do end the group description with a link, omit the 'http://' prefix. (Some links are too - # long and will break up into multiple lines if we include that prefix, so for consistency - # we omit the prefix on all links.) - general_group = parser.add_argument_group(title="Optional arguments") + general_group = parser.add_argument_group(title="Utility arguments") general_group.add_argument( "-h", "--help", action="help", help="Show this help message and exit" ) @@ -573,7 +656,9 @@ def add_invertible_flag( "-O", "--output", metavar="FORMAT", - help="Set a custom output format", + # The metavar overrides the default of displaying the choices, + # so we have to explicitly display them. + help=f"Set a custom output format (choices: {set(OUTPUT_CHOICES.keys())})", choices=OUTPUT_CHOICES, ) @@ -779,7 +864,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", @@ -1027,7 +1112,7 @@ def add_invertible_flag( "Mypy caches type information about modules into a cache to " "let you speed up future invocations of mypy. Also see " "mypy's daemon mode: " - "mypy.readthedocs.io/en/stable/mypy_daemon.html#mypy-daemon", + "https://mypy.readthedocs.io/en/stable/mypy_daemon.html#mypy-daemon.", ) incremental_group.add_argument( "-i", "--incremental", action="store_true", help=argparse.SUPPRESS @@ -1056,6 +1141,12 @@ def add_invertible_flag( action="store_true", help="Include fine-grained dependency information in the cache for the mypy daemon", ) + if server_options: + incremental_group.add_argument( + "--use-fine-grained-cache", + action="store_true", + help="Use the cache in fine-grained incremental mode (this flag only available for dmypy)", + ) incremental_group.add_argument( "--skip-version-check", action="store_true", @@ -1093,12 +1184,25 @@ def add_invertible_flag( internals_group.add_argument( "--disable-expression-cache", action="store_true", help=argparse.SUPPRESS ) - parser.add_argument( + experimental_group = parser.add_argument_group( + title="Experimental options", + description="Enable features that work well enough to be useful," + + " but perhaps not as well as you might wish." + + " These features may be enabled by default in the future," + + " or perhaps moved to another section.", + ) + experimental_group.add_argument( "--enable-incomplete-feature", action="append", metavar="{" + ",".join(sorted(INCOMPLETE_FEATURES)) + "}", help="Enable support of incomplete/experimental features for early preview", ) + experimental_group.add_argument( + "--find-occurrences", + metavar="CLASS.MEMBER", + dest="special-opts:find_occurrences", + help="Print out all usages of a class member", + ) internals_group.add_argument( "--custom-typeshed-dir", metavar="DIR", help="Use the custom typeshed in DIR" ) @@ -1116,7 +1220,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( @@ -1137,7 +1241,7 @@ def add_invertible_flag( if report_type not in {"memory-xml"}: report_group.add_argument( f"--{report_type.replace('_', '-')}-report", - metavar="DIR", + metavar="OUTPUT_DIR", dest=f"special-opts:{report_type}_report", ) @@ -1151,22 +1255,18 @@ def add_invertible_flag( "--skip-c-gen", dest="mypyc_skip_c_generation", action="store_true", help=argparse.SUPPRESS ) - other_group = parser.add_argument_group(title="Miscellaneous") - other_group.add_argument("--quickstart-file", help=argparse.SUPPRESS) - other_group.add_argument("--junit-xml", help="Write junit.xml to the given file") + misc_group = parser.add_argument_group(title="Miscellaneous") + misc_group.add_argument("--quickstart-file", help=argparse.SUPPRESS) + misc_group.add_argument("--junit-xml", help="Write junit.xml to the given file") imports_group.add_argument( "--junit-format", choices=["global", "per_file"], default="global", - help="If --junit-xml is set, specifies format. global: single test with all errors; per_file: one test entry per file with failures", - ) - other_group.add_argument( - "--find-occurrences", - metavar="CLASS.MEMBER", - dest="special-opts:find_occurrences", - help="Print out all usages of a class member (experimental)", + help="If --junit-xml is set, specifies format." + + " global: single test with all errors;" + + " per_file: one test entry per file with failures", ) - other_group.add_argument( + misc_group.add_argument( "--scripts-are-modules", action="store_true", help="Script x becomes module x instead of __main__", @@ -1177,7 +1277,7 @@ def add_invertible_flag( default=False, strict_flag=False, help="Install detected missing library stub packages using pip", - group=other_group, + group=misc_group, ) add_invertible_flag( "--non-interactive", @@ -1187,24 +1287,10 @@ def add_invertible_flag( "Install stubs without asking for confirmation and hide " + "errors, with --install-types" ), - group=other_group, + group=misc_group, inverse="--interactive", ) - if server_options: - # TODO: This flag is superfluous; remove after a short transition (2018-03-16) - other_group.add_argument( - "--experimental", - action="store_true", - dest="fine_grained_incremental", - help="Enable fine-grained incremental mode", - ) - other_group.add_argument( - "--use-fine-grained-cache", - action="store_true", - help="Use the cache in fine-grained incremental mode", - ) - # hidden options parser.add_argument( "--stats", action="store_true", dest="dump_type_stats", help=argparse.SUPPRESS @@ -1273,7 +1359,7 @@ def add_invertible_flag( code_group = parser.add_argument_group( title="Running code", description="Specify the code you want to type check. For more details, see " - "mypy.readthedocs.io/en/stable/running_mypy.html#running-mypy", + "https://mypy.readthedocs.io/en/stable/running_mypy.html#running-mypy.", ) add_invertible_flag( "--explicit-package-bases", @@ -1537,7 +1623,8 @@ def set_strict_flags() -> None: reason = cache.find_module(p) if reason is ModuleNotFoundReason.FOUND_WITHOUT_TYPE_HINTS: fail( - f"Package '{p}' cannot be type checked due to missing py.typed marker. See https://mypy.readthedocs.io/en/stable/installed_packages.html for more details", + f"Package '{p}' cannot be type checked due to missing py.typed marker." + + " See https://mypy.readthedocs.io/en/stable/installed_packages.html for more details", stderr, options, )