_optparse.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  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 optparse
  6. import rich_argparse._lazy_rich as r
  7. from rich_argparse._common import _HIGHLIGHTS, _fix_legacy_win_text, rich_fill, rich_wrap
  8. TYPE_CHECKING = False
  9. if TYPE_CHECKING:
  10. from typing import Literal
  11. GENERATE_USAGE = "==GENERATE_USAGE=="
  12. class RichHelpFormatter(optparse.HelpFormatter):
  13. """An optparse HelpFormatter class that renders using rich."""
  14. styles: dict[str, r.StyleType] = {
  15. "optparse.args": "cyan",
  16. "optparse.groups": "dark_orange",
  17. "optparse.help": "default",
  18. "optparse.metavar": "dark_cyan",
  19. "optparse.syntax": "bold",
  20. "optparse.text": "default",
  21. "optparse.prog": "grey50",
  22. }
  23. """A dict of rich styles to control the formatter styles.
  24. The following styles are used:
  25. - ``optparse.args``: for --options (e.g "--help")
  26. - ``optparse.groups``: for group names (e.g. "Options")
  27. - ``optparse.help``: for options's help text (e.g. "show this help message and exit")
  28. - ``optparse.metavar``: for meta variables (e.g. "FILE" in "--file=FILE")
  29. - ``argparse.prog``: for %prog in generated usage (e.g. "foo" in "Usage: foo [options]")
  30. - ``optparse.syntax``: for highlights of back-tick quoted text (e.g. "``` `some text` ```"),
  31. - ``optparse.text``: for the descriptions and epilog (e.g. "A foo program")
  32. """
  33. highlights: list[str] = _HIGHLIGHTS[:]
  34. """A list of regex patterns to highlight in the help text.
  35. It is used in the description, epilog, groups descriptions, and arguments' help. By default,
  36. it highlights ``--words-with-dashes`` with the `optparse.args` style and
  37. ``` `text in backquotes` ``` with the `optparse.syntax` style.
  38. To disable highlighting, clear this list (``RichHelpFormatter.highlights.clear()``).
  39. """
  40. def __init__(
  41. self,
  42. indent_increment: int,
  43. max_help_position: int,
  44. width: int | None,
  45. short_first: bool | Literal[0, 1],
  46. ) -> None:
  47. super().__init__(indent_increment, max_help_position, width, short_first)
  48. self._console: r.Console | None = None
  49. self.rich_option_strings: dict[optparse.Option, r.Text] = {}
  50. @property
  51. def console(self) -> r.Console:
  52. if self._console is None:
  53. self._console = r.Console(theme=r.Theme(self.styles))
  54. return self._console
  55. @console.setter
  56. def console(self, console: r.Console) -> None:
  57. self._console = console
  58. def _stringify(self, text: r.RenderableType) -> str:
  59. # Render a rich object to a string
  60. with self.console.capture() as capture:
  61. self.console.print(text, highlight=False, soft_wrap=True, end="")
  62. help = capture.get()
  63. help = "\n".join(line.rstrip() for line in help.split("\n"))
  64. return _fix_legacy_win_text(self.console, help)
  65. def rich_format_usage(self, usage: str) -> r.Text:
  66. raise NotImplementedError("subclasses must implement")
  67. def rich_format_heading(self, heading: str) -> r.Text:
  68. raise NotImplementedError("subclasses must implement")
  69. def _rich_format_text(self, text: str) -> r.Text:
  70. # HelpFormatter._format_text() equivalent that produces rich.text.Text
  71. text_width = max(self.width - 2 * self.current_indent, 11)
  72. indent = r.Text(" " * self.current_indent)
  73. rich_text = r.Text.from_markup(text, style="optparse.text")
  74. for highlight in self.highlights:
  75. rich_text.highlight_regex(highlight, style_prefix="optparse.")
  76. return rich_fill(self.console, rich_text, text_width, indent)
  77. def rich_format_description(self, description: str | None) -> r.Text:
  78. if not description:
  79. return r.Text()
  80. return self._rich_format_text(description) + r.Text("\n")
  81. def rich_format_epilog(self, epilog: str | None) -> r.Text:
  82. if not epilog:
  83. return r.Text()
  84. return r.Text("\n") + self._rich_format_text(epilog) + r.Text("\n")
  85. def format_usage(self, usage: str) -> str:
  86. if usage is GENERATE_USAGE:
  87. rich_usage = self._generate_usage()
  88. else:
  89. rich_usage = self.rich_format_usage(usage)
  90. return self._stringify(rich_usage)
  91. def format_heading(self, heading: str) -> str:
  92. return self._stringify(self.rich_format_heading(heading))
  93. def format_description(self, description: str | None) -> str:
  94. return self._stringify(self.rich_format_description(description))
  95. def format_epilog(self, epilog: str | None) -> str:
  96. return self._stringify(self.rich_format_epilog(epilog))
  97. def rich_expand_default(self, option: optparse.Option) -> r.Text:
  98. assert option.help is not None
  99. if self.parser is None or not self.default_tag:
  100. help = option.help
  101. else:
  102. default_value = self.parser.defaults.get(option.dest) # type: ignore[arg-type]
  103. if default_value is optparse.NO_DEFAULT or default_value is None:
  104. default_value = self.NO_DEFAULT_VALUE
  105. help = option.help.replace(self.default_tag, r.escape(str(default_value)))
  106. rich_help = r.Text.from_markup(help, style="optparse.help")
  107. for highlight in self.highlights:
  108. rich_help.highlight_regex(highlight, style_prefix="optparse.")
  109. return rich_help
  110. def rich_format_option(self, option: optparse.Option) -> r.Text:
  111. result: list[r.Text] = []
  112. opts = self.rich_option_strings[option]
  113. opt_width = self.help_position - self.current_indent - 2
  114. if len(opts) > opt_width:
  115. opts.append("\n")
  116. indent_first = self.help_position
  117. else: # start help on same line as opts
  118. opts.set_length(opt_width + 2)
  119. indent_first = 0
  120. opts.pad_left(self.current_indent)
  121. result.append(opts)
  122. if option.help:
  123. help_text = self.rich_expand_default(option)
  124. help_lines = rich_wrap(self.console, help_text, self.help_width)
  125. result.append(r.Text(" " * indent_first) + help_lines[0] + "\n")
  126. indent = r.Text(" " * self.help_position)
  127. for line in help_lines[1:]:
  128. result.append(indent + line + "\n")
  129. elif opts.plain[-1] != "\n":
  130. result.append(r.Text("\n"))
  131. else:
  132. pass # pragma: no cover
  133. return r.Text().join(result)
  134. def format_option(self, option: optparse.Option) -> str:
  135. return self._stringify(self.rich_format_option(option))
  136. def store_option_strings(self, parser: optparse.OptionParser) -> None:
  137. self.indent()
  138. max_len = 0
  139. for opt in parser.option_list:
  140. strings = self.rich_format_option_strings(opt)
  141. self.option_strings[opt] = strings.plain
  142. self.rich_option_strings[opt] = strings
  143. max_len = max(max_len, len(strings) + self.current_indent)
  144. self.indent()
  145. for group in parser.option_groups:
  146. for opt in group.option_list:
  147. strings = self.rich_format_option_strings(opt)
  148. self.option_strings[opt] = strings.plain
  149. self.rich_option_strings[opt] = strings
  150. max_len = max(max_len, len(strings) + self.current_indent)
  151. self.dedent()
  152. self.dedent()
  153. self.help_position = min(max_len + 2, self.max_help_position)
  154. self.help_width = max(self.width - self.help_position, 11)
  155. def rich_format_option_strings(self, option: optparse.Option) -> r.Text:
  156. if option.takes_value():
  157. if option.metavar:
  158. metavar = option.metavar
  159. else:
  160. assert option.dest is not None
  161. metavar = option.dest.upper()
  162. s_delim = self._short_opt_fmt.replace("%s", "")
  163. short_opts = [
  164. r.Text(s_delim).join(
  165. [r.Text(o, "optparse.args"), r.Text(metavar, "optparse.metavar")]
  166. )
  167. for o in option._short_opts
  168. ]
  169. l_delim = self._long_opt_fmt.replace("%s", "")
  170. long_opts = [
  171. r.Text(l_delim).join(
  172. [r.Text(o, "optparse.args"), r.Text(metavar, "optparse.metavar")]
  173. )
  174. for o in option._long_opts
  175. ]
  176. else:
  177. short_opts = [r.Text(o, style="optparse.args") for o in option._short_opts]
  178. long_opts = [r.Text(o, style="optparse.args") for o in option._long_opts]
  179. if self.short_first:
  180. opts = short_opts + long_opts
  181. else:
  182. opts = long_opts + short_opts
  183. return r.Text(", ").join(opts)
  184. def _generate_usage(self) -> r.Text:
  185. """Generate usage string from the parser's actions."""
  186. if self.parser is None:
  187. raise TypeError("Cannot generate usage if parser is not set")
  188. mark = "==GENERATED_USAGE_MARKER=="
  189. usage_lines: list[r.Text] = []
  190. prefix = self.rich_format_usage(mark).split(mark)[0]
  191. usage_lines.extend(prefix.split("\n"))
  192. usage_lines[-1].append(self.parser.get_prog_name(), "optparse.prog")
  193. indent = len(usage_lines[-1]) + 1
  194. for option in self.parser.option_list:
  195. if option.help == optparse.SUPPRESS_HELP:
  196. continue
  197. opt_str = option._short_opts[0] if option._short_opts else option.get_opt_string()
  198. option_usage = r.Text("[").append(opt_str, "optparse.args")
  199. if option.takes_value():
  200. metavar = option.metavar or option.dest.upper() # type: ignore[union-attr]
  201. option_usage.append(" ").append(metavar, "optparse.metavar")
  202. option_usage.append("]")
  203. if len(usage_lines[-1]) + len(option_usage) + 1 > self.width:
  204. usage_lines.append(r.Text(" " * indent) + option_usage)
  205. else:
  206. usage_lines[-1].append(" ").append(option_usage)
  207. usage_lines.append(r.Text())
  208. return r.Text("\n").join(usage_lines)
  209. class IndentedRichHelpFormatter(RichHelpFormatter):
  210. """Format help with indented section bodies."""
  211. def __init__(
  212. self,
  213. indent_increment: int = 2,
  214. max_help_position: int = 24,
  215. width: int | None = None,
  216. short_first: bool | Literal[0, 1] = 1,
  217. ) -> None:
  218. super().__init__(indent_increment, max_help_position, width, short_first)
  219. def rich_format_usage(self, usage: str) -> r.Text:
  220. usage_template = optparse._("Usage: %s\n") # type: ignore[attr-defined]
  221. usage = usage_template % usage
  222. prefix = (usage_template % "").rstrip()
  223. spans = [r.Span(0, len(prefix), "optparse.groups")]
  224. return r.Text(usage, spans=spans)
  225. def rich_format_heading(self, heading: str) -> r.Text:
  226. text = r.Text(" " * self.current_indent).append(f"{heading}:", "optparse.groups")
  227. return text + r.Text("\n")
  228. class TitledRichHelpFormatter(RichHelpFormatter):
  229. """Format help with underlined section headers."""
  230. def __init__(
  231. self,
  232. indent_increment: int = 0,
  233. max_help_position: int = 24,
  234. width: int | None = None,
  235. short_first: bool | Literal[0, 1] = 0,
  236. ) -> None:
  237. super().__init__(indent_increment, max_help_position, width, short_first)
  238. def rich_format_usage(self, usage: str) -> r.Text:
  239. heading = self.rich_format_heading(optparse._("Usage")) # type: ignore[attr-defined]
  240. return r.Text.assemble(heading, " ", usage, "\n")
  241. def rich_format_heading(self, heading: str) -> r.Text:
  242. underline = "=-"[self.level] * len(heading)
  243. return r.Text.assemble(
  244. (heading, "optparse.groups"), "\n", (underline, "optparse.groups"), "\n"
  245. )