Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[console] enhance detection and elimination of ANSI sequences #12216

Merged
merged 12 commits into from
Apr 4, 2024
4 changes: 2 additions & 2 deletions sphinx/util/_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from typing import TYPE_CHECKING

from sphinx.util.console import _strip_escape_sequences
from sphinx.util.console import strip_escape_sequences

if TYPE_CHECKING:
from typing import Protocol
Expand All @@ -25,7 +25,7 @@ def __init__(

def write(self, text: str, /) -> None:
self.stream_term.write(text)
self.stream_file.write(_strip_escape_sequences(text))
self.stream_file.write(strip_escape_sequences(text))

def flush(self) -> None:
if hasattr(self.stream_term, 'flush'):
Expand Down
30 changes: 17 additions & 13 deletions sphinx/util/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,20 @@ def turquoise(text: str) -> str: ... # NoQA: E704
except ImportError:
colorama = None

_CSI: Final[str] = re.escape('\x1b[') # 'ESC [': Control Sequence Introducer

_CSI = re.escape('\x1b[') # 'ESC [': Control Sequence Introducer
_ansi_re: re.Pattern[str] = re.compile(
# ANSI escape sequences for colors
_ansi_color_re: Final[re.Pattern[str]] = re.compile('\x1b.*?m')

# ANSI escape sequences
_ansi_re: Final[re.Pattern[str]] = re.compile(
_CSI
+ r"""
(
(\d\d;){0,2}\d\dm # ANSI colour code
|
\dK # ANSI Erase in Line
+ r"""(?:
(\d\d;){0,2}\d\dm # ANSI color code
|\dK # erase in line
)""",
re.VERBOSE | re.ASCII,
)
_ansi_color_re: Final[re.Pattern[str]] = re.compile('\x1b.*?m')

codes: dict[str, str] = {}

Expand Down Expand Up @@ -128,11 +129,14 @@ def escseq(name: str) -> str:


def strip_colors(s: str) -> str:
"""Strip all color escape sequences from *s*."""
# TODO: deprecate parameter *s* in favor of a positional-only parameter *text*
return _ansi_color_re.sub('', s)


def _strip_escape_sequences(s: str) -> str:
return _ansi_re.sub('', s)
def strip_escape_sequences(text: str, /) -> str:
"""Strip ANSI escape sequences from *text*."""
return _ansi_re.sub('', text)


def create_color_func(name: str) -> None:
Expand Down Expand Up @@ -165,9 +169,9 @@ def inner(text: str) -> str:
('lightgray', 'white'),
]

for i, (dark, light) in enumerate(_colors, 30):
codes[dark] = '\x1b[%im' % i
codes[light] = '\x1b[%im' % (i + 60)
for _i, (_dark, _light) in enumerate(_colors, 30):
codes[_dark] = '\x1b[%im' % _i
codes[_light] = '\x1b[%im' % (_i + 60)

_orig_codes = codes.copy()

Expand Down
4 changes: 2 additions & 2 deletions sphinx/util/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from typing import TYPE_CHECKING

from sphinx.errors import SphinxParallelError
from sphinx.util.console import _strip_escape_sequences
from sphinx.util.console import strip_escape_sequences

if TYPE_CHECKING:
from sphinx.application import Sphinx
Expand All @@ -31,7 +31,7 @@ def save_traceback(app: Sphinx | None, exc: BaseException) -> str:
last_msgs = exts_list = ''
else:
extensions = app.extensions.values()
last_msgs = '\n'.join(f'# {_strip_escape_sequences(s).strip()}'
last_msgs = '\n'.join(f'# {strip_escape_sequences(s).strip()}'
for s in app.messagelog)
exts_list = '\n'.join(f'# {ext.name} ({ext.version})' for ext in extensions
if ext.version != 'builtin')
Expand Down
37 changes: 37 additions & 0 deletions tests/test_util/test_util_console.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from __future__ import annotations

from typing import TYPE_CHECKING

import sphinx.util.console as term
from sphinx.util.console import strip_colors, strip_escape_sequences

if TYPE_CHECKING:
from typing import TypeVar

_T = TypeVar('_T')

ERASE_IN_LINE = '\x1b[2K'


def test_strip_colors():
s = 'hello world - '
assert strip_colors(s) == s, s
assert strip_colors(term.blue(s)) == s
assert strip_colors(term.blue(s) + ERASE_IN_LINE) == s + ERASE_IN_LINE

t = s + term.blue(s)
assert strip_colors(t + ERASE_IN_LINE) == s * 2 + ERASE_IN_LINE

# this fails but this shouldn't :(
# assert strip_colors('a' + term.blue('b') + ERASE_IN_LINE + 'c' + term.blue('d')) == 'abcd'


def test_strip_escape_sequences():
s = 'hello world - '
assert strip_escape_sequences(s) == s, s
assert strip_escape_sequences(term.blue(s)) == s
assert strip_escape_sequences(term.blue(s) + ERASE_IN_LINE) == s

t = s + term.blue(s)
assert strip_escape_sequences(t + ERASE_IN_LINE) == s * 2
assert strip_escape_sequences(t + ERASE_IN_LINE + t + ERASE_IN_LINE) == s * 4