Coverage for typer / rich_utils.py: 100%

299 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-03-26 21:46 +0000

1# Extracted and modified from https://github.com/ewels/rich-click 

2 

3import inspect 1acdefbg

4import io 1acdefbg

5from collections import defaultdict 1acdefbg

6from collections.abc import Iterable 1acdefbg

7from gettext import gettext as _ 1acdefbg

8from os import getenv 1acdefbg

9from typing import Any, Literal 1acdefbg

10 

11import click 1acdefbg

12from rich import box 1acdefbg

13from rich.align import Align 1acdefbg

14from rich.columns import Columns 1acdefbg

15from rich.console import Console, RenderableType, group 1acdefbg

16from rich.emoji import Emoji 1acdefbg

17from rich.highlighter import RegexHighlighter 1acdefbg

18from rich.markdown import Markdown 1acdefbg

19from rich.markup import escape 1acdefbg

20from rich.padding import Padding 1acdefbg

21from rich.panel import Panel 1acdefbg

22from rich.table import Table 1acdefbg

23from rich.text import Text 1acdefbg

24from rich.theme import Theme 1acdefbg

25from rich.traceback import Traceback 1acdefbg

26from typer.models import DeveloperExceptionConfig 1acdefbg

27 

28# Default styles 

29STYLE_OPTION = "bold cyan" 1acdefbg

30STYLE_SWITCH = "bold green" 1acdefbg

31STYLE_NEGATIVE_OPTION = "bold magenta" 1acdefbg

32STYLE_NEGATIVE_SWITCH = "bold red" 1acdefbg

33STYLE_METAVAR = "bold yellow" 1acdefbg

34STYLE_METAVAR_SEPARATOR = "dim" 1acdefbg

35STYLE_USAGE = "yellow" 1acdefbg

36STYLE_USAGE_COMMAND = "bold" 1acdefbg

37STYLE_DEPRECATED = "red" 1acdefbg

38STYLE_DEPRECATED_COMMAND = "dim" 1acdefbg

39STYLE_HELPTEXT_FIRST_LINE = "" 1acdefbg

40STYLE_HELPTEXT = "dim" 1acdefbg

41STYLE_OPTION_HELP = "" 1acdefbg

42STYLE_OPTION_DEFAULT = "dim" 1acdefbg

43STYLE_OPTION_ENVVAR = "dim yellow" 1acdefbg

44STYLE_REQUIRED_SHORT = "red" 1acdefbg

45STYLE_REQUIRED_LONG = "dim red" 1acdefbg

46STYLE_OPTIONS_PANEL_BORDER = "dim" 1acdefbg

47ALIGN_OPTIONS_PANEL: Literal["left", "center", "right"] = "left" 1acdefbg

48STYLE_OPTIONS_TABLE_SHOW_LINES = False 1acdefbg

49STYLE_OPTIONS_TABLE_LEADING = 0 1acdefbg

50STYLE_OPTIONS_TABLE_PAD_EDGE = False 1acdefbg

51STYLE_OPTIONS_TABLE_PADDING = (0, 1) 1acdefbg

52STYLE_OPTIONS_TABLE_BOX = "" 1acdefbg

53STYLE_OPTIONS_TABLE_ROW_STYLES = None 1acdefbg

54STYLE_OPTIONS_TABLE_BORDER_STYLE = None 1acdefbg

55STYLE_COMMANDS_PANEL_BORDER = "dim" 1acdefbg

56ALIGN_COMMANDS_PANEL: Literal["left", "center", "right"] = "left" 1acdefbg

57STYLE_COMMANDS_TABLE_SHOW_LINES = False 1acdefbg

58STYLE_COMMANDS_TABLE_LEADING = 0 1acdefbg

59STYLE_COMMANDS_TABLE_PAD_EDGE = False 1acdefbg

60STYLE_COMMANDS_TABLE_PADDING = (0, 1) 1acdefbg

61STYLE_COMMANDS_TABLE_BOX = "" 1acdefbg

62STYLE_COMMANDS_TABLE_ROW_STYLES = None 1acdefbg

63STYLE_COMMANDS_TABLE_BORDER_STYLE = None 1acdefbg

64STYLE_COMMANDS_TABLE_FIRST_COLUMN = "bold cyan" 1acdefbg

65STYLE_ERRORS_PANEL_BORDER = "red" 1acdefbg

66ALIGN_ERRORS_PANEL: Literal["left", "center", "right"] = "left" 1acdefbg

