Skip to content

Commit 4a72924

Browse files
Refactor: split up config loading logic to dispatch by filename/suffix
1 parent 76c2090 commit 4a72924

File tree

1 file changed

+84
-45
lines changed

1 file changed

+84
-45
lines changed

src/_pytest/config/findpaths.py

+84-45
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
from . import Config
1919
from iniconfig import IniConfig # NOQA: F401
2020

21+
PARSE_RESULT = Optional[Dict[str, Union[str, List[str]]]]
22+
2123

2224
def _parse_ini_config(path: Path) -> "IniConfig":
2325
"""Parse the given generic '.ini' file using legacy IniConfig parser, returning
@@ -33,52 +35,94 @@ def _parse_ini_config(path: Path) -> "IniConfig":
3335
raise UsageError(str(exc)) from exc
3436

3537

36-
def load_config_dict_from_file(
37-
filepath: Path,
38-
) -> Optional[Dict[str, Union[str, List[str]]]]:
39-
"""Load pytest configuration from the given file path, if supported.
38+
def _parse_pytest_ini(path: Path) -> PARSE_RESULT:
39+
"""Parse the legacy pytest.ini and return the contents of the pytest section
4040
41-
Return None if the file does not contain valid pytest configuration.
41+
if the file exists and lacks a pytest section, consider it empty"""
42+
iniconfig = _parse_ini_config(path)
43+
44+
if "pytest" in iniconfig:
45+
return dict(iniconfig["pytest"].items())
46+
else:
47+
# "pytest.ini" files are always the source of configuration, even if empty.
48+
return {}
49+
50+
51+
def _parse_ini_file(path: Path) -> PARSE_RESULT:
52+
"""Parses .ini files with expected pytest.ini sections
53+
54+
todo: investigate if tool:pytest should be added
4255
"""
56+
iniconfig = _parse_ini_config(path)
4357

44-
# Configuration from ini files are obtained from the [pytest] section, if present.
45-
if filepath.suffix == ".ini":
46-
iniconfig = _parse_ini_config(filepath)
58+
if "pytest" in iniconfig:
59+
return dict(iniconfig["pytest"].items())
60+
return None
4761

48-
if "pytest" in iniconfig:
49-
return dict(iniconfig["pytest"].items())
50-
else:
51-
# "pytest.ini" files are always the source of configuration, even if empty.
52-
if filepath.name == "pytest.ini":
53-
return {}
5462

55-
# '.cfg' files are considered if they contain a "[tool:pytest]" section.
56-
elif filepath.suffix == ".cfg":
57-
iniconfig = _parse_ini_config(filepath)
63+
def _parse_cfg_file(path: Path) -> PARSE_RESULT:
64+
"""Parses .cfg files, specifically used for setup.cfg support
5865
59-
if "tool:pytest" in iniconfig.sections:
60-
return dict(iniconfig["tool:pytest"].items())
61-
elif "pytest" in iniconfig.sections:
62-
# If a setup.cfg contains a "[pytest]" section, we raise a failure to indicate users that
63-
# plain "[pytest]" sections in setup.cfg files is no longer supported (#3086).
64-
fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False)
66+
tool:pytest as section name is required
67+
"""
68+
69+
iniconfig = _parse_ini_config(path)
70+
71+
if "tool:pytest" in iniconfig.sections:
72+
return dict(iniconfig["tool:pytest"].items())
73+
elif "pytest" in iniconfig.sections:
74+
# If a setup.cfg contains a "[pytest]" section, we raise a failure to indicate users that
75+
# plain "[pytest]" sections in setup.cfg files is no longer supported (#3086).
76+
fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False)
77+
else:
78+
return None
6579

66-
# '.toml' files are considered if they contain a [tool.pytest.ini_options] table.
67-
elif filepath.suffix == ".toml":
68-
import toml
6980

70-
config = toml.load(str(filepath))
81+
def _parse_pyproject_ini_options(
82+
path: Path,
83+
) -> PARSE_RESULT:
84+
"""Load backward compatible ini options from pyproject.toml"""
7185

72-
result = config.get("tool", {}).get("pytest", {}).get("ini_options", None)
73-
if result is not None:
74-
# TOML supports richer data types than ini files (strings, arrays, floats, ints, etc),
75-
# however we need to convert all scalar values to str for compatibility with the rest
76-
# of the configuration system, which expects strings only.
77-
def make_scalar(v: object) -> Union[str, List[str]]:
78-
return v if isinstance(v, list) else str(v)
86+
import toml
7987

80-
return {k: make_scalar(v) for k, v in result.items()}
88+
config = toml.load(path)
8189

90+
result = config.get("tool", {}).get("pytest", {}).get("ini_options", None)
91+
if result is not None:
92+
# TOML supports richer data types than ini files (strings, arrays, floats, ints, etc),
93+
# however we need to convert all scalar values to str for compatibility with the rest
94+
# of the configuration system, which expects strings only.
95+
def make_scalar(v: object) -> Union[str, List[str]]:
96+
return v if isinstance(v, list) else str(v)
97+
98+
return {k: make_scalar(v) for k, v in result.items()}
99+
else:
100+
return None
101+
102+
103+
CONFIG_LOADERS = {
104+
"pytest.ini": _parse_pytest_ini,
105+
"pyproject.toml": _parse_pyproject_ini_options,
106+
"tox.ini": _parse_ini_file,
107+
"setup.cfg": _parse_cfg_file,
108+
}
109+
110+
CONFIG_SUFFIXES = {
111+
".ini": _parse_ini_file,
112+
".cfg": _parse_cfg_file,
113+
".toml": _parse_pyproject_ini_options,
114+
}
115+
116+
117+
def load_config_dict_from_file(path: Path) -> PARSE_RESULT:
118+
"""Load pytest configuration from the given file path, if supported.
119+
120+
Return None if the file does not contain valid pytest configuration.
121+
"""
122+
if path.name in CONFIG_LOADERS:
123+
return CONFIG_LOADERS[path.name](path)
124+
if path.suffix in CONFIG_SUFFIXES:
125+
return CONFIG_SUFFIXES[path.suffix](path)
82126
return None
83127

84128

@@ -87,24 +131,19 @@ def locate_config(
87131
) -> Tuple[Optional[Path], Optional[Path], Dict[str, Union[str, List[str]]]]:
88132
"""Search in the list of arguments for a valid ini-file for pytest,
89133
and return a tuple of (rootdir, inifile, cfg-dict)."""
90-
config_names = [
91-
"pytest.ini",
92-
"pyproject.toml",
93-
"tox.ini",
94-
"setup.cfg",
95-
]
134+
96135
args = [x for x in args if not str(x).startswith("-")]
97136
if not args:
98137
args = [Path.cwd()]
99138
for arg in args:
100139
argpath = absolutepath(arg)
101140
for base in (argpath, *argpath.parents):
102-
for config_name in config_names:
141+
for config_name, loader in CONFIG_LOADERS.items():
103142
p = base / config_name
104143
if p.is_file():
105-
ini_config = load_config_dict_from_file(p)
106-
if ini_config is not None:
107-
return base, p, ini_config
144+
config = loader(p)
145+
if config is not None:
146+
return base, p, config
108147
return None, None, {}
109148

110149

0 commit comments

Comments
 (0)