"""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