diff --git a/requirements.txt b/requirements.txt index ee6b3b5bda9..1ec5619b967 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/setup.py b/setup.py index d27452e9766..f25996abf80 100644 --- a/setup.py +++ b/setup.py @@ -68,6 +68,7 @@ 'pyyaml', 'requests', 'six', + 'tabulate', ] if sys.version_info < (3, 4): diff --git a/src/azure/cli/_output.py b/src/azure/cli/_output.py index 23a9d8efa52..3840de5f780 100644 --- a/src/azure/cli/_output.py +++ b/src/azure/cli/_output.py @@ -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 @@ -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 @@ -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] @@ -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 @@ -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): diff --git a/src/azure/cli/tests/test_output.py b/src/azure/cli/tests/test_output.py index 6c35e2e61e5..2ba22996c96 100644 --- a/src/azure/cli/tests/test_output.py +++ b/src/azure/cli/tests/test_output.py @@ -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, @@ -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): @@ -266,7 +230,6 @@ 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 @@ -274,7 +237,6 @@ def test_output_format_ordereddict_not_sorted(self): 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 diff --git a/src/command_modules/azure-cli-storage/azure/cli/command_modules/storage/_validators.py b/src/command_modules/azure-cli-storage/azure/cli/command_modules/storage/_validators.py index ea89d9faa91..9f3cdf09d18 100644 --- a/src/command_modules/azure-cli-storage/azure/cli/command_modules/storage/_validators.py +++ b/src/command_modules/azure-cli-storage/azure/cli/command_modules/storage/_validators.py @@ -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