Coverage for typer / rich_utils.py: 100%

297 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-02-09 12:36 +0000

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

2 

3import inspect 1adebfgch

4import io 1adebfgch

5from collections import defaultdict 1adebfgch

6from collections.abc import Iterable 1adebfgch

7from gettext import gettext as _ 1adebfgch

8from os import getenv 1adebfgch

9from typing import Any, Literal, Optional, Union 1adebfgch

10 

11import click 1adebfgch

12from rich import box 1adebfgch

13from rich.align import Align 1adebfgch

14from rich.columns import Columns 1adebfgch

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

16from rich.emoji import Emoji 1adebfgch

17from rich.highlighter import RegexHighlighter 1adebfgch

18from rich.markdown import Markdown 1adebfgch

19from rich.markup import escape 1adebfgch

20from rich.padding import Padding 1adebfgch

21from rich.panel import Panel 1adebfgch

22from rich.table import Table 1adebfgch

23from rich.text import Text 1adebfgch

24from rich.theme import Theme 1adebfgch

25from rich.traceback import Traceback 1adebfgch

26from typer.models import DeveloperExceptionConfig 1adebfgch

27 

28# Default styles 

29STYLE_OPTION = "bold cyan" 1adebfgch

30STYLE_SWITCH = "bold green" 1adebfgch

31STYLE_NEGATIVE_OPTION = "bold magenta" 1adebfgch

32STYLE_NEGATIVE_SWITCH = "bold red" 1adebfgch

33STYLE_METAVAR = "bold yellow" 1adebfgch

34STYLE_METAVAR_SEPARATOR = "dim" 1adebfgch

35STYLE_USAGE = "yellow" 1adebfgch

36STYLE_USAGE_COMMAND = "bold" 1adebfgch

37STYLE_DEPRECATED = "red" 1adebfgch

38STYLE_DEPRECATED_COMMAND = "dim" 1adebfgch

39STYLE_HELPTEXT_FIRST_LINE = "" 1adebfgch

40STYLE_HELPTEXT = "dim" 1adebfgch

41STYLE_OPTION_HELP = "" 1adebfgch

42STYLE_OPTION_DEFAULT = "dim" 1adebfgch

43STYLE_OPTION_ENVVAR = "dim yellow" 1adebfgch

44STYLE_REQUIRED_SHORT = "red" 1adebfgch

45STYLE_REQUIRED_LONG = "dim red" 1adebfgch

46STYLE_OPTIONS_PANEL_BORDER = "dim" 1adebfgch

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

48STYLE_OPTIONS_TABLE_SHOW_LINES = False 1adebfgch

49STYLE_OPTIONS_TABLE_LEADING = 0 1adebfgch

50STYLE_OPTIONS_TABLE_PAD_EDGE = False 1adebfgch

51STYLE_OPTIONS_TABLE_PADDING = (0, 1) 1adebfgch

52STYLE_OPTIONS_TABLE_BOX = "" 1adebfgch

53STYLE_OPTIONS_TABLE_ROW_STYLES = None 1adebfgch

54STYLE_OPTIONS_TABLE_BORDER_STYLE = None 1adebfgch

55STYLE_COMMANDS_PANEL_BORDER = "dim" 1adebfgch

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

57STYLE_COMMANDS_TABLE_SHOW_LINES = False 1adebfgch

58STYLE_COMMANDS_TABLE_LEADING = 0 1adebfgch

59STYLE_COMMANDS_TABLE_PAD_EDGE = False 1adebfgch

60STYLE_COMMANDS_TABLE_PADDING = (0, 1) 1adebfgch

61STYLE_COMMANDS_TABLE_BOX = "" 1adebfgch

62STYLE_COMMANDS_TABLE_ROW_STYLES = None 1adebfgch

63STYLE_COMMANDS_TABLE_BORDER_STYLE = None 1adebfgch

64STYLE_COMMANDS_TABLE_FIRST_COLUMN = "bold cyan" 1adebfgch

65STYLE_ERRORS_PANEL_BORDER = "red" 1adebfgch

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

67STYLE_ERRORS_SUGGESTION = "dim" 1adebfgch

68STYLE_ABORTED = "red" 1adebfgch

