Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions Lib/_pyrepl/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,9 +459,15 @@ def do(self) -> None:
from site import gethistoryfile # type: ignore[attr-defined]

history = os.linesep.join(self.reader.history[:])
with self.reader.suspend():
pager = get_pager()
pager(history, gethistoryfile())
self.reader.console.restore()
pager = get_pager()
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):
Expand Down
1 change: 1 addition & 0 deletions Lib/_pyrepl/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class Event:

@dataclass
class Console(ABC):
posxy: tuple[int, int]
screen: list[str] = field(default_factory=list)
height: int = 25
width: int = 80
Expand Down
18 changes: 11 additions & 7 deletions Lib/_pyrepl/historical_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,13 +290,17 @@ def get_item(self, i: int) -> str:

@contextmanager
def suspend(self) -> SimpleContextManager:
with super().suspend():
try:
old_history = self.history[:]
del self.history[:]
yield
finally:
self.history[:] = old_history
with super().suspend(), self.suspend_history():
yield

@contextmanager
def suspend_history(self) -> SimpleContextManager:
try:
old_history = self.history[:]
del self.history[:]
yield
finally:
self.history[:] = old_history

def prepare(self) -> None:
super().prepare()
Expand Down
16 changes: 4 additions & 12 deletions Lib/_pyrepl/simple_interact.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def _clear_screen():
"exit": _sitebuiltins.Quitter('exit', ''),
"quit": _sitebuiltins.Quitter('quit' ,''),
"copyright": _sitebuiltins._Printer('copyright', sys.copyright),
"help": "help",
"help": _sitebuiltins._Helper(),
"clear": _clear_screen,
"\x1a": _sitebuiltins.Quitter('\x1a', ''),
}
Expand Down Expand Up @@ -124,18 +124,10 @@ def maybe_run_command(statement: str) -> bool:
reader.history.pop() # skip internal commands in history
command = REPL_COMMANDS[statement]
if callable(command):
command()
# Make sure that history does not change because of commands
with reader.suspend_history():
command()
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

while True:
Expand Down
48 changes: 26 additions & 22 deletions Lib/_pyrepl/unix_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ def refresh(self, screen, c_xy):
self.__hide_cursor()
self.__move(0, len(self.screen) - 1)
self.__write("\n")
self.__posxy = 0, len(self.screen)
self.posxy = 0, len(self.screen)
self.screen.append("")
else:
while len(self.screen) < len(screen):
Expand All @@ -250,7 +250,7 @@ def refresh(self, screen, c_xy):
self.__gone_tall = 1
self.__move = self.__move_tall

px, py = self.__posxy
px, py = self.posxy
old_offset = offset = self.__offset
height = self.height

Expand All @@ -271,15 +271,15 @@ def refresh(self, screen, c_xy):
if old_offset > offset and self._ri:
self.__hide_cursor()
self.__write_code(self._cup, 0, 0)
self.__posxy = 0, old_offset
self.posxy = 0, old_offset
for i in range(old_offset - offset):
self.__write_code(self._ri)
oldscr.pop(-1)
oldscr.insert(0, "")
elif old_offset < offset and self._ind:
self.__hide_cursor()
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):
self.__write_code(self._ind)
oldscr.pop(0)
Expand All @@ -299,7 +299,7 @@ def refresh(self, screen, c_xy):
while y < len(oldscr):
self.__hide_cursor()
self.__move(0, y)
self.__posxy = 0, y
self.posxy = 0, y
self.__write_code(self._el)
y += 1

Expand All @@ -321,7 +321,7 @@ def move_cursor(self, x, y):
self.event_queue.insert(Event("scroll", None))
else:
self.__move(x, y)
self.__posxy = x, y
self.posxy = x, y
self.flushoutput()

def prepare(self):
Expand Down Expand Up @@ -350,7 +350,7 @@ def prepare(self):

self.__buffer = []

self.__posxy = 0, 0
self.posxy = 0, 0
self.__gone_tall = 0
self.__move = self.__move_short
self.__offset = 0
Expand Down Expand Up @@ -559,7 +559,7 @@ def clear(self):
self.__write_code(self._clear)
self.__gone_tall = 1
self.__move = self.__move_tall
self.__posxy = 0, 0
self.posxy = 0, 0
self.screen = []

