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():