Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions src/azure-cli-core/azure/cli/core/style.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

"""
Support styled output.
Currently, only color is supported, underline/bold/italic may be supported in the future.
Design spec:
https://devdivdesignguide.azurewebsites.net/command-line-interface/color-guidelines-for-command-line-interface/
For a complete demo, see `src/azure-cli/azure/cli/command_modules/util/custom.py` and run `az demo style`.
"""

import sys
from enum import Enum

from colorama import Fore


class Style(str, Enum):
PRIMARY = "primary"
SECONDARY = "secondary"
IMPORTANT = "important"
ACTION = "action" # name TBD
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chenlomis, what style name should we use for light blue?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi there
Am slightly confused
Did you mean finding an internal short name for the the bright blue?
https://devdivdesignguide.azurewebsites.net/command-line-interface/color-guidelines-for-command-line-interface/#action-colors

HYPERLINK = "hyperlink"
# Message colors
ERROR = "error"
SUCCESS = "success"
WARNING = "warning"


THEME = {
# Style to ANSI escape sequence mapping
# https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
Style.PRIMARY: Fore.LIGHTWHITE_EX,
Style.SECONDARY: Fore.LIGHTBLACK_EX, # may use WHITE, but will lose contrast to LIGHTWHITE_EX
Style.IMPORTANT: Fore.LIGHTMAGENTA_EX,
Style.ACTION: Fore.LIGHTBLUE_EX,
Style.HYPERLINK: Fore.LIGHTCYAN_EX,
# Message colors
Style.ERROR: Fore.LIGHTRED_EX,
Style.SUCCESS: Fore.LIGHTGREEN_EX,
Style.WARNING: Fore.LIGHTYELLOW_EX,
}


def print_styled_text(styled, file=sys.stderr):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May I ask why the default value of the file parameter is sys.stderr instead of sys.stdout?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only the JSON output should go to stdout and the user may pipe it to downstream commands. Per the description of stderr:

https://en.wikipedia.org/wiki/Standard_streams#sys.stderr

This is similar to sys.stdout because it also prints directly to the Console. But the difference is that can be used to print Exceptions and Error messages and also info/debugging comments. (Which is why it is called Standard Error). This can be very useful when the stdout is used to write data instead.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, got it~

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As for az next, it is only used interactively, so the stream doesn't really matter as everything is printed to the screen.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

formatted = format_styled_text(styled)
print(formatted, file=file)


def format_styled_text(styled_text):
# https://python-prompt-toolkit.readthedocs.io/en/stable/pages/printing_text.html#style-text-tuples
formatted_parts = []

for text in styled_text:
# str can also be indexed, bypassing IndexError, so explicitly check if the type is tuple
if not (isinstance(text, tuple) and len(text) == 2):
from azure.cli.core.azclierror import CLIInternalError
raise CLIInternalError("Invalid styled text. It should be a list of 2-element tuples.")

style = text[0]
if style not in THEME:
from azure.cli.core.azclierror import CLIInternalError
raise CLIInternalError("Invalid style. Only use pre-defined style in Style enum.")

formatted_parts.append(THEME[text[0]] + text[1])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

key checking for THEME? Or add some error handling logics here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch. Added checks and tests as requested.


# Reset control sequence
formatted_parts.append(Fore.RESET)
return ''.join(formatted_parts)
48 changes: 48 additions & 0 deletions src/azure-cli-core/azure/cli/core/tests/test_style.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import unittest


class TestStyle(unittest.TestCase):

def test_format_styled_text(self):
from azure.cli.core.style import Style, format_styled_text
styled_text = [
(Style.PRIMARY, "Bright White: Primary text color\n"),
(Style.SECONDARY, "White: Secondary text color\n"),
(Style.IMPORTANT, "Bright Magenta: Important text color\n"),
(Style.ACTION, "Bright Blue: Commands, parameters, and system inputs\n"),
(Style.HYPERLINK, "Bright Cyan: Hyperlink\n"),
(Style.ERROR, "Bright Red: Error message indicator\n"),
(Style.SUCCESS, "Bright Green: Success message indicator\n"),
(Style.WARNING, "Bright Yellow: Warning message indicator\n"),
]
formatted = format_styled_text(styled_text)
excepted = """\x1b[97mBright White: Primary text color
\x1b[90mWhite: Secondary text color
\x1b[95mBright Magenta: Important text color
\x1b[94mBright Blue: Commands, parameters, and system inputs
\x1b[96mBright Cyan: Hyperlink
\x1b[91mBright Red: Error message indicator
\x1b[92mBright Green: Success message indicator
\x1b[93mBright Yellow: Warning message indicator
\x1b[39m"""
self.assertEqual(formatted, excepted)

# Test invalid style
from azure.cli.core.azclierror import CLIInternalError
with self.assertRaisesRegex(CLIInternalError, "Invalid style."):
format_styled_text([("invalid_style", "dummy text",)])

# Test invalid styled style
with self.assertRaisesRegex(CLIInternalError, "Invalid styled text."):
format_styled_text([(Style.PRIMARY,)])
with self.assertRaisesRegex(CLIInternalError, "Invalid styled text."):
format_styled_text(["dummy text"])