69_TERMINAL_WIDTH = getenv("TERMINAL_WIDTH") 1adebfgch

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

71COLOR_SYSTEM: Optional[Literal["auto", "standard", "256", "truecolor", "windows"]] = ( 1adebfgch

72 "auto" # Set to None to disable colors 

73) 

74_TYPER_FORCE_DISABLE_TERMINAL = getenv("_TYPER_FORCE_DISABLE_TERMINAL") 1adebfgch

75FORCE_TERMINAL = ( 1adebfgch

76 True 

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

78 else None 

79) 

80if _TYPER_FORCE_DISABLE_TERMINAL: 1adebfgch

81 FORCE_TERMINAL = False 1adebfgch

82 

83# Fixed strings 

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

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

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

87REQUIRED_SHORT_STRING = "*" 1adebfgch

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

89RANGE_STRING = " [{}]" 1adebfgch

90ARGUMENTS_PANEL_TITLE = _("Arguments") 1adebfgch

91OPTIONS_PANEL_TITLE = _("Options") 1adebfgch

92COMMANDS_PANEL_TITLE = _("Commands") 1adebfgch

93ERRORS_PANEL_TITLE = _("Error") 1adebfgch

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

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

96 

97MARKUP_MODE_MARKDOWN = "markdown" 1adebfgch

98MARKUP_MODE_RICH = "rich" 1adebfgch

99_RICH_HELP_PANEL_NAME = "rich_help_panel" 1adebfgch

100 

101MarkupModeStrict = Literal["markdown", "rich"] 1adebfgch

102 

103 

104# Rich regex highlighter 

105class OptionHighlighter(RegexHighlighter): 1adebfgch

106 """Highlights our special options.""" 

107 

108 highlights = [ 1adebfgch

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

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

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

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

113 ] 

114 

115 

116class NegativeOptionHighlighter(RegexHighlighter): 1adebfgch

117 highlights = [ 1adebfgch

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

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

120 ] 

121 

122 

123highlighter = OptionHighlighter() 1adebfgch

124negative_highlighter = NegativeOptionHighlighter() 1adebfgch

125 

126 

127def _get_rich_console(stderr: bool = False) -> Console: 1adebfgch

128 return Console( 1adebfgch

129 theme=Theme( 

130 { 

131 "option": STYLE_OPTION, 

132 "switch": STYLE_SWITCH, 

133 "negative_option": STYLE_NEGATIVE_OPTION, 

134 "negative_switch": STYLE_NEGATIVE_SWITCH, 

135 "metavar": STYLE_METAVAR, 

136 "metavar_sep": STYLE_METAVAR_SEPARATOR, 

137 "usage": STYLE_USAGE, 

138 }, 

139 ), 

140 highlighter=highlighter, 

141 color_system=COLOR_SYSTEM, 

142 force_terminal=FORCE_TERMINAL, 

143 width=MAX_WIDTH, 

144 stderr=stderr, 

145 ) 

146 

147 

148def _make_rich_text( 1adebfgch

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

150) -> Union[Markdown, Text]: 

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

152 

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

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

155 """ 

156 # Remove indentations from input text 

157 text = inspect.cleandoc(text) 1adebfgch

158 if markup_mode == MARKUP_MODE_MARKDOWN: 1adebfgch

159 text = Emoji.replace(text) 1adebfgch

160 return Markdown(text, style=style) 1adebfgch

161 else: 

162 assert markup_mode == MARKUP_MODE_RICH 1adebfgch

163 return highlighter(Text.from_markup(text, style=style)) 1adebfgch

164 

165 

166@group() 1adebfgch

167def _get_help_text( 1adebfgch

168 *, 

169 obj: Union[click.Command, click.Group], 

170 markup_mode: MarkupModeStrict, 

171) -> Iterable[Union[Markdown, Text]]: 

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

173 

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

175 Rich Text object or as Markdown. 

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

177 """ 

178 # Prepend deprecated status 

179 if obj.deprecated: 1adebfgch

180 yield Text(DEPRECATED_STRING, style=STYLE_DEPRECATED) 1adebfgch

181 

182 # Fetch and dedent the help text 

183 help_text = inspect.cleandoc(obj.help or "") 1adebfgch

184 

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

186 help_text = help_text.partition("\f")[0] 1adebfgch

