From 21225ff0a6b7f309dfb9879d79c12dfcd7f9a535 Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Mon, 24 Jun 2019 22:26:33 -0700 Subject: [PATCH 1/4] Undo help.yaml changes in #8325, #8503 and #8535 --- doc/authoring_help.md | 96 +---- doc/sphinx/azhelpgen/azhelpgen.py | 9 +- scripts/temp_help/convert_all.py | 228 ---------- scripts/temp_help/help_convert.py | 400 ------------------ src/azure-cli-core/azure/cli/core/_help.py | 234 +--------- .../azure/cli/core/_help_loaders.py | 235 ---------- .../azure/cli/core/file_util.py | 11 +- .../azure/cli/core/tests/test_help.py | 354 +++------------- .../azure/cli/core/tests/test_help_loaders.py | 141 ------ 9 files changed, 77 insertions(+), 1631 deletions(-) delete mode 100644 scripts/temp_help/convert_all.py delete mode 100644 scripts/temp_help/help_convert.py delete mode 100644 src/azure-cli-core/azure/cli/core/_help_loaders.py delete mode 100644 src/azure-cli-core/azure/cli/core/tests/test_help_loaders.py diff --git a/doc/authoring_help.md b/doc/authoring_help.md index 69ae7b0b87c..2d21e1924d8 100644 --- a/doc/authoring_help.md +++ b/doc/authoring_help.md @@ -12,20 +12,11 @@ To override help for a given command: 1. Search code base for "account clear". 2. Search result: src/command_modules/azure-cli-**profile**/azure/cli/command_modules/**profile**/commands.py. 3. Result shows "account clear" is in the "profile" module. -2. Using the module name, find the YAML help file which follows the path pattern.: - 1. src/command_modules/azure-cli-**[module name]**/azure/cli/command_modules/**[module name]**/_help.py
- **or**
- src/command_modules/azure-cli-**[module name]**/azure/cli/command_modules/**[module name]**/help.yaml +2. Using the module name, find the YAML help file which follows the path pattern: + 1. src/command_modules/azure-cli-**[module name]**/azure/cli/command_modules/**[module name]**/_help.py. 2. If the file doesn't exist, it can be created. 3. Find or create a help entry with the name of the command/group you want to document. See example below. - -> ###Notes:
-> 1. If using **_help.py** files for help authoring, the command module's **\_\_init\_\_.py** file must import the **_help.py** file. i.e:
-> `import azure.cli.command_modules.examplemod._help`
-> 2. The Help Authoring System now supports **help.yaml** files. Eventually, **_help.py** files will be replaced by **help.yaml**. - - ### Example YAML help file, _help.py ###
@@ -74,72 +65,6 @@ helps['account'] = """
             """
 
