2024-05-21 13:44:09 -03:00
|
|
|
|
import itertools
|
2024-05-22 02:56:35 -03:00
|
|
|
|
import functools
|
2024-06-04 13:09:31 -03:00
|
|
|
|
import rlcompleter
|
2024-05-21 13:44:09 -03:00
|
|
|
|
from unittest import TestCase
|
2024-06-17 12:35:20 -03:00
|
|
|
|
from unittest.mock import MagicMock
|
2024-05-21 13:44:09 -03:00
|
|
|
|
|
2024-05-22 02:56:35 -03:00
|
|
|
|
from .support import handle_all_events, handle_events_narrow_console, code_to_events, prepare_reader
|
2024-05-21 13:44:09 -03:00
|
|
|
|
from _pyrepl.console import Event
|
2024-06-03 14:07:06 -03:00
|
|
|
|
from _pyrepl.reader import Reader
|
2024-05-21 13:44:09 -03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestReader(TestCase):
|
|
|
|
|
def assert_screen_equals(self, reader, expected):
|
2024-06-04 13:09:31 -03:00
|
|
|
|
actual = reader.screen
|
2024-05-21 13:44:09 -03:00
|
|
|
|
expected = expected.split("\n")
|
|
|
|
|
self.assertListEqual(actual, expected)
|
|
|
|
|
|
|
|
|
|
def test_calc_screen_wrap_simple(self):
|
|
|
|
|
events = code_to_events(10 * "a")
|
|
|
|
|
reader, _ = handle_events_narrow_console(events)
|
|
|
|
|
self.assert_screen_equals(reader, f"{9*"a"}\\\na")
|
|
|
|
|
|
|
|
|
|
def test_calc_screen_wrap_wide_characters(self):
|
|
|
|
|
events = code_to_events(8 * "a" + "樂")
|
|
|
|
|
reader, _ = handle_events_narrow_console(events)
|
|
|
|
|
self.assert_screen_equals(reader, f"{8*"a"}\\\n樂")
|
|
|
|
|
|
|
|
|
|
def test_calc_screen_wrap_three_lines(self):
|
|
|
|
|
events = code_to_events(20 * "a")
|
|
|
|
|
reader, _ = handle_events_narrow_console(events)
|
|
|
|
|
self.assert_screen_equals(reader, f"{9*"a"}\\\n{9*"a"}\\\naa")
|
|
|
|
|
|
2024-08-25 19:54:06 -03:00
|
|
|
|
def test_calc_screen_prompt_handling(self):
|
|
|
|
|
def prepare_reader_keep_prompts(*args, **kwargs):
|
|
|
|
|
reader = prepare_reader(*args, **kwargs)
|
|
|
|
|
del reader.get_prompt
|
|
|
|
|
reader.ps1 = ">>> "
|
|
|
|
|
reader.ps2 = ">>> "
|
|
|
|
|
reader.ps3 = "... "
|
|
|
|
|
reader.ps4 = ""
|
|
|
|
|
reader.can_colorize = False
|
|
|
|
|
reader.paste_mode = False
|
|
|
|
|
return reader
|
|
|
|
|
|
|
|
|
|
events = code_to_events("if some_condition:\nsome_function()")
|
|
|
|
|
reader, _ = handle_events_narrow_console(
|
|
|
|
|
events,
|
|
|
|
|
prepare_reader=prepare_reader_keep_prompts,
|
|
|
|
|
)
|
|
|
|
|
# fmt: off
|
|
|
|
|
self.assert_screen_equals(
|
|
|
|
|
reader,
|
|
|
|
|
(
|
|
|
|
|
">>> if so\\\n"
|
|
|
|
|
"me_condit\\\n"
|
|
|
|
|
"ion:\n"
|
|
|
|
|
"... s\\\n"
|
|
|
|
|
"ome_funct\\\n"
|
|
|
|
|
"ion()"
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
# fmt: on
|
|
|
|
|
|
2024-05-21 13:44:09 -03:00
|
|
|
|
def test_calc_screen_wrap_three_lines_mixed_character(self):
|
|
|
|
|
# fmt: off
|
|
|
|
|
code = (
|
|
|
|
|
"def f():\n"
|
|
|
|
|
f" {8*"a"}\n"
|
|
|
|
|
f" {5*"樂"}"
|
|
|
|
|
)
|
|
|
|
|
# fmt: on
|
|
|
|
|
|
|
|
|
|
events = code_to_events(code)
|
|
|
|
|
reader, _ = handle_events_narrow_console(events)
|
|
|
|
|
|
|
|
|
|
# fmt: off
|
|
|
|
|
self.assert_screen_equals(reader, (
|
|
|
|
|
"def f():\n"
|
|
|
|
|
f" {7*"a"}\\\n"
|
|
|
|
|
"a\n"
|
|
|
|
|
f" {3*"樂"}\\\n"
|
|
|
|
|
"樂樂"
|
|
|
|
|
))
|
|
|
|
|
# fmt: on
|
|
|
|
|
|
|
|
|
|
def test_calc_screen_backspace(self):
|
|
|
|
|
events = itertools.chain(
|
|
|
|
|
code_to_events("aaa"),
|
|
|
|
|
[
|
|
|
|
|
Event(evt="key", data="backspace", raw=bytearray(b"\x7f")),
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
reader, _ = handle_all_events(events)
|
|
|
|
|
self.assert_screen_equals(reader, "aa")
|
|
|
|
|
|
|
|
|
|
def test_calc_screen_wrap_removes_after_backspace(self):
|
|
|
|
|
events = itertools.chain(
|
|
|
|
|
code_to_events(10 * "a"),
|
|
|
|
|
[
|
|
|
|
|
Event(evt="key", data="backspace", raw=bytearray(b"\x7f")),
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
reader, _ = handle_events_narrow_console(events)
|
|
|
|
|
self.assert_screen_equals(reader, 9 * "a")
|
|
|
|
|
|
|
|
|
|
def test_calc_screen_backspace_in_second_line_after_wrap(self):
|
|
|
|
|
events = itertools.chain(
|
|
|
|
|
code_to_events(11 * "a"),
|
|
|
|
|
[
|
|
|
|
|
Event(evt="key", data="backspace", raw=bytearray(b"\x7f")),
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
reader, _ = handle_events_narrow_console(events)
|
|
|
|
|
self.assert_screen_equals(reader, f"{9*"a"}\\\na")
|
|
|
|
|
|
|
|
|
|
def test_setpos_for_xy_simple(self):
|
|
|
|
|
events = code_to_events("11+11")
|
|
|
|
|
reader, _ = handle_all_events(events)
|
|
|
|
|
reader.setpos_from_xy(0, 0)
|
|
|
|
|
self.assertEqual(reader.pos, 0)
|
|
|
|
|
|
2024-07-13 07:44:18 -03:00
|
|
|
|
def test_control_characters(self):
|
|
|
|
|
code = 'flag = "🏳️🌈"'
|
|
|
|
|
events = code_to_events(code)
|
|
|
|
|
reader, _ = handle_all_events(events)
|
|
|
|
|
self.assert_screen_equals(reader, 'flag = "🏳️\\u200d🌈"')
|
|
|
|
|
|
2024-05-21 13:44:09 -03:00
|
|
|
|
def test_setpos_from_xy_multiple_lines(self):
|
|
|
|
|
# fmt: off
|
|
|
|
|
code = (
|
|
|
|
|
"def foo():\n"
|
|
|
|
|
" return 1"
|
|
|
|
|
)
|
|
|
|
|
# fmt: on
|
|
|
|
|
|
|
|
|
|
events = code_to_events(code)
|
|
|
|
|
reader, _ = handle_all_events(events)
|
|
|
|
|
reader.setpos_from_xy(2, 1)
|
|
|
|
|
self.assertEqual(reader.pos, 13)
|
|
|
|
|
|
|
|
|
|
def test_setpos_from_xy_after_wrap(self):
|
|
|
|
|
# fmt: off
|
|
|
|
|
code = (
|
|
|
|
|
"def foo():\n"
|
|
|
|
|
" hello"
|
|
|
|
|
)
|
|
|
|
|
# fmt: on
|
|
|
|
|
|
|
|
|
|
events = code_to_events(code)
|
|
|
|
|
reader, _ = handle_events_narrow_console(events)
|
|
|
|
|
reader.setpos_from_xy(2, 2)
|
|
|
|
|
self.assertEqual(reader.pos, 13)
|
|
|
|
|
|
|
|
|
|
def test_setpos_fromxy_in_wrapped_line(self):
|
|
|
|
|
# fmt: off
|
|
|
|
|
code = (
|
|
|
|
|
"def foo():\n"
|
|
|
|
|
" hello"
|
|
|
|
|
)
|
|
|
|
|
# fmt: on
|
|
|
|
|
|
|
|
|
|
events = code_to_events(code)
|
|
|
|
|
reader, _ = handle_events_narrow_console(events)
|
|
|
|
|
reader.setpos_from_xy(0, 1)
|
|
|
|
|
self.assertEqual(reader.pos, 9)
|
|
|
|
|
|
|
|
|
|
def test_up_arrow_after_ctrl_r(self):
|
|
|
|
|
events = iter(
|
|
|
|
|
[
|
|
|
|
|
Event(evt="key", data="\x12", raw=bytearray(b"\x12")),
|
|
|
|
|
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
reader, _ = handle_all_events(events)
|
|
|
|
|
self.assert_screen_equals(reader, "")
|
2024-05-22 02:56:35 -03:00
|
|
|
|
|
|
|
|
|
def test_newline_within_block_trailing_whitespace(self):
|
|
|
|
|
# fmt: off
|
|
|
|
|
code = (
|
|
|
|
|
"def foo():\n"
|
|
|
|
|
"a = 1\n"
|
|
|
|
|
)
|
|
|
|
|
# fmt: on
|
|
|
|
|
|
|
|
|
|
events = itertools.chain(
|
|
|
|
|
code_to_events(code),
|
|
|
|
|
[
|
|
|
|
|
# go to the end of the first line
|
|
|
|
|
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
|
|
|
|
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
|
|
|
|
Event(evt="key", data="\x05", raw=bytearray(b"\x1bO5")),
|
|
|
|
|
# new lines in-block shouldn't terminate the block
|
|
|
|
|
Event(evt="key", data="\n", raw=bytearray(b"\n")),
|
|
|
|
|
Event(evt="key", data="\n", raw=bytearray(b"\n")),
|
|
|
|
|
# end of line 2
|
|
|
|
|
Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
|
|
|
|
|
Event(evt="key", data="\x05", raw=bytearray(b"\x1bO5")),
|
|
|
|
|
# a double new line in-block should terminate the block
|
|
|
|
|
# even if its followed by whitespace
|
|
|
|
|
Event(evt="key", data="\n", raw=bytearray(b"\n")),
|
|
|
|
|
Event(evt="key", data="\n", raw=bytearray(b"\n")),
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
no_paste_reader = functools.partial(prepare_reader, paste_mode=False)
|
|
|
|
|
reader, _ = handle_all_events(events, prepare_reader=no_paste_reader)
|
|
|
|
|
|
|
|
|
|
expected = (
|
|
|
|
|
"def foo():\n"
|
2024-05-31 06:02:54 -03:00
|
|
|
|
" \n"
|
|
|
|
|
" \n"
|
2024-05-22 02:56:35 -03:00
|
|
|
|
" a = 1\n"
|
|
|
|
|
" \n"
|
|
|
|
|
" " # HistoricalReader will trim trailing whitespace
|
|
|
|
|
)
|
|
|
|
|
self.assert_screen_equals(reader, expected)
|
|
|
|
|
self.assertTrue(reader.finished)
|
2024-06-03 14:07:06 -03:00
|
|
|
|
|
2024-06-04 15:32:43 -03:00
|
|
|
|
def test_input_hook_is_called_if_set(self):
|
|
|
|
|
input_hook = MagicMock()
|
|
|
|
|
def _prepare_console(events):
|
|
|
|
|
console = MagicMock()
|
|
|
|
|
console.get_event.side_effect = events
|
|
|
|
|
console.height = 100
|
|
|
|
|
console.width = 80
|
|
|
|
|
console.input_hook = input_hook
|
|
|
|
|
return console
|
|
|
|
|
|
|
|
|
|
events = code_to_events("a")
|
|
|
|
|
reader, _ = handle_all_events(events, prepare_console=_prepare_console)
|
|
|
|
|
|
|
|
|
|
self.assertEqual(len(input_hook.mock_calls), 4)
|
|
|
|
|
|
2024-06-04 14:46:33 -03:00
|
|
|
|
def test_keyboard_interrupt_clears_screen(self):
|
|
|
|
|
namespace = {"itertools": itertools}
|
|
|
|
|
code = "import itertools\nitertools."
|
|
|
|
|
events = itertools.chain(code_to_events(code), [
|
|
|
|
|
Event(evt='key', data='\t', raw=bytearray(b'\t')), # Two tabs for completion
|
|
|
|
|
Event(evt='key', data='\t', raw=bytearray(b'\t')),
|
|
|
|
|
Event(evt='key', data='\x03', raw=bytearray(b'\x03')), # Ctrl-C
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
completing_reader = functools.partial(
|
|
|
|
|
prepare_reader,
|
|
|
|
|
readline_completer=rlcompleter.Completer(namespace).complete
|
|
|
|
|
)
|
|
|
|
|
reader, _ = handle_all_events(events, prepare_reader=completing_reader)
|
|
|
|
|
self.assertEqual(reader.calc_screen(), code.split("\n"))
|
|
|
|
|
|
2024-06-03 14:07:06 -03:00
|
|
|
|
def test_prompt_length(self):
|
|
|
|
|
# Handles simple ASCII prompt
|
|
|
|
|
ps1 = ">>> "
|
|
|
|
|
prompt, l = Reader.process_prompt(ps1)
|
|
|
|
|
self.assertEqual(prompt, ps1)
|
|
|
|
|
self.assertEqual(l, 4)
|
|
|
|
|
|
|
|
|
|
# Handles ANSI escape sequences
|
|
|
|
|
ps1 = "\033[0;32m>>> \033[0m"
|
|
|
|
|
prompt, l = Reader.process_prompt(ps1)
|
|
|
|
|
self.assertEqual(prompt, "\033[0;32m>>> \033[0m")
|
|
|
|
|
self.assertEqual(l, 4)
|
|
|
|
|
|
|
|
|
|
# Handles ANSI escape sequences bracketed in \001 .. \002
|
|
|
|
|
ps1 = "\001\033[0;32m\002>>> \001\033[0m\002"
|
|
|
|
|
prompt, l = Reader.process_prompt(ps1)
|
|
|
|
|
self.assertEqual(prompt, "\033[0;32m>>> \033[0m")
|
|
|
|
|
self.assertEqual(l, 4)
|
|
|
|
|
|
|
|
|
|
# Handles wide characters in prompt
|
|
|
|
|
ps1 = "樂>> "
|
|
|
|
|
prompt, l = Reader.process_prompt(ps1)
|
|
|
|
|
self.assertEqual(prompt, ps1)
|
|
|
|
|
self.assertEqual(l, 5)
|
|
|
|
|
|
|
|
|
|
# Handles wide characters AND ANSI sequences together
|
|
|
|
|
ps1 = "\001\033[0;32m\002樂>\001\033[0m\002> "
|
|
|
|
|
prompt, l = Reader.process_prompt(ps1)
|
|
|
|
|
self.assertEqual(prompt, "\033[0;32m樂>\033[0m> ")
|
|
|
|
|
self.assertEqual(l, 5)
|
2024-06-04 13:09:31 -03:00
|
|
|
|
|
|
|
|
|
def test_completions_updated_on_key_press(self):
|
|
|
|
|
namespace = {"itertools": itertools}
|
|
|
|
|
code = "itertools."
|
|
|
|
|
events = itertools.chain(code_to_events(code), [
|
|
|
|
|
Event(evt='key', data='\t', raw=bytearray(b'\t')), # Two tabs for completion
|
|
|
|
|
Event(evt='key', data='\t', raw=bytearray(b'\t')),
|
|
|
|
|
], code_to_events("a"))
|
|
|
|
|
|
|
|
|
|
completing_reader = functools.partial(
|
|
|
|
|
prepare_reader,
|
|
|
|
|
readline_completer=rlcompleter.Completer(namespace).complete
|
|
|
|
|
)
|
|
|
|
|
reader, _ = handle_all_events(events, prepare_reader=completing_reader)
|
|
|
|
|
|
|
|
|
|
actual = reader.screen
|
|
|
|
|
self.assertEqual(len(actual), 2)
|
|
|
|
|
self.assertEqual(actual[0].rstrip(), "itertools.accumulate(")
|
|
|
|
|
self.assertEqual(actual[1], f"{code}a")
|
|
|
|
|
|
|
|
|
|
def test_key_press_on_tab_press_once(self):
|
|
|
|
|
namespace = {"itertools": itertools}
|
|
|
|
|
code = "itertools."
|
|
|
|
|
events = itertools.chain(code_to_events(code), [
|
|
|
|
|
Event(evt='key', data='\t', raw=bytearray(b'\t')),
|
|
|
|
|
], code_to_events("a"))
|
|
|
|
|
|
|
|
|
|
completing_reader = functools.partial(
|
|
|
|
|
prepare_reader,
|
|
|
|
|
readline_completer=rlcompleter.Completer(namespace).complete
|
|
|
|
|
)
|
|
|
|
|
reader, _ = handle_all_events(events, prepare_reader=completing_reader)
|
|
|
|
|
|
|
|
|
|
self.assert_screen_equals(reader, f"{code}a")
|