launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #11866
[Merge] lp:~allenap/maas/introducing-commandant into lp:maas
Gavin Panella has proposed merging lp:~allenap/maas/introducing-commandant into lp:maas.
Requested reviews:
MAAS Maintainers (maas-maintainers)
For more details, see:
https://code.launchpad.net/~allenap/maas/introducing-commandant/+merge/123992
--
https://code.launchpad.net/~allenap/maas/introducing-commandant/+merge/123992
Your team MAAS Maintainers is requested to review the proposed merge of lp:~allenap/maas/introducing-commandant into lp:maas.
=== modified file 'Makefile'
--- Makefile 2012-09-11 22:54:20 +0000
+++ Makefile 2012-09-12 15:31:22 +0000
@@ -19,7 +19,9 @@
build: \
bin/buildout \
bin/database \
- bin/maas bin/test.maas bin/test.maastesting \
+ bin/maas bin/test.maas \
+ bin/maascli bin/test.maascli \
+ bin/test.maastesting \
bin/twistd.pserv bin/test.pserv \
bin/twistd.txlongpoll \
bin/py bin/ipy \
@@ -62,6 +64,14 @@
$(buildout) install maas-test
@touch --no-create $@
+bin/maascli: bin/buildout buildout.cfg versions.cfg setup.py
+ $(buildout) install maascli
+ @touch --no-create $@
+
+bin/test.maascli: bin/buildout buildout.cfg versions.cfg setup.py
+ $(buildout) install maascli-test
+ @touch --no-create $@
+
bin/test.maastesting: bin/buildout buildout.cfg versions.cfg setup.py
$(buildout) install maastesting-test
@touch --no-create $@
=== modified file 'buildout.cfg'
--- buildout.cfg 2012-08-21 15:59:40 +0000
+++ buildout.cfg 2012-09-12 15:31:22 +0000
@@ -3,6 +3,8 @@
flake8
maas
maas-test
+ maascli
+ maascli-test
maastesting-test
pserv
pserv-test
@@ -106,6 +108,29 @@
extra-paths =
${maas:extra-paths}
+[maascli]
+recipe = zc.recipe.egg
+eggs =
+entry-points =
+ maascli=maascli:main
+extra-paths =
+ ${common:extra-paths}
+scripts =
+ maascli
+
+[maascli-test]
+recipe = zc.recipe.egg
+eggs =
+ ${maascli:eggs}
+ ${common:test-eggs}
+entry-points =
+ test.maascli=nose.core:TestProgram
+initialization =
+ sys.argv[1:1] = ["--where=src/maascli"]
+extra-paths = ${maascli:extra-paths}
+scripts =
+ test.maascli
+
[maastesting-test]
recipe = zc.recipe.egg
eggs =
=== added directory 'src/maascli'
=== added file 'src/maascli/__init__.py'
--- src/maascli/__init__.py 1970-01-01 00:00:00 +0000
+++ src/maascli/__init__.py 2012-09-12 15:31:22 +0000
@@ -0,0 +1,39 @@
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""The MAAS command-line interface."""
+
+from __future__ import (
+ absolute_import,
+ print_function,
+ unicode_literals,
+ )
+
+__metaclass__ = type
+__all__ = []
+
+from glob import iglob
+from os.path import (
+ dirname,
+ join,
+ )
+import sys
+
+# Add `lib` in the current directory into sys.path.
+sys.path[:0] = iglob(join(dirname(__file__), "lib"))
+
+from commandant import builtins
+from commandant.controller import CommandController
+
+
+def main(argv=sys.argv):
+ controller = CommandController(
+ program_name=__name__, program_version="2.0",
+ program_summary="Control MAAS using its API from the command-line.",
+ program_url="http://maas.ubuntu.com/")
+ # At this point controller.load_path(...) can be used to load commands
+ # from a pre-agreed location on the filesystem, so that the command set
+ # will grow and shrink with the installed packages.
+ controller.load_module(builtins)
+ controller.install_bzrlib_hooks()
+ controller.run(argv[1:])
=== added file 'src/maascli/__main__.py'
--- src/maascli/__main__.py 1970-01-01 00:00:00 +0000
+++ src/maascli/__main__.py 2012-09-12 15:31:22 +0000
@@ -0,0 +1,18 @@
+#!/usr/bin/env python2.7
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Command-line interface for MAAS."""
+
+from __future__ import (
+ absolute_import,
+ print_function,
+ unicode_literals,
+ )
+
+__metaclass__ = type
+
+from . import main
+
+
+main()
=== added directory 'src/maascli/lib'
=== added file 'src/maascli/lib/Makefile'
--- src/maascli/lib/Makefile 1970-01-01 00:00:00 +0000
+++ src/maascli/lib/Makefile 2012-09-12 15:31:22 +0000
@@ -0,0 +1,8 @@
+commandant:
+ bzr export $@ --revision 45 lp:commandant/commandant
+ $(RM) -rv $@/test*
+
+clean:
+ $(RM) -r commandant
+
+.PHONY: clean
=== added directory 'src/maascli/lib/commandant'
=== added file 'src/maascli/lib/commandant/__init__.py'
--- src/maascli/lib/commandant/__init__.py 1970-01-01 00:00:00 +0000
+++ src/maascli/lib/commandant/__init__.py 2012-09-12 15:31:22 +0000
@@ -0,0 +1,16 @@
+"""Commandant is a framework for building command-oriented tools.
+
+A command-oriented command-line program takes a command name as its first
+argument, which it finds and runs, passing along any subsequent
+arguments. Bazaar is command-oriented, for instance. Commandant is inspired
+by Bazaar's user interface and uses bzrlib in its internal implementation.
+
+Commandant is a command discovery and execution tool. Executables, such as
+shell scripts, can be used as commands. Commands can also be implemented in
+Python. These commands, along with help topics, are bundled together in a
+directory. Commandant, when pointed at the directory containing the commands,
+provides a Bazaar-like user interface to discover and run them.
+"""
+
+__version__ = "0.4.0"
+__version_info__ = (0, 4, 0)
=== added file 'src/maascli/lib/commandant/builtins.py'
--- src/maascli/lib/commandant/builtins.py 1970-01-01 00:00:00 +0000
+++ src/maascli/lib/commandant/builtins.py 2012-09-12 15:31:22 +0000
@@ -0,0 +1,167 @@
+# Commandant is a framework for building command-oriented tools.
+# Copyright (C) 2009-2010 Jamshed Kakar.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""Builtin commands."""
+
+from cStringIO import StringIO
+import os
+from platform import platform
+
+import bzrlib
+from bzrlib.commands import Command
+from bzrlib.option import Option
+
+import commandant
+from commandant.help_topics import HelpTopic, CommandHelpTopic
+from commandant.formatting import print_columns
+
+
+class cmd_version(Command):
+ """Show version of commandant."""
+
+ takes_options = [Option("short", help="Print just the version number.")]
+
+ def run(self, short=None):
+ """Print the version."""
+ if short:
+ print >>self.outf, "%s" % (self.controller.program_version,)
+ else:
+ print >>self.outf, "%s %s" % (self.controller.program_name,
+ self.controller.program_version)
+ python_path = os.path.dirname(os.__file__)
+ bzrlib_path = bzrlib.__path__[0]
+ commandant_path = os.path.abspath(commandant.__path__[0])
+ print >>self.outf, " Platform:", platform(aliased=1)
+ print >>self.outf, " Python standard library:", python_path
+ print >>self.outf, " bzrlib:", bzrlib_path
+ print >>self.outf, " commandant:", commandant_path
+
+
+class cmd_help(Command):
+ """Show help about a command or topic."""
+
+ aliases = ["?", "--help", "-?", "-h"]
+ takes_args = ["topic?"]
+ _see_also = ["topics"]
+
+ def run(self, topic=None):
+ """
+ Show help for the C{bzrlib.commands.Command} or L{HelpTopic} matching
+ C{name}.
+
+ @param topic: Optionally, the name of the topic to show. Default is
+ C{basic}.
+ """
+ if topic is None:
+ topic = "basic"
+ text = None
+ command = self.controller.get_command(topic)
+ help_topic = self.controller.get_help_topic(topic)
+ if help_topic:
+ text = help_topic.get_text().strip()
+ elif command:
+ help_topic = CommandHelpTopic(command)
+ help_topic.controller = self.controller
+ text = help_topic.get_text()
+ if text:
+ print >>self.outf, text
+ elif not (command or help_topic):
+ print >>self.outf, "%s is an unknown command or topic." % (topic,)
+
+
+class topic_basic(HelpTopic):
+ """Show basic help about this program."""
+
+ def get_summary(self):
+ """Get a short topic summary for use in a topic listing."""
+ return "Basic commands."
+
+ def get_text(self):
+ """Get topic content."""
+ return """\
+%(program-name)s -- %(program-summary)s
+%(program-url)s
+
+Basic commands:
+ %(program-name)s help commands List all commands
+ %(program-name)s help topics List all help topics
+""" % {"program-name": self.controller.program_name,
+ "program-summary": self.controller.program_summary,
+ "program-url": self.controller.program_url}
+
+
+class topic_commands(HelpTopic):
+ """List available commands with a short summary describing each one."""
+
+ def get_summary(self):
+ """Get a short topic summary for use in a topic listing."""
+ return "Basic help for all commands."
+
+ def get_text(self):
+ """Get topic content."""
+ stream = StringIO()
+ result = []
+ for name in self.controller.get_command_names():
+ command = self.controller.get_command(name)
+ help_topic = self.controller.get_help_topic(name)
+ if not help_topic and command:
+ help_topic = CommandHelpTopic(command)
+ if self.controller is not None:
+ help_topic.controller = self.controller
+ summary = ""
+ if help_topic:
+ summary = help_topic.get_summary()
+ if self.include_command(command):
+ result.append((name, summary))
+ result.sort(key=lambda item: item[0])
+ print_columns(stream, result)
+ return stream.getvalue()
+
+ def include_command(self, command):
+ """Return C{True} if C{command} is visible."""
+ return not command.hidden
+
+
+class topic_hidden_commands(topic_commands):
+ """List hidden commands with a short summary describing each one."""
+
+ def get_summary(self):
+ """Get a short topic summary for use in a topic listing."""
+ return "Basic help for hidden commands."
+
+ def include_command(self, command):
+ """Return C{True} if C{command} is hidden."""
+ return command.hidden
+
+
+class topic_topics(HelpTopic):
+ """List available help topics with a short summary describing each one."""
+
+ def get_summary(self):
+ """Get a short topic summary for use in a topic listing."""
+ return "Topics list."
+
+ def get_text(self):
+ """Get topic content."""
+ stream = StringIO()
+ command_names = self.controller.get_command_names()
+ help_topic_names = self.controller.get_help_topic_names()
+ result = [(name, self.controller.get_help_topic(name).get_summary())
+ for name in help_topic_names if name not in command_names]
+ result.sort(key=lambda item: item[0])
+ print_columns(stream, result)
+ return stream.getvalue()
=== added file 'src/maascli/lib/commandant/commands.py'
--- src/maascli/lib/commandant/commands.py 1970-01-01 00:00:00 +0000
+++ src/maascli/lib/commandant/commands.py 2012-09-12 15:31:22 +0000
@@ -0,0 +1,121 @@
+# Commandant is a framework for building command-oriented tools.
+# Copyright (C) 2009-2010 Jamshed Kakar.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""Infrastructure extends C{bzrlib.commands.Command} to support executables."""
+
+import os
+import sys
+
+from twisted.internet.defer import Deferred
+
+from bzrlib.commands import Command
+
+
+class ExecutableCommand(Command):
+ """Specialized command runs an executable program."""
+
+ path = None
+
+ def run_argv_aliases(self, argv, alias_argv=None):
+ """
+ Disable proper handling of argv and aliases so that arguments can be
+ passed directly to the executable.
+ """
+ self.run(argv)
+
+ def run(self, argv):
+ """
+ Run the executable, passing whatever arguments were passed to the
+ command.
+ """
+ if argv:
+ os.system("%s %s" % (self.path, " ".join(argv)))
+ else:
+ os.system(self.path)
+
+
+class TwistedCommand(Command):
+ """A command that runs with a Twisted reactor."""
+
+ _return_value = None
+ _failure_value = None
+
+ def get_reactor(self):
+ """Get the Twisted reactor to use when running this command."""
+ from twisted.internet import reactor
+ return reactor
+
+ def run_argv_aliases(self, argv, alias_argv=None):
+ """Start a reactor for the command to run in."""
+ self._start_reactor(argv, alias_argv)
+ if self._failure_value is not None:
+ type, value, traceback = self._failure_value
+ raise type, value, traceback
+ return self._return_value
+
+ def _start_reactor(self, argv, alias_argv):
+ """Start a reactor and queue a call to run the command."""
+ reactor = self.get_reactor()
+ reactor.callLater(0, self._run_command, argv, alias_argv)
+ reactor.run()
+ return self._return_value
+
+ def _run_command(self, argv, alias_argv):
+ """Run the command and stop the reactor when it completes."""
+ subclass = super(TwistedCommand, self)
+ try:
+ result = subclass.run_argv_aliases(argv, alias_argv)
+ except:
+ self._failure_value = sys.exc_info()
+ return self._stop_reactor(self._failure_value)
+ else:
+ return self._stop_reactor(result)
+
+ def _capture_return_value(self, result):
+ """Store the return value after running the command."""
+ self._return_value = result
+
+ def _capture_failure_value(self, failure):
+ """Store the failure value after running the command."""
+ self._failure_value = (failure.type, failure.value, failure.tb)
+
+ def _stop_reactor(self, result):
+ """Stop the reactor."""
+ reactor = self.get_reactor()
+ if isinstance(result, Deferred):
+ result.addErrback(self._capture_failure_value)
+ result.addCallback(self._capture_return_value)
+ # Use callLater to stop the reactor so that application code can
+ # add callbacks to the Deferred after its been returned by the run
+ # method.
+ result.addCallback(
+ lambda ignored: reactor.callLater(0, reactor.stop))
+ else:
+ self._capture_return_value(result)
+ reactor.callLater(0, reactor.stop)
+
+ def run(self):
+ """Actually run the command.
+
+ This method is invoked inside a running Twisted reactor, with the
+ options and arguments bound to keyword parameters.
+
+ Return a C{Deferred} or None if the command was successful. It's okay
+ for this method to allow an exception to raise up.
+ """
+ raise NotImplementedError("Command '%r' needs to be implemented."
+ % self.name())
=== added file 'src/maascli/lib/commandant/controller.py'
--- src/maascli/lib/commandant/controller.py 1970-01-01 00:00:00 +0000
+++ src/maascli/lib/commandant/controller.py 2012-09-12 15:31:22 +0000
@@ -0,0 +1,265 @@
+# Commandant is a framework for building command-oriented tools.
+# Copyright (C) 2009-2010 Jamshed Kakar.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""Infrastructure to run C{bzrlib.commands.Command}s and L{HelpTopic}s."""
+
+import os
+import shutil
+import stat
+import sys
+import tempfile
+
+import bzrlib.ui
+from bzrlib.commands import run_bzr, Command
+
+from commandant import __version__
+from commandant.commands import ExecutableCommand
+from commandant.help_topics import FileHelpTopic
+
+
+DEFAULT_PROGRAM_NAME = "commandant"
+DEFAULT_PROGRAM_VERSION = __version__
+DEFAULT_PROGRAM_SUMMARY = "A framework for building command-oriented tools."
+DEFAULT_PROGRAM_URL = "http://launchpad.net/commandant"
+
+
+class CommandRegistry(object):
+
+ def __init__(self):
+ self._commands = {}
+
+ def install_bzrlib_hooks(self):
+ """
+ Register this controller with C{Command.hooks} so that the controller
+ can take advantage of Bazaar's command infrastructure. C{bzrlib.ui}
+ is initialized for use in a terminal during this process.
+
+ L{_list_commands} and L{_get_command} are registered as callbacks for
+ the C{list_commands} and C{get_commands} hooks, respectively.
+ """
+ Command.hooks.install_named_hook(
+ "list_commands", self._list_commands, "commandant commands")
+ Command.hooks.install_named_hook(
+ "get_command", self._get_command, "commandant commands")
+ bzrlib.ui.ui_factory = bzrlib.ui.make_ui_for_terminal(
+ sys.stdin, sys.stdout, sys.stderr)
+
+ def _list_commands(self, names):
+ """
+ Hook to find C{bzrlib.commands.Command} names is called by C{bzrlib}.
+
+ @param names: A set of C{bzrlib.commands.Command} names to update with
+ names from this controller.
+ """
+ names.update(self._commands.iterkeys())
+ return names
+
+ def _get_command(self, command, name):
+ """
+ Hook to get the C{bzrlib.commands.Command} for C{name} is called by
+ C{bzrlib}.
+
+ @param command: A C{bzrlib.commands.Command}, or C{None}, to be
+ returned if a command matching C{name} can't be found.
+ @param name: The name of the C{bzrlib.commands.Command} to retrieve.
+ @return: The C{bzrlib.commands.Command} from the index or C{command}
+ if one isn't available for C{name}.
+ """
+ try:
+ local_command = self._commands[name]()
+ except KeyError:
+ return command
+ local_command.controller = self
+ return local_command
+
+ def register_command(self, name, command_class):
+ """Register a C{bzrlib.commands.Command} with this controller.
+
+ @param name: The name to register the command with.
+ @param command_class: A type object, typically a subclass of
+ C{bzrlib.commands.Command} to use when the command is invoked.
+ """
+ self._commands[name] = command_class
+
+
+class HelpTopicRegistry(object):
+
+ def __init__(self):
+ self._help_topics = {}
+
+ def register_help_topic(self, name, help_topic_class):
+ """Register a C{bzrlib.commands.Command} to this controller.
+
+ @param name: The name to register the command with.
+ @param command_class: A type object, typically a subclass of
+ C{bzrlib.commands.Command} to use when the command is invoked.
+ """
+ self._help_topics[name] = help_topic_class
+
+ def get_help_topic_names(self):
+ """Get a C{set} of help topic names."""
+ return set(self._help_topics.iterkeys())
+
+ def get_help_topic(self, name):
+ """
+ Get the help topic matching C{name} or C{None} if a match isn't found.
+ """
+ try:
+ help_topic = self._help_topics[name]()
+ except KeyError:
+ return None
+ help_topic.controller = self
+ return help_topic
+
+
+class CommandDiscoveryMixin(object):
+
+ def load_path(self, path):
+ """Load C{bzrlib.commands.Command}s and L{HelpTopic}s from C{path}.
+
+ Python files foundin C{path} are loaded and
+ C{bzrlib.commands.Command}s and L{HelpTopic}s within are loaded.
+ L{ExecutableCommand}s are created for executable programs in C{path}
+ and L{FileHelpTopic}s are created for text file help topics.
+ """
+ package_path = tempfile.mkdtemp()
+ try:
+ for filename in os.listdir(path):
+ file_path = os.path.join(path, filename)
+ if filename.endswith("~") or not os.path.exists(file_path):
+ continue
+ file_mode = os.stat(file_path)[0]
+ if not os.path.isfile(file_path):
+ continue
+ if file_mode | stat.S_IEXEC == file_mode:
+ sanitized_name = filename.replace("_", "-")
+ executable = type(
+ "Executable", (ExecutableCommand,),
+ {"path": file_path})
+ self.register_command(sanitized_name, executable)
+ elif filename.endswith(".py"):
+ command_module = import_module(filename, file_path,
+ package_path)
+ self.load_module(command_module)
+ elif filename.endswith(".txt"):
+ sanitized_name = filename.replace("_", "-")[:-4]
+ topic = type(
+ "Topic", (FileHelpTopic,),
+ {"path": file_path})
+ self.register_help_topic(sanitized_name, topic)
+ finally:
+ shutil.rmtree(package_path)
+
+ def load_module(self, module):
+ """Load C{bzrlib.commands.Command}s and L{HelpTopic}s from C{module}.
+
+ Objects found in the module with names that start with C{cmd_} are
+ treated as C{bzrlib.commands.Command}s and objects with names that
+ start with C{topic_} are treated as L{HelpTopic}s.
+ """
+ for name in module.__dict__:
+ if name.startswith("cmd_"):
+ sanitized_name = name[4:].replace("_", "-")
+ self.register_command(sanitized_name, module.__dict__[name])
+ elif name.startswith("topic_"):
+ sanitized_name = name[6:].replace("_", "-")
+ self.register_help_topic(sanitized_name, module.__dict__[name])
+
+ def get_command_names(self):
+ """
+ Get the C{set} of C{bzrlib.commands.Command} names registered with
+ this controller.
+
+ This method is equivalent to C{bzrlib.commands.all_command_names} when
+ the controller is installed with C{bzrlib}.
+ """
+ return set(self._commands.iterkeys())
+
+ def get_command(self, name):
+ """Get the C{bzrlib.commands.Command} registered for C{name}.
+
+ @return: The C{bzrlib.commands.Command} from the index or C{None} if
+ one isn't available for C{name}.
+ """
+ return self._get_command(None, name)
+
+
+class CommandExecutionMixin(object):
+
+ def run(self, argv):
+ """Run the C{bzrlib.commands.Command} specified in C{argv}.
+
+ @raise BzrCommandError: Raised if a matching command can't be found.
+ """
+ run_bzr(argv)
+
+
+class CommandController(CommandRegistry, HelpTopicRegistry,
+ CommandDiscoveryMixin, CommandExecutionMixin):
+ """C{bzrlib.commands.Command} discovery and execution controller.
+
+ A L{CommandController} is a container for named C{bzrlib.commands.Command}s
+ and L{HelpTopic}s types. The L{load_module} and L{load_path} methods load
+ C{bzrlib.commands.Command} and L{HelpTopic} types from modules and from
+ the file system. The L{register_command} and L{register_help_topic}
+ methods register C{bzrlib.commands.Command}s and L{HelpTopic}s types with
+ the controller.
+
+ A controller is an execution engine for commands. The L{run} method
+ accepts command line arguments, finds a matching command, and runs it.
+ """
+
+ def __init__(self, program_name=None, program_version=None,
+ program_summary=None, program_url=None):
+ CommandRegistry.__init__(self)
+ HelpTopicRegistry.__init__(self)
+ self.program_name = program_name or DEFAULT_PROGRAM_NAME
+ self.program_version = program_version or DEFAULT_PROGRAM_VERSION
+ self.program_summary = program_summary or DEFAULT_PROGRAM_SUMMARY
+ self.program_url = program_url or DEFAULT_PROGRAM_URL
+
+
+def import_module(filename, file_path, package_path):
+ """Import a module and make it a child of C{commandant_command}.
+
+ The module source in C{filename} at C{file_path} is copied to a temporary
+ directory, a Python package called C{commandant_command}.
+
+ @param filename: The name of the module file.
+ @param file_path: The path to the module file.
+ @param package_path: The path for the new C{commandant_command} package.
+ @return: The new module.
+ """
+ module_path = os.path.join(package_path, "commandant_command")
+ if not os.path.exists(module_path):
+ os.mkdir(module_path)
+
+ init_path = os.path.join(module_path, "__init__.py")
+ open(init_path, "w").close()
+
+ source_code = open(file_path, "r").read()
+ module_file_path = os.path.join(module_path, filename)
+ module_file = open(module_file_path, "w")
+ module_file.write(source_code)
+ module_file.close()
+
+ name = filename[:-3]
+ sys.path.append(package_path)
+ try:
+ return __import__("commandant_command.%s" % (name,), fromlist=[name])
+ finally:
+ sys.path.pop()
=== added file 'src/maascli/lib/commandant/entry_point.py'
--- src/maascli/lib/commandant/entry_point.py 1970-01-01 00:00:00 +0000
+++ src/maascli/lib/commandant/entry_point.py 2012-09-12 15:31:22 +0000
@@ -0,0 +1,48 @@
+# Commandant is a framework for building command-oriented tools.
+# Copyright (C) 2009-2010 Jamshed Kakar.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""Bootstrap code starts and runs Commandant."""
+
+from commandant import builtins
+from commandant.errors import UsageError
+from commandant.controller import CommandController
+
+
+def main(argv):
+ """Run the command named in C{argv}.
+
+ If a command name isn't provided the C{help} command is shown.
+
+ @raises UsageError: Raised if too few arguments are provided.
+ @param argv: A list command-line arguments. The first argument should be
+ the path to C{bzrlib.commands.Command}s and L{HelpTopic}s to load and
+ the second argument should be the name of the command to run. Any
+ further arguments are passed to the command.
+ """
+ if len(argv) < 2 or (len(argv) > 1 and argv[1].startswith("-")):
+ raise UsageError(
+ "You must provide a path to the commands you want to run.")
+ elif len(argv) < 3:
+ argv.append("help")
+
+ # Load commands topic from the user-supplied path after loading builtins,
+ # in case any of the user's commands or topics replace builtin ones.
+ controller = CommandController()
+ controller.load_module(builtins)
+ controller.load_path(argv[1])
+ controller.install_bzrlib_hooks()
+ controller.run(argv[2:])
=== added file 'src/maascli/lib/commandant/errors.py'
--- src/maascli/lib/commandant/errors.py 1970-01-01 00:00:00 +0000
+++ src/maascli/lib/commandant/errors.py 2012-09-12 15:31:22 +0000
@@ -0,0 +1,28 @@
+# Commandant is a framework for building command-oriented tools.
+# Copyright (C) 2009-2010 Jamshed Kakar.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""Exception classes."""
+
+
+class CommandantError(StandardError):
+ """Base class for Commandant exceptions."""
+ pass
+
+
+class UsageError(CommandantError):
+ """Raised when too few command-line arguments are provided."""
+ pass
=== added file 'src/maascli/lib/commandant/formatting.py'
--- src/maascli/lib/commandant/formatting.py 1970-01-01 00:00:00 +0000
+++ src/maascli/lib/commandant/formatting.py 2012-09-12 15:31:22 +0000
@@ -0,0 +1,59 @@
+# Commandant is a framework for building command-oriented tools.
+# Copyright (C) 2009-2010 Jamshed Kakar.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""Infrastructure for pretty-printing formatted output."""
+
+
+def print_columns(outf, rows, shrink_index=None, max_width=78, padding=2):
+ """Calculate optimal column widths and print C{rows} to C{outf}.
+
+ @param outf: The stream to write to.
+ @param rows: A list of rows to print. Each row is a tuple of columns.
+ All rows must contain the same number of columns.
+ @param shrink_index: The index of the column to shrink, if the columns
+ provided exceed C{max_width}. Shrinking is disabled by default.
+ @param max_width: The maximum number of characters per line. Defaults to
+ 78, though it isn't enforced unless C{shrink_index} is specified.
+ @param padding: The number of blank characters to output between columns.
+ Defaults to 2.
+ """
+ if not rows:
+ return
+
+ widths = []
+ for row in rows:
+ if not widths:
+ widths = [len(column) for i, column in enumerate(row)]
+ else:
+ widths = [
+ max(widths[i], len(column)) for i, column in enumerate(row)]
+
+ if shrink_index is not None:
+ fixed_width = sum(width + padding for i, width in enumerate(widths)
+ if i != shrink_index)
+ if fixed_width + widths[shrink_index] > max_width:
+ widths[shrink_index] = max_width - fixed_width
+
+ padding_space = "".ljust(padding)
+ for row in rows:
+ output = []
+ for i, column in enumerate(row):
+ text = column[:widths[i]].ljust(widths[i])
+ if (i + 1 == len(row)):
+ text = text.strip()
+ output.append(text)
+ print >>outf, padding_space.join(output)
=== added file 'src/maascli/lib/commandant/help_topics.py'
--- src/maascli/lib/commandant/help_topics.py 1970-01-01 00:00:00 +0000
+++ src/maascli/lib/commandant/help_topics.py 2012-09-12 15:31:22 +0000
@@ -0,0 +1,131 @@
+# Commandant is a framework for building command-oriented tools.
+# Copyright (C) 2009-2010 Jamshed Kakar.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""Infrastructure for building L{HelpTopic} components."""
+
+from inspect import getdoc
+
+
+class HelpTopic(object):
+ """A help topic."""
+
+ controller = None
+
+ def get_summary(self):
+ """Get a short topic summary for use in a topic listing."""
+ raise NotImplementedError("Must be implemented by sub-class.")
+
+ def get_text(self):
+ """Get topic content."""
+ raise NotImplementedError("Must be implemented by sub-class.")
+
+
+class DocstringHelpTopic(object):
+ """A help topic that loads content from its docstring."""
+
+ def __init__(self):
+ super(DocstringHelpTopic, self).__init__()
+ self._summary = None
+ self._text = None
+
+ def _get_docstring(self):
+ """Get the docstring for this help topic."""
+ return getdoc(self)
+
+ def _load_help_text(self):
+ """Load summary and text content."""
+ docstring = self._get_docstring()
+ if not docstring:
+ return "", ""
+ else:
+ return self._load_docstring(docstring)
+
+ def _load_docstring(self, docstring):
+ """Load summary and text content from a docstring."""
+ lines = [line for line in docstring.splitlines()]
+ return lines[0], "\n".join(lines[1:]).strip()
+
+ def get_summary(self):
+ """Get a short topic summary for use in a topic listing."""
+ if self._summary is None:
+ (self._summary, self._text) = self._load_help_text()
+ return self._summary
+
+ def get_text(self):
+ """Get topic content."""
+ if self._text is None:
+ (self._summary, self._text) = self._load_help_text()
+ return self._text
+
+
+class FileHelpTopic(HelpTopic):
+ """A help topic that loads content from a file."""
+
+ path = None
+
+ def __init__(self):
+ super(FileHelpTopic, self).__init__()
+ self._summary = None
+ self._text = None
+
+ def _load(self):
+ """Load and cache summary and text content."""
+ file = open(self.path, "r")
+ self._summary = file.readline().strip()
+ self._text = file.read().strip()
+ # FIXME Test this.
+ file.close()
+
+ def get_summary(self):
+ """Get a short topic summary for use in a topic listing."""
+ if self._summary is None:
+ self._load()
+ return self._summary
+
+ def get_text(self):
+ """Get topic content."""
+ if self._text is None:
+ self._load()
+ return self._text
+
+
+class CommandHelpTopic(DocstringHelpTopic):
+ """
+ A help topic that loads content from a C{bzrlib.commands.Command}
+ docstring.
+ """
+
+ def __init__(self, command):
+ super(CommandHelpTopic, self).__init__()
+ self.command = command
+
+ def _get_docstring(self):
+ """Get the docstring for the command."""
+ return getdoc(self.command)
+
+ def _load_docstring(self, docstring):
+ """Load summary and text content from a docstring."""
+ lines = [line for line in docstring.splitlines()]
+ summary = lines[0]
+ try:
+ text = self.command.get_help_text().strip()
+ text = text.replace("bzr", self.controller.program_name)
+ except NotImplementedError:
+ pass
+ else:
+ return summary, text
+ return summary, ""
Follow ups