Coverage for typer/rich_utils.py: 100%

300 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-12-19 20:29 +0000

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

2 

3import inspect 1aefbghcdi

4import io 1aefbghcdi

5import sys 1aefbghcdi

6from collections import defaultdict 1aefbghcdi

7from gettext import gettext as _ 1aefbghcdi

8from os import getenv 1aefbghcdi

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

10 

11import click 1aefbghcdi

12from rich import box 1aefbghcdi

13from rich.align import Align 1aefbghcdi

14from rich.columns import Columns 1aefbghcdi

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

16from rich.emoji import Emoji 1aefbghcdi

17from rich.highlighter import RegexHighlighter 1aefbghcdi

18from rich.markdown import Markdown 1aefbghcdi

19from rich.markup import escape 1aefbghcdi

20from rich.padding import Padding 1aefbghcdi

21from rich.panel import Panel 1aefbghcdi

22from rich.table import Table 1aefbghcdi

23from rich.text import Text 1aefbghcdi

24from rich.theme import Theme 1aefbghcdi

25from rich.traceback import Traceback 1aefbghcdi

26from typer.models import DeveloperExceptionConfig 1aefbghcdi

27 

28if sys.version_info >= (3, 9): 1aefbghcdi

29 from typing import Literal 1aefbghdi

30else: 

31 from typing_extensions import Literal 1c

32 

33# Default styles 

34STYLE_OPTION = "bold cyan" 1aefbghcdi

35STYLE_SWITCH = "bold green" 1aefbghcdi

36STYLE_NEGATIVE_OPTION = "bold magenta" 1aefbghcdi

37STYLE_NEGATIVE_SWITCH = "bold red" 1aefbghcdi

38STYLE_METAVAR = "bold yellow" 1aefbghcdi

39STYLE_METAVAR_SEPARATOR = "dim" 1aefbghcdi

40STYLE_USAGE = "yellow" 1aefbghcdi

41STYLE_USAGE_COMMAND = "bold" 1aefbghcdi

42STYLE_DEPRECATED = "red" 1aefbghcdi

43STYLE_DEPRECATED_COMMAND = "dim" 1aefbghcdi

44STYLE_HELPTEXT_FIRST_LINE = "" 1aefbghcdi

45STYLE_HELPTEXT = "dim" 1aefbghcdi

46STYLE_OPTION_HELP = "" 1aefbghcdi

47STYLE_OPTION_DEFAULT = "dim" 1aefbghcdi

48STYLE_OPTION_ENVVAR = "dim yellow" 1aefbghcdi

49STYLE_REQUIRED_SHORT = "red" 1aefbghcdi

50STYLE_REQUIRED_LONG = "dim red" 1aefbghcdi

51STYLE_OPTIONS_PANEL_BORDER = "dim" 1aefbghcdi

52ALIGN_OPTIONS_PANEL: Literal["left", "center", "right"] = "left" 1aefbghcdi

53STYLE_OPTIONS_TABLE_SHOW_LINES = False 1aefbghcdi

54STYLE_OPTIONS_TABLE_LEADING = 0 1aefbghcdi

55STYLE_OPTIONS_TABLE_PAD_EDGE = False 1aefbghcdi

56STYLE_OPTIONS_TABLE_PADDING = (0, 1) 1aefbghcdi

57STYLE_OPTIONS_TABLE_BOX = "" 1aefbghcdi

58STYLE_OPTIONS_TABLE_ROW_STYLES = None 1aefbghcdi

59STYLE_OPTIONS_TABLE_BORDER_STYLE = None 1aefbghcdi

60STYLE_COMMANDS_PANEL_BORDER = "dim" 1aefbghcdi

61ALIGN_COMMANDS_PANEL: Literal["left", "center", "right"] = "left" 1aefbghcdi

62STYLE_COMMANDS_TABLE_SHOW_LINES = False 1aefbghcdi

63STYLE_COMMANDS_TABLE_LEADING = 0 1aefbghcdi

64STYLE_COMMANDS_TABLE_PAD_EDGE = False 1aefbghcdi

65STYLE_COMMANDS_TABLE_PADDING = (0, 1) 1aefbghcdi

66STYLE_COMMANDS_TABLE_BOX = "" 1aefbghcdi

67STYLE_COMMANDS_TABLE_ROW_STYLES = None 1aefbghcdi

68STYLE_COMMANDS_TABLE_BORDER_STYLE = None 1aefbghcdi

69STYLE_COMMANDS_TABLE_FIRST_COLUMN = "bold cyan" 1aefbghcdi