67STYLE_ERRORS_SUGGESTION = "dim" 1acdefbg

68STYLE_ABORTED = "red" 1acdefbg

69_TERMINAL_WIDTH = getenv("TERMINAL_WIDTH") 1acdefbg

70MAX_WIDTH = int(_TERMINAL_WIDTH) if _TERMINAL_WIDTH else None 1acdefbg

71COLOR_SYSTEM: Literal["auto", "standard", "256", "truecolor", "windows"] | None = ( 1acdefbg

72 "auto" # Set to None to disable colors 

73) 

74_TYPER_FORCE_DISABLE_TERMINAL = getenv("_TYPER_FORCE_DISABLE_TERMINAL") 1acdefbg

75FORCE_TERMINAL = ( 1acdefbg

76 True 

77 if getenv("GITHUB_ACTIONS") or getenv("FORCE_COLOR") or getenv("PY_COLORS") 

78 else None 

79) 

80if _TYPER_FORCE_DISABLE_TERMINAL: 1acdefbg

81 FORCE_TERMINAL = False 1acdefbg

82 

83# Fixed strings 

84DEPRECATED_STRING = _("(deprecated) ") 1acdefbg

85DEFAULT_STRING = _("[default: {}]") 1acdefbg

86ENVVAR_STRING = _("[env var: {}]") 1acdefbg

87REQUIRED_SHORT_STRING = "*" 1acdefbg

88REQUIRED_LONG_STRING = _("[required]") 1acdefbg

89RANGE_STRING = " [{}]" 1acdefbg

90ARGUMENTS_PANEL_TITLE = _("Arguments") 1acdefbg

91OPTIONS_PANEL_TITLE = _("Options") 1acdefbg

92COMMANDS_PANEL_TITLE = _("Commands") 1acdefbg

93ERRORS_PANEL_TITLE = _("Error") 1acdefbg

94ABORTED_TEXT = _("Aborted.") 1acdefbg

95RICH_HELP = _("Try [blue]'{command_path} {help_option}'[/] for help.") 1acdefbg

96 

97MARKUP_MODE_MARKDOWN = "markdown" 1acdefbg

98MARKUP_MODE_RICH = "rich" 1acdefbg

99_RICH_HELP_PANEL_NAME = "rich_help_panel" 1acdefbg

100ANSI_PREFIX = "\033[" 1acdefbg

101 

102MarkupModeStrict = Literal["markdown", "rich"] 1acdefbg

103 

104 

105# Rich regex highlighter 

106class OptionHighlighter(RegexHighlighter): 1acdefbg

107 """Highlights our special options.""" 

108 

109 highlights = [ 1acdefbg

110 r"(^|\W)(?P<switch>\-\w+)(?![a-zA-Z0-9])", 

111 r"(^|\W)(?P<option>\-\-[\w\-]+)(?![a-zA-Z0-9])", 

112 r"(?P<metavar>\<[^\>]+\>)", 

113 r"(?P<usage>Usage: )", 

114 ] 

115 

116 

117class NegativeOptionHighlighter(RegexHighlighter): 1acdefbg

118 highlights = [ 1acdefbg

119 r"(^|\W)(?P<negative_switch>\-\w+)(?![a-zA-Z0-9])", 

120 r"(^|\W)(?P<negative_option>\-\-[\w\-]+)(?![a-zA-Z0-9])", 

121 ] 

122 

123 

124# Highlighter to make [ | ] and <> dim 

125class MetavarHighlighter(RegexHighlighter): 1acdefbg

126 highlights = [ 1acdefbg

127 r"^(?P<metavar_sep>(\[|<))", 

128 r"(?P<metavar_sep>\|)", 

129 r"(?P<metavar_sep>(\]|>))(\.\.\.)?$", 

130 ] 

131 

132 

133highlighter = OptionHighlighter() 1acdefbg

134negative_highlighter = NegativeOptionHighlighter() 1acdefbg

135metavar_highlighter = MetavarHighlighter() 1acdefbg

136 

137 

138def _has_ansi_character(text: str) -> bool: 1acdefbg

139 return ANSI_PREFIX in text 1acdefbg

140 

141 

142def _get_rich_console(stderr: bool = False) -> Console: 1acdefbg

