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.
- parityos_serialization-0.1.0/.gitignore +96 -0
- parityos_serialization-0.1.0/CHANGELOG.md +21 -0
- parityos_serialization-0.1.0/LICENSE.txt +37 -0
- parityos_serialization-0.1.0/PKG-INFO +59 -0
- parityos_serialization-0.1.0/README.md +40 -0
- parityos_serialization-0.1.0/docs/api/index.rst +12 -0
- parityos_serialization-0.1.0/docs/index.md +26 -0
- parityos_serialization-0.1.0/docs/user_guide/class_tagging/import_path_class_tagger.md +48 -0
- parityos_serialization-0.1.0/docs/user_guide/class_tagging/index.md +19 -0
- parityos_serialization-0.1.0/docs/user_guide/class_tagging/name_class_tagger.md +84 -0
- parityos_serialization-0.1.0/docs/user_guide/custom_classes/class_hooks.md +53 -0
- parityos_serialization-0.1.0/docs/user_guide/custom_classes/index.md +25 -0
- parityos_serialization-0.1.0/docs/user_guide/custom_classes/predicate_hooks.md +78 -0
- parityos_serialization-0.1.0/docs/user_guide/custom_classes/resolution_order.md +20 -0
- parityos_serialization-0.1.0/docs/user_guide/fallback/index.md +12 -0
- parityos_serialization-0.1.0/docs/user_guide/fallback/pickle.md +9 -0
- parityos_serialization-0.1.0/docs/user_guide/index.md +145 -0
- parityos_serialization-0.1.0/docs/user_guide/pass_through_classes.md +13 -0
- parityos_serialization-0.1.0/docs/user_guide/serialization_formats.md +20 -0
- parityos_serialization-0.1.0/pyproject.toml +43 -0
- parityos_serialization-0.1.0/src/parityos_serialization/__init__.py +5 -0
- parityos_serialization-0.1.0/src/parityos_serialization/_version.py +3 -0
- parityos_serialization-0.1.0/src/parityos_serialization/py.typed +0 -0
- parityos_serialization-0.1.0/src/parityos_serialization/serializer.py +996 -0
- parityos_serialization-0.1.0/src/parityos_serialization/utils/__init__.py +0 -0
- parityos_serialization-0.1.0/src/parityos_serialization/utils/class_tagger.py +508 -0
- parityos_serialization-0.1.0/src/parityos_serialization/utils/misc_utils.py +129 -0
- parityos_serialization-0.1.0/src/parityos_serialization/utils/typed_data_converter.py +150 -0
- parityos_serialization-0.1.0/src/parityos_serialization/utils/typedefs.py +221 -0
- parityos_serialization-0.1.0/test_parityos_serialization/__init__.py +0 -0
- parityos_serialization-0.1.0/test_parityos_serialization/conftest.py +61 -0
- parityos_serialization-0.1.0/test_parityos_serialization/helpers/__init__.py +0 -0
- parityos_serialization-0.1.0/test_parityos_serialization/helpers/custom_serializers.py +87 -0
- parityos_serialization-0.1.0/test_parityos_serialization/helpers/data_converter.py +63 -0
- parityos_serialization-0.1.0/test_parityos_serialization/helpers/dummy_classes.py +387 -0
- parityos_serialization-0.1.0/test_parityos_serialization/test_default_data_converter.py +695 -0
- parityos_serialization-0.1.0/test_parityos_serialization/test_serializer.py +796 -0
- parityos_serialization-0.1.0/test_parityos_serialization/test_version.py +105 -0
- parityos_serialization-0.1.0/test_parityos_serialization/utils/__init__.py +0 -0
- parityos_serialization-0.1.0/test_parityos_serialization/utils/test_class_tagger.py +438 -0
- parityos_serialization-0.1.0/test_parityos_serialization/utils/test_misc_utils.py +185 -0
- 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
|
+
:::
|