Coverage for typer/rich_utils.py: 100%

292 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-04-14 00:18 +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 # Add a newline inbetween the header and the remaining paragraphs 

204 yield Text("") 1iaebfgcdh

205 

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

207 remaining_paragraphs = help_text.split("\n\n")[1:] 1iaebfgcdh

208 if remaining_paragraphs: 1iaebfgcdh

209 if markup_mode != MARKUP_MODE_RICH: 1iaebfgcdh

210 # Remove single linebreaks 

211 remaining_paragraphs = [ 1aebfgcdh

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

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

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

215 for x in remaining_paragraphs 

216 ] 

217 # Join back together 

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

219 else: 

220 # Join with double linebreaks if markdown 

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

222 

223 yield _make_rich_text( 1iaebfgcdh

224 text=remaining_lines, 

225 style=STYLE_HELPTEXT, 

226 markup_mode=markup_mode, 

227 ) 

228 

229 

230def _get_parameter_help( 1aebfgcdh

231 *, 

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

233 ctx: click.Context, 

234 markup_mode: MarkupMode, 

235) -> Columns: 

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

237 

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

239 as a Rich Text object or as Markdown. 

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

241 applicable. 

242 """ 

243 # import here to avoid cyclic imports 

244 from .core import TyperArgument, TyperOption 1iaebfgcdh

245 

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

247 

248 # Get the environment variable first 

249 

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

251 var_str = "" 1iaebfgcdh

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

253 if envvar is None: 1iaebfgcdh

254 if ( 1abcd

255 getattr(param, "allow_from_autoenv", None) 

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

257 and param.name is not None 

258 ): 

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

260 if envvar is not None: 1iaebfgcdh

261 var_str = ( 1aebfgcdh

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

263 ) 

264 

265 # Main help text 

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

267 if help_value: 1iaebfgcdh

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

269 # Remove single linebreaks 

270 if markup_mode != MARKUP_MODE_MARKDOWN: 1iaebfgcdh

271 paragraphs = [ 1aebfgcdh

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

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

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

275 for x in paragraphs 

276 ] 

277 items.append( 1iaebfgcdh

278 _make_rich_text( 

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

280 style=STYLE_OPTION_HELP, 

281 markup_mode=markup_mode, 

282 ) 

283 ) 

284 

285 # Environment variable AFTER help text 

286 if envvar and getattr(param, "show_envvar", None): 1iaebfgcdh

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

288 

289 # Default value 

290 # This uses Typer's specific param._get_default_string 

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

292 if param.show_default: 1iaebfgcdh

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

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

295 default_str = param._get_default_string( 1iaebfgcdh

296 ctx=ctx, 

297 show_default_is_str=show_default_is_str, 

298 default_value=default_value, 

299 ) 

300 if default_str: 1iaebfgcdh

301 items.append( 1iaebfgcdh

302 Text( 

303 DEFAULT_STRING.format(default_str), 

304 style=STYLE_OPTION_DEFAULT, 

305 ) 

306 ) 

307 

308 # Required? 

309 if param.required: 1iaebfgcdh

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

311 

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

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

314 return Columns(items) 1iaebfgcdh

315 

316 

317def _make_command_help( 1aebfgcdh

318 *, 

319 help_text: str, 

320 markup_mode: MarkupMode, 

321) -> Union[Text, Markdown]: 

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

323 

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

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

326 

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

328 Rich Text object or as Markdown. 

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

330 """ 

331 paragraphs = inspect.cleandoc(help_text).split("\n\n") 1iaebfgcdh

332 # Remove single linebreaks 

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

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

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

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

337 return _make_rich_text( 1iaebfgcdh

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

339 style=STYLE_OPTION_HELP, 

340 markup_mode=markup_mode, 

341 ) 

342 

343 

344def _print_options_panel( 1aebfgcdh

345 *, 

346 name: str, 

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

348 ctx: click.Context, 

349 markup_mode: MarkupMode, 

350 console: Console, 

351) -> None: 

352 options_rows: List[List[RenderableType]] = [] 1iaebfgcdh

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

354 for param in params: 1iaebfgcdh

