diff --git a/tests/test_toml_document.py b/tests/test_toml_document.py index 7ba61f5..0b24166 100644 --- a/tests/test_toml_document.py +++ b/tests/test_toml_document.py @@ -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}}}]} diff --git a/tomlkit/container.py b/tomlkit/container.py index 8ff30d9..d64e48c 100644 --- a/tomlkit/container.py +++ b/tomlkit/container.py @@ -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 ( @@ -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(" ") @@ -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 @@ -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)