Skip to content

Commit

Permalink
Table format revisions (Azure#748)
Browse files Browse the repository at this point in the history
* Table format revisions

- Table output is generic and automatically extracts fields from the result.
- No longer use simple_output_query as it was specific for commands and as packages will support multiple API versions, this solution is no longer feasible.

* Support callable again for table format after discussion

When a callable is set for a command, it will be used as long as there is no query active.
If there's a query active, the callable will not be used so the user has to specify a full query that can generate an appropriate table.
  • Loading branch information
derekbekoe authored Aug 26, 2016
1 parent 7e2b7f8 commit 975904d
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 128 deletions.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ pylint==1.5.4
pyyaml==3.11
requests==2.9.1
six==1.10.0
tabulate==0.7.5
vcrpy==1.7.4
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
'pyyaml',
'requests',
'six',
'tabulate',
]

if sys.version_info < (3, 4):
Expand Down
123 changes: 51 additions & 72 deletions src/azure/cli/_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from collections import OrderedDict
from six import StringIO, text_type, u
import colorama
from tabulate import tabulate

from azure.cli._util import CLIError
import azure.cli._logging as _logging
Expand Down Expand Up @@ -40,31 +41,6 @@ def format_json_color(obj):
from pygments import highlight, lexers, formatters
return highlight(format_json(obj), lexers.JsonLexer(), formatters.TerminalFormatter()) # pylint: disable=no-member

def format_table(obj):
result = obj.result
try:
if not obj.simple_output_query and not obj.is_query_active:
raise ValueError('No query specified and no built-in query available.')
if obj.simple_output_query and not obj.is_query_active:
if callable(obj.simple_output_query):
result = obj.simple_output_query(result)
else:
from jmespath import compile as compile_jmespath, search, Options
result = compile_jmespath(obj.simple_output_query).search(result,
Options(OrderedDict))
obj_list = result if isinstance(result, list) else [result]
to = TableOutput()
for item in obj_list:
for item_key in item:
to.cell(item_key, item[item_key])
to.end_row()
return to.dump()
except (ValueError, KeyError, TypeError):
logger.debug(traceback.format_exc())
raise CLIError("Table output unavailable. "\
"Change output type with --output or use "\
"the --query option to specify an appropriate query. "\
"Use --debug for more info.")

def format_text(obj):
result = obj.result
Expand All @@ -78,6 +54,20 @@ def format_text(obj):
except TypeError:
return ''

def format_table(obj):
result = obj.result
try:
if obj.simple_output_query and not obj.is_query_active:
if callable(obj.simple_output_query):
result = obj.simple_output_query(result)
result_list = result if isinstance(result, list) else [result]
return TableOutput.dump(result_list)
except:
logger.debug(traceback.format_exc())
raise CLIError("Table output unavailable. "\
"Use the --query option to specify an appropriate query. "\
"Use --debug for more info.")

def format_list(obj):
result = obj.result
result_list = result if isinstance(result, list) else [result]
Expand Down Expand Up @@ -125,6 +115,42 @@ def out(self, obj):
def get_formatter(format_type):
return OutputProducer.format_dict.get(format_type, format_list)

class TableOutput(object): #pylint: disable=too-few-public-methods

SKIP_KEYS = ['id', 'type']

@staticmethod
def _capitalize_first_char(x):
return x[0].upper() + x[1:] if x and len(x) > 0 else x

@staticmethod
def _auto_table_item(item):
new_entry = OrderedDict()
for k in item.keys():
if k in TableOutput.SKIP_KEYS:
continue
if item[k] and not isinstance(item[k], (list, dict, set)):
new_entry[TableOutput._capitalize_first_char(k)] = item[k]
return new_entry

@staticmethod
def _auto_table(result):
if isinstance(result, list):
new_result = []
for item in result:
new_result.append(TableOutput._auto_table_item(item))
return new_result
else:
return TableOutput._auto_table_item(result)

@staticmethod
def dump(data):
table_data = TableOutput._auto_table(data)
table_str = tabulate(table_data, headers="keys", tablefmt="simple") if table_data else ''
if table_str == '\n':
raise ValueError('Unable to extract fields for table.')
return table_str + '\n'

class ListOutput(object): #pylint: disable=too-few-public-methods

# Match the capital letters in a camel case string
Expand Down Expand Up @@ -196,53 +222,6 @@ def dump(self, data):
io.close()
return result

class TableOutput(object):

unsupported_types = (list, dict, set)

def __init__(self):
self._rows = [{}]
self._columns = {}
self._column_order = []

def dump(self):
if len(self._rows) == 1:
return

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.get(c, '-').ljust(w) for c, w in cols))
io.write('\n')
result = io.getvalue()
io.close()
return result

@property
def any_rows(self):
return len(self._rows) > 1

def cell(self, name, value):
if isinstance(value, TableOutput.unsupported_types):
raise TypeError('Table output does not support objects of type {}.\n'\
'Offending object name={} value={}'.format(
[ut.__name__ for ut in TableOutput.unsupported_types], name, value))
n = str(name)
v = str(value)
max_width = self._columns.get(n)
if max_width is None:
self._column_order.append(n)
max_width = len(n)
self._rows[-1][n] = v
self._columns[n] = max(max_width, len(v))

