Coverage for typer/rich_utils.py: 100%

281 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-09-09 18:26 +0000

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

2 

3import inspect 1haebfcdg

4import sys 1haebfcdg

5from collections import defaultdict 1haebfcdg

6from gettext import gettext as _ 1haebfcdg

7from os import getenv 1haebfcdg

8from typing import Any, DefaultDict, Dict, Iterable, List, Optional, Union 1haebfcdg

9 

10import click 1haebfcdg

11from rich import box 1haebfcdg

12from rich.align import Align 1haebfcdg

13from rich.columns import Columns 1haebfcdg

14from rich.console import Console, RenderableType, group 1haebfcdg

15from rich.emoji import Emoji 1haebfcdg

16from rich.highlighter import RegexHighlighter 1haebfcdg

17from rich.markdown import Markdown 1haebfcdg

18from rich.padding import Padding 1haebfcdg

19from rich.panel import Panel 1haebfcdg

20from rich.table import Table 1haebfcdg

21from rich.text import Text 1haebfcdg

22from rich.theme import Theme 1haebfcdg

23 

24if sys.version_info >= (3, 8): 1haebfcdg

25 from typing import Literal 1aebfcdg

26else: 

27 from typing_extensions import Literal 1h

28 

29# Default styles 

30STYLE_OPTION = "bold cyan" 1haebfcdg

31STYLE_SWITCH = "bold green" 1haebfcdg

32STYLE_NEGATIVE_OPTION = "bold magenta" 1haebfcdg

33STYLE_NEGATIVE_SWITCH = "bold red" 1haebfcdg

34STYLE_METAVAR = "bold yellow" 1haebfcdg

35STYLE_METAVAR_SEPARATOR = "dim" 1haebfcdg

36STYLE_USAGE = "yellow" 1haebfcdg

37STYLE_USAGE_COMMAND = "bold" 1haebfcdg

38STYLE_DEPRECATED = "red" 1haebfcdg

39STYLE_DEPRECATED_COMMAND = "dim" 1haebfcdg

40STYLE_HELPTEXT_FIRST_LINE = "" 1haebfcdg

41STYLE_HELPTEXT = "dim" 1haebfcdg

42STYLE_OPTION_HELP = "" 1haebfcdg

43STYLE_OPTION_DEFAULT = "dim" 1haebfcdg

44STYLE_OPTION_ENVVAR = "dim yellow" 1haebfcdg

45STYLE_REQUIRED_SHORT = "red" 1haebfcdg

46STYLE_REQUIRED_LONG = "dim red" 1haebfcdg

47STYLE_OPTIONS_PANEL_BORDER = "dim" 1haebfcdg

48ALIGN_OPTIONS_PANEL: Literal["left", "center", "right"] = "left" 1haebfcdg

49STYLE_OPTIONS_TABLE_SHOW_LINES = False 1haebfcdg

50STYLE_OPTIONS_TABLE_LEADING = 0 1haebfcdg

51STYLE_OPTIONS_TABLE_PAD_EDGE = False 1haebfcdg

52STYLE_OPTIONS_TABLE_PADDING = (0, 1) 1haebfcdg

53STYLE_OPTIONS_TABLE_BOX = "" 1haebfcdg

54STYLE_OPTIONS_TABLE_ROW_STYLES = None 1haebfcdg

55STYLE_OPTIONS_TABLE_BORDER_STYLE = None 1haebfcdg

56STYLE_COMMANDS_PANEL_BORDER = "dim" 1haebfcdg

57ALIGN_COMMANDS_PANEL: Literal["left", "center", "right"] = "left" 1haebfcdg

58STYLE_COMMANDS_TABLE_SHOW_LINES = False 1haebfcdg

59STYLE_COMMANDS_TABLE_LEADING = 0 1haebfcdg

60STYLE_COMMANDS_TABLE_PAD_EDGE = False 1haebfcdg

61STYLE_COMMANDS_TABLE_PADDING = (0, 1) 1haebfcdg

62STYLE_COMMANDS_TABLE_BOX = "" 1haebfcdg

63STYLE_COMMANDS_TABLE_ROW_STYLES = None 1haebfcdg

