11from __future__ import annotations
22
3+ import os
34import re
45import sys
56from argparse import (
1415 _SubParsersAction ,
1516)
1617from collections import defaultdict
18+ from contextlib import contextmanager
1719from pathlib import Path
1820from typing import TYPE_CHECKING , Any , ClassVar , NamedTuple , cast
21+ from unittest .mock import patch
1922
2023from docutils .nodes import (
2124 Element ,
@@ -334,6 +337,10 @@ def _mk_sub_command(self, aliases: list[str], help_msg: str, parser: ArgumentPar
334337 sub_title_prefix : str = self .options ["group_sub_title_prefix" ]
335338 title_prefix : str = self .options ["group_title_prefix" ]
336339
340+ if sys .version_info >= (3 , 14 ):
341+ # https://github.com/python/cpython/issues/139809
342+ parser .prog = _strip_ansi_colors (parser .prog )
343+
337344 title_text = self ._build_sub_cmd_title (parser , sub_title_prefix , title_prefix )
338345 title_ref : str = parser .prog
339346 if aliases :
@@ -366,7 +373,8 @@ def _mk_sub_command(self, aliases: list[str], help_msg: str, parser: ArgumentPar
366373 return group_section
367374
368375 def _build_sub_cmd_title (self , parser : ArgumentParser , sub_title_prefix : str , title_prefix : str ) -> str :
369- title_text , elements = "" , parser .prog .split (" " )
376+ prog = _strip_ansi_colors (parser .prog )
377+ title_text , elements = "" , prog .split (" " )
370378 if title_prefix is not None :
371379 title_prefix = title_prefix .replace ("{prog}" , elements [0 ])
372380 if title_prefix :
@@ -379,7 +387,7 @@ def _build_sub_cmd_title(self, parser: ArgumentParser, sub_title_prefix: str, ti
379387 title_text += f"{ elements [0 ]} "
380388 title_text = self ._append_title (title_text , sub_title_prefix , elements [0 ], elements [1 ])
381389 else :
382- title_text += parser . prog
390+ title_text += prog
383391 return title_text .rstrip ()
384392
385393 @staticmethod
@@ -392,10 +400,16 @@ def _append_title(title_text: str, sub_title_prefix: str, prog: str, sub_cmd: st
392400
393401 def _mk_usage (self , parser : ArgumentParser ) -> literal_block :
394402 parser .formatter_class = lambda prog : HelpFormatter (prog , width = self .options .get ("usage_width" , 100 ))
395- texts = parser .format_usage ()[len ("usage: " ) :].splitlines ()
403+ with self .no_color ():
404+ texts = parser .format_usage ()[len ("usage: " ) :].splitlines ()
396405 texts = [line if at == 0 else f"{ ' ' * (len (parser .prog ) + 1 )} { line .lstrip ()} " for at , line in enumerate (texts )]
397406 return literal_block ("" , Text ("\n " .join (texts )))
398407
408+ @contextmanager
409+ def no_color (self ) -> Iterator [None ]:
410+ with patch .dict (os .environ , {"NO_COLOR" : "1" }, clear = False ):
411+ yield None
412+
399413
400414SINGLE_QUOTE = re .compile (r"[']+(.+?)[']+" )
401415DOUBLE_QUOTE = re .compile (r'["]+(.+?)["]+' )
@@ -417,6 +431,15 @@ def _parse_known_args_hook(self: ArgumentParser, *args: Any, **kwargs: Any) -> N
417431 raise HookError (self )
418432
419433
434+ _ANSI_COLOR_RE = re .compile (r"\x1b\[[0-9;]*m" )
435+
436+
437+ def _strip_ansi_colors (text : str ) -> str :
438+ """Remove ANSI color/style escape sequences (SGR codes) from text."""
439+ # needed due to https://github.com/python/cpython/issues/139809
440+ return _ANSI_COLOR_RE .sub ("" , text )
441+
442+
420443__all__ = [
421444 "SphinxArgparseCli" ,
422445]
0 commit comments