diff --git a/src/azure/cli/_output.py b/src/azure/cli/_output.py index 511fc42b2c5..88acb27eb49 100644 --- a/src/azure/cli/_output.py +++ b/src/azure/cli/_output.py @@ -2,13 +2,9 @@ import sys import json +import re -try: - # Python 3 - from io import StringIO -except ImportError: - # Python 2 - from StringIO import StringIO #pylint: disable=import-error +from six import StringIO class OutputFormatException(Exception): pass @@ -40,12 +36,18 @@ def format_text(obj): except TypeError: return '' +def format_list(obj): + obj_list = obj if isinstance(obj, list) else [obj] + lo = ListOutput() + return lo.dump(obj_list) + class OutputProducer(object): #pylint: disable=too-few-public-methods format_dict = { 'json': format_json, 'table': format_table, - 'text': format_text + 'text': format_text, + 'list': format_list } def __init__(self, formatter=format_json, file=sys.stdout): #pylint: disable=redefined-builtin @@ -57,7 +59,81 @@ def out(self, obj): @staticmethod def get_formatter(format_type): - return OutputProducer.format_dict.get(format_type, format_json) + return OutputProducer.format_dict.get(format_type, format_list) + +class ListOutput(object): #pylint: disable=too-few-public-methods + + # Match the capital letters in a camel case string + FORMAT_KEYS_PATTERN = re.compile('([A-Z][^A-Z]*)') + + def __init__(self): + self._formatted_keys_cache = {} + + @staticmethod + def _get_max_key_len(keys): + return len(max(keys, key=len)) if keys else 0 + + @staticmethod + def _sort_key_func(key, item): + # We want dictionaries to be last so use ASCII char 126 ~ to + # prefix dictionary and list key names. + if isinstance(item[key], dict): + return '~~'+key + elif isinstance(item[key], list): + return '~'+key + else: + return key + + def _get_formatted_key(self, key): + def _format_key(key): + words = [word for word in re.split(ListOutput.FORMAT_KEYS_PATTERN, key) if word] + return ' '.join(words).title() + + try: + return self._formatted_keys_cache[key] + except KeyError: + self._formatted_keys_cache[key] = _format_key(key) + return self._formatted_keys_cache[key] + + @staticmethod + def _dump_line(io, line, indent): + io.write(' ' * indent) + io.write(line) + io.write('\n') + + def _dump_object(self, io, obj, indent): + if isinstance(obj, list): + for array_item in obj: + self._dump_object(io, array_item, indent) + elif isinstance(obj, dict): + # Get the formatted keys for this item + # Skip dicts/lists because those will be handled recursively later. + # We use this object to calc key width and don't want to dicts/lists in this. + obj_fk = {k: self._get_formatted_key(k) + for k in obj if not isinstance(obj[k], dict) and not isinstance(obj[k], list)} + key_width = ListOutput._get_max_key_len(obj_fk.values()) + for key in sorted(obj, key=lambda x: ListOutput._sort_key_func(x, obj)): + if isinstance(obj[key], dict) or isinstance(obj[key], list): + # complex object + io.write('\n') + ListOutput._dump_line(io, self._get_formatted_key(key).upper(), indent+1) + self._dump_object(io, obj[key] if obj[key] else 'None', indent+1) + else: + # non-complex so write it + line = '%s : %s' % (self._get_formatted_key(key).ljust(key_width), + 'None' if obj[key] is None else obj[key]) + ListOutput._dump_line(io, line, indent) + else: + ListOutput._dump_line(io, obj, indent) + + def dump(self, data): + io = StringIO() + for obj in data: + self._dump_object(io, obj, 0) + io.write('\n') + result = io.getvalue() + io.close() + return result class TableOutput(object): def __init__(self): @@ -69,16 +145,18 @@ def dump(self): if len(self._rows) == 1: return - with StringIO() as io: - cols = [(c, self._columns[c]) for c in self._column_order] - io.write(' | '.join(c.center(w) for c, w in cols)) + io = StringIO() + cols = [(c, self._columns[c]) for c in self._column_order] + io.write(' | '.join(c.center(w) for c, w in cols)) + io.write('\n') + io.write('-|-'.join('-' * w for c, w in cols)) + io.write('\n') + for r in self._rows[:-1]: + io.write(' | '.join(r[c].ljust(w) for c, w in cols)) io.write('\n') - io.write('-|-'.join('-' * w for c, w in cols)) - io.write('\n') - for r in self._rows[:-1]: - io.write(' | '.join(r[c].ljust(w) for c, w in cols)) - io.write('\n') - return io.getvalue() + result = io.getvalue() + io.close() + return result @property def any_rows(self): @@ -109,17 +187,19 @@ def add(self, identifier, value): self.identifiers[identifier] = [value] def dump(self): - with StringIO() as io: - for identifier in sorted(self.identifiers): - io.write(identifier.upper()) + io = StringIO() + for identifier in sorted(self.identifiers): + io.write(identifier.upper()) + io.write('\t') + for col in self.identifiers[identifier]: + if isinstance(col, str): + io.write(col) + else: + # TODO: Need to handle complex objects + io.write("null") io.write('\t') - for col in self.identifiers[identifier]: - if isinstance(col, str): - io.write(col) - else: - # TODO: Need to handle complex objects - io.write("null") - io.write('\t') - io.write('\n') - return io.getvalue() + io.write('\n') + result = io.getvalue() + io.close() + return result diff --git a/src/azure/cli/tests/test_output.py b/src/azure/cli/tests/test_output.py index 14ab85cb96c..c2c62f32357 100644 --- a/src/azure/cli/tests/test_output.py +++ b/src/azure/cli/tests/test_output.py @@ -9,7 +9,8 @@ # Python 2 from StringIO import StringIO -from azure.cli._output import OutputProducer, OutputFormatException, format_json, format_table, format_text +from azure.cli._output import (OutputProducer, OutputFormatException, format_json, format_table, format_list, format_text, + ListOutput) import azure.cli._util as util class TestOutput(unittest.TestCase): @@ -42,8 +43,6 @@ def test_out_json_valid(self): """)) def test_out_table_valid(self): - """ - """ output_producer = OutputProducer(formatter=format_table, file=self.io) output_producer.out({'active': True, 'id': '0b1f6472'}) self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( @@ -53,5 +52,109 @@ def test_out_table_valid(self): """)) + def test_out_list_valid(self): + output_producer = OutputProducer(formatter=format_list, file=self.io) + output_producer.out({'active': True, 'id': '0b1f6472'}) + self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( +"""Active : True +Id : 0b1f6472 + + +""")) + + def test_out_list_valid_none_val(self): + output_producer = OutputProducer(formatter=format_list, file=self.io) + output_producer.out({'active': None, 'id': '0b1f6472'}) + self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( +"""Active : None +Id : 0b1f6472 + + +""")) + + def test_out_list_valid_empty_array(self): + output_producer = OutputProducer(formatter=format_list, file=self.io) + output_producer.out({'active': None, 'id': '0b1f6472', 'hosts': []}) + self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( +"""Active : None +Id : 0b1f6472 + + HOSTS + None + + +""")) + + def test_out_list_valid_array_complex(self): + output_producer = OutputProducer(formatter=format_list, file=self.io) + output_producer.out([{'active': True, 'id': '783yesdf'}, {'active': False, 'id': '3hjnme32'}, {'active': False, 'id': '23hiujbs'}]) + self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( +"""Active : True +Id : 783yesdf + +Active : False +Id : 3hjnme32 + +Active : False +Id : 23hiujbs + + +""")) + + def test_out_list_valid_str_array(self): + output_producer = OutputProducer(formatter=format_list, file=self.io) + output_producer.out(['location', 'id', 'host', 'server']) + self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( +"""location + +id + +host + +server + + +""")) + + def test_out_list_valid_complex_array(self): + output_producer = OutputProducer(formatter=format_list, file=self.io) + output_producer.out({'active': True, 'id': '0b1f6472', 'myarray': ['1', '2', '3', '4']}) + self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( +"""Active : True +Id : 0b1f6472 + + MYARRAY + 1 + 2 + 3 + 4 + + +""")) + + def test_out_list_format_key_simple(self): + lo = ListOutput() + self.assertEqual(lo._formatted_keys_cache, {}) + lo._get_formatted_key('locationId') + self.assertEqual(lo._formatted_keys_cache, {'locationId': 'Location Id'}) + + def test_out_list_format_key_single(self): + lo = ListOutput() + self.assertEqual(lo._formatted_keys_cache, {}) + lo._get_formatted_key('location') + self.assertEqual(lo._formatted_keys_cache, {'location': 'Location'}) + + def test_out_list_format_key_multiple_caps(self): + lo = ListOutput() + self.assertEqual(lo._formatted_keys_cache, {}) + lo._get_formatted_key('fooIDS') + self.assertEqual(lo._formatted_keys_cache, {'fooIDS': 'Foo I D S'}) + + def test_out_list_format_key_multiple_words(self): + lo = ListOutput() + self.assertEqual(lo._formatted_keys_cache, {}) + lo._get_formatted_key('locationIdState') + self.assertEqual(lo._formatted_keys_cache, {'locationIdState': 'Location Id State'}) + if __name__ == '__main__': unittest.main()