Scan content/commands/ for .toml files at startup and register them as commands alongside Python-defined ones. Two flavors: handler-based (points to a Python callable via module:function) and message-based (auto-generates a handler from inline text). Includes example MOTD command, type validation, error logging, and full test coverage.
141 lines
3.7 KiB
Python
141 lines
3.7 KiB
Python
"""Content loading from TOML files."""
|
|
|
|
import importlib
|
|
import logging
|
|
import tomllib
|
|
from pathlib import Path
|
|
from typing import cast
|
|
|
|
from mudlib.commands import CommandDefinition, CommandHandler
|
|
from mudlib.player import Player
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
def _resolve_handler(handler_ref: str) -> CommandHandler:
|
|
"""Resolve a handler reference string to a callable.
|
|
|
|
Args:
|
|
handler_ref: String in format "module.path:function_name"
|
|
|
|
Returns:
|
|
The resolved callable
|
|
|
|
Raises:
|
|
ImportError: If the module cannot be imported
|
|
AttributeError: If the function doesn't exist in the module
|
|
"""
|
|
if ":" not in handler_ref:
|
|
msg = (
|
|
f"Handler reference must be in format 'module:function', got: {handler_ref}"
|
|
)
|
|
raise ValueError(msg)
|
|
|
|
module_path, func_name = handler_ref.split(":", 1)
|
|
module = importlib.import_module(module_path)
|
|
handler = getattr(module, func_name)
|
|
|
|
if not callable(handler):
|
|
msg = f"Handler reference {handler_ref} is not callable"
|
|
raise TypeError(msg)
|
|
|
|
return cast(CommandHandler, handler)
|
|
|
|
|
|
def _make_message_handler(message: str) -> CommandHandler:
|
|
"""Create a handler that sends a fixed message to the player.
|
|
|
|
Args:
|
|
message: The message text to send
|
|
|
|
Returns:
|
|
An async handler function
|
|
"""
|
|
|
|
async def handler(player: Player, args: str) -> None:
|
|
player.writer.write(message.strip() + "\r\n")
|
|
await player.writer.drain()
|
|
|
|
return handler
|
|
|
|
|
|
def load_command(path: Path) -> CommandDefinition:
|
|
"""Load a command definition from a TOML file.
|
|
|
|
Args:
|
|
path: Path to the .toml file
|
|
|
|
Returns:
|
|
CommandDefinition instance
|
|
|
|
Raises:
|
|
KeyError: If required fields are missing
|
|
ValueError: If neither handler nor message is specified
|
|
TypeError: If handler or message fields have wrong types
|
|
ImportError: If handler reference cannot be resolved
|
|
"""
|
|
with open(path, "rb") as f:
|
|
data = tomllib.load(f)
|
|
|
|
name = data["name"]
|
|
|
|
# Determine the handler
|
|
handler_ref = data.get("handler")
|
|
message = data.get("message")
|
|
|
|
if handler_ref and message:
|
|
msg = "Command definition cannot specify both 'handler' and 'message'"
|
|
raise ValueError(msg)
|
|
|
|
if not handler_ref and not message:
|
|
msg = "Command definition must specify either 'handler' or 'message'"
|
|
raise ValueError(msg)
|
|
|
|
# Validate types
|
|
if handler_ref and not isinstance(handler_ref, str):
|
|
msg = f"'handler' must be a string in {path}"
|
|
raise TypeError(msg)
|
|
|
|
if message and not isinstance(message, str):
|
|
msg = f"'message' must be a string in {path}"
|
|
raise TypeError(msg)
|
|
|
|
handler: CommandHandler
|
|
if handler_ref:
|
|
handler = _resolve_handler(handler_ref)
|
|
else:
|
|
# message must be a string here since we checked it's not None
|
|
handler = _make_message_handler(cast(str, message))
|
|
|
|
# Optional fields with defaults
|
|
aliases = data.get("aliases", [])
|
|
mode = data.get("mode", "normal")
|
|
help_text = data.get("help", "")
|
|
|
|
return CommandDefinition(
|
|
name=name,
|
|
handler=handler,
|
|
aliases=aliases,
|
|
mode=mode,
|
|
help=help_text,
|
|
)
|
|
|
|
|
|
def load_commands(directory: Path) -> list[CommandDefinition]:
|
|
"""Load all command definitions from a directory.
|
|
|
|
Args:
|
|
directory: Path to directory containing .toml files
|
|
|
|
Returns:
|
|
List of CommandDefinition instances
|
|
"""
|
|
commands = []
|
|
|
|
for path in directory.glob("*.toml"):
|
|
try:
|
|
commands.append(load_command(path))
|
|
except Exception as e:
|
|
log.warning("failed to load command from %s: %s", path, e)
|
|
|
|
return commands
|