70STYLE_ERRORS_PANEL_BORDER = "red" 1aefbghcdi

71ALIGN_ERRORS_PANEL: Literal["left", "center", "right"] = "left" 1aefbghcdi

72STYLE_ERRORS_SUGGESTION = "dim" 1aefbghcdi

73STYLE_ABORTED = "red" 1aefbghcdi

74_TERMINAL_WIDTH = getenv("TERMINAL_WIDTH") 1aefbghcdi

75MAX_WIDTH = int(_TERMINAL_WIDTH) if _TERMINAL_WIDTH else None 1aefbghcdi

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

77 "auto" # Set to None to disable colors 

78) 

79_TYPER_FORCE_DISABLE_TERMINAL = getenv("_TYPER_FORCE_DISABLE_TERMINAL") 1aefbghcdi

80FORCE_TERMINAL = ( 1aefbghcdi

81 True 

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

83 else None 

84) 

85if _TYPER_FORCE_DISABLE_TERMINAL: 1aefbghcdi

86 FORCE_TERMINAL = False 1aefbghcdi

87 

88# Fixed strings 

89DEPRECATED_STRING = _("(deprecated) ") 1aefbghcdi

90DEFAULT_STRING = _("[default: {}]") 1aefbghcdi

91ENVVAR_STRING = _("[env var: {}]") 1aefbghcdi

92REQUIRED_SHORT_STRING = "*" 1aefbghcdi

93REQUIRED_LONG_STRING = _("[required]") 1aefbghcdi

94RANGE_STRING = " [{}]" 1aefbghcdi

95ARGUMENTS_PANEL_TITLE = _("Arguments") 1aefbghcdi

96OPTIONS_PANEL_TITLE = _("Options") 1aefbghcdi

97COMMANDS_PANEL_TITLE = _("Commands") 1aefbghcdi

98ERRORS_PANEL_TITLE = _("Error") 1aefbghcdi

99ABORTED_TEXT = _("Aborted.") 1aefbghcdi

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

101 

102MARKUP_MODE_MARKDOWN = "markdown" 1aefbghcdi

103MARKUP_MODE_RICH = "rich" 1aefbghcdi

104_RICH_HELP_PANEL_NAME = "rich_help_panel" 1aefbghcdi

105 

106MarkupModeStrict = Literal["markdown", "rich"] 1aefbghcdi

107 

108 

109# Rich regex highlighter 

110class OptionHighlighter(RegexHighlighter): 1aefbghcdi

111 """Highlights our special options.""" 

112 

113 highlights = [ 1aefbghcdi

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

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

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

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

118 ] 

119 

120 

121class NegativeOptionHighlighter(RegexHighlighter): 1aefbghcdi

122 highlights = [ 1aefbghcdi

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

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

125 ] 

126 

127 

128highlighter = OptionHighlighter() 1aefbghcdi

129negative_highlighter = NegativeOptionHighlighter() 1aefbghcdi

130 

131 

132def _get_rich_console(stderr: bool = False) -> Console: 1aefbghcdi

133 return Console( 1aefbghcdi

134 theme=Theme( 

135 { 

136 "option": STYLE_OPTION, 

137 "switch": STYLE_SWITCH, 

138 "negative_option": STYLE_NEGATIVE_OPTION, 

139 "negative_switch": STYLE_NEGATIVE_SWITCH, 

140 "metavar": STYLE_METAVAR, 

141 "metavar_sep": STYLE_METAVAR_SEPARATOR, 

142 "usage": STYLE_USAGE, 

143 }, 

144 ), 

145 highlighter=highlighter, 

146 color_system=COLOR_SYSTEM, 

147 force_terminal=FORCE_TERMINAL, 

148 width=MAX_WIDTH, 

149 stderr=stderr, 

150 ) 

151 

152 

153def _make_rich_text( 1aefbghcdi

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

155) -> Union[Markdown, Text]: 

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

157 

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

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

160 """ 

161 # Remove indentations from input text 

162 text = inspect.cleandoc(text) 1aefbghcdi

163 if markup_mode == MARKUP_MODE_MARKDOWN: 1aefbghcdi

164 text = Emoji.replace(text) 1aefbghcdi

165 return Markdown(text, style=style) 1aefbghcdi

166 else: 

167 assert markup_mode == MARKUP_MODE_RICH 1aefbghcdi

168 return highlighter(Text.from_markup(text, style=style)) 1aefbghcdi

169 

170 

171@group() 1aefbghcdi

172def _get_help_text( 1aefbghcdi

173 *, 

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

175 markup_mode: MarkupModeStrict, 

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

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

178 

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

180 Rich Text object or as Markdown. 

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

182 """ 

