Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions docs/source/command_line.rst
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,10 @@ Config file

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

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

Specifying :option:`--config-file= <--config-file>` (with no filename) will ignore *all*
config files.
Expand Down
56 changes: 52 additions & 4 deletions docs/source/config_file.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ the following configuration files (in this order):

1. ``mypy.ini``
2. ``.mypy.ini``
3. ``pyproject.toml`` (containing a ``[tool.mypy]`` section)
4. ``setup.cfg`` (containing a ``[mypy]`` section)
3. ``mypy.toml``
4. ``.mypy.toml``
5. ``pyproject.toml`` (containing a ``[tool.mypy]`` section)
6. ``setup.cfg`` (containing a ``[mypy]`` section)

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

- A section named ``[mypy]`` must be present. This specifies
the global flags.
Mypy also supports TOML configuration in two forms:

* ``pyproject.toml`` with options under ``[tool.mypy]`` and per-module
overrides under ``[[tool.mypy.overrides]]``
* ``mypy.toml`` or ``.mypy.toml`` with options at the top level and
per-module overrides under ``[[mypy.overrides]]``

- In INI-based config files, a section named ``[mypy]`` must be present.
This specifies the global flags.

- Additional sections named ``[mypy-PATTERN1,PATTERN2,...]`` may be
present, where ``PATTERN1``, ``PATTERN2``, etc., are comma-separated
Expand Down Expand Up @@ -1280,6 +1289,45 @@ of your repo (or append it to the end of an existing ``pyproject.toml`` file) an
]
ignore_missing_imports = true

Using a mypy.toml file
**********************

``mypy.toml`` and ``.mypy.toml`` are also supported. They use the same
TOML value rules as ``pyproject.toml``, but the mypy options live at the
top level instead of under ``[tool.mypy]``.

Example ``mypy.toml``
*********************

.. code-block:: toml

# mypy global options:

python_version = "3.9"
warn_return_any = true
warn_unused_configs = true
exclude = [
'^file1\.py$', # TOML literal string (single-quotes, no escaping necessary)
"^file2\\.py$", # TOML basic string (double-quotes, backslash and other characters need escaping)
]

# mypy per-module options:

[[mypy.overrides]]
module = "mycode.foo.*"
disallow_untyped_defs = true

[[mypy.overrides]]
module = "mycode.bar"
warn_return_any = false

[[mypy.overrides]]
module = [
"somelibrary",
"some_other_library"
]
ignore_missing_imports = true

.. _lxml: https://pypi.org/project/lxml/
.. _SQLite: https://www.sqlite.org/
.. _PEP 518: https://www.python.org/dev/peps/pep-0518/
Expand Down
76 changes: 55 additions & 21 deletions mypy/config_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,12 +245,10 @@ def _parse_individual_file(
if is_toml(config_file):
with open(config_file, "rb") as f:
toml_data = 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 = get_mypy_toml_data(config_file, toml_data)
if toml_data is None:
return None
toml_data = {"mypy": toml_data["mypy"]}
parser = destructure_overrides(toml_data)
parser = destructure_overrides(toml_data, config_file)
config_types = toml_config_types
else:
parser = configparser.RawConfigParser()
Expand Down Expand Up @@ -397,20 +395,45 @@ def is_toml(filename: str) -> bool:
return filename.lower().endswith(".toml")


def destructure_overrides(toml_data: dict[str, Any]) -> dict[str, Any]:
"""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.
def is_pyproject(filename: str) -> bool:
return os.path.basename(filename) == "pyproject.toml"

E.g. the following pyproject.toml file:

[[tool.mypy.overrides]]
def get_mypy_toml_data(config_file: str, toml_data: dict[str, Any]) -> dict[str, Any] | None:
if is_pyproject(config_file):
toml_data = toml_data.get("tool", {})
if "mypy" not in toml_data:
return None
return {"mypy": toml_data["mypy"]}

if "mypy" in toml_data:
return toml_data

return {"mypy": toml_data}


def _toml_module_error(config_file: str, message: str) -> str:
if is_pyproject(config_file):
return message.format(prefix="tool.mypy", override="[[tool.mypy.overrides]]")
return message.format(prefix="mypy", override="[[mypy.overrides]]")


def destructure_overrides(toml_data: dict[str, Any], config_file: str) -> dict[str, Any]:
"""Convert TOML overrides sections into the flatter ini-style structure.

``pyproject.toml`` uses ``[[tool.mypy.overrides]]``.
``mypy.toml`` and ``.mypy.toml`` use ``[[mypy.overrides]]``.

E.g. the following TOML file:

[[mypy.overrides]]
module = [
"a.b",
"b.*"
]
disallow_untyped_defs = true

[[tool.mypy.overrides]]
[[mypy.overrides]]
module = 'c'
disallow_untyped_defs = false

Expand All @@ -434,16 +457,22 @@ def destructure_overrides(toml_data: dict[str, Any]) -> dict[str, Any]:

if not isinstance(toml_data["mypy"]["overrides"], list):
raise ConfigTOMLValueError(
"tool.mypy.overrides sections must be an array. Please make "
"sure you are using double brackets like so: [[tool.mypy.overrides]]"
_toml_module_error(
config_file,
"{prefix}.overrides sections must be an array. Please make "
"sure you are using double brackets like so: {override}",
)
)

result = toml_data.copy()
for override in result["mypy"]["overrides"]:
if "module" not in override:
raise ConfigTOMLValueError(
"toml config file contains a [[tool.mypy.overrides]] "
"section, but no module to override was specified."
_toml_module_error(
config_file,
"toml config file contains a {override} section, but no module to "
"override was specified.",
)
)

if isinstance(override["module"], str):
Expand All @@ -452,9 +481,11 @@ def destructure_overrides(toml_data: dict[str, Any]) -> dict[str, Any]:
modules = override["module"]
else:
raise ConfigTOMLValueError(
"toml config file contains a [[tool.mypy.overrides]] "
"section with a module value that is not a string or a list of "
"strings"
_toml_module_error(
config_file,
"toml config file contains a {override} section with a module value "
"that is not a string or a list of strings",
)
)

for module in modules:
Expand All @@ -470,9 +501,12 @@ def destructure_overrides(toml_data: dict[str, Any]) -> dict[str, Any]:
and result[old_config_name][new_key] != new_value
):
raise ConfigTOMLValueError(
"toml config file contains "
"[[tool.mypy.overrides]] sections with conflicting "
f"values. Module '{module}' has two different values for '{new_key}'"
_toml_module_error(
config_file,
"toml config file contains {override} sections with "
f"conflicting values. Module '{module}' has "
f"two different values for '{new_key}'",
)
)
result[old_config_name][new_key] = new_value