187 

188 # Get the first paragraph 

189 first_line, *remaining_paragraphs = help_text.split("\n\n") 1adebfgch

190 

191 # Remove single linebreaks 

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

193 first_line = first_line.replace("\n", " ") 1adebfgch

194 yield _make_rich_text( 1adebfgch

195 text=first_line.strip(), 

196 style=STYLE_HELPTEXT_FIRST_LINE, 

197 markup_mode=markup_mode, 

198 ) 

199 

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

201 if remaining_paragraphs: 1adebfgch

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

203 yield Text("") 1adebfgch

204 # Join with double linebreaks for markdown and Rich markup 

205 remaining_lines = "\n\n".join(remaining_paragraphs) 1adebfgch

206 

207 yield _make_rich_text( 1adebfgch

208 text=remaining_lines, 

209 style=STYLE_HELPTEXT, 

210 markup_mode=markup_mode, 

211 ) 

212 

213 

214def _get_parameter_help( 1adebfgch

215 *, 

216 param: Union[click.Option, click.Argument, click.Parameter], 

217 ctx: click.Context, 

218 markup_mode: MarkupModeStrict, 

219) -> Columns: 

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

221 

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

223 as a Rich Text object or as Markdown. 

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

225 applicable. 

226 """ 

227 # import here to avoid cyclic imports 

228 from .core import TyperArgument, TyperOption 1adebfgch

229 

230 items: list[Union[Text, Markdown]] = [] 1adebfgch

231 

232 # Get the environment variable first 

233 

234 envvar = getattr(param, "envvar", None) 1adebfgch

235 var_str = "" 1adebfgch

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

237 if envvar is None: 1adebfgch

238 if ( 1abc

239 getattr(param, "allow_from_autoenv", None) 

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

241 and param.name is not None 

242 ): 

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

244 if envvar is not None: 1adebfgch

245 var_str = ( 1adebfgch

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

247 ) 

248 

249 # Main help text 

250 help_value: Union[str, None] = getattr(param, "help", None) 1adebfgch

251 if help_value: 1adebfgch

252 paragraphs = help_value.split("\n\n") 1adebfgch

253 # Remove single linebreaks 

254 if markup_mode != MARKUP_MODE_MARKDOWN: 1adebfgch

255 paragraphs = [ 1adebfgch

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

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

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

259 for x in paragraphs 

260 ] 

261 items.append( 1adebfgch

262 _make_rich_text( 

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

264 style=STYLE_OPTION_HELP, 

265 markup_mode=markup_mode, 

266 ) 

267 ) 

268 

269 # Environment variable AFTER help text 

270 if envvar and getattr(param, "show_envvar", None): 1adebfgch

271 items.append(Text(ENVVAR_STRING.format(var_str), style=STYLE_OPTION_ENVVAR)) 1adebfgch

272 

273 # Default value 

274 # This uses Typer's specific param._get_default_string 

275 if isinstance(param, (TyperOption, TyperArgument)): 1adebfgch

276 default_value = param._extract_default_help_str(ctx=ctx) 1adebfgch

277 show_default_is_str = isinstance(param.show_default, str) 1adebfgch

278 if show_default_is_str or ( 1adebfgch

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

280 ): 

281 default_str = param._get_default_string( 1adebfgch

282 ctx=ctx, 

283 show_default_is_str=show_default_is_str, 

284 default_value=default_value, 

285 ) 

286 if default_str: 1adebfgch

287 items.append( 1adebfgch

288 Text( 

289 DEFAULT_STRING.format(default_str), 

290 style=STYLE_OPTION_DEFAULT, 

291 ) 

292 ) 

293 

294 # Required? 

295 if param.required: 1adebfgch

296 items.append(Text(REQUIRED_LONG_STRING, style=STYLE_REQUIRED_LONG)) 1adebfgch

297 

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

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

300 return Columns(items) 1adebfgch

301 

302 

303def _make_command_help( 1adebfgch

304 *, 

305 help_text: str, 

306 markup_mode: MarkupModeStrict, 

307) -> Union[Text, Markdown]: 

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

309 

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

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

312 

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

314 Rich Text object or as Markdown. 

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

316 """ 

317 paragraphs = inspect.cleandoc(help_text).split("\n\n") 1adebfgch

