qutebrowser/tests/unit/keyinput/test_keyutils.py

654 lines
26 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import operator
import hypothesis
from hypothesis import strategies
import pytest
from qutebrowser.qt import machinery
from qutebrowser.qt.core import Qt, QEvent, pyqtSignal
from qutebrowser.qt.gui import QKeyEvent, QKeySequence
from qutebrowser.qt.widgets import QWidget
from helpers import testutils
from unit.keyinput import key_data
from qutebrowser.keyinput import keyutils
from qutebrowser.utils import utils
pyqt_enum_workaround_skip = pytest.mark.skipif(
isinstance(keyutils._NIL_KEY, int),
reason="Can't create QKey for unknown keys with this PyQt version"
)
try:
OE_KEY = Qt.Key(ord('Œ'))
except ValueError:
OE_KEY = None # affected tests skipped
@pytest.fixture(params=key_data.KEYS, ids=lambda k: k.attribute)
def qt_key(request):
"""Get all existing keys from key_data.py.
Keys which don't exist with this Qt version result in skipped tests.
"""
key = request.param
if key.member is None:
pytest.skip("Did not find key {}".format(key.attribute))
return key
@pytest.fixture(params=key_data.MODIFIERS, ids=lambda m: m.attribute)
def qt_mod(request):
"""Get all existing modifiers from key_data.py."""
mod = request.param
assert mod.member is not None
return mod
@pytest.fixture(params=[key for key in key_data.KEYS if key.qtest],
ids=lambda k: k.attribute)
def qtest_key(request):
"""Get keys from key_data.py which can be used with QTest."""
return request.param
def test_key_data_keys():
"""Make sure all possible keys are in key_data.KEYS."""
key_names = {name.removeprefix("Key_")
for name in testutils.enum_members(Qt, Qt.Key)}
key_data_names = {key.attribute for key in sorted(key_data.KEYS)}
diff = key_names - key_data_names
assert not diff
def test_key_data_modifiers():
"""Make sure all possible modifiers are in key_data.MODIFIERS."""
mod_names = {name.removesuffix("Modifier")
for name, value in testutils.enum_members(Qt, Qt.KeyboardModifier).items()
if value not in [Qt.KeyboardModifier.NoModifier, Qt.KeyboardModifier.KeyboardModifierMask]}
mod_data_names = {mod.attribute for mod in sorted(key_data.MODIFIERS)}
diff = mod_names - mod_data_names
assert not diff
class KeyTesterWidget(QWidget):
"""Widget to get the text of QKeyPressEvents.
This is done so we can check QTest::keyToAscii (qasciikey.cpp) as we can't
call that directly, only via QTest::keyPress.
"""
got_text = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self.text = None
def keyPressEvent(self, e):
self.text = e.text()
self.got_text.emit()
class TestKeyInfoText:
@pytest.mark.parametrize('upper', [False, True])
def test_text(self, qt_key, upper):
"""Test KeyInfo.text() with all possible keys.
See key_data.py for inputs and expected values.
"""
modifiers = Qt.KeyboardModifier.ShiftModifier if upper else Qt.KeyboardModifier.NoModifier
info = keyutils.KeyInfo(qt_key.member, modifiers=modifiers)
expected = qt_key.uppertext if upper else qt_key.text
assert info.text() == expected
@pytest.fixture
def key_tester(self, qtbot):
w = KeyTesterWidget()
qtbot.add_widget(w)
return w
def test_text_qtest(self, qtest_key, qtbot, key_tester):
"""Make sure KeyInfo.text() lines up with QTest::keyToAscii.
See key_data.py for inputs and expected values.
"""
with qtbot.wait_signal(key_tester.got_text):
qtbot.keyPress(key_tester, qtest_key.member)
info = keyutils.KeyInfo(qtest_key.member)
assert info.text() == key_tester.text.lower()
class TestKeyToString:
def test_to_string(self, qt_key):
assert keyutils._key_to_string(qt_key.member) == qt_key.name
def test_modifiers_to_string(self, qt_mod):
expected = qt_mod.name + '+'
assert keyutils._modifiers_to_string(qt_mod.member) == expected
@pytest.mark.skipif(machinery.IS_QT6, reason="Can't delete enum members on PyQt 6")
def test_missing(self, monkeypatch):
monkeypatch.delattr(keyutils.Qt, 'Key_AltGr')
# We don't want to test the key which is actually missing - we only
# want to know if the mapping still behaves properly.
assert keyutils._key_to_string(Qt.Key.Key_A) == 'A'
@pytest.mark.parametrize('key, modifiers, expected', [
(Qt.Key.Key_A, Qt.KeyboardModifier.NoModifier, 'a'),
(Qt.Key.Key_A, Qt.KeyboardModifier.ShiftModifier, 'A'),
(Qt.Key.Key_Space, Qt.KeyboardModifier.NoModifier, '<Space>'),
(Qt.Key.Key_Space, Qt.KeyboardModifier.ShiftModifier, '<Shift+Space>'),
(Qt.Key.Key_Tab, Qt.KeyboardModifier.ShiftModifier, '<Shift+Tab>'),
(Qt.Key.Key_A, Qt.KeyboardModifier.ControlModifier, '<Ctrl+a>'),
(Qt.Key.Key_A, Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier, '<Ctrl+Shift+a>'),
(Qt.Key.Key_A,
Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.AltModifier | Qt.KeyboardModifier.MetaModifier | Qt.KeyboardModifier.ShiftModifier,
'<Meta+Ctrl+Alt+Shift+a>'),
pytest.param(OE_KEY, Qt.KeyboardModifier.NoModifier, '<Œ>',
marks=pyqt_enum_workaround_skip),
pytest.param(OE_KEY, Qt.KeyboardModifier.ShiftModifier, '<Shift+Œ>',
marks=pyqt_enum_workaround_skip),
pytest.param(OE_KEY, Qt.KeyboardModifier.GroupSwitchModifier, '<AltGr+Œ>',
marks=pyqt_enum_workaround_skip),
pytest.param(OE_KEY, Qt.KeyboardModifier.GroupSwitchModifier | Qt.KeyboardModifier.ShiftModifier, '<AltGr+Shift+Œ>'),
(Qt.Key.Key_Shift, Qt.KeyboardModifier.ShiftModifier, '<Shift>'),
(Qt.Key.Key_Shift, Qt.KeyboardModifier.ShiftModifier | Qt.KeyboardModifier.ControlModifier, '<Ctrl+Shift>'),
(Qt.Key.Key_Alt, Qt.KeyboardModifier.AltModifier, '<Alt>'),
(Qt.Key.Key_Shift, Qt.KeyboardModifier.GroupSwitchModifier | Qt.KeyboardModifier.ShiftModifier, '<AltGr+Shift>'),
(Qt.Key.Key_AltGr, Qt.KeyboardModifier.GroupSwitchModifier, '<AltGr>'),
])
def test_key_info_str(key, modifiers, expected):
assert str(keyutils.KeyInfo(key, modifiers)) == expected
def test_key_info_repr():
info = keyutils.KeyInfo(Qt.Key.Key_A, Qt.KeyboardModifier.ShiftModifier)
expected = (
"<qutebrowser.keyinput.keyutils.KeyInfo "
"key='Key_A' modifiers='ShiftModifier' text='A'>")
assert repr(info) == expected
@pytest.mark.parametrize('info1, info2, equal', [
(keyutils.KeyInfo(Qt.Key.Key_A, Qt.KeyboardModifier.NoModifier),
keyutils.KeyInfo(Qt.Key.Key_A, Qt.KeyboardModifier.NoModifier),
True),
(keyutils.KeyInfo(Qt.Key.Key_A, Qt.KeyboardModifier.NoModifier),
keyutils.KeyInfo(Qt.Key.Key_B, Qt.KeyboardModifier.NoModifier),
False),
(keyutils.KeyInfo(Qt.Key.Key_A, Qt.KeyboardModifier.NoModifier),
keyutils.KeyInfo(Qt.Key.Key_B, Qt.KeyboardModifier.ControlModifier),
False),
])
def test_hash(info1, info2, equal):
assert (hash(info1) == hash(info2)) == equal
@pytest.mark.parametrize('key, modifiers, text, expected', [
(0xd83c, Qt.KeyboardModifier.NoModifier, '🏻', '<🏻>'),
(0xd867, Qt.KeyboardModifier.NoModifier, '𩷶', '<𩷶>'),
(0xd867, Qt.KeyboardModifier.ShiftModifier, '𩷶', '<Shift+𩷶>'),
])
def test_surrogates(key, modifiers, text, expected, pyqt_enum_workaround):
evt = QKeyEvent(QEvent.Type.KeyPress, key, modifiers, text)
with pyqt_enum_workaround():
info = keyutils.KeyInfo.from_event(evt)
assert str(info) == expected
@pytest.mark.parametrize('keys, expected', [
([0x1f3fb], '<🏻>'),
([0x29df6], '<𩷶>'),
([Qt.Key.Key_Shift, 0x29df6], '<Shift><𩷶>'),
([0x1f468, 0x200d, 0x1f468, 0x200d, 0x1f466], '<👨><><👨><><👦>'),
])
@pyqt_enum_workaround_skip
def test_surrogate_sequences(keys, expected):
infos = [keyutils.KeyInfo(Qt.Key(key)) for key in keys]
seq = keyutils.KeySequence(*infos)
assert str(seq) == expected
# This shouldn't happen, but if it does we should handle it well
def test_surrogate_error(pyqt_enum_workaround):
evt = QKeyEvent(QEvent.Type.KeyPress, 0xd83e, Qt.KeyboardModifier.NoModifier, '🤞🏻')
with pytest.raises(keyutils.KeyParseError), pyqt_enum_workaround():
keyutils.KeyInfo.from_event(evt)
@pytest.mark.parametrize('keystr, expected', [
('foo', "Could not parse 'foo': error"),
(None, "Could not parse keystring: error"),
])
def test_key_parse_error(keystr, expected):
exc = keyutils.KeyParseError(keystr, "error")
assert str(exc) == expected
@pytest.mark.parametrize('keystr, parts', [
('a', ['a']),
('ab', ['a', 'b']),
('a<', ['a', '<']),
('a>', ['a', '>']),
('<a', ['<', 'a']),
('>a', ['>', 'a']),
('aA', ['a', 'Shift+A']),
('a<Ctrl+a>b', ['a', 'ctrl+a', 'b']),
('<Ctrl+a>a', ['ctrl+a', 'a']),
('a<Ctrl+a>', ['a', 'ctrl+a']),
('<Ctrl-a>', ['ctrl+a']),
('<Num-a>', ['num+a']),
])
def test_parse_keystr(keystr, parts):
assert list(keyutils._parse_keystring(keystr)) == parts
class TestKeySequence:
def test_init(self):
seq = keyutils.KeySequence(
keyutils.KeyInfo(Qt.Key.Key_A),
keyutils.KeyInfo(Qt.Key.Key_B),
keyutils.KeyInfo(Qt.Key.Key_C),
keyutils.KeyInfo(Qt.Key.Key_D),
keyutils.KeyInfo(Qt.Key.Key_E),
)
assert len(seq._sequences) == 2
assert len(seq._sequences[0]) == 4
assert len(seq._sequences[1]) == 1
def test_init_empty(self):
seq = keyutils.KeySequence()
assert not seq
@pytest.mark.parametrize('key', [Qt.Key.Key_unknown, keyutils._NIL_KEY])
def test_init_unknown(self, key):
with pytest.raises(keyutils.KeyParseError):
keyutils.KeySequence(keyutils.KeyInfo(key))
def test_init_invalid(self):
with pytest.raises(AssertionError):
keyutils.KeyInfo(-1)
def test_parse_unknown(self):
with pytest.raises(keyutils.KeyParseError):
keyutils.KeySequence.parse('\x1f')
@pytest.mark.parametrize('orig, normalized', [
('<Control+x>', '<Ctrl+x>'),
('<Windows+x>', '<Meta+x>'),
('<Super+x>', '<Meta+x>'),
('<Mod4+x>', '<Meta+x>'),
('<Command+x>', '<Meta+x>'),
('<Cmd+x>', '<Meta+x>'),
('<Mod1+x>', '<Alt+x>'),
('<Control-->', '<Ctrl+->'),
('<Windows++>', '<Meta++>'),
('<ctrl-x>', '<Ctrl+x>'),
('<control+x>', '<Ctrl+x>'),
('<a>b', 'ab'),
])
def test_str_normalization(self, orig, normalized):
assert str(keyutils.KeySequence.parse(orig)) == normalized
def test_iter(self):
infos = [keyutils.KeyInfo(Qt.Key.Key_A, Qt.KeyboardModifier.ControlModifier),
keyutils.KeyInfo(Qt.Key.Key_B, Qt.KeyboardModifier.ShiftModifier),
keyutils.KeyInfo(Qt.Key.Key_C),
keyutils.KeyInfo(Qt.Key.Key_D),
keyutils.KeyInfo(Qt.Key.Key_E)]
seq = keyutils.KeySequence(*infos)
assert list(seq) == infos
def test_repr(self):
seq = keyutils.KeySequence(keyutils.KeyInfo(Qt.Key.Key_A, Qt.KeyboardModifier.ControlModifier),
keyutils.KeyInfo(Qt.Key.Key_B, Qt.KeyboardModifier.ShiftModifier))
assert repr(seq) == ("<qutebrowser.keyinput.keyutils.KeySequence "
"keys='<Ctrl+a>B'>")
@pytest.mark.parametrize('sequences, expected', [
(['a', ''], ['', 'a']),
(['abcdf', 'abcd', 'abcde'], ['abcd', 'abcde', 'abcdf']),
])
def test_sorting(self, sequences, expected):
result = sorted(keyutils.KeySequence.parse(seq) for seq in sequences)
expected_result = [keyutils.KeySequence.parse(seq) for seq in expected]
assert result == expected_result
@pytest.mark.parametrize('seq1, seq2, op, result', [
('a', 'a', operator.eq, True),
('a', '<a>', operator.eq, True),
('a', '<Shift-a>', operator.eq, False),
('a', 'b', operator.lt, True),
('a', 'b', operator.le, True),
])
def test_operators(self, seq1, seq2, op, result):
seq1 = keyutils.KeySequence.parse(seq1)
seq2 = keyutils.KeySequence.parse(seq2)
assert op(seq1, seq2) == result
opposite = {
operator.lt: operator.ge,
operator.gt: operator.le,
operator.le: operator.gt,
operator.ge: operator.lt,
operator.eq: operator.ne,
operator.ne: operator.eq,
}
assert opposite[op](seq1, seq2) != result
@pytest.mark.parametrize('op, result', [
(operator.eq, False),
(operator.ne, True),
])
def test_operators_other_type(self, op, result):
seq = keyutils.KeySequence.parse('a')
assert op(seq, 'x') == result
@pytest.mark.parametrize('seq1, seq2, equal', [
('a', 'a', True),
('a', 'A', False),
('a', '<a>', True),
('abcd', 'abcde', False),
])
def test_hash(self, seq1, seq2, equal):
seq1 = keyutils.KeySequence.parse(seq1)
seq2 = keyutils.KeySequence.parse(seq2)
assert (hash(seq1) == hash(seq2)) == equal
@pytest.mark.parametrize('seq, length', [
('', 0),
('a', 1),
('A', 1),
('<Ctrl-a>', 1),
('abcde', 5)
])
def test_len(self, seq, length):
assert len(keyutils.KeySequence.parse(seq)) == length
def test_bool(self):
seq1 = keyutils.KeySequence.parse('abcd')
seq2 = keyutils.KeySequence()
assert seq1
assert not seq2
def test_getitem(self):
seq = keyutils.KeySequence.parse('ab')
expected = keyutils.KeyInfo(Qt.Key.Key_B, Qt.KeyboardModifier.NoModifier)
assert seq[1] == expected
def test_getitem_slice(self):
s1 = 'abcdef'
s2 = 'de'
seq = keyutils.KeySequence.parse(s1)
expected = keyutils.KeySequence.parse(s2)
assert s1[3:5] == s2
assert seq[3:5] == expected
MATCH_TESTS = [
# config: abcd
('abc', 'abcd', QKeySequence.SequenceMatch.PartialMatch),
('abcd', 'abcd', QKeySequence.SequenceMatch.ExactMatch),
('ax', 'abcd', QKeySequence.SequenceMatch.NoMatch),
('abcdef', 'abcd', QKeySequence.SequenceMatch.NoMatch),
# config: abcd ef
('abc', 'abcdef', QKeySequence.SequenceMatch.PartialMatch),
('abcde', 'abcdef', QKeySequence.SequenceMatch.PartialMatch),
('abcd', 'abcdef', QKeySequence.SequenceMatch.PartialMatch),
('abcdx', 'abcdef', QKeySequence.SequenceMatch.NoMatch),
('ax', 'abcdef', QKeySequence.SequenceMatch.NoMatch),
('abcdefg', 'abcdef', QKeySequence.SequenceMatch.NoMatch),
('abcdef', 'abcdef', QKeySequence.SequenceMatch.ExactMatch),
# other examples
('ab', 'a', QKeySequence.SequenceMatch.NoMatch),
# empty strings
('', '', QKeySequence.SequenceMatch.ExactMatch),
('', 'a', QKeySequence.SequenceMatch.PartialMatch),
('a', '', QKeySequence.SequenceMatch.NoMatch)]
@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) == match_type
@pytest.mark.parametrize('old, key, modifiers, text, expected', [
('a', Qt.Key.Key_B, Qt.KeyboardModifier.NoModifier, 'b', 'ab'),
('a', Qt.Key.Key_B, Qt.KeyboardModifier.ShiftModifier, 'B', 'aB'),
('a', Qt.Key.Key_B, Qt.KeyboardModifier.AltModifier | Qt.KeyboardModifier.ShiftModifier, 'B',
'a<Alt+Shift+b>'),
# Modifier stripping with symbols
('', Qt.Key.Key_Colon, Qt.KeyboardModifier.NoModifier, ':', ':'),
('', Qt.Key.Key_Colon, Qt.KeyboardModifier.ShiftModifier, ':', ':'),
('', Qt.Key.Key_Colon, Qt.KeyboardModifier.AltModifier | Qt.KeyboardModifier.ShiftModifier, ':',
'<Alt+Shift+:>'),
# Swapping Control/Meta on macOS
('', Qt.Key.Key_A, Qt.KeyboardModifier.ControlModifier, '',
'<Meta+A>' if utils.is_mac else '<Ctrl+A>'),
('', Qt.Key.Key_A, Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier, '',
'<Meta+Shift+A>' if utils.is_mac else '<Ctrl+Shift+A>'),
('', Qt.Key.Key_A, Qt.KeyboardModifier.MetaModifier, '',
'<Ctrl+A>' if utils.is_mac else '<Meta+A>'),
# Handling of Backtab
('', Qt.Key.Key_Backtab, Qt.KeyboardModifier.NoModifier, '', '<Backtab>'),
('', Qt.Key.Key_Backtab, Qt.KeyboardModifier.ShiftModifier, '', '<Shift+Tab>'),
('', Qt.Key.Key_Backtab, Qt.KeyboardModifier.AltModifier | Qt.KeyboardModifier.ShiftModifier, '',
'<Alt+Shift+Tab>'),
# Stripping of Qt.KeyboardModifier.GroupSwitchModifier
('', Qt.Key.Key_A, Qt.KeyboardModifier.GroupSwitchModifier, 'a', 'a'),
])
def test_append_event(self, old, key, modifiers, text, expected):
seq = keyutils.KeySequence.parse(old)
event = QKeyEvent(QEvent.Type.KeyPress, key, modifiers, text)
new = seq.append_event(event)
assert new == keyutils.KeySequence.parse(expected)
@pytest.mark.fake_os('mac')
@pytest.mark.parametrize('modifiers, expected', [
(Qt.KeyboardModifier.ControlModifier,
Qt.KeyboardModifier.MetaModifier),
(Qt.KeyboardModifier.MetaModifier,
Qt.KeyboardModifier.ControlModifier),
(Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.MetaModifier,
Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.MetaModifier),
(Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier,
Qt.KeyboardModifier.MetaModifier | Qt.KeyboardModifier.ShiftModifier),
(Qt.KeyboardModifier.MetaModifier | Qt.KeyboardModifier.ShiftModifier,
Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier),
(Qt.KeyboardModifier.ShiftModifier, Qt.KeyboardModifier.ShiftModifier),
])
def test_fake_mac(self, modifiers, expected):
"""Make sure Control/Meta are swapped with a simulated Mac."""
seq = keyutils.KeySequence()
info = keyutils.KeyInfo(key=Qt.Key.Key_A, modifiers=modifiers)
new = seq.append_event(info.to_event())
assert new[0] == keyutils.KeyInfo(Qt.Key.Key_A, expected)
@pytest.mark.parametrize('key', [Qt.Key.Key_unknown, 0x0])
def test_append_event_invalid(self, key):
seq = keyutils.KeySequence()
event = QKeyEvent(QEvent.Type.KeyPress, key, Qt.KeyboardModifier.NoModifier, '')
with pytest.raises(keyutils.KeyParseError):
seq.append_event(event)
def test_strip_modifiers(self):
seq = keyutils.KeySequence(keyutils.KeyInfo(Qt.Key.Key_0),
keyutils.KeyInfo(Qt.Key.Key_1, Qt.KeyboardModifier.KeypadModifier),
keyutils.KeyInfo(Qt.Key.Key_A, Qt.KeyboardModifier.ControlModifier))
expected = keyutils.KeySequence(keyutils.KeyInfo(Qt.Key.Key_0),
keyutils.KeyInfo(Qt.Key.Key_1),
keyutils.KeyInfo(Qt.Key.Key_A, Qt.KeyboardModifier.ControlModifier))
assert seq.strip_modifiers() == expected
@pytest.mark.parametrize('inp, mappings, expected', [
('foobar', {'b': 't'}, 'footar'),
('foo<Ctrl+x>bar', {'<Ctrl+x>': '<Ctrl+y>'}, 'foo<Ctrl+y>bar'),
('foobar', {'b': 'sa'}, 'foosaar'),
])
def test_with_mappings(self, inp, mappings, expected):
seq = keyutils.KeySequence.parse(inp)
seq2 = seq.with_mappings({
keyutils.KeySequence.parse(k): keyutils.KeySequence.parse(v)
for k, v in mappings.items()
})
assert seq2 == keyutils.KeySequence.parse(expected)
@pytest.mark.parametrize('keystr, expected', [
('<Ctrl-Alt-y>',
keyutils.KeySequence(keyutils.KeyInfo(Qt.Key.Key_Y, Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.AltModifier))),
('x', keyutils.KeySequence(keyutils.KeyInfo(Qt.Key.Key_X))),
('X', keyutils.KeySequence(keyutils.KeyInfo(Qt.Key.Key_X, Qt.KeyboardModifier.ShiftModifier))),
('<Escape>', keyutils.KeySequence(keyutils.KeyInfo(Qt.Key.Key_Escape))),
('xyz', keyutils.KeySequence(keyutils.KeyInfo(Qt.Key.Key_X), keyutils.KeyInfo(Qt.Key.Key_Y), keyutils.KeyInfo(Qt.Key.Key_Z))),
('<Control-x><Meta-y>',
keyutils.KeySequence(keyutils.KeyInfo(Qt.Key.Key_X, Qt.KeyboardModifier.ControlModifier),
keyutils.KeyInfo(Qt.Key.Key_Y, Qt.KeyboardModifier.MetaModifier))),
('<Shift-x>', keyutils.KeySequence(keyutils.KeyInfo(Qt.Key.Key_X, Qt.KeyboardModifier.ShiftModifier))),
('<Alt-x>', keyutils.KeySequence(keyutils.KeyInfo(Qt.Key.Key_X, Qt.KeyboardModifier.AltModifier))),
('<Control-x>', keyutils.KeySequence(keyutils.KeyInfo(Qt.Key.Key_X, Qt.KeyboardModifier.ControlModifier))),
('<Meta-x>', keyutils.KeySequence(keyutils.KeyInfo(Qt.Key.Key_X, Qt.KeyboardModifier.MetaModifier))),
('<Num-x>', keyutils.KeySequence(keyutils.KeyInfo(Qt.Key.Key_X, Qt.KeyboardModifier.KeypadModifier))),
('>', keyutils.KeySequence(keyutils.KeyInfo(Qt.Key.Key_Greater))),
('<', keyutils.KeySequence(keyutils.KeyInfo(Qt.Key.Key_Less))),
('a>', keyutils.KeySequence(keyutils.KeyInfo(Qt.Key.Key_A), keyutils.KeyInfo(Qt.Key.Key_Greater))),
('a<', keyutils.KeySequence(keyutils.KeyInfo(Qt.Key.Key_A), keyutils.KeyInfo(Qt.Key.Key_Less))),
('>a', keyutils.KeySequence(keyutils.KeyInfo(Qt.Key.Key_Greater), keyutils.KeyInfo(Qt.Key.Key_A))),
('<a', keyutils.KeySequence(keyutils.KeyInfo(Qt.Key.Key_Less), keyutils.KeyInfo(Qt.Key.Key_A))),
('<alt+greater>',
keyutils.KeySequence(keyutils.KeyInfo(Qt.Key.Key_Greater, Qt.KeyboardModifier.AltModifier))),
('<alt+less>',
keyutils.KeySequence(keyutils.KeyInfo(Qt.Key.Key_Less, Qt.KeyboardModifier.AltModifier))),
('<<', keyutils.KeySequence(Qt.Key_Less, Qt.Key_Less)),
('>>', keyutils.KeySequence(Qt.Key_Greater, Qt.Key_Greater)),
('><', keyutils.KeySequence(Qt.Key_Greater, Qt.Key_Less)),
('<alt+<>',
keyutils.KeySequence(Qt.Key_Less | Qt.AltModifier)),
('<alt+>>', keyutils.KeyParseError),
('<blub>', keyutils.KeyParseError),
('<>', keyutils.KeyParseError),
('\U00010000', keyutils.KeyParseError),
])
def test_parse(self, keystr, expected):
if expected is keyutils.KeyParseError:
with pytest.raises(keyutils.KeyParseError):
keyutils.KeySequence.parse(keystr)
else:
assert keyutils.KeySequence.parse(keystr) == expected
@hypothesis.given(strategies.text())
def test_parse_hypothesis(self, keystr):
try:
seq = keyutils.KeySequence.parse(keystr)
except keyutils.KeyParseError:
pass
else:
str(seq)
def test_key_info_from_event():
ev = QKeyEvent(QEvent.Type.KeyPress, Qt.Key.Key_A, Qt.KeyboardModifier.ShiftModifier, 'A')
info = keyutils.KeyInfo.from_event(ev)
assert info.key == Qt.Key.Key_A
assert info.modifiers == Qt.KeyboardModifier.ShiftModifier
def test_key_info_to_event():
info = keyutils.KeyInfo(Qt.Key.Key_A, Qt.KeyboardModifier.ShiftModifier)
ev = info.to_event()
assert ev.key() == Qt.Key.Key_A
assert ev.modifiers() == Qt.KeyboardModifier.ShiftModifier
assert ev.text() == 'A'
def test_key_info_to_qt():
info = keyutils.KeyInfo(Qt.Key.Key_A, Qt.KeyboardModifier.ShiftModifier)
assert info.to_qt() == Qt.Key.Key_A | Qt.KeyboardModifier.ShiftModifier
@pytest.mark.parametrize('key, printable', [
(Qt.Key.Key_Control, False),
(Qt.Key.Key_Escape, False),
(Qt.Key.Key_Tab, False),
(Qt.Key.Key_Backtab, False),
(Qt.Key.Key_Backspace, False),
(Qt.Key.Key_Return, False),
(Qt.Key.Key_Enter, False),
(Qt.Key.Key_Space, False),
# Used by Qt for unknown keys
pytest.param(keyutils._NIL_KEY, False, marks=pyqt_enum_workaround_skip),
(Qt.Key.Key_ydiaeresis, True),
(Qt.Key.Key_X, True),
])
def test_is_printable(key, printable):
assert keyutils._is_printable(key) == printable
info = keyutils.KeyInfo(key, Qt.KeyboardModifier.NoModifier)
assert info.is_special() != printable
@pytest.mark.parametrize('key, modifiers, special', [
(Qt.Key.Key_Escape, Qt.KeyboardModifier.NoModifier, True),
(Qt.Key.Key_Escape, Qt.KeyboardModifier.ShiftModifier, True),
(Qt.Key.Key_Escape, Qt.KeyboardModifier.ControlModifier, True),
(Qt.Key.Key_X, Qt.KeyboardModifier.ControlModifier, True),
(Qt.Key.Key_X, Qt.KeyboardModifier.NoModifier, False),
(Qt.Key.Key_2, Qt.KeyboardModifier.KeypadModifier, True),
(Qt.Key.Key_2, Qt.KeyboardModifier.NoModifier, False),
(Qt.Key.Key_Shift, Qt.KeyboardModifier.ShiftModifier, True),
(Qt.Key.Key_Control, Qt.KeyboardModifier.ControlModifier, True),
(Qt.Key.Key_Alt, Qt.KeyboardModifier.AltModifier, True),
(Qt.Key.Key_Meta, Qt.KeyboardModifier.MetaModifier, True),
(Qt.Key.Key_Mode_switch, Qt.KeyboardModifier.GroupSwitchModifier, True),
])
def test_is_special(key, modifiers, special):
assert keyutils.KeyInfo(key, modifiers).is_special() == special
@pytest.mark.parametrize('key, ismodifier', [
(Qt.Key.Key_Control, True),
(Qt.Key.Key_X, False),
(Qt.Key.Key_Super_L, False), # Modifier but not in _MODIFIER_MAP
])
def test_is_modifier_key(key, ismodifier):
assert keyutils.KeyInfo(key).is_modifier_key() == ismodifier
@pytest.mark.parametrize('func', [
keyutils._assert_plain_key,
keyutils._assert_plain_modifier,
keyutils._is_printable,
keyutils._key_to_string,
keyutils._modifiers_to_string,
keyutils.KeyInfo,
])
def test_non_plain(func):
comb = Qt.Key.Key_X | Qt.KeyboardModifier.ControlModifier
if machinery.IS_QT6:
# QKeyCombination
comb = comb.toCombined()
with pytest.raises(AssertionError):
func(comb)