Skip to content

Commit

Permalink
- Removed now defunct 'unexpected parameters' parameter for all comma…
Browse files Browse the repository at this point in the history
…nd 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
  • Loading branch information
johanste committed Mar 26, 2016
1 parent 23db3f7 commit e4106b8
Show file tree
Hide file tree
Showing 21 changed files with 220 additions and 432 deletions.
8 changes: 1 addition & 7 deletions azure-cli.pyproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
<InterpreterId>{1dd9c42b-5980-42ce-a2c3-46d3bf0eede4}</InterpreterId>
<InterpreterVersion>3.5</InterpreterVersion>
<EnableNativeCodeDebugging>False</EnableNativeCodeDebugging>
<CommandLineArguments>
</CommandLineArguments>
<CommandLineArguments>vm list-all --query "[].name"</CommandLineArguments>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug'" />
<PropertyGroup Condition="'$(Configuration)' == 'Release'" />
Expand All @@ -36,7 +35,6 @@
<Compile Include="azure\cli\commands\_auto_command.py" />
<Compile Include="azure\cli\commands\_command_creation.py" />
<Compile Include="azure\cli\commands\__init__.py" />
<Compile Include="azure\cli\extensions\experimental.py" />
<Compile Include="azure\cli\extensions\query.py">
<SubType>Code</SubType>
</Compile>
Expand All @@ -48,15 +46,11 @@
<Compile Include="azure\cli\tests\command_specs\test_spec_vm.py" />
<Compile Include="azure\cli\tests\command_specs\__init__.py" />
<Compile Include="azure\cli\parser.py" />
<Compile Include="azure\cli\tests\test_argparse.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="azure\cli\tests\test_autocommand.py" />
<Compile Include="azure\cli\tests\test_commands.py" />
<Compile Include="azure\cli\tests\test_connection_verify.py" />
<Compile Include="azure\cli\tests\test_output.py" />
<Compile Include="azure\cli\_debug.py" />
<Compile Include="azure\cli\_event_dispatcher.py" />
<Compile Include="azure\cli\_locale.py" />
<Compile Include="azure\cli\tests\test_profile.py" />
<Compile Include="azure\cli\_logging.py" />
Expand Down
2 changes: 2 additions & 0 deletions pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
applicationinsights==0.10.0
argcomplete==1.1.0
azure==2.0.0rc1
jmespath
mock==1.3.0
Expand Down
46 changes: 0 additions & 46 deletions src/azure/cli/_event_dispatcher.py

This file was deleted.

33 changes: 2 additions & 31 deletions src/azure/cli/_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
144 changes: 101 additions & 43 deletions src/azure/cli/application.py
Original file line number Diff line number Diff line change
@@ -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
18 changes: 10 additions & 8 deletions src/azure/cli/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -58,33 +58,35 @@ 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.
'''
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
Loading

0 comments on commit e4106b8

Please sign in to comment.