typer.rich_utils

  1# Extracted and modified from https://github.com/ewels/rich-click
  2
  3import inspect
  4import sys
  5from collections import defaultdict
  6from gettext import gettext as _
  7from os import getenv
  8from typing import Any, DefaultDict, Dict, Iterable, List, Optional, Union
  9
 10import click
 11from rich import box
 12from rich.align import Align
 13from rich.columns import Columns
 14from rich.console import Console, RenderableType, group
 15from rich.emoji import Emoji
 16from rich.highlighter import RegexHighlighter
 17from rich.markdown import Markdown
 18from rich.padding import Padding
 19from rich.panel import Panel
 20from rich.table import Table
 21from rich.text import Text
 22from rich.theme import Theme
 23
 24if sys.version_info >= (3, 8):
 25    from typing import Literal
 26else:
 27    from typing_extensions import Literal
 28
 29# Default styles
 30STYLE_OPTION = "bold cyan"
 31STYLE_SWITCH = "bold green"
 32STYLE_NEGATIVE_OPTION = "bold magenta"
 33STYLE_NEGATIVE_SWITCH = "bold red"
 34STYLE_METAVAR = "bold yellow"
 35STYLE_METAVAR_SEPARATOR = "dim"
 36STYLE_USAGE = "yellow"
 37STYLE_USAGE_COMMAND = "bold"
 38STYLE_DEPRECATED = "red"
 39STYLE_DEPRECATED_COMMAND = "dim"
 40STYLE_HELPTEXT_FIRST_LINE = ""
 41STYLE_HELPTEXT = "dim"
 42STYLE_OPTION_HELP = ""
 43STYLE_OPTION_DEFAULT = "dim"
 44STYLE_OPTION_ENVVAR = "dim yellow"
 45STYLE_REQUIRED_SHORT = "red"
 46STYLE_REQUIRED_LONG = "dim red"
 47STYLE_OPTIONS_PANEL_BORDER = "dim"
 48ALIGN_OPTIONS_PANEL: Literal["left", "center", "right"] = "left"
 49STYLE_OPTIONS_TABLE_SHOW_LINES = False
 50STYLE_OPTIONS_TABLE_LEADING = 0
 51STYLE_OPTIONS_TABLE_PAD_EDGE = False
 52STYLE_OPTIONS_TABLE_PADDING = (0, 1)
 53STYLE_OPTIONS_TABLE_BOX = ""
 54STYLE_OPTIONS_TABLE_ROW_STYLES = None
 55STYLE_OPTIONS_TABLE_BORDER_STYLE = None
 56STYLE_COMMANDS_PANEL_BORDER = "dim"
 57ALIGN_COMMANDS_PANEL: Literal["left", "center", "right"] = "left"
 58STYLE_COMMANDS_TABLE_SHOW_LINES = False
 59STYLE_COMMANDS_TABLE_LEADING = 0
 60STYLE_COMMANDS_TABLE_PAD_EDGE = False
 61STYLE_COMMANDS_TABLE_PADDING = (0, 1)
 62STYLE_COMMANDS_TABLE_BOX = ""
 63STYLE_COMMANDS_TABLE_ROW_STYLES = None
 64STYLE_COMMANDS_TABLE_BORDER_STYLE = None
 65STYLE_ERRORS_PANEL_BORDER = "red"
 66ALIGN_ERRORS_PANEL: Literal["left", "center", "right"] = "left"
 67STYLE_ERRORS_SUGGESTION = "dim"
 68STYLE_ABORTED = "red"
 69_TERMINAL_WIDTH = getenv("TERMINAL_WIDTH")
 70MAX_WIDTH = int(_TERMINAL_WIDTH) if _TERMINAL_WIDTH else None
 71COLOR_SYSTEM: Optional[
 72    Literal["auto", "standard", "256", "truecolor", "windows"]
 73] = "auto"  # Set to None to disable colors
 74_TYPER_FORCE_DISABLE_TERMINAL = getenv("_TYPER_FORCE_DISABLE_TERMINAL")
 75FORCE_TERMINAL = (
 76    True
 77    if getenv("GITHUB_ACTIONS") or getenv("FORCE_COLOR") or getenv("PY_COLORS")
 78    else None
 79)
 80if _TYPER_FORCE_DISABLE_TERMINAL:
 81    FORCE_TERMINAL = False
 82
 83# Fixed strings
 84DEPRECATED_STRING = _("(deprecated) ")
 85DEFAULT_STRING = _("[default: {}]")
 86ENVVAR_STRING = _("[env var: {}]")
 87REQUIRED_SHORT_STRING = "*"
 88REQUIRED_LONG_STRING = _("[required]")
 89RANGE_STRING = " [{}]"
 90ARGUMENTS_PANEL_TITLE = _("Arguments")
 91OPTIONS_PANEL_TITLE = _("Options")
 92COMMANDS_PANEL_TITLE = _("Commands")
 93ERRORS_PANEL_TITLE = _("Error")
 94ABORTED_TEXT = _("Aborted.")
 95
 96MARKUP_MODE_MARKDOWN = "markdown"
 97MARKUP_MODE_RICH = "rich"
 98_RICH_HELP_PANEL_NAME = "rich_help_panel"
 99
