Skip to content

Commit 637cb15

Browse files
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...
1 parent 32a9efe commit 637cb15

File tree

1 file changed

+46
-26
lines changed

1 file changed

+46
-26
lines changed

mypy/config_parser.py

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import argparse
44
import configparser
5+
import datetime
56
import glob as fileglob
67
import os
78
import re
@@ -16,7 +17,7 @@
1617
import tomli as tomllib
1718

1819
from collections.abc import Mapping, MutableMapping, Sequence
19-
from typing import Any, Callable, Final, TextIO, TypeVar, TypedDict, Union
20+
from typing import Any, Callable, Final, TextIO, TypedDict, Union
2021
from typing_extensions import Never, TypeAlias
2122

2223
from mypy import defaults
@@ -243,33 +244,49 @@ def split_commas(value: str) -> list[str]:
243244
}
244245
)
245246

247+
_TomlValue = Union[str, int, float, bool, datetime.datetime, datetime.date, datetime.time, list['_TomlValue'], dict[str, '_TomlValue']]
248+
_TomlDict = dict[str, _TomlValue]
249+
_TomlDictMypy = TypedDict("_TomlDictMypy", {"mypy": _TomlDict})
250+
# Sort of like MutableMapping[str, _CONFIG_VALUE_TYPES], but with more (useless) types in it:
251+
_ParserHelper = _TomlDictMypy | configparser.RawConfigParser
246252

247253
def _parse_individual_file(
248254
config_file: str, stderr: TextIO | None = None
249-
) -> tuple[MutableMapping[str, _CONFIG_VALUE_TYPES], dict[str, _INI_PARSER_CALLABLE], str] | None:
250-
251-
if not os.path.exists(config_file):
252-
return None
253-
254-
parser: MutableMapping[str, Mapping[str, str]]
255+
) -> tuple[
256+
_ParserHelper,
257+
dict[str, _INI_PARSER_CALLABLE],
258+
str
259+
] | None:
255260
try:
256261
if is_toml(config_file):
257262
with open(config_file, "rb") as f:
258-
# This type is not actually 100% comprehensive. However, load returns Any, so it doesn't complain.
259-
toml_data: dict[str, dict[str, _CONFIG_VALUE_TYPES]] = tomllib.load(f)
263+
# tomllib.load returns dict[str, Any], so it doesn't complain about any type on the lhs.
264+
# However, this is probably the actual return type of tomllib.load,
265+
# assuming the optional parse_float is not used. (Indeed, we do not use it.)
266+
# See https://docs.python.org/3/library/tomllib.html#conversion-table
267+
# and https://github.com/hukkin/tomli/issues/261 for more info.
268+
toml_data: _TomlDict = tomllib.load(f)
260269
# Filter down to just mypy relevant toml keys
261270
toml_data_tool = toml_data.get("tool", {})
262-
if "mypy" not in toml_data_tool:
271+
if not(isinstance(toml_data_tool, dict)) or "mypy" not in toml_data_tool:
272+
# Here we might be dealing with a toml that just doesn't talk about mypy,
273+
# in which case we currently just ignore it. (Maybe we should really warn?)
263274
return None
264-
toml_data = {"mypy": toml_data_tool["mypy"]}
265-
parser = destructure_overrides(toml_data)
275+
if not isinstance(toml_data_tool["mypy"], dict):
276+
raise MypyConfigTOMLValueError(
277+
"If it exists, tool.mypy value must be a table, aka dict. "
278+
"Please make sure you are using appropriate syntax. "
279+
"https://toml.io/en/v1.0.0#table"
280+
)
281+
toml_data_mypy: _TomlDictMypy = {"mypy": toml_data_tool["mypy"]} #ignore other tools
282+
parser = destructure_overrides(toml_data_mypy)
266283
config_types = toml_config_types
267284
else:
268285
parser = configparser.RawConfigParser()
269286
parser.read(config_file)
270287
config_types = ini_config_types
271288

