Source code for runtimepy.channel.environment.command.processor

"""
A module implementing a channel-command processing interface.
"""

# built-in
from argparse import Namespace
from typing import Any, Awaitable, Callable, Optional, cast

# third-party
from vcorelib.logging import LoggerType

# internal
from runtimepy.channel.environment import ChannelEnvironment
from runtimepy.channel.environment.base import FieldOrChannel
from runtimepy.channel.environment.command.parser import (
    ChannelCommand,
    CommandParser,
)
from runtimepy.channel.environment.command.result import SUCCESS, CommandResult
from runtimepy.mixins.environment import ChannelEnvironmentMixin
from runtimepy.primitives.bool import Bool
from runtimepy.primitives.field import BitField
from runtimepy.ui.button import ActionButton

CommandHook = Callable[[Namespace, Optional[FieldOrChannel]], None]

# Custom commands require async processing (e.g. AsyncCommandProcessingMixin).
CustomCommand = Callable[
    [Namespace, Optional[FieldOrChannel]], Awaitable[None]
]


[docs] class ChannelCommandProcessor(ChannelEnvironmentMixin): """A command processing interface for channel environments.""" def __init__( self, env: ChannelEnvironment, logger: LoggerType, buttons: list[ActionButton] = None, **kwargs, ) -> None: """Initialize this instance.""" super().__init__(env=env, **kwargs) self.logger = logger self.hooks: list[CommandHook] = [] self.custom_commands: dict[str, CustomCommand] = {} self.parser_data: dict[str, Any] = {} self.parser = CommandParser(prog="") self.parser.data = self.parser_data self.parser.initialize() # Action buttons. if buttons is None: buttons = [] self.buttons: list[ActionButton] = buttons
[docs] def register_custom_commands( self, *custom_commands: CustomCommand ) -> None: """Register custom commands.""" for cmd in custom_commands: name = cmd.__name__ assert ( name not in self.custom_commands ), f"Duplicate custom command name '{name}'!" self.custom_commands[name] = cmd
[docs] def get_suggestion(self, value: str) -> Optional[str]: """Get an input suggestion.""" result = None args = self.parse(value) if args is not None: candidates = self.env.ns.length_sorted_suggestions( args.channel, delta=False ) if candidates: result = candidates[0] # Try to find a commandable suggestion. for candidate in candidates: chan = self.env.field_or_channel(candidate) if chan is not None and chan.commandable: result = candidate break if result is not None: result = args.command + " " + result return result
[docs] def do_set(self, args: Namespace) -> CommandResult: """Attempt to set a channel value.""" result = SUCCESS if not args.extra: return CommandResult(False, "No value specified.") try: self.env.set(args.channel, args.extra[0]) except (ValueError, KeyError) as exc: self.logger.exception( "Exception setting '%s':", args.channel, exc_info=exc ) result = CommandResult( False, f"Exception while setting '{args.channel}'." ) return result
[docs] def do_toggle( self, args: Namespace, channel: FieldOrChannel ) -> CommandResult: """Attempt to toggle a channel.""" if isinstance(channel, BitField): channel.invert() else: if not channel.type.is_boolean: return CommandResult( False, ( f"Channel '{args.channel}' is " f"{channel.type}, not boolean." ), ) cast(Bool, channel.raw).toggle() return SUCCESS
[docs] def handle_command(self, args: Namespace) -> CommandResult: """Handle a command from parsed arguments.""" result = SUCCESS # Handle remote commands by processing hooks and returning (hooks # implement remote command behavior and capability). if args.remote or args.command == ChannelCommand.CUSTOM: for hook in self.hooks: hook(args, None) return result if args.command == ChannelCommand.GET: if self.env.exists(args.channel): return CommandResult(True, str(self.env.value(args.channel))) channel = self.env.field_or_channel(args.channel) if channel is None: return CommandResult(False, f"No channel '{args.channel}'.") # Check if channel is commandable (or if a -f/--force flag is # set?). if not channel.commandable and not args.force: return CommandResult( False, ( f"Channel '{args.channel}' not " "commandable! (use -f/--force to bypass if necessary)" ), ) if args.command == ChannelCommand.TOGGLE: result = self.do_toggle(args, channel) elif args.command == ChannelCommand.SET: result = self.do_set(args) # Perform extra command actions. if result: for hook in self.hooks: hook(args, channel) return result
[docs] def parse(self, value: str) -> Optional[Namespace]: """Attempt to parse arguments.""" self.parser_data["error_message"] = None args = self.parser.parse_args(value.split()) return args if not self.parser_data["error_message"] else None
[docs] def command(self, value: str) -> CommandResult: """Process a command.""" args = self.parse(value) success = args is not None if not args or "help" in value: self.logger.info(self.parser.format_help()) reason = None if not success: reason = self.parser_data["error_message"] if "help" not in value: self.logger.info("Try 'help'.") result = CommandResult(success, reason) if success: assert args is not None result = self.handle_command(args) return result
[docs] async def handle_custom_command( self, args: Namespace, channel: Optional[FieldOrChannel] ) -> None: """Handle a custom command.""" if args.channel in self.custom_commands: await self.custom_commands[args.channel](args, channel) else: self.logger.warning( "Unknown custom command '%s' (try one of: %s).", args.channel, ", ".join(self.custom_commands), )
EnvironmentMap = dict[str, ChannelCommandProcessor]
[docs] class RemoteCommandProcessor(ChannelCommandProcessor): """A command processing interface for channel environments."""
[docs] def handle_command(self, args: Namespace) -> CommandResult: """Handle a command from parsed arguments.""" args.remote = True return super().handle_command(args)