Skip to content

Commit

Permalink
pythongh-118911: Trailing whitespace in a block shouldn't prevent the…
Browse files Browse the repository at this point in the history
… user from terminating the code block (python#119355)

Co-authored-by: Łukasz Langa <lukasz@langa.pl>
  • Loading branch information
aelsayed95 and ambv authored May 22, 2024
1 parent e6572e8 commit 5091c44
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 5091c44

Please sign in to comment.