From 3e03822702e3ac679ea7a685b72765f9c6c05890 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Wed, 24 Jun 2026 10:43:18 -0700 Subject: [PATCH 1/8] Use ANSI Escape Codes from Colorize --- Lib/_pyrepl/render.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Lib/_pyrepl/render.py b/Lib/_pyrepl/render.py index b821f35d850825e..66b90e5bb8a0f77 100644 --- a/Lib/_pyrepl/render.py +++ b/Lib/_pyrepl/render.py @@ -4,6 +4,8 @@ from dataclasses import dataclass, field from typing import Literal, Protocol, Self +from Lib._colorize import ANSIColors + from .utils import ANSI_ESCAPE_SEQUENCE, THEME, StyleRef, str_width from .types import CursorXY @@ -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) From 19fd3226d199a5fe938875dd401962dd78475ea4 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Wed, 24 Jun 2026 10:43:52 -0700 Subject: [PATCH 2/8] Print ANSI Color reset on finish and restore --- Lib/_pyrepl/unix_console.py | 4 ++++ Lib/_pyrepl/windows_console.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py index 9a4f8e0c623abfa..985ba14fe20f935 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, overload +from Lib._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..11f85a0e8c47c0e 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -39,6 +39,8 @@ ) from ctypes import Structure, POINTER, Union from typing import TYPE_CHECKING + +from Lib._colorize import ANSIColors from .console import Event, Console from .render import ( EMPTY_RENDER_LINE, @@ -480,6 +482,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 +650,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: From 6edc20f50581c1202047d19039d0988dcf05b4c4 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 18:00:41 +0000 Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/macOS/2026-06-24-18-00-39.gh-issue-152068.ThsmJU.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/macOS/2026-06-24-18-00-39.gh-issue-152068.ThsmJU.rst diff --git a/Misc/NEWS.d/next/macOS/2026-06-24-18-00-39.gh-issue-152068.ThsmJU.rst b/Misc/NEWS.d/next/macOS/2026-06-24-18-00-39.gh-issue-152068.ThsmJU.rst new file mode 100644 index 000000000000000..9ec8413b4ac424c --- /dev/null +++ b/Misc/NEWS.d/next/macOS/2026-06-24-18-00-39.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 3d363f1de9ad03bcf6f7d9c6e38b349863dec6e8 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Wed, 24 Jun 2026 13:29:59 -0700 Subject: [PATCH 4/8] Update imports --- Lib/_pyrepl/render.py | 4 ++-- Lib/_pyrepl/unix_console.py | 2 +- Lib/_pyrepl/windows_console.py | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Lib/_pyrepl/render.py b/Lib/_pyrepl/render.py index 66b90e5bb8a0f77..b835778e366c539 100644 --- a/Lib/_pyrepl/render.py +++ b/Lib/_pyrepl/render.py @@ -4,10 +4,10 @@ from dataclasses import dataclass, field from typing import Literal, Protocol, Self -from Lib._colorize import ANSIColors +from _colorize import ANSIColors -from .utils import ANSI_ESCAPE_SEQUENCE, THEME, StyleRef, str_width from .types import CursorXY +from .utils import ANSI_ESCAPE_SEQUENCE, THEME, StyleRef, str_width type RenderStyle = StyleRef | str | None type LineUpdateKind = Literal[ diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py index 985ba14fe20f935..8edd1c36a7232d7 100644 --- a/Lib/_pyrepl/unix_console.py +++ b/Lib/_pyrepl/unix_console.py @@ -36,7 +36,7 @@ from fcntl import ioctl from typing import TYPE_CHECKING, overload -from Lib._colorize import ANSIColors +from _colorize import ANSIColors from . import terminfo from .console import Console, Event diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 11f85a0e8c47c0e..3768a22ad16f7bb 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -40,7 +40,8 @@ from ctypes import Structure, POINTER, Union from typing import TYPE_CHECKING -from Lib._colorize import ANSIColors +from _colorize import ANSIColors + from .console import Event, Console from .render import ( EMPTY_RENDER_LINE, From 3a86dff4fa013e8dbef334e76c68f712e2c00d4f Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Thu, 2 Jul 2026 10:48:39 -0700 Subject: [PATCH 5/8] Update Misc/NEWS.d/next/macOS/2026-06-24-18-00-39.gh-issue-152068.ThsmJU.rst Co-authored-by: Pablo Galindo Salgado --- .../next/macOS/2026-06-24-18-00-39.gh-issue-152068.ThsmJU.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/macOS/2026-06-24-18-00-39.gh-issue-152068.ThsmJU.rst b/Misc/NEWS.d/next/macOS/2026-06-24-18-00-39.gh-issue-152068.ThsmJU.rst index 9ec8413b4ac424c..69c2e668e53cc25 100644 --- a/Misc/NEWS.d/next/macOS/2026-06-24-18-00-39.gh-issue-152068.ThsmJU.rst +++ b/Misc/NEWS.d/next/macOS/2026-06-24-18-00-39.gh-issue-152068.ThsmJU.rst @@ -1 +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 +Fix PyREPL leaving colorized output active when a line was split (notably on macOS Terminal.app) in the middle of a colorized keyword, so the ANSI reset sequence was not emitted. From e69cf97942eec1ba48ded4bd5eb46f1a99ce13ef Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Thu, 2 Jul 2026 12:18:57 -0700 Subject: [PATCH 6/8] Remove news entry --- .../next/macOS/2026-06-24-18-00-39.gh-issue-152068.ThsmJU.rst | 1 - 1 file changed, 1 deletion(-) delete mode 100644 Misc/NEWS.d/next/macOS/2026-06-24-18-00-39.gh-issue-152068.ThsmJU.rst diff --git a/Misc/NEWS.d/next/macOS/2026-06-24-18-00-39.gh-issue-152068.ThsmJU.rst b/Misc/NEWS.d/next/macOS/2026-06-24-18-00-39.gh-issue-152068.ThsmJU.rst deleted file mode 100644 index 69c2e668e53cc25..000000000000000 --- a/Misc/NEWS.d/next/macOS/2026-06-24-18-00-39.gh-issue-152068.ThsmJU.rst +++ /dev/null @@ -1 +0,0 @@ -Fix PyREPL leaving colorized output active when a line was split (notably on macOS Terminal.app) in the middle of a colorized keyword, so the ANSI reset sequence was not emitted. From 529de59f4bb3da325dcfe90163875b21ab6623b1 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 19:21:17 +0000 Subject: [PATCH 7/8] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2026-07-02-19-21-15.gh-issue-152068.ThsmJU.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2026-07-02-19-21-15.gh-issue-152068.ThsmJU.rst 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 e4d0eb229dff1d2802c4cce15c8ab051442d25dd Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Thu, 2 Jul 2026 13:40:08 -0700 Subject: [PATCH 8/8] gh-152068: Test ANSI reset is emitted on console finish() and restore() Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Lib/test/test_pyrepl/test_unix_console.py | 19 ++++++++++++++++ Lib/test/test_pyrepl/test_windows_console.py | 23 ++++++++++++++++++++ 2 files changed, 42 insertions(+) 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):