Skip to content

Commit 6e9dbf6

Browse files
committed
Make sure that frozenlists are pickle-able
Add contributors / changes updates Add missing file
1 parent e300cd8 commit 6e9dbf6

File tree

7 files changed

+76
-3
lines changed

7 files changed

+76
-3
lines changed

CHANGES/650.contrib

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
1-
The pull request template to be consistent with :ref:`the contributing guidelines
2-
<adding change notes with your prs>`, particularly the list of categories
1+
The pull request template to be consistent with :ref:`the contributing guidelines <adding change notes with your prs>`, particularly the list of categories
32
-- by :user:`musicinmybrain`.

CHANGES/718.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix bug where FrozenList could not be pickled/unpickled.
2+
-- by :user:`csm10495`.

CONTRIBUTORS.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
- Contributors -
22
----------------
33
Andrew Svetlov
4+
Charles Machalow
45
Edgar Ramírez-Mondragón
56
Marcin Konowalczyk
67
Martijn Pieters

docs/spelling_wordlist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ unicode
302302
unittest
303303
Unittest
304304
unix
305+
unpickled
305306
unsets
306307
unstripped
307308
upstr

frozenlist/__init__.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1+
import copy
12
import os
23
import types
34
from collections.abc import MutableSequence
45
from functools import total_ordering
6+
from typing import Any
57

68
__version__ = "1.8.1.dev0"
79

8-
__all__ = ("FrozenList", "PyFrozenList") # type: Tuple[str, ...]
10+
__all__ = (
11+
"FrozenList",
12+
"PyFrozenList",
13+
"_reconstruct_pyfrozenlist",
14+
) # type: Tuple[str, ...]
915

1016

1117
NO_EXTENSIONS = bool(os.environ.get("FROZENLIST_NO_EXTENSIONS")) # type: bool
@@ -73,7 +79,43 @@ def __hash__(self):
7379
else:
7480
raise RuntimeError("Cannot hash unfrozen list.")
7581

82+
def __deepcopy__(self, memo: dict[int, Any]):
83+
obj_id = id(self)
7684

85+
# Return existing copy if already processed (circular reference)
86+
if obj_id in memo:
87+
return memo[obj_id]
88+
89+
# Create new instance and register immediately
90+
new_list = self.__class__([])
91+
memo[obj_id] = new_list
92+
93+
# Deep copy items
94+
new_list._items[:] = [copy.deepcopy(item, memo) for item in self._items]
95+
96+
# Preserve frozen state
97+
if self._frozen:
98+
new_list.freeze()
99+
100+
return new_list
101+
102+
def __reduce__(self):
103+
return (
104+
_reconstruct_pyfrozenlist,
105+
(self._items, self._frozen),
106+
)
107+
108+
109+
def _reconstruct_pyfrozenlist(items, frozen):
110+
"""Helper function to reconstruct the pure Python FrozenList during unpickling.
111+
This function is needed since otherwise the class renaming confuses pickle."""
112+
fl = PyFrozenList(items)
113+
if frozen:
114+
fl.freeze()
115+
return fl
116+
117+
118+
# Store a reference to the pure Python implementation before it's potentially replaced
77119
PyFrozenList = FrozenList
78120

79121

frozenlist/_frozenlist.pyx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,5 +144,15 @@ cdef class FrozenList:
144144

145145
return new_list
146146

147+
def __reduce__(self):
148+
return (
149+
self.__class__,
150+
(self._items,),
151+
{"_frozen": self._frozen.load()},
152+
)
153+
154+
def __setstate__(self, state):
155+
self._frozen.store(state["_frozen"])
156+
147157

148158
MutableSequence.register(FrozenList)

tests/test_frozenlist.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# FIXME:
22
# mypy: disable-error-code="misc"
33

4+
import pickle
45
from collections.abc import MutableSequence
56
from copy import deepcopy
67

@@ -372,6 +373,23 @@ def test_deepcopy_multiple_references(self) -> None:
372373
assert len(copied[1]) == 3 # Should see the change
373374
assert len(shared) == 2 # Original unchanged
374375

376+
@pytest.mark.parametrize("freeze", [True, False])
377+
def test_picklability(self, freeze: bool) -> None:
378+
# Test that the list can be pickled and unpickled successfully
379+
orig = self.FrozenList([1, 2, 3])
380+
if freeze:
381+
orig.freeze()
382+
383+
assert orig.frozen == freeze
384+
385+
pickled = pickle.dumps(orig)
386+
unpickled = pickle.loads(pickled)
387+
assert unpickled == orig
388+
assert unpickled is not orig
389+
assert list(unpickled) == list(orig)
390+
391+
assert unpickled.frozen == freeze
392+
375393

376394
class TestFrozenList(FrozenListMixin):
377395
FrozenList = FrozenList # type: ignore[assignment] # FIXME

0 commit comments

Comments
 (0)