https://github.com/python/cpython/commit/cb0adb0d94764338ae6eedc359fc8312f04f8b69
commit: cb0adb0d94764338ae6eedc359fc8312f04f8b69
branch: 3.15
author: Miss Islington (bot) <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-07-04T13:49:59Z
summary:

[3.15] gh-152068: Reset PyREPL Colors on prompt finish (GH-152108) (#153029)

files:
A Misc/NEWS.d/next/Library/2026-07-02-19-21-15.gh-issue-152068.ThsmJU.rst
M Lib/_pyrepl/render.py
M Lib/_pyrepl/unix_console.py
M Lib/_pyrepl/windows_console.py
M Lib/test/test_pyrepl/test_unix_console.py
M Lib/test/test_pyrepl/test_windows_console.py

diff --git a/Lib/_pyrepl/render.py b/Lib/_pyrepl/render.py
index b821f35d850825e..b835778e366c539 100644
--- a/Lib/_pyrepl/render.py
+++ b/Lib/_pyrepl/render.py
@@ -4,8 +4,10 @@
 from dataclasses import dataclass, field
 from typing import Literal, Protocol, Self
 
-from .utils import ANSI_ESCAPE_SEQUENCE, THEME, StyleRef, str_width
+from _colorize import ANSIColors
+
 from .types import CursorXY
+from .utils import ANSI_ESCAPE_SEQUENCE, THEME, StyleRef, str_width
 
 type RenderStyle = StyleRef | str | None
 type LineUpdateKind = Literal[
@@ -55,7 +57,7 @@ def _style_escape(style: StyleRef) -> str:
 
 
 def _update_terminal_state(state: str, escape: str) -> str:
-    if escape in {"\x1b[0m", "\x1b[m"}:
+    if escape in {ANSIColors.RESET, "\x1b[m"}:
         return ""
     return state + escape
 
@@ -344,14 +346,14 @@ def render_cells(
             target_escape += visual_style
         if target_escape != active_escape:
             if active_escape:
-                rendered.append("\x1b[0m")
+                rendered.append(ANSIColors.RESET)
             if target_escape:
                 rendered.append(target_escape)
             active_escape = target_escape
         rendered.append(cell.text)
 
     if active_escape:
-        rendered.append("\x1b[0m")
+        rendered.append(ANSIColors.RESET)
     return "".join(rendered)
 
 
diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py
index 9c4644db53e3436..bfefa68e9207d87 100644
--- a/Lib/_pyrepl/unix_console.py
+++ b/Lib/_pyrepl/unix_console.py
@@ -36,6 +36,8 @@
 from fcntl import ioctl
 from typing import TYPE_CHECKING, cast, overload
 
+from _colorize import ANSIColors
+
 from . import terminfo
 from .console import Console, Event
 from .fancy_termios import tcgetattr, tcsetattr, TermState
@@ -517,6 +519,7 @@ def restore(self) -> None:
         Restore the console to the default state
         """
         trace("unix.restore")
+        self.__write(ANSIColors.RESET)
         self.__disable_bracketed_paste()
         self.__maybe_write_code(self._rmkx)
         self.flushoutput()
@@ -654,6 +657,7 @@ def finish(self):
         while y >= 0 and not rendered_lines[y].text:
             y -= 1
         self.__move(0, min(y, self.height + self.__offset - 1))
+        self.__write(ANSIColors.RESET)
         self.__write("\n\r")
         self.flushoutput()
 
diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py
index c1f9a19545d35fd..3768a22ad16f7bb 100644
--- a/Lib/_pyrepl/windows_console.py
+++ b/Lib/_pyrepl/windows_console.py
@@ -39,6 +39,9 @@
 )
 from ctypes import Structure, POINTER, Union
 from typing import TYPE_CHECKING
+
+from _colorize import ANSIColors
+
 from .console import Event, Console
 from .render import (
     EMPTY_RENDER_LINE,
@@ -480,6 +483,7 @@ def prepare(self) -> None:
     def restore(self) -> None:
         trace("windows.restore")
         if self.__vt_support:
+            self.__write(ANSIColors.RESET)
             # Recover to original mode before running REPL
             self._disable_bracketed_paste()
             if not SetConsoleMode(InHandle, self.__original_input_mode):
@@ -647,6 +651,7 @@ def finish(self) -> None:
         while y >= 0 and not rendered_lines[y].text:
             y -= 1
         self._move_relative(0, min(y, self.height + self.__offset - 1))
+        self.__write(ANSIColors.RESET)
         self.__write("\r\n")
 
     def flushoutput(self) -> None:
diff --git a/Lib/test/test_pyrepl/test_unix_console.py 
b/Lib/test/test_pyrepl/test_unix_console.py
index 71b2e17e3341510..2fc8398923cbf38 100644
--- a/Lib/test/test_pyrepl/test_unix_console.py
+++ b/Lib/test/test_pyrepl/test_unix_console.py
@@ -6,6 +6,7 @@
 import threading
 import unittest
 from functools import partial
+from _colorize import ANSIColors
 from test.support import force_color, os_helper, force_not_colorized_test_class
 from test.support import threading_helper
 
@@ -147,6 +148,24 @@ def test_no_newline(self, _os_write):
         self.assertNotIn(call(ANY, b'\n'), _os_write.mock_calls)
         con.restore()
 
+    def test_reset_on_finish(self, _os_write):
+        # gh-152068: finish() must emit the ANSI reset sequence so any
+        # active color does not leak past the prompt.
+        code = "1"
+        events = code_to_events(code)
+        _, con = handle_events_unix_console(events)
+        con.finish()
+        _os_write.assert_any_call(ANY, ANSIColors.RESET.encode(con.encoding))
+        con.restore()
+
+    def test_reset_on_restore(self, _os_write):
+        # gh-152068: restore() must emit the ANSI reset sequence.
+        code = "1"
+        events = code_to_events(code)
+        _, con = handle_events_unix_console(events)
+        con.restore()
+        _os_write.assert_any_call(ANY, ANSIColors.RESET.encode(con.encoding))
+
     def test_newline(self, _os_write):
         code = "\n"
         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 2b6075b3274c05c..32c4255aa6b1904 100644
--- a/Lib/test/test_pyrepl/test_windows_console.py
+++ b/Lib/test/test_pyrepl/test_windows_console.py
@@ -6,6 +6,7 @@
 
 
 import itertools
+from _colorize import ANSIColors
 from functools import partial
 from test.support import force_not_colorized_test_class
 from typing import Iterable
@@ -380,6 +381,28 @@ def test_multiline_ctrl_z(self):
         self.assertEqual(reader.cxy, (2, 3))
         con.restore()
 
+    def test_reset_on_finish(self):
+        # gh-152068: finish() must emit the ANSI reset sequence so any
+        # active color does not leak past the prompt.
+        code = "1"
+        events = code_to_events(code)
+        _, con = self.handle_events(events)
+        con.finish()
+        con.out.write.assert_any_call(ANSIColors.RESET.encode(con.encoding))
+        con.restore()
+
+    def test_reset_on_restore(self):
+        # gh-152068: restore() must emit the ANSI reset sequence when VT
+        # support is enabled.
+        code = "1"
+        events = code_to_events(code)
+        _, con = self.handle_events(events)
+        con._WindowsConsole__vt_support = True
+        con._WindowsConsole__original_input_mode = 0
+        with patch.object(wc, "SetConsoleMode", return_value=1):
+            con.restore()
+        con.out.write.assert_any_call(ANSIColors.RESET.encode(con.encoding))
+
 
 @patch.object(WindowsConsole, '__init__', _mock_console_init)
 class WindowsConsoleGetEventTests(TestCase):
diff --git 
a/Misc/NEWS.d/next/Library/2026-07-02-19-21-15.gh-issue-152068.ThsmJU.rst 
b/Misc/NEWS.d/next/Library/2026-07-02-19-21-15.gh-issue-152068.ThsmJU.rst
new file mode 100644
index 000000000000000..9ec8413b4ac424c
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-07-02-19-21-15.gh-issue-152068.ThsmJU.rst
@@ -0,0 +1 @@
+Fixes a bug when a line was split (particularly on macOS Terminal.app) in the 
middle of a colorized keyword, causing the ANSI Color Reset sequence (ESC0m) to 
not be properly printed, causing the output to be colored when it shouldn't

_______________________________________________
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