gh-119357: Increase test coverage for keymap in _pyrepl (#119358)

Co-authored-by: Łukasz Langa <lukasz@langa.pl>
This commit is contained in:
Eugene Triguba 2024-05-21 22:36:01 -04:00 committed by GitHub
parent c886bece3b
commit 73ab83b27f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 94 additions and 53 deletions

View File

@ -30,7 +30,7 @@ from .reader import Reader
# types # types
Command = commands.Command Command = commands.Command
if False: if False:
from .types import Callback, SimpleContextManager, KeySpec, CommandName from .types import KeySpec, CommandName
def prefix(wordlist: list[str], j: int = 0) -> str: def prefix(wordlist: list[str], j: int = 0) -> str:

View File

@ -19,38 +19,32 @@
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
""" """
functions for parsing keyspecs Keymap contains functions for parsing keyspecs and turning keyspecs into
appropriate sequences.
Support for turning keyspecs into appropriate sequences. A keyspec is a string representing a sequence of key presses that can
be bound to a command. All characters other than the backslash represent
themselves. In the traditional manner, a backslash introduces an escape
sequence.
pyrepl uses it's own bastardized keyspec format, which is meant to be pyrepl uses its own keyspec format that is meant to be a strict superset of
a strict superset of readline's \"KEYSEQ\" format (which is to say readline's KEYSEQ format. This means that if a spec is found that readline
that if you can come up with a spec readline accepts that this accepts that this doesn't, it should be logged as a bug. Note that this means
doesn't, you've found a bug and should tell me about it). we're using the `\\C-o' style of readline's keyspec, not the `Control-o' sort.
Note that this is the `\\C-o' style of readline keyspec, not the
`Control-o' sort.
A keyspec is a string representing a sequence of keypresses that can
be bound to a command.
All characters other than the backslash represent themselves. In the
traditional manner, a backslash introduces a escape sequence.
The extension to readline is that the sequence \\<KEY> denotes the The extension to readline is that the sequence \\<KEY> denotes the
sequence of charaters produced by hitting KEY. sequence of characters produced by hitting KEY.
Examples: Examples:
`a' - what you get when you hit the `a' key
`a' - what you get when you hit the `a' key
`\\EOA' - Escape - O - A (up, on my terminal) `\\EOA' - Escape - O - A (up, on my terminal)
`\\<UP>' - the up arrow key `\\<UP>' - the up arrow key
`\\<up>' - ditto (keynames are case insensitive) `\\<up>' - ditto (keynames are case-insensitive)
`\\C-o', `\\c-o' - control-o `\\C-o', `\\c-o' - control-o
`\\M-.' - meta-period `\\M-.' - meta-period
`\\E.' - ditto (that's how meta works for pyrepl) `\\E.' - ditto (that's how meta works for pyrepl)
`\\<tab>', `\\<TAB>', `\\t', `\\011', '\\x09', '\\X09', '\\C-i', '\\C-I' `\\<tab>', `\\<TAB>', `\\t', `\\011', '\\x09', '\\X09', '\\C-i', '\\C-I'
- all of these are the tab character. Can you think of any more? - all of these are the tab character.
""" """
_escapes = { _escapes = {
@ -111,7 +105,17 @@ class KeySpecError(Exception):
pass pass
def _parse_key1(key, s): def parse_keys(keys: str) -> list[str]:
"""Parse keys in keyspec format to a sequence of keys."""
s = 0
r: list[str] = []
while s < len(keys):
k, s = _parse_single_key_sequence(keys, s)
r.extend(k)
return r
def _parse_single_key_sequence(key: str, s: int) -> tuple[list[str], int]:
ctrl = 0 ctrl = 0
meta = 0 meta = 0
ret = "" ret = ""
@ -183,20 +187,11 @@ def _parse_key1(key, s):
ret = f"ctrl {ret}" ret = f"ctrl {ret}"
else: else:
raise KeySpecError("\\C- followed by invalid key") raise KeySpecError("\\C- followed by invalid key")
result = [ret], s
if meta: if meta:
ret = ["\033", ret] result[0].insert(0, "\033")
else: return result
ret = [ret]
return ret, s
def parse_keys(key: str) -> list[str]:
s = 0
r = []
while s < len(key):
k, s = _parse_key1(key, s)
r.extend(k)
return r
def compile_keymap(keymap, empty=b""): def compile_keymap(keymap, empty=b""):

View File

@ -1,41 +1,78 @@
import string
import unittest import unittest
from _pyrepl.keymap import parse_keys, compile_keymap from _pyrepl.keymap import _keynames, _escapes, parse_keys, compile_keymap, KeySpecError
class TestParseKeys(unittest.TestCase): class TestParseKeys(unittest.TestCase):
def test_single_character(self): def test_single_character(self):
self.assertEqual(parse_keys("a"), ["a"]) """Ensure that single ascii characters or single digits are parsed as single characters."""
self.assertEqual(parse_keys("b"), ["b"]) test_cases = [(key, [key]) for key in string.ascii_letters + string.digits]
self.assertEqual(parse_keys("1"), ["1"]) for test_key, expected_keys in test_cases:
with self.subTest(f"{test_key} should be parsed as {expected_keys}"):
self.assertEqual(parse_keys(test_key), expected_keys)
def test_keynames(self):
"""Ensure that keynames are parsed to their corresponding mapping.
A keyname is expected to be of the following form: \\<keyname> such as \\<left>
which would get parsed as "left".
"""
test_cases = [(f"\\<{keyname}>", [parsed_keyname]) for keyname, parsed_keyname in _keynames.items()]
for test_key, expected_keys in test_cases:
with self.subTest(f"{test_key} should be parsed as {expected_keys}"):
self.assertEqual(parse_keys(test_key), expected_keys)
def test_escape_sequences(self): def test_escape_sequences(self):
self.assertEqual(parse_keys("\\n"), ["\n"]) """Ensure that escaping sequences are parsed to their corresponding mapping."""
self.assertEqual(parse_keys("\\t"), ["\t"]) test_cases = [(f"\\{escape}", [parsed_escape]) for escape, parsed_escape in _escapes.items()]
self.assertEqual(parse_keys("\\\\"), ["\\"]) for test_key, expected_keys in test_cases:
self.assertEqual(parse_keys("\\'"), ["'"]) with self.subTest(f"{test_key} should be parsed as {expected_keys}"):
self.assertEqual(parse_keys('\\"'), ['"']) self.assertEqual(parse_keys(test_key), expected_keys)
def test_control_sequences(self): def test_control_sequences(self):
self.assertEqual(parse_keys("\\C-a"), ["\x01"]) """Ensure that supported control sequences are parsed successfully."""
self.assertEqual(parse_keys("\\C-b"), ["\x02"]) keys = ["@", "[", "]", "\\", "^", "_", "\\<space>", "\\<delete>"]
self.assertEqual(parse_keys("\\C-c"), ["\x03"]) keys.extend(string.ascii_letters)
test_cases = [(f"\\C-{key}", chr(ord(key) & 0x1F)) for key in []]
for test_key, expected_keys in test_cases:
with self.subTest(f"{test_key} should be parsed as {expected_keys}"):
self.assertEqual(parse_keys(test_key), expected_keys)
def test_meta_sequences(self): def test_meta_sequences(self):
self.assertEqual(parse_keys("\\M-a"), ["\033", "a"]) self.assertEqual(parse_keys("\\M-a"), ["\033", "a"])
self.assertEqual(parse_keys("\\M-b"), ["\033", "b"]) self.assertEqual(parse_keys("\\M-b"), ["\033", "b"])
self.assertEqual(parse_keys("\\M-c"), ["\033", "c"]) self.assertEqual(parse_keys("\\M-c"), ["\033", "c"])
def test_keynames(self):
self.assertEqual(parse_keys("\\<up>"), ["up"])
self.assertEqual(parse_keys("\\<down>"), ["down"])
self.assertEqual(parse_keys("\\<left>"), ["left"])
self.assertEqual(parse_keys("\\<right>"), ["right"])
def test_combinations(self): def test_combinations(self):
self.assertEqual(parse_keys("\\C-a\\n\\<up>"), ["\x01", "\n", "up"]) self.assertEqual(parse_keys("\\C-a\\n\\<up>"), ["\x01", "\n", "up"])
self.assertEqual(parse_keys("\\M-a\\t\\<down>"), ["\033", "a", "\t", "down"]) self.assertEqual(parse_keys("\\M-a\\t\\<down>"), ["\033", "a", "\t", "down"])
def test_keyspec_errors(self):
cases = [
("\\Ca", "\\C must be followed by `-'"),
("\\ca", "\\C must be followed by `-'"),
("\\C-\\C-", "doubled \\C-"),
("\\Ma", "\\M must be followed by `-'"),
("\\ma", "\\M must be followed by `-'"),
("\\M-\\M-", "doubled \\M-"),
("\\<left", "unterminated \\<"),
("\\<unsupported>", "unrecognised keyname"),
("\\", "unknown backslash escape"),
("\\C-\\<backspace>", "\\C- followed by invalid key")
]
for test_keys, expected_err in cases:
with self.subTest(f"{test_keys} should give error {expected_err}"):
with self.assertRaises(KeySpecError) as e:
parse_keys(test_keys)
self.assertIn(expected_err, str(e.exception))
def test_index_errors(self):
test_cases = ["\\", "\\C", "\\C-\\C"]
for test_keys in test_cases:
with self.assertRaises(IndexError):
parse_keys(test_keys)
class TestCompileKeymap(unittest.TestCase): class TestCompileKeymap(unittest.TestCase):
def test_empty_keymap(self): def test_empty_keymap(self):
@ -72,3 +109,12 @@ class TestCompileKeymap(unittest.TestCase):
keymap = {b"a": {b"b": {b"c": "action"}}} keymap = {b"a": {b"b": {b"c": "action"}}}
result = compile_keymap(keymap) result = compile_keymap(keymap)
self.assertEqual(result, {b"a": {b"b": {b"c": "action"}}}) self.assertEqual(result, {b"a": {b"b": {b"c": "action"}}})
def test_clashing_definitions(self):
km = {b'a': 'c', b'a' + b'b': 'd'}
with self.assertRaises(KeySpecError):
compile_keymap(km)
def test_non_bytes_key(self):
with self.assertRaises(TypeError):
compile_keymap({123: 'a'})