Skip to content

Commit 33f60e4

Browse files
hauntsaninjawesleywright
authored andcommitted
Walk up for all config files and handle precedence (#18482)
Follow up to #16965 Fixes #16070 Handles other mypy configuration files and handles precedence between them. Also fixes few small things, like use in git worktrees
1 parent 68cffa7 commit 33f60e4

File tree

7 files changed

+254
-135
lines changed

7 files changed

+254
-135
lines changed

CHANGELOG.md

+22-10
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,16 @@ garbage collector.
99

1010
Contributed by Jukka Lehtosalo (PR [18306](https://github.com/python/mypy/pull/18306)).
1111

12-
### Drop Support for Python 3.8
13-
14-
Mypy no longer supports running with Python 3.8, which has reached end-of-life.
15-
When running mypy with Python 3.9+, it is still possible to type check code
16-
that needs to support Python 3.8 with the `--python-version 3.8` argument.
17-
Support for this will be dropped in the first half of 2025!
18-
19-
Contributed by Marc Mueller (PR [17492](https://github.com/python/mypy/pull/17492)).
20-
2112
### Mypyc accelerated mypy wheels for aarch64
2213

2314
Mypy can compile itself to C extension modules using mypyc. This makes mypy 3-5x faster
2415
than if mypy is interpreted with pure Python. We now build and upload mypyc accelerated
2516
mypy wheels for `manylinux_aarch64` to PyPI, making it easy for users on such platforms
2617
to realise this speedup.
2718

28-
Contributed by Christian Bundy (PR [mypy_mypyc-wheels#76](https://github.com/mypyc/mypy_mypyc-wheels/pull/76))
19+
Contributed by Christian Bundy and Marc Mueller
20+
(PR [mypy_mypyc-wheels#76](https://github.com/mypyc/mypy_mypyc-wheels/pull/76),
21+
PR [mypy_mypyc-wheels#89](https://github.com/mypyc/mypy_mypyc-wheels/pull/89)).
2922

3023
### `--strict-bytes`
3124

@@ -48,6 +41,16 @@ Contributed by Christoph Tyralla (PR [18180](https://github.com/python/mypy/pull
4841
(Speaking of partial types, another reminder that mypy plans on enabling `--local-partial-types`
4942
by default in **mypy 2.0**).
5043

44+
### Better discovery of configuration files
45+
46+
Mypy will now walk up the filesystem (up until a repository or file system root) to discover
47+
configuration files. See the
48+
[mypy configuration file documentation](https://mypy.readthedocs.io/en/stable/config_file.html)
49+
for more details.
50+
51+
Contributed by Mikhail Shiryaev and Shantanu Jain
52+
(PR [16965](https://github.com/python/mypy/pull/16965), PR [18482](https://github.com/python/mypy/pull/18482)
53+
5154
### Better line numbers for decorators and slice expressions
5255

5356
Mypy now uses more correct line numbers for decorators and slice expressions. In some cases, this
@@ -56,6 +59,15 @@ may necessitate changing the location of a `# type: ignore` comment.
5659
Contributed by Shantanu Jain (PR [18392](https://github.com/python/mypy/pull/18392),
5760
PR [18397](https://github.com/python/mypy/pull/18397)).
5861

62+
### Drop Support for Python 3.8
63+
64+
Mypy no longer supports running with Python 3.8, which has reached end-of-life.
65+
When running mypy with Python 3.9+, it is still possible to type check code
66+
that needs to support Python 3.8 with the `--python-version 3.8` argument.
67+
Support for this will be dropped in the first half of 2025!
68+
69+
Contributed by Marc Mueller (PR [17492](https://github.com/python/mypy/pull/17492)).
70+
5971
## Mypy 1.14
6072

6173
We’ve just uploaded mypy 1.14 to the Python Package Index ([PyPI](https://pypi.org/project/mypy/)).

docs/source/config_file.rst

+21-13
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,30 @@ Mypy is very configurable. This is most useful when introducing typing to
77
an existing codebase. See :ref:`existing-code` for concrete advice for
88
that situation.
99

10-
Mypy supports reading configuration settings from a file with the following precedence order:
10+
Mypy supports reading configuration settings from a file. By default, mypy will
11+
discover configuration files by walking up the file system (up until the root of
12+
a repository or the root of the filesystem). In each directory, it will look for
13+
the following configuration files (in this order):
1114

12-
1. ``./mypy.ini``
13-
2. ``./.mypy.ini``
14-
3. ``./pyproject.toml``
15-
4. ``./setup.cfg``
16-
5. ``$XDG_CONFIG_HOME/mypy/config``
17-
6. ``~/.config/mypy/config``
18-
7. ``~/.mypy.ini``
15+
1. ``mypy.ini``
16+
2. ``.mypy.ini``
17+
3. ``pyproject.toml`` (containing a ``[tool.mypy]`` section)
18+
4. ``setup.cfg`` (containing a ``[mypy]`` section)
19+
20+
If no configuration file is found by this method, mypy will then look for
21+
configuration files in the following locations (in this order):
22+
23+
1. ``$XDG_CONFIG_HOME/mypy/config``
24+
2. ``~/.config/mypy/config``
25+
3. ``~/.mypy.ini``
26+
27+
The :option:`--config-file <mypy --config-file>` command-line flag has the
28+
highest precedence and must point towards a valid configuration file;
29+
otherwise mypy will report an error and exit. Without the command line option,
30+
mypy will look for configuration files in the precedence order above.
1931

2032
It is important to understand that there is no merging of configuration
21-
files, as it would lead to ambiguity. The :option:`--config-file <mypy --config-file>`
22-
command-line flag has the highest precedence and
23-
must be correct; otherwise mypy will report an error and exit. Without the
24-
command line option, mypy will look for configuration files in the
25-
precedence order above.
33+
files, as it would lead to ambiguity.
2634

2735
Most flags correspond closely to :ref:`command-line flags
2836
<command-line>` but there are some differences in flag names and some

mypy/config_parser.py

+77-38
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
else:
1616
import tomli as tomllib
1717

18-
from collections.abc import Iterable, Mapping, MutableMapping, Sequence
18+
from collections.abc import Mapping, MutableMapping, Sequence
1919
from typing import Any, Callable, Final, TextIO, Union
2020
from typing_extensions import TypeAlias as _TypeAlias
2121

@@ -217,6 +217,72 @@ def split_commas(value: str) -> list[str]:
217217
)
218218

219219

220+
def _parse_individual_file(
221+
config_file: str, stderr: TextIO | None = None
222+
) -> tuple[MutableMapping[str, Any], dict[str, _INI_PARSER_CALLABLE], str] | None:
223+
224+
if not os.path.exists(config_file):
225+
return None
226+
227+
parser: MutableMapping[str, Any]
228+
try:
229+
if is_toml(config_file):
230+
with open(config_file, "rb") as f:
231+
toml_data = tomllib.load(f)
232+
# Filter down to just mypy relevant toml keys
233+
toml_data = toml_data.get("tool", {})
234+
if "mypy" not in toml_data:
235+
return None
236+
toml_data = {"mypy": toml_data["mypy"]}
237+
parser = destructure_overrides(toml_data)
238+
config_types = toml_config_types
239+
else:
240+
parser = configparser.RawConfigParser()
241+
parser.read(config_file)
242+
config_types = ini_config_types
243+
244+
except (tomllib.TOMLDecodeError, configparser.Error, ConfigTOMLValueError) as err:
245+
print(f"{config_file}: {err}", file=stderr)
246+
return None
247+
248+
if os.path.basename(config_file) in defaults.SHARED_CONFIG_NAMES and "mypy" not in parser:
249+
return None
250+
251+
return parser, config_types, config_file
252+
253+
254+
def _find_config_file(
255+
stderr: TextIO | None = None,
256+
) -> tuple[MutableMapping[str, Any], dict[str, _INI_PARSER_CALLABLE], str] | None:
257+
258+
current_dir = os.path.abspath(os.getcwd())
259+
260+
while True:
261+
for name in defaults.CONFIG_NAMES + defaults.SHARED_CONFIG_NAMES:
262+
config_file = os.path.relpath(os.path.join(current_dir, name))
263+
ret = _parse_individual_file(config_file, stderr)
264+
if ret is None:
265+
continue
266+
return ret
267+
268+
if any(
269+
os.path.exists(os.path.join(current_dir, cvs_root)) for cvs_root in (".git", ".hg")
270+
):
271+
break
272+
parent_dir = os.path.dirname(current_dir)
273+
if parent_dir == current_dir:
274+
break
275+
current_dir = parent_dir
276+
277+
for config_file in defaults.USER_CONFIG_FILES:
278+
ret = _parse_individual_file(config_file, stderr)
279+
if ret is None:
280+
continue
281+
return ret
282+
283+
return None
284+
285+
220286
def parse_config_file(
221287
options: Options,
222288
set_strict_flags: Callable[[], None],
@@ -233,47 +299,20 @@ def parse_config_file(
233299
stdout = stdout or sys.stdout
234300
stderr = stderr or sys.stderr
235301

236-
if filename is not None:
237-
config_files: tuple[str, ...] = (filename,)
238-
else:
239-
config_files_iter: Iterable[str] = map(os.path.expanduser, defaults.CONFIG_FILES)
240-
config_files = tuple(config_files_iter)
241-
242-
config_parser = configparser.RawConfigParser()
243-
244-
for config_file in config_files:
245-
if not os.path.exists(config_file):
246-
continue
247-
try:
248-
if is_toml(config_file):
249-
with open(config_file, "rb") as f:
250-
toml_data = tomllib.load(f)
251-
# Filter down to just mypy relevant toml keys
252-
toml_data = toml_data.get("tool", {})
253-
if "mypy" not in toml_data:
254-
continue
255-
toml_data = {"mypy": toml_data["mypy"]}
256-
parser: MutableMapping[str, Any] = destructure_overrides(toml_data)
257-
config_types = toml_config_types
258-
else:
259-
config_parser.read(config_file)
260-
parser = config_parser
261-
config_types = ini_config_types
262-
except (tomllib.TOMLDecodeError, configparser.Error, ConfigTOMLValueError) as err:
263-
print(f"{config_file}: {err}", file=stderr)
264-
else:
265-
if config_file in defaults.SHARED_CONFIG_FILES and "mypy" not in parser:
266-
continue
267-
file_read = config_file
268-
options.config_file = file_read
269-
break
270-
else:
302+
ret = (
303+
_parse_individual_file(filename, stderr)
304+
if filename is not None
305+
else _find_config_file(stderr)
306+
)
307+
if ret is None:
271308
return
309+
parser, config_types, file_read = ret
272310

273-
os.environ["MYPY_CONFIG_FILE_DIR"] = os.path.dirname(os.path.abspath(config_file))
311+
options.config_file = file_read
312+
os.environ["MYPY_CONFIG_FILE_DIR"] = os.path.dirname(os.path.abspath(file_read))
274313

275314
if "mypy" not in parser:
276-
if filename or file_read not in defaults.SHARED_CONFIG_FILES:
315+
if filename or os.path.basename(file_read) not in defaults.SHARED_CONFIG_NAMES:
277316
print(f"{file_read}: No [mypy] section in config file", file=stderr)
278317
else:
279318
section = parser["mypy"]

mypy/defaults.py

+3-38
Original file line numberDiff line numberDiff line change
@@ -12,50 +12,15 @@
1212
# mypy, at least version PYTHON3_VERSION is needed.
1313
PYTHON3_VERSION_MIN: Final = (3, 8) # Keep in sync with typeshed's python support
1414

15+
CACHE_DIR: Final = ".mypy_cache"
1516

16-
def find_pyproject() -> str:
17-
"""Search for file pyproject.toml in the parent directories recursively.
18-
19-
It resolves symlinks, so if there is any symlink up in the tree, it does not respect them
20-
21-
If the file is not found until the root of FS or repository, PYPROJECT_FILE is used
22-
"""
23-
24-
def is_root(current_dir: str) -> bool:
25-
parent = os.path.join(current_dir, os.path.pardir)
26-
return os.path.samefile(current_dir, parent) or any(
27-
os.path.isdir(os.path.join(current_dir, cvs_root)) for cvs_root in (".git", ".hg")
28-
)
29-
30-
# Preserve the original behavior, returning PYPROJECT_FILE if exists
31-
if os.path.isfile(PYPROJECT_FILE) or is_root(os.path.curdir):
32-
return PYPROJECT_FILE
33-
34-
# And iterate over the tree
35-
current_dir = os.path.pardir
36-
while not is_root(current_dir):
37-
config_file = os.path.join(current_dir, PYPROJECT_FILE)
38-
if os.path.isfile(config_file):
39-
return config_file
40-
parent = os.path.join(current_dir, os.path.pardir)
41-
current_dir = parent
42-
43-
return PYPROJECT_FILE
44-
17+
CONFIG_NAMES: Final = ["mypy.ini", ".mypy.ini"]
18+
SHARED_CONFIG_NAMES: Final = ["pyproject.toml", "setup.cfg"]
4519

46-
CACHE_DIR: Final = ".mypy_cache"
47-
CONFIG_FILE: Final = ["mypy.ini", ".mypy.ini"]
48-
PYPROJECT_FILE: Final = "pyproject.toml"
49-
PYPROJECT_CONFIG_FILES: Final = [find_pyproject()]
50-
SHARED_CONFIG_FILES: Final = ["setup.cfg"]
5120
USER_CONFIG_FILES: Final = ["~/.config/mypy/config", "~/.mypy.ini"]
5221
if os.environ.get("XDG_CONFIG_HOME"):
5322
USER_CONFIG_FILES.insert(0, os.path.join(os.environ["XDG_CONFIG_HOME"], "mypy/config"))
5423

55-
CONFIG_FILES: Final = (
56-
CONFIG_FILE + PYPROJECT_CONFIG_FILES + SHARED_CONFIG_FILES + USER_CONFIG_FILES
57-
)
58-
5924
# This must include all reporters defined in mypy.report. This is defined here
6025
# to make reporter names available without importing mypy.report -- this speeds
6126
# up startup.

mypy/main.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,7 @@ def add_invertible_flag(
564564
"--config-file",
565565
help=(
566566
f"Configuration file, must have a [mypy] section "
567-
f"(defaults to {', '.join(defaults.CONFIG_FILES)})"
567+
f"(defaults to {', '.join(defaults.CONFIG_NAMES + defaults.SHARED_CONFIG_NAMES)})"
568568
),
569569
)
570570
add_invertible_flag(

0 commit comments

Comments
 (0)