Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
- Fix `comment()` producing invalid TOML for a multiline string by prefixing every line with `#`, not just the first. ([#449](https://github.com/python-poetry/tomlkit/issues/449))
- Fix the separator comma being swallowed by a trailing comment when appending a key to a multiline inline table, leaving the new key without a separator so the result no longer round-trips. ([#512](https://github.com/python-poetry/tomlkit/issues/512))
- 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))

## [0.15.0] - 2026-05-10

Expand Down
48 changes: 35 additions & 13 deletions tests/test_toml_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
from tomlkit import ws
from tomlkit._utils import _utc
from tomlkit.api import document
from tomlkit.exceptions import KeyAlreadyPresent
from tomlkit.exceptions import NonExistentKey
from tomlkit.exceptions import ParseError
from tomlkit.exceptions import TOMLKitError
from tomlkit.toml_document import TOMLDocument


Expand Down Expand Up @@ -588,18 +589,39 @@ def test_unwrap_out_of_order_tables() -> None:


def test_unwrap_preserves_raise_on_invalid_out_of_order_fragment() -> None:
# Regression guard for the unwrap() fast path: this document is actually
# *invalid* TOML -- `b` is a value under [a], then reopened as a table by
# the out-of-order [a.b]. Ideally tomlkit would reject it at parse (it does
# for the in-order form, and so does the stdlib tomllib); today the conflict
# is only detected lazily when the out-of-order proxy is built, so it
# surfaces at access/unwrap time. This test does NOT bless that deferred
# timing -- it only pins that the faster unwrap() keeps going through the
# proxy and still raises, rather than silently merging the conflict into a
# corrupted dict.
doc = parse("[a]\nb = true\n[zz]\nq = 9\n[a.b]\narr = [1, 2]\n")
with pytest.raises(KeyAlreadyPresent):
doc.unwrap()
# Regression guard: this document is actually *invalid* TOML -- `b` is a value
# under [a], then reopened as a table by the out-of-order [a.b]. The in-order
# form and stdlib tomllib both reject it. Since the Container.append concrete-
# +super fix, tomlkit now rejects it at parse time as well.
with pytest.raises(ParseError):
parse("[a]\nb = true\n[zz]\nq = 9\n[a.b]\narr = [1, 2]\n")


def test_reject_out_of_order_dotted_key_redefinition_at_parse() -> None:
# https://github.com/python-poetry/tomlkit/issues/523
# A dotted key (b.c) creates an implicit table definition that is later
# redefined by an explicit [a.b] table header after an unrelated table [zz].
# This is invalid TOML (defining a table multiple times) and must be rejected
# at parse, not silently accepted.
with pytest.raises(ParseError):
parse("[a]\nb.c = 1\n[zz]\nq = 9\n[a.b]\nd = 2\n")


def test_reject_out_of_order_dotted_prefix_at_parse() -> None:
# https://github.com/python-poetry/tomlkit/issues/523
# A non-dotted candidate key (b) that is a prefix of an existing dotted key
# (b.c) means the table would be defined twice — once implicitly by the
# dotted key and once explicitly by the table header. This must raise.
with pytest.raises((ParseError, TOMLKitError)):
parse("[a]\nb.c=1\n[a.b]\nd=2\n")


def test_valid_out_of_order_independent_tables() -> None:
# [zz] splits unrelated tables; the concrete+super merge must not break
# valid out-of-order extensions.
doc = parse("[a]\nx=1\n[zz]\n[a.b]\nc=1\n")
assert doc.unwrap() == {"a": {"x": 1, "b": {"c": 1}}, "zz": {}}
assert doc.as_string() == "[a]\nx=1\n[zz]\n[a.b]\nc=1\n"


def test_out_of_order_table_merges_aot_fragments() -> None:
Expand Down
18 changes: 18 additions & 0 deletions tomlkit/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,16 @@ def append(
self._validate_table_candidate(current, item)
elif not item.is_super_table():
raise KeyAlreadyPresent(key)
else:
# An existing concrete table (current) is being extended by
# a super-table (item) — e.g. [a] b=1 then [a.b] c=2 out of
# order, or [a] b.c=1 then [a.b] d=2. Validate that the
# super-table does not redefine any existing key, raising
# early at parse time. When validation passes, fall through
# — _raw_append below will create an out-of-order entry and
# preserve table ordering in the document.
assert isinstance(current, Table)
self._validate_table_candidate(current, item)
elif isinstance(item, AoT):
if not isinstance(current, AoT):
# Tried to define an AoT after a table with the same name.
Expand Down Expand Up @@ -360,6 +370,8 @@ def append(
previous_item.trivia.trail += "\n"

self._raw_append(key, item)
if validate and key is not None:
self._validate_out_of_order_table(key)
return self

def _validate_table_candidate(self, current: Table, candidate: Table) -> None:
Expand All @@ -384,6 +396,12 @@ def _validate_table_candidate(self, current: Table, candidate: Table) -> None:
continue

if not k.is_dotted():
# Even when the candidate key itself is not dotted, an
# existing dotted key may already use it as a prefix —
# e.g. [a] b.c=1 then [a.b] d=2 (b prefixes b.c).
for existing_key in current.value._map:
if existing_key.is_dotted() and next(iter(existing_key)) == k:
raise TOMLKitError("Redefinition of an existing table")
continue

head = next(iter(k))
Expand Down
Loading