272-
except (tomllib.TOMLDecodeError, configparser.Error, ConfigTOMLValueError) as err:
289+
except (FileNotFoundError, tomllib.TOMLDecodeError, configparser.Error, MypyConfigTOMLValueError) as err:
273290
print(f"{config_file}: {err}", file=stderr)
274291
return None
275292

@@ -281,7 +298,7 @@ def _parse_individual_file(
281298

282299
def _find_config_file(
283300
stderr: TextIO | None = None,
284-
) -> tuple[MutableMapping[str, Mapping[str, str]], dict[str, _INI_PARSER_CALLABLE], str] | None:
301+
) -> tuple[_ParserHelper, dict[str, _INI_PARSER_CALLABLE], str] | None:
285302

286303
current_dir = os.path.abspath(os.getcwd())
287304

@@ -406,13 +423,10 @@ def get_prefix(file_read: str, name: str) -> str:
406423

407424

408425
def is_toml(filename: str) -> bool:
426+
"""Detect if a file "is toml", in the sense that it's named *.toml (case-insensitive)."""
409427
return filename.lower().endswith(".toml")
410428

411-
T = TypeVar("T")
412-
_TypeThatDOWants = TypedDict("_TypeThatDOWants", {"mypy": dict[str , dict[str, _CONFIG_VALUE_TYPES]]})
413-
def destructure_overrides(
414-
toml_data: _TypeThatDOWants
415-
) -> _TypeThatDOWants:
429+
def destructure_overrides(toml_data: _TomlDictMypy) -> _TomlDictMypy:
416430
"""Take the new [[tool.mypy.overrides]] section array in the pyproject.toml file,
417431
and convert it back to a flatter structure that the existing config_parser can handle.
418432
@@ -444,19 +458,25 @@ def destructure_overrides(
444458
},
445459
}
446460
"""
461+
447462
if "overrides" not in toml_data["mypy"]:
448463
return toml_data
449464

450-
if not isinstance(toml_data["mypy"]["overrides"], list):
451-
raise ConfigTOMLValueError(
465+
result = toml_data.copy()
466+
if not isinstance(result["mypy"]["overrides"], list):
467+
raise MypyConfigTOMLValueError(
452468
"tool.mypy.overrides sections must be an array. Please make "
453469
"sure you are using double brackets like so: [[tool.mypy.overrides]]"
454470
)
455471

456-
result = toml_data.copy()
457472
for override in result["mypy"]["overrides"]:
473+
if not isinstance(override, dict):
474+
raise MypyConfigTOMLValueError(
475+
"tool.mypy.overrides sections must be an array of tables. Please make "
476+
"sure you are using double brackets like so: [[tool.mypy.overrides]]"
477+
)
458478
if "module" not in override:
459-
raise ConfigTOMLValueError(
479+
raise MypyConfigTOMLValueError(
460480
"toml config file contains a [[tool.mypy.overrides]] "
461481
"section, but no module to override was specified."
462482
)
@@ -466,7 +486,7 @@ def destructure_overrides(
466486
elif isinstance(override["module"], list):
467487
modules = override["module"]
468488
else:
469-
raise ConfigTOMLValueError(
489+
raise MypyConfigTOMLValueError(
470490
"toml config file contains a [[tool.mypy.overrides]] "
471491
"section with a module value that is not a string or a list of "
472492
"strings"
@@ -484,7 +504,7 @@ def destructure_overrides(
484504
new_key in result[old_config_name]
485505
and result[old_config_name][new_key] != new_value
486506
):
487-
raise ConfigTOMLValueError(
507+
raise MypyConfigTOMLValueError(
488508
"toml config file contains "
489509
"[[tool.mypy.overrides]] sections with conflicting "
490510
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:
740760
return "module = ['%s']" % ("', '".join(sorted(modules)))
741761

742762

743-
class ConfigTOMLValueError(ValueError):
763+
class MypyConfigTOMLValueError(ValueError):
744764
pass

0 commit comments

Comments
 (0)