Skip to content

Commit 4b9e10d

Browse files
authored
gh-121499: Fix multi-line history rendering in the REPL (#121531)
Signed-off-by: Pablo Galindo <[email protected]>
1 parent e745996 commit 4b9e10d

File tree

5 files changed

+62
-0
lines changed

5 files changed

+62
-0
lines changed

Lib/_pyrepl/historical_reader.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ def select_item(self, i: int) -> None:
264264
self.historyi = i
265265
self.pos = len(self.buffer)
266266
self.dirty = True
267+
self.last_refresh_cache.invalidated = True
267268

268269
def get_item(self, i: int) -> str:
269270
if i != len(self.history):

Lib/_pyrepl/reader.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ class RefreshCache:
253253
pos: int = field(init=False)
254254
cxy: tuple[int, int] = field(init=False)
255255
dimensions: tuple[int, int] = field(init=False)
256+
invalidated: bool = False
256257

257258
def update_cache(self,
258259
reader: Reader,
@@ -265,14 +266,19 @@ def update_cache(self,
265266
self.pos = reader.pos
266267
self.cxy = reader.cxy
267268
self.dimensions = reader.console.width, reader.console.height
269+
self.invalidated = False
268270

269271
def valid(self, reader: Reader) -> bool:
272+
if self.invalidated:
273+
return False
270274
dimensions = reader.console.width, reader.console.height
271275
dimensions_changed = dimensions != self.dimensions
272276
paste_changed = reader.in_bracketed_paste != self.in_bracketed_paste
273277
return not (dimensions_changed or paste_changed)
274278

275279
def get_cached_location(self, reader: Reader) -> tuple[int, int]:
280+
if self.invalidated:
281+
raise ValueError("Cache is invalidated")
276282
offset = 0
277283
earliest_common_pos = min(reader.pos, self.pos)
278284
num_common_lines = len(self.line_end_offsets)

Lib/test/test_pyrepl/support.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,20 @@ def code_to_events(code: str):
3838
yield Event(evt="key", data=c, raw=bytearray(c.encode("utf-8")))
3939

4040

41+
def clean_screen(screen: Iterable[str]):
42+
"""Cleans color and console characters out of a screen output.
43+
44+
This is useful for screen testing, it increases the test readability since
45+
it strips out all the unreadable side of the screen.
46+
"""
47+
output = []
48+
for line in screen:
49+
if line.startswith(">>>") or line.startswith("..."):
50+
line = line[3:]
51+
output.append(line)
52+
return "\n".join(output).strip()
53+
54+
4155
def prepare_reader(console: Console, **kwargs):
4256
config = ReadlineConfig(readline_completer=kwargs.pop("readline_completer", None))
4357
reader = ReadlineAlikeReader(console=console, config=config)

Lib/test/test_pyrepl/test_pyrepl.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
more_lines,
2222
multiline_input,
2323
code_to_events,
24+
clean_screen
2425
)
2526
from _pyrepl.console import Event
2627
from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig
@@ -483,13 +484,15 @@ def prepare_reader(self, events):
483484
console = FakeConsole(events)
484485
config = ReadlineConfig(readline_completer=None)
485486
reader = ReadlineAlikeReader(console=console, config=config)
487+
reader.can_colorize = False
486488
return reader
487489

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

491493
output = multiline_input(reader)
492494
self.assertEqual(output, "1+1")
495+
self.assertEqual(clean_screen(reader.screen), "1+1")
493496

494497
def test_multiline_edit(self):
495498
events = itertools.chain(
@@ -519,8 +522,10 @@ def test_multiline_edit(self):
519522

520523
output = multiline_input(reader)
521524
self.assertEqual(output, "def f():\n ...\n ")
525+
self.assertEqual(clean_screen(reader.screen), "def f():\n ...")
522526
output = multiline_input(reader)
523527
self.assertEqual(output, "def g():\n pass\n ")
528+
self.assertEqual(clean_screen(reader.screen), "def g():\n pass")
524529

525530
def test_history_navigation_with_up_arrow(self):
526531
events = itertools.chain(
@@ -539,12 +544,40 @@ def test_history_navigation_with_up_arrow(self):
539544

540545
output = multiline_input(reader)
541546
self.assertEqual(output, "1+1")
547+
self.assertEqual(clean_screen(reader.screen), "1+1")
542548
output = multiline_input(reader)
543549
self.assertEqual(output, "2+2")
550+
self.assertEqual(clean_screen(reader.screen), "2+2")
544551
output = multiline_input(reader)
545552
self.assertEqual(output, "2+2")
553+
self.assertEqual(clean_screen(reader.screen), "2+2")
546554
output = multiline_input(reader)
547555
self.assertEqual(output, "1+1")
556+
self.assertEqual(clean_screen(reader.screen), "1+1")
557+
558+
def test_history_with_multiline_entries(self):
559+
code = "def foo():\nx = 1\ny = 2\nz = 3\n\ndef bar():\nreturn 42\n\n"
560+
events = list(itertools.chain(
561+
code_to_events(code),
562+
[
563+
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
564+
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
565+
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
566+
Event(evt="key", data="\n", raw=bytearray(b"\n")),
567+
Event(evt="key", data="\n", raw=bytearray(b"\n")),
568+
]
569+
))
570+
571+
reader = self.prepare_reader(events)
572+
output = multiline_input(reader)
573+
output = multiline_input(reader)
574+
output = multiline_input(reader)
575+
self.assertEqual(
576+
clean_screen(reader.screen),
577+
'def foo():\n x = 1\n y = 2\n z = 3'
578+
)
579+
self.assertEqual(output, "def foo():\n x = 1\n y = 2\n z = 3\n ")
580+
548581

549582
def test_history_navigation_with_down_arrow(self):
550583
events = itertools.chain(
@@ -562,6 +595,7 @@ def test_history_navigation_with_down_arrow(self):
562595

563596
output = multiline_input(reader)
564597
self.assertEqual(output, "1+1")
598+
self.assertEqual(clean_screen(reader.screen), "1+1")
565599

566600
def test_history_search(self):
567601
events = itertools.chain(
@@ -578,18 +612,23 @@ def test_history_search(self):
578612

579613
output = multiline_input(reader)
580614
self.assertEqual(output, "1+1")
615+
self.assertEqual(clean_screen(reader.screen), "1+1")
581616
output = multiline_input(reader)
582617
self.assertEqual(output, "2+2")
618+
self.assertEqual(clean_screen(reader.screen), "2+2")
583619
output = multiline_input(reader)
584620
self.assertEqual(output, "3+3")
621+
self.assertEqual(clean_screen(reader.screen), "3+3")
585622
output = multiline_input(reader)
586623
self.assertEqual(output, "1+1")
624+
self.assertEqual(clean_screen(reader.screen), "1+1")
587625

588626
def test_control_character(self):
589627
events = code_to_events("c\x1d\n")
590628
reader = self.prepare_reader(events)
591629
output = multiline_input(reader)
592630
self.assertEqual(output, "c\x1d")
631+
self.assertEqual(clean_screen(reader.screen), "c")
593632

594633

595634
class TestPyReplCompleter(TestCase):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix a bug affecting how multi-line history was being rendered in the new
2+
REPL after interacting with the new screen cache. Patch by Pablo Galindo

0 commit comments

Comments
 (0)