https://github.com/python/cpython/commit/8a2deea1fc725f8147254f87c6042fcf75a1d03b
commit: 8a2deea1fc725f8147254f87c6042fcf75a1d03b
branch: main
author: Jan-Eric Nitschke <[email protected]>
committer: ambv <[email protected]>
date: 2026-01-02T14:04:37+01:00
summary:

gh-128067: Fix pyrepl overriding printed output without newlines (#138732)

Co-authored-by: Łukasz Langa <[email protected]>

files:
A Misc/NEWS.d/next/Windows/2025-09-14-13-35-44.gh-issue-128067.BGdP_A.rst
M Lib/_pyrepl/unix_console.py
M Lib/_pyrepl/windows_console.py
M Lib/test/test_pyrepl/test_pyrepl.py
M Lib/test/test_pyrepl/test_unix_console.py
M Lib/test/test_pyrepl/test_windows_console.py
M Misc/ACKS

diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py
index 09247de748ee3b..937b5df6ff7d4c 100644
--- a/Lib/_pyrepl/unix_console.py
+++ b/Lib/_pyrepl/unix_console.py
@@ -251,8 +251,9 @@ def refresh(self, screen, c_xy):
         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 @@ def __tputs(self, fmt, prog=delayprog):
         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)
diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py
index f9f5988af0b9ef..a703a061e8edea 100644
--- a/Lib/_pyrepl/windows_console.py
+++ b/Lib/_pyrepl/windows_console.py
@@ -183,8 +183,9 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) 
-> None:
 
         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("")
 
@@ -501,7 +502,7 @@ def clear(self) -> None:
         """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
diff --git a/Lib/test/test_pyrepl/test_pyrepl.py 
b/Lib/test/test_pyrepl/test_pyrepl.py
index 65a252c95e5842..90cadd0b2ad494 100644
--- a/Lib/test/test_pyrepl/test_pyrepl.py
+++ b/Lib/test/test_pyrepl/test_pyrepl.py
@@ -1846,6 +1846,44 @@ def test_detect_pip_usage_in_repl(self):
                 )
                 self.assertIn(hint, output)
 
+    @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)
+
+        # Define escape sequences that don't affect cursor position or visual 
output
+        bracketed_paste_mode = r'\x1b\[\?2004[hl]'     # Enable/disable 
bracketed paste
+        application_cursor_keys = r'\x1b\[\?1[hl]'     # Enable/disable 
application cursor keys
+        application_keypad_mode = r'\x1b[=>]'          # Enable/disable 
application keypad
+        insert_character = r'\x1b\[(?:1)?@(?=[ -~])'   # Insert exactly 1 char 
(safe form)
+        cursor_visibility = r'\x1b\[\?25[hl]'          # Show/hide cursor
+        cursor_blinking = r'\x1b\[\?12[hl]'            # Start/stop cursor 
blinking
+        device_attributes = r'\x1b\[\?[01]c'           # Device Attributes 
(DA) queries/responses
+
+        safe_escapes = re.compile(
+            f'{bracketed_paste_mode}|'
+            f'{application_cursor_keys}|'
+            f'{application_keypad_mode}|'
+            f'{insert_character}|'
+            f'{cursor_visibility}|'
+            f'{cursor_blinking}|'
+            f'{device_attributes}'
+        )
+        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.
 
diff --git a/Lib/test/test_pyrepl/test_unix_console.py 
b/Lib/test/test_pyrepl/test_unix_console.py
index f4fb9237ffdfd0..680adbc2d968f0 100644
--- a/Lib/test/test_pyrepl/test_unix_console.py
+++ b/Lib/test/test_pyrepl/test_unix_console.py
@@ -102,6 +102,20 @@ def unix_console(events, **kwargs):
 @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)
diff --git a/Lib/test/test_pyrepl/test_windows_console.py 
b/Lib/test/test_pyrepl/test_windows_console.py
index f9607e02c604ff..065706472e52be 100644
--- a/Lib/test/test_pyrepl/test_windows_console.py
+++ b/Lib/test/test_pyrepl/test_windows_console.py
@@ -72,6 +72,20 @@ def handle_events_short(self, events, **kwargs):
     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)
diff --git a/Misc/ACKS b/Misc/ACKS
index bb6b6bde822a4e..671fcf88c75af9 100644
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -1352,6 +1352,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
index 00000000000000..f68cda21db7f03
--- /dev/null
+++ b/Misc/NEWS.d/next/Windows/2025-09-14-13-35-44.gh-issue-128067.BGdP_A.rst
@@ -0,0 +1 @@
+Fix a bug in PyREPL on Windows where output without a trailing newline was 
overwritten by the next prompt.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to