318 # Remove single linebreaks 

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

320 paragraphs[0] = paragraphs[0].replace("\n", " ") 1adebfgch

321 elif paragraphs[0].startswith("\b"): 1adebfgch

322 paragraphs[0] = paragraphs[0].replace("\b\n", "") 1adebfgch

323 return _make_rich_text( 1adebfgch

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

325 style=STYLE_OPTION_HELP, 

326 markup_mode=markup_mode, 

327 ) 

328 

329 

330def _print_options_panel( 1adebfgch

331 *, 

332 name: str, 

333 params: Union[list[click.Option], list[click.Argument]], 

334 ctx: click.Context, 

335 markup_mode: MarkupModeStrict, 

336 console: Console, 

337) -> None: 

338 options_rows: list[list[RenderableType]] = [] 1adebfgch

339 required_rows: list[Union[str, Text]] = [] 1adebfgch

340 for param in params: 1adebfgch

341 # Short and long form 

342 opt_long_strs = [] 1adebfgch

343 opt_short_strs = [] 1adebfgch

344 secondary_opt_long_strs = [] 1adebfgch

345 secondary_opt_short_strs = [] 1adebfgch

346 for opt_str in param.opts: 1adebfgch

347 if "--" in opt_str: 1adebfgch

348 opt_long_strs.append(opt_str) 1adebfgch

349 else: 

350 opt_short_strs.append(opt_str) 1adebfgch

351 for opt_str in param.secondary_opts: 1adebfgch

352 if "--" in opt_str: 1adebfgch

353 secondary_opt_long_strs.append(opt_str) 1adebfgch

354 else: 

355 secondary_opt_short_strs.append(opt_str) 1adebfgch

356 

357 # Column for a metavar, if we have one 

358 metavar = Text(style=STYLE_METAVAR, overflow="fold") 1adebfgch

359 # TODO: when deprecating Click < 8.2, make ctx required 

360 signature = inspect.signature(param.make_metavar) 1adebfgch

361 if "ctx" in signature.parameters: 1adebfgch

362 metavar_str = param.make_metavar(ctx=ctx) 1adebfgch

363 else: 

364 # Click < 8.2 

365 metavar_str = param.make_metavar() # type: ignore[call-arg] 1b

366 

367 # Do it ourselves if this is a positional argument 

368 if ( 1abc

369 isinstance(param, click.Argument) 

370 and param.name 

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

372 ): 

373 metavar_str = param.type.name.upper() 1adebfgch

374 

375 # Skip booleans and choices (handled above) 

376 if metavar_str != "BOOLEAN": 1adebfgch

377 metavar.append(metavar_str) 1adebfgch

378 

379 # Range - from 

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

381 # skip count with default range type 

382 if ( 1abc

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

384 and isinstance(param, click.Option) 

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

386 ): 

387 range_str = param.type._describe_range() 1adebfgch

388 if range_str: 1adebfgch

389 metavar.append(RANGE_STRING.format(range_str)) 1adebfgch

390 

391 # Required asterisk 

392 required: Union[str, Text] = "" 1adebfgch

393 if param.required: 1adebfgch

394 required = Text(REQUIRED_SHORT_STRING, style=STYLE_REQUIRED_SHORT) 1adebfgch

395 

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

397 class MetavarHighlighter(RegexHighlighter): 1adebfgch

398 highlights = [ 1adebfgch

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

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

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

402 ] 

403 

404 metavar_highlighter = MetavarHighlighter() 1adebfgch

405 

406 required_rows.append(required) 1adebfgch

407 options_rows.append( 1adebfgch

408 [ 

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

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

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

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

413 metavar_highlighter(metavar), 

414 _get_parameter_help( 

415 param=param, 

416 ctx=ctx, 

417 markup_mode=markup_mode, 

418 ), 

419 ] 

420 ) 

421 rows_with_required: list[list[RenderableType]] = [] 1adebfgch

422 if any(required_rows): 1adebfgch

423 for required, row in zip(required_rows, options_rows): 1adebfgch

424 rows_with_required.append([required, *row]) 1adebfgch

425 else: 

426 rows_with_required = options_rows 1adebfgch

427 if options_rows: 1adebfgch

