Skip to content

Commit cb0adb0

Browse files
[3.15] gh-152068: Reset PyREPL Colors on prompt finish (GH-152108) (#153029)
1 parent fdc17ca commit cb0adb0

6 files changed

Lines changed: 58 additions & 4 deletions

File tree

Lib/_pyrepl/render.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
from dataclasses import dataclass, field
55
from typing import Literal, Protocol, Self
66

7-
from .utils import ANSI_ESCAPE_SEQUENCE, THEME, StyleRef, str_width
7+
from _colorize import ANSIColors
8+
89
from .types import CursorXY
10+
from .utils import ANSI_ESCAPE_SEQUENCE, THEME, StyleRef, str_width
911

1012
type RenderStyle = StyleRef | str | None
1113
type LineUpdateKind = Literal[
@@ -55,7 +57,7 @@ def _style_escape(style: StyleRef) -> str:
5557

5658

5759
def _update_terminal_state(state: str, escape: str) -> str:
58-
if escape in {"\x1b[0m", "\x1b[m"}:
60+
if escape in {ANSIColors.RESET, "\x1b[m"}:
5961
return ""
6062
return state + escape
6163

@@ -344,14 +346,14 @@ def render_cells(
344346
target_escape += visual_style
345347
if target_escape != active_escape:
346348
if active_escape:
347-
rendered.append("\x1b[0m")
349+
rendered.append(ANSIColors.RESET)
348350
if target_escape:
349351
rendered.append(target_escape)
350352
active_escape = target_escape
351353
rendered.append(cell.text)
352354

353355
if active_escape:
354-
rendered.append("\x1b[0m")
356+
rendered.append(ANSIColors.RESET)
355357
return "".join(rendered)
356358

357359

Lib/_pyrepl/unix_console.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
from fcntl import ioctl
3737
from typing import TYPE_CHECKING, cast, overload
3838

39+
from _colorize import ANSIColors
40+
3941
from . import terminfo
4042
from .console import Console, Event
4143
from .fancy_termios import tcgetattr, tcsetattr, TermState
@@ -517,6 +519,7 @@ def restore(self) -> None:
517519
Restore the console to the default state
518520
"""
519521
trace("unix.restore")
522+
self.__write(ANSIColors.RESET)
520523
self.__disable_bracketed_paste()
521524
self.__maybe_write_code(self._rmkx)
522525
self.flushoutput()
@@ -654,6 +657,7 @@ def finish(self):
654657
while y >= 0 and not rendered_lines[y].text:
655658
y -= 1
656659
self.__move(0, min(y, self.height + self.__offset - 1))
660+
self.__write(ANSIColors.RESET)
657661
self.__write("\n\r")
658662
self.flushoutput()
659663

Lib/_pyrepl/windows_console.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@
3939
)
4040
from ctypes import Structure, POINTER, Union
4141
from typing import TYPE_CHECKING
42+
43+
from _colorize import ANSIColors
44+
4245
from .console import Event, Console
4346
from .render import (
4447
EMPTY_RENDER_LINE,
@@ -480,6 +483,7 @@ def prepare(self) -> None:
480483
def restore(self) -> None:
481484
trace("windows.restore")
482485
if self.__vt_support:
486+
self.__write(ANSIColors.RESET)
483487
# Recover to original mode before running REPL
484488
self._disable_bracketed_paste()
485489
if not SetConsoleMode(InHandle, self.__original_input_mode):
@@ -647,6 +651,7 @@ def finish(self) -> None:
647651
while y >= 0 and not rendered_lines[y].text:
648652
y -= 1
649653
self._move_relative(0, min(y, self.height + self.__offset - 1))
654+
self.__write(ANSIColors.RESET)
650655
self.__write("\r\n")
651656

652657
def flushoutput(self) -> None:

Lib/test/test_pyrepl/test_unix_console.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import threading
77
import unittest
88
from functools import partial
9+
from _colorize import ANSIColors
910
from test.support import force_color, os_helper, force_not_colorized_test_class
1011
from test.support import threading_helper
1112

@@ -147,6 +148,24 @@ def test_no_newline(self, _os_write):
147148
self.assertNotIn(call(ANY, b'\n'), _os_write.mock_calls)
148149
con.restore()
149150

151+
def test_reset_on_finish(self, _os_write):
152+
# gh-152068: finish() must emit the ANSI reset sequence so any
153+
# active color does not leak past the prompt.
154+
code = "1"
155+
events = code_to_events(code)
156+
_, con = handle_events_unix_console(events)
157+
con.finish()
158+
_os_write.assert_any_call(ANY, ANSIColors.RESET.encode(con.encoding))
159+
con.restore()
160+
161+
def test_reset_on_restore(self, _os_write):
162+
# gh-152068: restore() must emit the ANSI reset sequence.
163+
code = "1"
164+
events = code_to_events(code)
165+
_, con = handle_events_unix_console(events)
166+
con.restore()
167+
_os_write.assert_any_call(ANY, ANSIColors.RESET.encode(con.encoding))
168+
150169
def test_newline(self, _os_write):
151170
code = "\n"
152171
events = code_to_events(code)

Lib/test/test_pyrepl/test_windows_console.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77

88
import itertools
9+
from _colorize import ANSIColors
910
from functools import partial
1011
from test.support import force_not_colorized_test_class
1112
from typing import Iterable
@@ -380,6 +381,28 @@ def test_multiline_ctrl_z(self):
380381
self.assertEqual(reader.cxy, (2, 3))
381382
con.restore()
382383

384+
def test_reset_on_finish(self):
385+
# gh-152068: finish() must emit the ANSI reset sequence so any
386+
# active color does not leak past the prompt.
387+
code = "1"
388+
events = code_to_events(code)
389+
_, con = self.handle_events(events)
390+
con.finish()
391+
con.out.write.assert_any_call(ANSIColors.RESET.encode(con.encoding))
392+
con.restore()
393+
394+
def test_reset_on_restore(self):
395+
# gh-152068: restore() must emit the ANSI reset sequence when VT
396+
# support is enabled.
397+
code = "1"
398+
events = code_to_events(code)
399+
_, con = self.handle_events(events)
400+
con._WindowsConsole__vt_support = True
401+
con._WindowsConsole__original_input_mode = 0
402+
with patch.object(wc, "SetConsoleMode", return_value=1):
403+
con.restore()
404+
con.out.write.assert_any_call(ANSIColors.RESET.encode(con.encoding))
405+
383406

384407
@patch.object(WindowsConsole, '__init__', _mock_console_init)
385408
class WindowsConsoleGetEventTests(TestCase):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +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

0 commit comments

Comments
 (0)