From 5b1da7614dc29905a51f01fde2dbc986ec102c9c Mon Sep 17 00:00:00 2001 From: saucoide Date: Sun, 14 Jul 2024 12:35:39 +0200 Subject: [PATCH 1/2] handle cases when multiple statements are pasted in the repl console.compile with the "single" param throws an exception when there are multiple statements, never allowing to adding newlines to a pasted code block (gh-121610) This add a few extra checks to allow extending when in an indented block, and tests for a few examples --- Lib/_pyrepl/simple_interact.py | 31 +++++--- Lib/test/test_pyrepl/test_interact.py | 103 +++++++++++++++++++++++++- 2 files changed, 123 insertions(+), 11 deletions(-) diff --git a/Lib/_pyrepl/simple_interact.py b/Lib/_pyrepl/simple_interact.py index bc16c1f6a23159..099e3506f85918 100644 --- a/Lib/_pyrepl/simple_interact.py +++ b/Lib/_pyrepl/simple_interact.py @@ -28,6 +28,7 @@ import _sitebuiltins import linecache import builtins +import functools import sys import code from types import ModuleType @@ -80,6 +81,24 @@ def _clear_screen(): "clear": _clear_screen, } +def _more_lines(console: code.InteractiveConsole, unicodetext: str) -> bool: + # ooh, look at the hack: + src = _strip_final_indent(unicodetext) + try: + code = console.compile(src, "", "single") + except (OverflowError, SyntaxError, ValueError): + lines = src.splitlines(keepends=True) + if len(lines) == 1: + return False + + last_line = lines[-1] + return ( + last_line.startswith(" ") + or last_line.startswith("\t") + or last_line.strip() != "" + ) and not last_line.endswith("\n") + else: + return code is None def run_multiline_interactive_console( namespace: dict[str, Any], @@ -96,6 +115,8 @@ def run_multiline_interactive_console( if future_flags: console.compile.compiler.flags |= future_flags + more_lines = functools.partial(_more_lines, console) + input_n = 0 def maybe_run_command(statement: str) -> bool: @@ -121,16 +142,6 @@ def maybe_run_command(statement: str) -> bool: return False - def more_lines(unicodetext: str) -> bool: - # ooh, look at the hack: - src = _strip_final_indent(unicodetext) - try: - code = console.compile(src, "", "single") - except (OverflowError, SyntaxError, ValueError): - return False - else: - return code is None - while 1: try: try: diff --git a/Lib/test/test_pyrepl/test_interact.py b/Lib/test/test_pyrepl/test_interact.py index 31f08cdb25e078..2448e4eb8be662 100644 --- a/Lib/test/test_pyrepl/test_interact.py +++ b/Lib/test/test_pyrepl/test_interact.py @@ -7,7 +7,7 @@ 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): @@ -111,3 +111,104 @@ def test_no_active_future(self): result = console.runsource(source) self.assertFalse(result) self.assertEqual(f.getvalue(), "{'x': }\n") + + +class TestMoreLines(unittest.TestCase): + def test_invalid_syntax_single_line(self): + namespace = {} + code = "if foo" + console = InteractiveColoredConsole(namespace, filename="") + self.assertFalse(_more_lines(console, code)) + + def test_empty_line(self): + namespace = {} + code = "" + console = InteractiveColoredConsole(namespace, filename="") + self.assertFalse(_more_lines(console, code)) + + def test_valid_single_statement(self): + namespace = {} + code = "foo = 1" + console = InteractiveColoredConsole(namespace, filename="") + self.assertFalse(_more_lines(console, code)) + + def test_multiline_single_assignment(self): + namespace = {} + code = dedent("""\ + foo = [ + 1, + 2, + 3, + ]""") + console = InteractiveColoredConsole(namespace, filename="") + self.assertFalse(_more_lines(console, code)) + + def test_multiline_single_block(self): + namespace = {} + code = dedent("""\ + def foo(): + '''docs''' + + return 1""") + console = InteractiveColoredConsole(namespace, filename="") + self.assertTrue(_more_lines(console, code)) + + def test_multiple_statements_single_line(self): + namespace = {} + code = "foo = 1;bar = 2" + console = InteractiveColoredConsole(namespace, filename="") + self.assertFalse(_more_lines(console, code)) + + def test_multiple_statements(self): + namespace = {} + code = dedent("""\ + import time + + foo = 1""") + console = InteractiveColoredConsole(namespace, filename="") + 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="") + 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="") + 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="") + self.assertFalse(_more_lines(console, code)) + + def test_incomplete_statement(self): + namespace = {} + code = "if foo:" + console = InteractiveColoredConsole(namespace, filename="") + self.assertTrue(_more_lines(console, code)) From 07da5d4073e48fe17e75bdd9cca1ec2cc27d3b2a Mon Sep 17 00:00:00 2001 From: saucoide Date: Sun, 14 Jul 2024 13:08:21 +0200 Subject: [PATCH 2/2] make linter happy --- Lib/test/test_pyrepl/test_interact.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_pyrepl/test_interact.py b/Lib/test/test_pyrepl/test_interact.py index 2448e4eb8be662..369dab316af132 100644 --- a/Lib/test/test_pyrepl/test_interact.py +++ b/Lib/test/test_pyrepl/test_interact.py @@ -148,7 +148,7 @@ def test_multiline_single_block(self): code = dedent("""\ def foo(): '''docs''' - + return 1""") console = InteractiveColoredConsole(namespace, filename="") self.assertTrue(_more_lines(console, code)) @@ -163,7 +163,7 @@ def test_multiple_statements(self): namespace = {} code = dedent("""\ import time - + foo = 1""") console = InteractiveColoredConsole(namespace, filename="") self.assertTrue(_more_lines(console, code)) @@ -172,7 +172,7 @@ def test_multiple_blocks(self): namespace = {} code = dedent("""\ from dataclasses import dataclass - + @dataclass class Point: x: float @@ -184,7 +184,7 @@ def test_multiple_blocks_empty_newline(self): namespace = {} code = dedent("""\ from dataclasses import dataclass - + @dataclass class Point: x: float