summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPablo Galindo Salgado <Pablogsal@gmail.com>2024-07-13 10:54:10 (GMT)
committerGitHub <noreply@github.com>2024-07-13 10:54:10 (GMT)
commit4b9e10d0ea352592049c1f2a00318d7274143fa4 (patch)
tree1c2c4a8484e656db28fd88dac57702c69bcf60da
parente745996b2d24b9234f896bdc2f3320e49287dcd0 (diff)
downloadcpython-4b9e10d0ea352592049c1f2a00318d7274143fa4.zip
cpython-4b9e10d0ea352592049c1f2a00318d7274143fa4.tar.gz
cpython-4b9e10d0ea352592049c1f2a00318d7274143fa4.tar.bz2
gh-121499: Fix multi-line history rendering in the REPL (#121531)
Signed-off-by: Pablo Galindo <pablogsal@gmail.com>
-rw-r--r--Lib/_pyrepl/historical_reader.py1
-rw-r--r--Lib/_pyrepl/reader.py6
-rw-r--r--Lib/test/test_pyrepl/support.py14
-rw-r--r--Lib/test/test_pyrepl/test_pyrepl.py39
-rw-r--r--Misc/NEWS.d/next/Core and Builtins/2024-07-09-13-53-18.gh-issue-121499.rpp7il.rst2
5 files changed, 62 insertions, 0 deletions
diff --git a/Lib/_pyrepl/historical_reader.py b/Lib/_pyrepl/historical_reader.py
index dd90912..7f4d067 100644
--- a/Lib/_pyrepl/historical_reader.py
+++ b/Lib/_pyrepl/historical_reader.py
@@ -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):
diff --git a/Lib/_pyrepl/reader.py b/Lib/_pyrepl/reader.py
index 1622d30..b2da038 100644
--- a/Lib/_pyrepl/reader.py
+++ b/Lib/_pyrepl/reader.py
@@ -253,6 +253,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,
@@ -265,14 +266,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)
diff --git a/Lib/test/test_pyrepl/support.py b/Lib/test/test_pyrepl/support.py
index 70e1228..58b1a92 100644
--- a/Lib/test/test_pyrepl/support.py
+++ b/Lib/test/test_pyrepl/support.py
@@ -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)
diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py
index 015b690..4320610 100644
--- a/Lib/test/test_pyrepl/test_pyrepl.py
+++ b/Lib/test/test_pyrepl/test_pyrepl.py
@@ -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):
diff --git a/Misc/NEWS.d/next/Core and Builtins/2024-07-09-13-53-18.gh-issue-121499.rpp7il.rst b/Misc/NEWS.d/next/Core and Builtins/2024-07-09-13-53-18.gh-issue-121499.rpp7il.rst
new file mode 100644
index 0000000..aec8ab9
--- /dev/null
+++ b/Misc/NEWS.d/next/Core and Builtins/2024-07-09-13-53-18.gh-issue-121499.rpp7il.rst
@@ -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