428 t_styles: dict[str, Any] = { 1adebfgch

429 "show_lines": STYLE_OPTIONS_TABLE_SHOW_LINES, 

430 "leading": STYLE_OPTIONS_TABLE_LEADING, 

431 "box": STYLE_OPTIONS_TABLE_BOX, 

432 "border_style": STYLE_OPTIONS_TABLE_BORDER_STYLE, 

433 "row_styles": STYLE_OPTIONS_TABLE_ROW_STYLES, 

434 "pad_edge": STYLE_OPTIONS_TABLE_PAD_EDGE, 

435 "padding": STYLE_OPTIONS_TABLE_PADDING, 

436 } 

437 box_style = getattr(box, t_styles.pop("box"), None) 1adebfgch

438 

439 options_table = Table( 1adebfgch

440 highlight=True, 

441 show_header=False, 

442 expand=True, 

443 box=box_style, 

444 **t_styles, 

445 ) 

446 for row in rows_with_required: 1adebfgch

447 options_table.add_row(*row) 1adebfgch

448 console.print( 1adebfgch

449 Panel( 

450 options_table, 

451 border_style=STYLE_OPTIONS_PANEL_BORDER, 

452 title=name, 

453 title_align=ALIGN_OPTIONS_PANEL, 

454 ) 

455 ) 

456 

457 

458def _print_commands_panel( 1adebfgch

459 *, 

460 name: str, 

461 commands: list[click.Command], 

462 markup_mode: MarkupModeStrict, 

463 console: Console, 

464 cmd_len: int, 

465) -> None: 

466 t_styles: dict[str, Any] = { 1adebfgch

467 "show_lines": STYLE_COMMANDS_TABLE_SHOW_LINES, 

468 "leading": STYLE_COMMANDS_TABLE_LEADING, 

469 "box": STYLE_COMMANDS_TABLE_BOX, 

470 "border_style": STYLE_COMMANDS_TABLE_BORDER_STYLE, 

471 "row_styles": STYLE_COMMANDS_TABLE_ROW_STYLES, 

472 "pad_edge": STYLE_COMMANDS_TABLE_PAD_EDGE, 

473 "padding": STYLE_COMMANDS_TABLE_PADDING, 

474 } 

475 box_style = getattr(box, t_styles.pop("box"), None) 1adebfgch

476 

477 commands_table = Table( 1adebfgch

478 highlight=False, 

479 show_header=False, 

480 expand=True, 

481 box=box_style, 

482 **t_styles, 

483 ) 

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

485 # regex 

486 commands_table.add_column( 1adebfgch

487 style=STYLE_COMMANDS_TABLE_FIRST_COLUMN, 

488 no_wrap=True, 

489 width=cmd_len, 

490 ) 

491 

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

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

494 # other panels. 

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

496 rows: list[list[Union[RenderableType, None]]] = [] 1adebfgch

497 deprecated_rows: list[Union[RenderableType, None]] = [] 1adebfgch

498 for command in commands: 1adebfgch

499 helptext = command.short_help or command.help or "" 1adebfgch

500 command_name = command.name or "" 1adebfgch

501 if command.deprecated: 1adebfgch

502 command_name_text = Text(f"{command_name}", style=STYLE_DEPRECATED_COMMAND) 1adebfgch

503 deprecated_rows.append(Text(DEPRECATED_STRING, style=STYLE_DEPRECATED)) 1adebfgch

504 else: 

505 command_name_text = Text(command_name) 1adebfgch

506 deprecated_rows.append(None) 1adebfgch

507 rows.append( 1adebfgch

508 [ 

509 command_name_text, 

510 _make_command_help( 

511 help_text=helptext, 

512 markup_mode=markup_mode, 

513 ), 

514 ] 

515 ) 

516 rows_with_deprecated = rows 1adebfgch

517 if any(deprecated_rows): 1adebfgch

518 rows_with_deprecated = [] 1adebfgch

519 for row, deprecated_text in zip(rows, deprecated_rows): 1adebfgch

520 rows_with_deprecated.append([*row, deprecated_text]) 1adebfgch

521 for row in rows_with_deprecated: 1adebfgch

522 commands_table.add_row(*row) 1adebfgch

523 if commands_table.row_count: 1adebfgch

