Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-118911: Trailing whitespace in a block shouldn't prevent the user from terminating the code block #119355

Merged
merged 8 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Loading