Coverage for typer / rich_utils.py: 100%

315 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-11-09 00:15 +0000

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

2 

3import inspect 1acdebfg

4import io 1acdebfg

5from collections import defaultdict 1acdebfg

6from collections.abc import Iterable 1acdebfg

7from gettext import gettext as _ 1acdebfg

8from os import getenv 1acdebfg

9from typing import Any, Literal 1acdebfg

10 

11from rich import box 1acdebfg

12from rich.align import Align 1acdebfg

13from rich.columns import Columns 1acdebfg

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

15from rich.emoji import Emoji 1acdebfg

16from rich.highlighter import RegexHighlighter 1acdebfg

17from rich.markdown import Markdown 1acdebfg

18from rich.markup import escape 1acdebfg

19from rich.padding import Padding 1acdebfg

20from rich.panel import Panel 1acdebfg

21from rich.table import Table 1acdebfg

22from rich.text import Text 1acdebfg

23from rich.theme import Theme 1acdebfg

24from rich.traceback import Traceback 1acdebfg

25from typer.models import DeveloperExceptionConfig 1acdebfg

26 

27from . import _click 1acdebfg

28from ._click import types 1acdebfg

29from .core import TyperArgument, TyperGroup, TyperOption 1acdebfg

30 

31# Default styles 

32STYLE_OPTION = "bold cyan" 1acdebfg

33STYLE_SWITCH = "bold green" 1acdebfg

34STYLE_NEGATIVE_OPTION = "bold magenta" 1acdebfg

35STYLE_NEGATIVE_SWITCH = "bold red" 1acdebfg

36STYLE_TYPES = "bold yellow" 1acdebfg

37STYLE_TYPES_SEPARATOR = "dim" 1acdebfg

38STYLE_USAGE = "yellow" 1acdebfg

39STYLE_USAGE_COMMAND = "bold" 1acdebfg

40STYLE_DEPRECATED = "red" 1acdebfg

41STYLE_DEPRECATED_COMMAND = "dim" 1acdebfg

42STYLE_HELPTEXT_FIRST_LINE = "" 1acdebfg

43STYLE_HELPTEXT = "dim" 1acdebfg

44STYLE_OPTION_HELP = "" 1acdebfg

45STYLE_OPTION_DEFAULT = "dim" 1acdebfg

46STYLE_OPTION_ENVVAR = "dim yellow" 1acdebfg

47STYLE_REQUIRED_SHORT = "red" 1acdebfg

48STYLE_REQUIRED_LONG = "dim red" 1acdebfg

49STYLE_OPTIONS_PANEL_BORDER = "dim" 1acdebfg

50ALIGN_OPTIONS_PANEL: Literal["left", "center", "right"] = "left" 1acdebfg

51STYLE_OPTIONS_TABLE_SHOW_LINES = False 1acdebfg

52STYLE_OPTIONS_TABLE_LEADING = 0 1acdebfg

53STYLE_OPTIONS_TABLE_PAD_EDGE = False 1acdebfg

54STYLE_OPTIONS_TABLE_PADDING = (0, 1) 1acdebfg

55STYLE_OPTIONS_TABLE_BOX = "" 1acdebfg

56STYLE_OPTIONS_TABLE_ROW_STYLES = None 1acdebfg

57STYLE_OPTIONS_TABLE_BORDER_STYLE = None 1acdebfg

58STYLE_COMMANDS_PANEL_BORDER = "dim" 1acdebfg

59ALIGN_COMMANDS_PANEL: Literal["left", "center", "right"] = "left" 1acdebfg

60STYLE_COMMANDS_TABLE_SHOW_LINES = False 1acdebfg

61STYLE_COMMANDS_TABLE_LEADING = 0 1acdebfg

62STYLE_COMMANDS_TABLE_PAD_EDGE = False 1acdebfg

63STYLE_COMMANDS_TABLE_PADDING = (0, 1) 1acdebfg

64STYLE_COMMANDS_TABLE_BOX = "" 1acdebfg

