Skip to content

Add dependency injection for the device constructor - #20

Merged
glopesdev merged 7 commits into
mainfrom
feat-dep-injection-on-device
Aug 12, 2026
Merged

Add dependency injection for the device constructor#20
glopesdev merged 7 commits into
mainfrom
feat-dep-injection-on-device

Conversation

@bruno-f-cruz

@bruno-f-cruz bruno-f-cruz commented Aug 10, 2026

Copy link
Copy Markdown
Member

Device takes the device module at construction rather than through inheritance, so identity validation is driven by the module and no subclass is needed. This is the same shape DatasetReader already uses.

# before
class Behavior(Device):
    __whoami__ = 1216

# after
with Device(transport, behavior) as dev: ...

Device is generic on its module

device = Device(transport, behavior)   # Device[BehaviorModule]
device.module                          # BehaviorModule

device = Device(transport)             # Device[None]
device.module                          # None

The module type is inferred from the device_module parameter, and device.module returns it, so a type checker can see what was injected. A new module property exposes it for introspection and for passing downstream without keeping a second reference.

Identity validation

WHO_AM_I in the injected module drives the check on open. It is skipped when no module is passed, and when the module declares 0, which is how an unregistered device is marked.

__whoami__ is removed rather than deprecated. Every downstream device package is regenerated on publication, so there is nothing in the wild to catch.

open_serial_device takes a module, a subclass, or nothing

open_serial_device(behavior, port="COM3")    # Device[BehaviorModule]
open_serial_device(MyDevice, port="COM3")    # MyDevice, subclass preserved
open_serial_device(port="COM3")              # Device[None]

The runtime dispatch is a single isinstance(device_or_module, type) check, since modules are instances and device subclasses are types.

What the module does not do

It is consulted only for the identity check. _registers is not pre-populated from it and remains built lazily by subscribe, so an event is parsed only for a register that has been subscribed. The module register map is not the concern of the device.

The docstrings and the harp-device README previously said otherwise, and are corrected here.

read, write and subscribe are unchanged, and register classes still come from the module rather than from the device instance. Subclassing Device still works for adding methods; the module is passed at construction.

Typing markers

py.typed moved from harp/device/ into each of core, client and schema. The namespace split turned harp/device/ into a shared directory, so leaving the marker there had harp-device claiming typing for the whole harp.device namespace, and a future harp.device.behavior distribution installing beside it would have collided on that filename. Every other package in the repository keeps its marker inside a real package.

Examples

Documentation examples now import modules rather than names, following the convention that a register carries the portion it comes from at the point of use:

from harp.device import behavior, client, core

with client.Device(transport, behavior) as device:
    device.read(core.WhoAmI)                 # a common register
    device.read(behavior.DigitalInputState)  # declared by the schema

This also removes an error the old examples carried. A device module names only the registers its own schema declares, so behavior.WhoAmI raised AttributeError.

Testing

Full suite passes at 359 tests, with ruff, ruff format, pyright, codespell and mkdocs build --strict clean.

New assert_type fixtures in tests/conformance.py cover all three construction forms and check that device.module resolves correctly in each.

@bruno-f-cruz bruno-f-cruz changed the title Add dependency injection pattern and typing tests Add dependency injection pattern for device constructor Aug 10, 2026
@bruno-f-cruz bruno-f-cruz changed the title Add dependency injection pattern for device constructor Add dependency injection pattern for Device constructor Aug 10, 2026
The overload no longer annotates self with the class type variable,
which is not allowed on __init__ and produced a pyright
reportInvalidTypeVarUse warning. The module type is inferred from the
device_module parameter instead, so Device(transport, behavior) still
resolves to Device[BehaviorModule].
The module is consulted only for the identity check on open. It does
not pre-populate the register map, so an event is parsed only for a
register that has been subscribed.

Examples import modules rather than names, so a register carries the
portion it comes from at the point of use, core.WhoAmI beside
behavior.AnalogData.
@glopesdev
glopesdev force-pushed the feat-dep-injection-on-device branch from 9c59998 to e405d7b Compare August 12, 2026 18:52
@glopesdev glopesdev changed the title Add dependency injection pattern for Device constructor Add dependency injection for the device constructor Aug 12, 2026
@glopesdev
glopesdev self-requested a review August 12, 2026 19:16
The marker sat in harp/device/, which the split turned into a shared
namespace directory, so a device package installing beside it would
have collided on the same file. Each portion now carries its own.
The error message says which module expected the identity, since now
identity moved from a class attribute onto the injected module.

Tests were added to cover the two paths: a WHO_AM_I of 0 skipping the
read entirely, and the module property round-tripping.
open_serial_device tries the type[D] overload first. A class carrying
REGISTER_MAP and WHO_AM_I satisfies the module protocol structurally,
so the module overload would otherwise win and return
Device[type[Subclass]] rather than the subclass itself.
@glopesdev
glopesdev merged commit 371a0fb into main Aug 12, 2026
13 checks passed
@glopesdev
glopesdev deleted the feat-dep-injection-on-device branch August 12, 2026 21:53
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.

2 participants