64STYLE_COMMANDS_TABLE_BORDER_STYLE = None 1haebfcdg

65STYLE_ERRORS_PANEL_BORDER = "red" 1haebfcdg

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

67STYLE_ERRORS_SUGGESTION = "dim" 1haebfcdg

68STYLE_ABORTED = "red" 1haebfcdg

69_TERMINAL_WIDTH = getenv("TERMINAL_WIDTH") 1haebfcdg

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

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

72 "auto" # Set to None to disable colors 

73) 

74_TYPER_FORCE_DISABLE_TERMINAL = getenv("_TYPER_FORCE_DISABLE_TERMINAL") 1haebfcdg

75FORCE_TERMINAL = ( 1aebfcdg

76 True 

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

78 else None 

79) 

80if _TYPER_FORCE_DISABLE_TERMINAL: 1haebfcdg

81 FORCE_TERMINAL = False 1haebfcdg

82 

83# Fixed strings 

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

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

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

87REQUIRED_SHORT_STRING = "*" 1haebfcdg

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

89RANGE_STRING = " [{}]" 1haebfcdg

90ARGUMENTS_PANEL_TITLE = _("Arguments") 1haebfcdg

91OPTIONS_PANEL_TITLE = _("Options") 1haebfcdg

92COMMANDS_PANEL_TITLE = _("Commands") 1haebfcdg

93ERRORS_PANEL_TITLE = _("Error") 1haebfcdg

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

95 

96MARKUP_MODE_MARKDOWN = "markdown" 1haebfcdg

97MARKUP_MODE_RICH = "rich" 1haebfcdg

98_RICH_HELP_PANEL_NAME = "rich_help_panel" 1haebfcdg

99 

100MarkupMode = Literal["markdown", "rich", None] 1haebfcdg

101 

102 

103# Rich regex highlighter 

104class OptionHighlighter(RegexHighlighter): 1haebfcdg

105 """Highlights our special options.""" 

106 

107 highlights = [ 1aebfcdg

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

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

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

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

112 ] 

113 

114 

115class NegativeOptionHighlighter(RegexHighlighter): 1haebfcdg

116 highlights = [ 1aebfcdg

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

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

119 ] 

120 

121 

122highlighter = OptionHighlighter() 1haebfcdg

123negative_highlighter = NegativeOptionHighlighter() 1haebfcdg

124 

125 

126def _get_rich_console(stderr: bool = False) -> Console: 1haebfcdg

127 return Console( 1haebfcdg

128 theme=Theme( 

129 { 

130 "option": STYLE_OPTION, 

131 "switch": STYLE_SWITCH, 

132 "negative_option": STYLE_NEGATIVE_OPTION, 

133 "negative_switch": STYLE_NEGATIVE_SWITCH, 

134 "metavar": STYLE_METAVAR, 

135 "metavar_sep": STYLE_METAVAR_SEPARATOR, 

136 "usage": STYLE_USAGE, 

137 }, 

138 ), 

139 highlighter=highlighter, 

140 color_system=COLOR_SYSTEM, 

141 force_terminal=FORCE_TERMINAL, 

142 width=MAX_WIDTH, 

143 stderr=stderr, 

144 ) 

145 

146 

147def _make_rich_text( 1aebfcdg

148 *, text: str, style: str = "", markup_mode: MarkupMode 

149) -> Union[Markdown, Text]: 

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

151 

152 By default, return the text as a Rich Text with the request style. 

153 If `rich_markdown_enable` is `True`, also parse the text for Rich markup strings. 

154 If `rich_markup_enable` is `True`, parse as Markdown. 

155 

156 Only one of `rich_markdown_enable` or `rich_markup_enable` can be True. 

157 If both are True, `rich_markdown_enable` takes precedence. 

