diff --git a/CHANGELOG.md b/CHANGELOG.md index faad379..aff30d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ - Fix a `KeyAlreadyPresent` error when parsing or accessing an out-of-order table whose array-of-tables elements are split across the table's parts. ([#505](https://github.com/python-poetry/tomlkit/issues/505)) - 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 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)) ## [0.15.0] - 2026-05-10 diff --git a/tests/test_toml_document.py b/tests/test_toml_document.py index 0ee5ed8..ca303e8 100644 --- a/tests/test_toml_document.py +++ b/tests/test_toml_document.py @@ -1560,3 +1560,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 040bde7..6c80ad4 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):