Skip to content
Open
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
21 changes: 21 additions & 0 deletions tests/test_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -1079,6 +1079,27 @@ def test_adding_to_nested_dotted_key_inside_inline_table() -> None:
assert parse(rendered).as_string() == rendered


def test_adding_a_sub_table_to_a_dotted_key_inside_inline_table() -> None:
doc = parse("a = {b.c = 1, d = 2}\n")
doc["a"]["b"]["e"] = {"f": 3}

# A sub-table of a dotted key can only be expressed inside an inline table
# as an inline table; rendering it as its own ``b.e. = f = 3`` pair is not
# valid TOML. The added value must survive the round-trip.
rendered = doc.as_string()
assert parse(rendered) == {"a": {"b": {"c": 1, "e": {"f": 3}}, "d": 2}}
assert parse(rendered).as_string() == rendered


def test_adding_a_nested_sub_table_to_a_dotted_key_inside_inline_table() -> None:
doc = parse("a = {b.c = 1}\n")
doc["a"]["b"]["d"] = {"e": {"f": 3}}

rendered = doc.as_string()
assert parse(rendered) == {"a": {"b": {"c": 1, "d": {"e": {"f": 3}}}}}
assert parse(rendered).as_string() == rendered


def test_appending_to_comma_first_array_does_not_double_separator() -> None:
doc = parse(
"""\
Expand Down
39 changes: 36 additions & 3 deletions tomlkit/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -2156,14 +2156,30 @@ def as_string(self) -> str:
def _render_dotted(self, key: Key, table: Table) -> list[str]:
"""Render a table materialized from a dotted key as a list of
``prefix.child = value`` strings, recursing into nested dotted
children."""
children. A child that is a real sub-table (its own key is not dotted)
is rendered as an inline table instead, since ``prefix.child = body``
is not a valid value."""
prefix = f"{key.as_string()}.{key.sep}"
parts = []
parts: list[str] = []
for k, v in table.value.body:
if k is None:
continue
if isinstance(v, Table):
if isinstance(v, Table) and k.is_dotted():
# Only a *dotted* child keeps the ``prefix.child`` shape; a child
# whose own key is not dotted is a real sub-table, which has no
# ``prefix.child = value`` form and must be rendered inline.
parts.extend(f"{prefix}{sub}" for sub in self._render_dotted(k, v))
elif isinstance(v, Table):
# A real sub-table nested under a dotted key can only be
# expressed inside an inline table as an inline table; rendering
# it as ``prefix.child = <body>`` would emit invalid TOML such as
# ``a.e. = f = 3``. Sub-tables nest arbitrarily deep, so convert
# the whole subtree.
value = InlineTable(_as_inline_container(v), v.trivia, new=True)
parts.append(
f"{prefix}{k.as_string()}{k.sep}{value.as_string()}"
f"{v.trivia.comment_ws}{v.trivia.comment}"
)
else:
trail = v.trivia.trail.replace("\n", "")
parts.append(
Expand Down Expand Up @@ -2363,6 +2379,23 @@ def _getstate(self, protocol: int = 3) -> tuple[list[Table], str | None, bool]:
return self._body, self.name, self._parsed


def _as_inline_container(table: Table) -> container.Container:
"""Return a copy of ``table``'s container with every nested table converted
so that it renders as an inline table.

Used to render a sub-table of a dotted key inside an inline table, where
the only valid representation of a nested table is another inline table.
"""
from tomlkit.container import Container

result = Container()
for k, v in table.value.body:
if k is not None and isinstance(v, Table):
v = InlineTable(_as_inline_container(v), v.trivia, new=True)
result.append(k, v)
return result


class Null(Item):
"""
A null item.
Expand Down
Loading