Skip to content
Merged
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
1 change: 1 addition & 0 deletions changelog/13830.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Configuration overrides (``-o``/``--override-ini``) are now processed during startup rather than during :func:`config.getini() <pytest.Config.getini>`.
32 changes: 5 additions & 27 deletions src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1083,7 +1083,6 @@ def __init__(
self.trace = self.pluginmanager.trace.root.get("config")
self.hook: pluggy.HookRelay = PathAwareHookProxy(self.pluginmanager.hook) # type: ignore[assignment]
self._inicache: dict[str, Any] = {}
self._override_ini: Sequence[str] = ()
self._opt2dest: dict[str, str] = {}
self._cleanup_stack = contextlib.ExitStack()
self.pluginmanager.register(self, "pytestconfig")
Expand Down Expand Up @@ -1251,6 +1250,7 @@ def _initini(self, args: Sequence[str]) -> None:
)
rootpath, inipath, inicfg, ignored_config_files = determine_setup(
inifile=ns.inifilename,
override_ini=ns.override_ini,
args=ns.file_or_dir + unknown_args,
rootdir_cmd_arg=ns.rootdir or None,
invocation_dir=self.invocation_params.dir,
Expand All @@ -1272,7 +1272,6 @@ def _initini(self, args: Sequence[str]) -> None:
type="args",
default=[],
)
self._override_ini = ns.override_ini or ()

def _consider_importhook(self, args: Sequence[str]) -> None:
"""Install the PEP 302 import hook if using assertion rewriting.
Expand Down Expand Up @@ -1641,14 +1640,10 @@ def _getini(self, name: str):
_description, type, default = self._parser._inidict[name]
except KeyError as e:
raise ValueError(f"unknown configuration value: {name!r}") from e
override_value = self._get_override_ini_value(name)
if override_value is None:
try:
value = self.inicfg[name]
except KeyError:
return default
else:
value = override_value
try:
value = self.inicfg[name]
except KeyError:
return default
# Coerce the values based on types.
#
# Note: some coercions are only required if we are reading from .ini files, because
Expand Down Expand Up @@ -1719,23 +1714,6 @@ def _getconftest_pathlist(
values.append(relroot)
return values

def _get_override_ini_value(self, name: str) -> str | None:
value = None
# override_ini is a list of "ini=value" options.
# Always use the last item if multiple values are set for same ini-name,
# e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2.
for ini_config in self._override_ini:
try:
key, user_ini_value = ini_config.split("=", 1)
except ValueError as e:
raise UsageError(
f"-o/--override-ini expects option=value style (got: {ini_config!r})."
) from e
else:
if key == name:
value = user_ini_value
return value

def getoption(self, name: str, default: Any = notset, skip: bool = False):
"""Return command line option value.

Expand Down
33 changes: 32 additions & 1 deletion src/_pytest/config/findpaths.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,12 +181,35 @@ def get_dir_from_path(path: Path) -> Path:
return [get_dir_from_path(path) for path in possible_paths if safe_exists(path)]


def parse_override_ini(override_ini: Sequence[str] | None) -> dict[str, str]:
"""Parse the -o/--override-ini command line arguments and return the overrides.

:raises UsageError:
If one of the values is malformed.
"""
overrides = {}
# override_ini is a list of "ini=value" options.
# Always use the last item if multiple values are set for same ini-name,
# e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2.
for ini_config in override_ini or ():
try:
key, user_ini_value = ini_config.split("=", 1)
except ValueError as e:
raise UsageError(
f"-o/--override-ini expects option=value style (got: {ini_config!r})."
) from e
else:
overrides[key] = user_ini_value
return overrides


CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead."


