Skip to content

Commit b2211ba

Browse files
feat(serdes): complete polish - Error rename, optimized bit I/O, enriched errors, docs, demo
1 parent da494f2 commit b2211ba

9 files changed

Lines changed: 338 additions & 103 deletions

File tree

.github/workflows/test-and-release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,12 @@ jobs:
4646
- name: Run build and test
4747
run: |
4848
if [ "$RUNNER_OS" == "Linux" ]; then
49-
nox --non-interactive --error-on-missing-interpreters --session test pristine lint --python ${{ matrix.python }}
49+
nox --non-interactive --error-on-missing-interpreters --session test pristine lint demo --python ${{ matrix.python }}
5050
nox --non-interactive --session docs
5151
elif [ "$RUNNER_OS" == "Windows" ]; then
5252
nox --forcecolor --non-interactive --session test pristine lint
5353
elif [ "$RUNNER_OS" == "macOS" ]; then
54-
nox --non-interactive --error-on-missing-interpreters --session test pristine lint --python ${{ matrix.python }}
54+
nox --non-interactive --error-on-missing-interpreters --session test pristine lint demo --python ${{ matrix.python }}
5555
else
5656
echo "${{ runner.os }} not supported"
5757
exit 1

demo/DemoMessage.1.0.dsdl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# DemoMessage.1.0.dsdl
2+
# Self-contained demo type for serialization/deserialization demonstration
3+
4+
bool flag
5+
uint32 counter
6+
float64 temperature
7+
float32[4] numeric_data
8+
uint8[<=256] text_data # UTF-8 encoded text data
9+
uint8[<=64] binary_data # Raw binary data
10+
11+
@extent 1024 * 8

demo/demo_serdes.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Self-contained demo of pydsdl serialization and deserialization.
4+
5+
Demonstrates the complete workflow:
6+
1. Load a custom DSDL type using read_files()
7+
2. Create an object using the representation convention (dict, list, str, bytes, primitives)
8+
3. Serialize the object to bytes
9+
4. Deserialize the bytes back to an object
10+
5. Verify roundtrip equality
11+
"""
12+
13+
import pydsdl
14+
from pathlib import Path
15+
16+
17+
def main() -> None:
18+
SCRIPT_DIR = Path(__file__).parent
19+
DSDL_FILE = SCRIPT_DIR / "DemoMessage.1.0.dsdl"
20+
21+
print("=" * 70)
22+
print("PyDSDL Serialization/Deserialization Demo")
23+
print("=" * 70)
24+
25+
print("\n[Step 1] Loading DSDL type from:", DSDL_FILE.name)
26+
27+
types, _ = pydsdl.read_files(
28+
dsdl_files=DSDL_FILE,
29+
root_namespace_directories_or_names=SCRIPT_DIR,
30+
lookup_directories=[]
31+
)
32+
33+
schema = types[0]
34+
print(f"✓ Loaded type: {schema.full_name} v{schema.version.major}.{schema.version.minor}")
35+
print(f" Fields: {[f.name for f in schema.fields_except_padding]}")
36+
37+
print("\n[Step 2] Creating example object")
38+
print(" Representation convention: dict→struct, list→array, primitives as-is")
39+
obj = {
40+
"flag": True,
41+
"counter": 42,
42+
"temperature": 23.5,
43+
"numeric_data": [1.0, 2.0, 3.0, 4.0],
44+
"text_data": list("Hello, SerDes!".encode("utf-8")),
45+
"binary_data": [0x00, 0x01, 0x02, 0x03]
46+
}
47+
48+
print(" Object structure:")
49+
for key, value in obj.items():
50+
value_repr = repr(value)
51+
if len(value_repr) > 50:
52+
value_repr = value_repr[:47] + "..."
53+
print(f" {key:15} = {value_repr} ({type(value).__name__})")
54+
55+
print("\n[Step 3] Serializing object to bytes")
56+
serialized_data = pydsdl.serialize(schema, obj)
57+
print(f"✓ Serialized to {len(serialized_data)} bytes")
58+
print(f" Hex: {serialized_data.hex()}")
59+
60+
print("\n[Step 4] Deserializing bytes back to object")
61+
deserialized = pydsdl.deserialize(schema, serialized_data)
62+
print("✓ Deserialized successfully")
63+
print(" Deserialized structure:")
64+
for key, value in deserialized.items():
65+
value_repr = repr(value)
66+
if len(value_repr) > 50:
67+
value_repr = value_repr[:47] + "..."
68+
print(f" {key:15} = {value_repr} ({type(value).__name__})")
69+
70+
print("\n[Step 5] Verifying roundtrip equality")
71+
assert obj == deserialized, "Roundtrip failed! Objects don't match."
72+
print("✓ Roundtrip verification passed: Original == Deserialized")
73+
74+
print("\n" + "=" * 70)
75+
print("Demo completed successfully!")
76+
print("=" * 70)
77+
78+
79+
if __name__ == "__main__":
80+
main()

docs/pages/pydsdl.rst

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,41 @@ The main functions
1919
.. autofunction:: pydsdl.read_files
2020

2121

22+
Serialization
23+
+++++++++++++
24+
25+
PyDSDL provides built-in serialization and deserialization functions for binary encoding/decoding
26+
of DSDL types without code generation.
27+
28+
.. autofunction:: pydsdl.serialize
29+
.. autofunction:: pydsdl.deserialize
30+
31+
Object Representation Convention
32+
---------------------------------
33+
34+
Deserialized objects use Python primitives:
35+
36+
- **Composites (StructureType)**: ``dict[str, Any]`` with field names as keys
37+
- **Unions (UnionType)**: ``dict`` with exactly one key (the active variant)
38+
- **Arrays (Fixed/Variable)**: ``list``
39+
- **UTF-8 arrays**: ``str``
40+
- **Byte arrays**: ``bytes``
41+
- **Primitives**: ``bool``, ``int``, ``float``
42+
- **Void**: ``None`` (skipped in output)
43+
44+
Example::
45+
46+
obj = {"flag": True, "values": [1, 2, 3], "text": "hello"}
47+
data = pydsdl.serialize(schema, obj)
48+
reconstructed = pydsdl.deserialize(schema, data)
49+
assert obj == reconstructed
50+
51+
See ``demo/demo_serdes.py`` for a complete working example.
52+
53+
.. autoexception:: pydsdl.SerDesError
54+
:show-inheritance:
55+
56+
2257
Type model
2358
++++++++++
2459

@@ -36,14 +71,17 @@ Exceptions
3671

3772
.. computron-injection::
3873
:filename: ../descendant_diagram.py
39-
:argv: FrontendError
74+
:argv: Error
4075

41-
.. autoexception:: pydsdl.FrontendError
76+
.. autoexception:: pydsdl.Error
4277
:undoc-members:
4378
:no-inherited-members:
4479
:show-inheritance:
4580
:special-members:
4681

82+
.. note::
83+
``FrontendError`` is retained as a backward-compatibility alias for ``Error``.
84+
4785
.. autoexception:: pydsdl.InvalidDefinitionError
4886
:undoc-members:
4987
:no-inherited-members:

noxfile.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ def pristine(session):
7272
exe("import pydsdl")
7373

7474

75+
@nox.session(python=PYTHONS[-1:])
76+
def demo(session):
77+
"""Run the serialization/deserialization demo script."""
78+
session.install("-e", ".")
79+
session.run("python", "demo/demo_serdes.py")
80+
81+
7582
@nox.session(python=PYTHONS, reuse_venv=True)
7683
def lint(session):
7784
session.log("Using the newest supported Python: %s", is_latest_python(session))

pydsdl/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from ._namespace import read_files as read_files
3131

3232
# Error model.
33+
from ._error import Error as Error
3334
from ._error import FrontendError as FrontendError
3435
from ._error import InvalidDefinitionError as InvalidDefinitionError
3536
from ._error import InternalError as InternalError

pydsdl/_error.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import urllib.parse
1010

1111

12-
class FrontendError(Exception): # PEP8 says that the "Exception" suffix is redundant and should not be used.
12+
class Error(Exception): # PEP8 says that the "Exception" suffix is redundant and should not be used.
1313
"""
1414
This is the root exception type for all custom exceptions defined in the library.
1515
This type itself is not expected to be particularly useful to the library user;
@@ -71,7 +71,11 @@ def __repr__(self) -> str:
7171
return self.__class__.__name__ + ": " + repr(self.__str__())
7272

