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()
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.
Inherited Members
- rich.highlighter.RegexHighlighter
- base_style
- highlight
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.
Inherited Members
- rich.highlighter.RegexHighlighter
- base_style
- highlight
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.
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.
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.
716def print_with_rich(text: str) -> None: 717 """Print richly formatted message.""" 718 console = _get_rich_console() 719 console.print(text)
Print richly formatted message.
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.