From fe028c97dabda0c01a00982b87918e1e5f6fa6a9 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Tue, 21 May 2024 14:53:09 -0400 Subject: [PATCH 1/3] gh-111201: Speed up paste mode in the REPL --- Lib/_pyrepl/commands.py | 5 ++--- Lib/_pyrepl/reader.py | 8 ++++---- Lib/_pyrepl/readline.py | 8 ++++---- Lib/_pyrepl/simple_interact.py | 7 +++---- Lib/_pyrepl/utils.py | 8 ++++++-- 5 files changed, 19 insertions(+), 17 deletions(-) diff --git a/Lib/_pyrepl/commands.py b/Lib/_pyrepl/commands.py index 51c7afebede5a8..fb138d9476d4e7 100644 --- a/Lib/_pyrepl/commands.py +++ b/Lib/_pyrepl/commands.py @@ -458,8 +458,6 @@ def do(self) -> None: class paste_mode(Command): def do(self) -> None: - if not self.reader.paste_mode: - self.reader.was_paste_mode_activated = True self.reader.paste_mode = not self.reader.paste_mode self.reader.dirty = True @@ -467,9 +465,10 @@ def do(self) -> None: class enable_bracketed_paste(Command): def do(self) -> None: self.reader.paste_mode = True - self.reader.was_paste_mode_activated = True + self.reader.in_bracketed_paste = True class disable_bracketed_paste(Command): def do(self) -> None: self.reader.paste_mode = False + self.reader.in_bracketed_paste = False self.reader.dirty = True diff --git a/Lib/_pyrepl/reader.py b/Lib/_pyrepl/reader.py index 2c8c9e7dc4b5da..a1cf2f5a76df89 100644 --- a/Lib/_pyrepl/reader.py +++ b/Lib/_pyrepl/reader.py @@ -52,7 +52,7 @@ def disp_str(buffer: str) -> tuple[str, list[int]]: b: list[int] = [] s: list[str] = [] for c in buffer: - if unicodedata.category(c).startswith("C"): + if ord(c) > 128 and unicodedata.category(c).startswith("C"): c = r"\u%04x" % ord(c) s.append(c) b.append(wlen(c)) @@ -223,7 +223,7 @@ class Reader: dirty: bool = False finished: bool = False paste_mode: bool = False - was_paste_mode_activated: bool = False + in_bracketed_paste: bool = False commands: dict[str, type[Command]] = field(default_factory=make_default_commands) last_command: type[Command] | None = None syntax_table: dict[str, int] = field(default_factory=make_default_syntax_table) @@ -422,7 +422,7 @@ def get_prompt(self, lineno: int, cursor_on_line: bool) -> str: elif "\n" in self.buffer: if lineno == 0: prompt = self.ps2 - elif lineno == self.buffer.count("\n"): + elif self.ps4 and lineno == self.buffer.count("\n"): prompt = self.ps4 else: prompt = self.ps3 @@ -585,7 +585,7 @@ def do_cmd(self, cmd: tuple[str, list[str]]) -> None: self.after_command(command) - if self.dirty: + if self.dirty and not self.in_bracketed_paste: self.refresh() else: self.update_cursor() diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py index 0adecf235a4eb4..aa5366a937b836 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -290,7 +290,7 @@ def input(self, prompt: object = "") -> str: reader.ps1 = str(prompt) return reader.readline(startup_hook=self.startup_hook) - def multiline_input(self, more_lines: MoreLinesCallable, ps1: str, ps2: str) -> tuple[str, bool]: + def multiline_input(self, more_lines: MoreLinesCallable, ps1: str, ps2: str) -> str: """Read an input on possibly multiple lines, asking for more lines as long as 'more_lines(unicodetext)' returns an object whose boolean value is true. @@ -300,12 +300,12 @@ def multiline_input(self, more_lines: MoreLinesCallable, ps1: str, ps2: str) -> try: reader.more_lines = more_lines reader.ps1 = reader.ps2 = ps1 - reader.ps3 = reader.ps4 = ps2 - return reader.readline(), reader.was_paste_mode_activated + reader.ps3 = ps2 + reader.ps4 = "" + return reader.readline() finally: reader.more_lines = saved reader.paste_mode = False - reader.was_paste_mode_activated = False def parse_and_bind(self, string: str) -> None: pass # XXX we don't support parsing GNU-readline-style init files diff --git a/Lib/_pyrepl/simple_interact.py b/Lib/_pyrepl/simple_interact.py index 31b2097a78a226..d783aedf1eab8d 100644 --- a/Lib/_pyrepl/simple_interact.py +++ b/Lib/_pyrepl/simple_interact.py @@ -61,6 +61,7 @@ def _strip_final_indent(text: str) -> str: "quit": _sitebuiltins.Quitter('quit' ,''), "copyright": _sitebuiltins._Printer('copyright', sys.copyright), "help": "help", + "clear": "clear_screen", } class InteractiveColoredConsole(code.InteractiveConsole): @@ -135,7 +136,7 @@ def more_lines(unicodetext: str) -> bool: ps1 = getattr(sys, "ps1", ">>> ") ps2 = getattr(sys, "ps2", "... ") try: - statement, contains_pasted_code = multiline_input(more_lines, ps1, ps2) + statement = multiline_input(more_lines, ps1, ps2) except EOFError: break @@ -144,10 +145,8 @@ def more_lines(unicodetext: str) -> bool: input_name = f"" linecache._register_code(input_name, statement, "") # type: ignore[attr-defined] - symbol = "single" if not contains_pasted_code else "exec" + symbol = "single" more = console.push(_strip_final_indent(statement), filename=input_name, _symbol=symbol) # type: ignore[call-arg] - if contains_pasted_code and more: - more = console.push(_strip_final_indent(statement), filename=input_name, _symbol="single") # type: ignore[call-arg] assert not more input_n += 1 except KeyboardInterrupt: diff --git a/Lib/_pyrepl/utils.py b/Lib/_pyrepl/utils.py index cd1df7c49a216d..96e917e487d91a 100644 --- a/Lib/_pyrepl/utils.py +++ b/Lib/_pyrepl/utils.py @@ -1,10 +1,14 @@ import re import unicodedata +import functools ANSI_ESCAPE_SEQUENCE = re.compile(r"\x1b\[[ -@]*[A-~]") +@functools.cache def str_width(c: str) -> int: + if ord(c) < 128: + return 1 w = unicodedata.east_asian_width(c) if w in ('N', 'Na', 'H', 'A'): return 1 @@ -13,6 +17,6 @@ def str_width(c: str) -> int: def wlen(s: str) -> int: length = sum(str_width(i) for i in s) - # remove lengths of any escape sequences - return length - sum(len(i) for i in ANSI_ESCAPE_SEQUENCE.findall(s)) + sequence = ANSI_ESCAPE_SEQUENCE.findall(s) + return length - sum(len(i) for i in sequence) From 696d85dc372462f010aa362b7c008a5f787697ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Tue, 21 May 2024 22:38:48 +0200 Subject: [PATCH 2/3] Fix Pablo's linting mishap --- Lib/_pyrepl/simple_interact.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/_pyrepl/simple_interact.py b/Lib/_pyrepl/simple_interact.py index d783aedf1eab8d..6715858b1e2e77 100644 --- a/Lib/_pyrepl/simple_interact.py +++ b/Lib/_pyrepl/simple_interact.py @@ -145,7 +145,7 @@ def more_lines(unicodetext: str) -> bool: input_name = f"" linecache._register_code(input_name, statement, "") # type: ignore[attr-defined] - symbol = "single" + symbol = "single" more = console.push(_strip_final_indent(statement), filename=input_name, _symbol=symbol) # type: ignore[call-arg] assert not more input_n += 1 From dc81f20c28a41c65fb9e511139844d5045a55863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Wed, 22 May 2024 06:58:09 +0200 Subject: [PATCH 3/3] close but no cigar --- Lib/test/test_pyrepl/test_pyrepl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 930f6759fb0b48..7b5217e4b01fd0 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -578,7 +578,7 @@ def test_func(self): reader = self.prepare_reader(events, namespace) mock_get_reader.return_value = reader output = readline_multiline_input(more_lines, ">>>", "...") - self.assertEqual(output[0], "dummy.test_func.__") + self.assertEqual(output, "dummy.test_func.__") self.assertEqual(mock_stderr.getvalue(), "")