From c95306e071ee670c7436249e20841108d4f2431a Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Thu, 8 May 2025 23:25:45 -0700 Subject: [PATCH 01/32] failed attempt, very regex-heavy --- docs/source/html_builder.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/source/html_builder.py b/docs/source/html_builder.py index ea3594e0617b..731d2178f629 100644 --- a/docs/source/html_builder.py +++ b/docs/source/html_builder.py @@ -2,6 +2,7 @@ import json import os +import re import textwrap from pathlib import Path from typing import Any @@ -11,6 +12,7 @@ from sphinx.builders.html import StandaloneHTMLBuilder from sphinx.environment import BuildEnvironment +from mypy.api import run class MypyHTMLBuilder(StandaloneHTMLBuilder): def __init__(self, app: Sphinx, env: BuildEnvironment) -> None: @@ -21,6 +23,28 @@ def write_doc(self, docname: str, doctree: document) -> None: super().write_doc(docname, doctree) self._ref_to_doc.update({_id: docname for _id in doctree.ids}) + def _add_strict_to_doc(self) -> None: + target_filename = "command_line.html" + p = Path(self.outdir) / target_filename + text = p.read_bytes() + lead_in = b"over time." + c = text.count(lead_in) + complaint = f"Expected '{lead_in}' in {target_filename}, so I could add the strict flags after it, but " + if c < 1: + raise ValueError(complaint+"it was not there.") + elif c > 1: + raise ValueError(complaint+"it occurred in multiple locations, so I don't know what to do.") + help_text = run(['--help'])[0] + strict_regex = r"Strict mode; enables the following flags: (.*?)\r?\n --" + strict_part = re.match(strict_regex, help_text, re.DOTALL) + if strict_part is None: + print(help_text) + raise ValueError(f"Needed to match the regex r'{strict_regex}' in mypy --help, to enhance the docs, but it was not there.") + strict_part = strict_part[0] + if not strict_part or strict_part.isspace() or len(strict_part) < 20 or len(strict_part) > 2000: + raise ValueError(f"List of strict flags in the output is {strict_part}, which doesn't look right") + p.write_bytes(text.replace(lead_in, lead_in+b" The current list is: " + bytes(strict_part, encoding="ascii"))) + def _verify_error_codes(self) -> None: from mypy.errorcodes import error_codes @@ -55,6 +79,7 @@ def _write_ref_redirector(self) -> None: def finish(self) -> None: super().finish() self._write_ref_redirector() + self._add_strict_to_doc() def setup(app: Sphinx) -> dict[str, Any]: From 5dbb0106645f1f9bbf7f9eadd892a96494e3bc0e Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Thu, 8 May 2025 23:51:21 -0700 Subject: [PATCH 02/32] ok I mean it works now but it doubles up insertions --- docs/source/html_builder.py | 13 ++++--------- mypy/main.py | 6 ++---- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/docs/source/html_builder.py b/docs/source/html_builder.py index 731d2178f629..f556e6443e30 100644 --- a/docs/source/html_builder.py +++ b/docs/source/html_builder.py @@ -12,7 +12,7 @@ from sphinx.builders.html import StandaloneHTMLBuilder from sphinx.environment import BuildEnvironment -from mypy.api import run +from mypy.main import process_options class MypyHTMLBuilder(StandaloneHTMLBuilder): def __init__(self, app: Sphinx, env: BuildEnvironment) -> None: @@ -34,15 +34,10 @@ def _add_strict_to_doc(self) -> None: raise ValueError(complaint+"it was not there.") elif c > 1: raise ValueError(complaint+"it occurred in multiple locations, so I don't know what to do.") - help_text = run(['--help'])[0] - strict_regex = r"Strict mode; enables the following flags: (.*?)\r?\n --" - strict_part = re.match(strict_regex, help_text, re.DOTALL) - if strict_part is None: - print(help_text) - raise ValueError(f"Needed to match the regex r'{strict_regex}' in mypy --help, to enhance the docs, but it was not there.") - strict_part = strict_part[0] + help_text = process_options(['-c', 'pass']) + strict_part = help_text[2].split(": ")[1] if not strict_part or strict_part.isspace() or len(strict_part) < 20 or len(strict_part) > 2000: - raise ValueError(f"List of strict flags in the output is {strict_part}, which doesn't look right") + raise ValueError(f"{strict_part=}, which doesn't look right.") p.write_bytes(text.replace(lead_in, lead_in+b" The current list is: " + bytes(strict_part, encoding="ascii"))) def _verify_error_codes(self) -> None: diff --git a/mypy/main.py b/mypy/main.py index b2abf06897de..f39711fbfb83 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -471,7 +471,7 @@ def process_options( fscache: FileSystemCache | None = None, program: str = "mypy", header: str = HEADER, -) -> tuple[list[BuildSource], Options]: +) -> tuple[list[BuildSource], Options, str]: """Parse command line arguments. If a FileSystemCache is passed in, and package_root options are given, @@ -1521,11 +1521,9 @@ def set_strict_flags() -> None: targets.extend(p_targets) for m in special_opts.modules: targets.append(BuildSource(None, m, None)) - return targets, options elif special_opts.command: options.build_type = BuildType.PROGRAM_TEXT targets = [BuildSource(None, None, "\n".join(special_opts.command))] - return targets, options else: try: targets = create_source_list(special_opts.files, options, fscache) @@ -1534,7 +1532,7 @@ def set_strict_flags() -> None: # exceptions of different types. except InvalidSourceList as e2: fail(str(e2), stderr, options) - return targets, options + return targets, options, strict_help def process_package_roots( From c5c085776db8d0dde60815e1c7f7fcd0dfc613b3 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Fri, 9 May 2025 00:36:41 -0700 Subject: [PATCH 03/32] works perfectly --- docs/source/command_line.rst | 5 ++++- docs/source/html_builder.py | 23 ++++++----------------- mypy/main.py | 6 ++++-- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index b455e287017e..1787645ae122 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -764,7 +764,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 the current version of mypy, the list is: + + .. include:: strict_list.rst + .. option:: --disable-error-code diff --git a/docs/source/html_builder.py b/docs/source/html_builder.py index f556e6443e30..80322c8e4d67 100644 --- a/docs/source/html_builder.py +++ b/docs/source/html_builder.py @@ -2,7 +2,6 @@ import json import os -import re import textwrap from pathlib import Path from typing import Any @@ -18,27 +17,18 @@ class MypyHTMLBuilder(StandaloneHTMLBuilder): def __init__(self, app: Sphinx, env: BuildEnvironment) -> None: super().__init__(app, env) self._ref_to_doc = {} + self._add_strict_list() def write_doc(self, docname: str, doctree: document) -> None: super().write_doc(docname, doctree) self._ref_to_doc.update({_id: docname for _id in doctree.ids}) - def _add_strict_to_doc(self) -> None: - target_filename = "command_line.html" - p = Path(self.outdir) / target_filename - text = p.read_bytes() - lead_in = b"over time." - c = text.count(lead_in) - complaint = f"Expected '{lead_in}' in {target_filename}, so I could add the strict flags after it, but " - if c < 1: - raise ValueError(complaint+"it was not there.") - elif c > 1: - raise ValueError(complaint+"it occurred in multiple locations, so I don't know what to do.") - help_text = process_options(['-c', 'pass']) - strict_part = help_text[2].split(": ")[1] + def _add_strict_list(self) -> None: + p = Path(self.outdir).parent.parent / "source" / "strict_list.rst" + strict_part = ", ".join(f":option:`{s} `" for s in process_options(['-c', 'pass'])[2]) if not strict_part or strict_part.isspace() or len(strict_part) < 20 or len(strict_part) > 2000: - raise ValueError(f"{strict_part=}, which doesn't look right.") - p.write_bytes(text.replace(lead_in, lead_in+b" The current list is: " + bytes(strict_part, encoding="ascii"))) + raise ValueError(f"{strict_part=}, which doesn't look right (by a simple heuristic).") + p.write_text(strict_part) def _verify_error_codes(self) -> None: from mypy.errorcodes import error_codes @@ -74,7 +64,6 @@ def _write_ref_redirector(self) -> None: def finish(self) -> None: super().finish() self._write_ref_redirector() - self._add_strict_to_doc() def setup(app: Sphinx) -> dict[str, Any]: diff --git a/mypy/main.py b/mypy/main.py index f39711fbfb83..88312302ffa1 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -471,11 +471,13 @@ def process_options( fscache: FileSystemCache | None = None, program: str = "mypy", header: str = HEADER, -) -> tuple[list[BuildSource], Options, str]: +) -> tuple[list[BuildSource], Options, list[str]]: """Parse command line arguments. If a FileSystemCache is passed in, and package_root options are given, call fscache.set_package_root() to set the cache's package root. + + Returns a tuple of: a list of source file, an Options collected from flags, and the computed list of strict flags. """ stdout = stdout or sys.stdout stderr = stderr or sys.stderr @@ -1532,7 +1534,7 @@ def set_strict_flags() -> None: # exceptions of different types. except InvalidSourceList as e2: fail(str(e2), stderr, options) - return targets, options, strict_help + return targets, options, strict_flag_names def process_package_roots( From 82eb39432c891bd92b006ec5673a69524ecc517c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 9 May 2025 07:41:42 +0000 Subject: [PATCH 04/32] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/source/html_builder.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/source/html_builder.py b/docs/source/html_builder.py index 80322c8e4d67..9d2bc025980b 100644 --- a/docs/source/html_builder.py +++ b/docs/source/html_builder.py @@ -13,6 +13,7 @@ from mypy.main import process_options + class MypyHTMLBuilder(StandaloneHTMLBuilder): def __init__(self, app: Sphinx, env: BuildEnvironment) -> None: super().__init__(app, env) @@ -25,9 +26,16 @@ def write_doc(self, docname: str, doctree: document) -> None: def _add_strict_list(self) -> None: p = Path(self.outdir).parent.parent / "source" / "strict_list.rst" - strict_part = ", ".join(f":option:`{s} `" for s in process_options(['-c', 'pass'])[2]) - if not strict_part or strict_part.isspace() or len(strict_part) < 20 or len(strict_part) > 2000: - raise ValueError(f"{strict_part=}, which doesn't look right (by a simple heuristic).") + strict_part = ", ".join( + f":option:`{s} `" for s in process_options(["-c", "pass"])[2] + ) + if ( + not strict_part + or strict_part.isspace() + or len(strict_part) < 20 + or len(strict_part) > 2000 + ): + raise ValueError(f"{strict_part=}, which doesn't look right (by a simple heuristic).") p.write_text(strict_part) def _verify_error_codes(self) -> None: From 15236bd288571fcffbc291d52be99746dc06949e Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Fri, 9 May 2025 00:59:32 -0700 Subject: [PATCH 05/32] get the strict flags another way, to make all the tests pass --- docs/source/html_builder.py | 4 +++- mypy/main.py | 13 ++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/source/html_builder.py b/docs/source/html_builder.py index 9d2bc025980b..6ab6476cc712 100644 --- a/docs/source/html_builder.py +++ b/docs/source/html_builder.py @@ -26,8 +26,10 @@ def write_doc(self, docname: str, doctree: document) -> None: def _add_strict_list(self) -> None: p = Path(self.outdir).parent.parent / "source" / "strict_list.rst" + strict_flags: list[str] = [] + process_options(["-c", "pass"], list_to_fill_with_strict_flags=strict_flags) strict_part = ", ".join( - f":option:`{s} `" for s in process_options(["-c", "pass"])[2] + f":option:`{s} `" for s in strict_flags ) if ( not strict_part diff --git a/mypy/main.py b/mypy/main.py index 88312302ffa1..8671722f9d35 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -471,13 +471,18 @@ def process_options( fscache: FileSystemCache | None = None, program: str = "mypy", header: str = HEADER, -) -> tuple[list[BuildSource], Options, list[str]]: + list_to_fill_with_strict_flags: list[str] | None = None +) -> tuple[list[BuildSource], Options]: """Parse command line arguments. If a FileSystemCache is passed in, and package_root options are given, call fscache.set_package_root() to set the cache's package root. - Returns a tuple of: a list of source file, an Options collected from flags, and the computed list of strict flags. + Returns a tuple of: a list of source files, an Options collected from flags. + + If list_to_fill_with_strict_flags is provided and not none, + then that list will be extended with the computed list of flags that --strict enables + (as a sort of secret return option). """ stdout = stdout or sys.stdout stderr = stderr or sys.stderr @@ -1534,7 +1539,9 @@ def set_strict_flags() -> None: # exceptions of different types. except InvalidSourceList as e2: fail(str(e2), stderr, options) - return targets, options, strict_flag_names + if list_to_fill_with_strict_flags is not None: + list_to_fill_with_strict_flags.extend(strict_flag_names) + return targets, options def process_package_roots( From 4081906f6b8c548634e9ac7e87b92a110fb39884 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 9 May 2025 08:00:57 +0000 Subject: [PATCH 06/32] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/source/html_builder.py | 4 +--- mypy/main.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/source/html_builder.py b/docs/source/html_builder.py index 6ab6476cc712..b5980190a707 100644 --- a/docs/source/html_builder.py +++ b/docs/source/html_builder.py @@ -28,9 +28,7 @@ def _add_strict_list(self) -> None: p = Path(self.outdir).parent.parent / "source" / "strict_list.rst" strict_flags: list[str] = [] process_options(["-c", "pass"], list_to_fill_with_strict_flags=strict_flags) - strict_part = ", ".join( - f":option:`{s} `" for s in strict_flags - ) + strict_part = ", ".join(f":option:`{s} `" for s in strict_flags) if ( not strict_part or strict_part.isspace() diff --git a/mypy/main.py b/mypy/main.py index 8671722f9d35..a7e2b3acd42c 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -471,7 +471,7 @@ def process_options( fscache: FileSystemCache | None = None, program: str = "mypy", header: str = HEADER, - list_to_fill_with_strict_flags: list[str] | None = None + list_to_fill_with_strict_flags: list[str] | None = None, ) -> tuple[list[BuildSource], Options]: """Parse command line arguments. From b415f0f0e8c9ae60528131831a71c7e8bf253fdc Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Fri, 9 May 2025 01:14:33 -0700 Subject: [PATCH 07/32] generate the strict list in the build dir, so there won't be any funky path trouble --- docs/source/command_line.rst | 2 +- docs/source/html_builder.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 1787645ae122..04968a7530a8 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -764,7 +764,7 @@ of the above sections. :option:`mypy --help` output. Note: the exact list of flags enabled by running :option:`--strict` may change - over time. For the current version of mypy, the list is: + over time. For the this version of mypy, the list is: .. include:: strict_list.rst diff --git a/docs/source/html_builder.py b/docs/source/html_builder.py index b5980190a707..9d0a7e84e18c 100644 --- a/docs/source/html_builder.py +++ b/docs/source/html_builder.py @@ -15,9 +15,11 @@ class MypyHTMLBuilder(StandaloneHTMLBuilder): + strict_file: Path def __init__(self, app: Sphinx, env: BuildEnvironment) -> None: super().__init__(app, env) self._ref_to_doc = {} + self.strict_file = Path(self.outdir) / "strict_list.rst" self._add_strict_list() def write_doc(self, docname: str, doctree: document) -> None: @@ -25,7 +27,6 @@ def write_doc(self, docname: str, doctree: document) -> None: self._ref_to_doc.update({_id: docname for _id in doctree.ids}) def _add_strict_list(self) -> None: - p = Path(self.outdir).parent.parent / "source" / "strict_list.rst" strict_flags: list[str] = [] process_options(["-c", "pass"], list_to_fill_with_strict_flags=strict_flags) strict_part = ", ".join(f":option:`{s} `" for s in strict_flags) @@ -36,7 +37,7 @@ def _add_strict_list(self) -> None: or len(strict_part) > 2000 ): raise ValueError(f"{strict_part=}, which doesn't look right (by a simple heuristic).") - p.write_text(strict_part) + self.strict_file.write_text(strict_part) def _verify_error_codes(self) -> None: from mypy.errorcodes import error_codes @@ -72,6 +73,7 @@ def _write_ref_redirector(self) -> None: def finish(self) -> None: super().finish() self._write_ref_redirector() + self.strict_file.unlink() def setup(app: Sphinx) -> dict[str, Any]: From 00580de3cfe1a3a78270294a8ca34db4bba5ea25 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 9 May 2025 08:16:49 +0000 Subject: [PATCH 08/32] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/source/html_builder.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/html_builder.py b/docs/source/html_builder.py index 9d0a7e84e18c..3791e17a867d 100644 --- a/docs/source/html_builder.py +++ b/docs/source/html_builder.py @@ -16,6 +16,7 @@ class MypyHTMLBuilder(StandaloneHTMLBuilder): strict_file: Path + def __init__(self, app: Sphinx, env: BuildEnvironment) -> None: super().__init__(app, env) self._ref_to_doc = {} From 98bc704c60eabe26ae11fd959745b1d21ddeda07 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Fri, 9 May 2025 01:32:37 -0700 Subject: [PATCH 09/32] um, I meant srcdir. Sure. --- docs/source/command_line.rst | 2 +- docs/source/html_builder.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 04968a7530a8..37ba83bccbc1 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -764,7 +764,7 @@ of the above sections. :option:`mypy --help` output. Note: the exact list of flags enabled by running :option:`--strict` may change - over time. For the this version of mypy, the list is: + over time. For this version of mypy, the list is: .. include:: strict_list.rst diff --git a/docs/source/html_builder.py b/docs/source/html_builder.py index 3791e17a867d..38800872c0cb 100644 --- a/docs/source/html_builder.py +++ b/docs/source/html_builder.py @@ -15,12 +15,12 @@ class MypyHTMLBuilder(StandaloneHTMLBuilder): - strict_file: Path + strict_file: Sphinx._StrPath def __init__(self, app: Sphinx, env: BuildEnvironment) -> None: super().__init__(app, env) self._ref_to_doc = {} - self.strict_file = Path(self.outdir) / "strict_list.rst" + self.strict_file = Path(self.srcdir) / "strict_list.rst" self._add_strict_list() def write_doc(self, docname: str, doctree: document) -> None: From a5464167115cb44c892e73ba5f2a7461052f128d Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Fri, 9 May 2025 01:33:42 -0700 Subject: [PATCH 10/32] bruh --- docs/source/html_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/html_builder.py b/docs/source/html_builder.py index 38800872c0cb..26d1e63db940 100644 --- a/docs/source/html_builder.py +++ b/docs/source/html_builder.py @@ -15,7 +15,7 @@ class MypyHTMLBuilder(StandaloneHTMLBuilder): - strict_file: Sphinx._StrPath + strict_file: Path def __init__(self, app: Sphinx, env: BuildEnvironment) -> None: super().__init__(app, env) From c044f8060ffe426f7a288eaa1b9fed29353b084d Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Fri, 9 May 2025 22:18:16 -0700 Subject: [PATCH 11/32] refactor out define_options --- docs/source/html_builder.py | 6 ++-- mypy/main.py | 57 +++++++++++++++++++------------------ 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/docs/source/html_builder.py b/docs/source/html_builder.py index 26d1e63db940..71d48c985c4e 100644 --- a/docs/source/html_builder.py +++ b/docs/source/html_builder.py @@ -11,7 +11,7 @@ from sphinx.builders.html import StandaloneHTMLBuilder from sphinx.environment import BuildEnvironment -from mypy.main import process_options +from mypy.main import define_options class MypyHTMLBuilder(StandaloneHTMLBuilder): @@ -28,8 +28,8 @@ def write_doc(self, docname: str, doctree: document) -> None: self._ref_to_doc.update({_id: docname for _id in doctree.ids}) def _add_strict_list(self) -> None: - strict_flags: list[str] = [] - process_options(["-c", "pass"], list_to_fill_with_strict_flags=strict_flags) + strict_flags: list[str] + _, strict_flags, _ = define_options() strict_part = ", ".join(f":option:`{s} `" for s in strict_flags) if ( not strict_part diff --git a/mypy/main.py b/mypy/main.py index a7e2b3acd42c..9b890dda9cfa 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -461,32 +461,12 @@ def __call__( parser._print_message(formatter.format_help(), self.stdout) parser.exit() - -def process_options( - args: list[str], - stdout: TextIO | None = None, - stderr: TextIO | None = None, - require_targets: bool = True, - server_options: bool = False, - fscache: FileSystemCache | None = None, - program: str = "mypy", - header: str = HEADER, - list_to_fill_with_strict_flags: list[str] | None = None, -) -> tuple[list[BuildSource], Options]: - """Parse command line arguments. - - If a FileSystemCache is passed in, and package_root options are given, - call fscache.set_package_root() to set the cache's package root. - - Returns a tuple of: a list of source files, an Options collected from flags. - - If list_to_fill_with_strict_flags is provided and not none, - then that list will be extended with the computed list of flags that --strict enables - (as a sort of secret return option). - """ - stdout = stdout or sys.stdout - stderr = stderr or sys.stderr - +def define_options(program: str = "mypy", header: str = HEADER, stdout: TextIO = sys.stdout, 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). + 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), + strict_flag_assignments (expected consumer: main's process_options).""" parser = CapturableArgumentParser( prog=program, usage=header, @@ -1347,6 +1327,29 @@ def add_invertible_flag( dest="special-opts:files", help="Type-check given files or directories", ) + return parser, strict_flag_names, strict_flag_assignments + +def process_options( + args: list[str], + stdout: TextIO | None = None, + stderr: TextIO | None = None, + require_targets: bool = True, + server_options: bool = False, + fscache: FileSystemCache | None = None, + program: str = "mypy", + header: str = HEADER, +) -> tuple[list[BuildSource], Options]: + """Parse command line arguments. + + If a FileSystemCache is passed in, and package_root options are given, + call fscache.set_package_root() to set the cache's package root. + + Returns a tuple of: a list of source files, an Options collected from flags. + """ + stdout = stdout if stdout is not None else sys.stdout + stderr = stderr if stderr is not None else sys.stderr + + parser, _, strict_flag_assignments = define_options(header, program, stdout, stderr, server_options) # Parse arguments once into a dummy namespace so we can get the # filename for the config file and know if the user requested all strict options. @@ -1539,8 +1542,6 @@ def set_strict_flags() -> None: # exceptions of different types. except InvalidSourceList as e2: fail(str(e2), stderr, options) - if list_to_fill_with_strict_flags is not None: - list_to_fill_with_strict_flags.extend(strict_flag_names) return targets, options From 6a47e7d981942b268e20e72330cce29c10582f99 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 05:23:15 +0000 Subject: [PATCH 12/32] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/main.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/mypy/main.py b/mypy/main.py index 9b890dda9cfa..52866b5d27c7 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -461,7 +461,14 @@ def __call__( parser._print_message(formatter.format_help(), self.stdout) parser.exit() -def define_options(program: str = "mypy", header: str = HEADER, stdout: TextIO = sys.stdout, stderr: TextIO = sys.stderr, server_options: bool = False) -> tuple[CapturableArgumentParser, list[str], list[tuple[str, bool]]]: + +def define_options( + program: str = "mypy", + header: str = HEADER, + stdout: TextIO = sys.stdout, + 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). Returns a tuple of: a parser object, that can parse command line arguments to mypy (expected consumer: main's process_options), @@ -1329,6 +1336,7 @@ def add_invertible_flag( ) return parser, strict_flag_names, strict_flag_assignments + def process_options( args: list[str], stdout: TextIO | None = None, @@ -1349,7 +1357,9 @@ def process_options( stdout = stdout if stdout is not None else sys.stdout stderr = stderr if stderr is not None else sys.stderr - parser, _, strict_flag_assignments = define_options(header, program, stdout, stderr, server_options) + parser, _, strict_flag_assignments = define_options( + header, program, stdout, stderr, server_options + ) # Parse arguments once into a dummy namespace so we can get the # filename for the config file and know if the user requested all strict options. From 9ba2c9b378481564835c34cc3e0e823964e9c6db Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Fri, 9 May 2025 22:48:11 -0700 Subject: [PATCH 13/32] remove --experimental from dmypy's options after 'a short transition' (five years) --- mypy/main.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/mypy/main.py b/mypy/main.py index 52866b5d27c7..b2d29e180657 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -1190,13 +1190,6 @@ def add_invertible_flag( ) 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", From 8087150d8508372dbe64c4972e73446163daf705 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Fri, 9 May 2025 23:19:05 -0700 Subject: [PATCH 14/32] move --use-fine-grained-cache to Incremental group --- mypy/main.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/mypy/main.py b/mypy/main.py index b2d29e180657..d5a2bd0640c8 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -1055,6 +1055,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", @@ -1189,13 +1195,6 @@ def add_invertible_flag( inverse="--interactive", ) - if server_options: - 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 From 3a75ee63cf7e5bef513505e88696468fa720587f Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Fri, 9 May 2025 23:21:06 -0700 Subject: [PATCH 15/32] create new 'experimental' group, rescuing --enable-incomplete-feature from being ungrouped also, take another experimental group from the misc group into this group, and rename the misc group to misc --- mypy/main.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/mypy/main.py b/mypy/main.py index d5a2bd0640c8..b19635075af6 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -1097,12 +1097,23 @@ def add_invertible_flag( internals_group.add_argument( "--new-type-inference", 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" ) @@ -1155,22 +1166,16 @@ 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)", - ) - other_group.add_argument( + misc_group.add_argument( "--scripts-are-modules", action="store_true", help="Script x becomes module x instead of __main__", @@ -1181,7 +1186,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", @@ -1191,7 +1196,7 @@ def add_invertible_flag( "Install stubs without asking for confirmation and hide " + "errors, with --install-types" ), - group=other_group, + group=misc_group, inverse="--interactive", ) From 4f1e4603febe7313a72e1a5bf478a13a5efa7fac Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Fri, 9 May 2025 23:46:02 -0700 Subject: [PATCH 16/32] rename Optional arguments to Utility arguments, since all arguments are optional although, arguably, they all provide Utility as well. wait, was this 'Optional arguments' header supposed to introduce *all* of the arguments, and just has a couple misc ones under it, as a sort of pun? Well, whatever. --- mypy/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/main.py b/mypy/main.py index b19635075af6..6a059d06838c 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -551,7 +551,7 @@ def add_invertible_flag( # 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" ) From 2a8a8e24f464a3e8af10497dd2f81fe0c802a58e Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Fri, 9 May 2025 23:47:33 -0700 Subject: [PATCH 17/32] The metavar overrides the default of displaying the choices, so we have to explicitly display them. --- mypy/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/main.py b/mypy/main.py index 6a059d06838c..a1fcb4b4b93f 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -573,7 +573,8 @@ 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, ) From 8c61a64954a101bde76d01d18f41916cf7c6a075 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Fri, 9 May 2025 23:54:45 -0700 Subject: [PATCH 18/32] update documentation --- docs/source/command_line.rst | 131 ++++++++++++++++++----------------- 1 file changed, 66 insertions(+), 65 deletions(-) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 37ba83bccbc1..792ca1b158ec 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 @@ -1035,6 +1036,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() -> {"int": int, "str": str}: + return {"int": 42, "str": "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 ***************** @@ -1096,65 +1161,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 ************* @@ -1201,11 +1207,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 From 350fd38923d9a74d829fe5500352cdf2ccdb670c Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Fri, 9 May 2025 23:55:44 -0700 Subject: [PATCH 19/32] update documentation so it's clear that an int field doesn't need to be called int --- docs/source/command_line.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 792ca1b158ec..e4d70c57bfcf 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -1093,8 +1093,8 @@ Experimental features .. code-block:: python - def test_values() -> {"int": int, "str": str}: - return {"int": 42, "str": "test"} + def test_values() -> {"foo": int, "bar": str}: + return {"foo": 42, "bar": "test"} .. option:: --find-occurrences CLASS.MEMBER From c0aa284f44a16fb11bf0176f72bfe9f9af4d0797 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Sat, 10 May 2025 00:24:42 -0700 Subject: [PATCH 20/32] begin to enforce rules, although it's surprisingly failing --- mypy/main.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/mypy/main.py b/mypy/main.py index a1fcb4b4b93f..d8a09f311abd 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -369,12 +369,16 @@ def infer_python_executable(options: Options, special_opts: argparse.Namespace) Define MYPYPATH for additional module search path entries. Define MYPY_CACHE_DIR to override configuration cache_dir path.""" +def is_terminal_punctuation(char: str) -> bool: + return char in (".", "?", "!") 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 +386,27 @@ 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, + description: str | None = None, + **kwargs, + ) -> argparse._ArgumentGroup: + if title not in ["positional arguments", "options"]: # These are built-in names, ignore them. + if not title[0].isupper(): + raise ValueError(f"CLI documentation style error: Title of group {title} 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} 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} must NOT end with terminal punction. (Currently, '{title[-1]}'.)") + if description and not is_terminal_punctuation(title[-1]): + raise ValueError(f"CLI documentation style error: Description of group {title} must end with terminal punction. (Currently, '{description[-1]}'.)") + return super().add_argument_group(title, description, **kwargs) + # ===================== # Help-printing methods # ===================== From c76183d4b0c4973cacebf8741766410ec6813299 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Sat, 10 May 2025 01:51:36 -0700 Subject: [PATCH 21/32] continue automating, and correcting --- mypy/main.py | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/mypy/main.py b/mypy/main.py index d8a09f311abd..adb302653690 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -372,6 +372,30 @@ def infer_python_executable(options: Options, special_opts: argparse.Namespace) def is_terminal_punctuation(char: str) -> bool: return char in (".", "?", "!") +class 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, help=None, **kwargs) -> argparse.Action: + if self.argument_group.title == "Report generation": + if help and help != argparse.SUPPRESS: + raise ValueError(f"CLI documentation style error: help description for the Report generation flag {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"CLI documentation style error: flag help description for {name_or_flags} must be provided. (Currently, '{help}'.)") + if help[0] != help[0].upper(): + raise ValueError(f"CLI documentation style error: flag help description for {name_or_flags} must start with a capital letter (or unicameral symbol). (Currently, '{help}'.)") + if help[-1] == '.': + raise ValueError(f"CLI documentation style error: flag help description for {name_or_flags} 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) -> None: + self.argument_group._add_action(action) class CapturableArgumentParser(argparse.ArgumentParser): """Override ArgumentParser methods that use sys.stdout/sys.stderr directly. @@ -395,7 +419,7 @@ def add_argument_group( title: str, description: str | None = None, **kwargs, - ) -> argparse._ArgumentGroup: + ) -> ArgumentGroup: if title not in ["positional arguments", "options"]: # These are built-in names, ignore them. if not title[0].isupper(): raise ValueError(f"CLI documentation style error: Title of group {title} must start with a capital letter. (Currently, '{title[0]}'.)") @@ -403,9 +427,9 @@ def add_argument_group( raise ValueError(f"CLI documentation style error: Description of group {title} 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} must NOT end with terminal punction. (Currently, '{title[-1]}'.)") - if description and not is_terminal_punctuation(title[-1]): + if description and not is_terminal_punctuation(description[-1]): raise ValueError(f"CLI documentation style error: Description of group {title} must end with terminal punction. (Currently, '{description[-1]}'.)") - return super().add_argument_group(title, description, **kwargs) + return ArgumentGroup(super().add_argument_group(title, description, **kwargs)) # ===================== # Help-printing methods @@ -805,7 +829,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#no-strict-optional", + ".", ) add_invertible_flag( "--implicit-optional", @@ -1052,7 +1076,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 @@ -1157,7 +1181,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( @@ -1178,7 +1202,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", ) @@ -1294,7 +1318,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", From 72f1e5c041aa407eb92d9c9a232ca332f81ffb9b Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Sun, 11 May 2025 05:06:16 -0700 Subject: [PATCH 22/32] blacken --- mypy/main.py | 57 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/mypy/main.py b/mypy/main.py index adb302653690..50bf4e164d15 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -369,33 +369,45 @@ def infer_python_executable(options: Options, special_opts: argparse.Namespace) Define MYPYPATH for additional module search path entries. Define MYPY_CACHE_DIR to override configuration cache_dir path.""" + def is_terminal_punctuation(char: str) -> bool: return char in (".", "?", "!") + class 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, help=None, **kwargs) -> argparse.Action: + def add_argument(self, *name_or_flags, help=None, **kwargs) -> argparse.Action: if self.argument_group.title == "Report generation": if help and help != argparse.SUPPRESS: - raise ValueError(f"CLI documentation style error: help description for the Report generation flag {name_or_flags} was unexpectedly provided. (Currently, '{help}'.)" + raise ValueError( + f"CLI documentation style error: help description for the Report generation flag {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"CLI documentation style error: flag help description for {name_or_flags} must be provided. (Currently, '{help}'.)") + raise ValueError( + f"CLI documentation style error: flag help description for {name_or_flags} must be provided. (Currently, '{help}'.)" + ) if help[0] != help[0].upper(): - raise ValueError(f"CLI documentation style error: flag help description for {name_or_flags} must start with a capital letter (or unicameral symbol). (Currently, '{help}'.)") - if help[-1] == '.': - raise ValueError(f"CLI documentation style error: flag help description for {name_or_flags} must NOT end with a period. (Currently, '{help}'.)") + raise ValueError( + f"CLI documentation style error: flag help description for {name_or_flags} must start with a capital letter (or unicameral symbol). (Currently, '{help}'.)" + ) + if help[-1] == ".": + raise ValueError( + f"CLI documentation style error: flag help description for {name_or_flags} 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) -> None: self.argument_group._add_action(action) + + class CapturableArgumentParser(argparse.ArgumentParser): """Override ArgumentParser methods that use sys.stdout/sys.stderr directly. @@ -415,20 +427,28 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: # ===================== # We just hard fail on these, as CI will ensure the runtime errors never get to users. def add_argument_group( - self, - title: str, - description: str | None = None, - **kwargs, + self, title: str, description: str | None = None, **kwargs ) -> ArgumentGroup: - if title not in ["positional arguments", "options"]: # These are built-in names, ignore them. + if title not in [ + "positional arguments", + "options", + ]: # These are built-in names, ignore them. if not title[0].isupper(): - raise ValueError(f"CLI documentation style error: Title of group {title} must start with a capital letter. (Currently, '{title[0]}'.)") + raise ValueError( + f"CLI documentation style error: Title of group {title} 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} must start with a capital letter. (Currently, '{description[0]}'.)") + raise ValueError( + f"CLI documentation style error: Description of group {title} 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} must NOT end with terminal punction. (Currently, '{title[-1]}'.)") + raise ValueError( + f"CLI documentation style error: Title of group {title} 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} must end with terminal punction. (Currently, '{description[-1]}'.)") + raise ValueError( + f"CLI documentation style error: Description of group {title} must end with terminal punction. (Currently, '{description[-1]}'.)" + ) return ArgumentGroup(super().add_argument_group(title, description, **kwargs)) # ===================== @@ -1148,9 +1168,10 @@ def add_invertible_flag( "--new-type-inference", action="store_true", help=argparse.SUPPRESS ) experimental_group = parser.add_argument_group( - title="Experimental options", description="Enable features that work well enough to be useful," + 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." + + " These features may be enabled by default in the future, or perhaps moved to another section.", ) experimental_group.add_argument( "--enable-incomplete-feature", From 31dc2ff9993c9936f628e712bd769c82434e0af0 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Sun, 11 May 2025 14:39:54 -0700 Subject: [PATCH 23/32] remove the concern about links as it no longer seems to be useful --- mypy/main.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/mypy/main.py b/mypy/main.py index 50bf4e164d15..32b4af883929 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -615,10 +615,6 @@ 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="Utility arguments") general_group.add_argument( From 2c45a29bbcbef8bb04915f5f301afda17df0f60e Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Sun, 11 May 2025 14:51:17 -0700 Subject: [PATCH 24/32] improve cli formatting error message and also linewrapping --- mypy/main.py | 46 ++++++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/mypy/main.py b/mypy/main.py index 32b4af883929..b9ea304412f8 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -384,23 +384,27 @@ def __init__(self, argument_group: argparse._ArgumentGroup) -> None: def add_argument(self, *name_or_flags, help=None, **kwargs) -> argparse.Action: if self.argument_group.title == "Report generation": if help and help != argparse.SUPPRESS: - raise ValueError( - f"CLI documentation style error: help description for the Report generation flag {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." + 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"CLI documentation style error: flag help description for {name_or_flags} must be provided. (Currently, '{help}'.)" + 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"CLI documentation style error: flag help description for {name_or_flags} must start with a capital letter (or unicameral symbol). (Currently, '{help}'.)" + 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"CLI documentation style error: flag help description for {name_or_flags} must NOT end with a period. (Currently, '{help}'.)" + 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) @@ -435,19 +439,23 @@ def add_argument_group( ]: # These are built-in names, ignore them. if not title[0].isupper(): raise ValueError( - f"CLI documentation style error: Title of group {title} must start with a capital letter. (Currently, '{title[0]}'.)" + 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} must start with a capital letter. (Currently, '{description[0]}'.)" + 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} must NOT end with terminal punction. (Currently, '{title[-1]}'.)" + 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} must end with terminal punction. (Currently, '{description[-1]}'.)" + 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)) @@ -538,7 +546,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), @@ -638,7 +647,8 @@ def add_invertible_flag( "-O", "--output", metavar="FORMAT", - # The metavar overrides the default of displaying the choices, so we have to explicitly display them. + # 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, ) @@ -1167,7 +1177,8 @@ def add_invertible_flag( 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.", + + " These features may be enabled by default in the future," + + " or perhaps moved to another section.", ) experimental_group.add_argument( "--enable-incomplete-feature", @@ -1240,7 +1251,9 @@ def add_invertible_flag( "--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", + help="If --junit-xml is set, specifies format." + + " global: single test with all errors;" + + " per_file: one test entry per file with failures", ) misc_group.add_argument( "--scripts-are-modules", @@ -1596,7 +1609,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, ) From 67dd1156c60f2371349c5cffe59c08491ac057b1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 11 May 2025 22:36:15 +0000 Subject: [PATCH 25/32] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/main.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mypy/main.py b/mypy/main.py index b9ea304412f8..f06c67197696 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -386,20 +386,20 @@ def add_argument(self, *name_or_flags, help=None, **kwargs) -> argparse.Action: 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." + + 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}'.)" + + 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}'.)" + + f" must start with a capital letter (or unicameral symbol). (Currently, '{help}'.)" ) if help[-1] == ".": raise ValueError( From 185371aaeae9136312587b1147cfc155f090fec4 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Sun, 11 May 2025 15:02:28 -0700 Subject: [PATCH 26/32] readd link I accidentally deleted --- mypy/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/main.py b/mypy/main.py index f06c67197696..d885081cd988 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -855,7 +855,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#no-strict-optional.", ) add_invertible_flag( "--implicit-optional", From 91233ca18b188be575b2fedaeef389aa7597be5c Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Sun, 11 May 2025 16:12:23 -0700 Subject: [PATCH 27/32] name that occurs in python 3.9 --- mypy/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypy/main.py b/mypy/main.py index d885081cd988..46b19d7b16ca 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -436,6 +436,7 @@ def add_argument_group( if title not in [ "positional arguments", "options", + "optional argument" # name in python 3.9 ]: # These are built-in names, ignore them. if not title[0].isupper(): raise ValueError( From 52d2a4c4a3f93f3043cd3def40f47dfb22bcc0fb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 11 May 2025 23:13:48 +0000 Subject: [PATCH 28/32] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/main.py b/mypy/main.py index 46b19d7b16ca..cf41cda960be 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -436,7 +436,7 @@ def add_argument_group( if title not in [ "positional arguments", "options", - "optional argument" # name in python 3.9 + "optional argument", # name in python 3.9 ]: # These are built-in names, ignore them. if not title[0].isupper(): raise ValueError( From 26e221d14ac52596e110f981d09f2cd183341b3a Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Sun, 11 May 2025 16:34:40 -0700 Subject: [PATCH 29/32] fix ci problems (type errors) --- mypy/main.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/mypy/main.py b/mypy/main.py index cf41cda960be..ad5f61a89dfc 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -374,14 +374,16 @@ def is_terminal_punctuation(char: str) -> bool: return char in (".", "?", "!") -class ArgumentGroup: +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, help=None, **kwargs) -> argparse.Action: + 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( @@ -408,8 +410,9 @@ def add_argument(self, *name_or_flags, help=None, **kwargs) -> argparse.Action: ) return self.argument_group.add_argument(*name_or_flags, help=help, **kwargs) - def _add_action(self, action) -> None: - self.argument_group._add_action(action) + 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): @@ -431,8 +434,13 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: # ===================== # We just hard fail on these, as CI will ensure the runtime errors never get to users. def add_argument_group( - self, title: str, description: str | None = None, **kwargs + 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", From 970686271b82b45e599fcadaf5801fa8c9a4cff9 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Sun, 11 May 2025 16:35:44 -0700 Subject: [PATCH 30/32] hold on a minute, this needs an s --- mypy/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/main.py b/mypy/main.py index ad5f61a89dfc..71b7548d7b68 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -444,7 +444,7 @@ def add_argument_group( if title not in [ "positional arguments", "options", - "optional argument", # name in python 3.9 + "optional arguments", # name in python 3.9 ]: # These are built-in names, ignore them. if not title[0].isupper(): raise ValueError( From 01324aabe2779e394be17e6673f8598e321ebe9a Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Sat, 10 May 2025 02:23:10 -0700 Subject: [PATCH 31/32] my arguments were in the wrong order --- mypy/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/main.py b/mypy/main.py index 71b7548d7b68..d7b69382eeec 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -1444,7 +1444,7 @@ def process_options( stderr = stderr if stderr is not None else sys.stderr parser, _, strict_flag_assignments = define_options( - header, program, stdout, stderr, server_options + program, header, stdout, stderr, server_options ) # Parse arguments once into a dummy namespace so we can get the From a3b62b54f1e723dedbb5373b2ee26dbc9fa2855e Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Sun, 3 Aug 2025 14:37:23 -0700 Subject: [PATCH 32/32] Update main.py: fix terminal punctuation I was so real for this... --- mypy/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/main.py b/mypy/main.py index 643b6110149e..b102311eb1f7 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -864,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",