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