Skip to content
Merged
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
4 changes: 4 additions & 0 deletions csp_bot/commands/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 11 additions & 1 deletion csp_bot/commands/framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.

Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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."""

Expand Down
10 changes: 8 additions & 2 deletions csp_bot/commands/help.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions csp_bot/commands/legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
52 changes: 52 additions & 0 deletions csp_bot/tests/test_help_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -61,3 +65,51 @@ def test_help_symphony_keeps_table_rendering() -> None:

assert "<h3>Bot Commands Help</h3>" in result.content
assert "<table>" 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")