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
47 changes: 47 additions & 0 deletions tests/test_toml_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -1666,3 +1666,50 @@ def test_scalar_is_not_captured_by_table_rendered_from_dotted_key() -> None:
doc["z"] = 2

assert doc.as_string() == "a.b = 1\nz = 2\n"


def test_subtable_of_dotted_key_keeps_parent_header_path() -> None:
# https://github.com/python-poetry/tomlkit/issues/557
# A dotted-key super table nested inside a real table (`[t]` -> `a.b = 1`)
# renders its leaf inline, but a sub-table added under it must keep the full
# header path (`[t.a.c]`), not escape to the document root as `[a.c]`.
doc = parse("[t]\na.b = 1\n")
doc["t"]["a"]["c"] = {}

expected = """\
[t]
a.b = 1

[t.a.c]
"""

assert doc.as_string() == expected
assert parse(doc.as_string()) == {"t": {"a": {"b": 1, "c": {}}}}
# Idempotent: re-rendering the parsed result must not drift.
assert parse(doc.as_string()).as_string() == expected

# Chained dotted parent keeps leaves relative while the sub-table is absolute.
doc = parse("[t]\na.b.x = 1\n")
doc["t"]["a"]["b"]["c"] = {}

assert doc.as_string() == "[t]\na.b.x = 1\n\n[t.a.b.c]\n"
assert parse(doc.as_string()) == {"t": {"a": {"b": {"x": 1, "c": {}}}}}


def test_subtable_of_dotted_key_in_aot_keeps_parent_header_path() -> None:
# https://github.com/python-poetry/tomlkit/issues/557
# The same rule applies inside an array-of-tables element: a sub-table added
# under a dotted key must render as `[servers.a.c]`, not leak to root `[a.c]`.
doc = parse("[[servers]]\na.b = 1\n")
doc["servers"][0]["a"]["c"] = {"x": 9}

expected = """\
[[servers]]
a.b = 1

[servers.a.c]
x = 9
"""

assert doc.as_string() == expected
assert parse(doc.as_string()) == {"servers": [{"a": {"b": 1, "c": {"x": 9}}}]}
47 changes: 37 additions & 10 deletions tomlkit/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -658,17 +658,35 @@ def as_string(self) -> str:

return s

def _render_table(self, key: Key, table: Table, prefix: str | None = None) -> str:
def _render_table(
self,
key: Key,
table: Table,
prefix: str | None = None,
header_prefix: str | None = None,
) -> str:
cur = ""

if table.display_name is not None:
_key = table.display_name
_hkey = _key
else:
_key = key.as_string()

if prefix is not None:
_key = prefix + "." + _key

# ``_hkey`` is the absolute key path used for the headers of nested
# sub-tables. It can diverge from ``_key`` when this table was reached
# through a dotted key: the table's own leaf items render relative to
# the nearest real header (so ``a.b`` stays under ``[t]``), but a
# genuine sub-table nested inside it still needs the full path
# (``[t.a.c]``, not ``[a.c]``).
if header_prefix is not None:
_hkey = header_prefix + "." + key.as_string()
else:
_hkey = _key

if (
not table.is_super_table()
or (
Expand Down Expand Up @@ -718,12 +736,19 @@ def _render_table(self, key: Key, table: Table, prefix: str | None = None) -> st
assert k is not None
if v.is_super_table():
if k.is_dotted() and not key.is_dotted():
# Dotted key inside table
cur += self._render_table(k, v)
# Outermost dotted key inside a table: leaf items render
# relative to this header, but a genuine sub-table nested
# inside still needs the absolute path (carried by
# ``header_prefix``).
cur += self._render_table(k, v, header_prefix=_hkey)
else:
cur += self._render_table(k, v, prefix=_key)
# Super-table continuing a chain: keep leaves relative
# via ``_key`` while tracking the absolute header path.
cur += self._render_table(
k, v, prefix=_key, header_prefix=_hkey
)
else:
cur += self._render_table(k, v, prefix=_key)
cur += self._render_table(k, v, prefix=_hkey, header_prefix=_hkey)
elif isinstance(v, AoT):
if (
cur.strip(" ")
Expand All @@ -732,7 +757,7 @@ def _render_table(self, key: Key, table: Table, prefix: str | None = None) -> st
):
cur += "\n"
assert k is not None
cur += self._render_aot(k, v, prefix=_key)
cur += self._render_aot(k, v, prefix=_hkey)
else:
cur += self._render_simple_item(
k, v, prefix=_key if key.is_dotted() else None
Expand Down Expand Up @@ -772,12 +797,14 @@ def _render_aot_table(self, table: Table, prefix: str | None = None) -> str:
assert k is not None
if v.is_super_table():
if k.is_dotted():
# Dotted key inside table
cur += self._render_table(k, v)
# Dotted key inside an array-of-tables element: leaf items
# render relative to the ``[[...]]`` header, but a genuine
# sub-table nested inside still needs the absolute path.
cur += self._render_table(k, v, header_prefix=_key)
else:
cur += self._render_table(k, v, prefix=_key)
cur += self._render_table(k, v, prefix=_key, header_prefix=_key)
else:
cur += self._render_table(k, v, prefix=_key)
cur += self._render_table(k, v, prefix=_key, header_prefix=_key)
elif isinstance(v, AoT):
assert k is not None
cur += self._render_aot(k, v, prefix=_key)
Expand Down