From 35fb7f87db7ebb5ae295d22ee6f313ffc3e2387a Mon Sep 17 00:00:00 2001 From: kai ru <69238381+kairu-ms@users.noreply.github.com> Date: Mon, 22 Mar 2021 16:56:49 +0800 Subject: [PATCH 1/7] azCommandGroup refactoring --- src/azure-cli-core/azure/cli/core/__init__.py | 25 + .../azure/cli/core/commands/__init__.py | 66 ++- .../azure/cli/core/commands/arm.py | 362 +------------ .../cli/core/commands/command_operation.py | 489 ++++++++++++++++++ 4 files changed, 568 insertions(+), 374 deletions(-) create mode 100644 src/azure-cli-core/azure/cli/core/commands/command_operation.py diff --git a/src/azure-cli-core/azure/cli/core/__init__.py b/src/azure-cli-core/azure/cli/core/__init__.py index 6f40999d61a..77f9887159c 100644 --- a/src/azure-cli-core/azure/cli/core/__init__.py +++ b/src/azure-cli-core/azure/cli/core/__init__.py @@ -770,6 +770,7 @@ def command_group(self, group_name, command_type=None, **kwargs): def argument_context(self, scope, **kwargs): return self._argument_context_cls(self, scope, **kwargs) + # keyvault and batch are using this function def _cli_command(self, name, operation=None, handler=None, argument_loader=None, description_loader=None, **kwargs): from knack.deprecation import Deprecated @@ -829,6 +830,30 @@ def default_description_loader(): handler or default_command_handler, **kwargs) + def add_cli_command(self, name, command_operation, **kwargs): + from knack.deprecation import Deprecated + from .commands.command_operation import BaseCommandOperation + if not issubclass(type(command_operation), BaseCommandOperation): + raise TypeError("CommandOperation must be an instance of subclass of BaseCommandOperation." + " Got instance of '{}'".format(type(command_operation))) + + kwargs['deprecate_info'] = Deprecated.ensure_new_style_deprecation(self.cli_ctx, kwargs, 'command') + + name = ' '.join(name.split()) + + if self.supported_api_version(resource_type=kwargs.get('resource_type'), + min_api=kwargs.get('min_api'), + max_api=kwargs.get('max_api'), + operation_group=kwargs.get('operation_group')): + self._populate_command_group_table_with_subgroups(' '.join(name.split()[:-1])) + self.command_table[name] = self.command_cls(loader=self, + name=name, + handler=command_operation.handler, + arguments_loader=command_operation.arguments_loader, + description_loader=command_operation.description_loader, + command_operation=command_operation, + **kwargs) + def get_op_handler(self, operation, operation_group=None): """ Import and load the operation handler """ # Patch the unversioned sdk path to include the appropriate API version for the diff --git a/src/azure-cli-core/azure/cli/core/commands/__init__.py b/src/azure-cli-core/azure/cli/core/commands/__init__.py index 6f1f37d9a11..3beb0584f93 100644 --- a/src/azure-cli-core/azure/cli/core/commands/__init__.py +++ b/src/azure-cli-core/azure/cli/core/commands/__init__.py @@ -1157,6 +1157,12 @@ def update(self, other=None, **kwargs): class AzCommandGroup(CommandGroup): def __init__(self, command_loader, group_name, **kwargs): + """ + :param command_loader: The command loader that commands will be registered into + :type command_loader: azure.cli.core.AzCommandsLoader + :param group_name: The name of the group of commands in the command hierarchy + :type group_name: str + """ merged_kwargs = self._merge_kwargs(kwargs, base_kwargs=command_loader.module_kwargs) operations_tmpl = merged_kwargs.pop('operations_tmpl', None) super(AzCommandGroup, self).__init__(command_loader, group_name, @@ -1244,16 +1250,24 @@ def custom_command(self, name, method_name=None, **kwargs): return self._command(name, method_name=method_name, custom_command=True, **kwargs) def _command(self, name, method_name, custom_command=False, **kwargs): + from .command_operation import CommandOperation + self._check_stale() merged_kwargs = self._flatten_kwargs(kwargs, get_command_type_kwarg(custom_command)) self._apply_tags(merged_kwargs, kwargs, name) operations_tmpl = merged_kwargs['operations_tmpl'] - command_name = '{} {}'.format(self.group_name, name) if self.group_name else name - self.command_loader._cli_command(command_name, # pylint: disable=protected-access - operation=operations_tmpl.format(method_name), - **merged_kwargs) + operation = operations_tmpl.format(method_name) + command_name = '{} {}'.format(self.group_name, name) if self.group_name else name + command_operation = CommandOperation( + ctx=self.command_loader, + operation=operation, + **merged_kwargs + ) + self.command_loader.add_cli_command(command_name, + command_operation=command_operation, + **merged_kwargs) return command_name # pylint: disable=no-self-use @@ -1283,7 +1297,7 @@ def generic_update_command(self, name, getter_name='get', getter_type=None, setter_name='create_or_update', setter_type=None, setter_arg_name='parameters', child_collection_prop_name=None, child_collection_key='name', child_arg_name='item_name', custom_func_name=None, custom_func_type=None, **kwargs): - from azure.cli.core.commands.arm import _cli_generic_update_command + from azure.cli.core.commands.command_operation import GenericUpdateCommandOperation self._check_stale() merged_kwargs = self._flatten_kwargs(kwargs, get_command_type_kwarg()) merged_kwargs_custom = self._flatten_kwargs(kwargs, get_command_type_kwarg(custom_command=True)) @@ -1293,9 +1307,9 @@ def generic_update_command(self, name, getter_name='get', getter_type=None, setter_op = self._resolve_operation(merged_kwargs, setter_name, setter_type) custom_func_op = self._resolve_operation(merged_kwargs_custom, custom_func_name, custom_func_type, custom_command=True) if custom_func_name else None - _cli_generic_update_command( - self.command_loader, - '{} {}'.format(self.group_name, name), + command_name = '{} {}'.format(self.group_name, name) if self.group_name else name + command_operation = GenericUpdateCommandOperation( + ctx=self.command_loader, getter_op=getter_op, setter_op=setter_op, setter_arg_name=setter_arg_name, @@ -1303,7 +1317,11 @@ def generic_update_command(self, name, getter_name='get', getter_type=None, child_collection_prop_name=child_collection_prop_name, child_collection_key=child_collection_key, child_arg_name=child_arg_name, - **merged_kwargs) + **merged_kwargs + ) + self.command_loader.add_cli_command(command_name, + command_operation=command_operation, + **merged_kwargs) def wait_command(self, name, getter_name='get', **kwargs): self._wait_command(name, getter_name=getter_name, custom_command=False, **kwargs) @@ -1315,7 +1333,7 @@ def generic_wait_command(self, name, getter_name='get', getter_type=None, **kwar self._wait_command(name, getter_name=getter_name, getter_type=getter_type, **kwargs) def _wait_command(self, name, getter_name='get', getter_type=None, custom_command=False, **kwargs): - from azure.cli.core.commands.arm import _cli_wait_command + from azure.cli.core.commands.command_operation import WaitCommandOperation self._check_stale() merged_kwargs = self._flatten_kwargs(kwargs, get_command_type_kwarg(custom_command)) self._apply_tags(merged_kwargs, kwargs, name) @@ -1323,8 +1341,16 @@ def _wait_command(self, name, getter_name='get', getter_type=None, custom_comman if getter_type: merged_kwargs = _merge_kwargs(getter_type.settings, merged_kwargs, CLI_COMMAND_KWARGS) getter_op = self._resolve_operation(merged_kwargs, getter_name, getter_type, custom_command=custom_command) - _cli_wait_command(self.command_loader, '{} {}'.format(self.group_name, name), getter_op=getter_op, - custom_command=custom_command, **merged_kwargs) + + command_name = '{} {}'.format(self.group_name, name) if self.group_name else name + command_operation = WaitCommandOperation( + ctx=self.command_loader, + operation=getter_op, + **merged_kwargs + ) + self.command_loader.add_cli_command(command_name, + command_operation=command_operation, + **merged_kwargs) def show_command(self, name, getter_name='get', **kwargs): self._show_command(name, getter_name=getter_name, custom_command=False, **kwargs) @@ -1333,16 +1359,24 @@ def custom_show_command(self, name, getter_name='get', **kwargs): self._show_command(name, getter_name=getter_name, custom_command=True, **kwargs) def _show_command(self, name, getter_name='get', getter_type=None, custom_command=False, **kwargs): - from azure.cli.core.commands.arm import _cli_show_command + from azure.cli.core.commands.command_operation import ShowCommandOperation self._check_stale() merged_kwargs = self._flatten_kwargs(kwargs, get_command_type_kwarg(custom_command)) self._apply_tags(merged_kwargs, kwargs, name) if getter_type: merged_kwargs = _merge_kwargs(getter_type.settings, merged_kwargs, CLI_COMMAND_KWARGS) - getter_op = self._resolve_operation(merged_kwargs, getter_name, getter_type, custom_command=custom_command) - _cli_show_command(self.command_loader, '{} {}'.format(self.group_name, name), getter_op=getter_op, - custom_command=custom_command, **merged_kwargs) + operation = self._resolve_operation(merged_kwargs, getter_name, getter_type, custom_command=custom_command) + + command_name = '{} {}'.format(self.group_name, name) if self.group_name else name + command_operation = ShowCommandOperation( + ctx=self.command_loader, + operation=operation, + **merged_kwargs + ) + self.command_loader.add_cli_command(command_name, + command_operation=command_operation, + **merged_kwargs) def _apply_tags(self, merged_kwargs, kwargs, command_name): # don't inherit deprecation or preview info from command group diff --git a/src/azure-cli-core/azure/cli/core/commands/arm.py b/src/azure-cli-core/azure/cli/core/commands/arm.py index a1b6c7cb8da..a48078aafc5 100644 --- a/src/azure-cli-core/azure/cli/core/commands/arm.py +++ b/src/azure-cli-core/azure/cli/core/commands/arm.py @@ -7,21 +7,19 @@ import argparse from collections import OrderedDict -import copy import json import re -from azure.cli.core import AzCommandsLoader, EXCLUDED_PARAMS -from azure.cli.core.commands import LongRunningOperation, _is_poller, cached_get, cached_put +from azure.cli.core import EXCLUDED_PARAMS +from azure.cli.core.commands import LongRunningOperation from azure.cli.core.commands.client_factory import get_mgmt_service_client from azure.cli.core.commands.events import EVENT_INVOKER_PRE_LOAD_ARGUMENTS from azure.cli.core.commands.validators import IterateValue -from azure.cli.core.util import ( - shell_safe_json_parse, augment_no_wait_handler_args, get_command_type_kwarg, find_child_item) +from azure.cli.core.util import shell_safe_json_parse, get_command_type_kwarg from azure.cli.core.profiles import ResourceType, get_sdk from knack.arguments import CLICommandArgument, ignore_type -from knack.introspection import extract_args_from_signature, extract_full_summary_from_signature +from knack.introspection import extract_args_from_signature from knack.log import get_logger from knack.util import todict, CLIError @@ -406,358 +404,6 @@ def get_arguments_loader(context, getter_op, cmd_args=None, operation_group=None return cmd_args -# pylint: disable=too-many-statements -def _cli_generic_update_command(context, name, getter_op, setter_op, setter_arg_name='parameters', - child_collection_prop_name=None, child_collection_key='name', - child_arg_name='item_name', custom_function_op=None, **kwargs): - if not isinstance(context, AzCommandsLoader): - raise TypeError("'context' expected type '{}'. Got: '{}'".format(AzCommandsLoader.__name__, type(context))) - if not isinstance(getter_op, str): - raise TypeError("Getter operation must be a string. Got '{}'".format(getter_op)) - if not isinstance(setter_op, str): - raise TypeError("Setter operation must be a string. Got '{}'".format(setter_op)) - if custom_function_op and not isinstance(custom_function_op, str): - raise TypeError("Custom function operation must be a string. Got '{}'".format( - custom_function_op)) - - def set_arguments_loader(): - return dict(extract_args_from_signature(context.get_op_handler( - setter_op, operation_group=kwargs.get('operation_group')), excluded_params=EXCLUDED_PARAMS)) - - def function_arguments_loader(): - if not custom_function_op: - return {} - - custom_op = context.get_op_handler(custom_function_op) - context._apply_doc_string(custom_op, kwargs) # pylint: disable=protected-access - return dict(extract_args_from_signature(custom_op, excluded_params=EXCLUDED_PARAMS)) - - def generic_update_arguments_loader(): - arguments = get_arguments_loader(context, getter_op, operation_group=kwargs.get('operation_group')) - arguments.update(set_arguments_loader()) - arguments.update(function_arguments_loader()) - arguments.pop('instance', None) # inherited from custom_function(instance, ...) - arguments.pop('parent', None) - arguments.pop('expand', None) # possibly inherited from the getter - arguments.pop(setter_arg_name, None) - - # Add the generic update parameters - class OrderedArgsAction(argparse.Action): # pylint:disable=too-few-public-methods - - def __call__(self, parser, namespace, values, option_string=None): - if not getattr(namespace, 'ordered_arguments', None): - setattr(namespace, 'ordered_arguments', []) - namespace.ordered_arguments.append((option_string, values)) - - group_name = 'Generic Update' - arguments['properties_to_set'] = CLICommandArgument( - 'properties_to_set', options_list=['--set'], nargs='+', - action=OrderedArgsAction, default=[], - help='Update an object by specifying a property path and value to set. Example: {}'.format(set_usage), - metavar='KEY=VALUE', arg_group=group_name - ) - arguments['properties_to_add'] = CLICommandArgument( - 'properties_to_add', options_list=['--add'], nargs='+', - action=OrderedArgsAction, default=[], - help='Add an object to a list of objects by specifying a path and ' - 'key value pairs. Example: {}'.format(add_usage), - metavar='LIST KEY=VALUE', arg_group=group_name - ) - arguments['properties_to_remove'] = CLICommandArgument( - 'properties_to_remove', options_list=['--remove'], nargs='+', - action=OrderedArgsAction, default=[], - help='Remove a property or an element from a list. Example: {}'.format(remove_usage), - metavar='LIST INDEX', arg_group=group_name - ) - arguments['force_string'] = CLICommandArgument( - 'force_string', action='store_true', arg_group=group_name, - help="When using 'set' or 'add', preserve string literals instead of attempting to convert to JSON." - ) - return [(k, v) for k, v in arguments.items()] - - def _extract_handler_and_args(args, commmand_kwargs, op, context): - from azure.cli.core.commands.client_factory import resolve_client_arg_name - factory = _get_client_factory(name, **commmand_kwargs) - client = None - if factory: - try: - client = factory(context.cli_ctx) - except TypeError: - client = factory(context.cli_ctx, args) - - client_arg_name = resolve_client_arg_name(op, kwargs) - op_handler = context.get_op_handler(op, operation_group=kwargs.get('operation_group')) - raw_args = dict(extract_args_from_signature(op_handler, excluded_params=EXCLUDED_NON_CLIENT_PARAMS)) - op_args = {key: val for key, val in args.items() if key in raw_args} - if client_arg_name in raw_args: - op_args[client_arg_name] = client - return op_handler, op_args - - def handler(args): # pylint: disable=too-many-branches,too-many-statements - cmd = args.get('cmd') - context_copy = copy.copy(context) - context_copy.cli_ctx = cmd.cli_ctx - force_string = args.get('force_string', False) - ordered_arguments = args.pop('ordered_arguments', []) - dest_names = child_arg_name.split('.') - child_names = [args.get(key, None) for key in dest_names] - for item in ['properties_to_add', 'properties_to_set', 'properties_to_remove']: - if args[item]: - raise CLIError("Unexpected '{}' was not empty.".format(item)) - del args[item] - - getter, getterargs = _extract_handler_and_args(args, cmd.command_kwargs, getter_op, context_copy) - - if child_collection_prop_name: - parent = cached_get(cmd, getter, **getterargs) - instance = find_child_item( - parent, *child_names, path=child_collection_prop_name, key_path=child_collection_key) - else: - parent = None - instance = cached_get(cmd, getter, **getterargs) - - # pass instance to the custom_function, if provided - if custom_function_op: - custom_function, custom_func_args = _extract_handler_and_args( - args, cmd.command_kwargs, custom_function_op, context_copy) - if child_collection_prop_name: - parent = custom_function(instance=instance, parent=parent, **custom_func_args) - else: - instance = custom_function(instance=instance, **custom_func_args) - - # apply generic updates after custom updates - setter, setterargs = _extract_handler_and_args(args, cmd.command_kwargs, setter_op, context_copy) - - for arg in ordered_arguments: - arg_type, arg_values = arg - if arg_type == '--set': - try: - for expression in arg_values: - set_properties(instance, expression, force_string) - except ValueError: - raise CLIError('invalid syntax: {}'.format(set_usage)) - elif arg_type == '--add': - try: - add_properties(instance, arg_values, force_string) - except ValueError: - raise CLIError('invalid syntax: {}'.format(add_usage)) - elif arg_type == '--remove': - try: - remove_properties(instance, arg_values) - except ValueError: - raise CLIError('invalid syntax: {}'.format(remove_usage)) - - # Done... update the instance! - setterargs[setter_arg_name] = parent if child_collection_prop_name else instance - - # Handle no-wait - supports_no_wait = cmd.command_kwargs.get('supports_no_wait', None) - if supports_no_wait: - no_wait_enabled = args.get('no_wait', False) - augment_no_wait_handler_args(no_wait_enabled, - setter, - setterargs) - else: - no_wait_param = cmd.command_kwargs.get('no_wait_param', None) - if no_wait_param: - setterargs[no_wait_param] = args[no_wait_param] - - if setter_arg_name == 'parameters': - result = cached_put(cmd, setter, **setterargs) - else: - result = cached_put(cmd, setter, setterargs[setter_arg_name], setter_arg_name=setter_arg_name, **setterargs) - - if supports_no_wait and no_wait_enabled: - return None - - no_wait_param = cmd.command_kwargs.get('no_wait_param', None) - if no_wait_param and setterargs.get(no_wait_param, None): - return None - - if _is_poller(result): - result = result.result() - - if child_collection_prop_name: - result = find_child_item( - result, *child_names, path=child_collection_prop_name, key_path=child_collection_key) - return result - - context._cli_command(name, handler=handler, argument_loader=generic_update_arguments_loader, **kwargs) # pylint: disable=protected-access - - -def _cli_wait_command(context, name, getter_op, custom_command=False, **kwargs): - - if not isinstance(getter_op, str): - raise ValueError("Getter operation must be a string. Got '{}'".format(type(getter_op))) - - factory = _get_client_factory(name, custom_command=custom_command, **kwargs) - - def generic_wait_arguments_loader(): - cmd_args = get_arguments_loader(context, getter_op, operation_group=kwargs.get('operation_group')) - - group_name = 'Wait Condition' - cmd_args['timeout'] = CLICommandArgument( - 'timeout', options_list=['--timeout'], default=3600, arg_group=group_name, type=int, - help='maximum wait in seconds' - ) - cmd_args['interval'] = CLICommandArgument( - 'interval', options_list=['--interval'], default=30, arg_group=group_name, type=int, - help='polling interval in seconds' - ) - cmd_args['deleted'] = CLICommandArgument( - 'deleted', options_list=['--deleted'], action='store_true', arg_group=group_name, - help='wait until deleted' - ) - cmd_args['created'] = CLICommandArgument( - 'created', options_list=['--created'], action='store_true', arg_group=group_name, - help="wait until created with 'provisioningState' at 'Succeeded'" - ) - cmd_args['updated'] = CLICommandArgument( - 'updated', options_list=['--updated'], action='store_true', arg_group=group_name, - help="wait until updated with provisioningState at 'Succeeded'" - ) - cmd_args['exists'] = CLICommandArgument( - 'exists', options_list=['--exists'], action='store_true', arg_group=group_name, - help="wait until the resource exists" - ) - cmd_args['custom'] = CLICommandArgument( - 'custom', options_list=['--custom'], arg_group=group_name, - help="Wait until the condition satisfies a custom JMESPath query. E.g. " - "provisioningState!='InProgress', " - "instanceView.statuses[?code=='PowerState/running']" - ) - return [(k, v) for k, v in cmd_args.items()] - - def get_provisioning_state(instance): - provisioning_state = getattr(instance, 'provisioning_state', None) - if not provisioning_state: - # some SDK, like resource-group, has 'provisioning_state' under 'properties' - properties = getattr(instance, 'properties', None) - if properties: - provisioning_state = getattr(properties, 'provisioning_state', None) - # some SDK, like keyvault, has 'provisioningState' under 'properties.additional_properties' - if not provisioning_state: - additional_properties = getattr(properties, 'additional_properties', {}) - provisioning_state = additional_properties.get('provisioningState') - return provisioning_state - - def handler(args): - from azure.cli.core.commands.client_factory import resolve_client_arg_name - from msrest.exceptions import ClientException - from azure.core.exceptions import HttpResponseError - import time - - context_copy = copy.copy(context) - getter_args = dict(extract_args_from_signature(context.get_op_handler( - getter_op, operation_group=kwargs.get('operation_group')), excluded_params=EXCLUDED_NON_CLIENT_PARAMS)) - cmd = args.get('cmd') if 'cmd' in getter_args else args.pop('cmd') - context_copy.cli_ctx = cmd.cli_ctx - operations_tmpl = _get_operations_tmpl(cmd, custom_command=custom_command) - client_arg_name = resolve_client_arg_name(operations_tmpl, kwargs) - try: - client = factory(context_copy.cli_ctx) if factory else None - except TypeError: - client = factory(context_copy.cli_ctx, args) if factory else None - if client and (client_arg_name in getter_args): - args[client_arg_name] = client - - getter = context_copy.get_op_handler(getter_op, operation_group=kwargs.get('operation_group')) - - timeout = args.pop('timeout') - interval = args.pop('interval') - wait_for_created = args.pop('created') - wait_for_deleted = args.pop('deleted') - wait_for_updated = args.pop('updated') - wait_for_exists = args.pop('exists') - custom_condition = args.pop('custom') - if not any([wait_for_created, wait_for_updated, wait_for_deleted, - wait_for_exists, custom_condition]): - raise CLIError( - "incorrect usage: --created | --updated | --deleted | --exists | --custom JMESPATH") - - progress_indicator = context_copy.cli_ctx.get_progress_controller() - progress_indicator.begin() - for _ in range(0, timeout, interval): - try: - progress_indicator.add(message='Waiting') - instance = getter(**args) - if wait_for_exists: - progress_indicator.end() - return None - provisioning_state = get_provisioning_state(instance) - # until we have any needs to wait for 'Failed', let us bail out on this - if provisioning_state: - provisioning_state = provisioning_state.lower() - if provisioning_state == 'failed': - progress_indicator.stop() - raise CLIError('The operation failed') - if ((wait_for_created or wait_for_updated) and provisioning_state == 'succeeded') or \ - custom_condition and bool(verify_property(instance, custom_condition)): - progress_indicator.end() - return None - except (ClientException, HttpResponseError) as ex: - progress_indicator.stop() - if getattr(ex, 'status_code', None) == 404: - if wait_for_deleted: - return None - if not any([wait_for_created, wait_for_exists, custom_condition]): - raise - else: - raise - except Exception: # pylint: disable=broad-except - progress_indicator.stop() - raise - - time.sleep(interval) - - progress_indicator.end() - return CLIError('Wait operation timed-out after {} seconds'.format(timeout)) - - context._cli_command(name, handler=handler, argument_loader=generic_wait_arguments_loader, **kwargs) # pylint: disable=protected-access - - -def _cli_show_command(context, name, getter_op, custom_command=False, **kwargs): - - if not isinstance(getter_op, str): - raise ValueError("Getter operation must be a string. Got '{}'".format(type(getter_op))) - - factory = _get_client_factory(name, custom_command=custom_command, **kwargs) - - def generic_show_arguments_loader(): - cmd_args = get_arguments_loader(context, getter_op, operation_group=kwargs.get('operation_group')) - return [(k, v) for k, v in cmd_args.items()] - - def description_loader(): - return extract_full_summary_from_signature( - context.get_op_handler(getter_op, operation_group=kwargs.get('operation_group'))) - - def handler(args): - from azure.cli.core.commands.client_factory import resolve_client_arg_name - context_copy = copy.copy(context) - getter_args = dict(extract_args_from_signature( - context_copy.get_op_handler(getter_op, operation_group=kwargs.get('operation_group')), - excluded_params=EXCLUDED_NON_CLIENT_PARAMS)) - cmd = args.get('cmd') if 'cmd' in getter_args else args.pop('cmd') - context_copy.cli_ctx = cmd.cli_ctx - operations_tmpl = _get_operations_tmpl(cmd, custom_command=custom_command) - client_arg_name = resolve_client_arg_name(operations_tmpl, kwargs) - try: - client = factory(context_copy.cli_ctx) if factory else None - except TypeError: - client = factory(context_copy.cli_ctx, args) if factory else None - - if client and (client_arg_name in getter_args): - args[client_arg_name] = client - - getter = context_copy.get_op_handler(getter_op, operation_group=kwargs.get('operation_group')) - try: - return getter(**args) - except Exception as ex: # pylint: disable=broad-except - show_exception_handler(ex) - context._cli_command(name, handler=handler, argument_loader=generic_show_arguments_loader, # pylint: disable=protected-access - description_loader=description_loader, **kwargs) - - def show_exception_handler(ex): if getattr(getattr(ex, 'response', ex), 'status_code', None) == 404: import sys diff --git a/src/azure-cli-core/azure/cli/core/commands/command_operation.py b/src/azure-cli-core/azure/cli/core/commands/command_operation.py new file mode 100644 index 00000000000..fdc5b76459a --- /dev/null +++ b/src/azure-cli-core/azure/cli/core/commands/command_operation.py @@ -0,0 +1,489 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +import argparse +from six import string_types, get_method_function +from azure.cli.core import AzCommandsLoader, EXCLUDED_PARAMS +from knack.introspection import extract_args_from_signature, extract_full_summary_from_signature +from knack.arguments import CLICommandArgument, ignore_type + + +class BaseCommandOperation: + + def __init__(self, ctx, **kwargs): + if not isinstance(ctx, AzCommandsLoader): + raise TypeError("'ctx' expected type '{}'. Got: '{}'".format(AzCommandsLoader.__name__, type(ctx))) + self.ctx = ctx + self.cmd = None + self.kwargs = kwargs + self.client_factory = kwargs.get('client_factory') + self.operation_group = kwargs.get('operation_group') + + @property + def cli_ctx(self): + return self.cmd.cli_ctx if self.cmd else self.ctx.cli_ctx + + def handler(self, command_args): + raise NotImplementedError() + + def arguments_loader(self): + raise NotImplementedError() + + def description_loader(self): + raise NotImplementedError() + + def get_op_handler(self, operation): + """ Import and load the operation handler """ + # Patch the unversioned sdk path to include the appropriate API version for the + # resource type in question. + from importlib import import_module + import types + + from azure.cli.core.profiles import AZURE_API_PROFILES + from azure.cli.core.profiles._shared import get_versioned_sdk_path + + for rt in AZURE_API_PROFILES[self.cli_ctx.cloud.profile]: + if operation.startswith(rt.import_prefix + '.'): + operation = operation.replace(rt.import_prefix, + get_versioned_sdk_path(self.cli_ctx.cloud.profile, rt, + operation_group=self.operation_group)) + + try: + mod_to_import, attr_path = operation.split('#') + handler = import_module(mod_to_import) + for part in attr_path.split('.'): + handler = getattr(handler, part) + if isinstance(handler, types.FunctionType): + return handler + return get_method_function(handler) + except (ValueError, AttributeError): + raise ValueError("The operation '{}' is invalid.".format(operation)) + + def load_getter_op_arguments(self, getter_op, cmd_args=None): + op = self.get_op_handler(getter_op) + getter_args = dict( + extract_args_from_signature(op, excluded_params=EXCLUDED_PARAMS)) + cmd_args = cmd_args or {} + cmd_args.update(getter_args) + cmd_args['cmd'] = CLICommandArgument('cmd', arg_type=ignore_type) + return cmd_args + + def apply_doc_string(self, handler): + return self.ctx._apply_doc_string(handler, self.kwargs) # pylint: disable=protected-access + + def load_op_description(self, handler=None): + if handler is None: + def default_handler(): + """""" # default_handler should have __doc__ property + handler = default_handler + self.apply_doc_string(handler) + return extract_full_summary_from_signature(handler) + + def resolve_client_arg_name(self, operation): + from azure.cli.core.commands.client_factory import resolve_client_arg_name + return resolve_client_arg_name(operation, self.kwargs) + + +class CommandOperation(BaseCommandOperation): + + def __init__(self, ctx, operation, **kwargs): + if not isinstance(operation, string_types): + raise TypeError("Operation must be a string. Got '{}'".format(operation)) + super(CommandOperation, self).__init__(ctx, **kwargs) + self.operation = operation + + def handler(self, command_args): + from azure.cli.core.util import get_arg_list, augment_no_wait_handler_args + + op = self.get_op_handler(self.operation) + op_args = get_arg_list(op) + cmd = command_args.get('cmd') if 'cmd' in op_args else command_args.pop('cmd') + + client = self.client_factory(cmd.cli_ctx, command_args) if self.client_factory else None + supports_no_wait = self.kwargs.get('supports_no_wait', None) + if supports_no_wait: + no_wait_enabled = command_args.pop('no_wait', False) + augment_no_wait_handler_args(no_wait_enabled, op, command_args) + if client: + client_arg_name = self.resolve_client_arg_name(self.operation) + if client_arg_name in op_args: + command_args[client_arg_name] = client + return op(**command_args) + + def arguments_loader(self): + op = self.get_op_handler(self.operation) + self.apply_doc_string(op) + cmd_args = list(extract_args_from_signature(op, excluded_params=self.ctx.excluded_command_handler_args)) + return cmd_args + + def description_loader(self): + op = self.get_op_handler(self.operation) + return self.load_op_description(op) + + +class WaitCommandOperation(BaseCommandOperation): + + def __init__(self, ctx, operation, **kwargs): + if not isinstance(operation, string_types): + raise TypeError("operation must be a string. Got '{}'".format(operation)) + super(WaitCommandOperation, self).__init__(ctx, **kwargs) + self.operation = operation + + def handler(self, command_args): # pylint: disable=too-many-statements + from msrest.exceptions import ClientException + from azure.core.exceptions import HttpResponseError + from knack.util import CLIError + from azure.cli.core.commands.arm import EXCLUDED_NON_CLIENT_PARAMS, verify_property + + import time + + op = self.get_op_handler(self.operation) + + getter_args = dict(extract_args_from_signature(op, excluded_params=EXCLUDED_NON_CLIENT_PARAMS)) + self.cmd = command_args.get('cmd') if 'cmd' in getter_args else command_args.pop('cmd') + + client_arg_name = self.resolve_client_arg_name(self.operation) + try: + client = self.client_factory(self.cli_ctx) if self.client_factory else None + except TypeError: + client = self.client_factory(self.cli_ctx, command_args) if self.client_factory else None + if client and (client_arg_name in getter_args): + command_args[client_arg_name] = client + + getter = self.get_op_handler(self.operation) + + timeout = command_args.pop('timeout') + interval = command_args.pop('interval') + wait_for_created = command_args.pop('created') + wait_for_deleted = command_args.pop('deleted') + wait_for_updated = command_args.pop('updated') + wait_for_exists = command_args.pop('exists') + custom_condition = command_args.pop('custom') + if not any([wait_for_created, wait_for_updated, wait_for_deleted, + wait_for_exists, custom_condition]): + raise CLIError( + "incorrect usage: --created | --updated | --deleted | --exists | --custom JMESPATH") + + progress_indicator = self.cli_ctx.get_progress_controller() + progress_indicator.begin() + for _ in range(0, timeout, interval): + try: + progress_indicator.add(message='Waiting') + instance = getter(**command_args) + if wait_for_exists: + progress_indicator.end() + return None + provisioning_state = self._get_provisioning_state(instance) + # until we have any needs to wait for 'Failed', let us bail out on this + if provisioning_state: + provisioning_state = provisioning_state.lower() + if provisioning_state == 'failed': + progress_indicator.stop() + raise CLIError('The operation failed') + if ((wait_for_created or wait_for_updated) and provisioning_state == 'succeeded') or \ + custom_condition and bool(verify_property(instance, custom_condition)): + progress_indicator.end() + return None + except (ClientException, HttpResponseError) as ex: + progress_indicator.stop() + if getattr(ex, 'status_code', None) == 404: + if wait_for_deleted: + return None + if not any([wait_for_created, wait_for_exists, custom_condition]): + raise + else: + raise + except Exception: # pylint: disable=broad-except + progress_indicator.stop() + raise + + time.sleep(interval) + + progress_indicator.end() + return CLIError('Wait operation timed-out after {} seconds'.format(timeout)) + + @staticmethod + def _get_provisioning_state(instance): + provisioning_state = getattr(instance, 'provisioning_state', None) + if not provisioning_state: + # some SDK, like resource-group, has 'provisioning_state' under 'properties' + properties = getattr(instance, 'properties', None) + if properties: + provisioning_state = getattr(properties, 'provisioning_state', None) + # some SDK, like keyvault, has 'provisioningState' under 'properties.additional_properties' + if not provisioning_state: + additional_properties = getattr(properties, 'additional_properties', {}) + provisioning_state = additional_properties.get('provisioningState') + return provisioning_state + + def arguments_loader(self): + cmd_args = self.load_getter_op_arguments(self.operation) + + group_name = 'Wait Condition' + cmd_args['timeout'] = CLICommandArgument( + 'timeout', options_list=['--timeout'], default=3600, arg_group=group_name, type=int, + help='maximum wait in seconds' + ) + cmd_args['interval'] = CLICommandArgument( + 'interval', options_list=['--interval'], default=30, arg_group=group_name, type=int, + help='polling interval in seconds' + ) + cmd_args['deleted'] = CLICommandArgument( + 'deleted', options_list=['--deleted'], action='store_true', arg_group=group_name, + help='wait until deleted' + ) + cmd_args['created'] = CLICommandArgument( + 'created', options_list=['--created'], action='store_true', arg_group=group_name, + help="wait until created with 'provisioningState' at 'Succeeded'" + ) + cmd_args['updated'] = CLICommandArgument( + 'updated', options_list=['--updated'], action='store_true', arg_group=group_name, + help="wait until updated with provisioningState at 'Succeeded'" + ) + cmd_args['exists'] = CLICommandArgument( + 'exists', options_list=['--exists'], action='store_true', arg_group=group_name, + help="wait until the resource exists" + ) + cmd_args['custom'] = CLICommandArgument( + 'custom', options_list=['--custom'], arg_group=group_name, + help="Wait until the condition satisfies a custom JMESPath query. E.g. " + "provisioningState!='InProgress', " + "instanceView.statuses[?code=='PowerState/running']" + ) + return [(k, v) for k, v in cmd_args.items()] + + def description_loader(self): + return self.load_op_description() + + +class ShowCommandOperation(BaseCommandOperation): + + def __init__(self, ctx, operation, **kwargs): + if not isinstance(operation, string_types): + raise TypeError("operation must be a string. Got '{}'".format(operation)) + super(ShowCommandOperation, self).__init__(ctx, **kwargs) + self.operation = operation + + def handler(self, command_args): + from azure.cli.core.commands.arm import show_exception_handler, EXCLUDED_NON_CLIENT_PARAMS + + op = self.get_op_handler(self.operation) + getter_args = dict(extract_args_from_signature(op, excluded_params=EXCLUDED_NON_CLIENT_PARAMS)) + + self.cmd = command_args.get('cmd') if 'cmd' in getter_args else command_args.pop('cmd') + + client_arg_name = self.resolve_client_arg_name(self.operation) + try: + client = self.client_factory(self.cli_ctx) if self.client_factory else None + except TypeError: + client = self.client_factory(self.cli_ctx, command_args) if self.client_factory else None + + if client and (client_arg_name in getter_args): + command_args[client_arg_name] = client + + op = self.get_op_handler(self.operation) + try: + return op(**command_args) + except Exception as ex: # pylint: disable=broad-except + show_exception_handler(ex) + + def arguments_loader(self): + cmd_args = self.load_getter_op_arguments(self.operation) + return [(k, v) for k, v in cmd_args.items()] + + def description_loader(self): + op = self.get_op_handler(self.operation) + return self.load_op_description(op) + + +class GenericUpdateCommandOperation(BaseCommandOperation): # pylint: disable=too-many-instance-attributes + + class OrderedArgsAction(argparse.Action): # pylint:disable=too-few-public-methods + def __call__(self, parser, namespace, values, option_string=None): + if not getattr(namespace, 'ordered_arguments', None): + setattr(namespace, 'ordered_arguments', []) + namespace.ordered_arguments.append((option_string, values)) + + def __init__(self, ctx, getter_op, setter_op, setter_arg_name, custom_function_op, child_collection_prop_name, + child_collection_key, child_arg_name, **kwargs): + if not isinstance(getter_op, string_types): + raise TypeError("Getter operation must be a string. Got '{}'".format(getter_op)) + if not isinstance(setter_op, string_types): + raise TypeError("Setter operation must be a string. Got '{}'".format(setter_op)) + if custom_function_op and not isinstance(custom_function_op, string_types): + raise TypeError("Custom function operation must be a string. Got '{}'".format(custom_function_op)) + super(GenericUpdateCommandOperation, self).__init__(ctx, **kwargs) + + self.getter_operation = getter_op + self.setter_operation = setter_op + self.custom_function_operation = custom_function_op + self.setter_arg_name = setter_arg_name + self.child_collection_prop_name = child_collection_prop_name + self.child_collection_key = child_collection_key + self.child_arg_name = child_arg_name + + def handler(self, command_args): # pylint: disable=too-many-locals, too-many-statements, too-many-branches + from knack.util import CLIError + from azure.cli.core.commands import cached_get, cached_put, _is_poller + from azure.cli.core.util import find_child_item, augment_no_wait_handler_args + from azure.cli.core.commands.arm import add_usage, remove_usage, set_usage,\ + add_properties, remove_properties, set_properties + + self.cmd = command_args.get('cmd') + + force_string = command_args.get('force_string', False) + ordered_arguments = command_args.pop('ordered_arguments', []) + dest_names = self.child_arg_name.split('.') + child_names = [command_args.get(key, None) for key in dest_names] + for item in ['properties_to_add', 'properties_to_set', 'properties_to_remove']: + if command_args[item]: + raise CLIError("Unexpected '{}' was not empty.".format(item)) + del command_args[item] + + getter, getterargs = self._extract_handler_and_args(command_args, self.getter_operation) + + if self.child_collection_prop_name: + parent = cached_get(self.cmd, getter, **getterargs) + instance = find_child_item( + parent, *child_names, path=self.child_collection_prop_name, key_path=self.child_collection_key) + else: + parent = None + instance = cached_get(self.cmd, getter, **getterargs) + + # pass instance to the custom_function, if provided + if self.custom_function_operation: + custom_function, custom_func_args = self._extract_handler_and_args( + command_args, self.custom_function_operation) + if self.child_collection_prop_name: + parent = custom_function(instance=instance, parent=parent, **custom_func_args) + else: + instance = custom_function(instance=instance, **custom_func_args) + + # apply generic updates after custom updates + setter, setterargs = self._extract_handler_and_args(command_args, self.setter_operation) + + for arg in ordered_arguments: + arg_type, arg_values = arg + if arg_type == '--set': + try: + for expression in arg_values: + set_properties(instance, expression, force_string) + except ValueError: + raise CLIError('invalid syntax: {}'.format(set_usage)) + elif arg_type == '--add': + try: + add_properties(instance, arg_values, force_string) + except ValueError: + raise CLIError('invalid syntax: {}'.format(add_usage)) + elif arg_type == '--remove': + try: + remove_properties(instance, arg_values) + except ValueError: + raise CLIError('invalid syntax: {}'.format(remove_usage)) + + # Done... update the instance! + setterargs[self.setter_arg_name] = parent if self.child_collection_prop_name else instance + + # Handle no-wait + supports_no_wait = self.cmd.command_kwargs.get('supports_no_wait', None) + if supports_no_wait: + no_wait_enabled = command_args.get('no_wait', False) + augment_no_wait_handler_args(no_wait_enabled, + setter, + setterargs) + else: + no_wait_param = self.cmd.command_kwargs.get('no_wait_param', None) + if no_wait_param: + setterargs[no_wait_param] = command_args[no_wait_param] + + if self.setter_arg_name == 'parameters': + result = cached_put(self.cmd, setter, **setterargs) + else: + result = cached_put(self.cmd, setter, setterargs[self.setter_arg_name], + setter_arg_name=self.setter_arg_name, **setterargs) + + if supports_no_wait and no_wait_enabled: + return None + + no_wait_param = self.cmd.command_kwargs.get('no_wait_param', None) + if no_wait_param and setterargs.get(no_wait_param, None): + return None + + if _is_poller(result): + result = result.result() + + if self.child_collection_prop_name: + result = find_child_item( + result, *child_names, path=self.child_collection_prop_name, key_path=self.child_collection_key) + return result + + def _extract_handler_and_args(self, args, operation): + from azure.cli.core.commands.arm import EXCLUDED_NON_CLIENT_PARAMS + + client = None + if self.client_factory: + try: + client = self.client_factory(self.cli_ctx) + except TypeError: + client = self.client_factory(self.cli_ctx, args) + + client_arg_name = self.resolve_client_arg_name(operation) + op = self.get_op_handler(operation) + raw_args = dict(extract_args_from_signature(op, excluded_params=EXCLUDED_NON_CLIENT_PARAMS)) + op_args = {key: val for key, val in args.items() if key in raw_args} + if client_arg_name in raw_args: + op_args[client_arg_name] = client + return op, op_args + + def arguments_loader(self): + from azure.cli.core.commands.arm import set_usage, add_usage, remove_usage + + arguments = self.load_getter_op_arguments(self.getter_operation) + arguments.update(self._set_arguments_loader()) + arguments.update(self._function_arguments_loader()) + arguments.pop('instance', None) # inherited from custom_function(instance, ...) + arguments.pop('parent', None) + arguments.pop('expand', None) # possibly inherited from the getter + arguments.pop(self.setter_arg_name, None) + + # Add the generic update parameters + group_name = 'Generic Update' + arguments['properties_to_set'] = CLICommandArgument( + 'properties_to_set', options_list=['--set'], nargs='+', + action=self.OrderedArgsAction, default=[], + help='Update an object by specifying a property path and value to set. Example: {}'.format(set_usage), + metavar='KEY=VALUE', arg_group=group_name + ) + arguments['properties_to_add'] = CLICommandArgument( + 'properties_to_add', options_list=['--add'], nargs='+', + action=self.OrderedArgsAction, default=[], + help='Add an object to a list of objects by specifying a path and ' + 'key value pairs. Example: {}'.format(add_usage), + metavar='LIST KEY=VALUE', arg_group=group_name + ) + arguments['properties_to_remove'] = CLICommandArgument( + 'properties_to_remove', options_list=['--remove'], nargs='+', + action=self.OrderedArgsAction, default=[], + help='Remove a property or an element from a list. Example: {}'.format(remove_usage), + metavar='LIST INDEX', arg_group=group_name + ) + arguments['force_string'] = CLICommandArgument( + 'force_string', action='store_true', arg_group=group_name, + help="When using 'set' or 'add', preserve string literals instead of attempting to convert to JSON." + ) + return [(k, v) for k, v in arguments.items()] + + def _set_arguments_loader(self): + op = self.get_op_handler(self.setter_operation) + return dict(extract_args_from_signature(op, excluded_params=EXCLUDED_PARAMS)) + + def _function_arguments_loader(self): + if not self.custom_function_operation: + return {} + op = self.get_op_handler(self.custom_function_operation) + self.apply_doc_string(op) # pylint: disable=protected-access + return dict(extract_args_from_signature(op, excluded_params=EXCLUDED_PARAMS)) + + def description_loader(self): + return self.load_op_description() From 4514b1f48c83a72b627af6a9ae7a6b407d16f308 Mon Sep 17 00:00:00 2001 From: kai ru <69238381+kairu-ms@users.noreply.github.com> Date: Tue, 23 Mar 2021 14:06:12 +0800 Subject: [PATCH 2/7] rename and add comments --- src/azure-cli-core/azure/cli/core/__init__.py | 4 +- .../azure/cli/core/commands/__init__.py | 26 +-- .../cli/core/commands/command_operation.py | 168 ++++++++++-------- 3 files changed, 110 insertions(+), 88 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/__init__.py b/src/azure-cli-core/azure/cli/core/__init__.py index 77f9887159c..3a6277f850e 100644 --- a/src/azure-cli-core/azure/cli/core/__init__.py +++ b/src/azure-cli-core/azure/cli/core/__init__.py @@ -770,7 +770,8 @@ def command_group(self, group_name, command_type=None, **kwargs): def argument_context(self, scope, **kwargs): return self._argument_context_cls(self, scope, **kwargs) - # keyvault and batch are using this function + # Please use add_cli_command instead of _cli_command. + # Currently "keyvault" and "batch" modules are still rely on this function, so it cannot be removed now. def _cli_command(self, name, operation=None, handler=None, argument_loader=None, description_loader=None, **kwargs): from knack.deprecation import Deprecated @@ -831,6 +832,7 @@ def default_description_loader(): **kwargs) def add_cli_command(self, name, command_operation, **kwargs): + """Register a command in command_table with command operation provided""" from knack.deprecation import Deprecated from .commands.command_operation import BaseCommandOperation if not issubclass(type(command_operation), BaseCommandOperation): diff --git a/src/azure-cli-core/azure/cli/core/commands/__init__.py b/src/azure-cli-core/azure/cli/core/commands/__init__.py index 3beb0584f93..f486f675fcc 100644 --- a/src/azure-cli-core/azure/cli/core/commands/__init__.py +++ b/src/azure-cli-core/azure/cli/core/commands/__init__.py @@ -1257,12 +1257,12 @@ def _command(self, name, method_name, custom_command=False, **kwargs): self._apply_tags(merged_kwargs, kwargs, name) operations_tmpl = merged_kwargs['operations_tmpl'] - operation = operations_tmpl.format(method_name) + op_path = operations_tmpl.format(method_name) command_name = '{} {}'.format(self.group_name, name) if self.group_name else name command_operation = CommandOperation( ctx=self.command_loader, - operation=operation, + op_path=op_path, **merged_kwargs ) self.command_loader.add_cli_command(command_name, @@ -1303,17 +1303,17 @@ def generic_update_command(self, name, getter_name='get', getter_type=None, merged_kwargs_custom = self._flatten_kwargs(kwargs, get_command_type_kwarg(custom_command=True)) self._apply_tags(merged_kwargs, kwargs, name) - getter_op = self._resolve_operation(merged_kwargs, getter_name, getter_type) - setter_op = self._resolve_operation(merged_kwargs, setter_name, setter_type) - custom_func_op = self._resolve_operation(merged_kwargs_custom, custom_func_name, custom_func_type, - custom_command=True) if custom_func_name else None + getter_op_path = self._resolve_operation(merged_kwargs, getter_name, getter_type) + setter_op_path = self._resolve_operation(merged_kwargs, setter_name, setter_type) + custom_function_op_path = self._resolve_operation(merged_kwargs_custom, custom_func_name, custom_func_type, + custom_command=True) if custom_func_name else None command_name = '{} {}'.format(self.group_name, name) if self.group_name else name command_operation = GenericUpdateCommandOperation( ctx=self.command_loader, - getter_op=getter_op, - setter_op=setter_op, + getter_op_path=getter_op_path, + setter_op_path=setter_op_path, setter_arg_name=setter_arg_name, - custom_function_op=custom_func_op, + custom_function_op_path=custom_function_op_path, child_collection_prop_name=child_collection_prop_name, child_collection_key=child_collection_key, child_arg_name=child_arg_name, @@ -1340,12 +1340,12 @@ def _wait_command(self, name, getter_name='get', getter_type=None, custom_comman if getter_type: merged_kwargs = _merge_kwargs(getter_type.settings, merged_kwargs, CLI_COMMAND_KWARGS) - getter_op = self._resolve_operation(merged_kwargs, getter_name, getter_type, custom_command=custom_command) + getter_op_path = self._resolve_operation(merged_kwargs, getter_name, getter_type, custom_command=custom_command) command_name = '{} {}'.format(self.group_name, name) if self.group_name else name command_operation = WaitCommandOperation( ctx=self.command_loader, - operation=getter_op, + op_path=getter_op_path, **merged_kwargs ) self.command_loader.add_cli_command(command_name, @@ -1366,12 +1366,12 @@ def _show_command(self, name, getter_name='get', getter_type=None, custom_comman if getter_type: merged_kwargs = _merge_kwargs(getter_type.settings, merged_kwargs, CLI_COMMAND_KWARGS) - operation = self._resolve_operation(merged_kwargs, getter_name, getter_type, custom_command=custom_command) + op_path = self._resolve_operation(merged_kwargs, getter_name, getter_type, custom_command=custom_command) command_name = '{} {}'.format(self.group_name, name) if self.group_name else name command_operation = ShowCommandOperation( ctx=self.command_loader, - operation=operation, + op_path=op_path, **merged_kwargs ) self.command_loader.add_cli_command(command_name, diff --git a/src/azure-cli-core/azure/cli/core/commands/command_operation.py b/src/azure-cli-core/azure/cli/core/commands/command_operation.py index fdc5b76459a..c733f5d9e95 100644 --- a/src/azure-cli-core/azure/cli/core/commands/command_operation.py +++ b/src/azure-cli-core/azure/cli/core/commands/command_operation.py @@ -10,11 +10,12 @@ class BaseCommandOperation: + """ Base class of command operation classes """ def __init__(self, ctx, **kwargs): if not isinstance(ctx, AzCommandsLoader): raise TypeError("'ctx' expected type '{}'. Got: '{}'".format(AzCommandsLoader.__name__, type(ctx))) - self.ctx = ctx + self.ctx = ctx # AzCommandsLoader self.cmd = None self.kwargs = kwargs self.client_factory = kwargs.get('client_factory') @@ -22,19 +23,23 @@ def __init__(self, ctx, **kwargs): @property def cli_ctx(self): + """ Return the cli_ctx of command or commands loader """ return self.cmd.cli_ctx if self.cmd else self.ctx.cli_ctx def handler(self, command_args): + """ Callback function of CLICommand handler """ raise NotImplementedError() def arguments_loader(self): + """ Callback function of CLICommand arguments_loader """ raise NotImplementedError() def description_loader(self): + """ Callback function of CLICommand description_loader """ raise NotImplementedError() - def get_op_handler(self, operation): - """ Import and load the operation handler """ + def get_op_handler(self, op_path): + """ Import and load the operation handler by path """ # Patch the unversioned sdk path to include the appropriate API version for the # resource type in question. from importlib import import_module @@ -44,13 +49,13 @@ def get_op_handler(self, operation): from azure.cli.core.profiles._shared import get_versioned_sdk_path for rt in AZURE_API_PROFILES[self.cli_ctx.cloud.profile]: - if operation.startswith(rt.import_prefix + '.'): - operation = operation.replace(rt.import_prefix, - get_versioned_sdk_path(self.cli_ctx.cloud.profile, rt, - operation_group=self.operation_group)) + if op_path.startswith(rt.import_prefix + '.'): + op_path = op_path.replace(rt.import_prefix, + get_versioned_sdk_path(self.cli_ctx.cloud.profile, rt, + operation_group=self.operation_group)) try: - mod_to_import, attr_path = operation.split('#') + mod_to_import, attr_path = op_path.split('#') handler = import_module(mod_to_import) for part in attr_path.split('.'): handler = getattr(handler, part) @@ -58,79 +63,86 @@ def get_op_handler(self, operation): return handler return get_method_function(handler) except (ValueError, AttributeError): - raise ValueError("The operation '{}' is invalid.".format(operation)) + raise ValueError("The operation '{}' is invalid.".format(op_path)) - def load_getter_op_arguments(self, getter_op, cmd_args=None): - op = self.get_op_handler(getter_op) + def load_getter_op_arguments(self, getter_op_path, cmd_args=None): + """ Load arguments from function signature of getter command op """ + op = self.get_op_handler(getter_op_path) getter_args = dict( extract_args_from_signature(op, excluded_params=EXCLUDED_PARAMS)) cmd_args = cmd_args or {} cmd_args.update(getter_args) + # The cmd argument is required when calling self.handler function. cmd_args['cmd'] = CLICommandArgument('cmd', arg_type=ignore_type) return cmd_args def apply_doc_string(self, handler): return self.ctx._apply_doc_string(handler, self.kwargs) # pylint: disable=protected-access - def load_op_description(self, handler=None): + def load_op_handler_description(self, handler=None): + """ Load the description from function signature of command op """ if handler is None: def default_handler(): - """""" # default_handler should have __doc__ property + """""" # Use empty __doc__ property here, which is required in extract_full_summary_from_signature handler = default_handler self.apply_doc_string(handler) return extract_full_summary_from_signature(handler) - def resolve_client_arg_name(self, operation): + def resolve_client_arg_name(self, op_path): from azure.cli.core.commands.client_factory import resolve_client_arg_name - return resolve_client_arg_name(operation, self.kwargs) + return resolve_client_arg_name(op_path, self.kwargs) class CommandOperation(BaseCommandOperation): - def __init__(self, ctx, operation, **kwargs): - if not isinstance(operation, string_types): - raise TypeError("Operation must be a string. Got '{}'".format(operation)) + def __init__(self, ctx, op_path, **kwargs): + if not isinstance(op_path, string_types): + raise TypeError("Operation must be a string. Got '{}'".format(op_path)) super(CommandOperation, self).__init__(ctx, **kwargs) - self.operation = operation + self.op_path = op_path def handler(self, command_args): + """ Callback function of CLICommand handler """ from azure.cli.core.util import get_arg_list, augment_no_wait_handler_args - op = self.get_op_handler(self.operation) + op = self.get_op_handler(self.op_path) op_args = get_arg_list(op) - cmd = command_args.get('cmd') if 'cmd' in op_args else command_args.pop('cmd') + self.cmd = command_args.get('cmd') if 'cmd' in op_args else command_args.pop('cmd') - client = self.client_factory(cmd.cli_ctx, command_args) if self.client_factory else None + client = self.client_factory(self.cli_ctx, command_args) if self.client_factory else None supports_no_wait = self.kwargs.get('supports_no_wait', None) if supports_no_wait: no_wait_enabled = command_args.pop('no_wait', False) augment_no_wait_handler_args(no_wait_enabled, op, command_args) if client: - client_arg_name = self.resolve_client_arg_name(self.operation) + client_arg_name = self.resolve_client_arg_name(self.op_path) if client_arg_name in op_args: command_args[client_arg_name] = client return op(**command_args) def arguments_loader(self): - op = self.get_op_handler(self.operation) + """ Callback function of CLICommand arguments_loader """ + op = self.get_op_handler(self.op_path) self.apply_doc_string(op) cmd_args = list(extract_args_from_signature(op, excluded_params=self.ctx.excluded_command_handler_args)) return cmd_args def description_loader(self): - op = self.get_op_handler(self.operation) - return self.load_op_description(op) + """ Callback function of CLICommand description_loader """ + op = self.get_op_handler(self.op_path) + return self.load_op_handler_description(op) class WaitCommandOperation(BaseCommandOperation): - def __init__(self, ctx, operation, **kwargs): - if not isinstance(operation, string_types): - raise TypeError("operation must be a string. Got '{}'".format(operation)) + def __init__(self, ctx, op_path, **kwargs): + if not isinstance(op_path, string_types): + raise TypeError("operation must be a string. Got '{}'".format(op_path)) super(WaitCommandOperation, self).__init__(ctx, **kwargs) - self.operation = operation + self.op_path = op_path def handler(self, command_args): # pylint: disable=too-many-statements + """ Callback function of CLICommand handler """ from msrest.exceptions import ClientException from azure.core.exceptions import HttpResponseError from knack.util import CLIError @@ -138,12 +150,11 @@ def handler(self, command_args): # pylint: disable=too-many-statements import time - op = self.get_op_handler(self.operation) - + op = self.get_op_handler(self.op_path) getter_args = dict(extract_args_from_signature(op, excluded_params=EXCLUDED_NON_CLIENT_PARAMS)) self.cmd = command_args.get('cmd') if 'cmd' in getter_args else command_args.pop('cmd') - client_arg_name = self.resolve_client_arg_name(self.operation) + client_arg_name = self.resolve_client_arg_name(self.op_path) try: client = self.client_factory(self.cli_ctx) if self.client_factory else None except TypeError: @@ -151,7 +162,7 @@ def handler(self, command_args): # pylint: disable=too-many-statements if client and (client_arg_name in getter_args): command_args[client_arg_name] = client - getter = self.get_op_handler(self.operation) + getter = self.get_op_handler(self.op_path) # Fetch op handler again after cmd property is set timeout = command_args.pop('timeout') interval = command_args.pop('interval') @@ -218,7 +229,8 @@ def _get_provisioning_state(instance): return provisioning_state def arguments_loader(self): - cmd_args = self.load_getter_op_arguments(self.operation) + """ Callback function of CLICommand arguments_loader """ + cmd_args = self.load_getter_op_arguments(self.op_path) group_name = 'Wait Condition' cmd_args['timeout'] = CLICommandArgument( @@ -254,26 +266,28 @@ def arguments_loader(self): return [(k, v) for k, v in cmd_args.items()] def description_loader(self): - return self.load_op_description() + """ Callback function of CLICommand description_loader """ + return self.load_op_handler_description() class ShowCommandOperation(BaseCommandOperation): - def __init__(self, ctx, operation, **kwargs): - if not isinstance(operation, string_types): - raise TypeError("operation must be a string. Got '{}'".format(operation)) + def __init__(self, ctx, op_path, **kwargs): + if not isinstance(op_path, string_types): + raise TypeError("operation must be a string. Got '{}'".format(op_path)) super(ShowCommandOperation, self).__init__(ctx, **kwargs) - self.operation = operation + self.op_path = op_path def handler(self, command_args): + """ Callback function of CLICommand handler """ from azure.cli.core.commands.arm import show_exception_handler, EXCLUDED_NON_CLIENT_PARAMS - op = self.get_op_handler(self.operation) + op = self.get_op_handler(self.op_path) getter_args = dict(extract_args_from_signature(op, excluded_params=EXCLUDED_NON_CLIENT_PARAMS)) self.cmd = command_args.get('cmd') if 'cmd' in getter_args else command_args.pop('cmd') - client_arg_name = self.resolve_client_arg_name(self.operation) + client_arg_name = self.resolve_client_arg_name(self.op_path) try: client = self.client_factory(self.cli_ctx) if self.client_factory else None except TypeError: @@ -282,48 +296,52 @@ def handler(self, command_args): if client and (client_arg_name in getter_args): command_args[client_arg_name] = client - op = self.get_op_handler(self.operation) + op = self.get_op_handler(self.op_path) # Fetch op handler again after cmd property is set try: return op(**command_args) except Exception as ex: # pylint: disable=broad-except show_exception_handler(ex) def arguments_loader(self): - cmd_args = self.load_getter_op_arguments(self.operation) + """ Callback function of CLICommand arguments_loader """ + cmd_args = self.load_getter_op_arguments(self.op_path) return [(k, v) for k, v in cmd_args.items()] def description_loader(self): - op = self.get_op_handler(self.operation) - return self.load_op_description(op) + """ Callback function of CLICommand description_loader """ + op = self.get_op_handler(self.op_path) + return self.load_op_handler_description(op) class GenericUpdateCommandOperation(BaseCommandOperation): # pylint: disable=too-many-instance-attributes class OrderedArgsAction(argparse.Action): # pylint:disable=too-few-public-methods + """Action for 'properties_to_set', 'properties_to_add' and 'properties_to_remove' arguments""" def __call__(self, parser, namespace, values, option_string=None): if not getattr(namespace, 'ordered_arguments', None): setattr(namespace, 'ordered_arguments', []) namespace.ordered_arguments.append((option_string, values)) - def __init__(self, ctx, getter_op, setter_op, setter_arg_name, custom_function_op, child_collection_prop_name, - child_collection_key, child_arg_name, **kwargs): - if not isinstance(getter_op, string_types): - raise TypeError("Getter operation must be a string. Got '{}'".format(getter_op)) - if not isinstance(setter_op, string_types): - raise TypeError("Setter operation must be a string. Got '{}'".format(setter_op)) - if custom_function_op and not isinstance(custom_function_op, string_types): - raise TypeError("Custom function operation must be a string. Got '{}'".format(custom_function_op)) + def __init__(self, ctx, getter_op_path, setter_op_path, setter_arg_name, custom_function_op_path, + child_collection_prop_name, child_collection_key, child_arg_name, **kwargs): + if not isinstance(getter_op_path, string_types): + raise TypeError("Getter operation must be a string. Got '{}'".format(getter_op_path)) + if not isinstance(setter_op_path, string_types): + raise TypeError("Setter operation must be a string. Got '{}'".format(setter_op_path)) + if custom_function_op_path and not isinstance(custom_function_op_path, string_types): + raise TypeError("Custom function operation must be a string. Got '{}'".format(custom_function_op_path)) super(GenericUpdateCommandOperation, self).__init__(ctx, **kwargs) - self.getter_operation = getter_op - self.setter_operation = setter_op - self.custom_function_operation = custom_function_op + self.getter_op_path = getter_op_path + self.setter_op_path = setter_op_path + self.custom_function_op_path = custom_function_op_path self.setter_arg_name = setter_arg_name self.child_collection_prop_name = child_collection_prop_name self.child_collection_key = child_collection_key self.child_arg_name = child_arg_name def handler(self, command_args): # pylint: disable=too-many-locals, too-many-statements, too-many-branches + """ Callback function of CLICommand handler """ from knack.util import CLIError from azure.cli.core.commands import cached_get, cached_put, _is_poller from azure.cli.core.util import find_child_item, augment_no_wait_handler_args @@ -341,7 +359,7 @@ def handler(self, command_args): # pylint: disable=too-many-locals, too-many-st raise CLIError("Unexpected '{}' was not empty.".format(item)) del command_args[item] - getter, getterargs = self._extract_handler_and_args(command_args, self.getter_operation) + getter, getterargs = self._extract_op_handler_and_args(command_args, self.getter_op_path) if self.child_collection_prop_name: parent = cached_get(self.cmd, getter, **getterargs) @@ -352,16 +370,16 @@ def handler(self, command_args): # pylint: disable=too-many-locals, too-many-st instance = cached_get(self.cmd, getter, **getterargs) # pass instance to the custom_function, if provided - if self.custom_function_operation: - custom_function, custom_func_args = self._extract_handler_and_args( - command_args, self.custom_function_operation) + if self.custom_function_op_path: + custom_function, custom_func_args = self._extract_op_handler_and_args( + command_args, self.custom_function_op_path) if self.child_collection_prop_name: parent = custom_function(instance=instance, parent=parent, **custom_func_args) else: instance = custom_function(instance=instance, **custom_func_args) # apply generic updates after custom updates - setter, setterargs = self._extract_handler_and_args(command_args, self.setter_operation) + setter, setterargs = self._extract_op_handler_and_args(command_args, self.setter_op_path) for arg in ordered_arguments: arg_type, arg_values = arg @@ -418,7 +436,7 @@ def handler(self, command_args): # pylint: disable=too-many-locals, too-many-st result, *child_names, path=self.child_collection_prop_name, key_path=self.child_collection_key) return result - def _extract_handler_and_args(self, args, operation): + def _extract_op_handler_and_args(self, args, op_path): from azure.cli.core.commands.arm import EXCLUDED_NON_CLIENT_PARAMS client = None @@ -428,8 +446,8 @@ def _extract_handler_and_args(self, args, operation): except TypeError: client = self.client_factory(self.cli_ctx, args) - client_arg_name = self.resolve_client_arg_name(operation) - op = self.get_op_handler(operation) + client_arg_name = self.resolve_client_arg_name(op_path) + op = self.get_op_handler(op_path) raw_args = dict(extract_args_from_signature(op, excluded_params=EXCLUDED_NON_CLIENT_PARAMS)) op_args = {key: val for key, val in args.items() if key in raw_args} if client_arg_name in raw_args: @@ -437,11 +455,12 @@ def _extract_handler_and_args(self, args, operation): return op, op_args def arguments_loader(self): + """ Callback function of CLICommand arguments_loader """ from azure.cli.core.commands.arm import set_usage, add_usage, remove_usage - arguments = self.load_getter_op_arguments(self.getter_operation) - arguments.update(self._set_arguments_loader()) - arguments.update(self._function_arguments_loader()) + arguments = self.load_getter_op_arguments(self.getter_op_path) + arguments.update(self.load_setter_op_arguments()) + arguments.update(self.load_custom_function_op_arguments()) arguments.pop('instance', None) # inherited from custom_function(instance, ...) arguments.pop('parent', None) arguments.pop('expand', None) # possibly inherited from the getter @@ -474,16 +493,17 @@ def arguments_loader(self): ) return [(k, v) for k, v in arguments.items()] - def _set_arguments_loader(self): - op = self.get_op_handler(self.setter_operation) + def load_setter_op_arguments(self): + op = self.get_op_handler(self.setter_op_path) return dict(extract_args_from_signature(op, excluded_params=EXCLUDED_PARAMS)) - def _function_arguments_loader(self): - if not self.custom_function_operation: + def load_custom_function_op_arguments(self): + if not self.custom_function_op_path: return {} - op = self.get_op_handler(self.custom_function_operation) + op = self.get_op_handler(self.custom_function_op_path) self.apply_doc_string(op) # pylint: disable=protected-access return dict(extract_args_from_signature(op, excluded_params=EXCLUDED_PARAMS)) def description_loader(self): - return self.load_op_description() + """ Callback function of CLICommand description_loader """ + return self.load_op_handler_description() From 70b59772d2b21d8e74127f90a5d4bef1acca0240 Mon Sep 17 00:00:00 2001 From: kai ru <69238381+kairu-ms@users.noreply.github.com> Date: Tue, 23 Mar 2021 16:08:28 +0800 Subject: [PATCH 3/7] use command_loader --- .../azure/cli/core/commands/__init__.py | 8 ++--- .../cli/core/commands/command_operation.py | 33 ++++++++++--------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/commands/__init__.py b/src/azure-cli-core/azure/cli/core/commands/__init__.py index f486f675fcc..aa0873abd7b 100644 --- a/src/azure-cli-core/azure/cli/core/commands/__init__.py +++ b/src/azure-cli-core/azure/cli/core/commands/__init__.py @@ -1261,7 +1261,7 @@ def _command(self, name, method_name, custom_command=False, **kwargs): command_name = '{} {}'.format(self.group_name, name) if self.group_name else name command_operation = CommandOperation( - ctx=self.command_loader, + command_loader=self.command_loader, op_path=op_path, **merged_kwargs ) @@ -1309,7 +1309,7 @@ def generic_update_command(self, name, getter_name='get', getter_type=None, custom_command=True) if custom_func_name else None command_name = '{} {}'.format(self.group_name, name) if self.group_name else name command_operation = GenericUpdateCommandOperation( - ctx=self.command_loader, + command_loader=self.command_loader, getter_op_path=getter_op_path, setter_op_path=setter_op_path, setter_arg_name=setter_arg_name, @@ -1344,7 +1344,7 @@ def _wait_command(self, name, getter_name='get', getter_type=None, custom_comman command_name = '{} {}'.format(self.group_name, name) if self.group_name else name command_operation = WaitCommandOperation( - ctx=self.command_loader, + command_loader=self.command_loader, op_path=getter_op_path, **merged_kwargs ) @@ -1370,7 +1370,7 @@ def _show_command(self, name, getter_name='get', getter_type=None, custom_comman command_name = '{} {}'.format(self.group_name, name) if self.group_name else name command_operation = ShowCommandOperation( - ctx=self.command_loader, + command_loader=self.command_loader, op_path=op_path, **merged_kwargs ) diff --git a/src/azure-cli-core/azure/cli/core/commands/command_operation.py b/src/azure-cli-core/azure/cli/core/commands/command_operation.py index c733f5d9e95..523c4db4132 100644 --- a/src/azure-cli-core/azure/cli/core/commands/command_operation.py +++ b/src/azure-cli-core/azure/cli/core/commands/command_operation.py @@ -12,10 +12,11 @@ class BaseCommandOperation: """ Base class of command operation classes """ - def __init__(self, ctx, **kwargs): - if not isinstance(ctx, AzCommandsLoader): - raise TypeError("'ctx' expected type '{}'. Got: '{}'".format(AzCommandsLoader.__name__, type(ctx))) - self.ctx = ctx # AzCommandsLoader + def __init__(self, command_loader, **kwargs): + if not isinstance(command_loader, AzCommandsLoader): + raise TypeError("'command_loader' expected type '{}'. Got: '{}'".format( + AzCommandsLoader.__name__, type(command_loader))) + self.command_loader = command_loader self.cmd = None self.kwargs = kwargs self.client_factory = kwargs.get('client_factory') @@ -23,8 +24,8 @@ def __init__(self, ctx, **kwargs): @property def cli_ctx(self): - """ Return the cli_ctx of command or commands loader """ - return self.cmd.cli_ctx if self.cmd else self.ctx.cli_ctx + """ Return the cli_ctx of command or command_loader """ + return self.cmd.cli_ctx if self.cmd else self.command_loader.cli_ctx def handler(self, command_args): """ Callback function of CLICommand handler """ @@ -77,7 +78,7 @@ def load_getter_op_arguments(self, getter_op_path, cmd_args=None): return cmd_args def apply_doc_string(self, handler): - return self.ctx._apply_doc_string(handler, self.kwargs) # pylint: disable=protected-access + return self.command_loader._apply_doc_string(handler, self.kwargs) # pylint: disable=protected-access def load_op_handler_description(self, handler=None): """ Load the description from function signature of command op """ @@ -95,10 +96,10 @@ def resolve_client_arg_name(self, op_path): class CommandOperation(BaseCommandOperation): - def __init__(self, ctx, op_path, **kwargs): + def __init__(self, command_loader, op_path, **kwargs): if not isinstance(op_path, string_types): raise TypeError("Operation must be a string. Got '{}'".format(op_path)) - super(CommandOperation, self).__init__(ctx, **kwargs) + super(CommandOperation, self).__init__(command_loader, **kwargs) self.op_path = op_path def handler(self, command_args): @@ -124,7 +125,7 @@ def arguments_loader(self): """ Callback function of CLICommand arguments_loader """ op = self.get_op_handler(self.op_path) self.apply_doc_string(op) - cmd_args = list(extract_args_from_signature(op, excluded_params=self.ctx.excluded_command_handler_args)) + cmd_args = list(extract_args_from_signature(op, excluded_params=self.command_loader.excluded_command_handler_args)) return cmd_args def description_loader(self): @@ -135,10 +136,10 @@ def description_loader(self): class WaitCommandOperation(BaseCommandOperation): - def __init__(self, ctx, op_path, **kwargs): + def __init__(self, command_loader, op_path, **kwargs): if not isinstance(op_path, string_types): raise TypeError("operation must be a string. Got '{}'".format(op_path)) - super(WaitCommandOperation, self).__init__(ctx, **kwargs) + super(WaitCommandOperation, self).__init__(command_loader, **kwargs) self.op_path = op_path def handler(self, command_args): # pylint: disable=too-many-statements @@ -272,10 +273,10 @@ def description_loader(self): class ShowCommandOperation(BaseCommandOperation): - def __init__(self, ctx, op_path, **kwargs): + def __init__(self, command_loader, op_path, **kwargs): if not isinstance(op_path, string_types): raise TypeError("operation must be a string. Got '{}'".format(op_path)) - super(ShowCommandOperation, self).__init__(ctx, **kwargs) + super(ShowCommandOperation, self).__init__(command_loader, **kwargs) self.op_path = op_path def handler(self, command_args): @@ -322,7 +323,7 @@ def __call__(self, parser, namespace, values, option_string=None): setattr(namespace, 'ordered_arguments', []) namespace.ordered_arguments.append((option_string, values)) - def __init__(self, ctx, getter_op_path, setter_op_path, setter_arg_name, custom_function_op_path, + def __init__(self, command_loader, getter_op_path, setter_op_path, setter_arg_name, custom_function_op_path, child_collection_prop_name, child_collection_key, child_arg_name, **kwargs): if not isinstance(getter_op_path, string_types): raise TypeError("Getter operation must be a string. Got '{}'".format(getter_op_path)) @@ -330,7 +331,7 @@ def __init__(self, ctx, getter_op_path, setter_op_path, setter_arg_name, custom_ raise TypeError("Setter operation must be a string. Got '{}'".format(setter_op_path)) if custom_function_op_path and not isinstance(custom_function_op_path, string_types): raise TypeError("Custom function operation must be a string. Got '{}'".format(custom_function_op_path)) - super(GenericUpdateCommandOperation, self).__init__(ctx, **kwargs) + super(GenericUpdateCommandOperation, self).__init__(command_loader, **kwargs) self.getter_op_path = getter_op_path self.setter_op_path = setter_op_path From 194928b0fd4b0165152018fb023297362d2cbc0e Mon Sep 17 00:00:00 2001 From: kai ru <69238381+kairu-ms@users.noreply.github.com> Date: Tue, 23 Mar 2021 16:13:19 +0800 Subject: [PATCH 4/7] Drop six --- .../azure/cli/core/commands/command_operation.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/commands/command_operation.py b/src/azure-cli-core/azure/cli/core/commands/command_operation.py index 523c4db4132..0323f07bbd4 100644 --- a/src/azure-cli-core/azure/cli/core/commands/command_operation.py +++ b/src/azure-cli-core/azure/cli/core/commands/command_operation.py @@ -3,7 +3,6 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- import argparse -from six import string_types, get_method_function from azure.cli.core import AzCommandsLoader, EXCLUDED_PARAMS from knack.introspection import extract_args_from_signature, extract_full_summary_from_signature from knack.arguments import CLICommandArgument, ignore_type @@ -62,7 +61,7 @@ def get_op_handler(self, op_path): handler = getattr(handler, part) if isinstance(handler, types.FunctionType): return handler - return get_method_function(handler) + return handler.__func__ except (ValueError, AttributeError): raise ValueError("The operation '{}' is invalid.".format(op_path)) @@ -97,7 +96,7 @@ def resolve_client_arg_name(self, op_path): class CommandOperation(BaseCommandOperation): def __init__(self, command_loader, op_path, **kwargs): - if not isinstance(op_path, string_types): + if not isinstance(op_path, str): raise TypeError("Operation must be a string. Got '{}'".format(op_path)) super(CommandOperation, self).__init__(command_loader, **kwargs) self.op_path = op_path @@ -137,7 +136,7 @@ def description_loader(self): class WaitCommandOperation(BaseCommandOperation): def __init__(self, command_loader, op_path, **kwargs): - if not isinstance(op_path, string_types): + if not isinstance(op_path, str): raise TypeError("operation must be a string. Got '{}'".format(op_path)) super(WaitCommandOperation, self).__init__(command_loader, **kwargs) self.op_path = op_path @@ -274,7 +273,7 @@ def description_loader(self): class ShowCommandOperation(BaseCommandOperation): def __init__(self, command_loader, op_path, **kwargs): - if not isinstance(op_path, string_types): + if not isinstance(op_path, str): raise TypeError("operation must be a string. Got '{}'".format(op_path)) super(ShowCommandOperation, self).__init__(command_loader, **kwargs) self.op_path = op_path @@ -325,11 +324,11 @@ def __call__(self, parser, namespace, values, option_string=None): def __init__(self, command_loader, getter_op_path, setter_op_path, setter_arg_name, custom_function_op_path, child_collection_prop_name, child_collection_key, child_arg_name, **kwargs): - if not isinstance(getter_op_path, string_types): + if not isinstance(getter_op_path, str): raise TypeError("Getter operation must be a string. Got '{}'".format(getter_op_path)) - if not isinstance(setter_op_path, string_types): + if not isinstance(setter_op_path, str): raise TypeError("Setter operation must be a string. Got '{}'".format(setter_op_path)) - if custom_function_op_path and not isinstance(custom_function_op_path, string_types): + if custom_function_op_path and not isinstance(custom_function_op_path, str): raise TypeError("Custom function operation must be a string. Got '{}'".format(custom_function_op_path)) super(GenericUpdateCommandOperation, self).__init__(command_loader, **kwargs) From f4fc8bb0511cf462731372c8a31df238e799df5c Mon Sep 17 00:00:00 2001 From: kai ru <69238381+kairu-ms@users.noreply.github.com> Date: Tue, 23 Mar 2021 16:49:22 +0800 Subject: [PATCH 5/7] Alphabetical order --- .../cli/core/commands/command_operation.py | 360 +++++++++--------- 1 file changed, 180 insertions(+), 180 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/commands/command_operation.py b/src/azure-cli-core/azure/cli/core/commands/command_operation.py index 0323f07bbd4..de28aebbeb2 100644 --- a/src/azure-cli-core/azure/cli/core/commands/command_operation.py +++ b/src/azure-cli-core/azure/cli/core/commands/command_operation.py @@ -133,186 +133,6 @@ def description_loader(self): return self.load_op_handler_description(op) -class WaitCommandOperation(BaseCommandOperation): - - def __init__(self, command_loader, op_path, **kwargs): - if not isinstance(op_path, str): - raise TypeError("operation must be a string. Got '{}'".format(op_path)) - super(WaitCommandOperation, self).__init__(command_loader, **kwargs) - self.op_path = op_path - - def handler(self, command_args): # pylint: disable=too-many-statements - """ Callback function of CLICommand handler """ - from msrest.exceptions import ClientException - from azure.core.exceptions import HttpResponseError - from knack.util import CLIError - from azure.cli.core.commands.arm import EXCLUDED_NON_CLIENT_PARAMS, verify_property - - import time - - op = self.get_op_handler(self.op_path) - getter_args = dict(extract_args_from_signature(op, excluded_params=EXCLUDED_NON_CLIENT_PARAMS)) - self.cmd = command_args.get('cmd') if 'cmd' in getter_args else command_args.pop('cmd') - - client_arg_name = self.resolve_client_arg_name(self.op_path) - try: - client = self.client_factory(self.cli_ctx) if self.client_factory else None - except TypeError: - client = self.client_factory(self.cli_ctx, command_args) if self.client_factory else None - if client and (client_arg_name in getter_args): - command_args[client_arg_name] = client - - getter = self.get_op_handler(self.op_path) # Fetch op handler again after cmd property is set - - timeout = command_args.pop('timeout') - interval = command_args.pop('interval') - wait_for_created = command_args.pop('created') - wait_for_deleted = command_args.pop('deleted') - wait_for_updated = command_args.pop('updated') - wait_for_exists = command_args.pop('exists') - custom_condition = command_args.pop('custom') - if not any([wait_for_created, wait_for_updated, wait_for_deleted, - wait_for_exists, custom_condition]): - raise CLIError( - "incorrect usage: --created | --updated | --deleted | --exists | --custom JMESPATH") - - progress_indicator = self.cli_ctx.get_progress_controller() - progress_indicator.begin() - for _ in range(0, timeout, interval): - try: - progress_indicator.add(message='Waiting') - instance = getter(**command_args) - if wait_for_exists: - progress_indicator.end() - return None - provisioning_state = self._get_provisioning_state(instance) - # until we have any needs to wait for 'Failed', let us bail out on this - if provisioning_state: - provisioning_state = provisioning_state.lower() - if provisioning_state == 'failed': - progress_indicator.stop() - raise CLIError('The operation failed') - if ((wait_for_created or wait_for_updated) and provisioning_state == 'succeeded') or \ - custom_condition and bool(verify_property(instance, custom_condition)): - progress_indicator.end() - return None - except (ClientException, HttpResponseError) as ex: - progress_indicator.stop() - if getattr(ex, 'status_code', None) == 404: - if wait_for_deleted: - return None - if not any([wait_for_created, wait_for_exists, custom_condition]): - raise - else: - raise - except Exception: # pylint: disable=broad-except - progress_indicator.stop() - raise - - time.sleep(interval) - - progress_indicator.end() - return CLIError('Wait operation timed-out after {} seconds'.format(timeout)) - - @staticmethod - def _get_provisioning_state(instance): - provisioning_state = getattr(instance, 'provisioning_state', None) - if not provisioning_state: - # some SDK, like resource-group, has 'provisioning_state' under 'properties' - properties = getattr(instance, 'properties', None) - if properties: - provisioning_state = getattr(properties, 'provisioning_state', None) - # some SDK, like keyvault, has 'provisioningState' under 'properties.additional_properties' - if not provisioning_state: - additional_properties = getattr(properties, 'additional_properties', {}) - provisioning_state = additional_properties.get('provisioningState') - return provisioning_state - - def arguments_loader(self): - """ Callback function of CLICommand arguments_loader """ - cmd_args = self.load_getter_op_arguments(self.op_path) - - group_name = 'Wait Condition' - cmd_args['timeout'] = CLICommandArgument( - 'timeout', options_list=['--timeout'], default=3600, arg_group=group_name, type=int, - help='maximum wait in seconds' - ) - cmd_args['interval'] = CLICommandArgument( - 'interval', options_list=['--interval'], default=30, arg_group=group_name, type=int, - help='polling interval in seconds' - ) - cmd_args['deleted'] = CLICommandArgument( - 'deleted', options_list=['--deleted'], action='store_true', arg_group=group_name, - help='wait until deleted' - ) - cmd_args['created'] = CLICommandArgument( - 'created', options_list=['--created'], action='store_true', arg_group=group_name, - help="wait until created with 'provisioningState' at 'Succeeded'" - ) - cmd_args['updated'] = CLICommandArgument( - 'updated', options_list=['--updated'], action='store_true', arg_group=group_name, - help="wait until updated with provisioningState at 'Succeeded'" - ) - cmd_args['exists'] = CLICommandArgument( - 'exists', options_list=['--exists'], action='store_true', arg_group=group_name, - help="wait until the resource exists" - ) - cmd_args['custom'] = CLICommandArgument( - 'custom', options_list=['--custom'], arg_group=group_name, - help="Wait until the condition satisfies a custom JMESPath query. E.g. " - "provisioningState!='InProgress', " - "instanceView.statuses[?code=='PowerState/running']" - ) - return [(k, v) for k, v in cmd_args.items()] - - def description_loader(self): - """ Callback function of CLICommand description_loader """ - return self.load_op_handler_description() - - -class ShowCommandOperation(BaseCommandOperation): - - def __init__(self, command_loader, op_path, **kwargs): - if not isinstance(op_path, str): - raise TypeError("operation must be a string. Got '{}'".format(op_path)) - super(ShowCommandOperation, self).__init__(command_loader, **kwargs) - self.op_path = op_path - - def handler(self, command_args): - """ Callback function of CLICommand handler """ - from azure.cli.core.commands.arm import show_exception_handler, EXCLUDED_NON_CLIENT_PARAMS - - op = self.get_op_handler(self.op_path) - getter_args = dict(extract_args_from_signature(op, excluded_params=EXCLUDED_NON_CLIENT_PARAMS)) - - self.cmd = command_args.get('cmd') if 'cmd' in getter_args else command_args.pop('cmd') - - client_arg_name = self.resolve_client_arg_name(self.op_path) - try: - client = self.client_factory(self.cli_ctx) if self.client_factory else None - except TypeError: - client = self.client_factory(self.cli_ctx, command_args) if self.client_factory else None - - if client and (client_arg_name in getter_args): - command_args[client_arg_name] = client - - op = self.get_op_handler(self.op_path) # Fetch op handler again after cmd property is set - try: - return op(**command_args) - except Exception as ex: # pylint: disable=broad-except - show_exception_handler(ex) - - def arguments_loader(self): - """ Callback function of CLICommand arguments_loader """ - cmd_args = self.load_getter_op_arguments(self.op_path) - return [(k, v) for k, v in cmd_args.items()] - - def description_loader(self): - """ Callback function of CLICommand description_loader """ - op = self.get_op_handler(self.op_path) - return self.load_op_handler_description(op) - - class GenericUpdateCommandOperation(BaseCommandOperation): # pylint: disable=too-many-instance-attributes class OrderedArgsAction(argparse.Action): # pylint:disable=too-few-public-methods @@ -507,3 +327,183 @@ def load_custom_function_op_arguments(self): def description_loader(self): """ Callback function of CLICommand description_loader """ return self.load_op_handler_description() + + +class ShowCommandOperation(BaseCommandOperation): + + def __init__(self, command_loader, op_path, **kwargs): + if not isinstance(op_path, str): + raise TypeError("operation must be a string. Got '{}'".format(op_path)) + super(ShowCommandOperation, self).__init__(command_loader, **kwargs) + self.op_path = op_path + + def handler(self, command_args): + """ Callback function of CLICommand handler """ + from azure.cli.core.commands.arm import show_exception_handler, EXCLUDED_NON_CLIENT_PARAMS + + op = self.get_op_handler(self.op_path) + getter_args = dict(extract_args_from_signature(op, excluded_params=EXCLUDED_NON_CLIENT_PARAMS)) + + self.cmd = command_args.get('cmd') if 'cmd' in getter_args else command_args.pop('cmd') + + client_arg_name = self.resolve_client_arg_name(self.op_path) + try: + client = self.client_factory(self.cli_ctx) if self.client_factory else None + except TypeError: + client = self.client_factory(self.cli_ctx, command_args) if self.client_factory else None + + if client and (client_arg_name in getter_args): + command_args[client_arg_name] = client + + op = self.get_op_handler(self.op_path) # Fetch op handler again after cmd property is set + try: + return op(**command_args) + except Exception as ex: # pylint: disable=broad-except + show_exception_handler(ex) + + def arguments_loader(self): + """ Callback function of CLICommand arguments_loader """ + cmd_args = self.load_getter_op_arguments(self.op_path) + return [(k, v) for k, v in cmd_args.items()] + + def description_loader(self): + """ Callback function of CLICommand description_loader """ + op = self.get_op_handler(self.op_path) + return self.load_op_handler_description(op) + + +class WaitCommandOperation(BaseCommandOperation): + + def __init__(self, command_loader, op_path, **kwargs): + if not isinstance(op_path, str): + raise TypeError("operation must be a string. Got '{}'".format(op_path)) + super(WaitCommandOperation, self).__init__(command_loader, **kwargs) + self.op_path = op_path + + def handler(self, command_args): # pylint: disable=too-many-statements + """ Callback function of CLICommand handler """ + from msrest.exceptions import ClientException + from azure.core.exceptions import HttpResponseError + from knack.util import CLIError + from azure.cli.core.commands.arm import EXCLUDED_NON_CLIENT_PARAMS, verify_property + + import time + + op = self.get_op_handler(self.op_path) + getter_args = dict(extract_args_from_signature(op, excluded_params=EXCLUDED_NON_CLIENT_PARAMS)) + self.cmd = command_args.get('cmd') if 'cmd' in getter_args else command_args.pop('cmd') + + client_arg_name = self.resolve_client_arg_name(self.op_path) + try: + client = self.client_factory(self.cli_ctx) if self.client_factory else None + except TypeError: + client = self.client_factory(self.cli_ctx, command_args) if self.client_factory else None + if client and (client_arg_name in getter_args): + command_args[client_arg_name] = client + + getter = self.get_op_handler(self.op_path) # Fetch op handler again after cmd property is set + + timeout = command_args.pop('timeout') + interval = command_args.pop('interval') + wait_for_created = command_args.pop('created') + wait_for_deleted = command_args.pop('deleted') + wait_for_updated = command_args.pop('updated') + wait_for_exists = command_args.pop('exists') + custom_condition = command_args.pop('custom') + if not any([wait_for_created, wait_for_updated, wait_for_deleted, + wait_for_exists, custom_condition]): + raise CLIError( + "incorrect usage: --created | --updated | --deleted | --exists | --custom JMESPATH") + + progress_indicator = self.cli_ctx.get_progress_controller() + progress_indicator.begin() + for _ in range(0, timeout, interval): + try: + progress_indicator.add(message='Waiting') + instance = getter(**command_args) + if wait_for_exists: + progress_indicator.end() + return None + provisioning_state = self._get_provisioning_state(instance) + # until we have any needs to wait for 'Failed', let us bail out on this + if provisioning_state: + provisioning_state = provisioning_state.lower() + if provisioning_state == 'failed': + progress_indicator.stop() + raise CLIError('The operation failed') + if ((wait_for_created or wait_for_updated) and provisioning_state == 'succeeded') or \ + custom_condition and bool(verify_property(instance, custom_condition)): + progress_indicator.end() + return None + except (ClientException, HttpResponseError) as ex: + progress_indicator.stop() + if getattr(ex, 'status_code', None) == 404: + if wait_for_deleted: + return None + if not any([wait_for_created, wait_for_exists, custom_condition]): + raise + else: + raise + except Exception: # pylint: disable=broad-except + progress_indicator.stop() + raise + + time.sleep(interval) + + progress_indicator.end() + return CLIError('Wait operation timed-out after {} seconds'.format(timeout)) + + @staticmethod + def _get_provisioning_state(instance): + provisioning_state = getattr(instance, 'provisioning_state', None) + if not provisioning_state: + # some SDK, like resource-group, has 'provisioning_state' under 'properties' + properties = getattr(instance, 'properties', None) + if properties: + provisioning_state = getattr(properties, 'provisioning_state', None) + # some SDK, like keyvault, has 'provisioningState' under 'properties.additional_properties' + if not provisioning_state: + additional_properties = getattr(properties, 'additional_properties', {}) + provisioning_state = additional_properties.get('provisioningState') + return provisioning_state + + def arguments_loader(self): + """ Callback function of CLICommand arguments_loader """ + cmd_args = self.load_getter_op_arguments(self.op_path) + + group_name = 'Wait Condition' + cmd_args['timeout'] = CLICommandArgument( + 'timeout', options_list=['--timeout'], default=3600, arg_group=group_name, type=int, + help='maximum wait in seconds' + ) + cmd_args['interval'] = CLICommandArgument( + 'interval', options_list=['--interval'], default=30, arg_group=group_name, type=int, + help='polling interval in seconds' + ) + cmd_args['deleted'] = CLICommandArgument( + 'deleted', options_list=['--deleted'], action='store_true', arg_group=group_name, + help='wait until deleted' + ) + cmd_args['created'] = CLICommandArgument( + 'created', options_list=['--created'], action='store_true', arg_group=group_name, + help="wait until created with 'provisioningState' at 'Succeeded'" + ) + cmd_args['updated'] = CLICommandArgument( + 'updated', options_list=['--updated'], action='store_true', arg_group=group_name, + help="wait until updated with provisioningState at 'Succeeded'" + ) + cmd_args['exists'] = CLICommandArgument( + 'exists', options_list=['--exists'], action='store_true', arg_group=group_name, + help="wait until the resource exists" + ) + cmd_args['custom'] = CLICommandArgument( + 'custom', options_list=['--custom'], arg_group=group_name, + help="Wait until the condition satisfies a custom JMESPath query. E.g. " + "provisioningState!='InProgress', " + "instanceView.statuses[?code=='PowerState/running']" + ) + return [(k, v) for k, v in cmd_args.items()] + + def description_loader(self): + """ Callback function of CLICommand description_loader """ + return self.load_op_handler_description() From bec78b955aca7c7cea805256327563270245b5d8 Mon Sep 17 00:00:00 2001 From: kai ru <69238381+kairu-ms@users.noreply.github.com> Date: Tue, 23 Mar 2021 17:05:25 +0800 Subject: [PATCH 6/7] rename kwargs by merged_kwargs --- .../cli/core/commands/command_operation.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/commands/command_operation.py b/src/azure-cli-core/azure/cli/core/commands/command_operation.py index de28aebbeb2..5cc348058e5 100644 --- a/src/azure-cli-core/azure/cli/core/commands/command_operation.py +++ b/src/azure-cli-core/azure/cli/core/commands/command_operation.py @@ -11,15 +11,15 @@ class BaseCommandOperation: """ Base class of command operation classes """ - def __init__(self, command_loader, **kwargs): + def __init__(self, command_loader, **merged_kwargs): if not isinstance(command_loader, AzCommandsLoader): raise TypeError("'command_loader' expected type '{}'. Got: '{}'".format( AzCommandsLoader.__name__, type(command_loader))) self.command_loader = command_loader self.cmd = None - self.kwargs = kwargs - self.client_factory = kwargs.get('client_factory') - self.operation_group = kwargs.get('operation_group') + self.merged_kwargs = merged_kwargs + self.client_factory = merged_kwargs.get('client_factory') + self.operation_group = merged_kwargs.get('operation_group') @property def cli_ctx(self): @@ -77,7 +77,7 @@ def load_getter_op_arguments(self, getter_op_path, cmd_args=None): return cmd_args def apply_doc_string(self, handler): - return self.command_loader._apply_doc_string(handler, self.kwargs) # pylint: disable=protected-access + return self.command_loader._apply_doc_string(handler, self.merged_kwargs) # pylint: disable=protected-access def load_op_handler_description(self, handler=None): """ Load the description from function signature of command op """ @@ -90,15 +90,15 @@ def default_handler(): def resolve_client_arg_name(self, op_path): from azure.cli.core.commands.client_factory import resolve_client_arg_name - return resolve_client_arg_name(op_path, self.kwargs) + return resolve_client_arg_name(op_path, self.merged_kwargs) class CommandOperation(BaseCommandOperation): - def __init__(self, command_loader, op_path, **kwargs): + def __init__(self, command_loader, op_path, **merged_kwargs): if not isinstance(op_path, str): raise TypeError("Operation must be a string. Got '{}'".format(op_path)) - super(CommandOperation, self).__init__(command_loader, **kwargs) + super(CommandOperation, self).__init__(command_loader, **merged_kwargs) self.op_path = op_path def handler(self, command_args): @@ -110,7 +110,7 @@ def handler(self, command_args): self.cmd = command_args.get('cmd') if 'cmd' in op_args else command_args.pop('cmd') client = self.client_factory(self.cli_ctx, command_args) if self.client_factory else None - supports_no_wait = self.kwargs.get('supports_no_wait', None) + supports_no_wait = self.merged_kwargs.get('supports_no_wait', None) if supports_no_wait: no_wait_enabled = command_args.pop('no_wait', False) augment_no_wait_handler_args(no_wait_enabled, op, command_args) @@ -143,14 +143,14 @@ def __call__(self, parser, namespace, values, option_string=None): namespace.ordered_arguments.append((option_string, values)) def __init__(self, command_loader, getter_op_path, setter_op_path, setter_arg_name, custom_function_op_path, - child_collection_prop_name, child_collection_key, child_arg_name, **kwargs): + child_collection_prop_name, child_collection_key, child_arg_name, **merged_kwargs): if not isinstance(getter_op_path, str): raise TypeError("Getter operation must be a string. Got '{}'".format(getter_op_path)) if not isinstance(setter_op_path, str): raise TypeError("Setter operation must be a string. Got '{}'".format(setter_op_path)) if custom_function_op_path and not isinstance(custom_function_op_path, str): raise TypeError("Custom function operation must be a string. Got '{}'".format(custom_function_op_path)) - super(GenericUpdateCommandOperation, self).__init__(command_loader, **kwargs) + super(GenericUpdateCommandOperation, self).__init__(command_loader, **merged_kwargs) self.getter_op_path = getter_op_path self.setter_op_path = setter_op_path @@ -331,10 +331,10 @@ def description_loader(self): class ShowCommandOperation(BaseCommandOperation): - def __init__(self, command_loader, op_path, **kwargs): + def __init__(self, command_loader, op_path, **merged_kwargs): if not isinstance(op_path, str): raise TypeError("operation must be a string. Got '{}'".format(op_path)) - super(ShowCommandOperation, self).__init__(command_loader, **kwargs) + super(ShowCommandOperation, self).__init__(command_loader, **merged_kwargs) self.op_path = op_path def handler(self, command_args): @@ -374,10 +374,10 @@ def description_loader(self): class WaitCommandOperation(BaseCommandOperation): - def __init__(self, command_loader, op_path, **kwargs): + def __init__(self, command_loader, op_path, **merged_kwargs): if not isinstance(op_path, str): raise TypeError("operation must be a string. Got '{}'".format(op_path)) - super(WaitCommandOperation, self).__init__(command_loader, **kwargs) + super(WaitCommandOperation, self).__init__(command_loader, **merged_kwargs) self.op_path = op_path def handler(self, command_args): # pylint: disable=too-many-statements From 5866b8e66f16bc0e46be47d94186b25393c83028 Mon Sep 17 00:00:00 2001 From: kai ru <69238381+kairu-ms@users.noreply.github.com> Date: Wed, 24 Mar 2021 13:52:25 +0800 Subject: [PATCH 7/7] fix style issues --- .../azure/cli/core/commands/command_operation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/azure-cli-core/azure/cli/core/commands/command_operation.py b/src/azure-cli-core/azure/cli/core/commands/command_operation.py index 5cc348058e5..27f2cc77570 100644 --- a/src/azure-cli-core/azure/cli/core/commands/command_operation.py +++ b/src/azure-cli-core/azure/cli/core/commands/command_operation.py @@ -124,7 +124,8 @@ def arguments_loader(self): """ Callback function of CLICommand arguments_loader """ op = self.get_op_handler(self.op_path) self.apply_doc_string(op) - cmd_args = list(extract_args_from_signature(op, excluded_params=self.command_loader.excluded_command_handler_args)) + cmd_args = list(extract_args_from_signature( + op, excluded_params=self.command_loader.excluded_command_handler_args)) return cmd_args def description_loader(self):