65STYLE_COMMANDS_TABLE_ROW_STYLES = None 1acdebfg

66STYLE_COMMANDS_TABLE_BORDER_STYLE = None 1acdebfg

67STYLE_COMMANDS_TABLE_FIRST_COLUMN = "bold cyan" 1acdebfg

68STYLE_ERRORS_PANEL_BORDER = "red" 1acdebfg

69ALIGN_ERRORS_PANEL: Literal["left", "center", "right"] = "left" 1acdebfg

70STYLE_ERRORS_SUGGESTION = "dim" 1acdebfg

71STYLE_ABORTED = "red" 1acdebfg

72_TERMINAL_WIDTH = getenv("TERMINAL_WIDTH") 1acdebfg

73MAX_WIDTH = int(_TERMINAL_WIDTH) if _TERMINAL_WIDTH else None 1acdebfg

74COLOR_SYSTEM: Literal["auto", "standard", "256", "truecolor", "windows"] | None = ( 1acdebfg

75 "auto" # Set to None to disable colors 

76) 

77_TYPER_FORCE_DISABLE_TERMINAL = getenv("_TYPER_FORCE_DISABLE_TERMINAL") 1acdebfg

78FORCE_TERMINAL = ( 1acdebfg

79 True 

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

81 else None 

82) 

83if _TYPER_FORCE_DISABLE_TERMINAL: 1acdebfg

84 FORCE_TERMINAL = False 1acdebfg

85 

86# Fixed strings 

87DEPRECATED_STRING = _("(deprecated) ") 1acdebfg

88DEFAULT_STRING = _("[default: {}]") 1acdebfg

89ENVVAR_STRING = _("[env var: {}]") 1acdebfg

90REQUIRED_SHORT_STRING = "*" 1acdebfg

91REQUIRED_LONG_STRING = _("[required]") 1acdebfg

92RANGE_STRING = " [{}]" 1acdebfg

93ARGUMENTS_PANEL_TITLE = _("Arguments") 1acdebfg

94OPTIONS_PANEL_TITLE = _("Options") 1acdebfg

95COMMANDS_PANEL_TITLE = _("Commands") 1acdebfg

96ERRORS_PANEL_TITLE = _("Error") 1acdebfg

97ABORTED_TEXT = _("Aborted.") 1acdebfg

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

99 

100MARKUP_MODE_MARKDOWN = "markdown" 1acdebfg

101MARKUP_MODE_RICH = "rich" 1acdebfg

102_RICH_HELP_PANEL_NAME = "rich_help_panel" 1acdebfg

103ANSI_PREFIX = "\033[" 1acdebfg

104 

105MarkupModeStrict = Literal["markdown", "rich"] 1acdebfg

106 

107 

108# Rich regex highlighter 

109class OptionHighlighter(RegexHighlighter): 1acdebfg

110 """Highlights our special options.""" 

111 

112 highlights = [ 1acdebfg

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

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

115 r"(?P<types>\<[^\>]+\>)", 

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

117 ] 

118 

119 

120class NegativeOptionHighlighter(RegexHighlighter): 1acdebfg

121 highlights = [ 1acdebfg

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

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

124 ] 

125 

126 

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

128class TypesHighlighter(RegexHighlighter): 1acdebfg

129 highlights = [ 1acdebfg

130 r"^(?P<types_sep>(\[|<))", 

131 r"(?P<types_sep>\|)", 

132 r"(?P<types_sep>(\]|>))(\.\.\.)?$", 

133 ] 

134 

135 

136highlighter = OptionHighlighter() 1acdebfg

137negative_highlighter = NegativeOptionHighlighter() 1acdebfg

138types_highlighter = TypesHighlighter() 1acdebfg

139 

140 

141def _has_ansi_character(text: str) -> bool: 1acdebfg

142 return ANSI_PREFIX in text 1acdebfg

143 

144 

145def _get_rich_console(stderr: bool = False) -> Console: 1acdebfg

