Coverage for typer/rich_utils.py: 100%
300 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-12-19 20:29 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2025-12-19 20:29 +0000
1# Extracted and modified from https://github.com/ewels/rich-click
3import inspect 1aefbghcdi
4import io 1aefbghcdi
5import sys 1aefbghcdi
6from collections import defaultdict 1aefbghcdi
7from gettext import gettext as _ 1aefbghcdi
8from os import getenv 1aefbghcdi
9from typing import Any, DefaultDict, Dict, Iterable, List, Optional, Union 1aefbghcdi
11import click 1aefbghcdi
12from rich import box 1aefbghcdi
13from rich.align import Align 1aefbghcdi
14from rich.columns import Columns 1aefbghcdi
15from rich.console import Console, RenderableType, group 1aefbghcdi
16from rich.emoji import Emoji 1aefbghcdi
17from rich.highlighter import RegexHighlighter 1aefbghcdi
18from rich.markdown import Markdown 1aefbghcdi
19from rich.markup import escape 1aefbghcdi
20from rich.padding import Padding 1aefbghcdi
21from rich.panel import Panel 1aefbghcdi
22from rich.table import Table 1aefbghcdi
23from rich.text import Text 1aefbghcdi
24from rich.theme import Theme 1aefbghcdi
25from rich.traceback import Traceback 1aefbghcdi
26from typer.models import DeveloperExceptionConfig 1aefbghcdi
28if sys.version_info >= (3, 9): 1aefbghcdi
29 from typing import Literal 1aefbghdi
30else:
31 from typing_extensions import Literal 1c
33# Default styles
34STYLE_OPTION = "bold cyan" 1aefbghcdi
35STYLE_SWITCH = "bold green" 1aefbghcdi
36STYLE_NEGATIVE_OPTION = "bold magenta" 1aefbghcdi
37STYLE_NEGATIVE_SWITCH = "bold red" 1aefbghcdi
38STYLE_METAVAR = "bold yellow" 1aefbghcdi
39STYLE_METAVAR_SEPARATOR = "dim" 1aefbghcdi
40STYLE_USAGE = "yellow" 1aefbghcdi
41STYLE_USAGE_COMMAND = "bold" 1aefbghcdi
42STYLE_DEPRECATED = "red" 1aefbghcdi
43STYLE_DEPRECATED_COMMAND = "dim" 1aefbghcdi
44STYLE_HELPTEXT_FIRST_LINE = "" 1aefbghcdi
45STYLE_HELPTEXT = "dim" 1aefbghcdi
46STYLE_OPTION_HELP = "" 1aefbghcdi
47STYLE_OPTION_DEFAULT = "dim" 1aefbghcdi
48STYLE_OPTION_ENVVAR = "dim yellow" 1aefbghcdi
49STYLE_REQUIRED_SHORT = "red" 1aefbghcdi
50STYLE_REQUIRED_LONG = "dim red" 1aefbghcdi
51STYLE_OPTIONS_PANEL_BORDER = "dim" 1aefbghcdi
52ALIGN_OPTIONS_PANEL: Literal["left", "center", "right"] = "left" 1aefbghcdi
53STYLE_OPTIONS_TABLE_SHOW_LINES = False 1aefbghcdi
54STYLE_OPTIONS_TABLE_LEADING = 0 1aefbghcdi
55STYLE_OPTIONS_TABLE_PAD_EDGE = False 1aefbghcdi
56STYLE_OPTIONS_TABLE_PADDING = (0, 1) 1aefbghcdi
57STYLE_OPTIONS_TABLE_BOX = "" 1aefbghcdi
58STYLE_OPTIONS_TABLE_ROW_STYLES = None 1aefbghcdi
59STYLE_OPTIONS_TABLE_BORDER_STYLE = None 1aefbghcdi
60STYLE_COMMANDS_PANEL_BORDER = "dim" 1aefbghcdi
61ALIGN_COMMANDS_PANEL: Literal["left", "center", "right"] = "left" 1aefbghcdi
62STYLE_COMMANDS_TABLE_SHOW_LINES = False 1aefbghcdi
63STYLE_COMMANDS_TABLE_LEADING = 0 1aefbghcdi
64STYLE_COMMANDS_TABLE_PAD_EDGE = False 1aefbghcdi
65STYLE_COMMANDS_TABLE_PADDING = (0, 1) 1aefbghcdi
66STYLE_COMMANDS_TABLE_BOX = "" 1aefbghcdi
67STYLE_COMMANDS_TABLE_ROW_STYLES = None 1aefbghcdi
68STYLE_COMMANDS_TABLE_BORDER_STYLE = None 1aefbghcdi
69STYLE_COMMANDS_TABLE_FIRST_COLUMN = "bold cyan" 1aefbghcdi
70STYLE_ERRORS_PANEL_BORDER = "red" 1aefbghcdi
71ALIGN_ERRORS_PANEL: Literal["left", "center", "right"] = "left" 1aefbghcdi
72STYLE_ERRORS_SUGGESTION = "dim" 1aefbghcdi
73STYLE_ABORTED = "red" 1aefbghcdi
74_TERMINAL_WIDTH = getenv("TERMINAL_WIDTH") 1aefbghcdi
75MAX_WIDTH = int(_TERMINAL_WIDTH) if _TERMINAL_WIDTH else None 1aefbghcdi
76COLOR_SYSTEM: Optional[Literal["auto", "standard", "256", "truecolor", "windows"]] = ( 1aefbghcdi
77 "auto" # Set to None to disable colors
78)
79_TYPER_FORCE_DISABLE_TERMINAL = getenv("_TYPER_FORCE_DISABLE_TERMINAL") 1aefbghcdi
80FORCE_TERMINAL = ( 1aefbghcdi
81 True
82 if getenv("GITHUB_ACTIONS") or getenv("FORCE_COLOR") or getenv("PY_COLORS")
83 else None
84)
85if _TYPER_FORCE_DISABLE_TERMINAL: 1aefbghcdi
86 FORCE_TERMINAL = False 1aefbghcdi
88# Fixed strings
89DEPRECATED_STRING = _("(deprecated) ") 1aefbghcdi
90DEFAULT_STRING = _("[default: {}]") 1aefbghcdi
91ENVVAR_STRING = _("[env var: {}]") 1aefbghcdi
92REQUIRED_SHORT_STRING = "*" 1aefbghcdi
93REQUIRED_LONG_STRING = _("[required]") 1aefbghcdi
94RANGE_STRING = " [{}]" 1aefbghcdi
95ARGUMENTS_PANEL_TITLE = _("Arguments") 1aefbghcdi
96OPTIONS_PANEL_TITLE = _("Options") 1aefbghcdi
97COMMANDS_PANEL_TITLE = _("Commands") 1aefbghcdi
98ERRORS_PANEL_TITLE = _("Error") 1aefbghcdi
99ABORTED_TEXT = _("Aborted.") 1aefbghcdi
100RICH_HELP = _("Try [blue]'{command_path} {help_option}'[/] for help.") 1aefbghcdi
102MARKUP_MODE_MARKDOWN = "markdown" 1aefbghcdi
103MARKUP_MODE_RICH = "rich" 1aefbghcdi
104_RICH_HELP_PANEL_NAME = "rich_help_panel" 1aefbghcdi
106MarkupModeStrict = Literal["markdown", "rich"] 1aefbghcdi
109# Rich regex highlighter
110class OptionHighlighter(RegexHighlighter): 1aefbghcdi
111 """Highlights our special options."""
113 highlights = [ 1aefbghcdi
114 r"(^|\W)(?P<switch>\-\w+)(?![a-zA-Z0-9])",
115 r"(^|\W)(?P<option>\-\-[\w\-]+)(?![a-zA-Z0-9])",
116 r"(?P<metavar>\<[^\>]+\>)",
117 r"(?P<usage>Usage: )",
118 ]
121class NegativeOptionHighlighter(RegexHighlighter): 1aefbghcdi
122 highlights = [ 1aefbghcdi
123 r"(^|\W)(?P<negative_switch>\-\w+)(?![a-zA-Z0-9])",
124 r"(^|\W)(?P<negative_option>\-\-[\w\-]+)(?![a-zA-Z0-9])",
125 ]
128highlighter = OptionHighlighter() 1aefbghcdi
129negative_highlighter = NegativeOptionHighlighter() 1aefbghcdi
132def _get_rich_console(stderr: bool = False) -> Console: 1aefbghcdi
133 return Console( 1aefbghcdi
134 theme=Theme(
135 {
136 "option": STYLE_OPTION,
137 "switch": STYLE_SWITCH,
138 "negative_option": STYLE_NEGATIVE_OPTION,
139 "negative_switch": STYLE_NEGATIVE_SWITCH,
140 "metavar": STYLE_METAVAR,
141 "metavar_sep": STYLE_METAVAR_SEPARATOR,
142 "usage": STYLE_USAGE,
143 },
144 ),
145 highlighter=highlighter,
146 color_system=COLOR_SYSTEM,
147 force_terminal=FORCE_TERMINAL,
148 width=MAX_WIDTH,
149 stderr=stderr,
150 )
153def _make_rich_text( 1aefbghcdi
154 *, text: str, style: str = "", markup_mode: MarkupModeStrict
155) -> Union[Markdown, Text]:
156 """Take a string, remove indentations, and return styled text.
158 If `markup_mode` is `"rich"`, the text is parsed for Rich markup strings.
159 If `markup_mode` is `"markdown"`, parse as Markdown.
160 """
161 # Remove indentations from input text
162 text = inspect.cleandoc(text) 1aefbghcdi
163 if markup_mode == MARKUP_MODE_MARKDOWN: 1aefbghcdi
164 text = Emoji.replace(text) 1aefbghcdi
165 return Markdown(text, style=style) 1aefbghcdi
166 else:
167 assert markup_mode == MARKUP_MODE_RICH 1aefbghcdi
168 return highlighter(Text.from_markup(text, style=style)) 1aefbghcdi
171@group() 1aefbghcdi
172def _get_help_text( 1aefbghcdi
173 *,
174 obj: Union[click.Command, click.Group],
175 markup_mode: MarkupModeStrict,
176) -> Iterable[Union[Markdown, Text]]:
177 """Build primary help text for a click command or group.
179 Returns the prose help text for a command or group, rendered either as a
180 Rich Text object or as Markdown.
181 If the command is marked as deprecated, the deprecated string will be prepended.
182 """
183 # Prepend deprecated status
184 if obj.deprecated: 1aefbghcdi
185 yield Text(DEPRECATED_STRING, style=STYLE_DEPRECATED) 1aefbghcdi
187 # Fetch and dedent the help text
188 help_text = inspect.cleandoc(obj.help or "") 1aefbghcdi
190 # Trim off anything that comes after \f on its own line
191 help_text = help_text.partition("\f")[0] 1aefbghcdi
193 # Get the first paragraph
194 first_line, *remaining_paragraphs = help_text.split("\n\n") 1aefbghcdi
196 # Remove single linebreaks
197 if markup_mode != MARKUP_MODE_MARKDOWN and not first_line.startswith("\b"): 1aefbghcdi
198 first_line = first_line.replace("\n", " ") 1aefbghcdi
199 yield _make_rich_text( 1aefbghcdi
200 text=first_line.strip(),
201 style=STYLE_HELPTEXT_FIRST_LINE,
202 markup_mode=markup_mode,
203 )
205 # Get remaining lines, remove single line breaks and format as dim
206 if remaining_paragraphs: 1aefbghcdi
207 # Add a newline inbetween the header and the remaining paragraphs
208 yield Text("") 1aefbghcdi
209 # Join with double linebreaks for markdown and Rich markup
210 remaining_lines = "\n\n".join(remaining_paragraphs) 1aefbghcdi
212 yield _make_rich_text( 1aefbghcdi
213 text=remaining_lines,
214 style=STYLE_HELPTEXT,
215 markup_mode=markup_mode,
216 )
219def _get_parameter_help( 1aefbghcdi
220 *,
221 param: Union[click.Option, click.Argument, click.Parameter],
222 ctx: click.Context,
223 markup_mode: MarkupModeStrict,
224) -> Columns:
225 """Build primary help text for a click option or argument.
227 Returns the prose help text for an option or argument, rendered either
228 as a Rich Text object or as Markdown.
229 Additional elements are appended to show the default and required status if
230 applicable.
231 """
232 # import here to avoid cyclic imports
233 from .core import TyperArgument, TyperOption 1aefbghcdi
235 items: List[Union[Text, Markdown]] = [] 1aefbghcdi
237 # Get the environment variable first
239 envvar = getattr(param, "envvar", None) 1aefbghcdi
240 var_str = "" 1aefbghcdi
241 # https://github.com/pallets/click/blob/0aec1168ac591e159baf6f61026d6ae322c53aaf/src/click/core.py#L2720-L2726
242 if envvar is None: 1aefbghcdi
243 if ( 1abcd
244 getattr(param, "allow_from_autoenv", None)
245 and getattr(ctx, "auto_envvar_prefix", None) is not None
246 and param.name is not None
247 ):
248 envvar = f"{ctx.auto_envvar_prefix}_{param.name.upper()}" 1aefbghcdi
249 if envvar is not None: 1aefbghcdi
250 var_str = ( 1aefbghcdi
251 envvar if isinstance(envvar, str) else ", ".join(str(d) for d in envvar)
252 )
254 # Main help text
255 help_value: Union[str, None] = getattr(param, "help", None) 1aefbghcdi
256 if help_value: 1aefbghcdi
257 paragraphs = help_value.split("\n\n") 1aefbghcdi
258 # Remove single linebreaks
259 if markup_mode != MARKUP_MODE_MARKDOWN: 1aefbghcdi
260 paragraphs = [ 1aefbghcdi
261 x.replace("\n", " ").strip()
262 if not x.startswith("\b")
263 else "{}\n".format(x.strip("\b\n"))
264 for x in paragraphs
265 ]
266 items.append( 1aefbghcdi
267 _make_rich_text(
268 text="\n".join(paragraphs).strip(),
269 style=STYLE_OPTION_HELP,
270 markup_mode=markup_mode,
271 )
272 )
274 # Environment variable AFTER help text
275 if envvar and getattr(param, "show_envvar", None): 1aefbghcdi
276 items.append(Text(ENVVAR_STRING.format(var_str), style=STYLE_OPTION_ENVVAR)) 1aefbghcdi
278 # Default value
279 # This uses Typer's specific param._get_default_string
280 if isinstance(param, (TyperOption, TyperArgument)): 1aefbghcdi
281 default_value = param._extract_default_help_str(ctx=ctx) 1aefbghcdi
282 show_default_is_str = isinstance(param.show_default, str) 1aefbghcdi
283 if show_default_is_str or ( 1aefbghcdi
284 default_value is not None and (param.show_default or ctx.show_default)
285 ):
286 default_str = param._get_default_string( 1aefbghcdi
287 ctx=ctx,
288 show_default_is_str=show_default_is_str,
289 default_value=default_value,
290 )
291 if default_str: 1aefbghcdi
292 items.append( 1aefbghcdi
293 Text(
294 DEFAULT_STRING.format(default_str),
295 style=STYLE_OPTION_DEFAULT,
296 )
297 )
299 # Required?
300 if param.required: 1aefbghcdi
301 items.append(Text(REQUIRED_LONG_STRING, style=STYLE_REQUIRED_LONG)) 1aefbghcdi
303 # Use Columns - this allows us to group different renderable types
304 # (Text, Markdown) onto a single line.
305 return Columns(items) 1aefbghcdi
308def _make_command_help( 1aefbghcdi
309 *,
310 help_text: str,
311 markup_mode: MarkupModeStrict,
312) -> Union[Text, Markdown]:
313 """Build cli help text for a click group command.
315 That is, when calling help on groups with multiple subcommands
316 (not the main help text when calling the subcommand help).
318 Returns the first paragraph of help text for a command, rendered either as a
319 Rich Text object or as Markdown.
320 Ignores single newlines as paragraph markers, looks for double only.
321 """
322 paragraphs = inspect.cleandoc(help_text).split("\n\n") 1aefbghcdi
323 # Remove single linebreaks
324 if markup_mode != MARKUP_MODE_RICH and not paragraphs[0].startswith("\b"): 1aefbghcdi
325 paragraphs[0] = paragraphs[0].replace("\n", " ") 1aefbghcdi
326 elif paragraphs[0].startswith("\b"): 1aefbghcdi
327 paragraphs[0] = paragraphs[0].replace("\b\n", "") 1aefbghcdi
328 return _make_rich_text( 1aefbghcdi
329 text=paragraphs[0].strip(),
330 style=STYLE_OPTION_HELP,
331 markup_mode=markup_mode,
332 )
335def _print_options_panel( 1aefbghcdi
336 *,
337 name: str,
338 params: Union[List[click.Option], List[click.Argument]],
339 ctx: click.Context,
340 markup_mode: MarkupModeStrict,
341 console: Console,
342) -> None:
343 options_rows: List[List[RenderableType]] = [] 1aefbghcdi
344 required_rows: List[Union[str, Text]] = [] 1aefbghcdi
345 for param in params: 1aefbghcdi
346 # Short and long form
347 opt_long_strs = [] 1aefbghcdi
348 opt_short_strs = [] 1aefbghcdi
349 secondary_opt_long_strs = [] 1aefbghcdi
350 secondary_opt_short_strs = [] 1aefbghcdi
351 for opt_str in param.opts: 1aefbghcdi
352 if "--" in opt_str: 1aefbghcdi
353 opt_long_strs.append(opt_str) 1aefbghcdi
354 else:
355 opt_short_strs.append(opt_str) 1aefbghcdi
356 for opt_str in param.secondary_opts: 1aefbghcdi
357 if "--" in opt_str: 1aefbghcdi
358 secondary_opt_long_strs.append(opt_str) 1aefbghcdi
359 else:
360 secondary_opt_short_strs.append(opt_str) 1aefbghcdi
362 # Column for a metavar, if we have one
363 metavar = Text(style=STYLE_METAVAR, overflow="fold") 1aefbghcdi
364 # TODO: when deprecating Click < 8.2, make ctx required
365 signature = inspect.signature(param.make_metavar) 1aefbghcdi
366 if "ctx" in signature.parameters: 1aefbghcdi
367 metavar_str = param.make_metavar(ctx=ctx) 1aefbghcdi
368 else:
369 # Click < 8.2
370 metavar_str = param.make_metavar() # type: ignore[call-arg] 1bc
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() 1aefbghcdi
380 # Skip booleans and choices (handled above)
381 if metavar_str != "BOOLEAN": 1aefbghcdi
382 metavar.append(metavar_str) 1aefbghcdi
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() 1aefbghcdi
393 if range_str: 1aefbghcdi
394 metavar.append(RANGE_STRING.format(range_str)) 1aefbghcdi
396 # Required asterisk
397 required: Union[str, Text] = "" 1aefbghcdi
398 if param.required: 1aefbghcdi
399 required = Text(REQUIRED_SHORT_STRING, style=STYLE_REQUIRED_SHORT) 1aefbghcdi
401 # Highlighter to make [ | ] and <> dim
402 class MetavarHighlighter(RegexHighlighter): 1aefbghcdi
403 highlights = [ 1aefbghcdi
404 r"^(?P<metavar_sep>(\[|<))",
405 r"(?P<metavar_sep>\|)",
406 r"(?P<metavar_sep>(\]|>)$)",
407 ]
409 metavar_highlighter = MetavarHighlighter() 1aefbghcdi
411 required_rows.append(required) 1aefbghcdi
412 options_rows.append( 1aefbghcdi
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]] = [] 1aefbghcdi
427 if any(required_rows): 1aefbghcdi
428 for required, row in zip(required_rows, options_rows): 1aefbghcdi
429 rows_with_required.append([required, *row]) 1aefbghcdi
430 else:
431 rows_with_required = options_rows 1aefbghcdi
432 if options_rows: 1aefbghcdi
433 t_styles: Dict[str, Any] = { 1aefbghcdi
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) 1aefbghcdi
444 options_table = Table( 1aefbghcdi
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: 1aefbghcdi
452 options_table.add_row(*row) 1aefbghcdi
453 console.print( 1aefbghcdi
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( 1aefbghcdi
464 *,
465 name: str,
466 commands: List[click.Command],
467 markup_mode: MarkupModeStrict,
468 console: Console,
469 cmd_len: int,
470) -> None:
471 t_styles: Dict[str, Any] = { 1aefbghcdi
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) 1aefbghcdi
482 commands_table = Table( 1aefbghcdi
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( 1aefbghcdi
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) 1aefbghcdi
501 rows: List[List[Union[RenderableType, None]]] = [] 1aefbghcdi
502 deprecated_rows: List[Union[RenderableType, None]] = [] 1aefbghcdi
503 for command in commands: 1aefbghcdi
504 helptext = command.short_help or command.help or "" 1aefbghcdi
505 command_name = command.name or "" 1aefbghcdi
506 if command.deprecated: 1aefbghcdi
507 command_name_text = Text(f"{command_name}", style=STYLE_DEPRECATED_COMMAND) 1aefbghcdi
508 deprecated_rows.append(Text(DEPRECATED_STRING, style=STYLE_DEPRECATED)) 1aefbghcdi
509 else:
510 command_name_text = Text(command_name) 1aefbghcdi
511 deprecated_rows.append(None) 1aefbghcdi
512 rows.append( 1aefbghcdi
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 1aefbghcdi
522 if any(deprecated_rows): 1aefbghcdi
523 rows_with_deprecated = [] 1aefbghcdi
524 for row, deprecated_text in zip(rows, deprecated_rows): 1aefbghcdi
525 rows_with_deprecated.append([*row, deprecated_text]) 1aefbghcdi
526 for row in rows_with_deprecated: 1aefbghcdi
527 commands_table.add_row(*row) 1aefbghcdi
528 if commands_table.row_count: 1aefbghcdi
529 console.print( 1aefbghcdi
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( 1aefbghcdi
540 *,
541 obj: Union[click.Command, click.Group],
542 ctx: click.Context,
543 markup_mode: MarkupModeStrict,
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() 1aefbghcdi
555 # Print usage
556 console.print( 1aefbghcdi
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: 1aefbghcdi
562 # Print with some padding
563 console.print( 1aefbghcdi
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) 1aefbghcdi
576 panel_to_options: DefaultDict[str, List[click.Option]] = defaultdict(list) 1aefbghcdi
577 for param in obj.get_params(ctx): 1aefbghcdi
578 # Skip if option is hidden
579 if getattr(param, "hidden", False): 1aefbghcdi
580 continue 1aefbghcdi
581 if isinstance(param, click.Argument): 1aefbghcdi
582 panel_name = ( 1aefbghcdi
583 getattr(param, _RICH_HELP_PANEL_NAME, None) or ARGUMENTS_PANEL_TITLE
584 )
585 panel_to_arguments[panel_name].append(param) 1aefbghcdi
586 elif isinstance(param, click.Option): 1aefbghcdi
587 panel_name = ( 1aefbghcdi
588 getattr(param, _RICH_HELP_PANEL_NAME, None) or OPTIONS_PANEL_TITLE
589 )
590 panel_to_options[panel_name].append(param) 1aefbghcdi
591 default_arguments = panel_to_arguments.get(ARGUMENTS_PANEL_TITLE, []) 1aefbghcdi
592 _print_options_panel( 1aefbghcdi
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(): 1aefbghcdi
600 if panel_name == ARGUMENTS_PANEL_TITLE: 1aefbghcdi
601 # Already printed above
602 continue 1aefbghcdi
603 _print_options_panel( 1aefbghcdi
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, []) 1aefbghcdi
611 _print_options_panel( 1aefbghcdi
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(): 1aefbghcdi
619 if panel_name == OPTIONS_PANEL_TITLE: 1aefbghcdi
620 # Already printed above
621 continue 1aefbghcdi
622 _print_options_panel( 1aefbghcdi
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): 1aefbghcdi
631 panel_to_commands: DefaultDict[str, List[click.Command]] = defaultdict(list) 1aefbghcdi
632 for command_name in obj.list_commands(ctx): 1aefbghcdi
633 command = obj.get_command(ctx, command_name) 1aefbghcdi
634 if command and not command.hidden: 1aefbghcdi
635 panel_name = ( 1aefbghcdi
636 getattr(command, _RICH_HELP_PANEL_NAME, None)
637 or COMMANDS_PANEL_TITLE
638 )
639 panel_to_commands[panel_name].append(command) 1aefbghcdi
641 # Identify the longest command name in all panels
642 max_cmd_len = max( 1aefbghcdi
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, []) 1aefbghcdi
653 _print_commands_panel( 1aefbghcdi
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(): 1aefbghcdi
661 if panel_name == COMMANDS_PANEL_TITLE: 1aefbghcdi
662 # Already printed above
663 continue 1aefbghcdi
664 _print_commands_panel( 1aefbghcdi
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: 1aefbghcdi
674 # Remove single linebreaks, replace double with single
675 lines = obj.epilog.split("\n\n") 1aefbghcdi
676 epilogue = "\n".join([x.replace("\n", " ").strip() for x in lines]) 1aefbghcdi
677 epilogue_text = _make_rich_text(text=epilogue, markup_mode=markup_mode) 1aefbghcdi
678 console.print(Padding(Align(epilogue_text, pad=False), 1)) 1aefbghcdi
681def rich_format_error(self: click.ClickException) -> None: 1aefbghcdi
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 # Don't do anything when it's a NoArgsIsHelpError (without importing it, cf. #1278)
688 if self.__class__.__name__ == "NoArgsIsHelpError": 1aefbghcdi
689 return 1aefghdi
691 console = _get_rich_console(stderr=True) 1aefbghcdi
692 ctx: Union[click.Context, None] = getattr(self, "ctx", None) 1aefbghcdi
693 if ctx is not None: 1aefbghcdi
694 console.print(ctx.get_usage()) 1aefbghcdi
696 if ctx is not None and ctx.command.get_help_option(ctx) is not None: 1aefbghcdi
697 console.print( 1aefbghcdi
698 RICH_HELP.format(
699 command_path=ctx.command_path, help_option=ctx.help_option_names[0]
700 ),
701 style=STYLE_ERRORS_SUGGESTION,
702 )
704 console.print( 1aefbghcdi
705 Panel(
706 highlighter(self.format_message()),
707 border_style=STYLE_ERRORS_PANEL_BORDER,
708 title=ERRORS_PANEL_TITLE,
709 title_align=ALIGN_ERRORS_PANEL,
710 )
711 )
714def rich_abort_error() -> None: 1aefbghcdi
715 """Print richly formatted abort error."""
716 console = _get_rich_console(stderr=True) 1aefbghcdi
717 console.print(ABORTED_TEXT, style=STYLE_ABORTED) 1aefbghcdi
720def escape_before_html_export(input_text: str) -> str: 1aefbghcdi
721 """Ensure that the input string can be used for HTML export."""
722 return escape(input_text).strip() 1aefbghcdi
725def rich_to_html(input_text: str) -> str: 1aefbghcdi
726 """Print the HTML version of a rich-formatted input string.
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()) 1aefbghcdi
733 console.print(input_text, overflow="ignore", crop=False) 1aefbghcdi
735 return console.export_html(inline_styles=True, code_format="{code}").strip() 1aefbghcdi
738def rich_render_text(text: str) -> str: 1aefbghcdi
739 """Remove rich tags and render a pure text representation"""
740 console = _get_rich_console() 1aefbghcdi
741 return "".join(segment.text for segment in console.render(text)).rstrip("\n") 1aefbghcdi
744def get_traceback( 1aefbghcdi
745 exc: BaseException,
746 exception_config: DeveloperExceptionConfig,
747 internal_dir_names: List[str],
748) -> Traceback:
749 rich_tb = Traceback.from_exception( 1aefbghcdi
750 type(exc),
751 exc,
752 exc.__traceback__,
753 show_locals=exception_config.pretty_exceptions_show_locals,
754 suppress=internal_dir_names,
755 width=MAX_WIDTH,
756 )
757 return rich_tb 1aefbghcdi