Skip to content

Commit 13c31f1

Browse files
aelsayed95ambv
authored andcommitted
[3.13] pythongh-118911: Trailing whitespace in a block shouldn't prevent the user from terminating the code block (pythonGH-119355)
(cherry picked from commit 5091c44) Co-authored-by: Aya Elsayed <ayah.ehab11@gmail.com> Co-authored-by: Łukasz Langa <lukasz@langa.pl>
1 parent 7214598 commit 13c31f1

5 files changed

Lines changed: 79 additions & 11 deletions

File tree

Lib/_pyrepl/historical_reader.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ def select_item(self, i: int) -> None:
259259
self.transient_history[self.historyi] = self.get_unicode()
260260
buf = self.transient_history.get(i)
261261
if buf is None:
262-
buf = self.history[i]
262+
buf = self.history[i].rstrip()
263263
self.buffer = list(buf)
264264
self.historyi = i
265265
self.pos = len(self.buffer)

Lib/_pyrepl/readline.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,14 +212,27 @@ def do(self) -> None:
212212
r: ReadlineAlikeReader
213213
r = self.reader # type: ignore[assignment]
214214
r.dirty = True # this is needed to hide the completion menu, if visible
215-
#
215+
216216
# if there are already several lines and the cursor
217217
# is not on the last one, always insert a new \n.
218218
text = r.get_unicode()
219+
219220
if "\n" in r.buffer[r.pos :] or (
220221
r.more_lines is not None and r.more_lines(text)
221222
):
222-
#
223+
def _newline_before_pos():
224+
before_idx = r.pos - 1
225+
while before_idx > 0 and text[before_idx].isspace():
226+
before_idx -= 1
227+
return text[before_idx : r.pos].count("\n") > 0
228+
229+
# if there's already a new line before the cursor then
230+
# even if the cursor is followed by whitespace, we assume
231+
# the user is trying to terminate the block
232+
if _newline_before_pos() and text[r.pos:].isspace():
233+
self.finish = True
234+
return
235+
223236
# auto-indent the next line like the previous line
224237
prevlinestart, indent = _get_previous_line_indent(r.buffer, r.pos)
225238
r.insert("\n")

Lib/test/test_pyrepl/test_pyrepl.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -317,14 +317,21 @@ def test_multiline_edit(self):
317317
[
318318
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
319319
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
320-
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
321-
Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
322-
Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
323-
Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
324-
Event(evt="key", data="backspace", raw=bytearray(b"\x7f")),
320+
Event(evt="key", data="left", raw=bytearray(b"\x1bOD")),
321+
Event(evt="key", data="left", raw=bytearray(b"\x1bOD")),
322+
Event(evt="key", data="left", raw=bytearray(b"\x1bOD")),
323+
Event(evt="key", data="backspace", raw=bytearray(b"\x08")),
325324
Event(evt="key", data="g", raw=bytearray(b"g")),
326325
Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
327-
Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
326+
Event(evt="key", data="backspace", raw=bytearray(b"\x08")),
327+
Event(evt="key", data="delete", raw=bytearray(b"\x7F")),
328+
Event(evt="key", data="right", raw=bytearray(b"g")),
329+
Event(evt="key", data="backspace", raw=bytearray(b"\x08")),
330+
Event(evt="key", data="p", raw=bytearray(b"p")),
331+
Event(evt="key", data="a", raw=bytearray(b"a")),
332+
Event(evt="key", data="s", raw=bytearray(b"s")),
333+
Event(evt="key", data="s", raw=bytearray(b"s")),
334+
Event(evt="key", data="\n", raw=bytearray(b"\n")),
328335
Event(evt="key", data="\n", raw=bytearray(b"\n")),
329336
],
330337
)
@@ -333,7 +340,7 @@ def test_multiline_edit(self):
333340
output = multiline_input(reader)
334341
self.assertEqual(output, "def f():\n ...\n ")
335342
output = multiline_input(reader)
336-
self.assertEqual(output, "def g():\n ...\n ")
343+
self.assertEqual(output, "def g():\n pass\n ")
337344

338345
def test_history_navigation_with_up_arrow(self):
339346
events = itertools.chain(

Lib/test/test_pyrepl/test_reader.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import itertools
2+
import functools
23
from unittest import TestCase
34

4-
from .support import handle_all_events, handle_events_narrow_console, code_to_events
5+
from .support import handle_all_events, handle_events_narrow_console, code_to_events, prepare_reader
56
from _pyrepl.console import Event
67

78

@@ -133,3 +134,45 @@ def test_up_arrow_after_ctrl_r(self):
133134

134135
reader, _ = handle_all_events(events)
135136
self.assert_screen_equals(reader, "")
137+
138+
def test_newline_within_block_trailing_whitespace(self):
139+
# fmt: off
140+
code = (
141+
"def foo():\n"
142+
"a = 1\n"
143+
)
144+
# fmt: on
145+
146+
events = itertools.chain(
147+
code_to_events(code),
148+
[
149+
# go to the end of the first line
150+
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
151+
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
152+
Event(evt="key", data="\x05", raw=bytearray(b"\x1bO5")),
153+
# new lines in-block shouldn't terminate the block
154+
Event(evt="key", data="\n", raw=bytearray(b"\n")),
155+
Event(evt="key", data="\n", raw=bytearray(b"\n")),
156+
# end of line 2
157+
Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
158+
Event(evt="key", data="\x05", raw=bytearray(b"\x1bO5")),
159+
# a double new line in-block should terminate the block
160+
# even if its followed by whitespace
161+
Event(evt="key", data="\n", raw=bytearray(b"\n")),
162+
Event(evt="key", data="\n", raw=bytearray(b"\n")),
163+
],
164+
)
165+
166+
no_paste_reader = functools.partial(prepare_reader, paste_mode=False)
167+
reader, _ = handle_all_events(events, prepare_reader=no_paste_reader)
168+
169+
expected = (
170+
"def foo():\n"
171+
"\n"
172+
"\n"
173+
" a = 1\n"
174+
" \n"
175+
" " # HistoricalReader will trim trailing whitespace
176+
)
177+
self.assert_screen_equals(reader, expected)
178+
self.assertTrue(reader.finished)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
In PyREPL, updated ``maybe-accept``'s logic so that if the user hits
2+
:kbd:`Enter` twice, they are able to terminate the block even if there's
3+
trailing whitespace. Also, now when the user hits arrow up, the cursor
4+
is on the last functional line. This matches IPython's behavior.
5+
Patch by Aya Elsayed.

0 commit comments

Comments
 (0)