Align runtime modules with generated device packages - #25
Merged
Conversation
A module built from a schema now names the enums and payload classes its registers are built from, so it holds the same declarations as a generated device package. Each declaration resolves as type[Any] rather than type[RegisterBase[Any]], since the module holds three kinds of class. Parsing a schema that declares one name as both a register and a mask raises a validation error.
A register whose maskType names a mask the schema does not declare now resolves against the core register set, emitting the same enum a generated device package imports. Such a register would otherwise raise, so two of the published devices could not be built at runtime.
A register whose visibility is private is now always emitted with the underscore-prefixed class name the generator gives it. This means a runtime module has the same address space as the generated package for the same schema. Reading such a register by address, or resolving one from a logged dataset, would otherwise fail on the runtime path alone. The exclude_private parameter is removed rather than redefaulted, since create_registers and create_device_module disagreed on its default and the same schema therefore gave different register sets depending on which was called.
A maskType naming neither a declared nor a core mask now raises UnknownMaskError naming the register, the mask and the core set to choose from, rather than an assertion claiming the register declares no maskType at all. A register carrying a converter with nothing to convert to raises as well, which is the only case the assertion actually caught. Both survive python -O, whereas the assertion was stripped and the register decoded as its raw element type instead.
The reference copies of generated output carried neither __all__ nor the blank-line spacing the generator now emits, so parity ran against output three changes old. Refreshing them lets a test assert that a runtime module and the generated package publish an identical __all__ for the same schema.
bruno-f-cruz
requested changes
Aug 15, 2026
DeviceModule no longer overrides __getattr__. The standard library type stubs already declare it on types.ModuleType returning Any.
glopesdev
force-pushed
the
fix-runtime-emission
branch
from
August 15, 2026 18:38
a3892f5 to
4e906b0
Compare
bruno-f-cruz
approved these changes
Aug 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A module built from a
device.ymlat runtime should present the same API as the device package generated from that same schema. It did not, and the divergences ran from cosmetic to fatal: two of the eight published device schemas could not be built at runtime at all, because they reference the well-knownEnableFlagmask without declaring it and the emitter treated that register as having nothing to decode.What a runtime module now holds
A runtime module names everything its schema declares, the registers beside the enums and payload classes they are built from, which is what a generated package holds. Previously it named only the registers, so
behavior.EncoderModeMaskandbehavior.AnalogDataPayloadresolved on a generated package and raised on a runtime module, and the tests reached enums through a private descriptor accessor because no public path existed.Because the namespace now holds three kinds of class,
DeviceModule.__getattr__returnstype[Any]rather thantype[RegisterBase[Any]], and the conformance fixture asserts the weaker type. The declaration still exists, so a module attribute resolves to something rather than nothing.Registers marked
privateare emitted too, with the underscore-prefixed name the generator gives them.exclude_privateis removed rather than redefaulted.create_registersandcreate_device_modulepreviously disagreed on its default, so the same schema gave different register sets depending on which was called.Reusing the core masks
A
maskTypenaming a mask the schema does not declare now resolves against the core register set. A schema declaring its own mask of a core name still wins. The core mask is not named by the device module, following the existing rule that a device module does not name the common registers. It is reached fromharp.device.coreon both paths.The generator side of this is on
harp-tech/generators, where the Python target gained the same fallback and emitsfrom harp.device.core import EnableFlag. That work is a prerequisite for the reference modules refreshed here.Errors instead of silence
A name declared as both a register and a mask is now rejected when the schema is parsed, by a validator on the schema model, so every consumer inherits the check.
An unresolvable
maskTyperaisesUnknownMaskErrornaming the register, the mask and the core set to choose from. It replaces an assertion that claimed the register declared nomaskTypeat all, which was the wrong cause, and that vanished underpython -Oand left the register decoding as its raw element type. This is stricter than the generator in one case: an unresolvable mask raises even when aninterfaceTypeis present, where the generator ignores the bad mask and decodes through the interface type. No register in any of the eight device schemas or any fixture declares both, so nothing existing depends on the looser reading.Parity evidence
Checked against the generated
Harp.Behaviorpackage for the same schema: 91 registers on both sides, no mismatch in address, payload type or dtype layout including field names, and identical__all__. A sample of eight published representative device schemas build.