From 014af24731ff39520a9635694ed99dc9d09669c9 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 12 Nov 2008 00:46:04 -0700 Subject: Changed calling signature of output_for_cli(); started work on 'textui' backend plugin --- ipa | 7 +- ipalib/cli.py | 219 ++++++++++++++++++++++++++++++++++++------ ipalib/constants.py | 2 + ipalib/plugins/f_misc.py | 48 +++------ tests/test_ipalib/test_cli.py | 25 +++++ 5 files changed, 237 insertions(+), 64 deletions(-) diff --git a/ipa b/ipa index 67e8d10c4..cab12b6ba 100755 --- a/ipa +++ b/ipa @@ -30,7 +30,12 @@ from ipalib import api from ipalib.cli import CLI if __name__ == '__main__': + # If we can't explicitly determin the encoding, we assume UTF-8: + if sys.stdin.encoding is None: + encoding = 'UTF-8' + else: + encoding = sys.stdin.encoding cli = CLI(api, - (s.decode('utf-8') for s in sys.argv[1:]) + (s.decode(encoding) for s in sys.argv[1:]) ) sys.exit(cli.run()) diff --git a/ipalib/cli.py b/ipalib/cli.py index d72881547..8878c2124 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -22,20 +22,20 @@ Functionality for Command Line Interface. """ import re +import textwrap import sys import code import optparse import socket import frontend +import backend import errors import plugable import ipa_types from config import set_default_env, read_config import util - -def exit_error(error): - sys.exit('ipa: ERROR: %s' % error) +from constants import CLI_TAB def to_cli(name): @@ -55,21 +55,189 @@ def from_cli(cli_name): return str(cli_name).replace('-', '_') -class text_ui(frontend.Application): +class textui(backend.Backend): """ - Base class for CLI commands with special output needs. + Backend plugin to nicely format output to stdout. """ - def print_dashed(self, string, top=True, bottom=True): + def get_tty_width(self): + """ + Return the width (in characters) of output tty. + + If stdout is not a tty, this method will return ``None``. + """ + if sys.stdout.isatty(): + return 80 # FIXME: we need to return the actual tty width + + def max_col_width(self, rows, col=None): + """ + Return the max width (in characters) of a specified column. + + For example: + + >>> ui = textui() + >>> rows = [ + ... ('a', 'package'), + ... ('an', 'egg'), + ... ] + >>> ui.max_col_width(rows, col=0) # len('an') + 2 + >>> ui.max_col_width(rows, col=1) # len('package') + 7 + >>> ui.max_col_width(['a', 'cherry', 'py']) # len('cherry') + 6 + """ + if type(rows) not in (list, tuple): + raise TypeError( + 'rows: need %r or %r; got %r' % (list, tuple, rows) + ) + if len(rows) == 0: + return 0 + if col is None: + return max(len(row) for row in rows) + return max(len(row[col]) for row in rows) + + def print_dashed(self, string, above=True, below=True): + """ + Print a string with with a dashed line above and/or below. + + For example: + + >>> ui = textui() + >>> ui.print_dashed('Dashed above and below.') + ----------------------- + Dashed above and below. + ----------------------- + >>> ui.print_dashed('Only dashed below.', above=False) + Only dashed below. + ------------------ + >>> ui.print_dashed('Only dashed above.', below=False) + ------------------ + Only dashed above. + """ dashes = '-' * len(string) - if top: + if above: print dashes print string - if bottom: + if below: print dashes - def print_name(self, **kw): - self.print_dashed('%s:' % self.name, **kw) + def print_line(self, text, width=None): + """ + Force printing on a single line, using ellipsis if needed. + + For example: + + >>> ui = textui() + >>> ui.print_line('This line can fit!', width=18) + This line can fit! + >>> ui.print_line('This line wont quite fit!', width=18) + This line wont ... + + The above example aside, you normally should not specify the + ``width``. When you don't, it is automatically determined by calling + `textui.get_tty_width()`. + """ + if width is None: + width = self.get_tty_width() + if width is not None and width < len(text): + text = text[:width - 3] + '...' + print text + + def print_indented(self, text, indent=1): + """ + Print at specified indentation level. + + For example: + + >>> ui = textui() + >>> ui.print_indented('One indentation level.') + One indentation level. + >>> ui.print_indented('Two indentation levels.', indent=2) + Two indentation levels. + >>> ui.print_indented('No indentation.', indent=0) + No indentation. + """ + print (CLI_TAB * indent + text) + + def print_name(self, name): + """ + Print a command name. + + The typical use for this is to mark the start of output from a + command. For example, a hypothetical ``show_status`` command would + output something like this: + + >>> ui = textui() + >>> ui.print_name('show_status') + ------------ + show-status: + ------------ + """ + self.print_dashed('%s:' % to_cli(name)) + + def print_keyval(self, rows, indent=1): + """ + Print (key = value) pairs, one pair per line. + + For example: + + >>> items = [ + ... ('in_server', True), + ... ('mode', 'production'), + ... ] + >>> ui = textui() + >>> ui.print_keyval(items) + in_server = True + mode = 'production' + >>> ui.print_keyval(items, indent=0) + in_server = True + mode = 'production' + + Also see `textui.print_indented`. + """ + for row in rows: + self.print_indented('%s = %r' % row, indent) + + def print_count(self, count, singular, plural=None): + """ + Print a summary count. + + The typical use for this is to print the number of items returned + by a command, especially when this return count can vary. This + preferably should be used as a summary and should be the final text + a command outputs. + + For example: + + >>> ui = textui() + >>> ui.print_count(1, '%d goose', '%d geese') + ------- + 1 goose + ------- + >>> ui.print_count(['Don', 'Sue'], 'Found %d user', 'Found %d users') + ------------- + Found 2 users + ------------- + + If ``count`` is not an integer, it must be a list or tuple, and then + ``len(count)`` is used as the count. + """ + if type(count) is not int: + assert type(count) in (list, tuple) + count = len(count) + self.print_dashed( + self.choose_number(count, singular, plural) + ) + + def choose_number(self, n, singular, plural=None): + if n == 1 or plural is None: + return singular % n + return plural % n + + +def exit_error(error): + sys.exit('ipa: ERROR: %s' % error) class help(frontend.Application): @@ -87,10 +255,8 @@ class help(frontend.Application): self.application.build_parser(cmd).print_help() - - class console(frontend.Application): - 'Start the IPA interactive Python console.' + """Start the IPA interactive Python console.""" def run(self): code.interact( @@ -99,7 +265,7 @@ class console(frontend.Application): ) -class show_api(text_ui): +class show_api(frontend.Application): 'Show attributes on dynamic API object' takes_args = ('namespaces*',) @@ -153,7 +319,7 @@ class show_api(text_ui): self.__traverse_namespace(n, attr, lines, tab + 2) -class plugins(text_ui): +class plugins(frontend.Application): """Show all loaded plugins""" def run(self): @@ -162,21 +328,13 @@ class plugins(text_ui): (p.plugin, p.bases) for p in plugins ) - def output_for_cli(self, result): - self.print_name() - first = True + def output_for_cli(self, textui, result, **kw): + textui.print_name(self.name) for (plugin, bases) in result: - if first: - first = False - else: - print '' - print ' Plugin: %s' % plugin - print ' In namespaces: %s' % ', '.join(bases) - if len(result) == 1: - s = '1 plugin loaded.' - else: - s = '%d plugins loaded.' % len(result) - self.print_dashed(s) + textui.print_indented( + '%s: %s' % (plugin, ', '.join(bases)) + ) + textui.print_count(result, '%d plugin loaded', '%s plugins loaded') cli_application_commands = ( @@ -293,6 +451,7 @@ class CLI(object): self.api.load_plugins() for klass in cli_application_commands: self.api.register(klass) + self.api.register(textui) def bootstrap(self): """ @@ -376,7 +535,7 @@ class CLI(object): try: ret = cmd(**kw) if callable(cmd.output_for_cli): - cmd.output_for_cli(ret) + cmd.output_for_cli(self.api.Backend.textui, ret, **kw) return 0 except socket.error, e: print e[1] diff --git a/ipalib/constants.py b/ipalib/constants.py index b8f93d211..6210e6c8f 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -22,6 +22,8 @@ All constants centralized in one file. """ +# Used for a tab (or indentation level) when formatting for CLI: +CLI_TAB = ' ' # Two spaces # The section to read in the config files, i.e. [global] CONFIG_SECTION = 'global' diff --git a/ipalib/plugins/f_misc.py b/ipalib/plugins/f_misc.py index ff8569b1b..055e54d75 100644 --- a/ipalib/plugins/f_misc.py +++ b/ipalib/plugins/f_misc.py @@ -24,22 +24,11 @@ Misc frontend plugins. from ipalib import api, Command, Param, Bool -class env_and_context(Command): - """ - Base class for `env` and `context` commands. - """ - - def run(self, **kw): - if kw.get('server', False) and not self.api.env.in_server: - return self.forward() - return self.execute() - - def output_for_cli(self, ret): - for (key, value) in ret: - print '%s = %r' % (key, value) - - -class env(env_and_context): +# FIXME: We should not let env return anything in_server +# when mode == 'production'. This would allow an attacker to see the +# configuration of the server, potentially revealing compromising +# information. However, it's damn handy for testing/debugging. +class env(Command): """Show environment variables""" takes_options = ( @@ -48,26 +37,19 @@ class env(env_and_context): ), ) + def run(self, **kw): + if kw.get('server', False) and not self.api.env.in_server: + return self.forward() + return self.execute() + def execute(self): return tuple( (key, self.api.env[key]) for key in self.api.env ) -api.register(env) - - -class context(env_and_context): - """Show request context""" - - takes_options = ( - Param('server?', type=Bool(), default=False, - doc='Show request context in server', - ), - ) - - def execute(self): - return [ - (key, self.api.context[key]) for key in self.api.Context - ] + def output_for_cli(self, textui, result, **kw): + textui.print_name(self.name) + textui.print_keyval(result) + textui.print_count(result, '%d variable', '%d variables') -api.register(context) +api.register(env) diff --git a/tests/test_ipalib/test_cli.py b/tests/test_ipalib/test_cli.py index 565459421..8cedd0881 100644 --- a/tests/test_ipalib/test_cli.py +++ b/tests/test_ipalib/test_cli.py @@ -25,6 +25,31 @@ from tests.util import raises, get_api, ClassChecker from ipalib import cli, plugable, frontend, backend +class test_textui(ClassChecker): + _cls = cli.textui + + def test_max_col_width(self): + """ + Test the `ipalib.cli.textui.max_col_width` method. + """ + o = self.cls() + e = raises(TypeError, o.max_col_width, 'hello') + assert str(e) == 'rows: need %r or %r; got %r' % (list, tuple, 'hello') + rows = [ + 'hello', + 'naughty', + 'nurse', + ] + assert o.max_col_width(rows) == len('naughty') + rows = ( + ( 'a', 'bbb', 'ccccc'), + ('aa', 'bbbb', 'cccccc'), + ) + assert o.max_col_width(rows, col=0) == 2 + assert o.max_col_width(rows, col=1) == 4 + assert o.max_col_width(rows, col=2) == 6 + + def test_to_cli(): """ Test the `ipalib.cli.to_cli` function. -- cgit