From 22d94db8bee027319cc56592d93f7755f4b3a267 Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Sat, 20 Jun 2026 15:02:47 +0200 Subject: [PATCH] fix: preserve existing keys on dotted list writes BoxList.__setitem__ unconditionally replaced the element at the target index with a fresh empty container before recursing, so a second dotted write into an existing element (a[0].y after a[0].x) discarded the keys already stored there. Only replace the element when it is not already the right container type, matching the guard in Box.__setitem__. --- box/box_list.py | 10 ++++++++-- test/test_box_list.py | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/box/box_list.py b/box/box_list.py index 72609ff..819c205 100644 --- a/box/box_list.py +++ b/box/box_list.py @@ -106,9 +106,15 @@ def __setitem__(self, key, value): return super().__setitem__(pos, value) children = key[len(list_pos.group()) :].lstrip(".") if self.box_options.get("default_box"): + # Only replace the element at ``pos`` with a fresh container when it is + # not already the right one; otherwise a second dotted write into an + # existing element (e.g. ``a[0].y`` after ``a[0].x``) would discard the + # data already stored there. Mirrors the guard in Box.__setitem__. + current = super().__getitem__(pos) if children[0] == "[": - super().__setitem__(pos, box.BoxList(**self.box_options)) - else: + if not isinstance(current, box.BoxList): + super().__setitem__(pos, box.BoxList(**self.box_options)) + elif not isinstance(current, box.Box): super().__setitem__(pos, self.box_options.get("box_class")(**self.box_options)) return super().__getitem__(pos).__setitem__(children, value) super().__setitem__(key, value) diff --git a/test/test_box_list.py b/test/test_box_list.py index 9cd90e0..0fd7de8 100644 --- a/test/test_box_list.py +++ b/test/test_box_list.py @@ -241,6 +241,24 @@ def test_box_list_default_dots(self): box_3["a.b[0]"] = 42 assert box_3.a.b[0] == 42 + # A second dotted write into an existing list element must not discard + # the keys already written there (regression). + box_4 = Box(default_box=True, box_dots=True) + box_4["a[0].x"] = 1 + box_4["a[0].y"] = 2 + assert box_4.a[0].to_dict() == {"x": 1, "y": 2} + + box_5 = Box(default_box=True, box_dots=True) + box_5["a[0][0].x"] = 1 + box_5["a[0][1].y"] = 2 + assert box_5.a[0].to_list() == [{"x": 1}, {"y": 2}] + + # A scalar already at the position is still overwritten. + box_6 = Box(default_box=True, box_dots=True) + box_6["a[0]"] = 5 + box_6["a[0].x"] = 1 + assert box_6.a[0].to_dict() == {"x": 1} + def test_box_config_propagate(self): structure = Box(a=[Box(default_box=False)], default_box=True, box_inherent_settings=True) assert structure._box_config["default_box"] is True