]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.14] gh-128067: Fix pyrepl overriding printed output without newlines (GH-138732...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Fri, 2 Jan 2026 15:12:02 +0000 (16:12 +0100)
committerGitHub <noreply@github.com>
Fri, 2 Jan 2026 15:12:02 +0000 (16:12 +0100)
(cherry picked from commit 8a2deea1fc725f8147254f87c6042fcf75a1d03b)

Co-authored-by: Jan-Eric Nitschke <47750513+JanEricNitschke@users.noreply.github.com>
Co-authored-by: Łukasz Langa <lukasz@langa.pl>
Lib/_pyrepl/unix_console.py
Lib/_pyrepl/windows_console.py
Lib/test/test_pyrepl/test_pyrepl.py
Lib/test/test_pyrepl/test_unix_console.py
Lib/test/test_pyrepl/test_windows_console.py
Misc/ACKS
Misc/NEWS.d/next/Windows/2025-09-14-13-35-44.gh-issue-128067.BGdP_A.rst [new file with mode: 0644]

index 09247de748ee3bda76c9dc612fbd44ff350e7f66..937b5df6ff7d4c37fe4244d54effedb3542e8306 100644 (file)
@@ -251,8 +251,9 @@ class UnixConsole(Console):
         if not self.__gone_tall:
             while len(self.screen) < min(len(screen), self.height):
                 self.__hide_cursor()
-                self.__move(0, len(self.screen) - 1)
-                self.__write("\n")
+                if self.screen:
+                    self.__move(0, len(self.screen) - 1)
+                    self.__write("\n")
                 self.posxy = 0, len(self.screen)
                 self.screen.append("")
         else:
@@ -808,7 +809,7 @@ class UnixConsole(Console):
         will never do anyone any good."""
         # using .get() means that things will blow up
         # only if the bps is actually needed (which I'm
-        # betting is pretty unlkely)
+        # betting is pretty unlikely)
         bps = ratedict.get(self.__svtermstate.ospeed)
         while True:
             m = prog.search(fmt)
index c56dcd6d7dd434f1c8fa8b2c998eb3888a3a6939..1c4c7d434afc809a520f5f6a312f1159fe43c916 100644 (file)
@@ -183,8 +183,9 @@ class WindowsConsole(Console):
 
         while len(self.screen) < min(len(screen), self.height):
             self._hide_cursor()
-            self._move_relative(0, len(self.screen) - 1)
-            self.__write("\n")
+            if self.screen:
+                self._move_relative(0, len(self.screen) - 1)
+                self.__write("\n")
             self.posxy = 0, len(self.screen)
             self.screen.append("")
 
@@ -514,7 +515,7 @@ class WindowsConsole(Console):
         """Wipe the screen"""
         self.__write(CLEAR)
         self.posxy = 0, 0
-        self.screen = [""]
+        self.screen = []
 
     def finish(self) -> None:
         """Move the cursor to the end of the display and otherwise get
index 9cb60951586fc37583b7f2569d9609d5f7da61a5..fb55103da8c90ceda82831e65dc52b8258815051 100644 (file)
@@ -1836,6 +1836,69 @@ class TestMain(ReplTestCase):
         self.assertEqual(len(matches), 3)
 
 
+    @force_not_colorized
+    def test_no_newline(self):
+        env = os.environ.copy()
+        env.pop("PYTHON_BASIC_REPL", "")
+        env["PYTHON_BASIC_REPL"] = "1"
+
+        commands = "print('Something pretty long', end='')\nexit()\n"
+        expected_output_sequence = "Something pretty long>>> exit()"
+
+        basic_output, basic_exit_code = self.run_repl(commands, env=env)
+        self.assertEqual(basic_exit_code, 0)
+        self.assertIn(expected_output_sequence, basic_output)
+
+        output, exit_code = self.run_repl(commands)
+        self.assertEqual(exit_code, 0)
+
+        # Build patterns for escape sequences that don't affect cursor position
+        # or visual output. Use terminfo to get platform-specific sequences,
+        # falling back to hard-coded patterns for capabilities not in terminfo.
+        from _pyrepl.terminfo import TermInfo
+        ti = TermInfo(os.environ.get("TERM", ""))
+
+        safe_patterns = []
+
+        # smkx/rmkx - application cursor keys and keypad mode
+        smkx = ti.get("smkx")
+        rmkx = ti.get("rmkx")
+        if smkx:
+            safe_patterns.append(re.escape(smkx.decode("ascii")))
+        if rmkx:
+            safe_patterns.append(re.escape(rmkx.decode("ascii")))
+        if not smkx and not rmkx:
+            safe_patterns.append(r'\x1b\[\?1[hl]')  # application cursor keys
+            safe_patterns.append(r'\x1b[=>]')  # application keypad mode
+
+        # ich1 - insert character (only safe form that inserts exactly 1 char)
+        ich1 = ti.get("ich1")
+        if ich1:
+            safe_patterns.append(re.escape(ich1.decode("ascii")) + r'(?=[ -~])')
+        else:
+            safe_patterns.append(r'\x1b\[(?:1)?@(?=[ -~])')
+
+        # civis/cnorm - cursor visibility (may include cursor blinking control)
+        civis = ti.get("civis")
+        cnorm = ti.get("cnorm")
+        if civis:
+            safe_patterns.append(re.escape(civis.decode("ascii")))
+        if cnorm:
+            safe_patterns.append(re.escape(cnorm.decode("ascii")))
+        if not civis and not cnorm:
+            safe_patterns.append(r'\x1b\[\?25[hl]')  # cursor visibility
+            safe_patterns.append(r'\x1b\[\?12[hl]')  # cursor blinking
+
+        # Modern extensions not in standard terminfo - always use patterns
+        safe_patterns.append(r'\x1b\[\?2004[hl]')  # bracketed paste mode
+        safe_patterns.append(r'\x1b\[\?12[hl]')  # cursor blinking (may be separate)
+        safe_patterns.append(r'\x1b\[\?[01]c')  # device attributes
+
+        safe_escapes = re.compile('|'.join(safe_patterns))
+        cleaned_output = safe_escapes.sub('', output)
+        self.assertIn(expected_output_sequence, cleaned_output)
+
+
 class TestPyReplCtrlD(TestCase):
     """Test Ctrl+D behavior in _pyrepl to match old pre-3.13 REPL behavior.
 
index f4fb9237ffdfd0d6139854859e9dc8b204a619bc..680adbc2d968f0c98201df01854e23dd0572b691 100644 (file)
@@ -102,6 +102,20 @@ handle_events_unix_console_height_3 = partial(
 @patch("os.write")
 @force_not_colorized_test_class
 class TestConsole(TestCase):
+    def test_no_newline(self, _os_write):
+        code = "1"
+        events = code_to_events(code)
+        _, con = handle_events_unix_console(events)
+        self.assertNotIn(call(ANY, b'\n'), _os_write.mock_calls)
+        con.restore()
+
+    def test_newline(self, _os_write):
+        code = "\n"
+        events = code_to_events(code)
+        _, con = handle_events_unix_console(events)
+        _os_write.assert_any_call(ANY, b"\n")
+        con.restore()
+
     def test_simple_addition(self, _os_write):
         code = "12+34"
         events = code_to_events(code)
index f9607e02c604ff8cf764e23a6b2ee5f321b96337..065706472e52be475d91e982cc85a863df549947 100644 (file)
@@ -72,6 +72,20 @@ class WindowsConsoleTests(TestCase):
     def handle_events_height_3(self, events):
         return self.handle_events(events, height=3)
 
+    def test_no_newline(self):
+        code = "1"
+        events = code_to_events(code)
+        _, con = self.handle_events(events)
+        self.assertNotIn(call(b'\n'), con.out.write.mock_calls)
+        con.restore()
+
+    def test_newline(self):
+        code = "\n"
+        events = code_to_events(code)
+        _, con = self.handle_events(events)
+        con.out.write.assert_any_call(b"\n")
+        con.restore()
+
     def test_simple_addition(self):
         code = "12+34"
         events = code_to_events(code)
index 5dde69f43052ddde0cbaead1f82898b81415ae2b..949df55ea3912d4997f89010a470fc7875a00340 100644 (file)
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -1344,6 +1344,7 @@ Gustavo Niemeyer
 Oscar Nierstrasz
 Lysandros Nikolaou
 Hrvoje Nikšić
+Jan-Eric Nitschke
 Gregory Nofi
 Jesse Noller
 Bill Noon
diff --git a/Misc/NEWS.d/next/Windows/2025-09-14-13-35-44.gh-issue-128067.BGdP_A.rst b/Misc/NEWS.d/next/Windows/2025-09-14-13-35-44.gh-issue-128067.BGdP_A.rst
new file mode 100644 (file)
index 0000000..f68cda2
--- /dev/null
@@ -0,0 +1 @@
+Fix a bug in PyREPL on Windows where output without a trailing newline was overwritten by the next prompt.