Skip to content

Double-unregister of a callback raises KeyError (use set.discard) #39

@bluetoothbot

Description

@bluetoothbot

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions