diff --git a/tuskarclient/common/formatting.py b/tuskarclient/common/formatting.py new file mode 100644 index 0000000..eab1a73 --- /dev/null +++ b/tuskarclient/common/formatting.py @@ -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]) diff --git a/tuskarclient/common/utils.py b/tuskarclient/common/utils.py index 3c4525b..2298bfa 100644 --- a/tuskarclient/common/utils.py +++ b/tuskarclient/common/utils.py @@ -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 diff --git a/tuskarclient/tests/common/test_formatting.py b/tuskarclient/tests/common/test_formatting.py new file mode 100644 index 0000000..38c369a --- /dev/null +++ b/tuskarclient/tests/common/test_formatting.py @@ -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), + ) diff --git a/tuskarclient/tests/common/test_utils.py b/tuskarclient/tests/common/test_utils.py index 759bff8..ed2c4c1 100644 --- a/tuskarclient/tests/common/test_utils.py +++ b/tuskarclient/tests/common/test_utils.py @@ -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): diff --git a/tuskarclient/v1/racks_shell.py b/tuskarclient/v1/racks_shell.py index da008c4..e3c18ae 100644 --- a/tuskarclient/v1/racks_shell.py +++ b/tuskarclient/v1/racks_shell.py @@ -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)