=== modified file 'lava_tool/dispatcher.py'
@@ -20,61 +20,36 @@
Module with LavaDispatcher - the command dispatcher
"""
-import argparse
-import pkg_resources
-import sys
-
-from lava_tool.interface import LavaCommandError
-
-
-class LavaDispatcher(object):
+from lava_tool.interface import LavaCommandError, BaseDispatcher
+
+
+class LavaDispatcher(BaseDispatcher):
"""
Class implementing command line interface for launch control
"""
toolname = None
- description = None
- epilog = None
def __init__(self):
- # XXX The below needs to allow some customization.
- parser_args = dict(add_help=True)
- if self.description is not None:
- parser_args['description'] = self.description
- if self.epilog is not None:
- parser_args['epilog'] = self.epilog
- self.parser = argparse.ArgumentParser(**parser_args)
- self.subparsers = self.parser.add_subparsers(
- title="Sub-command to invoke")
+ super(LavaDispatcher, self).__init__()
prefixes = ['lava_tool']
if self.toolname is not None:
prefixes.append(self.toolname)
for prefix in prefixes:
- for entrypoint in pkg_resources.iter_entry_points(
- "%s.commands" % prefix):
- self.add_command_cls(entrypoint.load())
-
- def add_command_cls(self, command_cls):
- sub_parser = self.subparsers.add_parser(
- command_cls.get_name(),
- help=command_cls.get_help(),
- epilog=command_cls.get_epilog())
- sub_parser.set_defaults(command_cls=command_cls)
- sub_parser.set_defaults(sub_parser=sub_parser)
- command_cls.register_arguments(sub_parser)
-
- def dispatch(self, raw_args=None):
- args = self.parser.parse_args(raw_args)
- command = args.command_cls(self.parser, args)
- try:
- command.reparse_arguments(args.sub_parser, raw_args)
- except NotImplementedError:
- pass
- try:
- return command.invoke()
- except LavaCommandError as ex:
- print >> sys.stderr, "ERROR: %s" % (ex,)
- return 1
+ self.import_commands("%s.commands" % prefix)
+
+
+class LavaNonLegacyDispatcher(BaseDispatcher):
+ """
+ A dispatcher that wants to load only top-level commands,
+ not everything-and-the-kitchen-sink into one flat namespace
+
+ Available as `lava` command from shell
+ """
+
+ def __init__(self):
+ super(LavaNonLegacyDispatcher, self).__init__()
+ self.import_commands('lava.commands')
def run_with_dispatcher_class(cls):
@@ -83,3 +58,7 @@
def main():
run_with_dispatcher_class(LavaDispatcher)
+
+
+def main_nonlegacy():
+ run_with_dispatcher_class(LavaNonLegacyDispatcher)
=== modified file 'lava_tool/interface.py'
@@ -20,7 +20,11 @@
Interface for all lava-tool commands
"""
+import argparse
import inspect
+import logging
+import pkg_resources
+import sys
class LavaCommandError(Exception):
@@ -42,9 +46,11 @@
This method is called immediately after all arguments are parsed and
results are available. This gives subclasses a chance to configure
- themselves.
+ themselves. The provided parser is an instance of
+ argparse.ArgumentParser but it may not be the top-level parser (it will
+ be a parser specific for this command)
- The default implementation stores both arguments
+ The default implementation stores both arguments as instance attributes.
"""
self.parser = parser
self.args = args
@@ -109,3 +115,144 @@
exposed to the command line interface.
"""
pass
+
+
+class SubCommand(Command):
+ """
+ Base class for all command sub-command hubs.
+
+ This class is needed when one wants to get a custom level of command
+ options that can be freely extended, just like the top-level lava-tool
+ command.
+
+ For example, a SubCommand 'actions' will load additional commands from a
+ the 'lava.actions' namespace. For the end user it will be available as::
+
+ $ lava-tool foo actions xxx
+
+ Where xxx is one of the Commands that is declared to live in the namespace
+ provided by 'foo actions'.
+ """
+
+ namespace = None
+
+ @classmethod
+ def get_namespace(cls):
+ """
+ Return the pkg-resources entry point namespace name from which
+ sub-commands will be loaded.
+ """
+ return cls.namespace
+
+ @classmethod
+ def register_subcommands(cls, parser):
+ """
+ Register sub commands.
+
+ This method is called around the same time as register_arguments()
+ would be called for the plain command classes. It loads commands from
+ the entry point namespace returned by get_namespace() and registeres
+ them with a BaseDispatcher class. The parsers used by that dispatcher
+ are linked to the calling dispatcher parser so the new commands enrich
+ the top-level parser tree.
+
+ In addition, the provided parser stores a dispatcher instance in its
+ defaults. This is useful when one wants to access it later. To a final
+ command instance it shall be available as self.args.dispatcher.
+ """
+ dispatcher = BaseDispatcher(parser, name=cls.get_name())
+ namespace = cls.get_namespace()
+ if namespace is not None:
+ dispatcher.import_commands(namespace)
+ parser.set_defaults(dispatcher=dispatcher)
+
+
+class BaseDispatcher(object):
+ """
+ Class implementing command line interface for launch control
+ """
+
+ description = None
+ epilog = None
+
+ def __init__(self, parser=None, name=None):
+ self.parser = parser or self.construct_parser()
+ self.subparsers = self.parser.add_subparsers(
+ title="Sub-command to invoke")
+ self.name = name
+
+ def __repr__(self):
+ return "%r(name=%r)" % (self.__class__.__name__, self.name)
+
+ @classmethod
+ def construct_parser(cls):
+ """
+ Construct a parser for this dispatcher.
+
+ This is only used if the parser is not provided by the parent
+ dispatcher instance.
+ """
+ parser_args = dict(add_help=True)
+ if cls.description is not None:
+ parser_args['description'] = cls.description
+ if cls.epilog is not None:
+ parser_args['epilog'] = cls.epilog
+ return argparse.ArgumentParser(**parser_args)
+
+ def import_commands(self, entrypoint_name):
+ """
+ Import commands from given entry point namespace
+ """
+ logging.debug("Loading commands in entry point %r", entrypoint_name)
+ for entrypoint in pkg_resources.iter_entry_points(entrypoint_name):
+ self.add_command_cls(entrypoint.load())
+
+ def add_command_cls(self, command_cls):
+ """
+ Add a new command class to this dispatcher.
+
+ The command must be a subclass of Command or SubCommand.
+ """
+ logging.debug("Loading command class %r", command_cls)
+ # Create a sub-parser where the command/sub-command can register things.
+ sub_parser = self.subparsers.add_parser(
+ command_cls.get_name(),
+ help=command_cls.get_help(),
+ epilog=command_cls.get_epilog())
+ if issubclass(command_cls, SubCommand):
+ # Handle SubCommand somewhat different. Instead of calling
+ # register_arguments we call register_subcommands
+ command_cls.register_subcommands(sub_parser)
+ else:
+ # Handle plain commands easily by recording their commands in the
+ # dedicated sub-parser we've crated for them.
+ command_cls.register_arguments(sub_parser)
+ # In addition, since we don't want to require all sub-classes of
+ # Command to super-call register_arguments (everyone would forget
+ # this anyway) we manually register the command class for that
+ # sub-parser so that dispatch() can look it up later.
+ sub_parser.set_defaults(
+ command_cls=command_cls,
+ parser=sub_parser)
+
+ def dispatch(self, raw_args=None):
+ """
+ Dispatch a command with the specified arguments.
+
+ If arguments are left out they are looked up in sys.argv automatically
+ """
+ # First parse whatever input arguments we've got
+ args = self.parser.parse_args(raw_args)
+ # Then look up the command class and construct it with the parser it
+ # belongs to and the parsed arguments.
+ command = args.command_cls(args.parser, args)
+ try:
+ # Give the command a chance to re-parse command line arguments
+ command.reparse_arguments(args.parser, raw_args)
+ except NotImplementedError:
+ pass
+ try:
+ return command.invoke()
+ except LavaCommandError as ex:
+ print >> sys.stderr, "ERROR: %s" % (ex,)
+ return 1
=== modified file 'setup.py'
@@ -34,6 +34,9 @@
entry_points="""
[console_scripts]
lava-tool = lava_tool.dispatcher:main
+ lava = lava_tool.dispatcher:main_nonlegacy
+ [lava.commands]
+ help = lava_tool.commands.misc:help
[lava_tool.commands]
help = lava_tool.commands.misc:help
auth-add = lava_tool.commands.auth:auth_add