Skip to content

Commit

Permalink
[3.13] gh-118911: Trailing whitespace in a block shouldn't prevent th…
Browse files Browse the repository at this point in the history
…e user from terminating the code block (GH-119355) (#119404)

(cherry picked from commit 5091c44)

Co-authored-by: Aya Elsayed <ayah.ehab11@gmail.com>
Co-authored-by: Łukasz Langa <lukasz@langa.pl>
  • Loading branch information
3 people committed May 23, 2024
1 parent dbff1f1 commit 9fa1b4f
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 9 deletions.
2 changes: 1 addition & 1 deletion Lib/_pyrepl/historical_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ def select_item(self, i: int) -> None:
self.transient_history[self.historyi] = self.get_unicode()
buf = self.transient_history.get(i)
if buf is None:
buf = self.history[i]
buf = self.history[i].rstrip()
self.buffer = list(buf)
self.historyi = i
self.pos = len(self.buffer)
Expand Down
17 changes: 15 additions & 2 deletions Lib/_pyrepl/readline.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,14 +244,27 @@ def do(self) -> None:
r: ReadlineAlikeReader
r = self.reader # type: ignore[assignment]
r.dirty = True # this is needed to hide the completion menu, if visible
#

# if there are already several lines and the cursor
# is not on the last one, always insert a new \n.
text = r.get_unicode()

if "\n" in r.buffer[r.pos :] or (
r.more_lines is not None and r.more_lines(text)
):
#
def _newline_before_pos():
before_idx = r.pos - 1
while before_idx > 0 and text[before_idx].isspace():
before_idx -= 1
return text[before_idx : r.pos].count("\n") > 0

# if there's already a new line before the cursor then
# even if the cursor is followed by whitespace, we assume
# the user is trying to terminate the block
if _newline_before_pos() and text[r.pos:].isspace():
self.finish = True
return

# auto-indent the next line like the previous line
prevlinestart, indent = _get_previous_line_indent(r.buffer, r.pos)
r.insert("\n")
Expand Down
19 changes: 14 additions & 5 deletions Lib/test/test_pyrepl/test_pyrepl.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,12 +405,21 @@ def test_multiline_edit(self):
[
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
Event(evt="key", data="backspace", raw=bytearray(b"\x7f")),
Event(evt="key", data="left", raw=bytearray(b"\x1bOD")),
Event(evt="key", data="left", raw=bytearray(b"\x1bOD")),
Event(evt="key", data="left", raw=bytearray(b"\x1bOD")),
Event(evt="key", data="backspace", raw=bytearray(b"\x08")),
Event(evt="key", data="g", raw=bytearray(b"g")),
Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
Event(evt="key", data="backspace", raw=bytearray(b"\x08")),
Event(evt="key", data="delete", raw=bytearray(b"\x7F")),
Event(evt="key", data="right", raw=bytearray(b"g")),
Event(evt="key", data="backspace", raw=bytearray(b"\x08")),
Event(evt="key", data="p", raw=bytearray(b"p")),
Event(evt="key", data="a", raw=bytearray(b"a")),
Event(evt="key", data="s", raw=bytearray(b"s")),
Event(evt="key", data="s", raw=bytearray(b"s")),
Event(evt="key", data="\n", raw=bytearray(b"\n")),
Event(evt="key", data="\n", raw=bytearray(b"\n")),
],
)
Expand All @@ -419,7 +428,7 @@ def test_multiline_edit(self):
output = multiline_input(reader)
self.assertEqual(output, "def f():\n ...\n ")
output = multiline_input(reader)
self.assertEqual(output, "def g():\n ...\n ")
self.assertEqual(output, "def g():\n pass\n ")

def test_history_navigation_with_up_arrow(self):
events = itertools.chain(
Expand Down
45 changes: 44 additions & 1 deletion Lib/test/test_pyrepl/test_reader.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import itertools
import functools
from unittest import TestCase

from .support import handle_all_events, handle_events_narrow_console, code_to_events
from .support import handle_all_events, handle_events_narrow_console, code_to_events, prepare_reader
from _pyrepl.console import Event


Expand Down Expand Up @@ -133,3 +134,45 @@ def test_up_arrow_after_ctrl_r(self):

reader, _ = handle_all_events(events)
self.assert_screen_equals(reader, "")

def test_newline_within_block_trailing_whitespace(self):
# fmt: off
code = (
"def foo():\n"
"a = 1\n"
)
# fmt: on

events = itertools.chain(
code_to_events(code),
[
# go to the end of the first line
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
Event(evt="key", data="\x05", raw=bytearray(b"\x1bO5")),
# new lines in-block shouldn't terminate the block
Event(evt="key", data="\n", raw=bytearray(b"\n")),
Event(evt="key", data="\n", raw=bytearray(b"\n")),
# end of line 2
Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
Event(evt="key", data="\x05", raw=bytearray(b"\x1bO5")),
# a double new line in-block should terminate the block
# even if its followed by whitespace
Event(evt="key", data="\n", raw=bytearray(b"\n")),
Event(evt="key", data="\n", raw=bytearray(b"\n")),
],
)

no_paste_reader = functools.partial(prepare_reader, paste_mode=False)
reader, _ = handle_all_events(events, prepare_reader=no_paste_reader)

expected = (
"def foo():\n"
"\n"
"\n"
" a = 1\n"
" \n"
" " # HistoricalReader will trim trailing whitespace
)
self.assert_screen_equals(reader, expected)
self.assertTrue(reader.finished)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
In PyREPL, updated ``maybe-accept``'s logic so that if the user hits
:kbd:`Enter` twice, they are able to terminate the block even if there's
trailing whitespace. Also, now when the user hits arrow up, the cursor
is on the last functional line. This matches IPython's behavior.
Patch by Aya Elsayed.

0 comments on commit 9fa1b4f

Please sign in to comment.