parityos-serialization 0.1.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.
Files changed (42) hide show
  1. parityos_serialization-0.1.0/.gitignore +96 -0
  2. parityos_serialization-0.1.0/CHANGELOG.md +21 -0
  3. parityos_serialization-0.1.0/LICENSE.txt +37 -0
  4. parityos_serialization-0.1.0/PKG-INFO +59 -0
  5. parityos_serialization-0.1.0/README.md +40 -0
  6. parityos_serialization-0.1.0/docs/api/index.rst +12 -0
  7. parityos_serialization-0.1.0/docs/index.md +26 -0
  8. parityos_serialization-0.1.0/docs/user_guide/class_tagging/import_path_class_tagger.md +48 -0
  9. parityos_serialization-0.1.0/docs/user_guide/class_tagging/index.md +19 -0
  10. parityos_serialization-0.1.0/docs/user_guide/class_tagging/name_class_tagger.md +84 -0
  11. parityos_serialization-0.1.0/docs/user_guide/custom_classes/class_hooks.md +53 -0
  12. parityos_serialization-0.1.0/docs/user_guide/custom_classes/index.md +25 -0
  13. parityos_serialization-0.1.0/docs/user_guide/custom_classes/predicate_hooks.md +78 -0
  14. parityos_serialization-0.1.0/docs/user_guide/custom_classes/resolution_order.md +20 -0
  15. parityos_serialization-0.1.0/docs/user_guide/fallback/index.md +12 -0
  16. parityos_serialization-0.1.0/docs/user_guide/fallback/pickle.md +9 -0
  17. parityos_serialization-0.1.0/docs/user_guide/index.md +145 -0
  18. parityos_serialization-0.1.0/docs/user_guide/pass_through_classes.md +13 -0
  19. parityos_serialization-0.1.0/docs/user_guide/serialization_formats.md +20 -0
  20. parityos_serialization-0.1.0/pyproject.toml +43 -0
  21. parityos_serialization-0.1.0/src/parityos_serialization/__init__.py +5 -0
  22. parityos_serialization-0.1.0/src/parityos_serialization/_version.py +3 -0
  23. parityos_serialization-0.1.0/src/parityos_serialization/py.typed +0 -0
  24. parityos_serialization-0.1.0/src/parityos_serialization/serializer.py +996 -0
  25. parityos_serialization-0.1.0/src/parityos_serialization/utils/__init__.py +0 -0
  26. parityos_serialization-0.1.0/src/parityos_serialization/utils/class_tagger.py +508 -0
  27. parityos_serialization-0.1.0/src/parityos_serialization/utils/misc_utils.py +129 -0
  28. parityos_serialization-0.1.0/src/parityos_serialization/utils/typed_data_converter.py +150 -0
  29. parityos_serialization-0.1.0/src/parityos_serialization/utils/typedefs.py +221 -0
  30. parityos_serialization-0.1.0/test_parityos_serialization/__init__.py +0 -0
  31. parityos_serialization-0.1.0/test_parityos_serialization/conftest.py +61 -0
  32. parityos_serialization-0.1.0/test_parityos_serialization/helpers/__init__.py +0 -0
  33. parityos_serialization-0.1.0/test_parityos_serialization/helpers/custom_serializers.py +87 -0
  34. parityos_serialization-0.1.0/test_parityos_serialization/helpers/data_converter.py +63 -0
  35. parityos_serialization-0.1.0/test_parityos_serialization/helpers/dummy_classes.py +387 -0
  36. parityos_serialization-0.1.0/test_parityos_serialization/test_default_data_converter.py +695 -0
  37. parityos_serialization-0.1.0/test_parityos_serialization/test_serializer.py +796 -0
  38. parityos_serialization-0.1.0/test_parityos_serialization/test_version.py +105 -0
  39. parityos_serialization-0.1.0/test_parityos_serialization/utils/__init__.py +0 -0
  40. parityos_serialization-0.1.0/test_parityos_serialization/utils/test_class_tagger.py +438 -0
  41. parityos_serialization-0.1.0/test_parityos_serialization/utils/test_misc_utils.py +185 -0
  42. parityos_serialization-0.1.0/test_parityos_serialization/utils/test_typed_data_converter.py +66 -0