146 return Console( 1acdebfg

147 theme=Theme( 

148 { 

149 "option": STYLE_OPTION, 

150 "switch": STYLE_SWITCH, 

151 "negative_option": STYLE_NEGATIVE_OPTION, 

152 "negative_switch": STYLE_NEGATIVE_SWITCH, 

153 "types": STYLE_TYPES, 

154 "types_sep": STYLE_TYPES_SEPARATOR, 

155 "usage": STYLE_USAGE, 

156 }, 

157 ), 

158 highlighter=highlighter, 

159 color_system=COLOR_SYSTEM, 

160 force_terminal=FORCE_TERMINAL, 

161 width=MAX_WIDTH, 

162 stderr=stderr, 

163 ) 

164 

165 

166def _make_rich_text( 1acdebfg

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

168) -> Markdown | Text: 

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

170 

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

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

173 """ 

174 # Remove indentations from input text 

175 text = inspect.cleandoc(text) 1acdebfg

176 if markup_mode == MARKUP_MODE_MARKDOWN: 1acdebfg

177 text = Emoji.replace(text) 1acdebfg

178 return Markdown(text, style=style) 1acdebfg

179 else: 

180 assert markup_mode == MARKUP_MODE_RICH 1acdebfg

181 if _has_ansi_character(text): 1acdebfg

182 return highlighter(Text.from_ansi(text, style=style)) 1acdebfg

183 else: 

184 return highlighter(Text.from_markup(text, style=style)) 1acdebfg

185 

186 

187@group() 1acdebfg

188def _get_help_text( 1acdebfg

189 *, 

190 obj: _click.Command | TyperGroup, 

191 markup_mode: MarkupModeStrict, 

192) -> Iterable[Markdown | Text]: 

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

194 

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

196 Rich Text object or as Markdown. 

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

198 """ 

199 # Prepend deprecated status 

200 if obj.deprecated: 1acdebfg

201 yield Text(DEPRECATED_STRING, style=STYLE_DEPRECATED) 1acdebfg

202 

203 # Fetch and dedent the help text 

204 help_text = inspect.cleandoc(obj.help or "") 1acdebfg

205 

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

207 help_text = help_text.partition("\f")[0] 1acdebfg

208 

209 # Get the first paragraph 

210 first_line, *remaining_paragraphs = help_text.split("\n\n") 1acdebfg

211 

212 # Remove single linebreaks 

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

214 first_line = first_line.replace("\n", " ") 1acdebfg

215 yield _make_rich_text( 1acdebfg

216 text=first_line.strip(), 

217 style=STYLE_HELPTEXT_FIRST_LINE, 

218 markup_mode=markup_mode, 

219 ) 

220 

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

222 if remaining_paragraphs: 1acdebfg

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

224 yield Text("") 1acdebfg

225 remaining_lines = _fix_linebreaks(remaining_paragraphs, markup_mode) 1acdebfg

226 

227 yield _make_rich_text( 1acdebfg

228 text=remaining_lines, 

229 style=STYLE_HELPTEXT, 

230 markup_mode=markup_mode, 

231 ) 

232 

233 

234def _fix_linebreaks(paragraphs: list[str], markup_mode: MarkupModeStrict) -> str: 1acdebfg

235 # Join with double linebreaks for markdown and Rich markup 

236 return "\n\n".join(paragraphs) 1acdebfg

237 

238 

239def _get_parameter_help( 1acdebfg

240 *, 

241 param: TyperOption | TyperArgument | _click.Parameter, 

242 ctx: _click.Context, 

243 markup_mode: MarkupModeStrict, 

244) -> Columns: 

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

246 

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

248 as a Rich Text object or as Markdown. 

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

250 applicable. 

