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