From 6264ad9c4b04b5bd1deae02713a66e2bdc01c6b3 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sat, 4 Jul 2026 06:16:02 -0700 Subject: [PATCH 1/2] gh-152068: Reset PyREPL Colors on prompt finish (#152108) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use ANSI Escape Codes from Colorize * Print ANSI Color reset on finish and restore * 📜🤖 Added by blurb_it. * Update imports * Update Misc/NEWS.d/next/macOS/2026-06-24-18-00-39.gh-issue-152068.ThsmJU.rst Co-authored-by: Pablo Galindo Salgado * Remove news entry * 📜🤖 Added by blurb_it. * gh-152068: Test ANSI reset is emitted on console finish() and restore() Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com> Co-authored-by: Pablo Galindo Salgado Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Lib/_pyrepl/unix_console.py | 5 ++++ Lib/_pyrepl/windows_console.py | 6 +++++ Lib/test/test_pyrepl/test_unix_console.py | 21 ++++++++++++++++- Lib/test/test_pyrepl/test_windows_console.py | 23 +++++++++++++++++++ ...-07-02-19-21-15.gh-issue-152068.ThsmJU.rst | 1 + 5 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-02-19-21-15.gh-issue-152068.ThsmJU.rst diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py index 639d16db3f88d41..6b97b2e53bcfca0 100644 --- a/Lib/_pyrepl/unix_console.py +++ b/Lib/_pyrepl/unix_console.py @@ -33,6 +33,8 @@ import platform from fcntl import ioctl +from _colorize import ANSIColors + from . import terminfo from .console import Console, Event from .fancy_termios import tcgetattr, tcsetattr, TermState @@ -382,6 +384,8 @@ def restore(self): """ 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() @@ -518,6 +522,7 @@ def finish(self): while y >= 0 and not self.screen[y]: 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 46c6030748b8a6b..ee39c29f347b3be 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -37,6 +37,10 @@ SHORT, ) from ctypes import Structure, POINTER, Union +from typing import TYPE_CHECKING + +from _colorize import ANSIColors + from .console import Event, Console from .trace import trace from .utils import wlen @@ -363,6 +367,7 @@ def prepare(self) -> None: def restore(self) -> None: if self.__vt_support: + self.__write(ANSIColors.RESET) # Recover to original mode before running REPL self._disable_bracketed_paste() SetConsoleMode(InHandle, self.__original_input_mode) @@ -519,6 +524,7 @@ def finish(self) -> None: while y >= 0 and not self.screen[y]: 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 8198d489188f1e1..7b64029ae1d9e30 100644 --- a/Lib/test/test_pyrepl/test_unix_console.py +++ b/Lib/test/test_pyrepl/test_unix_console.py @@ -6,7 +6,8 @@ import threading import unittest from functools import partial -from test.support import os_helper, force_not_colorized_test_class +from _colorize import ANSIColors +from test.support import force_color, os_helper, force_not_colorized_test_class from test.support import threading_helper from unittest import TestCase @@ -107,6 +108,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 518ddee11253970..663f25f6e5308ed 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 @@ -366,6 +367,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)) + class WindowsConsoleGetEventTests(TestCase): # Virtual-Key Codes: https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes 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 From 865f762275d125886aded905ef777312da289dcc Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Wed, 22 Jul 2026 10:46:36 -0700 Subject: [PATCH 2/2] Address PR Comments --- Lib/_pyrepl/windows_console.py | 1 - Lib/test/test_pyrepl/test_unix_console.py | 2 +- Lib/test/test_pyrepl/test_windows_console.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index ee39c29f347b3be..09be054183eaf90 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -37,7 +37,6 @@ SHORT, ) from ctypes import Structure, POINTER, Union -from typing import TYPE_CHECKING from _colorize import ANSIColors diff --git a/Lib/test/test_pyrepl/test_unix_console.py b/Lib/test/test_pyrepl/test_unix_console.py index 7b64029ae1d9e30..6511e280d5ae482 100644 --- a/Lib/test/test_pyrepl/test_unix_console.py +++ b/Lib/test/test_pyrepl/test_unix_console.py @@ -7,7 +7,7 @@ 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 os_helper, force_not_colorized_test_class from test.support import threading_helper from unittest import TestCase diff --git a/Lib/test/test_pyrepl/test_windows_console.py b/Lib/test/test_pyrepl/test_windows_console.py index 663f25f6e5308ed..54905e605f3ff8e 100644 --- a/Lib/test/test_pyrepl/test_windows_console.py +++ b/Lib/test/test_pyrepl/test_windows_console.py @@ -11,7 +11,7 @@ from test.support import force_not_colorized_test_class from typing import Iterable from unittest import TestCase -from unittest.mock import MagicMock, call +from unittest.mock import MagicMock, call, patch from .support import handle_all_events, code_to_events from .support import prepare_reader as default_prepare_reader