From 83031a8b837bd99e7ec012efb20da25553fe713a Mon Sep 17 00:00:00 2001 From: chuenchen309 <48723787+chuenchen309@users.noreply.github.com> Date: Mon, 20 Jul 2026 06:14:32 +0800 Subject: [PATCH] Preserve box options in BoxList.__deepcopy__ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `BoxList.__deepcopy__` built the copy as `self.__class__()` with no `box_options`, so `copy.deepcopy()` silently dropped every option (`frozen_box`, `box_dots`, `default_box`, `box_intact_types`, …). A frozen BoxList became mutable (and unhashable) after a deepcopy. `BoxList.__copy__` and `Box.__deepcopy__` both preserve options; `BoxList.__deepcopy__` was the outlier. Mirror `Box.__deepcopy__`: build unfrozen, register in memo before recursing (keeps cycle-safety), populate, then re-freeze via `__init__`. --- box/box_list.py | 5 ++++- test/test_box_list.py | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/box/box_list.py b/box/box_list.py index 72609ff..6dd6509 100644 --- a/box/box_list.py +++ b/box/box_list.py @@ -169,11 +169,14 @@ def __copy__(self): return self.__class__((x for x in self), **self.box_options) def __deepcopy__(self, memo=None): - out = self.__class__() + frozen = self.box_options.get("frozen_box") + out = self.__class__(**{**self.box_options, "frozen_box": False}) memo = memo or {} memo[id(self)] = out for k in self: out.append(copy.deepcopy(k, memo=memo)) + if frozen: + out.__init__(**self.box_options) return out def __hash__(self) -> int: # type: ignore[override] diff --git a/test/test_box_list.py b/test/test_box_list.py index 9cd90e0..71067c3 100644 --- a/test/test_box_list.py +++ b/test/test_box_list.py @@ -74,6 +74,28 @@ def test_frozen_list(self): bl2[1] = 4 assert bl2[1] == 4 + def test_deepcopy_preserves_box_options(self): + import copy + + # deepcopy must preserve box_options, matching __copy__ and Box.__deepcopy__ + for options in ( + {"frozen_box": True}, + {"box_dots": True}, + {"default_box": True}, + {"box_intact_types": (dict,)}, + ): + bl = BoxList([{"a": 1}], **options) + key = list(options)[0] + assert copy.copy(bl).box_options.get(key) == options[key] + assert copy.deepcopy(bl).box_options.get(key) == options[key] + + # a frozen BoxList must stay frozen (immutable + hashable) after deepcopy + frozen = BoxList([5, 4, 3], frozen_box=True) + clone = copy.deepcopy(frozen) + with pytest.raises(BoxError): + clone.append(1) + assert hash(clone) == hash(frozen) + def test_box_list_to_json(self): bl = BoxList([{"item": 1, "CamelBad": 2}]) assert json.loads(bl.to_json())[0]["item"] == 1