Skip to content

Commit 274237d

Browse files
committed
config: deprecate config.inicfg
As a private attribute, we broke it in pytest 9.0.0, but since it's not using a `_` prefix and has some external usage, let's keep it working until pytest 10 and deprecate it instead. Fix #13946.
1 parent e4df841 commit 274237d

File tree

5 files changed

+158
-12
lines changed

5 files changed

+158
-12
lines changed

changelog/13946.deprecation.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
The private :attr:`config.inicfg <pytest.Config.inicfg>` attribute is now deprecated.
2+
Use :meth:`config.getini() <pytest.Config.getini>` to access configuration values instead.
3+
4+
See :ref:`config-inicfg` for more details.

doc/en/deprecations.rst

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,40 @@ Below is a complete list of all pytest features which are considered deprecated.
1515
:class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
1616

1717

18+
.. _config-inicfg:
19+
20+
``config.inicfg``
21+
~~~~~~~~~~~~~~~~~
22+
23+
.. deprecated:: 9.0
24+
25+
The private :attr:`config.inicfg <pytest.Config.inicfg>` attribute is deprecated.
26+
Use :meth:`config.getini() <pytest.Config.getini>` to access configuration values instead.
27+
28+
**Reading configuration values:**
29+
30+
Instead of accessing ``config.inicfg`` directly, use :meth:`config.getini() <pytest.Config.getini>`:
31+
32+
.. code-block:: python
33+
34+
# Deprecated
35+
value = config.inicfg["some_option"]
36+
37+
# Use this instead
38+
value = config.getini("some_option")
39+
40+
**Setting configuration values:**
41+
42+
Setting or deleting configuration values after initialization is not supported.
43+
If you need to override configuration values, use the ``-o`` command line option:
44+
45+
.. code-block:: bash
46+
47+
pytest -o some_option=value
48+
49+
or set them in your configuration file instead.
50+
51+
1852
.. _parametrize-iterators:
1953

2054
Non-Collection iterables in ``@pytest.mark.parametrize``

src/_pytest/config/__init__.py

Lines changed: 64 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
@@ -56,6 +59,7 @@
5659
from _pytest._code.code import TracebackStyle
5760
from _pytest._io import TerminalWriter
5861
from _pytest.compat import assert_never
62+
from _pytest.compat import deprecated
5963
from _pytest.compat import NOTSET
6064
from _pytest.config.argparsing import Argument
6165
from _pytest.config.argparsing import FILE_OR_DIR
@@ -979,6 +983,30 @@ def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]:
979983
yield from _iter_rewritable_modules(new_package_files)
980984

981985

986+
class _DeprecatedInicfgProxy(MutableMapping[str, Any]):
987+
"""Compatibility proxy for the deprecated Config.inicfg."""
988+
989+
__slots__ = ("_config",)
990+
991+
def __init__(self, config: Config) -> None:
992+
self._config = config
993+
994+
def __getitem__(self, key: str) -> Any:
995+
return self._config._inicfg[key].value
996+
997+
def __setitem__(self, key: str, value: Any) -> None:
998+
self._config._inicfg[key] = ConfigValue(value, origin="override", mode="toml")
999+
1000+
def __delitem__(self, key: str) -> None:
1001+
del self._config._inicfg[key]
1002+
1003+
def __iter__(self) -> Iterator[str]:
1004+
return iter(self._config._inicfg)
1005+
1006+
def __len__(self) -> int:
1007+
return len(self._config._inicfg)
1008+
1009+
9821010
@final
9831011
class Config:
9841012
"""Access to configuration values, pluginmanager and plugin hooks.
@@ -1089,6 +1117,7 @@ def __init__(
10891117
self.trace = self.pluginmanager.trace.root.get("config")
10901118
self.hook: pluggy.HookRelay = PathAwareHookProxy(self.pluginmanager.hook) # type: ignore[assignment]
10911119
self._inicache: dict[str, Any] = {}
1120+
self._inicfg: ConfigDict = {}
10921121
self._cleanup_stack = contextlib.ExitStack()
10931122
self.pluginmanager.register(self, "pytestconfig")
10941123
self._configured = False
@@ -1098,6 +1127,34 @@ def __init__(
10981127
self.args_source = Config.ArgsSource.ARGS
10991128
self.args: list[str] = []
11001129

1130+
if TYPE_CHECKING:
1131+
1132+
@deprecated(
1133+
"config.inicfg is deprecated, use config.getini() to access configuration values instead.",
1134+
)
1135+
@property
1136+
def inicfg(self) -> _DeprecatedInicfgProxy:
1137+
"""Deprecated access to configuration values.
1138+
1139+
.. deprecated:: 9.1
1140+
Use :meth:`getini` to access configuration values instead.
1141+
"""
1142+
raise NotImplementedError()
1143+
else:
1144+
1145+
@property
1146+
def inicfg(self) -> _DeprecatedInicfgProxy:
1147+
"""Deprecated access to configuration values.
1148+
1149+
.. deprecated:: 9.1
1150+
Use :meth:`getini` to access configuration values instead.
1151+
"""
1152+
warnings.warn(
1153+
_pytest.deprecated.CONFIG_INICFG,
1154+
stacklevel=2,
1155+
)
1156+
return _DeprecatedInicfgProxy(self)
1157+
11011158
@property
11021159
def rootpath(self) -> pathlib.Path:
11031160
"""The path to the :ref:`rootdir <rootdir>`.
@@ -1428,7 +1485,7 @@ def _warn_or_fail_if_strict(self, message: str) -> None:
14281485

