gh-123024: Correctly prepare/restore around help and show-history commands (#124485)

Co-authored-by: Emily Morehouse <emily@cuttlesoft.com>
Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com>
This commit is contained in:
Lysandros Nikolaou 2025-01-21 22:04:30 +01:00 committed by GitHub
parent d147e5e52c
commit 5a9afe2362
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 77 additions and 57 deletions

View file

@ -459,9 +459,15 @@ class show_history(Command):
from site import gethistoryfile # type: ignore[attr-defined] from site import gethistoryfile # type: ignore[attr-defined]
history = os.linesep.join(self.reader.history[:]) history = os.linesep.join(self.reader.history[:])
with self.reader.suspend(): self.reader.console.restore()
pager = get_pager() pager = get_pager()
pager(history, gethistoryfile()) pager(history, gethistoryfile())
self.reader.console.prepare()
# We need to copy over the state so that it's consistent between
# console and reader, and console does not overwrite/append stuff
self.reader.console.screen = self.reader.screen.copy()
self.reader.console.posxy = self.reader.cxy
class paste_mode(Command): class paste_mode(Command):

View file

@ -45,6 +45,7 @@ class Event:
@dataclass @dataclass
class Console(ABC): class Console(ABC):
posxy: tuple[int, int]
screen: list[str] = field(default_factory=list) screen: list[str] = field(default_factory=list)
height: int = 25 height: int = 25
width: int = 80 width: int = 80

View file

@ -290,13 +290,17 @@ class HistoricalReader(Reader):
@contextmanager @contextmanager
def suspend(self) -> SimpleContextManager: def suspend(self) -> SimpleContextManager:
with super().suspend(): with super().suspend(), self.suspend_history():
try: yield
old_history = self.history[:]
del self.history[:] @contextmanager
yield def suspend_history(self) -> SimpleContextManager:
finally: try:
self.history[:] = old_history old_history = self.history[:]
del self.history[:]
yield
finally:
self.history[:] = old_history
def prepare(self) -> None: def prepare(self) -> None:
super().prepare() super().prepare()

View file

@ -77,7 +77,7 @@ REPL_COMMANDS = {
"exit": _sitebuiltins.Quitter('exit', ''), "exit": _sitebuiltins.Quitter('exit', ''),
"quit": _sitebuiltins.Quitter('quit' ,''), "quit": _sitebuiltins.Quitter('quit' ,''),
"copyright": _sitebuiltins._Printer('copyright', sys.copyright), "copyright": _sitebuiltins._Printer('copyright', sys.copyright),
"help": "help", "help": _sitebuiltins._Helper(),
"clear": _clear_screen, "clear": _clear_screen,
"\x1a": _sitebuiltins.Quitter('\x1a', ''), "\x1a": _sitebuiltins.Quitter('\x1a', ''),
} }
@ -124,18 +124,10 @@ def run_multiline_interactive_console(
reader.history.pop() # skip internal commands in history reader.history.pop() # skip internal commands in history
command = REPL_COMMANDS[statement] command = REPL_COMMANDS[statement]
if callable(command): if callable(command):
command() # Make sure that history does not change because of commands
with reader.suspend_history():
command()
return True return True
if isinstance(command, str):
# Internal readline commands require a prepared reader like
# inside multiline_input.
reader.prepare()
reader.refresh()
reader.do_cmd((command, [statement]))
reader.restore()
return True
return False return False
while True: while True:

View file

@ -240,7 +240,7 @@ class UnixConsole(Console):
self.__hide_cursor() self.__hide_cursor()
self.__move(0, len(self.screen) - 1) self.__move(0, len(self.screen) - 1)
self.__write("\n") self.__write("\n")
self.__posxy = 0, len(self.screen) self.posxy = 0, len(self.screen)
self.screen.append("") self.screen.append("")
else: else:
while len(self.screen) < len(screen): while len(self.screen) < len(screen):
@ -250,7 +250,7 @@ class UnixConsole(Console):
self.__gone_tall = 1 self.__gone_tall = 1
self.__move = self.__move_tall self.__move = self.__move_tall
px, py = self.__posxy px, py = self.posxy
old_offset = offset = self.__offset old_offset = offset = self.__offset
height = self.height height = self.height
@ -271,7 +271,7 @@ class UnixConsole(Console):
if old_offset > offset and self._ri: if old_offset > offset and self._ri:
self.__hide_cursor() self.__hide_cursor()
self.__write_code(self._cup, 0, 0) self.__write_code(self._cup, 0, 0)
self.__posxy = 0, old_offset self.posxy = 0, old_offset
for i in range(old_offset - offset): for i in range(old_offset - offset):
self.__write_code(self._ri) self.__write_code(self._ri)
oldscr.pop(-1) oldscr.pop(-1)
@ -279,7 +279,7 @@ class UnixConsole(Console):
elif old_offset < offset and self._ind: elif old_offset < offset and self._ind:
self.__hide_cursor() self.__hide_cursor()
self.__write_code(self._cup, self.height - 1, 0) self.__write_code(self._cup, self.height - 1, 0)
self.__posxy = 0, old_offset + self.height - 1 self.posxy = 0, old_offset + self.height - 1
for i in range(offset - old_offset): for i in range(offset - old_offset):
self.__write_code(self._ind) self.__write_code(self._ind)
oldscr.pop(0) oldscr.pop(0)
@ -299,7 +299,7 @@ class UnixConsole(Console):
while y < len(oldscr): while y < len(oldscr):
self.__hide_cursor() self.__hide_cursor()
self.__move(0, y) self.__move(0, y)
self.__posxy = 0, y self.posxy = 0, y
self.__write_code(self._el) self.__write_code(self._el)
y += 1 y += 1
@ -321,7 +321,7 @@ class UnixConsole(Console):
self.event_queue.insert(Event("scroll", None)) self.event_queue.insert(Event("scroll", None))
else: else:
self.__move(x, y) self.__move(x, y)
self.__posxy = x, y self.posxy = x, y
self.flushoutput() self.flushoutput()
def prepare(self): def prepare(self):
@ -350,7 +350,7 @@ class UnixConsole(Console):
self.__buffer = [] self.__buffer = []
self.__posxy = 0, 0 self.posxy = 0, 0
self.__gone_tall = 0 self.__gone_tall = 0
self.__move = self.__move_short self.__move = self.__move_short
self.__offset = 0 self.__offset = 0
@ -559,7 +559,7 @@ class UnixConsole(Console):
self.__write_code(self._clear) self.__write_code(self._clear)
self.__gone_tall = 1 self.__gone_tall = 1
self.__move = self.__move_tall self.__move = self.__move_tall
self.__posxy = 0, 0 self.posxy = 0, 0
self.screen = [] self.screen = []
@property @property
@ -644,8 +644,8 @@ class UnixConsole(Console):
# if we need to insert a single character right after the first detected change # if we need to insert a single character right after the first detected change
if oldline[x_pos:] == newline[x_pos + 1 :] and self.ich1: if oldline[x_pos:] == newline[x_pos + 1 :] and self.ich1:
if ( if (
y == self.__posxy[1] y == self.posxy[1]
and x_coord > self.__posxy[0] and x_coord > self.posxy[0]
and oldline[px_pos:x_pos] == newline[px_pos + 1 : x_pos + 1] and oldline[px_pos:x_pos] == newline[px_pos + 1 : x_pos + 1]
): ):
x_pos = px_pos x_pos = px_pos
@ -654,7 +654,7 @@ class UnixConsole(Console):
self.__move(x_coord, y) self.__move(x_coord, y)
self.__write_code(self.ich1) self.__write_code(self.ich1)
self.__write(newline[x_pos]) self.__write(newline[x_pos])
self.__posxy = x_coord + character_width, y self.posxy = x_coord + character_width, y
# if it's a single character change in the middle of the line # if it's a single character change in the middle of the line
elif ( elif (
@ -665,7 +665,7 @@ class UnixConsole(Console):
character_width = wlen(newline[x_pos]) character_width = wlen(newline[x_pos])
self.__move(x_coord, y) self.__move(x_coord, y)
self.__write(newline[x_pos]) self.__write(newline[x_pos])
self.__posxy = x_coord + character_width, y self.posxy = x_coord + character_width, y
# if this is the last character to fit in the line and we edit in the middle of the line # if this is the last character to fit in the line and we edit in the middle of the line
elif ( elif (
@ -677,14 +677,14 @@ class UnixConsole(Console):
): ):
self.__hide_cursor() self.__hide_cursor()
self.__move(self.width - 2, y) self.__move(self.width - 2, y)
self.__posxy = self.width - 2, y self.posxy = self.width - 2, y
self.__write_code(self.dch1) self.__write_code(self.dch1)
character_width = wlen(newline[x_pos]) character_width = wlen(newline[x_pos])
self.__move(x_coord, y) self.__move(x_coord, y)
self.__write_code(self.ich1) self.__write_code(self.ich1)
self.__write(newline[x_pos]) self.__write(newline[x_pos])
self.__posxy = character_width + 1, y self.posxy = character_width + 1, y
else: else:
self.__hide_cursor() self.__hide_cursor()
@ -692,7 +692,7 @@ class UnixConsole(Console):
if wlen(oldline) > wlen(newline): if wlen(oldline) > wlen(newline):
self.__write_code(self._el) self.__write_code(self._el)
self.__write(newline[x_pos:]) self.__write(newline[x_pos:])
self.__posxy = wlen(newline), y self.posxy = wlen(newline), y
if "\x1b" in newline: if "\x1b" in newline:
# ANSI escape characters are present, so we can't assume # ANSI escape characters are present, so we can't assume
@ -711,32 +711,36 @@ class UnixConsole(Console):
self.__write_code(fmt, *args) self.__write_code(fmt, *args)
def __move_y_cuu1_cud1(self, y): def __move_y_cuu1_cud1(self, y):
dy = y - self.__posxy[1] assert self._cud1 is not None
assert self._cuu1 is not None
dy = y - self.posxy[1]
if dy > 0: if dy > 0:
self.__write_code(dy * self._cud1) self.__write_code(dy * self._cud1)
elif dy < 0: elif dy < 0:
self.__write_code((-dy) * self._cuu1) self.__write_code((-dy) * self._cuu1)
def __move_y_cuu_cud(self, y): def __move_y_cuu_cud(self, y):
dy = y - self.__posxy[1] dy = y - self.posxy[1]
if dy > 0: if dy > 0:
self.__write_code(self._cud, dy) self.__write_code(self._cud, dy)
elif dy < 0: elif dy < 0:
self.__write_code(self._cuu, -dy) self.__write_code(self._cuu, -dy)
def __move_x_hpa(self, x: int) -> None: def __move_x_hpa(self, x: int) -> None:
if x != self.__posxy[0]: if x != self.posxy[0]:
self.__write_code(self._hpa, x) self.__write_code(self._hpa, x)
def __move_x_cub1_cuf1(self, x: int) -> None: def __move_x_cub1_cuf1(self, x: int) -> None:
dx = x - self.__posxy[0] assert self._cuf1 is not None
assert self._cub1 is not None
dx = x - self.posxy[0]
if dx > 0: if dx > 0:
self.__write_code(self._cuf1 * dx) self.__write_code(self._cuf1 * dx)
elif dx < 0: elif dx < 0:
self.__write_code(self._cub1 * (-dx)) self.__write_code(self._cub1 * (-dx))
def __move_x_cub_cuf(self, x: int) -> None: def __move_x_cub_cuf(self, x: int) -> None:
dx = x - self.__posxy[0] dx = x - self.posxy[0]
if dx > 0: if dx > 0:
self.__write_code(self._cuf, dx) self.__write_code(self._cuf, dx)
elif dx < 0: elif dx < 0:
@ -766,12 +770,12 @@ class UnixConsole(Console):
def repaint(self): def repaint(self):
if not self.__gone_tall: if not self.__gone_tall:
self.__posxy = 0, self.__posxy[1] self.posxy = 0, self.posxy[1]
self.__write("\r") self.__write("\r")
ns = len(self.screen) * ["\000" * self.width] ns = len(self.screen) * ["\000" * self.width]
self.screen = ns self.screen = ns
else: else:
self.__posxy = 0, self.__offset self.posxy = 0, self.__offset
self.__move(0, self.__offset) self.__move(0, self.__offset)
ns = self.height * ["\000" * self.width] ns = self.height * ["\000" * self.width]
self.screen = ns self.screen = ns

View file

@ -152,10 +152,10 @@ class WindowsConsole(Console):
self._hide_cursor() self._hide_cursor()
self._move_relative(0, len(self.screen) - 1) self._move_relative(0, len(self.screen) - 1)
self.__write("\n") self.__write("\n")
self.__posxy = 0, len(self.screen) self.posxy = 0, len(self.screen)
self.screen.append("") self.screen.append("")
px, py = self.__posxy px, py = self.posxy
old_offset = offset = self.__offset old_offset = offset = self.__offset
height = self.height height = self.height
@ -171,7 +171,7 @@ class WindowsConsole(Console):
# portion of the window. We need to scroll the visible portion and the # portion of the window. We need to scroll the visible portion and the
# entire history # entire history
self._scroll(scroll_lines, self._getscrollbacksize()) self._scroll(scroll_lines, self._getscrollbacksize())
self.__posxy = self.__posxy[0], self.__posxy[1] + scroll_lines self.posxy = self.posxy[0], self.posxy[1] + scroll_lines
self.__offset += scroll_lines self.__offset += scroll_lines
for i in range(scroll_lines): for i in range(scroll_lines):
@ -197,7 +197,7 @@ class WindowsConsole(Console):
y = len(newscr) y = len(newscr)
while y < len(oldscr): while y < len(oldscr):
self._move_relative(0, y) self._move_relative(0, y)
self.__posxy = 0, y self.posxy = 0, y
self._erase_to_end() self._erase_to_end()
y += 1 y += 1
@ -254,11 +254,11 @@ class WindowsConsole(Console):
if wlen(newline) == self.width: if wlen(newline) == self.width:
# If we wrapped we want to start at the next line # If we wrapped we want to start at the next line
self._move_relative(0, y + 1) self._move_relative(0, y + 1)
self.__posxy = 0, y + 1 self.posxy = 0, y + 1
else: else:
self.__posxy = wlen(newline), y self.posxy = wlen(newline), y
if "\x1b" in newline or y != self.__posxy[1] or '\x1a' in newline: if "\x1b" in newline or y != self.posxy[1] or '\x1a' in newline:
# ANSI escape characters are present, so we can't assume # ANSI escape characters are present, so we can't assume
# anything about the position of the cursor. Moving the cursor # anything about the position of the cursor. Moving the cursor
# to the left margin should work to get to a known position. # to the left margin should work to get to a known position.
@ -320,7 +320,7 @@ class WindowsConsole(Console):
self.screen = [] self.screen = []
self.height, self.width = self.getheightwidth() self.height, self.width = self.getheightwidth()
self.__posxy = 0, 0 self.posxy = 0, 0
self.__gone_tall = 0 self.__gone_tall = 0
self.__offset = 0 self.__offset = 0
@ -328,9 +328,9 @@ class WindowsConsole(Console):
pass pass
def _move_relative(self, x: int, y: int) -> None: def _move_relative(self, x: int, y: int) -> None:
"""Moves relative to the current __posxy""" """Moves relative to the current posxy"""
dx = x - self.__posxy[0] dx = x - self.posxy[0]
dy = y - self.__posxy[1] dy = y - self.posxy[1]
if dx < 0: if dx < 0:
self.__write(MOVE_LEFT.format(-dx)) self.__write(MOVE_LEFT.format(-dx))
elif dx > 0: elif dx > 0:
@ -349,7 +349,7 @@ class WindowsConsole(Console):
self.event_queue.insert(0, Event("scroll", "")) self.event_queue.insert(0, Event("scroll", ""))
else: else:
self._move_relative(x, y) self._move_relative(x, y)
self.__posxy = x, y self.posxy = x, y
def set_cursor_vis(self, visible: bool) -> None: def set_cursor_vis(self, visible: bool) -> None:
if visible: if visible:
@ -455,7 +455,7 @@ class WindowsConsole(Console):
def clear(self) -> None: def clear(self) -> None:
"""Wipe the screen""" """Wipe the screen"""
self.__write(CLEAR) self.__write(CLEAR)
self.__posxy = 0, 0 self.posxy = 0, 0
self.screen = [""] self.screen = [""]
def finish(self) -> None: def finish(self) -> None:

View file

@ -1343,3 +1343,16 @@ class TestMain(ReplTestCase):
def test_keyboard_interrupt_after_isearch(self): def test_keyboard_interrupt_after_isearch(self):
output, exit_code = self.run_repl(["\x12", "\x03", "exit"]) output, exit_code = self.run_repl(["\x12", "\x03", "exit"])
self.assertEqual(exit_code, 0) self.assertEqual(exit_code, 0)
def test_prompt_after_help(self):
output, exit_code = self.run_repl(["help", "q", "exit"])
# Regex pattern to remove ANSI escape sequences
ansi_escape = re.compile(r"(\x1B(=|>|(\[)[0-?]*[ -\/]*[@-~]))")
cleaned_output = ansi_escape.sub("", output)
self.assertEqual(exit_code, 0)
# Ensure that we don't see multiple prompts after exiting `help`
# Extra stuff (newline and `exit` rewrites) are necessary
# because of how run_repl works.
self.assertNotIn(">>> \n>>> >>>", cleaned_output)