@@ -0,0 +1,96 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ .Python
8
+ build/
9
+ develop-eggs/
10
+ dist/
11
+ downloads/
12
+ eggs/
13
+ .eggs/
14
+ lib/
15
+ lib64/
16
+ parts/
17
+ sdist/
18
+ var/
19
+ wheels/
20
+ pip-wheel-metadata/
21
+ share/python-wheels/
22
+ *.egg-info/
23
+ .installed.cfg
24
+ *.egg
25
+ MANIFEST
26
+
27
+ # Installer logs
28
+ pip-log.txt
29
+ pip-delete-this-directory.txt
30
+
31
+ # Unit test / coverage reports
32
+ htmlcov/
33
+ .tox/
34
+ .nox/
35
+ .coverage
36
+ .coverage.*
37
+ .cache
38
+ coverage.xml
39
+ report.xml
40
+ *.cover
41
+ *.py,cover
42
+ .hypothesis/
43
+ .pytest_cache/
44
+
45
+ # Sphinx documentation
46
+ **/docs/_build/
47
+ **/docs/**/generated/
48
+
49
+ # Jupyter Notebook
50
+ .ipynb_checkpoints
51
+
52
+ # pyenv
53
+ .python-version
54
+
55
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
56
+ __pypackages__/
57
+
58
+ # Celery
59
+ celerybeat-schedule
60
+ celerybeat.pid
61
+
62
+ # Environments
63
+ *.env
64
+ .venv
65
+ env/
66
+ venv/
67
+ ENV/
68
+ env.bak/
69
+ venv.bak/
70
+ uv.lock
71
+
72
+ # mypy
73
+ .mypy_cache/
74
+ .dmypy.json
75
+ dmypy.json
76
+
77
+ # Pyre type checker
78
+ .pyre/
79
+
80
+ # IDE stuff
81
+ *.iws
82
+ *.iml
83
+ **/.idea/
84
+ **/.idea/**
85
+ **/.vscode
86
+
87
+ # Apple stuff
88
+ .DS_Store
89
+
90
+ # artifacts
91
+ .png
92
+ .tar
93
+
94
+ # uv
95
+ uv.toml
96
+
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+
9
+ ## [0.1.0] - 2026-06-10
10
+
11
+ ### Added
12
+
13
+ - More flexible tag creation and de-/reregistration (#139)
14
+ - Allow extra keywords during deserialization (#147)
15
+ - Remove registered cls from pass through containers (#151)
16
+ - Hooks take serializer (#159)
17
+ - Deserialization from data missing default values fails (#160)
18
+ - Convenience classes for different serialization tasks (#162)
19
+ - More flexible mapping serialization (#163)
20
+ - Allow editing of pass through classes (#165)
21
+ - Allow flexible fall back strategies (#166)
@@ -0,0 +1,37 @@
1
+ ParityOS Client Software License Terms (BSD-3-Clause)
2
+ =====================================================
3
+
4
+ Copyright 2023 Parity Quantum Computing GmbH (ParityQC)
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice,
10
+ this list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its contributors
17
+ may be used to endorse or promote products derived from this software
18
+ without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS”
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ ===============================================================================
32
+
33
+ Parity Quantum Computing GmbH
34
+ Rennweg 1 / Top 314 / 6020 Innsbruck, Austria
35
+ info@parityqc.com / www.parityqc.com
36
+
37
+ ===============================================================================
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.4
2
+ Name: parityos-serialization
3
+ Version: 0.1.0
4
+ Summary: Simple automatic (de)serialization of custom classes
5
+ Project-URL: Documentation, https://docs.parityqc.com/parityos-serialization/latest
6
+ Project-URL: Homepage, https://parityqc.com/
7
+ Author-email: ParityQC <parityos@parityqc.com>
8
+ License-Expression: BSD-3-Clause
9
+ License-File: LICENSE.txt
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.11
16
+ Requires-Dist: attrs<26.0
17
+ Requires-Dist: typing-extensions
18
+ Description-Content-Type: text/markdown
19
+
20
+ # ParityOS Serialization
21
+
22
+ A simple automatic serialization library that is largely inspired by [cattrs](https://github.com/python-attrs/cattrs). It shares *cattrs*'s philosophy of being non-intrusive and not requiring subclassing or abstract method implementation. Instead it relies on automatic object structure deduction for [attrs](https://github.com/python-attrs/attrs) classes or dataclasses and custom (de)serialization hooks which can be registered for custom classes. This is similar to [json](https://docs.python.org/3/library/json.html#module-json)'s `default` parameter to [`dump`](https://docs.python.org/3/library/json.html#json.dump) and `object_hook` parameter to [`load`](https://docs.python.org/3/library/json.html#json.load).
23
+
24
+ However with respect to *cattrs* it comes with some significant differences:
25
+
26
+ - All objects are serialized together with a unique class tag and the serializer relies on this information for deserialization, rather than type annotations of target fields. This alleviates many troubles *cattrs* has with deserializing subclasses of types used as type annotations.
27
+ - It inspects the object to serialize, not just its type, which allows e.g. serialization of classes themselves.
28
+ - It doesn't care about generics and typevars as all concrete type information is recorded in the serialized data. This especially allows for greater flexibility with using subclasses of generics.
29
+
30
+ For more information see the [documentation](https://docs.parityqc.com/parityos-serialization/latest)
31
+
32
+ ## Installation
33
+
34
+ It is recommended to install this package in a separate Python virtual environment. For example:
35
+
36
+ ```shell
37
+ # To create a standard Python virtual environment:
38
+ python -m venv my_new_venv && source my_new_venv/bin/activate
39
+ # Alternatively, to create a Anaconda/Miniconda environment:
40
+ conda create --name my_new_conda_env python=<version> && conda activate my_new_conda_env
41
+ # or a pyenv environment
42
+ pyenv virtualenv <version> my_new_venv && pyenv activate my_new_venv
43
+ # or a uv managed environment
44
+ uv venv -p <version>
45
+ ```
46
+ where `<version>` is a python version and one of `[3.11, 3.12, 3.13]`.
47
+
48
+ After activating the virtual environment, install via your favorite package manager, e.g.:
49
+
50
+ ```shell
51
+ # using pip
52
+ pip install parityos-serialization
53
+ # using uv
54
+ uv pip install parityos-serialization
55
+ ```
56
+
57
+ ## License
58
+
59
+ This software package is made available under the 3-Clause BSD License. See `License.txt` for details.
@@ -0,0 +1,40 @@
1
+ # ParityOS Serialization
2
+
3
+ A simple automatic serialization library that is largely inspired by [cattrs](https://github.com/python-attrs/cattrs). It shares *cattrs*'s philosophy of being non-intrusive and not requiring subclassing or abstract method implementation. Instead it relies on automatic object structure deduction for [attrs](https://github.com/python-attrs/attrs) classes or dataclasses and custom (de)serialization hooks which can be registered for custom classes. This is similar to [json](https://docs.python.org/3/library/json.html#module-json)'s `default` parameter to [`dump`](https://docs.python.org/3/library/json.html#json.dump) and `object_hook` parameter to [`load`](https://docs.python.org/3/library/json.html#json.load).
4
+
5
+ However with respect to *cattrs* it comes with some significant differences:
6
+
7
+ - All objects are serialized together with a unique class tag and the serializer relies on this information for deserialization, rather than type annotations of target fields. This alleviates many troubles *cattrs* has with deserializing subclasses of types used as type annotations.
8
+ - It inspects the object to serialize, not just its type, which allows e.g. serialization of classes themselves.
9
+ - It doesn't care about generics and typevars as all concrete type information is recorded in the serialized data. This especially allows for greater flexibility with using subclasses of generics.
10
+
11
+ For more information see the [documentation](https://docs.parityqc.com/parityos-serialization/latest)
12
+
13
+ ## Installation
14
+
15
+ It is recommended to install this package in a separate Python virtual environment. For example:
16
+
17
+ ```shell
18
+ # To create a standard Python virtual environment:
19
+ python -m venv my_new_venv && source my_new_venv/bin/activate
20
+ # Alternatively, to create a Anaconda/Miniconda environment:
21
+ conda create --name my_new_conda_env python=<version> && conda activate my_new_conda_env
22
+ # or a pyenv environment
23
+ pyenv virtualenv <version> my_new_venv && pyenv activate my_new_venv
24
+ # or a uv managed environment
25
+ uv venv -p <version>
26
+ ```
27
+ where `<version>` is a python version and one of `[3.11, 3.12, 3.13]`.
28
+
29
+ After activating the virtual environment, install via your favorite package manager, e.g.:
30
+
31
+ ```shell
32
+ # using pip
33
+ pip install parityos-serialization
34
+ # using uv
35
+ uv pip install parityos-serialization
36
+ ```
37
+
38
+ ## License
39
+
40
+ This software package is made available under the 3-Clause BSD License. See `License.txt` for details.
@@ -0,0 +1,12 @@
1
+ ..
2
+ comment: This file only exists to trigger the recursive autosummary generation, it is not linked
3
+ in any TOC or referenced in any other markdown file.
4
+
5
+ :orphan:
6
+
7
+ .. autosummary::
8
+ :toctree: generated
9
+ :recursive:
10
+ :nosignatures:
11
+
12
+ parityos_serialization
@@ -0,0 +1,26 @@
1
+ # ParityOS Serialization
2
+
3
+ A simple automatic serialization library that is largely inspired by [cattrs](https://github.com/python-attrs/cattrs). It shares *cattrs*'s philosophy of being non-intrusive and not requiring subclassing or abstract method implementation. Instead it relies on automatic object structure deduction for [attrs](https://github.com/python-attrs/attrs) classes or dataclasses and custom (de)serialization hooks which can be registered for custom classes. This is similar to [json](https://docs.python.org/3/library/json.html#module-json)'s `default` parameter to [`dump`](https://docs.python.org/3/library/json.html#json.dump) and `object_hook` parameter to [`load`](https://docs.python.org/3/library/json.html#json.load).
4
+
5
+ However with respect to *cattrs* it comes with some significant differences:
6
+
7
+ - All objects are serialized together with a unique class tag and the serializer relies on this information for deserialization, rather than type annotations of target fields. This alleviates many troubles *cattrs* has with deserializing subclasses of types used as type annotations.
8
+ - It inspects the object to serialize, not just its type, which allows e.g. serialization of classes themselves.
9
+ - It doesn't care about generics and typevars as all concrete type information is recorded in the serialized data. This especially allows for greater flexibility with using subclasses of generics.
10
+
11
+
12
+ ```{toctree}
13
+ :maxdepth: 2
14
+ :hidden:
15
+ :caption: Documentation
16
+
17
+ user_guide/index
18
+ ```
19
+
20
+ ```{toctree}
21
+ :maxdepth: 2
22
+ :hidden:
23
+ :caption: API Reference
24
+
25
+ api/generated/parityos_serialization
26
+ ```
@@ -0,0 +1,48 @@
1
+ # ImportPathClassTagger
2
+
3
+ To remove the necessity for class registration and a mapping between class tags and classes, the {any}`ImportPathClassTagger` uses absolute import paths as class tags. This enables bootstrapping at deserialization by gradually importing all needed classes without having to know and import all involved classes beforehand. This convenience comes at the price of brittleness of serialized data against code base refactoring, especially against moving and renaming of classes. This is similar to {any}`pickle` which suffers from the same brittleness. However while pickle files are binary and import paths of involved objects cannot be retrieved or changed, the output of the Serializer is human readable and lends itself to easy refactoring if import paths that have changed between code base versions.
4
+
5
+ ```python
6
+ # Example 6:
7
+ from attrs import frozen
8
+ from parityos_serialization.serializer import DeterministicJsonSerializer
9
+ from parityos_serialization.utils.class_tagger import ImportPathClassTagger
10
+
11
+ serializer = DeterministicJsonSerializer(class_tagger=ImportPathClassTagger())
12
+
13
+
14
+ @frozen
15
+ class Foo:
16
+ bar: int
17
+ baz: str
18
+
19
+
20
+ @frozen
21
+ class Bar(Foo):
22
+ fox: dict[str, int]
23
+
24
+
25
+ obj = Bar(42, "qux", {"a": 22})
26
+ dct = serializer.serialize(obj)
27
+ obj2 = serializer.deserialize(dct)
28
+ assert obj == obj2
29
+ print(dct)
30
+ # {
31
+ # "_data": {
32
+ # "bar": 42,
33
+ # "baz": "qux",
34
+ # "fox": {
35
+ # "_data": [["a", 22]],
36
+ # "_cls": "builtins.dict",
37
+ # },
38
+ # },
39
+ # "_cls": "__main__.Bar",
40
+ # }
41
+ ```
42
+ Here, no registration of any custom classes is necessary. In turn, the class tags contain the full import path of the serialized class objects.
43
+
44
+ Notably, this strategy also works in cases where the target classes have not been imported before deserialization.
45
+
46
+ :::{caution}
47
+ Be sure you know and trust the content of the data to be deserialized using an {any}`ImportPathClassTagger`, as it can import arbitrary modules which might contain and execute unwanted code.
48
+ :::
@@ -0,0 +1,19 @@
1
+ # Class Tagging Strategies
2
+ ```{toctree}
3
+ :hidden:
4
+
5
+ name_class_tagger
6
+ import_path_class_tagger
7
+ ```
8
+
9
+ The `class_tagger` in a {any}`Serializer` is responsible for
10
+ - generating class tags from objects for serialization
11
+ - determining the target class from a class tag in the serialized data for deserialization.
12
+
13
+ The {any}`DeterministicJsonSerializer` uses a {any}`NameClassTagger` as default, which generates tags based on class names and keeps an internal mapping between tags and imported classes. Other strategies can be used by passing other class objects that implement the {any}`ClassTagger` interface for the `class_tagger` parameter.
14
+
15
+ This library supplies two built-in class taggers
16
+ - {any}`NameClassTagger`
17
+ - {any}`ImportPathClassTagger`
18
+
19
+ In the {ref}`Example 2 <example-nested>` we needed to register custom classes `AFoo`, `DFoo`, `Bar`, `Parity` with the serializer's {any}`NameClassTagger`. See {doc}`NameClassTagger <name_class_tagger>` for details and {doc}`ImportPathClassTagger <import_path_class_tagger>` for a strategy where this step is not necessary. {any}`DeterministicJsonSerializer` however uses {any}`NameClassTagger` as default as it is more robust against moving classes during refactoring.
@@ -0,0 +1,84 @@
1
+ # NameClassTagger
2
+
3
+ It uses the *class name* of the object as a class tag and keeps an internal mapping between classes and class tags. Classes have to be explicitly registered with the class tagger by either passing a sequence of classes to the constructor or by using the {any}`register_classes <NameClassTagger.register_classes>` method. Pre-registered classes can be unregistered from an existing tagger with the {any}`deregister_classes <NameClassTagger.deregister_classes>` method.
4
+
5
+ The following built-in python classes are pre-registered:
6
+ - {any}`object`
7
+ - {any}`type`
8
+ - {any}`GenericAlias <types.GenericAlias>`
9
+ - {any}`set`
10
+ - {any}`frozenset`
11
+ - {any}`list`
12
+ - {any}`tuple`
13
+ - {any}`dict`
14
+ - {any}`MappingProxyType <types.MappingProxyType>`
15
+
16
+ Non-built-in classes must be registered first with the class tagger before they can be deserialized.
17
+
18
+ ```python
19
+ # Example 4:
20
+ from attrs import frozen
21
+ from parityos_serialization.serializer import DeterministicJsonSerializer
22
+
23
+
24
+ @frozen
25
+ class Foo:
26
+ bar: int
27
+ baz: str
28
+
29
+
30
+ serializer = DeterministicJsonSerializer()
31
+ serializer.class_tagger.register_classes(Foo)
32
+ obj = Foo(42, "qux")
33
+ dct = serializer.serialize(obj)
34
+ obj2 = serializer.deserialize(dct)
35
+ assert obj == obj2
36
+ print(dct)
37
+ # {'_data': {'bar': 42, 'baz': 'qux'}, '_cls': 'Foo'}
38
+ ```
39
+
40
+ As it can be daunting and unpractical to register all classes explicitly, there is a class decorator that enables automatic registration of the decorated class together with all its subclasses down its inheritance tree:
41
+
42
+ ```python
43
+ # Example 5:
44
+ from attrs import frozen
45
+ from parityos_serialization.serializer import DeterministicJsonSerializer
46
+ from parityos_serialization.utils.class_tagger import NameClassTagger, register
47
+
48
+ class_tagger = NameClassTagger()
49
+ serializer = DeterministicJsonSerializer(class_tagger=class_tagger)
50
+
51
+
52
+ @register(class_tagger)
53
+ @frozen
54
+ class Foo:
55
+ bar: int
56
+ baz: str
57
+
58
+
59
+ @frozen
60
+ class Bar(Foo):
61
+ fox: dict[str, int]
62
+
63
+
64
+ obj = Bar(42, "qux", {"a": 22})
65
+ dct = serializer.serialize(obj)
66
+ obj2 = serializer.deserialize(dct)
67
+ assert obj == obj2
68
+ print(dct)
69
+ # {
70
+ # "_data": {
71
+ # "bar": 42,
72
+ # "baz": "qux",
73
+ # "fox": {
74
+ # "_data": [["a", 22]],
75
+ # "_cls": "dict",
76
+ # },
77
+ # },
78
+ # "_cls": "Bar",
79
+ # }
80
+ ```
81
+
82
+ You can see that no explicit call to {any}`serializer.class_tagger.register_classes <NameClassTagger.register_classes>` was necessary. However, the class tagger object needs to be available at definition time of the classes to register for passing it as argument to the {any}`register <parityos_serialization.utils.class_tagger.register>` decorator. It is therefore often advisable to use a module constant class tagger and serializer for such purposes.
83
+
84
+ At deserialization time all classes present in the serialized data must be registered with the class tagger. Consequently, all modules that contain code for registering involved classes must be imported before deserialization, even if these classes do not explicitly appear in the file that calls the deserialization.
@@ -0,0 +1,53 @@
1
+ # Class Hooks
2
+ To add support for e.g. {any}`complex` we register class hooks using {any}`register_class_serialization_hook <Serializer.register_class_serialization_hook>` and {any}`register_class_deserialization_hook <Serializer.register_class_deserialization_hook>`.
3
+
4
+ ```python
5
+ # Example 7:
6
+ from parityos_serialization.serializer import (
7
+ DeterministicJsonSerializer,
8
+ Serializer,
9
+ )
10
+ from parityos_serialization.utils.class_tagger import NameClassTagger
11
+ from parityos_serialization.utils.typedefs import (
12
+ TypedDataDict,
13
+ )
14
+
15
+
16
+ # serialization hook
17
+ def serialize_complex(
18
+ x: complex, _serializer: Serializer[TypedDataDict, NameClassTagger]
19
+ ) -> dict[str, float]:
20
+ return {"real": x.real, "imag": x.imag}
21
+
22
+
23
+ # deserialization hook
24
+ def deserialize_complex(
25
+ data: dict[str, float],
26
+ _: type[complex],
27
+ _serializer: Serializer[TypedDataDict, NameClassTagger],
28
+ ) -> complex:
29
+ return complex(data["real"], data["imag"])
30
+
31
+
32
+ serializer = DeterministicJsonSerializer()
33
+ serializer.register_class_serialization_hook(complex, serialize_complex)
34
+ serializer.register_class_deserialization_hook(complex, deserialize_complex)
35
+ serializer.class_tagger.register_classes(complex)
36
+
37
+ obj = complex(4, 2)
38
+ dct = serializer.serialize(obj)
39
+ obj2 = serializer.deserialize(dct)
40
+ assert obj == obj2
41
+ print(dct)
42
+ # {'_data': {'real': 4.0, 'imag': 2.0}, '_cls': 'complex'}
43
+ ```
44
+ Here we have registered hooks that are used for all objects of type {any}`complex`.
45
+
46
+ A *class serialization hook* is a function that takes an object of the registered class `T` and a serializer of class `S` and returns the serialized **data**.
47
+ This structure is formalized in the {any}`SerializeHook` type alias. The class tag will be injected by the serializer automatically, so the hook does not need to do this.
48
+
49
+ A *class deserialization hook* is a function that takes the serialized data, the target class `type[T]` and a serializer of type `S` and returns an **object of type `T`** constructed from the serialized data. This structure is formalized in the {any}`DeserializeHook` type alias. The unpacking of the class tag is done by the serializer automatically and the deserialization hook will receive already unpacked serialized data and the target class `type[T]`.
50
+
51
+ Registering a class hook for a class that already has a hook registered overwrites the old hook.
52
+
53
+ Class hook resolution uses {any}`singledispatch <functools.singledispatch>`'s resolution method based on the object's {any}`mro <type.__mro__>`. For a mechanism to register hooks more broadly across many classes, see {doc}`Predicate Hooks <predicate_hooks>`.
@@ -0,0 +1,25 @@
1
+ # Custom classes
2
+ ```{toctree}
3
+ :hidden:
4
+
5
+ class_hooks
6
+ predicate_hooks
7
+ resolution_order
8
+ ```
9
+
10
+ Any classes that are not {doc}`pass-through classes <../pass_through_classes>` must be handled by registering hooks for them. {any}`DeterministicJsonSerializer` has some predefined hooks registered and thus supports (de)serialization of the following built-in types out of the box:
11
+
12
+ | class | output |
13
+ | --- | --- |
14
+ |{any}`tuple` |{any}`list`|
15
+ |{any}`set` |sorted {any}`list`|
16
+ |{any}`frozenset` |sorted {any}`list`|
17
+ |{any}`dict` |sorted {any}`list` of (key, value) pair {any}`list`s|
18
+ |{any}`MappingProxyType <types.MappingProxyType>`|sorted {any}`list` of (key, value) pair {any}`list`s|
19
+ |{any}`Enum <enum.Enum>` |element name as {any}`str`|
20
+ |{any}`type` |class name as {any}`str`|
21
+ |{any}`GenericAlias <types.GenericAlias>` |{any}`dict` with fields "origin" and "args" containing the class names of origin and args classes|
22
+ |{any}`dataclass <dataclasses.dataclass>` |{any}`dict` with serialized fields from {any}`dataclasses.fields`|
23
+ |{any}`attrs` classes |`dict` with serialized fields from {any}`attrs.fields`|
24
+
25
+ For any custom classes not belonging to the built-in supported classes, {doc}`class hooks <class_hooks>` or {doc}`predicate hooks <predicate_hooks>` facilitating their (de)serialization can be registered with the serializer.
@@ -0,0 +1,78 @@
1
+ # Predicate Hooks
2
+ In contrast to {doc}`Class Hooks <class_hooks>`, predicate hooks enable registering hooks more broadly based on class or object properties, rather than just the class and its inheritance tree itself. This is done through registering boolean predicate functions together with (de)serialization hooks. They receive the object, inspect it and return `True` if the corresponding hook should be used for the object. A prime usecase example for this mechanism are {any}`DeterministicJsonSerializer`'s default hooks registered for {any}`dataclasses <dataclasses.dataclass>` and {any}`attrs` classes, which immediately catches all such class objects by inspecting whether they are dataclasses or attrs classes. Otherwise, an individual class hook would be needed for every single dataclass or attrs class, while a predicate hook can catch all such classes in one go.
3
+
4
+ (example-predicate-hook)=
5
+ ```python
6
+ # Example 8:
7
+ from typing import Any, Protocol, Self
8
+
9
+ from parityos_serialization.serializer import DeterministicJsonSerializer, Serializer
10
+ from parityos_serialization.utils.class_tagger import NameClassTagger
11
+ from parityos_serialization.utils.typedefs import TypedDataDict
12
+ from typing_extensions import override
13
+
14
+
15
+ class PointLike2D(Protocol):
16
+ x: int
17
+ y: int
18
+
19
+ @classmethod
20
+ def from_2d_coordinates(cls, x: int, y: int) -> Self: ...
21
+
22
+
23
+ class Point2D:
24
+ x: int
25
+ y: int
26
+
27
+ def __init__(self, x: int, y: int) -> None:
28
+ self.x = x
29
+ self.y = y
30
+
31
+ @classmethod
32
+ def from_2d_coordinates(cls, x: int, y: int):
33
+ return cls(x, y)
34
+
35
+ @override
36
+ def __eq__(self, other: object) -> bool:
37
+ return vars(self) == vars(other)
38
+
39
+
40
+ # serialization predicate
41
+ def is_pointlike_obj(obj: object) -> bool:
42
+ return hasattr(obj, "x") and hasattr(obj, "y")
43
+
44
+
45
+ # serialization hook
46
+ def serialize_pointlike(
47
+ obj: PointLike2D, _serializer: Serializer[TypedDataDict, NameClassTagger]
48
+ ) -> dict[str, int]:
49
+ return {"x": obj.x, "y": obj.y}
50
+
51
+
52
+ # deserialization predicate
53
+ def is_pointlike_data(data: Any, _: type):
54
+ return isinstance(data, dict) and data.keys() == {"x", "y"}
55
+
56
+
57
+ # deserialization hook
58
+ def deserialize_pointlike(
59
+ data: dict[str, int],
60
+ cls: type[PointLike2D],
61
+ _serializer: Serializer[TypedDataDict, NameClassTagger],
62
+ ) -> PointLike2D:
63
+ return cls.from_2d_coordinates(data["x"], data["y"])
64
+
65
+
66
+ serializer = DeterministicJsonSerializer()
67
+ serializer.register_predicate_serialization_hook(is_pointlike_obj, serialize_pointlike)
68
+ serializer.register_predicate_deserialization_hook(is_pointlike_data, deserialize_pointlike)
69
+ serializer.class_tagger.register_classes(Point2D)
70
+
71
+ obj = Point2D(4, 2)
72
+ dct = serializer.serialize(obj)
73
+ obj2 = serializer.deserialize(dct)
74
+ assert obj == obj2
75
+ print(dct)
76
+ # {'_data': {'x': 4, 'y': 2}, '_cls': 'Point2D'}
77
+ ```
78
+ You can imagine e.g. implementing a `Point3D` (whose `from_2d_coordinates` classmethod would just set the `z` coordinate to zero) which would also be supported by these hooks.
@@ -0,0 +1,20 @@
1
+ # Hook Resolution Order
2
+ Class hooks are evaluated before Predicate Hooks. Class hooks work by dictionary lookup using {any}`functools.singledispatch`'s resolution algorithm. Predicate-hook pairs are registered and stored in an ordered sequence and are evaluated in reverse order of registration, i.e. most recently registered predicates are evaluated first. This enables a hierarchical organization of predicate hook pairs from fine grained to increasing coarseness.
3
+
4
+ ## Serialization
5
+ Objects are serialized in the following order of preference:
6
+
7
+ 1. If the *object* is an instance of one of the registered {any}`pass_through_classes <Serializer.pass_through_classes>`, the object is returned unaltered.
8
+ 2. If the *object's class* or any of its base classes has a *class serialization hook* registered for it, the corresponding hook is used.
9
+ 3. If any of the registered *serialization predicates* evaluate to `True` on the object, its corresponding serialization hook is used.
10
+ 4. If none of the above apply, the *fallback strategy* is applied
11
+
12
+
13
+ ## Deserialization
14
+ Likewise, objects are deserialized in the following order of preference, where `cls` is the target class corresponding to the class tag contained in the serialized data:
15
+
16
+ 1. If the *serialized data* is an instance of one of the registered {any}`pass_through_classes <Serializer.pass_through_classes>`, the data is returned unaltered.
17
+ 2. If `cls` or any of its base classes has a *class deserialization hook* registered for it, the corresponding hook is used.
18
+ 3. If any of the registered *deserialization predicates* evaluate to `True` on `cls` or the serialized data, the corresponding deserialization hook is used.
19
+ 4. If none of the above apply, the *fallback strategy* is applied
20
+
@@ -0,0 +1,12 @@
1
+ # Fallback Strategies
2
+ ```{toctree}
3
+ :hidden:
4
+
5
+ pickle
6
+ ```
7
+ The `fallback_strategy` parameter of the {any}`Serializer` defines the strategy for dealing with objects that are not pass-through classes or aren't caught by any hooks. This library offers a few preconfigured strategies as variants of the {any}`FallBackStrategy` enum:
8
+ - `FallBackStrategy.RAISE`: raise an error for objects that are not covered otherwise.
9
+ - `FallBackStrategy.PASS_THROUGH`: treat uncaught objects as pass-through classes.
10
+ - `FallBackStrategy.PICKLE_INTERFACE`: use catch-all hooks utilizing the pickle interface (see {doc}`Pickle Fallback Strategy <pickle>`).
11
+
12
+ Alternatively, serializers can be initialized with a custom pair of fallback catch-all serialization and deserialization hooks, packaged in a {any}`HookPair` object.
@@ -0,0 +1,9 @@
1
+ # Pickle Fallback Strategy
2
+
3
+ The pickle fallback strategy uses predefined hooks that utilize {any}`__getstate__ <object.__getstate__>` for serialization and a custom implemented {any}`__setstate__ <object.__setstate__>` if available or otherwise pickle's default strategy for restoring an object's state. Both slotted and non-slotted classes are supported.
4
+
5
+ For more information about stateful objects and the pickle serialization interface, see also how to [handle stateful objects](https://docs.python.org/3/library/pickle.html#handling-stateful-objects).
6
+
7
+ :::{caution}
8
+ Pickle's default restoration strategy for dict classes just updates an object's `__dict__` attribute. There is no stable way to check which attributes a dict class expects like in slotted classes, as a newly created object has an empty `__dict__`. A dict class can therefore be restored with any `dict[str, Any]` without errors, but the resulting class object might be broken with missing or extra `__dict__` fields. The serializer thus has no way to ensure a healthy object when restoring dict classes using the pickle fallback strategy. We recommend using attrs classes, dataclasses or pure slotted classes instead.
9
+ :::