]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-123024: Correctly prepare/restore around help and show-history commands (#124485)
authorLysandros Nikolaou <lisandrosnik@gmail.com>
Tue, 21 Jan 2025 21:04:30 +0000 (22:04 +0100)
committerGitHub <noreply@github.com>
Tue, 21 Jan 2025 21:04:30 +0000 (21:04 +0000)
Co-authored-by: Emily Morehouse <emily@cuttlesoft.com>
Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com>
Lib/_pyrepl/commands.py
Lib/_pyrepl/console.py
Lib/_pyrepl/historical_reader.py
Lib/_pyrepl/simple_interact.py
Lib/_pyrepl/unix_console.py
Lib/_pyrepl/windows_console.py
Lib/test/test_pyrepl/test_pyrepl.py

index c3fce91013b001346f63939ae331535e3219fbd3..285841ca5e5b1c76ac52ce995d0494869569525d 100644 (file)
@@ -459,9 +459,15 @@ class show_history(Command):
         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):
index 03266c4dfc2dd85fbb60f03e97eb6773eb65b545..0d78890b4f45d5bcb85216fd7e6a68fa878e0194 100644 (file)
@@ -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
index 5d416f336ad5d2b26fe0969c06699dc97e5136e5..c4b95fa2e81ee60317ad1ac1ef5544270945986c 100644 (file)
@@ -290,13 +290,17 @@ class HistoricalReader(Reader):
 
     @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()
index a5033496712a73dbd660ae2b16be6a91ecd54a3a..a065174ad42fb67ebd95a63dabaf545986a9cd77 100644 (file)
@@ -77,7 +77,7 @@ REPL_COMMANDS = {
     "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', ''),
 }
@@ -124,18 +124,10 @@ def run_multiline_interactive_console(
         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:
index 63e8fc24dd7625a8c7603374b96d89de5dfc908b..add31d52f7865134b90983420a058595fa3e4693 100644 (file)
@@ -240,7 +240,7 @@ class UnixConsole(Console):
                 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):
@@ -250,7 +250,7 @@ class UnixConsole(Console):
             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
 
@@ -271,7 +271,7 @@ class UnixConsole(Console):
         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)
@@ -279,7 +279,7 @@ class UnixConsole(Console):
         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)
@@ -299,7 +299,7 @@ class UnixConsole(Console):
         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
 
@@ -321,7 +321,7 @@ class UnixConsole(Console):
             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):
@@ -350,7 +350,7 @@ class UnixConsole(Console):
 
         self.__buffer = []
 
-        self.__posxy = 0, 0
+        self.posxy = 0, 0
         self.__gone_tall = 0
         self.__move = self.__move_short
         self.__offset = 0
@@ -559,7 +559,7 @@ class UnixConsole(Console):
         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
@@ -644,8 +644,8 @@ class UnixConsole(Console):
         # 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
@@ -654,7 +654,7 @@ class UnixConsole(Console):
             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 (
@@ -665,7 +665,7 @@ class UnixConsole(Console):
             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 (
@@ -677,14 +677,14 @@ class UnixConsole(Console):
         ):
             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()
@@ -692,7 +692,7 @@ class UnixConsole(Console):
             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
@@ -711,32 +711,36 @@ class UnixConsole(Console):
             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:
@@ -766,12 +770,12 @@ class UnixConsole(Console):
 
     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
index e738fd09c65758bb52ab4a71e9796c4adf195934..e1ecd9845aefb46eed7ffc904e8ff00e6a55219e 100644 (file)
@@ -152,10 +152,10 @@ class WindowsConsole(Console):
             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
 
@@ -171,7 +171,7 @@ class WindowsConsole(Console):
             # 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):
@@ -197,7 +197,7 @@ class WindowsConsole(Console):
         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
 
@@ -254,11 +254,11 @@ class WindowsConsole(Console):
         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.
@@ -320,7 +320,7 @@ class WindowsConsole(Console):
         self.screen = []
         self.height, self.width = self.getheightwidth()
 
-        self.__posxy = 0, 0
+        self.posxy = 0, 0
         self.__gone_tall = 0
         self.__offset = 0
 
@@ -328,9 +328,9 @@ class WindowsConsole(Console):
         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:
@@ -349,7 +349,7 @@ class WindowsConsole(Console):
             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:
@@ -455,7 +455,7 @@ class WindowsConsole(Console):
     def clear(self) -> None:
         """Wipe the screen"""
         self.__write(CLEAR)
-        self.__posxy = 0, 0
+        self.posxy = 0, 0
         self.screen = [""]
 
     def finish(self) -> None:
index bbe19612437a1d7844d2e49a63cd62f4d1f8bee9..77e1faeabff3b29dcc56aafc4a848d59f9dec7ec 100644 (file)
@@ -1343,3 +1343,16 @@ class TestMain(ReplTestCase):
     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)