serializable 1.3.2__tar.gz → 1.3.4__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.3.2/serializable.egg-info → serializable-1.3.4}/PKG-INFO +54 -38
- serializable-1.3.4/README.md +86 -0
- {serializable-1.3.2 → serializable-1.3.4}/serializable/dataclass_serializable.py +4 -9
- {serializable-1.3.2 → serializable-1.3.4}/serializable/helpers.py +19 -0
- serializable-1.3.4/serializable/keyword_aliases.py +57 -0
- {serializable-1.3.2 → serializable-1.3.4}/serializable/serializable.py +6 -12
- {serializable-1.3.2 → serializable-1.3.4}/serializable/version.py +1 -1
- {serializable-1.3.2 → serializable-1.3.4/serializable.egg-info}/PKG-INFO +54 -38
- {serializable-1.3.2 → serializable-1.3.4}/serializable.egg-info/SOURCES.txt +1 -0
- {serializable-1.3.2 → serializable-1.3.4}/tests/test_keyword_aliases.py +68 -1
- serializable-1.3.2/README.md +0 -70
- {serializable-1.3.2 → serializable-1.3.4}/LICENSE +0 -0
- {serializable-1.3.2 → serializable-1.3.4}/MANIFEST.in +0 -0
- {serializable-1.3.2 → serializable-1.3.4}/pyproject.toml +0 -0
- {serializable-1.3.2 → serializable-1.3.4}/serializable/__init__.py +0 -0
- {serializable-1.3.2 → serializable-1.3.4}/serializable/primitive_types.py +0 -0
- {serializable-1.3.2 → serializable-1.3.4}/serializable/py.typed +0 -0
- {serializable-1.3.2 → serializable-1.3.4}/serializable.egg-info/dependency_links.txt +0 -0
- {serializable-1.3.2 → serializable-1.3.4}/serializable.egg-info/requires.txt +0 -0
- {serializable-1.3.2 → serializable-1.3.4}/serializable.egg-info/top_level.txt +0 -0
- {serializable-1.3.2 → serializable-1.3.4}/setup.cfg +0 -0
- {serializable-1.3.2 → serializable-1.3.4}/tests/__init__.py +0 -0
- {serializable-1.3.2 → serializable-1.3.4}/tests/common.py +0 -0
- {serializable-1.3.2 → serializable-1.3.4}/tests/test_class.py +0 -0
- {serializable-1.3.2 → serializable-1.3.4}/tests/test_dataclass_serializable.py +0 -0
- {serializable-1.3.2 → serializable-1.3.4}/tests/test_default_to_dict_implementation.py +0 -0
- {serializable-1.3.2 → serializable-1.3.4}/tests/test_function.py +0 -0
- {serializable-1.3.2 → serializable-1.3.4}/tests/test_namedtuple.py +0 -0
- {serializable-1.3.2 → serializable-1.3.4}/tests/test_nesting.py +0 -0
- {serializable-1.3.2 → serializable-1.3.4}/tests/test_object.py +0 -0
- {serializable-1.3.2 → serializable-1.3.4}/tests/test_pickle.py +0 -0
- {serializable-1.3.2 → serializable-1.3.4}/tests/test_primitive_values.py +0 -0
- {serializable-1.3.2 → serializable-1.3.4}/tests/test_set.py +0 -0
- {serializable-1.3.2 → serializable-1.3.4}/tests/test_tuple.py +0 -0
- {serializable-1.3.2 → serializable-1.3.4}/tests/test_typing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: serializable
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.4
|
|
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
|
|
@@ -231,15 +231,22 @@ Requires-Dist: build; extra == "dev"
|
|
|
231
231
|
Requires-Dist: twine; extra == "dev"
|
|
232
232
|
Dynamic: license-file
|
|
233
233
|
|
|
234
|
-
[](https://github.com/openvax/serializable/actions/workflows/tests.yml)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
</a>
|
|
234
|
+
[](https://github.com/openvax/serializable/actions/workflows/tests.yml)
|
|
235
|
+
[](https://openvax.github.io/serializable/)
|
|
236
|
+
[](https://pypi.org/project/serializable/)
|
|
238
237
|
[](https://pypistats.org/packages/serializable)
|
|
239
238
|
|
|
240
239
|
# serializable
|
|
241
240
|
|
|
242
|
-
|
|
241
|
+
serializable saves Python objects as JSON or pickles and reconstructs the same
|
|
242
|
+
classes, including nested objects. Subclass `DataclassSerializable` for a
|
|
243
|
+
dataclass, or `Serializable` for a class whose constructor arguments describe
|
|
244
|
+
its state. It is the serialization layer of the OpenVax libraries:
|
|
245
|
+
[varcode](https://github.com/openvax/varcode) and
|
|
246
|
+
[vaxrank](https://github.com/openvax/vaxrank) results,
|
|
247
|
+
[pyensembl](https://github.com/openvax/pyensembl) genomes and
|
|
248
|
+
[sercol](https://github.com/openvax/sercol) collections all save and reload
|
|
249
|
+
through it.
|
|
243
250
|
|
|
244
251
|
## Install
|
|
245
252
|
|
|
@@ -247,9 +254,9 @@ Base class with serialization methods for user-defined Python objects
|
|
|
247
254
|
python -m pip install serializable
|
|
248
255
|
```
|
|
249
256
|
|
|
250
|
-
|
|
257
|
+
Python 3.11 or later is required.
|
|
251
258
|
|
|
252
|
-
|
|
259
|
+
## Quick start
|
|
253
260
|
|
|
254
261
|
```python
|
|
255
262
|
from dataclasses import dataclass
|
|
@@ -260,44 +267,53 @@ class Point(DataclassSerializable):
|
|
|
260
267
|
x: float
|
|
261
268
|
y: float
|
|
262
269
|
|
|
263
|
-
|
|
264
|
-
|
|
270
|
+
point = Point(1.0, 2.0)
|
|
271
|
+
restored = Point.from_json(point.to_json())
|
|
272
|
+
print(point.to_dict())
|
|
273
|
+
print(restored)
|
|
274
|
+
print(restored == point)
|
|
265
275
|
```
|
|
266
276
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
`to_json`, `from_json`, `__reduce__` (for pickling), and other serialization
|
|
273
|
-
helpers.
|
|
274
|
-
|
|
275
|
-
A derived class must either:
|
|
277
|
+
```text
|
|
278
|
+
{'x': 1.0, 'y': 2.0}
|
|
279
|
+
Point(x=1.0, y=2.0)
|
|
280
|
+
True
|
|
281
|
+
```
|
|
276
282
|
|
|
277
|
-
|
|
278
|
-
|
|
283
|
+
`to_dict()` gives the constructor fields; `to_json()` also records the class
|
|
284
|
+
and its module so `from_json()` can rebuild it. The dataclass keeps its own
|
|
285
|
+
generated constructor, equality, representation and hashing. The recorded module and
|
|
286
|
+
class must be importable when loading, so load data only from sources you
|
|
287
|
+
trust.
|
|
279
288
|
|
|
280
|
-
|
|
289
|
+
## Documentation
|
|
281
290
|
|
|
282
|
-
|
|
291
|
+
The [documentation](https://openvax.github.io/serializable/) covers, in
|
|
292
|
+
reading order:
|
|
283
293
|
|
|
284
|
-
|
|
294
|
+
1. [Getting started](https://openvax.github.io/serializable/): the example
|
|
295
|
+
above, choosing a base class, and the format's limits.
|
|
296
|
+
2. [Save classes and files](https://openvax.github.io/serializable/guides/objects/):
|
|
297
|
+
constructor-based classes, JSON files, pickles and nested containers.
|
|
298
|
+
3. [Change field names](https://openvax.github.io/serializable/guides/migration/):
|
|
299
|
+
read data written before a constructor argument was renamed.
|
|
300
|
+
4. [API reference](https://openvax.github.io/serializable/reference/).
|
|
285
301
|
|
|
286
|
-
##
|
|
302
|
+
## Development
|
|
287
303
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
304
|
+
```sh
|
|
305
|
+
python -m pip install -e ".[dev]" # or ./develop.sh, which uses .venv
|
|
306
|
+
./format.sh # ruff format
|
|
307
|
+
./lint.sh # ruff check, ruff format --check and mypy
|
|
308
|
+
./test.sh # pytest with coverage
|
|
309
|
+
python -m pip install -r requirements-docs.txt
|
|
310
|
+
./docs.sh # mkdocs build --strict, then a rendered-link check
|
|
311
|
+
python scripts/check_docs_examples.py # run docs examples, compare output
|
|
312
|
+
```
|
|
294
313
|
|
|
295
|
-
|
|
314
|
+
Every PR bumps `serializable/version.py`. Maintainers release with
|
|
315
|
+
`./deploy.sh` from a clean `master`.
|
|
296
316
|
|
|
297
|
-
|
|
298
|
-
- [Migrate field names](docs/guides/migration.md)
|
|
299
|
-
- [API reference](docs/reference.md)
|
|
300
|
-
- [Writing and checking docs](docs/dev/documentation-style.md)
|
|
317
|
+
## License
|
|
301
318
|
|
|
302
|
-
|
|
303
|
-
by `./docs.sh`. Run `python scripts/check_docs_examples.py` to check the examples.
|
|
319
|
+
Apache 2.0; see [LICENSE](https://github.com/openvax/serializable/blob/master/LICENSE).
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
[](https://github.com/openvax/serializable/actions/workflows/tests.yml)
|
|
2
|
+
[](https://openvax.github.io/serializable/)
|
|
3
|
+
[](https://pypi.org/project/serializable/)
|
|
4
|
+
[](https://pypistats.org/packages/serializable)
|
|
5
|
+
|
|
6
|
+
# serializable
|
|
7
|
+
|
|
8
|
+
serializable saves Python objects as JSON or pickles and reconstructs the same
|
|
9
|
+
classes, including nested objects. Subclass `DataclassSerializable` for a
|
|
10
|
+
dataclass, or `Serializable` for a class whose constructor arguments describe
|
|
11
|
+
its state. It is the serialization layer of the OpenVax libraries:
|
|
12
|
+
[varcode](https://github.com/openvax/varcode) and
|
|
13
|
+
[vaxrank](https://github.com/openvax/vaxrank) results,
|
|
14
|
+
[pyensembl](https://github.com/openvax/pyensembl) genomes and
|
|
15
|
+
[sercol](https://github.com/openvax/sercol) collections all save and reload
|
|
16
|
+
through it.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
python -m pip install serializable
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Python 3.11 or later is required.
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from serializable import DataclassSerializable
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class Point(DataclassSerializable):
|
|
34
|
+
x: float
|
|
35
|
+
y: float
|
|
36
|
+
|
|
37
|
+
point = Point(1.0, 2.0)
|
|
38
|
+
restored = Point.from_json(point.to_json())
|
|
39
|
+
print(point.to_dict())
|
|
40
|
+
print(restored)
|
|
41
|
+
print(restored == point)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
{'x': 1.0, 'y': 2.0}
|
|
46
|
+
Point(x=1.0, y=2.0)
|
|
47
|
+
True
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`to_dict()` gives the constructor fields; `to_json()` also records the class
|
|
51
|
+
and its module so `from_json()` can rebuild it. The dataclass keeps its own
|
|
52
|
+
generated constructor, equality, representation and hashing. The recorded module and
|
|
53
|
+
class must be importable when loading, so load data only from sources you
|
|
54
|
+
trust.
|
|
55
|
+
|
|
56
|
+
## Documentation
|
|
57
|
+
|
|
58
|
+
The [documentation](https://openvax.github.io/serializable/) covers, in
|
|
59
|
+
reading order:
|
|
60
|
+
|
|
61
|
+
1. [Getting started](https://openvax.github.io/serializable/): the example
|
|
62
|
+
above, choosing a base class, and the format's limits.
|
|
63
|
+
2. [Save classes and files](https://openvax.github.io/serializable/guides/objects/):
|
|
64
|
+
constructor-based classes, JSON files, pickles and nested containers.
|
|
65
|
+
3. [Change field names](https://openvax.github.io/serializable/guides/migration/):
|
|
66
|
+
read data written before a constructor argument was renamed.
|
|
67
|
+
4. [API reference](https://openvax.github.io/serializable/reference/).
|
|
68
|
+
|
|
69
|
+
## Development
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
python -m pip install -e ".[dev]" # or ./develop.sh, which uses .venv
|
|
73
|
+
./format.sh # ruff format
|
|
74
|
+
./lint.sh # ruff check, ruff format --check and mypy
|
|
75
|
+
./test.sh # pytest with coverage
|
|
76
|
+
python -m pip install -r requirements-docs.txt
|
|
77
|
+
./docs.sh # mkdocs build --strict, then a rendered-link check
|
|
78
|
+
python scripts/check_docs_examples.py # run docs examples, compare output
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Every PR bumps `serializable/version.py`. Maintainers release with
|
|
82
|
+
`./deploy.sh` from a clean `master`.
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
Apache 2.0; see [LICENSE](https://github.com/openvax/serializable/blob/master/LICENSE).
|
|
@@ -45,6 +45,7 @@ from dataclasses import fields
|
|
|
45
45
|
from typing import Any, ClassVar, Optional, TypeVar
|
|
46
46
|
|
|
47
47
|
from .helpers import from_json, to_json
|
|
48
|
+
from .keyword_aliases import apply_keyword_aliases
|
|
48
49
|
|
|
49
50
|
_T = TypeVar("_T", bound="DataclassSerializable")
|
|
50
51
|
|
|
@@ -73,21 +74,15 @@ class DataclassSerializable:
|
|
|
73
74
|
def from_dict(cls: type[_T], state_dict: Mapping[str, Any]) -> _T:
|
|
74
75
|
"""Reconstruct an instance from a ``to_dict``-shaped dictionary,
|
|
75
76
|
applying ``_SERIALIZABLE_KEYWORD_ALIASES`` for backwards compat."""
|
|
76
|
-
|
|
77
|
-
for klass in cls.__mro__:
|
|
78
|
-
aliases = getattr(klass, "_SERIALIZABLE_KEYWORD_ALIASES", {})
|
|
79
|
-
for old_name, new_name in aliases.items():
|
|
80
|
-
if old_name in kwargs:
|
|
81
|
-
value = kwargs.pop(old_name)
|
|
82
|
-
if new_name is not None and new_name not in kwargs:
|
|
83
|
-
kwargs[new_name] = value
|
|
84
|
-
return cls(**kwargs)
|
|
77
|
+
return cls(**apply_keyword_aliases(cls, dict(state_dict)))
|
|
85
78
|
|
|
86
79
|
def to_json(self) -> str:
|
|
80
|
+
"""Return a JSON string from which ``from_json`` rebuilds this object."""
|
|
87
81
|
return to_json(self)
|
|
88
82
|
|
|
89
83
|
@classmethod
|
|
90
84
|
def from_json(cls: type[_T], json_string: str) -> _T:
|
|
85
|
+
"""Reconstruct an object from a string produced by ``to_json``."""
|
|
91
86
|
return from_json(json_string)
|
|
92
87
|
|
|
93
88
|
def __reduce__(self) -> tuple[Any, ...]:
|
|
@@ -57,6 +57,13 @@ def _class_init_arg_names(cls):
|
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
def simple_object_to_dict(self: object) -> dict[str, Any]:
|
|
60
|
+
"""
|
|
61
|
+
Map each positional __init__ argument name of an object's class to the
|
|
62
|
+
attribute of the same name. This is Serializable's default to_dict.
|
|
63
|
+
|
|
64
|
+
Raises AttributeError if the object has no attribute for an argument, and
|
|
65
|
+
ValueError if its class's __init__ arguments can't be determined.
|
|
66
|
+
"""
|
|
60
67
|
return {name: getattr(self, name) for name in init_arg_names(self)}
|
|
61
68
|
|
|
62
69
|
|
|
@@ -100,6 +107,9 @@ def class_from_serializable_representation(class_repr):
|
|
|
100
107
|
|
|
101
108
|
|
|
102
109
|
def get_module_name(obj):
|
|
110
|
+
"""
|
|
111
|
+
Name of the module where a class or function is defined (its __module__).
|
|
112
|
+
"""
|
|
103
113
|
module_name = obj.__module__
|
|
104
114
|
return module_name
|
|
105
115
|
|
|
@@ -143,6 +153,11 @@ SERIALIZED_DICTIONARY_KEYS_ELEMENT_PREFIX = SERIALIZED_DICTIONARY_KEYS_FIELD + "
|
|
|
143
153
|
|
|
144
154
|
|
|
145
155
|
def index_to_serialized_key_name(index):
|
|
156
|
+
"""
|
|
157
|
+
Placeholder key, such as __serialized_keys__element_3, that stands for the
|
|
158
|
+
non-string dictionary key at this index of the serialized key list.
|
|
159
|
+
Inverse of parse_serialized_keys_index.
|
|
160
|
+
"""
|
|
146
161
|
return f"{SERIALIZED_DICTIONARY_KEYS_ELEMENT_PREFIX}{index:d}"
|
|
147
162
|
|
|
148
163
|
|
|
@@ -246,6 +261,10 @@ def from_serializable_dict(x):
|
|
|
246
261
|
|
|
247
262
|
|
|
248
263
|
def list_to_serializable_repr(x):
|
|
264
|
+
"""
|
|
265
|
+
Convert each element of a list (or other iterable) with to_serializable_repr,
|
|
266
|
+
returning a new list.
|
|
267
|
+
"""
|
|
249
268
|
return [to_serializable_repr(element) for element in x]
|
|
250
269
|
|
|
251
270
|
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
"""
|
|
14
|
+
Migrate renamed or removed constructor keywords when loading old data.
|
|
15
|
+
|
|
16
|
+
Both ``Serializable`` and ``DataclassSerializable`` let a class declare
|
|
17
|
+
``_SERIALIZABLE_KEYWORD_ALIASES``, a dict mapping an old keyword to its new
|
|
18
|
+
name, or to ``None`` if the keyword was removed.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def apply_keyword_aliases(cls: type, kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
27
|
+
"""
|
|
28
|
+
Rename or drop old keyword arguments using a class's declared aliases.
|
|
29
|
+
|
|
30
|
+
Every class in ``cls``'s method resolution order contributes its
|
|
31
|
+
``_SERIALIZABLE_KEYWORD_ALIASES``. For each ``old_name -> new_name`` pair
|
|
32
|
+
whose ``old_name`` is in ``kwargs``, the old entry is removed. Its value is
|
|
33
|
+
stored under ``new_name`` unless ``new_name`` is ``None`` or empty (the
|
|
34
|
+
keyword was removed) or ``kwargs`` already has a value for ``new_name``
|
|
35
|
+
(the current name wins).
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
cls : type
|
|
40
|
+
Class whose aliases apply, typically the class being reconstructed.
|
|
41
|
+
|
|
42
|
+
kwargs : dict
|
|
43
|
+
Keyword arguments for ``cls``'s constructor. Modified in place.
|
|
44
|
+
|
|
45
|
+
Returns
|
|
46
|
+
-------
|
|
47
|
+
dict
|
|
48
|
+
The same ``kwargs`` object, updated.
|
|
49
|
+
"""
|
|
50
|
+
for klass in cls.__mro__:
|
|
51
|
+
aliases = getattr(klass, "_SERIALIZABLE_KEYWORD_ALIASES", {})
|
|
52
|
+
for old_name, new_name in aliases.items():
|
|
53
|
+
if old_name in kwargs:
|
|
54
|
+
value = kwargs.pop(old_name)
|
|
55
|
+
if new_name and new_name not in kwargs:
|
|
56
|
+
kwargs[new_name] = value
|
|
57
|
+
return kwargs
|
|
@@ -18,6 +18,7 @@ from collections.abc import Mapping
|
|
|
18
18
|
from typing import Any, ClassVar, Optional, TypeVar, Union
|
|
19
19
|
|
|
20
20
|
from .helpers import from_json, simple_object_to_dict, to_json
|
|
21
|
+
from .keyword_aliases import apply_keyword_aliases
|
|
21
22
|
|
|
22
23
|
_T = TypeVar("_T", bound="Serializable")
|
|
23
24
|
|
|
@@ -75,18 +76,11 @@ class Serializable:
|
|
|
75
76
|
@classmethod
|
|
76
77
|
def _update_kwargs(cls, kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
77
78
|
"""
|
|
78
|
-
Rename any old keyword arguments to preserve backwards compatibility
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
keyword_rename_dict = getattr(klass, "_SERIALIZABLE_KEYWORD_ALIASES", {})
|
|
84
|
-
for old_name, new_name in keyword_rename_dict.items():
|
|
85
|
-
if old_name in kwargs:
|
|
86
|
-
old_value = kwargs.pop(old_name)
|
|
87
|
-
if new_name and new_name not in kwargs:
|
|
88
|
-
kwargs[new_name] = old_value
|
|
89
|
-
return kwargs
|
|
79
|
+
Rename any old keyword arguments to preserve backwards compatibility.
|
|
80
|
+
|
|
81
|
+
Kept as an overridable hook for from_dict; see apply_keyword_aliases.
|
|
82
|
+
"""
|
|
83
|
+
return apply_keyword_aliases(cls, kwargs)
|
|
90
84
|
|
|
91
85
|
@classmethod
|
|
92
86
|
def from_dict(cls: type[_T], state_dict: Mapping[str, Any]) -> _T:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: serializable
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.4
|
|
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
|
|
@@ -231,15 +231,22 @@ Requires-Dist: build; extra == "dev"
|
|
|
231
231
|
Requires-Dist: twine; extra == "dev"
|
|
232
232
|
Dynamic: license-file
|
|
233
233
|
|
|
234
|
-
[](https://github.com/openvax/serializable/actions/workflows/tests.yml)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
</a>
|
|
234
|
+
[](https://github.com/openvax/serializable/actions/workflows/tests.yml)
|
|
235
|
+
[](https://openvax.github.io/serializable/)
|
|
236
|
+
[](https://pypi.org/project/serializable/)
|
|
238
237
|
[](https://pypistats.org/packages/serializable)
|
|
239
238
|
|
|
240
239
|
# serializable
|
|
241
240
|
|
|
242
|
-
|
|
241
|
+
serializable saves Python objects as JSON or pickles and reconstructs the same
|
|
242
|
+
classes, including nested objects. Subclass `DataclassSerializable` for a
|
|
243
|
+
dataclass, or `Serializable` for a class whose constructor arguments describe
|
|
244
|
+
its state. It is the serialization layer of the OpenVax libraries:
|
|
245
|
+
[varcode](https://github.com/openvax/varcode) and
|
|
246
|
+
[vaxrank](https://github.com/openvax/vaxrank) results,
|
|
247
|
+
[pyensembl](https://github.com/openvax/pyensembl) genomes and
|
|
248
|
+
[sercol](https://github.com/openvax/sercol) collections all save and reload
|
|
249
|
+
through it.
|
|
243
250
|
|
|
244
251
|
## Install
|
|
245
252
|
|
|
@@ -247,9 +254,9 @@ Base class with serialization methods for user-defined Python objects
|
|
|
247
254
|
python -m pip install serializable
|
|
248
255
|
```
|
|
249
256
|
|
|
250
|
-
|
|
257
|
+
Python 3.11 or later is required.
|
|
251
258
|
|
|
252
|
-
|
|
259
|
+
## Quick start
|
|
253
260
|
|
|
254
261
|
```python
|
|
255
262
|
from dataclasses import dataclass
|
|
@@ -260,44 +267,53 @@ class Point(DataclassSerializable):
|
|
|
260
267
|
x: float
|
|
261
268
|
y: float
|
|
262
269
|
|
|
263
|
-
|
|
264
|
-
|
|
270
|
+
point = Point(1.0, 2.0)
|
|
271
|
+
restored = Point.from_json(point.to_json())
|
|
272
|
+
print(point.to_dict())
|
|
273
|
+
print(restored)
|
|
274
|
+
print(restored == point)
|
|
265
275
|
```
|
|
266
276
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
`to_json`, `from_json`, `__reduce__` (for pickling), and other serialization
|
|
273
|
-
helpers.
|
|
274
|
-
|
|
275
|
-
A derived class must either:
|
|
277
|
+
```text
|
|
278
|
+
{'x': 1.0, 'y': 2.0}
|
|
279
|
+
Point(x=1.0, y=2.0)
|
|
280
|
+
True
|
|
281
|
+
```
|
|
276
282
|
|
|
277
|
-
|
|
278
|
-
|
|
283
|
+
`to_dict()` gives the constructor fields; `to_json()` also records the class
|
|
284
|
+
and its module so `from_json()` can rebuild it. The dataclass keeps its own
|
|
285
|
+
generated constructor, equality, representation and hashing. The recorded module and
|
|
286
|
+
class must be importable when loading, so load data only from sources you
|
|
287
|
+
trust.
|
|
279
288
|
|
|
280
|
-
|
|
289
|
+
## Documentation
|
|
281
290
|
|
|
282
|
-
|
|
291
|
+
The [documentation](https://openvax.github.io/serializable/) covers, in
|
|
292
|
+
reading order:
|
|
283
293
|
|
|
284
|
-
|
|
294
|
+
1. [Getting started](https://openvax.github.io/serializable/): the example
|
|
295
|
+
above, choosing a base class, and the format's limits.
|
|
296
|
+
2. [Save classes and files](https://openvax.github.io/serializable/guides/objects/):
|
|
297
|
+
constructor-based classes, JSON files, pickles and nested containers.
|
|
298
|
+
3. [Change field names](https://openvax.github.io/serializable/guides/migration/):
|
|
299
|
+
read data written before a constructor argument was renamed.
|
|
300
|
+
4. [API reference](https://openvax.github.io/serializable/reference/).
|
|
285
301
|
|
|
286
|
-
##
|
|
302
|
+
## Development
|
|
287
303
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
304
|
+
```sh
|
|
305
|
+
python -m pip install -e ".[dev]" # or ./develop.sh, which uses .venv
|
|
306
|
+
./format.sh # ruff format
|
|
307
|
+
./lint.sh # ruff check, ruff format --check and mypy
|
|
308
|
+
./test.sh # pytest with coverage
|
|
309
|
+
python -m pip install -r requirements-docs.txt
|
|
310
|
+
./docs.sh # mkdocs build --strict, then a rendered-link check
|
|
311
|
+
python scripts/check_docs_examples.py # run docs examples, compare output
|
|
312
|
+
```
|
|
294
313
|
|
|
295
|
-
|
|
314
|
+
Every PR bumps `serializable/version.py`. Maintainers release with
|
|
315
|
+
`./deploy.sh` from a clean `master`.
|
|
296
316
|
|
|
297
|
-
|
|
298
|
-
- [Migrate field names](docs/guides/migration.md)
|
|
299
|
-
- [API reference](docs/reference.md)
|
|
300
|
-
- [Writing and checking docs](docs/dev/documentation-style.md)
|
|
317
|
+
## License
|
|
301
318
|
|
|
302
|
-
|
|
303
|
-
by `./docs.sh`. Run `python scripts/check_docs_examples.py` to check the examples.
|
|
319
|
+
Apache 2.0; see [LICENSE](https://github.com/openvax/serializable/blob/master/LICENSE).
|
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
# limitations under the License.
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
from
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import ClassVar
|
|
16
|
+
|
|
17
|
+
from serializable import DataclassSerializable, Serializable
|
|
18
|
+
from serializable.keyword_aliases import apply_keyword_aliases
|
|
15
19
|
|
|
16
20
|
from .common import eq_
|
|
17
21
|
|
|
@@ -103,3 +107,66 @@ def test_from_dict_does_not_modify_input():
|
|
|
103
107
|
obj = TestClassWithKeywordAliases.from_dict(d)
|
|
104
108
|
eq_(obj.x, 1)
|
|
105
109
|
eq_(d, {"old_x": 1, "old_gone": 2})
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_apply_keyword_aliases_renames_drops_and_returns_same_dict():
|
|
113
|
+
kwargs = {"old_x": 1, "old_gone": 2}
|
|
114
|
+
result = apply_keyword_aliases(TestClassWithKeywordAliases, kwargs)
|
|
115
|
+
assert result is kwargs
|
|
116
|
+
eq_(result, {"x": 1})
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_apply_keyword_aliases_prefers_current_name():
|
|
120
|
+
kwargs = {"old_x": "old", "x": "new"}
|
|
121
|
+
eq_(apply_keyword_aliases(TestClassWithKeywordAliases, kwargs), {"x": "new"})
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_apply_keyword_aliases_uses_every_class_in_the_mro():
|
|
125
|
+
kwargs = {"old_x": 1, "old_y": 2, "old_gone": 3}
|
|
126
|
+
eq_(apply_keyword_aliases(DerivedClassWithMoreKeywordAliases, kwargs), {"x": 1, "y": 2})
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_apply_keyword_aliases_without_aliases_is_a_no_op():
|
|
130
|
+
eq_(apply_keyword_aliases(object, {"a": 1}), {"a": 1})
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass
|
|
134
|
+
class DataclassWithEmptyAlias(DataclassSerializable):
|
|
135
|
+
_SERIALIZABLE_KEYWORD_ALIASES: ClassVar[dict[str, str | None]] = {"legacy": ""}
|
|
136
|
+
x: int
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class SerializableWithEmptyAlias(Serializable):
|
|
140
|
+
_SERIALIZABLE_KEYWORD_ALIASES = {"legacy": ""}
|
|
141
|
+
|
|
142
|
+
def __init__(self, x):
|
|
143
|
+
self.x = x
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_both_base_classes_treat_an_empty_alias_as_removed():
|
|
147
|
+
# Serializable always dropped these; DataclassSerializable used to pass
|
|
148
|
+
# them to the constructor under the keyword "", which raised TypeError.
|
|
149
|
+
assert SerializableWithEmptyAlias.from_dict(
|
|
150
|
+
{"x": 1, "legacy": 2}
|
|
151
|
+
) == SerializableWithEmptyAlias(x=1)
|
|
152
|
+
assert DataclassWithEmptyAlias.from_dict({"x": 1, "legacy": 2}) == DataclassWithEmptyAlias(x=1)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class ClassOverridingUpdateKwargs(TestClassWithKeywordAliases):
|
|
156
|
+
def __init__(self, x, y):
|
|
157
|
+
self.x = x
|
|
158
|
+
self.y = y
|
|
159
|
+
|
|
160
|
+
@classmethod
|
|
161
|
+
def _update_kwargs(cls, kwargs):
|
|
162
|
+
kwargs = super()._update_kwargs(kwargs)
|
|
163
|
+
kwargs.setdefault("y", "filled in by override")
|
|
164
|
+
return kwargs
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_from_dict_still_calls_an_overridden_update_kwargs():
|
|
168
|
+
# _update_kwargs is kept as a hook: an override runs in from_dict and can
|
|
169
|
+
# still apply the inherited aliases through super().
|
|
170
|
+
obj = ClassOverridingUpdateKwargs.from_dict({"old_x": 1})
|
|
171
|
+
eq_(obj.x, 1)
|
|
172
|
+
eq_(obj.y, "filled in by override")
|
serializable-1.3.2/README.md
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
[](https://github.com/openvax/serializable/actions/workflows/tests.yml)
|
|
2
|
-
<a href="https://pypi.python.org/pypi/serializable/">
|
|
3
|
-
<img src="https://img.shields.io/pypi/v/serializable.svg?maxAge=1000" alt="PyPI" />
|
|
4
|
-
</a>
|
|
5
|
-
[](https://pypistats.org/packages/serializable)
|
|
6
|
-
|
|
7
|
-
# serializable
|
|
8
|
-
|
|
9
|
-
Base class with serialization methods for user-defined Python objects
|
|
10
|
-
|
|
11
|
-
## Install
|
|
12
|
-
|
|
13
|
-
```sh
|
|
14
|
-
python -m pip install serializable
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
## `DataclassSerializable` for `@dataclass` subclasses
|
|
18
|
-
|
|
19
|
-
If you're using `@dataclass` (e.g. in `vaxrank`, `pyensembl`, or `varcode`), inherit from `DataclassSerializable` instead of `Serializable`. It provides the same serialization surface — `to_dict` / `from_dict` / `to_json` / `from_json` — but leaves `__init__`, `__eq__`, `__repr__`, and `__hash__` to `@dataclass`, so you get dataclass-native equality and repr without conflicts.
|
|
20
|
-
|
|
21
|
-
```python
|
|
22
|
-
from dataclasses import dataclass
|
|
23
|
-
from serializable import DataclassSerializable
|
|
24
|
-
|
|
25
|
-
@dataclass
|
|
26
|
-
class Point(DataclassSerializable):
|
|
27
|
-
x: float
|
|
28
|
-
y: float
|
|
29
|
-
|
|
30
|
-
p = Point(1.0, 2.0)
|
|
31
|
-
assert Point.from_json(p.to_json()) == p
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
The on-wire JSON format is identical to `Serializable`, so mixed codebases interoperate: a `DataclassSerializable` instance can reference a legacy `Serializable` object (and vice versa) and still round-trip cleanly. The `_SERIALIZABLE_KEYWORD_ALIASES` hook works the same way for migrating field names across releases.
|
|
35
|
-
|
|
36
|
-
## Usage
|
|
37
|
-
|
|
38
|
-
Classes which inherit from `Serializable` are enabled with default implementations of
|
|
39
|
-
`to_json`, `from_json`, `__reduce__` (for pickling), and other serialization
|
|
40
|
-
helpers.
|
|
41
|
-
|
|
42
|
-
A derived class must either:
|
|
43
|
-
|
|
44
|
-
- have a member data matching the name of each positional argument to `__init__`
|
|
45
|
-
- provide a user-defined `to_dict()` method which returns a dictionary whose keys match the arguments to `__init__`
|
|
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
|
-
|
|
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).
|
|
52
|
-
|
|
53
|
-
## Limitations
|
|
54
|
-
|
|
55
|
-
The format records Python classes and modules, which must remain importable when
|
|
56
|
-
loading data. Keys starting with two underscores are reserved. Nested Python
|
|
57
|
-
values require the serialization helpers rather than ordinary JSON encoding.
|
|
58
|
-
`to_json` raises `ValueError` for NaN or infinite floats, which standard JSON
|
|
59
|
-
can't represent; `from_json` still reads the `NaN` / `Infinity` literals which
|
|
60
|
-
older versions wrote.
|
|
61
|
-
|
|
62
|
-
## Documentation
|
|
63
|
-
|
|
64
|
-
- [Save classes, files and nested objects](docs/guides/objects.md)
|
|
65
|
-
- [Migrate field names](docs/guides/migration.md)
|
|
66
|
-
- [API reference](docs/reference.md)
|
|
67
|
-
- [Writing and checking docs](docs/dev/documentation-style.md)
|
|
68
|
-
|
|
69
|
-
The site builds with `python -m pip install -r requirements-docs.txt` followed
|
|
70
|
-
by `./docs.sh`. Run `python scripts/check_docs_examples.py` to check the examples.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|