Skip to content

Commit 1738920

Browse files
committed
config: restore config.inicfg
Fix #13955.
1 parent 9668d3b commit 1738920

File tree

3 files changed

+89
-12
lines changed

3 files changed

+89
-12
lines changed

changelog/13946.bugfix.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
The private ``config.inicfg`` attribute was changed in a breaking manner in pytest 9.0.0.
2+
Due to its usage in the ecosystem, it is now restored to working order using a compatibility shim.
3+
It will be deprecated in pytest 9.1 and removed in pytest 10.

src/_pytest/config/__init__.py

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from collections.abc import Iterable
1212
from collections.abc import Iterator
1313
from collections.abc import Mapping
14+
from collections.abc import MutableMapping
1415
from collections.abc import Sequence
1516
import contextlib
1617
import copy
@@ -48,6 +49,8 @@
4849
from .compat import PathAwareHookProxy
4950
from .exceptions import PrintHelp as PrintHelp
5051
from .exceptions import UsageError as UsageError
52+
from .findpaths import ConfigDict
53+
from .findpaths import ConfigValue
5154
from .findpaths import determine_setup
5255
from _pytest import __version__
5356
import _pytest._code
@@ -979,6 +982,30 @@ def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]:
979982
yield from _iter_rewritable_modules(new_package_files)
980983

981984

985+
class _DeprecatedInicfgProxy(MutableMapping[str, Any]):
986+
"""Compatibility proxy for the deprecated Config.inicfg."""
987+
988+
__slots__ = ("_config",)
989+
990+
def __init__(self, config: Config) -> None:
991+
self._config = config
992+
993+
def __getitem__(self, key: str) -> Any:
994+
return self._config._inicfg[key].value
995+
996+
def __setitem__(self, key: str, value: Any) -> None:
997+
self._config._inicfg[key] = ConfigValue(value, origin="override", mode="toml")
998+
999+
def __delitem__(self, key: str) -> None:
1000+
del self._config._inicfg[key]
1001+
1002+
def __iter__(self) -> Iterator[str]:
1003+
return iter(self._config._inicfg)
1004+
1005+
def __len__(self) -> int:
1006+
return len(self._config._inicfg)
1007+
1008+
9821009
@final
9831010
class Config:
9841011
"""Access to configuration values, pluginmanager and plugin hooks.
@@ -1089,6 +1116,7 @@ def __init__(
10891116
self.trace = self.pluginmanager.trace.root.get("config")
10901117
self.hook: pluggy.HookRelay = PathAwareHookProxy(self.pluginmanager.hook) # type: ignore[assignment]
10911118
self._inicache: dict[str, Any] = {}
1119+
self._inicfg: ConfigDict = {}
10921120
self._cleanup_stack = contextlib.ExitStack()
10931121
self.pluginmanager.register(self, "pytestconfig")
10941122
self._configured = False
@@ -1098,6 +1126,10 @@ def __init__(
10981126
self.args_source = Config.ArgsSource.ARGS
10991127
self.args: list[str] = []
11001128

1129+
@property
1130+
def inicfg(self) -> _DeprecatedInicfgProxy:
1131+
return _DeprecatedInicfgProxy(self)
1132+
11011133
@property
11021134
def rootpath(self) -> pathlib.Path:
11031135
"""The path to the :ref:`rootdir <rootdir>`.
@@ -1428,7 +1460,7 @@ def _warn_or_fail_if_strict(self, message: str) -> None:
14281460

14291461
def _get_unknown_ini_keys(self) -> set[str]:
14301462
known_keys = self._parser._inidict.keys() | self._parser._ini_aliases.keys()
1431-
return self.inicfg.keys() - known_keys
1463+
return self._inicfg.keys() - known_keys
14321464

14331465
def parse(self, args: list[str], addopts: bool = True) -> None:
14341466
# Parse given cmdline arguments into this config object.
@@ -1459,7 +1491,7 @@ def parse(self, args: list[str], addopts: bool = True) -> None:
14591491
self._rootpath = rootpath
14601492
self._inipath = inipath
14611493
self._ignored_config_files = ignored_config_files
1462-
self.inicfg = inicfg
1494+
self._inicfg = inicfg
14631495
self._parser.extra_info["rootdir"] = str(self.rootpath)
14641496
self._parser.extra_info["inifile"] = str(self.inipath)
14651497

@@ -1636,14 +1668,14 @@ def _getini(self, name: str):
16361668
except KeyError as e:
16371669
raise ValueError(f"unknown configuration value: {name!r}") from e
16381670

