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]