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