Skip to content

Commit c8d59c1

Browse files
committed
Fix for test_addini_aliases_with_override_of_old
1 parent 5c75785 commit c8d59c1

File tree

4 files changed

+57
-35
lines changed

4 files changed

+57
-35
lines changed

src/_pytest/config/__init__.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1461,7 +1461,8 @@ def pytest_collection(self) -> Generator[None, object, object]:
14611461
def _checkversion(self) -> None:
14621462
import pytest
14631463

1464-
minver = self.inicfg.get("minversion", None)
1464+
minver_ini_value = self.inicfg.get("minversion", None)
1465+
minver = minver_ini_value.value if minver_ini_value is not None else None
14651466
if minver:
14661467
# Imported lazily to improve start-up time.
14671468
from packaging.version import Version
@@ -1645,21 +1646,24 @@ def _getini(self, name: str):
16451646
except KeyError as e:
16461647
raise ValueError(f"unknown configuration value: {name!r}") from e
16471648

1648-
# Try to get value from inicfg, checking canonical name first, then aliases.
1649-
# Canonical name takes precedence over any aliases.
1650-
value = None
1649+
# Collect all possible values (canonical name + aliases) from inicfg.
1650+
# Each candidate is (IniValue, is_canonical).
1651+
candidates = []
16511652
if canonical_name in self.inicfg:
1652-
value = self.inicfg[canonical_name]
1653-
else:
1654-
# Check if any alias for this canonical name exists in inicfg.
1655-
for alias, target in self._parser._ini_aliases.items():
1656-
if target == canonical_name and alias in self.inicfg:
1657-
value = self.inicfg[alias]
1658-
break
1653+
candidates.append((self.inicfg[canonical_name], True))
1654+
for alias, target in self._parser._ini_aliases.items():
1655+
if target == canonical_name and alias in self.inicfg:
1656+
candidates.append((self.inicfg[alias], False))
16591657

1660-
if value is None:
1658+
if not candidates:
16611659
return default
16621660

1661+
# Pick the best candidate based on precedence:
1662+
# 1. CLI override takes precedence over file, then
1663+
# 2. Canonical name takes precedence over alias.
1664+
ini_value = max(candidates, key=lambda x: (x[0].origin == "override", x[1]))[0]
1665+
value = ini_value.value
1666+
16631667
# Coerce the values based on types.
16641668
#
16651669
# Note: some coercions are only required if we are reading from .ini files, because

src/_pytest/config/findpaths.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
from collections.abc import Iterable
44
from collections.abc import Sequence
5+
from dataclasses import dataclass
56
import os
67
from pathlib import Path
78
import sys
9+
from typing import Literal
810
from typing import TypeAlias
911

1012
import iniconfig
@@ -16,9 +18,23 @@
1618
from _pytest.pathlib import safe_exists
1719

1820

19-
# Even though TOML supports richer data types, all values are converted to str/list[str] during
20-
# parsing to maintain compatibility with the rest of the configuration system.
21-
ConfigDict: TypeAlias = dict[str, str | list[str]]
21+
@dataclass(frozen=True)
22+
class IniValue:
23+
"""Represents an ini configuration value with its origin.
24+
25+
This allows tracking whether a value came from a configuration file
26+
or from a CLI override (--override-ini), which is important for
27+
determining precedence when dealing with ini option aliases.
28+
"""
29+
30+
# Even though TOML supports richer data types, all values are converted to
31+
# str/list[str] during parsing to maintain compatibility with the rest of
32+
# the configuration system.
33+
value: str | list[str]
34+
origin: Literal["file", "override"]
35+
36+
37+
ConfigDict: TypeAlias = dict[str, IniValue]
2238

2339

