diff --git a/AUTHORS.rst b/AUTHORS.rst index 6b44851..4c06d6e 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -37,6 +37,7 @@ Code contributions: - Jesper Schlegel (jesperschlegel) - J vanBemmel (jbemmel) - m-janicki +- Abhinav Maurya (Fire162) Suggestions and bug reporting: diff --git a/CHANGES.rst b/CHANGES.rst index c4a347d..130a60b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,8 @@ Changelog ========= +* Fixing #344 Frozen BoxList instances cannot be pickled (thanks to Abhinav Maurya) + Version 7.4.1 ------------- diff --git a/box/box_list.py b/box/box_list.py index 72609ff..2e3c327 100644 --- a/box/box_list.py +++ b/box/box_list.py @@ -35,6 +35,10 @@ _list_pos_re = re.compile(r"\[(\d+)\]") +def _frozen_box_list_method(*args, **kwargs): + raise BoxError("BoxList is frozen") + + class BoxList(list): """ Drop in replacement of list, that converts added objects to Box or BoxList @@ -58,12 +62,8 @@ def __init__(self, iterable: Iterable | None = None, box_class: type[box.Box] = self.append(x) self.box_org_ref = None if box_options.get("frozen_box"): - - def frozen(*args, **kwargs): - raise BoxError("BoxList is frozen") - for method in ["append", "extend", "insert", "pop", "remove", "reverse", "sort"]: - self.__setattr__(method, frozen) + self.__setattr__(method, _frozen_box_list_method) def __getitem__(self, item): if self.box_options.get("box_dots") and isinstance(item, str) and item.startswith("["): diff --git a/test/test_box_list.py b/test/test_box_list.py index 9cd90e0..e0bd071 100644 --- a/test/test_box_list.py +++ b/test/test_box_list.py @@ -4,6 +4,7 @@ import json import os +import pickle import shutil import sys import platform @@ -74,6 +75,54 @@ def test_frozen_list(self): bl2[1] = 4 assert bl2[1] == 4 + def test_frozen_list_pickle(self): + for protocol in range(pickle.HIGHEST_PROTOCOL + 1): + bl = BoxList([5, 4, 3], frozen_box=True) + dumped = pickle.dumps(bl, protocol=protocol) + loaded = pickle.loads(dumped) + assert loaded == [5, 4, 3] + assert loaded.box_options.get("frozen_box") is True + with pytest.raises(BoxError): + loaded.append(2) + with pytest.raises(BoxError): + loaded.extend([1]) + with pytest.raises(BoxError): + loaded.pop(0) + with pytest.raises(BoxError): + loaded.remove(5) + with pytest.raises(BoxError): + loaded.insert(0, 9) + with pytest.raises(BoxError): + loaded.reverse() + with pytest.raises(BoxError): + loaded.sort() + with pytest.raises(BoxError): + del loaded[0] + with pytest.raises(BoxError): + loaded[0] = 99 + + empty_bl = BoxList([], frozen_box=True) + empty_dumped = pickle.dumps(empty_bl, protocol=protocol) + empty_loaded = pickle.loads(empty_dumped) + assert empty_loaded == [] + assert empty_loaded.box_options.get("frozen_box") is True + with pytest.raises(BoxError): + empty_loaded.append(1) + + def test_box_list_pickle(self): + for protocol in range(pickle.HIGHEST_PROTOCOL + 1): + bl = BoxList([1, 2, 3]) + dumped = pickle.dumps(bl, protocol=protocol) + loaded = pickle.loads(dumped) + assert loaded == [1, 2, 3] + assert not loaded.box_options.get("frozen_box") + loaded.append(4) + assert loaded == [1, 2, 3, 4] + loaded[0] = 9 + assert loaded[0] == 9 + del loaded[0] + assert loaded == [2, 3, 4] + def test_box_list_to_json(self): bl = BoxList([{"item": 1, "CamelBad": 2}]) assert json.loads(bl.to_json())[0]["item"] == 1