Skip to content

Commit f589b69

Browse files
committed
gh-121499: Fix multi-line history rendering in the REPL
Signed-off-by: Pablo Galindo <[email protected]>
1 parent 8d0cafd commit f589b69

File tree

6 files changed

+59
-3
lines changed

6 files changed

+59
-3
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
@@ -254,6 +254,7 @@ class RefreshCache:
254254
pos: int = field(init=False)
255255
cxy: tuple[int, int] = field(init=False)
256256
dimensions: tuple[int, int] = field(init=False)
257+
invalidated: bool = False
257258

258259
def update_cache(self,
259260
reader: Reader,
@@ -266,14 +267,19 @@ def update_cache(self,
266267
self.pos = reader.pos
267268
self.cxy = reader.cxy
268269
self.dimensions = reader.console.width, reader.console.height
270+
self.invalidated = False
269271

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

276280
def get_cached_location(self, reader: Reader) -> tuple[int, int]:
281+
if self.invalidated:
282+
raise RuntimeError("Trying to use an invalid cache")
277283
offset = 0
278284
earliest_common_pos = min(reader.pos, self.pos)
279285
num_common_lines = len(self.line_end_offsets)

Lib/site.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -526,9 +526,8 @@ def register_readline():
526526

527527
def write_history():
528528
try:
529-
# _pyrepl.__main__ is executed as the __main__ module
530-
from __main__ import CAN_USE_PYREPL
531-
except ImportError:
529+
from _pyrepl.main import CAN_USE_PYREPL
530+
except ImportError as e:
532531
CAN_USE_PYREPL = False
533532

534533
try:

Lib/test/test_pyrepl/support.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ 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+
return '\n'.join(screen).replace(
48+
'\x1b[1;35m>>>\x1b[0m', '').replace('\x1b[1;35m...\x1b[0m', '').strip()
49+
50+
4151
def prepare_reader(console: Console, **kwargs):
4252
config = ReadlineConfig(readline_completer=kwargs.pop("readline_completer", None))
4353
reader = ReadlineAlikeReader(console=console, config=config)

Lib/test/test_pyrepl/test_pyrepl.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
more_lines,
1818
multiline_input,
1919
code_to_events,
20+
clean_screen
2021
)
2122
from _pyrepl.console import Event
2223
from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig
@@ -486,6 +487,7 @@ def test_basic(self):
486487

487488
output = multiline_input(reader)
488489
self.assertEqual(output, "1+1")
490+
self.assertEqual(clean_screen(reader.screen), "1+1")
489491

490492
def test_multiline_edit(self):
491493
events = itertools.chain(
@@ -515,8 +517,10 @@ def test_multiline_edit(self):
515517

516518
output = multiline_input(reader)
517519
self.assertEqual(output, "def f():\n ...\n ")
520+
self.assertEqual(clean_screen(reader.screen), "def f():\n ...")
518521
output = multiline_input(reader)
519522
self.assertEqual(output, "def g():\n pass\n ")
523+
self.assertEqual(clean_screen(reader.screen), "def g():\n pass")
520524

521525
def test_history_navigation_with_up_arrow(self):
522526
events = itertools.chain(
@@ -535,12 +539,40 @@ def test_history_navigation_with_up_arrow(self):
535539

536540
output = multiline_input(reader)
537541
self.assertEqual(output, "1+1")
542+
self.assertEqual(clean_screen(reader.screen), "1+1")
538543
output = multiline_input(reader)
539544
self.assertEqual(output, "2+2")
545+
self.assertEqual(clean_screen(reader.screen), "2+2")
540546
output = multiline_input(reader)
541547
self.assertEqual(output, "2+2")
548+
self.assertEqual(clean_screen(reader.screen), "2+2")
542549
output = multiline_input(reader)
543550
self.assertEqual(output, "1+1")
551+
self.assertEqual(clean_screen(reader.screen), "1+1")
552+
553+
def test_history_with_multiline_entries(self):
554+
code = "def foo():\nx = 1\ny = 2\nz = 3\n\ndef bar():\nreturn 42\n\n"
555+
events = list(itertools.chain(
556+
code_to_events(code),
557+
[
558+
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
559+
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
560+
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
561+
Event(evt="key", data="\n", raw=bytearray(b"\n")),
562+
Event(evt="key", data="\n", raw=bytearray(b"\n")),
563+
]
564+
))
565+
566+
reader = self.prepare_reader(events)
567+
output = multiline_input(reader)
568+
output = multiline_input(reader)
569+
output = multiline_input(reader)
570+
self.assertEqual(
571+
clean_screen(reader.screen),
572+
'def foo():\n x = 1\n y = 2\n z = 3'
573+
)
574+
self.assertEqual(output, "def foo():\n x = 1\n y = 2\n z = 3\n ")
575+
544576

545577
def test_history_navigation_with_down_arrow(self):
546578
events = itertools.chain(
@@ -558,6 +590,7 @@ def test_history_navigation_with_down_arrow(self):
558590

559591
output = multiline_input(reader)
560592
self.assertEqual(output, "1+1")
593+
self.assertEqual(clean_screen(reader.screen), "1+1")
561594

562595
def test_history_search(self):
563596
events = itertools.chain(
@@ -574,18 +607,23 @@ def test_history_search(self):
574607

575608
output = multiline_input(reader)
576609
self.assertEqual(output, "1+1")
610+
self.assertEqual(clean_screen(reader.screen), "1+1")
577611
output = multiline_input(reader)
578612
self.assertEqual(output, "2+2")
613+
self.assertEqual(clean_screen(reader.screen), "2+2")
579614
output = multiline_input(reader)
580615
self.assertEqual(output, "3+3")
616+
self.assertEqual(clean_screen(reader.screen), "3+3")
581617
output = multiline_input(reader)
582618
self.assertEqual(output, "1+1")
619+
self.assertEqual(clean_screen(reader.screen), "1+1")
583620

584621
def test_control_character(self):
585622
events = code_to_events("c\x1d\n")
586623
reader = self.prepare_reader(events)
587624
output = multiline_input(reader)
588625
self.assertEqual(output, "c\x1d")
626+
self.assertEqual(clean_screen(reader.screen), "c")
589627

590628

591629
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)