Skip to content

gh-121499: Fix multi-line history rendering in the REPL #121531

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

Merged
merged 1 commit into from
Jul 13, 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
1 change: 1 addition & 0 deletions Lib/_pyrepl/historical_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ def select_item(self, i: int) -> None:
self.historyi = i
self.pos = len(self.buffer)
self.dirty = True
self.last_refresh_cache.invalidated = True

def get_item(self, i: int) -> str:
if i != len(self.history):
Expand Down
6 changes: 6 additions & 0 deletions Lib/_pyrepl/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ class RefreshCache:
pos: int = field(init=False)
cxy: tuple[int, int] = field(init=False)
dimensions: tuple[int, int] = field(init=False)
invalidated: bool = False

def update_cache(self,
reader: Reader,
Expand All @@ -266,14 +267,19 @@ def update_cache(self,
self.pos = reader.pos
self.cxy = reader.cxy
self.dimensions = reader.console.width, reader.console.height
self.invalidated = False

def valid(self, reader: Reader) -> bool:
if self.invalidated:
return False
dimensions = reader.console.width, reader.console.height
dimensions_changed = dimensions != self.dimensions
paste_changed = reader.in_bracketed_paste != self.in_bracketed_paste
return not (dimensions_changed or paste_changed)

def get_cached_location(self, reader: Reader) -> tuple[int, int]:
if self.invalidated:
raise ValueError("Cache is invalidated")
offset = 0
earliest_common_pos = min(reader.pos, self.pos)
num_common_lines = len(self.line_end_offsets)
Expand Down
14 changes: 14 additions & 0 deletions Lib/test/test_pyrepl/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@ def code_to_events(code: str):
yield Event(evt="key", data=c, raw=bytearray(c.encode("utf-8")))


def clean_screen(screen: Iterable[str]):
"""Cleans color and console characters out of a screen output.

This is useful for screen testing, it increases the test readability since
it strips out all the unreadable side of the screen.
"""
output = []
for line in screen:
if line.startswith(">>>") or line.startswith("..."):
line = line[3:]
output.append(line)
return "\n".join(output).strip()


def prepare_reader(console: Console, **kwargs):
config = ReadlineConfig(readline_completer=kwargs.pop("readline_completer", None))
reader = ReadlineAlikeReader(console=console, config=config)
Expand Down
39 changes: 39 additions & 0 deletions Lib/test/test_pyrepl/test_pyrepl.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
more_lines,
multiline_input,
code_to_events,
clean_screen
)
from _pyrepl.console import Event
from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig
Expand Down Expand Up @@ -483,13 +484,15 @@ def prepare_reader(self, events):
console = FakeConsole(events)
config = ReadlineConfig(readline_completer=None)
reader = ReadlineAlikeReader(console=console, config=config)
reader.can_colorize = False
return reader

def test_basic(self):
reader = self.prepare_reader(code_to_events("1+1\n"))

output = multiline_input(reader)
self.assertEqual(output, "1+1")
self.assertEqual(clean_screen(reader.screen), "1+1")

def test_multiline_edit(self):
events = itertools.chain(
Expand Down Expand Up @@ -519,8 +522,10 @@ def test_multiline_edit(self):

output = multiline_input(reader)
self.assertEqual(output, "def f():\n ...\n ")
self.assertEqual(clean_screen(reader.screen), "def f():\n ...")
output = multiline_input(reader)
self.assertEqual(output, "def g():\n pass\n ")
self.assertEqual(clean_screen(reader.screen), "def g():\n pass")

def test_history_navigation_with_up_arrow(self):
events = itertools.chain(
Expand All @@ -539,12 +544,40 @@ def test_history_navigation_with_up_arrow(self):

output = multiline_input(reader)
self.assertEqual(output, "1+1")
self.assertEqual(clean_screen(reader.screen), "1+1")
output = multiline_input(reader)
self.assertEqual(output, "2+2")
self.assertEqual(clean_screen(reader.screen), "2+2")
output = multiline_input(reader)
self.assertEqual(output, "2+2")
self.assertEqual(clean_screen(reader.screen), "2+2")
output = multiline_input(reader)
self.assertEqual(output, "1+1")
self.assertEqual(clean_screen(reader.screen), "1+1")

def test_history_with_multiline_entries(self):
code = "def foo():\nx = 1\ny = 2\nz = 3\n\ndef bar():\nreturn 42\n\n"
events = list(itertools.chain(
code_to_events(code),
[
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="\n", raw=bytearray(b"\n")),
Event(evt="key", data="\n", raw=bytearray(b"\n")),
]
))

reader = self.prepare_reader(events)
output = multiline_input(reader)
output = multiline_input(reader)
output = multiline_input(reader)
self.assertEqual(
clean_screen(reader.screen),
'def foo():\n x = 1\n y = 2\n z = 3'
)
self.assertEqual(output, "def foo():\n x = 1\n y = 2\n z = 3\n ")


def test_history_navigation_with_down_arrow(self):
events = itertools.chain(
Expand All @@ -562,6 +595,7 @@ def test_history_navigation_with_down_arrow(self):

output = multiline_input(reader)
self.assertEqual(output, "1+1")
self.assertEqual(clean_screen(reader.screen), "1+1")

def test_history_search(self):
events = itertools.chain(
Expand All @@ -578,18 +612,23 @@ def test_history_search(self):

output = multiline_input(reader)
self.assertEqual(output, "1+1")
self.assertEqual(clean_screen(reader.screen), "1+1")
output = multiline_input(reader)
self.assertEqual(output, "2+2")
self.assertEqual(clean_screen(reader.screen), "2+2")
output = multiline_input(reader)
self.assertEqual(output, "3+3")
self.assertEqual(clean_screen(reader.screen), "3+3")
output = multiline_input(reader)
self.assertEqual(output, "1+1")
self.assertEqual(clean_screen(reader.screen), "1+1")

def test_control_character(self):
events = code_to_events("c\x1d\n")
reader = self.prepare_reader(events)
output = multiline_input(reader)
self.assertEqual(output, "c\x1d")
self.assertEqual(clean_screen(reader.screen), "c")


class TestPyReplCompleter(TestCase):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix a bug affecting how multi-line history was being rendered in the new
REPL after interacting with the new screen cache. Patch by Pablo Galindo
Loading