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.