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.
Files changed (35) hide show
  1. {serializable-1.3.2/serializable.egg-info → serializable-1.3.4}/PKG-INFO +54 -38
  2. serializable-1.3.4/README.md +86 -0
  3. {serializable-1.3.2 → serializable-1.3.4}/serializable/dataclass_serializable.py +4 -9
  4. {serializable-1.3.2 → serializable-1.3.4}/serializable/helpers.py +19 -0
  5. serializable-1.3.4/serializable/keyword_aliases.py +57 -0
  6. {serializable-1.3.2 → serializable-1.3.4}/serializable/serializable.py +6 -12
  7. {serializable-1.3.2 → serializable-1.3.4}/serializable/version.py +1 -1
  8. {serializable-1.3.2 → serializable-1.3.4/serializable.egg-info}/PKG-INFO +54 -38
  9. {serializable-1.3.2 → serializable-1.3.4}/serializable.egg-info/SOURCES.txt +1 -0
  10. {serializable-1.3.2 → serializable-1.3.4}/tests/test_keyword_aliases.py +68 -1
  11. serializable-1.3.2/README.md +0 -70
  12. {serializable-1.3.2 → serializable-1.3.4}/LICENSE +0 -0
  13. {serializable-1.3.2 → serializable-1.3.4}/MANIFEST.in +0 -0
  14. {serializable-1.3.2 → serializable-1.3.4}/pyproject.toml +0 -0
  15. {serializable-1.3.2 → serializable-1.3.4}/serializable/__init__.py +0 -0
  16. {serializable-1.3.2 → serializable-1.3.4}/serializable/primitive_types.py +0 -0
  17. {serializable-1.3.2 → serializable-1.3.4}/serializable/py.typed +0 -0
  18. {serializable-1.3.2 → serializable-1.3.4}/serializable.egg-info/dependency_links.txt +0 -0
  19. {serializable-1.3.2 → serializable-1.3.4}/serializable.egg-info/requires.txt +0 -0
  20. {serializable-1.3.2 → serializable-1.3.4}/serializable.egg-info/top_level.txt +0 -0
  21. {serializable-1.3.2 → serializable-1.3.4}/setup.cfg +0 -0
  22. {serializable-1.3.2 → serializable-1.3.4}/tests/__init__.py +0 -0
  23. {serializable-1.3.2 → serializable-1.3.4}/tests/common.py +0 -0
  24. {serializable-1.3.2 → serializable-1.3.4}/tests/test_class.py +0 -0
  25. {serializable-1.3.2 → serializable-1.3.4}/tests/test_dataclass_serializable.py +0 -0
  26. {serializable-1.3.2 → serializable-1.3.4}/tests/test_default_to_dict_implementation.py +0 -0
  27. {serializable-1.3.2 → serializable-1.3.4}/tests/test_function.py +0 -0
  28. {serializable-1.3.2 → serializable-1.3.4}/tests/test_namedtuple.py +0 -0
  29. {serializable-1.3.2 → serializable-1.3.4}/tests/test_nesting.py +0 -0
  30. {serializable-1.3.2 → serializable-1.3.4}/tests/test_object.py +0 -0
  31. {serializable-1.3.2 → serializable-1.3.4}/tests/test_pickle.py +0 -0
  32. {serializable-1.3.2 → serializable-1.3.4}/tests/test_primitive_values.py +0 -0
  33. {serializable-1.3.2 → serializable-1.3.4}/tests/test_set.py +0 -0
  34. {serializable-1.3.2 → serializable-1.3.4}/tests/test_tuple.py +0 -0
  35. {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.2
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
- [![Tests](https://github.com/openvax/serializable/actions/workflows/tests.yml/badge.svg)](https://github.com/openvax/serializable/actions/workflows/tests.yml)
235
- <a href="https://pypi.python.org/pypi/serializable/">
236
- <img src="https://img.shields.io/pypi/v/serializable.svg?maxAge=1000" alt="PyPI" />
237
- </a>
234
+ [![Tests](https://github.com/openvax/serializable/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/openvax/serializable/actions/workflows/tests.yml)
235
+ [![Documentation](https://github.com/openvax/serializable/actions/workflows/documentation.yml/badge.svg?branch=master)](https://openvax.github.io/serializable/)
236
+ [![PyPI](https://img.shields.io/pypi/v/serializable.svg)](https://pypi.org/project/serializable/)
238
237
  [![PyPI downloads](https://img.shields.io/pypi/dm/serializable.svg)](https://pypistats.org/packages/serializable)
239
238
 
240
239
  # serializable
241
240
 
242
- Base class with serialization methods for user-defined Python objects
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
- ## `DataclassSerializable` for `@dataclass` subclasses
257
+ Python 3.11 or later is required.
251
258
 
252
- 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.
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
- p = Point(1.0, 2.0)
264
- assert Point.from_json(p.to_json()) == p
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
- 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.
268
-
269
- ## Usage
270
-
271
- Classes which inherit from `Serializable` are enabled with default implementations of
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
- - have a member data matching the name of each positional argument to `__init__`
278
- - provide a user-defined `to_dict()` method which returns a dictionary whose keys match the arguments to `__init__`
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
- 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.
289
+ ## Documentation
281
290
 
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.
291
+ The [documentation](https://openvax.github.io/serializable/) covers, in
292
+ reading order:
283
293
 
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).
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
- ## Limitations
302
+ ## Development
287
303
 
288
- The format records Python classes and modules, which must remain importable when
289
- loading data. Keys starting with two underscores are reserved. Nested Python
290
- values require the serialization helpers rather than ordinary JSON encoding.
291
- `to_json` raises `ValueError` for NaN or infinite floats, which standard JSON
292
- can't represent; `from_json` still reads the `NaN` / `Infinity` literals which
293
- older versions wrote.
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
- ## Documentation
314
+ Every PR bumps `serializable/version.py`. Maintainers release with
315
+ `./deploy.sh` from a clean `master`.
296
316
 
297
- - [Save classes, files and nested objects](docs/guides/objects.md)
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
- The site builds with `python -m pip install -r requirements-docs.txt` followed
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
+ [![Tests](https://github.com/openvax/serializable/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/openvax/serializable/actions/workflows/tests.yml)
2
+ [![Documentation](https://github.com/openvax/serializable/actions/workflows/documentation.yml/badge.svg?branch=master)](https://openvax.github.io/serializable/)
3
+ [![PyPI](https://img.shields.io/pypi/v/serializable.svg)](https://pypi.org/project/serializable/)
4
+ [![PyPI downloads](https://img.shields.io/pypi/dm/serializable.svg)](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
- kwargs = dict(state_dict)
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
- # check every class in the inheritance chain for its own
81
- # definition of _SERIALIZABLE_KEYWORD_ALIASES
82
- for klass in cls.mro():
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,4 +1,4 @@
1
- __version__ = "1.3.2"
1
+ __version__ = "1.3.4"
2
2
 
3
3
 
4
4
  def print_version():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: serializable
3
- Version: 1.3.2
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
- [![Tests](https://github.com/openvax/serializable/actions/workflows/tests.yml/badge.svg)](https://github.com/openvax/serializable/actions/workflows/tests.yml)
235
- <a href="https://pypi.python.org/pypi/serializable/">
236
- <img src="https://img.shields.io/pypi/v/serializable.svg?maxAge=1000" alt="PyPI" />
237
- </a>
234
+ [![Tests](https://github.com/openvax/serializable/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/openvax/serializable/actions/workflows/tests.yml)
235
+ [![Documentation](https://github.com/openvax/serializable/actions/workflows/documentation.yml/badge.svg?branch=master)](https://openvax.github.io/serializable/)
236
+ [![PyPI](https://img.shields.io/pypi/v/serializable.svg)](https://pypi.org/project/serializable/)
238
237
  [![PyPI downloads](https://img.shields.io/pypi/dm/serializable.svg)](https://pypistats.org/packages/serializable)
239
238
 
240
239
  # serializable
241
240
 
242
- Base class with serialization methods for user-defined Python objects
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
- ## `DataclassSerializable` for `@dataclass` subclasses
257
+ Python 3.11 or later is required.
251
258
 
252
- 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.
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
- p = Point(1.0, 2.0)
264
- assert Point.from_json(p.to_json()) == p
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
- 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.
268
-
269
- ## Usage
270
-
271
- Classes which inherit from `Serializable` are enabled with default implementations of
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
- - have a member data matching the name of each positional argument to `__init__`
278
- - provide a user-defined `to_dict()` method which returns a dictionary whose keys match the arguments to `__init__`
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
- 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.
289
+ ## Documentation
281
290
 
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.
291
+ The [documentation](https://openvax.github.io/serializable/) covers, in
292
+ reading order:
283
293
 
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).
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
- ## Limitations
302
+ ## Development
287
303
 
288
- The format records Python classes and modules, which must remain importable when
289
- loading data. Keys starting with two underscores are reserved. Nested Python
290
- values require the serialization helpers rather than ordinary JSON encoding.
291
- `to_json` raises `ValueError` for NaN or infinite floats, which standard JSON
292
- can't represent; `from_json` still reads the `NaN` / `Infinity` literals which
293
- older versions wrote.
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
- ## Documentation
314
+ Every PR bumps `serializable/version.py`. Maintainers release with
315
+ `./deploy.sh` from a clean `master`.
296
316
 
297
- - [Save classes, files and nested objects](docs/guides/objects.md)
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
- The site builds with `python -m pip install -r requirements-docs.txt` followed
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).
@@ -5,6 +5,7 @@ pyproject.toml
5
5
  serializable/__init__.py
6
6
  serializable/dataclass_serializable.py
7
7
  serializable/helpers.py
8
+ serializable/keyword_aliases.py
8
9
  serializable/primitive_types.py
9
10
  serializable/py.typed
10
11
  serializable/serializable.py
@@ -11,7 +11,11 @@
11
11
  # limitations under the License.
12
12
 
13
13
 
14
- from serializable import Serializable
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")
@@ -1,70 +0,0 @@
1
- [![Tests](https://github.com/openvax/serializable/actions/workflows/tests.yml/badge.svg)](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
- [![PyPI downloads](https://img.shields.io/pypi/dm/serializable.svg)](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