From f6c024a9ccdca2131ee574a5ac3e7ade61bd0963 Mon Sep 17 00:00:00 2001 From: Derek Bekoe Date: Wed, 24 Aug 2016 09:33:15 -0700 Subject: [PATCH 1/2] 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. --- requirements.txt | 1 + setup.py | 1 + src/azure/cli/_output.py | 116 ++++++++++------------------- src/azure/cli/application.py | 2 - src/azure/cli/tests/test_output.py | 62 +++------------ 5 files changed, 51 insertions(+), 131 deletions(-) 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..07c62d41638 100644 --- a/src/azure/cli/_output.py +++ b/src/azure/cli/_output.py @@ -9,12 +9,11 @@ import platform import json import re -import traceback 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 logger = _logging.get_az_logger(__name__) @@ -40,31 +39,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 +52,11 @@ def format_text(obj): except TypeError: return '' +def format_table(obj): + result = obj.result + result_list = result if isinstance(result, list) else [result] + return TableOutput.dump(result_list) + def format_list(obj): result = obj.result result_list = result if isinstance(result, list) else [result] @@ -91,9 +70,8 @@ def format_tsv(obj): class CommandResultItem(object): #pylint: disable=too-few-public-methods - def __init__(self, result, simple_output_query=None, is_query_active=False): + def __init__(self, result, is_query_active=False): self.result = result - self.simple_output_query = simple_output_query self.is_query_active = is_query_active class OutputProducer(object): #pylint: disable=too-few-public-methods @@ -125,6 +103,39 @@ 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 sorted(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) + return tabulate(table_data, headers="keys", tablefmt="simple") + '\n' if table_data else '' + class ListOutput(object): #pylint: disable=too-few-public-methods # Match the capital letters in a camel case string @@ -196,53 +207,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/application.py b/src/azure/cli/application.py index b28727d5a13..ff8e8281650 100644 --- a/src/azure/cli/application.py +++ b/src/azure/cli/application.py @@ -126,8 +126,6 @@ def execute(self, argv): self.raise_event(self.TRANSFORM_RESULT, event_data=event_data) self.raise_event(self.FILTER_RESULT, event_data=event_data) return CommandResultItem(event_data['result'], - simple_output_query= - command_table[args.command].simple_output_query, is_query_active=self.session['query_active']) def raise_event(self, name, **kwargs): diff --git a/src/azure/cli/tests/test_output.py b/src/azure/cli/tests/test_output.py index 6c35e2e61e5..2b2477f71ba 100644 --- a/src/azure/cli/tests/test_output.py +++ b/src/azure/cli/tests/test_output.py @@ -69,69 +69,25 @@ 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) + output_producer.out(CommandResultItem({'active': True, 'val': '0b1f6472'})) 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}') + result_item = CommandResultItem([{'name': 'qwerty', 'val': '0b1f6472qwerty', 'sub': {'1'}}]) 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): From de028217245d3ca5b8253b06ccb536004274ff57 Mon Sep 17 00:00:00 2001 From: Derek Bekoe Date: Thu, 25 Aug 2016 14:55:18 -0700 Subject: [PATCH 2/2] 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. --- src/azure/cli/_output.py | 25 +++++++++++++++---- src/azure/cli/application.py | 2 ++ src/azure/cli/tests/test_output.py | 14 ++++++++--- .../command_modules/storage/_validators.py | 2 +- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/azure/cli/_output.py b/src/azure/cli/_output.py index 07c62d41638..3840de5f780 100644 --- a/src/azure/cli/_output.py +++ b/src/azure/cli/_output.py @@ -9,11 +9,13 @@ import platform import json import re +import traceback 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 logger = _logging.get_az_logger(__name__) @@ -54,8 +56,17 @@ def format_text(obj): def format_table(obj): result = obj.result - result_list = result if isinstance(result, list) else [result] - return TableOutput.dump(result_list) + 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 @@ -70,8 +81,9 @@ def format_tsv(obj): class CommandResultItem(object): #pylint: disable=too-few-public-methods - def __init__(self, result, is_query_active=False): + def __init__(self, result, simple_output_query=None, is_query_active=False): self.result = result + self.simple_output_query = simple_output_query self.is_query_active = is_query_active class OutputProducer(object): #pylint: disable=too-few-public-methods @@ -114,7 +126,7 @@ def _capitalize_first_char(x): @staticmethod def _auto_table_item(item): new_entry = OrderedDict() - for k in sorted(item.keys()): + for k in item.keys(): if k in TableOutput.SKIP_KEYS: continue if item[k] and not isinstance(item[k], (list, dict, set)): @@ -134,7 +146,10 @@ def _auto_table(result): @staticmethod def dump(data): table_data = TableOutput._auto_table(data) - return tabulate(table_data, headers="keys", tablefmt="simple") + '\n' if table_data else '' + 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 diff --git a/src/azure/cli/application.py b/src/azure/cli/application.py index ff8e8281650..b28727d5a13 100644 --- a/src/azure/cli/application.py +++ b/src/azure/cli/application.py @@ -126,6 +126,8 @@ def execute(self, argv): self.raise_event(self.TRANSFORM_RESULT, event_data=event_data) self.raise_event(self.FILTER_RESULT, event_data=event_data) return CommandResultItem(event_data['result'], + simple_output_query= + command_table[args.command].simple_output_query, is_query_active=self.session['query_active']) def raise_event(self, name, **kwargs): diff --git a/src/azure/cli/tests/test_output.py b/src/azure/cli/tests/test_output.py index 2b2477f71ba..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, @@ -71,7 +72,10 @@ def test_out_boolean_valid(self): def test_out_table(self): output_producer = OutputProducer(formatter=format_table, file=self.io) - output_producer.out(CommandResultItem({'active': True, 'val': '0b1f6472'})) + obj = OrderedDict() + obj['active'] = True + obj['val'] = '0b1f6472' + output_producer.out(CommandResultItem(obj)) self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( """ Active Val -------- -------- @@ -80,7 +84,11 @@ def test_out_table(self): def test_out_table_complex_obj(self): output_producer = OutputProducer(formatter=format_table, file=self.io) - result_item = CommandResultItem([{'name': 'qwerty', 'val': '0b1f6472qwerty', 'sub': {'1'}}]) + 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 Val @@ -222,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 @@ -230,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