251 """ 

252 # import here to avoid cyclic imports 

253 from .core import TyperArgument, TyperOption 1acdebfg

254 

255 items: list[Text | Markdown] = [] 1acdebfg

256 

257 # Get the environment variable first 

258 

259 envvar = getattr(param, "envvar", None) 1acdebfg

260 var_str = "" 1acdebfg

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

262 if envvar is None: 1acdebfg

263 if ( 1ab

264 getattr(param, "allow_from_autoenv", None) 

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

266 and param.name is not None 

267 ): 

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

269 if envvar is not None: 1acdebfg

270 var_str = ( 1acdebfg

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

272 ) 

273 

274 # Main help text 

275 help_value: str | None = getattr(param, "help", None) 1acdebfg

276 if help_value: 1acdebfg

277 paragraphs = help_value.split("\n\n") 1acdebfg

278 # Remove single linebreaks 

279 if markup_mode != MARKUP_MODE_MARKDOWN: 1acdebfg

280 paragraphs = [ 1acdebfg

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

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

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

284 for x in paragraphs 

285 ] 

286 items.append( 1acdebfg

287 _make_rich_text( 

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

289 style=STYLE_OPTION_HELP, 

290 markup_mode=markup_mode, 

291 ) 

292 ) 

293 

294 # Environment variable AFTER help text 

295 if envvar and getattr(param, "show_envvar", None): 1acdebfg

296 items.append(Text(ENVVAR_STRING.format(var_str), style=STYLE_OPTION_ENVVAR)) 1acdebfg

297 

298 # Default value 

299 # This uses Typer's specific param._get_default_string 

300 if isinstance(param, (TyperOption, TyperArgument)): 1acdebfg

301 default_value = param._extract_default_help_str(ctx=ctx) 1acdebfg

302 show_default_is_str = isinstance(param.show_default, str) 1acdebfg

303 if show_default_is_str or ( 1acdebfg

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

305 ): 

306 default_str = param._get_default_string( 1acdebfg

307 ctx=ctx, 

308 show_default_is_str=show_default_is_str, 

309 default_value=default_value, 

310 ) 

311 if default_str: 1acdebfg

312 items.append( 1acdebfg

313 Text( 

314 DEFAULT_STRING.format(default_str), 

315 style=STYLE_OPTION_DEFAULT, 

316 ) 

317 ) 

318 

319 # Required? 

320 if param.required: 1acdebfg

321 items.append(Text(REQUIRED_LONG_STRING, style=STYLE_REQUIRED_LONG)) 1acdebfg

322 

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

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

325 return Columns(items) 1acdebfg

326 

327 

328def _make_command_help( 1acdebfg

329 *, 

330 help_text: str, 

331 markup_mode: MarkupModeStrict, 

332) -> Text | Markdown: 

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

334 

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

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

337 

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

339 Rich Text object or as Markdown. 

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

341 """ 

342 paragraphs = inspect.cleandoc(help_text).split("\n\n") 1acdebfg

343 # Remove single linebreaks 

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

345 paragraphs[0] = paragraphs[0].replace("\n", " ") 1acdebfg

346 elif paragraphs[0].startswith("\b"): 1acdebfg

347 paragraphs[0] = paragraphs[0].replace("\b\n", "") 1acdebfg

348 return _make_rich_text( 1acdebfg

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

350 style=STYLE_OPTION_HELP, 

351 markup_mode=markup_mode, 

352 ) 

353 

354 

355def _print_options_panel( 1acdebfg

356 *, 

357 name: str, 

358 params: list[TyperOption] | list[TyperArgument], 

359 ctx: _click.Context, 

360 markup_mode: MarkupModeStrict, 

361 console: Console, 

362) -> None: 

363 options_rows: list[list[RenderableType]] = [] 1acdebfg

364 required_rows: list[str | Text] = [] 1acdebfg

365 for param in params: 1acdebfg

366 # Short and long form 

367 opt_long_strs = [] 1acdebfg

368 opt_short_strs = [] 1acdebfg

369 secondary_opt_long_strs = [] 1acdebfg

370 secondary_opt_short_strs = [] 1acdebfg

371 

372 # check whether argument has a metavar name or type set 

373 metavar_name = None 1acdebfg

374 metavar_type = None 1acdebfg

375 metavar_str = param.make_metavar(ctx=ctx) 1acdebfg

376 if isinstance(param, TyperArgument): 1acdebfg

377 if param.metavar is not None: 1acdebfg

