Skip to content

Align runtime modules with generated device packages - #25

Merged
glopesdev merged 7 commits into
mainfrom
fix-runtime-emission
Aug 15, 2026
Merged

Align runtime modules with generated device packages#25
glopesdev merged 7 commits into
mainfrom
fix-runtime-emission

Conversation

@glopesdev

Copy link
Copy Markdown
Contributor

A module built from a device.yml at 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-known EnableFlag mask 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.EncoderModeMask and behavior.AnalogDataPayload resolved 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__ returns type[Any] rather than type[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 private are emitted too, with the underscore-prefixed name the generator gives them. exclude_private is removed rather than redefaulted. create_registers and create_device_module previously disagreed on its default, so the same schema gave different register sets depending on which was called.

Reusing the core masks

A maskType naming 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 from harp.device.core on both paths.

The generator side of this is on harp-tech/generators, where the Python target gained the same fallback and emits from 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 maskType raises UnknownMaskError naming the register, the mask and the core set to choose from. It replaces an assertion that claimed the register declared no maskType at all, which was the wrong cause, and that vanished under python -O and left the register decoding as its raw element type. This is stricter than the generator in one case: an unresolvable mask raises even when an interfaceType is 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.Behavior package 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.

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.
@glopesdev
glopesdev requested a review from bruno-f-cruz August 15, 2026 00:41
@glopesdev glopesdev added the feature New planned feature label Aug 15, 2026
Comment thread src/packages/harp-device/src/harp/device/schema/_module.py Outdated
@glopesdev
glopesdev requested a review from bruno-f-cruz August 15, 2026 09:01
DeviceModule no longer overrides __getattr__. The standard library type
stubs already declare it on types.ModuleType returning Any.
@glopesdev
glopesdev force-pushed the fix-runtime-emission branch from a3892f5 to 4e906b0 Compare August 15, 2026 18:38
@glopesdev
glopesdev merged commit 41e7c6a into main Aug 15, 2026
13 checks passed
@glopesdev
glopesdev deleted the fix-runtime-emission branch August 15, 2026 19:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New planned feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants