diff --git a/.travis.yml b/.travis.yml index 2c5e2e0fee8..fc69fd75712 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,9 @@ python: install: - pip install azure==2.0.0a1 - pip install mock==1.3.0 + - pip install pylint==1.5.4 script: - export PYTHONPATH=$PATHONPATH:./src - python -m azure.cli - - python -m unittest discover -s src/azure/cli/tests + - pylint src/azure + - python -m unittest discover -s src/azure/cli/tests \ No newline at end of file diff --git a/azure-cli.pyproj b/azure-cli.pyproj index 4b08683558d..10e839a95da 100644 --- a/azure-cli.pyproj +++ b/azure-cli.pyproj @@ -44,6 +44,7 @@ Code + @@ -52,6 +53,7 @@ Code + diff --git a/pylintrc b/pylintrc new file mode 100644 index 00000000000..f9e4a383c98 --- /dev/null +++ b/pylintrc @@ -0,0 +1,13 @@ +[MESSAGES CONTROL] +# For all codes, run 'pylint --list-msgs' or go to 'http://pylint-messages.wikidot.com/all-codes' +# C0111 Missing docstring +# C0103 Invalid %s name "%s" +# C0303 Trailing whitespace +# I0011 Warning locally suppressed using disable-msg +# W0511 fixme +disable=C0111,C0103,C0303,I0011,W0511 +[DESIGN] +# Maximum number of locals for function / method body +max-locals=25 +# Maximum number of branch for function / method body +max-branches=20 \ No newline at end of file diff --git a/src/azure/cli/_argparse.py b/src/azure/cli/_argparse.py index 2e46c9186f4..623bfbadc8c 100644 --- a/src/azure/cli/_argparse.py +++ b/src/azure/cli/_argparse.py @@ -1,9 +1,7 @@ from __future__ import print_function -import json -import os import sys -from ._locale import get_file as locale_get_file +from ._locale import L, get_file as locale_get_file from ._logging import logger # Named arguments are prefixed with one of these strings @@ -20,7 +18,7 @@ class IncorrectUsageError(Exception): pass class Arguments(dict): - def __init__(self, source=None): + def __init__(self, source=None): #pylint: disable=super-init-not-called self.positional = [] if source: self.update(source) @@ -40,7 +38,7 @@ def __getattr__(self, key): except LookupError: pass logger.debug('Argument %s is required', key) - raise IncorrectUsageError(_("Argument {0} is required").format(key)) + raise IncorrectUsageError(L('Argument {0} is required').format(key)) def _read_arg(string): for prefix in ARG_PREFIXES: @@ -66,11 +64,15 @@ def __init__(self, prog): self.noun_map = { '$doc': 'azure-cli.txt', } - self.help_args = { '--help', '-h' } - self.complete_args = { '--complete' } - self.global_args = { '--verbose', '--debug' } - - def add_command(self, handler, name=None, description=None, args=None): + self.help_args = {'--help', '-h'} + self.complete_args = {'--complete'} + self.global_args = {'--verbose', '--debug'} + + def add_command(self, + handler, + name=None, + description=None, + args=None): '''Registers a command that may be parsed by this parser. `handler` is the function to call with two `Arguments` objects. @@ -105,7 +107,7 @@ def add_command(self, handler, name=None, description=None, args=None): m['$args'] = [] m['$kwargs'] = kw = {} m['$argdoc'] = ad = [] - for spec, desc in (args or []): + for spec, desc in args or []: if not any(spec.startswith(p) for p in ARG_PREFIXES): m['$args'].append(spec.strip('<> ')) ad.append((spec, desc)) @@ -121,7 +123,11 @@ def add_command(self, handler, name=None, description=None, args=None): ad.append(('/'.join(aliases), desc)) - def execute(self, args, show_usage=False, show_completions=False, out=sys.stdout): + def execute(self, + args, + show_usage=False, + show_completions=False, + out=sys.stdout): '''Parses `args` and invokes the associated handler. The handler is passed two `Arguments` objects with all arguments other @@ -142,10 +148,11 @@ def execute(self, args, show_usage=False, show_completions=False, out=sys.stdout if not show_completions: show_completions = any(a in self.complete_args for a in args) - all_global_args = set(a.lstrip('-/') for a in self.help_args | self.complete_args | self.global_args) + all_global_args = set( + a.lstrip('-/') for a in self.help_args | self.complete_args | self.global_args) def not_global(a): return a.lstrip('-/') not in all_global_args - it = filter(not_global, args).__iter__() + it = filter(not_global, args).__iter__() #pylint: disable=bad-builtin m = self.noun_map nouns = [] @@ -161,7 +168,6 @@ def not_global(a): n = next(it, '') try: - expected_args = m['$args'] expected_kwargs = m['$kwargs'] handler = m['$handler'] except LookupError: @@ -169,9 +175,9 @@ def not_global(a): show_usage = True if show_completions: - return self._display_completions(nouns, m, args, out) + return ArgumentParser._display_completions(m, out) if show_usage: - return self._display_usage(nouns, m, args, out) + return self._display_usage(nouns, m, out) parsed = Arguments() others = Arguments() @@ -189,8 +195,9 @@ def not_global(a): elif target_value[1] is True: # Arg with no value if value is not None: - print(_("argument '{0}' does not take a value").format(key_n), file=out) - return self._display_usage(nouns, m, args, out) + print(L("argument '{0}' does not take a value").format(key_n), + file=out) + return self._display_usage(nouns, m, out) parsed.add_from_dotted(target_value[0], True) else: # Arg with a value @@ -208,11 +215,11 @@ def not_global(a): return handler(parsed, others) except IncorrectUsageError as ex: print(str(ex), file=out) - return self._display_usage(nouns, m, args, out) + return self._display_usage(nouns, m, out) finally: sys.stdout = old_stdout - def _display_usage(self, nouns, noun_map, arguments, out=sys.stdout): + def _display_usage(self, nouns, noun_map, out=sys.stdout): spec = ' '.join(noun_map.get('$spec') or nouns) print(' {} {}'.format(self.prog, spec), file=out) print(file=out) @@ -246,7 +253,8 @@ def _display_usage(self, nouns, noun_map, arguments, out=sys.stdout): out.flush() logger.debug('Expected documentation at %s', doc_file) - def _display_completions(self, nouns, noun_map, arguments, out=sys.stdout): + @staticmethod + def _display_completions(noun_map, out=sys.stdout): completions = [k for k in noun_map if not k.startswith('$')] kwargs = noun_map.get('$kwargs') diff --git a/src/azure/cli/_debug.py b/src/azure/cli/_debug.py index 845ec253cd9..c5d8a1db767 100644 --- a/src/azure/cli/_debug.py +++ b/src/azure/cli/_debug.py @@ -5,7 +5,8 @@ def allow_debug_connection(client): if should_disable_connection_verify(): - logger.warn("Connection verification disabled by environment variable %s", DISABLE_VERIFY_VARIABLE_NAME) + logger.warning("Connection verification disabled by environment variable %s", + DISABLE_VERIFY_VARIABLE_NAME) client.config.connection.verify = False def should_disable_connection_verify(): diff --git a/src/azure/cli/_locale.py b/src/azure/cli/_locale.py index 97faae764d4..b56abe94a66 100644 --- a/src/azure/cli/_locale.py +++ b/src/azure/cli/_locale.py @@ -1,11 +1,16 @@ import os.path +from codecs import open as codecs_open -from codecs import open +_translations = dict() +_locale_dir = '' + +def L(key): + return _translations.get(key) or ''.format(key) def install(locale_dir): mapping = [] - with open(os.path.join(locale_dir, "messages.txt"), 'r', encoding='utf-8-sig') as f: + with codecs_open(os.path.join(locale_dir, "messages.txt"), 'r', encoding='utf-8-sig') as f: for i in f: if not i or i.startswith('#') or not i.strip(): continue @@ -14,16 +19,12 @@ def install(locale_dir): else: mapping[-1] = (mapping[-1][0], i.strip()) - translations = dict(mapping) - def _(key): - return translations.get(key) or ''.format(key) - _.locale_dir = locale_dir - - __builtins__['_'] = _ + globals()['_translations'] = dict(mapping) + globals()['_locale_dir'] = locale_dir def get_file(name): try: - src = _.locale_dir + src = _locale_dir except (NameError, AttributeError): raise RuntimeError("localizations not installed") diff --git a/src/azure/cli/_logging.py b/src/azure/cli/_logging.py index ada07b71ef4..0102e0aabe3 100644 --- a/src/azure/cli/_logging.py +++ b/src/azure/cli/_logging.py @@ -1,7 +1,7 @@ import logging as _logging import sys -__all__ = ['logging', 'configure_logging'] +__all__ = ['logger', 'configure_logging'] logger = _logging.Logger('az', _logging.WARNING) diff --git a/src/azure/cli/_output.py b/src/azure/cli/_output.py index 53d84bc87c6..529e25ea99f 100644 --- a/src/azure/cli/_output.py +++ b/src/azure/cli/_output.py @@ -8,7 +8,7 @@ from io import StringIO except ImportError: # Python 2 - from StringIO import StringIO + from StringIO import StringIO #pylint: disable=import-error class OutputFormatException(Exception): pass @@ -40,9 +40,9 @@ def format_text(obj): except TypeError: return '' -class OutputProducer(object): +class OutputProducer(object): #pylint: disable=too-few-public-methods - def __init__(self, formatter=format_json, file=sys.stdout): + def __init__(self, formatter=format_json, file=sys.stdout): #pylint: disable=redefined-builtin self.formatter = formatter self.file = file @@ -100,14 +100,14 @@ def add(self, identifier, value): def dump(self): with StringIO() as io: - for id in sorted(self.identifiers): - io.write(id.upper()) + for identifier in sorted(self.identifiers): + io.write(identifier.upper()) io.write('\t') - for col in self.identifiers[id]: + for col in self.identifiers[identifier]: if isinstance(col, str): io.write(col) else: - # TODO: Handle complex objects + # TODO: Need to handle complex objects io.write("null") io.write('\t') io.write('\n') diff --git a/src/azure/cli/_profile.py b/src/azure/cli/_profile.py index f72e70ecaa5..87834bbefb5 100644 --- a/src/azure/cli/_profile.py +++ b/src/azure/cli/_profile.py @@ -1,6 +1,6 @@ -from msrest.authentication import BasicTokenAuthentication +import collections +from msrest.authentication import BasicTokenAuthentication from .main import CONFIG -import collections class Profile(object): @@ -37,10 +37,9 @@ def set_subscriptions(self, new_subscriptions, access_token): for s in subscriptions: s['active'] = False - if new_active_one: - new_active_one['active'] = True - else: - new_subscriptions[0]['active'] = True + if not new_active_one: + new_active_one = new_subscriptions[0] + new_active_one['active'] = True else: new_subscriptions[0]['active'] = True diff --git a/src/azure/cli/_session.py b/src/azure/cli/_session.py index 45cdcab163d..39881574ef6 100644 --- a/src/azure/cli/_session.py +++ b/src/azure/cli/_session.py @@ -7,7 +7,7 @@ import collections -from codecs import open +from codecs import open as codecs_open class Session(collections.MutableMapping): '''A simple dict-like class that is backed by a JSON file. @@ -28,14 +28,14 @@ def load(self, filename, max_age=0): st = os.stat(self.filename) if st.st_mtime + max_age < time.clock(): self.save() - with open(self.filename, 'r', encoding='utf-8-sig') as f: + with codecs_open(self.filename, 'r', encoding='utf-8-sig') as f: self.data = json.load(f) except (OSError, IOError): self.save() def save(self): if self.filename: - with open(self.filename, 'w', encoding='utf-8-sig') as f: + with codecs_open(self.filename, 'w', encoding='utf-8-sig') as f: json.dump(self.data, f) def save_with_retry(self, retries=5): diff --git a/src/azure/cli/_util.py b/src/azure/cli/_util.py index 6449b631737..f667ddd59bb 100644 --- a/src/azure/cli/_util.py +++ b/src/azure/cli/_util.py @@ -1,3 +1,2 @@ - def normalize_newlines(str_to_normalize): return str_to_normalize.replace('\r\n', '\n') diff --git a/src/azure/cli/commands/__init__.py b/src/azure/cli/commands/__init__.py index f4172e18b04..b39c45a5492 100644 --- a/src/azure/cli/commands/__init__.py +++ b/src/azure/cli/commands/__init__.py @@ -21,21 +21,21 @@ def add_command(handler): return handler return add_command -def description(description): +def description(description_text): def add_description(handler): - _COMMANDS.setdefault(handler, {})['description'] = description - logger.debug('Added description "%s" to %s', description, handler) + _COMMANDS.setdefault(handler, {})['description'] = description_text + logger.debug('Added description "%s" to %s', description_text, handler) return handler return add_description -def option(spec, description=None): +def option(spec, description_text=None): def add_option(handler): - _COMMANDS.setdefault(handler, {}).setdefault('args', []).append((spec, description)) + _COMMANDS.setdefault(handler, {}).setdefault('args', []).append((spec, description_text)) logger.debug('Added option "%s" to %s', spec, handler) return handler return add_option -def add_to_parser(parser, command=None): +def add_to_parser(parser, command_name=None): '''Loads commands into the parser When `command` is specified, only commands from that module will be loaded. @@ -45,9 +45,9 @@ def add_to_parser(parser, command=None): # Importing the modules is sufficient to invoke the decorators. Then we can # get all of the commands from the _COMMANDS variable. loaded = False - if command: + if command_name: try: - __import__('azure.cli.commands.' + command) + __import__('azure.cli.commands.' + command_name) loaded = True except ImportError: # Unknown command - we'll load all below diff --git a/src/azure/cli/commands/account.py b/src/azure/cli/commands/account.py index 4df6168ecf4..c49a479b533 100644 --- a/src/azure/cli/commands/account.py +++ b/src/azure/cli/commands/account.py @@ -1,21 +1,22 @@ from .._profile import Profile from ..commands import command, description, option +from .._locale import L @command('account list') -@description(_('List the imported subscriptions.')) -def list_subscriptions(args, unexpected): +@description(L('List the imported subscriptions.')) +def list_subscriptions(args, unexpected): #pylint: disable=unused-argument profile = Profile() subscriptions = profile.load_subscriptions() return subscriptions @command('account set') -@description(_('Set the current subscription')) -@option('--subscription-id -n ', _('Subscription Id, unique name also works.')) -def set_active_subscription(args, unexpected): - id = args.get('subscription-id') +@description(L('Set the current subscription')) +@option('--subscription-id -n ', L('Subscription Id, unique name also works.')) +def set_active_subscription(args, unexpected): #pylint: disable=unused-argument + subscription_id = args.get('subscription-id') if not id: - raise ValueError(_('Please provide subscription id or unique name.')) + raise ValueError(L('Please provide subscription id or unique name.')) profile = Profile() - profile.set_active_subscription(id) + profile.set_active_subscription(subscription_id) diff --git a/src/azure/cli/commands/login.py b/src/azure/cli/commands/login.py index 09e30b57a51..31125f3492f 100644 --- a/src/azure/cli/commands/login.py +++ b/src/azure/cli/commands/login.py @@ -1,33 +1,36 @@ -from msrestazure.azure_active_directory import UserPassCredentials +from msrest import Serializer +from msrestazure.azure_active_directory import UserPassCredentials from azure.mgmt.resource.subscriptions import SubscriptionClient, \ SubscriptionClientConfiguration -from msrest import Serializer - from .._profile import Profile from ..commands import command, description, option from .._debug import should_disable_connection_verify +from .._locale import L CLIENT_ID = '04b07795-8ddb-461a-bbee-02f9e1bf7b46' @command('login') -@description(_('log in to an Azure subscription using Active Directory Organization Id')) -@option('--username -u ', _('organization Id. Microsoft Account is not yet supported.')) -@option('--password -p ', _('user password, will prompt if not given.')) -def login(args, unexpected): +@description(L('log in to an Azure subscription using Active Directory Organization Id')) +@option('--username -u ', L('organization Id. Microsoft Account is not yet supported.')) +@option('--password -p ', L('user password, will prompt if not given.')) +def login(args, unexpected): #pylint: disable=unused-argument username = args.get('username') password = args.get('password') if not password: import getpass - password = getpass.getpass(_('Password: ')) + password = getpass.getpass(L('Password: ')) - credentials = UserPassCredentials(username, password, client_id=CLIENT_ID, verify=not should_disable_connection_verify()) + credentials = UserPassCredentials(username, + password, + client_id=CLIENT_ID, + verify=not should_disable_connection_verify()) client = SubscriptionClient(SubscriptionClientConfiguration(credentials)) subscriptions = client.subscriptions.list() if not subscriptions: - raise RuntimeError(_('No subscriptions found for this account.')) + raise RuntimeError(L('No subscriptions found for this account.')) serializable = Serializer().serialize_data(subscriptions, "[Subscription]") diff --git a/src/azure/cli/commands/logout.py b/src/azure/cli/commands/logout.py index 341904576e7..d4ce01a2024 100644 --- a/src/azure/cli/commands/logout.py +++ b/src/azure/cli/commands/logout.py @@ -1,13 +1,14 @@ from .._profile import Profile from ..commands import command, description, option +from .._locale import L @command('logout') -@description(_('Log out from Azure subscription using Active Directory.')) -@option('--username -u ', _('User name used to log out from Azure Active Directory.')) -def logout(args, unexpected): +@description(L('Log out from Azure subscription using Active Directory.')) +@option('--username -u ', L('User name used to log out from Azure Active Directory.')) +def logout(args, unexpected): #pylint: disable=unused-argument username = args.get('username') if not username: - raise ValueError(_('Please provide a valid username to logout.')) + raise ValueError(L('Please provide a valid username to logout.')) profile = Profile() profile.logout(username) diff --git a/src/azure/cli/commands/resourcegroup.py b/src/azure/cli/commands/resourcegroup.py index 6a7eec557b5..bea80ae2cc7 100644 --- a/src/azure/cli/commands/resourcegroup.py +++ b/src/azure/cli/commands/resourcegroup.py @@ -1,24 +1,24 @@ from msrest import Serializer - -from ..commands import command, description, option +from ..commands import command, description from .._profile import Profile @command('resource group list') @description('List resource groups') # TODO: waiting on Python Azure SDK bug fixes -#@option('--tag-name -g ', _("the resource group's tag name")) -#@option('--tag-value -g ', _("the resource group's tag value")) -#@option('--top -g ', _("Top N resource groups to retrieve")) -def list_groups(args, unexpected): - from azure.mgmt.resource.resources import ResourceManagementClient, ResourceManagementClientConfiguration +# @option('--tag-name -g ', L('the resource group's tag name')) +# @option('--tag-value -g ', L('the resource group's tag value')) +# @option('--top -g ', L('Top N resource groups to retrieve')) +def list_groups(args, unexpected): #pylint: disable=unused-argument + from azure.mgmt.resource.resources import ResourceManagementClient, \ + ResourceManagementClientConfiguration from azure.mgmt.resource.resources.models import ResourceGroup, ResourceGroupFilter - rmc = ResourceManagementClient(ResourceManagementClientConfiguration(*Profile().get_login_credentials())) + rmc = ResourceManagementClient( + ResourceManagementClientConfiguration(*Profile().get_login_credentials())) # TODO: waiting on Python Azure SDK bug fixes #group_filter = ResourceGroupFilter(args.get('tag-name'), args.get('tag-value')) #groups = rmc.resource_groups.list(filter=None, top=args.get('top')) groups = rmc.resource_groups.list() - serializable = Serializer().serialize_data(groups, "[ResourceGroup]") return serializable diff --git a/src/azure/cli/commands/storage.py b/src/azure/cli/commands/storage.py index 15fde8488db..b765431c9e4 100644 --- a/src/azure/cli/commands/storage.py +++ b/src/azure/cli/commands/storage.py @@ -1,14 +1,14 @@ from msrest import Serializer - -from ..main import SESSION from ..commands import command, description, option from ._command_creation import get_service_client +from .._logging import logger +from .._locale import L @command('storage account list') -@description(_('List storage accounts')) -@option('--resource-group -g ', _('the resource group name')) -@option('--subscription -s ', _('the subscription id')) -def list_accounts(args, unexpected): +@description(L('List storage accounts')) +@option('--resource-group -g ', L('the resource group name')) +@option('--subscription -s ', L('the subscription id')) +def list_accounts(args, unexpected): #pylint: disable=unused-argument from azure.mgmt.storage import StorageManagementClient, StorageManagementClientConfiguration from azure.mgmt.storage.models import StorageAccount from msrestazure.azure_active_directory import UserPassCredentials @@ -26,10 +26,7 @@ def list_accounts(args, unexpected): @command('storage account check') @option('--account-name ') -def checkname(args, unexpected): +def checkname(args, unexpected): #pylint: disable=unused-argument from azure.mgmt.storage import StorageManagementClient, StorageManagementClientConfiguration - smc = get_service_client(StorageManagementClient, StorageManagementClientConfiguration) - logger.warn(smc.storage_accounts.check_name_availability(args.account_name)) - - + logger.warning(smc.storage_accounts.check_name_availability(args.account_name)) diff --git a/src/azure/cli/main.py b/src/azure/cli/main.py index 08aaeee6ea7..a685484ccfa 100644 --- a/src/azure/cli/main.py +++ b/src/azure/cli/main.py @@ -1,7 +1,6 @@ import os from ._argparse import ArgumentParser -from ._locale import install as locale_install from ._logging import configure_logging, logger from ._session import Session from ._output import OutputProducer @@ -12,17 +11,18 @@ # SESSION provides read-write session variables SESSION = Session() -# Load the user's preferred locale from their configuration -LOCALE = CONFIG.get('locale', 'en-US') -locale_install(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'locale', LOCALE)) - - def main(args): CONFIG.load(os.path.expanduser('~/az.json')) SESSION.load(os.path.expanduser('~/az.sess'), max_age=3600) configure_logging(args, CONFIG) + from ._locale import install as locale_install + locale_install(os.path.join(os.path.dirname(os.path.abspath(__file__)), + 'locale', + CONFIG.get('locale', 'en-US'))) + + parser = ArgumentParser("az") import azure.cli.commands as commands