2440
def _parse_ini_config(path: Path) -> iniconfig.IniConfig:
@@ -45,7 +61,7 @@ def load_config_dict_from_file(
4561
iniconfig = _parse_ini_config(filepath)
4662

4763
if "pytest" in iniconfig:
48-
return dict(iniconfig["pytest"].items())
64+
return {k: IniValue(v, "file") for k, v in iniconfig["pytest"].items()}
4965
else:
5066
# "pytest.ini" files are always the source of configuration, even if empty.
5167
if filepath.name == "pytest.ini":
@@ -56,7 +72,7 @@ def load_config_dict_from_file(
5672
iniconfig = _parse_ini_config(filepath)
5773

5874
if "tool:pytest" in iniconfig.sections:
59-
return dict(iniconfig["tool:pytest"].items())
75+
return {k: IniValue(v, "file") for k, v in iniconfig["tool:pytest"].items()}
6076
elif "pytest" in iniconfig.sections:
6177
# If a setup.cfg contains a "[pytest]" section, we raise a failure to indicate users that
6278
# plain "[pytest]" sections in setup.cfg files is no longer supported (#3086).
@@ -83,7 +99,7 @@ def load_config_dict_from_file(
8399
def make_scalar(v: object) -> str | list[str]:
84100
return v if isinstance(v, list) else str(v)
85101

86-
return {k: make_scalar(v) for k, v in result.items()}
102+
return {k: IniValue(make_scalar(v), "file") for k, v in result.items()}
87103

88104
return None
89105

@@ -181,7 +197,7 @@ def get_dir_from_path(path: Path) -> Path:
181197
return [get_dir_from_path(path) for path in possible_paths if safe_exists(path)]
182198

183199

184-
def parse_override_ini(override_ini: Sequence[str] | None) -> dict[str, str]:
200+
def parse_override_ini(override_ini: Sequence[str] | None) -> ConfigDict:
185201
"""Parse the -o/--override-ini command line arguments and return the overrides.
186202
187203
:raises UsageError:
@@ -199,7 +215,7 @@ def parse_override_ini(override_ini: Sequence[str] | None) -> dict[str, str]:
199215
f"-o/--override-ini expects option=value style (got: {ini_config!r})."
200216
) from e
201217
else:
202-
overrides[key] = user_ini_value
218+
overrides[key] = IniValue(user_ini_value, "override")
203219
return overrides
204220

205221

testing/test_config.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from _pytest.config.exceptions import UsageError
2626
from _pytest.config.findpaths import determine_setup
2727
from _pytest.config.findpaths import get_common_ancestor
28+
from _pytest.config.findpaths import IniValue
2829
from _pytest.config.findpaths import locate_config
2930
from _pytest.monkeypatch import MonkeyPatch
3031
from _pytest.pathlib import absolutepath
@@ -57,9 +58,9 @@ def test_getcfg_and_config(
5758
encoding="utf-8",
5859
)
5960
_, _, cfg, _ = locate_config(Path.cwd(), [sub])
60-
assert cfg["name"] == "value"
61+
assert cfg["name"] == IniValue("value", "file")
6162
config = pytester.parseconfigure(str(sub))
62-
assert config.inicfg["name"] == "value"
63+
assert config.inicfg["name"] == IniValue("value", "file")
6364

6465
def test_setupcfg_uses_toolpytest_with_pytest(self, pytester: Pytester) -> None:
6566
p1 = pytester.makepyfile("def test(): pass")
@@ -1313,7 +1314,7 @@ def test_inifilename(self, tmp_path: Path) -> None:
13131314

13141315
# this indicates this is the file used for getting configuration values
13151316
assert config.inipath == inipath
1316-
assert config.inicfg.get("name") == "value"
1317+
assert config.inicfg.get("name") == IniValue("value", "file")
13171318
assert config.inicfg.get("should_not_be_set") is None
13181319

13191320

@@ -1807,7 +1808,7 @@ def test_with_ini(self, tmp_path: Path, name: str, contents: str) -> None:
18071808
)
18081809
assert rootpath == tmp_path
18091810
assert parsed_inipath == inipath
1810-
assert ini_config == {"x": "10"}
1811+
assert ini_config["x"] == IniValue("10", "file")
18111812

18121813
@pytest.mark.parametrize("name", ["setup.cfg", "tox.ini"])
18131814
def test_pytestini_overrides_empty_other(self, tmp_path: Path, name: str) -> None:
@@ -1881,7 +1882,7 @@ def test_with_specific_inifile(
18811882
)
18821883
assert rootpath == tmp_path
18831884
assert inipath == p
1884-
assert ini_config == {"x": "10"}
1885+
assert ini_config["x"] == IniValue("10", "file")
18851886

18861887
def test_explicit_config_file_sets_rootdir(
18871888
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
@@ -2151,7 +2152,7 @@ def test_addopts_before_initini(
21512152
monkeypatch.setenv("PYTEST_ADDOPTS", f"-o cache_dir={cache_dir}")
21522153
config = _config_for_test
21532154
config._preparse([], addopts=True)
2154-
assert config.inicfg.get("cache_dir") == cache_dir
2155+
assert config.inicfg.get("cache_dir") == IniValue(cache_dir, "override")
21552156

21562157
def test_addopts_from_env_not_concatenated(
21572158
self, monkeypatch: MonkeyPatch, _config_for_test
@@ -2189,7 +2190,7 @@ def test_override_ini_does_not_contain_paths(
21892190
"""Check that -o no longer swallows all options after it (#3103)"""
21902191
config = _config_for_test
21912192
config._preparse(["-o", "cache_dir=/cache", "/some/test/path"])
2192-
assert config.inicfg.get("cache_dir") == "/cache"
2193+
assert config.inicfg.get("cache_dir") == IniValue("/cache", "override")
21932194

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

testing/test_findpaths.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from _pytest.config import UsageError
99
from _pytest.config.findpaths import get_common_ancestor
1010
from _pytest.config.findpaths import get_dirs_from_args
11+
from _pytest.config.findpaths import IniValue
1112
from _pytest.config.findpaths import is_fs_root
1213
from _pytest.config.findpaths import load_config_dict_from_file
1314
import pytest
@@ -24,13 +25,13 @@ def test_pytest_ini(self, tmp_path: Path) -> None:
2425
"""[pytest] section in pytest.ini files is read correctly"""
2526
fn = tmp_path / "pytest.ini"
2627
fn.write_text("[pytest]\nx=1", encoding="utf-8")
27-
assert load_config_dict_from_file(fn) == {"x": "1"}
28+
assert load_config_dict_from_file(fn) == {"x": IniValue("1", "file")}
2829

2930
def test_custom_ini(self, tmp_path: Path) -> None:
3031
"""[pytest] section in any .ini file is read correctly"""
3132
fn = tmp_path / "custom.ini"
3233
fn.write_text("[pytest]\nx=1", encoding="utf-8")
33-
assert load_config_dict_from_file(fn) == {"x": "1"}
34+
assert load_config_dict_from_file(fn) == {"x": IniValue("1", "file")}
3435

3536
def test_custom_ini_without_section(self, tmp_path: Path) -> None:
3637
"""Custom .ini files without [pytest] section are not considered for configuration"""
@@ -48,7 +49,7 @@ def test_valid_cfg_file(self, tmp_path: Path) -> None:
4849
"""Custom .cfg files with [tool:pytest] section are read correctly"""
4950
fn = tmp_path / "custom.cfg"
5051
fn.write_text("[tool:pytest]\nx=1", encoding="utf-8")
51-
assert load_config_dict_from_file(fn) == {"x": "1"}
52+
assert load_config_dict_from_file(fn) == {"x": IniValue("1", "file")}
5253

5354
def test_unsupported_pytest_section_in_cfg_file(self, tmp_path: Path) -> None:
5455
""".cfg files with [pytest] section are no longer supported and should fail to alert users"""
@@ -96,11 +97,11 @@ def test_valid_toml_file(self, tmp_path: Path) -> None:
9697
encoding="utf-8",
9798
)
9899
assert load_config_dict_from_file(fn) == {
99-
"x": "1",
100-
"y": "20.0",
101-
"values": ["tests", "integration"],
102-
"name": "foo",
103-
"heterogeneous_array": [1, "str"],
100+
"x": IniValue("1", "file"),
101+
"y": IniValue("20.0", "file"),
102+
"values": IniValue(["tests", "integration"], "file"),
103+
"name": IniValue("foo", "file"),
104+
"heterogeneous_array": IniValue([1, "str"], "file"), # type: ignore[list-item]
104105
}
105106

106107

0 commit comments

Comments
 (0)