378 metavar_name = param.metavar 1acdebfg

379 else: 

380 metavar_name = param.name or "" 1acdebfg

381 if isinstance(param, TyperOption): 1acdebfg

382 metavar_type = metavar_str 1acdebfg

383 elif isinstance(param, TyperArgument): 1acdebfg

384 metavar_type = param.type.get_metavar(param=param, ctx=ctx) 1acdebfg

385 if metavar_type is None: 1acdebfg

386 metavar_type = f"<{param.type.name}>" 1acdebfg

387 

388 for opt_str in param.opts: 1acdebfg

389 if "--" in opt_str: 1acdebfg

390 opt_long_strs.append(opt_str) 1acdebfg

391 elif metavar_name: 1acdebfg

392 opt_short_strs.append(metavar_name) 1acdebfg

393 else: 

394 opt_short_strs.append(opt_str) 1acdebfg

395 for opt_str in param.secondary_opts: 1acdebfg

396 if "--" in opt_str: 1acdebfg

397 secondary_opt_long_strs.append(opt_str) 1acdebfg

398 elif metavar_name: # pragma: no cover 1acdebfg

399 secondary_opt_short_strs.append(metavar_name) 

400 else: 

401 secondary_opt_short_strs.append(opt_str) 1acdebfg

402 

403 # Column for recording the type 

404 types_data = Text(style=STYLE_TYPES, overflow="fold") 1acdebfg

405 

406 # Fetch type 

407 if metavar_type and "bool" not in metavar_type.lower(): 1acdebfg

408 types_data.append(metavar_type) 1acdebfg

409 

410 # Range - from 

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

412 # skip count with default range type 

413 if ( 1ab

414 isinstance(param.type, types._NumberRangeBase) 

415 and isinstance(param, TyperOption) 

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

417 ): 

418 range_str = param.type._describe_range() 1acdebfg

419 if range_str: 1acdebfg

420 types_data.append(RANGE_STRING.format(range_str)) 1acdebfg

421 

422 # Required asterisk 

423 required: str | Text = "" 1acdebfg

424 if param.required: 1acdebfg

425 required = Text(REQUIRED_SHORT_STRING, style=STYLE_REQUIRED_SHORT) 1acdebfg

426 

427 required_rows.append(required) 1acdebfg

428 options_rows.append( 1acdebfg

429 [ 

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

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

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

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

434 types_highlighter(types_data), 

435 _get_parameter_help( 

436 param=param, 

437 ctx=ctx, 

438 markup_mode=markup_mode, 

439 ), 

440 ] 

441 ) 

442 rows_with_required: list[list[RenderableType]] = [] 1acdebfg

443 if any(required_rows): 1acdebfg

444 for required, row in zip(required_rows, options_rows, strict=True): 1acdebfg

445 rows_with_required.append([required, *row]) 1acdebfg

446 else: 

447 rows_with_required = options_rows 1acdebfg

448 if options_rows: 1acdebfg

449 t_styles: dict[str, Any] = { 1acdebfg

450 "show_lines": STYLE_OPTIONS_TABLE_SHOW_LINES, 

451 "leading": STYLE_OPTIONS_TABLE_LEADING, 

452 "box": STYLE_OPTIONS_TABLE_BOX, 

453 "border_style": STYLE_OPTIONS_TABLE_BORDER_STYLE, 

454 "row_styles": STYLE_OPTIONS_TABLE_ROW_STYLES, 

455 "pad_edge": STYLE_OPTIONS_TABLE_PAD_EDGE, 

456 "padding": STYLE_OPTIONS_TABLE_PADDING, 

457 } 

458 box_style = getattr(box, t_styles.pop("box"), None) 1acdebfg

459 

460 options_table = Table( 1acdebfg

461 highlight=True, 

462 show_header=False, 

463 expand=True, 

464 box=box_style, 

465 **t_styles, 

466 ) 

467 for row in rows_with_required: 1acdebfg

468 options_table.add_row(*row) 1acdebfg

