Todo: 集成多平台 解决因SaiNiu线程抢占资源问题 本地提交测试环境打包 和 正式打包脚本与正式环境打包bat 提交Python32环境包 改进多日志文件生成情况修改打包日志细节

This commit is contained in:
2025-09-18 15:52:03 +08:00
parent 8b9fc925fa
commit 7cfc0c22b7
7608 changed files with 2424791 additions and 25 deletions

View File

@@ -0,0 +1,15 @@
import os
import sys
from test.support import requires, load_package_tests
from test.support.import_helper import import_module
if sys.platform != "win32":
# On non-Windows platforms, testing pyrepl 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")
def load_tests(*args):
return load_package_tests(os.path.dirname(__file__), *args)

View File

@@ -0,0 +1,4 @@
import unittest
from test.test_pyrepl import load_tests
unittest.main()

View File

@@ -0,0 +1,175 @@
import os
from code import InteractiveConsole
from functools import partial
from typing import Iterable
from unittest.mock import MagicMock
from _pyrepl.console import Console, Event
from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig
from _pyrepl.simple_interact import _strip_final_indent
from _pyrepl.utils import unbracket, ANSI_ESCAPE_SEQUENCE
class ScreenEqualMixin:
def assert_screen_equal(
self, reader: ReadlineAlikeReader, expected: str, clean: bool = False
):
actual = clean_screen(reader) if clean else reader.screen
expected = expected.split("\n")
self.assertListEqual(actual, expected)
def multiline_input(reader: ReadlineAlikeReader, namespace: dict | None = 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 more_lines(text: str, namespace: dict | None = None):
if namespace is None:
namespace = {}
src = _strip_final_indent(text)
console = InteractiveConsole(namespace, filename="<stdin>")
try:
code = console.compile(src, "<stdin>", "single")
except (OverflowError, SyntaxError, ValueError):
return False
else:
return code is None
def code_to_events(code: str):
for c in code:
yield Event(evt="key", data=c, raw=bytearray(c.encode("utf-8")))
def clean_screen(reader: ReadlineAlikeReader) -> list[str]:
"""Cleans color and console characters out of a screen output.
This is useful for screen testing, it increases the test readability since
it strips out all the unreadable side of the screen.
"""
output = []
for line in reader.screen:
line = unbracket(line, including_content=True)
line = ANSI_ESCAPE_SEQUENCE.sub("", line)
for prefix in (reader.ps1, reader.ps2, reader.ps3, reader.ps4):
if line.startswith(prefix):
line = line[len(prefix):]
break
output.append(line)
return output
def prepare_reader(console: Console, **kwargs):
config = ReadlineConfig(readline_completer=kwargs.pop("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 prepare_console(events: Iterable[Event], **kwargs) -> MagicMock | Console:
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 handle_all_events(
events, prepare_console=prepare_console, prepare_reader=prepare_reader
):
console = prepare_console(events)
reader = prepare_reader(console)
try:
while True:
reader.handle1()
except StopIteration:
pass
except KeyboardInterrupt:
pass
return reader, console
handle_events_narrow_console = partial(
handle_all_events,
prepare_console=partial(prepare_console, width=10),
)
reader_no_colors = partial(prepare_reader, can_colorize=False)
reader_force_colors = partial(prepare_reader, can_colorize=True)
class FakeConsole(Console):
def __init__(self, events, encoding="utf-8") -> None:
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, timeout: float | None = None) -> bool:
return True
def repaint(self) -> None:
pass

View File

@@ -0,0 +1,192 @@
import tempfile
import unittest
import sys
from unittest.mock import patch
from test import support
try:
from _pyrepl.console import Event
from _pyrepl import base_eventqueue
except ImportError:
pass
try:
from _pyrepl import unix_eventqueue
except ImportError:
pass
try:
from _pyrepl import windows_eventqueue
except ImportError:
pass
class EventQueueTestBase:
"""OS-independent mixin"""
def make_eventqueue(self) -> base_eventqueue.BaseEventQueue:
raise NotImplementedError()
def test_get(self):
eq = self.make_eventqueue()
event = Event("key", "a", b"a")
eq.insert(event)
self.assertEqual(eq.get(), event)
def test_empty(self):
eq = self.make_eventqueue()
self.assertTrue(eq.empty())
eq.insert(Event("key", "a", b"a"))
self.assertFalse(eq.empty())
def test_flush_buf(self):
eq = self.make_eventqueue()
eq.buf.extend(b"test")
self.assertEqual(eq.flush_buf(), b"test")
self.assertEqual(eq.buf, bytearray())
def test_insert(self):
eq = self.make_eventqueue()
event = Event("key", "a", b"a")
eq.insert(event)
self.assertEqual(eq.events[0], event)
@patch("_pyrepl.base_eventqueue.keymap")
def test_push_with_key_in_keymap(self, mock_keymap):
mock_keymap.compile_keymap.return_value = {"a": "b"}
eq = self.make_eventqueue()
eq.keymap = {b"a": "b"}
eq.push(b"a")
mock_keymap.compile_keymap.assert_called()
self.assertEqual(eq.events[0].evt, "key")
self.assertEqual(eq.events[0].data, "b")
@patch("_pyrepl.base_eventqueue.keymap")
def test_push_without_key_in_keymap(self, mock_keymap):
mock_keymap.compile_keymap.return_value = {"a": "b"}
eq = self.make_eventqueue()
eq.keymap = {b"c": "d"}
eq.push(b"a")
mock_keymap.compile_keymap.assert_called()
self.assertEqual(eq.events[0].evt, "key")
self.assertEqual(eq.events[0].data, "a")
@patch("_pyrepl.base_eventqueue.keymap")
def test_push_with_keymap_in_keymap(self, mock_keymap):
mock_keymap.compile_keymap.return_value = {"a": "b"}
eq = self.make_eventqueue()
eq.keymap = {b"a": {b"b": "c"}}
eq.push(b"a")
mock_keymap.compile_keymap.assert_called()
self.assertTrue(eq.empty())
eq.push(b"b")
self.assertEqual(eq.events[0].evt, "key")
self.assertEqual(eq.events[0].data, "c")
eq.push(b"d")
self.assertEqual(eq.events[1].evt, "key")
self.assertEqual(eq.events[1].data, "d")
@patch("_pyrepl.base_eventqueue.keymap")
def test_push_with_keymap_in_keymap_and_escape(self, mock_keymap):
mock_keymap.compile_keymap.return_value = {"a": "b"}
eq = self.make_eventqueue()
eq.keymap = {b"a": {b"b": "c"}}
eq.push(b"a")
mock_keymap.compile_keymap.assert_called()
self.assertTrue(eq.empty())
eq.flush_buf()
eq.push(b"\033")
self.assertEqual(eq.events[0].evt, "key")
self.assertEqual(eq.events[0].data, "\033")
eq.push(b"b")
self.assertEqual(eq.events[1].evt, "key")
self.assertEqual(eq.events[1].data, "b")
def test_push_special_key(self):
eq = self.make_eventqueue()
eq.keymap = {}
eq.push(b"\x1b")
eq.push(b"[")
eq.push(b"A")
self.assertEqual(eq.events[0].evt, "key")
self.assertEqual(eq.events[0].data, "\x1b")
def test_push_unrecognized_escape_sequence(self):
eq = self.make_eventqueue()
eq.keymap = {}
eq.push(b"\x1b")
eq.push(b"[")
eq.push(b"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")
def test_push_unicode_character_as_str(self):
eq = self.make_eventqueue()
eq.keymap = {}
with self.assertRaises(AssertionError):
eq.push("ч")
with self.assertRaises(AssertionError):
eq.push("ñ")
def test_push_unicode_character_two_bytes(self):
eq = self.make_eventqueue()
eq.keymap = {}
encoded = "ч".encode(eq.encoding, "replace")
self.assertEqual(len(encoded), 2)
eq.push(encoded[0])
e = eq.get()
self.assertIsNone(e)
eq.push(encoded[1])
e = eq.get()
self.assertEqual(e.evt, "key")
self.assertEqual(e.data, "ч")
def test_push_single_chars_and_unicode_character_as_str(self):
eq = self.make_eventqueue()
eq.keymap = {}
def _event(evt, data, raw=None):
r = raw if raw is not None else data.encode(eq.encoding)
e = Event(evt, data, r)
return e
def _push(keys):
for k in keys:
eq.push(k)
self.assertIsInstance("ñ", str)
# If an exception happens during push, the existing events must be
# preserved and we can continue to push.
_push(b"b")
with self.assertRaises(AssertionError):
_push("ñ")
_push(b"a")
self.assertEqual(eq.get(), _event("key", "b"))
self.assertEqual(eq.get(), _event("key", "a"))
@unittest.skipIf(support.MS_WINDOWS, "No Unix event queue on Windows")
class TestUnixEventQueue(EventQueueTestBase, unittest.TestCase):
def setUp(self):
self.enterContext(patch("_pyrepl.curses.tigetstr", lambda x: b""))
self.file = tempfile.TemporaryFile()
def tearDown(self) -> None:
self.file.close()
def make_eventqueue(self) -> base_eventqueue.BaseEventQueue:
return unix_eventqueue.EventQueue(self.file.fileno(), "utf-8")
@unittest.skipUnless(support.MS_WINDOWS, "No Windows event queue on Unix")
class TestWindowsEventQueue(EventQueueTestBase, unittest.TestCase):
def make_eventqueue(self) -> base_eventqueue.BaseEventQueue:
return windows_eventqueue.EventQueue("utf-8")

View File

@@ -0,0 +1,102 @@
import unittest
from _pyrepl.console import Event
from _pyrepl.input import KeymapTranslator
class KeymapTranslatorTests(unittest.TestCase):
def test_push_single_key(self):
keymap = [("a", "command_a")]
translator = KeymapTranslator(keymap)
evt = Event("key", "a")
translator.push(evt)
result = translator.get()
self.assertEqual(result, ("command_a", ["a"]))
def test_push_multiple_keys(self):
keymap = [("ab", "command_ab")]
translator = KeymapTranslator(keymap)
evt1 = Event("key", "a")
evt2 = Event("key", "b")
translator.push(evt1)
translator.push(evt2)
result = translator.get()
self.assertEqual(result, ("command_ab", ["a", "b"]))
def test_push_invalid_key(self):
keymap = [("a", "command_a")]
translator = KeymapTranslator(keymap)
evt = Event("key", "b")
translator.push(evt)
result = translator.get()
self.assertEqual(result, (None, ["b"]))
def test_push_invalid_key_with_stack(self):
keymap = [("ab", "command_ab")]
translator = KeymapTranslator(keymap)
evt1 = Event("key", "a")
evt2 = Event("key", "c")
translator.push(evt1)
translator.push(evt2)
result = translator.get()
self.assertEqual(result, (None, ["a", "c"]))
def test_push_character_key(self):
keymap = [("a", "command_a")]
translator = KeymapTranslator(keymap)
evt = Event("key", "a")
translator.push(evt)
result = translator.get()
self.assertEqual(result, ("command_a", ["a"]))
def test_push_character_key_with_stack(self):
keymap = [("ab", "command_ab")]
translator = KeymapTranslator(keymap)
evt1 = Event("key", "a")
evt2 = Event("key", "b")
evt3 = Event("key", "c")
translator.push(evt1)
translator.push(evt2)
translator.push(evt3)
result = translator.get()
self.assertEqual(result, ("command_ab", ["a", "b"]))
def test_push_transition_key(self):
keymap = [("a", {"b": "command_ab"})]
translator = KeymapTranslator(keymap)
evt1 = Event("key", "a")
evt2 = Event("key", "b")
translator.push(evt1)
translator.push(evt2)
result = translator.get()
self.assertEqual(result, ("command_ab", ["a", "b"]))
def test_push_transition_key_interrupted(self):
keymap = [("a", {"b": "command_ab"})]
translator = KeymapTranslator(keymap)
evt1 = Event("key", "a")
evt2 = Event("key", "c")
evt3 = Event("key", "b")
translator.push(evt1)
translator.push(evt2)
translator.push(evt3)
result = translator.get()
self.assertEqual(result, (None, ["a", "c"]))
def test_push_invalid_key_with_unicode_category(self):
keymap = [("a", "command_a")]
translator = KeymapTranslator(keymap)
evt = Event("key", "\u0003") # Control character
translator.push(evt)
result = translator.get()
self.assertEqual(result, (None, ["\u0003"]))
def test_empty(self):
keymap = [("a", "command_a")]
translator = KeymapTranslator(keymap)
self.assertTrue(translator.empty())
evt = Event("key", "a")
translator.push(evt)
self.assertFalse(translator.empty())
translator.get()
self.assertTrue(translator.empty())

View File

@@ -0,0 +1,275 @@
import contextlib
import io
import unittest
from unittest.mock import patch
from textwrap import dedent
from test.support import force_not_colorized
from _pyrepl.console import InteractiveColoredConsole
from _pyrepl.simple_interact import _more_lines
class TestSimpleInteract(unittest.TestCase):
def test_multiple_statements(self):
namespace = {}
code = dedent("""\
class A:
def foo(self):
pass
class B:
def bar(self):
pass
a = 1
a
""")
console = InteractiveColoredConsole(namespace, filename="<stdin>")
f = io.StringIO()
with (
patch.object(InteractiveColoredConsole, "showsyntaxerror") as showsyntaxerror,
patch.object(InteractiveColoredConsole, "runsource", wraps=console.runsource) as runsource,
contextlib.redirect_stdout(f),
):
more = console.push(code, filename="<stdin>", _symbol="single") # type: ignore[call-arg]
self.assertFalse(more)
showsyntaxerror.assert_not_called()
def test_multiple_statements_output(self):
namespace = {}
code = dedent("""\
b = 1
b
a = 1
a
""")
console = InteractiveColoredConsole(namespace, filename="<stdin>")
f = io.StringIO()
with contextlib.redirect_stdout(f):
more = console.push(code, filename="<stdin>", _symbol="single") # type: ignore[call-arg]
self.assertFalse(more)
self.assertEqual(f.getvalue(), "1\n")
@force_not_colorized
def test_multiple_statements_fail_early(self):
console = InteractiveColoredConsole()
code = dedent("""\
raise Exception('foobar')
print('spam', 'eggs', sep='&')
""")
f = io.StringIO()
with contextlib.redirect_stderr(f):
console.runsource(code)
self.assertIn('Exception: foobar', f.getvalue())
self.assertNotIn('spam&eggs', f.getvalue())
def test_empty(self):
namespace = {}
code = ""
console = InteractiveColoredConsole(namespace, filename="<stdin>")
f = io.StringIO()
with contextlib.redirect_stdout(f):
more = console.push(code, filename="<stdin>", _symbol="single") # type: ignore[call-arg]
self.assertFalse(more)
self.assertEqual(f.getvalue(), "")
def test_runsource_compiles_and_runs_code(self):
console = InteractiveColoredConsole()
source = "print('Hello, world!')"
with patch.object(console, "runcode") as mock_runcode:
console.runsource(source)
mock_runcode.assert_called_once()
def test_runsource_returns_false_for_successful_compilation(self):
console = InteractiveColoredConsole()
source = "print('Hello, world!')"
f = io.StringIO()
with contextlib.redirect_stdout(f):
result = console.runsource(source)
self.assertFalse(result)
@force_not_colorized
def test_runsource_returns_false_for_failed_compilation(self):
console = InteractiveColoredConsole()
source = "print('Hello, world!'"
f = io.StringIO()
with contextlib.redirect_stderr(f):
result = console.runsource(source)
self.assertFalse(result)
self.assertIn('SyntaxError', f.getvalue())
@force_not_colorized
def test_runsource_show_syntax_error_location(self):
console = InteractiveColoredConsole()
source = "def f(x, x): ..."
f = io.StringIO()
with contextlib.redirect_stderr(f):
result = console.runsource(source)
self.assertFalse(result)
r = """
def f(x, x): ...
^
SyntaxError: duplicate argument 'x' in function definition"""
self.assertIn(r, f.getvalue())
def test_runsource_shows_syntax_error_for_failed_compilation(self):
console = InteractiveColoredConsole()
source = "print('Hello, world!'"
with patch.object(console, "showsyntaxerror") as mock_showsyntaxerror:
console.runsource(source)
mock_showsyntaxerror.assert_called_once()
source = dedent("""\
match 1:
case {0: _, 0j: _}:
pass
""")
with patch.object(console, "showsyntaxerror") as mock_showsyntaxerror:
console.runsource(source)
mock_showsyntaxerror.assert_called_once()
def test_runsource_survives_null_bytes(self):
console = InteractiveColoredConsole()
source = "\x00\n"
f = io.StringIO()
with contextlib.redirect_stdout(f), contextlib.redirect_stderr(f):
result = console.runsource(source)
self.assertFalse(result)
self.assertIn("source code string cannot contain null bytes", f.getvalue())
def test_no_active_future(self):
console = InteractiveColoredConsole()
source = dedent("""\
x: int = 1
print(__annotations__)
""")
f = io.StringIO()
with contextlib.redirect_stdout(f):
result = console.runsource(source)
self.assertFalse(result)
self.assertEqual(f.getvalue(), "{'x': <class 'int'>}\n")
def test_future_annotations(self):
console = InteractiveColoredConsole()
source = dedent("""\
from __future__ import annotations
def g(x: int): ...
print(g.__annotations__)
""")
f = io.StringIO()
with contextlib.redirect_stdout(f):
result = console.runsource(source)
self.assertFalse(result)
self.assertEqual(f.getvalue(), "{'x': 'int'}\n")
def test_future_barry_as_flufl(self):
console = InteractiveColoredConsole()
f = io.StringIO()
with contextlib.redirect_stdout(f):
result = console.runsource("from __future__ import barry_as_FLUFL\n")
result = console.runsource("""print("black" <> 'blue')\n""")
self.assertFalse(result)
self.assertEqual(f.getvalue(), "True\n")
class TestMoreLines(unittest.TestCase):
def test_invalid_syntax_single_line(self):
namespace = {}
code = "if foo"
console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertFalse(_more_lines(console, code))
def test_empty_line(self):
namespace = {}
code = ""
console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertFalse(_more_lines(console, code))
def test_valid_single_statement(self):
namespace = {}
code = "foo = 1"
console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertFalse(_more_lines(console, code))
def test_multiline_single_assignment(self):
namespace = {}
code = dedent("""\
foo = [
1,
2,
3,
]""")
console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertFalse(_more_lines(console, code))
def test_multiline_single_block(self):
namespace = {}
code = dedent("""\
def foo():
'''docs'''
return 1""")
console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertTrue(_more_lines(console, code))
def test_multiple_statements_single_line(self):
namespace = {}
code = "foo = 1;bar = 2"
console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertFalse(_more_lines(console, code))
def test_multiple_statements(self):
namespace = {}
code = dedent("""\
import time
foo = 1""")
console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertTrue(_more_lines(console, code))
def test_multiple_blocks(self):
namespace = {}
code = dedent("""\
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float""")
console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertTrue(_more_lines(console, code))
def test_multiple_blocks_empty_newline(self):
namespace = {}
code = dedent("""\
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
""")
console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertFalse(_more_lines(console, code))
def test_multiple_blocks_indented_newline(self):
namespace = {}
code = (
"from dataclasses import dataclass\n"
"\n"
"@dataclass\n"
"class Point:\n"
" x: float\n"
" y: float\n"
" "
)
console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertFalse(_more_lines(console, code))
def test_incomplete_statement(self):
namespace = {}
code = "if foo:"
console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertTrue(_more_lines(console, code))

View File

@@ -0,0 +1,120 @@
import string
import unittest
from _pyrepl.keymap import _keynames, _escapes, parse_keys, compile_keymap, KeySpecError
class TestParseKeys(unittest.TestCase):
def test_single_character(self):
"""Ensure that single ascii characters or single digits are parsed as single characters."""
test_cases = [(key, [key]) for key in string.ascii_letters + string.digits]
for test_key, expected_keys in test_cases:
with self.subTest(f"{test_key} should be parsed as {expected_keys}"):
self.assertEqual(parse_keys(test_key), expected_keys)
def test_keynames(self):
"""Ensure that keynames are parsed to their corresponding mapping.
A keyname is expected to be of the following form: \\<keyname> such as \\<left>
which would get parsed as "left".
"""
test_cases = [(f"\\<{keyname}>", [parsed_keyname]) for keyname, parsed_keyname in _keynames.items()]
for test_key, expected_keys in test_cases:
with self.subTest(f"{test_key} should be parsed as {expected_keys}"):
self.assertEqual(parse_keys(test_key), expected_keys)
def test_escape_sequences(self):
"""Ensure that escaping sequences are parsed to their corresponding mapping."""
test_cases = [(f"\\{escape}", [parsed_escape]) for escape, parsed_escape in _escapes.items()]
for test_key, expected_keys in test_cases:
with self.subTest(f"{test_key} should be parsed as {expected_keys}"):
self.assertEqual(parse_keys(test_key), expected_keys)
def test_control_sequences(self):
"""Ensure that supported control sequences are parsed successfully."""
keys = ["@", "[", "]", "\\", "^", "_", "\\<space>", "\\<delete>"]
keys.extend(string.ascii_letters)
test_cases = [(f"\\C-{key}", chr(ord(key) & 0x1F)) for key in []]
for test_key, expected_keys in test_cases:
with self.subTest(f"{test_key} should be parsed as {expected_keys}"):
self.assertEqual(parse_keys(test_key), expected_keys)
def test_meta_sequences(self):
self.assertEqual(parse_keys("\\M-a"), ["\033", "a"])
self.assertEqual(parse_keys("\\M-b"), ["\033", "b"])
self.assertEqual(parse_keys("\\M-c"), ["\033", "c"])
def test_combinations(self):
self.assertEqual(parse_keys("\\C-a\\n\\<up>"), ["\x01", "\n", "up"])
self.assertEqual(parse_keys("\\M-a\\t\\<down>"), ["\033", "a", "\t", "down"])
def test_keyspec_errors(self):
cases = [
("\\Ca", "\\C must be followed by `-'"),
("\\ca", "\\C must be followed by `-'"),
("\\C-\\C-", "doubled \\C-"),
("\\Ma", "\\M must be followed by `-'"),
("\\ma", "\\M must be followed by `-'"),
("\\M-\\M-", "doubled \\M-"),
("\\<left", "unterminated \\<"),
("\\<unsupported>", "unrecognised keyname"),
("\\", "unknown backslash escape"),
("\\C-\\<backspace>", "\\C- followed by invalid key")
]
for test_keys, expected_err in cases:
with self.subTest(f"{test_keys} should give error {expected_err}"):
with self.assertRaises(KeySpecError) as e:
parse_keys(test_keys)
self.assertIn(expected_err, str(e.exception))
def test_index_errors(self):
test_cases = ["\\", "\\C", "\\C-\\C"]
for test_keys in test_cases:
with self.assertRaises(IndexError):
parse_keys(test_keys)
class TestCompileKeymap(unittest.TestCase):
def test_empty_keymap(self):
keymap = {}
result = compile_keymap(keymap)
self.assertEqual(result, {})
def test_single_keymap(self):
keymap = {b"a": "action"}
result = compile_keymap(keymap)
self.assertEqual(result, {b"a": "action"})
def test_nested_keymap(self):
keymap = {b"a": {b"b": "action"}}
result = compile_keymap(keymap)
self.assertEqual(result, {b"a": {b"b": "action"}})
def test_empty_value(self):
keymap = {b"a": {b"": "action"}}
result = compile_keymap(keymap)
self.assertEqual(result, {b"a": {b"": "action"}})
def test_multiple_empty_values(self):
keymap = {b"a": {b"": "action1", b"b": "action2"}}
result = compile_keymap(keymap)
self.assertEqual(result, {b"a": {b"": "action1", b"b": "action2"}})
def test_multiple_keymaps(self):
keymap = {b"a": {b"b": "action1", b"c": "action2"}}
result = compile_keymap(keymap)
self.assertEqual(result, {b"a": {b"b": "action1", b"c": "action2"}})
def test_nested_multiple_keymaps(self):
keymap = {b"a": {b"b": {b"c": "action"}}}
result = compile_keymap(keymap)
self.assertEqual(result, {b"a": {b"b": {b"c": "action"}}})
def test_clashing_definitions(self):
km = {b'a': 'c', b'a' + b'b': 'd'}
with self.assertRaises(KeySpecError):
compile_keymap(km)
def test_non_bytes_key(self):
with self.assertRaises(TypeError):
compile_keymap({123: 'a'})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,357 @@
import itertools
import functools
import rlcompleter
from unittest import TestCase
from unittest.mock import MagicMock
from .support import handle_all_events, handle_events_narrow_console
from .support import ScreenEqualMixin, code_to_events
from .support import prepare_reader, prepare_console
from _pyrepl.console import Event
from _pyrepl.reader import Reader
class TestReader(ScreenEqualMixin, TestCase):
def test_calc_screen_wrap_simple(self):
events = code_to_events(10 * "a")
reader, _ = handle_events_narrow_console(events)
self.assert_screen_equal(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_equal(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_equal(reader, f"{9*"a"}\\\n{9*"a"}\\\naa")
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_equal(
reader,
(
">>> if so\\\n"
"me_condit\\\n"
"ion:\n"
"... s\\\n"
"ome_funct\\\n"
"ion()"
)
)
# fmt: on
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_equal(
reader,
(
"def f():\n"
f" {7*"a"}\\\n"
"a\n"
f" {3*""}\\\n"
"樂樂"
),
clean=True,
)
# 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_equal(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_equal(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_equal(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_control_characters(self):
code = 'flag = "🏳️‍🌈"'
events = code_to_events(code)
reader, _ = handle_all_events(events)
self.assert_screen_equal(reader, 'flag = "🏳️\\u200d🌈"', clean=True)
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_equal(reader, "")
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"
" \n"
" \n"
" a = 1\n"
" \n"
" " # HistoricalReader will trim trailing whitespace
)
self.assert_screen_equal(reader, expected, clean=True)
self.assertTrue(reader.finished)
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)
def test_keyboard_interrupt_clears_screen(self):
namespace = {"itertools": itertools}
code = "import itertools\nitertools."
events = itertools.chain(
code_to_events(code),
[
# Two tabs for completion
Event(evt="key", data="\t", raw=bytearray(b"\t")),
Event(evt="key", data="\t", raw=bytearray(b"\t")),
Event(evt="key", data="\x03", raw=bytearray(b"\x03")), # Ctrl-C
],
)
console = prepare_console(events)
reader = prepare_reader(
console,
readline_completer=rlcompleter.Completer(namespace).complete,
)
try:
# we're not using handle_all_events() here to be able to
# follow the KeyboardInterrupt sequence of events. Normally this
# happens in simple_interact.run_multiline_interactive_console.
while True:
reader.handle1()
except KeyboardInterrupt:
# at this point the completions are still visible
self.assertTrue(len(reader.screen) > 2)
reader.refresh()
# after the refresh, they are gone
self.assertEqual(len(reader.screen), 2)
self.assert_screen_equal(reader, code, clean=True)
else:
self.fail("KeyboardInterrupt not raised.")
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)
def test_completions_updated_on_key_press(self):
namespace = {"itertools": itertools}
code = "itertools."
events = itertools.chain(
code_to_events(code),
[
# Two tabs for completion
Event(evt="key", data="\t", raw=bytearray(b"\t")),
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], f"{code}a")
self.assertEqual(actual[1].rstrip(), "itertools.accumulate(")
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_equal(reader, f"{code}a")
def test_pos2xy_with_no_columns(self):
console = prepare_console([])
reader = prepare_reader(console)
# Simulate a resize to 0 columns
reader.screeninfo = []
self.assertEqual(reader.pos2xy(), (0, 0))
def test_setpos_from_xy_for_non_printing_char(self):
code = "# non \u200c printing character"
events = code_to_events(code)
reader, _ = handle_all_events(events)
reader.setpos_from_xy(8, 0)
self.assertEqual(reader.pos, 7)

View File

@@ -0,0 +1,329 @@
import itertools
import os
import sys
import unittest
from functools import partial
from test.support import os_helper
from unittest import TestCase
from unittest.mock import MagicMock, call, patch, ANY
from .support import handle_all_events, code_to_events, reader_no_colors
try:
from _pyrepl.console import Event
from _pyrepl.unix_console import UnixConsole
except ImportError:
pass
def unix_console(events, **kwargs):
console = UnixConsole()
console.get_event = MagicMock(side_effect=events)
height = kwargs.get("height", 25)
width = kwargs.get("width", 80)
console.getheightwidth = MagicMock(side_effect=lambda: (height, width))
console.prepare()
for key, val in kwargs.items():
setattr(console, key, val)
return console
handle_events_unix_console = partial(
handle_all_events,
prepare_console=partial(unix_console),
)
handle_events_narrow_unix_console = partial(
handle_all_events,
prepare_console=partial(unix_console, width=5),
)
handle_events_short_unix_console = partial(
handle_all_events,
prepare_console=partial(unix_console, height=1),
)
handle_events_unix_console_height_3 = partial(
handle_all_events, prepare_console=partial(unix_console, height=3)
)
TERM_CAPABILITIES = {
"bel": b"\x07",
"civis": b"\x1b[?25l",
"clear": b"\x1b[H\x1b[2J",
"cnorm": b"\x1b[?12l\x1b[?25h",
"cub": b"\x1b[%p1%dD",
"cub1": b"\x08",
"cud": b"\x1b[%p1%dB",
"cud1": b"\n",
"cuf": b"\x1b[%p1%dC",
"cuf1": b"\x1b[C",
"cup": b"\x1b[%i%p1%d;%p2%dH",
"cuu": b"\x1b[%p1%dA",
"cuu1": b"\x1b[A",
"dch1": b"\x1b[P",
"dch": b"\x1b[%p1%dP",
"el": b"\x1b[K",
"hpa": b"\x1b[%i%p1%dG",
"ich": b"\x1b[%p1%d@",
"ich1": None,
"ind": b"\n",
"pad": None,
"ri": b"\x1bM",
"rmkx": b"\x1b[?1l\x1b>",
"smkx": b"\x1b[?1h\x1b=",
}
@unittest.skipIf(sys.platform == "win32", "No Unix event queue on Windows")
@patch("_pyrepl.curses.tigetstr", lambda s: TERM_CAPABILITIES.get(s))
@patch(
"_pyrepl.curses.tparm",
lambda s, *args: s + b":" + b",".join(str(i).encode() for i in args),
)
@patch("_pyrepl.curses.setupterm", lambda a, b: None)
@patch(
"termios.tcgetattr",
lambda _: [
27394,
3,
19200,
536872399,
38400,
38400,
[
b"\x04",
b"\xff",
b"\xff",
b"\x7f",
b"\x17",
b"\x15",
b"\x12",
b"\x00",
b"\x03",
b"\x1c",
b"\x1a",
b"\x19",
b"\x11",
b"\x13",
b"\x16",
b"\x0f",
b"\x01",
b"\x00",
b"\x14",
b"\x00",
],
],
)
@patch("termios.tcsetattr", lambda a, b, c: None)
@patch("os.write")
class TestConsole(TestCase):
def test_simple_addition(self, _os_write):
code = "12+34"
events = code_to_events(code)
_, con = handle_events_unix_console(events)
_os_write.assert_any_call(ANY, b"1")
_os_write.assert_any_call(ANY, b"2")
_os_write.assert_any_call(ANY, b"+")
_os_write.assert_any_call(ANY, b"3")
_os_write.assert_any_call(ANY, b"4")
con.restore()
def test_wrap(self, _os_write):
code = "12+34"
events = code_to_events(code)
_, con = handle_events_narrow_unix_console(events)
_os_write.assert_any_call(ANY, b"1")
_os_write.assert_any_call(ANY, b"2")
_os_write.assert_any_call(ANY, b"+")
_os_write.assert_any_call(ANY, b"3")
_os_write.assert_any_call(ANY, b"\\")
_os_write.assert_any_call(ANY, b"\n")
_os_write.assert_any_call(ANY, b"4")
con.restore()
def test_cursor_left(self, _os_write):
code = "1"
events = itertools.chain(
code_to_events(code),
[Event(evt="key", data="left", raw=bytearray(b"\x1bOD"))],
)
_, con = handle_events_unix_console(events)
_os_write.assert_any_call(ANY, TERM_CAPABILITIES["cub"] + b":1")
con.restore()
def test_cursor_left_right(self, _os_write):
code = "1"
events = itertools.chain(
code_to_events(code),
[
Event(evt="key", data="left", raw=bytearray(b"\x1bOD")),
Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
],
)
_, con = handle_events_unix_console(events)
_os_write.assert_any_call(ANY, TERM_CAPABILITIES["cub"] + b":1")
_os_write.assert_any_call(ANY, TERM_CAPABILITIES["cuf"] + b":1")
con.restore()
def test_cursor_up(self, _os_write):
code = "1\n2+3"
events = itertools.chain(
code_to_events(code),
[Event(evt="key", data="up", raw=bytearray(b"\x1bOA"))],
)
_, con = handle_events_unix_console(events)
_os_write.assert_any_call(ANY, TERM_CAPABILITIES["cuu"] + b":1")
con.restore()
def test_cursor_up_down(self, _os_write):
code = "1\n2+3"
events = itertools.chain(
code_to_events(code),
[
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
],
)
_, con = handle_events_unix_console(events)
_os_write.assert_any_call(ANY, TERM_CAPABILITIES["cuu"] + b":1")
_os_write.assert_any_call(ANY, TERM_CAPABILITIES["cud"] + b":1")
con.restore()
def test_cursor_back_write(self, _os_write):
events = itertools.chain(
code_to_events("1"),
[Event(evt="key", data="left", raw=bytearray(b"\x1bOD"))],
code_to_events("2"),
)
_, con = handle_events_unix_console(events)
_os_write.assert_any_call(ANY, b"1")
_os_write.assert_any_call(ANY, TERM_CAPABILITIES["cub"] + b":1")
_os_write.assert_any_call(ANY, b"2")
con.restore()
def test_multiline_function_move_up_short_terminal(self, _os_write):
# fmt: off
code = (
"def f():\n"
" foo"
)
# fmt: on
events = itertools.chain(
code_to_events(code),
[
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
Event(evt="scroll", data=None),
],
)
_, con = handle_events_short_unix_console(events)
_os_write.assert_any_call(ANY, TERM_CAPABILITIES["ri"] + b":")
con.restore()
def test_multiline_function_move_up_down_short_terminal(self, _os_write):
# fmt: off
code = (
"def f():\n"
" foo"
)
# fmt: on
events = itertools.chain(
code_to_events(code),
[
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
Event(evt="scroll", data=None),
Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
Event(evt="scroll", data=None),
],
)
_, con = handle_events_short_unix_console(events)
_os_write.assert_any_call(ANY, TERM_CAPABILITIES["ri"] + b":")
_os_write.assert_any_call(ANY, TERM_CAPABILITIES["ind"] + b":")
con.restore()
def test_resize_bigger_on_multiline_function(self, _os_write):
# fmt: off
code = (
"def f():\n"
" foo"
)
# fmt: on
events = itertools.chain(code_to_events(code))
reader, console = handle_events_short_unix_console(
events, prepare_reader=reader_no_colors
)
console.height = 2
console.getheightwidth = MagicMock(lambda _: (2, 80))
def same_reader(_):
return reader
def same_console(events):
console.get_event = MagicMock(side_effect=events)
return console
_, con = handle_all_events(
[Event(evt="resize", data=None)],
prepare_reader=same_reader,
prepare_console=same_console,
)
_os_write.assert_has_calls(
[
call(ANY, TERM_CAPABILITIES["ri"] + b":"),
call(ANY, TERM_CAPABILITIES["cup"] + b":0,0"),
call(ANY, b"def f():"),
]
)
console.restore()
con.restore()
def test_resize_smaller_on_multiline_function(self, _os_write):
# fmt: off
code = (
"def f():\n"
" foo"
)
# fmt: on
events = itertools.chain(code_to_events(code))
reader, console = handle_events_unix_console_height_3(events)
console.height = 1
console.getheightwidth = MagicMock(lambda _: (1, 80))
def same_reader(_):
return reader
def same_console(events):
console.get_event = MagicMock(side_effect=events)
return console
_, con = handle_all_events(
[Event(evt="resize", data=None)],
prepare_reader=same_reader,
prepare_console=same_console,
)
_os_write.assert_has_calls(
[
call(ANY, TERM_CAPABILITIES["ind"] + b":"),
call(ANY, TERM_CAPABILITIES["cup"] + b":0,0"),
call(ANY, b" foo"),
]
)
console.restore()
con.restore()
def test_getheightwidth_with_invalid_environ(self, _os_write):
# gh-128636
console = UnixConsole()
with os_helper.EnvironmentVarGuard() as env:
env["LINES"] = ""
self.assertIsInstance(console.getheightwidth(), tuple)
env["COLUMNS"] = ""
self.assertIsInstance(console.getheightwidth(), tuple)
os.environ = []
self.assertIsInstance(console.getheightwidth(), tuple)

View File

@@ -0,0 +1,27 @@
from unittest import TestCase
from _pyrepl.utils import str_width, wlen
class TestUtils(TestCase):
def test_str_width(self):
characters = ['a', '1', '_', '!', '\x1a', '\u263A', '\uffb9']
for c in characters:
self.assertEqual(str_width(c), 1)
characters = [chr(99989), chr(99999)]
for c in characters:
self.assertEqual(str_width(c), 2)
def test_wlen(self):
for c in ['a', 'b', '1', '!', '_']:
self.assertEqual(wlen(c), 1)
self.assertEqual(wlen('\x1a'), 2)
char_east_asian_width_N = chr(3800)
self.assertEqual(wlen(char_east_asian_width_N), 1)
char_east_asian_width_W = chr(4352)
self.assertEqual(wlen(char_east_asian_width_W), 2)
self.assertEqual(wlen('hello'), 5)
self.assertEqual(wlen('hello' + '\x1a'), 7)

View File

@@ -0,0 +1,567 @@
import sys
import unittest
if sys.platform != "win32":
raise unittest.SkipTest("test only relevant on win32")
import itertools
from functools import partial
from typing import Iterable
from unittest import TestCase
from unittest.mock import MagicMock, call
from .support import handle_all_events, code_to_events
try:
from _pyrepl.console import Event, Console
from _pyrepl.windows_console import (
WindowsConsole,
MOVE_LEFT,
MOVE_RIGHT,
MOVE_UP,
MOVE_DOWN,
ERASE_IN_LINE,
)
import _pyrepl.windows_console as wc
except ImportError:
pass
class WindowsConsoleTests(TestCase):
def console(self, events, **kwargs) -> Console:
console = WindowsConsole()
console.get_event = MagicMock(side_effect=events)
console._scroll = MagicMock()
console._hide_cursor = MagicMock()
console._show_cursor = MagicMock()
console._getscrollbacksize = MagicMock(42)
console.out = MagicMock()
height = kwargs.get("height", 25)
width = kwargs.get("width", 80)
console.getheightwidth = MagicMock(side_effect=lambda: (height, width))
console.prepare()
for key, val in kwargs.items():
setattr(console, key, val)
return console
def handle_events(self, events: Iterable[Event], **kwargs):
return handle_all_events(events, partial(self.console, **kwargs))
def handle_events_narrow(self, events):
return self.handle_events(events, width=5)
def handle_events_short(self, events):
return self.handle_events(events, height=1)
def handle_events_height_3(self, events):
return self.handle_events(events, height=3)
def test_simple_addition(self):
code = "12+34"
events = code_to_events(code)
_, con = self.handle_events(events)
con.out.write.assert_any_call(b"1")
con.out.write.assert_any_call(b"2")
con.out.write.assert_any_call(b"+")
con.out.write.assert_any_call(b"3")
con.out.write.assert_any_call(b"4")
con.restore()
def test_wrap(self):
code = "12+34"
events = code_to_events(code)
_, con = self.handle_events_narrow(events)
con.out.write.assert_any_call(b"1")
con.out.write.assert_any_call(b"2")
con.out.write.assert_any_call(b"+")
con.out.write.assert_any_call(b"3")
con.out.write.assert_any_call(b"\\")
con.out.write.assert_any_call(b"\n")
con.out.write.assert_any_call(b"4")
con.restore()
def test_resize_wider(self):
code = "1234567890"
events = code_to_events(code)
reader, console = self.handle_events_narrow(events)
console.height = 20
console.width = 80
console.getheightwidth = MagicMock(lambda _: (20, 80))
def same_reader(_):
return reader
def same_console(events):
console.get_event = MagicMock(side_effect=events)
return console
_, con = handle_all_events(
[Event(evt="resize", data=None)],
prepare_reader=same_reader,
prepare_console=same_console,
)
con.out.write.assert_any_call(self.move_right(2))
con.out.write.assert_any_call(self.move_up(2))
con.out.write.assert_any_call(b"567890")
con.restore()
def test_resize_narrower(self):
code = "1234567890"
events = code_to_events(code)
reader, console = self.handle_events(events)
console.height = 20
console.width = 4
console.getheightwidth = MagicMock(lambda _: (20, 4))
def same_reader(_):
return reader
def same_console(events):
console.get_event = MagicMock(side_effect=events)
return console
_, con = handle_all_events(
[Event(evt="resize", data=None)],
prepare_reader=same_reader,
prepare_console=same_console,
)
con.out.write.assert_any_call(b"456\\")
con.out.write.assert_any_call(b"789\\")
con.restore()
def test_cursor_left(self):
code = "1"
events = itertools.chain(
code_to_events(code),
[Event(evt="key", data="left", raw=bytearray(b"\x1bOD"))],
)
_, con = self.handle_events(events)
con.out.write.assert_any_call(self.move_left())
con.restore()
def test_cursor_left_right(self):
code = "1"
events = itertools.chain(
code_to_events(code),
[
Event(evt="key", data="left", raw=bytearray(b"\x1bOD")),
Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
],
)
_, con = self.handle_events(events)
con.out.write.assert_any_call(self.move_left())
con.out.write.assert_any_call(self.move_right())
con.restore()
def test_cursor_up(self):
code = "1\n2+3"
events = itertools.chain(
code_to_events(code),
[Event(evt="key", data="up", raw=bytearray(b"\x1bOA"))],
)
_, con = self.handle_events(events)
con.out.write.assert_any_call(self.move_up())
con.restore()
def test_cursor_up_down(self):
code = "1\n2+3"
events = itertools.chain(
code_to_events(code),
[
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
],
)
_, con = self.handle_events(events)
con.out.write.assert_any_call(self.move_up())
con.out.write.assert_any_call(self.move_down())
con.restore()
def test_cursor_back_write(self):
events = itertools.chain(
code_to_events("1"),
[Event(evt="key", data="left", raw=bytearray(b"\x1bOD"))],
code_to_events("2"),
)
_, con = self.handle_events(events)
con.out.write.assert_any_call(b"1")
con.out.write.assert_any_call(self.move_left())
con.out.write.assert_any_call(b"21")
con.restore()
def test_multiline_function_move_up_short_terminal(self):
# fmt: off
code = (
"def f():\n"
" foo"
)
# fmt: on
events = itertools.chain(
code_to_events(code),
[
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
Event(evt="scroll", data=None),
],
)
_, con = self.handle_events_short(events)
con.out.write.assert_any_call(self.move_left(5))
con.out.write.assert_any_call(self.move_up())
con.restore()
def test_multiline_function_move_up_down_short_terminal(self):
# fmt: off
code = (
"def f():\n"
" foo"
)
# fmt: on
events = itertools.chain(
code_to_events(code),
[
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
Event(evt="scroll", data=None),
Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
Event(evt="scroll", data=None),
],
)
_, con = self.handle_events_short(events)
con.out.write.assert_any_call(self.move_left(8))
con.out.write.assert_any_call(self.erase_in_line())
con.restore()
def test_resize_bigger_on_multiline_function(self):
# fmt: off
code = (
"def f():\n"
" foo"
)
# fmt: on
events = itertools.chain(code_to_events(code))
reader, console = self.handle_events_short(events)
console.height = 2
console.getheightwidth = MagicMock(lambda _: (2, 80))
def same_reader(_):
return reader
def same_console(events):
console.get_event = MagicMock(side_effect=events)
return console
_, con = handle_all_events(
[Event(evt="resize", data=None)],
prepare_reader=same_reader,
prepare_console=same_console,
)
con.out.write.assert_has_calls(
[
call(self.move_left(5)),
call(self.move_up()),
call(b"def f():"),
call(self.move_left(3)),
call(self.move_down()),
]
)
console.restore()
con.restore()
def test_resize_smaller_on_multiline_function(self):
# fmt: off
code = (
"def f():\n"
" foo"
)
# fmt: on
events = itertools.chain(code_to_events(code))
reader, console = self.handle_events_height_3(events)
console.height = 1
console.getheightwidth = MagicMock(lambda _: (1, 80))
def same_reader(_):
return reader
def same_console(events):
console.get_event = MagicMock(side_effect=events)
return console
_, con = handle_all_events(
[Event(evt="resize", data=None)],
prepare_reader=same_reader,
prepare_console=same_console,
)
con.out.write.assert_has_calls(
[
call(self.move_left(5)),
call(self.move_up()),
call(self.erase_in_line()),
call(b" foo"),
]
)
console.restore()
con.restore()
def move_up(self, lines=1):
return MOVE_UP.format(lines).encode("utf8")
def move_down(self, lines=1):
return MOVE_DOWN.format(lines).encode("utf8")
def move_left(self, cols=1):
return MOVE_LEFT.format(cols).encode("utf8")
def move_right(self, cols=1):
return MOVE_RIGHT.format(cols).encode("utf8")
def erase_in_line(self):
return ERASE_IN_LINE.encode("utf8")
def test_multiline_ctrl_z(self):
# see gh-126332
code = "abcdefghi"
events = itertools.chain(
code_to_events(code),
[
Event(evt="key", data='\x1a', raw=bytearray(b'\x1a')),
Event(evt="key", data='\x1a', raw=bytearray(b'\x1a')),
],
)
reader, con = self.handle_events_narrow(events)
self.assertEqual(reader.cxy, (2, 3))
con.restore()
class WindowsConsoleGetEventTests(TestCase):
# Virtual-Key Codes: https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
VK_BACK = 0x08
VK_RETURN = 0x0D
VK_LEFT = 0x25
VK_7 = 0x37
VK_M = 0x4D
# Used for miscellaneous characters; it can vary by keyboard.
# For the US standard keyboard, the '" key.
# For the German keyboard, the Ä key.
VK_OEM_7 = 0xDE
# State of control keys: https://learn.microsoft.com/en-us/windows/console/key-event-record-str
RIGHT_ALT_PRESSED = 0x0001
RIGHT_CTRL_PRESSED = 0x0004
LEFT_ALT_PRESSED = 0x0002
LEFT_CTRL_PRESSED = 0x0008
ENHANCED_KEY = 0x0100
SHIFT_PRESSED = 0x0010
def get_event(self, input_records, **kwargs) -> Console:
self.console = WindowsConsole(encoding='utf-8')
self.mock = MagicMock(side_effect=input_records)
self.console._read_input = self.mock
self.console._WindowsConsole__vt_support = kwargs.get("vt_support",
False)
event = self.console.get_event(block=False)
return event
def get_input_record(self, unicode_char, vcode=0, control=0):
return wc.INPUT_RECORD(
wc.KEY_EVENT,
wc.ConsoleEvent(KeyEvent=
wc.KeyEvent(
bKeyDown=True,
wRepeatCount=1,
wVirtualKeyCode=vcode,
wVirtualScanCode=0, # not used
uChar=wc.Char(unicode_char),
dwControlKeyState=control
)))
def test_EmptyBuffer(self):
self.assertEqual(self.get_event([None]), None)
self.assertEqual(self.mock.call_count, 1)
def test_WINDOW_BUFFER_SIZE_EVENT(self):
ir = wc.INPUT_RECORD(
wc.WINDOW_BUFFER_SIZE_EVENT,
wc.ConsoleEvent(WindowsBufferSizeEvent=
wc.WindowsBufferSizeEvent(
wc._COORD(0, 0))))
self.assertEqual(self.get_event([ir]), Event("resize", ""))
self.assertEqual(self.mock.call_count, 1)
def test_KEY_EVENT_up_ignored(self):
ir = wc.INPUT_RECORD(
wc.KEY_EVENT,
wc.ConsoleEvent(KeyEvent=
wc.KeyEvent(bKeyDown=False)))
self.assertEqual(self.get_event([ir]), None)
self.assertEqual(self.mock.call_count, 1)
def test_unhandled_events(self):
for event in (wc.FOCUS_EVENT, wc.MENU_EVENT, wc.MOUSE_EVENT):
ir = wc.INPUT_RECORD(
event,
# fake data, nothing is read except bKeyDown
wc.ConsoleEvent(KeyEvent=
wc.KeyEvent(bKeyDown=False)))
self.assertEqual(self.get_event([ir]), None)
self.assertEqual(self.mock.call_count, 1)
def test_enter(self):
ir = self.get_input_record("\r", self.VK_RETURN)
self.assertEqual(self.get_event([ir]), Event("key", "\n"))
self.assertEqual(self.mock.call_count, 1)
def test_backspace(self):
ir = self.get_input_record("\x08", self.VK_BACK)
self.assertEqual(
self.get_event([ir]), Event("key", "backspace"))
self.assertEqual(self.mock.call_count, 1)
def test_m(self):
ir = self.get_input_record("m", self.VK_M)
self.assertEqual(self.get_event([ir]), Event("key", "m"))
self.assertEqual(self.mock.call_count, 1)
def test_M(self):
ir = self.get_input_record("M", self.VK_M, self.SHIFT_PRESSED)
self.assertEqual(self.get_event([ir]), Event("key", "M"))
self.assertEqual(self.mock.call_count, 1)
def test_left(self):
# VK_LEFT is sent as ENHANCED_KEY
ir = self.get_input_record("\x00", self.VK_LEFT, self.ENHANCED_KEY)
self.assertEqual(self.get_event([ir]), Event("key", "left"))
self.assertEqual(self.mock.call_count, 1)
def test_left_RIGHT_CTRL_PRESSED(self):
ir = self.get_input_record(
"\x00", self.VK_LEFT, self.RIGHT_CTRL_PRESSED | self.ENHANCED_KEY)
self.assertEqual(
self.get_event([ir]), Event("key", "ctrl left"))
self.assertEqual(self.mock.call_count, 1)
def test_left_LEFT_CTRL_PRESSED(self):
ir = self.get_input_record(
"\x00", self.VK_LEFT, self.LEFT_CTRL_PRESSED | self.ENHANCED_KEY)
self.assertEqual(
self.get_event([ir]), Event("key", "ctrl left"))
self.assertEqual(self.mock.call_count, 1)
def test_left_RIGHT_ALT_PRESSED(self):
ir = self.get_input_record(
"\x00", self.VK_LEFT, self.RIGHT_ALT_PRESSED | self.ENHANCED_KEY)
self.assertEqual(self.get_event([ir]), Event(evt="key", data="\033"))
self.assertEqual(
self.console.get_event(), Event("key", "left"))
# self.mock is not called again, since the second time we read from the
# command queue
self.assertEqual(self.mock.call_count, 1)
def test_left_LEFT_ALT_PRESSED(self):
ir = self.get_input_record(
"\x00", self.VK_LEFT, self.LEFT_ALT_PRESSED | self.ENHANCED_KEY)
self.assertEqual(self.get_event([ir]), Event(evt="key", data="\033"))
self.assertEqual(
self.console.get_event(), Event("key", "left"))
self.assertEqual(self.mock.call_count, 1)
def test_m_LEFT_ALT_PRESSED_and_LEFT_CTRL_PRESSED(self):
# For the shift keys, Windows does not send anything when
# ALT and CTRL are both pressed, so let's test with VK_M.
# get_event() receives this input, but does not
# generate an event.
# This is for e.g. an English keyboard layout, for a
# German layout this returns `µ`, see test_AltGr_m.
ir = self.get_input_record(
"\x00", self.VK_M, self.LEFT_ALT_PRESSED | self.LEFT_CTRL_PRESSED)
self.assertEqual(self.get_event([ir]), None)
self.assertEqual(self.mock.call_count, 1)
def test_m_LEFT_ALT_PRESSED(self):
ir = self.get_input_record(
"m", vcode=self.VK_M, control=self.LEFT_ALT_PRESSED)
self.assertEqual(self.get_event([ir]), Event(evt="key", data="\033"))
self.assertEqual(self.console.get_event(), Event("key", "m"))
self.assertEqual(self.mock.call_count, 1)
def test_m_RIGHT_ALT_PRESSED(self):
ir = self.get_input_record(
"m", vcode=self.VK_M, control=self.RIGHT_ALT_PRESSED)
self.assertEqual(self.get_event([ir]), Event(evt="key", data="\033"))
self.assertEqual(self.console.get_event(), Event("key", "m"))
self.assertEqual(self.mock.call_count, 1)
def test_AltGr_7(self):
# E.g. on a German keyboard layout, '{' is entered via
# AltGr + 7, where AltGr is the right Alt key on the keyboard.
# In this case, Windows automatically sets
# RIGHT_ALT_PRESSED = 0x0001 + LEFT_CTRL_PRESSED = 0x0008
# This can also be entered like
# LeftAlt + LeftCtrl + 7 or
# LeftAlt + RightCtrl + 7
# See https://learn.microsoft.com/en-us/windows/console/key-event-record-str
# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-vkkeyscanw
ir = self.get_input_record(
"{", vcode=self.VK_7,
control=self.RIGHT_ALT_PRESSED | self.LEFT_CTRL_PRESSED)
self.assertEqual(self.get_event([ir]), Event("key", "{"))
self.assertEqual(self.mock.call_count, 1)
def test_AltGr_m(self):
# E.g. on a German keyboard layout, this yields 'µ'
# Let's use LEFT_ALT_PRESSED and RIGHT_CTRL_PRESSED this
# time, to cover that, too. See above in test_AltGr_7.
ir = self.get_input_record(
"µ", vcode=self.VK_M, control=self.LEFT_ALT_PRESSED | self.RIGHT_CTRL_PRESSED)
self.assertEqual(self.get_event([ir]), Event("key", "µ"))
self.assertEqual(self.mock.call_count, 1)
def test_umlaut_a_german(self):
ir = self.get_input_record("ä", self.VK_OEM_7)
self.assertEqual(self.get_event([ir]), Event("key", "ä"))
self.assertEqual(self.mock.call_count, 1)
# virtual terminal tests
# Note: wVirtualKeyCode, wVirtualScanCode and dwControlKeyState
# are always zero in this case.
# "\r" and backspace are handled specially, everything else
# is handled in "elif self.__vt_support:" in WindowsConsole.get_event().
# Hence, only one regular key ("m") and a terminal sequence
# are sufficient to test here, the real tests happen in test_eventqueue
# and test_keymap.
def test_enter_vt(self):
ir = self.get_input_record("\r")
self.assertEqual(self.get_event([ir], vt_support=True),
Event("key", "\n"))
self.assertEqual(self.mock.call_count, 1)
def test_backspace_vt(self):
ir = self.get_input_record("\x7f")
self.assertEqual(self.get_event([ir], vt_support=True),
Event("key", "backspace", b"\x7f"))
self.assertEqual(self.mock.call_count, 1)
def test_up_vt(self):
irs = [self.get_input_record(x) for x in "\x1b[A"]
self.assertEqual(self.get_event(irs, vt_support=True),
Event(evt='key', data='up', raw=bytearray(b'\x1b[A')))
self.assertEqual(self.mock.call_count, 3)
if __name__ == "__main__":
unittest.main()