diff mbox

[Branch,~linaro-validation/lava-tool/trunk] Rev 166: Merge sub-command and 'lava' command support

Message ID 20111214115019.29989.37108.launchpad@ackee.canonical.com
State Accepted
Headers show

Commit Message

Zygmunt Krynicki Dec. 14, 2011, 11:50 a.m. UTC
Merge authors:
  Zygmunt Krynicki (zkrynicki)
Related merge proposals:
  https://code.launchpad.net/~zkrynicki/lava-tool/next/+merge/85586
  proposed by: Zygmunt Krynicki (zkrynicki)
------------------------------------------------------------
revno: 166 [merge]
committer: Zygmunt Krynicki <zygmunt.krynicki@linaro.org>
branch nick: trunk
timestamp: Wed 2011-12-14 12:42:57 +0100
message:
  Merge sub-command and 'lava' command support
modified:
  lava_tool/dispatcher.py
  lava_tool/interface.py
  setup.py


--
lp:lava-tool
https://code.launchpad.net/~linaro-validation/lava-tool/trunk

You are subscribed to branch lp:lava-tool.
To unsubscribe from this branch go to https://code.launchpad.net/~linaro-validation/lava-tool/trunk/+edit-subscription
diff mbox

Patch

=== modified file 'lava_tool/dispatcher.py'
--- lava_tool/dispatcher.py	2011-06-27 17:23:41 +0000
+++ lava_tool/dispatcher.py	2011-12-13 16:19:52 +0000
@@ -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'
--- lava_tool/interface.py	2011-06-17 13:11:50 +0000
+++ lava_tool/interface.py	2011-12-13 16:20:05 +0000
@@ -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'
--- setup.py	2011-06-23 10:27:36 +0000
+++ setup.py	2011-12-13 16:18:55 +0000
@@ -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