Skip to content

refactor: self-registering packet registry + Position composition (non-breaking) - #310

Merged
hemna merged 3 commits into
masterfrom
experimental/packet-registry
Sep 24, 2026
Merged

hemna merged 3 commits into
masterfrom
experimental/packet-registry

Conversation

@hemna

@hemna hemna commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Two refactors to aprsd/packets/core.py on the experimental branch, inspired by the "type object" design pattern discussion:

  1. Self-registering packet registry (72541bf)
  2. Position value object composition (f30f764) — with a backward-compatibility shim so external plugins/extensions need no changes

Behavior is unchanged for existing callers: full test suite (782) passes, ruff format/lint clean, and all plugins/extensions in the ecosystem run their original, unmodified code (verified against all 26 plugin repos).

1. Self-registering packet registry

Replaces the get_packet_type() if/elif chain and the static TYPE_LOOKUP dict with a registry that packet classes register into automatically via __init_subclass__. Each concrete packet class declares a packet_type: ClassVar[str] and a matches(raw) classifier; TYPE_LOOKUP is derived from the registry so the two can no longer drift out of sync.

  • factory() now returns Packet | UnknownPacket (was wrongly annotated -> type[Packet]); _type deserialization uses a registry-backed name lookup instead of globals()
  • ThirdPartyPacket.subpacket is typed Optional[Packet] and from_dict() re-dispatches the raw sub-packet through factory() so dataclasses-json doesn't pre-build a lossy base Packet
  • The beacon fallback (unrecognized format + latitude → beacon) is preserved in get_packet_type(); dead factory() branches removed

Adding a new packet type is now a single-class change — no central dispatch table to edit.

2. Position value object composition (non-breaking)

Collapses the Packet → GPSPacket → {Beacon, MicE, Telemetry, Object, Weather} data spine. All position data (lat/lon/altitude/symbol/comment/course/speed/PHG/DAO) and its conversion/formatting logic (zulu time, DDM, human_info) now lives in a frozen Position value object that GPSPacket composes (has-a instead of is-a).

Backward compatibility — external plugins/extensions keep working unmodified:

  • GPS packet types accept both position=Position(...) (canonical) and the legacy flat kwargs (latitude=, longitude=, symbol=, comment=, speed=, ...). The legacy form emits a DeprecationWarning and is scheduled for removal in a future release.
  • Flat attribute reads (packet.latitude, packet.comment, ...) keep working via read-only delegating properties.
  • Per-type symbol defaults are preserved (ObjectPacket → r, WeatherPacket → _) when using the legacy flat form.
  • On-disk JSON is unchanged: to_dict()/from_dict() flatten/unflatten the nested position, so persisted object-store files keep loading.
  • The shim is explicitly marked TODO(remove) in the code (target: next major release) so the deprecated path doesn't linger.
  • GPSPacket inherits DataClassJsonMixin directly because the @dataclass_json decorator unconditionally overwrites to_dict()/from_dict() and would clobber the flattening overrides.

Docs / changelog

  • Added an [Unreleased] section to ChangeLog.md and regenerated docs/source/changelog.md
  • docs/prepare_changelog.py now also normalizes the [Unreleased] heading level so the Sphinx copy stays consistent with version headings
  • Packet API docs are autodoc-generated from the (updated) docstrings

Testing

  • pytest tests → 782 passed
  • ruff format + ruff check → clean
  • Plugin compatibility verified against all 26 plugin/extension repos in ~/devel/mine/hamradio/aprsd-plugins: the 6 that use GPS packet constructors/reads pass with their original, unmodified code against this branch (repeat 60/60, webchat 71/73 — the 2 remaining failures are a pre-existing config issue that reproduces at the base commit without this PR, gps/weewx/rich-cli/digipi 1/1 each)

Replace the get_packet_type() if/elif chain and static TYPE_LOOKUP with a
self-registering packet registry.  Each concrete Packet subclass declares a
packet_type ClassVar and a matches() classifier; __init_subclass__ registers
it automatically and TYPE_LOOKUP is derived from the registry so the two can
no longer drift out of sync.

- factory() now returns Packet | UnknownPacket (was wrongly typed as
  type[Packet]); _type deserialization uses a registry-backed name lookup
  instead of globals()
- ThirdPartyPacket.subpacket is typed Optional[Packet] and from_dict()
  re-dispatches the raw sub-packet through factory() so dataclasses-json
  doesn't pre-build a lossy base Packet
- beacon fallback (unrecognized format + latitude) preserved in
  get_packet_type(); dead factory branches removed

Behavior is unchanged: full test suite (775) passes.
Replace the flat GPS fields on GPSPacket and its subclasses with a
composed Position value object (has-a instead of is-a), collapsing the
Packet -> GPSPacket -> {Beacon, MicE, Telemetry, Object, Weather}
data spine.  Position owns the coordinates, symbol, movement/PHG data
and the conversion/formatting logic (zulu time, DDM, human_info).

- GPSPacket holds position: Optional[Position]; subclass defaults
  preserved via _position_default() (ObjectPacket 'r', WeatherPacket '_')
- to_dict()/from_dict() flatten/unflatten so the on-disk JSON format
  is unchanged; verified via objectstore save/load round-trip
- GPSPacket inherits DataClassJsonMixin directly because the
  @dataclass_json decorator overwrites to_dict()/from_dict() and would
  clobber the flattening overrides
- constructor is breaking: position=Position(...) instead of
  latitude=.../longitude=...; updated tx.py, log.py and all tests

Full test suite (775) passes; ruff format + lint clean.
Add an [Unreleased] section to ChangeLog.md covering the self-registering
packet registry and the Position composition refactor, and regenerate
docs/source/changelog.md via prepare_changelog.py.  Also teach
prepare_changelog.py to normalize the [Unreleased] heading level so the
Sphinx copy stays consistent with version headings.
@hemna hemna changed the title refactor: self-registering packet registry + Position composition refactor: self-registering packet registry + Position composition (non-breaking) Sep 24, 2026
@hemna
hemna force-pushed the experimental/packet-registry branch from 0564bad to 03a5fd0 Compare September 24, 2026 14:21
@hemna
hemna merged commit 186bfc4 into master Sep 24, 2026
8 checks passed
@hemna
hemna deleted the experimental/packet-registry branch September 24, 2026 14:40
hemna added a commit that referenced this pull request Sep 24, 2026
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