diff --git a/kobo/argcli.py b/kobo/argcli.py new file mode 100644 index 00000000..9a6c1744 --- /dev/null +++ b/kobo/argcli.py @@ -0,0 +1,118 @@ +import argparse +import sys + +from kobo.plugins import Plugin + + +class ArgparseCommand(Plugin): + """ + Base class for argparse-based commands. + + Commands must: + - inherit from ArgparseCommand + - implement add_arguments(self) + - implement run(self, *args, **kwargs) + """ + + def __init__(self, parser): + Plugin.__init__(self) + self.parser = parser + + def add_arguments(self): + """Register argparse arguments.""" + raise NotImplementedError + + def run(self, *args, **kwargs): + """Execute command.""" + raise NotImplementedError + + +class CommandArgumentParser(object): + """ + Argparse-based command dispatcher. + + This intentionally does NOT inherit from argparse.ArgumentParser. + It is a coordinator that: + - parses the global command name + - dispatches to an argparse-capable command + """ + + def __init__( + self, + command_container, + prog=None, + default_command=None, + default_profile=None, + ): + self.container = command_container + self.default_command = default_command + self.default_profile = default_profile + + self.parser = argparse.ArgumentParser( + prog=prog, + usage="%(prog)s [args] [--help]", + formatter_class=argparse.RawTextHelpFormatter, + ) + + self.parser.add_argument( + "command", + nargs="?", + help="command to execute", + ) + + # everything after the command name + self.parser.add_argument( + "args", + nargs=argparse.REMAINDER, + help=argparse.SUPPRESS, + ) + + def _load_profile(self, profile): + """ + Hook for loading a profile. + Implemented here only as a placeholder. + """ + pass + + def run(self, argv=None): + cmd, cmd_ns, cmd_args = self.parse_args(argv) + + cmd_kwargs = vars(cmd_ns) + + # load profile if requested + if self.default_profile and "profile" in cmd_kwargs: + self._load_profile(cmd_kwargs["profile"]) + + return cmd.run(*cmd_args, **cmd_kwargs) + + def parse_args(self, argv=None): + argv = argv if argv is not None else sys.argv[1:] + ns = self.parser.parse_args(argv) + + command_name = ns.command or self.default_command + + if not command_name: + self.parser.error("no command specified") + + if command_name not in self.container.plugins: + self.parser.error("unknown command: %s" % command_name) + + CommandClass = self.container[command_name] + + if not issubclass(CommandClass, ArgparseCommand): + self.parser.error("command '%s' does not support argparse" % command_name) + + cmd_parser = argparse.ArgumentParser( + prog="%s %s" % (self.parser.prog, command_name), + formatter_class=argparse.RawTextHelpFormatter, + ) + + cmd_parser.container = self.container + + cmd = CommandClass(cmd_parser) + cmd.add_arguments() + + # allow extra args (optparse compatibility) + cmd_ns, remainder = cmd_parser.parse_known_args(ns.args) + + return cmd, cmd_ns, remainder diff --git a/tests/plugins/commands/__init__.py b/tests/plugins/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/plugins/commands/cmd_fake_echo.py b/tests/plugins/commands/cmd_fake_echo.py new file mode 100644 index 00000000..755a26dc --- /dev/null +++ b/tests/plugins/commands/cmd_fake_echo.py @@ -0,0 +1,15 @@ +from kobo.argcli import ArgparseCommand + +__all__ = ("Cmd_Fake_Echo",) + + +class Cmd_Fake_Echo(ArgparseCommand): + enabled = True + name = "fake-echo" + + def add_arguments(self): + self.parser.add_argument("words", nargs="+") + + def run(self, *args, **kwargs): + # store result on container + self.parser.container.result = kwargs["words"] diff --git a/tests/test_argcli.py b/tests/test_argcli.py new file mode 100644 index 00000000..18663ce5 --- /dev/null +++ b/tests/test_argcli.py @@ -0,0 +1,29 @@ +import kobo.client +from kobo.argcli import CommandArgumentParser +from tests.plugins import commands + + +class FakeCommandContainer(kobo.client.ClientCommandContainer): + pass + + +FakeCommandContainer.register_module(commands, prefix="cmd_") + + +class FakeCommandArgumentParser(CommandArgumentParser): + def load_pub_profile(self, profile=None): + pass + + +def test_argparse_command_registered_via_module(): + conf = kobo.conf.PyConfigParser() + container = FakeCommandContainer(conf) + + parser = FakeCommandArgumentParser( + command_container=container, + default_profile="default-profile", + ) + + parser.run(["cmd-fake-echo", "hello", "world"]) + + assert container.result == ["hello", "world"]