Changed calling signature of output_for_cli(); started work on 'textui' backend plugin

This commit is contained in:
Jason Gerard DeRose
2008-11-12 00:46:04 -07:00
parent f3869d7b24
commit 014af24731
5 changed files with 237 additions and 64 deletions

7
ipa
View File

@@ -30,7 +30,12 @@ from ipalib import api
from ipalib.cli import CLI from ipalib.cli import CLI
if __name__ == '__main__': 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, 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()) sys.exit(cli.run())

View File

@@ -22,20 +22,20 @@ Functionality for Command Line Interface.
""" """
import re import re
import textwrap
import sys import sys
import code import code
import optparse import optparse
import socket import socket
import frontend import frontend
import backend
import errors import errors
import plugable import plugable
import ipa_types import ipa_types
from config import set_default_env, read_config from config import set_default_env, read_config
import util import util
from constants import CLI_TAB
def exit_error(error):
sys.exit('ipa: ERROR: %s' % error)
def to_cli(name): def to_cli(name):
@@ -55,21 +55,189 @@ def from_cli(cli_name):
return str(cli_name).replace('-', '_') 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) dashes = '-' * len(string)
if top: if above:
print dashes print dashes
print string print string
if bottom: if below:
print dashes print dashes
def print_name(self, **kw): def print_line(self, text, width=None):
self.print_dashed('%s:' % self.name, **kw) """
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): class help(frontend.Application):
@@ -87,10 +255,8 @@ class help(frontend.Application):
self.application.build_parser(cmd).print_help() self.application.build_parser(cmd).print_help()
class console(frontend.Application): class console(frontend.Application):
'Start the IPA interactive Python console.' """Start the IPA interactive Python console."""
def run(self): def run(self):
code.interact( 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' 'Show attributes on dynamic API object'
takes_args = ('namespaces*',) takes_args = ('namespaces*',)
@@ -153,7 +319,7 @@ class show_api(text_ui):
self.__traverse_namespace(n, attr, lines, tab + 2) self.__traverse_namespace(n, attr, lines, tab + 2)
class plugins(text_ui): class plugins(frontend.Application):
"""Show all loaded plugins""" """Show all loaded plugins"""
def run(self): def run(self):
@@ -162,21 +328,13 @@ class plugins(text_ui):
(p.plugin, p.bases) for p in plugins (p.plugin, p.bases) for p in plugins
) )
def output_for_cli(self, result): def output_for_cli(self, textui, result, **kw):
self.print_name() textui.print_name(self.name)
first = True
for (plugin, bases) in result: for (plugin, bases) in result:
if first: textui.print_indented(
first = False '%s: %s' % (plugin, ', '.join(bases))
else: )
print '' textui.print_count(result, '%d plugin loaded', '%s plugins loaded')
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)
cli_application_commands = ( cli_application_commands = (
@@ -293,6 +451,7 @@ class CLI(object):
self.api.load_plugins() self.api.load_plugins()
for klass in cli_application_commands: for klass in cli_application_commands:
self.api.register(klass) self.api.register(klass)
self.api.register(textui)
def bootstrap(self): def bootstrap(self):
""" """
@@ -376,7 +535,7 @@ class CLI(object):
try: try:
ret = cmd(**kw) ret = cmd(**kw)
if callable(cmd.output_for_cli): if callable(cmd.output_for_cli):
cmd.output_for_cli(ret) cmd.output_for_cli(self.api.Backend.textui, ret, **kw)
return 0 return 0
except socket.error, e: except socket.error, e:
print e[1] print e[1]

View File

@@ -22,6 +22,8 @@
All constants centralized in one file. 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] # The section to read in the config files, i.e. [global]
CONFIG_SECTION = 'global' CONFIG_SECTION = 'global'

View File

@@ -24,22 +24,11 @@ Misc frontend plugins.
from ipalib import api, Command, Param, Bool from ipalib import api, Command, Param, Bool
class env_and_context(Command): # FIXME: We should not let env return anything in_server
""" # when mode == 'production'. This would allow an attacker to see the
Base class for `env` and `context` commands. # configuration of the server, potentially revealing compromising
""" # information. However, it's damn handy for testing/debugging.
class env(Command):
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):
"""Show environment variables""" """Show environment variables"""
takes_options = ( 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): def execute(self):
return tuple( return tuple(
(key, self.api.env[key]) for key in self.api.env (key, self.api.env[key]) for key in self.api.env
) )
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(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
]
api.register(context)

View File

@@ -25,6 +25,31 @@ from tests.util import raises, get_api, ClassChecker
from ipalib import cli, plugable, frontend, backend 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(): def test_to_cli():
""" """
Test the `ipalib.cli.to_cli` function. Test the `ipalib.cli.to_cli` function.