Skip to content

Commit a495936

Browse files
yeldarbyclaude
andcommitted
feat(cli): alphabetize subcommand options and commands via SortedGroup
SortedGroup now sorts options on individual commands (not just groups) by overriding get_command() to sort params before help rendering. Sort order: required options first, then alphabetical by flag name, --help always last. Commands within groups also alphabetized. Applied to all 20 Typer() instances across 16 handler files. 374 tests pass, all linting clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 072be4c commit a495936

17 files changed

Lines changed: 78 additions & 32 deletions

roboflow/cli/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import typer
1414

1515
import roboflow
16+
from roboflow.cli._compat import SortedGroup
1617

1718
# ---------------------------------------------------------------------------
1819
# Root application
@@ -27,6 +28,7 @@
2728
app = typer.Typer(
2829
name="roboflow",
2930
help=_DESCRIPTION,
31+
cls=SortedGroup,
3032
pretty_exceptions_enable=False,
3133
rich_markup_mode="rich",
3234
add_completion=False,

roboflow/cli/_compat.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,51 @@
1111
import types
1212
from typing import Any
1313

14+
import click
1415
import typer # noqa: TC002 — needed at runtime for Context type
1516

1617

18+
def _sort_params(params: list[click.Parameter]) -> None:
19+
"""Sort params in-place: required first, then alphabetical by option name."""
20+
params.sort(
21+
key=lambda p: (
22+
# --help always last
23+
"help" in (p.opts if hasattr(p, "opts") else [p.name or ""]),
24+
# Required options first
25+
not getattr(p, "required", False),
26+
# Arguments before options (positionals first)
27+
not isinstance(p, click.Argument),
28+
# Alphabetical by the first long option name
29+
(p.opts[0].lstrip("-") if hasattr(p, "opts") and p.opts else p.name or ""),
30+
)
31+
)
32+
33+
34+
class SortedGroup(typer.core.TyperGroup):
35+
"""Click Group that alphabetizes commands and options in --help output.
36+
37+
Use as ``cls=SortedGroup`` when creating Typer apps so that subcommand
38+
help pages show options and commands in alphabetical order (with
39+
required options first).
40+
"""
41+
42+
def list_commands(self, ctx: click.Context) -> list[str]: # type: ignore[override]
43+
return sorted(super().list_commands(ctx))
44+
45+
def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
46+
"""Sort options alphabetically before rendering help."""
47+
_sort_params(self.params)
48+
super().format_help(ctx, formatter)
49+
50+
def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None: # type: ignore[override]
51+
"""Wrap returned commands to sort their options too."""
52+
cmd = super().get_command(ctx, cmd_name)
53+
if cmd is not None and not isinstance(cmd, SortedGroup):
54+
# Sort the command's params for its --help output
55+
_sort_params(cmd.params)
56+
return cmd
57+
58+
1759
def ctx_to_args(ctx: typer.Context, **kwargs: Any) -> types.SimpleNamespace:
1860
"""Convert a typer Context (with global opts in ``ctx.obj``) to an args namespace.
1961

roboflow/cli/handlers/annotation.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66

77
import typer
88

9-
from roboflow.cli._compat import ctx_to_args
9+
from roboflow.cli._compat import SortedGroup, ctx_to_args
1010

11-
annotation_app = typer.Typer(help="Annotation management commands", no_args_is_help=True)
12-
batch_app = typer.Typer(help="Annotation batch commands", no_args_is_help=True)
13-
job_app = typer.Typer(help="Annotation job commands", no_args_is_help=True)
11+
annotation_app = typer.Typer(cls=SortedGroup, help="Annotation management commands", no_args_is_help=True)
12+
batch_app = typer.Typer(cls=SortedGroup, help="Annotation batch commands", no_args_is_help=True)
13+
job_app = typer.Typer(cls=SortedGroup, help="Annotation job commands", no_args_is_help=True)
1414

1515
annotation_app.add_typer(batch_app, name="batch")
1616
annotation_app.add_typer(job_app, name="job")

roboflow/cli/handlers/auth.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66

77
import typer
88

9-
from roboflow.cli._compat import ctx_to_args
9+
from roboflow.cli._compat import SortedGroup, ctx_to_args
1010

11-
auth_app = typer.Typer(help="Manage authentication and credentials", no_args_is_help=True)
11+
auth_app = typer.Typer(cls=SortedGroup, help="Manage authentication and credentials", no_args_is_help=True)
1212

1313

1414
@auth_app.command("login")

roboflow/cli/handlers/batch.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66

77
import typer
88

9-
from roboflow.cli._compat import ctx_to_args
9+
from roboflow.cli._compat import SortedGroup, ctx_to_args
1010

11-
batch_app = typer.Typer(help="Batch processing operations", no_args_is_help=True)
11+
batch_app = typer.Typer(cls=SortedGroup, help="Batch processing operations", no_args_is_help=True)
1212

1313

1414
def _stub(args) -> None: # noqa: ANN001

roboflow/cli/handlers/completion.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
import click
1313
import typer
1414

15-
completion_app = typer.Typer(help="Generate shell completions", no_args_is_help=True)
15+
from roboflow.cli._compat import SortedGroup
16+
17+
completion_app = typer.Typer(cls=SortedGroup, help="Generate shell completions", no_args_is_help=True)
1618

1719

1820
def _generate_completion(shell: str) -> None:

roboflow/cli/handlers/deployment.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
import typer
1515

16-
from roboflow.cli._compat import ctx_to_args
16+
from roboflow.cli._compat import SortedGroup, ctx_to_args
1717

1818
# ---------------------------------------------------------------------------
1919
# Wrapper that captures legacy handler stdout/exit and normalises output
@@ -69,7 +69,7 @@ def _wrapped(args): # noqa: ANN001
6969
return _wrapped
7070

7171

72-
deployment_app = typer.Typer(help="Manage dedicated deployments", no_args_is_help=True)
72+
deployment_app = typer.Typer(cls=SortedGroup, help="Manage dedicated deployments", no_args_is_help=True)
7373

7474

7575
# ---------------------------------------------------------------------------

roboflow/cli/handlers/folder.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66

77
import typer
88

9-
from roboflow.cli._compat import ctx_to_args
9+
from roboflow.cli._compat import SortedGroup, ctx_to_args
1010

11-
folder_app = typer.Typer(help="Manage workspace folders", no_args_is_help=True)
11+
folder_app = typer.Typer(cls=SortedGroup, help="Manage workspace folders", no_args_is_help=True)
1212

1313

1414
@folder_app.command("list")

roboflow/cli/handlers/image.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66

77
import typer
88

9-
from roboflow.cli._compat import ctx_to_args
9+
from roboflow.cli._compat import SortedGroup, ctx_to_args
1010

11-
image_app = typer.Typer(help="Image management commands", no_args_is_help=True)
11+
image_app = typer.Typer(cls=SortedGroup, help="Image management commands", no_args_is_help=True)
1212

1313

1414
@image_app.command("upload")

roboflow/cli/handlers/model.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66

77
import typer
88

9-
from roboflow.cli._compat import ctx_to_args
9+
from roboflow.cli._compat import SortedGroup, ctx_to_args
1010

11-
model_app = typer.Typer(help="Manage trained models", no_args_is_help=True)
11+
model_app = typer.Typer(cls=SortedGroup, help="Manage trained models", no_args_is_help=True)
1212

1313

1414
@model_app.command("list")

0 commit comments

Comments
 (0)