diff --git a/changelog/13830.misc.rst b/changelog/13830.misc.rst new file mode 100644 index 00000000000..0d81ce2f14f --- /dev/null +++ b/changelog/13830.misc.rst @@ -0,0 +1 @@ +Configuration overrides (``-o``/``--override-ini``) are now processed during startup rather than during :func:`config.getini() `. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 4df15c4db7f..c26455e3d8b 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -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") @@ -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, @@ -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. @@ -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 @@ -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. diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 3f08c8121a9..2dea16dd0d5 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -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, @@ -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) @@ -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 + return rootdir, inipath, inicfg, ignored_config_files def is_fs_root(p: Path) -> bool: diff --git a/testing/test_config.py b/testing/test_config.py index 9efac8739b7..d85e95046c1 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -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(), @@ -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(), @@ -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(), @@ -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(), @@ -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(), @@ -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(), @@ -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(), @@ -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(), @@ -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(), @@ -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(), @@ -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(), @@ -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(), @@ -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(), @@ -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 @@ -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)"""