143 return Console( 1acdefbg

144 theme=Theme( 

145 { 

146 "option": STYLE_OPTION, 

147 "switch": STYLE_SWITCH, 

148 "negative_option": STYLE_NEGATIVE_OPTION, 

149 "negative_switch": STYLE_NEGATIVE_SWITCH, 

150 "metavar": STYLE_METAVAR, 

151 "metavar_sep": STYLE_METAVAR_SEPARATOR, 

152 "usage": STYLE_USAGE, 

153 }, 

154 ), 

155 highlighter=highlighter, 

156 color_system=COLOR_SYSTEM, 

157 force_terminal=FORCE_TERMINAL, 

158 width=MAX_WIDTH, 

159 stderr=stderr, 

160 ) 

161 

162 

163def _make_rich_text( 1acdefbg

164 *, text: str, style: str = "", markup_mode: MarkupModeStrict 

165) -> Markdown | Text: 

166 """Take a string, remove indentations, and return styled text. 

167 

168 If `markup_mode` is `"rich"`, the text is parsed for Rich markup strings. 

169 If `markup_mode` is `"markdown"`, parse as Markdown. 

170 """ 

171 # Remove indentations from input text 

172 text = inspect.cleandoc(text) 1acdefbg

173 if markup_mode == MARKUP_MODE_MARKDOWN: 1acdefbg

174 text = Emoji.replace(text) 1acdefbg

175 return Markdown(text, style=style) 1acdefbg

176 else: 

177 assert markup_mode == MARKUP_MODE_RICH 1acdefbg

178 if _has_ansi_character(text): 1acdefbg

179 return highlighter(Text.from_ansi(text, style=style)) 1acdefbg

180 else: 

181 return highlighter(Text.from_markup(text, style=style)) 1acdefbg

182 

183 

184@group() 1acdefbg

185def _get_help_text( 1acdefbg

186 *, 

187 obj: click.Command | click.Group, 

188 markup_mode: MarkupModeStrict, 

189) -> Iterable[Markdown | Text]: 

190 """Build primary help text for a click command or group. 

191 

192 Returns the prose help text for a command or group, rendered either as a 

193 Rich Text object or as Markdown. 

194 If the command is marked as deprecated, the deprecated string will be prepended. 

195 """ 

196 # Prepend deprecated status 

197 if obj.deprecated: 1acdefbg

198 yield Text(DEPRECATED_STRING, style=STYLE_DEPRECATED) 1acdefbg

199 

200 # Fetch and dedent the help text 

201 help_text = inspect.cleandoc(obj.help or "") 1acdefbg

202 

203 # Trim off anything that comes after \f on its own line 

204 help_text = help_text.partition("\f")[0] 1acdefbg

205 

206 # Get the first paragraph 

207 first_line, *remaining_paragraphs = help_text.split("\n\n") 1acdefbg

208 

209 # Remove single linebreaks 

210 if markup_mode != MARKUP_MODE_MARKDOWN and not first_line.startswith("\b"): 1acdefbg

211 first_line = first_line.replace("\n", " ") 1acdefbg

212 yield _make_rich_text( 1acdefbg

213 text=first_line.strip(), 

214 style=STYLE_HELPTEXT_FIRST_LINE, 

215 markup_mode=markup_mode, 

216 ) 

217 

218 # Get remaining lines, remove single line breaks and format as dim 

219 if remaining_paragraphs: 1acdefbg

220 # Add a newline inbetween the header and the remaining paragraphs 

221 yield Text("") 1acdefbg

222 # Join with double linebreaks for markdown and Rich markup 

223 remaining_lines = "\n\n".join(remaining_paragraphs) 1acdefbg

224 

225 yield _make_rich_text( 1acdefbg

226 text=remaining_lines, 

227 style=STYLE_HELPTEXT, 

228 markup_mode=markup_mode, 

229 ) 

230 

231 

232def _get_parameter_help( 1acdefbg

233 *, 

234 param: click.Option | click.Argument | click.Parameter, 

235 ctx: click.Context, 

236 markup_mode: MarkupModeStrict, 

237) -> Columns: 

238 """Build primary help text for a click option or argument. 

239 

240 Returns the prose help text for an option or argument, rendered either 

241 as a Rich Text object or as Markdown. 

242 Additional elements are appended to show the default and required status if 

243 applicable. 

244 """ 

245 # import here to avoid cyclic imports 

246 from .core import TyperArgument, TyperOption 1acdefbg

247 

248 items: list[Text | Markdown] = [] 1acdefbg

249 

250 # Get the environment variable first 

251 

252 envvar = getattr(param, "envvar", None) 1acdefbg

