bleak/pairing: add the pairing callback API#1992
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a new pairing API surface (callbacks + IO capability derivation) and a focused unit test suite to validate capability selection from provided callbacks.
Changes:
- Introduces
bleak.pairingwithPairingCallbacks,IOCapability, andio_capability(). - Documents BLE pairing ceremonies and how callbacks map to advertised IO capabilities.
- Adds parametrized tests to validate
io_capability()behavior across callback combinations.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
bleak/pairing.py |
Adds pairing types + capability derivation function and documentation. |
tests/test_pairing.py |
Adds parametrized tests for callback-to-capability mapping. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #1992 +/- ##
===========================================
+ Coverage 52.45% 52.98% +0.52%
===========================================
Files 43 44 +1
Lines 4097 4143 +46
Branches 504 508 +4
===========================================
+ Hits 2149 2195 +46
Misses 1817 1817
Partials 131 131
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
| """The largest valid BLE passkey; passkeys are six decimal digits (000000-999999).""" | ||
|
|
||
|
|
||
| class PairingCallbacks(NamedTuple): |
There was a problem hiding this comment.
Maybe this was explained in the other PR, but why is this a NamedTuple instead of a regular class?
I think a regular class would be nice since it would have named parameters.
There was a problem hiding this comment.
NamedTuple has named fields, it's a struct. This is better than a class because it's fit for the specific purpose: a collection of optional functions.
The other option would be a frozen dataclass, or both + a protocol to allow users to create their own object. I believe that would allow users to define a class with static methods and properties to fulfill the protocol, but that's very clunky compared to a struct.
There was a problem hiding this comment.
Re: the specific question, NamedTuples do support named parameters:
# Passkey Entry (input) — type in the value the peer is displaying
async def request_passkey(device: BLEDevice) -> int | None:
raw = input(f"Enter the passkey shown on {device.name}: ")
return int(raw) if raw else None
callbacks = PairingCallbacks(request_passkey=request_passkey)# every field passed by name, order irrelevant
callbacks = PairingCallbacks(
display_passkey=display_passkey,
request_passkey=request_passkey,
)Usage
>>> callbacks = PairingCallbacks(confirm=confirm)
>>> callbacks.confirm is confirm
True
>>> callbacks.request_passkey is None # unset fields default to None
True
>>> io_capability(callbacks)
<IOCapability.DISPLAY_YES_NO: 2>There was a problem hiding this comment.
I do like the suggestion of a protocol, but I don't think that works technically with optional methods.
dataclass doesn't seem quite right since we don't care about equality and things like that (we are just dealing with functions).
Re: the parameters, I meant the parameters of the Callable types.
Anyway, in general I do tend to prefer functional style programming over object oriented, so in that regard the NamedTuple is nice. I see pros and cons each way and not a clear "best" option.
There was a problem hiding this comment.
Re: the parameters, I meant the parameters of the Callable types.
Aw, good point! I'm going to experiment with a protocol union that allows users to impl ABCs that satisfy some subset of the callbacks. The lib code will be slightly more complex, but the user code will be more self-documenting.
|
|
||
|
|
||
| @runtime_checkable | ||
| class CanConfirm(Protocol): |
There was a problem hiding this comment.
| class CanConfirm(Protocol): | |
| class SupportsConfirm(Protocol): |
There seems to be a convention already in Python to use Supports for protocols.
| """ | ||
|
|
||
|
|
||
| class ConfirmBase(ABC, CanConfirm): |
There was a problem hiding this comment.
Do we really need these ABCs? It seems like we could just make the protocol @runtime_checkable and inherit from that directly.
There was a problem hiding this comment.
Seem to be narrowing on something elegant: combining abstractmethod with the runtime_checkable. Appears to be PEP 544 compliant.
There was a problem hiding this comment.
Pushed that version, I like it a lot.
This is the foundation the BlueZ and WinRT pairing implementations build on. Supersedes the bleak.agent module from hbldh#1864 / hbldh#1990 (renamed to bleak.pairing; "agent" is a BlueZ-ism). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Split out of #1990 — the backend-agnostic foundation, with no wiring yet so it can be reviewed on its own.
What
Adds a new
bleak.pairingmodule:PairingCallbacks— aNamedTupleof optional async handlers:confirm(device, passkey)(Numeric Comparison) andrequest_passkey(device)/display_passkey(device, passkey)(Passkey Entry, both directions).IOCapability— the advertised Security Manager I/O capability.io_capability(callbacks)— derives the capability from which callbacks are set (e.g.confirmimplies a display).MAX_PASSKEY— the 6-digit passkey ceiling.Which callbacks are provided drives the negotiated pairing ceremony; with none, Just Works is accepted automatically (unchanged behavior). The backends consume this in follow-up PRs.
Testing
io_capability()across every callback combination.🤖 Generated with Claude Code