Expand Down
2 changes: 1 addition & 1 deletion mypy/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

CACHE_DIR: Final = ".mypy_cache"

CONFIG_NAMES: Final = ["mypy.ini", ".mypy.ini"]
CONFIG_NAMES: Final = ["mypy.ini", ".mypy.ini", "mypy.toml", ".mypy.toml"]
SHARED_CONFIG_NAMES: Final = ["pyproject.toml", "setup.cfg"]

USER_CONFIG_FILES: list[str] = ["~/.config/mypy/config", "~/.mypy.ini"]
Expand Down
19 changes: 18 additions & 1 deletion mypy/test/test_config_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ def chdir(target: Path) -> Iterator[None]:
def write_config(path: Path, content: str | None = None) -> None:
if path.suffix == ".toml":
if content is None:
content = "[tool.mypy]\nstrict = true"
if path.name == "pyproject.toml":
content = "[tool.mypy]\nstrict = true"
else:
content = "strict = true"
path.write_text(content)
else:
if content is None:
Expand Down Expand Up @@ -82,6 +85,8 @@ def test_precedence(self) -> None:
setup_cfg = tmpdir / "setup.cfg"
mypy_ini = tmpdir / "mypy.ini"
dot_mypy = tmpdir / ".mypy.ini"
mypy_toml = tmpdir / "mypy.toml"
dot_mypy_toml = tmpdir / ".mypy.toml"

child = tmpdir / "child"
child.mkdir()
Expand All @@ -91,6 +96,8 @@ def test_precedence(self) -> None:
write_config(setup_cfg)
write_config(mypy_ini)
write_config(dot_mypy)
write_config(mypy_toml)
write_config(dot_mypy_toml)

with chdir(cwd):
result = _find_config_file()
Expand All @@ -105,6 +112,16 @@ def test_precedence(self) -> None:
dot_mypy.unlink()
result = _find_config_file()
assert result is not None
assert os.path.basename(result[2]) == "mypy.toml"

mypy_toml.unlink()
result = _find_config_file()
assert result is not None
assert os.path.basename(result[2]) == ".mypy.toml"

dot_mypy_toml.unlink()
result = _find_config_file()
assert result is not None
assert os.path.basename(result[2]) == "pyproject.toml"

pyproject.unlink()
Expand Down
8 changes: 7 additions & 1 deletion mypy/test/testcmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@
python3_path = sys.executable

# Files containing test case descriptions.
cmdline_files = ["cmdline.test", "cmdline.pyproject.test", "reports.test", "envvars.test"]
cmdline_files = [
"cmdline.test",
"cmdline.pyproject.test",
"cmdline.mypy_toml.test",
"reports.test",
"envvars.test",
]


class PythonCmdlineSuite(DataSuite):
Expand Down
Loading
Loading