From 7a3b4370007ba2ac9d0f6997a40775c2b2203f68 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Sat, 10 Oct 2026 20:04:46 -0400 Subject: [PATCH] Add optional usage metadata to command help Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com> --- csp_bot/commands/base.py | 4 +++ csp_bot/commands/framework.py | 12 ++++++- csp_bot/commands/help.py | 10 ++++-- csp_bot/commands/legacy.py | 4 +++ csp_bot/tests/test_help_command.py | 52 ++++++++++++++++++++++++++++++ 5 files changed, 79 insertions(+), 3 deletions(-) diff --git a/csp_bot/commands/base.py b/csp_bot/commands/base.py index 3151c66..d7ed494 100644 --- a/csp_bot/commands/base.py +++ b/csp_bot/commands/base.py @@ -46,6 +46,10 @@ def help(self) -> str: """Return the help text for the command.""" ... + def usage(self) -> str: + """Return optional command syntax, without a label.""" + return "" + @abstractmethod def num_recipients(self) -> int: """Return how many users this command can tag. diff --git a/csp_bot/commands/framework.py b/csp_bot/commands/framework.py index 8b80d07..838e09f 100644 --- a/csp_bot/commands/framework.py +++ b/csp_bot/commands/framework.py @@ -31,7 +31,7 @@ class CommandEntry: """Internal registry entry for a command.""" - __slots__ = ("backends", "handler", "help", "is_class", "name") + __slots__ = ("backends", "handler", "help", "is_class", "name", "usage") def __init__( self, @@ -40,18 +40,22 @@ def __init__( handler: Any, backends: list[str] | None = None, is_class: bool = False, + usage: str = "", ): self.name = name self.help = help self.handler = handler self.backends = backends or [] self.is_class = is_class + self.usage = usage def command( name: str, help: str = "", backends: list[str] | None = None, + *, + usage: str = "", ) -> Callable: """Decorator to register a function as a bot command. @@ -67,6 +71,7 @@ def command( name: The command name (e.g. "echo" for /echo). help: Help text shown by the /help command. backends: List of backends this command supports. Empty = all. + usage: Optional command syntax, without a label. Returns: The original function, registered in the global command registry. @@ -85,12 +90,14 @@ def decorator(fn: Callable) -> Callable: handler=fn, backends=backends, is_class=False, + usage=usage, ) _COMMAND_REGISTRY[name] = entry # Stash metadata on the function for introspection fn._command_name = name fn._command_help = help fn._command_backends = backends or [] + fn._command_usage = usage return fn return decorator @@ -133,6 +140,9 @@ def execute(self, ctx: CommandContext) -> str: help: str = "" """Help text shown by the /help command.""" + usage: str = "" + """Optional command syntax, without a label.""" + backends: list[str] = Field(default_factory=list) """Backends this command supports. Empty = all.""" diff --git a/csp_bot/commands/help.py b/csp_bot/commands/help.py index 80b1769..1c41f48 100644 --- a/csp_bot/commands/help.py +++ b/csp_bot/commands/help.py @@ -29,8 +29,14 @@ def _command_name(runner: Any) -> str: def _command_help(runner: Any) -> str: if isinstance(runner, BaseCommand): - return runner.help() - return getattr(runner, "help", "") or "" + description = runner.help() + usage = runner.usage() + else: + description = getattr(runner, "help", "") or "" + usage = getattr(runner, "usage", "") or "" + if not usage: + return description + return f"{description} Syntax: {usage}" if description else f"Syntax: {usage}" def _render_table_help(rows: list, backend: str) -> str: diff --git a/csp_bot/commands/legacy.py b/csp_bot/commands/legacy.py index 7b48454..ea9b8d5 100644 --- a/csp_bot/commands/legacy.py +++ b/csp_bot/commands/legacy.py @@ -46,6 +46,10 @@ def name(self) -> str: def help(self) -> str: return self._command.help() + @property + def usage(self) -> str: + return self._command.usage() + @property def backends(self) -> list[str]: return self._command.backends() diff --git a/csp_bot/tests/test_help_command.py b/csp_bot/tests/test_help_command.py index ad58ace..c340d83 100644 --- a/csp_bot/tests/test_help_command.py +++ b/csp_bot/tests/test_help_command.py @@ -2,9 +2,13 @@ from types import SimpleNamespace +import pytest from chatom import Message, User +from csp_bot.commands.echo import EchoCommand +from csp_bot.commands.framework import Command, clear_registry, command, get_registered_commands from csp_bot.commands.help import HelpCommand +from csp_bot.commands.legacy import LegacyCommandAdapter from csp_bot.structs import BotCommand, CommandVariant @@ -61,3 +65,51 @@ def test_help_symphony_keeps_table_rendering() -> None: assert "

Bot Commands Help

" in result.content assert "" in result.content + + +@pytest.mark.parametrize("backend", ["discord", "slack", "telegram", "symphony"]) +@pytest.mark.parametrize("runner_kind", ["base", "adapter", "metadata", "decorated", "stateful"]) +def test_help_renders_optional_usage_after_description(backend, runner_kind): + class UsageCommand(EchoCommand): + def help(self) -> str: + return "Echo a message." + + def usage(self) -> str: + return "/echo message" + + if runner_kind == "base": + runner = UsageCommand() + elif runner_kind == "adapter": + runner = LegacyCommandAdapter(UsageCommand()) + elif runner_kind == "metadata": + runner = SimpleNamespace(name="Echo", help="Echo a message.", usage="/echo message", backends=[]) + elif runner_kind == "stateful": + runner = Command(name="Echo", help="Echo a message.", usage="/echo message") + else: + clear_registry() + try: + + @command(name="echo", help="Echo a message.", usage="/echo message") + def echo(context): + return context.args_text + + runner = get_registered_commands()["echo"] + finally: + clear_registry() + + result = HelpCommand().execute(_command(backend), {"echo": runner}) + assert "Echo a message. Syntax: /echo message" in result.content + assert result.content.count("Syntax:") == 1 + + +def test_help_only_base_command_keeps_existing_output(): + runner = EchoCommand() + assert runner.usage() == "" + result = HelpCommand().execute(_command("discord"), {"echo": runner}) + assert result.content.endswith(f"- `/echo` **{runner.name()}**: {runner.help()}") + + +def test_help_renders_usage_without_description(): + runner = Command(name="Echo", usage="/echo message") + result = HelpCommand().execute(_command("discord"), {"echo": runner}) + assert result.content.endswith("- `/echo` **Echo**: Syntax: /echo message")