Merge "Advanced print formatting for CLI"
This commit is contained in:
commit
bf2f35506c
143
tuskarclient/common/formatting.py
Normal file
143
tuskarclient/common/formatting.py
Normal file
@ -0,0 +1,143 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import prettytable
|
||||
|
||||
|
||||
def pretty_choice_list(l):
|
||||
return ', '.join("'%s'" % i for i in l)
|
||||
|
||||
|
||||
def print_list(objs, fields, formatters={}, custom_labels={}, sortby=0):
|
||||
'''Prints a list of objects.
|
||||
|
||||
:param objs: list of objects to print
|
||||
:param fields: list of attributes of the objects to print;
|
||||
attributes beginning with '!' have a special meaning - they
|
||||
should be used with custom field labels and formatters only,
|
||||
and the formatter receives the whole object
|
||||
:param formatters: dict of functions that perform pre-print
|
||||
formatting of attributes (keys are strings from `fields`
|
||||
parameter, values are functions that take one parameter - the
|
||||
attribute)
|
||||
:param custom_labels: dict of label overrides for fields (keys are
|
||||
strings from `fields` parameter, values are custom labels -
|
||||
headers of the table)
|
||||
'''
|
||||
field_labels = [custom_labels.get(f, f) for f in fields]
|
||||
pt = prettytable.PrettyTable([f for f in field_labels],
|
||||
caching=False, print_empty=False)
|
||||
pt.align = 'l'
|
||||
|
||||
for o in objs:
|
||||
row = []
|
||||
for field in fields:
|
||||
if field[0] == '!': # custom field
|
||||
if field in formatters:
|
||||
row.append(formatters[field](o))
|
||||
else:
|
||||
raise KeyError(
|
||||
'Custom field "%s" needs a formatter.' % field)
|
||||
else: # attribute-based field
|
||||
if hasattr(o, field) and field in formatters:
|
||||
row.append(formatters[field](getattr(o, field)))
|
||||
else:
|
||||
row.append(getattr(o, field, ''))
|
||||
pt.add_row(row)
|
||||
print pt.get_string(sortby=field_labels[sortby])
|
||||
|
||||
|
||||
def print_dict(d, formatters={}, custom_labels={}):
|
||||
'''Prints a dict.
|
||||
|
||||
:param d: dict to print
|
||||
:param formatters: dict of functions that perform pre-print
|
||||
formatting of dict values (keys are keys from `d` parameter,
|
||||
values are functions that take one parameter - the dict value
|
||||
to format)
|
||||
:param custom_labels: dict of label overrides for keys (keys are
|
||||
keys from `d` parameter, values are custom labels)
|
||||
'''
|
||||
pt = prettytable.PrettyTable(['Property', 'Value'],
|
||||
caching=False, print_empty=False)
|
||||
pt.align = 'l'
|
||||
|
||||
for field in d.keys():
|
||||
label = custom_labels.get(field, field)
|
||||
if field in formatters:
|
||||
pt.add_row([label, formatters[field](d[field])])
|
||||
else:
|
||||
pt.add_row([label, d[field]])
|
||||
print pt.get_string(sortby='Property')
|
||||
|
||||
|
||||
def attr_proxy(attr, formatter=lambda a: a, allow_undefined=True):
|
||||
'''Creates a new formatter function. It will format an object for
|
||||
output by printing it's attribute or running another formatter on
|
||||
that attribute.
|
||||
|
||||
:param attr: name of the attribute to look for on an object
|
||||
:param formatter: formatter to run on that attribute (if not given,
|
||||
the attribute is returned as-is)
|
||||
:param allow_undefined: if true, the created function will return
|
||||
None if `attr` is not defined on the formatted object
|
||||
'''
|
||||
def formatter_proxy(obj):
|
||||
try:
|
||||
attr_value = getattr(obj, attr)
|
||||
except AttributeError as e:
|
||||
if allow_undefined:
|
||||
return None
|
||||
else:
|
||||
raise e
|
||||
return formatter(attr_value)
|
||||
|
||||
return formatter_proxy
|
||||
|
||||
|
||||
def capacities_formatter(capacities):
|
||||
'''Formats a list of capacities for output. Capacity is a dict
|
||||
containing 'name', 'value' and 'unit' keys.
|
||||
'''
|
||||
sorted_capacities = sorted(capacities,
|
||||
lambda c1, c2: cmp(c1['name'], c2['name']))
|
||||
return '\n'.join(['{0}: {1} {2}'.format(c['name'], c['value'], c['unit'])
|
||||
for c in sorted_capacities])
|
||||
|
||||
|
||||
def links_formatter(links):
|
||||
'''Formats a list of links. Link is a dict that has 'href' and
|
||||
'rel' keys.
|
||||
'''
|
||||
sorted_links = sorted(links, lambda l1, l2: cmp(l1['rel'], l2['rel']))
|
||||
return '\n'.join(['{0}: {1}'.format(l['rel'], l['href'])
|
||||
for l in sorted_links])
|
||||
|
||||
|
||||
def resource_links_formatter(links):
|
||||
'''Formats an array of resource links. Resource link is a dict
|
||||
with keys 'id' and 'links'. Under 'links' key there is an array of
|
||||
links. Link is a dict with 'href' and 'rel' keys. Currently we
|
||||
expect only one link to be in the array, so we print the first
|
||||
one. (We cannot fetch by 'rel', values in 'rel' are not used
|
||||
consistently.)
|
||||
'''
|
||||
sorted_links = sorted(links, lambda l1, l2: cmp(l1['id'], l2['id']))
|
||||
return '\n'.join(['{0}: {1}'.format(l['id'], l['links'][0]['href'])
|
||||
for l in sorted_links])
|
||||
|
||||
|
||||
def resource_link_formatter(link):
|
||||
'''Formats one resource link. See docs of
|
||||
`resource_links_formatter` for more details.
|
||||
'''
|
||||
return resource_links_formatter([link])
|
@ -15,11 +15,8 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
import textwrap
|
||||
import uuid
|
||||
|
||||
import prettytable
|
||||
|
||||
from tuskarclient import exc
|
||||
from tuskarclient.openstack.common import importutils
|
||||
|
||||
@ -62,50 +59,6 @@ def arg(*args, **kwargs):
|
||||
return _decorator
|
||||
|
||||
|
||||
def pretty_choice_list(l):
|
||||
return ', '.join("'%s'" % i for i in l)
|
||||
|
||||
|
||||
def print_list(objs, fields, field_labels, formatters={}, sortby=0):
|
||||
pt = prettytable.PrettyTable([f for f in field_labels],
|
||||
caching=False, print_empty=False)
|
||||
pt.align = 'l'
|
||||
|
||||
for o in objs:
|
||||
row = []
|
||||
for field in fields:
|
||||
if field in formatters:
|
||||
row.append(formatters[field](o))
|
||||
else:
|
||||
data = getattr(o, field, '')
|
||||
row.append(data)
|
||||
pt.add_row(row)
|
||||
print pt.get_string(sortby=field_labels[sortby])
|
||||
|
||||
|
||||
def print_dict(d, dict_property="Property", wrap=0):
|
||||
pt = prettytable.PrettyTable([dict_property, 'Value'],
|
||||
caching=False, print_empty=False)
|
||||
pt.align = 'l'
|
||||
for k, v in d.iteritems():
|
||||
# convert dict to str to check length
|
||||
if isinstance(v, dict):
|
||||
v = str(v)
|
||||
if wrap > 0:
|
||||
v = textwrap.fill(str(v), wrap)
|
||||
# if value has a newline, add in multiple rows
|
||||
# e.g. fault with stacktrace
|
||||
if v and isinstance(v, basestring) and r'\n' in v:
|
||||
lines = v.strip().split(r'\n')
|
||||
col1 = k
|
||||
for line in lines:
|
||||
pt.add_row([col1, line])
|
||||
col1 = ''
|
||||
else:
|
||||
pt.add_row([k, v])
|
||||
print pt.get_string()
|
||||
|
||||
|
||||
def find_resource(manager, name_or_id):
|
||||
"""Helper for the _find_* methods."""
|
||||
# first try to get entity as integer id
|
||||
|
128
tuskarclient/tests/common/test_formatting.py
Normal file
128
tuskarclient/tests/common/test_formatting.py
Normal file
@ -0,0 +1,128 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import cStringIO
|
||||
import mock
|
||||
|
||||
import tuskarclient.common.formatting as fmt
|
||||
import tuskarclient.tests.utils as tutils
|
||||
|
||||
|
||||
class PrintTest(tutils.TestCase):
|
||||
|
||||
@mock.patch('sys.stdout', new_callable=cStringIO.StringIO)
|
||||
def test_print_dict(self, mock_out):
|
||||
dict_ = {'k': 'v', 'key': 'value'}
|
||||
formatters = {'key': lambda v: 'custom ' + v}
|
||||
custom_labels = {'k': 'custom_key'}
|
||||
|
||||
fmt.print_dict(dict_, formatters, custom_labels)
|
||||
self.assertEqual(
|
||||
('+------------+--------------+\n'
|
||||
'| Property | Value |\n'
|
||||
'+------------+--------------+\n'
|
||||
'| custom_key | v |\n'
|
||||
'| key | custom value |\n'
|
||||
'+------------+--------------+\n'),
|
||||
mock_out.getvalue()
|
||||
)
|
||||
|
||||
@mock.patch('sys.stdout', new_callable=cStringIO.StringIO)
|
||||
def test_print_list(self, mock_out):
|
||||
fields = ['thing', 'color', '!artistic_name']
|
||||
formatters = {
|
||||
'!artistic_name': lambda obj: '{0} {1}'.format(obj.color,
|
||||
obj.thing),
|
||||
'color': lambda c: c.split(' ')[1],
|
||||
}
|
||||
custom_labels = {'thing': 'name', '!artistic_name': 'artistic name'}
|
||||
|
||||
fmt.print_list(self.objects(), fields, formatters, custom_labels)
|
||||
self.assertEqual(
|
||||
('+------+-------+-----------------+\n'
|
||||
'| name | color | artistic name |\n'
|
||||
'+------+-------+-----------------+\n'
|
||||
'| moon | green | dark green moon |\n'
|
||||
'| sun | blue | bright blue sun |\n'
|
||||
'+------+-------+-----------------+\n'),
|
||||
mock_out.getvalue()
|
||||
)
|
||||
|
||||
@mock.patch('sys.stdout', new_callable=cStringIO.StringIO)
|
||||
def test_print_list_custom_field_without_formatter(self, mock_out):
|
||||
fields = ['!artistic_name']
|
||||
|
||||
self.assertRaises(KeyError, fmt.print_list, self.objects(), fields)
|
||||
|
||||
def objects(self):
|
||||
return [
|
||||
mock.Mock(thing='sun', color='bright blue'),
|
||||
mock.Mock(thing='moon', color='dark green'),
|
||||
]
|
||||
|
||||
|
||||
class FormattersTest(tutils.TestCase):
|
||||
|
||||
def test_attr_formatter_plain(self):
|
||||
obj = mock.Mock()
|
||||
obj.foo = 'bar'
|
||||
foo_formatter = fmt.attr_proxy('foo')
|
||||
self.assertEqual('bar', foo_formatter(obj))
|
||||
|
||||
def test_attr_formatter_chained(self):
|
||||
obj = mock.Mock()
|
||||
obj.letters = ['a', 'b', 'c']
|
||||
letters_formatter = fmt.attr_proxy('letters', len)
|
||||
self.assertEqual(3, letters_formatter(obj))
|
||||
|
||||
def test_capacities_formatter(self):
|
||||
capacities = [
|
||||
{'name': 'memory', 'value': '1024', 'unit': 'MB'},
|
||||
{'name': 'cpu', 'value': '2', 'unit': 'CPU'},
|
||||
]
|
||||
self.assertEqual(
|
||||
('cpu: 2 CPU\n'
|
||||
'memory: 1024 MB'),
|
||||
fmt.capacities_formatter(capacities),
|
||||
)
|
||||
|
||||
def test_links_formatter(self):
|
||||
links = [
|
||||
{'rel': 'self', 'href': 'http://self-url'},
|
||||
{'rel': 'parent', 'href': 'http://parent-url'},
|
||||
]
|
||||
self.assertEqual(
|
||||
('parent: http://parent-url\n'
|
||||
'self: http://self-url'),
|
||||
fmt.links_formatter(links),
|
||||
)
|
||||
|
||||
def test_resource_links_formatter(self):
|
||||
resource_links = [
|
||||
{'id': 3, 'links': [{'rel': 'self', 'href': 'http://three'}]},
|
||||
{'id': 5, 'links': [{'rel': 'self', 'href': 'http://five'}]},
|
||||
]
|
||||
self.assertEqual(
|
||||
('3: http://three\n'
|
||||
'5: http://five'),
|
||||
fmt.resource_links_formatter(resource_links),
|
||||
)
|
||||
|
||||
def test_resource_link_formatter(self):
|
||||
resource_link = {
|
||||
'id': 3,
|
||||
'links': [{'rel': 'self', 'href': 'http://three'}]
|
||||
}
|
||||
self.assertEqual(
|
||||
('3: http://three'),
|
||||
fmt.resource_link_formatter(resource_link),
|
||||
)
|
@ -14,40 +14,12 @@
|
||||
# under the License.
|
||||
|
||||
|
||||
import cStringIO
|
||||
import mock
|
||||
import sys
|
||||
|
||||
from tuskarclient.common import utils
|
||||
from tuskarclient.tests import utils as test_utils
|
||||
|
||||
|
||||
class UtilsTest(test_utils.TestCase):
|
||||
|
||||
def test_prettytable(self):
|
||||
class Struct:
|
||||
def __init__(self, **entries):
|
||||
self.__dict__.update(entries)
|
||||
|
||||
# test that the prettytable output is wellformatted (left-aligned)
|
||||
saved_stdout = sys.stdout
|
||||
try:
|
||||
sys.stdout = output_dict = cStringIO.StringIO()
|
||||
utils.print_dict({'K': 'k', 'Key': 'Value'})
|
||||
|
||||
finally:
|
||||
sys.stdout = saved_stdout
|
||||
|
||||
self.assertEqual(output_dict.getvalue(), '''\
|
||||
+----------+-------+
|
||||
| Property | Value |
|
||||
+----------+-------+
|
||||
| K | k |
|
||||
| Key | Value |
|
||||
+----------+-------+
|
||||
''')
|
||||
|
||||
|
||||
class DefineCommandsTest(test_utils.TestCase):
|
||||
|
||||
def test_define_commands_from_module(self):
|
||||
|
@ -10,6 +10,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import tuskarclient.common.formatting as fmt
|
||||
from tuskarclient.common import utils
|
||||
from tuskarclient import exc
|
||||
|
||||
@ -21,15 +22,28 @@ def do_rack_show(tuskar, args):
|
||||
rack = utils.find_resource(tuskar.racks, args.id)
|
||||
except exc.HTTPNotFound:
|
||||
raise exc.CommandError("Rack not found: %s" % args.id)
|
||||
formatters = {
|
||||
'capacities': fmt.capacities_formatter,
|
||||
'chassis': fmt.resource_link_formatter,
|
||||
'links': fmt.links_formatter,
|
||||
'nodes': fmt.resource_links_formatter,
|
||||
'resource_class': fmt.resource_link_formatter,
|
||||
}
|
||||
|
||||
utils.print_dict(rack.to_dict())
|
||||
rack_dict = rack.to_dict()
|
||||
# Workaround for API inconsistency, where empty chassis link
|
||||
# prints out as '{}'.
|
||||
if 'chassis' in rack_dict and not rack_dict['chassis']:
|
||||
del rack_dict['chassis']
|
||||
|
||||
fmt.print_dict(rack_dict, formatters)
|
||||
|
||||
|
||||
# TODO(jistr): This is PoC, not final implementation
|
||||
def do_rack_list(tuskar, args):
|
||||
racks = tuskar.racks.list()
|
||||
fields = ['name', 'subnet', 'state', 'nodes']
|
||||
labels = ["Name", "Subnet", "State", "Nodes"]
|
||||
formatters = {'nodes': lambda rack: len(rack.nodes)}
|
||||
fields = ['id', 'name', 'subnet', 'state', 'nodes']
|
||||
labels = {'nodes': '# of nodes'}
|
||||
formatters = {'nodes': len}
|
||||
|
||||
utils.print_list(racks, fields, labels, formatters, 0)
|
||||
fmt.print_list(racks, fields, formatters, labels)
|
||||
|
Loading…
x
Reference in New Issue
Block a user