mirror of https://github.com/python/cpython
gh-119357: Increase test coverage for keymap in _pyrepl (#119358)
Co-authored-by: Łukasz Langa <lukasz@langa.pl>
This commit is contained in:
parent
c886bece3b
commit
73ab83b27f
|
@ -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:
|
||||||
|
|
|
@ -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""):
|
||||||
|
|
|
@ -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'})
|
||||||
|
|
Loading…
Reference in New Issue