469 console.print( 1acdebfg

470 Panel( 

471 options_table, 

472 border_style=STYLE_OPTIONS_PANEL_BORDER, 

473 title=name, 

474 title_align=ALIGN_OPTIONS_PANEL, 

475 ) 

476 ) 

477 

478 

479def _print_commands_panel( 1acdebfg

480 *, 

481 name: str, 

482 commands: list[_click.Command], 

483 markup_mode: MarkupModeStrict, 

484 console: Console, 

485 cmd_len: int, 

486) -> None: 

487 t_styles: dict[str, Any] = { 1acdebfg

488 "show_lines": STYLE_COMMANDS_TABLE_SHOW_LINES, 

489 "leading": STYLE_COMMANDS_TABLE_LEADING, 

490 "box": STYLE_COMMANDS_TABLE_BOX, 

491 "border_style": STYLE_COMMANDS_TABLE_BORDER_STYLE, 

492 "row_styles": STYLE_COMMANDS_TABLE_ROW_STYLES, 

493 "pad_edge": STYLE_COMMANDS_TABLE_PAD_EDGE, 

494 "padding": STYLE_COMMANDS_TABLE_PADDING, 

495 } 

496 box_style = getattr(box, t_styles.pop("box"), None) 1acdebfg

497 

498 commands_table = Table( 1acdebfg

499 highlight=False, 

500 show_header=False, 

501 expand=True, 

502 box=box_style, 

503 **t_styles, 

504 ) 

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

506 # regex 

507 commands_table.add_column( 1acdebfg

508 style=STYLE_COMMANDS_TABLE_FIRST_COLUMN, 

509 no_wrap=True, 

510 width=cmd_len, 

511 ) 

512 

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

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

515 # other panels. 

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

517 rows: list[list[RenderableType | None]] = [] 1acdebfg

518 deprecated_rows: list[RenderableType | None] = [] 1acdebfg

519 for command in commands: 1acdebfg

520 helptext = command.short_help or command.help or "" 1acdebfg

521 command_name = command.name or "" 1acdebfg

522 if command.deprecated: 1acdebfg

523 command_name_text = Text(f"{command_name}", style=STYLE_DEPRECATED_COMMAND) 1acdebfg

524 deprecated_rows.append(Text(DEPRECATED_STRING, style=STYLE_DEPRECATED)) 1acdebfg

525 else: 

526 command_name_text = Text(command_name) 1acdebfg

527 deprecated_rows.append(None) 1acdebfg

528 rows.append( 1acdebfg

529 [ 

530 command_name_text, 

531 _make_command_help( 

532 help_text=helptext, 

533 markup_mode=markup_mode, 

534 ), 

535 ] 

536 ) 

537 rows_with_deprecated = rows 1acdebfg

538 if any(deprecated_rows): 1acdebfg

539 rows_with_deprecated = [] 1acdebfg

540 for row, deprecated_text in zip(rows, deprecated_rows, strict=True): 1acdebfg

541 rows_with_deprecated.append([*row, deprecated_text]) 1acdebfg

542 for row in rows_with_deprecated: 1acdebfg

543 commands_table.add_row(*row) 1acdebfg

544 if commands_table.row_count: 1acdebfg

545 console.print( 1acdebfg

546 Panel( 

547 commands_table, 

548 border_style=STYLE_COMMANDS_PANEL_BORDER, 

549 title=name, 

550 title_align=ALIGN_COMMANDS_PANEL, 

551 ) 

552 ) 

553 

554 

555def rich_format_help( 1acdebfg

556 *, 

557 obj: _click.Command | TyperGroup, 

558 ctx: _click.Context, 

559 markup_mode: MarkupModeStrict, 

560) -> None: 

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

562 

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

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

565 

566 Replacement for the click function format_help(). 

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

