Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 109 additions & 29 deletions src/azure/cli/_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -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):
Expand Down Expand Up @@ -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

109 changes: 106 additions & 3 deletions src/azure/cli/tests/test_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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(
Expand All @@ -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()