183 # Prepend deprecated status 

184 if obj.deprecated: 1aefbghcdi

185 yield Text(DEPRECATED_STRING, style=STYLE_DEPRECATED) 1aefbghcdi

186 

187 # Fetch and dedent the help text 

188 help_text = inspect.cleandoc(obj.help or "") 1aefbghcdi

189 

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

191 help_text = help_text.partition("\f")[0] 1aefbghcdi

192 

193 # Get the first paragraph 

194 first_line, *remaining_paragraphs = help_text.split("\n\n") 1aefbghcdi

195 

196 # Remove single linebreaks 

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

198 first_line = first_line.replace("\n", " ") 1aefbghcdi

199 yield _make_rich_text( 1aefbghcdi

200 text=first_line.strip(), 

201 style=STYLE_HELPTEXT_FIRST_LINE, 

202 markup_mode=markup_mode, 

203 ) 

204 

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

206 if remaining_paragraphs: 1aefbghcdi

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

208 yield Text("") 1aefbghcdi

209 # Join with double linebreaks for markdown and Rich markup 

210 remaining_lines = "\n\n".join(remaining_paragraphs) 1aefbghcdi

211 

212 yield _make_rich_text( 1aefbghcdi

213 text=remaining_lines, 

214 style=STYLE_HELPTEXT, 

215 markup_mode=markup_mode, 

216 ) 

217 

218 

219def _get_parameter_help( 1aefbghcdi

220 *, 

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

222 ctx: click.Context, 

223 markup_mode: MarkupModeStrict, 

224) -> Columns: 

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

226 

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

228 as a Rich Text object or as Markdown. 

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

230 applicable. 

231 """ 

232 # import here to avoid cyclic imports 

233 from .core import TyperArgument, TyperOption 1aefbghcdi

234 

235 items: List[Union[Text, Markdown]] = [] 1aefbghcdi

236 

237 # Get the environment variable first 

238 

239 envvar = getattr(param, "envvar", None) 1aefbghcdi

240 var_str = "" 1aefbghcdi

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

242 if envvar is None: 1aefbghcdi

243 if ( 1abcd

244 getattr(param, "allow_from_autoenv", None) 

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

246 and param.name is not None 

247 ): 

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

249 if envvar is not None: 1aefbghcdi

250 var_str = ( 1aefbghcdi

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

252 ) 

253 

254 # Main help text 

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

256 if help_value: 1aefbghcdi

257 paragraphs = help_value.split("\n\n") 1aefbghcdi

258 # Remove single linebreaks 

259 if markup_mode != MARKUP_MODE_MARKDOWN: 1aefbghcdi

260 paragraphs = [ 1aefbghcdi

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

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

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

264 for x in paragraphs 

265 ] 

266 items.append( 1aefbghcdi

267 _make_rich_text( 

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

269 style=STYLE_OPTION_HELP, 

270 markup_mode=markup_mode, 

271 ) 

272 ) 

273 

274 # Environment variable AFTER help text 

275 if envvar and getattr(param, "show_envvar", None): 1aefbghcdi

276 items.append(Text(ENVVAR_STRING.format(var_str), style=STYLE_OPTION_ENVVAR)) 1aefbghcdi

277 

278 # Default value 

279 # This uses Typer's specific param._get_default_string 

280 if isinstance(param, (TyperOption, TyperArgument)): 1aefbghcdi

281 default_value = param._extract_default_help_str(ctx=ctx) 1aefbghcdi

282 show_default_is_str = isinstance(param.show_default, str) 1aefbghcdi

283 if show_default_is_str or ( 1aefbghcdi

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

285 ): 

286 default_str = param._get_default_string( 1aefbghcdi

287 ctx=ctx, 

288 show_default_is_str=show_default_is_str, 

289 default_value=default_value, 

290 ) 

291 if default_str: 1aefbghcdi

292 items.append( 1aefbghcdi

293 Text( 

294 DEFAULT_STRING.format(default_str), 

295 style=STYLE_OPTION_DEFAULT, 

296 ) 

297 ) 

298 

299 # Required? 

300 if param.required: 1aefbghcdi

301 items.append(Text(REQUIRED_LONG_STRING, style=STYLE_REQUIRED_LONG)) 1aefbghcdi

302 

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

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

305 return Columns(items) 1aefbghcdi

306 

307 

308def _make_command_help( 1aefbghcdi

309 *, 

310 help_text: str, 

311 markup_mode: MarkupModeStrict, 

312) -> Union[Text, Markdown]: 

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

314 

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

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

317 

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

319 Rich Text object or as Markdown. 

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

321 """ 

