Skip to content

Commit 73ab83b

Browse files
eugenetrigubaambv
andauthored
gh-119357: Increase test coverage for keymap in _pyrepl (#119358)
Co-authored-by: Łukasz Langa <lukasz@langa.pl>
1 parent c886bec commit 73ab83b

File tree

3 files changed

+94
-53
lines changed

3 files changed

+94
-53
lines changed

Diff for: Lib/_pyrepl/completing_reader.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
# types
3131
Command = commands.Command
3232
if False:
33-
from .types import Callback, SimpleContextManager, KeySpec, CommandName
33+
from .types import KeySpec, CommandName
3434

3535

3636
def prefix(wordlist: list[str], j: int = 0) -> str:

Diff for: Lib/_pyrepl/keymap.py

+29-34
Original file line numberDiff line numberDiff line change
@@ -19,38 +19,32 @@
1919
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
2020

2121
"""
22-
functions for parsing keyspecs
22+
Keymap contains functions for parsing keyspecs and turning keyspecs into
23+
appropriate sequences.
2324
24-
Support for turning keyspecs into appropriate sequences.
25+
A keyspec is a string representing a sequence of key presses that can
26+
be bound to a command. All characters other than the backslash represent
27+
themselves. In the traditional manner, a backslash introduces an escape
28+
sequence.
2529
26-
pyrepl uses it's own bastardized keyspec format, which is meant to be
27-
a strict superset of readline's \"KEYSEQ\" format (which is to say
28-
that if you can come up with a spec readline accepts that this
29-
doesn't, you've found a bug and should tell me about it).
30-
31-
Note that this is the `\\C-o' style of readline keyspec, not the
32-
`Control-o' sort.
33-
34-
A keyspec is a string representing a sequence of keypresses that can
35-
be bound to a command.
36-
37-
All characters other than the backslash represent themselves. In the
38-
traditional manner, a backslash introduces a escape sequence.
30+
pyrepl uses its own keyspec format that is meant to be a strict superset of
31+
readline's KEYSEQ format. This means that if a spec is found that readline
32+
accepts that this doesn't, it should be logged as a bug. Note that this means
33+
we're using the `\\C-o' style of readline's keyspec, not the `Control-o' sort.
3934
4035
The extension to readline is that the sequence \\<KEY> denotes the
41-
sequence of charaters produced by hitting KEY.
36+
sequence of characters produced by hitting KEY.
4237
4338
Examples:
44-
45-
`a' - what you get when you hit the `a' key
39+
`a' - what you get when you hit the `a' key
4640
`\\EOA' - Escape - O - A (up, on my terminal)
4741
`\\<UP>' - the up arrow key
48-
`\\<up>' - ditto (keynames are case insensitive)
42+
`\\<up>' - ditto (keynames are case-insensitive)
4943
`\\C-o', `\\c-o' - control-o
5044
`\\M-.' - meta-period
5145
`\\E.' - ditto (that's how meta works for pyrepl)
5246
`\\<tab>', `\\<TAB>', `\\t', `\\011', '\\x09', '\\X09', '\\C-i', '\\C-I'
53-
- all of these are the tab character. Can you think of any more?
47+
- all of these are the tab character.
5448
"""
5549

5650
_escapes = {
@@ -111,7 +105,17 @@ class KeySpecError(Exception):
111105
pass
112106

113107

114-
def _parse_key1(key, s):
108+
def parse_keys(keys: str) -> list[str]:
109+
"""Parse keys in keyspec format to a sequence of keys."""
110+
s = 0
111+
r: list[str] = []
112+
while s < len(keys):
113+
k, s = _parse_single_key_sequence(keys, s)
114+
r.extend(k)
115+
return r
116+
117+
118+
def _parse_single_key_sequence(key: str, s: int) -> tuple[list[str], int]:
115119
ctrl = 0
116120
meta = 0
117121
ret = ""
@@ -183,20 +187,11 @@ def _parse_key1(key, s):
183187
ret = f"ctrl {ret}"
184188
else:
185189
raise KeySpecError("\\C- followed by invalid key")
186-
if meta:
187-
ret = ["\033", ret]
188-
else:
189-
ret = [ret]
190-
return ret, s
191190

192-
193-
def parse_keys(key: str) -> list[str]:
194-
s = 0
195-
r = []
196-
while s < len(key):
197-
k, s = _parse_key1(key, s)
198-
r.extend(k)
199-
return r
191+
result = [ret], s
192+
if meta:
193+
result[0].insert(0, "\033")
194+
return result
200195

201196

202197
def compile_keymap(keymap, empty=b""):

Diff for: Lib/test/test_pyrepl/test_keymap.py

+64-18
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,78 @@
1+
import string
12
import unittest
23

3-
from _pyrepl.keymap import parse_keys, compile_keymap
4+
from _pyrepl.keymap import _keynames, _escapes, parse_keys, compile_keymap, KeySpecError
45

56

67
class TestParseKeys(unittest.TestCase):
78
def test_single_character(self):
8-
self.assertEqual(parse_keys("a"), ["a"])
9-
self.assertEqual(parse_keys("b"), ["b"])
10-
self.assertEqual(parse_keys("1"), ["1"])
9+
"""Ensure that single ascii characters or single digits are parsed as single characters."""
10+
test_cases = [(key, [key]) for key in string.ascii_letters + string.digits]
11+
for test_key, expected_keys in test_cases:
12+
with self.subTest(f"{test_key} should be parsed as {expected_keys}"):
13+
self.assertEqual(parse_keys(test_key), expected_keys)
14+
15+
def test_keynames(self):
16+
"""Ensure that keynames are parsed to their corresponding mapping.
17+
18+
A keyname is expected to be of the following form: \\<keyname> such as \\<left>
19+
which would get parsed as "left".
20+
"""
21+
test_cases = [(f"\\<{keyname}>", [parsed_keyname]) for keyname, parsed_keyname in _keynames.items()]
22+
for test_key, expected_keys in test_cases:
23+
with self.subTest(f"{test_key} should be parsed as {expected_keys}"):
24+
self.assertEqual(parse_keys(test_key), expected_keys)
1125

1226
def test_escape_sequences(self):
13-
self.assertEqual(parse_keys("\\n"), ["\n"])
14-
self.assertEqual(parse_keys("\\t"), ["\t"])
15-
self.assertEqual(parse_keys("\\\\"), ["\\"])
16-
self.assertEqual(parse_keys("\\'"), ["'"])
17-
self.assertEqual(parse_keys('\\"'), ['"'])
27+
"""Ensure that escaping sequences are parsed to their corresponding mapping."""
28+
test_cases = [(f"\\{escape}", [parsed_escape]) for escape, parsed_escape in _escapes.items()]
29+
for test_key, expected_keys in test_cases:
30+
with self.subTest(f"{test_key} should be parsed as {expected_keys}"):
31+
self.assertEqual(parse_keys(test_key), expected_keys)
1832

1933
def test_control_sequences(self):
20-
self.assertEqual(parse_keys("\\C-a"), ["\x01"])
21-
self.assertEqual(parse_keys("\\C-b"), ["\x02"])
22-
self.assertEqual(parse_keys("\\C-c"), ["\x03"])
34+
"""Ensure that supported control sequences are parsed successfully."""
35+
keys = ["@", "[", "]", "\\", "^", "_", "\\<space>", "\\<delete>"]
36+
keys.extend(string.ascii_letters)
37+
test_cases = [(f"\\C-{key}", chr(ord(key) & 0x1F)) for key in []]
38+
for test_key, expected_keys in test_cases:
39+
with self.subTest(f"{test_key} should be parsed as {expected_keys}"):
40+
self.assertEqual(parse_keys(test_key), expected_keys)
2341

2442
def test_meta_sequences(self):
2543
self.assertEqual(parse_keys("\\M-a"), ["\033", "a"])
2644
self.assertEqual(parse_keys("\\M-b"), ["\033", "b"])
2745
self.assertEqual(parse_keys("\\M-c"), ["\033", "c"])
2846

29-
def test_keynames(self):
30-
self.assertEqual(parse_keys("\\<up>"), ["up"])
31-
self.assertEqual(parse_keys("\\<down>"), ["down"])
32-
self.assertEqual(parse_keys("\\<left>"), ["left"])
33-
self.assertEqual(parse_keys("\\<right>"), ["right"])
34-
3547
def test_combinations(self):
3648
self.assertEqual(parse_keys("\\C-a\\n\\<up>"), ["\x01", "\n", "up"])
3749
self.assertEqual(parse_keys("\\M-a\\t\\<down>"), ["\033", "a", "\t", "down"])
3850

51+
def test_keyspec_errors(self):
52+
cases = [
53+
("\\Ca", "\\C must be followed by `-'"),
54+
("\\ca", "\\C must be followed by `-'"),
55+
("\\C-\\C-", "doubled \\C-"),
56+
("\\Ma", "\\M must be followed by `-'"),
57+
("\\ma", "\\M must be followed by `-'"),
58+
("\\M-\\M-", "doubled \\M-"),
59+
("\\<left", "unterminated \\<"),
60+
("\\<unsupported>", "unrecognised keyname"),
61+
("\\大", "unknown backslash escape"),
62+
("\\C-\\<backspace>", "\\C- followed by invalid key")
63+
]
64+
for test_keys, expected_err in cases:
65+
with self.subTest(f"{test_keys} should give error {expected_err}"):
66+
with self.assertRaises(KeySpecError) as e:
67+
parse_keys(test_keys)
68+
self.assertIn(expected_err, str(e.exception))
69+
70+
def test_index_errors(self):
71+
test_cases = ["\\", "\\C", "\\C-\\C"]
72+
for test_keys in test_cases:
73+
with self.assertRaises(IndexError):
74+
parse_keys(test_keys)
75+
3976

4077
class TestCompileKeymap(unittest.TestCase):
4178
def test_empty_keymap(self):
@@ -72,3 +109,12 @@ def test_nested_multiple_keymaps(self):
72109
keymap = {b"a": {b"b": {b"c": "action"}}}
73110
result = compile_keymap(keymap)
74111
self.assertEqual(result, {b"a": {b"b": {b"c": "action"}}})
112+
113+
def test_clashing_definitions(self):
114+
km = {b'a': 'c', b'a' + b'b': 'd'}
115+
with self.assertRaises(KeySpecError):
116+
compile_keymap(km)
117+
118+
def test_non_bytes_key(self):
119+
with self.assertRaises(TypeError):
120+
compile_keymap({123: 'a'})

0 commit comments

Comments
 (0)