Skip to content

feat(persistent_data): pure-Python counterpart (espp.persistent_data) - #864

Merged
finger563 merged 3 commits into
mainfrom
feat/persistent-data-python
Oct 31, 2026
Merged

finger563 merged 3 commits into
mainfrom
feat/persistent-data-python

Conversation

@finger563

@finger563 finger563 commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Summary

espp.persistent_data: the Python counterpart of the persistent_data component (#857), so a PC tool can read, edit or generate a device's versioned settings file, and migration chains can be exercised on the host.

  • Pure Python, no extension module. The file is a 20-byte header (magic, schema version, layout fingerprint, length, CRC32) in front of an alpaca payload encoded with fixed_length_encoding | strict: fixed-width little-endian integers, 32-bit length prefixes, nothing else. struct + zlib cover it (alpaca's CRC32 is the zlib one; verified on the vectors). pyalpaca stays an upstream tool; it would have added a native build to every wheel for the variable-length encoding this format never uses.
  • Dataclasses as schema versions, newest first, mirroring the C++ type list: a VERSION class variable, migrate(older) on every version but the last, width aliases (u8 … u64, i8 … i64, f32, f64, char) for numeric fields — a bare int / float is a TypeError, never a silent width guess — plus array(T, N), unordered(dict | set), Optional[T], tuple[...], enums with an Annotated underlying width, nested dataclasses.
  • The layout fingerprint is computed from the annotations by the grammar in the component README, so a file written in Python verifies on the device and vice versa; a Python/C++ struct mismatch surfaces as layout_mismatch.
  • PersistentData(versions, path): load, load_or_create, save, update, erase, encode, decode; atomic temp-and-rename writes; PersistentDataError.errc mirrors PersistentDataErrc; thread-safe.
  • Test python/persistent_data_test.py (78 checks): describe() bytes, fingerprints and whole encoded files byte-for-byte against python/persistent_data_vectors.txt generated by the C++ codec (enum, fixed array, nested struct, every integer width, float/double, string, vector), the v1 → v2 → v3 migration chain, dropped / newer / reused versions, every corruption path, the remaining field types (map, unordered_map, set, optional, tuple, char) and the file operations. Runs from the source tree without a wheel; added to the cibuildwheel test command.
  • Exported from the package (from . import persistent_data), documented in the component README ("Python" section) and python/README.md.

Test plan

  • python/persistent_data_test.py: 78/78 on Python 3.15 and 3.12 from the source tree
  • 78/78 through the real espp package: host library built with ESPP_BUILD_PYTHON=ON from this branch, persistent_data.py present in the installed package
  • main (chore(serialization): bump alpaca to upstream master (portable with_version hash) #863, alpaca bump) merged in cleanly; test still 78/78
  • Self-review round: code-quality bot findings (two explicit excepts, unused import) plus a logged post-migration-rewrite failure, a clear error for VERSION declared as a field, deserialize_failed for a dataclass constructor that raises, parse exported; pyflakes clean; 78/78 on 3.15 / 3.12 from source and through the installed package
  • Build-wheels CI runs the new test on every wheel

🤖 Generated with Claude Code

https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU

finger563 and others added 2 commits October 30, 2026 21:19
Reads and writes the firmware's versioned settings files from Python with
no extension module: a 20-byte header (magic, schema version, layout
fingerprint, length, CRC32) in front of an alpaca payload encoded with
fixed_length_encoding | strict, which is fixed-width little-endian
integers and 32-bit length prefixes, so struct + zlib cover it.

Schema versions are dataclasses, newest first, mirroring the C++ type
list: a VERSION class variable, migrate(older) on every version but the
last, width aliases (u8 ... u64, i8 ... i64, f32, f64, char) for numeric
fields (a bare int / float is an error, never a guess), array(T, N) for
std::array, unordered(dict/set) for the unordered containers,
Optional[T], tuple[...], enums with an Annotated underlying width, and
nested dataclasses. The layout fingerprint is derived from the
annotations by the grammar in the component README, so a file written in
Python verifies on the device and vice versa; a mismatch is
layout_mismatch, never wrong values.

PersistentData(versions, path): load / load_or_create / save / update /
erase / encode / decode, atomic temp + rename writes, the same error
taxonomy as PersistentDataErrc (PersistentDataError.errc), thread-safe.

python/persistent_data_test.py checks describe() bytes, fingerprints and
whole encoded files byte-for-byte against vectors generated by the C++
codec (python/persistent_data_vectors.txt), the migration chain, every
error path, the other field types and the file operations (76 checks);
it runs from the source tree without a built wheel and is added to the
wheel test command. Exported as espp.persistent_data; documented in the
component README and python/README.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
@github-actions

Copy link
Copy Markdown

✅Static analysis result - no issues found! ✅

- Code-quality bot: the two bare `except: pass` (erase of a missing file,
  temp-file cleanup after a failed write) are explicit now, and the unused
  ClassVar import is gone.
- load(): a failed post-migration rewrite is logged (logging, module
  logger) instead of silently swallowed, matching the C++ warning.
- A VERSION declared as a dataclass field instead of a ClassVar is rejected
  with a message saying so (it used to surface as a confusing "bare int"
  error from the payload spec).
- A dataclass constructor that raises during decode (a __post_init__
  validation) is reported as deserialize_failed instead of escaping as a
  raw exception.
- parse() is exported; the test imports Annotated from typing rather than
  through the module; the module docstring's type-mapping table is a valid
  RST table (column widths).
- Two new test checks for the VERSION-as-field and constructor-failure
  cases (78 total). pyflakes clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
@finger563
finger563 merged commit 637b0c7 into main Oct 31, 2026
22 checks passed
@finger563
finger563 deleted the feat/persistent-data-python branch October 31, 2026 04:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant