diff options
author | Doug Hellmann <doug.hellmann@gmail.com> | 2012-08-02 10:05:36 -0700 |
---|---|---|
committer | Doug Hellmann <doug.hellmann@gmail.com> | 2012-08-02 10:05:36 -0700 |
commit | 22d32f60d3481b89a366fcf3416992618cb5a9e3 (patch) | |
tree | 1dce3d965f14f8b9dc27e5ba9b56e2b084014638 | |
parent | ce0aa4609c76e60ea580afcfee1f8103931b0fee (diff) | |
parent | 8896e385ebc963145677303bf8d6eb134dcf582c (diff) | |
download | cliff-tablib-22d32f60d3481b89a366fcf3416992618cb5a9e3.tar.gz |
Merge pull request #1 from dhellmann/feature/package-tablib-support1.0
Start cliff-tablib repo
36 files changed, 88 insertions, 2100 deletions
@@ -1,14 +1,16 @@ ======================================================= - cliff -- Command Line Interface Formulation Framework + cliff-tablib -- Formatting Extensions for Cliff ======================================================= .. image:: https://secure.travis-ci.org/dhellmann/cliff.png?branch=master -cliff is a framework for building command line programs. It uses -setuptools entry points to provide subcommands, output formatters, and -other extensions. +cliff-tablib is a set of formatter extensions for producing JSON, +YAML, and HTML output in programs created with the cliff +framework. Installing cliff-tablib activates these formatters for any +cliff-based programs automatically. Documentation ============= -Documentation for cliff is hosted on readthedocs.org at http://readthedocs.org/docs/cliff/en/latest/ +Documentation for cliff-tablib is hosted on readthedocs.org at +http://cliff-tablib.readthedocs.org/ diff --git a/announce.rst b/announce.rst index 91bcfac..170703f 100644 --- a/announce.rst +++ b/announce.rst @@ -1,6 +1,6 @@ -====================================================================== - cliff -- Command Line Interface Formulation Framework -- version 1.0 -====================================================================== +================================================= + cliff-tablib -- Formatting Extensions for Cliff +================================================= .. tags:: python cliff release DreamHost @@ -8,25 +8,22 @@ cliff is a framework for building command line programs. It uses setuptools entry points to provide subcommands, output formatters, and other extensions. +cliff-tablib is a set of formatter extensions for producing JSON, +YAML, and HTML output. Installing cliff-tablib activates these +formatters for any cliff-based programs automatically. + What's New In This Release? =========================== -- Add trailing newlines after output from tablib-based formatters - (JSON, YAML, and HTML). Contributed by Matt Joyce. -- Some PEP-8 fixes. -- Refactor the API in ``Command`` to add ``take_action()`` - and make ``run()`` a concrete method. Existing users should only - need to rename ``run()`` to ``take_action()`` since the function - signatures have not changed. -- In ``Lister`` and ``ShowOne`` use ``take_action()`` instead of - ``get_data()``. +- This is the first release in which the tablib extensions + are being released as a separate project from cliff. Documentation ============= -`Documentation for cliff`_ is hosted on `readthedocs.org`_ +`Documentation for cliff-tablib`_ is hosted on `readthedocs.org`_ -.. _Documentation for cliff: http://readthedocs.org/docs/cliff/en/latest/ +.. _Documentation for cliff-tablib: http://readthedocs.org/docs/cliff-tablib/en/latest/ .. _readthedocs.org: http://readthedocs.org @@ -35,9 +32,9 @@ Installation Use pip:: - $ pip install cliff + $ pip install cliff-tablib See `the installation guide`_ for more details. -.. _the installation guide: http://cliff.readthedocs.org/en/latest/install.html +.. _the installation guide: http://cliff-tablib.readthedocs.org/en/latest/install.html diff --git a/cliff/app.py b/cliff/app.py deleted file mode 100644 index bc0b65e..0000000 --- a/cliff/app.py +++ /dev/null @@ -1,241 +0,0 @@ -"""Application base class. -""" - -import argparse -import logging -import logging.handlers -import os -import sys - -from .help import HelpAction, HelpCommand -from .interactive import InteractiveApp - -LOG = logging.getLogger(__name__) - - -class App(object): - """Application base class. - - :param description: one-liner explaining the program purpose - :paramtype description: str - :param version: application version number - :paramtype version: str - :param command_manager: plugin loader - :paramtype command_manager: cliff.commandmanager.CommandManager - :param stdin: Standard input stream - :paramtype stdin: readable I/O stream - :param stdout: Standard output stream - :paramtype stdout: writable I/O stream - :param stderr: Standard error output stream - :paramtype stderr: writable I/O stream - :param interactive_app_factory: callable to create an - interactive application - :paramtype interactive_app_factory: cliff.interactive.InteractiveApp - """ - - NAME = os.path.splitext(os.path.basename(sys.argv[0]))[0] - - CONSOLE_MESSAGE_FORMAT = '%(message)s' - LOG_FILE_MESSAGE_FORMAT = \ - '[%(asctime)s] %(levelname)-8s %(name)s %(message)s' - DEFAULT_VERBOSE_LEVEL = 1 - - def __init__(self, description, version, command_manager, - stdin=None, stdout=None, stderr=None, - interactive_app_factory=InteractiveApp): - """Initialize the application. - """ - self.command_manager = command_manager - self.command_manager.add_command('help', HelpCommand) - self.stdin = stdin or sys.stdin - self.stdout = stdout or sys.stdout - self.stderr = stderr or sys.stderr - self.interactive_app_factory = interactive_app_factory - self.parser = self.build_option_parser(description, version) - self.interactive_mode = False - - def build_option_parser(self, description, version): - """Return an argparse option parser for this application. - - Subclasses may override this method to extend - the parser with more global options. - - :param description: full description of the application - :paramtype description: str - :param version: version number for the application - :paramtype version: str - """ - parser = argparse.ArgumentParser( - description=description, - add_help=False, - ) - parser.add_argument( - '--version', - action='version', - version='%(prog)s {0}'.format(version), - ) - parser.add_argument( - '-v', '--verbose', - action='count', - dest='verbose_level', - default=self.DEFAULT_VERBOSE_LEVEL, - help='Increase verbosity of output. Can be repeated.', - ) - parser.add_argument( - '-q', '--quiet', - action='store_const', - dest='verbose_level', - const=0, - help='suppress output except warnings and errors', - ) - parser.add_argument( - '-h', '--help', - action=HelpAction, - nargs=0, - default=self, # tricky - help="show this help message and exit", - ) - parser.add_argument( - '--debug', - default=False, - action='store_true', - help='show tracebacks on errors', - ) - return parser - - def configure_logging(self): - """Create logging handlers for any log output. - """ - root_logger = logging.getLogger('') - - # Set up logging to a file - root_logger.setLevel(logging.DEBUG) - file_handler = logging.handlers.RotatingFileHandler( - self.NAME + '.log', - maxBytes=10240, - backupCount=1, - ) - formatter = logging.Formatter(self.LOG_FILE_MESSAGE_FORMAT) - file_handler.setFormatter(formatter) - root_logger.addHandler(file_handler) - - # Send higher-level messages to the console via stderr - console = logging.StreamHandler(self.stderr) - console_level = {0: logging.WARNING, - 1: logging.INFO, - 2: logging.DEBUG, - }.get(self.options.verbose_level, logging.DEBUG) - console.setLevel(console_level) - formatter = logging.Formatter(self.CONSOLE_MESSAGE_FORMAT) - console.setFormatter(formatter) - root_logger.addHandler(console) - return - - def run(self, argv): - """Equivalent to the main program for the application. - - :param argv: input arguments and options - :paramtype argv: list of str - """ - try: - self.options, remainder = self.parser.parse_known_args(argv) - self.configure_logging() - self.interactive_mode = not remainder - self.initialize_app(remainder) - except Exception as err: - if hasattr(self, 'options'): - debug = self.options.debug - else: - debug = True - if debug: - LOG.exception(err) - raise - else: - LOG.error(err) - return 1 - result = 1 - if self.interactive_mode: - result = self.interact() - else: - result = self.run_subcommand(remainder) - return result - - # FIXME(dhellmann): Consider moving these command handling methods - # to a separate class. - def initialize_app(self, argv): - """Hook for subclasses to take global initialization action - after the arguments are parsed but before a command is run. - Invoked only once, even in interactive mode. - - :param argv: List of arguments, including the subcommand to run. - Empty for interactive mode. - """ - return - - def prepare_to_run_command(self, cmd): - """Perform any preliminary work needed to run a command. - - :param cmd: command processor being invoked - :paramtype cmd: cliff.command.Command - """ - return - - def clean_up(self, cmd, result, err): - """Hook run after a command is done to shutdown the app. - - :param cmd: command processor being invoked - :paramtype cmd: cliff.command.Command - :param result: return value of cmd - :paramtype result: int - :param err: exception or None - :paramtype err: Exception - """ - return - - def interact(self): - interpreter = self.interactive_app_factory(self, - self.command_manager, - self.stdin, - self.stdout, - ) - interpreter.cmdloop() - return 0 - - def run_subcommand(self, argv): - subcommand = self.command_manager.find_command(argv) - cmd_factory, cmd_name, sub_argv = subcommand - cmd = cmd_factory(self, self.options) - err = None - result = 1 - try: - self.prepare_to_run_command(cmd) - full_name = (cmd_name - if self.interactive_mode - else ' '.join([self.NAME, cmd_name]) - ) - cmd_parser = cmd.get_parser(full_name) - parsed_args = cmd_parser.parse_args(sub_argv) - result = cmd.run(parsed_args) - except Exception as err: - if self.options.debug: - LOG.exception(err) - else: - LOG.error(err) - try: - self.clean_up(cmd, result, err) - except Exception as err2: - if self.options.debug: - LOG.exception(err2) - else: - LOG.error('Could not clean up: %s', err2) - if self.options.debug: - raise - else: - try: - self.clean_up(cmd, result, None) - except Exception as err3: - if self.options.debug: - LOG.exception(err3) - else: - LOG.error('Could not clean up: %s', err3) - return result diff --git a/cliff/command.py b/cliff/command.py deleted file mode 100644 index 1661313..0000000 --- a/cliff/command.py +++ /dev/null @@ -1,50 +0,0 @@ - -import abc -import argparse -import inspect - - -class Command(object): - """Base class for command plugins. - - :param app: Application instance invoking the command. - :paramtype app: cliff.app.App - """ - __metaclass__ = abc.ABCMeta - - def __init__(self, app, app_args): - self.app = app - self.app_args = app_args - return - - def get_description(self): - """Return the command description. - """ - return inspect.getdoc(self.__class__) or '' - - def get_parser(self, prog_name): - """Return an :class:`argparse.ArgumentParser`. - """ - parser = argparse.ArgumentParser( - description=self.get_description(), - prog=prog_name, - ) - return parser - - @abc.abstractmethod - def take_action(self, parsed_args): - """Override to do something useful. - """ - - def run(self, parsed_args): - """Invoked by the application when the command is run. - - Developers implementing commands should override - :meth:`take_action`. - - Developers creating new command base classes (such as - :class:`Lister` and :class:`ShowOne`) should override this - method to wrap :meth:`take_action`. - """ - self.take_action(parsed_args) - return 0 diff --git a/cliff/commandmanager.py b/cliff/commandmanager.py deleted file mode 100644 index 135714a..0000000 --- a/cliff/commandmanager.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Discover and lookup command plugins. -""" - -import logging - -import pkg_resources - - -LOG = logging.getLogger(__name__) - - -class EntryPointWrapper(object): - """Wrap up a command class already imported to make it look like a plugin. - """ - - def __init__(self, name, command_class): - self.name = name - self.command_class = command_class - - def load(self): - return self.command_class - - -class CommandManager(object): - """Discovers commands and handles lookup based on argv data. - - :param namespace: String containing the setuptools entrypoint namespace - for the plugins to be loaded. For example, - ``'cliff.formatter.list'``. - """ - def __init__(self, namespace): - self.commands = {} - self.namespace = namespace - self._load_commands() - - def _load_commands(self): - for ep in pkg_resources.iter_entry_points(self.namespace): - LOG.debug('found command %r', ep.name) - self.commands[ep.name.replace('_', ' ')] = ep - return - - def __iter__(self): - return iter(self.commands.items()) - - def add_command(self, name, command_class): - self.commands[name] = EntryPointWrapper(name, command_class) - - def find_command(self, argv): - """Given an argument list, find a command and - return the processor and any remaining arguments. - """ - search_args = argv[:] - name = '' - while search_args: - if search_args[0].startswith('-'): - raise ValueError('Invalid command %r' % search_args[0]) - next_val = search_args.pop(0) - name = '%s %s' % (name, next_val) if name else next_val - if name in self.commands: - cmd_ep = self.commands[name] - cmd_factory = cmd_ep.load() - return (cmd_factory, name, search_args) - else: - raise ValueError('Unknown command %r' % - (argv,)) diff --git a/cliff/display.py b/cliff/display.py deleted file mode 100644 index 3c05760..0000000 --- a/cliff/display.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Application base class for displaying data. -""" -import abc -import logging - -import pkg_resources - -from .command import Command - - -LOG = logging.getLogger(__name__) - - -class DisplayCommandBase(Command): - """Command base class for displaying data about a single object. - """ - __metaclass__ = abc.ABCMeta - - def __init__(self, app, app_args): - super(DisplayCommandBase, self).__init__(app, app_args) - self.load_formatter_plugins() - - @abc.abstractproperty - def formatter_namespace(self): - "String specifying the namespace to use for loading formatter plugins." - - @abc.abstractproperty - def formatter_default(self): - "String specifying the name of the default formatter." - - def load_formatter_plugins(self): - self.formatters = {} - for ep in pkg_resources.iter_entry_points(self.formatter_namespace): - try: - self.formatters[ep.name] = ep.load()() - except Exception as err: - LOG.error(err) - if self.app_args.debug: - raise - return - - def get_parser(self, prog_name): - parser = super(DisplayCommandBase, self).get_parser(prog_name) - formatter_group = parser.add_argument_group( - title='output formatters', - description='output formatter options', - ) - formatter_choices = sorted(self.formatters.keys()) - formatter_default = self.formatter_default - if formatter_default not in formatter_choices: - formatter_default = formatter_choices[0] - formatter_group.add_argument( - '-f', '--format', - dest='formatter', - action='store', - choices=formatter_choices, - default=formatter_default, - help='the output format, defaults to %s' % formatter_default, - ) - formatter_group.add_argument( - '-c', '--column', - action='append', - default=[], - dest='columns', - metavar='COLUMN', - help='specify the column(s) to include, can be repeated', - ) - for name, formatter in sorted(self.formatters.items()): - formatter.add_argument_group(parser) - return parser - - @abc.abstractmethod - def produce_output(self, parsed_args, column_names, data): - """Use the formatter to generate the output. - - :param parsed_args: argparse.Namespace instance with argument values - :param column_names: sequence of strings containing names - of output columns - :param data: iterable with values matching the column names - """ - - def run(self, parsed_args): - self.formatter = self.formatters[parsed_args.formatter] - column_names, data = self.take_action(parsed_args) - self.produce_output(parsed_args, column_names, data) - return 0 diff --git a/cliff/formatters/__init__.py b/cliff/formatters/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/cliff/formatters/__init__.py +++ /dev/null diff --git a/cliff/formatters/base.py b/cliff/formatters/base.py deleted file mode 100644 index 43b8f17..0000000 --- a/cliff/formatters/base.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Base classes for formatters. -""" - -import abc - - -class Formatter(object): - __metaclass__ = abc.ABCMeta - - @abc.abstractmethod - def add_argument_group(self, parser): - """Add any options to the argument parser. - - Should use our own argument group. - """ - - -class ListFormatter(Formatter): - """Base class for formatters that know how to deal with multiple objects. - """ - __metaclass__ = abc.ABCMeta - - @abc.abstractmethod - def emit_list(self, column_names, data, stdout, parsed_args): - """Format and print the list from the iterable data source. - - :param column_names: names of the columns - :param data: iterable data source, one tuple per object - with values in order of column names - :param stdout: output stream where data should be written - :param parsed_args: argparse namespace from our local options - """ - - -class SingleFormatter(Formatter): - """Base class for formatters that work with single objects. - """ - __metaclass__ = abc.ABCMeta - - @abc.abstractmethod - def emit_one(self, column_names, data, stdout, parsed_args): - """Format and print the values associated with the single object. - - :param column_names: names of the columns - :param data: iterable data source with values in order of column names - :param stdout: output stream where data should be written - :param parsed_args: argparse namespace from our local options - """ diff --git a/cliff/formatters/commaseparated.py b/cliff/formatters/commaseparated.py deleted file mode 100644 index 155e0ca..0000000 --- a/cliff/formatters/commaseparated.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Output formatters using csv format. -""" - -import csv - -from .base import ListFormatter - - -class CSVLister(ListFormatter): - - QUOTE_MODES = { - 'all': csv.QUOTE_ALL, - 'minimal': csv.QUOTE_MINIMAL, - 'nonnumeric': csv.QUOTE_NONNUMERIC, - 'none': csv.QUOTE_NONE, - } - - def add_argument_group(self, parser): - group = parser.add_argument_group('CSV Formatter') - group.add_argument( - '--quote', - choices=sorted(self.QUOTE_MODES.keys()), - dest='quote_mode', - default='nonnumeric', - help='when to include quotes, defaults to nonnumeric', - ) - - def emit_list(self, column_names, data, stdout, parsed_args): - writer = csv.writer(stdout, - quoting=self.QUOTE_MODES[parsed_args.quote_mode], - ) - writer.writerow(column_names) - for row in data: - writer.writerow(row) - return diff --git a/cliff/formatters/shell.py b/cliff/formatters/shell.py deleted file mode 100644 index c45dc2b..0000000 --- a/cliff/formatters/shell.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Output formatters using shell syntax. -""" - -from .base import SingleFormatter - - -class ShellFormatter(SingleFormatter): - - def add_argument_group(self, parser): - group = parser.add_argument_group( - title='shell formatter', - description='a format a UNIX shell can parse (variable="value")', - ) - group.add_argument( - '--variable', - action='append', - default=[], - dest='variables', - metavar='VARIABLE', - help='specify the variable(s) to include, can be repeated', - ) - group.add_argument( - '--prefix', - action='store', - default='', - dest='prefix', - help='add a prefix to all variable names', - ) - - def emit_one(self, column_names, data, stdout, parsed_args): - variable_names = [c.lower().replace(' ', '_') - for c in column_names - ] - desired_columns = parsed_args.variables - for name, value in zip(variable_names, data): - if name in desired_columns or not desired_columns: - stdout.write('%s%s="%s"\n' % (parsed_args.prefix, name, value)) - return diff --git a/cliff/formatters/table.py b/cliff/formatters/table.py deleted file mode 100644 index d03bcca..0000000 --- a/cliff/formatters/table.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Output formatters using prettytable. -""" - -import prettytable - -from .base import ListFormatter, SingleFormatter - - -class TableFormatter(ListFormatter, SingleFormatter): - - ALIGNMENTS = { - int: 'r', - str: 'l', - float: 'r', - } - try: - ALIGNMENTS[unicode] = 'l' - except NameError: - pass - - def add_argument_group(self, parser): - pass - - def emit_list(self, column_names, data, stdout, parsed_args): - x = prettytable.PrettyTable(column_names) - x.padding_width = 1 - # Figure out the types of the columns in the - # first row and set the alignment of the - # output accordingly. - data_iter = iter(data) - try: - first_row = next(data_iter) - except StopIteration: - pass - else: - for value, name in zip(first_row, column_names): - alignment = self.ALIGNMENTS.get(type(value), 'l') - x.align[name] = alignment - # Now iterate over the data and add the rows. - x.add_row(first_row) - for row in data_iter: - x.add_row(row) - formatted = x.get_string(fields=column_names) - stdout.write(formatted) - stdout.write('\n') - return - - def emit_one(self, column_names, data, stdout, parsed_args): - x = prettytable.PrettyTable(field_names=('Field', 'Value')) - x.padding_width = 1 - # Align all columns left because the values are - # not all the same type. - x.align['Field'] = 'l' - x.align['Value'] = 'l' - for name, value in zip(column_names, data): - x.add_row((name, value)) - formatted = x.get_string(fields=('Field', 'Value')) - stdout.write(formatted) - stdout.write('\n') - return diff --git a/cliff/help.py b/cliff/help.py deleted file mode 100644 index 9a7f848..0000000 --- a/cliff/help.py +++ /dev/null @@ -1,77 +0,0 @@ -import argparse -import logging -import sys - -from .command import Command - - -class HelpAction(argparse.Action): - """Provide a custom action so the -h and --help options - to the main app will print a list of the commands. - - The commands are determined by checking the CommandManager - instance, passed in as the "default" value for the action. - """ - def __call__(self, parser, namespace, values, option_string=None): - log = logging.getLogger(__name__) - app = self.default - parser.print_help(app.stdout) - app.stdout.write('\nCommands:\n') - command_manager = app.command_manager - for name, ep in sorted(command_manager): - try: - factory = ep.load() - except Exception as err: - app.stdout.write('Could not load %r\n' % ep) - continue - try: - cmd = factory(self, None) - except Exception as err: - app.stdout.write('Could not instantiate %r: %s\n' % (ep, err)) - continue - one_liner = cmd.get_description().split('\n')[0] - app.stdout.write(' %-13s %s\n' % (name, one_liner)) - sys.exit(0) - - -class HelpCommand(Command): - """print detailed help for another command - """ - - def get_parser(self, prog_name): - parser = super(HelpCommand, self).get_parser(prog_name) - parser.add_argument('cmd', - nargs='*', - help='name of the command', - ) - return parser - - def take_action(self, parsed_args): - if parsed_args.cmd: - try: - the_cmd = self.app.command_manager.find_command( - parsed_args.cmd, - ) - cmd_factory, cmd_name, search_args = the_cmd - except ValueError: - # Did not find an exact match - cmd = parsed_args.cmd[0] - fuzzy_matches = [k[0] for k in self.app.command_manager - if k[0].startswith(cmd) - ] - if not fuzzy_matches: - raise - self.app.stdout.write('Command "%s" matches:\n' % cmd) - for fm in fuzzy_matches: - self.app.stdout.write(' %s\n' % fm) - return - cmd = cmd_factory(self.app, search_args) - full_name = (cmd_name - if self.app.interactive_mode - else ' '.join([self.app.NAME, cmd_name]) - ) - cmd_parser = cmd.get_parser(full_name) - else: - cmd_parser = self.get_parser(' '.join([self.app.NAME, 'help'])) - cmd_parser.print_help(self.app.stdout) - return 0 diff --git a/cliff/interactive.py b/cliff/interactive.py deleted file mode 100644 index 77aea9a..0000000 --- a/cliff/interactive.py +++ /dev/null @@ -1,115 +0,0 @@ -"""Application base class. -""" - -import itertools -import logging -import logging.handlers -import shlex - -import cmd2 - -LOG = logging.getLogger(__name__) - - -class InteractiveApp(cmd2.Cmd): - """Provides "interactive mode" features. - - Refer to the cmd2_ and cmd_ documentation for details - about subclassing and configuring this class. - - .. _cmd2: http://packages.python.org/cmd2/index.html - .. _cmd: http://docs.python.org/library/cmd.html - - :param parent_app: The calling application (expected to be derived - from :class:`cliff.main.App`). - :param command_manager: A :class:`cliff.commandmanager.CommandManager` - instance. - :param stdin: Standard input stream - :param stdout: Standard output stream - """ - - use_rawinput = True - doc_header = "Shell commands (type help <topic>):" - app_cmd_header = "Application commands (type help <topic>):" - - def __init__(self, parent_app, command_manager, stdin, stdout): - self.parent_app = parent_app - self.prompt = '(%s) ' % parent_app.NAME - self.command_manager = command_manager - cmd2.Cmd.__init__(self, 'tab', stdin=stdin, stdout=stdout) - - def default(self, line): - # Tie in the the default command processor to - # dispatch commands known to the command manager. - # We send the message through our parent app, - # since it already has the logic for executing - # the subcommand. - line_parts = shlex.split(line.parsed.raw) - self.parent_app.run_subcommand(line_parts) - - def completedefault(self, text, line, begidx, endidx): - # Tab-completion for commands known to the command manager. - # Does not handle options on the commands. - if not text: - completions = sorted(n for n, v in self.command_manager) - else: - completions = sorted(n for n, v in self.command_manager - if n.startswith(text) - ) - return completions - - def help_help(self): - # Use the command manager to get instructions for "help" - self.default('help help') - - def do_help(self, arg): - if arg: - # Check if the arg is a builtin command or something - # coming from the command manager - arg_parts = shlex.split(arg) - method_name = '_'.join( - itertools.chain( - ['do'], - itertools.takewhile(lambda x: not x.startswith('-'), - arg_parts) - ) - ) - # Have the command manager version of the help - # command produce the help text since cmd and - # cmd2 do not provide help for "help" - if hasattr(self, method_name): - return cmd2.Cmd.do_help(self, arg) - # Dispatch to the underlying help command, - # which knows how to provide help for extension - # commands. - self.default('help ' + arg) - else: - cmd2.Cmd.do_help(self, arg) - cmd_names = [n for n, v in self.command_manager] - self.print_topics(self.app_cmd_header, cmd_names, 15, 80) - return - - def get_names(self): - # Override the base class version to filter out - # things that look like they should be hidden - # from the user. - return [n - for n in cmd2.Cmd.get_names(self) - if not n.startswith('do__') - ] - - def precmd(self, statement): - # Pre-process the parsed command in case it looks like one of - # our subcommands, since cmd2 does not handle multi-part - # command names by default. - line_parts = shlex.split(statement.parsed.raw) - try: - the_cmd = self.command_manager.find_command(line_parts) - cmd_factory, cmd_name, sub_argv = the_cmd - except ValueError: - # Not a plugin command - pass - else: - statement.parsed.command = cmd_name - statement.parsed.args = ' '.join(sub_argv) - return statement diff --git a/cliff/lister.py b/cliff/lister.py deleted file mode 100644 index 406be97..0000000 --- a/cliff/lister.py +++ /dev/null @@ -1,57 +0,0 @@ -"""Application base class for providing a list of data as output. -""" -import abc -import itertools -import logging - -from .display import DisplayCommandBase - - -LOG = logging.getLogger(__name__) - - -class Lister(DisplayCommandBase): - """Command base class for providing a list of data as output. - """ - __metaclass__ = abc.ABCMeta - - @property - def formatter_namespace(self): - return 'cliff.formatter.list' - - @property - def formatter_default(self): - return 'table' - - @abc.abstractmethod - def take_action(self, parsed_args): - """Return a tuple containing the column names and an iterable - containing the data to be listed. - """ - - def produce_output(self, parsed_args, column_names, data): - if not parsed_args.columns: - columns_to_include = column_names - data_gen = data - else: - columns_to_include = [c for c in column_names - if c in parsed_args.columns - ] - if not columns_to_include: - raise ValueError('No recognized column names in %s' % - str(parsed_args.columns)) - # Set up argument to compress() - selector = [(c in columns_to_include) - for c in column_names] - # Generator expression to only return the parts of a row - # of data that the user has expressed interest in - # seeing. We have to convert the compress() output to a - # list so the table formatter can ask for its length. - data_gen = (list(itertools.compress(row, selector)) - for row in data) - self.formatter.emit_list(columns_to_include, - data_gen, - self.app.stdout, - parsed_args, - ) - return 0 diff --git a/cliff/show.py b/cliff/show.py deleted file mode 100644 index 39e94b1..0000000 --- a/cliff/show.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Application base class for displaying data about a single object. -""" -import abc -import itertools -import logging - -from .display import DisplayCommandBase - - -LOG = logging.getLogger(__name__) - - -class ShowOne(DisplayCommandBase): - """Command base class for displaying data about a single object. - """ - __metaclass__ = abc.ABCMeta - - @property - def formatter_namespace(self): - return 'cliff.formatter.show' - - @property - def formatter_default(self): - return 'table' - - @abc.abstractmethod - def take_action(self, parsed_args): - """Return a two-part tuple with a tuple of column names - and a tuple of values. - """ - - def produce_output(self, parsed_args, column_names, data): - if not parsed_args.columns: - columns_to_include = column_names - else: - columns_to_include = [c for c in column_names - if c in parsed_args.columns] - # Set up argument to compress() - selector = [(c in columns_to_include) - for c in column_names] - data = list(itertools.compress(data, selector)) - self.formatter.emit_one(columns_to_include, - data, - self.app.stdout, - parsed_args) - return 0 diff --git a/cliff/__init__.py b/clifftablib/__init__.py index e69de29..e69de29 100644 --- a/cliff/__init__.py +++ b/clifftablib/__init__.py diff --git a/cliff/formatters/tablibformatters.py b/clifftablib/formatters.py index 7409a70..d7e5ca0 100644 --- a/cliff/formatters/tablibformatters.py +++ b/clifftablib/formatters.py @@ -1,7 +1,7 @@ """Output formatters using tablib. """ -from .base import ListFormatter, SingleFormatter +from cliff.formatters.base import ListFormatter, SingleFormatter import tablib diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 6f01983..0000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -httplib2==0.7.4 -prettytable==0.5 diff --git a/docs/source/classes.rst b/docs/source/classes.rst deleted file mode 100644 index 14e80e9..0000000 --- a/docs/source/classes.rst +++ /dev/null @@ -1,63 +0,0 @@ -=============== - Cliff Classes -=============== - -Application -=========== - -App ---- - -.. autoclass:: cliff.app.App - :members: - -InteractiveApp --------------- - -.. autoclass:: cliff.interactive.InteractiveApp - :members: - -CommandManager --------------- - -.. autoclass:: cliff.commandmanager.CommandManager - :members: - -Command -------- - -.. autoclass:: cliff.command.Command - :members: - -ShowOne -------- - -.. autoclass:: cliff.show.ShowOne - :members: - -Lister ------- - -.. autoclass:: cliff.lister.Lister - :members: - -Formatting Output -================= - -Formatter ---------- - -.. autoclass:: cliff.formatters.base.Formatter - :members: - -ListFormatter -------------- - -.. autoclass:: cliff.formatters.base.ListFormatter - :members: - -SingleFormatter ---------------- - -.. autoclass:: cliff.formatters.base.SingleFormatter - :members: diff --git a/docs/source/conf.py b/docs/source/conf.py index 9e4b98e..e0bf72a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -11,6 +11,7 @@ # All configuration values have a default; values that are commented out # serve to show the default. +import datetime import sys, os # If extensions (or modules to document with autodoc) are in another directory, @@ -40,8 +41,8 @@ source_suffix = '.rst' master_doc = 'index' # General information about the project. -project = u'cliff' -copyright = u'2012, Doug Hellmann' +project = u'cliff-tablib' +copyright = u'%s, Doug Hellmann' % datetime.date.today().year # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -164,7 +165,7 @@ html_static_path = ['_static'] #html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'cliffdoc' +htmlhelp_basename = 'cliff-tablibdoc' # -- Options for LaTeX output -------------------------------------------------- @@ -183,7 +184,7 @@ latex_elements = { # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'cliff.tex', u'cliff Documentation', + ('index', 'cliff-tablib.tex', u'cliff-tablibDocumentation', u'Doug Hellmann', 'manual'), ] @@ -213,7 +214,7 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'cliff', u'cliff Documentation', + ('index', 'cliff-tablib', u'cliff-tablib Documentation', [u'Doug Hellmann'], 1) ] @@ -227,8 +228,8 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'cliff', u'cliff Documentation', - u'Doug Hellmann', 'cliff', 'One line description of project.', + ('index', 'cliff-tablib', u'cliff-tablib Documentation', + u'Doug Hellmann', 'cliff-tablib', 'One line description of project.', 'Miscellaneous'), ] diff --git a/docs/source/demoapp.rst b/docs/source/demoapp.rst deleted file mode 100644 index 66e66cb..0000000 --- a/docs/source/demoapp.rst +++ /dev/null @@ -1,258 +0,0 @@ -======================== - Exploring the Demo App -======================== - -The cliff source package includes a ``demoapp`` directory containing -an example main program with several command plugins. - -Setup -===== - -To install and experiment with the demo app you should create a -virtual environment and activate it. This will make it easy to remove -the app later, since it doesn't do anything useful and you aren't -likely to want to hang onto it after you understand how it works. - -:: - - $ pip install virtualenv - $ virtualenv .venv - $ . .venv/bin/activate - (.venv)$ - -Next, install cliff in the same environment. - -:: - - (.venv)$ python setup.py install - -Finally, install the demo application into the virtual environment. - -:: - - (.venv)$ cd demoapp - (.venv)$ python setup.py install - -Usage -===== - -Both cliff and the demo installed, you can now run the command -``cliffdemo``. - -For basic command usage instructions and a list of the commands -available from the plugins, run:: - - (.venv)$ cliffdemo -h - -or:: - - (.venv)$ cliffdemo --help - -Run the ``simple`` command by passing its name as argument to ``cliffdemo``. - -:: - - (.venv)$ cliffdemo simple - -The ``simple`` command prints this output to the console: - -:: - - sending greeting - hi! - - -To see help for an individual command, use the ``help`` command:: - - (.venv)$ cliffdemo help files - -The Source -========== - -The ``cliffdemo`` application is defined in a ``cliffdemo`` package -containing several modules. - -main.py -------- - -The main application is defined in ``main.py``: - -.. literalinclude:: ../../demoapp/cliffdemo/main.py - :linenos: - -The :class:`DemoApp` class inherits from :class:`App` and overrides -:func:`__init__` to set the program description and version number. It -also passes a :class:`CommandManager` instance configured to look for -plugins in the ``cliff.demo`` namespace. - -The :func:`initialize_app` method of :class:`DemoApp` will be invoked -after the main program arguments are parsed, but before any command -processing is performed and before the application enters interactive -mode. This hook is intended for opening connections to remote web -services, databases, etc. using arguments passed to the main -application. - -The :func:`prepare_to_run_command` method of :class:`DemoApp` will be -invoked after a command is identified, but before the command is given -its arguments and run. This hook is intended for pre-command -validation or setup that must be repeated and cannot be handled by -:func:`initialize_app`. - -The :func:`clean_up` method of :class:`DemoApp` is invoked after a -command runs. If the command raised an exception, the exception object -is passed to :func:`clean_up`. Otherwise the ``err`` argument is -``None``. - -The :func:`main` function defined in ``main.py`` is registered as a -console script entry point so that :class:`DemoApp` can be run from -the command line (see the discussion of ``setup.py`` below). - -simple.py ---------- - -Two commands are defined in ``simple.py``: - -.. literalinclude:: ../../demoapp/cliffdemo/simple.py - :linenos: - -:class:`Simple` demonstrates using logging to emit messages on the -console at different verbose levels. - -:: - - (.venv)$ cliffdemo simple - sending greeting - hi! - - (.venv)$ cliffdemo -v simple - prepare_to_run_command Simple - sending greeting - debugging - hi! - clean_up Simple - - (.venv)$ cliffdemo -q simple - hi! - -:class:`Error` always raises a :class:`RuntimeError` exception when it -is invoked, and can be used to experiment with the error handling -features of cliff. - -:: - - (.venv)$ cliffdemo error - causing error - ERROR: this is the expected exception - - (.venv)$ cliffdemo -v error - prepare_to_run_command Error - causing error - ERROR: this is the expected exception - clean_up Error - got an error: this is the expected exception - - (.venv)$ cliffdemo --debug error - causing error - this is the expected exception - Traceback (most recent call last): - File ".../cliff/app.py", line 218, in run_subcommand - result = cmd.run(parsed_args) - File ".../cliff/command.py", line 43, in run - self.take_action(parsed_args) - File ".../demoapp/cliffdemo/simple.py", line 24, in take_action - raise RuntimeError('this is the expected exception') - RuntimeError: this is the expected exception - Traceback (most recent call last): - File "/Users/dhellmann/Envs/cliff/bin/cliffdemo", line 9, in <module> - load_entry_point('cliffdemo==0.1', 'console_scripts', 'cliffdemo')() - File ".../demoapp/cliffdemo/main.py", line 33, in main - return myapp.run(argv) - File ".../cliff/app.py", line 160, in run - result = self.run_subcommand(remainder) - File ".../cliff/app.py", line 218, in run_subcommand - result = cmd.run(parsed_args) - File ".../cliff/command.py", line 43, in run - self.take_action(parsed_args) - File ".../demoapp/cliffdemo/simple.py", line 24, in take_action - raise RuntimeError('this is the expected exception') - RuntimeError: this is the expected exception - -.. _demoapp-list: - -list.py -------- - -``list.py`` includes a single command derived from -:class:`cliff.lister.Lister` which prints a list of the files in the -current directory. - -.. literalinclude:: ../../demoapp/cliffdemo/list.py - :linenos: - -:class:`Files` prepares the data, and :class:`Lister` manages the -output formatter and printing the data to the console. - -:: - - (.venv)$ cliffdemo files - +---------------+------+ - | Name | Size | - +---------------+------+ - | build | 136 | - | cliffdemo.log | 2546 | - | Makefile | 5569 | - | source | 408 | - +---------------+------+ - - (.venv)$ cliffdemo files -f csv - "Name","Size" - "build",136 - "cliffdemo.log",2690 - "Makefile",5569 - "source",408 - -.. _demoapp-show: - -show.py -------- - -``show.py`` includes a single command derived from -:class:`cliff.show.ShowOne` which prints the properties of the named -file. - -.. literalinclude:: ../../demoapp/cliffdemo/show.py - :linenos: - -:class:`File` prepares the data, and :class:`ShowOne` manages the -output formatter and printing the data to the console. - -:: - - (.venv)$ cliffdemo file setup.py - +---------------+--------------+ - | Field | Value | - +---------------+--------------+ - | Name | setup.py | - | Size | 5825 | - | UID | 502 | - | GID | 20 | - | Modified Time | 1335569964.0 | - +---------------+--------------+ - - -setup.py --------- - -The demo application is packaged using distribute_, the modern -implementation of setuptools. - -.. literalinclude:: ../../demoapp/setup.py - :linenos: - -The important parts of the packaging instructions are the -``entry_points`` settings. All of the commands are registered in the -``cliff.demo`` namespace. Each main program should define its own -command namespace so that it only loads the command plugins that it -should be managing. - -.. _distribute: http://packages.python.org/distribute/ diff --git a/docs/source/developers.rst b/docs/source/developers.rst index 46b9808..539dfb1 100644 --- a/docs/source/developers.rst +++ b/docs/source/developers.rst @@ -2,23 +2,17 @@ For Developers ================ -If you would like to contribute to cliff directly, these instructions -should help you get started. Patches, bug reports, and feature -requests are all welcome through the `GitHub project -<https://github.com/dreamhost/cliff>`_. Contributions in the form of -patches or pull requests are easier to integrate and will receive -priority attention. - -.. note:: - - Before contributing new features to clif core, please consider - whether they should be implemented as an extension instead. The - architecture is highly pluggable precisely to keep the core small. +If you would like to contribute to cliff-tablib directly, these +instructions should help you get started. Patches, bug reports, and +feature requests are all welcome through the `GitHub project +<https://github.com/dreamhost/cliff-tablib>`_. Contributions in the +form of patches or pull requests are easier to integrate and will +receive priority attention. Building Documentation ====================== -The documentation for cliff is written in reStructuredText and +The documentation for cliff-tablib is written in reStructuredText and converted to HTML using Sphinx. The build itself is driven by make. You will need the following packages in order to build the docs: @@ -36,12 +30,12 @@ documentation:: loading pickled environment... done building [html]: targets for 1 source files that are out of date updating environment: 1 added, 1 changed, 0 removed - reading sources... [100%] index + reading sources... [100%] index looking for now-outdated files... none found pickling environment... done done preparing documents... done - writing output... [100%] index + writing output... [100%] index writing additional files... genindex search copying static files... done dumping search index... done @@ -49,17 +43,15 @@ documentation:: build succeeded, 2 warnings. Build finished. The HTML pages are in build/html. - + The output version of the documentation ends up in ``./docs/build/html`` inside your sandbox. Running Tests ============= -.. image:: https://secure.travis-ci.org/dhellmann/cliff.png?branch=master - -The test suite for clif uses tox_, which must be installed separately -(``pip install tox``). +The test suite for cliff-tablib uses tox_, which must be installed +separately (``pip install tox``). To run the tests under Python 2.7 and 3.2, run ``tox`` from the top level directory of the git repository. @@ -73,5 +65,3 @@ Add new tests by modifying an existing file or creating new script in the ``tests`` directory. .. _tox: http://codespeak.net/tox - -.. _developer-templates: diff --git a/docs/source/history.rst b/docs/source/history.rst index e391488..e8a39d1 100644 --- a/docs/source/history.rst +++ b/docs/source/history.rst @@ -4,59 +4,6 @@ 1.0 - - Add trailing newlines after output from tablib-based formatters - (JSON, YAML, and HTML). Contributed by Matt Joyce. - - Some :pep:`8` fixes. - - Refactor the API in :class:`Command` to add :func:`take_action` - and make :func:`run` a concrete method. Existing users should only - need to rename :func:`run()` to :func:`take_action()` since the - function signatures have not changed. - - In :class:`Lister` and :class:`ShowOne` use :func:`take_action` - instead of :func:`get_data`. + - Initial public release, created by pulling the tablib integration + out of cliff and repackaging it. -0.7 - - - Clean up interactive mode flag settting. - - Add support for Python 2.6, contributed by heavenshell. - - Fix multi-word commands in interactive mode. - -0.6 - - - Pass the non-global argument list to :func:`initialize_app` to be - used in initialization work. - -0.5.1 - - - Remove pinned version requirement for PrettyTable until the - OpenStack clients catch up to the API change. - -0.5 - - - Asking for help about a command by prefix lists all matching - commands. - - Add formatters for HTML, JSON, and YAML. - -0.4 - - - Add shell formatter for single objects. - - Add interactive mode. - - Expand documentation. - -0.3 - - - Add ShowOne base class for commands that show details about single - objects. - - Fix a problem with Lister when there is no data to be printed. - -0.2 - - - Incorporate changes from dtroyer to replace use of optparse in App - with argparse. - - Added "help" subcommand to replace ``--help`` option handling in - subcommands. - -0.1 - - - Initial public release. - - Included App, CommandManager, Lister, csv and table formatters, a - demo application, and basic documentation. diff --git a/docs/source/index.rst b/docs/source/index.rst index 0a13664..bf0ae2b 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,10 +1,11 @@ -======================================================= - cliff -- Command Line Interface Formulation Framework -======================================================= +============================================= + cliff-tablib -- tablib Formatters for cliff +============================================= -cliff is a framework for building command line programs. It uses -plugins to define sub-commands, output formatters, and other -extensions. +cliff-tablib includes formatters to be used in applications based on +the cliff_ framework. + +.. _cliff: http://pypi.python.org/pypi/cliff Contents: @@ -12,11 +13,8 @@ Contents: :maxdepth: 2 introduction - demoapp list_commands show_commands - interactive_mode - classes install developers history diff --git a/docs/source/install.rst b/docs/source/install.rst index 1bd67d0..e985a4b 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -5,33 +5,33 @@ Python Versions =============== -cliff is being developed under Python 2.7 and tested with Python 3.2. +cliff-tablib is being developed under Python 2.7 and tested with Python 3.2. .. _install-basic: Basic Installation ================== -cliff should be installed into the same site-packages area where the -application and extensions are installed (either a virtualenv or the -global site-packages). You may need administrative privileges to do -that. The easiest way to install it is using pip_:: +cliff-tablib should be installed into the same site-packages area +where the application and extensions are installed (either a +virtualenv or the global site-packages). You may need administrative +privileges to do that. The easiest way to install it is using pip_:: - $ pip install cliff + $ pip install cliff-tablib or:: - $ sudo pip install cliff + $ sudo pip install cliff-tablib .. _pip: http://pypi.python.org/pypi/pip Source Code =========== -The source is hosted on github: https://github.com/dreamhost/cliff +The source is hosted on github: https://github.com/dreamhost/cliff-tablib Reporting Bugs ============== Please report bugs through the github project: -https://github.com/dreamhost/cliff/issues +https://github.com/dreamhost/cliff-tablib/issues diff --git a/docs/source/interactive_mode.rst b/docs/source/interactive_mode.rst deleted file mode 100644 index 519e89a..0000000 --- a/docs/source/interactive_mode.rst +++ /dev/null @@ -1,94 +0,0 @@ -================== - Interactive Mode -================== - -In addition to running single commands from the command line, cliff -supports an interactive mode in which the user is presented with a -separate command shell. All of the command plugins available from the -command line are automatically configured as commands within the -shell. - -Refer to the cmd2_ documentation for more details about features of -the shell. - -.. _cmd2: http://packages.python.org/cmd2/index.html - -.. todo:: Add details about configuring and interacting with the shell (copy from cmd2 docs) - -Example -======= - -The ``cliffdemo`` application enters interactive mode if no command is -specified on the command line. - -:: - - (.venv)$ cliffdemo - (cliffdemo) help - - Shell commands (type help <topic>): - =================================== - cmdenvironment edit hi l list pause r save shell show - ed help history li load py run set shortcuts - - Undocumented commands: - ====================== - EOF eof exit q quit - - Application commands (type help <topic>): - ========================================= - files help simple file error two part - -To obtain instructions for a built-in or application command, use the -``help`` command: - -:: - - (cliffdemo) help simple - usage: simple [-h] - - A simple command that prints a message. - - optional arguments: - -h, --help show this help message and exit - -The commands can be run, including options and arguments, as on the -regular command line: - -:: - - (cliffdemo) simple - sending greeting - hi! - (cliffdemo) files - +----------------------+-------+ - | Name | Size | - +----------------------+-------+ - | .git | 578 | - | .gitignore | 268 | - | .tox | 238 | - | .venv | 204 | - | announce.rst | 1015 | - | announce.rst~ | 708 | - | cliff | 884 | - | cliff.egg-info | 340 | - | cliffdemo.log | 2193 | - | cliffdemo.log.1 | 10225 | - | demoapp | 408 | - | dist | 136 | - | distribute_setup.py | 15285 | - | distribute_setup.pyc | 15196 | - | docs | 238 | - | LICENSE | 11358 | - | Makefile | 376 | - | Makefile~ | 94 | - | MANIFEST.in | 186 | - | MANIFEST.in~ | 344 | - | README.rst | 1063 | - | setup.py | 5855 | - | setup.py~ | 8128 | - | tests | 204 | - | tox.ini | 76 | - | tox.ini~ | 421 | - +----------------------+-------+ - (cliffdemo) diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst index d9312b9..def96eb 100644 --- a/docs/source/introduction.rst +++ b/docs/source/introduction.rst @@ -4,61 +4,5 @@ The cliff framework is meant to be used to create multi-level commands such as subversion and git, where the main program handles some basic -argument parsing and then invokes a sub-command to do the work. - -Command Plugins -=============== - -Cliff takes advantage of Python's ability to load code dynamically to -allow the sub-commands of a main program to be implemented, packaged, -and distributed separately from the main program. This organization -provides a unified view of the command for *users*, while giving -developers the opportunity organize source code in any way they see -fit. - -Cliff Objects -============= - -Cliff is organized around four objects that are combined to create a -useful command line program. - -The Application ---------------- - -An :class:`cliff.app.App` is the main program that you run from the shell -command prompt. It is responsible for global operations that apply to -all of the commands, such as configuring logging and setting up I/O -streams. - -The CommandManager ------------------- - -The :class:`cliff.commandmanager.CommandManager` knows how to load -individual command plugins. The default implementation uses -`setuptools entry points`_ but any mechanism for loading commands can -be used by replacing the default :class:`CommandManager` when -instantiating an :class:`App`. - -The Command ------------ - -The :class:`cliff.command.Command` class is where the real work -happens. The rest of the framework is present to help the user -discover the command plugins and invoke them, and to provide runtime -support for those plugins. Each :class:`Command` subclass is -responsible for taking action based on instructions from the user. It -defines its own local argument parser (usually using argparse_) and a -:func:`take_action` method that does the appropriate work. - -The Interactive Application ---------------------------- - -The main program uses an :class:`cliff.interactive.InteractiveApp` -instance to provide a command-shell mode in which the user can type -multiple commands before the program exits. Many cliff-based -applications will be able to use the default implementation of -:class:`InteractiveApp` without subclassing it. - -.. _setuptools entry points: http://packages.python.org/distribute/setuptools.html - -.. _argparse: http://docs.python.org/library/argparse.html +argument parsing and then invokes a sub-command to do the work. This +package adds JSON, YAML, and HTML output formatters to those commands. diff --git a/docs/source/list_commands.rst b/docs/source/list_commands.rst index f76188b..62fbebb 100644 --- a/docs/source/list_commands.rst +++ b/docs/source/list_commands.rst @@ -1,50 +1,11 @@ -=============== - List Commands -=============== +======================== + List Output Formatters +======================== -One of the most common patterns with command line programs is the need -to print lists of data. cliff provides a base class for commands of -this type so that they only need to prepare the data, and the user can -choose from one of several output formatter plugins to see the list of -data in their preferred format. - -Lister -====== - -The :class:`cliff.lister.Lister` base class API extends -:class:`Command` to allow :func:`take_action` to return data to be -formatted using a user-selectable formatter. Subclasses should provide -a :func:`take_action` implementation that returns a two member tuple -containing a tuple with the names of the columns in the dataset and an -iterable that will yield the data to be output. See the description of -:ref:`the files command in the demoapp <demoapp-list>` for details. - -List Output Formatters -====================== - -cliff is delivered with two output formatters for list -commands. :class:`Lister` adds a command line switch to let the user -specify the formatter they want, so you don't have to do any extra -work in your application. - -csv ---- - -The ``csv`` formatter produces a comma-separated-values document as -output. CSV data can be imported into a database or spreadsheet for -further manipulation. - -:: - - (.venv)$ cliffdemo files -f csv - "Name","Size" - "build",136 - "cliffdemo.log",2690 - "Makefile",5569 - "source",408 +cliff-tablib delivers several new output formatters for list commands. html ----- +==== The ``html`` formatter uses tablib_ to produce HTML output as a table. @@ -69,39 +30,19 @@ The ``html`` formatter uses tablib_ to produce HTML output as a table. </table> json ----- +==== The ``json`` formatter uses tablib_ to produce JSON output. :: - + (.venv)$ cliffdemo files -f json [{"Name": "build", "Size": 136}, {"Name": "cliffdemo.log", "Size": 3461}, {"Name": "Makefile", "Size": 5569}, {"Name": "requirements.txt", "Size": 33}, {"Name": "source", "Size": 782}] -table ------ - -The ``table`` formatter uses PrettyTable_ to produce output formatted -for human consumption. - -.. _PrettyTable: http://code.google.com/p/prettytable/ - -:: - - (.venv)$ cliffdemo files - +---------------+------+ - | Name | Size | - +---------------+------+ - | build | 136 | - | cliffdemo.log | 2546 | - | Makefile | 5569 | - | source | 408 | - +---------------+------+ - yaml ----- +==== The ``yaml`` formatter uses tablib_ to produce YAML output as a sequence of mappings. @@ -115,14 +56,4 @@ sequence of mappings. - {Name: requirements.txt, Size: 33} - {Name: source, Size: 816} - -Creating Your Own Formatter ---------------------------- - -If the standard formatters do not meet your needs, you can bundle -another formatter with your program by subclassing from -:class:`cliff.formatters.base.ListFormatter` and registering the -plugin in the ``cliff.formatter.list`` namespace. - - .. _tablib: https://github.com/kennethreitz/tablib diff --git a/docs/source/show_commands.rst b/docs/source/show_commands.rst index e848960..661acec 100644 --- a/docs/source/show_commands.rst +++ b/docs/source/show_commands.rst @@ -1,27 +1,6 @@ -=============== - Show Commands -=============== - -One of the most common patterns with command line programs is the need -to print properties of objects. cliff provides a base class for -commands of this type so that they only need to prepare the data, and -the user can choose from one of several output formatter plugins to -see the data in their preferred format. - -ShowOne -======= - -The :class:`cliff.show.ShowOne` base class API extends -:class:`Command` to allow :func:`take_action` to return data to be -formatted using a user-selectable formatter. Subclasses should provide -a :func:`take_action` implementation that returns a two member tuple -containing a tuple with the names of the columns in the dataset and an -iterable that contains the data values associated with those -names. See the description of :ref:`the file command in the demoapp -<demoapp-show>` for details. - -Show Output Formatters -====================== +======================== + Show Output Formatters +======================== cliff is delivered with output formatters for show commands. :class:`ShowOne` adds a command line switch to let the user @@ -65,47 +44,6 @@ The ``json`` formatter uses tablib_ to produce JSON output. "Value": 6373}, {"Field": "UID", "Value": 527}, {"Field": "GID", "Value": 501}, {"Field": "Modified Time", "Value": 1336353173.0}] -shell ------ - -The ``shell`` formatter produces output that can be parsed directly by -a typical UNIX shell as variable assignments. This avoids extra -parsing overhead in shell scripts. - -:: - - (.venv)$ cliffdemo file -f shell setup.py - name="setup.py" - size="5916" - uid="527" - gid="501" - modified_time="1335655655.0" - - (.venv)$ eval "$(cliffdemo file -f shell --prefix example_ setup.py)" - (.venv)$ echo $example_size - 5916 - -table ------ - -The ``table`` formatter uses PrettyTable_ to produce output -formatted for human consumption. - -.. _PrettyTable: http://code.google.com/p/prettytable/ - -:: - - (.venv)$ cliffdemo file setup.py - +---------------+--------------+ - | Field | Value | - +---------------+--------------+ - | Name | setup.py | - | Size | 5825 | - | UID | 502 | - | GID | 20 | - | Modified Time | 1335569964.0 | - +---------------+--------------+ - yaml ---- @@ -121,13 +59,5 @@ sequence of mappings. - {Field: GID, Value: 501} - {Field: Modified Time, Value: 1336353173.0} -Creating Your Own Formatter ---------------------------- - -If the standard formatters do not meet your needs, you can bundle -another formatter with your program by subclassing from -:class:`cliff.formatters.base.ShowFormatter` and registering the -plugin in the ``cliff.formatter.show`` namespace. - .. _tablib: https://github.com/kennethreitz/tablib @@ -1,7 +1,5 @@ #!/usr/bin/env python -PROJECT = 'cliff' - # Change docs/source/conf.py too! VERSION = '1.0' @@ -21,15 +19,9 @@ try: except IOError: long_description = '' -install_requires = ['distribute', - 'PrettyTable', - 'cmd2', +install_requires = ['cliff', 'tablib', ] -try: - import argparse -except ImportError: - install_requires.append('argparse') ############################################################################## @@ -128,17 +120,16 @@ def find_package_data( setup( - name=PROJECT, + name='cliff-tablib', version=VERSION, - description='Command Line Interface Formulation Framework', + description='tablib formatters for cliff', long_description=long_description, author='Doug Hellmann', author_email='doug.hellmann@gmail.com', - url='https://github.com/dreamhost/cliff', - download_url='https://github.com/dreamhost/cliff/tarball/master', + url='https://github.com/dreamhost/cliff-tablib', classifiers=['Development Status :: 3 - Alpha', 'License :: OSI Approved :: Apache Software License', @@ -155,7 +146,7 @@ setup( scripts=[], - provides=['cliff', + provides=['clifftablib', ], install_requires=install_requires, @@ -165,25 +156,21 @@ setup( # Scan the input for package information # to grab any data files (text, images, etc.) # associated with sub-packages. - package_data=find_package_data(PROJECT, - package=PROJECT, + package_data=find_package_data('clifftablib', + package='clifftablib', only_in_packages=False, ), entry_points={ 'cliff.formatter.list': [ - 'table = cliff.formatters.table:TableFormatter', - 'csv = cliff.formatters.commaseparated:CSVLister', - 'yaml = cliff.formatters.tablibformatters:YamlFormatter', - 'html = cliff.formatters.tablibformatters:HtmlFormatter', - 'json = cliff.formatters.tablibformatters:JsonFormatter', + 'yaml = clifftablib.formatters:YamlFormatter', + 'html = clifftablib.formatters:HtmlFormatter', + 'json = clifftablib.formatters:JsonFormatter', ], 'cliff.formatter.show': [ - 'table = cliff.formatters.table:TableFormatter', - 'shell = cliff.formatters.shell:ShellFormatter', - 'yaml = cliff.formatters.tablibformatters:YamlFormatter', - 'html = cliff.formatters.tablibformatters:HtmlFormatter', - 'json = cliff.formatters.tablibformatters:JsonFormatter', + 'yaml = clifftablib.formatters:YamlFormatter', + 'html = clifftablib.formatters:HtmlFormatter', + 'json = clifftablib.formatters:JsonFormatter', ], }, diff --git a/test-requirements.txt b/test-requirements.txt index 9a04558..9ce317a 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,7 +2,6 @@ nose mock coverage pep8 -cmd2 +cliff distribute -PrettyTable tablib diff --git a/tests/test_app.py b/tests/test_app.py deleted file mode 100644 index 8bff071..0000000 --- a/tests/test_app.py +++ /dev/null @@ -1,170 +0,0 @@ -from cliff.app import App -from cliff.command import Command -from cliff.commandmanager import CommandManager - -import mock - - -def make_app(): - cmd_mgr = CommandManager('cliff.tests') - - # Register a command that succeeds - command = mock.MagicMock(spec=Command) - command_inst = mock.MagicMock(spec=Command) - command_inst.run.return_value = 0 - command.return_value = command_inst - cmd_mgr.add_command('mock', command) - - # Register a command that fails - err_command = mock.Mock(name='err_command', spec=Command) - err_command_inst = mock.Mock(spec=Command) - err_command_inst.run = mock.Mock(side_effect=RuntimeError('test exception')) - err_command.return_value = err_command_inst - cmd_mgr.add_command('error', err_command) - - app = App('testing interactive mode', - '1', - cmd_mgr, - stderr=mock.Mock(), # suppress warning messages - ) - return app, command - - -def test_no_args_triggers_interactive_mode(): - app, command = make_app() - app.interact = mock.MagicMock(name='inspect') - app.run([]) - app.interact.assert_called_once_with() - - -def test_interactive_mode_cmdloop(): - app, command = make_app() - app.interactive_app_factory = mock.MagicMock(name='interactive_app_factory') - app.run([]) - app.interactive_app_factory.return_value.cmdloop.assert_called_once_with() - - -def test_initialize_app(): - app, command = make_app() - app.initialize_app = mock.MagicMock(name='initialize_app') - app.run(['mock']) - app.initialize_app.assert_called_once_with(['mock']) - - -def test_prepare_to_run_command(): - app, command = make_app() - app.prepare_to_run_command = mock.MagicMock(name='prepare_to_run_command') - app.run(['mock']) - app.prepare_to_run_command.assert_called_once_with(command()) - - -def test_clean_up_success(): - app, command = make_app() - app.clean_up = mock.MagicMock(name='clean_up') - app.run(['mock']) - app.clean_up.assert_called_once_with(command.return_value, 0, None) - - -def test_clean_up_error(): - app, command = make_app() - - app.clean_up = mock.MagicMock(name='clean_up') - app.run(['error']) - - app.clean_up.assert_called_once() - call_args = app.clean_up.call_args_list[0] - assert call_args == mock.call(mock.ANY, 1, mock.ANY) - args, kwargs = call_args - assert isinstance(args[2], RuntimeError) - assert args[2].args == ('test exception',) - - -def test_clean_up_error_debug(): - app, command = make_app() - - app.clean_up = mock.MagicMock(name='clean_up') - try: - app.run(['--debug', 'error']) - except RuntimeError as err: - assert app.clean_up.call_args_list[0][0][2] is err - else: - assert False, 'Should have had an exception' - - app.clean_up.assert_called_once() - call_args = app.clean_up.call_args_list[0] - assert call_args == mock.call(mock.ANY, 1, mock.ANY) - args, kwargs = call_args - assert isinstance(args[2], RuntimeError) - assert args[2].args == ('test exception',) - - -def test_error_handling_clean_up_raises_exception(): - app, command = make_app() - - app.clean_up = mock.MagicMock( - name='clean_up', - side_effect=RuntimeError('within clean_up'), - ) - app.run(['error']) - - app.clean_up.assert_called_once() - call_args = app.clean_up.call_args_list[0] - assert call_args == mock.call(mock.ANY, 1, mock.ANY) - args, kwargs = call_args - assert isinstance(args[2], RuntimeError) - assert args[2].args == ('test exception',) - - -def test_error_handling_clean_up_raises_exception_debug(): - app, command = make_app() - - app.clean_up = mock.MagicMock( - name='clean_up', - side_effect=RuntimeError('within clean_up'), - ) - try: - app.run(['--debug', 'error']) - except RuntimeError as err: - if not hasattr(err, '__context__'): - # The exception passed to clean_up is not the exception - # caused *by* clean_up. This test is only valid in python - # 2 because under v3 the original exception is re-raised - # with the new one as a __context__ attribute. - assert app.clean_up.call_args_list[0][0][2] is not err - else: - assert False, 'Should have had an exception' - - app.clean_up.assert_called_once() - call_args = app.clean_up.call_args_list[0] - assert call_args == mock.call(mock.ANY, 1, mock.ANY) - args, kwargs = call_args - assert isinstance(args[2], RuntimeError) - assert args[2].args == ('test exception',) - - -def test_normal_clean_up_raises_exception(): - app, command = make_app() - - app.clean_up = mock.MagicMock( - name='clean_up', - side_effect=RuntimeError('within clean_up'), - ) - app.run(['mock']) - - app.clean_up.assert_called_once() - call_args = app.clean_up.call_args_list[0] - assert call_args == mock.call(mock.ANY, 0, None) - - -def test_normal_clean_up_raises_exception_debug(): - app, command = make_app() - - app.clean_up = mock.MagicMock( - name='clean_up', - side_effect=RuntimeError('within clean_up'), - ) - app.run(['--debug', 'mock']) - - app.clean_up.assert_called_once() - call_args = app.clean_up.call_args_list[0] - assert call_args == mock.call(mock.ANY, 0, None) diff --git a/tests/test_command.py b/tests/test_command.py deleted file mode 100644 index 39fde51..0000000 --- a/tests/test_command.py +++ /dev/null @@ -1,22 +0,0 @@ - -from cliff.command import Command - - -class TestCommand(Command): - """Description of command. - """ - - def take_action(self, parsed_args): - return - - -def test_get_description(): - cmd = TestCommand(None, None) - desc = cmd.get_description() - assert desc == "Description of command.\n " - - -def test_get_parser(): - cmd = TestCommand(None, None) - parser = cmd.get_parser('NAME') - assert parser.prog == 'NAME' diff --git a/tests/test_commandmanager.py b/tests/test_commandmanager.py deleted file mode 100644 index 1945f2e..0000000 --- a/tests/test_commandmanager.py +++ /dev/null @@ -1,95 +0,0 @@ - -import mock - -from cliff.commandmanager import CommandManager - - -class TestCommand(object): - @classmethod - def load(cls): - return cls - - def __init__(self): - return - - -class TestCommandManager(CommandManager): - def _load_commands(self): - self.commands = { - 'one': TestCommand, - 'two words': TestCommand, - 'three word command': TestCommand, - } - - -def test_lookup_and_find(): - def check(mgr, argv): - cmd, name, remaining = mgr.find_command(argv) - assert cmd - assert name == ' '.join(argv) - assert not remaining - mgr = TestCommandManager('test') - for expected in [['one'], - ['two', 'words'], - ['three', 'word', 'command'], - ]: - yield check, mgr, expected - return - - -def test_lookup_with_remainder(): - def check(mgr, argv): - cmd, name, remaining = mgr.find_command(argv) - assert cmd - assert remaining == ['--opt'] - mgr = TestCommandManager('test') - for expected in [['one', '--opt'], - ['two', 'words', '--opt'], - ['three', 'word', 'command', '--opt'], - ]: - yield check, mgr, expected - return - - -def test_find_invalid_command(): - mgr = TestCommandManager('test') - def check_one(argv): - try: - mgr.find_command(argv) - except ValueError as err: - assert '-b' in ('%s' % err) - else: - assert False, 'expected a failure' - for argv in [['a', '-b'], - ['-b'], - ]: - yield check_one, argv - - -def test_find_unknown_command(): - mgr = TestCommandManager('test') - try: - mgr.find_command(['a', 'b']) - except ValueError as err: - assert "['a', 'b']" in ('%s' % err) - else: - assert False, 'expected a failure' - - -def test_add_command(): - mgr = TestCommandManager('test') - mock_cmd = mock.Mock() - mgr.add_command('mock', mock_cmd) - found_cmd, name, args = mgr.find_command(['mock']) - assert found_cmd is mock_cmd - - -def test_load_commands(): - testcmd = mock.Mock(name='testcmd') - testcmd.name.replace.return_value = 'test' - mock_pkg_resources = mock.Mock(return_value=[testcmd]) - with mock.patch('pkg_resources.iter_entry_points', mock_pkg_resources) as iter_entry_points: - mgr = CommandManager('test') - assert iter_entry_points.called_once_with('test') - names = [n for n, v in mgr] - assert names == ['test'] diff --git a/tests/test_help.py b/tests/test_help.py deleted file mode 100644 index 6d67356..0000000 --- a/tests/test_help.py +++ /dev/null @@ -1,116 +0,0 @@ -try: - from StringIO import StringIO -except: - from io import StringIO - -import mock - -from cliff.app import App -from cliff.command import Command -from cliff.commandmanager import CommandManager -from cliff.help import HelpCommand - - -class TestParser(object): - - def print_help(self, stdout): - stdout.write('TestParser') - - -class TestCommand(Command): - - @classmethod - def load(cls): - return cls - - def get_parser(self, ignore): - # Make it look like this class is the parser - # so parse_args() is called. - return TestParser() - - def take_action(self, args): - return - - -class TestCommandManager(CommandManager): - def _load_commands(self): - self.commands = { - 'one': TestCommand, - 'two words': TestCommand, - 'three word command': TestCommand, - } - - -def test_show_help_for_command(): - # FIXME(dhellmann): Are commands tied too closely to the app? Or - # do commands know too much about apps by using them to get to the - # command manager? - stdout = StringIO() - app = App('testing', '1', TestCommandManager('cliff.test'), stdout=stdout) - app.NAME = 'test' - help_cmd = HelpCommand(app, mock.Mock()) - parser = help_cmd.get_parser('test') - parsed_args = parser.parse_args(['one']) - try: - help_cmd.run(parsed_args) - except SystemExit: - pass - assert stdout.getvalue() == 'TestParser' - - -def test_list_matching_commands(): - # FIXME(dhellmann): Are commands tied too closely to the app? Or - # do commands know too much about apps by using them to get to the - # command manager? - stdout = StringIO() - app = App('testing', '1', TestCommandManager('cliff.test'), stdout=stdout) - app.NAME = 'test' - help_cmd = HelpCommand(app, mock.Mock()) - parser = help_cmd.get_parser('test') - parsed_args = parser.parse_args(['t']) - try: - help_cmd.run(parsed_args) - except SystemExit: - pass - help_output = stdout.getvalue() - assert 'Command "t" matches:' in help_output - assert 'two' in help_output - assert 'three' in help_output - - -def test_list_matching_commands_no_match(): - # FIXME(dhellmann): Are commands tied too closely to the app? Or - # do commands know too much about apps by using them to get to the - # command manager? - stdout = StringIO() - app = App('testing', '1', TestCommandManager('cliff.test'), stdout=stdout) - app.NAME = 'test' - help_cmd = HelpCommand(app, mock.Mock()) - parser = help_cmd.get_parser('test') - parsed_args = parser.parse_args(['z']) - try: - help_cmd.run(parsed_args) - except SystemExit: - pass - except ValueError: - pass - else: - assert False, 'Should have seen a ValueError' - - -def test_show_help_for_help(): - # FIXME(dhellmann): Are commands tied too closely to the app? Or - # do commands know too much about apps by using them to get to the - # command manager? - stdout = StringIO() - app = App('testing', '1', TestCommandManager('cliff.test'), stdout=stdout) - app.NAME = 'test' - help_cmd = HelpCommand(app, mock.Mock()) - parser = help_cmd.get_parser('test') - parsed_args = parser.parse_args([]) - try: - help_cmd.run(parsed_args) - except SystemExit: - pass - help_text = stdout.getvalue() - assert 'usage: test help [-h]' in help_text @@ -2,7 +2,7 @@ envlist = py27,py32,pep8 [testenv] -commands = nosetests -d --with-coverage --cover-inclusive --cover-package cliff [] +commands = nosetests -d --with-coverage --cover-inclusive --cover-package clifftablib [] deps = nose mock @@ -10,4 +10,4 @@ deps = [testenv:pep8] deps = pep8 -commands = pep8 --repeat --ignore=E501 --ignore=E123 --show-source cliff +commands = pep8 --repeat --ignore=E501 --ignore=E123 --show-source clifftablib |