From 4de2e7d9d2e44e911439fc501f50d9763868aa3e Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Wed, 10 Oct 2018 18:46:11 -0700 Subject: [PATCH 01/27] Dummy help.yaml file --- .../azure/cli/command_modules/cdn/help.yaml | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/help.yaml diff --git a/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/help.yaml b/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/help.yaml new file mode 100644 index 00000000000..ae818a92e08 --- /dev/null +++ b/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/help.yaml @@ -0,0 +1,181 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +--- +- version: 0 + +- anchors: null + + +- content: ## try removing content key + +- group: + name: cdn + summary: 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: + - name: Azure cdn docs + url: "https://docs.microsoft.com/en-us/azure/cdn/" + - name: cli cdn reference + url: "https://docs.microsoft.com/en-us/cli/azure/cdn?view=azure-cli-latest" + +- group: + name: cdn profile + summary: Manage CDN profiles to define an edge network. + +- command: + name: cdn profile create + summary: Create a new CDN profile. + description: Dummy super long description. As you would expect it is long and has multiple sentences. + links: + - name: create new cdn profile and endpoint + url: "https://docs.microsoft.com/en-us/azure/cdn/cdn-create-new-endpoint" + arguments: + - name: --sku #use + 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. + examples: + - summary: Create a CDN profile using Verizon premium CDN. + description: much longer description that goes into more depth about the example. spans multiple lines + command: > # commands? for multiple step. parser should not enforce rules. + az cdn profile create -g group -n profile --sku Premium_Verizon +- group: + name: cdn endpoint + summary: Manage CDN endpoints. + +- command: + name: cdn endpoint create + summary: Create a named endpoint to connect to a CDN. + examples: + - summary: Create an endpoint to service content for hostname over HTTP or HTTPS. + command: > + az cdn endpoint create -g group -n endpoint --profile-name profile \\ + --origin www.example.com + - summary: 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 + - summary: 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 + + +## arg group form 1 +#- command: +# name: foo create +# summary: foo create command +# +# arg-group: +# name: bar # bar arguments +# arguments: # this is a dummy arg group +# - name: --bar-name +# summary: This is a bar-name. +# - name: --bar-id +# summary: This is a bar-id. +# - name: --bar-sku +# summary: this is a bar-sku +# +# arguments: +# - name: --tag +# summary: This is a tag argument. +# - name: --bag +# summary: This is a bag argument. +# - name: --lag +# summary: This is a lag argument. +# +## arg group from 2 +#- command: +# name: foo create +# summary: foo create command +# arguments: +# - name: --bar-name +# summary: This is a bar-name. +# arg-group: bar +# +# - name: --bar-id +# summary: This is a bar-id. +# arg-group: bar +# +# - name: --bar-sku +# summary: this is a bar-sku +# arg-group: bar +# +# - name: --tag +# summary: This is a tag argument. +# +# - name: --bag +# summary: This is a bag argument. +# +# - name: --lag +# summary: This is a lag argument. +# +## arg group form 2 with anchors and references. +#- command: +# name: foo create +# summary: foo create command +# arguments: +# - name: --bar-name +# summary: This is a bar-name. +# arg-group: &b bar +# +# - name: --bar-id +# summary: This is a bar-id. +# arg-group: *b +# +# - name: --bar-sku +# summary: this is a bar-sku +# arg-group: *b +# +# - name: --tag +# summary: This is a tag argument. +# +# - name: --bag +# summary: This is a bag argument. +# +# - name: --lag +# summary: This is a lag argument. +# +# +## value source for parameters +#- command: +# name: foo create +# summary: foo create command +# arguments: +# - name: --nums +# summary: This is a tag argument. +# value-source: Nums must range from 1 to 50 +# +# - name: --floats +# summary: This is a bag argument. +# value-source: +# - Values range from -5.0 to 5.0 +# - links: +# - name: Float range specs +# url: "https://float-range-specs.com" +# +# - name: --lag +# summary: This is a lag argument. +# +# - name: --date +# summary: Date +# value-source: Date should be in format DD-MM-YY. E.g. 01-01-01 + + + +# make this a more concise richer document.... +# +# value-source should be a string with their type. type can be string, link or command. helps doc team. +# show different options in powerpoint for arg group and links. we want consistent way to hyperlink ideallly... +# figuring out what to do with value source +# best way for multi step example / best way for arg group / best way for value source / link specification for parameters. value source could be date time. +# az parse yaml \ No newline at end of file From 79b56631afb0875cf8ed1513b5c3342b5b6c9b5e Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Thu, 11 Oct 2018 13:56:23 -0700 Subject: [PATCH 02/27] Parse command for testing yaml parsing Parser can output contents as yaml updated help.yaml test file with some comments. --- .../azure/cli/command_modules/cdn/help.yaml | 50 +++++++++------ .../azure-cli-parser/HISTORY.rst | 9 +++ .../azure-cli-parser/README.rst | 3 + .../azure-cli-parser/azure/__init__.py | 6 ++ .../azure-cli-parser/azure/cli/__init__.py | 6 ++ .../azure/cli/command_modules/__init__.py | 6 ++ .../cli/command_modules/parser/__init__.py | 31 ++++++++++ .../command_modules/parser/_client_factory.py | 10 +++ .../azure/cli/command_modules/parser/_help.py | 18 ++++++ .../cli/command_modules/parser/_params.py | 15 +++++ .../cli/command_modules/parser/commands.py | 14 +++++ .../cli/command_modules/parser/custom.py | 32 ++++++++++ .../command_modules/parser/tests/__init__.py | 4 ++ .../parser/tests/test_example.py | 25 ++++++++ .../azure-cli-parser/azure_bdist_wheel.py | 54 ++++++++++++++++ .../azure-cli-parser/setup.cfg | 3 + src/command_modules/azure-cli-parser/setup.py | 61 +++++++++++++++++++ 17 files changed, 330 insertions(+), 17 deletions(-) create mode 100644 src/command_modules/azure-cli-parser/HISTORY.rst create mode 100644 src/command_modules/azure-cli-parser/README.rst create mode 100644 src/command_modules/azure-cli-parser/azure/__init__.py create mode 100644 src/command_modules/azure-cli-parser/azure/cli/__init__.py create mode 100644 src/command_modules/azure-cli-parser/azure/cli/command_modules/__init__.py create mode 100644 src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/__init__.py create mode 100644 src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/_client_factory.py create mode 100644 src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/_help.py create mode 100644 src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/_params.py create mode 100644 src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/commands.py create mode 100644 src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/custom.py create mode 100644 src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/tests/__init__.py create mode 100644 src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/tests/test_example.py create mode 100644 src/command_modules/azure-cli-parser/azure_bdist_wheel.py create mode 100644 src/command_modules/azure-cli-parser/setup.cfg create mode 100644 src/command_modules/azure-cli-parser/setup.py diff --git a/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/help.yaml b/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/help.yaml index ae818a92e08..92e65058624 100644 --- a/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/help.yaml +++ b/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/help.yaml @@ -2,13 +2,10 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- ---- -- version: 0 -- anchors: null +version: 0 - -- content: ## try removing content key +content: ## try removing content key - group: name: cdn @@ -43,28 +40,37 @@ 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-source: + - string: + Values range from -5.0 to 5.0 + - link: + - url: https://www.foo.com + - text: foo + - link: + - command: az sku list + - text: Get skus + examples: - summary: Create a CDN profile using Verizon premium CDN. description: much longer description that goes into more depth about the example. spans multiple lines command: > # commands? for multiple step. parser should not enforce rules. az cdn profile create -g group -n profile --sku Premium_Verizon -- group: - name: cdn endpoint - summary: Manage CDN endpoints. - command: name: cdn endpoint create - summary: Create a named endpoint to connect to a CDN. + summary: NEW AND IMPROVED!!!!! Create a named endpoint to connect to a CDN. examples: - - summary: Create an endpoint to service content for hostname over HTTP or HTTPS. + - summary: FOOO, cause why not. Create an endpoint to service content for hostname over HTTP or HTTPS. command: > az cdn endpoint create -g group -n endpoint --profile-name profile \\ --origin www.example.com - - summary: Create an endpoint with a custom domain origin with HTTP and HTTPS ports. + min_profile: latest + - summary: Bar bar. 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 - - summary: Create an endpoint with a custom domain with compression and only HTTPS. + max_profile: 2017-03-09-profile + - summary: Baz.... 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 @@ -158,17 +164,23 @@ # - name: --floats # summary: This is a bag argument. # value-source: -# - Values range from -5.0 to 5.0 -# - links: -# - name: Float range specs +# - string: +# Values range from -5.0 to 5.0 +# - link: +# - url / command +# - text +# - anchor / hypertext / text: Float range specs # url: "https://float-range-specs.com" # # - name: --lag # summary: This is a lag argument. +# value-source: +# command: az foo lag # # - name: --date # summary: Date -# value-source: Date should be in format DD-MM-YY. E.g. 01-01-01 +# value-source: +# - string: Date should be in format DD-MM-YY. E.g. 01-01-01 @@ -178,4 +190,8 @@ # show different options in powerpoint for arg group and links. we want consistent way to hyperlink ideallly... # figuring out what to do with value source # best way for multi step example / best way for arg group / best way for value source / link specification for parameters. value source could be date time. -# az parse yaml \ No newline at end of file +# az parse yaml + +# note parameters, we want to merge info from code and yaml, as done before. +# +# value-source (replacement or much more), arg_group, examples. \ No newline at end of file diff --git a/src/command_modules/azure-cli-parser/HISTORY.rst b/src/command_modules/azure-cli-parser/HISTORY.rst new file mode 100644 index 00000000000..8420f1f9b0a --- /dev/null +++ b/src/command_modules/azure-cli-parser/HISTORY.rst @@ -0,0 +1,9 @@ +.. :changelog: + +Release History +=============== + +0.0.1 ++++++ + +* Initial release of module. diff --git a/src/command_modules/azure-cli-parser/README.rst b/src/command_modules/azure-cli-parser/README.rst new file mode 100644 index 00000000000..dc708919d13 --- /dev/null +++ b/src/command_modules/azure-cli-parser/README.rst @@ -0,0 +1,3 @@ +Microsoft Azure CLI 'parse' Command Module +================================== + diff --git a/src/command_modules/azure-cli-parser/azure/__init__.py b/src/command_modules/azure-cli-parser/azure/__init__.py new file mode 100644 index 00000000000..73baee1e640 --- /dev/null +++ b/src/command_modules/azure-cli-parser/azure/__init__.py @@ -0,0 +1,6 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +import pkg_resources +pkg_resources.declare_namespace(__name__) diff --git a/src/command_modules/azure-cli-parser/azure/cli/__init__.py b/src/command_modules/azure-cli-parser/azure/cli/__init__.py new file mode 100644 index 00000000000..73baee1e640 --- /dev/null +++ b/src/command_modules/azure-cli-parser/azure/cli/__init__.py @@ -0,0 +1,6 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +import pkg_resources +pkg_resources.declare_namespace(__name__) diff --git a/src/command_modules/azure-cli-parser/azure/cli/command_modules/__init__.py b/src/command_modules/azure-cli-parser/azure/cli/command_modules/__init__.py new file mode 100644 index 00000000000..73baee1e640 --- /dev/null +++ b/src/command_modules/azure-cli-parser/azure/cli/command_modules/__init__.py @@ -0,0 +1,6 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +import pkg_resources +pkg_resources.declare_namespace(__name__) diff --git a/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/__init__.py b/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/__init__.py new file mode 100644 index 00000000000..7e85fbe9cd7 --- /dev/null +++ b/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/__init__.py @@ -0,0 +1,31 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azure.cli.core import AzCommandsLoader + +import azure.cli.command_modules.parser._help # pylint: disable=unused-import + + +class ParserCommandsLoader(AzCommandsLoader): + + def __init__(self, cli_ctx=None): + from azure.cli.core.commands import CliCommandType + example_custom = CliCommandType(operations_tmpl='azure.cli.command_modules.parser.custom#{}') + + super(ParserCommandsLoader, self).__init__(cli_ctx=cli_ctx, + min_profile='2017-03-10-profile', + custom_command_type=example_custom) + + def load_command_table(self, args): + from azure.cli.command_modules.parser.commands import load_command_table + load_command_table(self, args) + return self.command_table + + def load_arguments(self, command): + from azure.cli.command_modules.parser._params import load_arguments + load_arguments(self, command) + + +COMMAND_LOADER_CLS = ParserCommandsLoader diff --git a/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/_client_factory.py b/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/_client_factory.py new file mode 100644 index 00000000000..b849ab75bfe --- /dev/null +++ b/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/_client_factory.py @@ -0,0 +1,10 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +# def cf_example(cli_ctx, _): +# from azure.cli.core.commands.client_factory import get_mgmt_service_client +# from azure.mgmt.example import ExampleManagementClient +# return get_mgmt_service_client(cli_ctx, ExampleManagementClient).examples diff --git a/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/_help.py b/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/_help.py new file mode 100644 index 00000000000..44467007438 --- /dev/null +++ b/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/_help.py @@ -0,0 +1,18 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from knack.help_files import helps + +# pylint: disable=line-too-long + +helps['parser'] = """ + type: group + short-summary: Parser Commands. +""" + +helps['parser yaml'] = """ + type: command + short-summary: Parse yaml. +""" \ No newline at end of file diff --git a/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/_params.py b/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/_params.py new file mode 100644 index 00000000000..abd6696ca14 --- /dev/null +++ b/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/_params.py @@ -0,0 +1,15 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +# pylint: disable=line-too-long + + +def load_arguments(self, _): + + with self.argument_context('parser') as c: + c.argument('file_name', options_list=['--file-name', '-f'] , help='The name of the file to parse.') + + with self.argument_context('parser yaml') as c: + c.argument('as_yaml', action='store_true', help='Output returned as yaml.') \ No newline at end of file diff --git a/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/commands.py b/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/commands.py new file mode 100644 index 00000000000..822a4c8da9b --- /dev/null +++ b/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/commands.py @@ -0,0 +1,14 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +#pylint: disable=line-too-long + + +def load_command_table(self, _): + + with self.command_group('parser') as g: + g.custom_command('yaml', 'parse_yaml') + + diff --git a/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/custom.py b/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/custom.py new file mode 100644 index 00000000000..0b7172b97e8 --- /dev/null +++ b/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/custom.py @@ -0,0 +1,32 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from knack.log import get_logger +from knack.util import CLIError +import yaml + + +logger = get_logger(__name__) + + +def parse_yaml(file_name, as_yaml=False): + + try: + f = open(file_name) + except IOError as e: + raise CLIError("Error: {}".format(e)) + + contents = f.read() + + try: + contents = yaml.load(contents) + except yaml.YAMLError as e: + raise CLIError("Error: {}".format(e)) + + if as_yaml: + print(yaml.dump(contents, default_flow_style=False)) + return None + + return contents diff --git a/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/tests/__init__.py b/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/tests/__init__.py new file mode 100644 index 00000000000..34913fb394d --- /dev/null +++ b/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/tests/__init__.py @@ -0,0 +1,4 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- diff --git a/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/tests/test_example.py b/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/tests/test_example.py new file mode 100644 index 00000000000..2d3c2d5ca7c --- /dev/null +++ b/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/tests/test_example.py @@ -0,0 +1,25 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azure.cli.testsdk import ScenarioTest, ResourceGroupPreparer + + +class ExampleTests(ScenarioTest): + + @ResourceGroupPreparer(name_prefix='cli_test_example') # name_prefix not required, but can be useful + def test_example(self, resource_group): + + # kwargs will already have resource_group with the key 'rg' + self.kwargs = { + 'loc': 'WestUS', + 'name': self.create_random_name(prefix='redis', length=24), + } + + # refer to kwarg keys directly in-line + self.cmd('az example create -n {name} -g {rg} -l {loc}', check=[ + self.check('name', '{name}'), # use kwarg keys within your checks + self.check('resourceGroup', '{rg}'), + self.check('location', '{loc}') + ]) \ No newline at end of file diff --git a/src/command_modules/azure-cli-parser/azure_bdist_wheel.py b/src/command_modules/azure-cli-parser/azure_bdist_wheel.py new file mode 100644 index 00000000000..8a81d1b6177 --- /dev/null +++ b/src/command_modules/azure-cli-parser/azure_bdist_wheel.py @@ -0,0 +1,54 @@ +#------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +#-------------------------------------------------------------------------- + +from distutils import log as logger +import os.path + +from wheel.bdist_wheel import bdist_wheel +class azure_bdist_wheel(bdist_wheel): + """The purpose of this class is to build wheel a little differently than the sdist, + without requiring to build the wheel from the sdist (i.e. you can build the wheel + directly from source). + """ + + description = "Create an Azure wheel distribution" + + user_options = bdist_wheel.user_options + \ + [('azure-namespace-package=', None, + "Name of the deepest nspkg used")] + + def initialize_options(self): + bdist_wheel.initialize_options(self) + self.azure_namespace_package = None + + def finalize_options(self): + bdist_wheel.finalize_options(self) + if self.azure_namespace_package and not self.azure_namespace_package.endswith("-nspkg"): + raise ValueError("azure_namespace_package must finish by -nspkg") + + def run(self): + if not self.distribution.install_requires: + self.distribution.install_requires = [] + self.distribution.install_requires.append( + "{}>=2.0.0".format(self.azure_namespace_package)) + bdist_wheel.run(self) + + def write_record(self, bdist_dir, distinfo_dir): + if self.azure_namespace_package: + # Split and remove last part, assuming it's "nspkg" + subparts = self.azure_namespace_package.split('-')[0:-1] + folder_with_init = [os.path.join(*subparts[0:i+1]) for i in range(len(subparts))] + for azure_sub_package in folder_with_init: + init_file = os.path.join(bdist_dir, azure_sub_package, '__init__.py') + if os.path.isfile(init_file): + logger.info("manually remove {} while building the wheel".format(init_file)) + os.remove(init_file) + else: + raise ValueError("Unable to find {}. Are you sure of your namespace package?".format(init_file)) + bdist_wheel.write_record(self, bdist_dir, distinfo_dir) +cmdclass = { + 'bdist_wheel': azure_bdist_wheel, +} diff --git a/src/command_modules/azure-cli-parser/setup.cfg b/src/command_modules/azure-cli-parser/setup.cfg new file mode 100644 index 00000000000..3326c62a76e --- /dev/null +++ b/src/command_modules/azure-cli-parser/setup.cfg @@ -0,0 +1,3 @@ +[bdist_wheel] +universal=1 +azure-namespace-package=azure-cli-command_modules-nspkg diff --git a/src/command_modules/azure-cli-parser/setup.py b/src/command_modules/azure-cli-parser/setup.py new file mode 100644 index 00000000000..c05b356f2b7 --- /dev/null +++ b/src/command_modules/azure-cli-parser/setup.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from codecs import open +from setuptools import setup + +try: + from azure_bdist_wheel import cmdclass +except ImportError: + from distutils import log as logger + logger.warn("Wheel is not available, disabling bdist_wheel hook") + cmdclass = {} + +VERSION = "0.0.1" + +# The full list of classifiers is available at +# https://pypi.python.org/pypi?%3Aaction=list_classifiers +CLASSIFIERS = [ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'Intended Audience :: System Administrators', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', +] + +DEPENDENCIES = [ + 'azure-cli-core', +] + +with open('README.rst', 'r', encoding='utf-8') as f: + README = f.read() +with open('HISTORY.rst', 'r', encoding='utf-8') as f: + HISTORY = f.read() + +setup( + name='azure-cli-parser', + version=VERSION, + description='Microsoft Azure Command-Line Tools Parser Command Module', + long_description=README + '\n\n' + HISTORY, + license='MIT', + author='Microsoft Corporation', + author_email='azpycli@microsoft.com', + url='https://github.com/Azure/azure-cli', + classifiers=CLASSIFIERS, + packages=[ + 'azure', + 'azure.cli', + 'azure.cli.command_modules', + 'azure.cli.command_modules.parser', + ], + install_requires=DEPENDENCIES, + cmdclass=cmdclass +) From 4226773636cf5111f634193b5b9cd01b3b029f87 Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Mon, 15 Oct 2018 23:14:47 -0700 Subject: [PATCH 03/27] Added logic to parse help.yaml file and update help data objects. More helpfile updates. --- src/azure-cli-core/azure/cli/core/_help.py | 209 +++++++++++++++++- .../azure/cli/command_modules/cdn/help.yaml | 64 +++--- 2 files changed, 247 insertions(+), 26 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/_help.py b/src/azure-cli-core/azure/cli/core/_help.py index b701a4ca8f4..2c500a5b9d4 100644 --- a/src/azure-cli-core/azure/cli/core/_help.py +++ b/src/azure-cli-core/azure/cli/core/_help.py @@ -8,9 +8,11 @@ from knack.help import (HelpExample, HelpFile as KnackHelpFile, CommandHelpFile as KnackCommandHelpFile, + GroupHelpFile as KnackGroupHelpFile, CLIHelp, ArgumentGroupRegistry as KnackArgumentGroupRegistry) from knack.log import get_logger +from knack.util import CLIError from azure.cli.core.commands import ExtensionCommandSource @@ -52,6 +54,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 @@ -83,9 +86,59 @@ 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) + @staticmethod + def update_parser_with_help_file(nouns, cmd_loader_map, parser, is_group): + import inspect + import os + + command_nouns = " ".join(nouns) + loader = None + if is_group: + for k, v in cmd_loader_map.items(): + if k.startswith(command_nouns): + loader = v[0] + break + else: + loader = cmd_loader_map.get(command_nouns, [None])[0] + + if loader: + loader_file_path = inspect.getfile(loader.__class__) + dir = os.path.dirname(loader_file_path) + files = os.listdir(dir) + for file in files: + if file.endswith(".yaml") or file.endswith(".yml"): + help_file_path = os.path.join(dir, file) + with open(help_file_path, "r") as f: + text = f.read() + data = KnackHelpFile._load_help_file_from_string(text) + if isinstance(data, dict): + parser.help_file_data = data + return + + def show_help(self, cli_name, nouns, parser, is_group): + cmd_loader_map_ref = self.cli_ctx.invocation.commands_loader.cmd_to_loader_map + self.update_parser_with_help_file(nouns, cmd_loader_map_ref, parser, is_group) + super(AzCliHelp, self).show_help(cli_name, nouns, parser, is_group) + class CliHelpFile(KnackHelpFile): + GROUP_TYPE = "group" + COMMAND_TYPE = "command" + CONTENT_TYPES = [COMMAND_TYPE, GROUP_TYPE] + + def load(self, options): + + if hasattr(options, "help_file_data"): + data = self._load_from_parsed_yaml(options.help_file_data) + if "content" not in data: + self._load_from_data(data) + return + + # if unable to load data from parsed yaml, call superclass' load method + super(CliHelpFile, self).load(options) + + def _should_include_example(self, ex): min_profile = ex.get('min_profile') max_profile = ex.get('max_profile') @@ -108,12 +161,166 @@ def _load_from_data(self, data): if self._should_include_example(d): self.examples.append(HelpExample(d)) + # get data object from parsed yaml + def _load_from_parsed_yaml(self, data): + content = data.get("content") + info_type = None + info = None + new_data = {} + + for elem in content: + for key, value in elem.items(): + # find the command / group's help text + if value.get("name") and value.get("name") == self.command: + info_type = key + info = value + break + if info: + break + + # if a new command not found return old data object + if not info: + return data + + new_data["type"] = info_type + + if "summary" in info: + new_data["short-summary"] = info["summary"] + + if "description" in info: + new_data["long-summary"] = info["description"] + + if "examples" in info: + new_data["detailed_examples"] = info["examples"] + reg_examples = [] + for item in new_data["detailed_examples"]: + ex = {} + ex["name"] = item.get("summary", "") + ex["text"] = item.get("description", "") + ex["text"] = "{}\n{}".format(ex["text"], item.get("command", "")) if ex["text"] else item.get("command", "") + ex["min_profile"] = item.get('min_profile') + ex["max_profile"] = item.get('max_profile') + reg_examples.append(ex) + new_data["examples"] = reg_examples + + + if "links" in info: + text = self.get_links_as_text(info["links"]) + new_data["links"] = info["links"] + new_data["long-summary"] = "{}\n{}".format(new_data["long-summary"], text) \ + if new_data.get("long-summary") else text + + if "arguments" in info: + new_data["parameters"] = [] + for arg_info in info["arguments"]: + new_data["parameters"].append(self._get_parameter_info(arg_info)) + + return new_data + + def get_links_as_text(self, links): + text = "" + for link in links: + if "name" in link and "url" in link: + text += "- {}: {}.\n".format(link["name"], link["url"]) + elif "url" in link: + text += "- {}.\n".format(link["url"]) + return text + + def _get_parameter_info(self, arg_info): + params = {} + + # only update if new information + if "name" in arg_info: + params["name"] = arg_info["name"] + + if "summary" in arg_info: + params["short-summary"] = arg_info["summary"] + + if "description" in arg_info: + params["long-summary"] = arg_info["description"] + + if "value-source" in arg_info: + value_source = [] + for item in arg_info["value-source"]: + if "string" in item: + value_source.append(item["string"]) + elif "link" in item: + link_text = "{}".format(item["link"].get("text", "")) + if "command" in item["link"]: + link_text = "{} command: {} ".format(link_text, item["link"]["command"]) + if "url" in item["link"]: + link_text = "{} info: {} ".format(link_text, item["link"]["url"]) + value_source.append(link_text.strip()) + params["populator-commands"] = value_source + + return params + + + +class CliGroupHelpFile(KnackGroupHelpFile, CliHelpFile): + + def __init__(self, help_ctx, delimiters, parser): + + # try to update internal parsers' help_file_data + try: + if parser.choices and parser.help_file_data: + for options in parser.choices.values(): + options.help_file_data = parser.help_file_data + except AttributeError: + pass + + super(CliGroupHelpFile, self).__init__(help_ctx, delimiters, parser) + 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) + 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' + + # Todo: is this necessary? This has exactly the same behavior as superclass. + def _load_from_data(self, data): + + def _params_equal(data, param): + for name in param.name_source: + return data.get("name") == name.lstrip("-") + + + + 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 _params_equal(n, param)), None) + if loaded_param: + loaded_param["name"] = param.name + param.update_from_data(loaded_param) + loaded_params.append(param) + + self.parameters = loaded_params class ArgumentGroupRegistry(KnackArgumentGroupRegistry): # pylint: disable=too-few-public-methods diff --git a/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/help.yaml b/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/help.yaml index 92e65058624..d6823e82e2a 100644 --- a/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/help.yaml +++ b/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/help.yaml @@ -3,9 +3,9 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -version: 0 +version: 1 -content: ## try removing content key +content: - group: name: cdn @@ -18,59 +18,73 @@ content: ## try removing content key url: "https://docs.microsoft.com/en-us/azure/cdn/" - name: 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: Manage CDN profiles to define an edge network. + summary: 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. -- command: + 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: # or group name: cdn profile create - summary: Create a new CDN profile. - description: Dummy super long description. As you would expect it is long and has multiple sentences. + 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: - - name: create new cdn profile and endpoint + - name: NEW! create new cdn profile and endpoint url: "https://docs.microsoft.com/en-us/azure/cdn/cdn-create-new-endpoint" arguments: - - name: --sku #use + - name: name # or --name ? + summary: NEW! Cool Name of the CDN profile. + + - name: sku # or --sku summary: > The pricing tier (defines a CDN provider, feature list and rate) of the CDN profile. Defaults to Standard_Akamai. - description: > + description: > # NEW-ish? 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-source: + value-source: # NEW - ish - string: Values range from -5.0 to 5.0 - link: - - url: https://www.foo.com - - text: foo + url: https://www.foo.com + text: foo - link: - - command: az sku list - - text: Get skus - - examples: - - summary: Create a CDN profile using Verizon premium CDN. - description: much longer description that goes into more depth about the example. spans multiple lines - command: > # commands? for multiple step. parser should not enforce rules. - az cdn profile create -g group -n profile --sku Premium_Verizon + url: https://docs.microsoft.com/en-us/azure/cdn/cdn-features + 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: FOOO, cause why not. Create an endpoint to service content for hostname over HTTP or HTTPS. + - 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: Bar bar. Create an endpoint with a custom domain origin with HTTP and HTTPS ports. + - 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 - max_profile: 2017-03-09-profile - - summary: Baz.... Create an endpoint with a custom domain with compression and only HTTPS. + 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 From 0b73f6c6f6def7bd8304f36effa63d1bd440e807 Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Tue, 16 Oct 2018 14:22:19 -0700 Subject: [PATCH 04/27] _help.py now raises error if it fails to parse help.yaml or help.yaml is empty --- src/azure-cli-core/azure/cli/core/_help.py | 23 ++- .../azure/cli/command_modules/cdn/help.yaml | 133 +----------------- 2 files changed, 26 insertions(+), 130 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/_help.py b/src/azure-cli-core/azure/cli/core/_help.py index 2c500a5b9d4..71920dd98df 100644 --- a/src/azure-cli-core/azure/cli/core/_help.py +++ b/src/azure-cli-core/azure/cli/core/_help.py @@ -91,6 +91,22 @@ def update_parser_with_help_file(nouns, cmd_loader_map, parser, is_group): import inspect import os + def _parse_yaml_from_string(text, help_file_path): + import yaml + + dir = os.path.dirname(help_file_path) + base_name = os.path.basename(help_file_path) + + pretty_file_path = os.path.join(os.path.basename(dir), 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) loader = None if is_group: @@ -110,10 +126,9 @@ def update_parser_with_help_file(nouns, cmd_loader_map, parser, is_group): help_file_path = os.path.join(dir, file) with open(help_file_path, "r") as f: text = f.read() - data = KnackHelpFile._load_help_file_from_string(text) - if isinstance(data, dict): - parser.help_file_data = data - return + data = _parse_yaml_from_string(text, help_file_path) + parser.help_file_data = data + return def show_help(self, cli_name, nouns, parser, is_group): cmd_loader_map_ref = self.cli_ctx.invocation.commands_loader.cmd_to_loader_map diff --git a/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/help.yaml b/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/help.yaml index d6823e82e2a..ea043869bb0 100644 --- a/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/help.yaml +++ b/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/help.yaml @@ -3,6 +3,8 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +# Test helpfile! + version: 1 content: @@ -33,7 +35,7 @@ content: command: > # commands? for multiple step. parser should not enforce rules. az cdn profile create -g group -n profile --sku Premium_Verizon -- command: # or group +- 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. @@ -41,18 +43,18 @@ content: - name: NEW! create new cdn profile and endpoint url: "https://docs.microsoft.com/en-us/azure/cdn/cdn-create-new-endpoint" arguments: - - name: name # or --name ? + - name: name summary: NEW! Cool Name of the CDN profile. - - name: sku # or --sku + - name: sku summary: > The pricing tier (defines a CDN provider, feature list and rate) of the CDN profile. Defaults to Standard_Akamai. - description: > # NEW-ish? + 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-source: # NEW - ish + value-source: # UPDATED - string: Values range from -5.0 to 5.0 - link: @@ -88,124 +90,3 @@ content: command: > az cdn endpoint create -g group -n endpoint --profile-name profile \\ --origin www.example.com --no-http --enable-compression - - -## arg group form 1 -#- command: -# name: foo create -# summary: foo create command -# -# arg-group: -# name: bar # bar arguments -# arguments: # this is a dummy arg group -# - name: --bar-name -# summary: This is a bar-name. -# - name: --bar-id -# summary: This is a bar-id. -# - name: --bar-sku -# summary: this is a bar-sku -# -# arguments: -# - name: --tag -# summary: This is a tag argument. -# - name: --bag -# summary: This is a bag argument. -# - name: --lag -# summary: This is a lag argument. -# -## arg group from 2 -#- command: -# name: foo create -# summary: foo create command -# arguments: -# - name: --bar-name -# summary: This is a bar-name. -# arg-group: bar -# -# - name: --bar-id -# summary: This is a bar-id. -# arg-group: bar -# -# - name: --bar-sku -# summary: this is a bar-sku -# arg-group: bar -# -# - name: --tag -# summary: This is a tag argument. -# -# - name: --bag -# summary: This is a bag argument. -# -# - name: --lag -# summary: This is a lag argument. -# -## arg group form 2 with anchors and references. -#- command: -# name: foo create -# summary: foo create command -# arguments: -# - name: --bar-name -# summary: This is a bar-name. -# arg-group: &b bar -# -# - name: --bar-id -# summary: This is a bar-id. -# arg-group: *b -# -# - name: --bar-sku -# summary: this is a bar-sku -# arg-group: *b -# -# - name: --tag -# summary: This is a tag argument. -# -# - name: --bag -# summary: This is a bag argument. -# -# - name: --lag -# summary: This is a lag argument. -# -# -## value source for parameters -#- command: -# name: foo create -# summary: foo create command -# arguments: -# - name: --nums -# summary: This is a tag argument. -# value-source: Nums must range from 1 to 50 -# -# - name: --floats -# summary: This is a bag argument. -# value-source: -# - string: -# Values range from -5.0 to 5.0 -# - link: -# - url / command -# - text -# - anchor / hypertext / text: Float range specs -# url: "https://float-range-specs.com" -# -# - name: --lag -# summary: This is a lag argument. -# value-source: -# command: az foo lag -# -# - name: --date -# summary: Date -# value-source: -# - string: Date should be in format DD-MM-YY. E.g. 01-01-01 - - - -# make this a more concise richer document.... -# -# value-source should be a string with their type. type can be string, link or command. helps doc team. -# show different options in powerpoint for arg group and links. we want consistent way to hyperlink ideallly... -# figuring out what to do with value source -# best way for multi step example / best way for arg group / best way for value source / link specification for parameters. value source could be date time. -# az parse yaml - -# note parameters, we want to merge info from code and yaml, as done before. -# -# value-source (replacement or much more), arg_group, examples. \ No newline at end of file From 04a49016ebd4a8f09441d3d2dd90459f3b55701a Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Wed, 17 Oct 2018 16:22:31 -0700 Subject: [PATCH 05/27] updated _help.py: CLI now prints extension messages. Removed parser module. --- .../azure-cli-parser/HISTORY.rst | 9 --- .../azure-cli-parser/README.rst | 3 - .../azure-cli-parser/azure/__init__.py | 6 -- .../azure-cli-parser/azure/cli/__init__.py | 6 -- .../azure/cli/command_modules/__init__.py | 6 -- .../cli/command_modules/parser/__init__.py | 31 ---------- .../command_modules/parser/_client_factory.py | 10 --- .../azure/cli/command_modules/parser/_help.py | 18 ------ .../cli/command_modules/parser/_params.py | 15 ----- .../cli/command_modules/parser/commands.py | 14 ----- .../cli/command_modules/parser/custom.py | 32 ---------- .../command_modules/parser/tests/__init__.py | 4 -- .../parser/tests/test_example.py | 25 -------- .../azure-cli-parser/azure_bdist_wheel.py | 54 ---------------- .../azure-cli-parser/setup.cfg | 3 - src/command_modules/azure-cli-parser/setup.py | 61 ------------------- 16 files changed, 297 deletions(-) delete mode 100644 src/command_modules/azure-cli-parser/HISTORY.rst delete mode 100644 src/command_modules/azure-cli-parser/README.rst delete mode 100644 src/command_modules/azure-cli-parser/azure/__init__.py delete mode 100644 src/command_modules/azure-cli-parser/azure/cli/__init__.py delete mode 100644 src/command_modules/azure-cli-parser/azure/cli/command_modules/__init__.py delete mode 100644 src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/__init__.py delete mode 100644 src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/_client_factory.py delete mode 100644 src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/_help.py delete mode 100644 src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/_params.py delete mode 100644 src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/commands.py delete mode 100644 src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/custom.py delete mode 100644 src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/tests/__init__.py delete mode 100644 src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/tests/test_example.py delete mode 100644 src/command_modules/azure-cli-parser/azure_bdist_wheel.py delete mode 100644 src/command_modules/azure-cli-parser/setup.cfg delete mode 100644 src/command_modules/azure-cli-parser/setup.py diff --git a/src/command_modules/azure-cli-parser/HISTORY.rst b/src/command_modules/azure-cli-parser/HISTORY.rst deleted file mode 100644 index 8420f1f9b0a..00000000000 --- a/src/command_modules/azure-cli-parser/HISTORY.rst +++ /dev/null @@ -1,9 +0,0 @@ -.. :changelog: - -Release History -=============== - -0.0.1 -+++++ - -* Initial release of module. diff --git a/src/command_modules/azure-cli-parser/README.rst b/src/command_modules/azure-cli-parser/README.rst deleted file mode 100644 index dc708919d13..00000000000 --- a/src/command_modules/azure-cli-parser/README.rst +++ /dev/null @@ -1,3 +0,0 @@ -Microsoft Azure CLI 'parse' Command Module -================================== - diff --git a/src/command_modules/azure-cli-parser/azure/__init__.py b/src/command_modules/azure-cli-parser/azure/__init__.py deleted file mode 100644 index 73baee1e640..00000000000 --- a/src/command_modules/azure-cli-parser/azure/__init__.py +++ /dev/null @@ -1,6 +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 pkg_resources -pkg_resources.declare_namespace(__name__) diff --git a/src/command_modules/azure-cli-parser/azure/cli/__init__.py b/src/command_modules/azure-cli-parser/azure/cli/__init__.py deleted file mode 100644 index 73baee1e640..00000000000 --- a/src/command_modules/azure-cli-parser/azure/cli/__init__.py +++ /dev/null @@ -1,6 +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 pkg_resources -pkg_resources.declare_namespace(__name__) diff --git a/src/command_modules/azure-cli-parser/azure/cli/command_modules/__init__.py b/src/command_modules/azure-cli-parser/azure/cli/command_modules/__init__.py deleted file mode 100644 index 73baee1e640..00000000000 --- a/src/command_modules/azure-cli-parser/azure/cli/command_modules/__init__.py +++ /dev/null @@ -1,6 +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 pkg_resources -pkg_resources.declare_namespace(__name__) diff --git a/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/__init__.py b/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/__init__.py deleted file mode 100644 index 7e85fbe9cd7..00000000000 --- a/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/__init__.py +++ /dev/null @@ -1,31 +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 azure.cli.core import AzCommandsLoader - -import azure.cli.command_modules.parser._help # pylint: disable=unused-import - - -class ParserCommandsLoader(AzCommandsLoader): - - def __init__(self, cli_ctx=None): - from azure.cli.core.commands import CliCommandType - example_custom = CliCommandType(operations_tmpl='azure.cli.command_modules.parser.custom#{}') - - super(ParserCommandsLoader, self).__init__(cli_ctx=cli_ctx, - min_profile='2017-03-10-profile', - custom_command_type=example_custom) - - def load_command_table(self, args): - from azure.cli.command_modules.parser.commands import load_command_table - load_command_table(self, args) - return self.command_table - - def load_arguments(self, command): - from azure.cli.command_modules.parser._params import load_arguments - load_arguments(self, command) - - -COMMAND_LOADER_CLS = ParserCommandsLoader diff --git a/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/_client_factory.py b/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/_client_factory.py deleted file mode 100644 index b849ab75bfe..00000000000 --- a/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/_client_factory.py +++ /dev/null @@ -1,10 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- - - -# def cf_example(cli_ctx, _): -# from azure.cli.core.commands.client_factory import get_mgmt_service_client -# from azure.mgmt.example import ExampleManagementClient -# return get_mgmt_service_client(cli_ctx, ExampleManagementClient).examples diff --git a/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/_help.py b/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/_help.py deleted file mode 100644 index 44467007438..00000000000 --- a/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/_help.py +++ /dev/null @@ -1,18 +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.help_files import helps - -# pylint: disable=line-too-long - -helps['parser'] = """ - type: group - short-summary: Parser Commands. -""" - -helps['parser yaml'] = """ - type: command - short-summary: Parse yaml. -""" \ No newline at end of file diff --git a/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/_params.py b/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/_params.py deleted file mode 100644 index abd6696ca14..00000000000 --- a/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/_params.py +++ /dev/null @@ -1,15 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- - -# pylint: disable=line-too-long - - -def load_arguments(self, _): - - with self.argument_context('parser') as c: - c.argument('file_name', options_list=['--file-name', '-f'] , help='The name of the file to parse.') - - with self.argument_context('parser yaml') as c: - c.argument('as_yaml', action='store_true', help='Output returned as yaml.') \ No newline at end of file diff --git a/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/commands.py b/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/commands.py deleted file mode 100644 index 822a4c8da9b..00000000000 --- a/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/commands.py +++ /dev/null @@ -1,14 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- - -#pylint: disable=line-too-long - - -def load_command_table(self, _): - - with self.command_group('parser') as g: - g.custom_command('yaml', 'parse_yaml') - - diff --git a/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/custom.py b/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/custom.py deleted file mode 100644 index 0b7172b97e8..00000000000 --- a/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/custom.py +++ /dev/null @@ -1,32 +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.log import get_logger -from knack.util import CLIError -import yaml - - -logger = get_logger(__name__) - - -def parse_yaml(file_name, as_yaml=False): - - try: - f = open(file_name) - except IOError as e: - raise CLIError("Error: {}".format(e)) - - contents = f.read() - - try: - contents = yaml.load(contents) - except yaml.YAMLError as e: - raise CLIError("Error: {}".format(e)) - - if as_yaml: - print(yaml.dump(contents, default_flow_style=False)) - return None - - return contents diff --git a/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/tests/__init__.py b/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/tests/__init__.py deleted file mode 100644 index 34913fb394d..00000000000 --- a/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/tests/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- diff --git a/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/tests/test_example.py b/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/tests/test_example.py deleted file mode 100644 index 2d3c2d5ca7c..00000000000 --- a/src/command_modules/azure-cli-parser/azure/cli/command_modules/parser/tests/test_example.py +++ /dev/null @@ -1,25 +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 azure.cli.testsdk import ScenarioTest, ResourceGroupPreparer - - -class ExampleTests(ScenarioTest): - - @ResourceGroupPreparer(name_prefix='cli_test_example') # name_prefix not required, but can be useful - def test_example(self, resource_group): - - # kwargs will already have resource_group with the key 'rg' - self.kwargs = { - 'loc': 'WestUS', - 'name': self.create_random_name(prefix='redis', length=24), - } - - # refer to kwarg keys directly in-line - self.cmd('az example create -n {name} -g {rg} -l {loc}', check=[ - self.check('name', '{name}'), # use kwarg keys within your checks - self.check('resourceGroup', '{rg}'), - self.check('location', '{loc}') - ]) \ No newline at end of file diff --git a/src/command_modules/azure-cli-parser/azure_bdist_wheel.py b/src/command_modules/azure-cli-parser/azure_bdist_wheel.py deleted file mode 100644 index 8a81d1b6177..00000000000 --- a/src/command_modules/azure-cli-parser/azure_bdist_wheel.py +++ /dev/null @@ -1,54 +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 distutils import log as logger -import os.path - -from wheel.bdist_wheel import bdist_wheel -class azure_bdist_wheel(bdist_wheel): - """The purpose of this class is to build wheel a little differently than the sdist, - without requiring to build the wheel from the sdist (i.e. you can build the wheel - directly from source). - """ - - description = "Create an Azure wheel distribution" - - user_options = bdist_wheel.user_options + \ - [('azure-namespace-package=', None, - "Name of the deepest nspkg used")] - - def initialize_options(self): - bdist_wheel.initialize_options(self) - self.azure_namespace_package = None - - def finalize_options(self): - bdist_wheel.finalize_options(self) - if self.azure_namespace_package and not self.azure_namespace_package.endswith("-nspkg"): - raise ValueError("azure_namespace_package must finish by -nspkg") - - def run(self): - if not self.distribution.install_requires: - self.distribution.install_requires = [] - self.distribution.install_requires.append( - "{}>=2.0.0".format(self.azure_namespace_package)) - bdist_wheel.run(self) - - def write_record(self, bdist_dir, distinfo_dir): - if self.azure_namespace_package: - # Split and remove last part, assuming it's "nspkg" - subparts = self.azure_namespace_package.split('-')[0:-1] - folder_with_init = [os.path.join(*subparts[0:i+1]) for i in range(len(subparts))] - for azure_sub_package in folder_with_init: - init_file = os.path.join(bdist_dir, azure_sub_package, '__init__.py') - if os.path.isfile(init_file): - logger.info("manually remove {} while building the wheel".format(init_file)) - os.remove(init_file) - else: - raise ValueError("Unable to find {}. Are you sure of your namespace package?".format(init_file)) - bdist_wheel.write_record(self, bdist_dir, distinfo_dir) -cmdclass = { - 'bdist_wheel': azure_bdist_wheel, -} diff --git a/src/command_modules/azure-cli-parser/setup.cfg b/src/command_modules/azure-cli-parser/setup.cfg deleted file mode 100644 index 3326c62a76e..00000000000 --- a/src/command_modules/azure-cli-parser/setup.cfg +++ /dev/null @@ -1,3 +0,0 @@ -[bdist_wheel] -universal=1 -azure-namespace-package=azure-cli-command_modules-nspkg diff --git a/src/command_modules/azure-cli-parser/setup.py b/src/command_modules/azure-cli-parser/setup.py deleted file mode 100644 index c05b356f2b7..00000000000 --- a/src/command_modules/azure-cli-parser/setup.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- - -from codecs import open -from setuptools import setup - -try: - from azure_bdist_wheel import cmdclass -except ImportError: - from distutils import log as logger - logger.warn("Wheel is not available, disabling bdist_wheel hook") - cmdclass = {} - -VERSION = "0.0.1" - -# The full list of classifiers is available at -# https://pypi.python.org/pypi?%3Aaction=list_classifiers -CLASSIFIERS = [ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'Intended Audience :: System Administrators', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', -] - -DEPENDENCIES = [ - 'azure-cli-core', -] - -with open('README.rst', 'r', encoding='utf-8') as f: - README = f.read() -with open('HISTORY.rst', 'r', encoding='utf-8') as f: - HISTORY = f.read() - -setup( - name='azure-cli-parser', - version=VERSION, - description='Microsoft Azure Command-Line Tools Parser Command Module', - long_description=README + '\n\n' + HISTORY, - license='MIT', - author='Microsoft Corporation', - author_email='azpycli@microsoft.com', - url='https://github.com/Azure/azure-cli', - classifiers=CLASSIFIERS, - packages=[ - 'azure', - 'azure.cli', - 'azure.cli.command_modules', - 'azure.cli.command_modules.parser', - ], - install_requires=DEPENDENCIES, - cmdclass=cmdclass -) From 5c9562297d29f196fd6f952087f8f79335bc6de1 Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Wed, 17 Oct 2018 16:31:56 -0700 Subject: [PATCH 06/27] _help.py: Addressed pep8 warnings. Made some methods static --- src/azure-cli-core/azure/cli/core/_help.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/_help.py b/src/azure-cli-core/azure/cli/core/_help.py index 71920dd98df..ec43750327a 100644 --- a/src/azure-cli-core/azure/cli/core/_help.py +++ b/src/azure-cli-core/azure/cli/core/_help.py @@ -153,7 +153,6 @@ def load(self, options): # if unable to load data from parsed yaml, call superclass' load method super(CliHelpFile, self).load(options) - def _should_include_example(self, ex): min_profile = ex.get('min_profile') max_profile = ex.get('max_profile') @@ -218,9 +217,8 @@ def _load_from_parsed_yaml(self, data): reg_examples.append(ex) new_data["examples"] = reg_examples - if "links" in info: - text = self.get_links_as_text(info["links"]) + text = self._get_links_as_text(info["links"]) new_data["links"] = info["links"] new_data["long-summary"] = "{}\n{}".format(new_data["long-summary"], text) \ if new_data.get("long-summary") else text @@ -232,7 +230,8 @@ def _load_from_parsed_yaml(self, data): return new_data - def get_links_as_text(self, links): + @staticmethod + def _get_links_as_text(links): text = "" for link in links: if "name" in link and "url" in link: @@ -241,7 +240,8 @@ def get_links_as_text(self, links): text += "- {}.\n".format(link["url"]) return text - def _get_parameter_info(self, arg_info): + @staticmethod + def _get_parameter_info(arg_info): params = {} # only update if new information @@ -254,7 +254,7 @@ def _get_parameter_info(self, arg_info): if "description" in arg_info: params["long-summary"] = arg_info["description"] - if "value-source" in arg_info: + if "value-source" in arg_info: value_source = [] for item in arg_info["value-source"]: if "string" in item: @@ -271,7 +271,6 @@ def _get_parameter_info(self, arg_info): return params - class CliGroupHelpFile(KnackGroupHelpFile, CliHelpFile): def __init__(self, help_ctx, delimiters, parser): @@ -319,8 +318,6 @@ def _params_equal(data, param): for name in param.name_source: return data.get("name") == name.lstrip("-") - - super(CliCommandHelpFile, self)._load_from_data(data) if isinstance(data, str) or not self.parameters or not data.get('parameters'): From 01ebec587de33fa3e15ee29dca254d29182d9010 Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Thu, 18 Oct 2018 13:03:45 -0700 Subject: [PATCH 07/27] changed dir to dir_name as dir is a builtin function. --- src/azure-cli-core/azure/cli/core/_help.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/_help.py b/src/azure-cli-core/azure/cli/core/_help.py index ec43750327a..b0e404a8a43 100644 --- a/src/azure-cli-core/azure/cli/core/_help.py +++ b/src/azure-cli-core/azure/cli/core/_help.py @@ -94,10 +94,10 @@ def update_parser_with_help_file(nouns, cmd_loader_map, parser, is_group): def _parse_yaml_from_string(text, help_file_path): import yaml - dir = os.path.dirname(help_file_path) + dir_name = os.path.dirname(help_file_path) base_name = os.path.basename(help_file_path) - pretty_file_path = os.path.join(os.path.basename(dir), base_name) + pretty_file_path = os.path.join(os.path.basename(dir_name), base_name) try: data = yaml.load(text) @@ -119,11 +119,11 @@ def _parse_yaml_from_string(text, help_file_path): if loader: loader_file_path = inspect.getfile(loader.__class__) - dir = os.path.dirname(loader_file_path) - files = os.listdir(dir) + 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, file) + help_file_path = os.path.join(dir_name, file) with open(help_file_path, "r") as f: text = f.read() data = _parse_yaml_from_string(text, help_file_path) From 86e93bbd1afe1baa93e3b3cd2b1b95fda3aa639c Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Mon, 22 Oct 2018 11:48:53 -0700 Subject: [PATCH 08/27] Added some comments. Link urls have title field instad of name field. Command and Group information formatted differently. Addressed a few other PR comments. --- src/azure-cli-core/azure/cli/core/_help.py | 44 +++++++++---------- .../azure/cli/command_modules/cdn/help.yaml | 9 ++-- 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/_help.py b/src/azure-cli-core/azure/cli/core/_help.py index b0e404a8a43..5a960d2768d 100644 --- a/src/azure-cli-core/azure/cli/core/_help.py +++ b/src/azure-cli-core/azure/cli/core/_help.py @@ -5,11 +5,8 @@ from __future__ import print_function -from knack.help import (HelpExample, - HelpFile as KnackHelpFile, - CommandHelpFile as KnackCommandHelpFile, - GroupHelpFile as KnackGroupHelpFile, - CLIHelp, +from knack.help import (HelpExample, HelpFile as KnackHelpFile, CommandHelpFile as KnackCommandHelpFile, + GroupHelpFile as KnackGroupHelpFile, CLIHelp, HelpParameter, ArgumentGroupRegistry as KnackArgumentGroupRegistry) from knack.log import get_logger from knack.util import CLIError @@ -94,8 +91,7 @@ def update_parser_with_help_file(nouns, cmd_loader_map, parser, is_group): def _parse_yaml_from_string(text, help_file_path): import yaml - dir_name = os.path.dirname(help_file_path) - base_name = os.path.basename(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) @@ -132,25 +128,24 @@ def _parse_yaml_from_string(text, help_file_path): def show_help(self, cli_name, nouns, parser, is_group): cmd_loader_map_ref = self.cli_ctx.invocation.commands_loader.cmd_to_loader_map + # attempt to add the help.yaml file data to the parser self.update_parser_with_help_file(nouns, cmd_loader_map_ref, parser, is_group) super(AzCliHelp, self).show_help(cli_name, nouns, parser, is_group) class CliHelpFile(KnackHelpFile): - GROUP_TYPE = "group" - COMMAND_TYPE = "command" - CONTENT_TYPES = [COMMAND_TYPE, GROUP_TYPE] - def load(self, options): - + # if we successfully transformed the help.yaml data, call appropriate _load_from_data method. + # This is either CliHelpFile._load_from_data() or CliCommandHelpFile._load_from_data() which ultimately + # calls CliHelpFile._load_from_data() if hasattr(options, "help_file_data"): data = self._load_from_parsed_yaml(options.help_file_data) if "content" not in data: self._load_from_data(data) return - # if unable to load data from parsed yaml, call superclass' load method + # if unable to transfrom data from parsed yaml, call superclass' (regular) load method super(CliHelpFile, self).load(options) def _should_include_example(self, ex): @@ -232,13 +227,13 @@ def _load_from_parsed_yaml(self, data): @staticmethod def _get_links_as_text(links): - text = "" + text = [] for link in links: - if "name" in link and "url" in link: - text += "- {}: {}.\n".format(link["name"], link["url"]) - elif "url" in link: - text += "- {}.\n".format(link["url"]) - return text + if "url" in link: + text.append(link["url"]) + msg = "For more information, visit:" + links = ", ".join(text) + return "{} {}".format(msg, links) if links else "" @staticmethod def _get_parameter_info(arg_info): @@ -260,11 +255,11 @@ def _get_parameter_info(arg_info): if "string" in item: value_source.append(item["string"]) elif "link" in item: - link_text = "{}".format(item["link"].get("text", "")) - if "command" in item["link"]: - link_text = "{} command: {} ".format(link_text, item["link"]["command"]) + link_text = "" if "url" in item["link"]: - link_text = "{} info: {} ".format(link_text, item["link"]["url"]) + link_text = "url: {} ".format(item["link"]["url"]) + if "command" in item["link"]: + link_text = "command: {} ".format(item["link"]["command"]) value_source.append(link_text.strip()) params["populator-commands"] = value_source @@ -311,13 +306,14 @@ def __init__(self, help_ctx, delimiters, parser): help_param = next(p for p in self.parameters if p.name == '--help -h') help_param.group_name = 'Global Arguments' - # Todo: is this necessary? This has exactly the same behavior as superclass. def _load_from_data(self, data): def _params_equal(data, param): for name in param.name_source: return data.get("name") == name.lstrip("-") + # load help object from data. Will call CliHelpFile._load_from_data() or KnackCommandHelpFile._load_from_data() + # based on data contents / caller of this method. super(CliCommandHelpFile, self)._load_from_data(data) if isinstance(data, str) or not self.parameters or not data.get('parameters'): diff --git a/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/help.yaml b/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/help.yaml index ea043869bb0..ee0bba32694 100644 --- a/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/help.yaml +++ b/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/help.yaml @@ -16,9 +16,9 @@ content: 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: - - name: Azure cdn docs + - title: Azure cdn docs url: "https://docs.microsoft.com/en-us/azure/cdn/" - - name: cli cdn reference + - 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" @@ -40,7 +40,7 @@ content: 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: - - name: NEW! create new cdn profile and endpoint + - title: NEW! create new cdn profile and endpoint url: "https://docs.microsoft.com/en-us/azure/cdn/cdn-create-new-endpoint" arguments: - name: name @@ -56,12 +56,11 @@ content: There are different CDN skus, you know? Standard ones and even a premium one too. Et cetera. value-source: # UPDATED - string: - Values range from -5.0 to 5.0 + "Number range: -5.0 to 5.0" - link: url: https://www.foo.com text: foo - link: - url: https://docs.microsoft.com/en-us/azure/cdn/cdn-features command: az sku list text: Get skus From 857b004a5f1f6954f3267324ce88cb56e637c54b Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Mon, 22 Oct 2018 11:56:40 -0700 Subject: [PATCH 09/27] Fixed bug in _params_equal() --- src/azure-cli-core/azure/cli/core/_help.py | 4 +++- 1 file changed, 3 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 5a960d2768d..731f5a497c7 100644 --- a/src/azure-cli-core/azure/cli/core/_help.py +++ b/src/azure-cli-core/azure/cli/core/_help.py @@ -310,7 +310,9 @@ def _load_from_data(self, data): def _params_equal(data, param): for name in param.name_source: - return data.get("name") == name.lstrip("-") + if data.get("name") == name.lstrip("-"): + return True + return False # load help object from data. Will call CliHelpFile._load_from_data() or KnackCommandHelpFile._load_from_data() # based on data contents / caller of this method. From 0894682e4d18fa0095d11cdefeb714544181d888 Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Thu, 25 Oct 2018 15:35:16 -0700 Subject: [PATCH 10/27] checked out devs _help.py file. --- src/azure-cli-core/azure/cli/core/_help.py | 193 +-------------------- 1 file changed, 6 insertions(+), 187 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/_help.py b/src/azure-cli-core/azure/cli/core/_help.py index 731f5a497c7..3cd34e5336a 100644 --- a/src/azure-cli-core/azure/cli/core/_help.py +++ b/src/azure-cli-core/azure/cli/core/_help.py @@ -5,11 +5,13 @@ from __future__ import print_function -from knack.help import (HelpExample, HelpFile as KnackHelpFile, CommandHelpFile as KnackCommandHelpFile, - GroupHelpFile as KnackGroupHelpFile, CLIHelp, HelpParameter, +from knack.help import (HelpExample, + HelpFile as KnackHelpFile, + CommandHelpFile as KnackCommandHelpFile, + CLIHelp, + HelpParameter, ArgumentGroupRegistry as KnackArgumentGroupRegistry) from knack.log import get_logger -from knack.util import CLIError from azure.cli.core.commands import ExtensionCommandSource @@ -51,7 +53,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 @@ -83,71 +84,9 @@ 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) - @staticmethod - def update_parser_with_help_file(nouns, cmd_loader_map, parser, is_group): - 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) - loader = None - if is_group: - for k, v in cmd_loader_map.items(): - if k.startswith(command_nouns): - loader = v[0] - break - else: - loader = cmd_loader_map.get(command_nouns, [None])[0] - - 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() - data = _parse_yaml_from_string(text, help_file_path) - parser.help_file_data = data - return - - def show_help(self, cli_name, nouns, parser, is_group): - cmd_loader_map_ref = self.cli_ctx.invocation.commands_loader.cmd_to_loader_map - # attempt to add the help.yaml file data to the parser - self.update_parser_with_help_file(nouns, cmd_loader_map_ref, parser, is_group) - super(AzCliHelp, self).show_help(cli_name, nouns, parser, is_group) - class CliHelpFile(KnackHelpFile): - def load(self, options): - # if we successfully transformed the help.yaml data, call appropriate _load_from_data method. - # This is either CliHelpFile._load_from_data() or CliCommandHelpFile._load_from_data() which ultimately - # calls CliHelpFile._load_from_data() - if hasattr(options, "help_file_data"): - data = self._load_from_parsed_yaml(options.help_file_data) - if "content" not in data: - self._load_from_data(data) - return - - # if unable to transfrom data from parsed yaml, call superclass' (regular) load method - super(CliHelpFile, self).load(options) - def _should_include_example(self, ex): min_profile = ex.get('min_profile') max_profile = ex.get('max_profile') @@ -170,116 +109,6 @@ def _load_from_data(self, data): if self._should_include_example(d): self.examples.append(HelpExample(d)) - # get data object from parsed yaml - def _load_from_parsed_yaml(self, data): - content = data.get("content") - info_type = None - info = None - new_data = {} - - for elem in content: - for key, value in elem.items(): - # find the command / group's help text - if value.get("name") and value.get("name") == self.command: - info_type = key - info = value - break - if info: - break - - # if a new command not found return old data object - if not info: - return data - - new_data["type"] = info_type - - if "summary" in info: - new_data["short-summary"] = info["summary"] - - if "description" in info: - new_data["long-summary"] = info["description"] - - if "examples" in info: - new_data["detailed_examples"] = info["examples"] - reg_examples = [] - for item in new_data["detailed_examples"]: - ex = {} - ex["name"] = item.get("summary", "") - ex["text"] = item.get("description", "") - ex["text"] = "{}\n{}".format(ex["text"], item.get("command", "")) if ex["text"] else item.get("command", "") - ex["min_profile"] = item.get('min_profile') - ex["max_profile"] = item.get('max_profile') - reg_examples.append(ex) - new_data["examples"] = reg_examples - - if "links" in info: - text = self._get_links_as_text(info["links"]) - new_data["links"] = info["links"] - new_data["long-summary"] = "{}\n{}".format(new_data["long-summary"], text) \ - if new_data.get("long-summary") else text - - if "arguments" in info: - new_data["parameters"] = [] - for arg_info in info["arguments"]: - new_data["parameters"].append(self._get_parameter_info(arg_info)) - - return new_data - - @staticmethod - def _get_links_as_text(links): - text = [] - for link in links: - if "url" in link: - text.append(link["url"]) - msg = "For more information, visit:" - links = ", ".join(text) - return "{} {}".format(msg, links) if links else "" - - @staticmethod - def _get_parameter_info(arg_info): - params = {} - - # only update if new information - if "name" in arg_info: - params["name"] = arg_info["name"] - - if "summary" in arg_info: - params["short-summary"] = arg_info["summary"] - - if "description" in arg_info: - params["long-summary"] = arg_info["description"] - - if "value-source" in arg_info: - value_source = [] - for item in arg_info["value-source"]: - if "string" in item: - value_source.append(item["string"]) - elif "link" in item: - link_text = "" - if "url" in item["link"]: - link_text = "url: {} ".format(item["link"]["url"]) - if "command" in item["link"]: - link_text = "command: {} ".format(item["link"]["command"]) - value_source.append(link_text.strip()) - params["populator-commands"] = value_source - - return params - - -class CliGroupHelpFile(KnackGroupHelpFile, CliHelpFile): - - def __init__(self, help_ctx, delimiters, parser): - - # try to update internal parsers' help_file_data - try: - if parser.choices and parser.help_file_data: - for options in parser.choices.values(): - options.help_file_data = parser.help_file_data - except AttributeError: - pass - - super(CliGroupHelpFile, self).__init__(help_ctx, delimiters, parser) - class CliCommandHelpFile(KnackCommandHelpFile, CliHelpFile): @@ -307,15 +136,6 @@ def __init__(self, help_ctx, delimiters, parser): help_param.group_name = 'Global Arguments' def _load_from_data(self, data): - - def _params_equal(data, param): - for name in param.name_source: - if data.get("name") == name.lstrip("-"): - return True - return False - - # load help object from data. Will call CliHelpFile._load_from_data() or KnackCommandHelpFile._load_from_data() - # based on data contents / caller of this method. super(CliCommandHelpFile, self)._load_from_data(data) if isinstance(data, str) or not self.parameters or not data.get('parameters'): @@ -324,9 +144,8 @@ def _params_equal(data, param): loaded_params = [] loaded_param = {} for param in self.parameters: - loaded_param = next((n for n in data['parameters'] if _params_equal(n, param)), None) + loaded_param = next((n for n in data['parameters'] if n['name'] == param.name), None) if loaded_param: - loaded_param["name"] = param.name param.update_from_data(loaded_param) loaded_params.append(param) From 4a90f31a2de38c718ed2b07e2669fd57e5f99a69 Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Thu, 25 Oct 2018 21:17:39 -0700 Subject: [PATCH 11/27] Refactoring code. Todo: update print_header to print links. Fix bug where some command group names / parameter names are not updated from code. --- shellscript | 11 ++ src/azure-cli-core/azure/cli/core/_help.py | 84 +++++++- .../azure/cli/core/_help_util.py | 179 ++++++++++++++++++ .../azure/cli/command_modules/cdn/help.yaml | 4 +- 4 files changed, 275 insertions(+), 3 deletions(-) create mode 100644 shellscript create mode 100644 src/azure-cli-core/azure/cli/core/_help_util.py diff --git a/shellscript b/shellscript new file mode 100644 index 00000000000..2df3da89bd8 --- /dev/null +++ b/shellscript @@ -0,0 +1,11 @@ +echo "printing environment variables" +printenv +echo "..." +echo "$@" +for i in "$@" +do + echo $i >> variables.file +done +echo "printing variables.file" +cat variables.file +done diff --git a/src/azure-cli-core/azure/cli/core/_help.py b/src/azure-cli-core/azure/cli/core/_help.py index 3cd34e5336a..546ded9f1a2 100644 --- a/src/azure-cli-core/azure/cli/core/_help.py +++ b/src/azure-cli-core/azure/cli/core/_help.py @@ -8,10 +8,12 @@ from knack.help import (HelpExample, HelpFile as KnackHelpFile, CommandHelpFile as KnackCommandHelpFile, + GroupHelpFile as KnackGroupHelpFile, CLIHelp, HelpParameter, ArgumentGroupRegistry as KnackArgumentGroupRegistry) from knack.log import get_logger +from knack.util import CLIError from azure.cli.core.commands import ExtensionCommandSource @@ -53,6 +55,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 @@ -87,6 +90,11 @@ def _print_detailed_help(self, cli_name, help_file): 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) + def _should_include_example(self, ex): min_profile = ex.get('min_profile') max_profile = ex.get('max_profile') @@ -99,7 +107,7 @@ 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 @@ -109,6 +117,34 @@ def _load_from_data(self, data): if self._should_include_example(d): self.examples.append(HelpExample(d)) + def load(self, options): + # if the parser's command has an associated yaml help file, load data from it. + prog = options.prog if hasattr(options, "prog") else options._prog_prefix + command_nouns = prog.split()[1:] + cmd_loader_map_ref = self.help_ctx.cli_ctx.invocation.commands_loader.cmd_to_loader_map + + yaml_help = get_yaml_help_for_nouns(command_nouns, cmd_loader_map_ref) + if yaml_help and "version" in yaml_help: + self.yaml_help_version = yaml_help["version"] + + if self.yaml_help_version == 1: + from azure.cli.core._help_util import update_help_file + update_help_file(self, yaml_help, options) + return + + # Previous behavior. "version 0" + else: + super(CliHelpFile, self).load(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): @@ -151,6 +187,9 @@ def _load_from_data(self, data): self.parameters = loaded_params + def load(self, options): + # forces class to use this load method even if KnackGroupHelpFile overrides CliHelpFile's method. + CliHelpFile.load(self, options) class ArgumentGroupRegistry(KnackArgumentGroupRegistry): # pylint: disable=too-few-public-methods @@ -169,3 +208,46 @@ def __init__(self, group_list): for group in other_groups: self.priorities[group] = priority priority += 1 + + +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] + + 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 \ No newline at end of file diff --git a/src/azure-cli-core/azure/cli/core/_help_util.py b/src/azure-cli-core/azure/cli/core/_help_util.py new file mode 100644 index 00000000000..81200f221ed --- /dev/null +++ b/src/azure-cli-core/azure/cli/core/_help_util.py @@ -0,0 +1,179 @@ +from knack.help import (HelpParameter as KnackHelpParameter, HelpExample as KnackHelpExample) +from knack.help import HelpAuthoringException +from knack.util import CLIError + +class CliHelpExample(KnackHelpExample): # pylint: disable=too-few-public-methods + + def __init__(self, _data): + _data['name'] = _data.get('name', '') + _data['text'] = _data.get('text', '') + super(CliHelpExample, self).__init__(_data) + + self.command = _data.get('command', '') + self.description = _data.get('description', '') + + self.min_profile = _data.get('min_profile', '') + self.max_profile = _data.get('max_profile', '') + + self.text = "{}\n{}".format(self.description, self.command) if self.description else self.command + +class CliHelpParameter(KnackHelpParameter): # pylint: disable=too-many-instance-attributes + + def __init__(self, **kwargs): + super(CliHelpParameter, self).__init__(**kwargs) + self.raw_value_sources = [] + + def update_from_data(self, data): + if self.name != data.get('name'): + raise HelpAuthoringException(u"mismatched name {} vs. {}" + .format(self.name, + data.get('name'))) + + if data.get('summary'): + self.short_summary = data.get('summary') + + if data.get('description'): + self.long_summary = data.get('description') + + if data.get('value-source'): # todo: change to value-sources + self.raw_value_sources = data.get('value-source') + for value_source in self.raw_value_sources: + val_str = self._raw_value_source_to_string(value_source) + if val_str: + self.value_sources.append(val_str) + + @staticmethod + 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 = "url: {} ".format(value_source["link"]["url"]) + if "command" in value_source["link"]: + link_text = "command: {} ".format(value_source["link"]["command"]) + return link_text + return "" + + +def get_yaml_help_for_nouns(nouns, cmd_loader_map_ref, is_group): + 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) + loader = None + if is_group: + for k, v in cmd_loader_map_ref.items(): + if k.startswith(command_nouns): + loader = v[0] + break + else: + loader = cmd_loader_map_ref.get(command_nouns, [None])[0] + + 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 + + +def update_help_file(self, data, parser): + + 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 + + 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") and value.get("name") == self.command: + info_type = key + info = value + break + if info: + break + # if a new command not found return old data object + + if not info: + # if content does not have the desired command or command group, default to data in parser + description = getattr(parser, 'description', None) + try: + self.short_summary = description[:description.index('.')] + long_summary = description[description.index('.') + 1:].lstrip() + self.long_summary = ' '.join(long_summary.splitlines()) + except (ValueError, AttributeError): + self.short_summary = description + return + + self.type = info_type + if "summary" in info: + self.short_summary = info["summary"] + if "description" in info: + self.long_summary = info["description"] + if "examples" in info: + ex_list = [] + self.examples=[] + for item in info["examples"]: + ex = {} + ex["name"] = item.get("summary", "") + ex["command"] = item.get("command", "") + ex["description"] = item.get("description", "") + ex["min_profile"] = item.get('min_profile', "") + ex["max_profile"] = item.get('max_profile', "") + ex_list.append(ex) + for ex in ex_list: + if self._should_include_example(ex): + self.examples.append(CliHelpExample(ex)) + + if "links" in info: + self.links = info["links"] + + if "arguments" in info and hasattr(self, "parameters"): + + loaded_params = [] + for param in self.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__ = CliHelpParameter # cast param to CliHelpParameter + param.update_from_data(loaded_param) + loaded_params.append(param) + + self.parameters = loaded_params + + + + +# new_data["long-summary"] = "{}\n{}".format(new_data["long-summary"], text) \ +# if new_data.get("long-summary") else text + +# ex["text"] = "{}\n{}".format(ex["description"], ex["command"]) if ex["description"] else ex["command"] + diff --git a/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/help.yaml b/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/help.yaml index ee0bba32694..1a1dfab57ca 100644 --- a/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/help.yaml +++ b/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/help.yaml @@ -11,7 +11,7 @@ content: - group: name: cdn - summary: Manage Azure Content Delivery Networks (CDNs). + 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. @@ -24,7 +24,7 @@ content: - group: name: cdn profile - summary: Manage CDN profiles to define an edge network. Bla bla + 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: From 457f6ed5cf01034e76117eb3f1f71d35ffebc5df Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Thu, 18 Oct 2018 14:53:38 -0700 Subject: [PATCH 12/27] script to convert _help.py to help/yaml. Parses short and long summaries. Fields within commands/groups need to have some non-alphabetic ordering --- scripts/help_convert.py | 61 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 scripts/help_convert.py diff --git a/scripts/help_convert.py b/scripts/help_convert.py new file mode 100644 index 00000000000..c151137e412 --- /dev/null +++ b/scripts/help_convert.py @@ -0,0 +1,61 @@ +# -------------------------------------------------------------------------------------------- +# 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 yaml + +PACKAGE_PREFIX = "azure.cli.command_modules" + +def convert(target_mod): + help_dict = target_mod.helps + loader_file_path = os.path.abspath(target_mod.__file__) + + result = _get_new_yaml_dict(help_dict) + + out_file = os.path.join(os.path.dirname(loader_file_path),"help.yaml") + + with open(out_file, "w") as f: + yaml.dump(result, f, default_flow_style=False) + +def _get_new_yaml_dict(help_dict): + + result = dict(version=0, 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] + + if "short-summary" in help_dict: + elem_content["summary"] = help_dict["short-summary"] + + if "long-summary" in help_dict: + elem_content["description"] = help_dict["long-summary"] + + + + content.append(elem) + + return result + + +if __name__ == "__main__": + args = sys.argv[1:] + + if len(args) != 1: + msg = "Error: Script takes only one argument" + exit(msg) + + MOD_NAME = "{}.{}._help".format(PACKAGE_PREFIX, args[0]) + + target_mod = import_module(MOD_NAME) + + convert(target_mod) \ No newline at end of file From 43a036f610175e8aed2d80fffacadcc0ce359f97 Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Fri, 19 Oct 2018 14:30:35 -0700 Subject: [PATCH 13/27] Help conversion script uses ruamel.yaml. Output is ordered and visually appealing. '>' and '|' (part of yaml syntax) are preserved in values. --- scripts/help_convert.py | 54 ++++++++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/scripts/help_convert.py b/scripts/help_convert.py index c151137e412..5189985a0d3 100644 --- a/scripts/help_convert.py +++ b/scripts/help_convert.py @@ -6,7 +6,13 @@ import sys from importlib import import_module import os -import yaml + +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" @@ -19,7 +25,7 @@ def convert(target_mod): out_file = os.path.join(os.path.dirname(loader_file_path),"help.yaml") with open(out_file, "w") as f: - yaml.dump(result, f, default_flow_style=False) + yaml.dump(result, f) def _get_new_yaml_dict(help_dict): @@ -34,18 +40,48 @@ def _get_new_yaml_dict(help_dict): elem = {type: dict(name=command_or_group)} elem_content = elem[type] - if "short-summary" in help_dict: - elem_content["summary"] = help_dict["short-summary"] - - if "long-summary" in help_dict: - elem_content["description"] = help_dict["long-summary"] - - + _convert_summaries(old_dict=help_dict, new_dict=elem_content) + + 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 + + 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('-') + if "populator-commands" in param: + new_param["value-source"] = [] + for item in param["populator-commands"]: + new_param["value-source"].append(dict(string=item)) + _convert_summaries(old_dict=param, new_dict=new_param) + parameters.append(new_param) + help_dict["parameters"] = parameters 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"] if __name__ == "__main__": args = sys.argv[1:] From 9e60de99577a74ce610d86a30ee16f7ea47779e8 Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Fri, 19 Oct 2018 16:06:42 -0700 Subject: [PATCH 14/27] Conversion script now converts parameters to arguments (bug fix). Outputs arguments before examples --- scripts/help_convert.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/scripts/help_convert.py b/scripts/help_convert.py index 5189985a0d3..846a5589fc3 100644 --- a/scripts/help_convert.py +++ b/scripts/help_convert.py @@ -42,6 +42,23 @@ def _get_new_yaml_dict(help_dict): _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-source"] = [] + for item in param["populator-commands"]: + new_param["value-source"].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"]: @@ -57,22 +74,6 @@ def _get_new_yaml_dict(help_dict): elem_examples.append(new_ex) elem_content["examples"] = elem_examples - 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('-') - if "populator-commands" in param: - new_param["value-source"] = [] - for item in param["populator-commands"]: - new_param["value-source"].append(dict(string=item)) - _convert_summaries(old_dict=param, new_dict=new_param) - parameters.append(new_param) - help_dict["parameters"] = parameters - content.append(elem) return result From 49e669487c49fce501e092e8714ae8832fea9afe Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Thu, 1 Nov 2018 10:40:31 -0700 Subject: [PATCH 15/27] Removed unnecessary file. --- shellscript | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 shellscript diff --git a/shellscript b/shellscript deleted file mode 100644 index 2df3da89bd8..00000000000 --- a/shellscript +++ /dev/null @@ -1,11 +0,0 @@ -echo "printing environment variables" -printenv -echo "..." -echo "$@" -for i in "$@" -do - echo $i >> variables.file -done -echo "printing variables.file" -cat variables.file -done From 2d8c47400f425f08a5ce6799e6c0a3358aaa0735 Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Thu, 1 Nov 2018 10:53:00 -0700 Subject: [PATCH 16/27] Addressed style feedback. --- src/azure-cli-core/azure/cli/core/_help_util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/_help_util.py b/src/azure-cli-core/azure/cli/core/_help_util.py index 81200f221ed..b405f44db7b 100644 --- a/src/azure-cli-core/azure/cli/core/_help_util.py +++ b/src/azure-cli-core/azure/cli/core/_help_util.py @@ -90,7 +90,7 @@ def _parse_yaml_from_string(text, help_file_path): 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"): + if file.endswith((".yaml", ".yml")): help_file_path = os.path.join(dir_name, file) with open(help_file_path, "r") as f: text = f.read() @@ -114,7 +114,7 @@ def _name_is_equal(data, param): for elem in content: for key, value in elem.items(): # find the command / group's help text - if value.get("name") and value.get("name") == self.command: + if value.get("name") == self.command: info_type = key info = value break From 47e56d620eb3737e2b5a5555e8b7a01612249d0c Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Thu, 1 Nov 2018 15:25:45 -0700 Subject: [PATCH 17/27] Changes: create temp directory to hold temporary files and scripts. Updated value-source to value-sources. get_all_help uses CliGroupHelpfile instead of knack's CliGroupHelpfile --- src/azure-cli-core/azure/cli/core/_help_util.py | 14 +++----------- src/azure-cli-core/azure/cli/core/file_util.py | 5 ++--- .../azure/cli/command_modules/cdn/__init__.py | 2 +- .../command_modules/cdn => temp_help}/help.yaml | 2 +- {scripts => temp_help}/help_convert.py | 6 +++--- 5 files changed, 10 insertions(+), 19 deletions(-) rename {src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn => temp_help}/help.yaml (99%) rename {scripts => temp_help}/help_convert.py (94%) diff --git a/src/azure-cli-core/azure/cli/core/_help_util.py b/src/azure-cli-core/azure/cli/core/_help_util.py index b405f44db7b..8e6c3b4ff03 100644 --- a/src/azure-cli-core/azure/cli/core/_help_util.py +++ b/src/azure-cli-core/azure/cli/core/_help_util.py @@ -2,6 +2,7 @@ from knack.help import HelpAuthoringException from knack.util import CLIError + class CliHelpExample(KnackHelpExample): # pylint: disable=too-few-public-methods def __init__(self, _data): @@ -35,8 +36,8 @@ def update_from_data(self, data): if data.get('description'): self.long_summary = data.get('description') - if data.get('value-source'): # todo: change to value-sources - self.raw_value_sources = data.get('value-source') + if data.get('value-sources'): + self.raw_value_sources = data.get('value-sources') for value_source in self.raw_value_sources: val_str = self._raw_value_source_to_string(value_source) if val_str: @@ -168,12 +169,3 @@ def _name_is_equal(data, param): loaded_params.append(param) self.parameters = loaded_params - - - - -# new_data["long-summary"] = "{}\n{}".format(new_data["long-summary"], text) \ -# if new_data.get("long-summary") else text - -# ex["text"] = "{}\n{}".format(ex["description"], ex["command"]) if ex["description"] else ex["command"] - 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/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/__init__.py b/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/__init__.py index d1a88c15791..70d0b314478 100644 --- a/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/__init__.py +++ b/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/__init__.py @@ -3,7 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- # pylint: disable=unused-import -import azure.cli.command_modules.cdn._help +#import azure.cli.command_modules.cdn._help from azure.cli.core import AzCommandsLoader diff --git a/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/help.yaml b/temp_help/help.yaml similarity index 99% rename from src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/help.yaml rename to temp_help/help.yaml index 1a1dfab57ca..286f387f2fa 100644 --- a/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/help.yaml +++ b/temp_help/help.yaml @@ -54,7 +54,7 @@ content: 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-source: # UPDATED + value-sources: # UPDATED - string: "Number range: -5.0 to 5.0" - link: diff --git a/scripts/help_convert.py b/temp_help/help_convert.py similarity index 94% rename from scripts/help_convert.py rename to temp_help/help_convert.py index 846a5589fc3..c42fabcea6a 100644 --- a/scripts/help_convert.py +++ b/temp_help/help_convert.py @@ -29,7 +29,7 @@ def convert(target_mod): def _get_new_yaml_dict(help_dict): - result = dict(version=0, content=[]) + result = dict(version=1, content=[]) content = result['content'] for command_or_group, yaml_text in help_dict.items(): @@ -53,9 +53,9 @@ def _get_new_yaml_dict(help_dict): _convert_summaries(old_dict=param, new_dict=new_param) if "populator-commands" in param: - new_param["value-source"] = [] + new_param["value-sources"] = [] for item in param["populator-commands"]: - new_param["value-source"].append(dict(string=item)) + new_param["value-sources"].append(dict(string=item)) parameters.append(new_param) elem_content["arguments"] = parameters From 8548e73212032e6d6d19eb3d6ec78ccb6fc900b3 Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Thu, 1 Nov 2018 15:31:39 -0700 Subject: [PATCH 18/27] uncommented help import --- .../azure-cli-cdn/azure/cli/command_modules/cdn/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/__init__.py b/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/__init__.py index 70d0b314478..d1a88c15791 100644 --- a/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/__init__.py +++ b/src/command_modules/azure-cli-cdn/azure/cli/command_modules/cdn/__init__.py @@ -3,7 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- # pylint: disable=unused-import -#import azure.cli.command_modules.cdn._help +import azure.cli.command_modules.cdn._help from azure.cli.core import AzCommandsLoader From bc3a1703e05f97b4b0790ba3cfb11138d069ee21 Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Thu, 1 Nov 2018 19:28:20 -0700 Subject: [PATCH 19/27] Added tests to help script. --- temp_help/help_convert.py | 172 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 163 insertions(+), 9 deletions(-) diff --git a/temp_help/help_convert.py b/temp_help/help_convert.py index c42fabcea6a..01b1a03c9a2 100644 --- a/temp_help/help_convert.py +++ b/temp_help/help_convert.py @@ -6,6 +6,12 @@ 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 @@ -16,16 +22,22 @@ PACKAGE_PREFIX = "azure.cli.command_modules" -def convert(target_mod): +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__) - result = _get_new_yaml_dict(help_dict) - 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) - with open(out_file, "w") as f: - yaml.dump(result, f) + return out_file, result def _get_new_yaml_dict(help_dict): @@ -84,15 +96,157 @@ def _convert_summaries(old_dict, new_dict): 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 len(args) != 1: - msg = "Error: Script takes only one argument" + 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) - MOD_NAME = "{}.{}._help".format(PACKAGE_PREFIX, args[0]) + 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) - convert(target_mod) \ No newline at end of file + 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])) From 6d5b7f221d4c1699d74c48b99a305e3efbeb5351 Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Fri, 2 Nov 2018 10:33:24 -0700 Subject: [PATCH 20/27] PEP8 style changes. --- temp_help/help_convert.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/temp_help/help_convert.py b/temp_help/help_convert.py index 01b1a03c9a2..0e3791f0f7f 100644 --- a/temp_help/help_convert.py +++ b/temp_help/help_convert.py @@ -4,7 +4,7 @@ # -------------------------------------------------------------------------------------------- import sys -from importlib import import_module +from importlib import import_module import os import subprocess import difflib @@ -30,7 +30,7 @@ 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") + 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) @@ -39,6 +39,7 @@ def convert(target_mod, mod_name, test=False): return out_file, result + def _get_new_yaml_dict(help_dict): result = dict(version=1, content=[]) @@ -60,7 +61,7 @@ def _get_new_yaml_dict(help_dict): new_param = dict() if "name" in param: options = param["name"].split() - option = max(options, key = lambda x: len(x)) + option = max(options, key=lambda x: len(x)) new_param["name"] = option.lstrip('-') _convert_summaries(old_dict=param, new_dict=new_param) @@ -90,6 +91,7 @@ def _get_new_yaml_dict(help_dict): return result + def _convert_summaries(old_dict, new_dict): if "short-summary" in old_dict: new_dict["summary"] = old_dict["short-summary"] @@ -97,21 +99,21 @@ def _convert_summaries(old_dict, new_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) + 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)) + 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) + 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": @@ -169,6 +171,7 @@ def assert_true_or_warn(x, y): 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") From 01ebbf4172a3afb692002217510f36dbbddc71c9 Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Mon, 12 Nov 2018 18:43:28 -0800 Subject: [PATCH 21/27] Sphinx properly handles help groups. --- doc/sphinx/azhelpgen/azhelpgen.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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: From c5672489f14f100a3486347602a61cf15e0b016d Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Tue, 13 Nov 2018 18:11:17 -0800 Subject: [PATCH 22/27] Refactored _help.py and _help_util.py to _help_loaders.py --- src/azure-cli-core/azure/cli/core/_help.py | 130 ++++++++-------- .../azure/cli/core/_help_loaders.py | 146 ++++++++++++++++++ .../azure/cli/core/_help_util.py | 5 + 3 files changed, 216 insertions(+), 65 deletions(-) create mode 100644 src/azure-cli-core/azure/cli/core/_help_loaders.py diff --git a/src/azure-cli-core/azure/cli/core/_help.py b/src/azure-cli-core/azure/cli/core/_help.py index 546ded9f1a2..906cb00bb1c 100644 --- a/src/azure-cli-core/azure/cli/core/_help.py +++ b/src/azure-cli-core/azure/cli/core/_help.py @@ -5,13 +5,11 @@ from __future__ import print_function -from knack.help import (HelpExample, - HelpFile as KnackHelpFile, - CommandHelpFile as KnackCommandHelpFile, - GroupHelpFile as KnackGroupHelpFile, - CLIHelp, - HelpParameter, - 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, + HelpAuthoringException, CLIHelp) + from knack.log import get_logger from knack.util import CLIError @@ -74,6 +72,9 @@ def new_normalize_text(s): HelpObject._normalize_text = new_normalize_text # pylint: disable=protected-access + self._register_help_loaders() + + @staticmethod def _print_extensions_msg(help_file): if help_file.type != 'command': @@ -87,6 +88,19 @@ 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 _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 issubclass(cls, help_loaders.BaseHelpLoader) + + 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): @@ -94,6 +108,7 @@ 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') @@ -118,23 +133,9 @@ def _load_from_data(self, data): self.examples.append(HelpExample(d)) def load(self, options): - # if the parser's command has an associated yaml help file, load data from it. - prog = options.prog if hasattr(options, "prog") else options._prog_prefix - command_nouns = prog.split()[1:] - cmd_loader_map_ref = self.help_ctx.cli_ctx.invocation.commands_loader.cmd_to_loader_map - - yaml_help = get_yaml_help_for_nouns(command_nouns, cmd_loader_map_ref) - if yaml_help and "version" in yaml_help: - self.yaml_help_version = yaml_help["version"] - - if self.yaml_help_version == 1: - from azure.cli.core._help_util import update_help_file - update_help_file(self, yaml_help, options) - return - - # Previous behavior. "version 0" - else: - super(CliHelpFile, self).load(options) + ordered_loaders = sorted(self.help_ctx.versioned_loaders.values(), key=lambda ldr: ldr.VERSION) + for loader in ordered_loaders: + loader.load(self, options) class CliGroupHelpFile(KnackGroupHelpFile, CliHelpFile): @@ -210,44 +211,43 @@ def __init__(self, group_list): priority += 1 -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] - - 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 \ No newline at end of file +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.command = _data.get('command', '') + self.description = _data.get('description', '') + + self.min_profile = _data.get('min_profile', '') + self.max_profile = _data.get('max_profile', '') + + self.text = "{}\n{}".format(self.description, self.command) if self.description else self.command + + +class HelpParameter(KnackHelpParameter): # pylint: disable=too-many-instance-attributes + + def __init__(self, **kwargs): + super(HelpParameter, self).__init__(**kwargs) + self.raw_value_sources = [] + + def update_from_data(self, data): + if self.name != data.get('name'): + raise HelpAuthoringException(u"mismatched name {} vs. {}" + .format(self.name, + data.get('name'))) + + if data.get('summary'): + self.short_summary = data.get('summary') + + if data.get('description'): + self.long_summary = data.get('description') + + if data.get('value-sources'): + self.raw_value_sources = data.get('value-sources') + for value_source in self.raw_value_sources: + val_str = self._raw_value_source_to_string(value_source) + if val_str: + self.value_sources.append(val_str) \ 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..988187e4f8a --- /dev/null +++ b/src/azure-cli-core/azure/cli/core/_help_loaders.py @@ -0,0 +1,146 @@ +# -------------------------------------------------------------------------------------------- +# 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) + + + +class BaseHelpLoader(object): + VERSION = 0 + + def __init__(self, help_ctx=None): + self.help_ctx = help_ctx + + def load(self, help_obj, parser): + super(CliHelpFile, help_obj).load(parser) + + +class HelpLoaderV1(BaseHelpLoader): + VERSION = 1 + + # load help_obj with data if applicable + def 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 + if info: + break + + 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 + + + + @staticmethod + def _load_command_data(help_obj, info): + if "examples" in info: + ex_list = [] + help_obj.examples = [] + for item in info["examples"]: + ex = {} + ex["name"] = item.get("summary", "") + ex["command"] = item.get("command", "") + ex["description"] = item.get("description", "") + ex["min_profile"] = item.get('min_profile', "") + ex["max_profile"] = item.get('max_profile', "") + ex_list.append(ex) + for ex in ex_list: + if help_obj._should_include_example(ex): + help_obj.examples.append(HelpExample(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 + param.update_from_data(loaded_param) + loaded_params.append(param) + + help_obj.parameters = loaded_params + + + @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] + + 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 + + diff --git a/src/azure-cli-core/azure/cli/core/_help_util.py b/src/azure-cli-core/azure/cli/core/_help_util.py index 8e6c3b4ff03..9c6c570e851 100644 --- a/src/azure-cli-core/azure/cli/core/_help_util.py +++ b/src/azure-cli-core/azure/cli/core/_help_util.py @@ -1,3 +1,8 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + from knack.help import (HelpParameter as KnackHelpParameter, HelpExample as KnackHelpExample) from knack.help import HelpAuthoringException from knack.util import CLIError From 8ab7c9c7bd9e95ffc2e500ea5b1e3fa544ad1353 Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Wed, 14 Nov 2018 10:53:03 -0800 Subject: [PATCH 23/27] Removed _help_util.py --- .../azure/cli/core/_help_util.py | 176 ------------------ 1 file changed, 176 deletions(-) delete mode 100644 src/azure-cli-core/azure/cli/core/_help_util.py diff --git a/src/azure-cli-core/azure/cli/core/_help_util.py b/src/azure-cli-core/azure/cli/core/_help_util.py deleted file mode 100644 index 9c6c570e851..00000000000 --- a/src/azure-cli-core/azure/cli/core/_help_util.py +++ /dev/null @@ -1,176 +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.help import (HelpParameter as KnackHelpParameter, HelpExample as KnackHelpExample) -from knack.help import HelpAuthoringException -from knack.util import CLIError - - -class CliHelpExample(KnackHelpExample): # pylint: disable=too-few-public-methods - - def __init__(self, _data): - _data['name'] = _data.get('name', '') - _data['text'] = _data.get('text', '') - super(CliHelpExample, self).__init__(_data) - - self.command = _data.get('command', '') - self.description = _data.get('description', '') - - self.min_profile = _data.get('min_profile', '') - self.max_profile = _data.get('max_profile', '') - - self.text = "{}\n{}".format(self.description, self.command) if self.description else self.command - -class CliHelpParameter(KnackHelpParameter): # pylint: disable=too-many-instance-attributes - - def __init__(self, **kwargs): - super(CliHelpParameter, self).__init__(**kwargs) - self.raw_value_sources = [] - - def update_from_data(self, data): - if self.name != data.get('name'): - raise HelpAuthoringException(u"mismatched name {} vs. {}" - .format(self.name, - data.get('name'))) - - if data.get('summary'): - self.short_summary = data.get('summary') - - if data.get('description'): - self.long_summary = data.get('description') - - if data.get('value-sources'): - self.raw_value_sources = data.get('value-sources') - for value_source in self.raw_value_sources: - val_str = self._raw_value_source_to_string(value_source) - if val_str: - self.value_sources.append(val_str) - - @staticmethod - 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 = "url: {} ".format(value_source["link"]["url"]) - if "command" in value_source["link"]: - link_text = "command: {} ".format(value_source["link"]["command"]) - return link_text - return "" - - -def get_yaml_help_for_nouns(nouns, cmd_loader_map_ref, is_group): - 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) - loader = None - if is_group: - for k, v in cmd_loader_map_ref.items(): - if k.startswith(command_nouns): - loader = v[0] - break - else: - loader = cmd_loader_map_ref.get(command_nouns, [None])[0] - - 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", ".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 - - -def update_help_file(self, data, parser): - - 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 - - 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") == self.command: - info_type = key - info = value - break - if info: - break - # if a new command not found return old data object - - if not info: - # if content does not have the desired command or command group, default to data in parser - description = getattr(parser, 'description', None) - try: - self.short_summary = description[:description.index('.')] - long_summary = description[description.index('.') + 1:].lstrip() - self.long_summary = ' '.join(long_summary.splitlines()) - except (ValueError, AttributeError): - self.short_summary = description - return - - self.type = info_type - if "summary" in info: - self.short_summary = info["summary"] - if "description" in info: - self.long_summary = info["description"] - if "examples" in info: - ex_list = [] - self.examples=[] - for item in info["examples"]: - ex = {} - ex["name"] = item.get("summary", "") - ex["command"] = item.get("command", "") - ex["description"] = item.get("description", "") - ex["min_profile"] = item.get('min_profile', "") - ex["max_profile"] = item.get('max_profile', "") - ex_list.append(ex) - for ex in ex_list: - if self._should_include_example(ex): - self.examples.append(CliHelpExample(ex)) - - if "links" in info: - self.links = info["links"] - - if "arguments" in info and hasattr(self, "parameters"): - - loaded_params = [] - for param in self.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__ = CliHelpParameter # cast param to CliHelpParameter - param.update_from_data(loaded_param) - loaded_params.append(param) - - self.parameters = loaded_params From 9589f39e4ec7360888f4e62898ef7019018d5a4a Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Wed, 14 Nov 2018 15:52:36 -0800 Subject: [PATCH 24/27] Bug fixes --- src/azure-cli-core/azure/cli/core/_help.py | 24 +++++++++++---- .../azure/cli/core/_help_loaders.py | 29 ++++++++----------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/_help.py b/src/azure-cli-core/azure/cli/core/_help.py index 906cb00bb1c..f8bd66474c5 100644 --- a/src/azure-cli-core/azure/cli/core/_help.py +++ b/src/azure-cli-core/azure/cli/core/_help.py @@ -11,7 +11,6 @@ HelpAuthoringException, CLIHelp) from knack.log import get_logger -from knack.util import CLIError from azure.cli.core.commands import ExtensionCommandSource @@ -224,7 +223,8 @@ def __init__(self, _data): self.min_profile = _data.get('min_profile', '') self.max_profile = _data.get('max_profile', '') - self.text = "{}\n{}".format(self.description, self.command) if self.description else self.command + if not self.text: + self.text = "{}\n{}".format(self.description, self.command) if self.description else self.command class HelpParameter(KnackHelpParameter): # pylint: disable=too-many-instance-attributes @@ -235,9 +235,7 @@ def __init__(self, **kwargs): def update_from_data(self, data): if self.name != data.get('name'): - raise HelpAuthoringException(u"mismatched name {} vs. {}" - .format(self.name, - data.get('name'))) + raise HelpAuthoringException(u"mismatched name {} vs. {}".format(self.name, data.get('name'))) if data.get('summary'): self.short_summary = data.get('summary') @@ -246,8 +244,22 @@ def update_from_data(self, data): self.long_summary = data.get('description') if data.get('value-sources'): + self.value_sources = [] self.raw_value_sources = data.get('value-sources') for value_source in self.raw_value_sources: val_str = self._raw_value_source_to_string(value_source) if val_str: - self.value_sources.append(val_str) \ No newline at end of file + self.value_sources.append(val_str) + + @staticmethod + 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 = "url: {} ".format(value_source["link"]["url"]) + if "command" in value_source["link"]: + link_text = "command: {} ".format(value_source["link"]["command"]) + return link_text + return "" \ 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 index 988187e4f8a..153a2d5be40 100644 --- a/src/azure-cli-core/azure/cli/core/_help_loaders.py +++ b/src/azure-cli-core/azure/cli/core/_help_loaders.py @@ -8,7 +8,6 @@ from azure.cli.core._help import (HelpExample, HelpParameter, CliHelpFile) - class BaseHelpLoader(object): VERSION = 0 @@ -44,23 +43,19 @@ def load(self, help_obj, parser): info_type = key info = value break - if info: - break - - 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 - + # 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 @staticmethod def _load_command_data(help_obj, info): From b5c52795923b8ba80a66ef62ccbb7987c97e76ef Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Wed, 14 Nov 2018 18:51:33 -0800 Subject: [PATCH 25/27] More refactoring, script tests pass --- src/azure-cli-core/azure/cli/core/_help.py | 56 ++---- .../azure/cli/core/_help_loaders.py | 164 +++++++++++------- 2 files changed, 116 insertions(+), 104 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/_help.py b/src/azure-cli-core/azure/cli/core/_help.py index f8bd66474c5..783f0d7aa31 100644 --- a/src/azure-cli-core/azure/cli/core/_help.py +++ b/src/azure-cli-core/azure/cli/core/_help.py @@ -8,7 +8,7 @@ from knack.help import (HelpFile as KnackHelpFile, CommandHelpFile as KnackCommandHelpFile, GroupHelpFile as KnackGroupHelpFile, ArgumentGroupRegistry as KnackArgumentGroupRegistry, HelpExample as KnackHelpExample, HelpParameter as KnackHelpParameter, - HelpAuthoringException, CLIHelp) + CLIHelp) from knack.log import get_logger @@ -92,7 +92,7 @@ def _register_help_loaders(self): import inspect def is_loader_cls(cls): - return inspect.isclass(cls) and issubclass(cls, help_loaders.BaseHelpLoader) + 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): @@ -101,6 +101,7 @@ def is_loader_cls(cls): self.versioned_loaders = versioned_loaders + class CliHelpFile(KnackHelpFile): def __init__(self, help_ctx, delimiters): @@ -129,12 +130,12 @@ def _load_from_data(self, 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.load(self, options) + loader.versioned_load(self, options) class CliGroupHelpFile(KnackGroupHelpFile, CliHelpFile): @@ -188,9 +189,10 @@ def _load_from_data(self, data): self.parameters = loaded_params def load(self, options): - # forces class to use this load method even if KnackGroupHelpFile overrides CliHelpFile's method. + # 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 def __init__(self, group_list): @@ -212,54 +214,22 @@ def __init__(self, group_list): class HelpExample(KnackHelpExample): # pylint: disable=too-few-public-methods - def __init__(self, _data): + def __init__(self, **_data): _data['name'] = _data.get('name', '') _data['text'] = _data.get('text', '') super(HelpExample, self).__init__(_data) - self.command = _data.get('command', '') - self.description = _data.get('description', '') - self.min_profile = _data.get('min_profile', '') self.max_profile = _data.get('max_profile', '') - if not self.text: - self.text = "{}\n{}".format(self.description, self.command) if self.description else self.command + 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) - self.raw_value_sources = [] - - def update_from_data(self, data): - if self.name != data.get('name'): - raise HelpAuthoringException(u"mismatched name {} vs. {}".format(self.name, data.get('name'))) - - if data.get('summary'): - self.short_summary = data.get('summary') - - if data.get('description'): - self.long_summary = data.get('description') - - if data.get('value-sources'): - self.value_sources = [] - self.raw_value_sources = data.get('value-sources') - for value_source in self.raw_value_sources: - val_str = self._raw_value_source_to_string(value_source) - if val_str: - self.value_sources.append(val_str) - - @staticmethod - 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 = "url: {} ".format(value_source["link"]["url"]) - if "command" in value_source["link"]: - link_text = "command: {} ".format(value_source["link"]["command"]) - return link_text - return "" \ No newline at end of file + # 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 index 153a2d5be40..aa76e19ffca 100644 --- a/src/azure-cli-core/azure/cli/core/_help_loaders.py +++ b/src/azure-cli-core/azure/cli/core/_help_loaders.py @@ -6,15 +6,73 @@ 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): - VERSION = 0 - def __init__(self, help_ctx=None): self.help_ctx = help_ctx - def load(self, help_obj, parser): + 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) @@ -22,7 +80,7 @@ class HelpLoaderV1(BaseHelpLoader): VERSION = 1 # load help_obj with data if applicable - def load(self, help_obj, parser): + 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 @@ -57,22 +115,13 @@ def load(self, help_obj, parser): self._load_command_data(help_obj, info) return - @staticmethod - def _load_command_data(help_obj, info): + @classmethod + def _load_command_data(cls, help_obj, info): if "examples" in info: - ex_list = [] help_obj.examples = [] - for item in info["examples"]: - ex = {} - ex["name"] = item.get("summary", "") - ex["command"] = item.get("command", "") - ex["description"] = item.get("description", "") - ex["min_profile"] = item.get('min_profile', "") - ex["max_profile"] = item.get('max_profile', "") - ex_list.append(ex) - for ex in ex_list: + for ex in info["examples"]: if help_obj._should_include_example(ex): - help_obj.examples.append(HelpExample(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): @@ -89,53 +138,46 @@ def _name_is_equal(data, param): if loaded_param and isinstance(param, KnackHelpParameter): loaded_param["name"] = param.name param.__class__ = HelpParameter # cast param to CliHelpParameter - param.update_from_data(loaded_param) + cls._update_param_from_data(param, loaded_param) loaded_params.append(param) help_obj.parameters = loaded_params - @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] - - 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 - + 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 = "url: {} ".format(value_source["link"]["url"]) + if "command" in value_source["link"]: + link_text = "command: {} ".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) From a1732d108449f59abeacda6e20eb1788a186d14d Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Wed, 21 Nov 2018 16:02:32 -0800 Subject: [PATCH 26/27] Changes to restore _help.py to same state before rebase. --- src/azure-cli-core/azure/cli/core/_help.py | 21 +++++++++++++++++++-- 1 file changed, 19 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 783f0d7aa31..91ee54a140d 100644 --- a/src/azure-cli-core/azure/cli/core/_help.py +++ b/src/azure-cli-core/azure/cli/core/_help.py @@ -124,8 +124,21 @@ def _should_include_example(self, ex): # 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']: @@ -150,7 +163,11 @@ def load(self, options): class CliCommandHelpFile(KnackCommandHelpFile, CliHelpFile): def __init__(self, help_ctx, delimiters, parser): + 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 From dc1ad577d52c73722a20b631c22d97e0af440729 Mon Sep 17 00:00:00 2001 From: Oluwatosin Adewale Date: Wed, 21 Nov 2018 17:18:27 -0800 Subject: [PATCH 27/27] Updated AzCliHelp to print links associated with commands and groups. --- src/azure-cli-core/azure/cli/core/_help.py | 14 ++++++++++++-- src/azure-cli-core/azure/cli/core/_help_loaders.py | 4 ++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/_help.py b/src/azure-cli-core/azure/cli/core/_help.py index 91ee54a140d..869dec3757e 100644 --- a/src/azure-cli-core/azure/cli/core/_help.py +++ b/src/azure-cli-core/azure/cli/core/_help.py @@ -8,7 +8,7 @@ from knack.help import (HelpFile as KnackHelpFile, CommandHelpFile as KnackCommandHelpFile, GroupHelpFile as KnackGroupHelpFile, ArgumentGroupRegistry as KnackArgumentGroupRegistry, HelpExample as KnackHelpExample, HelpParameter as KnackHelpParameter, - CLIHelp) + _print_indent, CLIHelp) from knack.log import get_logger @@ -73,7 +73,7 @@ def new_normalize_text(s): self._register_help_loaders() - + # print methods @staticmethod def _print_extensions_msg(help_file): if help_file.type != 'command': @@ -87,6 +87,16 @@ 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 diff --git a/src/azure-cli-core/azure/cli/core/_help_loaders.py b/src/azure-cli-core/azure/cli/core/_help_loaders.py index aa76e19ffca..b224d72a9ea 100644 --- a/src/azure-cli-core/azure/cli/core/_help_loaders.py +++ b/src/azure-cli-core/azure/cli/core/_help_loaders.py @@ -152,9 +152,9 @@ def _raw_value_source_to_string(value_source): elif "link" in value_source: link_text = "" if "url" in value_source["link"]: - link_text = "url: {} ".format(value_source["link"]["url"]) + link_text = "{}".format(value_source["link"]["url"]) if "command" in value_source["link"]: - link_text = "command: {} ".format(value_source["link"]["command"]) + link_text = "{}".format(value_source["link"]["command"]) return link_text return ""