From e4106b8707e33b1c1aedb70df26a117f6882f854 Mon Sep 17 00:00:00 2001 From: Johan Stenberg Date: Fri, 25 Mar 2016 19:46:04 -0700 Subject: [PATCH] - Removed now defunct 'unexpected parameters' parameter for all command handlers - Fixed up storage account/key/connection string parameter combinations where they were accidentally set to always be required when they in fact are part of mutually exclusive parameter sets. We need to handle this better in the future - Renamed 'options' to 'arguments' in our command table. - Fixed up resource group alias (from --rg to -g) - Refactored application/session/configuration/command loading. The command package is now responsible for loading submodules. Renamed session to configuration and made it responsible for knowing what hints to give to the command modules in order to load the right set of modules. Moved event registreation and raising from what was previously the session object to the application object. - Moved transforming of returned SDK objects from Output to the Application module and made output only responsible for figuring out which formatter to use --- azure-cli.pyproj | 8 +- pylintrc | 2 + requirements.txt | 1 + src/azure/cli/_event_dispatcher.py | 46 ---- src/azure/cli/_output.py | 33 +-- src/azure/cli/application.py | 144 ++++++++---- src/azure/cli/commands/__init__.py | 18 +- src/azure/cli/commands/_auto_command.py | 8 +- src/azure/cli/commands/account.py | 9 +- src/azure/cli/commands/login.py | 15 +- src/azure/cli/commands/logout.py | 10 +- src/azure/cli/commands/network.py | 4 +- src/azure/cli/commands/resource.py | 6 +- src/azure/cli/commands/storage.py | 82 +++---- src/azure/cli/extensions/__init__.py | 6 +- src/azure/cli/extensions/query.py | 31 +-- src/azure/cli/extensions/transform.py | 4 +- src/azure/cli/main.py | 7 +- src/azure/cli/parser.py | 9 +- src/azure/cli/tests/test_argparse.py | 207 ------------------ src/azure/cli/tests/test_connection_verify.py | 2 +- 21 files changed, 220 insertions(+), 432 deletions(-) delete mode 100644 src/azure/cli/_event_dispatcher.py delete mode 100644 src/azure/cli/tests/test_argparse.py diff --git a/azure-cli.pyproj b/azure-cli.pyproj index f39c9761fea..f34f17e456e 100644 --- a/azure-cli.pyproj +++ b/azure-cli.pyproj @@ -15,8 +15,7 @@ {1dd9c42b-5980-42ce-a2c3-46d3bf0eede4} 3.5 False - - + vm list-all --query "[].name" @@ -36,7 +35,6 @@ - Code @@ -48,15 +46,11 @@ - - Code - - diff --git a/pylintrc b/pylintrc index f304190876a..e964c356744 100644 --- a/pylintrc +++ b/pylintrc @@ -6,6 +6,8 @@ # W0511 fixme disable=C0111,C0103,I0011,W0511 [DESIGN] +# Minimum lines number of a similarity. +min-similarity-lines=6 # Maximum number of locals for function / method body max-locals=25 # Maximum number of branch for function / method body diff --git a/requirements.txt b/requirements.txt index 45159a2c1ac..28463f097cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ applicationinsights==0.10.0 +argcomplete==1.1.0 azure==2.0.0rc1 jmespath mock==1.3.0 diff --git a/src/azure/cli/_event_dispatcher.py b/src/azure/cli/_event_dispatcher.py deleted file mode 100644 index 869ea9f0f7a..00000000000 --- a/src/azure/cli/_event_dispatcher.py +++ /dev/null @@ -1,46 +0,0 @@ -from collections import defaultdict - -class EventDispatcher(object): - """Register for and raise events. - - During the execution of a command, a set of events are raised - that allow extensions to change the flow of actions. - - Clients can register handlers by calling the `EventDispatcher.register` - method passing in the event handler function. - """ - - REGISTER_GLOBAL_PARAMETERS = 'RegisterGlobalParameters' - PARSING_PARAMETERS = 'ParsingParameters' - VALIDATING_PARAMETERS = 'ValidatingParameters' - EXECUTING_COMMAND = 'ExecutingCommand' - TRANSFORM_RESULT = 'TransformResult' - FILTER_RESULT = 'FilterResult' - - def __init__(self): - self._handlers = defaultdict(lambda: []) - - def raise_event(self, name, event_data): - for func in self._handlers[name]: - func(name, event_data) - - def register(self, name, handler): - '''Register a callable that will be called when the - event `name` is raised. - - param: name: The name of the event - param: handler: Function that takes two parameters; - name: name of the event raised - event_data: `dict` with event specific data. - ''' - self._handlers[name].append(handler) - - def event_handler(self, name): - '''Any function decorated by @event_handler will - be registered as a handler for the given event name - ''' - def wrapper(func): - self.register(name, func) - return func - return wrapper - diff --git a/src/azure/cli/_output.py b/src/azure/cli/_output.py index c74943191bf..2c19ca09eeb 100644 --- a/src/azure/cli/_output.py +++ b/src/azure/cli/_output.py @@ -3,8 +3,6 @@ import sys import json import re -from datetime import datetime -from enum import Enum from six import StringIO class OutputFormatException(Exception): @@ -51,40 +49,13 @@ class OutputProducer(object): #pylint: disable=too-few-public-methods 'list': format_list } - KEYS_CAMELCASE_PATTERN = re.compile('(?!^)_([a-zA-Z])') - def __init__(self, formatter=format_list, file=sys.stdout): #pylint: disable=redefined-builtin self.formatter = formatter self.file = file - def out(self, session, obj): - obj = OutputProducer.todict(obj) - event_data = {'result': obj} - session.raise_event(session.TRANSFORM_RESULT, event_data) - session.raise_event(session.FILTER_RESULT, event_data) - print(self.formatter(event_data['result']), file=self.file) + def out(self, obj): + print(self.formatter(obj), file=self.file) - @staticmethod - def todict(obj): #pylint: disable=too-many-return-statements - def to_camelcase(s): - return re.sub(OutputProducer.KEYS_CAMELCASE_PATTERN, lambda x: x.group(1).upper(), s) - - if isinstance(obj, dict): - return {k: OutputProducer.todict(v) for (k, v) in obj.items()} - elif isinstance(obj, list): - return [OutputProducer.todict(a) for a in obj] - elif isinstance(obj, Enum): - return obj.value - elif isinstance(obj, datetime): - return obj.isoformat() - elif hasattr(obj, '_asdict'): - return OutputProducer.todict(obj._asdict()) - elif hasattr(obj, '__dict__'): - return dict([(to_camelcase(k), OutputProducer.todict(v)) - for k, v in obj.__dict__.items() - if not callable(v) and not k.startswith('_')]) - else: - return obj @staticmethod def get_formatter(format_type): diff --git a/src/azure/cli/application.py b/src/azure/cli/application.py index d96c0da560d..411781d393e 100644 --- a/src/azure/cli/application.py +++ b/src/azure/cli/application.py @@ -1,75 +1,133 @@ +from collections import defaultdict +from datetime import datetime +import sys +import re import argparse import logging +from enum import Enum from .parser import AzCliCommandParser -import argcomplete -import logging -from ._event_dispatcher import EventDispatcher import azure.cli.extensions -class Session(EventDispatcher): - """The session object tracks session specific data such - as output formats, log settings as well as providing an - event dispatch mechanism that allows us to make modifications - during the execution of a command +class Configuration(object): # pylint: disable=too-few-public-methods + """The configuration object tracks session specific data such + as output formats, available commands etc. """ - def __init__(self): - super(Session, self).__init__() + def __init__(self, argv=None): + self.argv = argv or sys.argv self.log = logging.getLogger('az') self.output_format = 'list' + def get_command_table(self): + import azure.cli.commands as commands + + # Find the first noun on the command line and only load commands from that + # module to improve startup time. + for a in self.argv: + if not a.startswith('-'): + return commands.get_command_table(a) + + # No noun found, so load all commands. + return commands.get_command_table() + class Application(object): - def __init__(self, session=None): - self.session = session or Session() + TRANSFORM_RESULT = 'Application.TransformResults' + FILTER_RESULT = 'Application.FilterResults' + GLOBAL_PARSER_CREATED = 'GlobalParser.Created' + COMMAND_PARSER_CREATED = 'CommandParser.Created' + COMMAND_PARSER_LOADED = 'CommandParser.Loaded' + COMMAND_PARSER_PARSED = 'CommandParser.Parsed' + + def __init__(self, configuration=None): + self._event_handlers = defaultdict(lambda: []) + self.configuration = configuration or Configuration() # Register presence of and handlers for global parameters - self.session.register('GlobalParser.Created', self._register_builtin_arguments) - self.session.register('CommandParser.Parsed', self._handle_builtin_arguments) + self.register(self.GLOBAL_PARSER_CREATED, Application._register_builtin_arguments) + self.register(self.COMMAND_PARSER_LOADED, Application._enable_autocomplete) + self.register(self.COMMAND_PARSER_PARSED, self._handle_builtin_arguments) # Let other extensions make their presence known azure.cli.extensions.register_extensions(self) self.global_parser = AzCliCommandParser(prog='az', add_help=False) - self.session.raise_event('GlobalParser.Created', self.global_parser) + self.raise_event(self.GLOBAL_PARSER_CREATED, self.global_parser) self.parser = AzCliCommandParser(prog='az', parents=[self.global_parser]) - self.session.raise_event('CommandParser.Created', self.parser) - - def load_commands(self, argv): - import azure.cli.commands as commands - - # Find the first noun on the command line and only load commands from that - # module to improve startup time. - for a in argv: - if not a.startswith('-'): - commands.add_to_parser(self.parser, self.session, a) - break - else: - # No noun found, so load all commands. - commands.add_to_parser(self.parser, self.session) + self.raise_event(self.COMMAND_PARSER_CREATED, self.parser) - self.session.raise_event('CommandParser.Loaded', self.parser) - argcomplete.autocomplete(self.parser) + def load_commands(self): + self.parser.load_command_table(self.configuration.get_command_table()) + self.raise_event(self.COMMAND_PARSER_LOADED, self.parser) def execute(self, argv): args = self.parser.parse_args(argv) - self.session.raise_event('CommandParser.Parsed', args) + self.raise_event(self.COMMAND_PARSER_PARSED, args) # Consider - we are using any args that start with an underscore (_) as 'private' # arguments and remove them from the arguments that we pass to the actual function. # This does not feel quite right. params = dict([(key, value) for key, value in args.__dict__.items() if not key.startswith('_')]) - result = args.func(params, {}) # TODO: Unexpected parameters passed in? - return result + result = args.func(params) + + result = self.todict(result) + event_data = {'result': result} + self.raise_event(self.TRANSFORM_RESULT, event_data) + self.raise_event(self.FILTER_RESULT, event_data) + return event_data['result'] + + def raise_event(self, name, event_data): + '''Raise the event `name`. + ''' + for func in self._event_handlers[name]: + func(event_data) + + def register(self, name, handler): + '''Register a callable that will be called when the + event `name` is raised. + + param: name: The name of the event + param: handler: Function that takes two parameters; + name: name of the event raised + event_data: `dict` with event specific data. + ''' + self._event_handlers[name].append(handler) - def _register_builtin_arguments(self, name, parser): + KEYS_CAMELCASE_PATTERN = re.compile('(?!^)_([a-zA-Z])') + def todict(self, obj): #pylint: disable=too-many-return-statements + + def to_camelcase(s): + return re.sub(Application.KEYS_CAMELCASE_PATTERN, lambda x: x.group(1).upper(), s) + + if isinstance(obj, dict): + return {k: self.todict(v) for (k, v) in obj.items()} + elif isinstance(obj, list): + return [self.todict(a) for a in obj] + elif isinstance(obj, Enum): + return obj.value + elif isinstance(obj, datetime): + return obj.isoformat() + elif hasattr(obj, '_asdict'): + return self.todict(obj._asdict()) + elif hasattr(obj, '__dict__'): + return dict([(to_camelcase(k), self.todict(v)) + for k, v in obj.__dict__.items() + if not callable(v) and not k.startswith('_')]) + else: + return obj + + @staticmethod + def _enable_autocomplete(parser): + import argcomplete + argcomplete.autocomplete(parser) + + @staticmethod + def _register_builtin_arguments(parser): parser.add_argument('--subscription', dest='_subscription_id', help=argparse.SUPPRESS) - parser.add_argument('--output', '-o', dest='_output_format', choices=['list', 'json']) - - def _handle_builtin_arguments(self, name, args): - try: - self.session.output_format = args._output_format #pylint: disable=protected-access - del args._output_format - except Exception: - pass + parser.add_argument('--output', '-o', dest='_output_format', choices=['list', 'json'], + help=argparse.SUPPRESS) + + def _handle_builtin_arguments(self, args): + self.configuration.output_format = args._output_format #pylint: disable=protected-access + del args._output_format diff --git a/src/azure/cli/commands/__init__.py b/src/azure/cli/commands/__init__.py index e018fdf870f..40dec44f2fd 100644 --- a/src/azure/cli/commands/__init__.py +++ b/src/azure/cli/commands/__init__.py @@ -13,7 +13,7 @@ COMMON_PARAMETERS = { 'resource_group_name': { - 'name': '--resourcegroup --rg', + 'name': '--resourcegroup -g', 'metavar': 'RESOURCE GROUP', 'help': 'Name of resource group', 'required': True @@ -39,7 +39,7 @@ class CommandTable(defaultdict): ArgumentParser.add_parser. """ def __init__(self): - super(CommandTable, self).__init__(lambda: {'options': []}) + super(CommandTable, self).__init__(lambda: {'arguments': []}) def command(self, name, **kwargs): def wrapper(func): @@ -58,19 +58,19 @@ def option(self, name, **kwargs): def wrapper(func): opt = dict(kwargs) opt['name'] = name - self[func]['options'].append(opt) + self[func]['arguments'].append(opt) return func return wrapper -def get_command_table(command_name): +def _get_command_table(command_name): module = __import__('azure.cli.commands.' + command_name) for part in ('cli.commands.' + command_name).split('.'): module = getattr(module, part) return module.get_command_table() -def add_to_parser(parser, session, module_name=None): - '''Loads commands into the parser +def get_command_table(module_name=None): + '''Loads command table(s) When `module_name` is specified, only commands from that module will be loaded. If the module is not found, all commands are loaded. @@ -78,13 +78,15 @@ def add_to_parser(parser, session, module_name=None): loaded = False if module_name: try: - parser.load_command_table(session, get_command_table(module_name)) + command_table = _get_command_table(module_name) loaded = True except ImportError: # Unknown command - we'll load all below pass if not loaded: + command_table = {} for mod in COMMAND_MODULES: - parser.load_command_table(session, get_command_table(mod)) + command_table.update(_get_command_table(mod)) loaded = True + return command_table diff --git a/src/azure/cli/commands/_auto_command.py b/src/azure/cli/commands/_auto_command.py index ba0c2f0bfc0..cefeabc9987 100644 --- a/src/azure/cli/commands/_auto_command.py +++ b/src/azure/cli/commands/_auto_command.py @@ -5,9 +5,7 @@ from msrest.paging import Paged from msrest.exceptions import ClientException from azure.cli.parser import IncorrectUsageError -from ..commands import CommandTable, COMMON_PARAMETERS - -from argcomplete import warn +from ..commands import COMMON_PARAMETERS EXCLUDED_PARAMS = frozenset(['self', 'raw', 'custom_headers', 'operation_config']) @@ -51,7 +49,7 @@ def _get_member(obj, path): return obj def _make_func(client_factory, member_path, return_type_or_func, unbound_func): - def call_client(args, unexpected): #pylint: disable=unused-argument + def call_client(args): client = client_factory() ops_instance = _get_member(client, member_path) try: @@ -110,6 +108,6 @@ def build_operation(command_table, command_name, member_path, client_type, opera command_table[func] = { 'name': ' '.join([command_name, opname]), 'handler': func, - 'options': options + 'arguments': options } diff --git a/src/azure/cli/commands/account.py b/src/azure/cli/commands/account.py index f1c36f1486d..ab283db0b27 100644 --- a/src/azure/cli/commands/account.py +++ b/src/azure/cli/commands/account.py @@ -4,19 +4,20 @@ command_table = CommandTable() -def get_command_table(): +def get_command_table(): # pylint:disable=duplicate-code return command_table @command_table.command('account list', description=L('List the imported subscriptions.')) -def list_subscriptions(args, unexpected): #pylint: disable=unused-argument +def list_subscriptions(_): profile = Profile() subscriptions = profile.load_subscriptions() return subscriptions -@command_table.option('--subscription-id -n', metavar='SUBSCRIPTION_ID', dest='subscription_id', help=L('Subscription Id, unique name also works.')) +@command_table.option('--subscription-id -n', metavar='SUBSCRIPTION_ID', dest='subscription_id', + help=L('Subscription Id, unique name also works.')) @command_table.command('account set', description=L('Set the current subscription')) -def set_active_subscription(args, unexpected): #pylint: disable=unused-argument +def set_active_subscription(args): subscription_id = args.get('subscription-id') if not id: raise ValueError(L('Please provide subscription id or unique name.')) diff --git a/src/azure/cli/commands/login.py b/src/azure/cli/commands/login.py index e7cb086aca5..0b136e1ddd9 100644 --- a/src/azure/cli/commands/login.py +++ b/src/azure/cli/commands/login.py @@ -15,17 +15,20 @@ command_table = CommandTable() -def get_command_table(): +def get_command_table(): # pylint:disable=duplicate-code return command_table @command_table.option('--username -u', - help=L('organization Id or service principal. Microsoft Account is not yet supported.')) -@command_table.option('--password -p', help=L('user password or client secret, will prompt if not given.')) -@command_table.option('--service-principal', help=L('the credential represents a service principal.')) + help=L('organization Id or service principal. Microsoft Account is not yet supported.')) # pylint: disable=line-too-long +@command_table.option('--password -p', + help=L('user password or client secret, will prompt if not given.')) +@command_table.option('--service-principal', + help=L('the credential represents a service principal.')) @command_table.option('--tenant -t', help=L('the tenant associated with the service principal.')) -@command_table.command('login', description=L('log in to an Azure subscription using Active Directory Organization Id')) -def login(args, unexpected): #pylint: disable=unused-argument +@command_table.command('login', + description=L('log in to an Azure subscription using Active Directory Organization Id')) # pylint: disable=line-too-long +def login(args): interactive = False username = args.get('username') diff --git a/src/azure/cli/commands/logout.py b/src/azure/cli/commands/logout.py index 3242ec4d704..847e3f3af82 100644 --- a/src/azure/cli/commands/logout.py +++ b/src/azure/cli/commands/logout.py @@ -4,12 +4,14 @@ command_table = CommandTable() -def get_command_table(): +def get_command_table(): # pylint:disable=duplicate-code return command_table -@command_table.option('--username -u', help=L('User name used to log out from Azure Active Directory.'), +@command_table.option('--username -u', + help=L('User name used to log out from Azure Active Directory.'), required=True) -@command_table.command('logout', description=L('Log out from Azure subscription using Active Directory.')) -def logout(args, unexpected): #pylint: disable=unused-argument +@command_table.command('logout', + description=L('Log out from Azure subscription using Active Directory.')) +def logout(args): profile = Profile() profile.logout(args['username']) diff --git a/src/azure/cli/commands/network.py b/src/azure/cli/commands/network.py index 0a509bf8705..9124616f26b 100644 --- a/src/azure/cli/commands/network.py +++ b/src/azure/cli/commands/network.py @@ -255,7 +255,7 @@ def _network_client_factory(): @command_table.option('--address-space -a', metavar='ADDRESS SPACE', help=L('the VNet address-space in CIDR notation or multiple address-spaces, quoted and space-separated'), required=True) @command_table.option('--dns-servers -d', metavar='DNS SERVERS', help=L('the VNet DNS servers, quoted and space-separated')) @command_table.command('network vnet create') -def create_update_vnet(args, unexpected): #pylint: disable=unused-argument +def create_update_vnet(args): from azure.mgmt.network.models import AddressSpace, DhcpOptions, VirtualNetwork resource_group = args.get('resource-group') @@ -279,7 +279,7 @@ def create_update_vnet(args, unexpected): #pylint: disable=unused-argument @command_table.option('--vnet -v', help=L('the name of the subnet vnet'), required=True) @command_table.option('--address-prefix -a', help=L('the the address prefix in CIDR format'), required=True) @command_table.command('network subnet create') -def create_update_subnet(args, unexpected): #pylint: disable=unused-argument +def create_update_subnet(args): from azure.mgmt.network.models import Subnet resource_group = args.get('resource-group') diff --git a/src/azure/cli/commands/resource.py b/src/azure/cli/commands/resource.py index 2b651b631e9..055817934ed 100644 --- a/src/azure/cli/commands/resource.py +++ b/src/azure/cli/commands/resource.py @@ -14,7 +14,7 @@ def get_command_table(): @command_table.option('--tag-name -tn', help=L("the resource group's tag name")) @command_table.option('--tag-value -tv', help=L("the resource group's tag value")) @command_table.command('resource group list', description=L('List resource groups')) -def list_groups(args, unexpected): #pylint: disable=unused-argument +def list_groups(args): from azure.mgmt.resource.resources.models import ResourceGroup, ResourceGroupFilter rmc = get_mgmt_service_client(ResourceManagementClient, ResourceManagementClientConfiguration) @@ -40,8 +40,8 @@ def list_groups(args, unexpected): #pylint: disable=unused-argument required=True) @command_table.option('--api-version -o', help=L('the API version of the resource provider')) @command_table.option('--parent', - help=L('the name of the parent resource (if needed), in / format')) -def show_resource(args, unexpected): #pylint: disable=unused-argument + help=L('the name of the parent resource (if needed), in / format')) # pylint: disable=line-too-long +def show_resource(args): rmc = get_mgmt_service_client(ResourceManagementClient, ResourceManagementClientConfiguration) full_type = args.get('resource-type').split('/') diff --git a/src/azure/cli/commands/storage.py b/src/azure/cli/commands/storage.py index 49b5d3acd71..ab1a5e2b5b0 100644 --- a/src/azure/cli/commands/storage.py +++ b/src/azure/cli/commands/storage.py @@ -7,7 +7,6 @@ from azure.mgmt.storage import StorageManagementClient, StorageManagementClientConfiguration from azure.mgmt.storage.operations import StorageAccountsOperations -from ..parser import IncorrectUsageError from ..commands import CommandTable, COMMON_PARAMETERS as GLOBAL_COMMON_PARAMETERS from ._command_creation import get_mgmt_service_client, get_data_service_client from ..commands._auto_command import build_operation @@ -23,31 +22,39 @@ def extend_parameter(parameter_metadata, **kwargs): COMMON_PARAMETERS = GLOBAL_COMMON_PARAMETERS.copy() COMMON_PARAMETERS.update({ 'account-name': { - 'name': '--account-name -n', - 'help': L('the storage account name'), - 'required': not environ.get('AZURE_STORAGE_ACCOUNT'), - 'default': environ.get('AZURE_STORAGE_ACCOUNT') + 'name': '--account-name -n', + 'help': L('the storage account name'), + # While account name *may* actually be required if the environment variable hasn't been + # specified, it is only required unless the connection string has been specified + 'required': False, + 'default': environ.get('AZURE_STORAGE_ACCOUNT') }, - 'optional_resource_group_name': extend_parameter(GLOBAL_COMMON_PARAMETERS['resource_group_name'], required=False), + 'optional_resource_group_name': + extend_parameter(GLOBAL_COMMON_PARAMETERS['resource_group_name'], required=False), 'account_key': { - 'name': '--account-key -k', - 'help': L('the storage account key'), - 'required': not environ.get('AZURE_STORAGE_ACCESS_KEY'), - 'default': environ.get('AZURE_STORAGE_ACCESS_KEY') + 'name': '--account-key -k', + 'help': L('the storage account key'), + # While account key *may* actually be required if the environment variable hasn't been + # specified, it is only required unless the connection string has been specified + 'required': False, + 'default': environ.get('AZURE_STORAGE_ACCESS_KEY') }, 'blob-name': { 'name': '--blob-name -bn', - 'help': L('the name of the blob'), + 'help': L('the name of the blob'), 'required': True }, 'container-name': { 'name': '--container-name -c', 'required': True - }, + }, 'connection-string': { 'name': '--connection-string -t', 'help': L('the storage connection string'), - 'required': not environ.get('AZURE_STORAGE_CONNECTION_STRING'), + # You can either specify connection string or name/key. There is no convenient way + # to express this kind of relationship in argparse... + # TODO: Move to exclusive group + 'required': False, 'default': environ.get('AZURE_STORAGE_CONNECTION_STRING') } }) @@ -73,7 +80,7 @@ def _storage_client_factory(): @command_table.command('storage account list', description=L('List storage accounts.')) @command_table.option(**COMMON_PARAMETERS['optional_resource_group_name']) -def list_accounts(args, unexpected): #pylint: disable=unused-argument +def list_accounts(args): from azure.mgmt.storage.models import StorageAccount from msrestazure.azure_active_directory import UserPassCredentials smc = _storage_client_factory() @@ -88,9 +95,9 @@ def list_accounts(args, unexpected): #pylint: disable=unused-argument @command_table.description(L('Regenerate one or both keys for a storage account.')) @command_table.option(**COMMON_PARAMETERS['resource_group_name']) @command_table.option(**COMMON_PARAMETERS['account-name']) -@command_table.option('--key -y', default=['key1', 'key2'], +@command_table.option('--key -y', default=['key1', 'key2'], choices=['key1', 'key2'], help=L('Key to renew')) -def renew_account_keys(args, unexpected): #pylint: disable=unused-argument +def renew_account_keys(args): smc = _storage_client_factory() for key in args.get('key'): result = smc.storage_accounts.regenerate_key( @@ -103,7 +110,7 @@ def renew_account_keys(args, unexpected): #pylint: disable=unused-argument @command_table.command('storage account usage') @command_table.description( L('Show the current count and limit of the storage accounts under the subscription.')) -def show_account_usage(args, unexpected): #pylint: disable=unused-argument +def show_account_usage(_): smc = _storage_client_factory() return next((x for x in smc.usage.list() if x.name.value == 'StorageAccounts'), None) @@ -113,10 +120,10 @@ def show_account_usage(args, unexpected): #pylint: disable=unused-argument @command_table.option(**COMMON_PARAMETERS['account-name']) @command_table.option('--use-http', action='store_const', const='http', default='https', help=L('use http as the default endpoint protocol')) -def show_storage_connection_string(args, unexpected): #pylint: disable=unused-argument +def show_storage_connection_string(args): smc = _storage_client_factory() - endpoint_protocol = args.get('use-http') + endpoint_protocol = args.get('use-http') storage_account = args.get('account-name') keys = smc.storage_accounts.list_keys(args.get('resource_group_name'), storage_account) @@ -138,9 +145,9 @@ def show_storage_connection_string(args, unexpected): #pylint: disable=unused-ar @command_table.option(**COMMON_PARAMETERS['account-name']) @command_table.option(**COMMON_PARAMETERS['account_key']) @command_table.option(**COMMON_PARAMETERS['connection-string']) -@command_table.option('--public-access -p', default=None, choices=public_access_types.keys(), +@command_table.option('--public-access -p', default=None, choices=public_access_types.keys(), type=lambda x: public_access_types[x]) -def create_container(args, unexpected): #pylint: disable=unused-argument +def create_container(args): bbs = _get_blob_service_client(args) public_access = args.get('public-access') @@ -154,7 +161,7 @@ def create_container(args, unexpected): #pylint: disable=unused-argument @command_table.option(**COMMON_PARAMETERS['account_key']) @command_table.option(**COMMON_PARAMETERS['connection-string']) @command_table.option('--force -f', help=L('supress delete confirmation prompt')) -def delete_container(args, unexpected): #pylint: disable=unused-argument +def delete_container(args): bbs = _get_blob_service_client(args) container_name = args.get('container-name') prompt_for_delete = args.get('force') is None @@ -173,7 +180,7 @@ def delete_container(args, unexpected): #pylint: disable=unused-argument @command_table.option(**COMMON_PARAMETERS['account_key']) @command_table.option(**COMMON_PARAMETERS['connection-string']) @command_table.option('--prefix -p', help=L('container name prefix to filter by')) -def list_containers(args, unexpected): #pylint: disable=unused-argument +def list_containers(args): bbs = _get_blob_service_client(args) results = bbs.list_containers(args.get('prefix')) return results @@ -184,7 +191,7 @@ def list_containers(args, unexpected): #pylint: disable=unused-argument @command_table.option(**COMMON_PARAMETERS['account-name']) @command_table.option(**COMMON_PARAMETERS['account_key']) @command_table.option(**COMMON_PARAMETERS['connection-string']) -def show_container(args, unexpected): #pylint: disable=unused-argument +def show_container(args): bbs = _get_blob_service_client(args) result = bbs.get_container_properties(args.get('container-name')) return result @@ -200,7 +207,8 @@ def show_container(args, unexpected): #pylint: disable=unused-argument @command_table.option(**COMMON_PARAMETERS['account-name']) @command_table.option(**COMMON_PARAMETERS['account_key']) @command_table.option(**COMMON_PARAMETERS['connection-string']) -@command_table.option('--container.public-access -cpa', default=None, choices=public_access_types.keys(), +@command_table.option('--container.public-access -cpa', default=None, + choices=public_access_types.keys(), type=lambda x: public_access_types[x]) @command_table.option('--content.type -cst') @command_table.option('--content.disposition -csd') @@ -208,7 +216,7 @@ def show_container(args, unexpected): #pylint: disable=unused-argument @command_table.option('--content.language -csl') @command_table.option('--content.md5 -csm') @command_table.option('--content.cache-control -cscc') -def create_block_blob(args, unexpected): #pylint: disable=unused-argument +def create_block_blob(args): from azure.storage.blob import ContentSettings bbs = _get_blob_service_client(args) public_access = args.get('container.public-access') @@ -234,7 +242,7 @@ def create_block_blob(args, unexpected): #pylint: disable=unused-argument @command_table.option(**COMMON_PARAMETERS['account_key']) @command_table.option(**COMMON_PARAMETERS['connection-string']) @command_table.option('--prefix -p', help=L('blob name prefix to filter by')) -def list_blobs(args, unexpected): #pylint: disable=unused-argument +def list_blobs(args): bbs = _get_blob_service_client(args) blobs = bbs.list_blobs(args.get('container-name'), prefix=args.get('prefix')) @@ -244,7 +252,7 @@ def list_blobs(args, unexpected): #pylint: disable=unused-argument @command_table.description(L('Delete a blob from a container.')) @command_table.option(**COMMON_PARAMETERS['container-name']) @command_table.option(**COMMON_PARAMETERS['blob-name']) -def delete_blob(args, unexpected): #pylint: disable=unused-argument +def delete_blob(args): bbs = _get_blob_service_client(args) return bbs.delete_blob(args.get('container-name'), args.get('blob-name')) @@ -252,7 +260,7 @@ def delete_blob(args, unexpected): #pylint: disable=unused-argument @command_table.description(L('Show properties of the specified blob.')) @command_table.option(**COMMON_PARAMETERS['container-name']) @command_table.option(**COMMON_PARAMETERS['blob-name']) -def show_blob(args, unexpected): #pylint: disable=unused-argument +def show_blob(args): bbs = _get_blob_service_client(args) return bbs.get_blob_properties(args.get('container-name'), args.get('blob-name')) @@ -261,7 +269,7 @@ def show_blob(args, unexpected): #pylint: disable=unused-argument @command_table.option(**COMMON_PARAMETERS['container-name']) @command_table.option(**COMMON_PARAMETERS['blob-name']) @command_table.option('--download-to -dt', help=L('the file path to download to'), required=True) -def download_blob(args, unexpected): #pylint: disable=unused-argument +def download_blob(args): bbs = _get_blob_service_client(args) # show dot indicator of download progress (one for every 10%) @@ -280,7 +288,7 @@ def download_blob(args, unexpected): #pylint: disable=unused-argument @command_table.option(**COMMON_PARAMETERS['account-name']) @command_table.option(**COMMON_PARAMETERS['account_key']) @command_table.option(**COMMON_PARAMETERS['container-name']) -def storage_file_create(args, unexpected): #pylint: disable=unused-argument +def storage_file_create(args): fsc = _get_file_service_client(args) fsc.create_file_from_path(args.get('share-name'), args.get('directory-name'), @@ -292,16 +300,16 @@ def storage_file_create(args, unexpected): #pylint: disable=unused-argument def _get_blob_service_client(args): from azure.storage.blob import BlockBlobService return get_data_service_client(BlockBlobService, - args['storage-account'], - args['storage-account-key'], - args['connection-string']) + args.get('storage-account', None), + args.get('storage-account-key', None), + args.get('connection-string', None)) def _get_file_service_client(args): from azure.storage.file import FileService return get_data_service_client(FileService, - args['storage-account'], - args['storage-account-key'], - args['connection-string']) + args.get('storage-account', None), + args.get('storage-account-key', None), + args.get('connection-string', None)) def _update_progress(current, total): if total: diff --git a/src/azure/cli/extensions/__init__.py b/src/azure/cli/extensions/__init__.py index 094ccbf7fd9..84dffe9399f 100644 --- a/src/azure/cli/extensions/__init__.py +++ b/src/azure/cli/extensions/__init__.py @@ -1,8 +1,6 @@ from .query import register as register_query from .transform import register as register_transform -from .experimental import register as register_experimental def register_extensions(application): - register_query(application.session) - register_transform(application.session) - register_experimental(application.session) + register_query(application) + register_transform(application) diff --git a/src/azure/cli/extensions/query.py b/src/azure/cli/extensions/query.py index 960caa174d7..bbcd29e5c07 100644 --- a/src/azure/cli/extensions/query.py +++ b/src/azure/cli/extensions/query.py @@ -1,19 +1,24 @@ -def register(event_dispatcher): - def register_global_parameter(self, parser): +def register(application): + def register_global_parameter(parser): # Let the program know that we are adding a parameter --query - parser.add_argument('--query', dest='_jmespath_query', metavar='JMESPATH', help='JMESPath query string. See http://jmespath.org/ for more information and examples.') + parser.add_argument('--query', dest='_jmespath_query', metavar='JMESPATH', + help='JMESPath query string. See http://jmespath.org/ for more information and examples.') # pylint: disable=line-too-long - def handle_query_parameter(_, args): + def handle_query_parameter(args): # Of there is a query specified on the command line, we'll take care of that! - query_value = args._jmespath_query - del(args._jmespath_query) + try: + query_value = args._jmespath_query # pylint: disable=protected-access + del args._jmespath_query - if query_value: - def filter_output(_, event_data): - import jmespath - event_data['result'] = jmespath.search(query_value, event_data['result']) + if query_value: + def filter_output(event_data): + import jmespath + event_data['result'] = jmespath.search(query_value, event_data['result']) - event_dispatcher.register(event_dispatcher.FILTER_RESULT, filter_output) + application.register(application.FILTER_RESULT, filter_output) - event_dispatcher.register('GlobalParser.Created', register_global_parameter) - event_dispatcher.register('CommandParser.Parsed', handle_query_parameter) + except AttributeError: + pass + + application.register(application.GLOBAL_PARSER_CREATED, register_global_parameter) + application.register(application.COMMAND_PARSER_PARSED, handle_query_parameter) diff --git a/src/azure/cli/extensions/transform.py b/src/azure/cli/extensions/transform.py index f9152e1f816..5804d22b607 100644 --- a/src/azure/cli/extensions/transform.py +++ b/src/azure/cli/extensions/transform.py @@ -1,8 +1,7 @@ import re def register(event_dispatcher): - @event_dispatcher.event_handler(event_dispatcher.TRANSFORM_RESULT) - def resource_group_transform(_, event_data): # pylint: disable=unused-variable + def resource_group_transform(event_data): def parse_id(strid): parsed = {} parts = re.split('/', strid) @@ -25,3 +24,4 @@ def add_resource_group(obj): add_resource_group(obj[item_key]) add_resource_group(event_data['result']) + event_dispatcher.register('TransformResult', resource_group_transform) diff --git a/src/azure/cli/main.py b/src/azure/cli/main.py index ba3c4536424..063a71ca2ea 100644 --- a/src/azure/cli/main.py +++ b/src/azure/cli/main.py @@ -1,7 +1,6 @@ import os import sys -from .parser import AzCliCommandParser from .application import Application from ._logging import configure_logging, logger @@ -26,14 +25,14 @@ def main(args, file=sys.stdout): #pylint: disable=redefined-builtin CONFIG.get('locale', 'en-US'))) app = Application() - app.load_commands(args) + app.load_commands() try: cmd_result = app.execute(args) # Commands can return a dictionary/list of results # If they do, we print the results. if cmd_result: - formatter = OutputProducer.get_formatter(app.session.output_format) - OutputProducer(formatter=formatter, file=file).out(app.session, cmd_result) + formatter = OutputProducer.get_formatter(app.configuration.output_format) + OutputProducer(formatter=formatter, file=file).out(cmd_result) except RuntimeError as ex: logger.error(ex.args[0]) return ex.args[1] if len(ex.args) >= 2 else -1 diff --git a/src/azure/cli/parser.py b/src/azure/cli/parser.py index ee19c016bf9..6718dcbd265 100644 --- a/src/azure/cli/parser.py +++ b/src/azure/cli/parser.py @@ -15,7 +15,7 @@ def __init__(self, **kwargs): self.subparsers = {} self.parents = kwargs.get('parents', None) - def load_command_table(self, session, command_table): + def load_command_table(self, command_table): """Load a command table into our parser. """ # If we haven't already added a subparser, we @@ -31,11 +31,10 @@ def load_command_table(self, session, command_table): # To work around http://bugs.python.org/issue9253, we artificially add any new # parsers we add to the "choices" section of the subparser. subparser.choices[command_name] = command_name - command_parser = subparser.add_parser(command_name, description=metadata.get('description'), + command_parser = subparser.add_parser(command_name, + description=metadata.get('description'), parents=self.parents, conflict_handler='resolve') - session.raise_event('AzCliCommandParser.SubparserCreated', - {'parser': command_parser, 'metadata': metadata}) - for arg in metadata['options']: + for arg in metadata['arguments']: names = arg.pop('name').split() command_parser.add_argument(*names, **arg) diff --git a/src/azure/cli/tests/test_argparse.py b/src/azure/cli/tests/test_argparse.py deleted file mode 100644 index ba5265b75e1..00000000000 --- a/src/azure/cli/tests/test_argparse.py +++ /dev/null @@ -1,207 +0,0 @@ -import unittest -from six import StringIO - -from azure.cli._argparse import ArgumentParser, IncorrectUsageError -from azure.cli._logging import logger -import logging -import azure.cli._util as util - -class Test_argparse(unittest.TestCase): - @classmethod - def setUpClass(cls): - # Ensure initialization has occurred correctly - import azure.cli.main - logging.basicConfig(level=logging.DEBUG) - - @classmethod - def tearDownClass(cls): - logging.shutdown() - - def test_nouns(self): - p = ArgumentParser('test') - res = [False, False, False] - def set_n1(a, b): res[0] = True - def set_n2(a, b): res[1] = True - def set_n3(a, b): res[2] = True - p.add_command(set_n1, 'n1') - p.add_command(set_n2, 'n1 n2') - p.add_command(set_n3, 'n1 n2 n3') - - p.execute('n1 n2 n3'.split()) - self.assertSequenceEqual(res, (False, False, True)) - p.execute('n1'.split()) - self.assertSequenceEqual(res, (True, False, True)) - res[0] = False - p.execute('n1 n2'.split()) - self.assertSequenceEqual(res, (False, True, True)) - - def test_args(self): - p = ArgumentParser('test') - p.add_command(lambda a, b: (a, b), 'n1', args=[('--arg -a', '', False, None), ('-b ', '', False, None)]) - - cmd_result = p.execute('n1 -a x'.split()) - res, other = cmd_result.result - self.assertTrue(res.arg) - self.assertSequenceEqual(res.positional, ['x']) - - # Should recognize args with alternate prefix - cmd_result = p.execute('n1 /a'.split()) - res, other = cmd_result.result - self.assertTrue(res.arg) - cmd_result = p.execute('n1 /arg'.split()) - res, other = cmd_result.result - self.assertTrue(res.arg) - - # Should not recognize "------a" - cmd_result = p.execute('n1 ------a'.split()) - res, other = cmd_result.result - self.assertNotIn('arg', res) - # First two '--' match, so '----a' is added to dict - self.assertIn('----a', other) - - cmd_result = p.execute('n1 -a:x'.split()) - res = cmd_result.result - self.assertIsNone(res) - - cmd_result = p.execute('n1 -b -a x'.split()) - res, other = cmd_result.result - self.assertEqual(res.b, '-a') - self.assertSequenceEqual(res.positional, ['x']) - self.assertRaises(IncorrectUsageError, lambda: res.arg) - - cmd_result = p.execute('n1 -b:-a x'.split()) - res, other = cmd_result.result - self.assertEqual(res.b, '-a') - self.assertSequenceEqual(res.positional, ['x']) - self.assertRaises(IncorrectUsageError, lambda: res.arg) - - def test_unexpected_args(self): - p = ArgumentParser('test') - p.add_command(lambda a, b: (a, b), 'n1', args=[('-a', '', False, None)]) - - cmd_result = p.execute('n1 -b=2'.split()) - res, other = cmd_result.result - self.assertFalse(res) - self.assertEqual('2', other.b) - - cmd_result = p.execute('n1 -b.c.d=2'.split()) - res, other = cmd_result.result - self.assertFalse(res) - self.assertEqual('2', other.b.c.d) - - cmd_result = p.execute('n1 -b.c.d 2 -b.c.e:3'.split()) - res, other = cmd_result.result - self.assertFalse(res) - self.assertEqual('2', other.b.c.d) - self.assertEqual('3', other.b.c.e) - - def test_required_args(self): - p = ArgumentParser('test') - p.add_command(lambda a, b: (a, b), - 'n1', args=[('--arg -a', '', True, None), - ('-b ', '', False, None)]) - - cmd_result = p.execute('n1 -a x'.split()) - res, other = cmd_result.result - self.assertTrue(res.arg) - self.assertSequenceEqual(res.positional, ['x']) - - io = StringIO() - cmd_result = p.execute('n1 -b x'.split(), out=io) - self.assertIsNone(cmd_result.result) - self.assertTrue(io.getvalue().startswith("Missing required argument 'arg'")) - io.close() - - def test_specify_output_format(self): - p = ArgumentParser('test') - p.add_command(lambda a, b: (a, b), 'n1', args=[('--arg -a', '', True, None), ('-b ', '', False, None)]) - - cmd_result = p.execute('n1 -a x'.split()) - self.assertEqual(cmd_result.output_format, None) - - cmd_result = p.execute('n1 -a x --output json'.split()) - self.assertEqual(cmd_result.output_format, 'json') - - cmd_result = p.execute('n1 -a x --output table'.split()) - self.assertEqual(cmd_result.output_format, 'table') - - cmd_result = p.execute('n1 -a x --output text'.split()) - self.assertEqual(cmd_result.output_format, 'text') - - # Invalid format - io = StringIO() - cmd_res = p.execute('n1 -a x --output unknown'.split(), out=io) - self.assertIsNone(cmd_res.output_format) - self.assertTrue(io.getvalue().startswith("Invalid output format 'unknown'")) - io.close() - - # Invalid format - cmd_result = p.execute('n1 -a x --output'.split()) - self.assertEqual(cmd_result.output_format, None) - - def test_args_completion(self): - p = ArgumentParser('test') - p.add_command(lambda a, b: (a, b), 'n1', args=[('--arg -a', '', True, None), ('-b ', '', False, None)]) - - # Can't use "with StringIO() as ...", as Python2/StringIO doesn't have __exit__. - io = StringIO() - p.execute('n1 - --complete'.split(), - show_usage=False, - show_completions=True, - out=io) - candidates = util.normalize_newlines(io.getvalue()) - io.close() - self.assertEqual(candidates, '--arg\n-a\n-b\n') - - #matching '--arg for '--a' - io=StringIO() - p.execute('n1 --a --complete'.split(), - show_usage=False, - out=io) - candidates = util.normalize_newlines(io.getvalue()) - io.close() - self.assertEqual(candidates, '--arg\n') - - #matching 'n1' for 'n' - io = StringIO() - p.execute('n --complete'.split(), - show_usage=False, - show_completions=True, - out=io) - candidates = util.normalize_newlines(io.getvalue()) - io.close() - self.assertEqual(candidates, 'n1\n') - - #if --arg is used, then both '-a' and "--arg" should not be in the - #candidate list - io = StringIO() - p.execute('n1 --arg hello - --complete'.split(), - show_usage=False, - show_completions=True, - out=io) - candidates = util.normalize_newlines(io.getvalue()) - io.close() - self.assertEqual(candidates, '-b\n') - - #if all argument are used, candidate list is empty - io = StringIO() - p.execute('n1 -a -b --complete'.split(), - show_usage=False, - show_completions=True, - out=io) - candidates = util.normalize_newlines(io.getvalue()) - io.close() - self.assertEqual(candidates, '\n') - - #if at parameter value level, get nothing for N.Y.I. - io = StringIO() - p.execute('n1 -a --complete'.split(), - show_usage=False, - show_completions=True, - out=io) - candidates = util.normalize_newlines(io.getvalue()) - io.close() - self.assertEqual(candidates, '\n') - -if __name__ == '__main__': - unittest.main() diff --git a/src/azure/cli/tests/test_connection_verify.py b/src/azure/cli/tests/test_connection_verify.py index 7a6b1c6613b..ca6444f7ddf 100644 --- a/src/azure/cli/tests/test_connection_verify.py +++ b/src/azure/cli/tests/test_connection_verify.py @@ -5,7 +5,7 @@ except ImportError: from mock import MagicMock -from azure.cli._argparse import ArgumentParser, IncorrectUsageError +from azure.cli.parser import IncorrectUsageError from azure.cli._logging import logger import logging import azure.cli._debug as _debug