Source code for bsb.cli.commands

"""
Contains all of the logic required to create commands. It should always suffice to import
just this module for a user to create their own commands.

Inherit from :class:`BaseCommand` for regular CLI style commands, or from
:class:`BsbCommand` if you want more freedom in what exactly constitutes a command to the
BSB.
"""

import argparse

from ...exceptions import CommandError
from ...reporting import report


class BaseParser(argparse.ArgumentParser):
    """
    Inherits from argparse.ArgumentParser and overloads the ``error``
    method so that when an error occurs, instead of exiting and exception
    is thrown.
    """

    def error(self, message):
        """
        Raise message, instead of exiting.

        :param message: Error message
        :type message: str
        """
        raise CommandError(message)


_is_root = True


[docs] class BsbCommand:
[docs] def add_to_parser(self): raise NotImplementedError("Commands must implement a `add_to_parser` method.")
[docs] def handler(self, context): raise NotImplementedError("Commands must implement a `handler` method.")
def __init_subclass__(cls, parent=None, abstract=False, name=None, **kwargs): global _is_root if abstract: return if cls.add_to_parser is BsbCommand.add_to_parser: raise NotImplementedError("Commands must implement a `add_to_parser` method.") if cls.handler is BsbCommand.handler: raise NotImplementedError("Commands must implement a `handler` method.") if name is None: raise CommandError(f"{cls} must register a name.") cls.name = name cls._subcommands = [] # The very first registered command will be the RootCommand for `bsb` if _is_root: _is_root = False else: if parent is None: parent = RootCommand parent._subcommands.append(cls)
[docs] class BaseCommand(BsbCommand, abstract=True):
[docs] def add_to_parser(self, parent, context, locals, level): locals = locals.copy() locals.update(self.get_options()) parser = parent.add_parser(self.name) self.add_parser_arguments(parser) self.add_parser_options(parser, context, locals, level) parser.set_defaults(handler=self.execute_handler) self.add_subparsers(parser, context, self._subcommands, locals, level) return parser
[docs] def add_subparsers(self, parser, context, commands, locals, level): if len(commands) > 0: subparsers = parser.add_subparsers() for command in commands: c = command() c._parent = self c.add_to_parser(subparsers, context, locals, level + 1)
[docs] def execute_handler(self, namespace, dryrun=False): reduced = {} context = namespace._context for k, v in namespace.__dict__.items(): if v is None or k in ["_context", "handler"]: continue stripped = k.lstrip("_") level = len(k) - len(stripped) if stripped not in reduced or level > reduced[stripped][0]: reduced[stripped] = (level, v) namespace.__dict__ = {k: v[1] for k, v in reduced.items()} self.add_locals(context) context.set_cli_namespace(namespace) report(f"Context: {context}", level=4) if not dryrun: self.handler(context)
[docs] def add_locals(self, context): # Merge our options into the context, preserving those in the context as we're # going up the tree towards lower priority and less specific options. options = self.get_options() options.update(context.options) context.options = options if hasattr(self, "_parent"): self._parent.add_locals(context)
[docs] def add_parser_options(self, parser, context, locals, level): merged = {} merged.update(context.options) merged.update(locals) for option in merged.values(): option.add_to_parser(parser, level)
[docs] def get_options(self): raise NotImplementedError( "BaseCommands must implement a `get_options(self)` method." )
[docs] def add_parser_arguments(self, parser): raise NotImplementedError( "BaseCommands must implement an `add_parser_arguments(self, parser)` method." )
[docs] class RootCommand(BaseCommand, name="bsb"):
[docs] def handler(self, context): pass
[docs] def get_parser(self, context): parser = BaseParser() parser.set_defaults(_context=context) parser.set_defaults(handler=self.execute_handler) locals = self.get_options() self.add_parser_options(parser, context, locals, 0) self.add_subparsers(parser, context, self._subcommands, locals, 0) return parser
[docs] def get_options(self): return {}
[docs] def load_root_command(): from ...plugins import discover # Simply discovering the plugin modules should append them to their parent command # class using the `__init_subclass__` function. discover("commands") return RootCommand()
__all__ = ["BaseCommand", "BsbCommand", "RootCommand", "load_root_command"]