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
3 changes: 3 additions & 0 deletions changelog/13946.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
The private ``config.inicfg`` attribute was changed in a breaking manner in pytest 9.0.0.
Due to its usage in the ecosystem, it is now restored to working order using a compatibility shim.
It will be deprecated in pytest 9.1 and removed in pytest 10.
46 changes: 38 additions & 8 deletions src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from collections.abc import Iterable
from collections.abc import Iterator
from collections.abc import Mapping
from collections.abc import MutableMapping
from collections.abc import Sequence
import contextlib
import copy
Expand Down Expand Up @@ -47,6 +48,7 @@
from .compat import PathAwareHookProxy
from .exceptions import PrintHelp as PrintHelp
from .exceptions import UsageError as UsageError
from .findpaths import ConfigValue
from .findpaths import determine_setup
from _pytest import __version__
import _pytest._code
Expand Down Expand Up @@ -980,6 +982,30 @@ def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]:
yield from _iter_rewritable_modules(new_package_files)


class _DeprecatedInicfgProxy(MutableMapping[str, Any]):
"""Compatibility proxy for the deprecated Config.inicfg."""

__slots__ = ("_config",)

def __init__(self, config: Config) -> None:
self._config = config

def __getitem__(self, key: str) -> Any:
return self._config._inicfg[key].value

def __setitem__(self, key: str, value: Any) -> None:
self._config._inicfg[key] = ConfigValue(value, origin="override", mode="toml")

def __delitem__(self, key: str) -> None:
del self._config._inicfg[key]

def __iter__(self) -> Iterator[str]:
return iter(self._config._inicfg)

def __len__(self) -> int:
return len(self._config._inicfg)


@final
class Config:
"""Access to configuration values, pluginmanager and plugin hooks.
Expand Down Expand Up @@ -1100,6 +1126,10 @@ def __init__(
self.args_source = Config.ArgsSource.ARGS
self.args: list[str] = []

@property
def inicfg(self) -> _DeprecatedInicfgProxy:
return _DeprecatedInicfgProxy(self)

@property
def rootpath(self) -> pathlib.Path:
"""The path to the :ref:`rootdir <rootdir>`.
Expand Down Expand Up @@ -1376,7 +1406,7 @@ def pytest_collection(self) -> Generator[None, object, object]:
def _checkversion(self) -> None:
import pytest

minver_ini_value = self.inicfg.get("minversion", None)
minver_ini_value = self._inicfg.get("minversion", None)
minver = minver_ini_value.value if minver_ini_value is not None else None
if minver:
# Imported lazily to improve start-up time.
Expand Down Expand Up @@ -1440,7 +1470,7 @@ def _warn_or_fail_if_strict(self, message: str) -> None:

def _get_unknown_ini_keys(self) -> set[str]:
known_keys = self._parser._inidict.keys() | self._parser._ini_aliases.keys()
return self.inicfg.keys() - known_keys
return self._inicfg.keys() - known_keys

def parse(self, args: list[str], addopts: bool = True) -> None:
# Parse given cmdline arguments into this config object.
Expand Down Expand Up @@ -1471,7 +1501,7 @@ def parse(self, args: list[str], addopts: bool = True) -> None:
self._rootpath = rootpath
self._inipath = inipath
self._ignored_config_files = ignored_config_files
self.inicfg = inicfg
self._inicfg = inicfg
self._parser.extra_info["rootdir"] = str(self.rootpath)
self._parser.extra_info["inifile"] = str(self.inipath)

Expand Down Expand Up @@ -1648,14 +1678,14 @@ def _getini(self, name: str):
except KeyError as e:
raise ValueError(f"unknown configuration value: {name!r}") from e

# Collect all possible values (canonical name + aliases) from inicfg.
# Collect all possible values (canonical name + aliases) from _inicfg.
# Each candidate is (ConfigValue, is_canonical).
candidates = []
if canonical_name in self.inicfg:
candidates.append((self.inicfg[canonical_name], True))
if canonical_name in self._inicfg:
candidates.append((self._inicfg[canonical_name], True))
for alias, target in self._parser._ini_aliases.items():
if target == canonical_name and alias in self.inicfg:
candidates.append((self.inicfg[alias], False))
if target == canonical_name and alias in self._inicfg:
candidates.append((self._inicfg[alias], False))

if not candidates:
return default
Expand Down
52 changes: 47 additions & 5 deletions testing/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def test_getcfg_and_config(
_, _, cfg, _ = locate_config(Path.cwd(), [sub])
assert cfg["name"] == ConfigValue("value", origin="file", mode="ini")
config = pytester.parseconfigure(str(sub))
assert config.inicfg["name"] == ConfigValue("value", origin="file", mode="ini")
assert config._inicfg["name"] == ConfigValue("value", origin="file", mode="ini")

def test_setupcfg_uses_toolpytest_with_pytest(self, pytester: Pytester) -> None:
p1 = pytester.makepyfile("def test(): pass")
Expand Down Expand Up @@ -1434,10 +1434,10 @@ def test_inifilename(self, tmp_path: Path) -> None:

# this indicates this is the file used for getting configuration values
assert config.inipath == inipath
assert config.inicfg.get("name") == ConfigValue(
assert config._inicfg.get("name") == ConfigValue(
"value", origin="file", mode="ini"
)
assert config.inicfg.get("should_not_be_set") is None
assert config._inicfg.get("should_not_be_set") is None


def test_options_on_small_file_do_not_blow_up(pytester: Pytester) -> None:
Expand Down Expand Up @@ -2277,7 +2277,7 @@ def test_addopts_before_initini(
monkeypatch.setenv("PYTEST_ADDOPTS", f"-o cache_dir={cache_dir}")
config = _config_for_test
config.parse([], addopts=True)
assert config.inicfg.get("cache_dir") == ConfigValue(
assert config._inicfg.get("cache_dir") == ConfigValue(
cache_dir, origin="override", mode="ini"
)

Expand Down Expand Up @@ -2318,7 +2318,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.parse(["-o", "cache_dir=/cache", "/some/test/path"])
assert config.inicfg.get("cache_dir") == ConfigValue(
assert config._inicfg.get("cache_dir") == ConfigValue(
"/cache", origin="override", mode="ini"
)

Expand Down Expand Up @@ -2999,3 +2999,45 @@ def pytest_addoption(parser):

with pytest.raises(TypeError, match=r"expects a string.*got int"):
config.getini("string_not_string")


class TestInicfgDeprecation:
"""Tests for the upcoming deprecation of config.inicfg."""

def test_inicfg_deprecated(self, pytester: Pytester) -> None:
"""Test that accessing config.inicfg issues a deprecation warning (not yet)."""
pytester.makeini(
"""
[pytest]
minversion = 3.0
"""
)
config = pytester.parseconfig()

inicfg = config.inicfg

assert config.getini("minversion") == "3.0"
assert inicfg["minversion"] == "3.0"
assert inicfg.get("minversion") == "3.0"
del inicfg["minversion"]
inicfg["minversion"] = "4.0"
assert list(inicfg.keys()) == ["minversion"]
assert list(inicfg.items()) == [("minversion", "4.0")]
assert len(inicfg) == 1

def test_issue_13946_setting_bool_no_longer_crashes(
self, pytester: Pytester
) -> None:
"""Regression test for #13946 - setting inicfg doesn't cause a crash."""
pytester.makepyfile(
"""
def pytest_configure(config):
config.inicfg["xfail_strict"] = True

def test():
pass
"""
)

result = pytester.runpytest()
assert result.ret == 0