524 console.print( 1adebfgch

525 Panel( 

526 commands_table, 

527 border_style=STYLE_COMMANDS_PANEL_BORDER, 

528 title=name, 

529 title_align=ALIGN_COMMANDS_PANEL, 

530 ) 

531 ) 

532 

533 

534def rich_format_help( 1adebfgch

535 *, 

536 obj: Union[click.Command, click.Group], 

537 ctx: click.Context, 

538 markup_mode: MarkupModeStrict, 

539) -> None: 

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

541 

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

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

544 

545 Replacement for the click function format_help(). 

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

547 """ 

548 console = _get_rich_console() 1adebfgch

549 

550 # Print usage 

551 console.print( 1adebfgch

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

553 ) 

554 

555 # Print command / group help if we have some 

556 if obj.help: 1adebfgch

557 # Print with some padding 

558 console.print( 1adebfgch

559 Padding( 

560 Align( 

561 _get_help_text( 

562 obj=obj, 

563 markup_mode=markup_mode, 

564 ), 

565 pad=False, 

566 ), 

567 (0, 1, 1, 1), 

568 ) 

569 ) 

570 panel_to_arguments: defaultdict[str, list[click.Argument]] = defaultdict(list) 1adebfgch

571 panel_to_options: defaultdict[str, list[click.Option]] = defaultdict(list) 1adebfgch

572 for param in obj.get_params(ctx): 1adebfgch

573 # Skip if option is hidden 

574 if getattr(param, "hidden", False): 1adebfgch

575 continue 1adebfgch

576 if isinstance(param, click.Argument): 1adebfgch

577 panel_name = ( 1adebfgch

578 getattr(param, _RICH_HELP_PANEL_NAME, None) or ARGUMENTS_PANEL_TITLE 

579 ) 

580 panel_to_arguments[panel_name].append(param) 1adebfgch

581 elif isinstance(param, click.Option): 1adebfgch

582 panel_name = ( 1adebfgch

583 getattr(param, _RICH_HELP_PANEL_NAME, None) or OPTIONS_PANEL_TITLE 

584 ) 

585 panel_to_options[panel_name].append(param) 1adebfgch

586 default_arguments = panel_to_arguments.get(ARGUMENTS_PANEL_TITLE, []) 1adebfgch

587 _print_options_panel( 1adebfgch

588 name=ARGUMENTS_PANEL_TITLE, 

589 params=default_arguments, 

590 ctx=ctx, 

591 markup_mode=markup_mode, 

592 console=console, 

593 ) 

594 for panel_name, arguments in panel_to_arguments.items(): 1adebfgch

595 if panel_name == ARGUMENTS_PANEL_TITLE: 1adebfgch

596 # Already printed above 

597 continue 1adebfgch

598 _print_options_panel( 1adebfgch

599 name=panel_name, 

600 params=arguments, 

601 ctx=ctx, 

602 markup_mode=markup_mode, 

603 console=console, 

604 ) 

605 default_options = panel_to_options.get(OPTIONS_PANEL_TITLE, []) 1adebfgch

606 _print_options_panel( 1adebfgch

607 name=OPTIONS_PANEL_TITLE, 

608 params=default_options, 

609 ctx=ctx, 

610 markup_mode=markup_mode, 

611 console=console, 

612 ) 

613 for panel_name, options in panel_to_options.items(): 1adebfgch

614 if panel_name == OPTIONS_PANEL_TITLE: 1adebfgch

615 # Already printed above 

616 continue 1adebfgch

617 _print_options_panel( 1adebfgch

618 name=panel_name, 

619 params=options, 

620 ctx=ctx, 

621 markup_mode=markup_mode, 

622 console=console, 

623 ) 

624 

625 if isinstance(obj, click.Group): 1adebfgch

626 panel_to_commands: defaultdict[str, list[click.Command]] = defaultdict(list) 1adebfgch

627 for command_name in obj.list_commands(ctx): 1adebfgch

628 command = obj.get_command(ctx, command_name) 1adebfgch

629 if command and not command.hidden: 1adebfgch

630 panel_name = ( 1adebfgch

631 getattr(command, _RICH_HELP_PANEL_NAME, None) 

632 or COMMANDS_PANEL_TITLE 

633 ) 

634 panel_to_commands[panel_name].append(command) 1adebfgch

635 

636 # Identify the longest command name in all panels 

637 max_cmd_len = max( 1adebfgch

638 [ 

639 len(command.name or "") 

640 for commands in panel_to_commands.values() 

641 for command in commands 

642 ], 

643 default=0, 

644 ) 

645 

646 # Print each command group panel 

647 default_commands = panel_to_commands.get(COMMANDS_PANEL_TITLE, []) 1adebfgch

648 _print_commands_panel( 1adebfgch

649 name=COMMANDS_PANEL_TITLE, 

650 commands=default_commands, 

651 markup_mode=markup_mode, 

652 console=console, 

653 cmd_len=max_cmd_len, 

654 ) 

655 for panel_name, commands in panel_to_commands.items(): 1adebfgch

656 if panel_name == COMMANDS_PANEL_TITLE: 1adebfgch

657 # Already printed above 

658 continue 1adebfgch

659 _print_commands_panel( 1adebfgch

660 name=panel_name, 

661 commands=commands, 

662 markup_mode=markup_mode, 

663 console=console, 

664 cmd_len=max_cmd_len, 

665 ) 

666 

667 # Epilogue if we have it 

668 if obj.epilog: 1adebfgch

669 # Remove single linebreaks, replace double with single 

670 lines = obj.epilog.split("\n\n") 1adebfgch

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

672 epilogue_text = _make_rich_text(text=epilogue, markup_mode=markup_mode) 1adebfgch

673 console.print(Padding(Align(epilogue_text, pad=False), 1)) 1adebfgch

674 

675 

676def rich_format_error(self: click.ClickException) -> None: 1adebfgch

677 """Print richly formatted click errors. 