158 """ 

159 # Remove indentations from input text 

160 text = inspect.cleandoc(text) 1haebfcdg

161 if markup_mode == MARKUP_MODE_MARKDOWN: 1haebfcdg

162 text = Emoji.replace(text) 1haebfcdg

163 return Markdown(text, style=style) 1haebfcdg

164 if markup_mode == MARKUP_MODE_RICH: 1haebfcdg

165 return highlighter(Text.from_markup(text, style=style)) 1haebfcdg

166 else: 

167 return highlighter(Text(text, style=style)) 1haebfcdg

168 

169 

170@group() 1haebfcdg

171def _get_help_text( 1aebfcdg

172 *, 

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

174 markup_mode: MarkupMode, 

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

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

177 

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

179 Rich Text object or as Markdown. 

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

181 """ 

182 # Prepend deprecated status 

183 if obj.deprecated: 1haebfcdg

184 yield Text(DEPRECATED_STRING, style=STYLE_DEPRECATED) 1haebfcdg

185 

186 # Fetch and dedent the help text 

187 help_text = inspect.cleandoc(obj.help or "") 1haebfcdg

188 

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

190 help_text = help_text.partition("\f")[0] 1haebfcdg

191 

192 # Get the first paragraph 

193 first_line = help_text.split("\n\n")[0] 1haebfcdg

194 # Remove single linebreaks 

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

196 first_line = first_line.replace("\n", " ") 1haebfcdg

197 yield _make_rich_text( 1haebfcdg

198 text=first_line.strip(), 

199 style=STYLE_HELPTEXT_FIRST_LINE, 

200 markup_mode=markup_mode, 

201 ) 

202 

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

204 remaining_paragraphs = help_text.split("\n\n")[1:] 1haebfcdg

205 if remaining_paragraphs: 1haebfcdg

206 if markup_mode != MARKUP_MODE_RICH: 1haebfcdg

207 # Remove single linebreaks 

208 remaining_paragraphs = [ 1aebfcdg

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

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

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

212 for x in remaining_paragraphs 

213 ] 

214 # Join back together 

215 remaining_lines = "\n".join(remaining_paragraphs) 1haebfcdg

216 else: 

217 # Join with double linebreaks if markdown 

218 remaining_lines = "\n\n".join(remaining_paragraphs) 1haebfcdg

219 

220 yield _make_rich_text( 1haebfcdg

221 text=remaining_lines, 

222 style=STYLE_HELPTEXT, 

223 markup_mode=markup_mode, 

224 ) 

225 

226 

227def _get_parameter_help( 1aebfcdg

228 *, 

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

230 ctx: click.Context, 

231 markup_mode: MarkupMode, 

232) -> Columns: 

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

234 

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

236 as a Rich Text object or as Markdown. 

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

238 applicable. 

239 """ 

240 # import here to avoid cyclic imports 

241 from .core import TyperArgument, TyperOption 1haebfcdg

242 

243 items: List[Union[Text, Markdown]] = [] 1haebfcdg

244 

245 # Get the environment variable first 

246 

247 envvar = getattr(param, "envvar", None) 1haebfcdg

248 var_str = "" 1haebfcdg

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

250 if envvar is None: 1haebfcdg

251 if ( 1abcd

252 getattr(param, "allow_from_autoenv", None) 

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

254 and param.name is not None 

255 ): 

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

257 if envvar is not None: 1haebfcdg

258 var_str = ( 1aebfcdg

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

260 ) 

261 

262 # Main help text 

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

264 if help_value: 1haebfcdg

265 paragraphs = help_value.split("\n\n") 1haebfcdg

266 # Remove single linebreaks 

267 if markup_mode != MARKUP_MODE_MARKDOWN: 1haebfcdg

268 paragraphs = [ 1aebfcdg

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

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

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

272 for x in paragraphs 

273 ] 

274 items.append( 1haebfcdg

275 _make_rich_text( 

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

277 style=STYLE_OPTION_HELP, 

278 markup_mode=markup_mode, 

279 ) 

280 ) 

281 

282 # Environment variable AFTER help text 

283 if envvar and getattr(param, "show_envvar", None): 1haebfcdg

284 items.append(Text(ENVVAR_STRING.format(var_str), style=STYLE_OPTION_ENVVAR)) 1haebfcdg

285 

286 # Default value 

287 # This uses Typer's specific param._get_default_string 

288 if isinstance(param, (TyperOption, TyperArgument)): 1haebfcdg

289 if param.show_default: 1haebfcdg

290 show_default_is_str = isinstance(param.show_default, str) 1haebfcdg

291 default_value = param._extract_default_help_str(ctx=ctx) 1haebfcdg

292 default_str = param._get_default_string( 1haebfcdg

293 ctx=ctx, 

294 show_default_is_str=show_default_is_str, 

295 default_value=default_value, 

296 ) 

297 if default_str: 1haebfcdg

298 items.append( 1haebfcdg

299 Text( 

300 DEFAULT_STRING.format(default_str), 

301 style=STYLE_OPTION_DEFAULT, 

302 ) 

303 ) 

304 

305 # Required? 

306 if param.required: 1haebfcdg

307 items.append(Text(REQUIRED_LONG_STRING, style=STYLE_REQUIRED_LONG)) 1haebfcdg

308 

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

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

311 return Columns(items) 1haebfcdg

312 

313 

314def _make_command_help( 1aebfcdg

315 *, 

316 help_text: str, 

317 markup_mode: MarkupMode, 

318) -> Union[Text, Markdown]: 

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

320 

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

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

323 

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

325 Rich Text object or as Markdown. 

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

327 """ 

