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))
- Fix a `KeyAlreadyPresent` error when a dotted-key header extension (e.g. `[fruit.apple.texture]`) appears after an unrelated table following an array-of-tables (e.g. `[[fruit]]`). ([#261](https://github.com/python-poetry/tomlkit/issues/261))

## [0.15.0] - 2026-05-10

Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ Part of the implementation has been adapted, improved and fixed from [Molten](ht

See the [documentation](https://tomlkit.readthedocs.io/) for more information.

## Limitations

`tomlkit` preserves the layout of parsed documents where it can. One known
exception is an out-of-order sub-table that extends the last element of an array
of tables, such as `[fruit.apple.texture]` appearing after an unrelated table
following `[[fruit]]`. When the document is serialized again, that sub-table is
emitted inside the array element it belongs to. Its data is preserved, but its
physical position in the file is normalized.

## Installation

If you are using [uv](https://docs.astral.sh/uv), you can
Expand Down
63 changes: 63 additions & 0 deletions tests/test_toml_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,69 @@ def test_out_of_order_table_merges_three_aot_fragments() -> None:
assert hooks["state"]["z"] == 3


def test_aot_extension_via_dotted_key_after_unrelated_table() -> None:
content = """\
[[fruit]]
apple.color = "red"

[potato]

[fruit.apple.texture]
smooth = true
"""
doc = parse(content)
fruit = doc["fruit"]
assert len(fruit) == 1
assert fruit[0]["apple"]["color"] == "red"
assert fruit[0]["apple"]["texture"]["smooth"] is True
assert "potato" in doc
# Round-trip
assert parse(doc.as_string())["fruit"][0]["apple"]["texture"]["smooth"] is True


def test_aot_extension_deep_merges_existing_nested_table() -> None:
content = """\
[[fruit]]
apple.texture.rough = false

[potato]

[fruit.apple.texture]
smooth = true
"""
texture = parse(content)["fruit"][0]["apple"]["texture"]
assert texture["rough"] is False
assert texture["smooth"] is True


@pytest.mark.parametrize(
"content",
[
"""\
[[fruit]]
apple.texture = "rough"

[potato]

[fruit.apple.texture]
smooth = true
""",
"""\
[[fruit]]
apple.texture.smooth = false

[potato]

[fruit.apple.texture.smooth]
value = true
""",
],
)
def test_aot_extension_keeps_redefinition_errors(content: str) -> None:
with pytest.raises(tomlkit.exceptions.ParseError, match=r'Key ".*" already exists'):
parse(content)


def test_out_of_order_tables_are_still_dicts() -> None:
content = """
[a.a]
Expand Down
31 changes: 31 additions & 0 deletions tomlkit/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,37 @@ def append(
return self
elif isinstance(current, AoT):
if not item.is_aot_element():
if item.is_super_table():
# A dotted-key header like [fruit.apple.texture]
# appearing after an unrelated table extends the last
# element of the AoT (TOML spec: the [[fruit]]
# header preceding the dotted key is implied).
# Walk the super-table and merge into the last
# element, bypassing the normal append path which
# would trip over raw_append-created keys (they lack
# the _dotted flag that _handle_dotted_key sets).
def _deep_merge_into(
target: Container, source: Table
) -> None:
for sk, sv in source.value.body:
if sk is None:
continue
if sk in target:
existing = target.item(sk)
if isinstance(existing, Table) and isinstance(
sv, Table
):
_deep_merge_into(
existing.value,
sv, # type: ignore[arg-type]
)
else:
raise KeyAlreadyPresent(sk)
else:
target._raw_append(sk, sv)

_deep_merge_into(current[-1].value, item)
return self
# Tried to define a table after an AoT with the same name.
raise KeyAlreadyPresent(key)

Expand Down
Loading