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.
4 changes: 4 additions & 0 deletions changelog/13946.deprecation.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
The private ``config.inicfg`` attribute is now deprecated.
Use :meth:`config.getini() <pytest.Config.getini>` to access configuration values instead.

See :ref:`config-inicfg` for more details.
37 changes: 37 additions & 0 deletions doc/en/deprecations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,43 @@ Below is a complete list of all pytest features which are considered deprecated.
:class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.


.. _config-inicfg:

``config.inicfg``
~~~~~~~~~~~~~~~~~

.. deprecated:: 9.0

The private ``config.inicfg`` attribute is deprecated.
Use :meth:`config.getini() <pytest.Config.getini>` to access configuration values instead.

``config.inicfg`` was never documented and it should have had a ``_`` prefix from the start.
Pytest performs caching, transformation and aliasing on configuration options which make direct access to the raw ``config.inicfg`` untenable.

**Reading configuration values:**

Instead of accessing ``config.inicfg`` directly, use :meth:`config.getini() <pytest.Config.getini>`:

.. code-block:: python

# Deprecated
value = config.inicfg["some_option"]

# Use this instead
value = config.getini("some_option")

**Setting configuration values:**

Setting or deleting configuration values after initialization is not supported.
If you need to override configuration values, use the ``-o`` command line option:

.. code-block:: bash

pytest -o some_option=value

or set them in your configuration file instead.


.. _parametrize-iterators:

Non-Collection iterables in ``@pytest.mark.parametrize``
Expand Down
61 changes: 54 additions & 7 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 @@ -48,6 +49,8 @@
from .compat import PathAwareHookProxy
from .exceptions import PrintHelp as PrintHelp
from .exceptions import UsageError as UsageError
from .findpaths import ConfigDict
from .findpaths import ConfigValue
from .findpaths import determine_setup
from _pytest import __version__
import _pytest._code
Expand All @@ -56,6 +59,7 @@
from _pytest._code.code import TracebackStyle
from _pytest._io import TerminalWriter
from _pytest.compat import assert_never
from _pytest.compat import deprecated
from _pytest.compat import NOTSET
from _pytest.config.argparsing import Argument
from _pytest.config.argparsing import FILE_OR_DIR
Expand Down Expand Up @@ -979,6 +983,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 @@ -1089,6 +1117,7 @@ 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._inicfg: ConfigDict = {}
self._cleanup_stack = contextlib.ExitStack()
self.pluginmanager.register(self, "pytestconfig")
self._configured = False
Expand All @@ -1098,6 +1127,24 @@ def __init__(
self.args_source = Config.ArgsSource.ARGS
self.args: list[str] = []

if TYPE_CHECKING:

@deprecated(
"config.inicfg is deprecated, use config.getini() to access configuration values instead.",
)
@property
def inicfg(self) -> _DeprecatedInicfgProxy:
raise NotImplementedError()
else:

@property
def inicfg(self) -> _DeprecatedInicfgProxy:
warnings.warn(
_pytest.deprecated.CONFIG_INICFG,
stacklevel=2,
)
return _DeprecatedInicfgProxy(self)

@property
def rootpath(self) -> pathlib.Path:
"""The path to the :ref:`rootdir <rootdir>`.
Expand Down Expand Up @@ -1428,7 +1475,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 @@ -1459,7 +1506,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 @@ -1636,14 +1683,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
5 changes: 5 additions & 0 deletions src/_pytest/deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@
"See https://docs.pytest.org/en/stable/deprecations.html#parametrize-iterators",
)

CONFIG_INICFG = PytestRemovedIn10Warning(
"config.inicfg is deprecated, use config.getini() to access configuration values instead.\n"
"See https://docs.pytest.org/en/stable/deprecations.html#config-inicfg"
)

# You want to make some `__init__` or function "private".
#
# def my_private_function(some, args):
Expand Down
56 changes: 51 additions & 5 deletions testing/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from _pytest.monkeypatch import MonkeyPatch
from _pytest.pathlib import absolutepath
from _pytest.pytester import Pytester
from _pytest.warning_types import PytestDeprecationWarning
import pytest


Expand Down Expand Up @@ -60,7 +61,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 @@ -1433,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 @@ -2276,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 @@ -2317,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 @@ -2998,3 +2999,48 @@ 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 deprecation of config.inicfg."""

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

with pytest.warns(
PytestDeprecationWarning, match=r"config\.inicfg is deprecated"
):
inicfg = config.inicfg # type: ignore[deprecated]

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