322 paragraphs = inspect.cleandoc(help_text).split("\n\n") 1aefbghcdi

323 # Remove single linebreaks 

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

325 paragraphs[0] = paragraphs[0].replace("\n", " ") 1aefbghcdi

326 elif paragraphs[0].startswith("\b"): 1aefbghcdi

327 paragraphs[0] = paragraphs[0].replace("\b\n", "") 1aefbghcdi

328 return _make_rich_text( 1aefbghcdi

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

330 style=STYLE_OPTION_HELP, 

331 markup_mode=markup_mode, 

332 ) 

333 

334 

335def _print_options_panel( 1aefbghcdi

336 *, 

337 name: str, 

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

339 ctx: click.Context, 

340 markup_mode: MarkupModeStrict, 

341 console: Console, 

342) -> None: 

343 options_rows: List[List[RenderableType]] = [] 1aefbghcdi

344 required_rows: List[Union[str, Text]] = [] 1aefbghcdi

345 for param in params: 1aefbghcdi

346 # Short and long form 

347 opt_long_strs = [] 1aefbghcdi

348 opt_short_strs = [] 1aefbghcdi

349 secondary_opt_long_strs = [] 1aefbghcdi

350 secondary_opt_short_strs = [] 1aefbghcdi

351 for opt_str in param.opts: 1aefbghcdi

352 if "--" in opt_str: 1aefbghcdi

353 opt_long_strs.append(opt_str) 1aefbghcdi

354 else: 

355 opt_short_strs.append(opt_str) 1aefbghcdi

356 for opt_str in param.secondary_opts: 1aefbghcdi

357 if "--" in opt_str: 1aefbghcdi

358 secondary_opt_long_strs.append(opt_str) 1aefbghcdi

359 else: 

360 secondary_opt_short_strs.append(opt_str) 1aefbghcdi

361 

362 # Column for a metavar, if we have one 

363 metavar = Text(style=STYLE_METAVAR, overflow="fold") 1aefbghcdi

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

365 signature = inspect.signature(param.make_metavar) 1aefbghcdi

366 if "ctx" in signature.parameters: 1aefbghcdi

367 metavar_str = param.make_metavar(ctx=ctx) 1aefbghcdi

368 else: 

369 # Click < 8.2 

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

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

379 

380 # Skip booleans and choices (handled above) 

381 if metavar_str != "BOOLEAN": 1aefbghcdi

382 metavar.append(metavar_str) 1aefbghcdi

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

393 if range_str: 1aefbghcdi

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

395 

396 # Required asterisk 

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

398 if param.required: 1aefbghcdi

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

400 

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

402 class MetavarHighlighter(RegexHighlighter): 1aefbghcdi

403 highlights = [ 1aefbghcdi

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

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

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

407 ] 

408 

409 metavar_highlighter = MetavarHighlighter() 1aefbghcdi

410 

411 required_rows.append(required) 1aefbghcdi

