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
« 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
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
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
27from . import _click 1acdebfg
28from ._click import types 1acdebfg
29from .core import TyperArgument, TyperGroup, TyperOption 1acdebfg
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
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
100MARKUP_MODE_MARKDOWN = "markdown" 1acdebfg
101MARKUP_MODE_RICH = "rich" 1acdebfg
102_RICH_HELP_PANEL_NAME = "rich_help_panel" 1acdebfg
103ANSI_PREFIX = "\033[" 1acdebfg
105MarkupModeStrict = Literal["markdown", "rich"] 1acdebfg
108# Rich regex highlighter
109class OptionHighlighter(RegexHighlighter): 1acdebfg
110 """Highlights our special options."""
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 ]
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 ]
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 ]
136highlighter = OptionHighlighter() 1acdebfg
137negative_highlighter = NegativeOptionHighlighter() 1acdebfg
138types_highlighter = TypesHighlighter() 1acdebfg
141def _has_ansi_character(text: str) -> bool: 1acdebfg
142 return ANSI_PREFIX in text 1acdebfg
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 )
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.
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
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.
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
203 # Fetch and dedent the help text
204 help_text = inspect.cleandoc(obj.help or "") 1acdebfg
206 # Trim off anything that comes after \f on its own line
207 help_text = help_text.partition("\f")[0] 1acdebfg
209 # Get the first paragraph
210 first_line, *remaining_paragraphs = help_text.split("\n\n") 1acdebfg
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 )
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
227 yield _make_rich_text( 1acdebfg
228 text=remaining_lines,
229 style=STYLE_HELPTEXT,
230 markup_mode=markup_mode,
231 )
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
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.
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
255 items: list[Text | Markdown] = [] 1acdebfg
257 # Get the environment variable first
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 )
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 )
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
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 )
319 # Required?
320 if param.required: 1acdebfg
321 items.append(Text(REQUIRED_LONG_STRING, style=STYLE_REQUIRED_LONG)) 1acdebfg
323 # Use Columns - this allows us to group different renderable types
324 # (Text, Markdown) onto a single line.
325 return Columns(items) 1acdebfg
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.
335 That is, when calling help on groups with multiple subcommands
336 (not the main help text when calling the subcommand help).
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 )
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
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
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
403 # Column for recording the type
404 types_data = Text(style=STYLE_TYPES, overflow="fold") 1acdebfg
406 # Fetch type
407 if metavar_type and "bool" not in metavar_type.lower(): 1acdebfg
408 types_data.append(metavar_type) 1acdebfg
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
422 # Required asterisk
423 required: str | Text = "" 1acdebfg
424 if param.required: 1acdebfg
425 required = Text(REQUIRED_SHORT_STRING, style=STYLE_REQUIRED_SHORT) 1acdebfg
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
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 )
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
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 )
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 )
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.
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
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
571 # Print usage
572 console.print( 1acdebfg
573 Padding(highlighter(obj.get_usage(ctx)), 1), style=STYLE_USAGE_COMMAND
574 )
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 )
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
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 )
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 )
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
697def rich_format_error(self: _click.ClickException) -> None: 1acdebfg
698 """Print richly formatted click errors.
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
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
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 )
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 )
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
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
742def rich_to_html(input_text: str) -> str: 1acdebfg
743 """Print the HTML version of a rich-formatted input string.
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
750 console.print(input_text, overflow="ignore", crop=False) 1acdebfg
752 return console.export_html(inline_styles=True, code_format="{code}").strip() 1acdebfg
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
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