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