From d2c954ef6ef91135225ea5b474936829375c1ac0 Mon Sep 17 00:00:00 2001 From: Andrew Chen <48723787+chuenchen309@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:13:16 +0800 Subject: [PATCH] fix: don't reject an out-of-order super-table extended by a dotted key `Container._validate_table_candidate` rejected valid TOML: when a deep header like `[a.b.c]` makes `a.b` an implicit super-table, reopening `[a]` and extending `a.b` via a dotted key (`b.z = 1`) was rejected with "Redefinition of an existing table". No table is defined twice -- `a.b` only gains a new key -- and stdlib `tomllib` (the validity oracle the tests already use) accepts it, producing `{'a': {'b': {'c': {}, 'z': 1}}}`. The `if k.is_dotted(): raise` guard fired before the super-table recursion that follows it, so it never got the chance to validate the subtree for a real conflict. Only reject the dotted case when the existing entry is *not* a super table; otherwise fall through to the existing recursion, which raises only on an actual redefinition. The concrete-table case (`[a.b]` then `[a]` `b.z = 1`) stays rejected, matching tomllib and the redefinition rejections from #516/#530. Adds a regression test next to the sibling out-of-order tests; it is red before the fix (ParseError) and green after. Full suite passes (372). Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 6 ++++++ tests/test_toml_document.py | 11 +++++++++++ tomlkit/container.py | 4 +++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3737b80..2c5dc50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## [Unreleased] + +### Fixed + +- Fix valid TOML being rejected when an out-of-order table reopens an implicit super-table with a dotted key: `[a.b.c]` then `[a]` with `b.z = 1` extends the implicit `a.b` super-table rather than redefining it (stdlib `tomllib` accepts it), so it now parses and round-trips instead of raising `Redefinition of an existing table`. The concrete-table case (`[a.b]` then `[a]` `b.z = 1`) stays rejected. ([#565](https://github.com/python-poetry/tomlkit/pull/565)) + ## [0.15.1] - 2026-07-17 ### Changed diff --git a/tests/test_toml_document.py b/tests/test_toml_document.py index 7ba61f5..b0e88a7 100644 --- a/tests/test_toml_document.py +++ b/tests/test_toml_document.py @@ -643,6 +643,17 @@ 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_valid_out_of_order_super_table_extended_by_dotted_key() -> None: + # A deep header [a.b.c] makes a.b an implicit super-table; reopening [a] and + # extending a.b via a dotted key (b.z) only adds a key to that super-table, + # it is not a table redefinition. stdlib tomllib accepts this, so it must + # parse and round-trip byte-for-byte rather than raise. (The concrete-table + # form `[a.b]` then `[a]` `b.z=1` stays rejected, covered above.) + doc = parse("[a.b.c]\n[a]\nb.z = 1\n") + assert doc.unwrap() == {"a": {"b": {"c": {}, "z": 1}}} + assert doc.as_string() == "[a.b.c]\n[a]\nb.z = 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. diff --git a/tomlkit/container.py b/tomlkit/container.py index 8ff30d9..f13c13a 100644 --- a/tomlkit/container.py +++ b/tomlkit/container.py @@ -426,7 +426,9 @@ def _validate_table_candidate(self, current: Table, candidate: Table) -> None: existing = current.value.item(k) if isinstance(existing, (Table, AoT)) != isinstance(v, (Table, AoT)): raise KeyAlreadyPresent(k) - if k.is_dotted(): + if k.is_dotted() and not ( + isinstance(existing, Table) and existing.is_super_table() + ): raise TOMLKitError("Redefinition of an existing table") if isinstance(existing, Table) and isinstance(v, Table): if not existing.is_super_table() and not v.is_super_table():