253 var_str = "" 1acdefbg

254 # https://github.com/pallets/click/blob/0aec1168ac591e159baf6f61026d6ae322c53aaf/src/click/core.py#L2720-L2726 

255 if envvar is None: 1acdefbg

256 if ( 1ab

257 getattr(param, "allow_from_autoenv", None) 

258 and getattr(ctx, "auto_envvar_prefix", None) is not None 

259 and param.name is not None 

260 ): 

261 envvar = f"{ctx.auto_envvar_prefix}_{param.name.upper()}" 1acdefbg

262 if envvar is not None: 1acdefbg

263 var_str = ( 1acdefbg

264 envvar if isinstance(envvar, str) else ", ".join(str(d) for d in envvar) 

265 ) 

266 

267 # Main help text 

268 help_value: str | None = getattr(param, "help", None) 1acdefbg

269 if help_value: 1acdefbg

270 paragraphs = help_value.split("\n\n") 1acdefbg

271 # Remove single linebreaks 

272 if markup_mode != MARKUP_MODE_MARKDOWN: 1acdefbg

273 paragraphs = [ 1acdefbg

274 x.replace("\n", " ").strip() 

275 if not x.startswith("\b") 

276 else "{}\n".format(x.strip("\b\n")) 

277 for x in paragraphs 

278 ] 

279 items.append( 1acdefbg

280 _make_rich_text( 

281 text="\n".join(paragraphs).strip(), 

282 style=STYLE_OPTION_HELP, 

283 markup_mode=markup_mode, 

284 ) 

285 ) 

286 

287 # Environment variable AFTER help text 

288 if envvar and getattr(param, "show_envvar", None): 1acdefbg

289 items.append(Text(ENVVAR_STRING.format(var_str), style=STYLE_OPTION_ENVVAR)) 1acdefbg

290 

291 # Default value 

292 # This uses Typer's specific param._get_default_string 

293 if isinstance(param, (TyperOption, TyperArgument)): 1acdefbg

294 default_value = param._extract_default_help_str(ctx=ctx) 1acdefbg

295 show_default_is_str = isinstance(param.show_default, str) 1acdefbg

296 if show_default_is_str or ( 1acdefbg

297 default_value is not None and (param.show_default or ctx.show_default) 

298 ): 

299 default_str = param._get_default_string( 1acdefbg

300 ctx=ctx, 

301 show_default_is_str=show_default_is_str, 

302 default_value=default_value, 

303 ) 

304 if default_str: 1acdefbg

305 items.append( 1acdefbg

306 Text( 

307 DEFAULT_STRING.format(default_str), 

308 style=STYLE_OPTION_DEFAULT, 

309 ) 

310 ) 

311 

312 # Required? 

313 if param.required: 1acdefbg

314 items.append(Text(REQUIRED_LONG_STRING, style=STYLE_REQUIRED_LONG)) 1acdefbg

315 

316 # Use Columns - this allows us to group different renderable types 

317 # (Text, Markdown) onto a single line. 

318 return Columns(items) 1acdefbg

319 

320 

321def _make_command_help( 1acdefbg

322 *, 

323 help_text: str, 

324 markup_mode: MarkupModeStrict, 

325) -> Text | Markdown: 

326 """Build cli help text for a click group command. 

327 

328 That is, when calling help on groups with multiple subcommands 

329 (not the main help text when calling the subcommand help). 

330 

331 Returns the first paragraph of help text for a command, rendered either as a 

332 Rich Text object or as Markdown. 

333 Ignores single newlines as paragraph markers, looks for double only. 

334 """ 

335 paragraphs = inspect.cleandoc(help_text).split("\n\n") 1acdefbg

336 # Remove single linebreaks 

337 if markup_mode != MARKUP_MODE_RICH and not paragraphs[0].startswith("\b"): 1acdefbg

338 paragraphs[0] = paragraphs[0].replace("\n", " ") 1acdefbg

339 elif paragraphs[0].startswith("\b"): 1acdefbg

340 paragraphs[0] = paragraphs[0].replace("\b\n", "") 1acdefbg

341 return _make_rich_text( 1acdefbg

342 text=paragraphs[0].strip(), 

343 style=STYLE_OPTION_HELP, 

344 markup_mode=markup_mode, 

345 ) 

346 

347 

