From 1cf87f87415f03eb1004366582e399a85cfb9903 Mon Sep 17 00:00:00 2001 From: Sarath Francis Date: Fri, 26 Jun 2026 02:34:03 -0400 Subject: [PATCH] fix: avoid duplicate table header when adding a key to an out-of-order table Adding a plain key to an out-of-order table whose concrete `[x]` part is declared after its sub-tables (the "defining a super-table afterward is ok" spec form) wrote the value into the header-less super part, forcing it to render a second `[x]` header next to the existing concrete one. The dumped output then had a duplicate header and no longer parsed. Route the value into the existing concrete (non-super) part instead, so the header that already exists gains the key and no second header is emitted. --- CHANGELOG.md | 1 + tests/test_toml_document.py | 14 ++++++++++++++ tomlkit/container.py | 13 ++++++++++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index faad379..5bb23fb 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 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)) ## [0.15.0] - 2026-05-10 diff --git a/tests/test_toml_document.py b/tests/test_toml_document.py index 0ee5ed8..c319d93 100644 --- a/tests/test_toml_document.py +++ b/tests/test_toml_document.py @@ -643,6 +643,20 @@ def test_valid_out_of_order_independent_tables() -> None: assert doc.as_string() == "[a]\nx=1\n[zz]\n[a.b]\nc=1\n" +def test_set_value_on_out_of_order_table_with_empty_concrete_part() -> None: + # A super table defined after its sub-table (the "defining a super-table + # afterward is ok" spec example) leaves an empty concrete `[x]` part. + # Adding a plain value must land in that concrete part, not turn the + # header-less super part into a second `[x]` header -- which produced + # output with a duplicate header that no longer parsed. + doc = parse("[x.y.z.w]\n\n[x]\n") + doc["x"]["c"] = 3 + + assert doc["x"]["c"] == 3 + assert doc.as_string() == "[x.y.z.w]\n\n[x]\nc = 3\n" + assert parse(doc.as_string()).unwrap() == {"x": {"y": {"z": {"w": {}}}, "c": 3}} + + def test_out_of_order_table_merges_aot_fragments() -> None: # https://github.com/python-poetry/tomlkit/issues/505 content = """\ diff --git a/tomlkit/container.py b/tomlkit/container.py index 040bde7..9633c7d 100644 --- a/tomlkit/container.py +++ b/tomlkit/container.py @@ -1168,7 +1168,18 @@ def _is_table_or_aot(it: Any) -> bool: table[key] = value break else: - self._tables[0][key] = value + # No part holds a plain value yet, so the chosen part must + # start rendering its own ``[key]`` header. Prefer a part + # that is not a super table (it already renders that header): + # turning a header-less super part concrete here would emit a + # second, duplicate header next to an existing concrete part + # and produce TOML that no longer parses. + for table in self._tables: + if not table.is_super_table(): + table[key] = value + break + else: + self._tables[0][key] = value else: self._tables[0][key] = value else: