From 8ba942fcbc102b83a3948dcf5f348aa874babd01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afonso=20Janu=C3=A1rio?= Date: Fri, 25 Sep 2026 23:28:59 +0100 Subject: [PATCH] Fix item() reordering dict keys inside inline tables The list-of-dicts branch of item() builds each dict's sort key as (isinstance(i[1], dict), i[0] if _sort_keys else 1). The closing parenthesis only wraps i[0], so the dict-valued check stays active no matter what _sort_keys is, quietly moving dict-valued keys to the end even when the caller asked to keep their original order. For a concrete [table] or array of tables this went unnoticed because Container.append already repositions scalar keys ahead of any table header on insertion, independent of the order item() hands it keys in. Inline tables get no such correction, so the reordering is directly visible there, e.g. item([1, {"a": {"x": 1}, "b": 2}]) rendered {b = 2, a = {x = 1}} instead of preserving the original a, b order. Move the parenthesis to match the sibling top-level dict branch a few lines above, which already guards the whole tuple correctly. Verified against the full existing test suite (all 378 tests still pass) plus a new regression test covering both the default order-preserving case and sort_keys=True still forcing dict-valued keys last. Resolves #546. --- CHANGELOG.md | 1 + tests/test_items.py | 17 +++++++++++++++++ tomlkit/items.py | 2 +- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3737b80e..d38ef112 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ - Preserve trailing whitespace when replacing a super table, including assigning it onto itself. ([#534](https://github.com/python-poetry/tomlkit/pull/534)) - Fix `str()` and `repr()` of out-of-order table proxies to show their merged values. ([#536](https://github.com/python-poetry/tomlkit/pull/536)) - Reject decimal integer literals that exceed Python's integer-string conversion limit instead of coercing them to infinity. ([#538](https://github.com/python-poetry/tomlkit/pull/538)) +- Fix `item()` silently reordering a dict's keys (dict-valued keys forced last) when converting a list of dicts into an inline table, even with the default `sort_keys=False`. A misplaced parenthesis in the sort key left the reordering active regardless of `sort_keys`; concrete `[table]`/array-of-tables entries were unaffected since `Container.append` already repositions scalars ahead of table headers on its own. ([#546](https://github.com/python-poetry/tomlkit/issues/546)) ## [0.15.0] - 2026-05-10 diff --git a/tests/test_items.py b/tests/test_items.py index d7c06fbf..d0194d3b 100644 --- a/tests/test_items.py +++ b/tests/test_items.py @@ -654,6 +654,23 @@ def test_item_array_of_dicts_converted_to_aot() -> None: ) +def test_item_list_of_dicts_as_inline_table_keeps_key_order() -> None: + # A dict inside a list that also holds non-dict values is rendered as an + # InlineTable, not a [table]/AoT. Unlike a real table header, an inline + # table has no ordering constraint that would justify moving dict-valued + # keys to the end, so with the default sort_keys=False the original key + # order must survive even though "a"'s value is itself a dict. + a = item([1, {"a": {"x": 1}, "b": 2}]) + + assert a.as_string() == "[1, {a = {x = 1}, b = 2}]" + + # sort_keys=True should still sort, dict-valued keys last, even though + # "a" comes first in the input. + sorted_a = item([1, {"a": {"x": 1}, "b": 2}], _sort_keys=True) + + assert sorted_a.as_string() == "[1, {b = 2, a = {x = 1}}]" + + def test_add_float_to_int() -> None: content = "[table]\nmy_int = 2043" doc = parse(content) diff --git a/tomlkit/items.py b/tomlkit/items.py index c950e5d0..83ff0df5 100644 --- a/tomlkit/items.py +++ b/tomlkit/items.py @@ -167,7 +167,7 @@ def item(value: Any, _parent: Item | None = None, _sort_keys: bool = False) -> I for k, _v in sorted( v.items(), - key=lambda i: (isinstance(i[1], dict), i[0] if _sort_keys else 1), + key=lambda i: (isinstance(i[1], dict), i[0]) if _sort_keys else 1, ): i = item(_v, _parent=table, _sort_keys=_sort_keys) if isinstance(table, InlineTable):