22
33import argparse
44import configparser
5+ import datetime
56import glob as fileglob
67import os
78import re
1617 import tomli as tomllib
1718
1819from 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
2021from typing_extensions import Never , TypeAlias
2122
2223from 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
247253def _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
282299def _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
408425def 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