14291486
def _get_unknown_ini_keys(self) -> set[str]:
14301487
known_keys = self._parser._inidict.keys() | self._parser._ini_aliases.keys()
1431-
return self.inicfg.keys() - known_keys
1488+
return self._inicfg.keys() - known_keys
14321489

14331490
def parse(self, args: list[str], addopts: bool = True) -> None:
14341491
# Parse given cmdline arguments into this config object.
@@ -1459,7 +1516,7 @@ def parse(self, args: list[str], addopts: bool = True) -> None:
14591516
self._rootpath = rootpath
14601517
self._inipath = inipath
14611518
self._ignored_config_files = ignored_config_files
1462-
self.inicfg = inicfg
1519+
self._inicfg = inicfg
14631520
self._parser.extra_info["rootdir"] = str(self.rootpath)
14641521
self._parser.extra_info["inifile"] = str(self.inipath)
14651522

@@ -1636,14 +1693,14 @@ def _getini(self, name: str):
16361693
except KeyError as e:
16371694
raise ValueError(f"unknown configuration value: {name!r}") from e
16381695

1639-
# Collect all possible values (canonical name + aliases) from inicfg.
1696+
# Collect all possible values (canonical name + aliases) from _inicfg.
16401697
# Each candidate is (ConfigValue, is_canonical).
16411698
candidates = []
1642-
if canonical_name in self.inicfg:
1643-
candidates.append((self.inicfg[canonical_name], True))
1699+
if canonical_name in self._inicfg:
1700+
candidates.append((self._inicfg[canonical_name], True))
16441701
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))
1702+
if target == canonical_name and alias in self._inicfg:
1703+
candidates.append((self._inicfg[alias], False))
16471704

16481705
if not candidates:
16491706
return default

src/_pytest/deprecated.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@
8383
"See https://docs.pytest.org/en/stable/deprecations.html#parametrize-iterators",
8484
)
8585

86+
CONFIG_INICFG = PytestRemovedIn10Warning(
87+
"config.inicfg is deprecated, use config.getini() to access configuration values instead.\n"
88+
"See https://docs.pytest.org/en/stable/deprecations.html#config-inicfg"
89+
)
90+
8691
# You want to make some `__init__` or function "private".
8792
#
8893
# def my_private_function(some, args):

testing/test_config.py

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from _pytest.monkeypatch import MonkeyPatch
3131
from _pytest.pathlib import absolutepath
3232
from _pytest.pytester import Pytester
33+
from _pytest.warning_types import PytestDeprecationWarning
3334
import pytest
3435

3536

@@ -60,7 +61,7 @@ def test_getcfg_and_config(
6061
_, _, cfg, _ = locate_config(Path.cwd(), [sub])
6162
assert cfg["name"] == ConfigValue("value", origin="file", mode="ini")
6263
config = pytester.parseconfigure(str(sub))
63-
assert config.inicfg["name"] == ConfigValue("value", origin="file", mode="ini")
64+
assert config._inicfg["name"] == ConfigValue("value", origin="file", mode="ini")
6465

6566
def test_setupcfg_uses_toolpytest_with_pytest(self, pytester: Pytester) -> None:
6667
p1 = pytester.makepyfile("def test(): pass")
@@ -1433,10 +1434,10 @@ def test_inifilename(self, tmp_path: Path) -> None:
14331434

14341435
# this indicates this is the file used for getting configuration values
14351436
assert config.inipath == inipath
1436-
assert config.inicfg.get("name") == ConfigValue(
1437+
assert config._inicfg.get("name") == ConfigValue(
14371438
"value", origin="file", mode="ini"
14381439
)
1439-
assert config.inicfg.get("should_not_be_set") is None
1440+
assert config._inicfg.get("should_not_be_set") is None
14401441

14411442

14421443
def test_options_on_small_file_do_not_blow_up(pytester: Pytester) -> None:
@@ -2276,7 +2277,7 @@ def test_addopts_before_initini(
22762277
monkeypatch.setenv("PYTEST_ADDOPTS", f"-o cache_dir={cache_dir}")
22772278
config = _config_for_test
22782279
config.parse([], addopts=True)
2279-
assert config.inicfg.get("cache_dir") == ConfigValue(
2280+
assert config._inicfg.get("cache_dir") == ConfigValue(
22802281
cache_dir, origin="override", mode="ini"
22812282
)
22822283

@@ -2317,7 +2318,7 @@ def test_override_ini_does_not_contain_paths(
23172318
"""Check that -o no longer swallows all options after it (#3103)"""
23182319
config = _config_for_test
23192320
config.parse(["-o", "cache_dir=/cache", "/some/test/path"])
2320-
assert config.inicfg.get("cache_dir") == ConfigValue(
2321+
assert config._inicfg.get("cache_dir") == ConfigValue(
23212322
"/cache", origin="override", mode="ini"
23222323
)
23232324

@@ -2998,3 +2999,48 @@ def pytest_addoption(parser):
29982999

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

0 commit comments

Comments
 (0)