Skip to content

Refactor Device API to allow binding device's registers - #16

Open
bruno-f-cruz wants to merge 19 commits into
mainfrom
refactor-register-binding
Open

Refactor Device API to allow binding device's registers#16
bruno-f-cruz wants to merge 19 commits into
mainfrom
refactor-register-binding

Conversation

@bruno-f-cruz

@bruno-f-cruz bruno-f-cruz commented Jul 26, 2026

Copy link
Copy Markdown
Member

Closes #10 (register-binding half). Replaces the address-keyed REGISTER_MAP with a name-addressable register namespace that works for both runtime-generated (create_device) and statically generated devices, and gives static devices real editor autocomplete and payload-aware types. It also partially addresses a previous concern in #5 (comment) by making registers discoverable from the device itself.

What this adds

A device exposes its registers by name through device.registers, on the class or an instance:

Behavior.registers.AnalogData          # -> type[AnalogData], by name
Behavior.registers.WhoAmI              # common Harp registers, merged in automatically
Behavior.registers.by_address[44]      # {address: register_class} when you need the map
Behavior.registers.by_name["AnalogData"]   # {name: register_class}
  • Single source of truth. A device declares only its own registers in a __REGISTERS__ tuple. The base Device merges in the common Harp registers and derives device.registers in __init_subclass__ (the device's register wins on an address clash).
  • RegisterNamespace. Attribute / [name] / [address] access, by_name / by_address views, iteration, in, len, and __dir__ for REPL discovery. Undeclared attribute access falls back to __getattr__ typed type[RegisterBase[Any]], so any register name still type-checks; a CoreRegistersNamespace subclass declares specific names for precise types.
  • REGISTER_MAP is gone. No address dict to hand-build, no **_CORE_REGISTER_MAP spread. Address lookups move to device.registers.by_address.
  • device.registers is read-only. It is backed by a descriptor that defines __get__ but no __set__, so device.registers = ... is a type error. A read-only member is checked covariantly, which is what lets a subclass narrow it (see below) without an incompatible-override suppression.

Static type hints

CoreRegistersNamespace is a typed facade for the common Harp registers. A statically generated device narrows device.registers by declaring a small nested facade that subclasses it and re-typing registers to it:

from typing import ClassVar
from harp.device import CoreRegistersNamespace, Device


class Behavior(Device):
    __whoami__ = 1216
    __REGISTERS__ = (DigitalInputs, AnalogData, ...)   # device-specific only

    # Optional but recommended: makes `device.registers.<Name>` autocomplete and
    # type-aware. Never instantiated — the base builds the namespace from `__REGISTERS__`.
    class _Registers(CoreRegistersNamespace):
        DigitalInputs: type[DigitalInputs]
        AnalogData: type[AnalogData]
        # ... one line per device register ...

    registers: ClassVar[_Registers]

With this, device.registers.AnalogData autocompletes and read / write infer the payload type. Because registers is read-only on the base, the narrowing needs no reportIncompatibleVariableOverride suppression and no if TYPE_CHECKING guard — the facade is a plain nested class that is simply never instantiated.

Runtime devices (create_device) get the same device.registers surface for free — without static autocomplete, since the class is built at runtime.

What downstream code generators must emit

A statically generated device package emits, per device:

  1. Each register as a RegisterBase subclass (unchanged).
  2. A Device subclass that sets only __whoami__ (when known) and __REGISTERS__ — a tuple of the device's own registers. Do not include the common Harp registers and do not emit a REGISTER_MAP; both are handled by the base.
  3. A nested facade subclassing CoreRegistersNamespace, one Name: type[Name] per device register, plus registers: ClassVar[<Facade>], so editors autocomplete and type-check device.registers.<Name>.

The reference shape is tests/device/expected_device.py and the hand-written docs/examples/static_device/static_device.py.

Generators must not: emit REGISTER_MAP, spread the core registers into __REGISTERS__, or wrap the facade in if TYPE_CHECKING.

Dataset reader

harp-data's DatasetReader surfaces the same namespace via reader.registers (e.g. reader.registers.by_address), so demultiplexed files can be matched to registers by name or address without a separate map.

Narrow the Any-typed inputs of format_bulk / to_buffer / to_file to
PayloadBase | ArrayLike, ArrayLike | None, and MessageType | ArrayLike.
Give Device a base REGISTER_MAP ClassVar default ({}) that generated
subclasses override, and tidy the create_device docstring.
Add a "Generating a Device from a Schema" example walking through
`create_device`, including a win/loss discussion of runtime generation
vs. pre-generated device packages. Wire it into the examples index and
nav, surface `create_device`/`parse_device_schema`/`ConverterContext` in
the device API reference, and add a Quickstart to the root README.
@bruno-f-cruz
bruno-f-cruz force-pushed the refactor-register-binding branch from a31e01e to 7261bbf Compare July 27, 2026 02:13
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.

Add device-driven reader factory for harp.data

1 participant