Skip to content

Commit 5ad39cd

Browse files
committed
add support for mypy.toml and .mypy.toml configuration files
1 parent 69a0925 commit 5ad39cd

6 files changed

Lines changed: 140 additions & 31 deletions

File tree

docs/source/command_line.rst

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,10 @@ Config file
114114

115115
This flag makes mypy read configuration settings from the given file.
116116

117-
By default settings are read from ``mypy.ini``, ``.mypy.ini``, ``pyproject.toml``, or ``setup.cfg``
118-
in the current directory. Settings override mypy's built-in defaults and
119-
command line flags can override settings.
117+
By default settings are read from ``mypy.ini``, ``.mypy.ini``, ``mypy.toml``,
118+
``.mypy.toml``, ``pyproject.toml``, or ``setup.cfg`` in the current
119+
directory. Settings override mypy's built-in defaults and command line
120+
flags can override settings.
120121

121122
Specifying :option:`--config-file= <--config-file>` (with no filename) will ignore *all*
122123
config files.

docs/source/config_file.rst

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ the following configuration files (in this order):
1414

1515
1. ``mypy.ini``
1616
2. ``.mypy.ini``
17-
3. ``pyproject.toml`` (containing a ``[tool.mypy]`` section)
18-
4. ``setup.cfg`` (containing a ``[mypy]`` section)
17+
3. ``mypy.toml``
18+
4. ``.mypy.toml``
19+
5. ``pyproject.toml`` (containing a ``[tool.mypy]`` section)
20+
6. ``setup.cfg`` (containing a ``[mypy]`` section)
1921

2022
If no configuration file is found by this method, mypy will then look for
2123
configuration files in the following locations (in this order):
@@ -49,8 +51,15 @@ The configuration file format is the usual
4951
section names in square brackets and flag settings of the form
5052
`NAME = VALUE`. Comments start with ``#`` characters.
5153

52-
- A section named ``[mypy]`` must be present. This specifies
53-
the global flags.
54+
Mypy also supports TOML configuration in two forms:
55+
56+
* ``pyproject.toml`` with options under ``[tool.mypy]`` and per-module
57+
overrides under ``[[tool.mypy.overrides]]``
58+
* ``mypy.toml`` or ``.mypy.toml`` with options at the top level and
59+
per-module overrides under ``[[mypy.overrides]]``
60+
61+
- In INI-based config files, a section named ``[mypy]`` must be present.
62+
This specifies the global flags.
5463

5564
- Additional sections named ``[mypy-PATTERN1,PATTERN2,...]`` may be
5665
present, where ``PATTERN1``, ``PATTERN2``, etc., are comma-separated
@@ -1280,6 +1289,45 @@ of your repo (or append it to the end of an existing ``pyproject.toml`` file) an
12801289
]
12811290
ignore_missing_imports = true
12821291
1292+
Using a mypy.toml file
1293+
**********************
1294+
1295+
``mypy.toml`` and ``.mypy.toml`` are also supported. They use the same
1296+
TOML value rules as ``pyproject.toml``, but the mypy options live at the
1297+
top level instead of under ``[tool.mypy]``.
1298+
1299+
Example ``mypy.toml``
1300+
*********************
1301+
1302+
.. code-block:: toml
1303+
1304+
# mypy global options:
1305+
1306+
python_version = "3.9"
1307+
warn_return_any = true
1308+
warn_unused_configs = true
1309+
exclude = [
1310+
'^file1\.py$', # TOML literal string (single-quotes, no escaping necessary)
1311+
"^file2\\.py$", # TOML basic string (double-quotes, backslash and other characters need escaping)
1312+
]
1313+
1314+
# mypy per-module options:
1315+
1316+
[[mypy.overrides]]
1317+
module = "mycode.foo.*"
1318+
disallow_untyped_defs = true
1319+
1320+
[[mypy.overrides]]
1321+
module = "mycode.bar"
1322+
warn_return_any = false
1323+
1324+
[[mypy.overrides]]
1325+
module = [
1326+
"somelibrary",
1327+
"some_other_library"
1328+
]
1329+
ignore_missing_imports = true
1330+
12831331
.. _lxml: https://pypi.org/project/lxml/
12841332
.. _SQLite: https://www.sqlite.org/
12851333
.. _PEP 518: https://www.python.org/dev/peps/pep-0518/

