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