Skip to content

Commit 7214598

Browse files
[3.13] gh-119306: Break up _pyrepl tests (GH-119307) (#119362)
(cherry picked from commit f49df4f) Co-authored-by: Eugene Triguba <eugenetriguba@gmail.com>
1 parent 6892b40 commit 7214598

10 files changed

+883
-395
lines changed

Lib/test/test_pyrepl/__init__.py

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import os
2+
from test.support import requires, load_package_tests
3+
from test.support.import_helper import import_module
4+
5+
# Optionally test pyrepl. This currently requires that the
6+
# 'curses' resource be given on the regrtest command line using the -u
7+
# option. Additionally, we need to attempt to import curses and readline.
8+
requires("curses")
9+
curses = import_module("curses")
10+
readline = import_module("readline")
11+
12+
13+
def load_tests(*args):
14+
return load_package_tests(os.path.dirname(__file__), *args)

Lib/test/test_pyrepl/__main__.py

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import unittest
2+
from test.test_pyrepl import load_tests
3+
4+
unittest.main()

Lib/test/test_pyrepl/support.py

+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
from code import InteractiveConsole
2+
from functools import partial
3+
from typing import Iterable
4+
from unittest.mock import MagicMock
5+
6+
from _pyrepl.console import Console, Event
7+
from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig
8+
from _pyrepl.simple_interact import _strip_final_indent
9+
10+
11+
def multiline_input(reader: ReadlineAlikeReader, namespace: dict | None = None):
12+
saved = reader.more_lines
13+
try:
14+
reader.more_lines = partial(more_lines, namespace=namespace)
15+
reader.ps1 = reader.ps2 = ">>>"
16+
reader.ps3 = reader.ps4 = "..."
17+
return reader.readline()
18+
finally:
19+
reader.more_lines = saved
20+
reader.paste_mode = False
21+
22+
23+
def more_lines(text: str, namespace: dict | None = None):
24+
if namespace is None:
25+
namespace = {}
26+
src = _strip_final_indent(text)
27+
console = InteractiveConsole(namespace, filename="<stdin>")
28+
try:
29+
code = console.compile(src, "<stdin>", "single")
30+
except (OverflowError, SyntaxError, ValueError):
31+
return False
32+
else:
33+
return code is None
34+
35+
36+
def code_to_events(code: str):
37+
for c in code:
38+
yield Event(evt="key", data=c, raw=bytearray(c.encode("utf-8")))
39+
40+
41+
def prepare_reader(console: Console, **kwargs):
42+
config = ReadlineConfig(readline_completer=None)
43+
reader = ReadlineAlikeReader(console=console, config=config)
44+
reader.more_lines = partial(more_lines, namespace=None)
45+
reader.paste_mode = True # Avoid extra indents
46+
47+
def get_prompt(lineno, cursor_on_line) -> str:
48+
return ""
49+
50+
reader.get_prompt = get_prompt # Remove prompt for easier calculations of (x, y)
51+
52+
for key, val in kwargs.items():
53+
setattr(reader, key, val)
54+
55+
return reader
56+
57+
58+
def prepare_console(events: Iterable[Event], **kwargs):
59+
console = MagicMock()
60+
console.get_event.side_effect = events
61+
console.height = 100
62+
console.width = 80
63+
for key, val in kwargs.items():
64+
setattr(console, key, val)
65+
return console
66+
67+
68+
def handle_all_events(
69+
events, prepare_console=prepare_console, prepare_reader=prepare_reader
70+
):
71+
console = prepare_console(events)
72+
reader = prepare_reader(console)
73+
try:
74+
while True:
75+
reader.handle1()
76+
except StopIteration:
77+
pass
78+
return reader, console
79+
80+
81+
handle_events_narrow_console = partial(
82+
handle_all_events,
83+
prepare_console=partial(prepare_console, width=10),
84+
)
85+
86+
87+
class FakeConsole(Console):
88+
def __init__(self, events, encoding="utf-8"):
89+
self.events = iter(events)
90+
self.encoding = encoding
91+
self.screen = []
92+
self.height = 100
93+
self.width = 80
94+
95+
def get_event(self, block: bool = True) -> Event | None:
96+
return next(self.events)
97+
98+
def getpending(self) -> Event:
99+
return self.get_event(block=False)
100+
101+
def getheightwidth(self) -> tuple[int, int]:
102+
return self.height, self.width
103+
104+
def refresh(self, screen: list[str], xy: tuple[int, int]) -> None:
105+
pass
106+
107+
def prepare(self) -> None:
108+
pass
109+
110+
def restore(self) -> None:
111+
pass
112+
113+
def move_cursor(self, x: int, y: int) -> None:
114+
pass
115+
116+
def set_cursor_vis(self, visible: bool) -> None:
117+
pass
118+
119+
def push_char(self, char: int | bytes) -> None:
120+
pass
121+
122+
def beep(self) -> None:
123+
pass
124+
125+
def clear(self) -> None:
126+
pass
127+
128+
def finish(self) -> None:
129+
pass
130+
131+
def flushoutput(self) -> None:
132+
pass
133+
134+
def forgetinput(self) -> None:
135+
pass
136+
137+
def wait(self) -> None:
138+
pass
139+
140+
def repaint(self) -> None:
141+
pass

Lib/test/test_pyrepl/test_input.py

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import unittest
2+
3+
from _pyrepl.console import Event
4+
from _pyrepl.input import KeymapTranslator
5+
6+
7+
class KeymapTranslatorTests(unittest.TestCase):
8+
def test_push_single_key(self):
9+
keymap = [("a", "command_a")]
10+
translator = KeymapTranslator(keymap)
11+
evt = Event("key", "a")
12+
translator.push(evt)
13+
result = translator.get()
14+
self.assertEqual(result, ("command_a", ["a"]))
15+
16+
def test_push_multiple_keys(self):
17+
keymap = [("ab", "command_ab")]
18+
translator = KeymapTranslator(keymap)
19+
evt1 = Event("key", "a")
20+
evt2 = Event("key", "b")
21+
translator.push(evt1)
22+
translator.push(evt2)
23+
result = translator.get()
24+
self.assertEqual(result, ("command_ab", ["a", "b"]))
25+
26+
def test_push_invalid_key(self):
27+
keymap = [("a", "command_a")]
28+
translator = KeymapTranslator(keymap)
29+
evt = Event("key", "b")
30+
translator.push(evt)
31+
result = translator.get()
32+
self.assertEqual(result, (None, ["b"]))
33+
34+
def test_push_invalid_key_with_stack(self):
35+
keymap = [("ab", "command_ab")]
36+
translator = KeymapTranslator(keymap)
37+
evt1 = Event("key", "a")
38+
evt2 = Event("key", "c")
39+
translator.push(evt1)
40+
translator.push(evt2)
41+
result = translator.get()
42+
self.assertEqual(result, (None, ["a", "c"]))
43+
44+
def test_push_character_key(self):
45+
keymap = [("a", "command_a")]
46+
translator = KeymapTranslator(keymap)
47+
evt = Event("key", "a")
48+
translator.push(evt)
49+
result = translator.get()
50+
self.assertEqual(result, ("command_a", ["a"]))
51+
52+
def test_push_character_key_with_stack(self):
53+
keymap = [("ab", "command_ab")]
54+
translator = KeymapTranslator(keymap)
55+
evt1 = Event("key", "a")
56+
evt2 = Event("key", "b")
57+
evt3 = Event("key", "c")
58+
translator.push(evt1)
59+
translator.push(evt2)
60+
translator.push(evt3)
61+
result = translator.get()
62+
self.assertEqual(result, ("command_ab", ["a", "b"]))
63+
64+
def test_push_transition_key(self):
65+
keymap = [("a", {"b": "command_ab"})]
66+
translator = KeymapTranslator(keymap)
67+
evt1 = Event("key", "a")
68+
evt2 = Event("key", "b")
69+
translator.push(evt1)
70+
translator.push(evt2)
71+
result = translator.get()
72+
self.assertEqual(result, ("command_ab", ["a", "b"]))
73+
74+
def test_push_transition_key_interrupted(self):
75+
keymap = [("a", {"b": "command_ab"})]
76+
translator = KeymapTranslator(keymap)
77+
evt1 = Event("key", "a")
78+
evt2 = Event("key", "c")
79+
evt3 = Event("key", "b")
80+
translator.push(evt1)
81+
translator.push(evt2)
82+
translator.push(evt3)
83+
result = translator.get()
84+
self.assertEqual(result, (None, ["a", "c"]))
85+
86+
def test_push_invalid_key_with_unicode_category(self):
87+
keymap = [("a", "command_a")]
88+
translator = KeymapTranslator(keymap)
89+
evt = Event("key", "\u0003") # Control character
90+
translator.push(evt)
91+
result = translator.get()
92+
self.assertEqual(result, (None, ["\u0003"]))
93+
94+
def test_empty(self):
95+
keymap = [("a", "command_a")]
96+
translator = KeymapTranslator(keymap)
97+
self.assertTrue(translator.empty())
98+
evt = Event("key", "a")
99+
translator.push(evt)
100+
self.assertFalse(translator.empty())
101+
translator.get()
102+
self.assertTrue(translator.empty())

Lib/test/test_pyrepl/test_keymap.py

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import unittest
2+
3+
from _pyrepl.keymap import parse_keys, compile_keymap
4+
5+
6+
class TestParseKeys(unittest.TestCase):
7+
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"])
11+
12+
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('\\"'), ['"'])
18+
19+
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"])
23+
24+
def test_meta_sequences(self):
25+
self.assertEqual(parse_keys("\\M-a"), ["\033", "a"])
26+
self.assertEqual(parse_keys("\\M-b"), ["\033", "b"])
27+
self.assertEqual(parse_keys("\\M-c"), ["\033", "c"])
28+
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+
35+
def test_combinations(self):
36+
self.assertEqual(parse_keys("\\C-a\\n\\<up>"), ["\x01", "\n", "up"])
37+
self.assertEqual(parse_keys("\\M-a\\t\\<down>"), ["\033", "a", "\t", "down"])
38+
39+
40+
class TestCompileKeymap(unittest.TestCase):
41+
def test_empty_keymap(self):
42+
keymap = {}
43+
result = compile_keymap(keymap)
44+
self.assertEqual(result, {})
45+
46+
def test_single_keymap(self):
47+
keymap = {b"a": "action"}
48+
result = compile_keymap(keymap)
49+
self.assertEqual(result, {b"a": "action"})
50+
51+
def test_nested_keymap(self):
52+
keymap = {b"a": {b"b": "action"}}
53+
result = compile_keymap(keymap)
54+
self.assertEqual(result, {b"a": {b"b": "action"}})
55+
56+
def test_empty_value(self):
57+
keymap = {b"a": {b"": "action"}}
58+
result = compile_keymap(keymap)
59+
self.assertEqual(result, {b"a": {b"": "action"}})
60+
61+
def test_multiple_empty_values(self):
62+
keymap = {b"a": {b"": "action1", b"b": "action2"}}
63+
result = compile_keymap(keymap)
64+
self.assertEqual(result, {b"a": {b"": "action1", b"b": "action2"}})
65+
66+
def test_multiple_keymaps(self):
67+
keymap = {b"a": {b"b": "action1", b"c": "action2"}}
68+
result = compile_keymap(keymap)
69+
self.assertEqual(result, {b"a": {b"b": "action1", b"c": "action2"}})
70+
71+
def test_nested_multiple_keymaps(self):
72+
keymap = {b"a": {b"b": {b"c": "action"}}}
73+
result = compile_keymap(keymap)
74+
self.assertEqual(result, {b"a": {b"b": {b"c": "action"}}})

0 commit comments

Comments
 (0)