Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit e6e4efc

Browse filesBrowse files
lysnikolaoueugenetrigubaambv
authored
[3.13] gh-119357: Increase test coverage for keymap in _pyrepl (GH-119358) (#119414)
(cherry picked from commit 73ab83b) Co-authored-by: Eugene Triguba <eugenetriguba@gmail.com> Co-authored-by: Łukasz Langa <lukasz@langa.pl>
1 parent 3e30a38 commit e6e4efc
Copy full SHA for e6e4efc

File tree

3 files changed

+94
-53
lines changed
Filter options

3 files changed

+94
-53
lines changed

‎Lib/_pyrepl/completing_reader.py

Copy file name to clipboardExpand all lines: Lib/_pyrepl/completing_reader.py
+1-1Lines changed: 1 addition & 1 deletion
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:

‎Lib/_pyrepl/keymap.py

Copy file name to clipboardExpand all lines: Lib/_pyrepl/keymap.py
+29-34Lines changed: 29 additions & 34 deletions
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""):

‎Lib/test/test_pyrepl/test_keymap.py

Copy file name to clipboardExpand all lines: Lib/test/test_pyrepl/test_keymap.py
+64-18Lines changed: 64 additions & 18 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.