mirror of
https://github.com/python/cpython.git
synced 2025-08-04 08:59:19 +00:00
[3.13] gh-121499: Fix multi-line history rendering in the REPL (GH-121531) (#121679)
gh-121499: Fix multi-line history rendering in the REPL (GH-121531)
(cherry picked from commit 4b9e10d0ea
)
Signed-off-by: Pablo Galindo <pablogsal@gmail.com>
Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com>
This commit is contained in:
parent
5e8bb98419
commit
de51ee0c23
5 changed files with 62 additions and 0 deletions
|
@ -264,6 +264,7 @@ class HistoricalReader(Reader):
|
|||
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):
|
||||
|
|
|
@ -254,6 +254,7 @@ class Reader:
|
|||
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,
|
||||
|
@ -266,14 +267,19 @@ class Reader:
|
|||
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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -21,6 +21,7 @@ from .support import (
|
|||
more_lines,
|
||||
multiline_input,
|
||||
code_to_events,
|
||||
clean_screen
|
||||
)
|
||||
from _pyrepl.console import Event
|
||||
from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig
|
||||
|
@ -483,6 +484,7 @@ class TestPyReplOutput(TestCase):
|
|||
console = FakeConsole(events)
|
||||
config = ReadlineConfig(readline_completer=None)
|
||||
reader = ReadlineAlikeReader(console=console, config=config)
|
||||
reader.can_colorize = False
|
||||
return reader
|
||||
|
||||
def test_basic(self):
|
||||
|
@ -490,6 +492,7 @@ class TestPyReplOutput(TestCase):
|
|||
|
||||
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(
|
||||
|
@ -519,8 +522,10 @@ class TestPyReplOutput(TestCase):
|
|||
|
||||
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(
|
||||
|
@ -539,12 +544,40 @@ class TestPyReplOutput(TestCase):
|
|||
|
||||
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(
|
||||
|
@ -562,6 +595,7 @@ class TestPyReplOutput(TestCase):
|
|||
|
||||
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(
|
||||
|
@ -578,18 +612,23 @@ class TestPyReplOutput(TestCase):
|
|||
|
||||
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):
|
||||
|
|
|
@ -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…
Add table
Add a link
Reference in a new issue