Coverage for typer/rich_utils.py: 100%

281 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-11-13 11:07 +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, the text is not parsed for any special formatting. 

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) 1haebfcdg

158 if markup_mode == MARKUP_MODE_MARKDOWN: 1haebfcdg

159 text = Emoji.replace(text) 1haebfcdg

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

161 if markup_mode == MARKUP_MODE_RICH: 1haebfcdg

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

163 else: 

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

165 

166 

167@group() 1haebfcdg

168def _get_help_text( 1aebfcdg

169 *, 

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

171 markup_mode: MarkupMode, 

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

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

174 

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

176 Rich Text object or as Markdown. 

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

178 """ 

179 # Prepend deprecated status 

180 if obj.deprecated: 1haebfcdg

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

182 

183 # Fetch and dedent the help text 

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

185 

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

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

188 

189 # Get the first paragraph 

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

191 # Remove single linebreaks 

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

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

194 yield _make_rich_text( 1haebfcdg

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 remaining_paragraphs = help_text.split("\n\n")[1:] 1haebfcdg

202 if remaining_paragraphs: 1haebfcdg

203 if markup_mode != MARKUP_MODE_RICH: 1haebfcdg

204 # Remove single linebreaks 

205 remaining_paragraphs = [ 1aebfcdg

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

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

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

209 for x in remaining_paragraphs 

210 ] 

211 # Join back together 

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

213 else: 

214 # Join with double linebreaks if markdown 

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

216 

217 yield _make_rich_text( 1haebfcdg

218 text=remaining_lines, 

219 style=STYLE_HELPTEXT, 

220 markup_mode=markup_mode, 

221 ) 

222 

223 

224def _get_parameter_help( 1aebfcdg

225 *, 

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

227 ctx: click.Context, 

228 markup_mode: MarkupMode, 

229) -> Columns: 

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

231 

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

233 as a Rich Text object or as Markdown. 

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

235 applicable. 

236 """ 

237 # import here to avoid cyclic imports 

238 from .core import TyperArgument, TyperOption 1haebfcdg

239 

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

241 

242 # Get the environment variable first 

243 

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

245 var_str = "" 1haebfcdg

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

247 if envvar is None: 1haebfcdg

248 if ( 1abcd

249 getattr(param, "allow_from_autoenv", None) 

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

251 and param.name is not None 

252 ): 

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

254 if envvar is not None: 1haebfcdg

255 var_str = ( 1aebfcdg

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

257 ) 

258 

259 # Main help text 

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

261 if help_value: 1haebfcdg

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

263 # Remove single linebreaks 

264 if markup_mode != MARKUP_MODE_MARKDOWN: 1haebfcdg

265 paragraphs = [ 1aebfcdg

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

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

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

269 for x in paragraphs 

270 ] 

271 items.append( 1haebfcdg

272 _make_rich_text( 

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

274 style=STYLE_OPTION_HELP, 

275 markup_mode=markup_mode, 

276 ) 

277 ) 

278 

279 # Environment variable AFTER help text 

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

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

282 

283 # Default value 

284 # This uses Typer's specific param._get_default_string 

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

286 if param.show_default: 1haebfcdg

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

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

289 default_str = param._get_default_string( 1haebfcdg

290 ctx=ctx, 

291 show_default_is_str=show_default_is_str, 

292 default_value=default_value, 

293 ) 

294 if default_str: 1haebfcdg

295 items.append( 1haebfcdg

296 Text( 

297 DEFAULT_STRING.format(default_str), 

298 style=STYLE_OPTION_DEFAULT, 

299 ) 

300 ) 

301 

302 # Required? 

303 if param.required: 1haebfcdg

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

305 

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

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

308 return Columns(items) 1haebfcdg

309 

310 

311def _make_command_help( 1aebfcdg

312 *, 

313 help_text: str, 

314 markup_mode: MarkupMode, 

315) -> Union[Text, Markdown]: 

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

317 

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

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

320 

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

322 Rich Text object or as Markdown. 

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

324 """ 

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

326 # Remove single linebreaks 

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

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

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

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

331 return _make_rich_text( 1haebfcdg

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

333 style=STYLE_OPTION_HELP, 

334 markup_mode=markup_mode, 

335 ) 

336 

337 

338def _print_options_panel( 1aebfcdg

339 *, 

340 name: str, 

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

342 ctx: click.Context, 

343 markup_mode: MarkupMode, 

344 console: Console, 

345) -> None: 

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

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

348 for param in params: 1haebfcdg

349 # Short and long form 

350 opt_long_strs = [] 1haebfcdg

351 opt_short_strs = [] 1haebfcdg

352 secondary_opt_long_strs = [] 1haebfcdg

353 secondary_opt_short_strs = [] 1haebfcdg

354 for opt_str in param.opts: 1haebfcdg

355 if "--" in opt_str: 1haebfcdg

356 opt_long_strs.append(opt_str) 1haebfcdg

357 else: 

358 opt_short_strs.append(opt_str) 1haebfcdg

359 for opt_str in param.secondary_opts: 1haebfcdg

360 if "--" in opt_str: 1haebfcdg

361 secondary_opt_long_strs.append(opt_str) 1haebfcdg

362 else: 

363 secondary_opt_short_strs.append(opt_str) 1haebfcdg

364 

365 # Column for a metavar, if we have one 

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

367 metavar_str = param.make_metavar() 1haebfcdg

368 

369 # Do it ourselves if this is a positional argument 

370 if ( 1abcd

371 isinstance(param, click.Argument) 

372 and param.name 

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

374 ): 

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

376 

377 # Skip booleans and choices (handled above) 

378 if metavar_str != "BOOLEAN": 1haebfcdg

379 metavar.append(metavar_str) 1haebfcdg

380 

381 # Range - from 

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

383 # skip count with default range type 

384 if ( 1abcd

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

386 and isinstance(param, click.Option) 

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

388 ): 

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

390 if range_str: 1haebfcdg

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

392 

393 # Required asterisk 

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

395 if param.required: 1haebfcdg

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

397 

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

399 class MetavarHighlighter(RegexHighlighter): 1haebfcdg

400 highlights = [ 1aebfcdg

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

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

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

404 ] 

405 

406 metavar_highlighter = MetavarHighlighter() 1haebfcdg

407 

408 required_rows.append(required) 1haebfcdg

409 options_rows.append( 1haebfcdg

410 [ 

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

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

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

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

415 metavar_highlighter(metavar), 

416 _get_parameter_help( 

417 param=param, 

418 ctx=ctx, 

419 markup_mode=markup_mode, 

420 ), 

421 ] 

422 ) 

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

424 if any(required_rows): 1haebfcdg

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

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

427 else: 

428 rows_with_required = options_rows 1haebfcdg

429 if options_rows: 1haebfcdg

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

431 "show_lines": STYLE_OPTIONS_TABLE_SHOW_LINES, 

432 "leading": STYLE_OPTIONS_TABLE_LEADING, 

433 "box": STYLE_OPTIONS_TABLE_BOX, 

434 "border_style": STYLE_OPTIONS_TABLE_BORDER_STYLE, 

435 "row_styles": STYLE_OPTIONS_TABLE_ROW_STYLES, 

436 "pad_edge": STYLE_OPTIONS_TABLE_PAD_EDGE, 

437 "padding": STYLE_OPTIONS_TABLE_PADDING, 

438 } 

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

440 

441 options_table = Table( 1haebfcdg

442 highlight=True, 

443 show_header=False, 

444 expand=True, 

445 box=box_style, 

446 **t_styles, 

447 ) 

448 for row in rows_with_required: 1haebfcdg

449 options_table.add_row(*row) 1haebfcdg

450 console.print( 1haebfcdg

451 Panel( 

452 options_table, 

453 border_style=STYLE_OPTIONS_PANEL_BORDER, 

454 title=name, 

455 title_align=ALIGN_OPTIONS_PANEL, 

456 ) 

457 ) 

458 

459 

460def _print_commands_panel( 1aebfcdg

461 *, 

462 name: str, 

463 commands: List[click.Command], 

464 markup_mode: MarkupMode, 

465 console: Console, 

466 cmd_len: int, 

467) -> None: 

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

469 "show_lines": STYLE_COMMANDS_TABLE_SHOW_LINES, 

470 "leading": STYLE_COMMANDS_TABLE_LEADING, 

471 "box": STYLE_COMMANDS_TABLE_BOX, 

472 "border_style": STYLE_COMMANDS_TABLE_BORDER_STYLE, 

473 "row_styles": STYLE_COMMANDS_TABLE_ROW_STYLES, 

474 "pad_edge": STYLE_COMMANDS_TABLE_PAD_EDGE, 

475 "padding": STYLE_COMMANDS_TABLE_PADDING, 

476 } 

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

478 

479 commands_table = Table( 1haebfcdg

480 highlight=False, 

481 show_header=False, 

482 expand=True, 

483 box=box_style, 

484 **t_styles, 

485 ) 

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

487 # regex 

488 commands_table.add_column( 1haebfcdg

489 style="bold cyan", 

490 no_wrap=True, 

491 width=cmd_len, 

492 ) 

493 

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

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

496 # other panels. 

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

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

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

500 for command in commands: 1haebfcdg

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

502 command_name = command.name or "" 1haebfcdg

503 if command.deprecated: 1haebfcdg

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

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

506 else: 

507 command_name_text = Text(command_name) 1haebfcdg

508 deprecated_rows.append(None) 1haebfcdg

509 rows.append( 1haebfcdg

510 [ 

511 command_name_text, 

512 _make_command_help( 

513 help_text=helptext, 

514 markup_mode=markup_mode, 

515 ), 

516 ] 

517 ) 

518 rows_with_deprecated = rows 1haebfcdg

519 if any(deprecated_rows): 1haebfcdg

520 rows_with_deprecated = [] 1haebfcdg

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

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

523 for row in rows_with_deprecated: 1haebfcdg

524 commands_table.add_row(*row) 1haebfcdg

525 if commands_table.row_count: 1haebfcdg

526 console.print( 1haebfcdg

527 Panel( 

528 commands_table, 

529 border_style=STYLE_COMMANDS_PANEL_BORDER, 

530 title=name, 

531 title_align=ALIGN_COMMANDS_PANEL, 

532 ) 

533 ) 

534 

535 

536def rich_format_help( 1aebfcdg

537 *, 

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

539 ctx: click.Context, 

540 markup_mode: MarkupMode, 

541) -> None: 

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

543 

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

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

546 

547 Replacement for the click function format_help(). 

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

549 """ 

550 console = _get_rich_console() 1haebfcdg

551 

552 # Print usage 

553 console.print( 1haebfcdg

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

555 ) 

556 

557 # Print command / group help if we have some 

558 if obj.help: 1haebfcdg

559 # Print with some padding 

560 console.print( 1haebfcdg

561 Padding( 

562 Align( 

563 _get_help_text( 

564 obj=obj, 

565 markup_mode=markup_mode, 

566 ), 

567 pad=False, 

568 ), 

569 (0, 1, 1, 1), 

570 ) 

571 ) 

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

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

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

575 # Skip if option is hidden 

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

577 continue 1haebfcdg

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

579 panel_name = ( 1aebfcdg

580 getattr(param, _RICH_HELP_PANEL_NAME, None) or ARGUMENTS_PANEL_TITLE 

581 ) 

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

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

584 panel_name = ( 1aebfcdg

585 getattr(param, _RICH_HELP_PANEL_NAME, None) or OPTIONS_PANEL_TITLE 

586 ) 

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

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

589 _print_options_panel( 1haebfcdg

590 name=ARGUMENTS_PANEL_TITLE, 

591 params=default_arguments, 

592 ctx=ctx, 

593 markup_mode=markup_mode, 

594 console=console, 

595 ) 

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

597 if panel_name == ARGUMENTS_PANEL_TITLE: 1haebfcdg

598 # Already printed above 

599 continue 1haebfcdg

600 _print_options_panel( 1haebfcdg

601 name=panel_name, 

602 params=arguments, 

603 ctx=ctx, 

604 markup_mode=markup_mode, 

605 console=console, 

606 ) 

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

608 _print_options_panel( 1haebfcdg

609 name=OPTIONS_PANEL_TITLE, 

610 params=default_options, 

611 ctx=ctx, 

612 markup_mode=markup_mode, 

613 console=console, 

614 ) 

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

616 if panel_name == OPTIONS_PANEL_TITLE: 1haebfcdg

617 # Already printed above 

618 continue 1haebfcdg

619 _print_options_panel( 1haebfcdg

620 name=panel_name, 

621 params=options, 

622 ctx=ctx, 

623 markup_mode=markup_mode, 

624 console=console, 

625 ) 

626 

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

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

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

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

631 if command and not command.hidden: 1haebfcdg

632 panel_name = ( 1aebfcdg

633 getattr(command, _RICH_HELP_PANEL_NAME, None) 

634 or COMMANDS_PANEL_TITLE 

635 ) 

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

637 

638 # Identify the longest command name in all panels 

639 max_cmd_len = max( 1haebfcdg

640 [ 

641 len(command.name or "") 

642 for commands in panel_to_commands.values() 

643 for command in commands 

644 ], 

645 default=0, 

646 ) 

647 

648 # Print each command group panel 

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

650 _print_commands_panel( 1haebfcdg

651 name=COMMANDS_PANEL_TITLE, 

652 commands=default_commands, 

653 markup_mode=markup_mode, 

654 console=console, 

655 cmd_len=max_cmd_len, 

656 ) 

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

658 if panel_name == COMMANDS_PANEL_TITLE: 1haebfcdg

659 # Already printed above 

660 continue 1haebfcdg

661 _print_commands_panel( 1haebfcdg

662 name=panel_name, 

663 commands=commands, 

664 markup_mode=markup_mode, 

665 console=console, 

666 cmd_len=max_cmd_len, 

667 ) 

668 

669 # Epilogue if we have it 

670 if obj.epilog: 1haebfcdg

671 # Remove single linebreaks, replace double with single 

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

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

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

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

676 

677 

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

679 """Print richly formatted click errors. 

680 

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

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

683 """ 

684 console = _get_rich_console(stderr=True) 1haebfcdg

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

686 if ctx is not None: 1haebfcdg

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

688 

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

690 console.print( 1haebfcdg

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

692 style=STYLE_ERRORS_SUGGESTION, 

693 ) 

694 

695 console.print( 1haebfcdg

696 Panel( 

697 highlighter(self.format_message()), 

698 border_style=STYLE_ERRORS_PANEL_BORDER, 

699 title=ERRORS_PANEL_TITLE, 

700 title_align=ALIGN_ERRORS_PANEL, 

701 ) 

702 ) 

703 

704 

705def rich_abort_error() -> None: 1haebfcdg

706 """Print richly formatted abort error.""" 

707 console = _get_rich_console(stderr=True) 1haebfcdg

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