355 # Short and long form 

356 opt_long_strs = [] 1iaebfgcdh

357 opt_short_strs = [] 1iaebfgcdh

358 secondary_opt_long_strs = [] 1iaebfgcdh

359 secondary_opt_short_strs = [] 1iaebfgcdh

360 for opt_str in param.opts: 1iaebfgcdh

361 if "--" in opt_str: 1iaebfgcdh

362 opt_long_strs.append(opt_str) 1iaebfgcdh

363 else: 

364 opt_short_strs.append(opt_str) 1iaebfgcdh

365 for opt_str in param.secondary_opts: 1iaebfgcdh

366 if "--" in opt_str: 1iaebfgcdh

367 secondary_opt_long_strs.append(opt_str) 1iaebfgcdh

368 else: 

369 secondary_opt_short_strs.append(opt_str) 1iaebfgcdh

370 

371 # Column for a metavar, if we have one 

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

373 metavar_str = param.make_metavar() 1iaebfgcdh

374 

375 # Do it ourselves if this is a positional argument 

376 if ( 1abcd

377 isinstance(param, click.Argument) 

378 and param.name 

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

380 ): 

381 metavar_str = param.type.name.upper() 1iaebfgcdh

382 

383 # Skip booleans and choices (handled above) 

384 if metavar_str != "BOOLEAN": 1iaebfgcdh

385 metavar.append(metavar_str) 1iaebfgcdh

386 

387 # Range - from 

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

389 # skip count with default range type 

390 if ( 1abcd

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

392 and isinstance(param, click.Option) 

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

394 ): 

395 range_str = param.type._describe_range() 1iaebfgcdh

396 if range_str: 1iaebfgcdh

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

398 

399 # Required asterisk 

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

401 if param.required: 1iaebfgcdh

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

403 

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

405 class MetavarHighlighter(RegexHighlighter): 1iaebfgcdh

406 highlights = [ 1aebfgcdh

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

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

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

410 ] 

411 

412 metavar_highlighter = MetavarHighlighter() 1iaebfgcdh

413 

414 required_rows.append(required) 1iaebfgcdh

415 options_rows.append( 1iaebfgcdh

416 [ 

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

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

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

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

421 metavar_highlighter(metavar), 

422 _get_parameter_help( 

423 param=param, 

424 ctx=ctx, 

425 markup_mode=markup_mode, 

426 ), 

427 ] 

428 ) 

429 rows_with_required: List[List[RenderableType]] = [] 1iaebfgcdh

430 if any(required_rows): 1iaebfgcdh

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

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

433 else: 

434 rows_with_required = options_rows 1iaebfgcdh

435 if options_rows: 1iaebfgcdh

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

437 "show_lines": STYLE_OPTIONS_TABLE_SHOW_LINES, 

438 "leading": STYLE_OPTIONS_TABLE_LEADING, 

439 "box": STYLE_OPTIONS_TABLE_BOX, 

440 "border_style": STYLE_OPTIONS_TABLE_BORDER_STYLE, 

441 "row_styles": STYLE_OPTIONS_TABLE_ROW_STYLES, 

442 "pad_edge": STYLE_OPTIONS_TABLE_PAD_EDGE, 

443 "padding": STYLE_OPTIONS_TABLE_PADDING, 

444 } 

445 box_style = getattr(box, t_styles.pop("box"), None) 1iaebfgcdh

446 

447 options_table = Table( 1iaebfgcdh

448 highlight=True, 

449 show_header=False, 

450 expand=True, 

451 box=box_style, 

452 **t_styles, 

453 ) 

454 for row in rows_with_required: 1iaebfgcdh

455 options_table.add_row(*row) 1iaebfgcdh

456 console.print( 1iaebfgcdh

457 Panel( 

458 options_table, 

459 border_style=STYLE_OPTIONS_PANEL_BORDER, 

460 title=name, 

461 title_align=ALIGN_OPTIONS_PANEL, 

462 ) 

463 ) 

464 

465 