mypy/config_parser.py

Lines changed: 58 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -245,12 +245,10 @@ def _parse_individual_file(
245245
if is_toml(config_file):
246246
with open(config_file, "rb") as f:
247247
toml_data = tomllib.load(f)
248-
# Filter down to just mypy relevant toml keys
249-
toml_data = toml_data.get("tool", {})
250-
if "mypy" not in toml_data:
248+
toml_data = get_mypy_toml_data(config_file, toml_data)
249+
if toml_data is None:
251250
return None
252-
toml_data = {"mypy": toml_data["mypy"]}
253-
parser = destructure_overrides(toml_data)
251+
parser = destructure_overrides(toml_data, config_file)
254252
config_types = toml_config_types
255253
else:
256254
parser = configparser.RawConfigParser()
@@ -397,20 +395,48 @@ def is_toml(filename: str) -> bool:
397395
return filename.lower().endswith(".toml")
398396

399397

400-
def destructure_overrides(toml_data: dict[str, Any]) -> dict[str, Any]:
401-
"""Take the new [[tool.mypy.overrides]] section array in the pyproject.toml file,
402-
and convert it back to a flatter structure that the existing config_parser can handle.
398+
def is_pyproject(filename: str) -> bool:
399+
return os.path.basename(filename) == "pyproject.toml"
403400

404-
E.g. the following pyproject.toml file:
405401

406-
[[tool.mypy.overrides]]
402+
def get_mypy_toml_data(
403+
config_file: str,
404+
toml_data: dict[str, Any],
405+
) -> dict[str, Any] | None:
406+
if is_pyproject(config_file):
407+
toml_data = toml_data.get("tool", {})
408+
if "mypy" not in toml_data:
409+
return None
410+
return {"mypy": toml_data["mypy"]}
411+
412+
if "mypy" in toml_data:
413+
return toml_data
414+
415+
return {"mypy": toml_data}
416+
417+
418+
def _toml_module_error(config_file: str, message: str) -> str:
419+
if is_pyproject(config_file):
420+
return message.format(prefix="tool.mypy", override="[[tool.mypy.overrides]]")
421+
return message.format(prefix="mypy", override="[[mypy.overrides]]")
422+
423+
424+
def destructure_overrides(toml_data: dict[str, Any], config_file: str) -> dict[str, Any]:
425+
"""Convert TOML overrides sections into the flatter ini-style structure.
426+
427+
``pyproject.toml`` uses ``[[tool.mypy.overrides]]``.
428+
``mypy.toml`` and ``.mypy.toml`` use ``[[mypy.overrides]]``.
429+
430+
E.g. the following TOML file:
431+
432+
[[mypy.overrides]]
407433
module = [
408434
"a.b",
409435
"b.*"
410436
]
411437
disallow_untyped_defs = true
412438
413-
[[tool.mypy.overrides]]
439+
[[mypy.overrides]]
414440
module = 'c'
415441
disallow_untyped_defs = false
416442
@@ -434,16 +460,22 @@ def destructure_overrides(toml_data: dict[str, Any]) -> dict[str, Any]:
434460

435461
if not isinstance(toml_data["mypy"]["overrides"], list):
436462
raise ConfigTOMLValueError(
437-
"tool.mypy.overrides sections must be an array. Please make "
438-
"sure you are using double brackets like so: [[tool.mypy.overrides]]"
463+
_toml_module_error(
464+
config_file,
465+
"{prefix}.overrides sections must be an array. Please make "
466+
"sure you are using double brackets like so: {override}",
467+
),
439468
)
440469

441470
result = toml_data.copy()
442471
for override in result["mypy"]["overrides"]:
443472
if "module" not in override:
444473
raise ConfigTOMLValueError(
445-
"toml config file contains a [[tool.mypy.overrides]] "
446-
"section, but no module to override was specified."
474+
_toml_module_error(
475+
config_file,
476+
"toml config file contains a {override} section, but no module to "
477+
"override was specified.",
478+
),
447479
)
448480

449481
if isinstance(override["module"], str):
@@ -452,9 +484,11 @@ def destructure_overrides(toml_data: dict[str, Any]) -> dict[str, Any]:
452484
modules = override["module"]
453485
else:
454486
raise ConfigTOMLValueError(
455-
"toml config file contains a [[tool.mypy.overrides]] "
456-
"section with a module value that is not a string or a list of "
457-
"strings"
487+
_toml_module_error(
488+
config_file,
489+
"toml config file contains a {override} section with a module value "
490+
"that is not a string or a list of strings",
491+
),
458492
)
459493

460494
for module in modules:
@@ -470,9 +504,12 @@ def destructure_overrides(toml_data: dict[str, Any]) -> dict[str, Any]:
470504
and result[old_config_name][new_key] != new_value
471505
):
472506
raise ConfigTOMLValueError(
473-
"toml config file contains "
474-
"[[tool.mypy.overrides]] sections with conflicting "
475-
f"values. Module '{module}' has two different values for '{new_key}'"
507+
_toml_module_error(
508+
config_file,
509+
"toml config file contains {override} sections with "
510+
f"conflicting values. Module '{module}' has "
511+
f"two different values for '{new_key}'",
512+
),
476513
)
477514
result[old_config_name][new_key] = new_value
478515

