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 os
|
||||||
import sys
|
import sys
|
||||||
import textwrap
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import prettytable
|
|
||||||
|
|
||||||
from tuskarclient import exc
|
from tuskarclient import exc
|
||||||
from tuskarclient.openstack.common import importutils
|
from tuskarclient.openstack.common import importutils
|
||||||
|
|
||||||
@ -62,50 +59,6 @@ def arg(*args, **kwargs):
|
|||||||
return _decorator
|
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):
|
def find_resource(manager, name_or_id):
|
||||||
"""Helper for the _find_* methods."""
|
"""Helper for the _find_* methods."""
|
||||||
# first try to get entity as integer id
|
# 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.
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
import cStringIO
|
|
||||||
import mock
|
import mock
|
||||||
import sys
|
|
||||||
|
|
||||||
from tuskarclient.common import utils
|
from tuskarclient.common import utils
|
||||||
from tuskarclient.tests import utils as test_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):
|
class DefineCommandsTest(test_utils.TestCase):
|
||||||
|
|
||||||
def test_define_commands_from_module(self):
|
def test_define_commands_from_module(self):
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import tuskarclient.common.formatting as fmt
|
||||||
from tuskarclient.common import utils
|
from tuskarclient.common import utils
|
||||||
from tuskarclient import exc
|
from tuskarclient import exc
|
||||||
|
|
||||||
@ -21,15 +22,28 @@ def do_rack_show(tuskar, args):
|
|||||||
rack = utils.find_resource(tuskar.racks, args.id)
|
rack = utils.find_resource(tuskar.racks, args.id)
|
||||||
except exc.HTTPNotFound:
|
except exc.HTTPNotFound:
|
||||||
raise exc.CommandError("Rack not found: %s" % args.id)
|
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
|
# TODO(jistr): This is PoC, not final implementation
|
||||||
def do_rack_list(tuskar, args):
|
def do_rack_list(tuskar, args):
|
||||||
racks = tuskar.racks.list()
|
racks = tuskar.racks.list()
|
||||||
fields = ['name', 'subnet', 'state', 'nodes']
|
fields = ['id', 'name', 'subnet', 'state', 'nodes']
|
||||||
labels = ["Name", "Subnet", "State", "Nodes"]
|
labels = {'nodes': '# of nodes'}
|
||||||
formatters = {'nodes': lambda rack: len(rack.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