466def _print_commands_panel( 1aebfgcdh

467 *, 

468 name: str, 

469 commands: List[click.Command], 

470 markup_mode: MarkupMode, 

471 console: Console, 

472 cmd_len: int, 

473) -> None: 

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

475 "show_lines": STYLE_COMMANDS_TABLE_SHOW_LINES, 

476 "leading": STYLE_COMMANDS_TABLE_LEADING, 

477 "box": STYLE_COMMANDS_TABLE_BOX, 

478 "border_style": STYLE_COMMANDS_TABLE_BORDER_STYLE, 

479 "row_styles": STYLE_COMMANDS_TABLE_ROW_STYLES, 

480 "pad_edge": STYLE_COMMANDS_TABLE_PAD_EDGE, 

481 "padding": STYLE_COMMANDS_TABLE_PADDING, 

482 } 

483 box_style = getattr(box, t_styles.pop("box"), None) 1iaebfgcdh

484 

485 commands_table = Table( 1iaebfgcdh

486 highlight=False, 

487 show_header=False, 

488 expand=True, 

489 box=box_style, 

490 **t_styles, 

491 ) 

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

493 # regex 

494 commands_table.add_column( 1iaebfgcdh

495 style=STYLE_COMMANDS_TABLE_FIRST_COLUMN, 

496 no_wrap=True, 

497 width=cmd_len, 

498 ) 

499 

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

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

502 # other panels. 

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

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

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

506 for command in commands: 1iaebfgcdh

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

508 command_name = command.name or "" 1iaebfgcdh

509 if command.deprecated: 1iaebfgcdh

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

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

512 else: 

513 command_name_text = Text(command_name) 1iaebfgcdh

514 deprecated_rows.append(None) 1iaebfgcdh

515 rows.append( 1iaebfgcdh

516 [ 

517 command_name_text, 

518 _make_command_help( 

519 help_text=helptext, 

520 markup_mode=markup_mode, 

521 ), 

522 ] 

523 ) 

524 rows_with_deprecated = rows 1iaebfgcdh

525 if any(deprecated_rows): 1iaebfgcdh

526 rows_with_deprecated = [] 1iaebfgcdh

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

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

529 for row in rows_with_deprecated: 1iaebfgcdh

530 commands_table.add_row(*row) 1iaebfgcdh

531 if commands_table.row_count: 1iaebfgcdh

532 console.print( 1iaebfgcdh

533 Panel( 

534 commands_table, 

535 border_style=STYLE_COMMANDS_PANEL_BORDER, 

536 title=name, 

537 title_align=ALIGN_COMMANDS_PANEL, 

538 ) 

539 ) 

540 

541 

542def rich_format_help( 1aebfgcdh

543 *, 

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

545 ctx: click.Context, 

546 markup_mode: MarkupMode, 

547) -> None: 

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

549 

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

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

552 

553 Replacement for the click function format_help(). 

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

