serializable 1.1.2__tar.gz → 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {serializable-1.1.2 → serializable-1.2.0}/PKG-INFO +6 -2
- {serializable-1.1.2 → serializable-1.2.0}/README.md +5 -1
- {serializable-1.1.2 → serializable-1.2.0}/serializable/dataclass_serializable.py +5 -3
- {serializable-1.1.2 → serializable-1.2.0}/serializable/helpers.py +69 -40
- {serializable-1.1.2 → serializable-1.2.0}/serializable/serializable.py +14 -17
- {serializable-1.1.2 → serializable-1.2.0}/serializable/version.py +1 -1
- {serializable-1.1.2 → serializable-1.2.0}/serializable.egg-info/PKG-INFO +6 -2
- {serializable-1.1.2 → serializable-1.2.0}/serializable.egg-info/SOURCES.txt +1 -0
- {serializable-1.1.2 → serializable-1.2.0}/tests/test_default_to_dict_implementation.py +14 -0
- {serializable-1.1.2 → serializable-1.2.0}/tests/test_keyword_aliases.py +7 -0
- {serializable-1.1.2 → serializable-1.2.0}/tests/test_nesting.py +34 -1
- serializable-1.2.0/tests/test_pickle.py +102 -0
- {serializable-1.1.2 → serializable-1.2.0}/LICENSE +0 -0
- {serializable-1.1.2 → serializable-1.2.0}/pyproject.toml +0 -0
- {serializable-1.1.2 → serializable-1.2.0}/serializable/__init__.py +0 -0
- {serializable-1.1.2 → serializable-1.2.0}/serializable/primitive_types.py +0 -0
- {serializable-1.1.2 → serializable-1.2.0}/serializable.egg-info/dependency_links.txt +0 -0
- {serializable-1.1.2 → serializable-1.2.0}/serializable.egg-info/requires.txt +0 -0
- {serializable-1.1.2 → serializable-1.2.0}/serializable.egg-info/top_level.txt +0 -0
- {serializable-1.1.2 → serializable-1.2.0}/setup.cfg +0 -0
- {serializable-1.1.2 → serializable-1.2.0}/setup.py +0 -0
- {serializable-1.1.2 → serializable-1.2.0}/tests/test_class.py +0 -0
- {serializable-1.1.2 → serializable-1.2.0}/tests/test_dataclass_serializable.py +0 -0
- {serializable-1.1.2 → serializable-1.2.0}/tests/test_function.py +0 -0
- {serializable-1.1.2 → serializable-1.2.0}/tests/test_namedtuple.py +0 -0
- {serializable-1.1.2 → serializable-1.2.0}/tests/test_object.py +0 -0
- {serializable-1.1.2 → serializable-1.2.0}/tests/test_primitive_values.py +0 -0
- {serializable-1.1.2 → serializable-1.2.0}/tests/test_set.py +0 -0
- {serializable-1.1.2 → serializable-1.2.0}/tests/test_tuple.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: serializable
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Base class with serialization helpers for user-defined Python objects
|
|
5
5
|
Author-email: Alex Rubinsteyn <alex.rubinsteyn@unc.edu>
|
|
6
6
|
License: Apache License
|
|
@@ -274,9 +274,13 @@ helpers.
|
|
|
274
274
|
|
|
275
275
|
A derived class must either:
|
|
276
276
|
|
|
277
|
-
- have a member data matching the name of each argument to `__init__`
|
|
277
|
+
- have a member data matching the name of each positional argument to `__init__`
|
|
278
278
|
- provide a user-defined `to_dict()` method which returns a dictionary whose keys match the arguments to `__init__`
|
|
279
279
|
|
|
280
|
+
Keyword-only arguments to `__init__` (those after `*`) are left out of the default `to_dict()`, so they can be used for constructor options which aren't part of an object's serialized state.
|
|
281
|
+
|
|
282
|
+
Pickling goes through the same `to_dict()` / `from_dict()` pair, so pickles also survive renamed keywords (see below). Pickles written by versions before 1.2.0 can still be loaded.
|
|
283
|
+
|
|
280
284
|
If you change the keyword arguments to a class which derives from `Serializable` but would like to be able to deserialize older JSON representations then you can define a class-level dictionary called `_SERIALIZABLE_KEYWORD_ALIASES` which maps old keywords to new names (or `None` if a keyword was removed).
|
|
281
285
|
|
|
282
286
|
## Limitations
|
|
@@ -41,9 +41,13 @@ helpers.
|
|
|
41
41
|
|
|
42
42
|
A derived class must either:
|
|
43
43
|
|
|
44
|
-
- have a member data matching the name of each argument to `__init__`
|
|
44
|
+
- have a member data matching the name of each positional argument to `__init__`
|
|
45
45
|
- provide a user-defined `to_dict()` method which returns a dictionary whose keys match the arguments to `__init__`
|
|
46
46
|
|
|
47
|
+
Keyword-only arguments to `__init__` (those after `*`) are left out of the default `to_dict()`, so they can be used for constructor options which aren't part of an object's serialized state.
|
|
48
|
+
|
|
49
|
+
Pickling goes through the same `to_dict()` / `from_dict()` pair, so pickles also survive renamed keywords (see below). Pickles written by versions before 1.2.0 can still be loaded.
|
|
50
|
+
|
|
47
51
|
If you change the keyword arguments to a class which derives from `Serializable` but would like to be able to deserialize older JSON representations then you can define a class-level dictionary called `_SERIALIZABLE_KEYWORD_ALIASES` which maps old keywords to new names (or `None` if a keyword was removed).
|
|
48
52
|
|
|
49
53
|
## Limitations
|
|
@@ -43,7 +43,7 @@ from __future__ import annotations
|
|
|
43
43
|
from dataclasses import fields
|
|
44
44
|
from typing import Any, ClassVar
|
|
45
45
|
|
|
46
|
-
from .helpers import from_json,
|
|
46
|
+
from .helpers import from_json, to_json
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
class DataclassSerializable:
|
|
@@ -89,5 +89,7 @@ class DataclassSerializable:
|
|
|
89
89
|
def __reduce__(self):
|
|
90
90
|
"""Pickle via the same to_dict / from_dict path used for JSON so
|
|
91
91
|
pickled objects round-trip even when field order or internal
|
|
92
|
-
representation changes between releases.
|
|
93
|
-
|
|
92
|
+
representation changes between releases. Pickles written before
|
|
93
|
+
serializable 1.2.0 call from_serializable_repr instead, which remains
|
|
94
|
+
supported."""
|
|
95
|
+
return (type(self).from_dict, (self.to_dict(),))
|
|
@@ -15,36 +15,42 @@ Helper functions for deconstructing classes, functions, and user-defined
|
|
|
15
15
|
objects into serializable types.
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
+
from functools import lru_cache
|
|
18
19
|
from types import BuiltinFunctionType, FunctionType
|
|
19
20
|
|
|
20
21
|
import simplejson as json
|
|
21
22
|
|
|
22
|
-
from .primitive_types import
|
|
23
|
+
from .primitive_types import PRIMITIVE_TYPES
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
def init_arg_names(obj):
|
|
26
27
|
"""
|
|
27
|
-
Names of arguments to __init__ method of this
|
|
28
|
+
Names of the positional arguments to the __init__ method of this
|
|
29
|
+
object's class (excluding self).
|
|
30
|
+
|
|
31
|
+
Keyword-only arguments are deliberately excluded, so a class can accept
|
|
32
|
+
constructor options which aren't part of its serialized state.
|
|
28
33
|
"""
|
|
34
|
+
return _class_init_arg_names(type(obj))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Cached per class since this is called on every to_dict/__eq__/__hash__ of
|
|
38
|
+
# a Serializable which doesn't override to_dict. Assumes a class's __init__
|
|
39
|
+
# isn't replaced after its first instance has been serialized.
|
|
40
|
+
@lru_cache(maxsize=1024)
|
|
41
|
+
def _class_init_arg_names(cls):
|
|
29
42
|
# doing something wildly hacky by pulling out the arguments to
|
|
30
|
-
# __init__
|
|
31
|
-
# object
|
|
43
|
+
# __init__ and hoping that they match fields defined on the object
|
|
32
44
|
try:
|
|
33
|
-
init_code =
|
|
34
|
-
except AttributeError:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if hasattr(obj, "_fields"):
|
|
41
|
-
return obj._fields
|
|
42
|
-
raise ValueError(f"Cannot determine args to {obj}.__init__") from exc
|
|
43
|
-
|
|
44
|
-
arg_names = init_code.co_varnames[: init_code.co_argcount]
|
|
45
|
+
init_code = cls.__init__.__code__
|
|
46
|
+
except AttributeError as exc:
|
|
47
|
+
# if object is a namedtuple then we can return its fields
|
|
48
|
+
# as the required initial args
|
|
49
|
+
if hasattr(cls, "_fields"):
|
|
50
|
+
return cls._fields
|
|
51
|
+
raise ValueError(f"Cannot determine args to {cls.__qualname__}.__init__") from exc
|
|
45
52
|
# drop self argument
|
|
46
|
-
|
|
47
|
-
return nonself_arg_names
|
|
53
|
+
return init_code.co_varnames[1 : init_code.co_argcount]
|
|
48
54
|
|
|
49
55
|
|
|
50
56
|
def simple_object_to_dict(self):
|
|
@@ -185,10 +191,12 @@ def dict_to_serializable_repr(x):
|
|
|
185
191
|
return result
|
|
186
192
|
|
|
187
193
|
|
|
188
|
-
def
|
|
194
|
+
def _from_reconstructed_dict(x):
|
|
189
195
|
"""
|
|
190
|
-
|
|
191
|
-
|
|
196
|
+
Given a dictionary from a serializable representation whose values have
|
|
197
|
+
already been reconstructed, return the object it represents: a class or
|
|
198
|
+
function, an instance of a user-defined class, or a dictionary (restoring
|
|
199
|
+
any non-string keys). May modify `x`.
|
|
192
200
|
|
|
193
201
|
This is the most hackish part since we rely on key names such as
|
|
194
202
|
__name__, __class__, __module__ as metadata about how to reconstruct
|
|
@@ -200,26 +208,38 @@ def from_serializable_dict(x):
|
|
|
200
208
|
flattened result of to_dict() for user-defined objects.
|
|
201
209
|
"""
|
|
202
210
|
if "__name__" in x:
|
|
203
|
-
return _lookup_value(x
|
|
211
|
+
return _lookup_value(x["__module__"], x["__name__"])
|
|
212
|
+
|
|
213
|
+
serialized_keys = x.pop(SERIALIZED_DICTIONARY_KEYS_FIELD, None)
|
|
214
|
+
if serialized_keys:
|
|
215
|
+
non_string_key_objects = [from_json(serialized_key) for serialized_key in serialized_keys]
|
|
216
|
+
converted_dict = type(x)()
|
|
217
|
+
for k, v in x.items():
|
|
218
|
+
serialized_key_index = parse_serialized_keys_index(k)
|
|
219
|
+
if serialized_key_index is not None:
|
|
220
|
+
k = non_string_key_objects[serialized_key_index]
|
|
221
|
+
converted_dict[k] = v
|
|
222
|
+
x = converted_dict
|
|
223
|
+
|
|
224
|
+
if "__class__" not in x:
|
|
225
|
+
return x
|
|
226
|
+
class_object = x.pop("__class__")
|
|
227
|
+
if "__value__" in x:
|
|
228
|
+
return class_object(x["__value__"])
|
|
229
|
+
if hasattr(class_object, "from_dict"):
|
|
230
|
+
return class_object.from_dict(x)
|
|
231
|
+
return class_object(**x)
|
|
232
|
+
|
|
204
233
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
234
|
+
def from_serializable_dict(x):
|
|
235
|
+
"""
|
|
236
|
+
Reconstruct a dictionary, or the object it represents, by recursively
|
|
237
|
+
reconstructing all its keys and values. Does not modify `x`.
|
|
238
|
+
"""
|
|
208
239
|
converted_dict = type(x)()
|
|
209
240
|
for k, v in x.items():
|
|
210
|
-
serialized_key_index = parse_serialized_keys_index(k)
|
|
211
|
-
if serialized_key_index is not None:
|
|
212
|
-
k = non_string_key_objects[serialized_key_index]
|
|
213
|
-
|
|
214
241
|
converted_dict[k] = from_serializable_repr(v)
|
|
215
|
-
|
|
216
|
-
class_object = converted_dict.pop("__class__")
|
|
217
|
-
if "__value__" in converted_dict:
|
|
218
|
-
return class_object(converted_dict["__value__"])
|
|
219
|
-
if hasattr(class_object, "from_dict"):
|
|
220
|
-
return class_object.from_dict(converted_dict)
|
|
221
|
-
return class_object(**converted_dict)
|
|
222
|
-
return converted_dict
|
|
242
|
+
return _from_reconstructed_dict(converted_dict)
|
|
223
243
|
|
|
224
244
|
|
|
225
245
|
def list_to_serializable_repr(x):
|
|
@@ -242,12 +262,13 @@ def to_dict(obj):
|
|
|
242
262
|
raise ValueError(f"Cannot convert {obj} : {type(obj)} to dictionary") from exc
|
|
243
263
|
|
|
244
264
|
|
|
245
|
-
@return_primitive
|
|
246
265
|
def to_serializable_repr(x):
|
|
247
266
|
"""
|
|
248
267
|
Convert an instance of Serializable or a primitive collection containing
|
|
249
268
|
such instances into serializable types.
|
|
250
269
|
"""
|
|
270
|
+
if isinstance(x, PRIMITIVE_TYPES):
|
|
271
|
+
return x
|
|
251
272
|
t = type(x)
|
|
252
273
|
if isinstance(x, list):
|
|
253
274
|
return list_to_serializable_repr(x)
|
|
@@ -267,8 +288,12 @@ def to_serializable_repr(x):
|
|
|
267
288
|
return state_dictionary
|
|
268
289
|
|
|
269
290
|
|
|
270
|
-
@return_primitive
|
|
271
291
|
def from_serializable_repr(x):
|
|
292
|
+
"""
|
|
293
|
+
Inverse of to_serializable_repr. Does not modify `x`.
|
|
294
|
+
"""
|
|
295
|
+
if isinstance(x, PRIMITIVE_TYPES):
|
|
296
|
+
return x
|
|
272
297
|
t = type(x)
|
|
273
298
|
if isinstance(x, list):
|
|
274
299
|
return t([from_serializable_repr(element) for element in x])
|
|
@@ -286,4 +311,8 @@ def to_json(x):
|
|
|
286
311
|
|
|
287
312
|
|
|
288
313
|
def from_json(json_string):
|
|
289
|
-
|
|
314
|
+
"""
|
|
315
|
+
Inverse of to_json. Objects are reconstructed bottom-up while parsing,
|
|
316
|
+
equivalent to (but faster than) from_serializable_repr(json.loads(...)).
|
|
317
|
+
"""
|
|
318
|
+
return json.loads(json_string, object_hook=_from_reconstructed_dict)
|
|
@@ -13,13 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
from typing import ClassVar
|
|
15
15
|
|
|
16
|
-
from .helpers import
|
|
17
|
-
from_json,
|
|
18
|
-
from_serializable_repr,
|
|
19
|
-
simple_object_to_dict,
|
|
20
|
-
to_json,
|
|
21
|
-
to_serializable_repr,
|
|
22
|
-
)
|
|
16
|
+
from .helpers import from_json, simple_object_to_dict, to_json
|
|
23
17
|
|
|
24
18
|
|
|
25
19
|
class Serializable:
|
|
@@ -48,8 +42,10 @@ class Serializable:
|
|
|
48
42
|
(bool, string, int, float), primitive collections
|
|
49
43
|
(int, list, tuple, set) or instances of Serializable.
|
|
50
44
|
|
|
51
|
-
The default implementation is to assume all the arguments
|
|
52
|
-
have fields of the same name on a serializable object.
|
|
45
|
+
The default implementation is to assume all the positional arguments
|
|
46
|
+
to __init__ have fields of the same name on a serializable object.
|
|
47
|
+
Keyword-only arguments are excluded, so they can be used for
|
|
48
|
+
constructor options which aren't part of the serialized state.
|
|
53
49
|
"""
|
|
54
50
|
return simple_object_to_dict(self)
|
|
55
51
|
|
|
@@ -90,9 +86,9 @@ class Serializable:
|
|
|
90
86
|
def from_dict(cls, state_dict):
|
|
91
87
|
"""
|
|
92
88
|
Given a dictionary of flattened fields (result of calling to_dict()),
|
|
93
|
-
returns an instance.
|
|
89
|
+
returns an instance. Does not modify `state_dict`.
|
|
94
90
|
"""
|
|
95
|
-
state_dict = cls._reconstruct_nested_objects(state_dict)
|
|
91
|
+
state_dict = cls._reconstruct_nested_objects(dict(state_dict))
|
|
96
92
|
return cls(**cls._update_kwargs(state_dict))
|
|
97
93
|
|
|
98
94
|
def to_json(self):
|
|
@@ -127,10 +123,11 @@ class Serializable:
|
|
|
127
123
|
def __reduce__(self):
|
|
128
124
|
"""
|
|
129
125
|
Overriding this method directs the default pickler to reconstruct
|
|
130
|
-
this object using our from_dict method
|
|
131
|
-
|
|
126
|
+
this object using our from_dict method, so pickles survive the same
|
|
127
|
+
changes to a class (e.g. renamed keywords) as JSON does. Values in
|
|
128
|
+
to_dict() are pickled natively.
|
|
132
129
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return (
|
|
130
|
+
Pickles written before serializable 1.2.0 call from_serializable_repr
|
|
131
|
+
instead, which remains supported.
|
|
132
|
+
"""
|
|
133
|
+
return (type(self).from_dict, (self.to_dict(),))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: serializable
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Base class with serialization helpers for user-defined Python objects
|
|
5
5
|
Author-email: Alex Rubinsteyn <alex.rubinsteyn@unc.edu>
|
|
6
6
|
License: Apache License
|
|
@@ -274,9 +274,13 @@ helpers.
|
|
|
274
274
|
|
|
275
275
|
A derived class must either:
|
|
276
276
|
|
|
277
|
-
- have a member data matching the name of each argument to `__init__`
|
|
277
|
+
- have a member data matching the name of each positional argument to `__init__`
|
|
278
278
|
- provide a user-defined `to_dict()` method which returns a dictionary whose keys match the arguments to `__init__`
|
|
279
279
|
|
|
280
|
+
Keyword-only arguments to `__init__` (those after `*`) are left out of the default `to_dict()`, so they can be used for constructor options which aren't part of an object's serialized state.
|
|
281
|
+
|
|
282
|
+
Pickling goes through the same `to_dict()` / `from_dict()` pair, so pickles also survive renamed keywords (see below). Pickles written by versions before 1.2.0 can still be loaded.
|
|
283
|
+
|
|
280
284
|
If you change the keyword arguments to a class which derives from `Serializable` but would like to be able to deserialize older JSON representations then you can define a class-level dictionary called `_SERIALIZABLE_KEYWORD_ALIASES` which maps old keywords to new names (or `None` if a keyword was removed).
|
|
281
285
|
|
|
282
286
|
## Limitations
|
|
@@ -25,3 +25,17 @@ class A(Serializable):
|
|
|
25
25
|
def test_serializable_default_to_dict():
|
|
26
26
|
a = A(10, 1)
|
|
27
27
|
eq_(a, A.from_dict(a.to_dict()))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class WithKeywordOnlyOption(Serializable):
|
|
31
|
+
def __init__(self, x, *, scale=1):
|
|
32
|
+
# the keyword-only option isn't stored, so it can't be serialized
|
|
33
|
+
self.x = x * scale
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_keyword_only_args_excluded_from_default_to_dict():
|
|
37
|
+
# keyword-only args are constructor options rather than serialized state
|
|
38
|
+
# (varcode's SpliceOutcomeSet relies on this)
|
|
39
|
+
obj = WithKeywordOnlyOption(10, scale=2)
|
|
40
|
+
eq_(obj.to_dict(), {"x": 20})
|
|
41
|
+
eq_(obj, WithKeywordOnlyOption.from_json(obj.to_json()))
|
|
@@ -96,3 +96,10 @@ def test_renamed_keyword_inheritance():
|
|
|
96
96
|
eq_(obj.y, 2)
|
|
97
97
|
assert not hasattr(obj, "old_x")
|
|
98
98
|
assert not hasattr(obj, "old_y")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_from_dict_does_not_modify_input():
|
|
102
|
+
d = {"old_x": 1, "old_gone": 2}
|
|
103
|
+
obj = TestClassWithKeywordAliases.from_dict(d)
|
|
104
|
+
eq_(obj.x, 1)
|
|
105
|
+
eq_(d, {"old_x": 1, "old_gone": 2})
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
import copy
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from serializable import (
|
|
5
|
+
Serializable,
|
|
6
|
+
from_json,
|
|
7
|
+
from_serializable_repr,
|
|
8
|
+
to_json,
|
|
9
|
+
to_serializable_repr,
|
|
10
|
+
)
|
|
2
11
|
|
|
3
12
|
from .common import eq_
|
|
4
13
|
|
|
@@ -42,3 +51,27 @@ def test_object_with_dict_values():
|
|
|
42
51
|
def test_object_with_set_values():
|
|
43
52
|
x = B()
|
|
44
53
|
eq_(x, from_serializable_repr(to_serializable_repr(x)))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
nested = {
|
|
57
|
+
(1, 2): [A(1), B()],
|
|
58
|
+
"class": A,
|
|
59
|
+
"inner": {"t": (A({(3,): {4}}),), 5: "int key"},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_from_serializable_repr_does_not_modify_input():
|
|
64
|
+
serialized = to_serializable_repr(nested)
|
|
65
|
+
snapshot = copy.deepcopy(serialized)
|
|
66
|
+
eq_(nested, from_serializable_repr(serialized))
|
|
67
|
+
eq_(serialized, snapshot)
|
|
68
|
+
# decoding the same representation twice used to fail
|
|
69
|
+
eq_(nested, from_serializable_repr(serialized))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_from_json_matches_from_serializable_repr():
|
|
73
|
+
# from_json reconstructs objects while parsing; it must agree with
|
|
74
|
+
# decoding the parsed JSON in a separate pass
|
|
75
|
+
json_string = to_json(nested)
|
|
76
|
+
eq_(nested, from_json(json_string))
|
|
77
|
+
eq_(from_json(json_string), from_serializable_repr(json.loads(json_string)))
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
2
|
+
# you may not use this file except in compliance with the License.
|
|
3
|
+
# You may obtain a copy of the License at
|
|
4
|
+
#
|
|
5
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
#
|
|
7
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
8
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
9
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
10
|
+
# See the License for the specific language governing permissions and
|
|
11
|
+
# limitations under the License.
|
|
12
|
+
|
|
13
|
+
import copy
|
|
14
|
+
import datetime
|
|
15
|
+
import io
|
|
16
|
+
import pickle
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
|
|
19
|
+
import pytest
|
|
20
|
+
|
|
21
|
+
from serializable import (
|
|
22
|
+
DataclassSerializable,
|
|
23
|
+
Serializable,
|
|
24
|
+
from_serializable_repr,
|
|
25
|
+
to_serializable_repr,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from .common import eq_
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Point(Serializable):
|
|
32
|
+
def __init__(self, x, y):
|
|
33
|
+
self.x = x
|
|
34
|
+
self.y = y
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class Pair(DataclassSerializable):
|
|
39
|
+
left: object
|
|
40
|
+
right: object
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Interned(Serializable):
|
|
44
|
+
"""from_dict returns a cached instance, like pyensembl's EnsemblRelease"""
|
|
45
|
+
|
|
46
|
+
_instances = {}
|
|
47
|
+
|
|
48
|
+
def __init__(self, name):
|
|
49
|
+
self.name = name
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_dict(cls, state_dict):
|
|
53
|
+
name = state_dict["name"]
|
|
54
|
+
if name not in cls._instances:
|
|
55
|
+
cls._instances[name] = cls(name)
|
|
56
|
+
return cls._instances[name]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
nested = Point([Point(1, 2), (3, 4)], {(5, 6): {7, 8}, "pair": Pair("a", Point(9, 10))})
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Pre12Pickler(pickle.Pickler):
|
|
63
|
+
"""Pickles objects the way serializable < 1.2.0 did."""
|
|
64
|
+
|
|
65
|
+
def reducer_override(self, obj):
|
|
66
|
+
if isinstance(obj, (Serializable, DataclassSerializable)):
|
|
67
|
+
return (from_serializable_repr, (to_serializable_repr(obj),))
|
|
68
|
+
return NotImplemented
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.mark.parametrize("protocol", range(pickle.HIGHEST_PROTOCOL + 1))
|
|
72
|
+
def test_pickle_roundtrip_all_protocols(protocol):
|
|
73
|
+
eq_(nested, pickle.loads(pickle.dumps(nested, protocol=protocol)))
|
|
74
|
+
pair = Pair(nested, "b")
|
|
75
|
+
eq_(pair, pickle.loads(pickle.dumps(pair, protocol=protocol)))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_load_pickle_written_before_1_2():
|
|
79
|
+
objects = [nested, Pair(1, nested)]
|
|
80
|
+
buffer = io.BytesIO()
|
|
81
|
+
Pre12Pickler(buffer).dump(objects)
|
|
82
|
+
eq_(objects, pickle.loads(buffer.getvalue()))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_pickle_reconstructs_with_from_dict():
|
|
86
|
+
obj = Interned.from_dict({"name": "a"})
|
|
87
|
+
assert pickle.loads(pickle.dumps(obj)) is obj
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_pickle_value_without_serializable_representation():
|
|
91
|
+
# values in to_dict() are pickled natively, so they don't need a
|
|
92
|
+
# JSON-compatible representation
|
|
93
|
+
obj = Point(datetime.date(2026, 10, 6), None)
|
|
94
|
+
with pytest.raises(ValueError):
|
|
95
|
+
obj.to_json()
|
|
96
|
+
eq_(obj, pickle.loads(pickle.dumps(obj)))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_deepcopy():
|
|
100
|
+
copied = copy.deepcopy(nested)
|
|
101
|
+
eq_(nested, copied)
|
|
102
|
+
assert copied.x is not nested.x
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|