mypy/defaults.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
CACHE_DIR: Final = ".mypy_cache"
1616

17-
CONFIG_NAMES: Final = ["mypy.ini", ".mypy.ini"]
17+
CONFIG_NAMES: Final = ["mypy.ini", ".mypy.ini", "mypy.toml", ".mypy.toml"]
1818
SHARED_CONFIG_NAMES: Final = ["pyproject.toml", "setup.cfg"]
1919

2020
USER_CONFIG_FILES: list[str] = ["~/.config/mypy/config", "~/.mypy.ini"]

mypy/test/test_config_parser.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ def chdir(target: Path) -> Iterator[None]:
2525
def write_config(path: Path, content: str | None = None) -> None:
2626
if path.suffix == ".toml":
2727
if content is None:
28-
content = "[tool.mypy]\nstrict = true"
28+
if path.name == "pyproject.toml":
29+
content = "[tool.mypy]\nstrict = true"
30+
else:
31+
content = "strict = true"
2932
path.write_text(content)
3033
else:
3134
if content is None:
@@ -82,6 +85,8 @@ def test_precedence(self) -> None:
8285
setup_cfg = tmpdir / "setup.cfg"
8386
mypy_ini = tmpdir / "mypy.ini"
8487
dot_mypy = tmpdir / ".mypy.ini"
88+
mypy_toml = tmpdir / "mypy.toml"
89+
dot_mypy_toml = tmpdir / ".mypy.toml"
8590

8691
child = tmpdir / "child"
8792
child.mkdir()
@@ -91,6 +96,8 @@ def test_precedence(self) -> None:
9196
write_config(setup_cfg)
9297
write_config(mypy_ini)
9398
write_config(dot_mypy)
99+
write_config(mypy_toml)
100+
write_config(dot_mypy_toml)
94101

95102
with chdir(cwd):
96103
result = _find_config_file()
@@ -105,6 +112,16 @@ def test_precedence(self) -> None:
105112
dot_mypy.unlink()
106113
result = _find_config_file()
107114
assert result is not None
115+
assert os.path.basename(result[2]) == "mypy.toml"
116+
117+
mypy_toml.unlink()
118+
result = _find_config_file()
119+
assert result is not None
120+
assert os.path.basename(result[2]) == ".mypy.toml"
121+
122+
dot_mypy_toml.unlink()
123+
result = _find_config_file()
124+
assert result is not None
108125
assert os.path.basename(result[2]) == "pyproject.toml"
109126

110127
pyproject.unlink()

mypy/test/testcmdline.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@
3535
python3_path = sys.executable
3636

3737
# Files containing test case descriptions.
38-
cmdline_files = ["cmdline.test", "cmdline.pyproject.test", "reports.test", "envvars.test"]
38+
cmdline_files = [
39+
"cmdline.test",
40+
"cmdline.pyproject.test",
41+
"cmdline.mypy_toml.test",
42+
"reports.test",
43+
"envvars.test",
44+
]
3945

4046

4147
class PythonCmdlineSuite(DataSuite):

0 commit comments

Comments
 (0)