Add a proper MatchResult type instead of tuples
This commit is contained in:
parent
e0e7f6a797
commit
9557885ace
|
|
@ -23,6 +23,7 @@ import string
|
|||
import types
|
||||
import typing
|
||||
|
||||
import attr
|
||||
from PyQt5.QtCore import pyqtSignal, QObject, Qt
|
||||
from PyQt5.QtGui import QKeySequence, QKeyEvent
|
||||
from PyQt5.QtWidgets import QWidget
|
||||
|
|
@ -31,10 +32,21 @@ from qutebrowser.config import config
|
|||
from qutebrowser.utils import usertypes, log, utils
|
||||
from qutebrowser.keyinput import keyutils
|
||||
|
||||
_MatchType = typing.Tuple[QKeySequence.SequenceMatch, typing.Optional[str]]
|
||||
_MatchSequenceType = typing.Tuple[QKeySequence.SequenceMatch,
|
||||
typing.Optional[str],
|
||||
keyutils.KeySequence]
|
||||
|
||||
@attr.s(frozen=True)
|
||||
class MatchResult:
|
||||
|
||||
"""The result of matching a keybinding."""
|
||||
|
||||
match_type = attr.ib() # type: QKeySequence.SequenceMatch
|
||||
command = attr.ib() # type: typing.Optional[str]
|
||||
sequence = attr.ib() # type: keyutils.KeySequence
|
||||
|
||||
def __attrs_post_init__(self):
|
||||
if self.match_type == QKeySequence.ExactMatch:
|
||||
assert self.command is not None
|
||||
else:
|
||||
assert self.command is None
|
||||
|
||||
|
||||
class BindingTrie:
|
||||
|
|
@ -69,10 +81,10 @@ class BindingTrie:
|
|||
|
||||
def __getitem__(self,
|
||||
sequence: keyutils.KeySequence) -> typing.Optional[str]:
|
||||
matchtype, command = self.matches(sequence)
|
||||
if matchtype != QKeySequence.ExactMatch:
|
||||
result = self.matches(sequence)
|
||||
if result.match_type != QKeySequence.ExactMatch:
|
||||
raise KeyError(sequence)
|
||||
return command
|
||||
return result.command
|
||||
|
||||
def __setitem__(self, sequence: keyutils.KeySequence,
|
||||
command: str) -> None:
|
||||
|
|
@ -85,8 +97,7 @@ class BindingTrie:
|
|||
node.command = command
|
||||
|
||||
def __contains__(self, sequence: keyutils.KeySequence) -> bool:
|
||||
matchtype, _command = self.matches(sequence)
|
||||
return matchtype == QKeySequence.ExactMatch
|
||||
return self.matches(sequence).match_type == QKeySequence.ExactMatch
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return utils.get_repr(self, children=self.children,
|
||||
|
|
@ -97,33 +108,36 @@ class BindingTrie:
|
|||
for key in mapping:
|
||||
self[key] = mapping[key]
|
||||
|
||||
def matches(self, sequence: keyutils.KeySequence) -> _MatchType:
|
||||
def matches(self, sequence: keyutils.KeySequence) -> MatchResult:
|
||||
"""Try to match a given keystring with any bound keychain.
|
||||
|
||||
Args:
|
||||
sequence: The key sequence to match.
|
||||
|
||||
Return:
|
||||
A tuple (matchtype, binding).
|
||||
matchtype: QKeySequence.ExactMatch, QKeySequence.PartialMatch
|
||||
or QKeySequence.NoMatch.
|
||||
binding: - None with QKeySequence.PartialMatch or
|
||||
QKeySequence.NoMatch.
|
||||
- The found binding with QKeySequence.ExactMatch.
|
||||
A MatchResult object.
|
||||
"""
|
||||
node = self
|
||||
for key in sequence:
|
||||
try:
|
||||
node = node.children[key]
|
||||
except KeyError:
|
||||
return QKeySequence.NoMatch, None
|
||||
return MatchResult(match_type=QKeySequence.NoMatch,
|
||||
command=None,
|
||||
sequence=sequence)
|
||||
|
||||
if node.command is not None:
|
||||
return QKeySequence.ExactMatch, node.command
|
||||
return MatchResult(match_type=QKeySequence.ExactMatch,
|
||||
command=node.command,
|
||||
sequence=sequence)
|
||||
elif node.children:
|
||||
return QKeySequence.PartialMatch, None
|
||||
return MatchResult(match_type=QKeySequence.PartialMatch,
|
||||
command=None,
|
||||
sequence=sequence)
|
||||
else: # This can only happen when there are no bindings at all.
|
||||
return QKeySequence.NoMatch, None
|
||||
return MatchResult(match_type=QKeySequence.NoMatch,
|
||||
command=None,
|
||||
sequence=sequence)
|
||||
|
||||
|
||||
class BaseKeyParser(QObject):
|
||||
|
|
@ -188,7 +202,7 @@ class BaseKeyParser(QObject):
|
|||
if self.do_log:
|
||||
log.keyboard.debug(message)
|
||||
|
||||
def _match_key(self, sequence: keyutils.KeySequence) -> _MatchType:
|
||||
def _match_key(self, sequence: keyutils.KeySequence) -> MatchResult:
|
||||
"""Try to match a given keystring with any bound keychain.
|
||||
|
||||
Args:
|
||||
|
|
@ -202,19 +216,17 @@ class BaseKeyParser(QObject):
|
|||
"""
|
||||
assert sequence
|
||||
assert not isinstance(sequence, str)
|
||||
|
||||
return self.bindings.matches(sequence)
|
||||
|
||||
def _match_without_modifiers(
|
||||
self, sequence: keyutils.KeySequence) -> _MatchSequenceType:
|
||||
self, sequence: keyutils.KeySequence) -> MatchResult:
|
||||
"""Try to match a key with optional modifiers stripped."""
|
||||
self._debug_log("Trying match without modifiers")
|
||||
sequence = sequence.strip_modifiers()
|
||||
match, binding = self._match_key(sequence)
|
||||
return match, binding, sequence
|
||||
return self._match_key(sequence)
|
||||
|
||||
def _match_key_mapping(
|
||||
self, sequence: keyutils.KeySequence) -> _MatchSequenceType:
|
||||
self, sequence: keyutils.KeySequence) -> MatchResult:
|
||||
"""Try to match a key in bindings.key_mappings."""
|
||||
self._debug_log("Trying match with key_mappings")
|
||||
mapped = sequence.with_mappings(
|
||||
|
|
@ -222,10 +234,10 @@ class BaseKeyParser(QObject):
|
|||
if sequence != mapped:
|
||||
self._debug_log("Mapped {} -> {}".format(
|
||||
sequence, mapped))
|
||||
match, binding = self._match_key(mapped)
|
||||
sequence = mapped
|
||||
return match, binding, sequence
|
||||
return QKeySequence.NoMatch, None, sequence
|
||||
return self._match_key(mapped)
|
||||
return MatchResult(match_type=QKeySequence.NoMatch,
|
||||
command=None,
|
||||
sequence=sequence)
|
||||
|
||||
def _match_count(self, sequence: keyutils.KeySequence,
|
||||
dry_run: bool) -> bool:
|
||||
|
|
@ -274,40 +286,41 @@ class BaseKeyParser(QObject):
|
|||
self.clear_keystring()
|
||||
return QKeySequence.NoMatch
|
||||
|
||||
match, binding = self._match_key(sequence)
|
||||
if match == QKeySequence.NoMatch:
|
||||
match, binding, sequence = self._match_without_modifiers(sequence)
|
||||
if match == QKeySequence.NoMatch:
|
||||
match, binding, sequence = self._match_key_mapping(sequence)
|
||||
if match == QKeySequence.NoMatch:
|
||||
was_count = self._match_count(sequence, dry_run)
|
||||
result = self._match_key(sequence)
|
||||
if result.match_type == QKeySequence.NoMatch:
|
||||
result = self._match_without_modifiers(result.sequence)
|
||||
if result.match_type == QKeySequence.NoMatch:
|
||||
result = self._match_key_mapping(result.sequence)
|
||||
if result.match_type == QKeySequence.NoMatch:
|
||||
was_count = self._match_count(result.sequence, dry_run)
|
||||
if was_count:
|
||||
return QKeySequence.ExactMatch
|
||||
|
||||
if dry_run:
|
||||
return match
|
||||
return result.match_type
|
||||
|
||||
self._sequence = sequence
|
||||
self._sequence = result.sequence
|
||||
|
||||
if match == QKeySequence.ExactMatch:
|
||||
assert binding is not None
|
||||
if result.match_type == QKeySequence.ExactMatch:
|
||||
assert result.command is not None
|
||||
self._debug_log("Definitive match for '{}'.".format(
|
||||
sequence))
|
||||
result.sequence))
|
||||
count = int(self._count) if self._count else None
|
||||
self.clear_keystring()
|
||||
self.execute(binding, count)
|
||||
elif match == QKeySequence.PartialMatch:
|
||||
self.execute(result.command, count)
|
||||
elif result.match_type == QKeySequence.PartialMatch:
|
||||
self._debug_log("No match for '{}' (added {})".format(
|
||||
sequence, txt))
|
||||
result.sequence, txt))
|
||||
self.keystring_updated.emit(self._count + str(sequence))
|
||||
elif match == QKeySequence.NoMatch:
|
||||
elif result.match_type == QKeySequence.NoMatch:
|
||||
self._debug_log("Giving up with '{}', no matches".format(
|
||||
sequence))
|
||||
result.sequence))
|
||||
self.clear_keystring()
|
||||
else:
|
||||
raise utils.Unreachable("Invalid match value {!r}".format(match))
|
||||
raise utils.Unreachable("Invalid match value {!r}".format(
|
||||
result.match_type))
|
||||
|
||||
return match
|
||||
return result.match_type
|
||||
|
||||
@config.change_filter('bindings')
|
||||
def _on_config_changed(self) -> None:
|
||||
|
|
|
|||
|
|
@ -31,18 +31,21 @@ from qutebrowser.keyinput import keyutils
|
|||
from unit.keyinput import test_keyutils
|
||||
|
||||
|
||||
@pytest.mark.parametrize('entered, configured, expected',
|
||||
@pytest.mark.parametrize('entered, configured, match_type',
|
||||
test_keyutils.TestKeySequence.MATCH_TESTS)
|
||||
def test_matches_single(entered, configured, expected):
|
||||
def test_matches_single(entered, configured, match_type):
|
||||
entered = keyutils.KeySequence.parse(entered)
|
||||
configured = keyutils.KeySequence.parse(configured)
|
||||
trie = basekeyparser.BindingTrie()
|
||||
trie[configured] = "eeloo"
|
||||
ret_expected = "eeloo" if expected == QKeySequence.ExactMatch else None
|
||||
assert trie.matches(entered) == (expected, ret_expected)
|
||||
command = "eeloo" if match_type == QKeySequence.ExactMatch else None
|
||||
result = basekeyparser.MatchResult(match_type=match_type,
|
||||
command=command,
|
||||
sequence=entered)
|
||||
assert trie.matches(entered) == result
|
||||
|
||||
|
||||
@pytest.mark.parametrize('configured, expected_results', [
|
||||
@pytest.mark.parametrize('configured, expected', [
|
||||
([],
|
||||
# null match
|
||||
[('a', QKeySequence.NoMatch),
|
||||
|
|
@ -62,17 +65,20 @@ def test_matches_single(entered, configured, expected):
|
|||
[('a', QKeySequence.ExactMatch),
|
||||
('!', QKeySequence.NoMatch)]),
|
||||
])
|
||||
def test_matches_tree(configured, expected_results, benchmark):
|
||||
def test_matches_tree(configured, expected, benchmark):
|
||||
trie = basekeyparser.BindingTrie()
|
||||
trie.update({keyutils.KeySequence.parse(keys): "eeloo"
|
||||
for keys in configured})
|
||||
|
||||
def run():
|
||||
for entered, expected in expected_results:
|
||||
for entered, match_type in expected:
|
||||
sequence = keyutils.KeySequence.parse(entered)
|
||||
ret_expected = ("eeloo" if expected == QKeySequence.ExactMatch
|
||||
else None)
|
||||
assert trie.matches(sequence) == (expected, ret_expected)
|
||||
command = ("eeloo" if match_type == QKeySequence.ExactMatch
|
||||
else None)
|
||||
result = basekeyparser.MatchResult(match_type=match_type,
|
||||
command=command,
|
||||
sequence=sequence)
|
||||
assert trie.matches(sequence) == result
|
||||
|
||||
benchmark(run)
|
||||
|
||||
|
|
|
|||
|
|
@ -380,11 +380,11 @@ class TestKeySequence:
|
|||
('', 'a', QKeySequence.PartialMatch),
|
||||
('a', '', QKeySequence.NoMatch)]
|
||||
|
||||
@pytest.mark.parametrize('entered, configured, expected', MATCH_TESTS)
|
||||
def test_matches(self, entered, configured, expected):
|
||||
@pytest.mark.parametrize('entered, configured, match_type', MATCH_TESTS)
|
||||
def test_matches(self, entered, configured, match_type):
|
||||
entered = keyutils.KeySequence.parse(entered)
|
||||
configured = keyutils.KeySequence.parse(configured)
|
||||
assert entered.matches(configured) == expected
|
||||
assert entered.matches(configured) == match_type
|
||||
|
||||
@pytest.mark.parametrize('old, key, modifiers, text, expected', [
|
||||
('a', Qt.Key_B, Qt.NoModifier, 'b', 'ab'),
|
||||
|
|
|
|||
Loading…
Reference in New Issue