def end_row(self):
self._rows.append({})

class TextOutput(object):

def __init__(self):
Expand Down
72 changes: 17 additions & 55 deletions src/azure/cli/tests/test_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from __future__ import print_function
# pylint: disable=protected-access, bad-continuation, too-many-public-methods, trailing-whitespace
import unittest
from collections import OrderedDict
from six import StringIO

from azure.cli._output import (OutputProducer, format_json, format_table, format_list,
Expand Down Expand Up @@ -69,69 +70,32 @@ def test_out_boolean_valid(self):

# TABLE output tests

def test_out_table_valid_query1(self):
def test_out_table(self):
output_producer = OutputProducer(formatter=format_table, file=self.io)
result_item = CommandResultItem([{'name': 'qwerty', 'id': '0b1f6472qwerty'},
{'name': 'asdf', 'id': '0b1f6472asdf'}],
simple_output_query='[*].{Name:name, Id:id}')
output_producer.out(result_item)
self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines(
""" Name | Id
-------|---------------
qwerty | 0b1f6472qwerty
asdf | 0b1f6472asdf
"""))

def test_out_table_no_query(self):
output_producer = OutputProducer(formatter=format_table, file=self.io)
with self.assertRaises(util.CLIError):
output_producer.out(CommandResultItem({'active': True, 'id': '0b1f6472'}))

def test_out_table_valid_query2(self):
output_producer = OutputProducer(formatter=format_table, file=self.io)
result_item = CommandResultItem([{'name': 'qwerty', 'id': '0b1f6472qwerty'},
{'name': 'asdf', 'id': '0b1f6472asdf'}],
simple_output_query='[*].{Name:name}')
output_producer.out(result_item)
obj = OrderedDict()
obj['active'] = True
obj['val'] = '0b1f6472'
output_producer.out(CommandResultItem(obj))
self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines(
""" Name
------
qwerty
asdf
""" Active Val
-------- --------
1 0b1f6472
"""))

def test_out_table_bad_query(self):
output_producer = OutputProducer(formatter=format_table, file=self.io)
result_item = CommandResultItem([{'name': 'qwerty', 'id': '0b1f6472qwerty'},
{'name': 'asdf', 'id': '0b1f6472asdf'}],
simple_output_query='[*].{Name:name')
with self.assertRaises(util.CLIError):
output_producer.out(result_item)

def test_out_table_complex_obj(self):
output_producer = OutputProducer(formatter=format_table, file=self.io)
result_item = CommandResultItem([{'name': 'qwerty', 'id': '0b1f6472qwerty', 'sub': {'1'}}])
with self.assertRaises(util.CLIError):
output_producer.out(result_item)

def test_out_table_complex_obj_with_query_ok(self):
output_producer = OutputProducer(formatter=format_table, file=self.io)
result_item = CommandResultItem([{'name': 'qwerty', 'id': '0b1f6472qwerty', 'sub': {'1'}}],
simple_output_query='[*].{Name:name}')
obj = OrderedDict()
obj['name'] = 'qwerty'
obj['val'] = '0b1f6472qwerty'
obj['sub'] = {'1'}
result_item = CommandResultItem(obj)
output_producer.out(result_item)
self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines(
""" Name
------
qwerty
"""Name Val
------ --------------
qwerty 0b1f6472qwerty
"""))

def test_out_table_complex_obj_with_query_still_complex(self):
output_producer = OutputProducer(formatter=format_table, file=self.io)
result_item = CommandResultItem([{'name': 'qwerty', 'id': '0b1f6472qwerty', 'sub': {'1'}}],
simple_output_query='[*].{Name:name, Sub:sub}')
with self.assertRaises(util.CLIError):
output_producer.out(result_item)

# LIST output tests

def test_out_list_valid(self):
Expand Down Expand Up @@ -266,15 +230,13 @@ def test_output_format_dict_sort(self):
self.assertEqual(result, '2\t1\n')

def test_output_format_ordereddict_not_sorted(self):
from collections import OrderedDict
obj = OrderedDict()
obj['B'] = 1
obj['A'] = 2
result = format_tsv(CommandResultItem(obj))
self.assertEqual(result, '1\t2\n')

def test_output_format_ordereddict_list_not_sorted(self):
from collections import OrderedDict
obj1 = OrderedDict()
obj1['B'] = 1
obj1['A'] = 2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ def transform_metrics_list_output(result):
new_entry['Interval'] = interval
new_entry['Enabled'] = item['enabled']
new_entry['IncludeApis'] = item['includeApis']
new_entry['RetentionPolicy)'] = item['retentionPolicy']['days']
new_entry['RetentionPolicy'] = item['retentionPolicy']['days']
new_result.append(new_entry)
return new_result

Expand Down

0 comments on commit 975904d

Please sign in to comment.