Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d8d57a9
Add the implementation plan
pavel-kirienko Feb 15, 2026
e052b21
feat: implement complete SerDes infrastructure for PyDSDL
pavel-kirienko Feb 15, 2026
9d780ec
feat: implement public serialize/deserialize API (Task 7)
pavel-kirienko Feb 15, 2026
00a8cf5
fix: resolve mypy strict and black formatting issues
pavel-kirienko Feb 15, 2026
694391b
extract the tests
pavel-kirienko Feb 16, 2026
25e9b1a
docs
pavel-kirienko Feb 16, 2026
da494f2
update instructions
pavel-kirienko Feb 16, 2026
b2211ba
feat(serdes): complete polish - Error rename, optimized bit I/O, enri…
pavel-kirienko Feb 16, 2026
d1303fe
fix: address CI failures - format code, fix NaN check, skip test clas…
pavel-kirienko Feb 16, 2026
7c28f1e
fix: remove unused type ignore comment in _test_serdes.py
pavel-kirienko Feb 16, 2026
37bd976
cleanup
pavel-kirienko Feb 16, 2026
5cc8056
fix demo
pavel-kirienko Feb 16, 2026
99f5db1
update docs
pavel-kirienko Feb 16, 2026
0144a36
test
pavel-kirienko Feb 16, 2026
74240dc
Refactor serdes branch coverage tests into native pytest cases
pavel-kirienko Feb 16, 2026
4add38a
bump version
pavel-kirienko Feb 16, 2026
5bb1432
fix(serdes): enforce composite byte padding, delimited sub-reader bou…
pavel-kirienko Feb 16, 2026
c69a5c2
test(serdes): add regression tests for composite padding, sub-reader …
pavel-kirienko Feb 16, 2026
f5080db
refactor(test): use computed values in test assertions for self-docum…
pavel-kirienko Feb 16, 2026
9210a00
black
pavel-kirienko Feb 16, 2026
9183410
Minor edge cases
pavel-kirienko Feb 16, 2026
ae1611f
style: reformat tests for Black 25.x
pavel-kirienko Feb 16, 2026
b80df76
test(serdes): add shared helpers for expanded test suite
pavel-kirienko Feb 16, 2026
8856fdf
test(serdes): add Wave 2 spec-critical semantic tests
pavel-kirienko Feb 16, 2026
c7172c1
test(serdes): add Wave 3 systematic type coverage tests
pavel-kirienko Feb 16, 2026
7d245fb
test(serdes): add Wave 4 complex scenario tests
pavel-kirienko Feb 16, 2026
906c9da
test(serdes): fix type ignore comment placement for lint
pavel-kirienko Feb 16, 2026
37b4cd3
coverage 100%
pavel-kirienko Feb 16, 2026
a539a04
Clarify serdes dict contract in API docs
pavel-kirienko Feb 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/test-and-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@ jobs:
- name: Run build and test
run: |
if [ "$RUNNER_OS" == "Linux" ]; then
nox --non-interactive --error-on-missing-interpreters --session test pristine lint --python ${{ matrix.python }}
nox --non-interactive --error-on-missing-interpreters --session test pristine lint demo --python ${{ matrix.python }}
nox --non-interactive --session docs
elif [ "$RUNNER_OS" == "Windows" ]; then
nox --forcecolor --non-interactive --session test pristine lint
elif [ "$RUNNER_OS" == "macOS" ]; then
nox --non-interactive --error-on-missing-interpreters --session test pristine lint --python ${{ matrix.python }}
nox --non-interactive --error-on-missing-interpreters --session test pristine lint demo --python ${{ matrix.python }}
else
echo "${{ runner.os }} not supported"
exit 1
Expand Down
51 changes: 1 addition & 50 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
Expand All @@ -24,18 +19,10 @@ wheels/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
Expand All @@ -46,42 +33,20 @@ coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
.pyenv
Expand All @@ -90,33 +55,19 @@ venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
.sisyphus/
/site

# mypy
.mypy_cache/