348def _print_options_panel( 1acdefbg

349 *, 

350 name: str, 

351 params: list[click.Option] | list[click.Argument], 

352 ctx: click.Context, 

353 markup_mode: MarkupModeStrict, 

354 console: Console, 

355) -> None: 

356 options_rows: list[list[RenderableType]] = [] 1acdefbg

357 required_rows: list[str | Text] = [] 1acdefbg

358 for param in params: 1acdefbg

359 # Short and long form 

360 opt_long_strs = [] 1acdefbg

361 opt_short_strs = [] 1acdefbg

362 secondary_opt_long_strs = [] 1acdefbg

363 secondary_opt_short_strs = [] 1acdefbg

364 for opt_str in param.opts: 1acdefbg

365 if "--" in opt_str: 1acdefbg

366 opt_long_strs.append(opt_str) 1acdefbg

367 else: 

368 opt_short_strs.append(opt_str) 1acdefbg

369 for opt_str in param.secondary_opts: 1acdefbg

370 if "--" in opt_str: 1acdefbg

371 secondary_opt_long_strs.append(opt_str) 1acdefbg

372 else: 

373 secondary_opt_short_strs.append(opt_str) 1acdefbg

374 

375 # Column for a metavar, if we have one 

376 metavar = Text(style=STYLE_METAVAR, overflow="fold") 1acdefbg

377 metavar_str = param.make_metavar(ctx=ctx) 1acdefbg

378 # Do it ourselves if this is a positional argument 

379 if ( 1ab

380 isinstance(param, click.Argument) 

381 and param.name 

382 and metavar_str == param.name.upper() 

383 ): 

384 metavar_str = param.type.name.upper() 1acdefbg

385 

386 # Skip booleans and choices (handled above) 

387 if metavar_str != "BOOLEAN": 1acdefbg

388 metavar.append(metavar_str) 1acdefbg

389 

390 # Range - from 

391 # https://github.com/pallets/click/blob/c63c70dabd3f86ca68678b4f00951f78f52d0270/src/click/core.py#L2698-L2706 # noqa: E501 

392 # skip count with default range type 

393 if ( 1ab

394 isinstance(param.type, click.types._NumberRangeBase) 

395 and isinstance(param, click.Option) 

396 and not (param.count and param.type.min == 0 and param.type.max is None) 

397 ): 

398 range_str = param.type._describe_range() 1acdefbg

399 if range_str: 1acdefbg

400 metavar.append(RANGE_STRING.format(range_str)) 1acdefbg

401 

402 # Required asterisk 

403 required: str | Text = "" 1acdefbg

404 if param.required: 1acdefbg

405 required = Text(REQUIRED_SHORT_STRING, style=STYLE_REQUIRED_SHORT) 1acdefbg

406 

407 required_rows.append(required) 1acdefbg

408 options_rows.append( 1acdefbg

409 [ 

410 highlighter(",".join(opt_long_strs)), 

411 highlighter(",".join(opt_short_strs)), 

412 negative_highlighter(",".join(secondary_opt_long_strs)), 

413 negative_highlighter(",".join(secondary_opt_short_strs)), 

414 metavar_highlighter(metavar), 

415 _get_parameter_help( 

416 param=param, 

417 ctx=ctx, 

418 markup_mode=markup_mode, 

419 ), 

420 ] 

421 ) 

422 rows_with_required: list[list[RenderableType]] = [] 1acdefbg

423 if any(required_rows): 1acdefbg

424 for required, row in zip(required_rows, options_rows, strict=True): 1acdefbg

425 rows_with_required.append([required, *row]) 1acdefbg

426 else: 

427 rows_with_required = options_rows 1acdefbg

428 if options_rows: 1acdefbg

429 t_styles: dict[str, Any] = { 1acdefbg

430 "show_lines": STYLE_OPTIONS_TABLE_SHOW_LINES, 

431 "leading": STYLE_OPTIONS_TABLE_LEADING, 

432 "box": STYLE_OPTIONS_TABLE_BOX, 

433 "border_style": STYLE_OPTIONS_TABLE_BORDER_STYLE, 

434 "row_styles": STYLE_OPTIONS_TABLE_ROW_STYLES, 

435 "pad_edge": STYLE_OPTIONS_TABLE_PAD_EDGE, 

436 "padding": STYLE_OPTIONS_TABLE_PADDING, 

437 } 

438 box_style = getattr(box, t_styles.pop("box"), None) 1acdefbg

439 

440 options_table = Table( 1acdefbg

441 highlight=True, 

442 show_header=False, 

443 expand=True, 

444 box=box_style, 

445 **t_styles, 

446 ) 