678 

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

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

681 """ 

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

683 if self.__class__.__name__ == "NoArgsIsHelpError": 1adebfgch

684 return 1adefgch

685 

686 console = _get_rich_console(stderr=True) 1adebfgch

687 ctx: Union[click.Context, None] = getattr(self, "ctx", None) 1adebfgch

688 if ctx is not None: 1adebfgch

689 console.print(ctx.get_usage()) 1adebfgch

690 

691 if ctx is not None and ctx.command.get_help_option(ctx) is not None: 1adebfgch

692 console.print( 1adebfgch

693 RICH_HELP.format( 

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

695 ), 

696 style=STYLE_ERRORS_SUGGESTION, 

697 ) 

698 

699 console.print( 1adebfgch

700 Panel( 

701 highlighter(self.format_message()), 

702 border_style=STYLE_ERRORS_PANEL_BORDER, 

703 title=ERRORS_PANEL_TITLE, 

704 title_align=ALIGN_ERRORS_PANEL, 

705 ) 

706 ) 

707 

708 

709def rich_abort_error() -> None: 1adebfgch

710 """Print richly formatted abort error.""" 

711 console = _get_rich_console(stderr=True) 1adebfgch

712 console.print(ABORTED_TEXT, style=STYLE_ABORTED) 1adebfgch

713 

714 

715def escape_before_html_export(input_text: str) -> str: 1adebfgch

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

717 return escape(input_text).strip() 1adebfgch

718 

719 

720def rich_to_html(input_text: str) -> str: 1adebfgch

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

722 

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

724 HTML-formatted text spans into a markdown file. 

725 """ 

726 console = Console(record=True, highlight=False, file=io.StringIO()) 1adebfgch

727 

728 console.print(input_text, overflow="ignore", crop=False) 1adebfgch

729 

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

731 

732 

733def rich_render_text(text: str) -> str: 1adebfgch

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

735 console = _get_rich_console() 1adebfgch

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

737 

738 

739def get_traceback( 1adebfgch

740 exc: BaseException, 

741 exception_config: DeveloperExceptionConfig, 

742 internal_dir_names: list[str], 

743) -> Traceback: 

744 rich_tb = Traceback.from_exception( 1adebfgch

745 type(exc), 

746 exc, 

747 exc.__traceback__, 

748 show_locals=exception_config.pretty_exceptions_show_locals, 

749 suppress=internal_dir_names, 

750 width=MAX_WIDTH, 

751 ) 

752 return rich_tb 1adebfgch