From 5020d0d983fdd4b3da9ef2c0227853c73c0d7a83 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Sun, 10 Aug 2025 07:00:12 -0700 Subject: [PATCH 01/17] easy win: remove one Any --- mypy/config_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/config_parser.py b/mypy/config_parser.py index 5f08f342241e..b841d5f585f6 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -600,7 +600,7 @@ def parse_section( return results, report_dirs -def convert_to_boolean(value: Any | None) -> bool: +def convert_to_boolean(value: object) -> bool: """Return a boolean value translating from other types if necessary.""" if isinstance(value, bool): return value From 7cfba8e4eefd1be6691bc0808cf2300a355d43c1 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Sun, 10 Aug 2025 07:02:58 -0700 Subject: [PATCH 02/17] easy win 2: remove an Any --- mypy/config_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/config_parser.py b/mypy/config_parser.py index b841d5f585f6..0b512e56211b 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -31,7 +31,7 @@ class VersionTypeError(argparse.ArgumentTypeError): """Provide a fallback value if the Python version is unsupported.""" - def __init__(self, *args: Any, fallback: tuple[int, int]) -> None: + def __init__(self, *args: object, fallback: tuple[int, int]) -> None: self.fallback = fallback super().__init__(*args) From 32a9efe2833d9079a1fae8a90f99fc451487509e Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Sun, 10 Aug 2025 15:13:52 -0700 Subject: [PATCH 03/17] tentatively start refactoring more Anys --- mypy/config_parser.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/mypy/config_parser.py b/mypy/config_parser.py index 0b512e56211b..23e95fd3b2f7 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -16,7 +16,7 @@ import tomli as tomllib from collections.abc import Mapping, MutableMapping, Sequence -from typing import Any, Callable, Final, TextIO, Union +from typing import Any, Callable, Final, TextIO, TypeVar, TypedDict, Union from typing_extensions import Never, TypeAlias from mypy import defaults @@ -25,7 +25,7 @@ _CONFIG_VALUE_TYPES: TypeAlias = Union[ str, bool, int, float, dict[str, str], list[str], tuple[int, int] ] -_INI_PARSER_CALLABLE: TypeAlias = Callable[[Any], _CONFIG_VALUE_TYPES] +_INI_PARSER_CALLABLE: TypeAlias = Callable[[str], _CONFIG_VALUE_TYPES] class VersionTypeError(argparse.ArgumentTypeError): @@ -246,21 +246,22 @@ def split_commas(value: str) -> list[str]: def _parse_individual_file( config_file: str, stderr: TextIO | None = None -) -> tuple[MutableMapping[str, Any], dict[str, _INI_PARSER_CALLABLE], str] | None: +) -> tuple[MutableMapping[str, _CONFIG_VALUE_TYPES], dict[str, _INI_PARSER_CALLABLE], str] | None: if not os.path.exists(config_file): return None - parser: MutableMapping[str, Any] + parser: MutableMapping[str, Mapping[str, str]] try: if is_toml(config_file): with open(config_file, "rb") as f: - toml_data = tomllib.load(f) + # This type is not actually 100% comprehensive. However, load returns Any, so it doesn't complain. + toml_data: dict[str, dict[str, _CONFIG_VALUE_TYPES]] = tomllib.load(f) # Filter down to just mypy relevant toml keys - toml_data = toml_data.get("tool", {}) - if "mypy" not in toml_data: + toml_data_tool = toml_data.get("tool", {}) + if "mypy" not in toml_data_tool: return None - toml_data = {"mypy": toml_data["mypy"]} + toml_data = {"mypy": toml_data_tool["mypy"]} parser = destructure_overrides(toml_data) config_types = toml_config_types else: @@ -280,7 +281,7 @@ def _parse_individual_file( def _find_config_file( stderr: TextIO | None = None, -) -> tuple[MutableMapping[str, Any], dict[str, _INI_PARSER_CALLABLE], str] | None: +) -> tuple[MutableMapping[str, Mapping[str, str]], dict[str, _INI_PARSER_CALLABLE], str] | None: current_dir = os.path.abspath(os.getcwd()) @@ -407,8 +408,11 @@ def get_prefix(file_read: str, name: str) -> str: def is_toml(filename: str) -> bool: return filename.lower().endswith(".toml") - -def destructure_overrides(toml_data: dict[str, Any]) -> dict[str, Any]: +T = TypeVar("T") +_TypeThatDOWants = TypedDict("_TypeThatDOWants", {"mypy": dict[str , dict[str, _CONFIG_VALUE_TYPES]]}) +def destructure_overrides( + toml_data: _TypeThatDOWants + ) -> _TypeThatDOWants: """Take the new [[tool.mypy.overrides]] section array in the pyproject.toml file, and convert it back to a flatter structure that the existing config_parser can handle. From 637cb1550bb00f7aff3dd114650ee2784404cc55 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Wed, 13 Aug 2025 19:20:37 -0700 Subject: [PATCH 04/17] difficult win 1: correctly type toml, probably this actually is starting to collide with the other places Any is used, which is getting annoying, but you have to commit incremental progress at some time... --- mypy/config_parser.py | 72 +++++++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/mypy/config_parser.py b/mypy/config_parser.py index 23e95fd3b2f7..91e2126a16ab 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -2,6 +2,7 @@ import argparse import configparser +import datetime import glob as fileglob import os import re @@ -16,7 +17,7 @@ import tomli as tomllib from collections.abc import Mapping, MutableMapping, Sequence -from typing import Any, Callable, Final, TextIO, TypeVar, TypedDict, Union +from typing import Any, Callable, Final, TextIO, TypedDict, Union from typing_extensions import Never, TypeAlias from mypy import defaults @@ -243,33 +244,49 @@ def split_commas(value: str) -> list[str]: } ) +_TomlValue = Union[str, int, float, bool, datetime.datetime, datetime.date, datetime.time, list['_TomlValue'], dict[str, '_TomlValue']] +_TomlDict = dict[str, _TomlValue] +_TomlDictMypy = TypedDict("_TomlDictMypy", {"mypy": _TomlDict}) +# Sort of like MutableMapping[str, _CONFIG_VALUE_TYPES], but with more (useless) types in it: +_ParserHelper = _TomlDictMypy | configparser.RawConfigParser def _parse_individual_file( config_file: str, stderr: TextIO | None = None -) -> tuple[MutableMapping[str, _CONFIG_VALUE_TYPES], dict[str, _INI_PARSER_CALLABLE], str] | None: - - if not os.path.exists(config_file): - return None - - parser: MutableMapping[str, Mapping[str, str]] +) -> tuple[ + _ParserHelper, + dict[str, _INI_PARSER_CALLABLE], + str + ] | None: try: if is_toml(config_file): with open(config_file, "rb") as f: - # This type is not actually 100% comprehensive. However, load returns Any, so it doesn't complain. - toml_data: dict[str, dict[str, _CONFIG_VALUE_TYPES]] = tomllib.load(f) + # tomllib.load returns dict[str, Any], so it doesn't complain about any type on the lhs. + # However, this is probably the actual return type of tomllib.load, + # assuming the optional parse_float is not used. (Indeed, we do not use it.) + # See https://docs.python.org/3/library/tomllib.html#conversion-table + # and https://github.com/hukkin/tomli/issues/261 for more info. + toml_data: _TomlDict = tomllib.load(f) # Filter down to just mypy relevant toml keys toml_data_tool = toml_data.get("tool", {}) - if "mypy" not in toml_data_tool: + if not(isinstance(toml_data_tool, dict)) or "mypy" not in toml_data_tool: + # Here we might be dealing with a toml that just doesn't talk about mypy, + # in which case we currently just ignore it. (Maybe we should really warn?) return None - toml_data = {"mypy": toml_data_tool["mypy"]} - parser = destructure_overrides(toml_data) + if not isinstance(toml_data_tool["mypy"], dict): + raise MypyConfigTOMLValueError( + "If it exists, tool.mypy value must be a table, aka dict. " + "Please make sure you are using appropriate syntax. " + "https://toml.io/en/v1.0.0#table" + ) + toml_data_mypy: _TomlDictMypy = {"mypy": toml_data_tool["mypy"]} #ignore other tools + parser = destructure_overrides(toml_data_mypy) config_types = toml_config_types else: parser = configparser.RawConfigParser() parser.read(config_file) config_types = ini_config_types - except (tomllib.TOMLDecodeError, configparser.Error, ConfigTOMLValueError) as err: + except (FileNotFoundError, tomllib.TOMLDecodeError, configparser.Error, MypyConfigTOMLValueError) as err: print(f"{config_file}: {err}", file=stderr) return None @@ -281,7 +298,7 @@ def _parse_individual_file( def _find_config_file( stderr: TextIO | None = None, -) -> tuple[MutableMapping[str, Mapping[str, str]], dict[str, _INI_PARSER_CALLABLE], str] | None: +) -> tuple[_ParserHelper, dict[str, _INI_PARSER_CALLABLE], str] | None: current_dir = os.path.abspath(os.getcwd()) @@ -406,13 +423,10 @@ def get_prefix(file_read: str, name: str) -> str: def is_toml(filename: str) -> bool: + """Detect if a file "is toml", in the sense that it's named *.toml (case-insensitive).""" return filename.lower().endswith(".toml") -T = TypeVar("T") -_TypeThatDOWants = TypedDict("_TypeThatDOWants", {"mypy": dict[str , dict[str, _CONFIG_VALUE_TYPES]]}) -def destructure_overrides( - toml_data: _TypeThatDOWants - ) -> _TypeThatDOWants: +def destructure_overrides(toml_data: _TomlDictMypy) -> _TomlDictMypy: """Take the new [[tool.mypy.overrides]] section array in the pyproject.toml file, and convert it back to a flatter structure that the existing config_parser can handle. @@ -444,19 +458,25 @@ def destructure_overrides( }, } """ + if "overrides" not in toml_data["mypy"]: return toml_data - if not isinstance(toml_data["mypy"]["overrides"], list): - raise ConfigTOMLValueError( + result = toml_data.copy() + if not isinstance(result["mypy"]["overrides"], list): + raise MypyConfigTOMLValueError( "tool.mypy.overrides sections must be an array. Please make " "sure you are using double brackets like so: [[tool.mypy.overrides]]" ) - result = toml_data.copy() for override in result["mypy"]["overrides"]: + if not isinstance(override, dict): + raise MypyConfigTOMLValueError( + "tool.mypy.overrides sections must be an array of tables. Please make " + "sure you are using double brackets like so: [[tool.mypy.overrides]]" + ) if "module" not in override: - raise ConfigTOMLValueError( + raise MypyConfigTOMLValueError( "toml config file contains a [[tool.mypy.overrides]] " "section, but no module to override was specified." ) @@ -466,7 +486,7 @@ def destructure_overrides( elif isinstance(override["module"], list): modules = override["module"] else: - raise ConfigTOMLValueError( + raise MypyConfigTOMLValueError( "toml config file contains a [[tool.mypy.overrides]] " "section with a module value that is not a string or a list of " "strings" @@ -484,7 +504,7 @@ def destructure_overrides( new_key in result[old_config_name] and result[old_config_name][new_key] != new_value ): - raise ConfigTOMLValueError( + raise MypyConfigTOMLValueError( "toml config file contains " "[[tool.mypy.overrides]] sections with conflicting " f"values. Module '{module}' has two different values for '{new_key}'" @@ -740,5 +760,5 @@ def get_config_module_names(filename: str | None, modules: list[str]) -> str: return "module = ['%s']" % ("', '".join(sorted(modules))) -class ConfigTOMLValueError(ValueError): +class MypyConfigTOMLValueError(ValueError): pass From 0775b1ce6f666f893f9e12908b9e787727301057 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Wed, 13 Aug 2025 20:56:07 -0700 Subject: [PATCH 05/17] solve all of the typing problems?! --- mypy/config_parser.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/mypy/config_parser.py b/mypy/config_parser.py index 91e2126a16ab..5f9c5516b353 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -17,7 +17,7 @@ import tomli as tomllib from collections.abc import Mapping, MutableMapping, Sequence -from typing import Any, Callable, Final, TextIO, TypedDict, Union +from typing import Callable, Final, TextIO, TypedDict, Union from typing_extensions import Never, TypeAlias from mypy import defaults @@ -246,8 +246,7 @@ def split_commas(value: str) -> list[str]: _TomlValue = Union[str, int, float, bool, datetime.datetime, datetime.date, datetime.time, list['_TomlValue'], dict[str, '_TomlValue']] _TomlDict = dict[str, _TomlValue] -_TomlDictMypy = TypedDict("_TomlDictMypy", {"mypy": _TomlDict}) -# Sort of like MutableMapping[str, _CONFIG_VALUE_TYPES], but with more (useless) types in it: +_TomlDictMypy = dict[ str, _TomlDict ] _ParserHelper = _TomlDictMypy | configparser.RawConfigParser def _parse_individual_file( @@ -274,11 +273,12 @@ def _parse_individual_file( return None if not isinstance(toml_data_tool["mypy"], dict): raise MypyConfigTOMLValueError( - "If it exists, tool.mypy value must be a table, aka dict. " + "If it exists, tool.mypy must be a table, aka dict. " "Please make sure you are using appropriate syntax. " "https://toml.io/en/v1.0.0#table" ) - toml_data_mypy: _TomlDictMypy = {"mypy": toml_data_tool["mypy"]} #ignore other tools + # Ignore other tools' sections, filtering down to just ours: + toml_data_mypy: _TomlDictMypy = {"mypy": toml_data_tool["mypy"]} parser = destructure_overrides(toml_data_mypy) config_types = toml_config_types else: @@ -426,7 +426,7 @@ def is_toml(filename: str) -> bool: """Detect if a file "is toml", in the sense that it's named *.toml (case-insensitive).""" return filename.lower().endswith(".toml") -def destructure_overrides(toml_data: _TomlDictMypy) -> _TomlDictMypy: +def destructure_overrides(toml_data: _TomlDictMypy) -> _ParserHelper: """Take the new [[tool.mypy.overrides]] section array in the pyproject.toml file, and convert it back to a flatter structure that the existing config_parser can handle. @@ -514,13 +514,12 @@ def destructure_overrides(toml_data: _TomlDictMypy) -> _TomlDictMypy: del result["mypy"]["overrides"] return result - def parse_section( prefix: str, template: Options, set_strict_flags: Callable[[], None], - section: Mapping[str, Any], - config_types: dict[str, Any], + section: Mapping[str, object], + config_types: Mapping[str, object], stderr: TextIO = sys.stderr, ) -> tuple[dict[str, object], dict[str, str]]: """Parse one section of a config file. @@ -582,7 +581,7 @@ def parse_section( else: continue ct = type(dv) - v: Any = None + v = None try: if ct is bool: if isinstance(section, dict): @@ -596,6 +595,7 @@ def parse_section( print(f"{prefix}Can not invert non-boolean key {options_key}", file=stderr) continue try: + # Why does pyright complain that 0 positional arguments are accepted here? v = ct(section.get(key)) except VersionTypeError as err_version: print(f"{prefix}{key}: {err_version}", file=stderr) From a386f88b2375a273850ec5cccb4719129e582555 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Wed, 13 Aug 2025 20:58:13 -0700 Subject: [PATCH 06/17] rename my _TomlDictMypy to _TomlDictDict --- mypy/config_parser.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mypy/config_parser.py b/mypy/config_parser.py index 5f9c5516b353..4a52160f69d0 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -246,8 +246,8 @@ def split_commas(value: str) -> list[str]: _TomlValue = Union[str, int, float, bool, datetime.datetime, datetime.date, datetime.time, list['_TomlValue'], dict[str, '_TomlValue']] _TomlDict = dict[str, _TomlValue] -_TomlDictMypy = dict[ str, _TomlDict ] -_ParserHelper = _TomlDictMypy | configparser.RawConfigParser +_TomlDictDict = dict[ str, _TomlDict ] +_ParserHelper = _TomlDictDict | configparser.RawConfigParser def _parse_individual_file( config_file: str, stderr: TextIO | None = None @@ -278,7 +278,7 @@ def _parse_individual_file( "https://toml.io/en/v1.0.0#table" ) # Ignore other tools' sections, filtering down to just ours: - toml_data_mypy: _TomlDictMypy = {"mypy": toml_data_tool["mypy"]} + toml_data_mypy: _TomlDictDict = {"mypy": toml_data_tool["mypy"]} parser = destructure_overrides(toml_data_mypy) config_types = toml_config_types else: @@ -426,7 +426,7 @@ def is_toml(filename: str) -> bool: """Detect if a file "is toml", in the sense that it's named *.toml (case-insensitive).""" return filename.lower().endswith(".toml") -def destructure_overrides(toml_data: _TomlDictMypy) -> _ParserHelper: +def destructure_overrides(toml_data: _TomlDictDict) -> _ParserHelper: """Take the new [[tool.mypy.overrides]] section array in the pyproject.toml file, and convert it back to a flatter structure that the existing config_parser can handle. From f7171c04a90a9f1d400b59871385e541c01240bf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 03:59:36 +0000 Subject: [PATCH 07/17] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/config_parser.py | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/mypy/config_parser.py b/mypy/config_parser.py index 4a52160f69d0..dc804269ebb5 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -16,8 +16,8 @@ else: import tomli as tomllib -from collections.abc import Mapping, MutableMapping, Sequence -from typing import Callable, Final, TextIO, TypedDict, Union +from collections.abc import Mapping, Sequence +from typing import Callable, Final, TextIO, Union from typing_extensions import Never, TypeAlias from mypy import defaults @@ -244,18 +244,25 @@ def split_commas(value: str) -> list[str]: } ) -_TomlValue = Union[str, int, float, bool, datetime.datetime, datetime.date, datetime.time, list['_TomlValue'], dict[str, '_TomlValue']] +_TomlValue = Union[ + str, + int, + float, + bool, + datetime.datetime, + datetime.date, + datetime.time, + list["_TomlValue"], + dict[str, "_TomlValue"], +] _TomlDict = dict[str, _TomlValue] -_TomlDictDict = dict[ str, _TomlDict ] +_TomlDictDict = dict[str, _TomlDict] _ParserHelper = _TomlDictDict | configparser.RawConfigParser + def _parse_individual_file( config_file: str, stderr: TextIO | None = None -) -> tuple[ - _ParserHelper, - dict[str, _INI_PARSER_CALLABLE], - str - ] | None: +) -> tuple[_ParserHelper, dict[str, _INI_PARSER_CALLABLE], str] | None: try: if is_toml(config_file): with open(config_file, "rb") as f: @@ -267,7 +274,7 @@ def _parse_individual_file( toml_data: _TomlDict = tomllib.load(f) # Filter down to just mypy relevant toml keys toml_data_tool = toml_data.get("tool", {}) - if not(isinstance(toml_data_tool, dict)) or "mypy" not in toml_data_tool: + if not (isinstance(toml_data_tool, dict)) or "mypy" not in toml_data_tool: # Here we might be dealing with a toml that just doesn't talk about mypy, # in which case we currently just ignore it. (Maybe we should really warn?) return None @@ -286,7 +293,12 @@ def _parse_individual_file( parser.read(config_file) config_types = ini_config_types - except (FileNotFoundError, tomllib.TOMLDecodeError, configparser.Error, MypyConfigTOMLValueError) as err: + except ( + FileNotFoundError, + tomllib.TOMLDecodeError, + configparser.Error, + MypyConfigTOMLValueError, + ) as err: print(f"{config_file}: {err}", file=stderr) return None @@ -426,6 +438,7 @@ def is_toml(filename: str) -> bool: """Detect if a file "is toml", in the sense that it's named *.toml (case-insensitive).""" return filename.lower().endswith(".toml") + def destructure_overrides(toml_data: _TomlDictDict) -> _ParserHelper: """Take the new [[tool.mypy.overrides]] section array in the pyproject.toml file, and convert it back to a flatter structure that the existing config_parser can handle. @@ -514,6 +527,7 @@ def destructure_overrides(toml_data: _TomlDictDict) -> _ParserHelper: del result["mypy"]["overrides"] return result + def parse_section( prefix: str, template: Options, From 86edd12c12062a89838e9c0bc289f405c0392223 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Wed, 13 Aug 2025 21:04:52 -0700 Subject: [PATCH 08/17] use Union syntax instead of | for python 3.9 support also, trivial copyediting --- mypy/config_parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/config_parser.py b/mypy/config_parser.py index dc804269ebb5..786a488ab825 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -257,7 +257,7 @@ def split_commas(value: str) -> list[str]: ] _TomlDict = dict[str, _TomlValue] _TomlDictDict = dict[str, _TomlDict] -_ParserHelper = _TomlDictDict | configparser.RawConfigParser +_ParserHelper = Union[_TomlDictDict, configparser.RawConfigParser] def _parse_individual_file( @@ -274,7 +274,7 @@ def _parse_individual_file( toml_data: _TomlDict = tomllib.load(f) # Filter down to just mypy relevant toml keys toml_data_tool = toml_data.get("tool", {}) - if not (isinstance(toml_data_tool, dict)) or "mypy" not in toml_data_tool: + if not isinstance(toml_data_tool, dict) or "mypy" not in toml_data_tool: # Here we might be dealing with a toml that just doesn't talk about mypy, # in which case we currently just ignore it. (Maybe we should really warn?) return None From 8a821c057b1f83dec201fe7ea29c7917a1473e8f Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Wed, 13 Aug 2025 21:22:36 -0700 Subject: [PATCH 09/17] get rid of type: ignore[attr-defined] --- mypy/config_parser.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mypy/config_parser.py b/mypy/config_parser.py index 786a488ab825..7cf3a1dea655 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -598,10 +598,9 @@ def parse_section( v = None try: if ct is bool: - if isinstance(section, dict): - v = convert_to_boolean(section.get(key)) - else: - v = section.getboolean(key) # type: ignore[attr-defined] # Until better stub + # ConfigParser has an equivalent (but poorly-stubbed) getboolean method, + # which we do not use. https://docs.python.org/3/library/configparser.html#configparser.ConfigParser.getboolean + v = convert_to_boolean(section.get(key)) if invert: v = not v elif callable(ct): From c4ef2e900e67aaf2fbe4b830dc33aa4306835cd6 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Wed, 13 Aug 2025 22:02:44 -0700 Subject: [PATCH 10/17] attempt to enforce str/list[str] condition on modules list this actually did not matter for the code, I guess. The only reason to change it was because mypy was complaining because mypy doesn't allow redefinition that way (at least with these settings). But instead of beating mypy into doing what I want, I just made the types follow the error message here. --- mypy/config_parser.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/mypy/config_parser.py b/mypy/config_parser.py index 7cf3a1dea655..048efbec642c 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -494,17 +494,20 @@ def destructure_overrides(toml_data: _TomlDictDict) -> _ParserHelper: "section, but no module to override was specified." ) - if isinstance(override["module"], str): - modules = [override["module"]] - elif isinstance(override["module"], list): - modules = override["module"] - else: + def complain_str_list() -> Never: raise MypyConfigTOMLValueError( "toml config file contains a [[tool.mypy.overrides]] " "section with a module value that is not a string or a list of " "strings" ) + if isinstance(override["module"], str): + modules = [override["module"]] + elif isinstance(override["module"], list): + modules = [m if isinstance(m, str) else complain_str_list() for m in override["module"]] + else: + complain_str_list() + for module in modules: module_overrides = override.copy() del module_overrides["module"] From f90492b2b7243d8fb2120679cccf0d35f7520518 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 05:04:33 +0000 Subject: [PATCH 11/17] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/config_parser.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mypy/config_parser.py b/mypy/config_parser.py index 048efbec642c..d437d0c80d95 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -504,7 +504,9 @@ def complain_str_list() -> Never: if isinstance(override["module"], str): modules = [override["module"]] elif isinstance(override["module"], list): - modules = [m if isinstance(m, str) else complain_str_list() for m in override["module"]] + modules = [ + m if isinstance(m, str) else complain_str_list() for m in override["module"] + ] else: complain_str_list() From 1c388b62c5d5b11322b7a655bf8a840d7bb7ad23 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Wed, 13 Aug 2025 22:10:44 -0700 Subject: [PATCH 12/17] experiment with removing v = None This attempts to get at mypy/config_parser.py:620: error: Incompatible types in assignment (expression has type tuple[int, int], variable has type Optional[bool]) [assignment] however, I think this leaves v undefined in the else path, which is bad --- mypy/config_parser.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mypy/config_parser.py b/mypy/config_parser.py index d437d0c80d95..437b92468b1a 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -600,7 +600,6 @@ def parse_section( else: continue ct = type(dv) - v = None try: if ct is bool: # ConfigParser has an equivalent (but poorly-stubbed) getboolean method, From d604151b4be43ca1a27524118e242a371673256d Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Wed, 13 Aug 2025 22:21:59 -0700 Subject: [PATCH 13/17] explicitly hint v, to appease mypy mostly --- mypy/config_parser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypy/config_parser.py b/mypy/config_parser.py index 437b92468b1a..2efdfa93559b 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -600,6 +600,7 @@ def parse_section( else: continue ct = type(dv) + v: None | bool | object | tuple[int, int] = None try: if ct is bool: # ConfigParser has an equivalent (but poorly-stubbed) getboolean method, From 64863ef5321df92bb11ee9f518e92e469fe11886 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Wed, 13 Aug 2025 23:57:24 -0700 Subject: [PATCH 14/17] restore the existence check --- mypy/config_parser.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/mypy/config_parser.py b/mypy/config_parser.py index 2efdfa93559b..c22063e14a76 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -261,11 +261,18 @@ def split_commas(value: str) -> list[str]: def _parse_individual_file( - config_file: str, stderr: TextIO | None = None + config_filename: str, stderr: TextIO | None = None ) -> tuple[_ParserHelper, dict[str, _INI_PARSER_CALLABLE], str] | None: + """Internal utility function for doing the first part of parsing config files. + Returns None for most conditions where the file doesn't exist or isn't about mypy + or isn't formatted correctly. Sometimes this prints an error.""" + + if not os.path.exists(config_filename): + return None + try: - if is_toml(config_file): - with open(config_file, "rb") as f: + if is_toml(config_filename): + with open(config_filename, "rb") as f: # tomllib.load returns dict[str, Any], so it doesn't complain about any type on the lhs. # However, this is probably the actual return type of tomllib.load, # assuming the optional parse_float is not used. (Indeed, we do not use it.) @@ -290,7 +297,7 @@ def _parse_individual_file( config_types = toml_config_types else: parser = configparser.RawConfigParser() - parser.read(config_file) + parser.read(config_filename) config_types = ini_config_types except ( @@ -299,13 +306,13 @@ def _parse_individual_file( configparser.Error, MypyConfigTOMLValueError, ) as err: - print(f"{config_file}: {err}", file=stderr) + print(f"{config_filename}: {err}", file=stderr) return None - if os.path.basename(config_file) in defaults.SHARED_CONFIG_NAMES and "mypy" not in parser: + if os.path.basename(config_filename) in defaults.SHARED_CONFIG_NAMES and "mypy" not in parser: return None - return parser, config_types, config_file + return parser, config_types, config_filename def _find_config_file( From 3c8b3f4d32ed648b06dd19210a3f31beb90353f9 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Thu, 14 Aug 2025 06:51:58 -0700 Subject: [PATCH 15/17] minor type cleanup --- mypy/config_parser.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mypy/config_parser.py b/mypy/config_parser.py index c22063e14a76..83158940f8ff 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -271,6 +271,7 @@ def _parse_individual_file( return None try: + parser: _ParserHelper if is_toml(config_filename): with open(config_filename, "rb") as f: # tomllib.load returns dict[str, Any], so it doesn't complain about any type on the lhs. @@ -446,7 +447,7 @@ def is_toml(filename: str) -> bool: return filename.lower().endswith(".toml") -def destructure_overrides(toml_data: _TomlDictDict) -> _ParserHelper: +def destructure_overrides(toml_data: _TomlDictDict) -> _TomlDictDict: """Take the new [[tool.mypy.overrides]] section array in the pyproject.toml file, and convert it back to a flatter structure that the existing config_parser can handle. @@ -545,7 +546,7 @@ def parse_section( template: Options, set_strict_flags: Callable[[], None], section: Mapping[str, object], - config_types: Mapping[str, object], + config_types: Mapping[str, object], # this is probably dict[str, _INI_PARSER_CALLABLE], but that causes more type errors at the moment. stderr: TextIO = sys.stderr, ) -> tuple[dict[str, object], dict[str, str]]: """Parse one section of a config file. @@ -620,7 +621,8 @@ def parse_section( print(f"{prefix}Can not invert non-boolean key {options_key}", file=stderr) continue try: - # Why does pyright complain that 0 positional arguments are accepted here? + # pyright complains that 0 positional arguments are accepted here + # (because ct might be type(None) for all it knows) v = ct(section.get(key)) except VersionTypeError as err_version: print(f"{prefix}{key}: {err_version}", file=stderr) From b40cbe721bbb57429b3bc34bd324671c18aabe2e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 13:57:19 +0000 Subject: [PATCH 16/17] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/config_parser.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mypy/config_parser.py b/mypy/config_parser.py index 83158940f8ff..4a29f6711e9e 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -546,7 +546,9 @@ def parse_section( template: Options, set_strict_flags: Callable[[], None], section: Mapping[str, object], - config_types: Mapping[str, object], # this is probably dict[str, _INI_PARSER_CALLABLE], but that causes more type errors at the moment. + config_types: Mapping[ + str, object + ], # this is probably dict[str, _INI_PARSER_CALLABLE], but that causes more type errors at the moment. stderr: TextIO = sys.stderr, ) -> tuple[dict[str, object], dict[str, str]]: """Parse one section of a config file. From 502ec62458da3e7b7f2e53f143adfc92d68b6a08 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Thu, 14 Aug 2025 15:16:53 -0700 Subject: [PATCH 17/17] My plan is simple: we catch the type error. --- mypy/config_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/config_parser.py b/mypy/config_parser.py index 4a29f6711e9e..aeb35a3c0282 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -635,7 +635,7 @@ def parse_section( else: print(f"{prefix}Don't know what type {key} should have", file=stderr) continue - except ValueError as err: + except (ValueError, TypeError) as err: print(f"{prefix}{key}: {err}", file=stderr) continue if key == "strict":