328 paragraphs = inspect.cleandoc(help_text).split("\n\n") 1haebfcdg

329 # Remove single linebreaks 

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

331 paragraphs[0] = paragraphs[0].replace("\n", " ") 1haebfcdg

332 elif paragraphs[0].startswith("\b"): 1haebfcdg

333 paragraphs[0] = paragraphs[0].replace("\b\n", "") 1haebfcdg

334 return _make_rich_text( 1haebfcdg

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

336 style=STYLE_OPTION_HELP, 

337 markup_mode=markup_mode, 

338 ) 

339 

340 

341def _print_options_panel( 1aebfcdg

342 *, 

343 name: str, 

344 params: Union[List[click.Option], List[click.Argument]], 

345 ctx: click.Context, 

346 markup_mode: MarkupMode, 

347 console: Console, 

348) -> None: 

349 options_rows: List[List[RenderableType]] = [] 1haebfcdg

350 required_rows: List[Union[str, Text]] = [] 1haebfcdg

351 for param in params: 1haebfcdg

352 # Short and long form 

353 opt_long_strs = [] 1haebfcdg

354 opt_short_strs = [] 1haebfcdg

355 secondary_opt_long_strs = [] 1haebfcdg

356 secondary_opt_short_strs = [] 1haebfcdg

357 for opt_str in param.opts: 1haebfcdg

358 if "--" in opt_str: 1haebfcdg

359 opt_long_strs.append(opt_str) 1haebfcdg

360 else: 

361 opt_short_strs.append(opt_str) 1haebfcdg

362 for opt_str in param.secondary_opts: 1haebfcdg

363 if "--" in opt_str: 1haebfcdg

364 secondary_opt_long_strs.append(opt_str) 1haebfcdg

365 else: 

366 secondary_opt_short_strs.append(opt_str) 1haebfcdg

367 

368 # Column for a metavar, if we have one 

369 metavar = Text(style=STYLE_METAVAR, overflow="fold") 1haebfcdg

370 metavar_str = param.make_metavar() 1haebfcdg

371 

372 # Do it ourselves if this is a positional argument 

373 if ( 1abcd

374 isinstance(param, click.Argument) 

375 and param.name 

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

377 ): 

378 metavar_str = param.type.name.upper() 1haebfcdg

379 

380 # Skip booleans and choices (handled above) 

381 if metavar_str != "BOOLEAN": 1haebfcdg

382 metavar.append(metavar_str) 1haebfcdg

383 

384 # Range - from 

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

386 # skip count with default range type 

387 if ( 1abcd

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

389 and isinstance(param, click.Option) 

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

391 ): 

392 range_str = param.type._describe_range() 1haebfcdg

393 if range_str: 1haebfcdg

394 metavar.append(RANGE_STRING.format(range_str)) 1haebfcdg

395 

396 # Required asterisk 

397 required: Union[str, Text] = "" 1haebfcdg

398 if param.required: 1haebfcdg