7373

74-
class InternalError(FrontendError):
74+
# Backward compatibility alias
75+
FrontendError = Error
76+
77+
78+
class InternalError(Error):
7579
"""
7680
This exception is used to report internal errors in the front end itself that prevented it from
7781
processing the definitions. Every occurrence should be reported to the developers.
@@ -100,7 +104,7 @@ def __init__(
100104
super().__init__(text=text, path=path, line=line)
101105

102106

103-
class InvalidDefinitionError(FrontendError):
107+
class InvalidDefinitionError(Error):
104108
"""
105109
This exception type is used to point out mistakes and errors in DSDL definitions.
106110
This type is inherited by a dozen of specialized exception types; however, the class hierarchy beneath
@@ -113,21 +117,21 @@ class InvalidDefinitionError(FrontendError):
113117

114118
def _unittest_error() -> None:
115119
try:
116-
raise FrontendError("Hello world!")
120+
raise Error("Hello world!")
117121
except Exception as ex:
118122
assert str(ex) == "Hello world!"
119-
assert repr(ex) == "FrontendError: 'Hello world!'"
123+
assert repr(ex) == "Error: 'Hello world!'"
120124

121125
try:
122-
raise FrontendError("Hello world!", path=Path("path/to/file.dsdl"), line=123)
126+
raise Error("Hello world!", path=Path("path/to/file.dsdl"), line=123)
123127
except Exception as ex:
124128
assert str(ex) == "path/to/file.dsdl:123: Hello world!"
125-
assert repr(ex) == "FrontendError: 'path/to/file.dsdl:123: Hello world!'"
129+
assert repr(ex) == "Error: 'path/to/file.dsdl:123: Hello world!'"
126130

127131
try:
128-
raise FrontendError("Hello world!", path=Path("path/to/file.dsdl"))
132+
raise Error("Hello world!", path=Path("path/to/file.dsdl"))
129133
except Exception as ex:
130-
assert repr(ex) == "FrontendError: 'path/to/file.dsdl: Hello world!'"
134+
assert repr(ex) == "Error: 'path/to/file.dsdl: Hello world!'"
131135
assert str(ex) == "path/to/file.dsdl: Hello world!"
132136

133137

0 commit comments

Comments
 (0)