Coverage for typer/rich_utils.py: 100%

291 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-03-10 00:15 +0000

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

2 

3import inspect 1iaebfgcdh

4import io 1iaebfgcdh

5import sys 1iaebfgcdh

6from collections import defaultdict 1iaebfgcdh

7from gettext import gettext as _ 1iaebfgcdh

8from os import getenv 1iaebfgcdh

9from typing import Any, DefaultDict, Dict, Iterable, List, Optional, Union 1iaebfgcdh

10 

11import click 1iaebfgcdh

12from rich import box 1iaebfgcdh

13from rich.align import Align 1iaebfgcdh

14from rich.columns import Columns 1iaebfgcdh

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

16from rich.emoji import Emoji 1iaebfgcdh

17from rich.highlighter import RegexHighlighter 1iaebfgcdh

18from rich.markdown import Markdown 1iaebfgcdh

19from rich.padding import Padding 1iaebfgcdh

20from rich.panel import Panel 1iaebfgcdh

21from rich.table import Table 1iaebfgcdh

22from rich.text import Text 1iaebfgcdh

23from rich.theme import Theme 1iaebfgcdh

24 

25if sys.version_info >= (3, 9): 1iaebfgcdh

26 from typing import Literal 1aebfgdh

27else: 

28 from typing_extensions import Literal 1ic

29 

30# Default styles 

31STYLE_OPTION = "bold cyan" 1iaebfgcdh

32STYLE_SWITCH = "bold green" 1iaebfgcdh

33STYLE_NEGATIVE_OPTION = "bold magenta" 1iaebfgcdh

34STYLE_NEGATIVE_SWITCH = "bold red" 1iaebfgcdh

35STYLE_METAVAR = "bold yellow" 1iaebfgcdh

36STYLE_METAVAR_SEPARATOR = "dim" 1iaebfgcdh

37STYLE_USAGE = "yellow" 1iaebfgcdh

38STYLE_USAGE_COMMAND = "bold" 1iaebfgcdh

39STYLE_DEPRECATED = "red" 1iaebfgcdh

40STYLE_DEPRECATED_COMMAND = "dim" 1iaebfgcdh

41STYLE_HELPTEXT_FIRST_LINE = "" 1iaebfgcdh

42STYLE_HELPTEXT = "dim" 1iaebfgcdh

43STYLE_OPTION_HELP = "" 1iaebfgcdh

44STYLE_OPTION_DEFAULT = "dim" 1iaebfgcdh

45STYLE_OPTION_ENVVAR = "dim yellow" 1iaebfgcdh

46STYLE_REQUIRED_SHORT = "red" 1iaebfgcdh

47STYLE_REQUIRED_LONG = "dim red" 1iaebfgcdh

48STYLE_OPTIONS_PANEL_BORDER = "dim" 1iaebfgcdh

49ALIGN_OPTIONS_PANEL: Literal["left", "center", "right"] = "left" 1iaebfgcdh

50STYLE_OPTIONS_TABLE_SHOW_LINES = False 1iaebfgcdh

51STYLE_OPTIONS_TABLE_LEADING = 0 1iaebfgcdh

52STYLE_OPTIONS_TABLE_PAD_EDGE = False 1iaebfgcdh

53STYLE_OPTIONS_TABLE_PADDING = (0, 1) 1iaebfgcdh

54STYLE_OPTIONS_TABLE_BOX = "" 1iaebfgcdh

55STYLE_OPTIONS_TABLE_ROW_STYLES = None 1iaebfgcdh

56STYLE_OPTIONS_TABLE_BORDER_STYLE = None 1iaebfgcdh

57STYLE_COMMANDS_PANEL_BORDER = "dim" 1iaebfgcdh

58ALIGN_COMMANDS_PANEL: Literal["left", "center", "right"] = "left" 1iaebfgcdh

59STYLE_COMMANDS_TABLE_SHOW_LINES = False 1iaebfgcdh

60STYLE_COMMANDS_TABLE_LEADING = 0 1iaebfgcdh

61STYLE_COMMANDS_TABLE_PAD_EDGE = False 1iaebfgcdh

62STYLE_COMMANDS_TABLE_PADDING = (0, 1) 1iaebfgcdh

63STYLE_COMMANDS_TABLE_BOX = "" 1iaebfgcdh

64STYLE_COMMANDS_TABLE_ROW_STYLES = None 1iaebfgcdh

65STYLE_COMMANDS_TABLE_BORDER_STYLE = None 1iaebfgcdh