412 options_rows.append( 1aefbghcdi

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

427 if any(required_rows): 1aefbghcdi

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

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

430 else: 

431 rows_with_required = options_rows 1aefbghcdi

432 if options_rows: 1aefbghcdi

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

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

443 

444 options_table = Table( 1aefbghcdi

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

452 options_table.add_row(*row) 1aefbghcdi

453 console.print( 1aefbghcdi

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

464 *, 

465 name: str, 

466 commands: List[click.Command], 

467 markup_mode: MarkupModeStrict, 

468 console: Console, 

469 cmd_len: int, 

470) -> None: 

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

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

481 

482 commands_table = Table( 1aefbghcdi

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

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

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

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

503 for command in commands: 1aefbghcdi

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

505 command_name = command.name or "" 1aefbghcdi

506 if command.deprecated: 1aefbghcdi

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

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

509 else: 

510 command_name_text = Text(command_name) 1aefbghcdi

511 deprecated_rows.append(None) 1aefbghcdi

512 rows.append( 1aefbghcdi

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

522 if any(deprecated_rows): 1aefbghcdi

523 rows_with_deprecated = [] 1aefbghcdi

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

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

526 for row in rows_with_deprecated: 1aefbghcdi

527 commands_table.add_row(*row) 1aefbghcdi

528 if commands_table.row_count: 1aefbghcdi

529 console.print( 1aefbghcdi

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

540 *, 

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

542 ctx: click.Context, 

543 markup_mode: MarkupModeStrict, 

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

554 

555 # Print usage 

556 console.print( 1aefbghcdi

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

562 # Print with some padding 

563 console.print( 1aefbghcdi

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

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

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

578 # Skip if option is hidden 

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

580 continue 1aefbghcdi

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

582 panel_name = ( 1aefbghcdi

583 getattr(param, _RICH_HELP_PANEL_NAME, None) or ARGUMENTS_PANEL_TITLE 

584 ) 

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

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

587 panel_name = ( 1aefbghcdi

588 getattr(param, _RICH_HELP_PANEL_NAME, None) or OPTIONS_PANEL_TITLE 

589 ) 

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

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

592 _print_options_panel( 1aefbghcdi

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

600 if panel_name == ARGUMENTS_PANEL_TITLE: 1aefbghcdi

601 # Already printed above 

602 continue 1aefbghcdi

603 _print_options_panel( 1aefbghcdi

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

611 _print_options_panel( 1aefbghcdi

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

619 if panel_name == OPTIONS_PANEL_TITLE: 1aefbghcdi

620 # Already printed above 

621 continue 1aefbghcdi

622 _print_options_panel( 1aefbghcdi

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

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

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

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

634 if command and not command.hidden: 1aefbghcdi

635 panel_name = ( 1aefbghcdi

636 getattr(command, _RICH_HELP_PANEL_NAME, None) 

637 or COMMANDS_PANEL_TITLE 

638 ) 

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

640 

641 # Identify the longest command name in all panels 

642 max_cmd_len = max( 1aefbghcdi

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

653 _print_commands_panel( 1aefbghcdi

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

661 if panel_name == COMMANDS_PANEL_TITLE: 1aefbghcdi

662 # Already printed above 

663 continue 1aefbghcdi

664 _print_commands_panel( 1aefbghcdi

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

674 # Remove single linebreaks, replace double with single 

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

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

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

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

679 

680 

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

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 # Don't do anything when it's a NoArgsIsHelpError (without importing it, cf. #1278) 

688 if self.__class__.__name__ == "NoArgsIsHelpError": 1aefbghcdi

689 return 1aefghdi

690 

691 console = _get_rich_console(stderr=True) 1aefbghcdi

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

693 if ctx is not None: 1aefbghcdi

694 console.print(ctx.get_usage()) 1aefbghcdi

695 

696 if ctx is not None and ctx.command.get_help_option(ctx) is not None: 1aefbghcdi

697 console.print( 1aefbghcdi

698 RICH_HELP.format( 

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

700 ), 

701 style=STYLE_ERRORS_SUGGESTION, 

702 ) 

703 

704 console.print( 1aefbghcdi

705 Panel( 

706 highlighter(self.format_message()), 

707 border_style=STYLE_ERRORS_PANEL_BORDER, 

708 title=ERRORS_PANEL_TITLE, 

709 title_align=ALIGN_ERRORS_PANEL, 

710 ) 

711 ) 

712 

713 

714def rich_abort_error() -> None: 1aefbghcdi

715 """Print richly formatted abort error.""" 

716 console = _get_rich_console(stderr=True) 1aefbghcdi

717 console.print(ABORTED_TEXT, style=STYLE_ABORTED) 1aefbghcdi

718 

719 

720def escape_before_html_export(input_text: str) -> str: 1aefbghcdi

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

722 return escape(input_text).strip() 1aefbghcdi

723 

724 

725def rich_to_html(input_text: str) -> str: 1aefbghcdi

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

727 

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

729 HTML-formatted text spans into a markdown file. 

730 """ 

731 console = Console(record=True, highlight=False, file=io.StringIO()) 1aefbghcdi

732 

733 console.print(input_text, overflow="ignore", crop=False) 1aefbghcdi

734 

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

736 

737 

738def rich_render_text(text: str) -> str: 1aefbghcdi

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

740 console = _get_rich_console() 1aefbghcdi

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

742 

743 

744def get_traceback( 1aefbghcdi

745 exc: BaseException, 

746 exception_config: DeveloperExceptionConfig, 

747 internal_dir_names: List[str], 

748) -> Traceback: 

749 rich_tb = Traceback.from_exception( 1aefbghcdi

750 type(exc), 

751 exc, 

752 exc.__traceback__, 

753 show_locals=exception_config.pretty_exceptions_show_locals, 

754 suppress=internal_dir_names, 

755 width=MAX_WIDTH, 

756 ) 

757 return rich_tb 1aefbghcdi