# IDE
**/.idea/*
!**/.idea/dictionaries
!**/.idea/dictionaries/*
.xml
.vscode

# Project-specific testing environment
.dsdl-test/

# OS crap
.DS_Store

# cProfile
/prof

20 changes: 20 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Instructions for clankers

When reading source files, ingest them in their entirety instead of relying on search/grep tools.

Read `README.md` and `CONTRIBUTING.rst` first.

When implementing functional code changes, be sure to read the DSDL specification in <https://github.com/OpenCyphal/specification>.

## Project Structure & Module Organization

- Core library code lives in `pydsdl/`.
- Key internal subpackages are `pydsdl/_expression/`, `pydsdl/_serializable/`.
- Vendored dependencies are under `pydsdl/third_party/` (treat as external code; modify only when intentionally syncing).
- Tests are mostly co-located with implementation. Larger suites are in `pydsdl/_test*.py`, they are never imported.
- Documentation sources are in `docs/`.
- Test and release automation is in `noxfile.py` and `.github/`.

## Commit & Pull Request Guidelines

Provide detailed commit messages explaining the rationale behind the changes. Aim for a brief title with a following expanded description explaining what has been done and why was it necessary.
1 change: 1 addition & 0 deletions CLAUDE.md
12 changes: 6 additions & 6 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
Development guide
=================

This document is intended for library developers only.
This document is intended for library developers and AI agents only.
If you just want to use the library, you don't need to read it.

Development automation is managed by Nox; please see ``noxfile.py``.
Development automation is managed by Nox; please read ``noxfile.py``.

The coding style is PEP8 with max line length 120 characters.


Writing tests
Expand All @@ -26,18 +28,16 @@ outside of test-enabled environments.
import pytest # OK to import inside test functions only (rarely useful)
assert get_the_answer() == 42

For targeted test runs: ``pytest pydsdl -k _unittest_whatever -v``.


Supporting newer versions of Python
+++++++++++++++++++++++++++++++++++

Normally, this should be done a few months after a new version of CPython is released:

1. Update the CI/CD pipelines to enable the new Python version.
2. Update the CD configuration to make sure that the library is released using the newest version of Python.
3. Bump the version number using the ``.dev`` suffix to indicate that it is not release-ready until tested.

When the CI/CD pipelines pass, you are all set.


Releasing
+++++++++
Expand Down
10 changes: 10 additions & 0 deletions demo/DemoMessage.1.0.dsdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# A simple demo type.

bool flag
uint32 counter
float64 temperature
float32[4] numeric_data
utf8[<=256] text_data # UTF-8 encoded text data
byte[<=64] binary_data # Raw binary data

@extent 1024 * 8
59 changes: 59 additions & 0 deletions demo/demo_serdes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""
Self-contained demo of PyDSDL serialization.
"""

import pydsdl
from pathlib import Path

SCRIPT_DIR = Path(__file__).parent
DSDL_FILE = SCRIPT_DIR / "DemoMessage.1.0.dsdl"


def main() -> None:
print("Loading DSDL type from:", DSDL_FILE.name)
types, _ = pydsdl.read_files(
dsdl_files=DSDL_FILE,
root_namespace_directories_or_names=SCRIPT_DIR,
lookup_directories=[],
)
schema = types[0]
print(f"✓ Loaded type: {schema.full_name} v{schema.version.major}.{schema.version.minor}")
print(" Fields:", [f"{f.data_type} {f.name}" for f in schema.fields_except_padding])

print("Creating example object:")
obj = {
"flag": True,
"counter": 42,
"temperature": 23.5,
"numeric_data": [1.0, 2.0, 3, 4],
"text_data": "Hello, SerDes!",
"binary_data": b"\x00\x01\x02\x03",
}
for key, value in obj.items():
value_repr = repr(value)
if len(value_repr) > 50:
value_repr = value_repr[:47] + "..."
print(f" {key:15} = {value_repr} ({type(value).__name__})")

print("Serializing object to bytes")
serialized_data = pydsdl.serialize(schema, obj)
print(f"✓ Serialized to {len(serialized_data)} bytes:")
print(f" {serialized_data.hex()}")

print("Deserializing bytes back to object")
deserialized = pydsdl.deserialize(schema, serialized_data)
print("✓ Deserialized successfully:")
for key, value in deserialized.items():
value_repr = repr(value)
if len(value_repr) > 50:
value_repr = value_repr[:47] + "..."
print(f" {key:15} = {value_repr} ({type(value).__name__})")

print("Verifying roundtrip equality")
assert obj == deserialized, "Roundtrip failed! Objects don't match."
print("✓ Roundtrip verification passed: Original == Deserialized")


if __name__ == "__main__":
main()
15 changes: 13 additions & 2 deletions docs/pages/pydsdl.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ The main functions

.. autofunction:: pydsdl.read_namespace
.. autofunction:: pydsdl.read_files
.. autofunction:: pydsdl.serialize
.. autofunction:: pydsdl.deserialize


Type model
Expand All @@ -36,20 +38,29 @@ Exceptions

.. computron-injection::
:filename: ../descendant_diagram.py
:argv: FrontendError
:argv: Error

.. autoexception:: pydsdl.FrontendError
.. autoexception:: pydsdl.Error
:undoc-members:
:no-inherited-members:
:show-inheritance:
:special-members:

.. note::
``FrontendError`` is retained as a backward-compatibility alias for ``Error``.

.. autoexception:: pydsdl.InvalidDefinitionError
:undoc-members:
:no-inherited-members:
:show-inheritance:
:special-members:

.. autoexception:: pydsdl.SerDesError
:undoc-members:
:no-inherited-members:
:show-inheritance:
:special-members:

.. autoexception:: pydsdl.InternalError
:undoc-members:
:no-inherited-members:
Expand Down
7 changes: 7 additions & 0 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ def pristine(session):
exe("import pydsdl")


@nox.session(python=PYTHONS[-1:])
def demo(session):
"""Run the serialization/deserialization demo script."""
session.install("-e", ".")
session.run("python", "demo/demo_serdes.py")


@nox.session(python=PYTHONS, reuse_venv=True)
def lint(session):
session.log("Using the newest supported Python: %s", is_latest_python(session))
Expand Down
12 changes: 10 additions & 2 deletions pydsdl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import sys as _sys
from pathlib import Path as _Path

__version__ = "1.24.3"
__version__ = "1.25.0.rc0"
__version_info__ = tuple(map(int, __version__.split(".")[:3]))
__license__ = "MIT"
__author__ = "OpenCyphal"
Expand All @@ -30,10 +30,13 @@
from ._namespace import read_files as read_files

# Error model.
from ._error import FrontendError as FrontendError
from ._error import Error as Error
from ._error import InvalidDefinitionError as InvalidDefinitionError
from ._error import InternalError as InternalError

# Deprecated compatibility alias, to be removed.
FrontendError = Error

# Data type model - meta types.
from ._serializable import SerializableType as SerializableType
from ._serializable import PrimitiveType as PrimitiveType
Expand Down Expand Up @@ -75,4 +78,9 @@
from ._serializable import Version as Version
from ._bit_length_set import BitLengthSet as BitLengthSet

# Serialization/deserialization.
from ._serdes import serialize as serialize
from ._serdes import deserialize as deserialize
from ._serdes import SerDesError as SerDesError

_sys.path = _original_sys_path
4 changes: 2 additions & 2 deletions pydsdl/_dsdl_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from . import _parser
from ._data_type_builder import DataTypeBuilder, UndefinedDataTypeError
from ._dsdl import DefinitionVisitor, ReadableDSDLFile
from ._error import FrontendError, InternalError, InvalidDefinitionError
from ._error import Error, InternalError, InvalidDefinitionError
from ._serializable import CompositeType, Version

_logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -275,7 +275,7 @@ def read(
self._cached_type.fixed_port_id,
)
return self._cached_type
except FrontendError as ex: # pragma: no cover
except Error as ex: # pragma: no cover
ex.set_error_location_if_unknown(path=self.file_path)
raise ex
except (MemoryError, SystemError): # pragma: no cover
Expand Down
Loading