Skip to content

feat: add Publisher.publish dispatching method#442

Merged
nanomad merged 6 commits into
developfrom
feat/publisher-publish-dispatch
May 10, 2026
Merged

feat: add Publisher.publish dispatching method#442
nanomad merged 6 commits into
developfrom
feat/publisher-publish-dispatch

Conversation

@nanomad
Copy link
Copy Markdown
Contributor

@nanomad nanomad commented May 9, 2026

Summary

Adds a single Publisher.publish(key, value, no_prefix=False) method on the Publisher ABC that dispatches via isinstance to the existing typed publish_{bool,int,float,str} methods. Lets callers that hold a bool | int | float | str union forward to the right typed method without each reproducing the isinstance chain (with the bool-before-int ordering subtlety).

Also exports a PublishedValue type alias for the union, so callers can name the type at signature boundaries.

Motivated by PR #440 (fix/ha-number-optimistic-state), which currently inlines the dispatch in vehicle_command.py::__publish_state. With this method available, that and any future similar caller can collapse to a single self.publisher.publish(...) call.

Test plan

  • New tests/publisher/test_publish_dispatch.py adds conformance tests across every concrete Publisher subclass.
  • Critical regression cases: publish(key, True) routes to publish_bool (not publish_int); publish(key, 5) routes to publish_int.
  • All existing tests still pass.

nanomad added 3 commits May 10, 2026 11:45
Adds a single non-abstract `publish(key, value, no_prefix=False)` method on
the `Publisher` ABC that dispatches via isinstance to the existing typed
`publish_{bool,int,float,str}` methods, plus a `PublishedValue` type alias
for the union. The `bool`-before-`int` ordering is load-bearing because
`isinstance(True, int)` is `True` in Python.

Conformance tests cover every concrete subclass (`MqttPublisher`,
`ConsolePublisher`, `MessageCapturingConsolePublisher`) plus an
ABC-level minimal subclass, and explicitly lock the bool-vs-int ordering.
Renames `PublishedValue` -> `Publishable` and widens it from
`bool | int | float | str` to also include `dict[str, Any] | datetime`,
matching the full set of value shapes the gateway publishes. Extends
`Publisher.publish` to dispatch the wider union: dicts forward to
`publish_json` (with `retain` plumbed through), datetimes are stringified
via `datetime_to_str` and routed through `publish_str`. An unsupported
runtime type now raises `TypeError` instead of silently no-op-ing.

This subsumes the two duplicate `Publishable` constrained-TypeVar
declarations (in `status_publisher/__init__.py` and `vehicle.py`) and
their two near-identical `_publish_directly` chains, which now collapse
to a single `self.publisher.publish(...)` call. Methods that used to
parametrize over the constrained TypeVar (`_publish`,
`_transform_and_publish`, `__publish`) switch to PEP 695 bounded
generics: `[V: Publishable]`, `[T, V: Publishable]`. This is a small
semantic loosening (subclasses of e.g. `dict` are now valid `V`) but
runtime dispatch is `isinstance`-based and handles subclasses correctly.

Tests grow new conformance cases for the dict (with `retain` forwarding)
and datetime arms plus a regression test for the `TypeError` arm; the
one test patching the deleted `_publish_directly` now patches the
underlying publisher's `publish` instead.
Complete the typed publish API by giving `datetime` its own narrow
entry point — `publish_datetime` — that stringifies via
`datetime_to_str` and forwards to `publish_str` (and now `retain` too).
Removes the inline transformation in the dispatch chain.

`Publisher.publish()` now forwards `retain` to every arm of the typed
API, not just `publish_json`. Previously it was silently dropped for
str/int/bool/float; with #443 those typed methods now accept `retain`,
so the dispatcher can finally honor the kwarg uniformly.

Also folds the two remaining `datetime_to_str(...)` call sites in
`vehicle.py` (notify_car_activity, last_failed_refresh setter) through
the typed/Publishable APIs, dropping the now-unused import.
@nanomad nanomad force-pushed the feat/publisher-publish-dispatch branch from a24d8a8 to c3768bc Compare May 10, 2026 09:48
nanomad added 3 commits May 10, 2026 11:50
`Any` was overly permissive — at runtime `internal_publish` only ever
sees what the typed publish methods route to it (str/int/bool/float)
plus None from `clear_topic`. Reuse the `Publishable | None` alias to
make the contract explicit.

The `MessageCapturingConsolePublisher.map` test inspection store stays
typed `Any` so consumers can `json.loads(...)` serialized payloads
without per-call narrowing.
Same narrowing as ConsolePublisher.internal_publish: the private
`__publish` only receives Publishable | None at runtime (str/int/bool/
float from the typed methods, str from publish_json after JSON
serialization, None from clear_topic). Replace `Any` with the explicit
alias.
…pers

Replace `Publishable | None` on `MqttPublisher.__publish` and
`ConsolePublisher.internal_publish` with `WirePayload | None`, where
`WirePayload = bool | int | float | str` — the precise set of values
that crosses the publisher/transport boundary after the typed
publish_* methods do their stringification.

This catches accidental misuse if a future caller tried to hand a raw
`dict` or `datetime` to a wire-level helper, and matches what gmqtt
can actually serialize without surprises.
@nanomad nanomad merged commit 653da84 into develop May 10, 2026
6 checks passed
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