From 723514ef0140e13bf442c56c7d189385864b2704 Mon Sep 17 00:00:00 2001 From: LindseyZ1205 Date: Sun, 1 Nov 2026 14:08:54 -0500 Subject: [PATCH] Fix invalid TOML when adding a dotted key to an inline table A dotted key made with key() keeps the default " = " separator on every part, while the parser leaves it empty on all parts but the last one. An inline table renders those parts with their separator after the dot, so adding such a key rendered {c. = d = 2}. Leave their separator empty in _handle_dotted_key(), and put the space before the new entry on the table that the dotted key adds instead of on its value, which comes after the prefix. --- CHANGELOG.md | 1 + tests/test_api.py | 15 +++++++++++++++ tomlkit/container.py | 4 ++++ tomlkit/items.py | 17 ++++++++++++++--- 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b213c935..054bedf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixed - Arithmetic and `replace()` results for `Integer`, `Float`, `String`, `DateTime`, `Date` and `Time` no longer share the original item's `Trivia`, so changing the result's comment or indentation no longer rewrites the item still stored in the document. +- Fix a dotted key made with `key()` and added to an inline table (e.g. `table.add(key(["c", "d"]), 2)`) being written as invalid TOML such as `{c. = d = 2}`; it is now written as `{c.d = 2}`. ## [0.15.2] - 2026-10-16 diff --git a/tests/test_api.py b/tests/test_api.py index 14d245b6..d9a07b47 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -445,6 +445,21 @@ def test_add_dotted_key() -> None: assert table.as_string() == "foo.bar = 1\n" +def test_add_dotted_key_to_inline_table() -> None: + table = tomlkit.inline_table() + table.add(tomlkit.key(["foo", "bar"]), 1) + table[tomlkit.key(["foo", "baz"])] = 2 + assert table.as_string() == "{foo.bar = 1, foo.baz = 2}" + + +def test_add_dotted_key_to_parsed_inline_table() -> None: + doc = parse("a = {b.c = 1}\n") + doc["a"].add(tomlkit.key(["b", "d"]), 2) + doc["a"].append(tomlkit.key(["e", "f", "g"]), 3) + assert doc.as_string() == "a = {b.c = 1, b.d = 2, e.f.g = 3}\n" + assert doc == {"a": {"b": {"c": 1, "d": 2}, "e": {"f": {"g": 3}}}} + + def test_key_with_single_element_list_is_a_single_key() -> None: # Regression test for # https://github.com/python-poetry/tomlkit/issues/430: a one-element key diff --git a/tomlkit/container.py b/tomlkit/container.py index 9265c740..02d7e564 100644 --- a/tomlkit/container.py +++ b/tomlkit/container.py @@ -210,10 +210,14 @@ def _handle_dotted_key(self, key: Key, value: Item) -> None: if isinstance(value, (Table, AoT)): raise TOMLKitError("Can't add a table to a dotted key") name, *mid, last = key + # The parts before the last one are followed by a dot, not by the + # separator, so leave theirs empty as the parser does name._dotted = True + name.sep = "" table = current = Table(Container(True), Trivia(), False, is_super_table=True) for _name in mid: _name._dotted = True + _name.sep = "" new_table = Table(Container(True), Trivia(), False, is_super_table=True) current.append(_name, new_table) current = new_table diff --git a/tomlkit/items.py b/tomlkit/items.py index e84a40e4..c4dcb578 100644 --- a/tomlkit/items.py +++ b/tomlkit/items.py @@ -2088,13 +2088,24 @@ def append(self, key: Key | str | None, _item: Any) -> InlineTable: self._validate_child(_item) + needs_indent = False if not isinstance(_item, (Whitespace, Comment)): - if not _item.trivia.indent and len(self._value) > 0 and not self._new: - _item.trivia.indent = " " + needs_indent = ( + not _item.trivia.indent and len(self._value) > 0 and not self._new + ) if _item.trivia.comment: _item.trivia.comment = "" - self._value.append(key, _item) + if isinstance(key, Key) and key.is_multi(): + # The value of a dotted key is rendered after the key (``a.b = 1``), + # so the space goes on the table that the key adds as a new entry + self._value.append(key, _item) + if needs_indent: + self._value.body[-1][1].trivia.indent = " " + else: + if needs_indent: + _item.trivia.indent = " " + self._value.append(key, _item) if isinstance(key, Key): key = key.key