From dec6d8dab3b75d09cb67113f7669f82caf2c4d4a Mon Sep 17 00:00:00 2001 From: Jiashuo Li Date: Mon, 17 Feb 2020 19:17:33 +0800 Subject: [PATCH 1/6] Support yaml yamlc output --- knack/output.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/knack/output.py b/knack/output.py index e5e07ba..0228f5c 100644 --- a/knack/output.py +++ b/knack/output.py @@ -8,6 +8,7 @@ import errno import json import traceback +import sys from collections import OrderedDict from six import StringIO, text_type, u, string_types @@ -45,6 +46,26 @@ def format_json_color(obj): return highlight(format_json(obj), lexers.JsonLexer(), formatters.TerminalFormatter()) # pylint: disable=no-member +def format_yaml(obj): + from yaml import (safe_dump, representer) + import json + + try: + return safe_dump(obj.result, default_flow_style=False) + except representer.RepresenterError: + # yaml.safe_dump fails when obj.result is an OrderedDict. knack's --query implementation converts the result to an OrderedDict. https://github.com/microsoft/knack/blob/af674bfea793ff42ae31a381a21478bae4b71d7f/knack/query.py#L46. # pylint: disable=line-too-long + return safe_dump(json.loads(json.dumps(obj.result)), default_flow_style=False) + + +def format_yaml_color(obj): + from pygments import highlight, lexers, formatters + return highlight(format_yaml(obj), lexers.YamlLexer(), formatters.TerminalFormatter()) # pylint: disable=no-member + + +def format_none(_): + return "" + + def format_table(obj): result = obj.result try: @@ -78,8 +99,11 @@ class OutputProducer(object): _FORMAT_DICT = { 'json': format_json, 'jsonc': format_json_color, + 'yaml': format_yaml, + 'yamlc': format_yaml_color, 'table': format_table, 'tsv': format_tsv, + 'none': format_none, } @staticmethod @@ -142,6 +166,8 @@ def out(self, obj, formatter=None, out_file=None): # pylint: disable=no-self-us file=out_file, end='') def get_formatter(self, format_type): # pylint: disable=no-self-use + if not sys.stdout.isatty() and format_type in ['jsonc', 'yamlc']: + format_type = format_type[:-1] return OutputProducer._FORMAT_DICT[format_type] From e6858081f1f2f5955ec1339bf75095b7e5a72a56 Mon Sep 17 00:00:00 2001 From: Jiashuo Li Date: Mon, 17 Feb 2020 20:37:50 +0800 Subject: [PATCH 2/6] add tests, yaml output unicode support --- knack/output.py | 4 +-- tests/test_output.py | 60 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/knack/output.py b/knack/output.py index 0228f5c..54f25f5 100644 --- a/knack/output.py +++ b/knack/output.py @@ -51,10 +51,10 @@ def format_yaml(obj): import json try: - return safe_dump(obj.result, default_flow_style=False) + return safe_dump(obj.result, default_flow_style=False, allow_unicode=True) except representer.RepresenterError: # yaml.safe_dump fails when obj.result is an OrderedDict. knack's --query implementation converts the result to an OrderedDict. https://github.com/microsoft/knack/blob/af674bfea793ff42ae31a381a21478bae4b71d7f/knack/query.py#L46. # pylint: disable=line-too-long - return safe_dump(json.loads(json.dumps(obj.result)), default_flow_style=False) + return safe_dump(json.loads(json.dumps(obj.result)), default_flow_style=False, allow_unicode=True) def format_yaml_color(obj): diff --git a/tests/test_output.py b/tests/test_output.py index d2d7a4a..ab046e3 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- # -------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. @@ -12,7 +12,7 @@ from collections import OrderedDict from six import StringIO -from knack.output import OutputProducer, format_json, format_table, format_tsv +from knack.output import OutputProducer, format_json, format_yaml, format_table, format_tsv from knack.util import CommandResultItem, normalize_newlines from tests.util import MockContext @@ -30,6 +30,8 @@ def test_cli_ctx_type_error(self): with self.assertRaises(TypeError): OutputProducer(cli_ctx=object()) + # JSON output tests + def test_out_json_valid(self): """ The JSON output when the input is a dict should be the dict serialized to JSON @@ -89,6 +91,60 @@ def test_out_json_non_ASCII(self): "active": true, "contents": "生活很糟糕" } +""")) + + # YAML output tests + + def test_out_yaml_valid(self): + """ + Test Dict serialized to YAML + """ + output_producer = OutputProducer(cli_ctx=self.mock_ctx) + output_producer.out(CommandResultItem({'active': True, 'id': '0b1f6472'}), + formatter=format_yaml, out_file=self.io) + self.assertEqual(normalize_newlines(self.io.getvalue()), normalize_newlines( + """active: true +id: 0b1f6472 +""")) + + def test_out_yaml_from_ordered_dict(self): + """ + Test OrderedDict serialized to YAML + """ + output_producer = OutputProducer(cli_ctx=self.mock_ctx) + output_producer.out(CommandResultItem(OrderedDict({'active': True, 'id': '0b1f6472'})), + formatter=format_yaml, out_file=self.io) + self.assertEqual(normalize_newlines(self.io.getvalue()), normalize_newlines( + """active: true +id: 0b1f6472 +""")) + + def test_out_yaml_byte(self): + output_producer = OutputProducer(cli_ctx=self.mock_ctx) + output_producer.out(CommandResultItem({'active': True, 'contents': b'0b1f6472'}), + formatter=format_yaml, out_file=self.io) + self.assertEqual(normalize_newlines(self.io.getvalue()), normalize_newlines( + """active: true +contents: !!binary | + MGIxZjY0NzI= +""")) + + def test_out_yaml_byte_empty(self): + output_producer = OutputProducer(cli_ctx=self.mock_ctx) + output_producer.out(CommandResultItem({'active': True, 'contents': b''}), + formatter=format_yaml, out_file=self.io) + self.assertEqual(normalize_newlines(self.io.getvalue()), normalize_newlines( + """active: true +contents: !!binary "" +""")) + + def test_out_yaml_non_ASCII(self): + output_producer = OutputProducer(cli_ctx=self.mock_ctx) + output_producer.out(CommandResultItem({'active': True, 'contents': 'こんにちは'}), + formatter=format_yaml, out_file=self.io) + self.assertEqual(normalize_newlines(self.io.getvalue()), normalize_newlines( + """active: true +contents: こんにちは """)) # TABLE output tests From 2e7f6852b5d370383b4157a1c2a82e036311c55a Mon Sep 17 00:00:00 2001 From: Jiashuo Li Date: Mon, 17 Feb 2020 21:02:14 +0800 Subject: [PATCH 3/6] refactor --- knack/output.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/knack/output.py b/knack/output.py index 54f25f5..fb241ac 100644 --- a/knack/output.py +++ b/knack/output.py @@ -7,6 +7,7 @@ import errno import json +import yaml import traceback import sys from collections import OrderedDict @@ -47,14 +48,11 @@ def format_json_color(obj): def format_yaml(obj): - from yaml import (safe_dump, representer) - import json - try: - return safe_dump(obj.result, default_flow_style=False, allow_unicode=True) - except representer.RepresenterError: + return yaml.safe_dump(obj.result, default_flow_style=False, allow_unicode=True) + except yaml.representer.RepresenterError: # yaml.safe_dump fails when obj.result is an OrderedDict. knack's --query implementation converts the result to an OrderedDict. https://github.com/microsoft/knack/blob/af674bfea793ff42ae31a381a21478bae4b71d7f/knack/query.py#L46. # pylint: disable=line-too-long - return safe_dump(json.loads(json.dumps(obj.result)), default_flow_style=False, allow_unicode=True) + return yaml.safe_dump(json.loads(json.dumps(obj.result)), default_flow_style=False, allow_unicode=True) def format_yaml_color(obj): From 6bc634a0fedf5bef69de5f2b537ba44f2cf8dda3 Mon Sep 17 00:00:00 2001 From: Jiashuo Li Date: Mon, 17 Feb 2020 21:13:25 +0800 Subject: [PATCH 4/6] fix linter --- knack/output.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/knack/output.py b/knack/output.py index fb241ac..f827c20 100644 --- a/knack/output.py +++ b/knack/output.py @@ -7,11 +7,11 @@ import errno import json -import yaml import traceback import sys from collections import OrderedDict from six import StringIO, text_type, u, string_types +import yaml from .util import CLIError, CommandResultItem, CtxTypeError from .events import EVENT_INVOKER_POST_PARSE_ARGS, EVENT_PARSER_GLOBAL_CREATE From 682b5d57a41266b07f097f71921adc4023d2ccc9 Mon Sep 17 00:00:00 2001 From: Jiashuo Li Date: Mon, 17 Feb 2020 21:36:07 +0800 Subject: [PATCH 5/6] fix tests --- tests/test_help.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test_help.py b/tests/test_help.py index e5c09b9..e1ca100 100644 --- a/tests/test_help.py +++ b/tests/test_help.py @@ -295,7 +295,8 @@ def test_help_full_documentations(self): Global Arguments --debug : Increase logging verbosity to show all debug logs. --help -h : Show this help message and exit. - --output -o : Output format. Allowed values: json, jsonc, table, tsv. Default: json. + --output -o : Output format. Allowed values: json, jsonc, none, table, tsv, yaml, + yamlc. Default: json. --query : JMESPath query string. See http://jmespath.org/ for more information and examples. --verbose : Increase logging verbosity. Use --debug for full debug logs. @@ -328,7 +329,8 @@ def test_help_with_param_specified(self): Global Arguments --debug : Increase logging verbosity to show all debug logs. --help -h : Show this help message and exit. - --output -o : Output format. Allowed values: json, jsonc, table, tsv. Default: json. + --output -o : Output format. Allowed values: json, jsonc, none, table, tsv, yaml, yamlc. + Default: json. --query : JMESPath query string. See http://jmespath.org/ for more information and examples. --verbose : Increase logging verbosity. Use --debug for full debug logs. @@ -443,7 +445,8 @@ def register_globals(_, **kwargs): --debug : Increase logging verbosity to show all debug logs. --exampl : This is a new global argument. --help -h : Show this help message and exit. - --output -o : Output format. Allowed values: json, jsonc, table, tsv. Default: json. + --output -o : Output format. Allowed values: json, jsonc, none, table, tsv, yaml, yamlc. + Default: json. --query : JMESPath query string. See http://jmespath.org/ for more information and examples. --verbose : Increase logging verbosity. Use --debug for full debug logs. From 14323afe056ccb0ebe6c7682d65465772dbbab21 Mon Sep 17 00:00:00 2001 From: Jiashuo Li Date: Wed, 26 Feb 2020 12:08:06 +0800 Subject: [PATCH 6/6] improve readability --- knack/output.py | 7 +++++-- tests/test_output.py | 20 +++++++++++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/knack/output.py b/knack/output.py index f827c20..110f1e0 100644 --- a/knack/output.py +++ b/knack/output.py @@ -164,8 +164,11 @@ def out(self, obj, formatter=None, out_file=None): # pylint: disable=no-self-us file=out_file, end='') def get_formatter(self, format_type): # pylint: disable=no-self-use - if not sys.stdout.isatty() and format_type in ['jsonc', 'yamlc']: - format_type = format_type[:-1] + # remove color if stdout is not a tty + if not sys.stdout.isatty() and format_type == 'jsonc': + return OutputProducer._FORMAT_DICT['json'] + if not sys.stdout.isatty() and format_type == 'yamlc': + return OutputProducer._FORMAT_DICT['yaml'] return OutputProducer._FORMAT_DICT[format_type] diff --git a/tests/test_output.py b/tests/test_output.py index ab046e3..50dec07 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -9,10 +9,12 @@ from __future__ import print_function import unittest +import mock from collections import OrderedDict from six import StringIO -from knack.output import OutputProducer, format_json, format_yaml, format_table, format_tsv +from knack.output import OutputProducer, format_json, format_json_color, format_yaml, format_yaml_color, \ + format_table, format_tsv from knack.util import CommandResultItem, normalize_newlines from tests.util import MockContext @@ -275,6 +277,22 @@ def test_output_format_ordereddict_list_not_sorted(self): result = format_tsv(CommandResultItem([obj1, obj2])) self.assertEqual(result, '1\t2\n3\t4\n') + @mock.patch('sys.stdout.isatty', autospec=True) + def test_remove_color_no_tty(self, mock_isatty): + output_producer = OutputProducer(cli_ctx=self.mock_ctx) + + mock_isatty.return_value = False + formatter = output_producer.get_formatter('jsonc') + self.assertEqual(formatter, format_json) + formatter = output_producer.get_formatter('yamlc') + self.assertEqual(formatter, format_yaml) + + mock_isatty.return_value = True + formatter = output_producer.get_formatter('jsonc') + self.assertEqual(formatter, format_json_color) + formatter = output_producer.get_formatter('yamlc') + self.assertEqual(formatter, format_yaml_color) + if __name__ == '__main__': unittest.main()