Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 27 additions & 22 deletions samcli/cli/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
import click
from click import Group

from samcli.cli.formatters import RootCommandHelpTextFormatter
from samcli.cli.formatters import RootCommandHelpTextFormatter, get_terminal_width
from samcli.cli.root.command_list import SAM_CLI_COMMANDS
from samcli.cli.row_modifiers import HighlightNewRowNameModifier, RowDefinition, ShowcaseRowModifier
from samcli.cli.row_modifiers import RowDefinition, ShowcaseRowModifier

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -62,6 +62,18 @@ class BaseCommand(Group):
class CustomFormatterContext(click.Context):
formatter_class = RootCommandHelpTextFormatter

def __init__(self, *args, **kwargs):
if "max_content_width" not in kwargs:
kwargs["max_content_width"] = 140
# Explicitly set terminal width if not provided to ensure proper text wrapping
# Apply right padding to prevent text from wrapping at the terminal edge
if "terminal_width" not in kwargs:
terminal_width = get_terminal_width()
if terminal_width is not None:
# Apply padding but ensure minimum width of 60
kwargs["terminal_width"] = max(terminal_width - 5, 60)
super().__init__(*args, **kwargs)

context_class = CustomFormatterContext

def __init__(self, *args, cmd_packages=None, **kwargs):
Expand Down Expand Up @@ -106,7 +118,8 @@ def format_options(self, ctx: click.Context, formatter: RootCommandHelpTextForma
# mypy raises argument needs to be HelpFormatter as super class defines it.
# NOTE(sriram-mv): Re-order options so that they come after the commands.
self.format_commands(ctx, formatter)
opts = [RowDefinition(name="", text="\n")]

opts = []
for param in self.get_params(ctx):
row = param.get_help_record(ctx)
if row is not None:
Expand All @@ -118,33 +131,28 @@ def format_options(self, ctx: click.Context, formatter: RootCommandHelpTextForma
formatter.write_rd(opts)

with formatter.indented_section(name="Examples", extra_indents=1):
formatter.write_rd(
[
RowDefinition(
name="",
text="\n",
),
RowDefinition(
name="Get Started:",
text=click.style(f"$ {ctx.command_path} init"),
extra_row_modifiers=[ShowcaseRowModifier()],
),
],
)
with formatter.indented_section(name="Get Started", extra_indents=1):
formatter.write_text_rows(
[
RowDefinition(
name=click.style(f"$ {ctx.command_path} init"),
extra_row_modifiers=[ShowcaseRowModifier()],
),
],
)

def format_commands(self, ctx: click.Context, formatter: RootCommandHelpTextFormatter): # type: ignore
# NOTE(sriram-mv): `ignore` is put in place here for mypy even though it is the correct behavior,
# as the `formatter_class` can be set in subclass of Command. If ignore is not set,
# mypy raises argument needs to be HelpFormatter as super class defines it.
with formatter.section("Commands"):
with formatter.section("Learn"):
formatter.write_rd(
formatter.write_text_rows(
[
RowDefinition(
name="docs",
text=SAM_CLI_COMMANDS.get("docs", ""),
extra_row_modifiers=[HighlightNewRowNameModifier()],
)
),
]
)

Expand Down Expand Up @@ -173,12 +181,10 @@ def format_commands(self, ctx: click.Context, formatter: RootCommandHelpTextForm
RowDefinition(
name="sync",
text=SAM_CLI_COMMANDS.get("sync", ""),
extra_row_modifiers=[HighlightNewRowNameModifier()],
),
RowDefinition(
name="remote",
text=SAM_CLI_COMMANDS.get("remote", ""),
extra_row_modifiers=[HighlightNewRowNameModifier()],
),
],
)
Expand Down Expand Up @@ -217,7 +223,6 @@ def format_commands(self, ctx: click.Context, formatter: RootCommandHelpTextForm
RowDefinition(
name="list",
text=SAM_CLI_COMMANDS.get("list", ""),
extra_row_modifiers=[HighlightNewRowNameModifier()],
),
RowDefinition(
name="delete",
Expand Down
27 changes: 15 additions & 12 deletions samcli/cli/core/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,9 @@ def __init__(self, description, requires_credentials=False, *args, **kwargs):

def format_description(self, formatter: RootCommandHelpTextFormatter):
with formatter.indented_section(name="Description", extra_indents=1):
formatter.write_rd(
formatter.write_text_rows(
[
RowDefinition(
text="",
name=self.description + self.description_addendum,
),
],
Expand All @@ -55,17 +54,21 @@ def _format_options(
],
key=lambda row_def: row_def.rank,
)
extras = options.get("extras", [])

# Skip section entirely if no options and no extras
if not opts and not extras:
continue

with formatter.indented_section(name=option_heading, extra_indents=1):
formatter.write_rd(options.get("extras", [RowDefinition()]), **write_rd_overrides)
formatter.write_rd(
[RowDefinition(name="", text="\n")]
+ [
opt
for options in zip(opts, [RowDefinition(name="", text="\n")] * (len(opts)))
for opt in options
],
**write_rd_overrides,
)
rows = []
if extras:
rows.extend(extras)

if opts:
rows.extend(opts)

formatter.write_rd(rows, **write_rd_overrides)

@staticmethod
def convert_param_to_row_definition(ctx: Context, param: Parameter, rank: int):
Expand Down
48 changes: 47 additions & 1 deletion samcli/cli/formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Click Help Formatter Classes that are customized for the root command.
"""

import shutil
from contextlib import contextmanager
from typing import Iterator, Optional, Sequence

Expand All @@ -11,9 +12,24 @@
from samcli.cli.row_modifiers import BaseLineRowModifier, RowDefinition


def get_terminal_width() -> Optional[int]:
"""
Get the current terminal width.

Returns
-------
Optional[int]
Terminal width in columns, or None if it cannot be determined.
"""
try:
return shutil.get_terminal_size().columns
except (AttributeError, ValueError, OSError):
return None


class RootCommandHelpTextFormatter(HelpFormatter):
# Picked an additive constant that gives an aesthetically pleasing look.
ADDITIVE_JUSTIFICATION = 10
ADDITIVE_JUSTIFICATION = 6

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand All @@ -27,6 +43,14 @@ def __init__(self, *args, **kwargs):
self.modifiers = [BaseLineRowModifier()]

def write_usage(self, prog: str, args: str = "", prefix: Optional[str] = None) -> None:
"""Write the usage line with bold and underlined 'Usage:' prefix and a leading blank line."""
if prefix is None:
prefix = style("Usage:", bold=True, underline=True) + " "
else:
prefix = style(prefix, bold=True, underline=True)

# Add a blank line before usage
self.write("\n")
super().write_usage(prog=style(prog, bold=True), args=args, prefix=prefix)

def write_heading(self, heading: str) -> None:
Expand All @@ -48,6 +72,28 @@ def write_rd(

super().write_dl(modified_rows, col_max=col_max, col_spacing=col_spacing)

def write_text_rows(
self,
rows: Sequence[RowDefinition],
) -> None:
"""Write single-column text rows without two-column layout constraints.

This is useful for "examples" or other content that should appear as simple
indented text without the padding and wrapping behavior of write_dl.

Parameters
----------
rows : Sequence[RowDefinition]
Rows to write. Only the 'name' field and 'extra_row_modifiers' are used.
"""
for row in rows:
extra_row_modifiers = row.extra_row_modifiers or []
modified_row = row
for row_modifier in self.modifiers + extra_row_modifiers:
modified_row = row_modifier.apply(row=modified_row, justification_length=self.left_justification_length)
# Write the content with current indentation, no padding
self.write(f"{'':>{self.current_indent}}{modified_row.name.rstrip()}\n")

@contextmanager
def section(self, name: str) -> Iterator[None]:
with super().section(style(name, bold=True, underline=True)):
Expand Down
56 changes: 55 additions & 1 deletion samcli/cli/lazy_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,67 @@
import click
from click import ClickException

from samcli.cli.formatters import RootCommandHelpTextFormatter
from samcli.cli.row_modifiers import HighlightNewRowNameModifier, RowDefinition


class LazyGroup(click.Group):
def __init__(self, *args, lazy_subcommands=None, **kwargs):
class CustomFormatterContext(click.Context):
formatter_class = RootCommandHelpTextFormatter

def __init__(self, *args, **kwargs):
if "max_content_width" not in kwargs:
kwargs["max_content_width"] = 140
super().__init__(*args, **kwargs)

context_class = CustomFormatterContext

def __init__(self, *args, lazy_subcommands=None, new_commands=None, **kwargs):
# Set context_settings to use our custom formatter
if "context_settings" not in kwargs:
kwargs["context_settings"] = {}
kwargs["context_settings"]["help_option_names"] = ["-h", "--help"]

super().__init__(*args, **kwargs)
# lazy_subcommands is a map of the form:
# {command-name} -> {module-name}.{command-object-name}
self.lazy_subcommands = lazy_subcommands or {}
# new_commands is a set of command names that should be marked as NEW
self.new_commands = new_commands or set()

@staticmethod
def _write_rows_with_spacing(formatter: RootCommandHelpTextFormatter, section_name: str, rows: list):
"""Helper method to write rows."""
if rows:
with formatter.indented_section(name=section_name, extra_indents=1):
formatter.write_rd(rows)

def format_options(self, ctx: click.Context, formatter: RootCommandHelpTextFormatter): # type: ignore
"""Format options with spacing between each option."""
opts = [
RowDefinition(name=rv[0], text=rv[1])
for param in self.get_params(ctx)
if (rv := param.get_help_record(ctx)) is not None
]

self._write_rows_with_spacing(formatter, "Options", opts)

# Call format_commands to show the subcommands
self.format_commands(ctx, formatter)

def format_commands(self, ctx: click.Context, formatter: RootCommandHelpTextFormatter): # type: ignore
"""Format commands with spacing between each command."""
commands = [
RowDefinition(
name=subcommand,
text=cmd.get_short_help_str(limit=formatter.width),
extra_row_modifiers=[HighlightNewRowNameModifier()] if subcommand in self.new_commands else [],
)
for subcommand in self.list_commands(ctx)
if (cmd := self.get_command(ctx, subcommand)) is not None and not (hasattr(cmd, "hidden") and cmd.hidden)
]

self._write_rows_with_spacing(formatter, "Commands", commands)

def list_commands(self, ctx):
base = super().list_commands(ctx)
Expand Down
Loading
Loading