2024-05-05 16:32:23 -03:00
|
|
|
import itertools
|
|
|
|
import os
|
|
|
|
import rlcompleter
|
|
|
|
import sys
|
2024-05-06 06:50:42 -03:00
|
|
|
import tempfile
|
2024-05-05 16:32:23 -03:00
|
|
|
import unittest
|
|
|
|
from code import InteractiveConsole
|
|
|
|
from functools import partial
|
|
|
|
from unittest import TestCase
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
|
|
|
from test.support import requires
|
|
|
|
from test.support.import_helper import import_module
|
|
|
|
|
|
|
|
# Optionally test pyrepl. This currently requires that the
|
|
|
|
# 'curses' resource be given on the regrtest command line using the -u
|
|
|
|
# option. Additionally, we need to attempt to import curses and readline.
|
|
|
|
requires('curses')
|
|
|
|
curses = import_module('curses')
|
|
|
|
readline = import_module('readline')
|
|
|
|
|
|
|
|
from _pyrepl.console import Console, Event
|
|
|
|
from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig
|
|
|
|
from _pyrepl.simple_interact import _strip_final_indent
|
|
|
|
from _pyrepl.unix_eventqueue import EventQueue
|
|
|
|
|
|
|
|
|
|
|
|
def more_lines(unicodetext, namespace=None):
|
|
|
|
if namespace is None:
|
|
|
|
namespace = {}
|
|
|
|
src = _strip_final_indent(unicodetext)
|
|
|
|
console = InteractiveConsole(namespace, filename="<stdin>")
|
|
|
|
try:
|
|
|
|
code = console.compile(src, "<stdin>", "single")
|
|
|
|
except (OverflowError, SyntaxError, ValueError):
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
return code is None
|
|
|
|
|
|
|
|
|
|
|
|
def multiline_input(reader, namespace=None):
|
|
|
|
saved = reader.more_lines
|
|
|
|
try:
|
|
|
|
reader.more_lines = partial(more_lines, namespace=namespace)
|
|
|
|
reader.ps1 = reader.ps2 = ">>>"
|
|
|
|
reader.ps3 = reader.ps4 = "..."
|
|
|
|
return reader.readline()
|
|
|
|
finally:
|
|
|
|
reader.more_lines = saved
|
|
|
|
reader.paste_mode = False
|
|
|
|
|
|
|
|
|
|
|
|
def code_to_events(code):
|
|
|
|
for c in code:
|
|
|
|
yield Event(evt="key", data=c, raw=bytearray(c.encode("utf-8")))
|
|
|
|
|
|
|
|
|
|
|
|
def prepare_mock_console(events, **kwargs):
|
|
|
|
console = MagicMock()
|
|
|
|
console.get_event.side_effect = events
|
|
|
|
console.height = 100
|
|
|
|
console.width = 80
|
|
|
|
for key, val in kwargs.items():
|
|
|
|
setattr(console, key, val)
|
|
|
|
return console
|
|
|
|
|
|
|
|
|
|
|
|
def prepare_fake_console(**kwargs):
|
|
|
|
console = FakeConsole()
|
|
|
|
for key, val in kwargs.items():
|
|
|
|
setattr(console, key, val)
|
|
|
|
return console
|
|
|
|
|
|
|
|
|
|
|
|
def prepare_reader(console, **kwargs):
|
|
|
|
config = ReadlineConfig(readline_completer=None)
|
|
|
|
reader = ReadlineAlikeReader(console=console, config=config)
|
|
|
|
reader.more_lines = partial(more_lines, namespace=None)
|
|
|
|
reader.paste_mode = True # Avoid extra indents
|
|
|
|
|
|
|
|
def get_prompt(lineno, cursor_on_line) -> str:
|
|
|
|
return ""
|
|
|
|
|
|
|
|
reader.get_prompt = get_prompt # Remove prompt for easier calculations of (x, y)
|
|
|
|
|
|
|
|
for key, val in kwargs.items():
|
|
|
|
setattr(reader, key, val)
|
|
|
|
|
|
|
|
return reader
|
|
|
|
|
|
|
|
|
|
|
|
def handle_all_events(
|
|
|
|
events, prepare_console=prepare_mock_console, prepare_reader=prepare_reader
|
|
|
|
):
|
|
|
|
console = prepare_console(events)
|
|
|
|
reader = prepare_reader(console)
|
|
|
|
try:
|
|
|
|
while True:
|
|
|
|
reader.handle1()
|
|
|
|
except StopIteration:
|
|
|
|
pass
|
|
|
|
return reader, console
|
|
|
|
|
|
|
|
|
|
|
|
handle_events_narrow_console = partial(
|
|
|
|
handle_all_events, prepare_console=partial(prepare_mock_console, width=10)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
class FakeConsole(Console):
|
|
|
|
def __init__(self, events, encoding="utf-8"):
|
|
|
|
self.events = iter(events)
|
|
|
|
self.encoding = encoding
|
|
|
|
self.screen = []
|
|
|
|
self.height = 100
|
|
|
|
self.width = 80
|
|
|
|
|
|
|
|
def get_event(self, block: bool = True) -> Event | None:
|
|
|
|
return next(self.events)
|
|
|
|
|
|
|
|
def getpending(self) -> Event:
|
|
|
|
return self.get_event(block=False)
|
|
|
|
|
|
|
|
def getheightwidth(self) -> tuple[int, int]:
|
|
|
|
return self.height, self.width
|
|
|
|
|
|
|
|
def refresh(self, screen: list[str], xy: tuple[int, int]) -> None:
|
|
|
|
pass
|
|
|
|
|
|
|
|
def prepare(self) -> None:
|
|
|
|
pass
|
|
|
|
|
|
|
|
def restore(self) -> None:
|
|
|
|
pass
|
|
|
|
|
|
|
|
def move_cursor(self, x: int, y: int) -> None:
|
|
|
|
pass
|
|
|
|
|
|
|
|
def set_cursor_vis(self, visible: bool) -> None:
|
|
|
|
pass
|
|
|
|
|
|
|
|
def push_char(self, char: int | bytes) -> None:
|
|
|
|
pass
|
|
|
|
|
|
|
|
def beep(self) -> None:
|
|
|
|
pass
|
|
|
|
|
|
|
|
def clear(self) -> None:
|
|
|
|
pass
|
|
|
|
|
|
|
|
def finish(self) -> None:
|
|
|
|
pass
|
|
|
|
|
|
|
|
def flushoutput(self) -> None:
|
|
|
|
pass
|
|
|
|
|
|
|
|
def forgetinput(self) -> None:
|
|
|
|
pass
|
|
|
|
|
|
|
|
def wait(self) -> None:
|
|
|
|
pass
|
|
|
|
|
|
|
|
def repaint(self) -> None:
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class TestCursorPosition(TestCase):
|
|
|
|
def test_up_arrow_simple(self):
|
|
|
|
# fmt: off
|
|
|
|
code = (
|
|
|
|
'def f():\n'
|
|
|
|
' ...\n'
|
|
|
|
)
|
|
|
|
# fmt: on
|
|
|
|
events = itertools.chain(
|
|
|
|
code_to_events(code),
|
|
|
|
[
|
|
|
|
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
|
|
|
reader, console = handle_all_events(events)
|
|
|
|
self.assertEqual(reader.cxy, (0, 1))
|
|
|
|
console.move_cursor.assert_called_once_with(0, 1)
|
|
|
|
|
|
|
|
def test_down_arrow_end_of_input(self):
|
|
|
|
# fmt: off
|
|
|
|
code = (
|
|
|
|
'def f():\n'
|
|
|
|
' ...\n'
|
|
|
|
)
|
|
|
|
# fmt: on
|
|
|
|
events = itertools.chain(
|
|
|
|
code_to_events(code),
|
|
|
|
[
|
|
|
|
Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
|
|
|
reader, console = handle_all_events(events)
|
|
|
|
self.assertEqual(reader.cxy, (0, 2))
|
|
|
|
console.move_cursor.assert_called_once_with(0, 2)
|
|
|
|
|
|
|
|
def test_left_arrow_simple(self):
|
|
|
|
events = itertools.chain(
|
|
|
|
code_to_events("11+11"),
|
|
|
|
[
|
|
|
|
Event(evt="key", data="left", raw=bytearray(b"\x1bOD")),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
|
|
|
reader, console = handle_all_events(events)
|
|
|
|
self.assertEqual(reader.cxy, (4, 0))
|
|
|
|
console.move_cursor.assert_called_once_with(4, 0)
|
|
|
|
|
|
|
|
def test_right_arrow_end_of_line(self):
|
|
|
|
events = itertools.chain(
|
|
|
|
code_to_events("11+11"),
|
|
|
|
[
|
|
|
|
Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
|
|
|
reader, console = handle_all_events(events)
|
|
|
|
self.assertEqual(reader.cxy, (5, 0))
|
|
|
|
console.move_cursor.assert_called_once_with(5, 0)
|
|
|
|
|
|
|
|
def test_cursor_position_simple_character(self):
|
|
|
|
events = itertools.chain(code_to_events("k"))
|
|
|
|
|
|
|
|
reader, _ = handle_all_events(events)
|
|
|
|
self.assertEqual(reader.pos, 1)
|
|
|
|
|
|
|
|
# 1 for simple character
|
|
|
|
self.assertEqual(reader.cxy, (1, 0))
|
|
|
|
|
|
|
|
def test_cursor_position_double_width_character(self):
|
|
|
|
events = itertools.chain(code_to_events("樂"))
|
|
|
|
|
|
|
|
reader, _ = handle_all_events(events)
|
|
|
|
self.assertEqual(reader.pos, 1)
|
|
|
|
|
|
|
|
# 2 for wide character
|
|
|
|
self.assertEqual(reader.cxy, (2, 0))
|
|
|
|
|
|
|
|
def test_cursor_position_double_width_character_move_left(self):
|
|
|
|
events = itertools.chain(
|
|
|
|
code_to_events("樂"),
|
|
|
|
[
|
|
|
|
Event(evt="key", data="left", raw=bytearray(b"\x1bOD")),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
|
|
|
reader, _ = handle_all_events(events)
|
|
|
|
self.assertEqual(reader.pos, 0)
|
|
|
|
self.assertEqual(reader.cxy, (0, 0))
|
|
|
|
|
|
|
|
def test_cursor_position_double_width_character_move_left_right(self):
|
|
|
|
events = itertools.chain(
|
|
|
|
code_to_events("樂"),
|
|
|
|
[
|
|
|
|
Event(evt="key", data="left", raw=bytearray(b"\x1bOD")),
|
|
|
|
Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
|
|
|
reader, _ = handle_all_events(events)
|
|
|
|
self.assertEqual(reader.pos, 1)
|
|
|
|
|
|
|
|
# 2 for wide character
|
|
|
|
self.assertEqual(reader.cxy, (2, 0))
|
|
|
|
|
|
|
|
def test_cursor_position_double_width_characters_move_up(self):
|
|
|
|
for_loop = "for _ in _:"
|
|
|
|
|
|
|
|
# fmt: off
|
|
|
|
code = (
|
|
|
|
f"{for_loop}\n"
|
|
|
|
" ' 可口可乐; 可口可樂'"
|
|
|
|
)
|
|
|
|
# fmt: on
|
|
|
|
|
|
|
|
events = itertools.chain(
|
|
|
|
code_to_events(code),
|
|
|
|
[
|
|
|
|
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
|
|
|
reader, _ = handle_all_events(events)
|
|
|
|
|
|
|
|
# cursor at end of first line
|
|
|
|
self.assertEqual(reader.pos, len(for_loop))
|
|
|
|
self.assertEqual(reader.cxy, (len(for_loop), 0))
|
|
|
|
|
|
|
|
def test_cursor_position_double_width_characters_move_up_down(self):
|
|
|
|
for_loop = "for _ in _:"
|
|
|
|
|
|
|
|
# fmt: off
|
|
|
|
code = (
|
|
|
|
f"{for_loop}\n"
|
|
|
|
" ' 可口可乐; 可口可樂'"
|
|
|
|
)
|
|
|
|
# fmt: on
|
|
|
|
|
|
|
|
events = itertools.chain(
|
|
|
|
code_to_events(code),
|
|
|
|
[
|
|
|
|
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
|
|
|
Event(evt="key", data="left", raw=bytearray(b"\x1bOD")),
|
|
|
|
Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
|
|
|
reader, _ = handle_all_events(events)
|
|
|
|
|
|
|
|
# cursor here (showing 2nd line only):
|
|
|
|
# < ' 可口可乐; 可口可樂'>
|
|
|
|
# ^
|
|
|
|
self.assertEqual(reader.pos, 19)
|
|
|
|
self.assertEqual(reader.cxy, (10, 1))
|
|
|
|
|
|
|
|
def test_cursor_position_multiple_double_width_characters_move_left(self):
|
|
|
|
events = itertools.chain(
|
|
|
|
code_to_events("' 可口可乐; 可口可樂'"),
|
|
|
|
[
|
|
|
|
Event(evt="key", data="left", raw=bytearray(b"\x1bOD")),
|
|
|
|
Event(evt="key", data="left", raw=bytearray(b"\x1bOD")),
|
|
|
|
Event(evt="key", data="left", raw=bytearray(b"\x1bOD")),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
|
|
|
reader, _ = handle_all_events(events)
|
|
|
|
self.assertEqual(reader.pos, 10)
|
|
|
|
|
|
|
|
# 1 for quote, 1 for space, 2 per wide character,
|
|
|
|
# 1 for semicolon, 1 for space, 2 per wide character
|
|
|
|
self.assertEqual(reader.cxy, (16, 0))
|
|
|
|
|
|
|
|
def test_cursor_position_move_up_to_eol(self):
|
|
|
|
first_line = "for _ in _:"
|
|
|
|
second_line = " hello"
|
|
|
|
|
|
|
|
# fmt: off
|
|
|
|
code = (
|
|
|
|
f"{first_line}\n"
|
|
|
|
f"{second_line}\n"
|
|
|
|
" h\n"
|
|
|
|
" hel"
|
|
|
|
)
|
|
|
|
# fmt: on
|
|
|
|
|
|
|
|
events = itertools.chain(
|
|
|
|
code_to_events(code),
|
|
|
|
[
|
|
|
|
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
|
|
|
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
|
|
|
reader, _ = handle_all_events(events)
|
|
|
|
|
|
|
|
# Cursor should be at end of line 1, even though line 2 is shorter
|
|
|
|
# for _ in _:
|
|
|
|
# hello
|
|
|
|
# h
|
|
|
|
# hel
|
|
|
|
self.assertEqual(
|
|
|
|
reader.pos, len(first_line) + len(second_line) + 1
|
|
|
|
) # +1 for newline
|
|
|
|
self.assertEqual(reader.cxy, (len(second_line), 1))
|
|
|
|
|
|
|
|
def test_cursor_position_move_down_to_eol(self):
|
|
|
|
last_line = " hel"
|
|
|
|
|
|
|
|
# fmt: off
|
|
|
|
code = (
|
|
|
|
"for _ in _:\n"
|
|
|
|
" hello\n"
|
|
|
|
" h\n"
|
|
|
|
f"{last_line}"
|
|
|
|
)
|
|
|
|
# fmt: on
|
|
|
|
|
|
|
|
events = itertools.chain(
|
|
|
|
code_to_events(code),
|
|
|
|
[
|
|
|
|
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
|
|
|
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
|
|
|
Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
|
|
|
|
Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
|
|
|
reader, _ = handle_all_events(events)
|
|
|
|
|
|
|
|
# Cursor should be at end of line 3, even though line 2 is shorter
|
|
|
|
# for _ in _:
|
|
|
|
# hello
|
|
|
|
# h
|
|
|
|
# hel
|
|
|
|
self.assertEqual(reader.pos, len(code))
|
|
|
|
self.assertEqual(reader.cxy, (len(last_line), 3))
|
|
|
|
|
|
|
|
def test_cursor_position_multiple_mixed_lines_move_up(self):
|
|
|
|
# fmt: off
|
|
|
|
code = (
|
|
|
|
"def foo():\n"
|
|
|
|
" x = '可口可乐; 可口可樂'\n"
|
|
|
|
" y = 'abckdfjskldfjslkdjf'"
|
|
|
|
)
|
|
|
|
# fmt: on
|
|
|
|
|
|
|
|
events = itertools.chain(
|
|
|
|
code_to_events(code),
|
|
|
|
13 * [Event(evt="key", data="left", raw=bytearray(b"\x1bOD"))],
|
|
|
|
[Event(evt="key", data="up", raw=bytearray(b"\x1bOA"))],
|
|
|
|
)
|
|
|
|
|
|
|
|
reader, _ = handle_all_events(events)
|
|
|
|
|
|
|
|
# By moving left, we're before the s:
|
|
|
|
# y = 'abckdfjskldfjslkdjf'
|
|
|
|
# ^
|
|
|
|
# And we should move before the semi-colon despite the different offset
|
|
|
|
# x = '可口可乐; 可口可樂'
|
|
|
|
# ^
|
|
|
|
self.assertEqual(reader.pos, 22)
|
|
|
|
self.assertEqual(reader.cxy, (15, 1))
|
|
|
|
|
|
|
|
def test_cursor_position_after_wrap_and_move_up(self):
|
|
|
|
# fmt: off
|
|
|
|
code = (
|
|
|
|
"def foo():\n"
|
|
|
|
" hello"
|
|
|
|
)
|
|
|
|
# fmt: on
|
|
|
|
|
|
|
|
events = itertools.chain(
|
|
|
|
code_to_events(code),
|
|
|
|
[
|
|
|
|
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
reader, _ = handle_events_narrow_console(events)
|
|
|
|
|
|
|
|
# The code looks like this:
|
|
|
|
# def foo()\
|
|
|
|
# :
|
|
|
|
# hello
|
|
|
|
# After moving up we should be after the colon in line 2
|
|
|
|
self.assertEqual(reader.pos, 10)
|
|
|
|
self.assertEqual(reader.cxy, (1, 1))
|
|
|
|
|
|
|
|
|
|
|
|
class TestPyReplOutput(TestCase):
|
|
|
|
def prepare_reader(self, events):
|
|
|
|
console = FakeConsole(events)
|
|
|
|
config = ReadlineConfig(readline_completer=None)
|
|
|
|
reader = ReadlineAlikeReader(console=console, config=config)
|
|
|
|
return reader
|
|
|
|
|
|
|
|
def test_basic(self):
|
|
|
|
reader = self.prepare_reader(code_to_events("1+1\n"))
|
|
|
|
|
|
|
|
output = multiline_input(reader)
|
|
|
|
self.assertEqual(output, "1+1")
|
|
|
|
|
|
|
|
def test_multiline_edit(self):
|
|
|
|
events = itertools.chain(
|
|
|
|
code_to_events("def f():\n ...\n\n"),
|
|
|
|
[
|
|
|
|
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
|
|
|
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
|
|
|
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
|
|
|
Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
|
|
|
|
Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
|
|
|
|
Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
|
|
|
|
Event(evt="key", data="backspace", raw=bytearray(b"\x7f")),
|
|
|
|
Event(evt="key", data="g", raw=bytearray(b"g")),
|
|
|
|
Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
|
|
|
|
Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
|
|
|
|
Event(evt="key", data="\n", raw=bytearray(b"\n")),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
reader = self.prepare_reader(events)
|
|
|
|
|
|
|
|
output = multiline_input(reader)
|
|
|
|
self.assertEqual(output, "def f():\n ...\n ")
|
|
|
|
output = multiline_input(reader)
|
|
|
|
self.assertEqual(output, "def g():\n ...\n ")
|
|
|
|
|
|
|
|
def test_history_navigation_with_up_arrow(self):
|
|
|
|
events = itertools.chain(
|
|
|
|
code_to_events("1+1\n2+2\n"),
|
|
|
|
[
|
|
|
|
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
|
|
|
Event(evt="key", data="\n", raw=bytearray(b"\n")),
|
|
|
|
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
|
|
|
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
|
|
|
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
|
|
|
Event(evt="key", data="\n", raw=bytearray(b"\n")),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
|
|
|
reader = self.prepare_reader(events)
|
|
|
|
|
|
|
|
output = multiline_input(reader)
|
|
|
|
self.assertEqual(output, "1+1")
|
|
|
|
output = multiline_input(reader)
|
|
|
|
self.assertEqual(output, "2+2")
|
|
|
|
output = multiline_input(reader)
|
|
|
|
self.assertEqual(output, "2+2")
|
|
|
|
output = multiline_input(reader)
|
|
|
|
self.assertEqual(output, "1+1")
|
|
|
|
|
|
|
|
def test_history_navigation_with_down_arrow(self):
|
|
|
|
events = itertools.chain(
|
|
|
|
code_to_events("1+1\n2+2\n"),
|
|
|
|
[
|
|
|
|
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
|
|
|
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
|
|
|
Event(evt="key", data="\n", raw=bytearray(b"\n")),
|
|
|
|
Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
|
|
|
|
Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
|
|
|
reader = self.prepare_reader(events)
|
|
|
|
|
|
|
|
output = multiline_input(reader)
|
|
|
|
self.assertEqual(output, "1+1")
|
|
|
|
|
|
|
|
def test_history_search(self):
|
|
|
|
events = itertools.chain(
|
|
|
|
code_to_events("1+1\n2+2\n3+3\n"),
|
|
|
|
[
|
|
|
|
Event(evt="key", data="\x12", raw=bytearray(b"\x12")),
|
|
|
|
Event(evt="key", data="1", raw=bytearray(b"1")),
|
|
|
|
Event(evt="key", data="\n", raw=bytearray(b"\n")),
|
|
|
|
Event(evt="key", data="\n", raw=bytearray(b"\n")),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
|
|
|
reader = self.prepare_reader(events)
|
|
|
|
|
|
|
|
output = multiline_input(reader)
|
|
|
|
self.assertEqual(output, "1+1")
|
|
|
|
output = multiline_input(reader)
|
|
|
|
self.assertEqual(output, "2+2")
|
|
|
|
output = multiline_input(reader)
|
|
|
|
self.assertEqual(output, "3+3")
|
|
|
|
output = multiline_input(reader)
|
|
|
|
self.assertEqual(output, "1+1")
|
|
|
|
|
|
|
|
def test_control_character(self):
|
|
|
|
events = code_to_events("c\x1d\n")
|
|
|
|
reader = self.prepare_reader(events)
|
|
|
|
output = multiline_input(reader)
|
|
|
|
self.assertEqual(output, "c\x1d")
|
|
|
|
|
|
|
|
|
|
|
|
class TestPyReplCompleter(TestCase):
|
|
|
|
def prepare_reader(self, events, namespace):
|
|
|
|
console = FakeConsole(events)
|
|
|
|
config = ReadlineConfig()
|
|
|
|
config.readline_completer = rlcompleter.Completer(namespace).complete
|
|
|
|
reader = ReadlineAlikeReader(console=console, config=config)
|
|
|
|
return reader
|
|
|
|
|
|
|
|
def test_simple_completion(self):
|
|
|
|
events = code_to_events("os.geten\t\n")
|
|
|
|
|
|
|
|
namespace = {"os": os}
|
|
|
|
reader = self.prepare_reader(events, namespace)
|
|
|
|
|
|
|
|
output = multiline_input(reader, namespace)
|
|
|
|
self.assertEqual(output, "os.getenv")
|
|
|
|
|
|
|
|
def test_completion_with_many_options(self):
|
2024-05-06 07:56:47 -03:00
|
|
|
# Test with something that initially displays many options
|
|
|
|
# and then complete from one of them. The first time tab is
|
|
|
|
# pressed, the options are displayed (which corresponds to
|
|
|
|
# when the repl shows [ not unique ]) and the second completes
|
|
|
|
# from one of them.
|
|
|
|
events = code_to_events("os.\t\tO_AP\t\n")
|
2024-05-05 16:32:23 -03:00
|
|
|
|
|
|
|
namespace = {"os": os}
|
|
|
|
reader = self.prepare_reader(events, namespace)
|
|
|
|
|
|
|
|
output = multiline_input(reader, namespace)
|
2024-05-06 07:56:47 -03:00
|
|
|
self.assertEqual(output, "os.O_APPEND")
|
2024-05-05 16:32:23 -03:00
|
|
|
|
|
|
|
def test_empty_namespace_completion(self):
|
|
|
|
events = code_to_events("os.geten\t\n")
|
|
|
|
namespace = {}
|
|
|
|
reader = self.prepare_reader(events, namespace)
|
|
|
|
|
|
|
|
output = multiline_input(reader, namespace)
|
|
|
|
self.assertEqual(output, "os.geten")
|
|
|
|
|
|
|
|
def test_global_namespace_completion(self):
|
|
|
|
events = code_to_events("py\t\n")
|
|
|
|
namespace = {"python": None}
|
|
|
|
reader = self.prepare_reader(events, namespace)
|
|
|
|
output = multiline_input(reader, namespace)
|
|
|
|
self.assertEqual(output, "python")
|
|
|
|
|
|
|
|
|
|
|
|
@patch("_pyrepl.curses.tigetstr", lambda x: b"")
|
|
|
|
class TestUnivEventQueue(TestCase):
|
2024-05-06 06:50:42 -03:00
|
|
|
def setUp(self):
|
|
|
|
self.file = tempfile.TemporaryFile()
|
|
|
|
|
|
|
|
def tearDown(self) -> None:
|
|
|
|
self.file.close()
|
|
|
|
|
2024-05-05 16:32:23 -03:00
|
|
|
def test_get(self):
|
2024-05-06 06:50:42 -03:00
|
|
|
eq = EventQueue(self.file.fileno(), "utf-8")
|
2024-05-05 16:32:23 -03:00
|
|
|
event = Event("key", "a", b"a")
|
|
|
|
eq.insert(event)
|
|
|
|
self.assertEqual(eq.get(), event)
|
|
|
|
|
|
|
|
def test_empty(self):
|
2024-05-06 06:50:42 -03:00
|
|
|
eq = EventQueue(self.file.fileno(), "utf-8")
|
2024-05-05 16:32:23 -03:00
|
|
|
self.assertTrue(eq.empty())
|
|
|
|
eq.insert(Event("key", "a", b"a"))
|
|
|
|
self.assertFalse(eq.empty())
|
|
|
|
|
|
|
|
def test_flush_buf(self):
|
2024-05-06 06:50:42 -03:00
|
|
|
eq = EventQueue(self.file.fileno(), "utf-8")
|
2024-05-05 16:32:23 -03:00
|
|
|
eq.buf.extend(b"test")
|
|
|
|
self.assertEqual(eq.flush_buf(), b"test")
|
|
|
|
self.assertEqual(eq.buf, bytearray())
|
|
|
|
|
|
|
|
def test_insert(self):
|
2024-05-06 06:50:42 -03:00
|
|
|
eq = EventQueue(self.file.fileno(), "utf-8")
|
2024-05-05 16:32:23 -03:00
|
|
|
event = Event("key", "a", b"a")
|
|
|
|
eq.insert(event)
|
|
|
|
self.assertEqual(eq.events[0], event)
|
|
|
|
|
|
|
|
@patch("_pyrepl.unix_eventqueue.keymap")
|
|
|
|
def test_push_with_key_in_keymap(self, mock_keymap):
|
|
|
|
mock_keymap.compile_keymap.return_value = {"a": "b"}
|
2024-05-06 06:50:42 -03:00
|
|
|
eq = EventQueue(self.file.fileno(), "utf-8")
|
2024-05-05 16:32:23 -03:00
|
|
|
eq.keymap = {b"a": "b"}
|
|
|
|
eq.push("a")
|
2024-05-06 06:50:42 -03:00
|
|
|
mock_keymap.compile_keymap.assert_called()
|
2024-05-05 16:32:23 -03:00
|
|
|
self.assertEqual(eq.events[0].evt, "key")
|
|
|
|
self.assertEqual(eq.events[0].data, "b")
|
|
|
|
|
|
|
|
@patch("_pyrepl.unix_eventqueue.keymap")
|
|
|
|
def test_push_without_key_in_keymap(self, mock_keymap):
|
|
|
|
mock_keymap.compile_keymap.return_value = {"a": "b"}
|
2024-05-06 06:50:42 -03:00
|
|
|
eq = EventQueue(self.file.fileno(), "utf-8")
|
2024-05-05 16:32:23 -03:00
|
|
|
eq.keymap = {b"c": "d"}
|
|
|
|
eq.push("a")
|
2024-05-06 06:50:42 -03:00
|
|
|
mock_keymap.compile_keymap.assert_called()
|
2024-05-05 16:32:23 -03:00
|
|
|
self.assertEqual(eq.events[0].evt, "key")
|
|
|
|
self.assertEqual(eq.events[0].data, "a")
|
|
|
|
|
|
|
|
@patch("_pyrepl.unix_eventqueue.keymap")
|
|
|
|
def test_push_with_keymap_in_keymap(self, mock_keymap):
|
|
|
|
mock_keymap.compile_keymap.return_value = {"a": "b"}
|
2024-05-06 06:50:42 -03:00
|
|
|
eq = EventQueue(self.file.fileno(), "utf-8")
|
2024-05-05 16:32:23 -03:00
|
|
|
eq.keymap = {b"a": {b"b": "c"}}
|
|
|
|
eq.push("a")
|
2024-05-06 06:50:42 -03:00
|
|
|
mock_keymap.compile_keymap.assert_called()
|
2024-05-05 16:32:23 -03:00
|
|
|
self.assertTrue(eq.empty())
|
|
|
|
eq.push("b")
|
|
|
|
self.assertEqual(eq.events[0].evt, "key")
|
|
|
|
self.assertEqual(eq.events[0].data, "c")
|
|
|
|
eq.push("d")
|
|
|
|
self.assertEqual(eq.events[1].evt, "key")
|
|
|
|
self.assertEqual(eq.events[1].data, "d")
|
|
|
|
|
|
|
|
@patch("_pyrepl.unix_eventqueue.keymap")
|
|
|
|
def test_push_with_keymap_in_keymap_and_escape(self, mock_keymap):
|
|
|
|
mock_keymap.compile_keymap.return_value = {"a": "b"}
|
2024-05-06 06:50:42 -03:00
|
|
|
eq = EventQueue(self.file.fileno(), "utf-8")
|
2024-05-05 16:32:23 -03:00
|
|
|
eq.keymap = {b"a": {b"b": "c"}}
|
|
|
|
eq.push("a")
|
2024-05-06 06:50:42 -03:00
|
|
|
mock_keymap.compile_keymap.assert_called()
|
2024-05-05 16:32:23 -03:00
|
|
|
self.assertTrue(eq.empty())
|
|
|
|
eq.flush_buf()
|
|
|
|
eq.push("\033")
|
|
|
|
self.assertEqual(eq.events[0].evt, "key")
|
|
|
|
self.assertEqual(eq.events[0].data, "\033")
|
|
|
|
eq.push("b")
|
|
|
|
self.assertEqual(eq.events[1].evt, "key")
|
|
|
|
self.assertEqual(eq.events[1].data, "b")
|
|
|
|
|
|
|
|
def test_push_special_key(self):
|
2024-05-06 06:50:42 -03:00
|
|
|
eq = EventQueue(self.file.fileno(), "utf-8")
|
2024-05-05 16:32:23 -03:00
|
|
|
eq.keymap = {}
|
|
|
|
eq.push("\x1b")
|
|
|
|
eq.push("[")
|
|
|
|
eq.push("A")
|
|
|
|
self.assertEqual(eq.events[0].evt, "key")
|
|
|
|
self.assertEqual(eq.events[0].data, "\x1b")
|
|
|
|
|
|
|
|
def test_push_unrecognized_escape_sequence(self):
|
2024-05-06 06:50:42 -03:00
|
|
|
eq = EventQueue(self.file.fileno(), "utf-8")
|
2024-05-05 16:32:23 -03:00
|
|
|
eq.keymap = {}
|
|
|
|
eq.push("\x1b")
|
|
|
|
eq.push("[")
|
|
|
|
eq.push("Z")
|
|
|
|
self.assertEqual(len(eq.events), 3)
|
|
|
|
self.assertEqual(eq.events[0].evt, "key")
|
|
|
|
self.assertEqual(eq.events[0].data, "\x1b")
|
|
|
|
self.assertEqual(eq.events[1].evt, "key")
|
|
|
|
self.assertEqual(eq.events[1].data, "[")
|
|
|
|
self.assertEqual(eq.events[2].evt, "key")
|
|
|
|
self.assertEqual(eq.events[2].data, "Z")
|
|
|
|
|
|
|
|
|
|
|
|
class TestPasteEvent(TestCase):
|
|
|
|
def prepare_reader(self, events):
|
|
|
|
console = FakeConsole(events)
|
|
|
|
config = ReadlineConfig(readline_completer=None)
|
|
|
|
reader = ReadlineAlikeReader(console=console, config=config)
|
|
|
|
return reader
|
|
|
|
|
|
|
|
def test_paste(self):
|
|
|
|
# fmt: off
|
|
|
|
code = (
|
|
|
|
'def a():\n'
|
|
|
|
' for x in range(10):\n'
|
|
|
|
' if x%2:\n'
|
|
|
|
' print(x)\n'
|
|
|
|
' else:\n'
|
|
|
|
' pass\n'
|
|
|
|
)
|
|
|
|
# fmt: on
|
|
|
|
|
|
|
|
events = itertools.chain(
|
|
|
|
[
|
|
|
|
Event(evt="key", data="f3", raw=bytearray(b"\x1bOR")),
|
|
|
|
],
|
|
|
|
code_to_events(code),
|
|
|
|
[
|
|
|
|
Event(evt="key", data="f3", raw=bytearray(b"\x1bOR")),
|
|
|
|
],
|
|
|
|
code_to_events("\n"),
|
|
|
|
)
|
|
|
|
reader = self.prepare_reader(events)
|
|
|
|
output = multiline_input(reader)
|
|
|
|
self.assertEqual(output, code)
|
|
|
|
|
|
|
|
def test_paste_mid_newlines(self):
|
|
|
|
# fmt: off
|
|
|
|
code = (
|
|
|
|
'def f():\n'
|
|
|
|
' x = y\n'
|
|
|
|
' \n'
|
|
|
|
' y = z\n'
|
|
|
|
)
|
|
|
|
# fmt: on
|
|
|
|
|
|
|
|
events = itertools.chain(
|
|
|
|
[
|
|
|
|
Event(evt="key", data="f3", raw=bytearray(b"\x1bOR")),
|
|
|
|
],
|
|
|
|
code_to_events(code),
|
|
|
|
[
|
|
|
|
Event(evt="key", data="f3", raw=bytearray(b"\x1bOR")),
|
|
|
|
],
|
|
|
|
code_to_events("\n"),
|
|
|
|
)
|
|
|
|
reader = self.prepare_reader(events)
|
|
|
|
output = multiline_input(reader)
|
|
|
|
self.assertEqual(output, code)
|
|
|
|
|
|
|
|
def test_paste_mid_newlines_not_in_paste_mode(self):
|
|
|
|
# fmt: off
|
|
|
|
code = (
|
|
|
|
'def f():\n'
|
|
|
|
' x = y\n'
|
|
|
|
' \n'
|
|
|
|
' y = z\n\n'
|
|
|
|
)
|
|
|
|
|
|
|
|
expected = (
|
|
|
|
'def f():\n'
|
|
|
|
' x = y\n'
|
|
|
|
' '
|
|
|
|
)
|
|
|
|
# fmt: on
|
|
|
|
|
|
|
|
events = code_to_events(code)
|
|
|
|
reader = self.prepare_reader(events)
|
|
|
|
output = multiline_input(reader)
|
|
|
|
self.assertEqual(output, expected)
|
|
|
|
|
|
|
|
def test_paste_not_in_paste_mode(self):
|
|
|
|
# fmt: off
|
|
|
|
input_code = (
|
|
|
|
'def a():\n'
|
|
|
|
' for x in range(10):\n'
|
|
|
|
' if x%2:\n'
|
|
|
|
' print(x)\n'
|
|
|
|
' else:\n'
|
|
|
|
' pass\n\n'
|
|
|
|
)
|
|
|
|
|
|
|
|
output_code = (
|
|
|
|
'def a():\n'
|
|
|
|
' for x in range(10):\n'
|
|
|
|
' if x%2:\n'
|
|
|
|
' print(x)\n'
|
|
|
|
' else:'
|
|
|
|
)
|
|
|
|
# fmt: on
|
|
|
|
|
|
|
|
events = code_to_events(input_code)
|
|
|
|
reader = self.prepare_reader(events)
|
|
|
|
output = multiline_input(reader)
|
|
|
|
self.assertEqual(output, output_code)
|
|
|
|
|
|
|
|
|
|
|
|
class TestReader(TestCase):
|
|
|
|
def assert_screen_equals(self, reader, expected):
|
|
|
|
actual = reader.calc_screen()
|
|
|
|
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")
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
unittest.main()
|