diff --git a/knack/arguments.py b/knack/arguments.py index aabd038..8a5cc12 100644 --- a/knack/arguments.py +++ b/knack/arguments.py @@ -265,6 +265,7 @@ def _get_preview_arg_message(self): object_type = 'positional argument' preview_info = PreviewItem( + cli_ctx=self.command_loader.cli_ctx, target=target, object_type=object_type, message_func=_get_preview_arg_message diff --git a/knack/cli.py b/knack/cli.py index 7b54112..c2e9ca7 100644 --- a/knack/cli.py +++ b/knack/cli.py @@ -91,6 +91,7 @@ def __init__(self, self.output = self.output_cls(cli_ctx=self) self.result = None self.query = query_cls(cli_ctx=self) + self.enable_color = not self.config.get('core', 'no_color', fallback=False) @staticmethod def _should_show_version(args): @@ -187,6 +188,13 @@ def invoke(self, args, initial_invocation_data=None, out_file=None): raise TypeError('args should be a list or tuple.') exit_code = 0 try: + if self.enable_color: + import colorama + colorama.init() + if self.out_file == sys.__stdout__: + # point out_file to the new sys.stdout which is overwritten by colorama + self.out_file = sys.stdout + args = self.completion.get_completion_args() or args out_file = out_file or self.out_file @@ -218,6 +226,7 @@ def invoke(self, args, initial_invocation_data=None, out_file=None): exit_code = self.exception_handler(ex) self.result = CommandResultItem(None, error=ex) finally: - pass + if self.enable_color: + colorama.deinit() self.result.exit_code = exit_code return exit_code diff --git a/knack/commands.py b/knack/commands.py index 50584a8..ea57be0 100644 --- a/knack/commands.py +++ b/knack/commands.py @@ -302,6 +302,7 @@ def __init__(self, command_loader, group_name, operations_tmpl, **kwargs): kwargs['deprecate_info'].target = group_name if kwargs.get('is_preview', False): kwargs['preview_info'] = PreviewItem( + cli_ctx=self.command_loader.cli_ctx, target=group_name, object_type='command group' ) diff --git a/knack/help.py b/knack/help.py index 80ccd72..71759f4 100644 --- a/knack/help.py +++ b/knack/help.py @@ -684,8 +684,6 @@ def show_welcome(self, parser): self.print_description_list(help_file.children) def show_help(self, cli_name, nouns, parser, is_group): - import colorama - colorama.init(autoreset=True) delimiters = ' '.join(nouns) help_file = self.command_help_cls(self, delimiters, parser) if not is_group \ else self.group_help_cls(self, delimiters, parser) diff --git a/knack/invocation.py b/knack/invocation.py index b217f90..93f7f48 100644 --- a/knack/invocation.py +++ b/knack/invocation.py @@ -127,7 +127,6 @@ def execute(self, args): :return: The command result :rtype: knack.util.CommandResultItem """ - import colorama self.cli_ctx.raise_event(EVENT_INVOKER_PRE_CMD_TBL_CREATE, args=args) cmd_tbl = self.commands_loader.load_command_table(args) @@ -198,12 +197,10 @@ def execute(self, args): preview_kwargs['object_type'] = 'command' previews.append(ImplicitPreviewItem(**preview_kwargs)) - colorama.init() for d in deprecations: print(d.message, file=sys.stderr) for p in previews: print(p.message, file=sys.stderr) - colorama.deinit() cmd_result = parsed_args.func(params) cmd_result = todict(cmd_result) diff --git a/knack/log.py b/knack/log.py index 8f37300..03eada8 100644 --- a/knack/log.py +++ b/knack/log.py @@ -54,24 +54,10 @@ def wrap_msg_with_color(msg): return cls.COLOR_MAP.get(level, None) - def _should_enable_color(self): - try: - # Color if tty stream available - if self.stream.isatty(): - return True - except AttributeError: - pass - return False - - def __init__(self, log_level_config, log_format): - import platform - import colorama - + def __init__(self, log_level_config, log_format, enable_color): logging.StreamHandler.__init__(self) self.setLevel(log_level_config) - if platform.system() == 'Windows': - self.stream = colorama.AnsiToWin32(self.stream).stream - self.enable_color = self._should_enable_color() + self.enable_color = enable_color self.setFormatter(logging.Formatter(log_format[self.enable_color])) def format(self, record): @@ -153,9 +139,11 @@ def _determine_verbose_level(self, args): def _init_console_handlers(self, root_logger, cli_logger, log_level_config): root_logger.addHandler(_CustomStreamHandler(log_level_config['root'], - self.console_log_format['root'])) + self.console_log_format['root'], + self.cli_ctx.enable_color)) cli_logger.addHandler(_CustomStreamHandler(log_level_config[CLI_LOGGER_NAME], - self.console_log_format[CLI_LOGGER_NAME])) + self.console_log_format[CLI_LOGGER_NAME], + self.cli_ctx.enable_color)) def _init_logfile_handlers(self, root_logger, cli_logger): ensure_dir(self.log_dir) diff --git a/knack/output.py b/knack/output.py index bbe3277..4463ef0 100644 --- a/knack/output.py +++ b/knack/output.py @@ -146,11 +146,6 @@ def out(self, obj, formatter=None, out_file=None): # pylint: disable=no-self-us if not isinstance(obj, CommandResultItem): raise TypeError('Expected {} got {}'.format(CommandResultItem.__name__, type(obj))) - import platform - import colorama - - if platform.system() == 'Windows': - out_file = colorama.AnsiToWin32(out_file).stream output = formatter(obj) try: print(output, file=out_file, end='') diff --git a/knack/preview.py b/knack/preview.py index 23f97f7..3aa441a 100644 --- a/knack/preview.py +++ b/knack/preview.py @@ -32,7 +32,7 @@ def _get_command_group(name): # pylint: disable=too-many-instance-attributes class PreviewItem(StatusTag): - def __init__(self, cli_ctx=None, object_type='', target=None, tag_func=None, message_func=None, **kwargs): + def __init__(self, cli_ctx, object_type='', target=None, tag_func=None, message_func=None, **kwargs): """ Create a collection of preview metadata. :param cli_ctx: The CLI context associated with the preview item. diff --git a/knack/util.py b/knack/util.py index 8988dd5..ae78ab1 100644 --- a/knack/util.py +++ b/knack/util.py @@ -9,6 +9,8 @@ from datetime import date, time, datetime, timedelta from enum import Enum +NO_COLOR_VARIABLE_NAME = 'KNACK_NO_COLOR' + class CommandResultItem(object): # pylint: disable=too-few-public-methods def __init__(self, result, table_transformer=None, is_query_active=False, @@ -90,12 +92,13 @@ def show_in_help(self): @property def tag(self): """ Returns a tag object. """ - return ColorizedString(self._get_tag(self), self._color) + return ColorizedString(self._get_tag(self), self._color) if self.cli_ctx.enable_color else self._get_tag(self) @property def message(self): """ Returns a tuple with the formatted message string and the message length. """ - return ColorizedString(self._get_message(self), self._color) + return ColorizedString(self._get_message(self), self._color) if self.cli_ctx.enable_color \ + else "WARNING: " + self._get_message(self) def ensure_dir(d): diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py index 323f956..8611786 100644 --- a/tests/test_deprecation.py +++ b/tests/test_deprecation.py @@ -15,7 +15,7 @@ from knack.arguments import ArgumentsContext from knack.commands import CLICommand, CLICommandsLoader, CommandGroup -from tests.util import DummyCLI, redirect_io +from tests.util import DummyCLI, redirect_io, disable_color def example_handler(arg1, arg2=None, arg3=None): @@ -128,13 +128,32 @@ def test_deprecate_command_expiring_execute(self): expected = "This command has been deprecated and will be removed in version '1.0.0'. Use 'alt-cmd4' instead." self.assertIn(expected, actual) + @redirect_io + def test_deprecate_command_expiring_execute_no_color(self): + """ Ensure warning is displayed without color. """ + self.cli_ctx.enable_color = False + self.cli_ctx.invoke('cmd4 -b b'.split()) + actual = self.io.getvalue() + expected = "WARNING: This command has been deprecated and will be removed in version '1.0.0'" + self.assertIn(expected, actual) + @redirect_io def test_deprecate_command_expired_execute(self): """ Ensure expired command cannot be reached. """ with self.assertRaises(SystemExit): self.cli_ctx.invoke('cmd5 -h'.split()) actual = self.io.getvalue() - expected = """The most similar choices to 'cmd5'""" + expected = """cli: 'cmd5' is not in the 'cli' command group.""" + self.assertIn(expected, actual) + + @redirect_io + @disable_color + def test_deprecate_command_expired_execute_no_color(self): + """ Ensure error is displayed without color. """ + with self.assertRaises(SystemExit): + self.cli_ctx.invoke('cmd5 -h'.split()) + actual = self.io.getvalue() + expected = """ERROR: cli: 'cmd5' is not in the 'cli' command group.""" self.assertIn(expected, actual) @@ -228,6 +247,21 @@ def test_deprecate_command_group_help_expiring(self): """.format(self.cli_ctx.name) self.assertIn(expected, actual) + @redirect_io + @disable_color + def test_deprecate_command_group_help_expiring_no_color(self): + """ Ensure warning is displayed without color. """ + with self.assertRaises(SystemExit): + self.cli_ctx.invoke('group4 -h'.split()) + actual = self.io.getvalue() + expected = """ +Group + cli group4 + WARNING: This command group has been deprecated and will be removed in version \'1.0.0\'. Use + 'alt-group4' instead. +""".format(self.cli_ctx.name) + self.assertIn(expected, actual) + @redirect_io def test_deprecate_command_group_expired(self): """ Ensure expired command cannot be reached. """ @@ -411,6 +445,15 @@ def test_deprecate_options_execute_expiring(self): expected = "Option '--alt4' has been deprecated and will be removed in version '1.0.0'. Use '--opt4' instead." self.assertIn(expected, actual) + @redirect_io + @disable_color + def test_deprecate_options_execute_expiring_no_color(self): + """ Ensure error is displayed without color. """ + self.cli_ctx.invoke('arg-test --arg1 foo --opt1 bar --alt4 bar'.split()) + actual = self.io.getvalue() + expected = "WARNING: Option '--alt4' has been deprecated and will be removed in version '1.0.0'. Use '--opt4' instead." + self.assertIn(expected, actual) + @redirect_io def test_deprecate_options_execute_expiring_non_deprecated(self): """ Ensure non-expiring options can be used without warning. """ diff --git a/tests/test_preview.py b/tests/test_preview.py index f6e7084..1e40034 100644 --- a/tests/test_preview.py +++ b/tests/test_preview.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals, print_function +import os import unittest try: import mock @@ -17,7 +18,7 @@ from knack.arguments import ArgumentsContext from knack.commands import CLICommandsLoader, CommandGroup -from tests.util import DummyCLI, redirect_io +from tests.util import DummyCLI, redirect_io, disable_color def example_handler(arg1, arg2=None, arg3=None): @@ -87,6 +88,31 @@ def test_preview_command_plain_execute(self): expected = "This command is in preview. It may be changed/removed in a future release." self.assertIn(expected, actual) + @redirect_io + @disable_color + def test_preview_command_plain_execute_no_color(self): + """ Ensure warning is displayed without color. """ + self.cli_ctx.invoke('cmd1 -b b'.split()) + actual = self.io.getvalue() + self.assertIn("WARNING: This command is in preview. It may be changed/removed in a future release.", actual) + + @redirect_io + def test_preview_command_implicitly_execute(self): + """ Ensure general warning displayed when running command from a preview parent group. """ + self.cli_ctx.invoke('grp1 cmd1 -b b'.split()) + actual = self.io.getvalue() + expected = "Command group 'grp1' is in preview. It may be changed/removed in a future release." + self.assertIn(expected, actual) + + @redirect_io + @disable_color + def test_preview_command_implicitly_no_color(self): + """ Ensure warning is displayed without color. """ + self.cli_ctx.invoke('grp1 cmd1 -b b'.split()) + actual = self.io.getvalue() + expected = "WARNING: Command group 'grp1' is in preview. It may be changed/removed in a future release." + self.assertIn(expected, actual) + class TestCommandGroupPreview(unittest.TestCase): @@ -129,6 +155,23 @@ def test_preview_command_group_help_plain(self): Commands: cmd1 : Short summary here. +""".format(self.cli_ctx.name) + self.assertEqual(expected, actual) + + @redirect_io + @disable_color + def test_preview_command_group_help_plain_no_color(self): + """ Ensure warning is displayed without color. """ + with self.assertRaises(SystemExit): + self.cli_ctx.invoke('group1 -h'.split()) + actual = self.io.getvalue() + expected = """ +Group + cli group1 : A group. + WARNING: This command group is in preview. It may be changed/removed in a future release. +Commands: + cmd1 : Short summary here. + """.format(self.cli_ctx.name) self.assertEqual(expected, actual) @@ -190,6 +233,20 @@ def test_preview_arguments_command_help(self): """.format(self.cli_ctx.name) self.assertIn(expected, actual) + @redirect_io + @disable_color + def test_preview_arguments_command_help_no_color(self): + """ Ensure warning is displayed without color. """ + with self.assertRaises(SystemExit): + self.cli_ctx.invoke('arg-test -h'.split()) + actual = self.io.getvalue() + expected = """ +Arguments + --arg1 [Preview] [Required] : Arg1. + WARNING: Argument '--arg1' is in preview. It may be changed/removed in a future release. +""".format(self.cli_ctx.name) + self.assertIn(expected, actual) + @redirect_io def test_preview_arguments_execute(self): """ Ensure deprecated arguments can be used. """ @@ -201,6 +258,18 @@ def test_preview_arguments_execute(self): action_expected = "Side-effect from some original action!" self.assertIn(action_expected, actual) + @redirect_io + @disable_color + def test_preview_arguments_execute_no_color(self): + """ Ensure warning is displayed without color. """ + self.cli_ctx.invoke('arg-test --arg1 foo --opt1 bar'.split()) + actual = self.io.getvalue() + preview_expected = "WARNING: Argument '--arg1' is in preview. It may be changed/removed in a future release." + self.assertIn(preview_expected, actual) + + action_expected = "Side-effect from some original action!" + self.assertIn(action_expected, actual) + if __name__ == '__main__': unittest.main() diff --git a/tests/util.py b/tests/util.py index 57f8c7c..30f7675 100644 --- a/tests/util.py +++ b/tests/util.py @@ -12,6 +12,8 @@ import shutil import os from six import StringIO +import logging +from knack.log import CLI_LOGGER_NAME from knack.cli import CLI, CLICommandsLoader, CommandInvoker @@ -29,6 +31,21 @@ def wrapper(self): self.io.close() sys.stdout = original_stderr sys.stderr = original_stderr + + # Remove the handlers added by CLI, so that the next invoke call init them again with the new stderr + # Otherwise, the handlers will write to a closed StringIO from a preview test + root_logger = logging.getLogger() + cli_logger = logging.getLogger(CLI_LOGGER_NAME) + root_logger.handlers = root_logger.handlers[:-1] + cli_logger.handlers = cli_logger.handlers[:-1] + return wrapper + + +def disable_color(func): + def wrapper(self): + self.cli_ctx.enable_color = False + func(self) + self.cli_ctx.enable_color = True return wrapper