Coverage for typer/rich_utils.py: 100%
281 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-11-13 11:07 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-11-13 11:07 +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, the text is not parsed for any special formatting.
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) 1haebfcdg
158 if markup_mode == MARKUP_MODE_MARKDOWN: 1haebfcdg
159 text = Emoji.replace(text) 1haebfcdg
160 return Markdown(text, style=style) 1haebfcdg
161 if markup_mode == MARKUP_MODE_RICH: 1haebfcdg
162 return highlighter(Text.from_markup(text, style=style)) 1haebfcdg
163 else:
164 return highlighter(Text(text, style=style)) 1haebfcdg
167@group() 1haebfcdg
168def _get_help_text( 1aebfcdg
169 *,
170 obj: Union[click.Command, click.Group],
171 markup_mode: MarkupMode,
172) -> Iterable[Union[Markdown, Text]]:
173 """Build primary help text for a click command or group.
175 Returns the prose help text for a command or group, rendered either as a
176 Rich Text object or as Markdown.
177 If the command is marked as deprecated, the deprecated string will be prepended.
178 """
179 # Prepend deprecated status
180 if obj.deprecated: 1haebfcdg
181 yield Text(DEPRECATED_STRING, style=STYLE_DEPRECATED) 1haebfcdg
183 # Fetch and dedent the help text
184 help_text = inspect.cleandoc(obj.help or "") 1haebfcdg
186 # Trim off anything that comes after \f on its own line
187 help_text = help_text.partition("\f")[0] 1haebfcdg
189 # Get the first paragraph
190 first_line = help_text.split("\n\n")[0] 1haebfcdg
191 # Remove single linebreaks
192 if markup_mode != MARKUP_MODE_MARKDOWN and not first_line.startswith("\b"): 1haebfcdg
193 first_line = first_line.replace("\n", " ") 1haebfcdg
194 yield _make_rich_text( 1haebfcdg
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 remaining_paragraphs = help_text.split("\n\n")[1:] 1haebfcdg
202 if remaining_paragraphs: 1haebfcdg
203 if markup_mode != MARKUP_MODE_RICH: 1haebfcdg
204 # Remove single linebreaks
205 remaining_paragraphs = [ 1aebfcdg
206 x.replace("\n", " ").strip()
207 if not x.startswith("\b")
208 else "{}\n".format(x.strip("\b\n"))
209 for x in remaining_paragraphs
210 ]
211 # Join back together
212 remaining_lines = "\n".join(remaining_paragraphs) 1haebfcdg
213 else:
214 # Join with double linebreaks if markdown
215 remaining_lines = "\n\n".join(remaining_paragraphs) 1haebfcdg
217 yield _make_rich_text( 1haebfcdg
218 text=remaining_lines,
219 style=STYLE_HELPTEXT,
220 markup_mode=markup_mode,
221 )
224def _get_parameter_help( 1aebfcdg
225 *,
226 param: Union[click.Option, click.Argument, click.Parameter],
227 ctx: click.Context,
228 markup_mode: MarkupMode,
229) -> Columns:
230 """Build primary help text for a click option or argument.
232 Returns the prose help text for an option or argument, rendered either
233 as a Rich Text object or as Markdown.
234 Additional elements are appended to show the default and required status if
235 applicable.
236 """
237 # import here to avoid cyclic imports
238 from .core import TyperArgument, TyperOption 1haebfcdg
240 items: List[Union[Text, Markdown]] = [] 1haebfcdg
242 # Get the environment variable first
244 envvar = getattr(param, "envvar", None) 1haebfcdg
245 var_str = "" 1haebfcdg
246 # https://github.com/pallets/click/blob/0aec1168ac591e159baf6f61026d6ae322c53aaf/src/click/core.py#L2720-L2726
247 if envvar is None: 1haebfcdg
248 if ( 1abcd
249 getattr(param, "allow_from_autoenv", None)
250 and getattr(ctx, "auto_envvar_prefix", None) is not None
251 and param.name is not None
252 ):
253 envvar = f"{ctx.auto_envvar_prefix}_{param.name.upper()}" 1haebfcdg
254 if envvar is not None: 1haebfcdg
255 var_str = ( 1aebfcdg
256 envvar if isinstance(envvar, str) else ", ".join(str(d) for d in envvar)
257 )
259 # Main help text
260 help_value: Union[str, None] = getattr(param, "help", None) 1haebfcdg
261 if help_value: 1haebfcdg
262 paragraphs = help_value.split("\n\n") 1haebfcdg
263 # Remove single linebreaks
264 if markup_mode != MARKUP_MODE_MARKDOWN: 1haebfcdg
265 paragraphs = [ 1aebfcdg
266 x.replace("\n", " ").strip()
267 if not x.startswith("\b")
268 else "{}\n".format(x.strip("\b\n"))
269 for x in paragraphs
270 ]
271 items.append( 1haebfcdg
272 _make_rich_text(
273 text="\n".join(paragraphs).strip(),
274 style=STYLE_OPTION_HELP,
275 markup_mode=markup_mode,
276 )
277 )
279 # Environment variable AFTER help text
280 if envvar and getattr(param, "show_envvar", None): 1haebfcdg
281 items.append(Text(ENVVAR_STRING.format(var_str), style=STYLE_OPTION_ENVVAR)) 1haebfcdg
283 # Default value
284 # This uses Typer's specific param._get_default_string
285 if isinstance(param, (TyperOption, TyperArgument)): 1haebfcdg
286 if param.show_default: 1haebfcdg
287 show_default_is_str = isinstance(param.show_default, str) 1haebfcdg
288 default_value = param._extract_default_help_str(ctx=ctx) 1haebfcdg
289 default_str = param._get_default_string( 1haebfcdg
290 ctx=ctx,
291 show_default_is_str=show_default_is_str,
292 default_value=default_value,
293 )
294 if default_str: 1haebfcdg
295 items.append( 1haebfcdg
296 Text(
297 DEFAULT_STRING.format(default_str),
298 style=STYLE_OPTION_DEFAULT,
299 )
300 )
302 # Required?
303 if param.required: 1haebfcdg
304 items.append(Text(REQUIRED_LONG_STRING, style=STYLE_REQUIRED_LONG)) 1haebfcdg
306 # Use Columns - this allows us to group different renderable types
307 # (Text, Markdown) onto a single line.
308 return Columns(items) 1haebfcdg
311def _make_command_help( 1aebfcdg
312 *,
313 help_text: str,
314 markup_mode: MarkupMode,
315) -> Union[Text, Markdown]:
316 """Build cli help text for a click group command.
318 That is, when calling help on groups with multiple subcommands
319 (not the main help text when calling the subcommand help).
321 Returns the first paragraph of help text for a command, rendered either as a
322 Rich Text object or as Markdown.
323 Ignores single newlines as paragraph markers, looks for double only.
324 """
325 paragraphs = inspect.cleandoc(help_text).split("\n\n") 1haebfcdg
326 # Remove single linebreaks
327 if markup_mode != MARKUP_MODE_RICH and not paragraphs[0].startswith("\b"): 1haebfcdg
328 paragraphs[0] = paragraphs[0].replace("\n", " ") 1haebfcdg
329 elif paragraphs[0].startswith("\b"): 1haebfcdg
330 paragraphs[0] = paragraphs[0].replace("\b\n", "") 1haebfcdg
331 return _make_rich_text( 1haebfcdg
332 text=paragraphs[0].strip(),
333 style=STYLE_OPTION_HELP,
334 markup_mode=markup_mode,
335 )
338def _print_options_panel( 1aebfcdg
339 *,
340 name: str,
341 params: Union[List[click.Option], List[click.Argument]],
342 ctx: click.Context,
343 markup_mode: MarkupMode,
344 console: Console,
345) -> None:
346 options_rows: List[List[RenderableType]] = [] 1haebfcdg
347 required_rows: List[Union[str, Text]] = [] 1haebfcdg
348 for param in params: 1haebfcdg
349 # Short and long form
350 opt_long_strs = [] 1haebfcdg
351 opt_short_strs = [] 1haebfcdg
352 secondary_opt_long_strs = [] 1haebfcdg
353 secondary_opt_short_strs = [] 1haebfcdg
354 for opt_str in param.opts: 1haebfcdg
355 if "--" in opt_str: 1haebfcdg
356 opt_long_strs.append(opt_str) 1haebfcdg
357 else:
358 opt_short_strs.append(opt_str) 1haebfcdg
359 for opt_str in param.secondary_opts: 1haebfcdg
360 if "--" in opt_str: 1haebfcdg
361 secondary_opt_long_strs.append(opt_str) 1haebfcdg
362 else:
363 secondary_opt_short_strs.append(opt_str) 1haebfcdg
365 # Column for a metavar, if we have one
366 metavar = Text(style=STYLE_METAVAR, overflow="fold") 1haebfcdg
367 metavar_str = param.make_metavar() 1haebfcdg
369 # Do it ourselves if this is a positional argument
370 if ( 1abcd
371 isinstance(param, click.Argument)
372 and param.name
373 and metavar_str == param.name.upper()
374 ):
375 metavar_str = param.type.name.upper() 1haebfcdg
377 # Skip booleans and choices (handled above)
378 if metavar_str != "BOOLEAN": 1haebfcdg
379 metavar.append(metavar_str) 1haebfcdg
381 # Range - from
382 # https://github.com/pallets/click/blob/c63c70dabd3f86ca68678b4f00951f78f52d0270/src/click/core.py#L2698-L2706 # noqa: E501
383 # skip count with default range type
384 if ( 1abcd
385 isinstance(param.type, click.types._NumberRangeBase)
386 and isinstance(param, click.Option)
387 and not (param.count and param.type.min == 0 and param.type.max is None)
388 ):
389 range_str = param.type._describe_range() 1haebfcdg
390 if range_str: 1haebfcdg
391 metavar.append(RANGE_STRING.format(range_str)) 1haebfcdg
393 # Required asterisk
394 required: Union[str, Text] = "" 1haebfcdg
395 if param.required: 1haebfcdg
396 required = Text(REQUIRED_SHORT_STRING, style=STYLE_REQUIRED_SHORT) 1haebfcdg
398 # Highlighter to make [ | ] and <> dim
399 class MetavarHighlighter(RegexHighlighter): 1haebfcdg
400 highlights = [ 1aebfcdg
401 r"^(?P<metavar_sep>(\[|<))",
402 r"(?P<metavar_sep>\|)",
403 r"(?P<metavar_sep>(\]|>)$)",
404 ]
406 metavar_highlighter = MetavarHighlighter() 1haebfcdg
408 required_rows.append(required) 1haebfcdg
409 options_rows.append( 1haebfcdg
410 [
411 highlighter(",".join(opt_long_strs)),
412 highlighter(",".join(opt_short_strs)),
413 negative_highlighter(",".join(secondary_opt_long_strs)),
414 negative_highlighter(",".join(secondary_opt_short_strs)),
415 metavar_highlighter(metavar),
416 _get_parameter_help(
417 param=param,
418 ctx=ctx,
419 markup_mode=markup_mode,
420 ),
421 ]
422 )
423 rows_with_required: List[List[RenderableType]] = [] 1haebfcdg
424 if any(required_rows): 1haebfcdg
425 for required, row in zip(required_rows, options_rows): 1haebfcdg
426 rows_with_required.append([required, *row]) 1haebfcdg
427 else:
428 rows_with_required = options_rows 1haebfcdg
429 if options_rows: 1haebfcdg
430 t_styles: Dict[str, Any] = { 1aebfcdg
431 "show_lines": STYLE_OPTIONS_TABLE_SHOW_LINES,
432 "leading": STYLE_OPTIONS_TABLE_LEADING,
433 "box": STYLE_OPTIONS_TABLE_BOX,
434 "border_style": STYLE_OPTIONS_TABLE_BORDER_STYLE,
435 "row_styles": STYLE_OPTIONS_TABLE_ROW_STYLES,
436 "pad_edge": STYLE_OPTIONS_TABLE_PAD_EDGE,
437 "padding": STYLE_OPTIONS_TABLE_PADDING,
438 }
439 box_style = getattr(box, t_styles.pop("box"), None) 1haebfcdg
441 options_table = Table( 1haebfcdg
442 highlight=True,
443 show_header=False,
444 expand=True,
445 box=box_style,
446 **t_styles,
447 )
448 for row in rows_with_required: 1haebfcdg
449 options_table.add_row(*row) 1haebfcdg
450 console.print( 1haebfcdg
451 Panel(
452 options_table,
453 border_style=STYLE_OPTIONS_PANEL_BORDER,
454 title=name,
455 title_align=ALIGN_OPTIONS_PANEL,
456 )
457 )
460def _print_commands_panel( 1aebfcdg
461 *,
462 name: str,
463 commands: List[click.Command],
464 markup_mode: MarkupMode,
465 console: Console,
466 cmd_len: int,
467) -> None:
468 t_styles: Dict[str, Any] = { 1aebfcdg
469 "show_lines": STYLE_COMMANDS_TABLE_SHOW_LINES,
470 "leading": STYLE_COMMANDS_TABLE_LEADING,
471 "box": STYLE_COMMANDS_TABLE_BOX,
472 "border_style": STYLE_COMMANDS_TABLE_BORDER_STYLE,
473 "row_styles": STYLE_COMMANDS_TABLE_ROW_STYLES,
474 "pad_edge": STYLE_COMMANDS_TABLE_PAD_EDGE,
475 "padding": STYLE_COMMANDS_TABLE_PADDING,
476 }
477 box_style = getattr(box, t_styles.pop("box"), None) 1haebfcdg
479 commands_table = Table( 1haebfcdg
480 highlight=False,
481 show_header=False,
482 expand=True,
483 box=box_style,
484 **t_styles,
485 )
486 # Define formatting in first column, as commands don't match highlighter
487 # regex
488 commands_table.add_column( 1haebfcdg
489 style="bold cyan",
490 no_wrap=True,
491 width=cmd_len,
492 )
494 # A big ratio makes the description column be greedy and take all the space
495 # available instead of allowing the command column to grow and misalign with
496 # other panels.
497 commands_table.add_column("Description", justify="left", no_wrap=False, ratio=10) 1haebfcdg
498 rows: List[List[Union[RenderableType, None]]] = [] 1haebfcdg
499 deprecated_rows: List[Union[RenderableType, None]] = [] 1haebfcdg
500 for command in commands: 1haebfcdg
501 helptext = command.short_help or command.help or "" 1haebfcdg
502 command_name = command.name or "" 1haebfcdg
503 if command.deprecated: 1haebfcdg
504 command_name_text = Text(f"{command_name}", style=STYLE_DEPRECATED_COMMAND) 1haebfcdg
505 deprecated_rows.append(Text(DEPRECATED_STRING, style=STYLE_DEPRECATED)) 1haebfcdg
506 else:
507 command_name_text = Text(command_name) 1haebfcdg
508 deprecated_rows.append(None) 1haebfcdg
509 rows.append( 1haebfcdg
510 [
511 command_name_text,
512 _make_command_help(
513 help_text=helptext,
514 markup_mode=markup_mode,
515 ),
516 ]
517 )
518 rows_with_deprecated = rows 1haebfcdg
519 if any(deprecated_rows): 1haebfcdg
520 rows_with_deprecated = [] 1haebfcdg
521 for row, deprecated_text in zip(rows, deprecated_rows): 1haebfcdg
522 rows_with_deprecated.append([*row, deprecated_text]) 1haebfcdg
523 for row in rows_with_deprecated: 1haebfcdg
524 commands_table.add_row(*row) 1haebfcdg
525 if commands_table.row_count: 1haebfcdg
526 console.print( 1haebfcdg
527 Panel(
528 commands_table,
529 border_style=STYLE_COMMANDS_PANEL_BORDER,
530 title=name,
531 title_align=ALIGN_COMMANDS_PANEL,
532 )
533 )
536def rich_format_help( 1aebfcdg
537 *,
538 obj: Union[click.Command, click.Group],
539 ctx: click.Context,
540 markup_mode: MarkupMode,
541) -> None:
542 """Print nicely formatted help text using rich.
544 Based on original code from rich-cli, by @willmcgugan.
545 https://github.com/Textualize/rich-cli/blob/8a2767c7a340715fc6fbf4930ace717b9b2fc5e5/src/rich_cli/__main__.py#L162-L236
547 Replacement for the click function format_help().
548 Takes a command or group and builds the help text output.
549 """
550 console = _get_rich_console() 1haebfcdg
552 # Print usage
553 console.print( 1haebfcdg
554 Padding(highlighter(obj.get_usage(ctx)), 1), style=STYLE_USAGE_COMMAND
555 )
557 # Print command / group help if we have some
558 if obj.help: 1haebfcdg
559 # Print with some padding
560 console.print( 1haebfcdg
561 Padding(
562 Align(
563 _get_help_text(
564 obj=obj,
565 markup_mode=markup_mode,
566 ),
567 pad=False,
568 ),
569 (0, 1, 1, 1),
570 )
571 )
572 panel_to_arguments: DefaultDict[str, List[click.Argument]] = defaultdict(list) 1haebfcdg
573 panel_to_options: DefaultDict[str, List[click.Option]] = defaultdict(list) 1haebfcdg
574 for param in obj.get_params(ctx): 1haebfcdg
575 # Skip if option is hidden
576 if getattr(param, "hidden", False): 1haebfcdg
577 continue 1haebfcdg
578 if isinstance(param, click.Argument): 1haebfcdg
579 panel_name = ( 1aebfcdg
580 getattr(param, _RICH_HELP_PANEL_NAME, None) or ARGUMENTS_PANEL_TITLE
581 )
582 panel_to_arguments[panel_name].append(param) 1haebfcdg
583 elif isinstance(param, click.Option): 1haebfcdg
584 panel_name = ( 1aebfcdg
585 getattr(param, _RICH_HELP_PANEL_NAME, None) or OPTIONS_PANEL_TITLE
586 )
587 panel_to_options[panel_name].append(param) 1haebfcdg
588 default_arguments = panel_to_arguments.get(ARGUMENTS_PANEL_TITLE, []) 1haebfcdg
589 _print_options_panel( 1haebfcdg
590 name=ARGUMENTS_PANEL_TITLE,
591 params=default_arguments,
592 ctx=ctx,
593 markup_mode=markup_mode,
594 console=console,
595 )
596 for panel_name, arguments in panel_to_arguments.items(): 1haebfcdg
597 if panel_name == ARGUMENTS_PANEL_TITLE: 1haebfcdg
598 # Already printed above
599 continue 1haebfcdg
600 _print_options_panel( 1haebfcdg
601 name=panel_name,
602 params=arguments,
603 ctx=ctx,
604 markup_mode=markup_mode,
605 console=console,
606 )
607 default_options = panel_to_options.get(OPTIONS_PANEL_TITLE, []) 1haebfcdg
608 _print_options_panel( 1haebfcdg
609 name=OPTIONS_PANEL_TITLE,
610 params=default_options,
611 ctx=ctx,
612 markup_mode=markup_mode,
613 console=console,
614 )
615 for panel_name, options in panel_to_options.items(): 1haebfcdg
616 if panel_name == OPTIONS_PANEL_TITLE: 1haebfcdg
617 # Already printed above
618 continue 1haebfcdg
619 _print_options_panel( 1haebfcdg
620 name=panel_name,
621 params=options,
622 ctx=ctx,
623 markup_mode=markup_mode,
624 console=console,
625 )
627 if isinstance(obj, click.Group): 1haebfcdg
628 panel_to_commands: DefaultDict[str, List[click.Command]] = defaultdict(list) 1haebfcdg
629 for command_name in obj.list_commands(ctx): 1haebfcdg
630 command = obj.get_command(ctx, command_name) 1haebfcdg
631 if command and not command.hidden: 1haebfcdg
632 panel_name = ( 1aebfcdg
633 getattr(command, _RICH_HELP_PANEL_NAME, None)
634 or COMMANDS_PANEL_TITLE
635 )
636 panel_to_commands[panel_name].append(command) 1haebfcdg
638 # Identify the longest command name in all panels
639 max_cmd_len = max( 1haebfcdg
640 [
641 len(command.name or "")
642 for commands in panel_to_commands.values()
643 for command in commands
644 ],
645 default=0,
646 )
648 # Print each command group panel
649 default_commands = panel_to_commands.get(COMMANDS_PANEL_TITLE, []) 1haebfcdg
650 _print_commands_panel( 1haebfcdg
651 name=COMMANDS_PANEL_TITLE,
652 commands=default_commands,
653 markup_mode=markup_mode,
654 console=console,
655 cmd_len=max_cmd_len,
656 )
657 for panel_name, commands in panel_to_commands.items(): 1haebfcdg
658 if panel_name == COMMANDS_PANEL_TITLE: 1haebfcdg
659 # Already printed above
660 continue 1haebfcdg
661 _print_commands_panel( 1haebfcdg
662 name=panel_name,
663 commands=commands,
664 markup_mode=markup_mode,
665 console=console,
666 cmd_len=max_cmd_len,
667 )
669 # Epilogue if we have it
670 if obj.epilog: 1haebfcdg
671 # Remove single linebreaks, replace double with single
672 lines = obj.epilog.split("\n\n") 1haebfcdg
673 epilogue = "\n".join([x.replace("\n", " ").strip() for x in lines]) 1haebfcdg
674 epilogue_text = _make_rich_text(text=epilogue, markup_mode=markup_mode) 1haebfcdg
675 console.print(Padding(Align(epilogue_text, pad=False), 1)) 1haebfcdg
678def rich_format_error(self: click.ClickException) -> None: 1haebfcdg
679 """Print richly formatted click errors.
681 Called by custom exception handler to print richly formatted click errors.
682 Mimics original click.ClickException.echo() function but with rich formatting.
683 """
684 console = _get_rich_console(stderr=True) 1haebfcdg
685 ctx: Union[click.Context, None] = getattr(self, "ctx", None) 1haebfcdg
686 if ctx is not None: 1haebfcdg
687 console.print(ctx.get_usage()) 1haebfcdg
689 if ctx is not None and ctx.command.get_help_option(ctx) is not None: 1haebfcdg
690 console.print( 1haebfcdg
691 f"Try [blue]'{ctx.command_path} {ctx.help_option_names[0]}'[/] for help.",
692 style=STYLE_ERRORS_SUGGESTION,
693 )
695 console.print( 1haebfcdg
696 Panel(
697 highlighter(self.format_message()),
698 border_style=STYLE_ERRORS_PANEL_BORDER,
699 title=ERRORS_PANEL_TITLE,
700 title_align=ALIGN_ERRORS_PANEL,
701 )
702 )
705def rich_abort_error() -> None: 1haebfcdg
706 """Print richly formatted abort error."""
707 console = _get_rich_console(stderr=True) 1haebfcdg
708 console.print(ABORTED_TEXT, style=STYLE_ABORTED) 1haebfcdg