Problem
Signal._handlers is typed list[Callable[..., None]], and fire(**kwargs: Any) forwards arbitrary kwargs through to each registered handler. There is no compile-time, mypy-time, or runtime check that callers are passing the keys handlers expect (zc, service_type, state_change) or that handlers declare them. A typo in a fire-site key, or a handler that forgets one of the keyword args, fails only at the moment a real event happens to dispatch — which on quiet networks may be hours or days into the run. The signature also makes refactoring the dispatch contract effectively undetectable: adding or renaming a key shows up nowhere in type-checking. Because ServiceBrowser, AsyncServiceBrowser, and external consumers all wire callbacks through this mechanism, the contract is load-bearing for the whole browse API.
Why This Matters
The lack of contract masks integration bugs in third-party code that subscribes to ServiceBrowser, and it blocks mypy from helping anyone (including this repo) catch dispatch mismatches when the dispatch shape ever changes.
Suggested Fix
Lock the contract down. Define a Protocol describing the handler signature (def __call__(self, zeroconf: Zeroconf, service_type: str, name: str, state_change: ServiceStateChange) -> None), type _handlers as list[that protocol], and change fire to take positional/keyword parameters that match — def fire(self, *, zeroconf: Zeroconf, service_type: str, name: str, state_change: ServiceStateChange) -> None. Keep backward compatibility by leaving the register_handler accepting Callable[..., None] but type-narrowing internally. Test coverage already exists for the happy paths, so the migration is mostly mechanical.
Details
|
|
| Severity |
🟡 Medium |
| Category |
interface_risk |
| Location |
src/zeroconf/_services/__init__.py:51-78 |
| Effort |
🛠️ Moderate effort |
🤖 Created by Kōan from audit session
Problem
Signal._handlersis typedlist[Callable[..., None]], andfire(**kwargs: Any)forwards arbitrary kwargs through to each registered handler. There is no compile-time, mypy-time, or runtime check that callers are passing the keys handlers expect (zc,service_type,state_change) or that handlers declare them. A typo in a fire-site key, or a handler that forgets one of the keyword args, fails only at the moment a real event happens to dispatch — which on quiet networks may be hours or days into the run. The signature also makes refactoring the dispatch contract effectively undetectable: adding or renaming a key shows up nowhere in type-checking. BecauseServiceBrowser,AsyncServiceBrowser, and external consumers all wire callbacks through this mechanism, the contract is load-bearing for the whole browse API.Why This Matters
The lack of contract masks integration bugs in third-party code that subscribes to
ServiceBrowser, and it blocks mypy from helping anyone (including this repo) catch dispatch mismatches when the dispatch shape ever changes.Suggested Fix
Lock the contract down. Define a
Protocoldescribing the handler signature (def __call__(self, zeroconf: Zeroconf, service_type: str, name: str, state_change: ServiceStateChange) -> None), type_handlersaslist[that protocol], and changefireto take positional/keyword parameters that match —def fire(self, *, zeroconf: Zeroconf, service_type: str, name: str, state_change: ServiceStateChange) -> None. Keep backward compatibility by leaving theregister_handleracceptingCallable[..., None]but type-narrowing internally. Test coverage already exists for the happy paths, so the migration is mostly mechanical.Details
src/zeroconf/_services/__init__.py:51-78🤖 Created by Kōan from audit session