From e7c2989bb49e1e5e5c3882ebdd30df30f52d3160 Mon Sep 17 00:00:00 2001 From: apoorvdarshan Date: Sun, 5 Jul 2026 00:20:23 +0530 Subject: [PATCH] Fix serialization of frozen_box with nested lists frozen_box stores lists as tuples of Boxes for immutability, but Box.to_dict() only recursed into Box and BoxList values -- not tuples -- so the nested Boxes were left unconverted. to_yaml() then failed with RepresenterError since ruamel can't represent a Box. Recurse into tuples in to_dict(), converting nested Box/BoxList members back to native types while preserving the tuple. Fixes #272. --- CHANGES.rst | 5 +++++ box/box.py | 21 +++++++++++++++++++++ test/test_box.py | 16 ++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c4a347d..93844a0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,11 @@ Changelog ========= +Unreleased +---------- + +* Fixing #272 ``frozen_box`` with nested lists could not be serialized to YAML (thanks to apoorvdarshan) + Version 7.4.1 ------------- diff --git a/box/box.py b/box/box.py index 8252643..d982d99 100644 --- a/box/box.py +++ b/box/box.py @@ -97,6 +97,25 @@ def _recursive_tuples(iterable, box_class, recreate_tuples=False, **kwargs): return tuple(out_list) +def _to_native_tuple(iterable): + """Convert a tuple's Box/BoxList members back to native types. + + ``frozen_box`` stores lists as tuples of Boxes, so ``to_dict`` needs to + turn those nested Boxes back into plain dicts to remain serializable. + """ + out_list = [] + for i in iterable: + if isinstance(i, Box): + out_list.append(i.to_dict()) + elif isinstance(i, box.BoxList): + out_list.append(i.to_list()) + elif isinstance(i, tuple): + out_list.append(_to_native_tuple(i)) + else: + out_list.append(i) + return tuple(out_list) + + def _parse_box_dots(bx, item, setting=False): for idx, char in enumerate(item): if char == "[": @@ -815,6 +834,8 @@ def to_dict(self) -> dict: out_dict[k] = v.to_dict() elif isinstance(v, box.BoxList): out_dict[k] = v.to_list() + elif isinstance(v, tuple): + out_dict[k] = _to_native_tuple(v) return out_dict def update(self, *args, **kwargs): diff --git a/test/test_box.py b/test/test_box.py index e5c56e0..56dcfe2 100644 --- a/test/test_box.py +++ b/test/test_box.py @@ -435,6 +435,22 @@ def test_frozen(self): assert hash(bx3) + def test_frozen_box_with_lists_is_serializable(self): + # frozen_box stores lists as tuples of Boxes; to_dict must convert the + # nested Boxes back to native dicts so serialization works. See #272. + bx = Box({"a": [{"b": 123}, {"b": 222}], "c": [[{"d": 2}], [3, 4]]}, frozen_box=True) + + native = bx.to_dict() + assert isinstance(native["a"], tuple) + assert native["a"][0] == {"b": 123} + assert not isinstance(native["a"][0], Box) + assert not isinstance(native["c"][0][0], Box) + + # These used to raise RepresenterError / serialization errors. + yaml = YAML(typ="safe") + assert yaml.load(bx.to_yaml()) == {"a": [{"b": 123}, {"b": 222}], "c": [[{"d": 2}], [3, 4]]} + assert json.loads(bx.to_json()) == {"a": [{"b": 123}, {"b": 222}], "c": [[{"d": 2}], [3, 4]]} + def test_hashing(self): bx1 = Box(t=3, g=4, frozen_box=True) bx2 = Box(g=4, t=3, frozen_box=True)