From 672d1b44ed093dbd524863bd9a8ec5f50556be1b Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Tue, 7 May 2024 15:11:50 +0100 Subject: [PATCH 1/7] gh-111201: Allow pasted code to contain multiple statements in the REPL --- Lib/_pyrepl/simple_interact.py | 5 +++++ Lib/code.py | 4 ++-- Lib/test/test_pyrepl.py | 25 ++++++++++++++++++++++--- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/Lib/_pyrepl/simple_interact.py b/Lib/_pyrepl/simple_interact.py index 4bc8368169336a..ce853c6534c6ee 100644 --- a/Lib/_pyrepl/simple_interact.py +++ b/Lib/_pyrepl/simple_interact.py @@ -77,6 +77,11 @@ def __init__( def showtraceback(self): super().showtraceback(colorize=self.can_colorize) + def push(self, line, filename=None, symbol="single"): + if line.count("\n") > 0: + symbol = "exec" + return super().push(line, filename=filename, _symbol=symbol) + def run_multiline_interactive_console( mainmodule: ModuleType | None= None, future_flags: int = 0 diff --git a/Lib/code.py b/Lib/code.py index 1ee1ad62ff4506..9d124563f728c2 100644 --- a/Lib/code.py +++ b/Lib/code.py @@ -281,7 +281,7 @@ def interact(self, banner=None, exitmsg=None): elif exitmsg != '': self.write('%s\n' % exitmsg) - def push(self, line, filename=None): + def push(self, line, filename=None, _symbol="single"): """Push a line to the interpreter. The line should not have a trailing newline; it may have @@ -299,7 +299,7 @@ def push(self, line, filename=None): source = "\n".join(self.buffer) if filename is None: filename = self.filename - more = self.runsource(source, filename) + more = self.runsource(source, filename, symbol=_symbol) if not more: self.resetbuffer() return more diff --git a/Lib/test/test_pyrepl.py b/Lib/test/test_pyrepl.py index b7ae91b919527a..01308fdd98bb2f 100644 --- a/Lib/test/test_pyrepl.py +++ b/Lib/test/test_pyrepl.py @@ -23,6 +23,7 @@ from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig from _pyrepl.simple_interact import _strip_final_indent from _pyrepl.unix_eventqueue import EventQueue +from _pyrepl.simple_interact import InteractiveColoredConsole def more_lines(unicodetext, namespace=None): @@ -976,6 +977,24 @@ def test_setpos_fromxy_in_wrapped_line(self): reader.setpos_from_xy(0, 1) self.assertEqual(reader.pos, 9) - -if __name__ == "__main__": - unittest.main() +class TestInteractiveColoredConsole(unittest.TestCase): + def test_showtraceback(self): + console = InteractiveColoredConsole() + with patch('code.InteractiveConsole.showtraceback') as mock_showtraceback: + console.showtraceback() + mock_showtraceback.assert_called_once_with(colorize=console.can_colorize) + + def test_push_single_line(self): + console = InteractiveColoredConsole() + with patch('code.InteractiveConsole.runsource') as mock_runsource: + console.push('print("Hello, world!")') + mock_runsource.assert_called_once_with('print("Hello, world!")', '', symbol='single') + + def test_push_multiline(self): + console = InteractiveColoredConsole() + with patch('code.InteractiveConsole.runsource') as mock_runsource: + console.push('if True:\n print("Hello, world!")') + mock_runsource.assert_called_once_with('if True:\n print("Hello, world!")', '', symbol='exec') + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 1525bcfd1d05de9c5fe9683d4e701600d165a6b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Tue, 7 May 2024 16:16:04 +0200 Subject: [PATCH 2/7] Fix CI --- Lib/_pyrepl/simple_interact.py | 2 +- Lib/test/test_pyrepl.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/_pyrepl/simple_interact.py b/Lib/_pyrepl/simple_interact.py index ce853c6534c6ee..0a67f05f94da99 100644 --- a/Lib/_pyrepl/simple_interact.py +++ b/Lib/_pyrepl/simple_interact.py @@ -149,7 +149,7 @@ def more_lines(unicodetext: str) -> bool: input_name = f"" linecache._register_code(input_name, statement, "") # type: ignore[attr-defined] - more = console.push(_strip_final_indent(statement), filename=input_name) # type: ignore[call-arg] + more = console.push(_strip_final_indent(statement), filename=input_name) assert not more input_n += 1 except KeyboardInterrupt: diff --git a/Lib/test/test_pyrepl.py b/Lib/test/test_pyrepl.py index 01308fdd98bb2f..9741223b16725c 100644 --- a/Lib/test/test_pyrepl.py +++ b/Lib/test/test_pyrepl.py @@ -997,4 +997,4 @@ def test_push_multiline(self): mock_runsource.assert_called_once_with('if True:\n print("Hello, world!")', '', symbol='exec') if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() From b060efae85ff3ca53eb71bb43e0c854ceea09bfb Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Tue, 7 May 2024 15:25:27 +0100 Subject: [PATCH 3/7] Fix this better by detecting paste mode --- Lib/_pyrepl/commands.py | 3 +++ Lib/_pyrepl/reader.py | 1 + Lib/_pyrepl/readline.py | 3 ++- Lib/_pyrepl/simple_interact.py | 12 +++++------- Lib/test/test_pyrepl.py | 18 ------------------ 5 files changed, 11 insertions(+), 26 deletions(-) diff --git a/Lib/_pyrepl/commands.py b/Lib/_pyrepl/commands.py index bb6bebace30ec8..436a0ed08bd06d 100644 --- a/Lib/_pyrepl/commands.py +++ b/Lib/_pyrepl/commands.py @@ -460,6 +460,8 @@ 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,6 +469,7 @@ 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 class disable_bracketed_paste(Command): def do(self) -> None: diff --git a/Lib/_pyrepl/reader.py b/Lib/_pyrepl/reader.py index e36f65c176e81f..33e016b9969638 100644 --- a/Lib/_pyrepl/reader.py +++ b/Lib/_pyrepl/reader.py @@ -221,6 +221,7 @@ class Reader: dirty: bool = False finished: bool = False paste_mode: bool = False + was_paste_mode_activated: 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) diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py index 37ba98d4c8c87a..d28a7f3779f302 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -298,10 +298,11 @@ def multiline_input(self, more_lines, ps1, ps2): reader.more_lines = more_lines reader.ps1 = reader.ps2 = ps1 reader.ps3 = reader.ps4 = ps2 - return reader.readline() + return reader.readline(), reader.was_paste_mode_activated 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 0a67f05f94da99..1081399384a1e5 100644 --- a/Lib/_pyrepl/simple_interact.py +++ b/Lib/_pyrepl/simple_interact.py @@ -77,11 +77,6 @@ def __init__( def showtraceback(self): super().showtraceback(colorize=self.can_colorize) - def push(self, line, filename=None, symbol="single"): - if line.count("\n") > 0: - symbol = "exec" - return super().push(line, filename=filename, _symbol=symbol) - def run_multiline_interactive_console( mainmodule: ModuleType | None= None, future_flags: int = 0 @@ -140,7 +135,7 @@ def more_lines(unicodetext: str) -> bool: ps1 = getattr(sys, "ps1", ">>> ") ps2 = getattr(sys, "ps2", "... ") try: - statement = multiline_input(more_lines, ps1, ps2) + statement, contains_pasted_code = multiline_input(more_lines, ps1, ps2) except EOFError: break @@ -149,7 +144,10 @@ def more_lines(unicodetext: str) -> bool: input_name = f"" linecache._register_code(input_name, statement, "") # type: ignore[attr-defined] - more = console.push(_strip_final_indent(statement), filename=input_name) + symbol = "single" if not contains_pasted_code else "exec" + more = console.push(_strip_final_indent(statement), filename=input_name, _symbol=symbol) + if contains_pasted_code and more: + more = console.push(_strip_final_indent(statement), filename=input_name, _symbol="single") assert not more input_n += 1 except KeyboardInterrupt: diff --git a/Lib/test/test_pyrepl.py b/Lib/test/test_pyrepl.py index 9741223b16725c..2f375986de295a 100644 --- a/Lib/test/test_pyrepl.py +++ b/Lib/test/test_pyrepl.py @@ -977,24 +977,6 @@ def test_setpos_fromxy_in_wrapped_line(self): reader.setpos_from_xy(0, 1) self.assertEqual(reader.pos, 9) -class TestInteractiveColoredConsole(unittest.TestCase): - def test_showtraceback(self): - console = InteractiveColoredConsole() - with patch('code.InteractiveConsole.showtraceback') as mock_showtraceback: - console.showtraceback() - mock_showtraceback.assert_called_once_with(colorize=console.can_colorize) - - def test_push_single_line(self): - console = InteractiveColoredConsole() - with patch('code.InteractiveConsole.runsource') as mock_runsource: - console.push('print("Hello, world!")') - mock_runsource.assert_called_once_with('print("Hello, world!")', '', symbol='single') - - def test_push_multiline(self): - console = InteractiveColoredConsole() - with patch('code.InteractiveConsole.runsource') as mock_runsource: - console.push('if True:\n print("Hello, world!")') - mock_runsource.assert_called_once_with('if True:\n print("Hello, world!")', '', symbol='exec') if __name__ == '__main__': unittest.main() From 9db84785adbb2bef73f025145cea4b93fe6bf505 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Tue, 7 May 2024 16:54:13 +0200 Subject: [PATCH 4/7] Mypy thinks we're still in 3.12 land --- 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 1081399384a1e5..dc00ec9029191b 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" if not contains_pasted_code else "exec" - more = console.push(_strip_final_indent(statement), filename=input_name, _symbol=symbol) + 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") assert not more From 632dbead08c97e026c804f6637a8a5cefd8804c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Tue, 7 May 2024 16:54:48 +0200 Subject: [PATCH 5/7] Mypy still thinks we're still in 3.12 land --- 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 dc00ec9029191b..31b2097a78a226 100644 --- a/Lib/_pyrepl/simple_interact.py +++ b/Lib/_pyrepl/simple_interact.py @@ -147,7 +147,7 @@ def more_lines(unicodetext: str) -> bool: symbol = "single" if not contains_pasted_code else "exec" 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") + more = console.push(_strip_final_indent(statement), filename=input_name, _symbol="single") # type: ignore[call-arg] assert not more input_n += 1 except KeyboardInterrupt: From dc58f5d63fad08d4092e26c4f6925deb2742ea9b Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Tue, 7 May 2024 16:00:13 +0100 Subject: [PATCH 6/7] Mark dirty in end bracketed paste --- Lib/_pyrepl/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/_pyrepl/commands.py b/Lib/_pyrepl/commands.py index 436a0ed08bd06d..456cba0769c952 100644 --- a/Lib/_pyrepl/commands.py +++ b/Lib/_pyrepl/commands.py @@ -474,4 +474,4 @@ def do(self) -> None: class disable_bracketed_paste(Command): def do(self) -> None: self.reader.paste_mode = False - self.reader.insert("\n") + self.reader.dirty = True From 7331e9b688849cff6f87da4a7da8919a75e5212c Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Tue, 7 May 2024 17:03:25 +0200 Subject: [PATCH 7/7] Add test for single line bracketed paste --- Lib/test/test_pyrepl.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_pyrepl.py b/Lib/test/test_pyrepl.py index e7efb3a87f49db..c8990b699b214c 100644 --- a/Lib/test/test_pyrepl.py +++ b/Lib/test/test_pyrepl.py @@ -831,7 +831,6 @@ def test_bracketed_paste(self): ' else:\n' ' pass\n' ) - # fmt: on output_code = ( 'def a():\n' @@ -842,8 +841,8 @@ def test_bracketed_paste(self): '\n' ' else:\n' ' pass\n' - '\n' ) + # fmt: on paste_start = "\x1b[200~" paste_end = "\x1b[201~" @@ -858,6 +857,22 @@ def test_bracketed_paste(self): output = multiline_input(reader) self.assertEqual(output, output_code) + def test_bracketed_paste_single_line(self): + input_code = "oneline" + + paste_start = "\x1b[200~" + paste_end = "\x1b[201~" + + events = itertools.chain( + code_to_events(paste_start), + code_to_events(input_code), + code_to_events(paste_end), + code_to_events("\n"), + ) + reader = self.prepare_reader(events) + output = multiline_input(reader) + self.assertEqual(output, input_code) + class TestReader(TestCase): def assert_screen_equals(self, reader, expected):