diff --git a/doc/sphinx/azhelpgen/azhelpgen.py b/doc/sphinx/azhelpgen/azhelpgen.py index 428a798b057..3fed0dbc2cb 100644 --- a/doc/sphinx/azhelpgen/azhelpgen.py +++ b/doc/sphinx/azhelpgen/azhelpgen.py @@ -13,11 +13,10 @@ from knack.help_files import helps -from knack.help import GroupHelpFile from azure.cli.core import MainCommandsLoader, AzCli from azure.cli.core.commands import AzCliCommandInvoker from azure.cli.core.parser import AzCliCommandParser -from azure.cli.core._help import AzCliHelp, CliCommandHelpFile, ArgumentGroupRegistry +from azure.cli.core._help import AzCliHelp, CliCommandHelpFile, ArgumentGroupRegistry, CliGroupHelpFile USER_HOME = expanduser('~') @@ -44,7 +43,7 @@ def get_help_files(cli_ctx): help_files = [] for cmd, parser in zip(sub_parser_keys, sub_parser_values): try: - help_file = GroupHelpFile(help_ctx, cmd, parser) if _is_group(parser) else CliCommandHelpFile(help_ctx, cmd, parser) + help_file = CliGroupHelpFile(help_ctx, cmd, parser) if _is_group(parser) else CliCommandHelpFile(help_ctx, cmd, parser) help_file.load(parser) help_files.append(help_file) except Exception as ex: diff --git a/src/azure-cli-core/azure/cli/core/_help.py b/src/azure-cli-core/azure/cli/core/_help.py index b701a4ca8f4..869dec3757e 100644 --- a/src/azure-cli-core/azure/cli/core/_help.py +++ b/src/azure-cli-core/azure/cli/core/_help.py @@ -5,11 +5,11 @@ from __future__ import print_function -from knack.help import (HelpExample, - HelpFile as KnackHelpFile, - CommandHelpFile as KnackCommandHelpFile, - CLIHelp, - ArgumentGroupRegistry as KnackArgumentGroupRegistry) +from knack.help import (HelpFile as KnackHelpFile, CommandHelpFile as KnackCommandHelpFile, + GroupHelpFile as KnackGroupHelpFile, ArgumentGroupRegistry as KnackArgumentGroupRegistry, + HelpExample as KnackHelpExample, HelpParameter as KnackHelpParameter, + _print_indent, CLIHelp) + from knack.log import get_logger from azure.cli.core.commands import ExtensionCommandSource @@ -52,6 +52,7 @@ def __init__(self, cli_ctx): privacy_statement=PRIVACY_STATEMENT, welcome_message=WELCOME_MESSAGE, command_help_cls=CliCommandHelpFile, + group_help_cls=CliGroupHelpFile, help_cls=CliHelpFile) from knack.help import HelpObject @@ -70,6 +71,9 @@ def new_normalize_text(s): HelpObject._normalize_text = new_normalize_text # pylint: disable=protected-access + self._register_help_loaders() + + # print methods @staticmethod def _print_extensions_msg(help_file): if help_file.type != 'command': @@ -83,9 +87,39 @@ def _print_detailed_help(self, cli_name, help_file): AzCliHelp._print_extensions_msg(help_file) super(AzCliHelp, self)._print_detailed_help(cli_name, help_file) + def _print_header(self, cli_name, help_file): + super(AzCliHelp, self)._print_header(cli_name, help_file) + + links = help_file.links + if links: + link_text = "{} and {}".format(", ".join(links[0:-1]), links[-1]) if len(links) > 1 else links[0] + link_text = "For more information, see: {}\n".format(link_text) + _print_indent(link_text, 2, width=self.textwrap_width) + + + def _register_help_loaders(self): + import azure.cli.core._help_loaders as help_loaders + import inspect + + def is_loader_cls(cls): + return inspect.isclass(cls) and cls.__name__ != 'BaseHelpLoader'and issubclass(cls, help_loaders.BaseHelpLoader) # pylint: disable=line-too-long + + versioned_loaders = {} + for cls_name, loader_cls in inspect.getmembers(help_loaders, is_loader_cls): + loader = loader_cls(self) + versioned_loaders[cls_name] = loader + + self.versioned_loaders = versioned_loaders + class CliHelpFile(KnackHelpFile): + def __init__(self, help_ctx, delimiters): + # Each help file (for a command or group) has a version denoting the source of its data. + self.yaml_help_version = 0 + super(CliHelpFile, self).__init__(help_ctx, delimiters) + self.links = [] + def _should_include_example(self, ex): min_profile = ex.get('min_profile') max_profile = ex.get('max_profile') @@ -98,22 +132,92 @@ def _should_include_example(self, ex): min_api=min_profile, max_api=max_profile) return True - # Needs to override base implementation + # Needs to override base implementation to exclude unsupported examples. def _load_from_data(self, data): - super(CliHelpFile, self)._load_from_data(data) - self.examples = [] # clear examples set by knack + if not data: + return + + if isinstance(data, str): + self.long_summary = data + return + + if 'type' in data: + self.type = data['type'] + + if 'short-summary' in data: + self.short_summary = data['short-summary'] + + self.long_summary = data.get('long-summary') + if 'examples' in data: self.examples = [] for d in data['examples']: if self._should_include_example(d): - self.examples.append(HelpExample(d)) + self.examples.append(HelpExample(**d)) + + def load(self, options): + ordered_loaders = sorted(self.help_ctx.versioned_loaders.values(), key=lambda ldr: ldr.VERSION) + for loader in ordered_loaders: + loader.versioned_load(self, options) + + +class CliGroupHelpFile(KnackGroupHelpFile, CliHelpFile): + def __init__(self, help_ctx, delimiters, parser): + super(CliGroupHelpFile, self).__init__(help_ctx, delimiters, parser) + + def load(self, options): + # forces class to use this load method even if KnackGroupHelpFile overrides CliHelpFile's method. + CliHelpFile.load(self, options) class CliCommandHelpFile(KnackCommandHelpFile, CliHelpFile): def __init__(self, help_ctx, delimiters, parser): - self.command_source = getattr(parser, 'command_source', None) super(CliCommandHelpFile, self).__init__(help_ctx, delimiters, parser) + import argparse + self.type = 'command' + self.command_source = getattr(parser, 'command_source', None) + + self.parameters = [] + + for action in [a for a in parser._actions if a.help != argparse.SUPPRESS]: # pylint: disable=protected-access + if action.option_strings: + self._add_parameter_help(action) + else: + # use metavar for positional parameters + param_kwargs = { + 'name_source': [action.metavar or action.dest], + 'deprecate_info': getattr(action, 'deprecate_info', None), + 'description': action.help, + 'choices': action.choices, + 'required': False, + 'default': None, + 'group_name': 'Positional' + } + self.parameters.append(HelpParameter(**param_kwargs)) + + help_param = next(p for p in self.parameters if p.name == '--help -h') + help_param.group_name = 'Global Arguments' + + def _load_from_data(self, data): + super(CliCommandHelpFile, self)._load_from_data(data) + + if isinstance(data, str) or not self.parameters or not data.get('parameters'): + return + + loaded_params = [] + loaded_param = {} + for param in self.parameters: + loaded_param = next((n for n in data['parameters'] if n['name'] == param.name), None) + if loaded_param: + param.update_from_data(loaded_param) + loaded_params.append(param) + + self.parameters = loaded_params + + def load(self, options): + # forces class to use this load method even if KnackCommandHelpFile overrides CliHelpFile's method. + CliHelpFile.load(self, options) class ArgumentGroupRegistry(KnackArgumentGroupRegistry): # pylint: disable=too-few-public-methods @@ -133,3 +237,26 @@ def __init__(self, group_list): for group in other_groups: self.priorities[group] = priority priority += 1 + + +class HelpExample(KnackHelpExample): # pylint: disable=too-few-public-methods + + def __init__(self, **_data): + _data['name'] = _data.get('name', '') + _data['text'] = _data.get('text', '') + super(HelpExample, self).__init__(_data) + + self.min_profile = _data.get('min_profile', '') + self.max_profile = _data.get('max_profile', '') + + self.summary = _data.get('summary', '') + self.command = _data.get('command', '') + self.description = _data.get('description', '') + + +class HelpParameter(KnackHelpParameter): # pylint: disable=too-many-instance-attributes + + def __init__(self, **kwargs): + super(HelpParameter, self).__init__(**kwargs) + # new field + self.raw_value_sources = [] \ No newline at end of file diff --git a/src/azure-cli-core/azure/cli/core/_help_loaders.py b/src/azure-cli-core/azure/cli/core/_help_loaders.py new file mode 100644 index 00000000000..b224d72a9ea --- /dev/null +++ b/src/azure-cli-core/azure/cli/core/_help_loaders.py @@ -0,0 +1,183 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from knack.util import CLIError +from knack.help import HelpParameter as KnackHelpParameter +from azure.cli.core._help import (HelpExample, HelpParameter, CliHelpFile) +from knack.help import HelpAuthoringException + + +# BaseHelpLoader defining versioned loader interface. Also contains some helper methods. +class BaseHelpLoader(object): + def __init__(self, help_ctx=None): + self.help_ctx = help_ctx + + def versioned_load(self, help_obj, parser): + raise NotImplementedError + + @classmethod + def get_version(cls): + try: + cls.VERSION + except AttributeError: + raise NotImplementedError + + @staticmethod + def _get_yaml_help_for_nouns(nouns, cmd_loader_map_ref): + import inspect + import os + + def _parse_yaml_from_string(text, help_file_path): + import yaml + + dir_name, base_name = os.path.split(help_file_path) + + pretty_file_path = os.path.join(os.path.basename(dir_name), base_name) + + try: + data = yaml.load(text) + if not data: + raise CLIError("Error: Help file {} is empty".format(pretty_file_path)) + return data + except yaml.YAMLError as e: + raise CLIError("Error parsing {}:\n\n{}".format(pretty_file_path, e)) + + command_nouns = " ".join(nouns) + # if command in map, get the loader. Path of loader is path of helpfile. + loader = cmd_loader_map_ref.get(command_nouns, [None])[0] + + # otherwise likely a group, get the loader + if not loader: + for k, v in cmd_loader_map_ref.items(): + # if loader name starts with noun / group, this is a command in the command group + if k.startswith(command_nouns): + loader = v[0] + break + + if loader: + loader_file_path = inspect.getfile(loader.__class__) + dir_name = os.path.dirname(loader_file_path) + files = os.listdir(dir_name) + for file in files: + if file.endswith(".yaml") or file.endswith(".yml"): + help_file_path = os.path.join(dir_name, file) + with open(help_file_path, "r") as f: + text = f.read() + return _parse_yaml_from_string(text, help_file_path) + return None + + +class HelpLoaderV0(BaseHelpLoader): + VERSION = 0 + + def versioned_load(self, help_obj, parser): + super(CliHelpFile, help_obj).load(parser) + + +class HelpLoaderV1(BaseHelpLoader): + VERSION = 1 + + # load help_obj with data if applicable + def versioned_load(self, help_obj, parser): + prog = parser.prog if hasattr(parser, "prog") else parser._prog_prefix + command_nouns = prog.split()[1:] + cmd_loader_map_ref = self.help_ctx.cli_ctx.invocation.commands_loader.cmd_to_loader_map + + data = self._get_yaml_help_for_nouns(command_nouns, cmd_loader_map_ref) + + # proceed only if data applies to this help loader + if not (data and data.get("version", None) == self.VERSION): + return + + content = data.get("content") + info_type = None + info = None + for elem in content: + for key, value in elem.items(): + # find the command / group's help text + if value.get("name") == help_obj.command: + info_type = key + info = value + break + + # found the right entry in content, update help_obj + if info: + help_obj.type = info_type + if "summary" in info: + help_obj.short_summary = info["summary"] + if "description" in info: + help_obj.long_summary = info["description"] + if "links" in info: + help_obj.links = info["links"] + if help_obj.type == "command": + self._load_command_data(help_obj, info) + return + + @classmethod + def _load_command_data(cls, help_obj, info): + if "examples" in info: + help_obj.examples = [] + for ex in info["examples"]: + if help_obj._should_include_example(ex): + help_obj.examples.append(cls._get_example_from_data(ex)) + + if "arguments" in info and hasattr(help_obj, "parameters"): + def _name_is_equal(data, param): + if data.get('name', None) == param.name: + return True + for name in param.name_source: + if data.get("name") == name.lstrip("-"): + return True + return False + + loaded_params = [] + for param in help_obj.parameters: + loaded_param = next((n for n in info['arguments'] if _name_is_equal(n, param)), None) + if loaded_param and isinstance(param, KnackHelpParameter): + loaded_param["name"] = param.name + param.__class__ = HelpParameter # cast param to CliHelpParameter + cls._update_param_from_data(param, loaded_param) + loaded_params.append(param) + + help_obj.parameters = loaded_params + + @staticmethod + def _update_param_from_data(ex, data): + + def _raw_value_source_to_string(value_source): + if "string" in value_source: + return value_source["string"] + elif "link" in value_source: + link_text = "" + if "url" in value_source["link"]: + link_text = "{}".format(value_source["link"]["url"]) + if "command" in value_source["link"]: + link_text = "{}".format(value_source["link"]["command"]) + return link_text + return "" + + if ex.name != data.get('name'): + raise HelpAuthoringException(u"mismatched name {} vs. {}".format(ex.name, data.get('name'))) + + if data.get('summary'): + ex.short_summary = data.get('summary') + + if data.get('description'): + ex.long_summary = data.get('description') + + if data.get('value-sources'): + ex.value_sources = [] + ex.raw_value_sources = data.get('value-sources') + for value_source in ex.raw_value_sources: + val_str = _raw_value_source_to_string(value_source) + if val_str: + ex.value_sources.append(val_str) + + @staticmethod + def _get_example_from_data(_data): + summary, command, description = _data.get('summary', ''), _data.get('command', ''), _data.get('description', '') + _data['name'] = summary + _data['text'] = "{}\n{}".format(description, command) if description else command + return HelpExample(**_data) diff --git a/src/azure-cli-core/azure/cli/core/file_util.py b/src/azure-cli-core/azure/cli/core/file_util.py index d789a0bb494..de0c315a092 100644 --- a/src/azure-cli-core/azure/cli/core/file_util.py +++ b/src/azure-cli-core/azure/cli/core/file_util.py @@ -5,9 +5,8 @@ from __future__ import print_function from knack.util import CLIError -from knack.help import GroupHelpFile -from azure.cli.core._help import CliCommandHelpFile +from azure.cli.core._help import CliCommandHelpFile, CliGroupHelpFile def get_all_help(cli_ctx): @@ -28,7 +27,7 @@ def get_all_help(cli_ctx): help_files = [] for cmd, parser in zip(sub_parser_keys, sub_parser_values): try: - help_file = GroupHelpFile(help_ctx, cmd, parser) if _is_group(parser) \ + help_file = CliGroupHelpFile(help_ctx, cmd, parser) if _is_group(parser) \ else CliCommandHelpFile(help_ctx, cmd, parser) help_file.load(parser) help_files.append(help_file) diff --git a/temp_help/help.yaml b/temp_help/help.yaml new file mode 100644 index 00000000000..286f387f2fa --- /dev/null +++ b/temp_help/help.yaml @@ -0,0 +1,91 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +# Test helpfile! + +version: 1 + +content: + +- group: + name: cdn + summary: Super Manage Azure Content Delivery Networks (CDNs). + description: > + Dummy long description. This is a long description illustrating the description field in a group type. + A description is essentially a long summary. One would expect that it is multiple sentences long. + links: + - title: Azure cdn docs + url: "https://docs.microsoft.com/en-us/azure/cdn/" + - title: cli cdn reference + url: "https://docs.microsoft.com/en-us/cli/azure/cdn?view=azure-cli-latest" + - url: "https://aka.ms/just-a-url" + +- group: + name: cdn profile + summary: Super Manage CDN profiles to define an edge network. Bla bla + description: Who is the manager? You are!!!! You manage so hard, the latency on your cdn is very low. + + examples: + - summary: "New and improved summary: Create a CDN profile using Verizon premium CDN." + description: > + Much longer description that goes into more depth about the example. + This description spans multiple lines for example. + command: > # commands? for multiple step. parser should not enforce rules. + az cdn profile create -g group -n profile --sku Premium_Verizon + +- command: + name: cdn profile create + summary: NEW! Create a new CDN profile. You ma'am or sir are the creator. + description: NEW! Dummy super long description. As you would expect it is long and has multiple sentences. + links: + - title: NEW! create new cdn profile and endpoint + url: "https://docs.microsoft.com/en-us/azure/cdn/cdn-create-new-endpoint" + arguments: + - name: name + summary: NEW! Cool Name of the CDN profile. + + - name: sku + summary: > + The pricing tier (defines a CDN provider, feature list and rate) of the CDN profile. + Defaults to Standard_Akamai. + description: > + Much longer description going into the many intricacies about the different skus. + Here is some more unnecessary text explaining more and more about skus. + There are different CDN skus, you know? Standard ones and even a premium one too. Et cetera. + value-sources: # UPDATED + - string: + "Number range: -5.0 to 5.0" + - link: + url: https://www.foo.com + text: foo + - link: + command: az sku list + text: Get skus + +- command: + name: cdn endpoint create + summary: NEW AND IMPROVED!!!!! Create a named endpoint to connect to a CDN. + arguments: + - name: name + summary: Cool Name of the CDN endpoint. + - name: profile-name + summary: Unique name of cdn profile. Name of the CDN profile which is unique within the resource group. + + examples: + - summary: CREATE the coolest endpoint to service content for hostname over HTTP or HTTPS. + description: a very very long description of the example. + command: > + az cdn endpoint create -g group -n endpoint --profile-name profile \\ + --origin www.example.com + min_profile: latest + - summary: More creation. Create an endpoint with a custom domain origin with HTTP and HTTPS ports. + command: > + az cdn endpoint create -g group -n endpoint --profile-name profile \\ + --origin www.example.com 88 4444 + min_profile: 2017-03-09-profile + - summary: Even more creation. Create an endpoint with a custom domain with compression and only HTTPS. + command: > + az cdn endpoint create -g group -n endpoint --profile-name profile \\ + --origin www.example.com --no-http --enable-compression diff --git a/temp_help/help_convert.py b/temp_help/help_convert.py new file mode 100644 index 00000000000..0e3791f0f7f --- /dev/null +++ b/temp_help/help_convert.py @@ -0,0 +1,255 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import sys +from importlib import import_module +import os +import subprocess +import difflib +from pprint import pprint + +from azure.cli.core import get_default_cli +from azure.cli.core.file_util import get_all_help, create_invoker_and_load_cmds_and_args + +try: + from ruamel.yaml import YAML + yaml = YAML() +except ImportError as e: + msg = "{}\npip install ruamel.Yaml to use this script.".format(e) + exit(msg) + +PACKAGE_PREFIX = "azure.cli.command_modules" + +failed = 0 + + +# this must be called before loading any command modules. Otherwise helps object will have every help.py file's contents +def convert(target_mod, mod_name, test=False): + help_dict = target_mod.helps + loader_file_path = os.path.abspath(target_mod.__file__) + + out_file = os.path.join(os.path.dirname(loader_file_path), "help.yaml") + if test and os.path.exists(out_file): + print("{}/help.yaml already exists.\nPlease remove: {}\nand re-run this command.\n".format(mod_name, out_file)) + exit(1) + + result = _get_new_yaml_dict(help_dict) + + return out_file, result + + +def _get_new_yaml_dict(help_dict): + + result = dict(version=1, content=[]) + content = result['content'] + + for command_or_group, yaml_text in help_dict.items(): + help_dict = yaml.load(yaml_text) + + type = help_dict["type"] + + elem = {type: dict(name=command_or_group)} + elem_content = elem[type] + + _convert_summaries(old_dict=help_dict, new_dict=elem_content) + + if "parameters" in help_dict: + parameters = [] + for param in help_dict["parameters"]: + new_param = dict() + if "name" in param: + options = param["name"].split() + option = max(options, key=lambda x: len(x)) + new_param["name"] = option.lstrip('-') + _convert_summaries(old_dict=param, new_dict=new_param) + + if "populator-commands" in param: + new_param["value-sources"] = [] + for item in param["populator-commands"]: + new_param["value-sources"].append(dict(string=item)) + parameters.append(new_param) + elem_content["arguments"] = parameters + + if "examples" in help_dict: + elem_examples = [] + for ex in help_dict["examples"]: + new_ex = dict() + if "name" in ex: + new_ex["summary"] = ex["name"] + if "text" in ex: + new_ex["command"] = ex["text"] + if "min_profile" in ex: + new_ex["min_profile"] = ex["min_profile"] + if "max_profile" in ex: + new_ex["max_profile"] = ex["max_profile"] + elem_examples.append(new_ex) + elem_content["examples"] = elem_examples + + content.append(elem) + + return result + + +def _convert_summaries(old_dict, new_dict): + if "short-summary" in old_dict: + new_dict["summary"] = old_dict["short-summary"] + if "long-summary" in old_dict: + new_dict["description"] = old_dict["long-summary"] + +def assert_help_objs_equal(old_help, new_help): + assert_true_or_warn(old_help.name, new_help.name) + assert_true_or_warn(old_help.type, new_help.type) + assert_true_or_warn(old_help.short_summary, new_help.short_summary) + assert_true_or_warn(old_help.long_summary, new_help.long_summary) + assert_true_or_warn(old_help.command, new_help.command) + + old_examples = sorted(old_help.examples, key=lambda x: x.name) + new_examples = sorted(new_help.examples, key=lambda x: x.name) + assert_true_or_warn(len(old_examples), len(new_examples)) + # note: this cannot test if min / max version were added as these fields weren't stored in helpfile objects previously. + for old_ex, new_ex in zip(old_examples, new_examples): + assert_true_or_warn (old_ex.text, new_ex.text) + + assert_true_or_warn(old_help.deprecate_info, new_help.deprecate_info) + assert_true_or_warn(old_help.preview_info, new_help.preview_info) + + # group and not command, we are done checking. + if old_help.type == "group": + return + + old_parameters = sorted(old_help.parameters, key=lambda x: x.name_source) + new_parameters = sorted(new_help.parameters, key=lambda x: x.name_source) + assert_true_or_warn(len(old_parameters), len(new_parameters)) + assert_params_equal(old_parameters, new_parameters) + + +def assert_params_equal(old_parameters, new_parameters): + for old, new in zip(old_parameters, new_parameters): + assert_true_or_warn (old.short_summary, new.short_summary) + assert_true_or_warn (old.long_summary, new.long_summary) + + old_value_sources = sorted(old.value_sources) + new_value_sources = sorted(new.value_sources) + assert_true_or_warn (old_value_sources, new_value_sources) + + +def assert_true_or_warn(x, y): + try: + if x != y: + if isinstance(x, str): + matcher = difflib.SequenceMatcher(a=x, b=y) + print("Ratio: {}".format(matcher.ratio())) + d = difflib.Differ() + result = list(d.compare(x.splitlines(keepends=True), y.splitlines(keepends=True))) + + help_link = "https://docs.python.org/3.7/library/difflib.html#difflib.Differ" + print("Showing diff... (See {} for more info).".format(help_link)) + pprint(result) + + if matcher.ratio() > 0.9: + print("These two values have a similarity ratio of {}/1.0. " + "Test will count them as similar. Please review.".format(matcher.ratio())) + else: + assert x == y + else: + assert x == y + + except AssertionError: + # if is list try to find exactly where there is failure + if isinstance(x, list) and len(x) == len(y): + for x_1, y_1 in zip(x, y): + assert_true_or_warn(x_1, y_1) + else: + print("\nvalues:\n\n{}\n\nand\n\n{}\n\nare not equal.\n".format(x, y)) + + global failed + failed+=1 + + if failed > 15: + print("More than 15 assertions failed. Exiting tests.\n") + exit(1) + + +if __name__ == "__main__": + if sys.version_info[0] < 3: + raise Exception("This script requires Python 3") + + args = sys.argv[1:] + test = False + + if "--help" in args or "-h" in args: + print('Usage: python help_convert.py (MOD | MOD "TEST")\n') + exit(0) + + if len(args) > 2: + msg = 'Usage: python help_convert.py (MOD | MOD "TEST")\n' + exit(msg) + + if len(args) == 2: + if args[1].lower() != "test": + msg = 'Usage: python help_convert.py (MOD | MOD "TEST")\n' + exit(msg) + else: + test = True + + # attempt to find and load the desired module. + MOD_NAME = "{}.{}._help".format(PACKAGE_PREFIX, args[0]) + target_mod = import_module(MOD_NAME) + + if test: + # setup CLI to enable command loader + az_cli = get_default_cli() + + # convert _help.py contents to help.yaml. Write out help.yaml + print("Generating new help.yaml file contents. Holding off on writing contents...") + out_file, result = convert(target_mod, args[0], test=True) + + print("Loading Commands...") + # load commands, args, and help + create_invoker_and_load_cmds_and_args(az_cli) + + # format loaded help + print("Loading all old help...") + old_loaded_help = {data.command: data for data in get_all_help(az_cli) if data.command} + + print("Now writing out new help.yaml file contents...") + with open(out_file, "w") as f: + yaml.dump(result, f) + + print("Loading all help again...") + new_loaded_help = {data.command: data for data in get_all_help(az_cli) if data.command} + + diff_dict = {} + for command in old_loaded_help: + if command.startswith(args[0]): + diff_dict[command] = (old_loaded_help[command], new_loaded_help[command]) + + print("Verifying that help objects are the same for {0}/_help.py and {0}/help.yaml.".format(args[0])) + # verify that contents the same + for old, new in diff_dict.values(): + assert_help_objs_equal(old, new) + + print("Running linter on {}.".format(args[0])) + linter_args = ["azdev", "cli-lint", "--module", args[0]] + completed_process = subprocess.run(linter_args, stderr=subprocess.STDOUT) + if completed_process.returncode != 0: + if failed: + print("{} assertion(s) failed.".format(failed)) + + print("Done. Linter failed for {}/help.yaml.".format(args[0])) + exit(1) + + if not failed: + print("Done! Successfully tested and generated {0}/help.yaml in {0} module".format(args[0])) + else: + print("Done. {} assertion(s) failed.".format(failed)) + exit(1) + + else: + print("Generating help.yaml file...") + out_file, result = convert(target_mod, args[0]) + with open(out_file, "w") as f: + yaml.dump(result, f) + print("Done! Successfully generated {0}/help.yaml in {0} module.".format(args[0]))