diff --git a/CHANGELOG.md b/CHANGELOG.md index faad379..9216ef0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ - Fix invalid serialization with a duplicated comma when appending or inserting into a comma-first formatted array. ([#499](https://github.com/python-poetry/tomlkit/pull/499)) - Fix `ParseError` when a sub-table extends the last element of an array of tables after an unrelated table. ([#261](https://github.com/python-poetry/tomlkit/issues/261)) - Fix unparseable serialization when adding a key to a dotted-key table inside an inline table. ([#500](https://github.com/python-poetry/tomlkit/pull/500)) +- Fix `item()` reordering the keys of an inline table (dict-valued keys were forced last) under the default `sort_keys=False`. ([#546](https://github.com/python-poetry/tomlkit/issues/546)) - Fix a table replaced by a plain value being serialized inside the preceding table's body when other tables follow; the value now moves before the first table like other root-level values. ([#504](https://github.com/python-poetry/tomlkit/issues/504)) - Fix assigning a table over a dotted key (e.g. `doc["a"] = {...}` where `a` came from `a.b = ...`): the dotted prefix was duplicated onto the new `[a]` header, and the header then swallowed any sibling that follows it on round-trip. The replacement now renders as a plain table and, when needed, moves before the inline entries (values and dotted keys) it would otherwise capture. ([#513](https://github.com/python-poetry/tomlkit/issues/513), [#524](https://github.com/python-poetry/tomlkit/issues/524)) - Restore `dumps()` rendering mapping-like wrappers around a parsed document (e.g. `dotty_dict`'s `Dotty`) through their delegated `as_string`, preserving the original table order and layout instead of re-encoding through a plain dict — a 0.15.0 regression. ([#482](https://github.com/python-poetry/tomlkit/issues/482)) diff --git a/tests/test_api.py b/tests/test_api.py index b49c90a..b114cfd 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -347,6 +347,28 @@ def test_item_mixed_aray() -> None: assert dumps({"x": {"y": example}}).strip() == "[x]\ny = " + expected +def test_item_inline_table_preserves_key_order() -> None: + # Inline tables cannot capture following keys, so their key order must be + # preserved under the default sort_keys=False, even when a value is itself + # a dict/table (#546). + assert ( + tomlkit.item([[{"a": {"x": 1}, "b": 2}]]).as_string().strip() + == "[[{a = {x = 1}, b = 2}]]" + ) + + # Arrays of tables still render dict-valued keys last to avoid them + # capturing the following keys. + assert ( + tomlkit.item([{"a": {"x": 1}, "b": 2}]).as_string() == "b = 2\n\n[a]\nx = 1\n" + ) + + # Sorting is still honoured when explicitly requested. + assert ( + tomlkit.item([[{"b": 2, "a": {"x": 1}}]], _sort_keys=True).as_string().strip() + == "[[{b = 2, a = {x = 1}}]]" + ) + + def test_build_super_table() -> None: doc = tomlkit.document() table = tomlkit.table(True) diff --git a/tomlkit/items.py b/tomlkit/items.py index e9fdaff..1b0c29b 100644 --- a/tomlkit/items.py +++ b/tomlkit/items.py @@ -161,14 +161,22 @@ def item(value: Any, _parent: Item | None = None, _sort_keys: bool = False) -> I a = Array([], Trivia()) table_constructor = InlineTable + if table_constructor is Table: + # AoT tables render as ``[[table]]`` headers, so dict-valued keys + # must come last to avoid them capturing the following keys. + def _sort_key(i: tuple[Any, Any]) -> Any: + return (isinstance(i[1], dict), i[0] if _sort_keys else 1) + else: + # Inline tables cannot capture, so preserve insertion order unless + # explicitly sorting (matching the dict branch above). See #546. + def _sort_key(i: tuple[Any, Any]) -> Any: + return (isinstance(i[1], dict), i[0]) if _sort_keys else 1 + for v in value: if isinstance(v, dict): table = table_constructor(Container(), Trivia(), True) - for k, _v in sorted( - v.items(), - key=lambda i: (isinstance(i[1], dict), i[0] if _sort_keys else 1), - ): + for k, _v in sorted(v.items(), key=_sort_key): i = item(_v, _parent=table, _sort_keys=_sort_keys) if isinstance(table, InlineTable): i.trivia.trail = ""