Skip to content

Commit db882e6

Browse files
committed
Formalize cli shortcuts and customize cli help
1 parent 9a8e09c commit db882e6

File tree

6 files changed

+124
-15
lines changed

6 files changed

+124
-15
lines changed

plain-code/plain/code/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def maybe_exit(return_code: int) -> None:
119119

120120

121121
@without_runtime_setup
122-
@register_cli("fix")
122+
@register_cli("fix", shortcut_for="code")
123123
@cli.command()
124124
@click.pass_context
125125
@click.argument("path", default=".")

plain-models/plain/models/backups/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from .core import DatabaseBackups
1212

1313

14-
@register_cli("backups")
14+
@register_cli("backups", shortcut_for="models")
1515
@click.group("backups")
1616
def cli() -> None:
1717
"""Local database backups"""

plain-models/plain/models/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def list_models(package_labels: tuple[str, ...], app_only: bool) -> None:
136136
click.echo(f" package: {pkg_name}\n")
137137

138138

139-
@register_cli("makemigrations")
139+
@register_cli("makemigrations", shortcut_for="models")
140140
@cli.command()
141141
@click.argument("package_labels", nargs=-1)
142142
@click.option(
@@ -341,7 +341,7 @@ def write_migration_files(
341341
write_migration_files(changes)
342342

343343

344-
@register_cli("migrate")
344+
@register_cli("migrate", shortcut_for="models")
345345
@cli.command()
346346
@click.argument("package_label", required=False)
347347
@click.argument("migration_name", required=False)

plain/plain/cli/core.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,5 +140,51 @@ def list_commands(self, ctx: Context) -> list[str]:
140140
self._ensure_registry_loaded()
141141
return super().list_commands(ctx)
142142

143+
def format_commands(self, ctx: Context, formatter: Any) -> None:
144+
"""Format commands with separate sections for shortcuts and regular commands."""
145+
self._ensure_registry_loaded()
146+
147+
# Get all commands from both sources
148+
commands = []
149+
for source in self.sources:
150+
for name in source.list_commands(ctx):
151+
cmd = source.get_command(ctx, name)
152+
if cmd is not None:
153+
commands.append((name, cmd))
154+
155+
if not commands:
156+
return
157+
158+
# Get metadata about shortcuts from the registry
159+
shortcuts_metadata = cli_registry.get_shortcuts()
160+
161+
# Separate shortcuts from regular commands
162+
shortcuts = []
163+
regular_commands = []
164+
165+
for name, cmd in commands:
166+
if name in shortcuts_metadata:
167+
# This is a shortcut
168+
help_text = cmd.get_short_help_str(limit=200)
169+
shortcut_for = shortcuts_metadata[name].shortcut_for
170+
if shortcut_for:
171+
# Add italic styling to the alias information (more concise)
172+
alias_info = click.style(f"(→ {shortcut_for})", italic=True)
173+
help_text = f"{help_text} {alias_info}"
174+
shortcuts.append((name, help_text))
175+
else:
176+
# Regular command or group
177+
regular_commands.append((name, cmd.get_short_help_str(limit=200)))
178+
179+
# Write shortcuts section if any exist
180+
if shortcuts:
181+
with formatter.section("Shortcuts"):
182+
formatter.write_dl(sorted(shortcuts))
183+
184+
# Write regular commands section
185+
if regular_commands:
186+
with formatter.section("Commands"):
187+
formatter.write_dl(sorted(regular_commands))
188+
143189

144190
cli = PlainCommandCollection()

plain/plain/cli/formatting.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def write_usage(self, prog: str, args: str = "", prefix: str = "Usage: ") -> Non
1919
def write_dl(
2020
self,
2121
rows: list[tuple[str, str]],
22-
col_max: int = 30,
22+
col_max: int = 20,
2323
col_spacing: int = 2,
2424
) -> None:
2525
"""Writes a definition list into the buffer. This is how options
@@ -54,10 +54,15 @@ def write_dl(
5454
lines = wrapped_text.splitlines()
5555

5656
if lines:
57-
self.write(f"{lines[0]}\n")
57+
# Dim the description text
58+
first_line_styled = click.style(lines[0], dim=True)
59+
self.write(f"{first_line_styled}\n")
5860

5961
for line in lines[1:]:
60-
self.write(f"{'':>{first_col + self.current_indent}}{line}\n")
62+
line_styled = click.style(line, dim=True)
63+
self.write(
64+
f"{'':>{first_col + self.current_indent}}{line_styled}\n"
65+
)
6166
else:
6267
self.write("\n")
6368

@@ -66,6 +71,11 @@ class PlainContext(click.Context):
6671
formatter_class = PlainHelpFormatter
6772

6873
def __init__(self, *args: Any, **kwargs: Any):
74+
# Set a wider max_content_width for help text (default is 80)
75+
# This allows descriptions to fit more comfortably on one line
76+
if "max_content_width" not in kwargs:
77+
kwargs["max_content_width"] = 140
78+
6979
super().__init__(*args, **kwargs)
7080

7181
# Force colors in CI environments

plain/plain/cli/registry.py

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,33 @@
11
from __future__ import annotations
22

3-
from typing import Any
3+
from typing import Any, NamedTuple
44

55
from plain.packages import packages_registry
66

77

8+
class CommandMetadata(NamedTuple):
9+
"""Metadata about a registered command."""
10+
11+
cmd: Any
12+
shortcut_for: str | None = None
13+
14+
815
class CLIRegistry:
9-
def __init__(self):
10-
self._commands: dict[str, Any] = {}
16+
def __init__(self) -> None:
17+
self._commands: dict[str, CommandMetadata] = {}
1118

12-
def register_command(self, cmd: Any, name: str) -> None:
19+
def register_command(
20+
self, cmd: Any, name: str, shortcut_for: str | None = None
21+
) -> None:
1322
"""
1423
Register a CLI command or group with the specified name.
24+
25+
Args:
26+
cmd: The click command or group to register
27+
name: The name to register the command under
28+
shortcut_for: Optional parent command this is a shortcut for (e.g., "models" for migrate)
1529
"""
16-
self._commands[name] = cmd
30+
self._commands[name] = CommandMetadata(cmd=cmd, shortcut_for=shortcut_for)
1731

1832
def import_modules(self) -> None:
1933
"""
@@ -23,27 +37,66 @@ def import_modules(self) -> None:
2337

2438
def get_commands(self) -> dict[str, Any]:
2539
"""
26-
Get all registered commands.
40+
Get all registered commands (just the command objects, not metadata).
41+
"""
42+
return {name: metadata.cmd for name, metadata in self._commands.items()}
43+
44+
def get_commands_with_metadata(self) -> dict[str, CommandMetadata]:
45+
"""
46+
Get all registered commands with their metadata.
2747
"""
2848
return self._commands
2949

50+
def get_shortcuts(self) -> dict[str, CommandMetadata]:
51+
"""
52+
Get only commands that are shortcuts.
53+
"""
54+
return {
55+
name: metadata
56+
for name, metadata in self._commands.items()
57+
if metadata.shortcut_for
58+
}
59+
60+
def get_regular_commands(self) -> dict[str, CommandMetadata]:
61+
"""
62+
Get only commands that are not shortcuts.
63+
"""
64+
return {
65+
name: metadata
66+
for name, metadata in self._commands.items()
67+
if not metadata.shortcut_for
68+
}
69+
3070

3171
cli_registry = CLIRegistry()
3272

3373

34-
def register_cli(name: str) -> Any:
74+
def register_cli(name: str, shortcut_for: str | None = None) -> Any:
3575
"""
3676
Register a CLI command or group with the given name.
3777
78+
Args:
79+
name: The name to register the command under
80+
shortcut_for: Optional parent command this is a shortcut for.
81+
For example, @register_cli("migrate", shortcut_for="models")
82+
indicates that "plain migrate" is a shortcut for "plain models migrate"
83+
3884
Usage:
85+
# Register a regular command group
3986
@register_cli("users")
4087
@click.group()
4188
def users_cli():
4289
pass
90+
91+
# Register a shortcut command
92+
@register_cli("migrate", shortcut_for="models")
93+
@click.command()
94+
def migrate():
95+
pass
4396
"""
4497

4598
def wrapper(cmd: Any) -> Any:
46-
cli_registry.register_command(cmd, name)
99+
cli_registry.register_command(cmd, name, shortcut_for=shortcut_for)
47100
return cmd
48101

49102
return wrapper

0 commit comments

Comments
 (0)