447 for row in rows_with_required: 1acdefbg

448 options_table.add_row(*row) 1acdefbg

449 console.print( 1acdefbg

450 Panel( 

451 options_table, 

452 border_style=STYLE_OPTIONS_PANEL_BORDER, 

453 title=name, 

454 title_align=ALIGN_OPTIONS_PANEL, 

455 ) 

456 ) 

457 

458 

459def _print_commands_panel( 1acdefbg

460 *, 

461 name: str, 

462 commands: list[click.Command], 

463 markup_mode: MarkupModeStrict, 

464 console: Console, 

465 cmd_len: int, 

466) -> None: 

467 t_styles: dict[str, Any] = { 1acdefbg

468 "show_lines": STYLE_COMMANDS_TABLE_SHOW_LINES, 

469 "leading": STYLE_COMMANDS_TABLE_LEADING, 

470 "box": STYLE_COMMANDS_TABLE_BOX, 

471 "border_style": STYLE_COMMANDS_TABLE_BORDER_STYLE, 

472 "row_styles": STYLE_COMMANDS_TABLE_ROW_STYLES, 

473 "pad_edge": STYLE_COMMANDS_TABLE_PAD_EDGE, 

474 "padding": STYLE_COMMANDS_TABLE_PADDING, 

475 } 

476 box_style = getattr(box, t_styles.pop("box"), None) 1acdefbg

477 

478 commands_table = Table( 1acdefbg

479 highlight=False, 

480 show_header=False, 

481 expand=True, 

482 box=box_style, 

483 **t_styles, 

484 ) 

485 # Define formatting in first column, as commands don't match highlighter 

486 # regex 

487 commands_table.add_column( 1acdefbg

488 style=STYLE_COMMANDS_TABLE_FIRST_COLUMN, 

489 no_wrap=True, 

490 width=cmd_len, 

491 ) 

492 

493 # A big ratio makes the description column be greedy and take all the space 

494 # available instead of allowing the command column to grow and misalign with 

495 # other panels. 

496 commands_table.add_column("Description", justify="left", no_wrap=False, ratio=10) 1acdefbg

497 rows: list[list[RenderableType | None]] = [] 1acdefbg

498 deprecated_rows: list[RenderableType | None] = [] 1acdefbg

499 for command in commands: 1acdefbg

500 helptext = command.short_help or command.help or "" 1acdefbg

501 command_name = command.name or "" 1acdefbg

502 if command.deprecated: 1acdefbg

503 command_name_text = Text(f"{command_name}", style=STYLE_DEPRECATED_COMMAND) 1acdefbg

504 deprecated_rows.append(Text(DEPRECATED_STRING, style=STYLE_DEPRECATED)) 1acdefbg

505 else: 

506 command_name_text = Text(command_name) 1acdefbg

507 deprecated_rows.append(None) 1acdefbg

508 rows.append( 1acdefbg

509 [ 

510 command_name_text, 

511 _make_command_help( 

512 help_text=helptext, 

513 markup_mode=markup_mode, 

514 ), 

515 ] 

516 ) 

517 rows_with_deprecated = rows 1acdefbg

518 if any(deprecated_rows): 1acdefbg

519 rows_with_deprecated = [] 1acdefbg

520 for row, deprecated_text in zip(rows, deprecated_rows, strict=True): 1acdefbg

521 rows_with_deprecated.append([*row, deprecated_text]) 1acdefbg

522 for row in rows_with_deprecated: 1acdefbg

523 commands_table.add_row(*row) 1acdefbg

524 if commands_table.row_count: 1acdefbg

525 console.print( 1acdefbg

526 Panel( 

527 commands_table, 

528 border_style=STYLE_COMMANDS_PANEL_BORDER, 

529 title=name, 

530 title_align=ALIGN_COMMANDS_PANEL, 

531 ) 

532 ) 

533 

534 

535def rich_format_help( 1acdefbg

536 *, 

537 obj: click.Command | click.Group, 

538 ctx: click.Context, 

539 markup_mode: MarkupModeStrict, 

540) -> None: 

541 """Print nicely formatted help text using rich. 

542 

543 Based on original code from rich-cli, by @willmcgugan. 

544 https://github.com/Textualize/rich-cli/blob/8a2767c7a340715fc6fbf4930ace717b9b2fc5e5/src/rich_cli/__main__.py#L162-L236 

545 

546 Replacement for the click function format_help(). 

547 Takes a command or group and builds the help text output. 

548 """ 

