Skip to content

Commit 450de8a

Browse files
committed
Handle precedence correctly when searching for config file
1 parent 55a8840 commit 450de8a

File tree

3 files changed

+82
-75
lines changed

3 files changed

+82
-75
lines changed

mypy/config_parser.py

Lines changed: 78 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,73 @@ def split_commas(value: str) -> list[str]:
216216
}
217217
)
218218

219+
def _maybe_parse_individual_file(
220+
config_file: str, stderr: TextIO | None = None,
221+
) -> tuple[MutableMapping[str, Any], dict[str, _INI_PARSER_CALLABLE], str] | None:
222+
223+
if not os.path.exists(config_file):
224+
return None
225+
226+
config_parser = configparser.RawConfigParser()
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+
config_parser.read(config_file)
241+
parser = config_parser
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 = _maybe_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))
270+
for cvs_root in (".git", ".hg")
271+
):
272+
break
273+
parent_dir = os.path.dirname(current_dir)
274+
if parent_dir == current_dir:
275+
break
276+
current_dir = parent_dir
277+
278+
for config_file in defaults.USER_CONFIG_FILES:
279+
ret = _maybe_parse_individual_file(config_file, stderr)
280+
if ret is None:
281+
continue
282+
return ret
283+
284+
return None
285+
219286

220287
def parse_config_file(
221288
options: Options,
@@ -234,46 +301,21 @@ def parse_config_file(
234301
stderr = stderr or sys.stderr
235302

236303
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
304+
ret = _maybe_parse_individual_file(filename, stderr)
305+
if ret is None:
306+
return
307+
parser, config_types, file_read = ret
270308
else:
271-
return
309+
ret = _find_config_file(stderr)
310+
if ret is None:
311+
return
312+
parser, config_types, file_read = ret
272313

273-
os.environ["MYPY_CONFIG_FILE_DIR"] = os.path.dirname(os.path.abspath(config_file))
314+
options.config_file = file_read
315+
os.environ["MYPY_CONFIG_FILE_DIR"] = os.path.dirname(os.path.abspath(file_read))
274316

275317
if "mypy" not in parser:
276-
if filename or file_read not in defaults.SHARED_CONFIG_FILES:
318+
if filename or os.path.basename(file_read) not in defaults.SHARED_CONFIG_NAMES:
277319
print(f"{file_read}: No [mypy] section in config file", file=stderr)
278320
else:
279321
section = parser["mypy"]

mypy/defaults.py

Lines changed: 3 additions & 38 deletions
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

Lines changed: 1 addition & 1 deletion
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)