Coverage for typer/rich_utils.py: 100%
292 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-14 00:18 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-14 00:18 +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 # Add a newline inbetween the header and the remaining paragraphs
204 yield Text("") 1iaebfgcdh
206 # Get remaining lines, remove single line breaks and format as dim
207 remaining_paragraphs = help_text.split("\n\n")[1:] 1iaebfgcdh
208 if remaining_paragraphs: 1iaebfgcdh
209 if markup_mode != MARKUP_MODE_RICH: 1iaebfgcdh
210 # Remove single linebreaks
211 remaining_paragraphs = [ 1aebfgcdh
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) 1iaebfgcdh
219 else:
220 # Join with double linebreaks if markdown
221 remaining_lines = "\n\n".join(remaining_paragraphs) 1iaebfgcdh
223 yield _make_rich_text( 1iaebfgcdh
224 text=remaining_lines,
225 style=STYLE_HELPTEXT,
226 markup_mode=markup_mode,
227 )
230def _get_parameter_help( 1aebfgcdh
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.
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 1iaebfgcdh
246 items: List[Union[Text, Markdown]] = [] 1iaebfgcdh
248 # Get the environment variable first
250 envvar = getattr(param, "envvar", None) 1iaebfgcdh
251 var_str = "" 1iaebfgcdh
252 # https://github.com/pallets/click/blob/0aec1168ac591e159baf6f61026d6ae322c53aaf/src/click/core.py#L2720-L2726
253 if envvar is None: 1iaebfgcdh
254 if ( 1abcd
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()}" 1iaebfgcdh
260 if envvar is not None: 1iaebfgcdh
261 var_str = ( 1aebfgcdh
262 envvar if isinstance(envvar, str) else ", ".join(str(d) for d in envvar)
263 )
265 # Main help text
266 help_value: Union[str, None] = getattr(param, "help", None) 1iaebfgcdh
267 if help_value: 1iaebfgcdh
268 paragraphs = help_value.split("\n\n") 1iaebfgcdh
269 # Remove single linebreaks
270 if markup_mode != MARKUP_MODE_MARKDOWN: 1iaebfgcdh
271 paragraphs = [ 1aebfgcdh
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( 1iaebfgcdh
278 _make_rich_text(
279 text="\n".join(paragraphs).strip(),
280 style=STYLE_OPTION_HELP,
281 markup_mode=markup_mode,
282 )
283 )
285 # Environment variable AFTER help text
286 if envvar and getattr(param, "show_envvar", None): 1iaebfgcdh
287 items.append(Text(ENVVAR_STRING.format(var_str), style=STYLE_OPTION_ENVVAR)) 1iaebfgcdh
289 # Default value
290 # This uses Typer's specific param._get_default_string
291 if isinstance(param, (TyperOption, TyperArgument)): 1iaebfgcdh
292 if param.show_default: 1iaebfgcdh
293 show_default_is_str = isinstance(param.show_default, str) 1iaebfgcdh
294 default_value = param._extract_default_help_str(ctx=ctx) 1iaebfgcdh
295 default_str = param._get_default_string( 1iaebfgcdh
296 ctx=ctx,
297 show_default_is_str=show_default_is_str,
298 default_value=default_value,
299 )
300 if default_str: 1iaebfgcdh
301 items.append( 1iaebfgcdh
302 Text(
303 DEFAULT_STRING.format(default_str),
304 style=STYLE_OPTION_DEFAULT,
305 )
306 )
308 # Required?
309 if param.required: 1iaebfgcdh
310 items.append(Text(REQUIRED_LONG_STRING, style=STYLE_REQUIRED_LONG)) 1iaebfgcdh
312 # Use Columns - this allows us to group different renderable types
313 # (Text, Markdown) onto a single line.
314 return Columns(items) 1iaebfgcdh
317def _make_command_help( 1aebfgcdh
318 *,
319 help_text: str,
320 markup_mode: MarkupMode,
321) -> Union[Text, Markdown]:
322 """Build cli help text for a click group command.
324 That is, when calling help on groups with multiple subcommands
325 (not the main help text when calling the subcommand help).
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") 1iaebfgcdh
332 # Remove single linebreaks
333 if markup_mode != MARKUP_MODE_RICH and not paragraphs[0].startswith("\b"): 1iaebfgcdh
334 paragraphs[0] = paragraphs[0].replace("\n", " ") 1iaebfgcdh
335 elif paragraphs[0].startswith("\b"): 1iaebfgcdh
336 paragraphs[0] = paragraphs[0].replace("\b\n", "") 1iaebfgcdh
337 return _make_rich_text( 1iaebfgcdh
338 text=paragraphs[0].strip(),
339 style=STYLE_OPTION_HELP,
340 markup_mode=markup_mode,
341 )
344def _print_options_panel( 1aebfgcdh
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]] = [] 1iaebfgcdh
353 required_rows: List[Union[str, Text]] = [] 1iaebfgcdh
354 for param in params: 1iaebfgcdh
355 # Short and long form
356 opt_long_strs = [] 1iaebfgcdh
357 opt_short_strs = [] 1iaebfgcdh
358 secondary_opt_long_strs = [] 1iaebfgcdh
359 secondary_opt_short_strs = [] 1iaebfgcdh
360 for opt_str in param.opts: 1iaebfgcdh
361 if "--" in opt_str: 1iaebfgcdh
362 opt_long_strs.append(opt_str) 1iaebfgcdh
363 else:
364 opt_short_strs.append(opt_str) 1iaebfgcdh
365 for opt_str in param.secondary_opts: 1iaebfgcdh
366 if "--" in opt_str: 1iaebfgcdh
367 secondary_opt_long_strs.append(opt_str) 1iaebfgcdh
368 else:
369 secondary_opt_short_strs.append(opt_str) 1iaebfgcdh
371 # Column for a metavar, if we have one
372 metavar = Text(style=STYLE_METAVAR, overflow="fold") 1iaebfgcdh
373 metavar_str = param.make_metavar() 1iaebfgcdh
375 # Do it ourselves if this is a positional argument
376 if ( 1abcd
377 isinstance(param, click.Argument)
378 and param.name
379 and metavar_str == param.name.upper()
380 ):
381 metavar_str = param.type.name.upper() 1iaebfgcdh
383 # Skip booleans and choices (handled above)
384 if metavar_str != "BOOLEAN": 1iaebfgcdh
385 metavar.append(metavar_str) 1iaebfgcdh
387 # Range - from
388 # https://github.com/pallets/click/blob/c63c70dabd3f86ca68678b4f00951f78f52d0270/src/click/core.py#L2698-L2706 # noqa: E501
389 # skip count with default range type
390 if ( 1abcd
391 isinstance(param.type, click.types._NumberRangeBase)
392 and isinstance(param, click.Option)
393 and not (param.count and param.type.min == 0 and param.type.max is None)
394 ):
395 range_str = param.type._describe_range() 1iaebfgcdh
396 if range_str: 1iaebfgcdh
397 metavar.append(RANGE_STRING.format(range_str)) 1iaebfgcdh
399 # Required asterisk
400 required: Union[str, Text] = "" 1iaebfgcdh
401 if param.required: 1iaebfgcdh
402 required = Text(REQUIRED_SHORT_STRING, style=STYLE_REQUIRED_SHORT) 1iaebfgcdh
404 # Highlighter to make [ | ] and <> dim
405 class MetavarHighlighter(RegexHighlighter): 1iaebfgcdh
406 highlights = [ 1aebfgcdh
407 r"^(?P<metavar_sep>(\[|<))",
408 r"(?P<metavar_sep>\|)",
409 r"(?P<metavar_sep>(\]|>)$)",
410 ]
412 metavar_highlighter = MetavarHighlighter() 1iaebfgcdh
414 required_rows.append(required) 1iaebfgcdh
415 options_rows.append( 1iaebfgcdh
416 [
417 highlighter(",".join(opt_long_strs)),
418 highlighter(",".join(opt_short_strs)),
419 negative_highlighter(",".join(secondary_opt_long_strs)),
420 negative_highlighter(",".join(secondary_opt_short_strs)),
421 metavar_highlighter(metavar),
422 _get_parameter_help(
423 param=param,
424 ctx=ctx,
425 markup_mode=markup_mode,
426 ),
427 ]
428 )
429 rows_with_required: List[List[RenderableType]] = [] 1iaebfgcdh
430 if any(required_rows): 1iaebfgcdh
431 for required, row in zip(required_rows, options_rows): 1iaebfgcdh
432 rows_with_required.append([required, *row]) 1iaebfgcdh
433 else:
434 rows_with_required = options_rows 1iaebfgcdh
435 if options_rows: 1iaebfgcdh
436 t_styles: Dict[str, Any] = { 1aebfgcdh
437 "show_lines": STYLE_OPTIONS_TABLE_SHOW_LINES,
438 "leading": STYLE_OPTIONS_TABLE_LEADING,
439 "box": STYLE_OPTIONS_TABLE_BOX,
440 "border_style": STYLE_OPTIONS_TABLE_BORDER_STYLE,
441 "row_styles": STYLE_OPTIONS_TABLE_ROW_STYLES,
442 "pad_edge": STYLE_OPTIONS_TABLE_PAD_EDGE,
443 "padding": STYLE_OPTIONS_TABLE_PADDING,
444 }
445 box_style = getattr(box, t_styles.pop("box"), None) 1iaebfgcdh
447 options_table = Table( 1iaebfgcdh
448 highlight=True,
449 show_header=False,
450 expand=True,
451 box=box_style,
452 **t_styles,
453 )
454 for row in rows_with_required: 1iaebfgcdh
455 options_table.add_row(*row) 1iaebfgcdh
456 console.print( 1iaebfgcdh
457 Panel(
458 options_table,
459 border_style=STYLE_OPTIONS_PANEL_BORDER,
460 title=name,
461 title_align=ALIGN_OPTIONS_PANEL,
462 )
463 )
466def _print_commands_panel( 1aebfgcdh
467 *,
468 name: str,
469 commands: List[click.Command],
470 markup_mode: MarkupMode,
471 console: Console,
472 cmd_len: int,
473) -> None:
474 t_styles: Dict[str, Any] = { 1aebfgcdh
475 "show_lines": STYLE_COMMANDS_TABLE_SHOW_LINES,
476 "leading": STYLE_COMMANDS_TABLE_LEADING,
477 "box": STYLE_COMMANDS_TABLE_BOX,
478 "border_style": STYLE_COMMANDS_TABLE_BORDER_STYLE,
479 "row_styles": STYLE_COMMANDS_TABLE_ROW_STYLES,
480 "pad_edge": STYLE_COMMANDS_TABLE_PAD_EDGE,
481 "padding": STYLE_COMMANDS_TABLE_PADDING,
482 }
483 box_style = getattr(box, t_styles.pop("box"), None) 1iaebfgcdh
485 commands_table = Table( 1iaebfgcdh
486 highlight=False,
487 show_header=False,
488 expand=True,
489 box=box_style,
490 **t_styles,
491 )
492 # Define formatting in first column, as commands don't match highlighter
493 # regex
494 commands_table.add_column( 1iaebfgcdh
495 style=STYLE_COMMANDS_TABLE_FIRST_COLUMN,
496 no_wrap=True,
497 width=cmd_len,
498 )
500 # A big ratio makes the description column be greedy and take all the space
501 # available instead of allowing the command column to grow and misalign with
502 # other panels.
503 commands_table.add_column("Description", justify="left", no_wrap=False, ratio=10) 1iaebfgcdh
504 rows: List[List[Union[RenderableType, None]]] = [] 1iaebfgcdh
505 deprecated_rows: List[Union[RenderableType, None]] = [] 1iaebfgcdh
506 for command in commands: 1iaebfgcdh
507 helptext = command.short_help or command.help or "" 1iaebfgcdh
508 command_name = command.name or "" 1iaebfgcdh
509 if command.deprecated: 1iaebfgcdh
510 command_name_text = Text(f"{command_name}", style=STYLE_DEPRECATED_COMMAND) 1iaebfgcdh
511 deprecated_rows.append(Text(DEPRECATED_STRING, style=STYLE_DEPRECATED)) 1iaebfgcdh
512 else:
513 command_name_text = Text(command_name) 1iaebfgcdh
514 deprecated_rows.append(None) 1iaebfgcdh
515 rows.append( 1iaebfgcdh
516 [
517 command_name_text,
518 _make_command_help(
519 help_text=helptext,
520 markup_mode=markup_mode,
521 ),
522 ]
523 )
524 rows_with_deprecated = rows 1iaebfgcdh
525 if any(deprecated_rows): 1iaebfgcdh
526 rows_with_deprecated = [] 1iaebfgcdh
527 for row, deprecated_text in zip(rows, deprecated_rows): 1iaebfgcdh
528 rows_with_deprecated.append([*row, deprecated_text]) 1iaebfgcdh
529 for row in rows_with_deprecated: 1iaebfgcdh
530 commands_table.add_row(*row) 1iaebfgcdh
531 if commands_table.row_count: 1iaebfgcdh
532 console.print( 1iaebfgcdh
533 Panel(
534 commands_table,
535 border_style=STYLE_COMMANDS_PANEL_BORDER,
536 title=name,
537 title_align=ALIGN_COMMANDS_PANEL,
538 )
539 )
542def rich_format_help( 1aebfgcdh
543 *,
544 obj: Union[click.Command, click.Group],
545 ctx: click.Context,
546 markup_mode: MarkupMode,
547) -> None:
548 """Print nicely formatted help text using rich.
550 Based on original code from rich-cli, by @willmcgugan.
551 https://github.com/Textualize/rich-cli/blob/8a2767c7a340715fc6fbf4930ace717b9b2fc5e5/src/rich_cli/__main__.py#L162-L236
553 Replacement for the click function format_help().
554 Takes a command or group and builds the help text output.
555 """
556 console = _get_rich_console() 1iaebfgcdh
558 # Print usage
559 console.print( 1iaebfgcdh
560 Padding(highlighter(obj.get_usage(ctx)), 1), style=STYLE_USAGE_COMMAND
561 )
563 # Print command / group help if we have some
564 if obj.help: 1iaebfgcdh
565 # Print with some padding
566 console.print( 1iaebfgcdh
567 Padding(
568 Align(
569 _get_help_text(
570 obj=obj,
571 markup_mode=markup_mode,
572 ),
573 pad=False,
574 ),
575 (0, 1, 1, 1),
576 )
577 )
578 panel_to_arguments: DefaultDict[str, List[click.Argument]] = defaultdict(list) 1iaebfgcdh
579 panel_to_options: DefaultDict[str, List[click.Option]] = defaultdict(list) 1iaebfgcdh
580 for param in obj.get_params(ctx): 1iaebfgcdh
581 # Skip if option is hidden
582 if getattr(param, "hidden", False): 1iaebfgcdh
583 continue 1iaebfgcdh
584 if isinstance(param, click.Argument): 1iaebfgcdh
585 panel_name = ( 1aebfgcdh
586 getattr(param, _RICH_HELP_PANEL_NAME, None) or ARGUMENTS_PANEL_TITLE
587 )
588 panel_to_arguments[panel_name].append(param) 1iaebfgcdh
589 elif isinstance(param, click.Option): 1iaebfgcdh
590 panel_name = ( 1aebfgcdh
591 getattr(param, _RICH_HELP_PANEL_NAME, None) or OPTIONS_PANEL_TITLE
592 )
593 panel_to_options[panel_name].append(param) 1iaebfgcdh
594 default_arguments = panel_to_arguments.get(ARGUMENTS_PANEL_TITLE, []) 1iaebfgcdh
595 _print_options_panel( 1iaebfgcdh
596 name=ARGUMENTS_PANEL_TITLE,
597 params=default_arguments,
598 ctx=ctx,
599 markup_mode=markup_mode,
600 console=console,
601 )
602 for panel_name, arguments in panel_to_arguments.items(): 1iaebfgcdh
603 if panel_name == ARGUMENTS_PANEL_TITLE: 1iaebfgcdh
604 # Already printed above
605 continue 1iaebfgcdh
606 _print_options_panel( 1iaebfgcdh
607 name=panel_name,
608 params=arguments,
609 ctx=ctx,
610 markup_mode=markup_mode,
611 console=console,
612 )
613 default_options = panel_to_options.get(OPTIONS_PANEL_TITLE, []) 1iaebfgcdh
614 _print_options_panel( 1iaebfgcdh
615 name=OPTIONS_PANEL_TITLE,
616 params=default_options,
617 ctx=ctx,
618 markup_mode=markup_mode,
619 console=console,
620 )
621 for panel_name, options in panel_to_options.items(): 1iaebfgcdh
622 if panel_name == OPTIONS_PANEL_TITLE: 1iaebfgcdh
623 # Already printed above
624 continue 1iaebfgcdh
625 _print_options_panel( 1iaebfgcdh
626 name=panel_name,
627 params=options,
628 ctx=ctx,
629 markup_mode=markup_mode,
630 console=console,
631 )
633 if isinstance(obj, click.Group): 1iaebfgcdh
634 panel_to_commands: DefaultDict[str, List[click.Command]] = defaultdict(list) 1iaebfgcdh
635 for command_name in obj.list_commands(ctx): 1iaebfgcdh
636 command = obj.get_command(ctx, command_name) 1iaebfgcdh
637 if command and not command.hidden: 1iaebfgcdh
638 panel_name = ( 1aebfgcdh
639 getattr(command, _RICH_HELP_PANEL_NAME, None)
640 or COMMANDS_PANEL_TITLE
641 )
642 panel_to_commands[panel_name].append(command) 1iaebfgcdh
644 # Identify the longest command name in all panels
645 max_cmd_len = max( 1iaebfgcdh
646 [
647 len(command.name or "")
648 for commands in panel_to_commands.values()
649 for command in commands
650 ],
651 default=0,
652 )
654 # Print each command group panel
655 default_commands = panel_to_commands.get(COMMANDS_PANEL_TITLE, []) 1iaebfgcdh
656 _print_commands_panel( 1iaebfgcdh
657 name=COMMANDS_PANEL_TITLE,
658 commands=default_commands,
659 markup_mode=markup_mode,
660 console=console,
661 cmd_len=max_cmd_len,
662 )
663 for panel_name, commands in panel_to_commands.items(): 1iaebfgcdh
664 if panel_name == COMMANDS_PANEL_TITLE: 1iaebfgcdh
665 # Already printed above
666 continue 1iaebfgcdh
667 _print_commands_panel( 1iaebfgcdh
668 name=panel_name,
669 commands=commands,
670 markup_mode=markup_mode,
671 console=console,
672 cmd_len=max_cmd_len,
673 )
675 # Epilogue if we have it
676 if obj.epilog: 1iaebfgcdh
677 # Remove single linebreaks, replace double with single
678 lines = obj.epilog.split("\n\n") 1iaebfgcdh
679 epilogue = "\n".join([x.replace("\n", " ").strip() for x in lines]) 1iaebfgcdh
680 epilogue_text = _make_rich_text(text=epilogue, markup_mode=markup_mode) 1iaebfgcdh
681 console.print(Padding(Align(epilogue_text, pad=False), 1)) 1iaebfgcdh
684def rich_format_error(self: click.ClickException) -> None: 1iaebfgcdh
685 """Print richly formatted click errors.
687 Called by custom exception handler to print richly formatted click errors.
688 Mimics original click.ClickException.echo() function but with rich formatting.
689 """
690 console = _get_rich_console(stderr=True) 1iaebfgcdh
691 ctx: Union[click.Context, None] = getattr(self, "ctx", None) 1iaebfgcdh
692 if ctx is not None: 1iaebfgcdh
693 console.print(ctx.get_usage()) 1iaebfgcdh
695 if ctx is not None and ctx.command.get_help_option(ctx) is not None: 1iaebfgcdh
696 console.print( 1iaebfgcdh
697 RICH_HELP.format(
698 command_path=ctx.command_path, help_option=ctx.help_option_names[0]
699 ),
700 style=STYLE_ERRORS_SUGGESTION,
701 )
703 console.print( 1iaebfgcdh
704 Panel(
705 highlighter(self.format_message()),
706 border_style=STYLE_ERRORS_PANEL_BORDER,
707 title=ERRORS_PANEL_TITLE,
708 title_align=ALIGN_ERRORS_PANEL,
709 )
710 )
713def rich_abort_error() -> None: 1iaebfgcdh
714 """Print richly formatted abort error."""
715 console = _get_rich_console(stderr=True) 1iaebfgcdh
716 console.print(ABORTED_TEXT, style=STYLE_ABORTED) 1iaebfgcdh
719def rich_to_html(input_text: str) -> str: 1iaebfgcdh
720 """Print the HTML version of a rich-formatted input string.
722 This function does not provide a full HTML page, but can be used to insert
723 HTML-formatted text spans into a markdown file.
724 """
725 console = Console(record=True, highlight=False, file=io.StringIO()) 1iaebfgcdh
727 console.print(input_text, overflow="ignore", crop=False) 1iaebfgcdh
729 return console.export_html(inline_styles=True, code_format="{code}").strip() 1iaebfgcdh
732def rich_render_text(text: str) -> str: 1iaebfgcdh
733 """Remove rich tags and render a pure text representation"""
734 console = _get_rich_console() 1iaebfgcdh
735 return "".join(segment.text for segment in console.render(text)).rstrip("\n") 1iaebfgcdh