549 console = _get_rich_console() 1acdefbg

550 

551 # Print usage 

552 console.print( 1acdefbg

553 Padding(highlighter(obj.get_usage(ctx)), 1), style=STYLE_USAGE_COMMAND 

554 ) 

555 

556 # Print command / group help if we have some 

557 if obj.help: 1acdefbg

558 # Print with some padding 

559 console.print( 1acdefbg

560 Padding( 

561 Align( 

562 _get_help_text( 

563 obj=obj, 

564 markup_mode=markup_mode, 

565 ), 

566 pad=False, 

567 ), 

568 (0, 1, 1, 1), 

569 ) 

570 ) 

571 panel_to_arguments: defaultdict[str, list[click.Argument]] = defaultdict(list) 1acdefbg

572 panel_to_options: defaultdict[str, list[click.Option]] = defaultdict(list) 1acdefbg

573 for param in obj.get_params(ctx): 1acdefbg

574 # Skip if option is hidden 

575 if getattr(param, "hidden", False): 1acdefbg

576 continue 1acdefbg

577 if isinstance(param, click.Argument): 1acdefbg

578 panel_name = ( 1acdefbg

579 getattr(param, _RICH_HELP_PANEL_NAME, None) or ARGUMENTS_PANEL_TITLE 

580 ) 

581 panel_to_arguments[panel_name].append(param) 1acdefbg

582 elif isinstance(param, click.Option): 1acdefbg

583 panel_name = ( 1acdefbg

584 getattr(param, _RICH_HELP_PANEL_NAME, None) or OPTIONS_PANEL_TITLE 

585 ) 

586 panel_to_options[panel_name].append(param) 1acdefbg

587 default_arguments = panel_to_arguments.get(ARGUMENTS_PANEL_TITLE, []) 1acdefbg

588 _print_options_panel( 1acdefbg

589 name=ARGUMENTS_PANEL_TITLE, 

590 params=default_arguments, 

591 ctx=ctx, 

592 markup_mode=markup_mode, 

593 console=console, 

594 ) 

595 for panel_name, arguments in panel_to_arguments.items(): 1acdefbg

596 if panel_name == ARGUMENTS_PANEL_TITLE: 1acdefbg

597 # Already printed above 

598 continue 1acdefbg

599 _print_options_panel( 1acdefbg

600 name=panel_name, 

601 params=arguments, 

602 ctx=ctx, 

603 markup_mode=markup_mode, 

604 console=console, 

605 ) 

606 default_options = panel_to_options.get(OPTIONS_PANEL_TITLE, []) 1acdefbg

607 _print_options_panel( 1acdefbg

608 name=OPTIONS_PANEL_TITLE, 

609 params=default_options, 

610 ctx=ctx, 

611 markup_mode=markup_mode, 

612 console=console, 

613 ) 

614 for panel_name, options in panel_to_options.items(): 1acdefbg

615 if panel_name == OPTIONS_PANEL_TITLE: 1acdefbg

616 # Already printed above 

617 continue 1acdefbg

618 _print_options_panel( 1acdefbg

619 name=panel_name, 

620 params=options, 

621 ctx=ctx, 

622 markup_mode=markup_mode, 

623 console=console, 

624 ) 

625 

626 if isinstance(obj, click.Group): 1acdefbg

627 panel_to_commands: defaultdict[str, list[click.Command]] = defaultdict(list) 1acdefbg

628 for command_name in obj.list_commands(ctx): 1acdefbg

629 command = obj.get_command(ctx, command_name) 1acdefbg

630 if command and not command.hidden: 1acdefbg

631 panel_name = ( 1acdefbg

632 getattr(command, _RICH_HELP_PANEL_NAME, None) 

633 or COMMANDS_PANEL_TITLE 

634 ) 

635 panel_to_commands[panel_name].append(command) 1acdefbg

636 

637 # Identify the longest command name in all panels 

638 max_cmd_len = max( 1acdefbg

639 [ 

640 len(command.name or "") 

641 for commands in panel_to_commands.values() 

642 for command in commands 

643 ], 

644 default=0, 

645 ) 

646 

647 # Print each command group panel 

648 default_commands = panel_to_commands.get(COMMANDS_PANEL_TITLE, []) 1acdefbg

