aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/Lib/test/test_pyrepl
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/test/test_pyrepl')
-rw-r--r--Lib/test/test_pyrepl/support.py3
-rw-r--r--Lib/test/test_pyrepl/test_eventqueue.py78
-rw-r--r--Lib/test/test_pyrepl/test_interact.py2
-rw-r--r--Lib/test/test_pyrepl/test_pyrepl.py212
-rw-r--r--Lib/test/test_pyrepl/test_reader.py90
-rw-r--r--Lib/test/test_pyrepl/test_unix_console.py13
-rw-r--r--Lib/test/test_pyrepl/test_windows_console.py227
7 files changed, 546 insertions, 79 deletions
diff --git a/Lib/test/test_pyrepl/support.py b/Lib/test/test_pyrepl/support.py
index 3692e164cb9..4f7f9d77933 100644
--- a/Lib/test/test_pyrepl/support.py
+++ b/Lib/test/test_pyrepl/support.py
@@ -113,9 +113,6 @@ handle_events_narrow_console = partial(
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:
diff --git a/Lib/test/test_pyrepl/test_eventqueue.py b/Lib/test/test_pyrepl/test_eventqueue.py
index afb55710342..edfe6ac4748 100644
--- a/Lib/test/test_pyrepl/test_eventqueue.py
+++ b/Lib/test/test_pyrepl/test_eventqueue.py
@@ -53,7 +53,7 @@ class EventQueueTestBase:
mock_keymap.compile_keymap.return_value = {"a": "b"}
eq = self.make_eventqueue()
eq.keymap = {b"a": "b"}
- eq.push("a")
+ eq.push(b"a")
mock_keymap.compile_keymap.assert_called()
self.assertEqual(eq.events[0].evt, "key")
self.assertEqual(eq.events[0].data, "b")
@@ -63,7 +63,7 @@ class EventQueueTestBase:
mock_keymap.compile_keymap.return_value = {"a": "b"}
eq = self.make_eventqueue()
eq.keymap = {b"c": "d"}
- eq.push("a")
+ eq.push(b"a")
mock_keymap.compile_keymap.assert_called()
self.assertEqual(eq.events[0].evt, "key")
self.assertEqual(eq.events[0].data, "a")
@@ -73,13 +73,13 @@ class EventQueueTestBase:
mock_keymap.compile_keymap.return_value = {"a": "b"}
eq = self.make_eventqueue()
eq.keymap = {b"a": {b"b": "c"}}
- eq.push("a")
+ eq.push(b"a")
mock_keymap.compile_keymap.assert_called()
self.assertTrue(eq.empty())
- eq.push("b")
+ eq.push(b"b")
self.assertEqual(eq.events[0].evt, "key")
self.assertEqual(eq.events[0].data, "c")
- eq.push("d")
+ eq.push(b"d")
self.assertEqual(eq.events[1].evt, "key")
self.assertEqual(eq.events[1].data, "d")
@@ -88,32 +88,32 @@ class EventQueueTestBase:
mock_keymap.compile_keymap.return_value = {"a": "b"}
eq = self.make_eventqueue()
eq.keymap = {b"a": {b"b": "c"}}
- eq.push("a")
+ eq.push(b"a")
mock_keymap.compile_keymap.assert_called()
self.assertTrue(eq.empty())
eq.flush_buf()
- eq.push("\033")
+ eq.push(b"\033")
self.assertEqual(eq.events[0].evt, "key")
self.assertEqual(eq.events[0].data, "\033")
- eq.push("b")
+ 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("\x1b")
- eq.push("[")
- eq.push("A")
+ 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("\x1b")
- eq.push("[")
- eq.push("Z")
+ 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")
@@ -122,12 +122,54 @@ class EventQueueTestBase:
self.assertEqual(eq.events[2].evt, "key")
self.assertEqual(eq.events[2].data, "Z")
- def test_push_unicode_character(self):
+ def test_push_unicode_character_as_str(self):
eq = self.make_eventqueue()
eq.keymap = {}
- eq.push("ч")
- self.assertEqual(eq.events[0].evt, "key")
- self.assertEqual(eq.events[0].data, "ч")
+ 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")
diff --git a/Lib/test/test_pyrepl/test_interact.py b/Lib/test/test_pyrepl/test_interact.py
index a20719033fc..8c0eeab6dca 100644
--- a/Lib/test/test_pyrepl/test_interact.py
+++ b/Lib/test/test_pyrepl/test_interact.py
@@ -113,7 +113,7 @@ class TestSimpleInteract(unittest.TestCase):
r = """
def f(x, x): ...
^
-SyntaxError: duplicate argument 'x' in function definition"""
+SyntaxError: duplicate parameter 'x' in function definition"""
self.assertIn(r, f.getvalue())
def test_runsource_shows_syntax_error_for_failed_compilation(self):
diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py
index 93029ab6e08..98bae7dd703 100644
--- a/Lib/test/test_pyrepl/test_pyrepl.py
+++ b/Lib/test/test_pyrepl/test_pyrepl.py
@@ -8,9 +8,10 @@ import select
import subprocess
import sys
import tempfile
+from pkgutil import ModuleInfo
from unittest import TestCase, skipUnless, skipIf
from unittest.mock import patch
-from test.support import force_not_colorized, make_clean_env
+from test.support import force_not_colorized, make_clean_env, Py_DEBUG
from test.support import SHORT_TIMEOUT, STDLIB_DIR
from test.support.import_helper import import_module
from test.support.os_helper import EnvironmentVarGuard, unlink
@@ -452,6 +453,11 @@ class TestPyReplAutoindent(TestCase):
)
# fmt: on
+ events = code_to_events(input_code)
+ reader = self.prepare_reader(events)
+ output = multiline_input(reader)
+ self.assertEqual(output, output_code)
+
def test_auto_indent_continuation(self):
# auto indenting according to previous user indentation
# fmt: off
@@ -912,7 +918,14 @@ class TestPyReplCompleter(TestCase):
class TestPyReplModuleCompleter(TestCase):
def setUp(self):
+ import importlib
+ # Make iter_modules() search only the standard library.
+ # This makes the test more reliable in case there are
+ # other user packages/scripts on PYTHONPATH which can
+ # interfere with the completions.
+ lib_path = os.path.dirname(importlib.__path__[0])
self._saved_sys_path = sys.path
+ sys.path = [lib_path]
def tearDown(self):
sys.path = self._saved_sys_path
@@ -920,19 +933,12 @@ class TestPyReplModuleCompleter(TestCase):
def prepare_reader(self, events, namespace):
console = FakeConsole(events)
config = ReadlineConfig()
+ config.module_completer = ModuleCompleter(namespace)
config.readline_completer = rlcompleter.Completer(namespace).complete
reader = ReadlineAlikeReader(console=console, config=config)
return reader
def test_import_completions(self):
- import importlib
- # Make iter_modules() search only the standard library.
- # This makes the test more reliable in case there are
- # other user packages/scripts on PYTHONPATH which can
- # intefere with the completions.
- lib_path = os.path.dirname(importlib.__path__[0])
- sys.path = [lib_path]
-
cases = (
("import path\t\n", "import pathlib"),
("import importlib.\t\tres\t\n", "import importlib.resources"),
@@ -954,10 +960,38 @@ class TestPyReplModuleCompleter(TestCase):
output = reader.readline()
self.assertEqual(output, expected)
- def test_relative_import_completions(self):
+ @patch("pkgutil.iter_modules", lambda: [ModuleInfo(None, "public", True),
+ ModuleInfo(None, "_private", True)])
+ @patch("sys.builtin_module_names", ())
+ def test_private_completions(self):
+ cases = (
+ # Return public methods by default
+ ("import \t\n", "import public"),
+ ("from \t\n", "from public"),
+ # Return private methods if explicitly specified
+ ("import _\t\n", "import _private"),
+ ("from _\t\n", "from _private"),
+ )
+ for code, expected in cases:
+ with self.subTest(code=code):
+ events = code_to_events(code)
+ reader = self.prepare_reader(events, namespace={})
+ output = reader.readline()
+ self.assertEqual(output, expected)
+
+ @patch(
+ "_pyrepl._module_completer.ModuleCompleter.iter_submodules",
+ lambda *_: [
+ ModuleInfo(None, "public", True),
+ ModuleInfo(None, "_private", True),
+ ],
+ )
+ def test_sub_module_private_completions(self):
cases = (
- ("from .readl\t\n", "from .readline"),
- ("from . import readl\t\n", "from . import readline"),
+ # Return public methods by default
+ ("from foo import \t\n", "from foo import public"),
+ # Return private methods if explicitly specified
+ ("from foo import _\t\n", "from foo import _private"),
)
for code, expected in cases:
with self.subTest(code=code):
@@ -966,8 +1000,42 @@ class TestPyReplModuleCompleter(TestCase):
output = reader.readline()
self.assertEqual(output, expected)
- @patch("pkgutil.iter_modules", lambda: [(None, 'valid_name', None),
- (None, 'invalid-name', None)])
+ def test_builtin_completion_top_level(self):
+ import importlib
+ # Make iter_modules() search only the standard library.
+ # This makes the test more reliable in case there are
+ # other user packages/scripts on PYTHONPATH which can
+ # intefere with the completions.
+ lib_path = os.path.dirname(importlib.__path__[0])
+ sys.path = [lib_path]
+
+ cases = (
+ ("import bui\t\n", "import builtins"),
+ ("from bui\t\n", "from builtins"),
+ )
+ for code, expected in cases:
+ with self.subTest(code=code):
+ events = code_to_events(code)
+ reader = self.prepare_reader(events, namespace={})
+ output = reader.readline()
+ self.assertEqual(output, expected)
+
+ def test_relative_import_completions(self):
+ cases = (
+ (None, "from .readl\t\n", "from .readl"),
+ (None, "from . import readl\t\n", "from . import readl"),
+ ("_pyrepl", "from .readl\t\n", "from .readline"),
+ ("_pyrepl", "from . import readl\t\n", "from . import readline"),
+ )
+ for package, code, expected in cases:
+ with self.subTest(code=code):
+ events = code_to_events(code)
+ reader = self.prepare_reader(events, namespace={"__package__": package})
+ output = reader.readline()
+ self.assertEqual(output, expected)
+
+ @patch("pkgutil.iter_modules", lambda: [ModuleInfo(None, "valid_name", True),
+ ModuleInfo(None, "invalid-name", True)])
def test_invalid_identifiers(self):
# Make sure modules which are not valid identifiers
# are not suggested as those cannot be imported via 'import'.
@@ -983,6 +1051,19 @@ class TestPyReplModuleCompleter(TestCase):
output = reader.readline()
self.assertEqual(output, expected)
+ def test_no_fallback_on_regular_completion(self):
+ cases = (
+ ("import pri\t\n", "import pri"),
+ ("from pri\t\n", "from pri"),
+ ("from typing import Na\t\n", "from typing import Na"),
+ )
+ for code, expected in cases:
+ with self.subTest(code=code):
+ events = code_to_events(code)
+ reader = self.prepare_reader(events, namespace={})
+ output = reader.readline()
+ self.assertEqual(output, expected)
+
def test_get_path_and_prefix(self):
cases = (
('', ('', '')),
@@ -1050,11 +1131,15 @@ class TestPyReplModuleCompleter(TestCase):
self.assertEqual(actual, parsed)
# The parser should not get tripped up by any
# other preceding statements
- code = f'import xyz\n{code}'
- with self.subTest(code=code):
+ _code = f'import xyz\n{code}'
+ parser = ImportParser(_code)
+ actual = parser.parse()
+ with self.subTest(code=_code):
self.assertEqual(actual, parsed)
- code = f'import xyz;{code}'
- with self.subTest(code=code):
+ _code = f'import xyz;{code}'
+ parser = ImportParser(_code)
+ actual = parser.parse()
+ with self.subTest(code=_code):
self.assertEqual(actual, parsed)
def test_parse_error(self):
@@ -1327,7 +1412,7 @@ class TestMain(ReplTestCase):
)
@force_not_colorized
- def _run_repl_globals_test(self, expectations, *, as_file=False, as_module=False):
+ def _run_repl_globals_test(self, expectations, *, as_file=False, as_module=False, pythonstartup=False):
clean_env = make_clean_env()
clean_env["NO_COLOR"] = "1" # force_not_colorized doesn't touch subprocesses
@@ -1336,9 +1421,13 @@ class TestMain(ReplTestCase):
blue.mkdir()
mod = blue / "calx.py"
mod.write_text("FOO = 42", encoding="utf-8")
+ startup = blue / "startup.py"
+ startup.write_text("BAR = 64", encoding="utf-8")
commands = [
"print(f'^{" + var + "=}')" for var in expectations
] + ["exit()"]
+ if pythonstartup:
+ clean_env["PYTHONSTARTUP"] = str(startup)
if as_file and as_module:
self.fail("as_file and as_module are mutually exclusive")
elif as_file:
@@ -1357,7 +1446,13 @@ class TestMain(ReplTestCase):
skip=True,
)
else:
- self.fail("Choose one of as_file or as_module")
+ output, exit_code = self.run_repl(
+ commands,
+ cmdline_args=[],
+ env=clean_env,
+ cwd=td,
+ skip=True,
+ )
self.assertEqual(exit_code, 0)
for var, expected in expectations.items():
@@ -1370,6 +1465,23 @@ class TestMain(ReplTestCase):
self.assertNotIn("Exception", output)
self.assertNotIn("Traceback", output)
+ def test_globals_initialized_as_default(self):
+ expectations = {
+ "__name__": "'__main__'",
+ "__package__": "None",
+ # "__file__" is missing in -i, like in the basic REPL
+ }
+ self._run_repl_globals_test(expectations)
+
+ def test_globals_initialized_from_pythonstartup(self):
+ expectations = {
+ "BAR": "64",
+ "__name__": "'__main__'",
+ "__package__": "None",
+ # "__file__" is missing in -i, like in the basic REPL
+ }
+ self._run_repl_globals_test(expectations, pythonstartup=True)
+
def test_inspect_keeps_globals_from_inspected_file(self):
expectations = {
"FOO": "42",
@@ -1379,6 +1491,16 @@ class TestMain(ReplTestCase):
}
self._run_repl_globals_test(expectations, as_file=True)
+ def test_inspect_keeps_globals_from_inspected_file_with_pythonstartup(self):
+ expectations = {
+ "FOO": "42",
+ "BAR": "64",
+ "__name__": "'__main__'",
+ "__package__": "None",
+ # "__file__" is missing in -i, like in the basic REPL
+ }
+ self._run_repl_globals_test(expectations, as_file=True, pythonstartup=True)
+
def test_inspect_keeps_globals_from_inspected_module(self):
expectations = {
"FOO": "42",
@@ -1388,26 +1510,32 @@ class TestMain(ReplTestCase):
}
self._run_repl_globals_test(expectations, as_module=True)
+ def test_inspect_keeps_globals_from_inspected_module_with_pythonstartup(self):
+ expectations = {
+ "FOO": "42",
+ "BAR": "64",
+ "__name__": "'__main__'",
+ "__package__": "'blue'",
+ "__file__": re.compile(r"^'.*calx.py'$"),
+ }
+ self._run_repl_globals_test(expectations, as_module=True, pythonstartup=True)
+
@force_not_colorized
def test_python_basic_repl(self):
env = os.environ.copy()
- commands = ("from test.support import initialized_with_pyrepl\n"
- "initialized_with_pyrepl()\n"
- "exit()\n")
-
+ pyrepl_commands = "clear\nexit()\n"
env.pop("PYTHON_BASIC_REPL", None)
- output, exit_code = self.run_repl(commands, env=env, skip=True)
+ output, exit_code = self.run_repl(pyrepl_commands, env=env, skip=True)
self.assertEqual(exit_code, 0)
- self.assertIn("True", output)
- self.assertNotIn("False", output)
self.assertNotIn("Exception", output)
+ self.assertNotIn("NameError", output)
self.assertNotIn("Traceback", output)
+ basic_commands = "help\nexit()\n"
env["PYTHON_BASIC_REPL"] = "1"
- output, exit_code = self.run_repl(commands, env=env)
+ output, exit_code = self.run_repl(basic_commands, env=env)
self.assertEqual(exit_code, 0)
- self.assertIn("False", output)
- self.assertNotIn("True", output)
+ self.assertIn("Type help() for interactive help", output)
self.assertNotIn("Exception", output)
self.assertNotIn("Traceback", output)
@@ -1544,6 +1672,17 @@ class TestMain(ReplTestCase):
self.assertEqual(exit_code, 0)
self.assertNotIn("TypeError", output)
+ @force_not_colorized
+ def test_non_string_suggestion_candidates(self):
+ commands = ("import runpy\n"
+ "runpy._run_module_code('blech', {0: '', 'bluch': ''}, '')\n"
+ "exit()\n")
+
+ output, exit_code = self.run_repl(commands)
+ self.assertEqual(exit_code, 0)
+ self.assertNotIn("all elements in 'candidates' must be strings", output)
+ self.assertIn("bluch", output)
+
def test_readline_history_file(self):
# skip, if readline module is not available
readline = import_module('readline')
@@ -1605,3 +1744,16 @@ class TestMain(ReplTestCase):
# Extra stuff (newline and `exit` rewrites) are necessary
# because of how run_repl works.
self.assertNotIn(">>> \n>>> >>>", cleaned_output)
+
+ @skipUnless(Py_DEBUG, '-X showrefcount requires a Python debug build')
+ def test_showrefcount(self):
+ env = os.environ.copy()
+ env.pop("PYTHON_BASIC_REPL", "")
+ output, _ = self.run_repl("1\n1+2\nexit()\n", cmdline_args=['-Xshowrefcount'], env=env)
+ matches = re.findall(r'\[-?\d+ refs, \d+ blocks\]', output)
+ self.assertEqual(len(matches), 3)
+
+ env["PYTHON_BASIC_REPL"] = "1"
+ output, _ = self.run_repl("1\n1+2\nexit()\n", cmdline_args=['-Xshowrefcount'], env=env)
+ matches = re.findall(r'\[-?\d+ refs, \d+ blocks\]', output)
+ self.assertEqual(len(matches), 3)
diff --git a/Lib/test/test_pyrepl/test_reader.py b/Lib/test/test_pyrepl/test_reader.py
index 8d7fcf538d2..1f655264f1c 100644
--- a/Lib/test/test_pyrepl/test_reader.py
+++ b/Lib/test/test_pyrepl/test_reader.py
@@ -4,20 +4,21 @@ import rlcompleter
from textwrap import dedent
from unittest import TestCase
from unittest.mock import MagicMock
+from test.support import force_colorized_test_class, force_not_colorized_test_class
from .support import handle_all_events, handle_events_narrow_console
from .support import ScreenEqualMixin, code_to_events
-from .support import prepare_console, reader_force_colors
-from .support import reader_no_colors as prepare_reader
+from .support import prepare_reader, prepare_console
from _pyrepl.console import Event
from _pyrepl.reader import Reader
-from _colorize import theme
+from _colorize import default_theme
-overrides = {"RESET": "z", "SOFT_KEYWORD": "K"}
-colors = {overrides.get(k, k[0].lower()): v for k, v in theme.items()}
+overrides = {"reset": "z", "soft_keyword": "K"}
+colors = {overrides.get(k, k[0].lower()): v for k, v in default_theme.syntax.items()}
+@force_not_colorized_test_class
class TestReader(ScreenEqualMixin, TestCase):
def test_calc_screen_wrap_simple(self):
events = code_to_events(10 * "a")
@@ -127,13 +128,6 @@ class TestReader(ScreenEqualMixin, TestCase):
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, prepare_reader=reader_force_colors)
- self.assert_screen_equal(reader, 'flag = "🏳️\\u200d🌈"', clean=True)
- self.assert_screen_equal(reader, 'flag {o}={z} {s}"🏳️\\u200d🌈"{z}'.format(**colors))
-
def test_setpos_from_xy_multiple_lines(self):
# fmt: off
code = (
@@ -364,6 +358,8 @@ class TestReader(ScreenEqualMixin, TestCase):
reader.setpos_from_xy(8, 0)
self.assertEqual(reader.pos, 7)
+@force_colorized_test_class
+class TestReaderInColor(ScreenEqualMixin, TestCase):
def test_syntax_highlighting_basic(self):
code = dedent(
"""\
@@ -403,7 +399,7 @@ class TestReader(ScreenEqualMixin, TestCase):
)
expected_sync = expected.format(a="", **colors)
events = code_to_events(code)
- reader, _ = handle_all_events(events, prepare_reader=reader_force_colors)
+ reader, _ = handle_all_events(events)
self.assert_screen_equal(reader, code, clean=True)
self.assert_screen_equal(reader, expected_sync)
self.assertEqual(reader.pos, 2**7 + 2**8)
@@ -416,7 +412,7 @@ class TestReader(ScreenEqualMixin, TestCase):
[Event(evt="key", data="up", raw=bytearray(b"\x1bOA"))] * 13,
code_to_events("async "),
)
- reader, _ = handle_all_events(more_events, prepare_reader=reader_force_colors)
+ reader, _ = handle_all_events(more_events)
self.assert_screen_equal(reader, expected_async)
self.assertEqual(reader.pos, 21)
self.assertEqual(reader.cxy, (6, 1))
@@ -433,7 +429,7 @@ class TestReader(ScreenEqualMixin, TestCase):
"""
).format(**colors)
events = code_to_events(code)
- reader, _ = handle_all_events(events, prepare_reader=reader_force_colors)
+ reader, _ = handle_all_events(events)
self.assert_screen_equal(reader, code, clean=True)
self.assert_screen_equal(reader, expected)
@@ -451,7 +447,7 @@ class TestReader(ScreenEqualMixin, TestCase):
"""
).format(**colors)
events = code_to_events(code)
- reader, _ = handle_all_events(events, prepare_reader=reader_force_colors)
+ reader, _ = handle_all_events(events)
self.assert_screen_equal(reader, code, clean=True)
self.assert_screen_equal(reader, expected)
@@ -471,7 +467,7 @@ class TestReader(ScreenEqualMixin, TestCase):
"""
).format(**colors)
events = code_to_events(code)
- reader, _ = handle_all_events(events, prepare_reader=reader_force_colors)
+ reader, _ = handle_all_events(events)
self.assert_screen_equal(reader, code, clean=True)
self.assert_screen_equal(reader, expected)
@@ -497,6 +493,64 @@ class TestReader(ScreenEqualMixin, TestCase):
"""
).format(OB="{", CB="}", **colors)
events = code_to_events(code)
- reader, _ = handle_all_events(events, prepare_reader=reader_force_colors)
+ reader, _ = handle_all_events(events)
self.assert_screen_equal(reader, code, clean=True)
self.assert_screen_equal(reader, expected)
+
+ def test_syntax_highlighting_indentation_error(self):
+ code = dedent(
+ """\
+ def unfinished_function():
+ var = 1
+ oops
+ """
+ )
+ expected = dedent(
+ """\
+ {k}def{z} {d}unfinished_function{z}{o}({z}{o}){z}{o}:{z}
+ var {o}={z} {n}1{z}
+ oops
+ """
+ ).format(**colors)
+ events = code_to_events(code)
+ reader, _ = handle_all_events(events)
+ self.assert_screen_equal(reader, code, clean=True)
+ self.assert_screen_equal(reader, expected)
+
+ def test_syntax_highlighting_literal_brace_in_fstring_or_tstring(self):
+ code = dedent(
+ """\
+ f"{{"
+ f"}}"
+ f"a{{b"
+ f"a}}b"
+ f"a{{b}}c"
+ t"a{{b}}c"
+ f"{{{0}}}"
+ f"{ {0} }"
+ """
+ )
+ expected = dedent(
+ """\
+ {s}f"{z}{s}<<{z}{s}"{z}
+ {s}f"{z}{s}>>{z}{s}"{z}
+ {s}f"{z}{s}a<<{z}{s}b{z}{s}"{z}
+ {s}f"{z}{s}a>>{z}{s}b{z}{s}"{z}
+ {s}f"{z}{s}a<<{z}{s}b>>{z}{s}c{z}{s}"{z}
+ {s}t"{z}{s}a<<{z}{s}b>>{z}{s}c{z}{s}"{z}
+ {s}f"{z}{s}<<{z}{o}<{z}{n}0{z}{o}>{z}{s}>>{z}{s}"{z}
+ {s}f"{z}{o}<{z} {o}<{z}{n}0{z}{o}>{z} {o}>{z}{s}"{z}
+ """
+ ).format(**colors).replace("<", "{").replace(">", "}")
+ events = code_to_events(code)
+ reader, _ = handle_all_events(events)
+ self.assert_screen_equal(reader, code, clean=True)
+ self.maxDiff=None
+ self.assert_screen_equal(reader, expected)
+
+ 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)
+ self.assert_screen_equal(reader, 'flag {o}={z} {s}"🏳️\\u200d🌈"{z}'.format(**colors))
diff --git a/Lib/test/test_pyrepl/test_unix_console.py b/Lib/test/test_pyrepl/test_unix_console.py
index 7acb84a94f7..b3f7dc028fe 100644
--- a/Lib/test/test_pyrepl/test_unix_console.py
+++ b/Lib/test/test_pyrepl/test_unix_console.py
@@ -3,11 +3,12 @@ import os
import sys
import unittest
from functools import partial
-from test.support import os_helper
+from test.support import os_helper, force_not_colorized_test_class
+
from unittest import TestCase
from unittest.mock import MagicMock, call, patch, ANY
-from .support import handle_all_events, code_to_events, reader_no_colors
+from .support import handle_all_events, code_to_events
try:
from _pyrepl.console import Event
@@ -19,6 +20,7 @@ except ImportError:
def unix_console(events, **kwargs):
console = UnixConsole()
console.get_event = MagicMock(side_effect=events)
+ console.getpending = MagicMock(return_value=Event("key", ""))
height = kwargs.get("height", 25)
width = kwargs.get("width", 80)
@@ -33,12 +35,10 @@ def unix_console(events, **kwargs):
handle_events_unix_console = partial(
handle_all_events,
- prepare_reader=reader_no_colors,
prepare_console=unix_console,
)
handle_events_narrow_unix_console = partial(
handle_all_events,
- prepare_reader=reader_no_colors,
prepare_console=partial(unix_console, width=5),
)
handle_events_short_unix_console = partial(
@@ -120,6 +120,7 @@ TERM_CAPABILITIES = {
)
@patch("termios.tcsetattr", lambda a, b, c: None)
@patch("os.write")
+@force_not_colorized_test_class
class TestConsole(TestCase):
def test_simple_addition(self, _os_write):
code = "12+34"
@@ -255,9 +256,7 @@ class TestConsole(TestCase):
# fmt: on
events = itertools.chain(code_to_events(code))
- reader, console = handle_events_short_unix_console(
- events, prepare_reader=reader_no_colors
- )
+ reader, console = handle_events_short_unix_console(events)
console.height = 2
console.getheightwidth = MagicMock(lambda _: (2, 80))
diff --git a/Lib/test/test_pyrepl/test_windows_console.py b/Lib/test/test_pyrepl/test_windows_console.py
index e95fec46a85..f9607e02c60 100644
--- a/Lib/test/test_pyrepl/test_windows_console.py
+++ b/Lib/test/test_pyrepl/test_windows_console.py
@@ -7,12 +7,13 @@ if sys.platform != "win32":
import itertools
from functools import partial
+from test.support import force_not_colorized_test_class
from typing import Iterable
from unittest import TestCase
from unittest.mock import MagicMock, call
from .support import handle_all_events, code_to_events
-from .support import reader_no_colors as default_prepare_reader
+from .support import prepare_reader as default_prepare_reader
try:
from _pyrepl.console import Event, Console
@@ -24,14 +25,17 @@ try:
MOVE_DOWN,
ERASE_IN_LINE,
)
+ import _pyrepl.windows_console as wc
except ImportError:
pass
+@force_not_colorized_test_class
class WindowsConsoleTests(TestCase):
def console(self, events, **kwargs) -> Console:
console = WindowsConsole()
console.get_event = MagicMock(side_effect=events)
+ console.getpending = MagicMock(return_value=Event("key", ""))
console.wait = MagicMock()
console._scroll = MagicMock()
console._hide_cursor = MagicMock()
@@ -350,8 +354,227 @@ class WindowsConsoleTests(TestCase):
Event(evt="key", data='\x1a', raw=bytearray(b'\x1a')),
],
)
- reader, _ = self.handle_events_narrow(events)
+ 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)
+ self.console.wait = MagicMock(return_value=True)
+ 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__":