]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-152068: Reset PyREPL Colors on prompt finish (#152108)
authorEduardo Villalpando Mello <eduardovil@microsoft.com>
Sat, 4 Jul 2026 13:16:02 +0000 (06:16 -0700)
committerGitHub <noreply@github.com>
Sat, 4 Jul 2026 13:16:02 +0000 (14:16 +0100)
* 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 <Pablogsal@gmail.com>
* 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 <Pablogsal@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Lib/_pyrepl/render.py
Lib/_pyrepl/unix_console.py
Lib/_pyrepl/windows_console.py
Lib/test/test_pyrepl/test_unix_console.py
Lib/test/test_pyrepl/test_windows_console.py
Misc/NEWS.d/next/Library/2026-07-02-19-21-15.gh-issue-152068.ThsmJU.rst [new file with mode: 0644]

index b821f35d850825e5cb68ac2dc4d121b49c35e2bc..b835778e366c5391d9ab9ae3ab0e31cddb3c3ddb 100644 (file)
@@ -4,8 +4,10 @@ from collections.abc import Iterable, Sequence
 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)
 
 
index 9a4f8e0c623abfa6daee6836516087e80a448007..8edd1c36a7232d766adeabb16b6f3e61ffe30aa0 100644 (file)
@@ -36,6 +36,8 @@ from dataclasses import dataclass
 from fcntl import ioctl
 from typing import TYPE_CHECKING, overload
 
+from _colorize import ANSIColors
+
 from . import terminfo
 from .console import Console, Event
 from .fancy_termios import tcgetattr, tcsetattr, TermState
@@ -517,6 +519,7 @@ class UnixConsole(Console):
         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 @@ class UnixConsole(Console):
         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()
 
index c1f9a19545d35fd93f6e5e94cdacac3f62b5c95d..3768a22ad16f7bb44787cb0e97e775a62f35a057 100644 (file)
@@ -39,6 +39,9 @@ from ctypes.wintypes import (
 )
 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 @@ class WindowsConsole(Console):
     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 @@ class WindowsConsole(Console):
         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:
index 71b2e17e3341510b104bc656d59bde2af33652d1..2fc8398923cbf3889f6aea4386cd74bf6c14ccbe 100644 (file)
@@ -6,6 +6,7 @@ import sys
 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 @@ class TestConsole(TestCase):
         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)
index 2b6075b3274c05c6848394d5f533810920b2e0d0..32c4255aa6b1904442db5667e54661f919fb1539 100644 (file)
@@ -6,6 +6,7 @@ if sys.platform != "win32":
 
 
 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 @@ class WindowsConsoleTests(TestCase):
         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 (file)
index 0000000..9ec8413
--- /dev/null
@@ -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