399 required = Text(REQUIRED_SHORT_STRING, style=STYLE_REQUIRED_SHORT) 1haebfcdg

400 

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

402 class MetavarHighlighter(RegexHighlighter): 1haebfcdg

403 highlights = [ 1aebfcdg

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

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

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

407 ] 

408 

409 metavar_highlighter = MetavarHighlighter() 1haebfcdg

410 

411 required_rows.append(required) 1haebfcdg

412 options_rows.append( 1haebfcdg

413 [ 

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

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

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

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

418 metavar_highlighter(metavar), 

419 _get_parameter_help( 

420 param=param, 

421 ctx=ctx, 

422 markup_mode=markup_mode, 

423 ), 

424 ] 

425 ) 

426 rows_with_required: List[List[RenderableType]] = [] 1haebfcdg

427 if any(required_rows): 1haebfcdg

428 for required, row in zip(required_rows, options_rows): 1haebfcdg

429 rows_with_required.append([required, *row]) 1haebfcdg

430 else: 

431 rows_with_required = options_rows 1haebfcdg

432 if options_rows: 1haebfcdg

433 t_styles: Dict[str, Any] = { 1aebfcdg

434 "show_lines": STYLE_OPTIONS_TABLE_SHOW_LINES, 

435 "leading": STYLE_OPTIONS_TABLE_LEADING, 

436 "box": STYLE_OPTIONS_TABLE_BOX, 

437 "border_style": STYLE_OPTIONS_TABLE_BORDER_STYLE, 

438 "row_styles": STYLE_OPTIONS_TABLE_ROW_STYLES, 

439 "pad_edge": STYLE_OPTIONS_TABLE_PAD_EDGE, 

440 "padding": STYLE_OPTIONS_TABLE_PADDING, 

441 } 

442 box_style = getattr(box, t_styles.pop("box"), None) 1haebfcdg

443 

444 options_table = Table( 1haebfcdg

445 highlight=True, 

446 show_header=False, 

447 expand=True, 

448 box=box_style, 

449 **t_styles, 

450 ) 

451 for row in rows_with_required: 1haebfcdg

452 options_table.add_row(*row) 1haebfcdg

453 console.print( 1haebfcdg

454 Panel( 

455 options_table, 

456 border_style=STYLE_OPTIONS_PANEL_BORDER, 

457 title=name, 

458 title_align=ALIGN_OPTIONS_PANEL, 

459 ) 

460 ) 

461 

462 

463def _print_commands_panel( 1aebfcdg

464 *, 

465 name: str, 

466 commands: List[click.Command], 

467 markup_mode: MarkupMode, 

468 console: Console, 

469 cmd_len: int, 

470) -> None: 

471 t_styles: Dict[str, Any] = { 1aebfcdg

472 "show_lines": STYLE_COMMANDS_TABLE_SHOW_LINES, 

473 "leading": STYLE_COMMANDS_TABLE_LEADING, 

474 "box": STYLE_COMMANDS_TABLE_BOX, 

475 "border_style": STYLE_COMMANDS_TABLE_BORDER_STYLE, 

476 "row_styles": STYLE_COMMANDS_TABLE_ROW_STYLES, 

477 "pad_edge": STYLE_COMMANDS_TABLE_PAD_EDGE, 

478 "padding": STYLE_COMMANDS_TABLE_PADDING, 

479 } 

480 box_style = getattr(box, t_styles.pop("box"), None) 1haebfcdg

481 

482 commands_table = Table( 1haebfcdg

483 highlight=False, 

484 show_header=False, 

485 expand=True, 

486 box=box_style, 

487 **t_styles, 

488 ) 

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

490 # regex 

491 commands_table.add_column( 1haebfcdg

492 style="bold cyan", 

493 no_wrap=True, 

494 width=cmd_len, 

495 ) 

496 

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

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

499 # other panels. 

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

501 rows: List[List[Union[RenderableType, None]]] = [] 1haebfcdg

502 deprecated_rows: List[Union[RenderableType, None]] = [] 1haebfcdg

503 for command in commands: 1haebfcdg

504 helptext = command.short_help or command.help or "" 1haebfcdg

