From ae55098c2dd03da9515b110a8af4c7dd1a64e6b7 Mon Sep 17 00:00:00 2001 From: youdie006 Date: Wed, 7 Oct 2026 10:35:51 +0900 Subject: [PATCH] Keep out-of-order table indices valid after a table is removed OutOfOrderTableProxy maps each key to positions in self._tables, but _remove_table took a table out of that list without shifting the later positions, so a second delete through the same proxy indexed the wrong table or ran past the end: tool = doc["tool"]; del tool["black"]; del tool["ruff"] # IndexError Remove the table by identity and shift the later positions down. --- tests/test_toml_document.py | 30 ++++++++++++++++++++++++++++++ tomlkit/container.py | 8 ++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/tests/test_toml_document.py b/tests/test_toml_document.py index 116de352..3b4edf5c 100644 --- a/tests/test_toml_document.py +++ b/tests/test_toml_document.py @@ -1592,6 +1592,36 @@ def test_delete_key_from_out_of_order_table() -> None: ) +def test_delete_keys_through_the_same_out_of_order_table() -> None: + content = """\ +[tool.a] +x = 1 +[project] +name = "p" +[tool.b] +y = 2 +[other] +[tool.c] +z = 3 +""" + doc = parse(content) + tool = doc["tool"] + del tool["a"] + del tool["b"] + assert ( + doc.as_string() + == """\ +[project] +name = "p" +[other] +[tool.c] +z = 3 +""" + ) + tool["c"] = {"w": 4} + assert doc["tool"] == {"c": {"w": 4}} + + def test_parse_aot_without_ending_newline() -> None: content = '''\ [[products]] diff --git a/tomlkit/container.py b/tomlkit/container.py index 2f0fc034..c930b3c2 100644 --- a/tomlkit/container.py +++ b/tomlkit/container.py @@ -1227,7 +1227,11 @@ def _is_table_or_aot(it: Any) -> bool: def _remove_table(self, table: Table) -> None: """Remove table from the parent container""" - self._tables.remove(table) + pos = next(i for i, t in enumerate(self._tables) if t is table) + del self._tables[pos] + # Later tables shift down by one; keep the key -> table index map valid. + for indices in self._tables_map.values(): + indices[:] = [i - 1 if i > pos else i for i in indices if i != pos] for idx, body_item in enumerate(self._container._body): if body_item[1] is table: self._container._remove_at(idx) @@ -1238,7 +1242,7 @@ def __delitem__(self, key: Key | str) -> None: if _key not in self._tables_map: raise NonExistentKey(key) - for i in reversed(self._tables_map[_key]): + for i in reversed(list(self._tables_map[_key])): table = self._tables[i] del table[key] if not table and len(self._tables) > 1: