Skip to content

Commit a94ac56

Browse files
pablogsalambv
andauthored
gh-111201: Allow pasted code to contain multiple statements in the REPL (#118712)
Co-authored-by: Łukasz Langa <[email protected]>
1 parent 26bab42 commit a94ac56

File tree

6 files changed

+33
-9
lines changed

6 files changed

+33
-9
lines changed

Lib/_pyrepl/commands.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,15 +460,18 @@ def do(self) -> None:
460460
class paste_mode(Command):
461461

462462
def do(self) -> None:
463+
if not self.reader.paste_mode:
464+
self.reader.was_paste_mode_activated = True
463465
self.reader.paste_mode = not self.reader.paste_mode
464466
self.reader.dirty = True
465467

466468

467469
class enable_bracketed_paste(Command):
468470
def do(self) -> None:
469471
self.reader.paste_mode = True
472+
self.reader.was_paste_mode_activated = True
470473

471474
class disable_bracketed_paste(Command):
472475
def do(self) -> None:
473476
self.reader.paste_mode = False
474-
self.reader.insert("\n")
477+
self.reader.dirty = True

Lib/_pyrepl/reader.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ class Reader:
221221
dirty: bool = False
222222
finished: bool = False
223223
paste_mode: bool = False
224+
was_paste_mode_activated: bool = False
224225
commands: dict[str, type[Command]] = field(default_factory=make_default_commands)
225226
last_command: type[Command] | None = None
226227
syntax_table: dict[str, int] = field(default_factory=make_default_syntax_table)

Lib/_pyrepl/readline.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,10 +298,11 @@ def multiline_input(self, more_lines, ps1, ps2):
298298
reader.more_lines = more_lines
299299
reader.ps1 = reader.ps2 = ps1
300300
reader.ps3 = reader.ps4 = ps2
301-
return reader.readline()
301+
return reader.readline(), reader.was_paste_mode_activated
302302
finally:
303303
reader.more_lines = saved
304304
reader.paste_mode = False
305+
reader.was_paste_mode_activated = False
305306

306307
def parse_and_bind(self, string: str) -> None:
307308
pass # XXX we don't support parsing GNU-readline-style init files

Lib/_pyrepl/simple_interact.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def more_lines(unicodetext: str) -> bool:
135135
ps1 = getattr(sys, "ps1", ">>> ")
136136
ps2 = getattr(sys, "ps2", "... ")
137137
try:
138-
statement = multiline_input(more_lines, ps1, ps2)
138+
statement, contains_pasted_code = multiline_input(more_lines, ps1, ps2)
139139
except EOFError:
140140
break
141141

@@ -144,7 +144,10 @@ def more_lines(unicodetext: str) -> bool:
144144

145145
input_name = f"<python-input-{input_n}>"
146146
linecache._register_code(input_name, statement, "<stdin>") # type: ignore[attr-defined]
147-
more = console.push(_strip_final_indent(statement), filename=input_name) # type: ignore[call-arg]
147+
symbol = "single" if not contains_pasted_code else "exec"
148+
more = console.push(_strip_final_indent(statement), filename=input_name, _symbol=symbol) # type: ignore[call-arg]
149+
if contains_pasted_code and more:
150+
more = console.push(_strip_final_indent(statement), filename=input_name, _symbol="single") # type: ignore[call-arg]
148151
assert not more
149152
input_n += 1
150153
except KeyboardInterrupt:

Lib/code.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ def interact(self, banner=None, exitmsg=None):
281281
elif exitmsg != '':
282282
self.write('%s\n' % exitmsg)
283283

284-
def push(self, line, filename=None):
284+
def push(self, line, filename=None, _symbol="single"):
285285
"""Push a line to the interpreter.
286286
287287
The line should not have a trailing newline; it may have
@@ -299,7 +299,7 @@ def push(self, line, filename=None):
299299
source = "\n".join(self.buffer)
300300
if filename is None:
301301
filename = self.filename
302-
more = self.runsource(source, filename)
302+
more = self.runsource(source, filename, symbol=_symbol)
303303
if not more:
304304
self.resetbuffer()
305305
return more

Lib/test/test_pyrepl.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig
2424
from _pyrepl.simple_interact import _strip_final_indent
2525
from _pyrepl.unix_eventqueue import EventQueue
26+
from _pyrepl.simple_interact import InteractiveColoredConsole
2627

2728

2829
def more_lines(unicodetext, namespace=None):
@@ -830,7 +831,6 @@ def test_bracketed_paste(self):
830831
' else:\n'
831832
' pass\n'
832833
)
833-
# fmt: on
834834

835835
output_code = (
836836
'def a():\n'
@@ -841,8 +841,8 @@ def test_bracketed_paste(self):
841841
'\n'
842842
' else:\n'
843843
' pass\n'
844-
'\n'
845844
)
845+
# fmt: on
846846

847847
paste_start = "\x1b[200~"
848848
paste_end = "\x1b[201~"
@@ -857,6 +857,22 @@ def test_bracketed_paste(self):
857857
output = multiline_input(reader)
858858
self.assertEqual(output, output_code)
859859

860+
def test_bracketed_paste_single_line(self):
861+
input_code = "oneline"
862+
863+
paste_start = "\x1b[200~"
864+
paste_end = "\x1b[201~"
865+
866+
events = itertools.chain(
867+
code_to_events(paste_start),
868+
code_to_events(input_code),
869+
code_to_events(paste_end),
870+
code_to_events("\n"),
871+
)
872+
reader = self.prepare_reader(events)
873+
output = multiline_input(reader)
874+
self.assertEqual(output, input_code)
875+
860876

861877
class TestReader(TestCase):
862878
def assert_screen_equals(self, reader, expected):
@@ -986,5 +1002,5 @@ def test_up_arrow_after_ctrl_r(self):
9861002
self.assert_screen_equals(reader, "")
9871003

9881004

989-
if __name__ == "__main__":
1005+
if __name__ == '__main__':
9901006
unittest.main()

0 commit comments

Comments
 (0)