From 52300c15a89fefdb93ba55754b1e13b0b059fb09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Wed, 8 Oct 2025 13:49:01 -0700 Subject: [PATCH] Do not allow colors for parsing the CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Bernát Gábor --- src/sphinx_argparse_cli/_logic.py | 29 ++++++++++++++++++++++++++--- tests/test_logic.py | 4 +++- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/sphinx_argparse_cli/_logic.py b/src/sphinx_argparse_cli/_logic.py index 3c60201..9a3b433 100644 --- a/src/sphinx_argparse_cli/_logic.py +++ b/src/sphinx_argparse_cli/_logic.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os import re import sys from argparse import ( @@ -14,8 +15,10 @@ _SubParsersAction, ) from collections import defaultdict +from contextlib import contextmanager from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple, cast +from unittest.mock import patch from docutils.nodes import ( Element, @@ -334,6 +337,10 @@ def _mk_sub_command(self, aliases: list[str], help_msg: str, parser: ArgumentPar sub_title_prefix: str = self.options["group_sub_title_prefix"] title_prefix: str = self.options["group_title_prefix"] + if sys.version_info >= (3, 14): + # https://github.com/python/cpython/issues/139809 + parser.prog = _strip_ansi_colors(parser.prog) + title_text = self._build_sub_cmd_title(parser, sub_title_prefix, title_prefix) title_ref: str = parser.prog if aliases: @@ -366,7 +373,8 @@ def _mk_sub_command(self, aliases: list[str], help_msg: str, parser: ArgumentPar return group_section def _build_sub_cmd_title(self, parser: ArgumentParser, sub_title_prefix: str, title_prefix: str) -> str: - title_text, elements = "", parser.prog.split(" ") + prog = _strip_ansi_colors(parser.prog) + title_text, elements = "", prog.split(" ") if title_prefix is not None: title_prefix = title_prefix.replace("{prog}", elements[0]) if title_prefix: @@ -379,7 +387,7 @@ def _build_sub_cmd_title(self, parser: ArgumentParser, sub_title_prefix: str, ti title_text += f"{elements[0]} " title_text = self._append_title(title_text, sub_title_prefix, elements[0], elements[1]) else: - title_text += parser.prog + title_text += prog return title_text.rstrip() @staticmethod @@ -392,10 +400,16 @@ def _append_title(title_text: str, sub_title_prefix: str, prog: str, sub_cmd: st def _mk_usage(self, parser: ArgumentParser) -> literal_block: parser.formatter_class = lambda prog: HelpFormatter(prog, width=self.options.get("usage_width", 100)) - texts = parser.format_usage()[len("usage: ") :].splitlines() + with self.no_color(): + texts = parser.format_usage()[len("usage: ") :].splitlines() texts = [line if at == 0 else f"{' ' * (len(parser.prog) + 1)}{line.lstrip()}" for at, line in enumerate(texts)] return literal_block("", Text("\n".join(texts))) + @contextmanager + def no_color(self) -> Iterator[None]: + with patch.dict(os.environ, {"NO_COLOR": "1"}, clear=False): + yield None + SINGLE_QUOTE = re.compile(r"[']+(.+?)[']+") DOUBLE_QUOTE = re.compile(r'["]+(.+?)["]+') @@ -417,6 +431,15 @@ def _parse_known_args_hook(self: ArgumentParser, *args: Any, **kwargs: Any) -> N raise HookError(self) +_ANSI_COLOR_RE = re.compile(r"\x1b\[[0-9;]*m") + + +def _strip_ansi_colors(text: str) -> str: + """Remove ANSI color/style escape sequences (SGR codes) from text.""" + # needed due to https://github.com/python/cpython/issues/139809 + return _ANSI_COLOR_RE.sub("", text) + + __all__ = [ "SphinxArgparseCli", ] diff --git a/tests/test_logic.py b/tests/test_logic.py index e248254..b3d1680 100644 --- a/tests/test_logic.py +++ b/tests/test_logic.py @@ -23,7 +23,7 @@ def opt_grp_name() -> tuple[str, str]: @pytest.fixture -def build_outcome(app: SphinxTestApp, request: SubRequest) -> str: +def build_outcome(app: SphinxTestApp, request: SubRequest, monkeypatch: pytest.MonkeyPatch) -> str: prepare_marker = request.node.get_closest_marker("prepare") if prepare_marker: directive_args: list[str] | None = prepare_marker.kwargs.get("directive_args") @@ -41,6 +41,8 @@ def build_outcome(app: SphinxTestApp, request: SubRequest) -> str: assert sphinx_marker is not None ext = ext_mapping[sphinx_marker.kwargs.get("buildername")] + monkeypatch.setenv("FORCE_COLOR", "1") + monkeypatch.delenv("NO_COLOR", raising=False) app.build() return (Path(app.outdir) / f"index.{ext}").read_text()