From 84f03f6d6f61dfb5ce668239488ff1987caafa0f Mon Sep 17 00:00:00 2001 From: Vineeth Sai Date: Tue, 14 Jul 2026 13:44:51 -0700 Subject: [PATCH] Fix top-level scalar captured by a table rendered from a dotted key Appending a scalar to a document after a dotted-key entry (a.b = 1) whose super table had gained a child that renders a [a.c]-style header placed the scalar textually inside that table's scope, so it silently re-nested as a.c.z on round-trip. _get_last_index_before_table treated every dotted-key table as inline; now it also stops at a dotted-key table whose children render a table header, so new scalars are inserted before it, matching the existing behavior for regular tables. Purely inline dotted keys are unaffected. Fixes #543 --- CHANGELOG.md | 1 + tests/test_toml_document.py | 27 +++++++++++++++++++++++++++ tomlkit/container.py | 24 ++++++++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b550220..ea5acf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ - Out-of-order value-vs-table and dotted-key-vs-table redefinitions are now rejected at parse time instead of being silently accepted or raising only on access. The parser also detects when a non-dotted key is a prefix of an existing dotted key, matching the stdlib `tomllib` behaviour. ([#523](https://github.com/python-poetry/tomlkit/issues/523)) - Reject tables inserted into inline tables instead of serializing invalid TOML. ([#531](https://github.com/python-poetry/tomlkit/issues/531)) - Fix assigning an array of tables over a dotted key (e.g. `doc["a"] = aot(...)` where `a` came from `a.b = ...`): the new `[[a]]` header kept the dotted key's inline position and swallowed the following dotted sibling on round-trip. The array of tables now renders past the inline entries it would otherwise capture, mirroring the table fix for [#513](https://github.com/python-poetry/tomlkit/issues/513). ([#542](https://github.com/python-poetry/tomlkit/issues/542)) +- Fix a new top-level scalar being captured by a table rendered from a dotted key: appending a scalar after a dotted-key entry (e.g. `a.b = 1`) whose table had gained a `[a.c]`-style child placed the scalar inside that table's scope, silently re-nesting it on round-trip. Scalars now move before such an entry, like they do before regular tables. ([#543](https://github.com/python-poetry/tomlkit/issues/543)) - Fix invalid serialization with a duplicated `[table]` header when adding a key to an out-of-order table whose concrete header is declared after its sub-tables; the new key now lands in the existing concrete part instead of giving the header-less super part a second header. ([#545](https://github.com/python-poetry/tomlkit/pull/545)) - Fix a table's display name (its exact header spelling, including whitespace and quoting) being normalised when the table is assigned onto itself, e.g. `doc[k] = doc[k]` rewriting `[keys .'a'.'c']` to `[keys.a.'c']`. ([#291](https://github.com/python-poetry/tomlkit/issues/291)) - Fix missing newlines when appending a key after a dotted inline table, including when the original document has no trailing newline. ([#533](https://github.com/python-poetry/tomlkit/pull/533)) diff --git a/tests/test_toml_document.py b/tests/test_toml_document.py index 769c919..7ba61f5 100644 --- a/tests/test_toml_document.py +++ b/tests/test_toml_document.py @@ -1639,3 +1639,30 @@ def test_appending_to_super_table() -> None: """ assert doc.as_string() == expected + + +def test_scalar_is_not_captured_by_table_rendered_from_dotted_key() -> None: + # https://github.com/python-poetry/tomlkit/issues/543 + # A dotted-key super table renders inline (`a.b = 1`) until a child that + # renders a `[table]` header is added; a scalar appended after that must + # not land inside the table's scope. + doc = parse("a.b = 1\n") + doc["a"]["c"] = {} + doc["z"] = 2 + + expected = """\ +z = 2 + +a.b = 1 + +[a.c] +""" + + assert doc.as_string() == expected + assert parse(doc.as_string()) == {"z": 2, "a": {"b": 1, "c": {}}} + + # A purely inline dotted key still gets the scalar appended after it. + doc = parse("a.b = 1\n") + doc["z"] = 2 + + assert doc.as_string() == "a.b = 1\nz = 2\n" diff --git a/tomlkit/container.py b/tomlkit/container.py index 4e38253..8ff30d9 100644 --- a/tomlkit/container.py +++ b/tomlkit/container.py @@ -161,9 +161,33 @@ def _get_last_index_before_table(self) -> int: if isinstance(v, (Table, AoT)) and k is not None and not k.is_dotted(): break + + if ( + isinstance(v, Table) + and k is not None + and k.is_dotted() + and self._renders_table_header(v) + ): + # A dotted-key super table renders inline (`a.b = 1`) only as + # long as none of its children render a `[table]` header; once + # one does, anything appended after it would land inside that + # table's scope. + break last_index = i return last_index + 1 + def _renders_table_header(self, table: Table) -> bool: + for k, v in table.value.body: + if isinstance(v, AoT): + return True + if isinstance(v, Table): + if k is not None and k.is_dotted() and v.is_super_table(): + if self._renders_table_header(v): + return True + else: + return True + return False + def _validate_out_of_order_table(self, key: Key | None = None) -> None: if key is None: for k in list(self._out_of_order_keys):