Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions src/sphinx_argparse_cli/_logic.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import os
import re
import sys
from argparse import (
Expand All @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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'["]+(.+?)["]+')
Expand All @@ -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",
]
4 changes: 3 additions & 1 deletion tests/test_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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()

Expand Down