Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 ``[[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:

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

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

[[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(overrides="tool.mypy.overrides", override="[[tool.mypy.overrides]]")
return message.format(overrides="overrides", override="[[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 ``[[overrides]]``.

E.g. the following TOML file:

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

[[tool.mypy.overrides]]
[[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,
"{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