Skip to content

Commit 7d111da

Browse files
saucoideambv
andauthored
gh-121610: pyrepl - handle extending blocks when multi-statement blocks are pasted (GH-121757)
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 Co-authored-by: Łukasz Langa <lukasz@langa.pl>
1 parent 2b1b689 commit 7d111da

File tree

2 files changed

+123
-11
lines changed

2 files changed

+123
-11
lines changed

Lib/_pyrepl/simple_interact.py

+21-10
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
import _sitebuiltins
2929
import linecache
30+
import functools
3031
import sys
3132
import code
3233

@@ -78,6 +79,25 @@ def _clear_screen():
7879
}
7980

8081

82+
def _more_lines(console: code.InteractiveConsole, unicodetext: str) -> bool:
83+
# ooh, look at the hack:
84+
src = _strip_final_indent(unicodetext)
85+
try:
86+
code = console.compile(src, "<stdin>", "single")
87+
except (OverflowError, SyntaxError, ValueError):
88+
lines = src.splitlines(keepends=True)
89+
if len(lines) == 1:
90+
return False
91+
92+
last_line = lines[-1]
93+
was_indented = last_line.startswith((" ", "\t"))
94+
not_empty = last_line.strip() != ""
95+
incomplete = not last_line.endswith("\n")
96+
return (was_indented or not_empty) and incomplete
97+
else:
98+
return code is None
99+
100+
81101
def run_multiline_interactive_console(
82102
console: code.InteractiveConsole,
83103
*,
@@ -88,6 +108,7 @@ def run_multiline_interactive_console(
88108
if future_flags:
89109
console.compile.compiler.flags |= future_flags
90110

111+
more_lines = functools.partial(_more_lines, console)
91112
input_n = 0
92113

93114
def maybe_run_command(statement: str) -> bool:
@@ -113,16 +134,6 @@ def maybe_run_command(statement: str) -> bool:
113134

114135
return False
115136

116-
def more_lines(unicodetext: str) -> bool:
117-
# ooh, look at the hack:
118-
src = _strip_final_indent(unicodetext)
119-
try:
120-
code = console.compile(src, "<stdin>", "single")
121-
except (OverflowError, SyntaxError, ValueError):
122-
return False
123-
else:
124-
return code is None
125-
126137
while 1:
127138
try:
128139
try:

Lib/test/test_pyrepl/test_interact.py

+102-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from test.support import force_not_colorized
88

99
from _pyrepl.console import InteractiveColoredConsole
10-
10+
from _pyrepl.simple_interact import _more_lines
1111

1212
class TestSimpleInteract(unittest.TestCase):
1313
def test_multiple_statements(self):
@@ -111,3 +111,104 @@ def test_no_active_future(self):
111111
result = console.runsource(source)
112112
self.assertFalse(result)
113113
self.assertEqual(f.getvalue(), "{'x': <class 'int'>}\n")
114+
115+
116+
class TestMoreLines(unittest.TestCase):
117+
def test_invalid_syntax_single_line(self):
118+
namespace = {}
119+
code = "if foo"
120+
console = InteractiveColoredConsole(namespace, filename="<stdin>")
121+
self.assertFalse(_more_lines(console, code))
122+
123+
def test_empty_line(self):
124+
namespace = {}
125+
code = ""
126+
console = InteractiveColoredConsole(namespace, filename="<stdin>")
127+
self.assertFalse(_more_lines(console, code))
128+
129+
def test_valid_single_statement(self):
130+
namespace = {}
131+
code = "foo = 1"
132+
console = InteractiveColoredConsole(namespace, filename="<stdin>")
133+
self.assertFalse(_more_lines(console, code))
134+
135+
def test_multiline_single_assignment(self):
136+
namespace = {}
137+
code = dedent("""\
138+
foo = [
139+
1,
140+
2,
141+
3,
142+
]""")
143+
console = InteractiveColoredConsole(namespace, filename="<stdin>")
144+
self.assertFalse(_more_lines(console, code))
145+
146+
def test_multiline_single_block(self):
147+
namespace = {}
148+
code = dedent("""\
149+
def foo():
150+
'''docs'''
151+
152+
return 1""")
153+
console = InteractiveColoredConsole(namespace, filename="<stdin>")
154+
self.assertTrue(_more_lines(console, code))
155+
156+
def test_multiple_statements_single_line(self):
157+
namespace = {}
158+
code = "foo = 1;bar = 2"
159+
console = InteractiveColoredConsole(namespace, filename="<stdin>")
160+
self.assertFalse(_more_lines(console, code))
161+
162+
def test_multiple_statements(self):
163+
namespace = {}
164+
code = dedent("""\
165+
import time
166+
167+
foo = 1""")
168+
console = InteractiveColoredConsole(namespace, filename="<stdin>")
169+
self.assertTrue(_more_lines(console, code))
170+
171+
def test_multiple_blocks(self):
172+
namespace = {}
173+
code = dedent("""\
174+
from dataclasses import dataclass
175+
176+
@dataclass
177+
class Point:
178+
x: float
179+
y: float""")
180+
console = InteractiveColoredConsole(namespace, filename="<stdin>")
181+
self.assertTrue(_more_lines(console, code))
182+
183+
def test_multiple_blocks_empty_newline(self):
184+
namespace = {}
185+
code = dedent("""\
186+
from dataclasses import dataclass
187+
188+
@dataclass
189+
class Point:
190+
x: float
191+
y: float
192+
""")
193+
console = InteractiveColoredConsole(namespace, filename="<stdin>")
194+
self.assertFalse(_more_lines(console, code))
195+
196+
def test_multiple_blocks_indented_newline(self):
197+
namespace = {}
198+
code = (
199+
"from dataclasses import dataclass\n"
200+
"\n"
201+
"@dataclass\n"
202+
"class Point:\n"
203+
" x: float\n"
204+
" y: float\n"
205+
" "
206+
)
207+
console = InteractiveColoredConsole(namespace, filename="<stdin>")
208+
self.assertFalse(_more_lines(console, code))
209+
210+
def test_incomplete_statement(self):
211+
namespace = {}
212+
code = "if foo:"
213+
console = InteractiveColoredConsole(namespace, filename="<stdin>")
214+
self.assertTrue(_more_lines(console, code))

0 commit comments

Comments
 (0)