typer.rich_utils

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

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):
118class NegativeOptionHighlighter(RegexHighlighter):
119    highlights = [
120        r"(^|\W)(?P<negative_switch>\-\w+)(?![a-zA-Z0-9])",
121        r"(^|\W)(?P<negative_option>\-\-[\w\-]+)(?![a-zA-Z0-9])",
122    ]

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:
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_text(text=epilogue, markup_mode=markup_mode)
678        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:
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            RICH_HELP.format(
695                command_path=ctx.command_path, help_option=ctx.help_option_names[0]
696            ),
697            style=STYLE_ERRORS_SUGGESTION,
698        )
699
700    console.print(
701        Panel(
702            highlighter(self.format_message()),
703            border_style=STYLE_ERRORS_PANEL_BORDER,
704            title=ERRORS_PANEL_TITLE,
705            title_align=ALIGN_ERRORS_PANEL,
706        )
707    )

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:
710def rich_abort_error() -> None:
711    """Print richly formatted abort error."""
712    console = _get_rich_console(stderr=True)
713    console.print(ABORTED_TEXT, style=STYLE_ABORTED)

Print richly formatted abort error.

def rich_to_html(input_text: str) -> str:
722def rich_to_html(input_text: str) -> str:
723    """Print the HTML version of a rich-formatted input string.
724
725    This function does not provide a full HTML page, but can be used to insert
726    HTML-formatted text spans into a markdown file.
727    """
728    console = Console(record=True, highlight=False, file=io.StringIO())
729
730    console.print(input_text, overflow="ignore", crop=False)
731
732    return console.export_html(inline_styles=True, code_format="{code}").strip()

Print the HTML version of a rich-formatted input string.

This function does not provide a full HTML page, but can be used to insert HTML-formatted text spans into a markdown file.