Bug: double-unregister raises KeyError
AIOUSBWatcher.async_register_callback() returns an unregister callable backed by:
def _async_unregister_callback(self, callback: Callable[[], None]) -> None:
self._callbacks.remove(callback)
set.remove() raises KeyError if the element is absent. The unregister callable
is handed to the caller with no guard against being invoked twice, so any of these
reachable patterns crash:
- Caller defensively calls the returned unregister function more than once.
- A callback unregisters itself on first fire and the caller also calls the
unregister on teardown.
- The same callable was registered/unregistered and then unregistered again.
Fix
Use discard() (idempotent) instead of remove():
def _async_unregister_callback(self, callback: Callable[[], None]) -> None:
self._callbacks.discard(callback)
Regression test
@pytest.mark.asyncio
async def test_double_unregister_is_idempotent() -> None:
watcher = AIOUSBWatcher()
unregister = watcher.async_register_callback(lambda: None)
unregister()
unregister() # must not raise
Note: distinct from #37 (set mutation during iteration, addressed by #38). This is
about the unregister path itself being non-idempotent.
🤖 Filed by Kōan (review-mode audit)
Bug: double-unregister raises
KeyErrorAIOUSBWatcher.async_register_callback()returns an unregister callable backed by:set.remove()raisesKeyErrorif the element is absent. The unregister callableis handed to the caller with no guard against being invoked twice, so any of these
reachable patterns crash:
unregister on teardown.
Fix
Use
discard()(idempotent) instead ofremove():Regression test
Note: distinct from #37 (set mutation during iteration, addressed by #38). This is
about the unregister path itself being non-idempotent.
🤖 Filed by Kōan (review-mode audit)