Coverage for typer / rich_utils.py: 100%
299 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-03-26 21:46 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-03-26 21:46 +0000
1# Extracted and modified from https://github.com/ewels/rich-click
3import inspect 1acdefbg
4import io 1acdefbg
5from collections import defaultdict 1acdefbg
6from collections.abc import Iterable 1acdefbg
7from gettext import gettext as _ 1acdefbg
8from os import getenv 1acdefbg
9from typing import Any, Literal 1acdefbg
11import click 1acdefbg
12from rich import box 1acdefbg
13from rich.align import Align 1acdefbg
14from rich.columns import Columns 1acdefbg
15from rich.console import Console, RenderableType, group 1acdefbg
16from rich.emoji import Emoji 1acdefbg
17from rich.highlighter import RegexHighlighter 1acdefbg
18from rich.markdown import Markdown 1acdefbg
19from rich.markup import escape 1acdefbg
20from rich.padding import Padding 1acdefbg
21from rich.panel import Panel 1acdefbg
22from rich.table import Table 1acdefbg
23from rich.text import Text 1acdefbg
24from rich.theme import Theme 1acdefbg
25from rich.traceback import Traceback 1acdefbg
26from typer.models import DeveloperExceptionConfig 1acdefbg
28# Default styles
29STYLE_OPTION = "bold cyan" 1acdefbg
30STYLE_SWITCH = "bold green" 1acdefbg
31STYLE_NEGATIVE_OPTION = "bold magenta" 1acdefbg
32STYLE_NEGATIVE_SWITCH = "bold red" 1acdefbg
33STYLE_METAVAR = "bold yellow" 1acdefbg
34STYLE_METAVAR_SEPARATOR = "dim" 1acdefbg
35STYLE_USAGE = "yellow" 1acdefbg
36STYLE_USAGE_COMMAND = "bold" 1acdefbg
37STYLE_DEPRECATED = "red" 1acdefbg
38STYLE_DEPRECATED_COMMAND = "dim" 1acdefbg
39STYLE_HELPTEXT_FIRST_LINE = "" 1acdefbg
40STYLE_HELPTEXT = "dim" 1acdefbg
41STYLE_OPTION_HELP = "" 1acdefbg
42STYLE_OPTION_DEFAULT = "dim" 1acdefbg
43STYLE_OPTION_ENVVAR = "dim yellow" 1acdefbg
44STYLE_REQUIRED_SHORT = "red" 1acdefbg
45STYLE_REQUIRED_LONG = "dim red" 1acdefbg
46STYLE_OPTIONS_PANEL_BORDER = "dim" 1acdefbg
47ALIGN_OPTIONS_PANEL: Literal["left", "center", "right"] = "left" 1acdefbg
48STYLE_OPTIONS_TABLE_SHOW_LINES = False 1acdefbg
49STYLE_OPTIONS_TABLE_LEADING = 0 1acdefbg
50STYLE_OPTIONS_TABLE_PAD_EDGE = False 1acdefbg
51STYLE_OPTIONS_TABLE_PADDING = (0, 1) 1acdefbg
52STYLE_OPTIONS_TABLE_BOX = "" 1acdefbg
53STYLE_OPTIONS_TABLE_ROW_STYLES = None 1acdefbg
54STYLE_OPTIONS_TABLE_BORDER_STYLE = None 1acdefbg
55STYLE_COMMANDS_PANEL_BORDER = "dim" 1acdefbg
56ALIGN_COMMANDS_PANEL: Literal["left", "center", "right"] = "left" 1acdefbg
57STYLE_COMMANDS_TABLE_SHOW_LINES = False 1acdefbg
58STYLE_COMMANDS_TABLE_LEADING = 0 1acdefbg
59STYLE_COMMANDS_TABLE_PAD_EDGE = False 1acdefbg
60STYLE_COMMANDS_TABLE_PADDING = (0, 1) 1acdefbg
61STYLE_COMMANDS_TABLE_BOX = "" 1acdefbg
62STYLE_COMMANDS_TABLE_ROW_STYLES = None 1acdefbg
63STYLE_COMMANDS_TABLE_BORDER_STYLE = None 1acdefbg
64STYLE_COMMANDS_TABLE_FIRST_COLUMN = "bold cyan" 1acdefbg
65STYLE_ERRORS_PANEL_BORDER = "red" 1acdefbg
66ALIGN_ERRORS_PANEL: Literal["left", "center", "right"] = "left" 1acdefbg
67STYLE_ERRORS_SUGGESTION = "dim" 1acdefbg
68STYLE_ABORTED = "red" 1acdefbg
69_TERMINAL_WIDTH = getenv("TERMINAL_WIDTH") 1acdefbg
70MAX_WIDTH = int(_TERMINAL_WIDTH) if _TERMINAL_WIDTH else None 1acdefbg
71COLOR_SYSTEM: Literal["auto", "standard", "256", "truecolor", "windows"] | None = ( 1acdefbg
72 "auto" # Set to None to disable colors
73)
74_TYPER_FORCE_DISABLE_TERMINAL = getenv("_TYPER_FORCE_DISABLE_TERMINAL") 1acdefbg
75FORCE_TERMINAL = ( 1acdefbg
76 True
77 if getenv("GITHUB_ACTIONS") or getenv("FORCE_COLOR") or getenv("PY_COLORS")
78 else None
79)
80if _TYPER_FORCE_DISABLE_TERMINAL: 1acdefbg
81 FORCE_TERMINAL = False 1acdefbg
83# Fixed strings
84DEPRECATED_STRING = _("(deprecated) ") 1acdefbg
85DEFAULT_STRING = _("[default: {}]") 1acdefbg
86ENVVAR_STRING = _("[env var: {}]") 1acdefbg
87REQUIRED_SHORT_STRING = "*" 1acdefbg
88REQUIRED_LONG_STRING = _("[required]") 1acdefbg
89RANGE_STRING = " [{}]" 1acdefbg
90ARGUMENTS_PANEL_TITLE = _("Arguments") 1acdefbg
91OPTIONS_PANEL_TITLE = _("Options") 1acdefbg
92COMMANDS_PANEL_TITLE = _("Commands") 1acdefbg
93ERRORS_PANEL_TITLE = _("Error") 1acdefbg
94ABORTED_TEXT = _("Aborted.") 1acdefbg
95RICH_HELP = _("Try [blue]'{command_path} {help_option}'[/] for help.") 1acdefbg
97MARKUP_MODE_MARKDOWN = "markdown" 1acdefbg
98MARKUP_MODE_RICH = "rich" 1acdefbg
99_RICH_HELP_PANEL_NAME = "rich_help_panel" 1acdefbg
100ANSI_PREFIX = "\033[" 1acdefbg
102MarkupModeStrict = Literal["markdown", "rich"] 1acdefbg
105# Rich regex highlighter
106class OptionHighlighter(RegexHighlighter): 1acdefbg
107 """Highlights our special options."""
109 highlights = [ 1acdefbg
110 r"(^|\W)(?P<switch>\-\w+)(?![a-zA-Z0-9])",
111 r"(^|\W)(?P<option>\-\-[\w\-]+)(?![a-zA-Z0-9])",
112 r"(?P<metavar>\<[^\>]+\>)",
113 r"(?P<usage>Usage: )",
114 ]
117class NegativeOptionHighlighter(RegexHighlighter): 1acdefbg
118 highlights = [ 1acdefbg
119 r"(^|\W)(?P<negative_switch>\-\w+)(?![a-zA-Z0-9])",
120 r"(^|\W)(?P<negative_option>\-\-[\w\-]+)(?![a-zA-Z0-9])",
121 ]
124# Highlighter to make [ | ] and <> dim
125class MetavarHighlighter(RegexHighlighter): 1acdefbg
126 highlights = [ 1acdefbg
127 r"^(?P<metavar_sep>(\[|<))",
128 r"(?P<metavar_sep>\|)",
129 r"(?P<metavar_sep>(\]|>))(\.\.\.)?$",
130 ]
133highlighter = OptionHighlighter() 1acdefbg
134negative_highlighter = NegativeOptionHighlighter() 1acdefbg
135metavar_highlighter = MetavarHighlighter() 1acdefbg
138def _has_ansi_character(text: str) -> bool: 1acdefbg
139 return ANSI_PREFIX in text 1acdefbg
142def _get_rich_console(stderr: bool = False) -> Console: 1acdefbg
143 return Console( 1acdefbg
144 theme=Theme(
145 {
146 "option": STYLE_OPTION,
147 "switch": STYLE_SWITCH,
148 "negative_option": STYLE_NEGATIVE_OPTION,
149 "negative_switch": STYLE_NEGATIVE_SWITCH,
150 "metavar": STYLE_METAVAR,
151 "metavar_sep": STYLE_METAVAR_SEPARATOR,
152 "usage": STYLE_USAGE,
153 },
154 ),
155 highlighter=highlighter,
156 color_system=COLOR_SYSTEM,
157 force_terminal=FORCE_TERMINAL,
158 width=MAX_WIDTH,
159 stderr=stderr,
160 )
163def _make_rich_text( 1acdefbg
164 *, text: str, style: str = "", markup_mode: MarkupModeStrict
165) -> Markdown | Text:
166 """Take a string, remove indentations, and return styled text.
168 If `markup_mode` is `"rich"`, the text is parsed for Rich markup strings.
169 If `markup_mode` is `"markdown"`, parse as Markdown.
170 """
171 # Remove indentations from input text
172 text = inspect.cleandoc(text) 1acdefbg
173 if markup_mode == MARKUP_MODE_MARKDOWN: 1acdefbg
174 text = Emoji.replace(text) 1acdefbg
175 return Markdown(text, style=style) 1acdefbg
176 else:
177 assert markup_mode == MARKUP_MODE_RICH 1acdefbg
178 if _has_ansi_character(text): 1acdefbg
179 return highlighter(Text.from_ansi(text, style=style)) 1acdefbg
180 else:
181 return highlighter(Text.from_markup(text, style=style)) 1acdefbg
184@group() 1acdefbg
185def _get_help_text( 1acdefbg
186 *,
187 obj: click.Command | click.Group,
188 markup_mode: MarkupModeStrict,
189) -> Iterable[Markdown | Text]:
190 """Build primary help text for a click command or group.
192 Returns the prose help text for a command or group, rendered either as a
193 Rich Text object or as Markdown.
194 If the command is marked as deprecated, the deprecated string will be prepended.
195 """
196 # Prepend deprecated status
197 if obj.deprecated: 1acdefbg
198 yield Text(DEPRECATED_STRING, style=STYLE_DEPRECATED) 1acdefbg
200 # Fetch and dedent the help text
201 help_text = inspect.cleandoc(obj.help or "") 1acdefbg
203 # Trim off anything that comes after \f on its own line
204 help_text = help_text.partition("\f")[0] 1acdefbg
206 # Get the first paragraph
207 first_line, *remaining_paragraphs = help_text.split("\n\n") 1acdefbg
209 # Remove single linebreaks
210 if markup_mode != MARKUP_MODE_MARKDOWN and not first_line.startswith("\b"): 1acdefbg
211 first_line = first_line.replace("\n", " ") 1acdefbg
212 yield _make_rich_text( 1acdefbg
213 text=first_line.strip(),
214 style=STYLE_HELPTEXT_FIRST_LINE,
215 markup_mode=markup_mode,
216 )
218 # Get remaining lines, remove single line breaks and format as dim
219 if remaining_paragraphs: 1acdefbg
220 # Add a newline inbetween the header and the remaining paragraphs
221 yield Text("") 1acdefbg
222 # Join with double linebreaks for markdown and Rich markup
223 remaining_lines = "\n\n".join(remaining_paragraphs) 1acdefbg
225 yield _make_rich_text( 1acdefbg
226 text=remaining_lines,
227 style=STYLE_HELPTEXT,
228 markup_mode=markup_mode,
229 )
232def _get_parameter_help( 1acdefbg
233 *,
234 param: click.Option | click.Argument | click.Parameter,
235 ctx: click.Context,
236 markup_mode: MarkupModeStrict,
237) -> Columns:
238 """Build primary help text for a click option or argument.
240 Returns the prose help text for an option or argument, rendered either
241 as a Rich Text object or as Markdown.
242 Additional elements are appended to show the default and required status if
243 applicable.
244 """
245 # import here to avoid cyclic imports
246 from .core import TyperArgument, TyperOption 1acdefbg
248 items: list[Text | Markdown] = [] 1acdefbg
250 # Get the environment variable first
252 envvar = getattr(param, "envvar", None) 1acdefbg
253 var_str = "" 1acdefbg
254 # https://github.com/pallets/click/blob/0aec1168ac591e159baf6f61026d6ae322c53aaf/src/click/core.py#L2720-L2726
255 if envvar is None: 1acdefbg
256 if ( 1ab
257 getattr(param, "allow_from_autoenv", None)
258 and getattr(ctx, "auto_envvar_prefix", None) is not None
259 and param.name is not None
260 ):
261 envvar = f"{ctx.auto_envvar_prefix}_{param.name.upper()}" 1acdefbg
262 if envvar is not None: 1acdefbg
263 var_str = ( 1acdefbg
264 envvar if isinstance(envvar, str) else ", ".join(str(d) for d in envvar)
265 )
267 # Main help text
268 help_value: str | None = getattr(param, "help", None) 1acdefbg
269 if help_value: 1acdefbg
270 paragraphs = help_value.split("\n\n") 1acdefbg
271 # Remove single linebreaks
272 if markup_mode != MARKUP_MODE_MARKDOWN: 1acdefbg
273 paragraphs = [ 1acdefbg
274 x.replace("\n", " ").strip()
275 if not x.startswith("\b")
276 else "{}\n".format(x.strip("\b\n"))
277 for x in paragraphs
278 ]
279 items.append( 1acdefbg
280 _make_rich_text(
281 text="\n".join(paragraphs).strip(),
282 style=STYLE_OPTION_HELP,
283 markup_mode=markup_mode,
284 )
285 )
287 # Environment variable AFTER help text
288 if envvar and getattr(param, "show_envvar", None): 1acdefbg
289 items.append(Text(ENVVAR_STRING.format(var_str), style=STYLE_OPTION_ENVVAR)) 1acdefbg
291 # Default value
292 # This uses Typer's specific param._get_default_string
293 if isinstance(param, (TyperOption, TyperArgument)): 1acdefbg
294 default_value = param._extract_default_help_str(ctx=ctx) 1acdefbg
295 show_default_is_str = isinstance(param.show_default, str) 1acdefbg
296 if show_default_is_str or ( 1acdefbg
297 default_value is not None and (param.show_default or ctx.show_default)
298 ):
299 default_str = param._get_default_string( 1acdefbg
300 ctx=ctx,
301 show_default_is_str=show_default_is_str,
302 default_value=default_value,
303 )
304 if default_str: 1acdefbg
305 items.append( 1acdefbg
306 Text(
307 DEFAULT_STRING.format(default_str),
308 style=STYLE_OPTION_DEFAULT,
309 )
310 )
312 # Required?
313 if param.required: 1acdefbg
314 items.append(Text(REQUIRED_LONG_STRING, style=STYLE_REQUIRED_LONG)) 1acdefbg
316 # Use Columns - this allows us to group different renderable types
317 # (Text, Markdown) onto a single line.
318 return Columns(items) 1acdefbg
321def _make_command_help( 1acdefbg
322 *,
323 help_text: str,
324 markup_mode: MarkupModeStrict,
325) -> Text | Markdown:
326 """Build cli help text for a click group command.
328 That is, when calling help on groups with multiple subcommands
329 (not the main help text when calling the subcommand help).
331 Returns the first paragraph of help text for a command, rendered either as a
332 Rich Text object or as Markdown.
333 Ignores single newlines as paragraph markers, looks for double only.
334 """
335 paragraphs = inspect.cleandoc(help_text).split("\n\n") 1acdefbg
336 # Remove single linebreaks
337 if markup_mode != MARKUP_MODE_RICH and not paragraphs[0].startswith("\b"): 1acdefbg
338 paragraphs[0] = paragraphs[0].replace("\n", " ") 1acdefbg
339 elif paragraphs[0].startswith("\b"): 1acdefbg
340 paragraphs[0] = paragraphs[0].replace("\b\n", "") 1acdefbg
341 return _make_rich_text( 1acdefbg
342 text=paragraphs[0].strip(),
343 style=STYLE_OPTION_HELP,
344 markup_mode=markup_mode,
345 )
348def _print_options_panel( 1acdefbg
349 *,
350 name: str,
351 params: list[click.Option] | list[click.Argument],
352 ctx: click.Context,
353 markup_mode: MarkupModeStrict,
354 console: Console,
355) -> None:
356 options_rows: list[list[RenderableType]] = [] 1acdefbg
357 required_rows: list[str | Text] = [] 1acdefbg
358 for param in params: 1acdefbg
359 # Short and long form
360 opt_long_strs = [] 1acdefbg
361 opt_short_strs = [] 1acdefbg
362 secondary_opt_long_strs = [] 1acdefbg
363 secondary_opt_short_strs = [] 1acdefbg
364 for opt_str in param.opts: 1acdefbg
365 if "--" in opt_str: 1acdefbg
366 opt_long_strs.append(opt_str) 1acdefbg
367 else:
368 opt_short_strs.append(opt_str) 1acdefbg
369 for opt_str in param.secondary_opts: 1acdefbg
370 if "--" in opt_str: 1acdefbg
371 secondary_opt_long_strs.append(opt_str) 1acdefbg
372 else:
373 secondary_opt_short_strs.append(opt_str) 1acdefbg
375 # Column for a metavar, if we have one
376 metavar = Text(style=STYLE_METAVAR, overflow="fold") 1acdefbg
377 metavar_str = param.make_metavar(ctx=ctx) 1acdefbg
378 # Do it ourselves if this is a positional argument
379 if ( 1ab
380 isinstance(param, click.Argument)
381 and param.name
382 and metavar_str == param.name.upper()
383 ):
384 metavar_str = param.type.name.upper() 1acdefbg
386 # Skip booleans and choices (handled above)
387 if metavar_str != "BOOLEAN": 1acdefbg
388 metavar.append(metavar_str) 1acdefbg
390 # Range - from
391 # https://github.com/pallets/click/blob/c63c70dabd3f86ca68678b4f00951f78f52d0270/src/click/core.py#L2698-L2706 # noqa: E501
392 # skip count with default range type
393 if ( 1ab
394 isinstance(param.type, click.types._NumberRangeBase)
395 and isinstance(param, click.Option)
396 and not (param.count and param.type.min == 0 and param.type.max is None)
397 ):
398 range_str = param.type._describe_range() 1acdefbg
399 if range_str: 1acdefbg
400 metavar.append(RANGE_STRING.format(range_str)) 1acdefbg
402 # Required asterisk
403 required: str | Text = "" 1acdefbg
404 if param.required: 1acdefbg
405 required = Text(REQUIRED_SHORT_STRING, style=STYLE_REQUIRED_SHORT) 1acdefbg
407 required_rows.append(required) 1acdefbg
408 options_rows.append( 1acdefbg
409 [
410 highlighter(",".join(opt_long_strs)),
411 highlighter(",".join(opt_short_strs)),
412 negative_highlighter(",".join(secondary_opt_long_strs)),
413 negative_highlighter(",".join(secondary_opt_short_strs)),
414 metavar_highlighter(metavar),
415 _get_parameter_help(
416 param=param,
417 ctx=ctx,
418 markup_mode=markup_mode,
419 ),
420 ]
421 )
422 rows_with_required: list[list[RenderableType]] = [] 1acdefbg
423 if any(required_rows): 1acdefbg
424 for required, row in zip(required_rows, options_rows, strict=True): 1acdefbg
425 rows_with_required.append([required, *row]) 1acdefbg
426 else:
427 rows_with_required = options_rows 1acdefbg
428 if options_rows: 1acdefbg
429 t_styles: dict[str, Any] = { 1acdefbg
430 "show_lines": STYLE_OPTIONS_TABLE_SHOW_LINES,
431 "leading": STYLE_OPTIONS_TABLE_LEADING,
432 "box": STYLE_OPTIONS_TABLE_BOX,
433 "border_style": STYLE_OPTIONS_TABLE_BORDER_STYLE,
434 "row_styles": STYLE_OPTIONS_TABLE_ROW_STYLES,
435 "pad_edge": STYLE_OPTIONS_TABLE_PAD_EDGE,
436 "padding": STYLE_OPTIONS_TABLE_PADDING,
437 }
438 box_style = getattr(box, t_styles.pop("box"), None) 1acdefbg
440 options_table = Table( 1acdefbg
441 highlight=True,
442 show_header=False,
443 expand=True,
444 box=box_style,
445 **t_styles,
446 )
447 for row in rows_with_required: 1acdefbg
448 options_table.add_row(*row) 1acdefbg
449 console.print( 1acdefbg
450 Panel(
451 options_table,
452 border_style=STYLE_OPTIONS_PANEL_BORDER,
453 title=name,
454 title_align=ALIGN_OPTIONS_PANEL,
455 )
456 )
459def _print_commands_panel( 1acdefbg
460 *,
461 name: str,
462 commands: list[click.Command],
463 markup_mode: MarkupModeStrict,
464 console: Console,
465 cmd_len: int,
466) -> None:
467 t_styles: dict[str, Any] = { 1acdefbg
468 "show_lines": STYLE_COMMANDS_TABLE_SHOW_LINES,
469 "leading": STYLE_COMMANDS_TABLE_LEADING,
470 "box": STYLE_COMMANDS_TABLE_BOX,
471 "border_style": STYLE_COMMANDS_TABLE_BORDER_STYLE,
472 "row_styles": STYLE_COMMANDS_TABLE_ROW_STYLES,
473 "pad_edge": STYLE_COMMANDS_TABLE_PAD_EDGE,
474 "padding": STYLE_COMMANDS_TABLE_PADDING,
475 }
476 box_style = getattr(box, t_styles.pop("box"), None) 1acdefbg
478 commands_table = Table( 1acdefbg
479 highlight=False,
480 show_header=False,
481 expand=True,
482 box=box_style,
483 **t_styles,
484 )
485 # Define formatting in first column, as commands don't match highlighter
486 # regex
487 commands_table.add_column( 1acdefbg
488 style=STYLE_COMMANDS_TABLE_FIRST_COLUMN,
489 no_wrap=True,
490 width=cmd_len,
491 )
493 # A big ratio makes the description column be greedy and take all the space
494 # available instead of allowing the command column to grow and misalign with
495 # other panels.
496 commands_table.add_column("Description", justify="left", no_wrap=False, ratio=10) 1acdefbg
497 rows: list[list[RenderableType | None]] = [] 1acdefbg
498 deprecated_rows: list[RenderableType | None] = [] 1acdefbg
499 for command in commands: 1acdefbg
500 helptext = command.short_help or command.help or "" 1acdefbg
501 command_name = command.name or "" 1acdefbg
502 if command.deprecated: 1acdefbg
503 command_name_text = Text(f"{command_name}", style=STYLE_DEPRECATED_COMMAND) 1acdefbg
504 deprecated_rows.append(Text(DEPRECATED_STRING, style=STYLE_DEPRECATED)) 1acdefbg
505 else:
506 command_name_text = Text(command_name) 1acdefbg
507 deprecated_rows.append(None) 1acdefbg
508 rows.append( 1acdefbg
509 [
510 command_name_text,
511 _make_command_help(
512 help_text=helptext,
513 markup_mode=markup_mode,
514 ),
515 ]
516 )
517 rows_with_deprecated = rows 1acdefbg
518 if any(deprecated_rows): 1acdefbg
519 rows_with_deprecated = [] 1acdefbg
520 for row, deprecated_text in zip(rows, deprecated_rows, strict=True): 1acdefbg
521 rows_with_deprecated.append([*row, deprecated_text]) 1acdefbg
522 for row in rows_with_deprecated: 1acdefbg
523 commands_table.add_row(*row) 1acdefbg
524 if commands_table.row_count: 1acdefbg
525 console.print( 1acdefbg
526 Panel(
527 commands_table,
528 border_style=STYLE_COMMANDS_PANEL_BORDER,
529 title=name,
530 title_align=ALIGN_COMMANDS_PANEL,
531 )
532 )
535def rich_format_help( 1acdefbg
536 *,
537 obj: click.Command | click.Group,
538 ctx: click.Context,
539 markup_mode: MarkupModeStrict,
540) -> None:
541 """Print nicely formatted help text using rich.
543 Based on original code from rich-cli, by @willmcgugan.
544 https://github.com/Textualize/rich-cli/blob/8a2767c7a340715fc6fbf4930ace717b9b2fc5e5/src/rich_cli/__main__.py#L162-L236
546 Replacement for the click function format_help().
547 Takes a command or group and builds the help text output.
548 """
549 console = _get_rich_console() 1acdefbg
551 # Print usage
552 console.print( 1acdefbg
553 Padding(highlighter(obj.get_usage(ctx)), 1), style=STYLE_USAGE_COMMAND
554 )
556 # Print command / group help if we have some
557 if obj.help: 1acdefbg
558 # Print with some padding
559 console.print( 1acdefbg
560 Padding(
561 Align(
562 _get_help_text(
563 obj=obj,
564 markup_mode=markup_mode,
565 ),
566 pad=False,
567 ),
568 (0, 1, 1, 1),
569 )
570 )
571 panel_to_arguments: defaultdict[str, list[click.Argument]] = defaultdict(list) 1acdefbg
572 panel_to_options: defaultdict[str, list[click.Option]] = defaultdict(list) 1acdefbg
573 for param in obj.get_params(ctx): 1acdefbg
574 # Skip if option is hidden
575 if getattr(param, "hidden", False): 1acdefbg
576 continue 1acdefbg
577 if isinstance(param, click.Argument): 1acdefbg
578 panel_name = ( 1acdefbg
579 getattr(param, _RICH_HELP_PANEL_NAME, None) or ARGUMENTS_PANEL_TITLE
580 )
581 panel_to_arguments[panel_name].append(param) 1acdefbg
582 elif isinstance(param, click.Option): 1acdefbg
583 panel_name = ( 1acdefbg
584 getattr(param, _RICH_HELP_PANEL_NAME, None) or OPTIONS_PANEL_TITLE
585 )
586 panel_to_options[panel_name].append(param) 1acdefbg
587 default_arguments = panel_to_arguments.get(ARGUMENTS_PANEL_TITLE, []) 1acdefbg
588 _print_options_panel( 1acdefbg
589 name=ARGUMENTS_PANEL_TITLE,
590 params=default_arguments,
591 ctx=ctx,
592 markup_mode=markup_mode,
593 console=console,
594 )
595 for panel_name, arguments in panel_to_arguments.items(): 1acdefbg
596 if panel_name == ARGUMENTS_PANEL_TITLE: 1acdefbg
597 # Already printed above
598 continue 1acdefbg
599 _print_options_panel( 1acdefbg
600 name=panel_name,
601 params=arguments,
602 ctx=ctx,
603 markup_mode=markup_mode,
604 console=console,
605 )
606 default_options = panel_to_options.get(OPTIONS_PANEL_TITLE, []) 1acdefbg
607 _print_options_panel( 1acdefbg
608 name=OPTIONS_PANEL_TITLE,
609 params=default_options,
610 ctx=ctx,
611 markup_mode=markup_mode,
612 console=console,
613 )
614 for panel_name, options in panel_to_options.items(): 1acdefbg
615 if panel_name == OPTIONS_PANEL_TITLE: 1acdefbg
616 # Already printed above
617 continue 1acdefbg
618 _print_options_panel( 1acdefbg
619 name=panel_name,
620 params=options,
621 ctx=ctx,
622 markup_mode=markup_mode,
623 console=console,
624 )
626 if isinstance(obj, click.Group): 1acdefbg
627 panel_to_commands: defaultdict[str, list[click.Command]] = defaultdict(list) 1acdefbg
628 for command_name in obj.list_commands(ctx): 1acdefbg
629 command = obj.get_command(ctx, command_name) 1acdefbg
630 if command and not command.hidden: 1acdefbg
631 panel_name = ( 1acdefbg
632 getattr(command, _RICH_HELP_PANEL_NAME, None)
633 or COMMANDS_PANEL_TITLE
634 )
635 panel_to_commands[panel_name].append(command) 1acdefbg
637 # Identify the longest command name in all panels
638 max_cmd_len = max( 1acdefbg
639 [
640 len(command.name or "")
641 for commands in panel_to_commands.values()
642 for command in commands
643 ],
644 default=0,
645 )
647 # Print each command group panel
648 default_commands = panel_to_commands.get(COMMANDS_PANEL_TITLE, []) 1acdefbg
649 _print_commands_panel( 1acdefbg
650 name=COMMANDS_PANEL_TITLE,
651 commands=default_commands,
652 markup_mode=markup_mode,
653 console=console,
654 cmd_len=max_cmd_len,
655 )
656 for panel_name, commands in panel_to_commands.items(): 1acdefbg
657 if panel_name == COMMANDS_PANEL_TITLE: 1acdefbg
658 # Already printed above
659 continue 1acdefbg
660 _print_commands_panel( 1acdefbg
661 name=panel_name,
662 commands=commands,
663 markup_mode=markup_mode,
664 console=console,
665 cmd_len=max_cmd_len,
666 )
668 # Epilogue if we have it
669 if obj.epilog: 1acdefbg
670 # Remove single linebreaks, replace double with single
671 lines = obj.epilog.split("\n\n") 1acdefbg
672 epilogue = "\n".join([x.replace("\n", " ").strip() for x in lines]) 1acdefbg
673 epilogue_text = _make_rich_text(text=epilogue, markup_mode=markup_mode) 1acdefbg
674 console.print(Padding(Align(epilogue_text, pad=False), 1)) 1acdefbg
677def rich_format_error(self: click.ClickException) -> None: 1acdefbg
678 """Print richly formatted click errors.
680 Called by custom exception handler to print richly formatted click errors.
681 Mimics original click.ClickException.echo() function but with rich formatting.
682 """
683 # Don't do anything when it's a NoArgsIsHelpError (without importing it, cf. #1278)
684 if self.__class__.__name__ == "NoArgsIsHelpError": 1acdefbg
685 return 1acdefbg
687 console = _get_rich_console(stderr=True) 1acdefbg
688 ctx: click.Context | None = getattr(self, "ctx", None) 1acdefbg
689 if ctx is not None: 1acdefbg
690 console.print(ctx.get_usage()) 1acdefbg
692 if ctx is not None and ctx.command.get_help_option(ctx) is not None: 1acdefbg
693 console.print( 1acdefbg
694 RICH_HELP.format(
695 command_path=ctx.command_path, help_option=ctx.help_option_names[0]
696 ),
697 style=STYLE_ERRORS_SUGGESTION,
698 )
700 console.print( 1acdefbg
701 Panel(
702 highlighter(self.format_message()),
703 border_style=STYLE_ERRORS_PANEL_BORDER,
704 title=ERRORS_PANEL_TITLE,
705 title_align=ALIGN_ERRORS_PANEL,
706 )
707 )
710def rich_abort_error() -> None: 1acdefbg
711 """Print richly formatted abort error."""
712 console = _get_rich_console(stderr=True) 1acdefbg
713 console.print(ABORTED_TEXT, style=STYLE_ABORTED) 1acdefbg
716def escape_before_html_export(input_text: str) -> str: 1acdefbg
717 """Ensure that the input string can be used for HTML export."""
718 return escape(input_text).strip() 1acdefbg
721def rich_to_html(input_text: str) -> str: 1acdefbg
722 """Print the HTML version of a rich-formatted input string.
724 This function does not provide a full HTML page, but can be used to insert
725 HTML-formatted text spans into a markdown file.
726 """
727 console = Console(record=True, highlight=False, file=io.StringIO()) 1acdefbg
729 console.print(input_text, overflow="ignore", crop=False) 1acdefbg
731 return console.export_html(inline_styles=True, code_format="{code}").strip() 1acdefbg
734def rich_render_text(text: str) -> str: 1acdefbg
735 """Remove rich tags and render a pure text representation"""
736 console = _get_rich_console() 1acdefbg
737 return "".join(segment.text for segment in console.render(text)).rstrip("\n") 1acdefbg
740def get_traceback( 1acdefbg
741 exc: BaseException,
742 exception_config: DeveloperExceptionConfig,
743 internal_dir_names: list[str],
744) -> Traceback:
745 rich_tb = Traceback.from_exception( 1acdefbg
746 type(exc),
747 exc,
748 exc.__traceback__,
749 show_locals=exception_config.pretty_exceptions_show_locals,
750 suppress=internal_dir_names,
751 width=MAX_WIDTH,
752 )
753 return rich_tb 1acdefbg