568 """ 

569 console = _get_rich_console() 1acdebfg

570 

571 # Print usage 

572 console.print( 1acdebfg

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

574 ) 

575 

576 # Print command / group help if we have some 

577 if obj.help: 1acdebfg

578 # Print with some padding 

579 console.print( 1acdebfg

580 Padding( 

581 Align( 

582 _get_help_text( 

583 obj=obj, 

584 markup_mode=markup_mode, 

585 ), 

586 pad=False, 

587 ), 

588 (0, 1, 1, 1), 

589 ) 

590 ) 

591 panel_to_arguments: defaultdict[str, list[TyperArgument]] = defaultdict(list) 1acdebfg

592 panel_to_options: defaultdict[str, list[TyperOption]] = defaultdict(list) 1acdebfg

593 for param in obj.get_params(ctx): 1acdebfg

594 # Skip if option is hidden 

595 if getattr(param, "hidden", False): 1acdebfg

596 continue 1acdebfg

597 if isinstance(param, TyperArgument): 1acdebfg

598 panel_name = ( 1acdebfg

599 getattr(param, _RICH_HELP_PANEL_NAME, None) or ARGUMENTS_PANEL_TITLE 

600 ) 

601 panel_to_arguments[panel_name].append(param) 1acdebfg

602 elif isinstance(param, TyperOption): 1acdebfg

603 panel_name = ( 1acdebfg

604 getattr(param, _RICH_HELP_PANEL_NAME, None) or OPTIONS_PANEL_TITLE 

605 ) 

606 panel_to_options[panel_name].append(param) 1acdebfg

607 default_arguments = panel_to_arguments.get(ARGUMENTS_PANEL_TITLE, []) 1acdebfg

608 _print_options_panel( 1acdebfg

609 name=ARGUMENTS_PANEL_TITLE, 

610 params=default_arguments, 

611 ctx=ctx, 

612 markup_mode=markup_mode, 

613 console=console, 

614 ) 

615 for panel_name, arguments in panel_to_arguments.items(): 1acdebfg

616 if panel_name == ARGUMENTS_PANEL_TITLE: 1acdebfg

617 # Already printed above 

618 continue 1acdebfg

619 _print_options_panel( 1acdebfg

620 name=panel_name, 

621 params=arguments, 

622 ctx=ctx, 

623 markup_mode=markup_mode, 

624 console=console, 

625 ) 

626 default_options = panel_to_options.get(OPTIONS_PANEL_TITLE, []) 1acdebfg

627 _print_options_panel( 1acdebfg

628 name=OPTIONS_PANEL_TITLE, 

629 params=default_options, 

630 ctx=ctx, 

631 markup_mode=markup_mode, 

632 console=console, 

633 ) 

634 for panel_name, options in panel_to_options.items(): 1acdebfg

635 if panel_name == OPTIONS_PANEL_TITLE: 1acdebfg

636 # Already printed above 

637 continue 1acdebfg

638 _print_options_panel( 1acdebfg

639 name=panel_name, 

640 params=options, 

641 ctx=ctx, 

642 markup_mode=markup_mode, 

643 console=console, 

644 ) 

645 

646 if isinstance(obj, TyperGroup): 1acdebfg

647 panel_to_commands: defaultdict[str, list[_click.Command]] = defaultdict(list) 1acdebfg

648 for command_name in obj.list_commands(ctx): 1acdebfg

649 command = obj.get_command(ctx, command_name) 1acdebfg

650 if command and not command.hidden: 1acdebfg

651 panel_name = ( 1acdebfg

652 getattr(command, _RICH_HELP_PANEL_NAME, None) 

653 or COMMANDS_PANEL_TITLE 

654 ) 

655 panel_to_commands[panel_name].append(command) 1acdebfg

656 

657 # Identify the longest command name in all panels 

658 max_cmd_len = max( 1acdebfg

659 [ 

660 len(command.name or "") 

661 for commands in panel_to_commands.values() 

662 for command in commands 

663 ], 

664 default=0, 

665 ) 

666 

667 # Print each command group panel 

668 default_commands = panel_to_commands.get(COMMANDS_PANEL_TITLE, []) 1acdebfg

669 _print_commands_panel( 1acdebfg

670 name=COMMANDS_PANEL_TITLE, 

671 commands=default_commands, 

672 markup_mode=markup_mode, 

673 console=console, 

674 cmd_len=max_cmd_len, 

675 ) 

676 for panel_name, commands in panel_to_commands.items(): 1acdebfg

677 if panel_name == COMMANDS_PANEL_TITLE: 1acdebfg

678 # Already printed above 

679 continue 1acdebfg

680 _print_commands_panel( 1acdebfg

681 name=panel_name, 

682 commands=commands, 

683 markup_mode=markup_mode, 

684 console=console, 

685 cmd_len=max_cmd_len, 

686 ) 

687 

688 # Epilogue if we have it 

689 if obj.epilog: 1acdebfg

690 # Remove single linebreaks, replace double with single 

691 lines = obj.epilog.split("\n\n") 1acdebfg

692 epilogue = _fix_linebreaks(lines, markup_mode) 1acdebfg

693 epilogue_text = _make_rich_text(text=epilogue, markup_mode=markup_mode) 1acdebfg

694 console.print(Padding(Align(epilogue_text, pad=False), 1)) 1acdebfg

695 

696 

697def rich_format_error(self: _click.ClickException) -> None: 1acdebfg

698 """Print richly formatted click errors. 

