From e0686a93ecd48dcc4dc2554af59486d9440b48d5 Mon Sep 17 00:00:00 2001 From: Master-Norna <263870076+Master-Norna@users.noreply.github.com> Date: Sun, 25 Oct 2026 08:21:55 -0700 Subject: [PATCH] Honor custom default section names in INI serialization --- benedict/serializers/ini.py | 7 ++-- tests/dicts/io/test_io_dict_ini.py | 52 ++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/benedict/serializers/ini.py b/benedict/serializers/ini.py index 8abf61d7..987ce9ec 100644 --- a/benedict/serializers/ini.py +++ b/benedict/serializers/ini.py @@ -1,7 +1,6 @@ from __future__ import annotations from collections.abc import Callable -from configparser import DEFAULTSECT as default_section from configparser import RawConfigParser from io import StringIO from json.decoder import JSONDecodeError @@ -58,7 +57,7 @@ def decode(self, s: str, **kwargs: Any) -> dict[str, Any]: data: dict[str, Any] = {} for option, _ in parser.defaults().items(): data[option] = self._get_section_option_value( - parser, default_section, option + parser, parser.default_section, option ) for section in parser.sections(): data[section] = {} @@ -77,7 +76,9 @@ def encode(self, d: dict[str, Any], **kwargs: Any) -> str: parser = self._get_parser(options=kwargs) for key, value in d.items(): if not type_util.is_dict(value): - parser.set(default_section, key, None if value is None else f"{value}") + parser.set( + parser.default_section, key, None if value is None else f"{value}" + ) continue section = key parser.add_section(section) diff --git a/tests/dicts/io/test_io_dict_ini.py b/tests/dicts/io/test_io_dict_ini.py index 92fd9752..9f457b88 100644 --- a/tests/dicts/io/test_io_dict_ini.py +++ b/tests/dicts/io/test_io_dict_ini.py @@ -8,6 +8,58 @@ class io_dict_ini_test_case(io_dict_test_case): This class describes an IODict / ini test case. """ + def test_from_ini_with_custom_default_section(self) -> None: + s = ( + "[general]\ncount = 2\nratio = 1.5\nenabled = yes\nname = demo\n" + "[app]\ncount = 3\n" + ) + expected = { + "count": 2, + "ratio": 1.5, + "enabled": True, + "name": "demo", + "app": {"count": 3, "ratio": 1.5, "enabled": True, "name": "demo"}, + } + self.assertEqual(IODict.from_ini(s, default_section="general"), expected) + self.assertEqual(IODict(s, format="ini", default_section="general"), expected) + + def test_from_ini_with_custom_defaults_and_literal_default_section(self) -> None: + s = "[general]\ncount = 2\n[DEFAULT]\ncount = 9\n[app]\nname = demo\n" + expected = { + "count": 2, + "DEFAULT": {"count": 9}, + "app": {"count": 2, "name": "demo"}, + } + self.assertEqual(IODict.from_ini(s, default_section="general"), expected) + + def test_from_ini_with_custom_valueless_defaults(self) -> None: + s = "[general]\nflag\n[app]\nname = demo\n" + expected = {"flag": None, "app": {"flag": None, "name": "demo"}} + self.assertEqual( + IODict.from_ini(s, default_section="general", allow_no_value=True), + expected, + ) + + def test_to_ini_with_custom_default_section(self) -> None: + for value, line, options in ( + (2, "count = 2", {}), + (None, "count", {"allow_no_value": True}), + ): + with self.subTest(value=value): + d = IODict({"count": value, "app": {"name": "demo"}}) + s = d.to_ini(default_section="general", **options) + self.assertEqual(s, f"[general]\n{line}\n\n[app]\nname = demo\n\n") + self.assertEqual( + IODict.from_ini(s, default_section="general", **options), + {"count": value, "app": {"count": value, "name": "demo"}}, + ) + + def test_to_ini_with_custom_defaults_and_literal_default_section(self) -> None: + d = IODict({"DEFAULT": {"count": 9}, "count": 2}) + s = d.to_ini(default_section="general") + self.assertEqual(s, "[general]\ncount = 2\n\n[DEFAULT]\ncount = 9\n\n") + self.assertEqual(IODict.from_ini(s, default_section="general"), d) + def test_from_ini_with_valueless_options(self) -> None: s = "[app]\nflag\nempty =\ncount = 2\nratio = 1.5\nenabled = yes\n" expected = {