555 """ 

556 console = _get_rich_console() 1iaebfgcdh

557 

558 # Print usage 

559 console.print( 1iaebfgcdh

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

561 ) 

562 

563 # Print command / group help if we have some 

564 if obj.help: 1iaebfgcdh

565 # Print with some padding 

566 console.print( 1iaebfgcdh

567 Padding( 

568 Align( 

569 _get_help_text( 

570 obj=obj, 

571 markup_mode=markup_mode, 

572 ), 

573 pad=False, 

574 ), 

575 (0, 1, 1, 1), 

576 ) 

577 ) 

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

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

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

581 # Skip if option is hidden 

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

583 continue 1iaebfgcdh

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

585 panel_name = ( 1aebfgcdh

586 getattr(param, _RICH_HELP_PANEL_NAME, None) or ARGUMENTS_PANEL_TITLE 

587 ) 

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

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

590 panel_name = ( 1aebfgcdh

591 getattr(param, _RICH_HELP_PANEL_NAME, None) or OPTIONS_PANEL_TITLE 

592 ) 

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

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

595 _print_options_panel( 1iaebfgcdh

596 name=ARGUMENTS_PANEL_TITLE, 

597 params=default_arguments, 

598 ctx=ctx, 

599 markup_mode=markup_mode, 

600 console=console, 

601 ) 

602 for panel_name, arguments in panel_to_arguments.items(): 1iaebfgcdh

603 if panel_name == ARGUMENTS_PANEL_TITLE: 1iaebfgcdh

604 # Already printed above 

605 continue 1iaebfgcdh

606 _print_options_panel( 1iaebfgcdh

607 name=panel_name, 

608 params=arguments, 

609 ctx=ctx, 

610 markup_mode=markup_mode, 

611 console=console, 

612 ) 

613 default_options = panel_to_options.get(OPTIONS_PANEL_TITLE, []) 1iaebfgcdh

614 _print_options_panel( 1iaebfgcdh

615 name=OPTIONS_PANEL_TITLE, 

616 params=default_options, 

617 ctx=ctx, 

618 markup_mode=markup_mode, 

619 console=console, 

620 ) 

621 for panel_name, options in panel_to_options.items(): 1iaebfgcdh

622 if panel_name == OPTIONS_PANEL_TITLE: 1iaebfgcdh

623 # Already printed above 

624 continue 1iaebfgcdh

625 _print_options_panel( 1iaebfgcdh

626 name=panel_name, 

627 params=options, 

628 ctx=ctx, 

629 markup_mode=markup_mode, 

630 console=console, 

631 ) 

632 

633 if isinstance(obj, click.Group): 1iaebfgcdh

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

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

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

637 if command and not command.hidden: 1iaebfgcdh

638 panel_name = ( 1aebfgcdh

639 getattr(command, _RICH_HELP_PANEL_NAME, None) 

640 or COMMANDS_PANEL_TITLE 

641 ) 

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

643 

644 # Identify the longest command name in all panels 

645 max_cmd_len = max( 1iaebfgcdh

646 [ 

647 len(command.name or "") 

648 for commands in panel_to_commands.values() 

649 for command in commands 

650 ], 

651 default=0, 

652 ) 

653 

654 # Print each command group panel 

655 default_commands = panel_to_commands.get(COMMANDS_PANEL_TITLE, []) 1iaebfgcdh

656 _print_commands_panel( 1iaebfgcdh

657 name=COMMANDS_PANEL_TITLE, 

658 commands=default_commands, 

659 markup_mode=markup_mode, 

660 console=console, 

661 cmd_len=max_cmd_len, 

662 ) 

663 for panel_name, commands in panel_to_commands.items(): 1iaebfgcdh

664 if panel_name == COMMANDS_PANEL_TITLE: 1iaebfgcdh

665 # Already printed above 

666 continue 1iaebfgcdh

667 _print_commands_panel( 1iaebfgcdh

668 name=panel_name, 

669 commands=commands, 

670 markup_mode=markup_mode, 

671 console=console, 

672 cmd_len=max_cmd_len, 

673 ) 

674 

675 # Epilogue if we have it 

676 if obj.epilog: 1iaebfgcdh

677 # Remove single linebreaks, replace double with single 

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

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

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

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

682 

683 

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

685 """Print richly formatted click errors. 

686 

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

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

689 """ 

690 console = _get_rich_console(stderr=True) 1iaebfgcdh

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

692 if ctx is not None: 1iaebfgcdh

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

694 

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

696 console.print( 1iaebfgcdh

697 RICH_HELP.format( 

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

699 ), 

700 style=STYLE_ERRORS_SUGGESTION, 

701 ) 

702 

703 console.print( 1iaebfgcdh

704 Panel( 

705 highlighter(self.format_message()), 

706 border_style=STYLE_ERRORS_PANEL_BORDER, 

707 title=ERRORS_PANEL_TITLE, 

708 title_align=ALIGN_ERRORS_PANEL, 

709 ) 

710 ) 

711 

712 

713def rich_abort_error() -> None: 1iaebfgcdh

714 """Print richly formatted abort error.""" 

715 console = _get_rich_console(stderr=True) 1iaebfgcdh

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

717 

718 

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

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

721 

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

723 HTML-formatted text spans into a markdown file. 

724 """ 

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

726 

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

728 

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

730 

731 

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

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

734 console = _get_rich_console() 1iaebfgcdh

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