699 

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

701 Mimics original _click.ClickException.echo() function but with rich formatting. 

702 """ 

703 # Don't do anything when it's a NoArgsIsHelpError (without importing it, cf. #1278) 

704 if self.__class__.__name__ == "NoArgsIsHelpError": 1acdebfg

705 return 1acdebfg

706 

707 console = _get_rich_console(stderr=True) 1acdebfg

708 ctx: _click.Context | None = getattr(self, "ctx", None) 1acdebfg

709 if ctx is not None: 1acdebfg

710 console.print(highlighter(ctx.get_usage()), style=STYLE_USAGE_COMMAND) 1acdebfg

711 

712 if ctx is not None and ctx.command.get_help_option(ctx) is not None: 1acdebfg

713 console.print( 1acdebfg

714 RICH_HELP.format( 

715 command_path=escape(ctx.command_path), 

716 help_option=ctx.help_option_names[0], 

717 ), 

718 style=STYLE_ERRORS_SUGGESTION, 

719 ) 

720 

721 console.print( 1acdebfg

722 Panel( 

723 highlighter(self.format_message()), 

724 border_style=STYLE_ERRORS_PANEL_BORDER, 

725 title=ERRORS_PANEL_TITLE, 

726 title_align=ALIGN_ERRORS_PANEL, 

727 ) 

728 ) 

729 

730 

731def rich_abort_error() -> None: 1acdebfg

732 """Print richly formatted abort error.""" 

733 console = _get_rich_console(stderr=True) 1acdebfg

734 console.print(ABORTED_TEXT, style=STYLE_ABORTED) 1acdebfg

735 

736 

737def escape_before_html_export(input_text: str) -> str: 1acdebfg

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

739 return escape(input_text).strip() 1acdebfg

740 

741 

742def rich_to_html(input_text: str) -> str: 1acdebfg

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

744 

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

746 HTML-formatted text spans into a markdown file. 

747 """ 

748 console = Console(record=True, highlight=False, file=io.StringIO()) 1acdebfg

749 

750 console.print(input_text, overflow="ignore", crop=False) 1acdebfg

751 

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

753 

754 

755def rich_render_text(text: str) -> str: 1acdebfg

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

757 console = _get_rich_console() 1acdebfg

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

759 

760 

761def get_traceback( 1acdebfg

762 exc: BaseException, 

763 exception_config: DeveloperExceptionConfig, 

764 internal_dir_names: list[str], 

765) -> Traceback: 

766 rich_tb = Traceback.from_exception( 1acdebfg

767 type(exc), 

768 exc, 

769 exc.__traceback__, 

770 show_locals=exception_config.pretty_exceptions_show_locals, 

771 suppress=internal_dir_names, 

772 width=MAX_WIDTH, 

773 code_width=None, 

774 word_wrap=True, 

775 ) 

776 return rich_tb 1acdebfg