1639-
# Collect all possible values (canonical name + aliases) from inicfg.
1671+
# Collect all possible values (canonical name + aliases) from _inicfg.
16401672
# Each candidate is (ConfigValue, is_canonical).
16411673
candidates = []
1642-
if canonical_name in self.inicfg:
1643-
candidates.append((self.inicfg[canonical_name], True))
1674+
if canonical_name in self._inicfg:
1675+
candidates.append((self._inicfg[canonical_name], True))
16441676
for alias, target in self._parser._ini_aliases.items():
1645-
if target == canonical_name and alias in self.inicfg:
1646-
candidates.append((self.inicfg[alias], False))
1677+
if target == canonical_name and alias in self._inicfg:
1678+
candidates.append((self._inicfg[alias], False))
16471679

16481680
if not candidates:
16491681
return default

testing/test_config.py

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def test_getcfg_and_config(
6060
_, _, cfg, _ = locate_config(Path.cwd(), [sub])
6161
assert cfg["name"] == ConfigValue("value", origin="file", mode="ini")
6262
config = pytester.parseconfigure(str(sub))
63-
assert config.inicfg["name"] == ConfigValue("value", origin="file", mode="ini")
63+
assert config._inicfg["name"] == ConfigValue("value", origin="file", mode="ini")
6464

6565
def test_setupcfg_uses_toolpytest_with_pytest(self, pytester: Pytester) -> None:
6666
p1 = pytester.makepyfile("def test(): pass")
@@ -1433,10 +1433,10 @@ def test_inifilename(self, tmp_path: Path) -> None:
14331433

14341434
# this indicates this is the file used for getting configuration values
14351435
assert config.inipath == inipath
1436-
assert config.inicfg.get("name") == ConfigValue(
1436+
assert config._inicfg.get("name") == ConfigValue(
14371437
"value", origin="file", mode="ini"
14381438
)
1439-
assert config.inicfg.get("should_not_be_set") is None
1439+
assert config._inicfg.get("should_not_be_set") is None
14401440

14411441

14421442
def test_options_on_small_file_do_not_blow_up(pytester: Pytester) -> None:
@@ -2276,7 +2276,7 @@ def test_addopts_before_initini(
22762276
monkeypatch.setenv("PYTEST_ADDOPTS", f"-o cache_dir={cache_dir}")
22772277
config = _config_for_test
22782278
config.parse([], addopts=True)
2279-
assert config.inicfg.get("cache_dir") == ConfigValue(
2279+
assert config._inicfg.get("cache_dir") == ConfigValue(
22802280
cache_dir, origin="override", mode="ini"
22812281
)
22822282

@@ -2317,7 +2317,7 @@ def test_override_ini_does_not_contain_paths(
23172317
"""Check that -o no longer swallows all options after it (#3103)"""
23182318
config = _config_for_test
23192319
config.parse(["-o", "cache_dir=/cache", "/some/test/path"])
2320-
assert config.inicfg.get("cache_dir") == ConfigValue(
2320+
assert config._inicfg.get("cache_dir") == ConfigValue(
23212321
"/cache", origin="override", mode="ini"
23222322
)
23232323

@@ -2998,3 +2998,45 @@ def pytest_addoption(parser):
29982998

29992999
with pytest.raises(TypeError, match=r"expects a string.*got int"):
30003000
config.getini("string_not_string")
3001+
3002+
3003+
class TestInicfgDeprecation:
3004+
"""Tests for the upcoming deprecation of config.inicfg."""
3005+
3006+
def test_inicfg_deprecated(self, pytester: Pytester) -> None:
3007+
"""Test that accessing config.inicfg issues a deprecation warning (not yet)."""
3008+
pytester.makeini(
3009+
"""
3010+
[pytest]
3011+
minversion = 3.0
3012+
"""
3013+
)
3014+
config = pytester.parseconfig()
3015+
3016+
inicfg = config.inicfg
3017+
3018+
assert config.getini("minversion") == "3.0"
3019+
assert inicfg["minversion"] == "3.0"
3020+
assert inicfg.get("minversion") == "3.0"
3021+
del inicfg["minversion"]
3022+
inicfg["minversion"] = "4.0"
3023+
assert list(inicfg.keys()) == ["minversion"]
3024+
assert list(inicfg.items()) == [("minversion", "4.0")]
3025+
assert len(inicfg) == 1
3026+
3027+
def test_issue_13946_setting_bool_no_longer_crashes(
3028+
self, pytester: Pytester
3029+
) -> None:
3030+
"""Regression test for #13946 - setting inicfg doesn't cause a crash."""
3031+
pytester.makepyfile(
3032+
"""
3033+
def pytest_configure(config):
3034+
config.inicfg["xfail_strict"] = True
3035+
3036+
def test():
3037+
pass
3038+
"""
3039+
)
3040+
3041+
result = pytester.runpytest()
3042+
assert result.ret == 0

0 commit comments

Comments
 (0)