- -### Example YAML help file, help.yaml (Version 1) ### -
-#---------------------------------------------------------------------------------------------
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License. See License.txt in the project root for license information.
-#---------------------------------------------------------------------------------------------
-
-version: 1
-
-content:
-
-- command:
-    name: account clear
-    summary: Clear account
-    description: Longer summary of how the dummy account clear command works
-    links:
-      - title: Azure Accounts Webpage
-        url: https://azure.microsoft.com/en-us/account/
-      - url: https://aka.ms/just-a-url
-    arguments:
-      - name: --account-name
-        summary: Account name
-        description: |
-          Longer summary with newlines preserved.
-          Preserving newlines is helpful for paragraph breaks.
-        value-sources:
-          - link:
-              title: List accounts
-              command: az account list
-          - link:
-              title: Show an accounts details
-              command: az account show
-          - link:
-              title: Azure Accounts Webpage
-              url: https://azure.microsoft.com/en-us/account/
-          - link:
-              title: Azure Billing Documentation
-              url: https://docs.microsoft.com/en-us/azure/billing/
-          - string:
-              "Account name should be lower case with no numbers or special symbol."
-    examples:
-    - summary: Clear an account
-      description: >
-        This is a longer description of the example.
-        The > character collapses multiple lines into a single line,
-        which is good for on-screen wrapping.
-      command: |
-        az account clear --acount-name myaccount
-
-
- -You can also document groups using a similar format. - -
-
-- group:
-    name: account
-    summary: Manage Azure accounts
-    description: Longer summary of the account command group
-    links:
-      - title: Azure Accounts Webpage
-        url: https://azure.microsoft.com/en-us/account/
-      - url: https://aka.ms/just-a-url
-
- # Tips to write effective help for your command - Make sure the doc contains all the details that someone unfamiliar with the API needs to use the command. @@ -168,8 +93,7 @@ Here are the layers of Project Az help, with each layer overriding the layer bel | Help Display | |-------------------------------| -| YAML Authoring via *help.yaml*| -| YAML Authoring via *_help.py* | +| YAML Authoring (_help.py) | | Code Specified | | Docstring | | SDK Text | @@ -213,20 +137,6 @@ The first example is only supported on the `latest` and `2018-03-01-hybrid` prof supported-profiles: 2017-03-09-profile ``` -### help.yaml - -``` - examples: - - summary: Create a storage account MyStorageAccount in resource group MyResourceGroup in the West US region with locally redundant storage. - command: az storage account create -n MyStorageAccount -g MyResourceGroup -l westus --sku Standard_LRS - supported-profiles: latest, 2018-03-01-hybrid - # alternatively - # supported-profiles: latest, 2018-03-01-hybrid - - summary: Create a storage account MyStorageAccount in resource group MyResourceGroup in the West US region with locally redundant storage. - command: az storage account create -n MyStorageAccount -g MyResourceGroup -l westus --account-type Standard_LRS - supported-profiles: 2017-03-09-profile -``` - Here is how this looks in CLI `--help`: On profiles `latest` and `2018-03-01-hybrid`. diff --git a/doc/sphinx/azhelpgen/azhelpgen.py b/doc/sphinx/azhelpgen/azhelpgen.py index 8080412de0c..ba29c98a429 100644 --- a/doc/sphinx/azhelpgen/azhelpgen.py +++ b/doc/sphinx/azhelpgen/azhelpgen.py @@ -9,10 +9,11 @@ from os.path import expanduser from docutils import nodes from docutils.statemachine import ViewList + +# Directive not in latest release of sphinx, need to pip install sphinx==1.6.7 from sphinx.util.compat import Directive from sphinx.util.nodes import nested_parse_with_titles - from azure.cli.core import MainCommandsLoader, AzCli from azure.cli.core.commands import AzCliCommandInvoker from azure.cli.core.parser import AzCliCommandParser @@ -88,14 +89,14 @@ def make_rst(self): pass yield '{}:default: {}'.format(DOUBLEINDENT, arg.default) if arg.value_sources: - yield '{}:source: {}'.format(DOUBLEINDENT, ', '.join(_get_populator_commands(arg))) + yield '{}:source: {}'.format(DOUBLEINDENT, ', '.join(arg.value_sources)) yield '' yield '' if len(help_file.examples) > 0: for e in help_file.examples: - yield '{}.. cliexample:: {}'.format(INDENT, e.short_summary) + yield '{}.. cliexample:: {}'.format(INDENT, e.name) yield '' - yield DOUBLEINDENT + e.command.replace("\\", "\\\\") + yield DOUBLEINDENT + e.text.replace("\\", "\\\\") yield '' def run(self): diff --git a/scripts/temp_help/convert_all.py b/scripts/temp_help/convert_all.py deleted file mode 100644 index a763654fd98..00000000000 --- a/scripts/temp_help/convert_all.py +++ /dev/null @@ -1,228 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- - -import sys -import os -import subprocess - - -def get_repo_root(): - """ - Returns the root path to this repository. The root is where .git folder is. - """ - import os.path - here = os.path.dirname(os.path.realpath(__file__)) - - while not os.path.exists(os.path.join(here, '.git')): - here = os.path.dirname(here) - - return here - - -def comment_import_help(init_file, out_file): - f_out = open(out_file, "w") - - output = "" - updated = False - with open(init_file, "r") as f_in: - for line in f_in: - if "import" in line and "_help" in line and not updated: - updated = True - line = "# " + line - output += line - - f_out.write(output) - f_out.close() - return updated - -def decomment_import_help(init_file, out_file): - f_out = open(out_file, "w") - - output = "" - updated = False - with open(init_file, "r") as f_in: - for line in f_in: - if "import" in line and "_help" in line and not updated: - updated = True - line = line.lstrip("# ") - output += line - f_out.write(output) - f_out.close() - return updated - -def install_extension(ext_name): - command = "az extension add -n " + ext_name - completed = subprocess.run(command.split()) - if completed.returncode == 0: - print("{} was successfully installed.".format(ext_name)) - return True - else: - print("{} was not installed.".format(ext_name)) - return False - -def uninstall_extension(ext_name): - command = "az extension remove -n " + ext_name - completed = subprocess.run(command.split()) - if completed.returncode == 0: - print("{} was successfully uninstalled.".format(ext_name)) - return True - else: - print("{} was not uninstalled.".format(ext_name)) - return False - -if __name__ == "__main__": - args = sys.argv[1:] - - if args: - if args[0].lower() == "--core": - test = False - try: - if args[1].lower() == "--test": - test = True - except IndexError: - pass - - subprocess.run(["python", "./help_convert.py", "--get-all-mods"]) - - module_names = [] - with open("mod.txt", "r") as f: - for line in f: - module_names.append(line) - if "sqlvm" not in module_names: - module_names.append("sqlvm") - os.remove("mod.txt") - successes = 0 - with open(os.devnull, 'w') as devnull: # silence stdout by redirecting to devnull - for mod in module_names: - args = ["python", "./help_convert.py", mod] - if test: - args.append("--test") - completed_process = subprocess.run(args, stdout=devnull) - if completed_process.returncode == 0: - successes += 1 - - if successes: - print("\n----------------------------------------------------------" - "\nSuccessfuly converted {} help.py files to help.yaml files." - "\n----------------------------------------------------------".format(successes)) - - elif args[0].lower() == "--extensions": - pass - - elif args[0].lower() == "--count": - # Get info about help.py modules and converted help.yaml modules. - - src_root = os.path.join(get_repo_root(), "src", "command_modules") - - py_count = 0 - yaml_count = 0 - - for root, dirs, files in os.walk(src_root): - for file in files: - if file.endswith("_help.py") and os.path.dirname(root).endswith("command_modules") and os.path.join("build", "lib") not in root: - print("Found {}\n".format(os.path.join(root, file))) - py_count +=1 - if file.endswith("help.yaml") and os.path.dirname(root).endswith("command_modules"): - print("Found {}\n".format(os.path.join(root, file))) - yaml_count +=1 - - print("Found {} _help.py files\n".format(py_count)) - print("Found {} help.yaml files\n".format(yaml_count)) - - elif args[0].lower() == "--move-py": - - src_root = os.path.join(get_repo_root(), "src", "command_modules") - - py_count = 0 - yaml_count = 0 - failures = 0 - - for root, dirs, files in os.walk(src_root): - for file in files: - if file.endswith("_help.py") and os.path.dirname(root).endswith("command_modules") and os.path.join("build", "lib") not in root: - src = os.path.join(root, file) - dst = os.path.join(root, "foo.py") - print("Found {}\n".format(src)) - print("Renaming {}\n\tto {}\n.".format(src, dst)) - os.rename(src, dst) - py_count +=1 - - src = os.path.join(root, "__init__.py") - dst = os.path.join(root, "__init__2.py") - - success = comment_import_help(src, dst) - - if success: - os.remove(src) - os.rename(dst, src) - print("Commented out import in {}\n".format(src)) - else: - os.remove(dst) - print("Failed to comment out import in {}\n".format(src)) - failures+=1 - - - print("Renamed {} _help.py files to foo.py.\n".format(py_count)) - print("There were {} failures to decomment import statements\n".format(failures)) - - elif args[0].lower() == "--move-foo": - src_root = os.path.join(get_repo_root(), "src", "command_modules") - - py_count = 0 - yaml_count = 0 - failures = 0 - - for root, dirs, files in os.walk(src_root): - for file in files: - if file.endswith("foo.py") and os.path.dirname(root).endswith("command_modules") and os.path.join("build", "lib") not in root: - src = os.path.join(root, file) - dst = os.path.join(root, "_help.py") - print("Found {}\n".format(src)) - print("Renaming {}\n\tto {}\n.".format(src, dst)) - os.rename(src, dst) - py_count +=1 - - src = os.path.join(root, "__init__.py") - dst = os.path.join(root, "__init__2.py") - - success = decomment_import_help(src, dst) - - if success: - os.remove(src) - os.rename(dst, src) - print("De-commented out import in {}\n".format(src)) - else: - os.remove(dst) - print("Failed to de-comment out import in {}\n".format(src)) - failures+=1 - - print("Renamed {} foo.py files to _help.py.\n".format(py_count)) - print("There were {} failures to decomment import statements\n".format(failures)) - - elif args[0].lower() == "--add-extensions": - command = "az extension list-available --query [].name -o tsv" - completed = subprocess.run(command.split(), stdout=subprocess.PIPE, universal_newlines=True) - if completed.returncode == 0: - num_installed = 0 - extensions = completed.stdout.splitlines() - for ext in extensions: - success = install_extension(ext) - if success: - num_installed += 1 - - print("Installed {} extensions".format(num_installed)) - - elif args[0].lower() == "--remove-extensions": - command = "az extension list --query [].name -o tsv" - completed = subprocess.run(command.split(), stdout=subprocess.PIPE, universal_newlines=True) - if completed.returncode == 0: - num_installed = 0 - extensions = completed.stdout.splitlines() - for ext in extensions: - success = uninstall_extension(ext) - if success: - num_installed += 1 - - print("Uninstalled {} extensions".format(num_installed)) diff --git a/scripts/temp_help/help_convert.py b/scripts/temp_help/help_convert.py deleted file mode 100644 index 0b6abfe14df..00000000000 --- a/scripts/temp_help/help_convert.py +++ /dev/null @@ -1,400 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# 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 - -from logging import getLogger - -logger = getLogger(__name__) - -from knack.util import CLIError -from knack.help_files import helps -from azure.cli.core.mock import DummyCli -from azure.cli.core.util import get_installed_cli_distributions -from azure.cli.core._help import CliCommandHelpFile, CliGroupHelpFile -from azure.cli.core.file_util import _store_parsers, _is_group - -try: - from ruamel.yaml import YAML - yaml = YAML() - yaml.width = 1000 # prevents wrapping around in dumper. - yaml.allow_duplicate_keys = True # TODO: allow duplicate keys within help entries. see az container create. Remove this. -except ImportError as e: - msg = "{}\npip install ruamel.Yaml to use this script.".format(e) - exit(msg) - -PACKAGE_PREFIX = "azure.cli.command_modules" -CLI_PACKAGE_NAME = 'azure-cli' -COMPONENT_PREFIX = 'azure-cli-' - -failed = 0 - -loaded_helps = {} - -def get_all_help(cli_ctx): - invoker = cli_ctx.invocation - help_ctx = cli_ctx.help_cls(cli_ctx) - if not invoker: - raise CLIError('CLI context does not contain invocation.') - - parser_keys = [] - parser_values = [] - sub_parser_keys = [] - sub_parser_values = [] - _store_parsers(invoker.parser, parser_keys, parser_values, sub_parser_keys, sub_parser_values) - for cmd, parser in zip(parser_keys, parser_values): - if cmd not in sub_parser_keys: - sub_parser_keys.append(cmd) - sub_parser_values.append(parser) - help_files = [] - for cmd, parser in zip(sub_parser_keys, sub_parser_values): - if cmd in loaded_helps: - try: - 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: # pylint: disable=broad-except - print("Skipped '{}' due to '{}'".format(cmd, ex)) - help_files = sorted(help_files, key=lambda x: x.command) - assert {help_file.command for help_file in help_files} == set(loaded_helps.keys()) - print("Loaded {} help files".format(len(help_files))) - return help_files - - -def create_invoker_and_load_cmds_and_args(cli_ctx): - global loaded_helps - from knack import events - from azure.cli.core.commands import register_cache_arguments - from azure.cli.core.commands.arm import register_global_subscription_argument, register_ids_argument - - invoker = cli_ctx.invocation_cls(cli_ctx=cli_ctx, commands_loader_cls=cli_ctx.commands_loader_cls, - parser_cls=cli_ctx.parser_cls, help_cls=cli_ctx.help_cls) - cli_ctx.invocation = invoker - invoker.commands_loader.skip_applicability = True - temp_help = helps.copy() - invoker.commands_loader.load_command_table(None) # this ends up loading all the helpfiles, which could be problematic with duplicate key commands like acs create - helps.clear() - helps.update(temp_help) - - # turn off applicability check for applicable loaders - new_cmd_to_loader_map = {} - new_command_group_table = {} - new_command_table = {} - for cmd in loaded_helps.keys(): - if cmd in invoker.commands_loader.cmd_to_loader_map: # if a command from help, then update commands_loader - new_cmd_to_loader_map[cmd] = invoker.commands_loader.cmd_to_loader_map[cmd] - new_command_table[cmd] = invoker.commands_loader.command_table[cmd] - else: # else a group then update - new_command_group_table[cmd] = invoker.commands_loader.command_group_table[cmd] - - # include commands of groups in help.py, even though the commands themselves might not be in help.py, so their subparsers can be added. - for cmd in new_command_group_table.keys(): - all_cmds = invoker.commands_loader.cmd_to_loader_map.keys() - for old_cmd in all_cmds: - if old_cmd.startswith(cmd) and old_cmd != cmd: - new_cmd_to_loader_map[old_cmd] = invoker.commands_loader.cmd_to_loader_map[old_cmd] - new_command_table[old_cmd] = invoker.commands_loader.command_table[old_cmd] - - invoker.commands_loader.cmd_to_loader_map = new_cmd_to_loader_map - invoker.commands_loader.command_table = new_command_table - invoker.commands_loader.command_group_table = new_command_group_table - - for loaders in invoker.commands_loader.cmd_to_loader_map.values(): - for loader in loaders: - loader.skip_applicability = True - - for command in invoker.commands_loader.command_table: - invoker.commands_loader.load_arguments(command) - - assert len(new_command_table) == len(new_cmd_to_loader_map) - assert set(list(new_command_table.keys()) + list(new_command_group_table.keys())) >= set(loaded_helps.keys()) - - register_global_subscription_argument(cli_ctx) - register_ids_argument(cli_ctx) # global subscription must be registered first! - register_cache_arguments(cli_ctx) - cli_ctx.raise_event(events.EVENT_INVOKER_POST_CMD_TBL_CREATE, commands_loader=invoker.commands_loader) - invoker.parser.load_command_table(invoker.commands_loader) - -# this must be called before loading any command modules. Otherwise helps object will have every help.py file's contents -def convert(target_mod_or_file, mod_name, test=False): - global loaded_helps - - if os.path.exists(target_mod_or_file): - out_file = target_mod_or_file - target_mod = import_module("{}.{}._help".format(PACKAGE_PREFIX, mod_name)) - - else: # else not a file, but a mod. - try: - target_mod = import_module(target_mod_or_file) - except ModuleNotFoundError as e: # azure.cli.command_modules.core - logger.warning(e) - return None, None - 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. Will remove and rewrite: {}\n".format(mod_name, out_file)) - os.remove(out_file) - - # the modules keys are keys added to helps object from fresh import.... - help_dict = target_mod.helps - result = _get_new_yaml_dict(help_dict) - - # clear modules help from knack.helps, store help.py info - for key, value in help_dict.items(): - loaded_helps[key] = value - - return out_file, result - -def delete(target_mods): - for mod_name in target_mods: - try: - target_mod = import_module(mod_name) - except ModuleNotFoundError as e: # azure.cli.command_modules.core - print(e) - print("SKIPPING...") - continue - loader_file_path = os.path.abspath(target_mod.__file__) - yaml_file = os.path.join(os.path.dirname(loader_file_path), "help.yaml") - if os.path.exists(yaml_file): - print("Removing {}".format(yaml_file)) - os.remove(yaml_file) - -def get_all_mod_names(): - installed_dists = get_installed_cli_distributions() - mod_name_list = list(sorted(dist.key.replace(COMPONENT_PREFIX, '') for dist in installed_dists if dist.key.startswith(COMPONENT_PREFIX))) - return mod_name_list - - -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.safe_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() - new_param["name"] = max(options, key=lambda x: len(x)) - _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({"link": {"command" : 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"] - supported_profiles, unsupported_profiles = "supported-profiles", "unsupported-profiles" - if supported_profiles in ex: - new_ex[supported_profiles] = ex[supported_profiles] - if unsupported_profiles in ex: - new_ex[unsupported_profiles] = ex[unsupported_profiles] - 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.short_summary) - new_examples = sorted(new_help.examples, key=lambda x: x.short_summary) - 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.short_summary, new_ex.short_summary) - assert_true_or_warn (old_ex.command, new_ex.command) - assert_true_or_warn (old_ex.long_summary, new_ex.long_summary) - - # 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) - assert_true_or_warn(old.value_sources, new.value_sources) - - -def assert_true_or_warn(x, y): - try: - 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: - msg = "\nvalues:\n\n{}\n\nand\n\n{}\n\nare not equal.\n".format(x, y) - - global failed - failed+=1 - if failed: - msg = "{}{}".format(msg, "-------------------\nAssertion Failed!!!\nExiting test.\n-------------------\n") - exit(msg) - - -if __name__ == "__main__": - if sys.version_info[0] < 3: - raise Exception("This script requires Python 3") - - args = [arg.strip() for arg in sys.argv[1:]] - test = False - - msg = 'Usage: python help_convert.py (MOD | --test | --delete | --all | MOD --test | path/help.py --package {myext.package})\n' - - if "--help" in args or "-h" in args: - print(msg) - exit(0) - - if len(args) > 3 or len(args) == 0: - exit(msg) - - if len(args) == 2: - if args[1].lower() != "--test": - exit(msg) - else: - test = True - if len(args) == 3: - if not args[0].lower().endswith("_help.py") or args[1].lower() != "--package": - exit(msg) - else: - raise NotImplementedError - - target_mods = None - # if args[0].lower() in ["--test", "--all"]: - # # convert all modules and test - # mod_names = get_all_mod_names() - # target_mods = ["{}.{}._help".format(PACKAGE_PREFIX, mod) for mod in mod_names] - # if args[0].lower() == "--test": - # test = True - if args[0].lower() == "--delete": - mod_names = get_all_mod_names() - mod_names.append("sqlvm") - target_mods = ["{}.{}._help".format(PACKAGE_PREFIX, mod) for mod in mod_names] - delete(target_mods) - exit(0) - elif args[0].lower() == "--get-all-mods": - mod_names = get_all_mod_names() - with open("mod.txt", "w") as f: - for name in mod_names: - f.write(name + "\n") - exit(0) - else: - mod_names = [args[0]] - # attempt to find and load the desired module. - if "_help.py" not in mod_names[0]: - target_mods = ["{}.{}._help".format(PACKAGE_PREFIX, mod_names[0])] - else: - target_mods = [mod_names[0]] - mod_names = [os.path.split(os.path.split(mod_names[0])[0])[1]] # if help.py, get mod name - - if test: - # convert _help.py contents to help.yaml. Write out help.yaml - print("Generating new help.yaml file contents. Holding off on writing contents...") - file_to_help = {} - - for mod, mod_name in zip(target_mods, mod_names): - file, help_dict = convert(mod, mod_name, test=True) - if file is None or help_dict is None: - continue - file_to_help[file] = help_dict - - if not file_to_help: - exit("No help.yaml file generated for {}.".format(", ".join(mod_names))) - - print("Loading Commands...") - # setup CLI - az_cli = DummyCli() - create_invoker_and_load_cmds_and_args(az_cli) - - print("Loading all old help...") - all_cli_help = get_all_help(az_cli) - old_loaded_help = {data.command: data for data in all_cli_help} - - print("Now writing out new help.yaml file contents...") - for file, help_dict in file_to_help.items(): - logger.warning("Writing {} ...".format(file)) - with open(file, "w") as f: - yaml.safe_dump(help_dict, f) - - print("Loading all help again...") - all_cli_help = get_all_help(az_cli) - new_loaded_help = {data.command: data for data in all_cli_help} - - assert len(old_loaded_help) == len(new_loaded_help) - - diff_dict = {} - for command in old_loaded_help: - diff_dict[command] = (old_loaded_help[command], new_loaded_help[command]) - - logger.warning("Loaded {} help objects".format(len(new_loaded_help))) - logger.warning("Verifying that help objects are the same for _help.py and help.yaml.") - assert len(diff_dict) == len(loaded_helps) - for old, new in diff_dict.values(): - assert_help_objs_equal(old, new) - - else: - if len(target_mods) == 1: - print("Generating help.yaml file...") - out_file, result = convert(target_mods[0], mod_names[0]) - with open(out_file, "w") as f: - yaml.safe_dump(result, f) - print("Done! Successfully generated {0}/help.yaml in {0} module.".format(mod_names[0])) - else: - print("Generating help.yaml files...") - for mod, mod_name in zip(target_mods, mod_names): - out_file, result = convert(mod, mod_name) - if out_file is None or result is None: - continue - with open(out_file, "w") as f: - yaml.safe_dump(result, f) - print("Successfully generated {0}/help.yaml in {0} module.".format(mod_name)) - diff --git a/src/azure-cli-core/azure/cli/core/_help.py b/src/azure-cli-core/azure/cli/core/_help.py index 182e2895ab1..dd56a04a786 100644 --- a/src/azure-cli-core/azure/cli/core/_help.py +++ b/src/azure-cli-core/azure/cli/core/_help.py @@ -4,17 +4,12 @@ # -------------------------------------------------------------------------------------------- from __future__ import print_function -import argparse -from azure.cli.core.commands import ExtensionCommandSource - -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, HelpAuthoringException) +from knack.help import (HelpExample as KnackHelpExample, HelpFile as KnackHelpFile, CommandHelpFile as KnackCommandHelpFile, + CLIHelp, ArgumentGroupRegistry as KnackArgumentGroupRegistry, HelpAuthoringException) from knack.log import get_logger -from knack.util import CLIError +from azure.cli.core.commands import ExtensionCommandSource logger = get_logger(__name__) @@ -48,61 +43,11 @@ # PrintMixin class to decouple printing functionality from AZCLIHelp class. -# Most of these methods override print methods in CLIHelp class CLIPrintMixin(CLIHelp): - def _print_header(self, cli_name, help_file): - super(CLIPrintMixin, self)._print_header(cli_name, help_file) - - links = help_file.links - if links: - link_text = "{} and {}".format(", ".join([link["url"] for link in links[0:-1]]), - links[-1]["url"]) if len(links) > 1 else links[0]["url"] - link_text = "For more information, see: {}\n".format(link_text) - _print_indent(link_text, 2, width=self.textwrap_width) - def _print_detailed_help(self, cli_name, help_file): CLIPrintMixin._print_extensions_msg(help_file) super(CLIPrintMixin, self)._print_detailed_help(cli_name, help_file) - @staticmethod - def _get_choices_defaults_sources_str(p): - choice_str = u' Allowed values: {}.'.format(', '.join(sorted([str(x) for x in p.choices]))) \ - if p.choices else '' - default_str = u' Default: {}.'.format(p.default) if p.default and p.default != argparse.SUPPRESS else '' - value_sources_str = CLIPrintMixin._process_value_sources(p) if p.value_sources else '' - return u'{}{}{}'.format(choice_str, default_str, value_sources_str) - - @staticmethod - def _print_examples(help_file): - indent = 0 - _print_indent('Examples', indent) - for e in help_file.examples: - indent = 1 - _print_indent(u'{0}'.format(e.short_summary), indent) - indent = 2 - if e.long_summary: - _print_indent(u'{0}'.format(e.long_summary), indent) - _print_indent(u'{0}'.format(e.command), indent) - print('') - - @staticmethod - def _process_value_sources(p): - commands, strings, urls = [], [], [] - - for item in p.value_sources: - if "string" in item: - strings.append(item["string"]) - elif "link" in item and "command" in item["link"]: - commands.append(item["link"]["command"]) - elif "link" in item and "url" in item["link"]: - urls.append(item["link"]["url"]) - - command_str = u' Values from: {}.'.format(", ".join(commands)) if commands else '' - string_str = u' {}'.format(", ".join(strings)) if strings else '' - string_str = string_str + "." if string_str and not string_str.endswith(".") else string_str - urls_str = u' For more info, go to: {}.'.format(", ".join(urls)) if urls else '' - return u'{}{}{}'.format(command_str, string_str, urls_str) - @staticmethod def _print_extensions_msg(help_file): if help_file.type != 'command': @@ -120,7 +65,6 @@ 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 @@ -139,59 +83,9 @@ def new_normalize_text(s): HelpObject._normalize_text = new_normalize_text # pylint: disable=protected-access - self._register_help_loaders() - self._name_to_content = {} - - # override - def show_help(self, cli_name, nouns, parser, is_group): - self.update_loaders_with_help_file_contents(nouns) - super(AzCliHelp, self).show_help(cli_name, nouns, parser, is_group) - - 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 - - if len(versioned_loaders) != len({ldr.version for ldr in versioned_loaders.values()}): - ldrs_str = " ".join("{}-version:{}".format(cls_name, ldr.version) for cls_name, ldr in versioned_loaders.items()) # pylint: disable=line-too-long - raise CLIError("Two loaders have the same version. Loaders:\n\t{}".format(ldrs_str)) - - self.versioned_loaders = versioned_loaders - - def update_loaders_with_help_file_contents(self, nouns): - loader_file_names_dict = {} - file_name_set = set() - for ldr_cls_name, loader in self.versioned_loaders.items(): - new_file_names = loader.get_noun_help_file_names(nouns) or [] - loader_file_names_dict[ldr_cls_name] = new_file_names - file_name_set.update(new_file_names) - - for file_name in file_name_set: - if file_name not in self._name_to_content: - with open(file_name, 'r') as f: - self._name_to_content[file_name] = f.read() - - for ldr_cls_name, file_names in loader_file_names_dict.items(): - file_contents = {} - for name in file_names: - file_contents[name] = self._name_to_content[name] - self.versioned_loaders[ldr_cls_name].update_file_contents(file_contents) - 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. - super(CliHelpFile, self).__init__(help_ctx, delimiters) - self.links = [] - def _should_include_example(self, ex): supported_profiles = ex.get('supported-profiles') unsupported_profiles = ex.get('unsupported-profiles') @@ -211,92 +105,21 @@ def _should_include_example(self, ex): # Needs to override base implementation to exclude unsupported examples. def _load_from_data(self, data): - 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') + super(CliHelpFile, self)._load_from_data(data) + self.examples = [] # clear examples set by knack if 'examples' in data: self.examples = [] for d in data['examples']: if self._should_include_example(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 load(self, options): - # forces class to use this load method even if KnackGroupHelpFile overrides CliHelpFile's method. - CliHelpFile.load(self, options) + self.examples.append(HelpExample(d)) class CliCommandHelpFile(KnackCommandHelpFile, CliHelpFile): def __init__(self, help_ctx, delimiters, parser): - super(CliCommandHelpFile, self).__init__(help_ctx, delimiters, parser) - 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), - 'preview_info': getattr(action, 'preview_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' - - # update parameter type so we can use overriden update_from_data method to update value sources. - for param in self.parameters: - param.__class__ = HelpParameter - - 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) + super(CliCommandHelpFile, self).__init__(help_ctx, delimiters, parser) class ArgumentGroupRegistry(KnackArgumentGroupRegistry): # pylint: disable=too-few-public-methods @@ -317,48 +140,11 @@ def __init__(self, group_list): self.priorities[group] = priority priority += 1 - +# override to add support for supported and unsupported profiles class HelpExample(KnackHelpExample): # pylint: disable=too-few-public-methods - def __init__(self, **_data): + def __init__(self, _data): # Old attributes - _data['name'] = _data.get('name', '') - _data['text'] = _data.get('text', '') super(HelpExample, self).__init__(_data) - - self.name = _data.get('summary', '') if _data.get('summary', '') else self.name - self.text = _data.get('command', '') if _data.get('command', '') else self.text - - self.long_summary = _data.get('description', '') self.supported_profiles = _data.get('supported-profiles', None) - self.unsupported_profiles = _data.get('unsupported-profiles', None) - - # alias old params with new - @property - def short_summary(self): - return self.name - - @short_summary.setter - def short_summary(self, value): - self.name = value - - @property - def command(self): - return self.text - - @command.setter - def command(self, value): - self.text = value - - -class HelpParameter(KnackHelpParameter): # pylint: disable=too-many-instance-attributes - - def __init__(self, **kwargs): - super(HelpParameter, self).__init__(**kwargs) - - def update_from_data(self, data): - super(HelpParameter, self).update_from_data(data) - # original help.py value_sources are strings, update command strings to value-source dict - if self.value_sources: - self.value_sources = [str_or_dict if isinstance(str_or_dict, dict) else {"link": {"command": str_or_dict}} - for str_or_dict in self.value_sources] + self.unsupported_profiles = _data.get('unsupported-profiles', None) \ 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 deleted file mode 100644 index 4239b4fba06..00000000000 --- a/src/azure-cli-core/azure/cli/core/_help_loaders.py +++ /dev/null @@ -1,235 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- - -import abc -import os -import yaml - -from azure.cli.core._help import (HelpExample, CliHelpFile) - -from knack.util import CLIError -from knack.log import get_logger - -logger = get_logger(__name__) - -try: - ABC = abc.ABC -except AttributeError: # Python 2.7, abc exists, but not ABC - ABC = abc.ABCMeta('ABC', (object,), {'__slots__': ()}) - - -# BaseHelpLoader defining versioned loader interface. Also contains some helper methods. -class BaseHelpLoader(ABC): - def __init__(self, help_ctx=None): - self.help_ctx = help_ctx - self._entry_data = None - self._file_content_dict = {} - - def versioned_load(self, help_obj, parser): - if not self._file_content_dict: - return - self._entry_data = None - # Cycle through versioned_load helpers - self.load_entry_data(help_obj, parser) - if self._data_is_applicable(): - self.load_help_body(help_obj) - self.load_help_parameters(help_obj) - self.load_help_examples(help_obj) - self._entry_data = None - - def update_file_contents(self, file_contents): - self._file_content_dict.update(file_contents) - - @abc.abstractmethod - def get_noun_help_file_names(self, nouns): - pass - - @property - @abc.abstractmethod - def version(self): - pass - - def _data_is_applicable(self): - return self._entry_data and self.version == self._entry_data.get('version') - - @abc.abstractmethod - def load_entry_data(self, help_obj, parser): - pass - - @abc.abstractmethod - def load_help_body(self, help_obj): - pass - - @abc.abstractmethod - def load_help_parameters(self, help_obj): - pass - - @abc.abstractmethod - def load_help_examples(self, help_obj): - pass - - # Loader static helper methods - - # Update a help file object from a data dict using the attribute to key mapping - @staticmethod - def _update_obj_from_data_dict(obj, data, attr_key_tups): - for attr, key in attr_key_tups: - try: - setattr(obj, attr, data[key] or attr) - except (AttributeError, KeyError): - pass - - # update relevant help file object parameters from data. - @staticmethod - def _update_help_obj_params(help_obj, data_params, params_equal, attr_key_tups): - loaded_params = [] - for param_obj in help_obj.parameters: - loaded_param = next((n for n in data_params if params_equal(param_obj, n)), None) - if loaded_param: - BaseHelpLoader._update_obj_from_data_dict(param_obj, loaded_param, attr_key_tups) - loaded_params.append(param_obj) - help_obj.parameters = loaded_params - - -class YamlLoaderMixin(object): # pylint:disable=too-few-public-methods - """A class containing helper methods for Yaml Loaders.""" - - # get the list of yaml help file names for the command or group - @staticmethod - def _get_yaml_help_files_list(nouns, cmd_loader_map_ref): - import inspect - - command_nouns = " ".join(nouns) - # if command in map, get the loader. Path of loader is path of helpfile. - ldr_or_none = cmd_loader_map_ref.get(command_nouns, [None])[0] - if ldr_or_none: - loaders = {ldr_or_none} - else: - loaders = set() - - # otherwise likely a group, try to find all command loaders under group as the group help could be defined - # in either. - if not loaders: - for cmd_name, cmd_ldr in cmd_loader_map_ref.items(): - # if first word in loader name is the group, this is a command in the command group - if cmd_name.startswith(command_nouns + " "): - loaders.add(cmd_ldr[0]) - - results = [] - if loaders: - for loader in loaders: - 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("help.yaml") or file.endswith("help.yml"): - help_file_path = os.path.join(dir_name, file) - results.append(help_file_path) - return results - - @staticmethod - def _parse_yaml_from_string(text, help_file_path): - dir_name, base_name = os.path.split(help_file_path) - pretty_file_path = os.path.join(os.path.basename(dir_name), base_name) - - if not text: - raise CLIError("No content passed for {}.".format(pretty_file_path)) - - try: - return yaml.safe_load(text) - except yaml.YAMLError as e: - raise CLIError("Error parsing {}:\n\n{}".format(pretty_file_path, e)) - - -class HelpLoaderV0(BaseHelpLoader): - - @property - def version(self): - return 0 - - def versioned_load(self, help_obj, parser): - super(CliHelpFile, help_obj).load(parser) # pylint:disable=bad-super-call - - def get_noun_help_file_names(self, nouns): - pass - - def load_entry_data(self, help_obj, parser): - pass - - def load_help_body(self, help_obj): - pass - - def load_help_parameters(self, help_obj): - pass - - def load_help_examples(self, help_obj): - pass - - -class HelpLoaderV1(BaseHelpLoader, YamlLoaderMixin): - core_attrs_to_keys = [("short_summary", "summary"), ("long_summary", "description")] - body_attrs_to_keys = core_attrs_to_keys + [("links", "links")] - param_attrs_to_keys = core_attrs_to_keys + [("value_sources", "value-sources")] - - @property - def version(self): - return 1 - - def get_noun_help_file_names(self, nouns): - cmd_loader_map_ref = self.help_ctx.cli_ctx.invocation.commands_loader.cmd_to_loader_map - return self._get_yaml_help_files_list(nouns, cmd_loader_map_ref) - - def update_file_contents(self, file_contents): - for file_name in file_contents: - if file_name not in self._file_content_dict: - data_dict = {file_name: self._parse_yaml_from_string(file_contents[file_name], file_name)} - self._file_content_dict.update(data_dict) - - def load_entry_data(self, help_obj, parser): - prog = parser.prog if hasattr(parser, "prog") else parser._prog_prefix # pylint: disable=protected-access - command_nouns = prog.split()[1:] - cmd_loader_map_ref = self.help_ctx.cli_ctx.invocation.commands_loader.cmd_to_loader_map - - files_list = self._get_yaml_help_files_list(command_nouns, cmd_loader_map_ref) - data_list = [self._file_content_dict[name] for name in files_list] - - self._entry_data = self._get_entry_data(help_obj.command, data_list) - - def load_help_body(self, help_obj): - help_obj.long_summary = "" # similar to knack... - self._update_obj_from_data_dict(help_obj, self._entry_data, self.body_attrs_to_keys) - - def load_help_parameters(self, help_obj): - def params_equal(param, param_dict): - if param_dict['name'].startswith("--"): # for optionals, help file name must be one of the long options - return param_dict['name'] in param.name.split() - # for positionals, help file must name must match param name shown when -h is run - return param_dict['name'] == param.name - - if help_obj.type == "command" and hasattr(help_obj, "parameters") and self._entry_data.get("arguments"): - loaded_params = [] - for param_obj in help_obj.parameters: - loaded_param = next((n for n in self._entry_data["arguments"] if params_equal(param_obj, n)), None) - if loaded_param: - self._update_obj_from_data_dict(param_obj, loaded_param, self.param_attrs_to_keys) - loaded_params.append(param_obj) - help_obj.parameters = loaded_params - - def load_help_examples(self, help_obj): - if help_obj.type == "command" and self._entry_data.get("examples"): - help_obj.examples = [HelpExample(**ex) for ex in self._entry_data["examples"] if help_obj._should_include_example(ex)] # pylint: disable=line-too-long, protected-access - - @staticmethod - def _get_entry_data(cmd_name, data_list): - for data in data_list: - if data and data.get("content"): - try: - entry_data = next(value for elem in data.get("content") - for key, value in elem.items() if value.get("name") == cmd_name) - entry_data["version"] = data['version'] - return entry_data - except StopIteration: - continue - return None 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 1070bf7e236..72edf03c760 100644 --- a/src/azure-cli-core/azure/cli/core/file_util.py +++ b/src/azure-cli-core/azure/cli/core/file_util.py @@ -5,13 +5,13 @@ from __future__ import print_function -from azure.cli.core._help import CliCommandHelpFile, CliGroupHelpFile - -from knack.log import get_logger from knack.util import CLIError +from knack.help import GroupHelpFile +from knack.log import get_logger -logger = get_logger(__name__) +from azure.cli.core._help import CliCommandHelpFile +logger = get_logger(__name__) def get_all_help(cli_ctx, skip=True): invoker = cli_ctx.invocation @@ -32,8 +32,7 @@ def get_all_help(cli_ctx, skip=True): help_errors = {} for cmd, parser in zip(sub_parser_keys, sub_parser_values): try: - help_ctx.update_loaders_with_help_file_contents(cmd.split()) - help_file = CliGroupHelpFile(help_ctx, cmd, parser) if _is_group(parser) \ + help_file = GroupHelpFile(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/src/azure-cli-core/azure/cli/core/tests/test_help.py b/src/azure-cli-core/azure/cli/core/tests/test_help.py index b6bc961a036..be42c61a72d 100644 --- a/src/azure-cli-core/azure/cli/core/tests/test_help.py +++ b/src/azure-cli-core/azure/cli/core/tests/test_help.py @@ -7,14 +7,14 @@ import logging import shutil -import inspect -from inspect import getmembers as inspect_getmembers +from knack.help import GroupHelpFile, HelpAuthoringException + import unittest import mock import tempfile -from knack.help import GroupHelpFile, HelpAuthoringException +from azure.cli.core import AzCommandsLoader from azure.cli.core._help import CliCommandHelpFile from azure.cli.core.mock import DummyCli @@ -23,37 +23,6 @@ logger = logging.getLogger(__name__) -# Command loader module -MOCKED_COMMAND_LOADER_MOD = "test_help_loaders" - - -# mock command loader method so that only the mock command loader can be loaded. -def mock_load_command_loader(loader, args, name, prefix): - return _load_command_loader(loader, args, name, "azure.cli.core.tests.") - - -# mock inspect.getfile to discover directory containing help files. -def get_mocked_inspect_getfile(expected_arg, return_value): - - def inspect_getfile(obj): - if obj == expected_arg: - return return_value - else: - return inspect.getfile(obj) - return inspect_getfile - - -# mock getmembers so test help loader can be injected into AzCliHelp class' versioned loader list -def mock_inspect_getmembers(object, predicate=None): - import azure.cli.core.tests.test_help_loaders as possible_loaders - - if "azure.cli.core._help_loaders" in repr(object) and "is_loader_cls" in repr(predicate): - result = inspect_getmembers(object, predicate) - result.extend(inspect_getmembers(possible_loaders, predicate)) - return list(set(result)) - else: - return inspect_getmembers(object, predicate) - def _store_parsers(parser, d): for s in parser.subparsers.values(): @@ -72,7 +41,6 @@ def _get_parser_name(parser): # pylint:disable=protected-access return (parser._prog_prefix if hasattr(parser, '_prog_prefix') else parser.prog)[len('az '):] - def create_invoker_and_load_cmds_and_args(cli_ctx): from knack import events from azure.cli.core.commands import register_cache_arguments @@ -108,8 +76,54 @@ def create_invoker_and_load_cmds_and_args(cli_ctx): cli_ctx.raise_event(events.EVENT_INVOKER_POST_CMD_TBL_CREATE, commands_loader=invoker.commands_loader) invoker.parser.load_command_table(invoker.commands_loader) +# Command loader module +MOCKED_COMMAND_LOADER_MOD = "test_help" + +# mock command loader method so that only the mock command loader can be loaded. +def mock_load_command_loader(loader, args, name, prefix): + return _load_command_loader(loader, args, name, "azure.cli.core.tests.") + +# region TestCommandLoader +class TestCommandLoader(AzCommandsLoader): + + def __init__(self, cli_ctx=None): + from azure.cli.core.commands import CliCommandType + compute_custom = CliCommandType( + operations_tmpl='{}#{{}}'.format(__name__), + ) + super(TestCommandLoader, self).__init__(cli_ctx=cli_ctx, + custom_command_type=compute_custom) + self.cmd_to_loader_map = {} + + def load_command_table(self, args): + with self.command_group('test') as g: + g.custom_command('alpha', 'dummy_handler') + + return self.command_table + + def load_arguments(self, command): + with self.argument_context('test') as c: + c.argument('arg1', options_list=['--arg1', '-a']) + c.argument('arg2', options_list=['--arg2', '-b'], help="Help From code.") + with self.argument_context('test alpha') as c: + c.positional('arg4', metavar="ARG4") + self._update_command_definitions() # pylint: disable=protected-access + + +def dummy_handler(arg1, arg2=None, arg3=None, arg4=None): + """ + Short summary here. Long summary here. Still long summary. + :param arg1: arg1's docstring help text + :param arg2: arg2's docstring help text + :param arg3: arg3's docstring help text + :param arg4: arg4's docstring help text + """ + pass + +COMMAND_LOADER_CLS = TestCommandLoader +# endregion + -# TODO update this CLASS to properly load all help... . class HelpTest(unittest.TestCase): @classmethod def setUpClass(cls): @@ -196,130 +210,6 @@ def set_help_py(self): unsupported-profiles: 2017-03-09-profile """ - def set_help_yaml(self): - yaml_help = """ - version: 1 - content: - - group: - name: test - summary: Group yaml summary - description: Group yaml description. A.K.A long description - links: - - title: Azure Test Docs - url: "https://docs.microsoft.com/en-us/azure/test" - - url: "https://aka.ms/just-a-url" - - command: - name: test alpha - summary: Command yaml summary - description: Command yaml description. A.K.A long description - links: - - title: Azure Test Alpha Docs - url: "https://docs.microsoft.com/en-us/azure/test/alpha" - - url: "https://aka.ms/just-a-long-url" - arguments: - - name: --arg2 # we do not specify the short option in the name. - summary: Arg 2's summary - description: A true description of this parameter. - value-sources: - - string: "Number range: -5.0 to 5.0" - - link: - url: https://www.foo.com - title: foo - - link: - command: az test show - title: Show test details - - name: ARG4 # Note: positional's are discouraged in the CLI. - summary: Arg4's summary, yaml. Positional arg, not required - examples: - - summary: A simple example - description: More detail on the simple example. - command: az test alpha --arg1 apple --arg2 ball --arg3 cat - supported-profiles: 2018-03-01-hybrid, latest - - summary: Another example unsupported on latest - description: More detail on the unsupported example. - command: az test alpha --arg1 apple --arg2 ball - unsupported-profiles: 2017-03-09-profile - """ - return self._create_new_temp_file(yaml_help, suffix="help.yaml") - - def set_help_json(self): - json_help = """ - { - "version": 2, - "content": [ - { - "group": { - "name": "test", - "short": "Group json summary", - "long": "Group json description. A.K.A long description", - "hyper-links": [ - { - "title": "Azure Json Test Docs", - "url": "https://docs.microsoft.com/en-us/azure/test" - }, - { - "url": "https://aka.ms/just-a-url" - } - ] - } - }, - { - "command": { - "name": "test alpha", - "short": "Command json summary", - "long": "Command json description. A.K.A long description", - "hyper-links": [ - { - "title": "Azure Json Test Alpha Docs", - "url": "https://docs.microsoft.com/en-us/azure/test/alpha" - }, - { - "url": "https://aka.ms/just-a-long-url" - } - ], - "arguments": [ - { - "name": "--arg3", - "short": "Arg 3's json summary", - "long": "A truly true description of this parameter.", - "sources": [ - { - "string": "Number range: 0 to 10" - }, - { - "link": { - "url": "https://www.foo-json.com", - "title": "foo-json" - } - }, - { - "link": { - "command": "az test show", - "title": "Show test details. Json file" - } - } - ] - }, - { - "name": "ARG4", - "summary": "Arg4's summary, json. Positional arg, not required" - } - ], - "examples": [ - { - "summary": "A simple example from json", - "description": "More detail on the simple example.", - "command": "az test alpha --arg1 alpha --arg2 beta --arg3 chi", - "supported-profiles": "2018-03-01-hybrid, latest" - } - ] - } - } - ] - } - """ - return self._create_new_temp_file(json_help, suffix="help.json") - # Mock logic in core.MainCommandsLoader.load_command_table for retrieving installed modules. @mock.patch('pkgutil.iter_modules', side_effect=lambda x: [(None, MOCKED_COMMAND_LOADER_MOD, None)]) @mock.patch('azure.cli.core.commands._load_command_loader', side_effect=mock_load_command_loader) @@ -362,153 +252,17 @@ def test_load_from_help_py(self, mocked_load, mocked_pkg_util): self.assertEqual(obj_param_dict["--arg1 -a"].short_summary, "A short summary.") self.assertEqual(obj_param_dict["--arg1 -a"].short_summary, "A short summary.") - self.assertEqual(obj_param_dict["--arg1 -a"].value_sources[0]['link']['command'], "az foo bar") - self.assertEqual(obj_param_dict["--arg1 -a"].value_sources[1]['link']['command'], "az bar baz") + self.assertEqual(obj_param_dict["--arg1 -a"].value_sources[0], "az foo bar") + self.assertEqual(obj_param_dict["--arg1 -a"].value_sources[1], "az bar baz") - self.assertEqual(command_help_obj.examples[0].short_summary, "Alpha Example") - self.assertEqual(command_help_obj.examples[0].command, "az test alpha --arg1 a --arg2 b --arg3 c") + self.assertEqual(command_help_obj.examples[0].name, "Alpha Example") + self.assertEqual(command_help_obj.examples[0].text, "az test alpha --arg1 a --arg2 b --arg3 c") self.assertEqual(command_help_obj.examples[0].supported_profiles, "2018-03-01-hybrid, latest") self.assertEqual(command_help_obj.examples[0].unsupported_profiles, None) self.assertEqual(command_help_obj.examples[1].supported_profiles, None) self.assertEqual(command_help_obj.examples[1].unsupported_profiles, "2017-03-09-profile") - @mock.patch('pkgutil.iter_modules', side_effect=lambda x: [(None, MOCKED_COMMAND_LOADER_MOD, None)]) - @mock.patch('azure.cli.core.commands._load_command_loader', side_effect=mock_load_command_loader) - def test_load_from_help_yaml(self, mocked_load, mocked_pkg_util): - # setup help.py and help.yaml help. - self.set_help_py() - yaml_path = self.set_help_yaml() - create_invoker_and_load_cmds_and_args(self.test_cli) - - # mock logic in core._help_loaders for retrieving yaml file from loader path. - expected_arg = self.test_cli.invocation.commands_loader.cmd_to_loader_map['test alpha'][0].__class__ - with mock.patch('inspect.getfile', side_effect=get_mocked_inspect_getfile(expected_arg, yaml_path)): - group_help_obj = next((help for help in get_all_help(self.test_cli) if help.command == "test"), None) - command_help_obj = next((help for help in get_all_help(self.test_cli) if help.command == "test alpha"), None) # pylint: disable=line-too-long - - # Test that group and command help are successfully displayed. - with self.assertRaises(SystemExit): - self.test_cli.invoke(["test", "-h"]) - with self.assertRaises(SystemExit): - self.test_cli.invoke(["test", "alpha", "-h"]) - - # Test group help - self.assertIsNotNone(group_help_obj) - self.assertEqual(group_help_obj.short_summary, "Group yaml summary.") - self.assertEqual(group_help_obj.long_summary, "Group yaml description. A.K.A long description.") - self.assertEqual(group_help_obj.links[0], {"title": "Azure Test Docs", "url": "https://docs.microsoft.com/en-us/azure/test"}) - self.assertEqual(group_help_obj.links[1], {"url": "https://aka.ms/just-a-url"}) - - # Test command help - self.assertIsNotNone(command_help_obj) - self.assertEqual(command_help_obj.short_summary, "Command yaml summary.") - self.assertEqual(command_help_obj.long_summary, "Command yaml description. A.K.A long description.") - self.assertEqual(command_help_obj.links[0], {"title": "Azure Test Alpha Docs", - "url": "https://docs.microsoft.com/en-us/azure/test/alpha"}) - self.assertEqual(command_help_obj.links[1], {"url": "https://aka.ms/just-a-long-url"}) - - # test that parameters and help are loaded from command function docstring, argument registry help and help.yaml - obj_param_dict = {param.name: param for param in command_help_obj.parameters} - param_name_set = {"--arg1 -a", "--arg2 -b", "--arg3", "ARG4"} - self.assertTrue(set(obj_param_dict.keys()).issuperset(param_name_set)) - - self.assertEqual(obj_param_dict["--arg1 -a"].short_summary, "A short summary.") - self.assertEqual(obj_param_dict["--arg3"].short_summary, "Arg3's docstring help text.") - self.assertEqual(obj_param_dict["ARG4"].short_summary, "Arg4's summary, yaml. Positional arg, not required.") - - self.assertEqual(obj_param_dict["--arg2 -b"].short_summary, "Arg 2's summary.") - self.assertEqual(obj_param_dict["--arg2 -b"].long_summary, "A true description of this parameter.") - self.assertEqual(obj_param_dict["--arg2 -b"].value_sources[0], {"string": "Number range: -5.0 to 5.0"}) - self.assertEqual(obj_param_dict["--arg2 -b"].value_sources[1]['link'], {"url": "https://www.foo.com", - "title": "foo"}) - self.assertEqual(obj_param_dict["--arg2 -b"].value_sources[2]['link'], {"command": "az test show", - "title": "Show test details"}) - - self.assertEqual(command_help_obj.examples[0].short_summary, "A simple example") - self.assertEqual(command_help_obj.examples[0].long_summary, "More detail on the simple example.") - self.assertEqual(command_help_obj.examples[0].command, "az test alpha --arg1 apple --arg2 ball --arg3 cat") - self.assertEqual(command_help_obj.examples[0].supported_profiles, "2018-03-01-hybrid, latest") - self.assertEqual(command_help_obj.examples[0].unsupported_profiles, None) - - self.assertEqual(command_help_obj.examples[1].supported_profiles, None) - self.assertEqual(command_help_obj.examples[1].unsupported_profiles, "2017-03-09-profile") - - @mock.patch('inspect.getmembers', side_effect=mock_inspect_getmembers) - @mock.patch('pkgutil.iter_modules', side_effect=lambda x: [(None, MOCKED_COMMAND_LOADER_MOD, None)]) - @mock.patch('azure.cli.core.commands._load_command_loader', side_effect=mock_load_command_loader) - def test_load_from_help_json(self, mocked_load, mocked_pkg_util, mocked_getmembers): - # setup help.py, help.yaml and help.json - self.set_help_py() - path = self.set_help_yaml() # either (yaml or json) path should work. As both files are in the same temp dir. - self.set_help_json() - create_invoker_and_load_cmds_and_args(self.test_cli) - - # mock logic in core._help_loaders for retrieving yaml file from loader path. - expected_arg = self.test_cli.invocation.commands_loader.cmd_to_loader_map['test alpha'][0].__class__ - with mock.patch('inspect.getfile', side_effect=get_mocked_inspect_getfile(expected_arg, path)): - group_help_obj = next((help for help in get_all_help(self.test_cli) if help.command == "test"), None) - command_help_obj = next((help for help in get_all_help(self.test_cli) if help.command == "test alpha"), - None) - - # Test that group and command help are successfully displayed. - with self.assertRaises(SystemExit): - self.test_cli.invoke(["test", "-h"]) - with self.assertRaises(SystemExit): - self.test_cli.invoke(["test", "alpha", "-h"]) - - # Test group help - self.assertIsNotNone(group_help_obj) - self.assertEqual(group_help_obj.short_summary, "Group json summary.") - self.assertEqual(group_help_obj.long_summary, "Group json description. A.K.A long description.") - self.assertEqual(group_help_obj.links[0], {"title": "Azure Json Test Docs", - "url": "https://docs.microsoft.com/en-us/azure/test"}) - self.assertEqual(group_help_obj.links[1], {"url": "https://aka.ms/just-a-url"}) - - # Test command help - self.assertIsNotNone(command_help_obj) - self.assertEqual(command_help_obj.short_summary, "Command json summary.") - self.assertEqual(command_help_obj.long_summary, "Command json description. A.K.A long description.") - self.assertEqual(command_help_obj.links[0], {"title": "Azure Json Test Alpha Docs", - "url": "https://docs.microsoft.com/en-us/azure/test/alpha"}) - self.assertEqual(command_help_obj.links[1], {"url": "https://aka.ms/just-a-long-url"}) - - # test that parameters and help are loaded from command function docstring, argument registry help and help.yaml - obj_param_dict = {param.name: param for param in command_help_obj.parameters} - param_name_set = {"--arg1 -a", "--arg2 -b", "--arg3", "ARG4"} - self.assertTrue(set(obj_param_dict.keys()).issuperset(param_name_set)) - - self.assertEqual(obj_param_dict["--arg3"].short_summary, "Arg 3's json summary.") - self.assertEqual(obj_param_dict["--arg3"].long_summary, "A truly true description of this parameter.") - self.assertEqual(obj_param_dict["--arg3"].value_sources[0], {"string": "Number range: 0 to 10"}) - self.assertEqual(obj_param_dict["--arg3"].value_sources[1]['link'], - {"url": "https://www.foo-json.com", "title": "foo-json"}) - self.assertEqual(obj_param_dict["--arg3"].value_sources[2]['link'], - {"command": "az test show", "title": "Show test details. Json file"}) - - self.assertEqual(command_help_obj.examples[0].short_summary, "A simple example from json") - self.assertEqual(command_help_obj.examples[0].long_summary, "More detail on the simple example.") - self.assertEqual(command_help_obj.examples[0].command, "az test alpha --arg1 alpha --arg2 beta --arg3 chi") - self.assertEqual(command_help_obj.examples[0].supported_profiles, "2018-03-01-hybrid, latest") - - # validate other parameters, which have help from help.py and help.yamls - self.assertEqual(obj_param_dict["--arg1 -a"].short_summary, "A short summary.") - self.assertEqual(obj_param_dict["--arg2 -b"].short_summary, "Arg 2's summary.") - self.assertEqual(obj_param_dict["ARG4"].short_summary, "Arg4's summary, yaml. Positional arg, not required.") - # arg2's help from help.yaml still preserved. - self.assertEqual(obj_param_dict["--arg2 -b"].long_summary, "A true description of this parameter.") - self.assertEqual(obj_param_dict["--arg2 -b"].value_sources[0], {"string": "Number range: -5.0 to 5.0"}) - self.assertEqual(obj_param_dict["--arg2 -b"].value_sources[1]['link'], {"url": "https://www.foo.com", - "title": "foo"}) - self.assertEqual(obj_param_dict["--arg2 -b"].value_sources[2]['link'], {"command": "az test show", - "title": "Show test details"}) - - # create a temporary file in the temp dir. Return the path of the file. - def _create_new_temp_file(self, data, suffix=""): - with tempfile.NamedTemporaryFile(mode='w', dir=self._tempdirName, delete=False, suffix=suffix) as f: - f.write(data) - return f.name - class TestHelpSupportedProfiles(unittest.TestCase): def setUp(self): diff --git a/src/azure-cli-core/azure/cli/core/tests/test_help_loaders.py b/src/azure-cli-core/azure/cli/core/tests/test_help_loaders.py deleted file mode 100644 index a76e7454bca..00000000000 --- a/src/azure-cli-core/azure/cli/core/tests/test_help_loaders.py +++ /dev/null @@ -1,141 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# 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 azure.cli.core import AzCommandsLoader -from azure.cli.core._help_loaders import HelpLoaderV1 - - -# region TestCommandLoader -class TestCommandLoader(AzCommandsLoader): - - def __init__(self, cli_ctx=None): - from azure.cli.core.commands import CliCommandType - compute_custom = CliCommandType( - operations_tmpl='{}#{{}}'.format(__name__), - ) - super(TestCommandLoader, self).__init__(cli_ctx=cli_ctx, - custom_command_type=compute_custom) - self.cmd_to_loader_map = {} - - def load_command_table(self, args): - with self.command_group('test') as g: - g.custom_command('alpha', 'dummy_handler') - - return self.command_table - - def load_arguments(self, command): - with self.argument_context('test') as c: - c.argument('arg1', options_list=['--arg1', '-a']) - c.argument('arg2', options_list=['--arg2', '-b'], help="Help From code.") - with self.argument_context('test alpha') as c: - c.positional('arg4', metavar="ARG4") - self._update_command_definitions() # pylint: disable=protected-access - - -def dummy_handler(arg1, arg2=None, arg3=None, arg4=None): - """ - Short summary here. Long summary here. Still long summary. - :param arg1: arg1's docstring help text - :param arg2: arg2's docstring help text - :param arg3: arg3's docstring help text - :param arg4: arg4's docstring help text - """ - pass - - -COMMAND_LOADER_CLS = TestCommandLoader - -# region Test Help Loader - - -class JsonLoaderMixin(object): - """A class containing helper methods for Json Loaders.""" - - # get the list of json help file names for the command or group - @staticmethod - def _get_json_help_files_list(nouns, cmd_loader_map_ref): - import inspect - import os - - command_nouns = " ".join(nouns) - # if command in map, get the loader. Path of loader is path of helpfile. - ldr_or_none = cmd_loader_map_ref.get(command_nouns, [None])[0] - if ldr_or_none: - loaders = {ldr_or_none} - else: - loaders = set() - - # otherwise likely a group, try to find all command loaders under group as the group help could be defined - # in either. - if not loaders: - for cmd_name, cmd_ldr in cmd_loader_map_ref.items(): - # if first word in loader name is the group, this is a command in the command group - if cmd_name.startswith(command_nouns + " "): - loaders.add(cmd_ldr[0]) - - results = [] - if loaders: - for loader in loaders: - 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("help.json"): - help_file_path = os.path.join(dir_name, file) - results.append(help_file_path) - return results - - @staticmethod - def _parse_json_from_string(text, help_file_path): - import os - import json - - dir_name, base_name = os.path.split(help_file_path) - pretty_file_path = os.path.join(os.path.basename(dir_name), base_name) - - if not text: - raise CLIError("No content passed for {}.".format(pretty_file_path)) - - try: - return json.loads(text) - except ValueError as e: - raise CLIError("Error parsing {}:\n\n{}".format(pretty_file_path, e)) - - -# test Help Loader, loads from help.json -class DummyHelpLoader(HelpLoaderV1, JsonLoaderMixin): - # This loader has different keys in the data object. Except for "arguments" and "examples". - core_attrs_to_keys = [("short_summary", "short"), ("long_summary", "long")] - body_attrs_to_keys = core_attrs_to_keys + [("links", "hyper-links")] - param_attrs_to_keys = core_attrs_to_keys + [("value_sources", "sources")] - - @property - def version(self): - return 2 - - def get_noun_help_file_names(self, nouns): - cmd_loader_map_ref = self.help_ctx.cli_ctx.invocation.commands_loader.cmd_to_loader_map - return self._get_json_help_files_list(nouns, cmd_loader_map_ref) - - def update_file_contents(self, file_contents): - for file_name in file_contents: - if file_name not in self._file_content_dict: - data_dict = {file_name: self._parse_json_from_string(file_contents[file_name], file_name)} - self._file_content_dict.update(data_dict) - - def load_entry_data(self, help_obj, parser): - prog = parser.prog if hasattr(parser, "prog") else parser._prog_prefix # pylint: disable=protected-access - command_nouns = prog.split()[1:] - cmd_loader_map_ref = self.help_ctx.cli_ctx.invocation.commands_loader.cmd_to_loader_map - - files_list = self._get_json_help_files_list(command_nouns, cmd_loader_map_ref) - data_list = [self._file_content_dict[name] for name in files_list] - - self._entry_data = self._get_entry_data(help_obj.command, data_list) - - def load_help_body(self, help_obj): - self._update_obj_from_data_dict(help_obj, self._entry_data, self.body_attrs_to_keys) From 0c609e06032ad9a1cfc8e81b1e95bfd1614edfa7 Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Wed, 26 Jun 2019 12:26:15 -0700 Subject: [PATCH 2/4] style fixes --- src/azure-cli-core/azure/cli/core/_help.py | 3 ++- src/azure-cli-core/azure/cli/core/file_util.py | 1 + src/azure-cli-core/azure/cli/core/tests/test_help.py | 6 ++++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/azure-cli-core/azure/cli/core/_help.py b/src/azure-cli-core/azure/cli/core/_help.py index dd56a04a786..8f3ba65f722 100644 --- a/src/azure-cli-core/azure/cli/core/_help.py +++ b/src/azure-cli-core/azure/cli/core/_help.py @@ -140,6 +140,7 @@ def __init__(self, group_list): self.priorities[group] = priority priority += 1 + # override to add support for supported and unsupported profiles class HelpExample(KnackHelpExample): # pylint: disable=too-few-public-methods @@ -147,4 +148,4 @@ def __init__(self, _data): # Old attributes super(HelpExample, self).__init__(_data) self.supported_profiles = _data.get('supported-profiles', None) - self.unsupported_profiles = _data.get('unsupported-profiles', None) \ No newline at end of file + self.unsupported_profiles = _data.get('unsupported-profiles', None) 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 72edf03c760..c540ab2279b 100644 --- a/src/azure-cli-core/azure/cli/core/file_util.py +++ b/src/azure-cli-core/azure/cli/core/file_util.py @@ -13,6 +13,7 @@ logger = get_logger(__name__) + def get_all_help(cli_ctx, skip=True): invoker = cli_ctx.invocation help_ctx = cli_ctx.help_cls(cli_ctx) diff --git a/src/azure-cli-core/azure/cli/core/tests/test_help.py b/src/azure-cli-core/azure/cli/core/tests/test_help.py index be42c61a72d..fd5260b7680 100644 --- a/src/azure-cli-core/azure/cli/core/tests/test_help.py +++ b/src/azure-cli-core/azure/cli/core/tests/test_help.py @@ -41,6 +41,7 @@ def _get_parser_name(parser): # pylint:disable=protected-access return (parser._prog_prefix if hasattr(parser, '_prog_prefix') else parser.prog)[len('az '):] + def create_invoker_and_load_cmds_and_args(cli_ctx): from knack import events from azure.cli.core.commands import register_cache_arguments @@ -76,13 +77,16 @@ def create_invoker_and_load_cmds_and_args(cli_ctx): cli_ctx.raise_event(events.EVENT_INVOKER_POST_CMD_TBL_CREATE, commands_loader=invoker.commands_loader) invoker.parser.load_command_table(invoker.commands_loader) + # Command loader module MOCKED_COMMAND_LOADER_MOD = "test_help" + # mock command loader method so that only the mock command loader can be loaded. def mock_load_command_loader(loader, args, name, prefix): return _load_command_loader(loader, args, name, "azure.cli.core.tests.") + # region TestCommandLoader class TestCommandLoader(AzCommandsLoader): @@ -110,6 +114,7 @@ def load_arguments(self, command): self._update_command_definitions() # pylint: disable=protected-access +# handler for command to test getting help from docstring def dummy_handler(arg1, arg2=None, arg3=None, arg4=None): """ Short summary here. Long summary here. Still long summary. @@ -120,6 +125,7 @@ def dummy_handler(arg1, arg2=None, arg3=None, arg4=None): """ pass + COMMAND_LOADER_CLS = TestCommandLoader # endregion From 39ccc0308621f7f1811b14b4e0c933d161deb980 Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Thu, 27 Jun 2019 10:26:31 -0700 Subject: [PATCH 3/4] Fix line too long --- src/azure-cli-core/azure/cli/core/_help.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/_help.py b/src/azure-cli-core/azure/cli/core/_help.py index 8f3ba65f722..3b694ffa876 100644 --- a/src/azure-cli-core/azure/cli/core/_help.py +++ b/src/azure-cli-core/azure/cli/core/_help.py @@ -5,8 +5,9 @@ from __future__ import print_function -from knack.help import (HelpExample as KnackHelpExample, HelpFile as KnackHelpFile, CommandHelpFile as KnackCommandHelpFile, - CLIHelp, ArgumentGroupRegistry as KnackArgumentGroupRegistry, HelpAuthoringException) +from knack.help import (CLIHelp, HelpAuthoringException, + HelpExample as KnackHelpExample, HelpFile as KnackHelpFile, + CommandHelpFile as KnackCommandHelpFile, ArgumentGroupRegistry as KnackArgumentGroupRegistry) from knack.log import get_logger from azure.cli.core.commands import ExtensionCommandSource From 110fdb204f5f8fb2847adad045e6eb8dcb64c952 Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Thu, 27 Jun 2019 10:35:03 -0700 Subject: [PATCH 4/4] Remove unused function definitions. --- doc/sphinx/azhelpgen/azhelpgen.py | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/doc/sphinx/azhelpgen/azhelpgen.py b/doc/sphinx/azhelpgen/azhelpgen.py index ba29c98a429..68f4520d0e8 100644 --- a/doc/sphinx/azhelpgen/azhelpgen.py +++ b/doc/sphinx/azhelpgen/azhelpgen.py @@ -109,37 +109,11 @@ def run(self): nested_parse_with_titles(self.state, result, node) return node.children + def setup(app): app.add_directive('azhelpgen', AzHelpGenDirective) -def _store_parsers(parser, parser_keys, parser_values, sub_parser_keys, sub_parser_values): - for s in parser.subparsers.values(): - parser_keys.append(_get_parser_name(s)) - parser_values.append(s) - if _is_group(s): - for c in s.choices.values(): - sub_parser_keys.append(_get_parser_name(c)) - sub_parser_values.append(c) - _store_parsers(c, parser_keys, parser_values, sub_parser_keys, sub_parser_values) - def _load_doc_source_map(): with open('azhelpgen/doc_source_map.json') as open_file: return json.load(open_file) - -def _is_group(parser): - return getattr(parser, '_subparsers', None) is not None \ - or getattr(parser, 'choices', None) is not None - -def _get_parser_name(s): - return (s._prog_prefix if hasattr(s, '_prog_prefix') else s.prog)[3:] - - -def _get_populator_commands(param): - commands = [] - for value_source in param.value_sources: - try: - commands.append(value_source["link"]["command"]) - except KeyError: - continue - return commands