_argparse.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. # Source code: https://github.com/hamdanal/rich-argparse
  2. # MIT license: Copyright (c) Ali Hamdan <ali.hamdan.dev@gmail.com>
  3. # for internal use only
  4. from __future__ import annotations
  5. import argparse
  6. import re
  7. import sys
  8. import rich_argparse._lazy_rich as r
  9. from rich_argparse._common import (
  10. _HIGHLIGHTS,
  11. _fix_legacy_win_text,
  12. rich_fill,
  13. rich_strip,
  14. rich_wrap,
  15. )
  16. TYPE_CHECKING = False
  17. if TYPE_CHECKING:
  18. from argparse import Action, ArgumentParser, Namespace, _MutuallyExclusiveGroup
  19. from collections.abc import Callable, Iterable, Iterator, MutableMapping, Sequence
  20. from typing import Any, ClassVar
  21. from typing_extensions import Self
  22. class RichHelpFormatter(argparse.HelpFormatter):
  23. """An argparse HelpFormatter class that renders using rich."""
  24. group_name_formatter: ClassVar[Callable[[str], str]] = str.title
  25. """A function that formats group names. Defaults to ``str.title``."""
  26. styles: ClassVar[dict[str, r.StyleType]] = {
  27. "argparse.args": "cyan",
  28. "argparse.groups": "dark_orange",
  29. "argparse.help": "default",
  30. "argparse.metavar": "dark_cyan",
  31. "argparse.syntax": "bold",
  32. "argparse.text": "default",
  33. "argparse.prog": "grey50",
  34. "argparse.default": "italic",
  35. }
  36. """A dict of rich styles to control the formatter styles.
  37. The following styles are used:
  38. - ``argparse.args``: for positional-arguments and --options (e.g "--help")
  39. - ``argparse.groups``: for group names (e.g. "positional arguments")
  40. - ``argparse.help``: for argument's help text (e.g. "show this help message and exit")
  41. - ``argparse.metavar``: for meta variables (e.g. "FILE" in "--file FILE")
  42. - ``argparse.prog``: for %(prog)s in the usage (e.g. "foo" in "Usage: foo [options]")
  43. - ``argparse.syntax``: for highlights of back-tick quoted text (e.g. "``` `some text` ```")
  44. - ``argparse.text``: for the descriptions and epilog (e.g. "A foo program")
  45. - ``argparse.default``: for %(default)s in the help (e.g. "Value" in "(default: Value)")
  46. """
  47. highlights: ClassVar[list[str]] = _HIGHLIGHTS[:]
  48. """A list of regex patterns to highlight in the help text.
  49. It is used in the description, epilog, groups descriptions, and arguments' help. By default,
  50. it highlights ``--words-with-dashes`` with the `argparse.args` style and
  51. `` `text in backquotes` `` with the `argparse.syntax` style.
  52. To disable highlighting, clear this list (``RichHelpFormatter.highlights.clear()``).
  53. """
  54. usage_markup: ClassVar[bool] = False
  55. """If True, render the usage string passed to ``ArgumentParser(usage=...)`` as markup.
  56. Defaults to ``False`` meaning the text of the usage will be printed verbatim.
  57. Note that the auto-generated usage string is always colored.
  58. """
  59. help_markup: ClassVar[bool] = True
  60. """If True (default), render the help message of arguments as console markup."""
  61. text_markup: ClassVar[bool] = True
  62. """If True (default), render the descriptions and epilog as console markup."""
  63. _root_section: _Section
  64. _current_section: _Section
  65. def __init__(
  66. self,
  67. prog: str,
  68. indent_increment: int = 2,
  69. max_help_position: int = 24,
  70. width: int | None = None,
  71. console: r.Console | None = None,
  72. ) -> None:
  73. super().__init__(prog, indent_increment, max_help_position, width)
  74. self._console = console
  75. # https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting
  76. self._printf_style_pattern = re.compile(
  77. r"""
  78. % # Percent character
  79. (?:\((?P<mapping>[^)]*)\))? # Mapping key
  80. (?P<flag>[#0\-+ ])? # Conversion Flags
  81. (?P<width>\*|\d+)? # Minimum field width
  82. (?P<precision>\.(?:\*?|\d*))? # Precision
  83. [hlL]? # Length modifier (ignored)
  84. (?P<format>[diouxXeEfFgGcrsa%]) # Conversion type
  85. """,
  86. re.VERBOSE,
  87. )
  88. @property
  89. def console(self) -> r.Console:
  90. if self._console is None:
  91. self._console = r.Console()
  92. return self._console
  93. @console.setter
  94. def console(self, console: r.Console) -> None:
  95. self._console = console
  96. class _Section(argparse.HelpFormatter._Section):
  97. def __init__(
  98. self, formatter: RichHelpFormatter, parent: Self | None, heading: str | None = None
  99. ) -> None:
  100. if heading is not argparse.SUPPRESS and heading is not None:
  101. heading = f"{type(formatter).group_name_formatter(heading)}:"
  102. super().__init__(formatter, parent, heading)
  103. self.formatter: RichHelpFormatter
  104. self.rich_items: list[r.RenderableType] = []
  105. self.rich_actions: list[tuple[r.Text, r.Text | None]] = []
  106. if parent is not None:
  107. parent.rich_items.append(self)
  108. def _render_items(self, console: r.Console, options: r.ConsoleOptions) -> r.RenderResult:
  109. if not self.rich_items:
  110. return
  111. generated_options = options.update(no_wrap=True, overflow="ignore")
  112. new_line = r.Segment.line()
  113. for item in self.rich_items:
  114. if isinstance(item, RichHelpFormatter._Section):
  115. yield from console.render(item, options)
  116. elif isinstance(item, r.Padding): # user added rich renderable
  117. yield from console.render(item, options)
  118. yield new_line
  119. else: # argparse generated rich renderable
  120. yield from console.render(item, generated_options)
  121. def _render_actions(self, console: r.Console, options: r.ConsoleOptions) -> r.RenderResult:
  122. if not self.rich_actions:
  123. return
  124. options = options.update(no_wrap=True, overflow="ignore")
  125. help_pos = min(self.formatter._action_max_length + 2, self.formatter._max_help_position)
  126. help_width = max(self.formatter._width - help_pos, 11)
  127. indent = r.Text(" " * help_pos)
  128. for action_header, action_help in self.rich_actions:
  129. if not action_help:
  130. # no help, yield the header and finish
  131. yield from console.render(action_header, options)
  132. continue
  133. action_help_lines = self.formatter._rich_split_lines(action_help, help_width)
  134. if len(action_header) > help_pos - 2:
  135. # the header is too long, put it on its own line
  136. yield from console.render(action_header, options)
  137. action_header = indent
  138. action_header.set_length(help_pos)
  139. action_help_lines[0].rstrip()
  140. yield from console.render(action_header + action_help_lines[0], options)
  141. for line in action_help_lines[1:]:
  142. line.rstrip()
  143. yield from console.render(indent + line, options)
  144. yield ""
  145. def __rich_console__(self, console: r.Console, options: r.ConsoleOptions) -> r.RenderResult:
  146. if not self.rich_items and not self.rich_actions:
  147. return # empty section
  148. if self.heading is not argparse.SUPPRESS and self.heading is not None:
  149. yield r.Text(self.heading, style="argparse.groups", overflow="ignore")
  150. yield from self._render_items(console, options)
  151. yield from self._render_actions(console, options)
  152. def __rich_console__(self, console: r.Console, options: r.ConsoleOptions) -> r.RenderResult:
  153. with console.use_theme(r.Theme(self.styles)):
  154. root = console.render(self._root_section, options.update_width(self._width))
  155. new_line = r.Segment.line()
  156. add_empty_line = False
  157. for line_segments in r.Segment.split_lines(root):
  158. for i, segment in enumerate(reversed(line_segments), start=1):
  159. stripped = segment.text.rstrip()
  160. if stripped:
  161. if add_empty_line:
  162. yield new_line
  163. add_empty_line = False
  164. yield from line_segments[:-i]
  165. yield r.Segment(stripped, style=segment.style, control=segment.control)
  166. yield new_line
  167. break
  168. else: # empty line
  169. add_empty_line = True
  170. def add_text(self, text: r.RenderableType | None) -> None:
  171. if text is argparse.SUPPRESS or text is None:
  172. return
  173. elif isinstance(text, str):
  174. self._current_section.rich_items.append(self._rich_format_text(text))
  175. else:
  176. self.add_renderable(text)
  177. def add_renderable(self, renderable: r.RenderableType) -> None:
  178. padded = r.Padding.indent(renderable, self._current_indent)
  179. self._current_section.rich_items.append(padded)
  180. def add_usage(
  181. self,
  182. usage: str | None,
  183. actions: Iterable[Action],
  184. groups: Iterable[_MutuallyExclusiveGroup],
  185. prefix: str | None = None,
  186. ) -> None:
  187. if usage is argparse.SUPPRESS:
  188. return
  189. if prefix is None:
  190. prefix = self._format_usage(usage="", actions=(), groups=(), prefix=None).rstrip("\n")
  191. prefix_end = ": " if prefix.endswith(": ") else ""
  192. prefix = prefix[: len(prefix) - len(prefix_end)]
  193. prefix = r.strip_control_codes(type(self).group_name_formatter(prefix)) + prefix_end
  194. usage_spans = [r.Span(0, len(prefix.rstrip()), "argparse.groups")]
  195. usage_text = r.strip_control_codes(
  196. self._format_usage(usage, actions, groups, prefix=prefix)
  197. )
  198. if usage is None: # get colour spans for generated usage
  199. prog = r.strip_control_codes(f"{self._prog}")
  200. if actions:
  201. prog_start = usage_text.index(prog, len(prefix))
  202. usage_spans.append(r.Span(prog_start, prog_start + len(prog), "argparse.prog"))
  203. actions_start = len(prefix) + len(prog) + 1
  204. try:
  205. spans = list(self._rich_usage_spans(usage_text, actions_start, actions=actions))
  206. except ValueError:
  207. spans = []
  208. usage_spans.extend(spans)
  209. rich_usage = r.Text(usage_text)
  210. elif self.usage_markup: # treat user provided usage as markup
  211. usage_spans.extend(self._rich_prog_spans(prefix + r.Text.from_markup(usage).plain))
  212. rich_usage = r.Text.from_markup(usage_text)
  213. usage_spans.extend(rich_usage.spans)
  214. rich_usage.spans.clear()
  215. else: # treat user provided usage as plain text
  216. usage_spans.extend(self._rich_prog_spans(prefix + usage))
  217. rich_usage = r.Text(usage_text)
  218. rich_usage.spans.extend(usage_spans)
  219. self._root_section.rich_items.append(rich_usage)
  220. def add_argument(self, action: Action) -> None:
  221. super().add_argument(action)
  222. if action.help is not argparse.SUPPRESS:
  223. self._current_section.rich_actions.extend(self._rich_format_action(action))
  224. def format_help(self) -> str:
  225. with self.console.capture() as capture:
  226. self.console.print(self, crop=False)
  227. return _fix_legacy_win_text(self.console, capture.get())
  228. # ===============
  229. # Utility methods
  230. # ===============
  231. def _rich_prog_spans(self, usage: str) -> Iterator[r.Span]:
  232. if "%(prog)" not in usage:
  233. return
  234. params = {"prog": self._prog}
  235. formatted_usage = ""
  236. last = 0
  237. for m in self._printf_style_pattern.finditer(usage):
  238. start, end = m.span()
  239. formatted_usage += usage[last:start]
  240. sub = usage[start:end] % params
  241. prog_start = len(formatted_usage)
  242. prog_end = prog_start + len(sub)
  243. formatted_usage += sub
  244. last = end
  245. yield r.Span(prog_start, prog_end, "argparse.prog")
  246. def _rich_usage_spans(
  247. self, text: str, start: int, actions: Iterable[Action]
  248. ) -> Iterator[r.Span]:
  249. options: list[Action] = []
  250. positionals: list[Action] = []
  251. for action in actions:
  252. if action.help is not argparse.SUPPRESS:
  253. options.append(action) if action.option_strings else positionals.append(action)
  254. pos = start
  255. def find_span(_string: str) -> tuple[int, int]:
  256. stripped = r.strip_control_codes(_string)
  257. _start = text.index(stripped, pos)
  258. _end = _start + len(stripped)
  259. return _start, _end
  260. for action in options: # start with the options
  261. if sys.version_info >= (3, 9): # pragma: >=3.9 cover
  262. usage = action.format_usage()
  263. if isinstance(action, argparse.BooleanOptionalAction):
  264. for option_string in action.option_strings:
  265. start, end = find_span(option_string)
  266. yield r.Span(start, end, "argparse.args")
  267. pos = end + 1
  268. continue
  269. else: # pragma: <3.9 cover
  270. usage = action.option_strings[0]
  271. start, end = find_span(usage)
  272. yield r.Span(start, end, "argparse.args")
  273. pos = end + 1
  274. if action.nargs != 0:
  275. default_metavar = self._get_default_metavar_for_optional(action)
  276. for metavar_part, colorize in self._rich_metavar_parts(action, default_metavar):
  277. start, end = find_span(metavar_part)
  278. if colorize:
  279. yield r.Span(start, end, "argparse.metavar")
  280. pos = end
  281. pos = end + 1
  282. for action in positionals: # positionals come at the end
  283. default_metavar = self._get_default_metavar_for_positional(action)
  284. for metavar_part, colorize in self._rich_metavar_parts(action, default_metavar):
  285. start, end = find_span(metavar_part)
  286. if colorize:
  287. yield r.Span(start, end, "argparse.args")
  288. pos = end
  289. pos = end + 1
  290. def _rich_metavar_parts(
  291. self, action: Action, default_metavar: str
  292. ) -> Iterator[tuple[str, bool]]:
  293. get_metavar = self._metavar_formatter(action, default_metavar)
  294. # similar to self._format_args but yields (part, colorize) of the metavar
  295. if action.nargs is None:
  296. # '%s' % get_metavar(1)
  297. yield "%s" % get_metavar(1), True # noqa: UP031
  298. elif action.nargs == argparse.OPTIONAL:
  299. # '[%s]' % get_metavar(1)
  300. yield from (
  301. ("[", False),
  302. ("%s" % get_metavar(1), True), # noqa: UP031
  303. ("]", False),
  304. )
  305. elif action.nargs == argparse.ZERO_OR_MORE:
  306. if sys.version_info < (3, 9) or len(get_metavar(1)) == 2: # pragma: <3.9 cover
  307. metavar = get_metavar(2)
  308. # '[%s [%s ...]]' % metavar
  309. yield from (
  310. ("[", False),
  311. ("%s" % metavar[0], True), # noqa: UP031
  312. (" [", False),
  313. ("%s" % metavar[1], True), # noqa: UP031
  314. (" ", False),
  315. ("...", True),
  316. ("]]", False),
  317. )
  318. else: # pragma: >=3.9 cover
  319. # '[%s ...]' % metavar
  320. yield from (
  321. ("[", False),
  322. ("%s" % get_metavar(1), True), # noqa: UP031
  323. (" ", False),
  324. ("...", True),
  325. ("]", False),
  326. )
  327. elif action.nargs == argparse.ONE_OR_MORE:
  328. # '%s [%s ...]' % get_metavar(2)
  329. metavar = get_metavar(2)
  330. yield from (
  331. ("%s" % metavar[0], True), # noqa: UP031
  332. (" [", False),
  333. ("%s" % metavar[1], True), # noqa: UP031
  334. (" ", False),
  335. ("...", True),
  336. ("]", False),
  337. )
  338. elif action.nargs == argparse.REMAINDER:
  339. # '...'
  340. yield "...", True
  341. elif action.nargs == argparse.PARSER:
  342. # '%s ...' % get_metavar(1)
  343. yield from (
  344. ("%s" % get_metavar(1), True), # noqa: UP031
  345. (" ", False),
  346. ("...", True),
  347. )
  348. elif action.nargs == argparse.SUPPRESS:
  349. # ''
  350. yield "", False
  351. else:
  352. metavar = get_metavar(action.nargs) # type: ignore[arg-type]
  353. first = True
  354. for met in metavar:
  355. if first:
  356. first = False
  357. else:
  358. yield " ", False
  359. yield "%s" % met, True # noqa: UP031
  360. def _rich_whitespace_sub(self, text: r.Text) -> r.Text:
  361. # do this `self._whitespace_matcher.sub(' ', text).strip()` but text is Text
  362. spans = [m.span() for m in self._whitespace_matcher.finditer(text.plain)]
  363. for start, end in reversed(spans):
  364. if end - start > 1: # slow path
  365. space = text[start : start + 1]
  366. space.plain = " "
  367. text = text[:start] + space + text[end:]
  368. else: # performance shortcut
  369. text.plain = text.plain[:start] + " " + text.plain[end:]
  370. return rich_strip(text)
  371. # =====================================
  372. # Rich version of HelpFormatter methods
  373. # =====================================
  374. def _rich_expand_help(self, action: Action) -> r.Text:
  375. params = dict(vars(action), prog=self._prog)
  376. for name in list(params):
  377. if params[name] is argparse.SUPPRESS:
  378. del params[name]
  379. elif hasattr(params[name], "__name__"):
  380. params[name] = params[name].__name__
  381. if params.get("choices") is not None:
  382. params["choices"] = ", ".join([str(c) for c in params["choices"]])
  383. help_string = self._get_help_string(action)
  384. assert help_string is not None
  385. # raise ValueError if needed
  386. help_string % params # pyright: ignore[reportUnusedExpression]
  387. parts = []
  388. defaults: list[str] = []
  389. default_sub_template = "rich-argparse-f3ae8b55df34d5d83a8189d2e4766e68-{}-argparse-rich"
  390. default_n = 0
  391. last = 0
  392. for m in self._printf_style_pattern.finditer(help_string):
  393. start, end = m.span()
  394. parts.append(help_string[last:start])
  395. sub = help_string[start:end] % params
  396. if m.group("mapping") == "default":
  397. defaults.append(sub)
  398. sub = default_sub_template.format(default_n)
  399. default_n += 1
  400. else:
  401. sub = r.escape(sub)
  402. parts.append(sub)
  403. last = end
  404. parts.append(help_string[last:])
  405. rich_help = (
  406. r.Text.from_markup("".join(parts), style="argparse.help")
  407. if self.help_markup
  408. else r.Text("".join(parts), style="argparse.help")
  409. )
  410. for i, default in reversed(list(enumerate(defaults))):
  411. default_sub = default_sub_template.format(i)
  412. try:
  413. start = rich_help.plain.rindex(default_sub)
  414. except ValueError:
  415. # This could happen in cases like `[default: %(default)s]` with markup activated
  416. import warnings
  417. action_id = next(iter(action.option_strings), action.dest)
  418. printf_pat = self._printf_style_pattern.pattern
  419. repl = next(
  420. (
  421. repr(m.group(1))[1:-1]
  422. for m in re.finditer(rf"\[([^\]]*{printf_pat}[^\]]*)\]", help_string, re.X)
  423. if m.group("mapping") == "default"
  424. ),
  425. "default: %(default)s", # pragma: >=3.9 cover # fails on Python 3.8!
  426. )
  427. msg = (
  428. f"Failed to process default value in help string of argument {action_id!r}."
  429. f"\nHint: try disabling rich markup: `RichHelpFormatter.help_markup = False`"
  430. f"\n or replace brackets by parenthesis: `[{repl}]` -> `({repl})`"
  431. )
  432. warnings.warn(msg, UserWarning, stacklevel=4)
  433. continue
  434. end = start + len(default_sub)
  435. rich_help = (
  436. rich_help[:start].append(default, style="argparse.default").append(rich_help[end:])
  437. )
  438. for highlight in self.highlights:
  439. rich_help.highlight_regex(highlight, style_prefix="argparse.")
  440. return rich_help
  441. def _rich_format_text(self, text: str) -> r.Text:
  442. if "%(prog)" in text:
  443. text = text % {"prog": r.escape(self._prog)}
  444. rich_text = (
  445. r.Text.from_markup(text, style="argparse.text")
  446. if self.text_markup
  447. else r.Text(text, style="argparse.text")
  448. )
  449. for highlight in self.highlights:
  450. rich_text.highlight_regex(highlight, style_prefix="argparse.")
  451. text_width = max(self._width - self._current_indent * 2, 11)
  452. indent = r.Text(" " * self._current_indent)
  453. return self._rich_fill_text(rich_text, text_width, indent)
  454. def _rich_format_action(self, action: Action) -> Iterator[tuple[r.Text, r.Text | None]]:
  455. header = self._rich_format_action_invocation(action)
  456. header.pad_left(self._current_indent)
  457. help = self._rich_expand_help(action) if action.help and action.help.strip() else None
  458. yield header, help
  459. for subaction in self._iter_indented_subactions(action):
  460. yield from self._rich_format_action(subaction)
  461. def _rich_format_action_invocation(self, action: Action) -> r.Text:
  462. if not action.option_strings:
  463. return r.Text().append(self._format_action_invocation(action), style="argparse.args")
  464. else:
  465. action_header = r.Text(", ").join(
  466. r.Text(o, "argparse.args") for o in action.option_strings
  467. )
  468. if action.nargs != 0:
  469. default = self._get_default_metavar_for_optional(action)
  470. action_header.append(" ")
  471. for metavar_part, colorize in self._rich_metavar_parts(action, default):
  472. style = "argparse.metavar" if colorize else None
  473. action_header.append(metavar_part, style=style)
  474. return action_header
  475. def _rich_split_lines(self, text: r.Text, width: int) -> r.Lines:
  476. return rich_wrap(self.console, self._rich_whitespace_sub(text), width)
  477. def _rich_fill_text(self, text: r.Text, width: int, indent: r.Text) -> r.Text:
  478. return rich_fill(self.console, self._rich_whitespace_sub(text), width, indent) + "\n\n"
  479. class RawDescriptionRichHelpFormatter(RichHelpFormatter):
  480. """Rich help message formatter which retains any formatting in descriptions."""
  481. def _rich_fill_text(self, text: r.Text, width: int, indent: r.Text) -> r.Text:
  482. return r.Text("\n").join(indent + line for line in text.split()) + "\n\n"
  483. class RawTextRichHelpFormatter(RawDescriptionRichHelpFormatter):
  484. """Rich help message formatter which retains formatting of all help text."""
  485. def _rich_split_lines(self, text: r.Text, width: int) -> r.Lines:
  486. return text.split()
  487. class ArgumentDefaultsRichHelpFormatter(argparse.ArgumentDefaultsHelpFormatter, RichHelpFormatter):
  488. """Rich help message formatter which adds default values to argument help."""
  489. class MetavarTypeRichHelpFormatter(argparse.MetavarTypeHelpFormatter, RichHelpFormatter):
  490. """Rich help message formatter which uses the argument 'type' as the default
  491. metavar value (instead of the argument 'dest').
  492. """
  493. class HelpPreviewAction(argparse.Action):
  494. """Action that renders the help to SVG, HTML, or text file and exits."""
  495. def __init__(
  496. self,
  497. option_strings: Sequence[str],
  498. dest: str = argparse.SUPPRESS,
  499. default: str = argparse.SUPPRESS,
  500. help: str = argparse.SUPPRESS,
  501. *,
  502. path: str | None = None,
  503. export_kwds: MutableMapping[str, Any] | None = None,
  504. ) -> None:
  505. super().__init__(option_strings, dest, nargs="?", const=path, default=default, help=help)
  506. self.export_kwds = export_kwds or {}
  507. def __call__(
  508. self,
  509. parser: ArgumentParser,
  510. namespace: Namespace,
  511. values: str | Sequence[Any] | None,
  512. option_string: str | None = None,
  513. ) -> None:
  514. path = values
  515. if path is None:
  516. parser.exit(1, "error: help preview path is not provided\n")
  517. if not isinstance(path, str):
  518. parser.exit(1, "error: help preview path must be a string\n")
  519. if not path.endswith((".svg", ".html", ".txt")):
  520. parser.exit(1, "error: help preview path must end with .svg, .html, or .txt\n")
  521. import io
  522. text = r.Text.from_ansi(parser.format_help())
  523. console = r.Console(file=io.StringIO(), record=True)
  524. console.print(text, crop=False)
  525. if path.endswith(".svg"):
  526. self.export_kwds.setdefault("title", "")
  527. console.save_svg(path, **self.export_kwds)
  528. elif path.endswith(".html"):
  529. console.save_html(path, **self.export_kwds)
  530. elif path.endswith(".txt"):
  531. console.save_text(path, **self.export_kwds)
  532. else:
  533. raise AssertionError("unreachable")
  534. parser.exit(0, f"Help preview saved to {path}\n")