From 7b9260cd678db201c45ac74d38eaf0effd557ee8 Mon Sep 17 00:00:00 2001 From: Sskift <163287349+Sskift@users.noreply.github.com> Date: Tue, 3 Nov 2026 01:40:49 +0800 Subject: [PATCH 1/2] Preserve split arrays of tables in document values --- tests/test_toml_document.py | 21 +++++++++++++++++++++ tomlkit/container.py | 14 ++++++-------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/tests/test_toml_document.py b/tests/test_toml_document.py index 84b114b9..de8da1f4 100644 --- a/tests/test_toml_document.py +++ b/tests/test_toml_document.py @@ -689,6 +689,27 @@ def test_set_value_on_out_of_order_table_with_empty_concrete_part() -> None: assert parse(doc.as_string()).unwrap() == {"x": {"y": {"z": {"w": {}}}, "c": 3}} +@pytest.mark.parametrize("path", ["hooks.Stop", "hooks.events.Stop"]) +@pytest.mark.parametrize("count", [2, 3]) +def test_document_value_preserves_split_arrays_of_tables(path: str, count: int) -> None: + content = "".join( + f"[[{path}]]\nvalue = {i}\n[unrelated{i}]\nvalue = {i}\n" for i in range(count) + ) + doc = parse(content) + expected: dict[str, Any] = {} + parent = expected + *parents, key = path.split(".") + for name in parents: + parent = parent.setdefault(name, {}) + parent[key] = [{"value": i} for i in range(count)] + expected.update({f"unrelated{i}": {"value": i} for i in range(count)}) + + assert doc.unwrap() == expected + assert doc.value == expected + assert doc == expected + assert doc.as_string() == content + + def test_out_of_order_table_merges_aot_fragments() -> None: # https://github.com/python-poetry/tomlkit/issues/505 content = """\ diff --git a/tomlkit/container.py b/tomlkit/container.py index 9265c740..5ed124de 100644 --- a/tomlkit/container.py +++ b/tomlkit/container.py @@ -13,7 +13,6 @@ from tomlkit._compat import decode from tomlkit._types import _CustomDict -from tomlkit._utils import merge_dicts from tomlkit.exceptions import KeyAlreadyPresent from tomlkit.exceptions import NonExistentKey from tomlkit.exceptions import TOMLKitError @@ -157,19 +156,18 @@ def value(self) -> dict[str, Any]: """The wrapped dict value""" d: dict[str, Any] = {} for k, v in self._body: - if k is None: + if k is None or k.key in d: continue - key_str = k.key - val: Any = v.value + # Resolve all out-of-order parts together so split arrays of tables + # retain every element, just as item access and unwrap() do. + resolved = self.item(k) if isinstance(self._map[k], tuple) else v + val: Any = resolved.value if isinstance(val, Container): val = val.value - if key_str in d: - merge_dicts(d[key_str], val) - else: - d[key_str] = val + d[k.key] = val return d From 83092bf5935b1f78a80a0a74bf0eebb4d30e4ddc Mon Sep 17 00:00:00 2001 From: Sskift <163287349+Sskift@users.noreply.github.com> Date: Tue, 3 Nov 2026 01:45:23 +0800 Subject: [PATCH 2/2] Merge array-of-table values without revalidating scalar tables --- tests/test_toml_document.py | 9 +++++++++ tomlkit/_utils.py | 4 ++++ tomlkit/container.py | 14 ++++++++------ 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/tests/test_toml_document.py b/tests/test_toml_document.py index de8da1f4..18a12be2 100644 --- a/tests/test_toml_document.py +++ b/tests/test_toml_document.py @@ -710,6 +710,15 @@ def test_document_value_preserves_split_arrays_of_tables(path: str, count: int) assert doc.as_string() == content +def test_document_value_preserves_wrapped_array_items() -> None: + doc = parse("[a]\nvalues = [1, 2]\n[other]\nx = 3\n[a.child]\nx = 4\n") + original = doc["a"]["values"] + values = doc.value + assert values["a"]["values"] is original + assert values["a"]["values"] == [1, 2] + assert doc.value["a"]["values"] == [1, 2] + + def test_out_of_order_table_merges_aot_fragments() -> None: # https://github.com/python-poetry/tomlkit/issues/505 content = """\ diff --git a/tomlkit/_utils.py b/tomlkit/_utils.py index c79d8790..8721f030 100644 --- a/tomlkit/_utils.py +++ b/tomlkit/_utils.py @@ -160,5 +160,9 @@ def merge_dicts(d1: dict[str, Any], d2: dict[str, Any]) -> None: for k, v in d2.items(): if k in d1 and isinstance(d1[k], dict) and isinstance(v, Mapping): merge_dicts(d1[k], dict(v)) + elif k in d1 and type(d1[k]) is list and type(v) is list: + # AoT.value creates plain lists for each fragment. Arrays retain + # their Array wrapper and must keep the usual replacement behavior. + d1[k].extend(v) else: d1[k] = d2[k] diff --git a/tomlkit/container.py b/tomlkit/container.py index 5ed124de..9265c740 100644 --- a/tomlkit/container.py +++ b/tomlkit/container.py @@ -13,6 +13,7 @@ from tomlkit._compat import decode from tomlkit._types import _CustomDict +from tomlkit._utils import merge_dicts from tomlkit.exceptions import KeyAlreadyPresent from tomlkit.exceptions import NonExistentKey from tomlkit.exceptions import TOMLKitError @@ -156,18 +157,19 @@ def value(self) -> dict[str, Any]: """The wrapped dict value""" d: dict[str, Any] = {} for k, v in self._body: - if k is None or k.key in d: + if k is None: continue - # Resolve all out-of-order parts together so split arrays of tables - # retain every element, just as item access and unwrap() do. - resolved = self.item(k) if isinstance(self._map[k], tuple) else v - val: Any = resolved.value + key_str = k.key + val: Any = v.value if isinstance(val, Container): val = val.value - d[k.key] = val + if key_str in d: + merge_dicts(d[key_str], val) + else: + d[key_str] = val return d