From ce24b4988e8551c88c165b3b28323b5beded1a3f Mon Sep 17 00:00:00 2001 From: fan wu <336798967+FanWu-ai@users.noreply.github.com> Date: Tue, 20 Oct 2026 06:49:12 +0900 Subject: [PATCH] Remove every matching key binding and invalidate caches --- .../key_binding/key_bindings.py | 4 +- tests/test_key_binding.py | 77 +++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/src/prompt_toolkit/key_binding/key_bindings.py b/src/prompt_toolkit/key_binding/key_bindings.py index 5fd56394b..3429411f2 100644 --- a/src/prompt_toolkit/key_binding/key_bindings.py +++ b/src/prompt_toolkit/key_binding/key_bindings.py @@ -337,7 +337,7 @@ def remove(self, *args: Keys | str | KeyHandlerCallable) -> None: function = args[0] # Remove the given function. - for b in self.bindings: + for b in self.bindings[:]: if b.handler == function: self.bindings.remove(b) found = True @@ -349,7 +349,7 @@ def remove(self, *args: Keys | str | KeyHandlerCallable) -> None: # Remove this sequence of key bindings. keys = tuple(_parse_key(k) for k in args) - for b in self.bindings: + for b in self.bindings[:]: if b.keys == keys: self.bindings.remove(b) found = True diff --git a/tests/test_key_binding.py b/tests/test_key_binding.py index 479559c61..b55e707e0 100644 --- a/tests/test_key_binding.py +++ b/tests/test_key_binding.py @@ -202,3 +202,80 @@ def handler(event): assert events[1].previous_key_sequence[0].data == "a" assert events[1].previous_key_sequence[1].key == "a" assert events[1].previous_key_sequence[1].data == "a" + + +@pytest.mark.parametrize("remove_by_handler", [False, True]) +@pytest.mark.parametrize("match_count", [2, 3, 4]) +@pytest.mark.parametrize("adjacent", [False, True]) +def test_remove_all_matching_bindings( + handlers, remove_by_handler, match_count, adjacent +): + from prompt_toolkit.key_binding.key_bindings import merge_key_bindings + + bindings = KeyBindings() + removed = handlers.removed + retained = handlers.retained + bindings.add("b")(retained) + for _ in range(match_count): + bindings.add("a", "a")(removed) + if not adjacent: + bindings.add("b")(retained) + bindings.add("c")(retained) + + original_list = bindings.bindings + expected = [b for b in original_list if b.handler == retained] + merged = merge_key_bindings([bindings]) + # Populate both lookup caches in the original registry and its proxy. + for registry in (bindings, merged): + assert len(registry.get_bindings_for_keys(("a", "a"))) == match_count + assert len(registry.get_bindings_starting_with_keys(("a",))) == match_count + version = bindings._version + + if remove_by_handler: + bindings.remove(removed) + else: + bindings.remove("a", "a") + + assert bindings.bindings is original_list + assert bindings.bindings == expected + assert bindings._version != version + for registry in (bindings, merged): + assert registry.get_bindings_for_keys(("a", "a")) == [] + assert registry.get_bindings_starting_with_keys(("a",)) == [] + assert registry.bindings == expected + + with set_dummy_app(): + processor = KeyProcessor(merged) + processor.feed_multiple([KeyPress("a"), KeyPress("a"), KeyPress("c")]) + processor.process_keys() + assert handlers.called == ["retained"] + + +def test_remove_missing_handler_keeps_registry(handlers): + bindings = KeyBindings() + handler = handlers.retained + bindings.add("a")(handler) + expected = bindings.bindings[:] + version = bindings._version + with pytest.raises(ValueError, match="Binding not found"): + bindings.remove(handlers.missing) + assert bindings.bindings == expected + assert bindings._version == version + + +def test_remove_handler_with_multiple_keys(handlers): + bindings = KeyBindings() + handler = handlers.removed + bindings.add("a")(handler) + bindings.add("b")(handler) + bindings.add("c")(handler) + bindings.remove(handler) + assert bindings.bindings == [] + + +def test_remove_duplicate_key_aliases(handlers): + bindings = KeyBindings() + bindings.add("enter")(handlers.first) + bindings.add("c-m")(handlers.second) + bindings.remove("enter") + assert bindings.bindings == []