@property
Expand Down Expand Up @@ -644,8 +644,8 @@ def __write_changed_line(self, y, oldline, newline, px_coord):
# 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 (
y == self.__posxy[1]
and x_coord > self.__posxy[0]
y == self.posxy[1]
and x_coord > self.posxy[0]
and oldline[px_pos:x_pos] == newline[px_pos + 1 : x_pos + 1]
):
x_pos = px_pos
Expand All @@ -654,7 +654,7 @@ def __write_changed_line(self, y, oldline, newline, px_coord):
self.__move(x_coord, y)
self.__write_code(self.ich1)
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
elif (
Expand All @@ -665,7 +665,7 @@ def __write_changed_line(self, y, oldline, newline, px_coord):
character_width = wlen(newline[x_pos])
self.__move(x_coord, y)
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
elif (
Expand All @@ -677,22 +677,22 @@ def __write_changed_line(self, y, oldline, newline, px_coord):
):
self.__hide_cursor()
self.__move(self.width - 2, y)
self.__posxy = self.width - 2, y
self.posxy = self.width - 2, y
self.__write_code(self.dch1)

character_width = wlen(newline[x_pos])
self.__move(x_coord, y)
self.__write_code(self.ich1)
self.__write(newline[x_pos])
self.__posxy = character_width + 1, y
self.posxy = character_width + 1, y

else:
self.__hide_cursor()
self.__move(x_coord, y)
if wlen(oldline) > wlen(newline):
self.__write_code(self._el)
self.__write(newline[x_pos:])
self.__posxy = wlen(newline), y
self.posxy = wlen(newline), y

if "\x1b" in newline:
# ANSI escape characters are present, so we can't assume
Expand All @@ -711,32 +711,36 @@ def __maybe_write_code(self, fmt, *args):
self.__write_code(fmt, *args)

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:
self.__write_code(dy * self._cud1)
elif dy < 0:
self.__write_code((-dy) * self._cuu1)

def __move_y_cuu_cud(self, y):
dy = y - self.__posxy[1]
dy = y - self.posxy[1]
if dy > 0:
self.__write_code(self._cud, dy)
elif dy < 0:
self.__write_code(self._cuu, -dy)

def __move_x_hpa(self, x: int) -> None:
if x != self.__posxy[0]:
if x != self.posxy[0]:
self.__write_code(self._hpa, x)

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:
self.__write_code(self._cuf1 * dx)
elif dx < 0:
self.__write_code(self._cub1 * (-dx))

def __move_x_cub_cuf(self, x: int) -> None:
dx = x - self.__posxy[0]
dx = x - self.posxy[0]
if dx > 0:
self.__write_code(self._cuf, dx)
elif dx < 0:
Expand Down Expand Up @@ -766,12 +770,12 @@ def __show_cursor(self):

def repaint(self):
if not self.__gone_tall:
self.__posxy = 0, self.__posxy[1]
self.posxy = 0, self.posxy[1]
self.__write("\r")
ns = len(self.screen) * ["\000" * self.width]
self.screen = ns
else:
self.__posxy = 0, self.__offset
self.posxy = 0, self.__offset
self.__move(0, self.__offset)
ns = self.height * ["\000" * self.width]
self.screen = ns
Expand Down
26 changes: 13 additions & 13 deletions Lib/_pyrepl/windows_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,10 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None:
self._hide_cursor()
self._move_relative(0, len(self.screen) - 1)
self.__write("\n")
self.__posxy = 0, len(self.screen)
self.posxy = 0, len(self.screen)
self.screen.append("")

px, py = self.__posxy
px, py = self.posxy
old_offset = offset = self.__offset
height = self.height

Expand All @@ -171,7 +171,7 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None:
# portion of the window. We need to scroll the visible portion and the
# entire history
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

for i in range(scroll_lines):
Expand All @@ -197,7 +197,7 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None:
y = len(newscr)
while y < len(oldscr):
self._move_relative(0, y)
self.__posxy = 0, y
self.posxy = 0, y
self._erase_to_end()
y += 1

Expand Down Expand Up @@ -254,11 +254,11 @@ def __write_changed_line(
if wlen(newline) == self.width:
# If we wrapped we want to start at the next line
self._move_relative(0, y + 1)
self.__posxy = 0, y + 1
self.posxy = 0, y + 1
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
# anything about the position of the cursor. Moving the cursor
# to the left margin should work to get to a known position.
Expand Down Expand Up @@ -320,17 +320,17 @@ def prepare(self) -> None:
self.screen = []
self.height, self.width = self.getheightwidth()

self.__posxy = 0, 0
self.posxy = 0, 0
self.__gone_tall = 0
self.__offset = 0

def restore(self) -> None:
pass

def _move_relative(self, x: int, y: int) -> None:
"""Moves relative to the current __posxy"""
dx = x - self.__posxy[0]
dy = y - self.__posxy[1]
"""Moves relative to the current posxy"""
dx = x - self.posxy[0]
dy = y - self.posxy[1]
if dx < 0:
self.__write(MOVE_LEFT.format(-dx))
elif dx > 0:
Expand All @@ -349,7 +349,7 @@ def move_cursor(self, x: int, y: int) -> None:
self.event_queue.insert(0, Event("scroll", ""))
else:
self._move_relative(x, y)
self.__posxy = x, y
self.posxy = x, y

def set_cursor_vis(self, visible: bool) -> None:
if visible:
Expand Down Expand Up @@ -455,7 +455,7 @@ def beep(self) -> None:
def clear(self) -> None:
"""Wipe the screen"""
self.__write(CLEAR)
self.__posxy = 0, 0
self.posxy = 0, 0
self.screen = [""]

def finish(self) -> None:
Expand Down
13 changes: 13 additions & 0 deletions Lib/test/test_pyrepl/test_pyrepl.py
Original file line number Diff line number Diff line change
Expand Up @@ -1343,3 +1343,16 @@ def test_readline_history_file(self):
def test_keyboard_interrupt_after_isearch(self):
output, exit_code = self.run_repl(["\x12", "\x03", "exit"])
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)
Loading