Skip to content

Wire delegates on characteristics and services - #563

Closed
ikalchev wants to merge 1 commit into
refactor/getter-error-isolationfrom
refactor/legacy-facade
Closed

ikalchev wants to merge 1 commit into
refactor/getter-error-isolationfrom
refactor/legacy-facade

Conversation

@ikalchev

Copy link
Copy Markdown
Owner

Stacked on #561 (APIs: #562). The registration side of the delegate interface — delegates can now be assigned and validated, still without being dispatched.

What

  • Characteristic.delegate / Service.delegate properties — assign a CharacteristicDelegate / ServiceDelegate instance, or its class (instantiated with no arguments). None or del removes it.
  • Assignment is when everything is decided ("handled in init only", as the interface promises): the overridden methods are recorded, and an overridden method that is not a coroutine function fails at the assignment with a TypeError naming the method — no sync/async detection later, ever.
  • Service.configure_char(..., delegate=...) wires a delegate while configuring the characteristic, alongside the legacy callbacks.
class MyLock(CharacteristicDelegate):
    async def on_set(self, value, context):
        ...

char = service.configure_char("LockTargetState", delegate=MyLock)  # class or instance
  • The legacy attributes become storage properties: setter_callback / getter_callback on Characteristic and Service keep returning the assigned callable unchanged, so the driver, the camera stack and tests that patch.object them see no difference — including the delattr teardown of patch.object, which is why the properties implement deleters. One instance may serve many characteristics; the context says which one a call is about.

Not in this PR (deliberately): dispatch. The write and read pipelines call delegates starting with the next PR in the stack (the gate pipeline); until then a delegate is wired, validated, but not called. Which of delegate vs. legacy callback wins is also decided there. CHANGELOG stays untouched per review.

Tests

+14 tests: instance/class/removal/replacement round-trips, wrong-type and sync-override TypeErrors with the right messages, hook recording, legacy/delegate coexistence, legacy removal semantics, configure_char wiring at both the instance and class level, and the service-level equivalents. Three existing tests reach assert_called_once_with through the old plain attributes; they now keep their mock in a local, which is cleaner anyway.

Stack tip: 1743 tests pass, ruff clean, ty at the pre-existing baseline with zero new findings, docs -W clean.

The registration side of the delegate interface: a characteristic or a
service gets a delegate property, and Service.configure_char accepts
one. Assigning an instance or a class - instantiated with no arguments
- is when everything about the delegate is decided: the driver records
which methods it overrides, and an overridden method that is not a
coroutine function fails right there with a TypeError. The read and
write paths never introspect a delegate again; None or del removes it.

The legacy setter_callback/getter_callback attributes become storage
properties that keep returning the assigned callable, so existing code
- the driver, the camera stack, tests that patch them - sees no change.
Assigning a delegate does not remove a legacy callback; which one wins
is decided when the pipelines dispatch delegates.

The driver does not call delegates yet - that is the gate pipeline
change. This wires them so everything about them is validated at
assignment, as the interface promises.
@codecov

codecov Bot commented Nov 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.10390% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.08%. Comparing base (e64d6e5) to head (08e6cd1).

Files with missing lines Patch % Lines
pyhap/service.py 91.30% 2 Missing ⚠️
pyhap/characteristic.py 96.66% 1 Missing ⚠️
Additional details and impacted files
@@                         Coverage Diff                         @@
##           refactor/getter-error-isolation     #563      +/-   ##
===================================================================
+ Coverage                            92.06%   92.08%   +0.02%     
===================================================================
  Files                                   54       54              
  Lines                                10920    10996      +76     
  Branches                              1407     1413       +6     
===================================================================
+ Hits                                 10053    10126      +73     
- Misses                                 579      582       +3     
  Partials                               288      288              
Files with missing lines Coverage Δ
pyhap/delegates.py 100.00% <100.00%> (ø)
pyhap/characteristic.py 97.85% <96.66%> (-0.15%) ⬇️
pyhap/service.py 97.72% <91.30%> (-2.28%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ikalchev

Copy link
Copy Markdown
Owner Author

Closing per maintainer decision — continuing locally; the work continues on local branches and will return as PRs when the design settles.

@ikalchev ikalchev closed this Nov 13, 2026
@ikalchev
ikalchev deleted the refactor/legacy-facade branch November 13, 2026 20:18
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