Hoist offloader/receiver lifecycle scaffolding to a shared base#638
Merged
Conversation
The two sibling controllers each carried the same four init lines (_db, _tasks, _listeners, _shutdown_callbacks) and an identical _track_task method body. Single-inheritance base class (:class:`_RemoteBuildBase` in ``_shared.py``) collapses the duplication without the type-checker headaches the multiple-inheritance mixin attempt had — every attribute is declared once and reached through one MRO step. Concrete subclasses call ``super().__init__(device_builder)`` and layer role-specific state on top; their own ``start`` / ``stop`` continue to live on each class since the sequences are role-specific. ``drain_tasks`` stays as a free function in the same module — it's stateless and the per-role ``stop`` callers already use it that way. A wider ``TaskTracker``-style base that :class:`DeviceStateMonitor` could also share is deferred to a follow-up PR.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #638 +/- ##
==========================================
- Coverage 99.22% 99.22% -0.01%
==========================================
Files 92 92
Lines 10804 10800 -4
==========================================
- Hits 10720 10716 -4
Misses 84 84
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Contributor
There was a problem hiding this comment.
Pull request overview
Refactors the remote-build offloader/receiver siblings to share identical lifecycle scaffolding via a new _RemoteBuildBase in controllers/remote_build/_shared.py, reducing duplication while preserving role-specific start/stop behavior.
Changes:
- Introduce
_RemoteBuildBase(shared_db,_tasks,_listeners,_shutdown_callbacks, and_track_task) alongside existingdrain_tasks. - Update
OffloaderControllerto inherit from_RemoteBuildBaseand remove duplicated init/task-tracking code. - Update
ReceiverControllerto inherit from_RemoteBuildBaseand remove duplicated init/task-tracking code.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| esphome_device_builder/controllers/remote_build/_shared.py | Adds _RemoteBuildBase lifecycle scaffolding shared by offloader/receiver and expands module docs. |
| esphome_device_builder/controllers/remote_build/offloader.py | Switches offloader to inherit from _RemoteBuildBase, removing duplicated fields and _track_task. |
| esphome_device_builder/controllers/remote_build/receiver.py | Switches receiver to inherit from _RemoteBuildBase, removing duplicated fields and _track_task. |
CLAUDE.md's docstring rule: consumer-facing, no padding with implementation history. The first pass leaned on the mixin- rejection rationale and the field-purpose paraphrases that the well-named fields already convey. Module docstring now lists what the module exposes (the base class and the free helper) and what each is for. Class docstring tells subclasses how to use it -- super().__init__, where stop() responsibilities lie -- and drops the per-field explainers.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this implement/fix?
Hoists the four lines of lifecycle scaffolding that
:class:
OffloaderControllerand :class:ReceiverControllercarried verbatim into a small base class —
:class:
_RemoteBuildBaseincontrollers/remote_build/_shared.py.The duplicated state:
self._db = device_builder— every controller in thepackage needs the
DeviceBuilderref.self._tasks: set[asyncio.Task[None]] = set()— strong-refset for fire-and-forget tasks.
self._listeners = ExitStack()— bus-listener unsubscriberswalked at stop time.
self._shutdown_callbacks: list[ShutdownCallback] = []—per-file
Storeflush callbacks walked at stop time.Plus the identical 4-line
_track_taskmethod.After this PR, each sibling's
__init__shrinks by ~5 lines(state +
_track_taskbody), the duplicate state-init ordermatches between siblings by virtue of the same constructor,
and any future role controller in this package can inherit
the same scaffolding without copy-paste.
Why single-inheritance and not a mixin
The earlier mixin attempt at the role split was rejected
because multiple-inheritance state-sharing made type-checker
attribute resolution unreliable (a mixin reaching for
self._pairingscouldn't be statically confirmed to land ona class that defined it). Single-inheritance doesn't have
that problem: every attribute resolves through one MRO step,
mypy sees them on the base, and the subclasses' specific
state stays on the subclasses.
What stayed where
drain_tasksis still a free function in the same module —it's stateless and the per-role
stopcallers already passin a specific iterable (
self._tasks,self._pair_status_listeners.values(),h.task for h in self._peer_link_clients.values()), so thefunction shape is the right one. Moving it to a method
would force callers to instance-bind for no win.
startandstopstay on the subclass —the sequences are role-specific (offloader brings up mDNS
browse + peer-link clients; receiver brings up
SubmitJobReceiver/ArtifactsDownloadSender/JobFanout), and a base-class default no-op wouldn'thelp.
Follow-up
A wider
TaskTracker-style base that:class:
DeviceStateMonitorcould also share is deferred to afollow-up PR. The monitor has the same
task.add_done_callback(self._tasks.discard)pattern inthree places but doesn't take a
DeviceBuilder, doesn't keeplisteners, and doesn't manage shutdown callbacks — so the
shared base for it has to be carved out separately, and
_RemoteBuildBaseitself would then inherit from thatsmaller base.
Behaviour preservation
Pure refactor. Each
__init__initializes the same fourfields with the same default values;
_track_taskis thesame 4 lines just on the parent now. 3117 tests pass, ruff
Related issue or feature (if applicable):
Conversation flagged the lifecycle duplication; this
collapses it now that the role split has landed and
type-checker behaviour is settled.
Types of changes
bugfixnew-featureenhancementbreaking-changerefactordocsmaintenancecidependenciesFrontend coordination
Checklist
ruff,codespell, yaml/json/python checks).tests/where applicable.components.jsonhas not been hand-edited (regenerate viascript/sync_components.pyif a sync is needed).docs/ARCHITECTURE.mdand/ordocs/API.md.