505 command_name = command.name or "" 1haebfcdg

506 if command.deprecated: 1haebfcdg

507 command_name_text = Text(f"{command_name}", style=STYLE_DEPRECATED_COMMAND) 1haebfcdg

508 deprecated_rows.append(Text(DEPRECATED_STRING, style=STYLE_DEPRECATED)) 1haebfcdg

509 else: 

510 command_name_text = Text(command_name) 1haebfcdg

511 deprecated_rows.append(None) 1haebfcdg

512 rows.append( 1haebfcdg

513 [ 

514 command_name_text, 

515 _make_command_help( 

516 help_text=helptext, 

517 markup_mode=markup_mode, 

518 ), 

519 ] 

520 ) 

521 rows_with_deprecated = rows 1haebfcdg

522 if any(deprecated_rows): 1haebfcdg

523 rows_with_deprecated = [] 1haebfcdg

524 for row, deprecated_text in zip(rows, deprecated_rows): 1haebfcdg

525 rows_with_deprecated.append([*row, deprecated_text]) 1haebfcdg

526 for row in rows_with_deprecated: 1haebfcdg

527 commands_table.add_row(*row) 1haebfcdg

528 if commands_table.row_count: 1haebfcdg

529 console.print( 1haebfcdg

530 Panel( 

531 commands_table, 

532 border_style=STYLE_COMMANDS_PANEL_BORDER, 

533 title=name, 

534 title_align=ALIGN_COMMANDS_PANEL, 

535 ) 

536 ) 

537 

538 

539def rich_format_help( 1aebfcdg

540 *, 

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

542 ctx: click.Context, 

543 markup_mode: MarkupMode, 

544) -> None: 

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

546 

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

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

549 

550 Replacement for the click function format_help(). 

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

