From patchwork Wed Dec 14 11:50:19 2011 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Zygmunt Krynicki X-Patchwork-Id: 5691 Return-Path: X-Original-To: patchwork@peony.canonical.com Delivered-To: patchwork@peony.canonical.com Received: from fiordland.canonical.com (fiordland.canonical.com [91.189.94.145]) by peony.canonical.com (Postfix) with ESMTP id 45C9223E18 for ; Wed, 14 Dec 2011 11:50:21 +0000 (UTC) Received: from mail-ey0-f180.google.com (mail-ey0-f180.google.com [209.85.215.180]) by fiordland.canonical.com (Postfix) with ESMTP id 3B07AA183F4 for ; Wed, 14 Dec 2011 11:50:21 +0000 (UTC) Received: by mail-ey0-f180.google.com with SMTP id k10so412041eaa.11 for ; Wed, 14 Dec 2011 03:50:21 -0800 (PST) Received: by 10.205.129.137 with SMTP id hi9mr535682bkc.90.1323863421064; Wed, 14 Dec 2011 03:50:21 -0800 (PST) X-Forwarded-To: linaro-patchwork@canonical.com X-Forwarded-For: patch@linaro.org linaro-patchwork@canonical.com Delivered-To: patches@linaro.org Received: by 10.205.129.2 with SMTP id hg2cs6126bkc; Wed, 14 Dec 2011 03:50:20 -0800 (PST) Received: by 10.227.206.4 with SMTP id fs4mr1704537wbb.21.1323863419576; Wed, 14 Dec 2011 03:50:19 -0800 (PST) Received: from indium.canonical.com (indium.canonical.com. [91.189.90.7]) by mx.google.com with ESMTPS id gh20si1090588wbb.53.2011.12.14.03.50.19 (version=TLSv1/SSLv3 cipher=OTHER); Wed, 14 Dec 2011 03:50:19 -0800 (PST) Received-SPF: pass (google.com: best guess record for domain of bounces@canonical.com designates 91.189.90.7 as permitted sender) client-ip=91.189.90.7; Authentication-Results: mx.google.com; spf=pass (google.com: best guess record for domain of bounces@canonical.com designates 91.189.90.7 as permitted sender) smtp.mail=bounces@canonical.com Received: from ackee.canonical.com ([91.189.89.26]) by indium.canonical.com with esmtp (Exim 4.71 #1 (Debian)) id 1RanLn-000869-9l for ; Wed, 14 Dec 2011 11:50:19 +0000 Received: from ackee.canonical.com (localhost [127.0.0.1]) by ackee.canonical.com (Postfix) with ESMTP id 39D91E032C for ; Wed, 14 Dec 2011 11:50:19 +0000 (UTC) MIME-Version: 1.0 X-Launchpad-Project: lava-tool X-Launchpad-Branch: ~linaro-validation/lava-tool/trunk X-Launchpad-Message-Rationale: Subscriber X-Launchpad-Branch-Revision-Number: 166 X-Launchpad-Notification-Type: branch-revision To: Linaro Patch Tracker From: noreply@launchpad.net Subject: [Branch ~linaro-validation/lava-tool/trunk] Rev 166: Merge sub-command and 'lava' command support Message-Id: <20111214115019.29989.37108.launchpad@ackee.canonical.com> Date: Wed, 14 Dec 2011 11:50:19 -0000 Reply-To: noreply@launchpad.net Sender: bounces@canonical.com Errors-To: bounces@canonical.com Precedence: bulk X-Generated-By: Launchpad (canonical.com); Revision="14487"; Instance="launchpad-lazr.conf" X-Launchpad-Hash: 5e3a667330126f0c6c005185eb8018cd8debe97d 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 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 === 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