649 _print_commands_panel( 1acdefbg

650 name=COMMANDS_PANEL_TITLE, 

651 commands=default_commands, 

652 markup_mode=markup_mode, 

653 console=console, 

654 cmd_len=max_cmd_len, 

655 ) 

656 for panel_name, commands in panel_to_commands.items(): 1acdefbg

657 if panel_name == COMMANDS_PANEL_TITLE: 1acdefbg

658 # Already printed above 

659 continue 1acdefbg

660 _print_commands_panel( 1acdefbg

661 name=panel_name, 

662 commands=commands, 

663 markup_mode=markup_mode, 

664 console=console, 

665 cmd_len=max_cmd_len, 

666 ) 

667 

668 # Epilogue if we have it 

669 if obj.epilog: 1acdefbg

670 # Remove single linebreaks, replace double with single 

671 lines = obj.epilog.split("\n\n") 1acdefbg

672 epilogue = "\n".join([x.replace("\n", " ").strip() for x in lines]) 1acdefbg

673 epilogue_text = _make_rich_text(text=epilogue, markup_mode=markup_mode) 1acdefbg

674 console.print(Padding(Align(epilogue_text, pad=False), 1)) 1acdefbg

675 

676 

677def rich_format_error(self: click.ClickException) -> None: 1acdefbg

678 """Print richly formatted click errors. 

679 

680 Called by custom exception handler to print richly formatted click errors. 

681 Mimics original click.ClickException.echo() function but with rich formatting. 

682 """ 

683 # Don't do anything when it's a NoArgsIsHelpError (without importing it, cf. #1278) 

684 if self.__class__.__name__ == "NoArgsIsHelpError": 1acdefbg

685 return 1acdefbg

686 

687 console = _get_rich_console(stderr=True) 1acdefbg

688 ctx: click.Context | None = getattr(self, "ctx", None) 1acdefbg

689 if ctx is not None: 1acdefbg

690 console.print(ctx.get_usage()) 1acdefbg

691 

692 if ctx is not None and ctx.command.get_help_option(ctx) is not None: 1acdefbg

693 console.print( 1acdefbg

694 RICH_HELP.format( 

695 command_path=ctx.command_path, help_option=ctx.help_option_names[0] 

696 ), 

697 style=STYLE_ERRORS_SUGGESTION, 

698 ) 

699 

700 console.print( 1acdefbg

701 Panel( 

702 highlighter(self.format_message()), 

703 border_style=STYLE_ERRORS_PANEL_BORDER, 

704 title=ERRORS_PANEL_TITLE, 

705 title_align=ALIGN_ERRORS_PANEL, 

706 ) 

707 ) 

708 

709 

710def rich_abort_error() -> None: 1acdefbg

711 """Print richly formatted abort error.""" 

712 console = _get_rich_console(stderr=True) 1acdefbg

713 console.print(ABORTED_TEXT, style=STYLE_ABORTED) 1acdefbg

714 

715 

716def escape_before_html_export(input_text: str) -> str: 1acdefbg

717 """Ensure that the input string can be used for HTML export.""" 

718 return escape(input_text).strip() 1acdefbg

719 

720 

721def rich_to_html(input_text: str) -> str: 1acdefbg

722 """Print the HTML version of a rich-formatted input string. 

723 

724 This function does not provide a full HTML page, but can be used to insert 

725 HTML-formatted text spans into a markdown file. 

726 """ 

727 console = Console(record=True, highlight=False, file=io.StringIO()) 1acdefbg

728 

729 console.print(input_text, overflow="ignore", crop=False) 1acdefbg

730 

731 return console.export_html(inline_styles=True, code_format="{code}").strip() 1acdefbg

732 

733 

734def rich_render_text(text: str) -> str: 1acdefbg

735 """Remove rich tags and render a pure text representation""" 

736 console = _get_rich_console() 1acdefbg

737 return "".join(segment.text for segment in console.render(text)).rstrip("\n") 1acdefbg

738 

739 

740def get_traceback( 1acdefbg

741 exc: BaseException, 

742 exception_config: DeveloperExceptionConfig, 

743 internal_dir_names: list[str], 

744) -> Traceback: 

745 rich_tb = Traceback.from_exception( 1acdefbg

746 type(exc), 

747 exc, 

748 exc.__traceback__, 

749 show_locals=exception_config.pretty_exceptions_show_locals, 

750 suppress=internal_dir_names, 

751 width=MAX_WIDTH, 

752 ) 

753 return rich_tb 1acdefbg