552 """ 

553 console = _get_rich_console() 1haebfcdg

554 

555 # Print usage 

556 console.print( 1haebfcdg

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

558 ) 

559 

560 # Print command / group help if we have some 

561 if obj.help: 1haebfcdg

562 # Print with some padding 

563 console.print( 1haebfcdg

564 Padding( 

565 Align( 

566 _get_help_text( 

567 obj=obj, 

568 markup_mode=markup_mode, 

569 ), 

570 pad=False, 

571 ), 

572 (0, 1, 1, 1), 

573 ) 

574 ) 

575 panel_to_arguments: DefaultDict[str, List[click.Argument]] = defaultdict(list) 1haebfcdg

576 panel_to_options: DefaultDict[str, List[click.Option]] = defaultdict(list) 1haebfcdg

577 for param in obj.get_params(ctx): 1haebfcdg

578 # Skip if option is hidden 

579 if getattr(param, "hidden", False): 1haebfcdg

580 continue 1haebfcdg

581 if isinstance(param, click.Argument): 1haebfcdg

582 panel_name = ( 1aebfcdg

583 getattr(param, _RICH_HELP_PANEL_NAME, None) or ARGUMENTS_PANEL_TITLE 

584 ) 

585 panel_to_arguments[panel_name].append(param) 1haebfcdg

586 elif isinstance(param, click.Option): 1haebfcdg

587 panel_name = ( 1aebfcdg

588 getattr(param, _RICH_HELP_PANEL_NAME, None) or OPTIONS_PANEL_TITLE 

589 ) 

590 panel_to_options[panel_name].append(param) 1haebfcdg

591 default_arguments = panel_to_arguments.get(ARGUMENTS_PANEL_TITLE, []) 1haebfcdg

592 _print_options_panel( 1haebfcdg

593 name=ARGUMENTS_PANEL_TITLE, 

594 params=default_arguments, 

595 ctx=ctx, 

596 markup_mode=markup_mode, 

597 console=console, 

598 ) 

599 for panel_name, arguments in panel_to_arguments.items(): 1haebfcdg

600 if panel_name == ARGUMENTS_PANEL_TITLE: 1haebfcdg

601 # Already printed above 

602 continue 1haebfcdg

603 _print_options_panel( 1haebfcdg

604 name=panel_name, 

605 params=arguments, 

606 ctx=ctx, 

607 markup_mode=markup_mode, 

608 console=console, 

609 ) 

610 default_options = panel_to_options.get(OPTIONS_PANEL_TITLE, []) 1haebfcdg

611 _print_options_panel( 1haebfcdg

612 name=OPTIONS_PANEL_TITLE, 

613 params=default_options, 

614 ctx=ctx, 

615 markup_mode=markup_mode, 

616 console=console, 

617 ) 

618 for panel_name, options in panel_to_options.items(): 1haebfcdg

619 if panel_name == OPTIONS_PANEL_TITLE: 1haebfcdg

620 # Already printed above 

621 continue 1haebfcdg

622 _print_options_panel( 1haebfcdg

623 name=panel_name, 

624 params=options, 

625 ctx=ctx, 

626 markup_mode=markup_mode, 

627 console=console, 

628 ) 

629 

630 if isinstance(obj, click.Group): 1haebfcdg

631 panel_to_commands: DefaultDict[str, List[click.Command]] = defaultdict(list) 1haebfcdg

632 for command_name in obj.list_commands(ctx): 1haebfcdg

633 command = obj.get_command(ctx, command_name) 1haebfcdg

634 if command and not command.hidden: 1haebfcdg

635 panel_name = ( 1aebfcdg

636 getattr(command, _RICH_HELP_PANEL_NAME, None) 

637 or COMMANDS_PANEL_TITLE 

638 ) 

639 panel_to_commands[panel_name].append(command) 1haebfcdg

640 

641 # Identify the longest command name in all panels 

642 max_cmd_len = max( 1haebfcdg

643 [ 

644 len(command.name or "") 

645 for commands in panel_to_commands.values() 

646 for command in commands 

647 ], 

648 default=0, 

649 ) 

650 

651 # Print each command group panel 

652 default_commands = panel_to_commands.get(COMMANDS_PANEL_TITLE, []) 1haebfcdg

653 _print_commands_panel( 1haebfcdg

654 name=COMMANDS_PANEL_TITLE, 

655 commands=default_commands, 

656 markup_mode=markup_mode, 

657 console=console, 

658 cmd_len=max_cmd_len, 

659 ) 

660 for panel_name, commands in panel_to_commands.items(): 1haebfcdg

661 if panel_name == COMMANDS_PANEL_TITLE: 1haebfcdg

662 # Already printed above 

663 continue 1haebfcdg

664 _print_commands_panel( 1haebfcdg

665 name=panel_name, 

666 commands=commands, 

667 markup_mode=markup_mode, 

668 console=console, 

669 cmd_len=max_cmd_len, 

670 ) 

671 

672 # Epilogue if we have it 

673 if obj.epilog: 1haebfcdg

674 # Remove single linebreaks, replace double with single 

675 lines = obj.epilog.split("\n\n") 1haebfcdg

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

677 epilogue_text = _make_rich_text(text=epilogue, markup_mode=markup_mode) 1haebfcdg

678 console.print(Padding(Align(epilogue_text, pad=False), 1)) 1haebfcdg

679 

680 

681def rich_format_error(self: click.ClickException) -> None: 1haebfcdg

682 """Print richly formatted click errors. 

683 

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

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

686 """ 

687 console = _get_rich_console(stderr=True) 1haebfcdg

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

689 if ctx is not None: 1haebfcdg

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

691 

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

693 console.print( 1haebfcdg

694 f"Try [blue]'{ctx.command_path} {ctx.help_option_names[0]}'[/] for help.", 

695 style=STYLE_ERRORS_SUGGESTION, 

696 ) 

697 

698 console.print( 1haebfcdg

699 Panel( 

700 highlighter(self.format_message()), 

701 border_style=STYLE_ERRORS_PANEL_BORDER, 

702 title=ERRORS_PANEL_TITLE, 

703 title_align=ALIGN_ERRORS_PANEL, 

704 ) 

705 ) 

706 

707 

708def rich_abort_error() -> None: 1haebfcdg

709 """Print richly formatted abort error.""" 

710 console = _get_rich_console(stderr=True) 1haebfcdg

711 console.print(ABORTED_TEXT, style=STYLE_ABORTED) 1haebfcdg