100MarkupMode = Literal["markdown", "rich", None]
101
102
103# Rich regex highlighter
104class OptionHighlighter(RegexHighlighter):
105    """Highlights our special options."""
106
107    highlights = [
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    ]
113
114
115class NegativeOptionHighlighter(RegexHighlighter):
116    highlights = [
117        r"(^|\W)(?P<negative_switch>\-\w+)(?![a-zA-Z0-9])",
118        r"(^|\W)(?P<negative_option>\-\-[\w\-]+)(?![a-zA-Z0-9])",
119    ]
120
121
122highlighter = OptionHighlighter()
123negative_highlighter = NegativeOptionHighlighter()
124
125
126def _get_rich_console(stderr: bool = False) -> Console:
127    return Console(
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    )
145
146
147def _make_rich_rext(
148    *, text: str, style: str = "", markup_mode: MarkupMode
149) -> Union[Markdown, Text]:
150    """Take a string, remove indentations, and return styled text.
151
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.
155
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)
161    if markup_mode == MARKUP_MODE_MARKDOWN:
162        text = Emoji.replace(text)
163        return Markdown(text, style=style)
164    if markup_mode == MARKUP_MODE_RICH:
165        return highlighter(Text.from_markup(text, style=style))
166    else:
167        return highlighter(Text(text, style=style))
168
169
170@group()
171def _get_help_text(
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.
177
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:
184        yield Text(DEPRECATED_STRING, style=STYLE_DEPRECATED)
185
186    # Fetch and dedent the help text
187    help_text = inspect.cleandoc(obj.help or "")
188
189    # Trim off anything that comes after \f on its own line
190    help_text = help_text.partition("\f")[0]
191
192    # Get the first paragraph
193    first_line = help_text.split("\n\n")[0]
194    # Remove single linebreaks
195    if markup_mode != MARKUP_MODE_MARKDOWN and not first_line.startswith("\b"):
196        first_line = first_line.replace("\n", " ")
197    yield _make_rich_rext(
198        text=first_line.strip(),
199        style=STYLE_HELPTEXT_FIRST_LINE,
200        markup_mode=markup_mode,
201    )
202
203    # Get remaining lines, remove single line breaks and format as dim
204    remaining_paragraphs = help_text.split("\n\n")[1:]
205    if remaining_paragraphs:
206        if markup_mode != MARKUP_MODE_RICH:
207            # Remove single linebreaks
208            remaining_paragraphs = [
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)
216        else:
217            # Join with double linebreaks if markdown
218            remaining_lines = "\n\n".join(remaining_paragraphs)
219
220        yield _make_rich_rext(
221            text=remaining_lines,
222            style=STYLE_HELPTEXT,
223            markup_mode=markup_mode,
224        )
225
226
227def _get_parameter_help(
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.
234
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
242
243    items: List[Union[Text, Markdown]] = []
244
245    # Get the environment variable first
246
247    envvar = getattr(param, "envvar", None)
248    var_str = ""
249    # https://github.com/pallets/click/blob/0aec1168ac591e159baf6f61026d6ae322c53aaf/src/click/core.py#L2720-L2726
250    if envvar is None:
251        if (
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()}"
257    if envvar is not None:
258        var_str = (
259            envvar if isinstance(envvar, str) else ", ".join(str(d) for d in envvar)
260        )
261
262    # Main help text
263    help_value: Union[str, None] = getattr(param, "help", None)
264    if help_value:
265        paragraphs = help_value.split("\n\n")
266        # Remove single linebreaks
267        if markup_mode != MARKUP_MODE_MARKDOWN:
268            paragraphs = [
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(
275            _make_rich_rext(
276                text="\n".join(paragraphs).strip(),
277                style=STYLE_OPTION_HELP,
278                markup_mode=markup_mode,
279            )
280        )
281
282    # Environment variable AFTER help text
283    if envvar and getattr(param, "show_envvar", None):
284        items.append(Text(ENVVAR_STRING.format(var_str), style=STYLE_OPTION_ENVVAR))
285
286    # Default value
287    # This uses Typer's specific param._get_default_string
288    if isinstance(param, (TyperOption, TyperArgument)):
289        if param.show_default:
290            show_default_is_str = isinstance(param.show_default, str)
291            default_value = param._extract_default_help_str(ctx=ctx)
292            default_str = param._get_default_string(
293                ctx=ctx,
294                show_default_is_str=show_default_is_str,
295                default_value=default_value,
296            )
297            if default_str:
298                items.append(
299                    Text(
300                        DEFAULT_STRING.format(default_str),
301                        style=STYLE_OPTION_DEFAULT,
302                    )
303                )
304
305    # Required?
306    if param.required:
307        items.append(Text(REQUIRED_LONG_STRING, style=STYLE_REQUIRED_LONG))
308
309    # Use Columns - this allows us to group different renderable types
310    # (Text, Markdown) onto a single line.
311    return Columns(items)
312
313
314def _make_command_help(
315    *,
316    help_text: str,
317    markup_mode: MarkupMode,
318) -> Union[Text, Markdown]:
319    """Build cli help text for a click group command.
320
321    That is, when calling help on groups with multiple subcommands
322    (not the main help text when calling the subcommand help).
323
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")
329    # Remove single linebreaks
330    if markup_mode != MARKUP_MODE_RICH and not paragraphs[0].startswith("\b"):
331        paragraphs[0] = paragraphs[0].replace("\n", " ")
332    elif paragraphs[0].startswith("\b"):
333        paragraphs[0] = paragraphs[0].replace("\b\n", "")
334    return _make_rich_rext(
335        text=paragraphs[0].strip(),
336        style=STYLE_OPTION_HELP,
337        markup_mode=markup_mode,
338    )
339
340
341def _print_options_panel(
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]] = []
350    required_rows: List[Union[str, Text]] = []
351    for param in params:
352        # Short and long form
353        opt_long_strs = []
354        opt_short_strs = []
355        secondary_opt_long_strs = []
356        secondary_opt_short_strs = []
357        for opt_str in param.opts:
358            if "--" in opt_str:
359                opt_long_strs.append(opt_str)
360            else:
361                opt_short_strs.append(opt_str)
362        for opt_str in param.secondary_opts:
363            if "--" in opt_str:
364                secondary_opt_long_strs.append(opt_str)
365            else:
366                secondary_opt_short_strs.append(opt_str)
367
368        # Column for a metavar, if we have one
369        metavar = Text(style=STYLE_METAVAR, overflow="fold")
370        metavar_str = param.make_metavar()
371
372        # Do it ourselves if this is a positional argument
373        if (
374            isinstance(param, click.Argument)
375            and param.name
376            and metavar_str == param.name.upper()
377        ):
378            metavar_str = param.type.name.upper()
379
380        # Skip booleans and choices (handled above)
381        if metavar_str != "BOOLEAN":
382            metavar.append(metavar_str)
383
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 (
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()
393            if range_str:
394                metavar.append(RANGE_STRING.format(range_str))
395
396        # Required asterisk
397        required: Union[str, Text] = ""
398        if param.required:
399            required = Text(REQUIRED_SHORT_STRING, style=STYLE_REQUIRED_SHORT)
400
401        # Highlighter to make [ | ] and <> dim
402        class MetavarHighlighter(RegexHighlighter):
403            highlights = [
404                r"^(?P<metavar_sep>(\[|<))",
405                r"(?P<metavar_sep>\|)",
406                r"(?P<metavar_sep>(\]|>)$)",
407            ]
408
409        metavar_highlighter = MetavarHighlighter()
410
411        required_rows.append(required)
412        options_rows.append(
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]] = []
427    if any(required_rows):
428        for required, row in zip(required_rows, options_rows):
429            rows_with_required.append([required, *row])
430    else:
431        rows_with_required = options_rows
432    if options_rows:
433        t_styles: Dict[str, Any] = {
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)
443
444        options_table = Table(
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:
452            options_table.add_row(*row)
453        console.print(
454            Panel(
455                options_table,
456                border_style=STYLE_OPTIONS_PANEL_BORDER,
457                title=name,
458                title_align=ALIGN_OPTIONS_PANEL,
459            )
460        )
461
462
463def _print_commands_panel(
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] = {
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)
481
482    commands_table = Table(
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(
492        style="bold cyan",
493        no_wrap=True,
494        width=cmd_len,
495    )
496
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)
501    rows: List[List[Union[RenderableType, None]]] = []
502    deprecated_rows: List[Union[RenderableType, None]] = []
503    for command in commands:
504        helptext = command.short_help or command.help or ""
505        command_name = command.name or ""
506        if command.deprecated:
507            command_name_text = Text(f"{command_name}", style=STYLE_DEPRECATED_COMMAND)
508            deprecated_rows.append(Text(DEPRECATED_STRING, style=STYLE_DEPRECATED))
509        else:
510            command_name_text = Text(command_name)
511            deprecated_rows.append(None)
512        rows.append(
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
522    if any(deprecated_rows):
523        rows_with_deprecated = []
524        for row, deprecated_text in zip(rows, deprecated_rows):
525            rows_with_deprecated.append([*row, deprecated_text])
526    for row in rows_with_deprecated:
527        commands_table.add_row(*row)
528    if commands_table.row_count:
529        console.print(
530            Panel(
531                commands_table,
532                border_style=STYLE_COMMANDS_PANEL_BORDER,
533                title=name,
534                title_align=ALIGN_COMMANDS_PANEL,
535            )
536        )
537
538
539def rich_format_help(
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.
546
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
549
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()
554
555    # Print usage
556    console.print(
557        Padding(highlighter(obj.get_usage(ctx)), 1), style=STYLE_USAGE_COMMAND
558    )
559
560    # Print command / group help if we have some
561    if obj.help:
562        # Print with some padding
563        console.print(
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)
576    panel_to_options: DefaultDict[str, List[click.Option]] = defaultdict(list)
577    for param in obj.get_params(ctx):
578        # Skip if option is hidden
579        if getattr(param, "hidden", False):
580            continue
581        if isinstance(param, click.Argument):
582            panel_name = (
583                getattr(param, _RICH_HELP_PANEL_NAME, None) or ARGUMENTS_PANEL_TITLE
584            )
585            panel_to_arguments[panel_name].append(param)
586        elif isinstance(param, click.Option):
587            panel_name = (
588                getattr(param, _RICH_HELP_PANEL_NAME, None) or OPTIONS_PANEL_TITLE
589            )
590            panel_to_options[panel_name].append(param)
591    default_arguments = panel_to_arguments.get(ARGUMENTS_PANEL_TITLE, [])
592    _print_options_panel(
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():
600        if panel_name == ARGUMENTS_PANEL_TITLE:
601            # Already printed above
602            continue
603        _print_options_panel(
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, [])
611    _print_options_panel(
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():
619        if panel_name == OPTIONS_PANEL_TITLE:
620            # Already printed above
621            continue
622        _print_options_panel(
623            name=panel_name,
624            params=options,
625            ctx=ctx,
626            markup_mode=markup_mode,
627            console=console,
628        )
629
630    if isinstance(obj, click.Group):
631        panel_to_commands: DefaultDict[str, List[click.Command]] = defaultdict(list)
632        for command_name in obj.list_commands(ctx):
633            command = obj.get_command(ctx, command_name)
634            if command and not command.hidden:
635                panel_name = (
636                    getattr(command, _RICH_HELP_PANEL_NAME, None)
637                    or COMMANDS_PANEL_TITLE
638                )
639                panel_to_commands[panel_name].append(command)
640
641        # Identify the longest command name in all panels
642        max_cmd_len = max(
643            [
644                len(command.name or "")
645                for commands in panel_to_commands.values()
646                for command in commands
647            ],
648            default=0,
649        )
650
651        # Print each command group panel
652        default_commands = panel_to_commands.get(COMMANDS_PANEL_TITLE, [])
653        _print_commands_panel(
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():
661            if panel_name == COMMANDS_PANEL_TITLE:
662                # Already printed above
663                continue
664            _print_commands_panel(
665                name=panel_name,
666                commands=commands,
667                markup_mode=markup_mode,
668                console=console,
669                cmd_len=max_cmd_len,
670            )
671
672    # Epilogue if we have it
673    if obj.epilog:
674        # Remove single linebreaks, replace double with single
675        lines = obj.epilog.split("\n\n")
676        epilogue = "\n".join([x.replace("\n", " ").strip() for x in lines])
677        epilogue_text = _make_rich_rext(text=epilogue, markup_mode=markup_mode)
678        console.print(Padding(Align(epilogue_text, pad=False), 1))
679
680
681def rich_format_error(self: click.ClickException) -> None:
682    """Print richly formatted click errors.
683
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)
688    ctx: Union[click.Context, None] = getattr(self, "ctx", None)
689    if ctx is not None:
690        console.print(ctx.get_usage())
691
692    if ctx is not None and ctx.command.get_help_option(ctx) is not None:
693        console.print(
694            f"Try [blue]'{ctx.command_path} {ctx.help_option_names[0]}'[/] for help.",
695            style=STYLE_ERRORS_SUGGESTION,
696        )
697
698    console.print(
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    )
706
707
708def rich_abort_error() -> None:
709    """Print richly formatted abort error."""
710    console = _get_rich_console(stderr=True)
711    console.print(ABORTED_TEXT, style=STYLE_ABORTED)
STYLE_OPTION = 'bold cyan'
STYLE_SWITCH = 'bold green'
STYLE_NEGATIVE_OPTION = 'bold magenta'
STYLE_NEGATIVE_SWITCH = 'bold red'
STYLE_METAVAR = 'bold yellow'
STYLE_METAVAR_SEPARATOR = 'dim'
STYLE_USAGE = 'yellow'
STYLE_USAGE_COMMAND = 'bold'
STYLE_DEPRECATED = 'red'
STYLE_DEPRECATED_COMMAND = 'dim'
STYLE_HELPTEXT_FIRST_LINE = ''
STYLE_HELPTEXT = 'dim'
STYLE_OPTION_HELP = ''
STYLE_OPTION_DEFAULT = 'dim'
STYLE_OPTION_ENVVAR = 'dim yellow'
STYLE_REQUIRED_SHORT = 'red'
STYLE_REQUIRED_LONG = 'dim red'
STYLE_OPTIONS_PANEL_BORDER = 'dim'
ALIGN_OPTIONS_PANEL: Literal['left', 'center', 'right'] = 'left'
STYLE_OPTIONS_TABLE_SHOW_LINES = False
STYLE_OPTIONS_TABLE_LEADING = 0
STYLE_OPTIONS_TABLE_PAD_EDGE = False
STYLE_OPTIONS_TABLE_PADDING = (0, 1)
STYLE_OPTIONS_TABLE_BOX = ''
STYLE_OPTIONS_TABLE_ROW_STYLES = None
STYLE_OPTIONS_TABLE_BORDER_STYLE = None
STYLE_COMMANDS_PANEL_BORDER = 'dim'
ALIGN_COMMANDS_PANEL: Literal['left', 'center', 'right'] = 'left'
STYLE_COMMANDS_TABLE_SHOW_LINES = False
STYLE_COMMANDS_TABLE_LEADING = 0
STYLE_COMMANDS_TABLE_PAD_EDGE = False
STYLE_COMMANDS_TABLE_PADDING = (0, 1)
STYLE_COMMANDS_TABLE_BOX = ''
STYLE_COMMANDS_TABLE_ROW_STYLES = None
STYLE_COMMANDS_TABLE_BORDER_STYLE = None
STYLE_ERRORS_PANEL_BORDER = 'red'
ALIGN_ERRORS_PANEL: Literal['left', 'center', 'right'] = 'left'
STYLE_ERRORS_SUGGESTION = 'dim'
STYLE_ABORTED = 'red'
MAX_WIDTH = None
COLOR_SYSTEM: Optional[Literal['auto', 'standard', '256', 'truecolor', 'windows']] = 'auto'
FORCE_TERMINAL = None
DEPRECATED_STRING = '(deprecated) '
DEFAULT_STRING = '[default: {}]'
ENVVAR_STRING = '[env var: {}]'
REQUIRED_SHORT_STRING = '*'
REQUIRED_LONG_STRING = '[required]'
RANGE_STRING = ' [{}]'
ARGUMENTS_PANEL_TITLE = 'Arguments'
OPTIONS_PANEL_TITLE = 'Options'
COMMANDS_PANEL_TITLE = 'Commands'
ERRORS_PANEL_TITLE = 'Error'
ABORTED_TEXT = 'Aborted.'
MARKUP_MODE_MARKDOWN = 'markdown'
MARKUP_MODE_RICH = 'rich'
MarkupMode = typing.Literal['markdown', 'rich', None]
class OptionHighlighter(rich.highlighter.RegexHighlighter):
105class OptionHighlighter(RegexHighlighter):
106    """Highlights our special options."""
107
108    highlights = [
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    ]

Highlights our special options.

highlights = ['(^|\\W)(?P<switch>\\-\\w+)(?![a-zA-Z0-9])', '(^|\\W)(?P<option>\\-\\-[\\w\\-]+)(?![a-zA-Z0-9])', '(?P<metavar>\\<[^\\>]+\\>)', '(?P<usage>Usage: )']
Inherited Members
rich.highlighter.RegexHighlighter
base_style
highlight
class NegativeOptionHighlighter(rich.highlighter.RegexHighlighter):
116class NegativeOptionHighlighter(RegexHighlighter):
117    highlights = [
118        r"(^|\W)(?P<negative_switch>\-\w+)(?![a-zA-Z0-9])",
119        r"(^|\W)(?P<negative_option>\-\-[\w\-]+)(?![a-zA-Z0-9])",
120    ]

Applies highlighting from a list of regular expressions.

highlights = ['(^|\\W)(?P<negative_switch>\\-\\w+)(?![a-zA-Z0-9])', '(^|\\W)(?P<negative_option>\\-\\-[\\w\\-]+)(?![a-zA-Z0-9])']
Inherited Members
rich.highlighter.RegexHighlighter
base_style
highlight
highlighter = <OptionHighlighter object>
negative_highlighter = <NegativeOptionHighlighter object>
def rich_format_help( *, obj: Union[click.core.Command, click.core.Group], ctx: click.core.Context, markup_mode: Literal['markdown', 'rich', None]) -> None:
540def rich_format_help(
541    *,
542    obj: Union[click.Command, click.Group],
543    ctx: click.Context,
544    markup_mode: MarkupMode,
545) -> None:
546    """Print nicely formatted help text using rich.
547
548    Based on original code from rich-cli, by @willmcgugan.
549    https://github.com/Textualize/rich-cli/blob/8a2767c7a340715fc6fbf4930ace717b9b2fc5e5/src/rich_cli/__main__.py#L162-L236
550
551    Replacement for the click function format_help().
552    Takes a command or group and builds the help text output.
553    """
554    console = _get_rich_console()
555
556    # Print usage
557    console.print(
558        Padding(highlighter(obj.get_usage(ctx)), 1), style=STYLE_USAGE_COMMAND
559    )
560
561    # Print command / group help if we have some
562    if obj.help:
563        # Print with some padding
564        console.print(
565            Padding(
566                Align(
567                    _get_help_text(
568                        obj=obj,
569                        markup_mode=markup_mode,
570                    ),
571                    pad=False,
572                ),
573                (0, 1, 1, 1),
574            )
575        )
576    panel_to_arguments: DefaultDict[str, List[click.Argument]] = defaultdict(list)
577    panel_to_options: DefaultDict[str, List[click.Option]] = defaultdict(list)
578    for param in obj.get_params(ctx):
579        # Skip if option is hidden
580        if getattr(param, "hidden", False):
581            continue
582        if isinstance(param, click.Argument):
583            panel_name = (
584                getattr(param, _RICH_HELP_PANEL_NAME, None) or ARGUMENTS_PANEL_TITLE
585            )
586            panel_to_arguments[panel_name].append(param)
587        elif isinstance(param, click.Option):
588            panel_name = (
589                getattr(param, _RICH_HELP_PANEL_NAME, None) or OPTIONS_PANEL_TITLE
590            )
591            panel_to_options[panel_name].append(param)
592    default_arguments = panel_to_arguments.get(ARGUMENTS_PANEL_TITLE, [])
593    _print_options_panel(
594        name=ARGUMENTS_PANEL_TITLE,
595        params=default_arguments,
596        ctx=ctx,
597        markup_mode=markup_mode,
598        console=console,
599    )
600    for panel_name, arguments in panel_to_arguments.items():
601        if panel_name == ARGUMENTS_PANEL_TITLE:
602            # Already printed above
603            continue
604        _print_options_panel(
605            name=panel_name,
606            params=arguments,
607            ctx=ctx,
608            markup_mode=markup_mode,
609            console=console,
610        )
611    default_options = panel_to_options.get(OPTIONS_PANEL_TITLE, [])
612    _print_options_panel(
613        name=OPTIONS_PANEL_TITLE,
614        params=default_options,
615        ctx=ctx,
616        markup_mode=markup_mode,
617        console=console,
618    )
619    for panel_name, options in panel_to_options.items():
620        if panel_name == OPTIONS_PANEL_TITLE:
621            # Already printed above
622            continue
623        _print_options_panel(
624            name=panel_name,
625            params=options,
626            ctx=ctx,
627            markup_mode=markup_mode,
628            console=console,
629        )
630
631    if isinstance(obj, click.Group):
632        panel_to_commands: DefaultDict[str, List[click.Command]] = defaultdict(list)
633        for command_name in obj.list_commands(ctx):
634            command = obj.get_command(ctx, command_name)
635            if command and not command.hidden:
636                panel_name = (
637                    getattr(command, _RICH_HELP_PANEL_NAME, None)
638                    or COMMANDS_PANEL_TITLE
639                )
640                panel_to_commands[panel_name].append(command)
641
642        # Identify the longest command name in all panels
643        max_cmd_len = max(
644            [
645                len(command.name or "")
646                for commands in panel_to_commands.values()
647                for command in commands
648            ],
649            default=0,
650        )
651
652        # Print each command group panel
653        default_commands = panel_to_commands.get(COMMANDS_PANEL_TITLE, [])
654        _print_commands_panel(
655            name=COMMANDS_PANEL_TITLE,
656            commands=default_commands,
657            markup_mode=markup_mode,
658            console=console,
659            cmd_len=max_cmd_len,
660        )
661        for panel_name, commands in panel_to_commands.items():
662            if panel_name == COMMANDS_PANEL_TITLE:
663                # Already printed above
664                continue
665            _print_commands_panel(
666                name=panel_name,
667                commands=commands,
668                markup_mode=markup_mode,
669                console=console,
670                cmd_len=max_cmd_len,
671            )
672
673    # Epilogue if we have it
674    if obj.epilog:
675        # Remove single linebreaks, replace double with single
676        lines = obj.epilog.split("\n\n")
677        epilogue = "\n".join([x.replace("\n", " ").strip() for x in lines])
678        epilogue_text = _make_rich_rext(text=epilogue, markup_mode=markup_mode)
679        console.print(Padding(Align(epilogue_text, pad=False), 1))

Print nicely formatted help text using rich.

Based on original code from rich-cli, by @willmcgugan. https://github.com/Textualize/rich-cli/blob/8a2767c7a340715fc6fbf4930ace717b9b2fc5e5/src/rich_cli/__main__.py#L162-L236

Replacement for the click function format_help(). Takes a command or group and builds the help text output.

def rich_format_error(self: click.exceptions.ClickException) -> None:
682def rich_format_error(self: click.ClickException) -> None:
683    """Print richly formatted click errors.
684
685    Called by custom exception handler to print richly formatted click errors.
686    Mimics original click.ClickException.echo() function but with rich formatting.
687    """
688    console = _get_rich_console(stderr=True)
689    ctx: Union[click.Context, None] = getattr(self, "ctx", None)
690    if ctx is not None:
691        console.print(ctx.get_usage())
692
693    if ctx is not None and ctx.command.get_help_option(ctx) is not None:
694        console.print(
695            f"Try [blue]'{ctx.command_path} {ctx.help_option_names[0]}'[/] for help.",
696            style=STYLE_ERRORS_SUGGESTION,
697        )
698
699    console.print(
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    )

Print richly formatted click errors.

Called by custom exception handler to print richly formatted click errors. Mimics original click.ClickException.echo() function but with rich formatting.

def rich_abort_error() -> None:
709def rich_abort_error() -> None:
710    """Print richly formatted abort error."""
711    console = _get_rich_console(stderr=True)
712    console.print(ABORTED_TEXT, style=STYLE_ABORTED)

Print richly formatted abort error.