Skip to content

Commit ac3b04e

Browse files
Keep unwrap() key order after a value is replaced (#591)
* Keep unwrap() key order after a value is replaced _replace_at() removes the key from _map and re-inserts it, which moves it to the end of the dict's insertion order while _body keeps the original slot. dumps() and keys() walk _body and stay correct; unwrap() walks _map and does not: doc = parse("a = 1\nb = 2\nc = 3\n") doc["b"] = 9 dumps(doc) # 'a = 1\nb = 9\nc = 3\n' list(doc.keys()) # ['a', 'b', 'c'] list(doc.unwrap()) # ['a', 'c', 'b'] unwrap() moved to _map in #521 on the stated assumption that it "iterates in the same insertion order as the old self.items()", which a replacement breaks. Order comes from the body index instead, so the _map fast path is kept and dumps() stays the reference. Replacements that genuinely move an element still move: a bare key promoted to a table has to be emitted after the inline entries, and unwrap() follows the body there too. Both cases are covered. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 49d6efa commit ac3b04e

2 files changed

Lines changed: 28 additions & 3 deletions

File tree

‎tests/test_toml_document.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,27 @@ def test_toml_document_without_super_tables() -> None:
160160
assert "tool" in d
161161

162162

163+
def test_unwrap_keeps_key_order_after_replacing_a_value() -> None:
164+
# unwrap() reads _map, which re-inserts a replaced key and so moves it
165+
# last; the order has to follow the body, like dumps() and keys() do.
166+
doc = parse("a = 1\nb = 2\nc = 3\n")
167+
doc["b"] = 9
168+
169+
assert list(doc.unwrap()) == ["a", "b", "c"]
170+
assert list(doc.keys()) == ["a", "b", "c"]
171+
assert tomlkit.dumps(doc) == "a = 1\nb = 9\nc = 3\n"
172+
173+
174+
def test_unwrap_follows_the_body_when_a_value_becomes_a_table() -> None:
175+
# here moving the key is correct: a bare key promoted to [table] has to be
176+
# emitted after the inline entries, and unwrap() should agree with dumps()
177+
doc = parse("a = 1\nb = 2\n")
178+
doc["a"] = {"x": 1}
179+
180+
assert tomlkit.dumps(doc) == "b = 2\n\n[a]\nx = 1\n"
181+
assert list(doc.unwrap()) == ["b", "a"]
182+
183+
163184
def test_toml_document_unwrap() -> None:
164185
content = """[tool.poetry]
165186
name = "foo"

‎tomlkit/container.py‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,13 @@ def unwrap(self) -> dict[str, Any]:
6767
# rebuilds a SingleKey from the bare string on every key only to throw
6868
# it away. Out-of-order keys (a tuple index) still go through
6969
# OutOfOrderTableProxy so their validation (and fragment merge) runs
70-
# exactly as before. _map iterates in the same insertion order as the
71-
# old self.items().
72-
for key, idx in self._map.items():
70+
# exactly as before. _map is keyed for lookup, not ordered: replacing a
71+
# value re-inserts its key and moves it last, so take the order from the
72+
# body index, which is what dumps() and items() follow.
73+
for key, idx in sorted(
74+
self._map.items(),
75+
key=lambda item: item[1][0] if isinstance(item[1], tuple) else item[1],
76+
):
7377
if isinstance(idx, tuple):
7478
value: Any = OutOfOrderTableProxy(self, idx)
7579
else:

0 commit comments

Comments
 (0)