Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -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
-------------

Expand Down
21 changes: 21 additions & 0 deletions box/box.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 == "[":
Expand Down Expand Up @@ -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):
Expand Down
16 changes: 16 additions & 0 deletions test/test_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down