-
Notifications
You must be signed in to change notification settings - Fork 96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Color] Detect isatty when enabling color #209
Changes from 3 commits
d75391a
69c24dc
ac8e2c2
d1cc6f4
6ac1274
db41a9f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,7 +12,7 @@ | |
from .completion import CLICompletion | ||
from .output import OutputProducer | ||
from .log import CLILogging, get_logger | ||
from .util import CLIError | ||
from .util import CLIError, isatty | ||
from .config import CLIConfig | ||
from .query import CLIQuery | ||
from .events import EVENT_CLI_PRE_EXECUTE, EVENT_CLI_POST_EXECUTE | ||
|
@@ -22,6 +22,10 @@ | |
|
||
logger = get_logger(__name__) | ||
|
||
# Temporarily force color to be enabled even when out_file is not stdout. | ||
# This is only intended for testing purpose. | ||
_KNACK_TEST_FORCE_ENABLE_COLOR = False | ||
|
||
|
||
class CLI(object): # pylint: disable=too-many-instance-attributes | ||
""" The main driver for the CLI """ | ||
|
@@ -91,8 +95,29 @@ def __init__(self, | |
self.output = self.output_cls(cli_ctx=self) | ||
self.result = None | ||
self.query = query_cls(cli_ctx=self) | ||
|
||
# As logging is initialized in `invoke`, call `logger.debug` or `logger.info` here won't work. | ||
self.init_debug_log = [] | ||
self.init_info_log = [] | ||
|
||
self.only_show_errors = self.config.getboolean('core', 'only_show_errors', fallback=False) | ||
self.enable_color = not self.config.getboolean('core', 'no_color', fallback=False) | ||
|
||
# Color is only enabled when all conditions are met: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good comments! |
||
# 1. [core] no_color config is not set | ||
# 2. stdout is a tty | ||
# Otherwise, if the downstream command doesn't support color, Knack will fail with | ||
# BrokenPipeError: [Errno 32] Broken pipe, like `az --version | head --lines=1` | ||
# https://github.com/Azure/azure-cli/issues/13413 | ||
# 3. stderr is a tty | ||
# Otherwise, the output in stderr won't have LEVEL tag | ||
# 4. out_file is stdout | ||
conditions = (not self.config.getboolean('core', 'no_color', fallback=False), | ||
isatty(sys.stdout), isatty(sys.stderr), self.out_file is sys.stdout) | ||
self.enable_color = all(conditions) | ||
# Delay showing the debug message, as logging is not initialized yet | ||
self.init_debug_log.append("enable_color({}) = enable_color_config({}) && " | ||
"stdout.isatty({}) && stderr.isatty({}) && out_file_is_stdout({})" | ||
.format(self.enable_color, *conditions)) | ||
|
||
@staticmethod | ||
def _should_show_version(args): | ||
|
@@ -171,6 +196,15 @@ def exception_handler(self, ex): # pylint: disable=no-self-use | |
logger.exception(ex) | ||
return 1 | ||
|
||
def _print_init_log(self): | ||
"""Print the debug/info log from CLI.__init__""" | ||
if self.init_debug_log: | ||
logger.debug('__init__ debug log:\n%s', '\n'.join(self.init_debug_log)) | ||
self.init_debug_log.clear() | ||
if self.init_info_log: | ||
logger.info('__init__ info log:\n%s', '\n'.join(self.init_info_log)) | ||
self.init_info_log.clear() | ||
|
||
def invoke(self, args, initial_invocation_data=None, out_file=None): | ||
""" Invoke a command. | ||
|
||
|
@@ -189,18 +223,18 @@ 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: | ||
out_file = out_file or self.out_file | ||
if out_file is sys.stdout and self.enable_color or _KNACK_TEST_FORCE_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 | ||
# point out_file to the new sys.stdout which is overwritten by colorama | ||
out_file = sys.stdout | ||
|
||
args = self.completion.get_completion_args() or args | ||
out_file = out_file or self.out_file | ||
|
||
self.logging.configure(args) | ||
logger.debug('Command arguments: %s', args) | ||
self._print_init_log() | ||
|
||
self.raise_event(EVENT_CLI_PRE_EXECUTE) | ||
if CLI._should_show_version(args): | ||
|
@@ -232,7 +266,8 @@ def invoke(self, args, initial_invocation_data=None, out_file=None): | |
finally: | ||
self.raise_event(EVENT_CLI_POST_EXECUTE) | ||
|
||
if self.enable_color: | ||
if self.enable_color or _KNACK_TEST_FORCE_ENABLE_COLOR: | ||
import colorama | ||
colorama.deinit() | ||
|
||
return exit_code |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,6 @@ | |
import errno | ||
import json | ||
import traceback | ||
import sys | ||
from collections import OrderedDict | ||
from six import StringIO, text_type, u, string_types | ||
|
||
|
@@ -162,9 +161,9 @@ def out(self, obj, formatter=None, out_file=None): # pylint: disable=no-self-us | |
|
||
def get_formatter(self, format_type): # pylint: disable=no-self-use | ||
# remove color if stdout is not a tty | ||
if not sys.stdout.isatty() and format_type == 'jsonc': | ||
if not self.cli_ctx.enable_color and format_type == 'jsonc': | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct the logic to use |
||
return OutputProducer._FORMAT_DICT['json'] | ||
if not sys.stdout.isatty() and format_type == 'yamlc': | ||
if not self.cli_ctx.enable_color and format_type == 'yamlc': | ||
return OutputProducer._FORMAT_DICT['yaml'] | ||
return OutputProducer._FORMAT_DICT[format_type] | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -151,3 +151,18 @@ def todict(obj, post_processor=None): # pylint: disable=too-many-return-stateme | |
if not callable(v) and not k.startswith('_')} | ||
return post_processor(obj, result) if post_processor else result | ||
return obj | ||
|
||
|
||
def isatty(stream): | ||
# Code copied from | ||
# https://github.com/tartley/colorama/blob/3d8d48a95de10be25b161c914f274ec6c41d3129/colorama/ansitowin32.py#L43-L53 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can code under BSD-3-Clause be used in MIT project? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Refactored to remove unnecessary code. |
||
import sys | ||
if 'PYCHARM_HOSTED' in os.environ: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PyCharm's integrated terminal is not a tty but it does support color, so detect if the command is run in PyCharm's integrated terminal. If so, enable color. However, this still has some edge cases (Azure/azure-cli#9903, Azure/azure-sdk-for-python#11362) when colorama is used within PyCharm: tartley/colorama#263. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Even though colorama already has this logic and can strip escape sequences There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe you can put comment into code. |
||
if stream is not None and (stream is sys.__stdout__ or stream is sys.__stderr__): | ||
return True | ||
try: | ||
stream_isatty = stream.isatty | ||
except AttributeError: | ||
return False | ||
else: | ||
return stream_isatty() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add lists for debug and info logs which can't be printed with
logger.debug
andlogger.info
yet, until the logger has been initialized.