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
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:

@tjprescott Travis Prescott (tjprescott) Aug 26, 2016

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if obj.simple_output_query and not obj.is_query_active: [](start = 8, length = 55)

If you are going to ignore the callable if query is specified, I would recommend a warning log at a minimum indicating that's what's happening. #Resolved

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still recommend this, but you can address it (or not) in the next PR since you'll be working in this vicinity anyways. :)


In reply to: 76473993 [](ancestors = 76473993)

if callable(obj.simple_output_query):

@tjprescott Travis Prescott (tjprescott) Aug 26, 2016

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If simple output query is just the callable, why not rename it "table_transformer" or something. The old name was always pretty vague and now it's REALLY vague considering it only does that one thing. You shouldn't need the callable check on it now since we're saying that the callable is the only supported thing. If a dev passed a query string he should be given an error message he can debug rather than have it simply ignore him (or her). :) #Resolved

@derekbekoe Derek Bekoe (derekbekoe) Aug 26, 2016

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep agreed but since I plan to remove the jmespath queries in another PR, I still needed the if statement.
The name will also change from simple_output_query. #Resolved

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.')

@tjprescott Travis Prescott (tjprescott) Aug 26, 2016

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this what we talked about at the meeting, where if there is data but the extracted table is empty we throw an error? #Resolved

@derekbekoe Derek Bekoe (derekbekoe) Aug 26, 2016

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep #Resolved

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