From eebee343f5e4cf7fc36d479bdf58993cd9283922 Mon Sep 17 00:00:00 2001 From: Maarten ter Huurne Date: Tue, 25 Nov 2025 00:47:32 +0100 Subject: [PATCH] Support TOML formatting with a non-empty context Since we support binding tables that are not at the root of a TOML file, it makes sense to also support formatting non-root tables. This is done by passing a `context` argument that is prefixed to all table names in the TOML output. --- src/dataclass_binder/_impl.py | 18 ++++++++++++------ tests/test_formatting.py | 25 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/dataclass_binder/_impl.py b/src/dataclass_binder/_impl.py index 64764c8..1ff5324 100644 --- a/src/dataclass_binder/_impl.py +++ b/src/dataclass_binder/_impl.py @@ -436,7 +436,7 @@ def _bind_to_class(self, toml_dict: Mapping[str, Any], instance: T | None, conte else: return replace(instance, **parsed) # type: ignore[type-var] - def format_toml(self) -> Iterator[str]: + def format_toml(self, context: str = "") -> Iterator[str]: """ Yield lines of TOML text for populating the dataclass or object that we are binding to. @@ -444,11 +444,14 @@ def format_toml(self) -> Iterator[str]: If we are binding to a class, example values for mandatory fields will be derived from the field types; these example values can be syntactically incorrect placeholders. + + The `context` parameter contains a dot-separated key path for the bound object/class: + this will be prefixed to all yielded TOML table names. """ - return self._format_toml_root(template=False) + return self._format_toml_root(context=context, template=False) - def format_toml_template(self) -> Iterator[str]: + def format_toml_template(self, context: str = "") -> Iterator[str]: """ Yield lines of TOML text as a template for populating the dataclass or object that we are binding to. @@ -457,12 +460,15 @@ def format_toml_template(self) -> Iterator[str]: If we are binding to an object, values from that object will be used to populate the template. If we are binding to a class, example values will be derived from the field types. + + The `context` parameter contains a dot-separated key path for the bound object/class: + this will be prefixed to all yielded TOML table names. """ - return self._format_toml_root(template=True) + return self._format_toml_root(context=context, template=True) - def _format_toml_root(self, *, template: bool) -> Iterator[str]: - table = Table(self, "", self._instance, None) + def _format_toml_root(self, *, context: str, template: bool) -> Iterator[str]: + table = Table(self, context, self._instance, None) lines = table.format_table(set(), template=template) for line in lines: if line: diff --git a/tests/test_formatting.py b/tests/test_formatting.py index ffe5ed2..febba12 100644 --- a/tests/test_formatting.py +++ b/tests/test_formatting.py @@ -231,6 +231,12 @@ class Inner: behind_the_curtain: str = field(init=False, default="wizard") +@dataclass(kw_only=True) +class Outer: + top_level: int + nested: Inner + + @pytest.mark.parametrize("optional", (True, False)) @pytest.mark.parametrize("string", (True, False)) def test_format_value_nested_dataclass(*, optional: bool, string: bool) -> None: @@ -239,6 +245,25 @@ def test_format_value_nested_dataclass(*, optional: bool, string: bool) -> None: assert round_trip_value(value, dc) == value +@pytest.mark.parametrize( + ("context", "expected_headers"), + [ + ("", ["[nested]"]), + ("one", ["[one]", "[one.nested]"]), + ("one.two", ["[one.two]", "[one.two.nested]"]), + ], +) +def test_format_value_context(context: str, expected_headers: list[str]) -> None: + obj = Outer(top_level=123, nested=Inner(key_containing_underscores=True, maybesuffix=timedelta(days=2))) + + lines = list(Binder(obj).format_toml_template(context=context)) + toml = "\n".join(lines) + print(toml) # noqa: T201 + + actual_headers = [line for line in lines if line.startswith("[")] + assert actual_headers == expected_headers + + def test_format_value_unsupported_type() -> None: with pytest.raises(TypeError, match=r"^NoneType$"): format_toml_pair("unsupported", None)