66STYLE_COMMANDS_TABLE_FIRST_COLUMN = "bold cyan" 1iaebfgcdh

67STYLE_ERRORS_PANEL_BORDER = "red" 1iaebfgcdh

68ALIGN_ERRORS_PANEL: Literal["left", "center", "right"] = "left" 1iaebfgcdh

69STYLE_ERRORS_SUGGESTION = "dim" 1iaebfgcdh

70STYLE_ABORTED = "red" 1iaebfgcdh

71_TERMINAL_WIDTH = getenv("TERMINAL_WIDTH") 1iaebfgcdh

72MAX_WIDTH = int(_TERMINAL_WIDTH) if _TERMINAL_WIDTH else None 1iaebfgcdh

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

74 "auto" # Set to None to disable colors 

75) 

76_TYPER_FORCE_DISABLE_TERMINAL = getenv("_TYPER_FORCE_DISABLE_TERMINAL") 1iaebfgcdh

77FORCE_TERMINAL = ( 1aebfgcdh

78 True 

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

80 else None 

81) 

82if _TYPER_FORCE_DISABLE_TERMINAL: 1iaebfgcdh

83 FORCE_TERMINAL = False 1iaebfgcdh

84 

85# Fixed strings 

86DEPRECATED_STRING = _("(deprecated) ") 1iaebfgcdh

87DEFAULT_STRING = _("[default: {}]") 1iaebfgcdh

88ENVVAR_STRING = _("[env var: {}]") 1iaebfgcdh

89REQUIRED_SHORT_STRING = "*" 1iaebfgcdh

90REQUIRED_LONG_STRING = _("[required]") 1iaebfgcdh

91RANGE_STRING = " [{}]" 1iaebfgcdh

92ARGUMENTS_PANEL_TITLE = _("Arguments") 1iaebfgcdh

93OPTIONS_PANEL_TITLE = _("Options") 1iaebfgcdh

94COMMANDS_PANEL_TITLE = _("Commands") 1iaebfgcdh

95ERRORS_PANEL_TITLE = _("Error") 1iaebfgcdh

96ABORTED_TEXT = _("Aborted.") 1iaebfgcdh

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

98 

99MARKUP_MODE_MARKDOWN = "markdown" 1iaebfgcdh

100MARKUP_MODE_RICH = "rich" 1iaebfgcdh

101_RICH_HELP_PANEL_NAME = "rich_help_panel" 1iaebfgcdh

102 

103MarkupMode = Literal["markdown", "rich", None] 1iaebfgcdh

104 

105 

106# Rich regex highlighter 

107class OptionHighlighter(RegexHighlighter): 1iaebfgcdh

108 """Highlights our special options.""" 

109 

110 highlights = [ 1aebfgcdh

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

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

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

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

115 ] 

116 

117 

118class NegativeOptionHighlighter(RegexHighlighter): 1iaebfgcdh

119 highlights = [ 1aebfgcdh

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

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

122 ] 

123 

124 

125highlighter = OptionHighlighter() 1iaebfgcdh

126negative_highlighter = NegativeOptionHighlighter() 1iaebfgcdh

127 

128 

129def _get_rich_console(stderr: bool = False) -> Console: 1iaebfgcdh

130 return Console( 1iaebfgcdh

131 theme=Theme( 

132 { 

133 "option": STYLE_OPTION, 

134 "switch": STYLE_SWITCH, 

135 "negative_option": STYLE_NEGATIVE_OPTION, 

136 "negative_switch": STYLE_NEGATIVE_SWITCH, 

137 "metavar": STYLE_METAVAR, 

138 "metavar_sep": STYLE_METAVAR_SEPARATOR, 

139 "usage": STYLE_USAGE, 

140 }, 

141 ), 

142 highlighter=highlighter, 

143 color_system=COLOR_SYSTEM, 

144 force_terminal=FORCE_TERMINAL, 

145 width=MAX_WIDTH, 

146 stderr=stderr, 

147 ) 

148 

149 

150def _make_rich_text( 1aebfgcdh

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

152) -> Union[Markdown, Text]: 

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

154 

155 By default, the text is not parsed for any special formatting. 

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

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