def determine_setup(
*,
inifile: str | None,
override_ini: Sequence[str] | None,
args: Sequence[str],
rootdir_cmd_arg: str | None,
invocation_dir: Path,
Expand All @@ -196,12 +219,16 @@ def determine_setup(

:param inifile:
The `--inifile` command line argument, if given.
:param override_ini:
The -o/--override-ini command line arguments, if given.
:param args:
The free command line arguments.
:param rootdir_cmd_arg:
The `--rootdir` command line argument, if given.
:param invocation_dir:
The working directory when pytest was invoked.

:raises UsageError:
"""
rootdir = None
dirs = get_dirs_from_args(args)
Expand Down Expand Up @@ -238,8 +265,12 @@ def determine_setup(
raise UsageError(
f"Directory '{rootdir}' not found. Check your '--rootdir' option."
)

ini_overrides = parse_override_ini(override_ini)
inicfg.update(ini_overrides)

assert rootdir is not None
return rootdir, inipath, inicfg or {}, ignored_config_files
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated cleanup, the or isn't needed.

return rootdir, inipath, inicfg, ignored_config_files


def is_fs_root(p: Path) -> bool:
Expand Down
17 changes: 15 additions & 2 deletions testing/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1631,6 +1631,7 @@ def test_with_ini(self, tmp_path: Path, name: str, contents: str) -> None:
for args in ([str(tmp_path)], [str(a)], [str(b)]):
rootpath, parsed_inipath, *_ = determine_setup(
inifile=None,
override_ini=None,
args=args,
rootdir_cmd_arg=None,
invocation_dir=Path.cwd(),
Expand All @@ -1639,6 +1640,7 @@ def test_with_ini(self, tmp_path: Path, name: str, contents: str) -> None:
assert parsed_inipath == inipath
rootpath, parsed_inipath, ini_config, _ = determine_setup(
inifile=None,
override_ini=None,
args=[str(b), str(a)],
rootdir_cmd_arg=None,
invocation_dir=Path.cwd(),
Expand All @@ -1656,6 +1658,7 @@ def test_pytestini_overrides_empty_other(self, tmp_path: Path, name: str) -> Non
(a / name).touch()
rootpath, parsed_inipath, *_ = determine_setup(
inifile=None,
override_ini=None,
args=[str(a)],
rootdir_cmd_arg=None,
invocation_dir=Path.cwd(),
Expand All @@ -1670,6 +1673,7 @@ def test_setuppy_fallback(self, tmp_path: Path) -> None:
(tmp_path / "setup.py").touch()
rootpath, inipath, inicfg, _ = determine_setup(
inifile=None,
override_ini=None,
args=[str(a)],
rootdir_cmd_arg=None,
invocation_dir=Path.cwd(),
Expand All @@ -1682,6 +1686,7 @@ def test_nothing(self, tmp_path: Path, monkeypatch: MonkeyPatch) -> None:
monkeypatch.chdir(tmp_path)
rootpath, inipath, inicfg, _ = determine_setup(
inifile=None,
override_ini=None,
args=[str(tmp_path)],
rootdir_cmd_arg=None,
invocation_dir=Path.cwd(),
Expand Down Expand Up @@ -1709,6 +1714,7 @@ def test_with_specific_inifile(
p.write_text(contents, encoding="utf-8")
rootpath, inipath, ini_config, _ = determine_setup(
inifile=str(p),
override_ini=None,
args=[str(tmp_path)],
rootdir_cmd_arg=None,
invocation_dir=Path.cwd(),
Expand All @@ -1728,6 +1734,7 @@ def test_explicit_config_file_sets_rootdir(
# No config file is explicitly given: rootdir is determined to be cwd.
rootpath, found_inipath, *_ = determine_setup(
inifile=None,
override_ini=None,
args=[str(tests_dir)],
rootdir_cmd_arg=None,
invocation_dir=Path.cwd(),
Expand All @@ -1740,6 +1747,7 @@ def test_explicit_config_file_sets_rootdir(
inipath.touch()
rootpath, found_inipath, *_ = determine_setup(
inifile=str(inipath),
override_ini=None,
args=[str(tests_dir)],
rootdir_cmd_arg=None,
invocation_dir=Path.cwd(),
Expand All @@ -1757,6 +1765,7 @@ def test_with_arg_outside_cwd_without_inifile(
b.mkdir()
rootpath, inifile, *_ = determine_setup(
inifile=None,
override_ini=None,
args=[str(a), str(b)],
rootdir_cmd_arg=None,
invocation_dir=Path.cwd(),
Expand All @@ -1773,6 +1782,7 @@ def test_with_arg_outside_cwd_with_inifile(self, tmp_path: Path) -> None:
inipath.touch()
rootpath, parsed_inipath, *_ = determine_setup(
inifile=None,
override_ini=None,
args=[str(a), str(b)],
rootdir_cmd_arg=None,
invocation_dir=Path.cwd(),
Expand All @@ -1787,6 +1797,7 @@ def test_with_non_dir_arg(
monkeypatch.chdir(tmp_path)
rootpath, inipath, *_ = determine_setup(
inifile=None,
override_ini=None,
args=dirs,
rootdir_cmd_arg=None,
invocation_dir=Path.cwd(),
Expand All @@ -1803,6 +1814,7 @@ def test_with_existing_file_in_subdir(
monkeypatch.chdir(tmp_path)
rootpath, inipath, *_ = determine_setup(
inifile=None,
override_ini=None,
args=["a/exist"],
rootdir_cmd_arg=None,
invocation_dir=Path.cwd(),
Expand All @@ -1822,6 +1834,7 @@ def test_with_config_also_in_parent_directory(

rootpath, inipath, *_ = determine_setup(
inifile=None,
override_ini=None,
args=["tests/"],
rootdir_cmd_arg=None,
invocation_dir=Path.cwd(),
Expand Down Expand Up @@ -1978,7 +1991,7 @@ def test_addopts_before_initini(
monkeypatch.setenv("PYTEST_ADDOPTS", f"-o cache_dir={cache_dir}")
config = _config_for_test
config._preparse([], addopts=True)
assert config._override_ini == [f"cache_dir={cache_dir}"]
assert config.inicfg.get("cache_dir") == cache_dir

def test_addopts_from_env_not_concatenated(
self, monkeypatch: MonkeyPatch, _config_for_test
Expand Down Expand Up @@ -2016,7 +2029,7 @@ def test_override_ini_does_not_contain_paths(
"""Check that -o no longer swallows all options after it (#3103)"""
config = _config_for_test
config._preparse(["-o", "cache_dir=/cache", "/some/test/path"])
assert config._override_ini == ["cache_dir=/cache"]
assert config.inicfg.get("cache_dir") == "/cache"

def test_multiple_override_ini_options(self, pytester: Pytester) -> None:
"""Ensure a file path following a '-o' option does not generate an error (#3103)"""
Expand Down