Source code for lmi.scripts.common.command.base

# Copyright (c) 2013, Red Hat, Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and documentation are
# those of the authors and should not be interpreted as representing official
# policies, either expressed or implied, of the FreeBSD Project.
#
# Authors: Michal Minar <miminar@redhat.com>
#
"""
Module defining base command class for all possible commands of ``lmi``
meta-command.
"""

import abc
import re

# regular expression matching leading whitespaces not until the first line
# containing non-white-spaces
RE_LSPACES = re.compile(r'\A(\s*$.)*', re.DOTALL | re.MULTILINE)

[docs]class LmiBaseCommand(object): """ Abstract base class for all commands handling command line arguemtns. Instances of this class are organized in a tree with root element being the ``lmi`` meta-command (if not running in interactive mode). Each such instance can have more child commands if its :py:meth:`LmiBaseCommand.is_end_point` method return ``False``. Each has one parent command except for the top level one, whose :py:attr:`parent` property returns ``None``. Set of commands is organized in a tree, where each command (except for the root) has its own parent. :py:meth:`is_end_point` method distinguish leaves from nodes. The path from root command to the leaf is a sequence of commands passed to command line. If the :py:meth:`LmiBaseCommand.has_own_usage` returns ``True``, the parent command won't process the whole command line and the remainder will be passed as a second argument to the :py:meth:`LmiBaseCommand.run` method. :param app: Main application object. :param string cmd_name: Name of command. :param parent: Parent command. :type parent: :py:class:`LmiBaseCommand` """ __metaclass__ = abc.ABCMeta @classmethod
[docs] def get_description(cls): """ Return description for this command. This is usually a first line of documentation string of a class. :rtype: string """ if cls.__doc__ is None: return "" return cls.__doc__.strip().split("\n", 1)[0]
@classmethod
[docs] def is_end_point(cls): """ :returns: ``True``, if this command parses the rest of command line and can not have any child subcommands. :rtype: boolean """ return True
@classmethod
[docs] def has_own_usage(cls): """ :returns: ``True``, if this command has its own usage string, which is returned by :py:meth:`LmiBaseCommand.get_description`. Otherwise the parent command must be queried. :rtype: boolean """ return False
@classmethod
[docs] def child_commands(cls): """ Abstract class method returning dictionary of child commands with structure: :: { "command-name" : cmd_factory, ... } Dictionary contains just a direct children (commands, which may immediately follow this particular command on command line). """ raise NotImplementedError("child_commands() method must be overriden" " in a subclass")
def __init__(self, app, cmd_name, parent=None): if not isinstance(cmd_name, basestring): raise TypeError('cmd_name must be a string') if parent is not None and not isinstance(parent, LmiBaseCommand): raise TypeError('parent must be an LmiBaseCommand instance') self._app = app self._cmd_name = cmd_name.strip() self._parent = parent @property
[docs] def app(self): """ Return application object. """ return self._app
@property
[docs] def parent(self): """ Return parent command. """ return self._parent
@property
[docs] def cmd_name(self): """ Name of this subcommand as a single word. """ return self._cmd_name
@property
[docs] def cmd_full_name(self): """ Name of this subcommand with all prior commands included. It's the sequence of commands as given on command line up to this subcommand without any options present. In interactive mode this won't contain the name of binary (``sys.argv[0]``). :returns: Concatenation of all preceding commands with :py:attr:`cmd_name`. :rtype: string """ return ' '.join(self.cmd_name_args)
@property
[docs] def cmd_name_args(self): """ The same as :py:attr:`cmd_full_name`, except the result is a list of subcommands. :returns: List of command strings as given on command line up to this command. :rtype: list """ if self.parent is not None: return self.parent.cmd_name_args + [self.cmd_name] return [self._cmd_name]
@property
[docs] def docopt_cmd_name_args(self): """ Arguments array for docopt parser. Similar to :py:meth:`LmiBaseCommand.cmd_name_args` except for the leading binary name, which is omitted here. :rtype: list """ if self.app.interactive_mode: return self.cmd_name_args return self.cmd_name_args[1:]
[docs] def get_usage(self, proper=False): """ Get command usage. Return value of this function is used by docopt parser as usage string. Command tree is traversed upwards until command with defined usage string is found. End point commands (leaves) require manually written usage, so the first command in the sequence of parents with own usage string is obtained and its usage returned. For nodes missing own usage string this can be generated based on its subcommands. :param boolean proper: Says, whether the usage string written manually is required or not. It applies only to node (not a leaf) commands without its own usage string. """ if self.is_end_point() or self.has_own_usage() or proper: # get proper (manually written) usage, also referred as *own* cmd = self while not cmd.has_own_usage() and cmd.parent is not None: cmd = cmd.parent if cmd.__doc__ is None: docstr = "Usage: %s\n" % self.cmd_full_name else: docstr = ( ( cmd.__doc__.rstrip() % {'cmd' : cmd.cmd_full_name } ) + "\n") match = RE_LSPACES.match(docstr) if match: # strip leading spaces docstr = docstr[match.end(0):] else: # generate usage string from what is known, applies to nodes # without own usage hlp = [] if self.get_description(): hlp.append(self.get_description()) hlp.append("") hlp.append("Usage:") hlp.append(" %s (--help | <command> [<args> ...])" % self.cmd_full_name) hlp.append("") hlp.append("Commands:") cmd_max_len = max(len(c) for c in self.child_commands()) for name, cmd in sorted(self.child_commands().items()): hlp.append((" %%-%ds %%s" % cmd_max_len) % (name, cmd.get_description())) docstr = "\n".join(hlp) + "\n" return docstr
@abc.abstractmethod
[docs] def run(self, args): """ Handle the command line arguments. If this is not an end point command, it will pass the unhandled arguments to one of it's child commands. So the arguments are processed recursively by the instances of this class. :param list args: Arguments passed to the command line that were not yet parsed. It's the contents of ``sys.argv`` (if in non-interactive mode) from the current command on. :returns: Exit code of application. This maybe also be a boolean value or ``None``. ``None`` and ``True`` are treated as a success causing exit code to be 0. :rtype: integer """ raise NotImplementedError("run method must be overriden in subclass")