if __name__ == '__main__':
unittest.main()
10 changes: 10 additions & 0 deletions src/azure-cli/azure/cli/command_modules/util/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,13 @@
type: command
short-summary: Upgrade Azure CLI and extensions
"""

helps['demo'] = """
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious is this a convention to add a demo command here? The command is convenient for developers but also visible to users right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious is this a convention to add a demo command here?

No. We don't have such convention. The only existing usage of this pattern is az self-test:

g.command('self-test', 'check_cli', deprecate_info=g.deprecate(hide=True))

The command is convenient for developers but also visible to users right?

It is not visible to the user, as the parent command group has been marked as hide=True:

with self.command_group('demo', deprecate_info=g.deprecate(hide=True)) as g:
g.custom_command('style', 'demo_style')

type: group
short-summary: Demos for designing, developing and demonstrating Azure CLI.
"""

helps['demo style'] = """
type: command
short-summary: A demo showing supported text styles.
"""
3 changes: 3 additions & 0 deletions src/azure-cli/azure/cli/command_modules/util/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ def load_command_table(self, _):

with self.command_group('') as g:
g.custom_command('upgrade', 'upgrade_version', is_preview=True)

with self.command_group('demo', deprecate_info=g.deprecate(hide=True)) as g:
g.custom_command('style', 'demo_style')
74 changes: 74 additions & 0 deletions src/azure-cli/azure/cli/command_modules/util/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,77 @@ def upgrade_version(cmd, update_all=None, yes=None): # pylint: disable=too-many
"More details in https://docs.microsoft.com/cli/azure/update-azure-cli#automatic-update"
logger.warning("Upgrade finished.%s", "" if cmd.cli_ctx.config.getboolean('auto-upgrade', 'enable', False)
else auto_upgrade_msg)


def demo_style(cmd): # pylint: disable=unused-argument
from azure.cli.core.style import Style, print_styled_text
print("[available styles]\n")
styled_text = [
(Style.PRIMARY, "Bright White: Primary text color\n"),
(Style.SECONDARY, "White: Secondary text color\n"),
(Style.IMPORTANT, "Bright Magenta: Important text color\n"),
(Style.ACTION, "Bright Blue: Commands, parameters, and system inputs\n"),
(Style.HYPERLINK, "Bright Cyan: Hyperlink\n"),
(Style.ERROR, "Bright Red: Error message indicator\n"),
(Style.SUCCESS, "Bright Green: Success message indicator\n"),
(Style.WARNING, "Bright Yellow: Warning message indicator\n"),
]
print_styled_text(styled_text)

print("[interactive]\n")
# NOTE! Unicode character ⦾ ⦿ will most likely not be displayed correctly
styled_text = [
(Style.ACTION, "?"),
(Style.PRIMARY, " Select a SKU for your app:\n"),
(Style.PRIMARY, "⦾ Free "),
(Style.SECONDARY, "Dev/Test workloads: 1 GB memory, 60 minutes/day compute\n"),
(Style.PRIMARY, "⦾ Basic "),
(Style.SECONDARY, "Dev/Test workloads: 1.75 GB memory, monthly charges apply\n"),
(Style.PRIMARY, "⦾ Standard "),
(Style.SECONDARY, "Production workloads: 1.75 GB memory, monthly charges apply\n"),
(Style.ACTION, "⦿ Premium "),
(Style.SECONDARY, "Production workloads: 3.5 GB memory, monthly charges apply\n"),
]
print_styled_text(styled_text)

print("[progress report]\n")
# NOTE! Unicode character ✓ will most likely not be displayed correctly
styled_text = [
(Style.SUCCESS, '(✓) Done: '),
(Style.PRIMARY, "Creating a resource group for myfancyapp\n"),
(Style.SUCCESS, '(✓) Done: '),
(Style.PRIMARY, "Creating an App Service Plan for myfancyappplan on a "),
(Style.IMPORTANT, "premium instance"),
(Style.PRIMARY, " that has a "),
(Style.IMPORTANT, "monthly charge"),
(Style.PRIMARY, "\n"),
(Style.SUCCESS, '(✓) Done: '),
(Style.PRIMARY, "Creating a webapp named myfancyapp\n"),
]
print_styled_text(styled_text)

print("[error handing]\n")
styled_text = [
(Style.ERROR, "ERROR: Command not found: az storage create\n"),
(Style.PRIMARY, "TRY\n"),
(Style.ACTION, "az storage account create --name"),
(Style.PRIMARY, " mystorageaccount "),
(Style.ACTION, "--resource-group"),
(Style.PRIMARY, " MyResourceGroup\n"),
(Style.SECONDARY, "Create a storage account. For more detail, see "),
(Style.HYPERLINK, "https://docs.microsoft.com/en-us/azure/storage/common/storage-account-create?"
"tabs=azure-cli#create-a-storage-account-1"),
(Style.SECONDARY, "\n"),
]
print_styled_text(styled_text)

print("[post-output hint]\n")
styled_text = [
(Style.PRIMARY, "The default subscription is "),
(Style.IMPORTANT, "AzureSDKTest (0b1f6471-1bf0-4dda-aec3-cb9272f09590)"),
(Style.PRIMARY, ". To switch to another subscription, run "),
(Style.ACTION, "az account set --subscription"),
(Style.PRIMARY, " <subscription ID>\n"),
(Style.WARNING, "WARNING: The subscription has been disabled!")
]
print_styled_text(styled_text)