158 """ 

159 # Remove indentations from input text 

160 text = inspect.cleandoc(text) 1iaebfgcdh

161 if markup_mode == MARKUP_MODE_MARKDOWN: 1iaebfgcdh

162 text = Emoji.replace(text) 1iaebfgcdh

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

164 if markup_mode == MARKUP_MODE_RICH: 1iaebfgcdh

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

166 else: 

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

168 

169 

170@group() 1iaebfgcdh

171def _get_help_text( 1aebfgcdh

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: 1iaebfgcdh

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

185 

186 # Fetch and dedent the help text 

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

188 

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

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

191 

192 # Get the first paragraph 

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

194 # Remove single linebreaks 

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

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

197 yield _make_rich_text( 1iaebfgcdh

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:] 1iaebfgcdh

205 if remaining_paragraphs: 1iaebfgcdh

206 if markup_mode != MARKUP_MODE_RICH: 1iaebfgcdh

207 # Remove single linebreaks 

208 remaining_paragraphs = [ 1aebfgcdh

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

216 else: 

217 # Join with double linebreaks if markdown 

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

219 

220 yield _make_rich_text( 1iaebfgcdh

221 text=remaining_lines, 

222 style=STYLE_HELPTEXT, 

223 markup_mode=markup_mode, 

224 ) 

225 

226 

227def _get_parameter_help( 1aebfgcdh

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 1iaebfgcdh

242 

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

244 

245 # Get the environment variable first 

246 

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

248 var_str = "" 1iaebfgcdh

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

250 if envvar is None: 1iaebfgcdh

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()}" 1iaebfgcdh

257 if envvar is not None: 1iaebfgcdh

258 var_str = ( 1aebfgcdh

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

264 if help_value: 1iaebfgcdh

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

266 # Remove single linebreaks 

267 if markup_mode != MARKUP_MODE_MARKDOWN: 1iaebfgcdh

268 paragraphs = [ 1aebfgcdh

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( 1iaebfgcdh

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): 1iaebfgcdh

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

285 

286 # Default value 

287 # This uses Typer's specific param._get_default_string 

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

289 if param.show_default: 1iaebfgcdh

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

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

292 default_str = param._get_default_string( 1iaebfgcdh

293 ctx=ctx, 

294 show_default_is_str=show_default_is_str, 

295 default_value=default_value, 

296 ) 

297 if default_str: 1iaebfgcdh

298 items.append( 1iaebfgcdh

299 Text( 

300 DEFAULT_STRING.format(default_str), 

301 style=STYLE_OPTION_DEFAULT, 

302 ) 

303 ) 

304 

305 # Required? 

306 if param.required: 1iaebfgcdh

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

308 

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

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

311 return Columns(items) 1iaebfgcdh

312 

313 

314def _make_command_help( 1aebfgcdh

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") 1iaebfgcdh

329 # Remove single linebreaks 

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

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

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

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

334 return _make_rich_text( 1iaebfgcdh

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

336 style=STYLE_OPTION_HELP, 

337 markup_mode=markup_mode, 

338 ) 

339 

340 

341def _print_options_panel( 1aebfgcdh

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]] = [] 1iaebfgcdh

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

351 for param in params: 1iaebfgcdh

352 # Short and long form 

353 opt_long_strs = [] 1iaebfgcdh

354 opt_short_strs = [] 1iaebfgcdh

355 secondary_opt_long_strs = [] 1iaebfgcdh

356 secondary_opt_short_strs = [] 1iaebfgcdh

357 for opt_str in param.opts: 1iaebfgcdh

358 if "--" in opt_str: 1iaebfgcdh

359 opt_long_strs.append(opt_str) 1iaebfgcdh

360 else: 

361 opt_short_strs.append(opt_str) 1iaebfgcdh

362 for opt_str in param.secondary_opts: 1iaebfgcdh

363 if "--" in opt_str: 1iaebfgcdh

364 secondary_opt_long_strs.append(opt_str) 1iaebfgcdh

365 else: 

366 secondary_opt_short_strs.append(opt_str) 1iaebfgcdh

367 

368 # Column for a metavar, if we have one 

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

370 metavar_str = param.make_metavar() 1iaebfgcdh

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() 1iaebfgcdh

379 

380 # Skip booleans and choices (handled above) 

381 if metavar_str != "BOOLEAN": 1iaebfgcdh

382 metavar.append(metavar_str) 1iaebfgcdh

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() 1iaebfgcdh

393 if range_str: 1iaebfgcdh

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

395 

396 # Required asterisk 

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

398 if param.required: 1iaebfgcdh

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

400 

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

402 class MetavarHighlighter(RegexHighlighter): 1iaebfgcdh

403 highlights = [ 1aebfgcdh

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

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

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

407 ] 

408 

409 metavar_highlighter = MetavarHighlighter() 1iaebfgcdh

410 

411 required_rows.append(required) 1iaebfgcdh

412 options_rows.append( 1iaebfgcdh

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]] = [] 1iaebfgcdh

427 if any(required_rows): 1iaebfgcdh

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

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

430 else: 

431 rows_with_required = options_rows 1iaebfgcdh

432 if options_rows: 1iaebfgcdh

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

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

443 

444 options_table = Table( 1iaebfgcdh

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: 1iaebfgcdh

452 options_table.add_row(*row) 1iaebfgcdh

453 console.print( 1iaebfgcdh

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( 1aebfgcdh

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] = { 1aebfgcdh

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

481 

482 commands_table = Table( 1iaebfgcdh

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( 1iaebfgcdh

492 style=STYLE_COMMANDS_TABLE_FIRST_COLUMN, 

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

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

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

503 for command in commands: 1iaebfgcdh

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

505 command_name = command.name or "" 1iaebfgcdh

506 if command.deprecated: 1iaebfgcdh

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

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

509 else: 

510 command_name_text = Text(command_name) 1iaebfgcdh

511 deprecated_rows.append(None) 1iaebfgcdh

512 rows.append( 1iaebfgcdh

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 1iaebfgcdh

522 if any(deprecated_rows): 1iaebfgcdh

523 rows_with_deprecated = [] 1iaebfgcdh

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

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

526 for row in rows_with_deprecated: 1iaebfgcdh

527 commands_table.add_row(*row) 1iaebfgcdh

528 if commands_table.row_count: 1iaebfgcdh

529 console.print( 1iaebfgcdh

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( 1aebfgcdh

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() 1iaebfgcdh

554 

555 # Print usage 

556 console.print( 1iaebfgcdh

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: 1iaebfgcdh

562 # Print with some padding 

563 console.print( 1iaebfgcdh

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

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

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

578 # Skip if option is hidden 

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

580 continue 1iaebfgcdh

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

582 panel_name = ( 1aebfgcdh

583 getattr(param, _RICH_HELP_PANEL_NAME, None) or ARGUMENTS_PANEL_TITLE 

584 ) 

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

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

587 panel_name = ( 1aebfgcdh

588 getattr(param, _RICH_HELP_PANEL_NAME, None) or OPTIONS_PANEL_TITLE 

589 ) 

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

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

592 _print_options_panel( 1iaebfgcdh

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(): 1iaebfgcdh

600 if panel_name == ARGUMENTS_PANEL_TITLE: 1iaebfgcdh

601 # Already printed above 

602 continue 1iaebfgcdh

603 _print_options_panel( 1iaebfgcdh

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, []) 1iaebfgcdh

611 _print_options_panel( 1iaebfgcdh

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(): 1iaebfgcdh

619 if panel_name == OPTIONS_PANEL_TITLE: 1iaebfgcdh

620 # Already printed above 

621 continue 1iaebfgcdh

622 _print_options_panel( 1iaebfgcdh

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): 1iaebfgcdh

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

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

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

634 if command and not command.hidden: 1iaebfgcdh

635 panel_name = ( 1aebfgcdh

636 getattr(command, _RICH_HELP_PANEL_NAME, None) 

637 or COMMANDS_PANEL_TITLE 

638 ) 

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

640 

641 # Identify the longest command name in all panels 

642 max_cmd_len = max( 1iaebfgcdh

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, []) 1iaebfgcdh

653 _print_commands_panel( 1iaebfgcdh

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(): 1iaebfgcdh

661 if panel_name == COMMANDS_PANEL_TITLE: 1iaebfgcdh

662 # Already printed above 

663 continue 1iaebfgcdh

664 _print_commands_panel( 1iaebfgcdh

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: 1iaebfgcdh

674 # Remove single linebreaks, replace double with single 

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

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

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

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

679 

680 

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

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

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

689 if ctx is not None: 1iaebfgcdh

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

691 

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

693 console.print( 1iaebfgcdh

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( 1iaebfgcdh

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: 1iaebfgcdh

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

712 console = _get_rich_console(stderr=True) 1iaebfgcdh

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

714 

715 

716def rich_to_html(input_text: str) -> str: 1iaebfgcdh

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

718 

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

720 HTML-formatted text spans into a markdown file. 

721 """ 

722 console = Console(record=True, highlight=False, file=io.StringIO()) 1iaebfgcdh

723 

724 console.print(input_text, overflow="ignore", crop=False) 1iaebfgcdh

725 

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

727 

728 

729def rich_render_text(text: str) -> str: 1iaebfgcdh

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

731 console = _get_rich_console() 1iaebfgcdh

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