diff --git a/azure-cli.pyproj b/azure-cli.pyproj index 131d0a662ba..729ddbf4a9c 100644 --- a/azure-cli.pyproj +++ b/azure-cli.pyproj @@ -126,26 +126,6 @@ - - - - - - - - - - - - - - - - - - - - Code @@ -161,9 +141,6 @@ Code - - - @@ -220,11 +197,6 @@ - - - - - @@ -251,20 +223,6 @@ - - - - - - - - - - - - - - diff --git a/src/azure/cli/_help.py b/src/azure/cli/_help.py index b588174bea8..a59c964c6f5 100644 --- a/src/azure/cli/_help.py +++ b/src/azure/cli/_help.py @@ -75,10 +75,18 @@ def print_arguments(help_file): required_tag = L(' [Required]') max_name_length = max(len(p.name) + (len(required_tag) if p.required else 0) for p in help_file.parameters) - for p in sorted(help_file.parameters, key=lambda p: str(not p.required) + p.name): + last_group_name = None + for p in sorted(help_file.parameters, + key=lambda p: str(p.group_name or 'A') + + str(not p.required) + p.name): indent = 1 required_text = required_tag if p.required else '' p.short_summary = (p.short_summary if p.short_summary else '') + _get_choices_str(p) + if p.group_name != last_group_name: + if p.group_name: + print('') + print(p.group_name) + last_group_name = p.group_name _print_indent('{0}{1}{2}{3}'.format(p.name, _get_column_indent(p.name + required_text, max_name_length), @@ -92,7 +100,6 @@ def print_arguments(help_file): _print_indent('{0}'.format(p.long_summary.rstrip()), indent) if p.value_sources: - _print_indent('') _print_indent(L("Values from: {0}").format(', '.join(p.value_sources)), indent) if p.long_summary or p.value_sources: @@ -105,7 +112,7 @@ def _print_header(help_file): _print_indent(L('Command') if help_file.type == 'command' else L('Group'), indent) indent += 1 - _print_indent('{0}{1}'.format(help_file.command, + _print_indent('{0}{1}'.format('az ' + help_file.command, ': ' + help_file.short_summary if help_file.short_summary else ''), @@ -138,6 +145,7 @@ def _get_choices_str(p): def _print_examples(help_file): indent = 0 + print('') _print_indent(L('Examples'), indent) for e in help_file.examples: @@ -218,7 +226,11 @@ def __init__(self, delimiters, parser): action.help, required=action.required, choices=action.choices, - default=action.default)) + default=action.default, + group_name=action.container.description)) + + help_param = next(p for p in self.parameters if p.name == '--help -h') + help_param.group_name = 'Global Arguments' def _load_from_data(self, data): super(CommandHelpFile, self)._load_from_data(data) @@ -243,7 +255,8 @@ def _load_from_data(self, data): class HelpParameter(object): #pylint: disable=too-few-public-methods, too-many-instance-attributes - def __init__(self, param_name, description, required, choices=None, default=None): #pylint: disable=too-many-arguments + def __init__(self, param_name, description, required, choices=None, #pylint: disable=too-many-arguments + default=None, group_name=None): self.name = param_name self.required = required self.type = 'string' @@ -252,6 +265,7 @@ def __init__(self, param_name, description, required, choices=None, default=None self.value_sources = [] self.choices = choices self.default = default + self.group_name = group_name def update_from_data(self, data): if self.name != data.get('name'): diff --git a/src/azure/cli/application.py b/src/azure/cli/application.py index edcecfc487f..06f7cc11474 100644 --- a/src/azure/cli/application.py +++ b/src/azure/cli/application.py @@ -50,7 +50,8 @@ def __init__(self, configuration): azure.cli.extensions.register_extensions(self) self.global_parser = AzCliCommandParser(prog='az', add_help=False) - self.raise_event(self.GLOBAL_PARSER_CREATED, self.global_parser) + global_group = self.global_parser.add_argument_group('global', 'Global Arguments') + self.raise_event(self.GLOBAL_PARSER_CREATED, global_group) self.parser = AzCliCommandParser(prog='az', parents=[self.global_parser]) self.raise_event(self.COMMAND_PARSER_CREATED, self.parser) @@ -134,17 +135,20 @@ def _enable_autocomplete(parser): 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', 'tsv'], - default='list', - help='Output format') + def _register_builtin_arguments(global_group): + global_group.add_argument('--subscription', dest='_subscription_id', help=argparse.SUPPRESS) + global_group.add_argument('--output', '-o', dest='_output_format', + choices=['list', 'json', 'tsv'], + default='list', + help='Output format') # The arguments for verbosity don't get parsed by argparse but we add it here for help. - parser.add_argument('--verbose', dest='_log_verbosity_verbose', - help='Increase logging verbosity. Use --debug for full debug logs.') - parser.add_argument('--debug', dest='_log_verbosity_debug', - help='Increase logging verbosity to show all debug logs.') + global_group.add_argument('--verbose', dest='_log_verbosity_verbose', + help='Increase logging verbosity.' + ' Use --debug for full debug logs.', + action='store_true') + global_group.add_argument('--debug', dest='_log_verbosity_debug', + help='Increase logging verbosity to show all debug logs.', + action='store_true') def _handle_builtin_arguments(self, args): self.configuration.output_format = args._output_format #pylint: disable=protected-access diff --git a/src/azure/cli/extensions/query.py b/src/azure/cli/extensions/query.py index 46aa8302301..a54579d22db 100644 --- a/src/azure/cli/extensions/query.py +++ b/src/azure/cli/extensions/query.py @@ -1,9 +1,9 @@ import collections -def _register_global_parameter(parser): +def _register_global_parameter(global_group): # 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.') # pylint: disable=line-too-long + global_group.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 register(application): def handle_query_parameter(args): diff --git a/src/azure/cli/tests/test_help.py b/src/azure/cli/tests/test_help.py index 55c9788d341..1cbbd37c58d 100644 --- a/src/azure/cli/tests/test_help.py +++ b/src/azure/cli/tests/test_help.py @@ -106,7 +106,7 @@ def test_handler(args): with self.assertRaises(SystemExit): app.execute('n1 -h'.split()) - self.assertEqual(True, io.getvalue().startswith('\nCommand\n n1\n long description')) + self.assertEqual(True, io.getvalue().startswith('\nCommand\n az n1\n long description')) @redirect_io def test_help_long_description_and_short_description(self): @@ -131,7 +131,7 @@ def test_handler(args): with self.assertRaises(SystemExit): app.execute('n1 -h'.split()) - self.assertEqual(True, io.getvalue().startswith('\nCommand\n n1: short description\n long description')) + self.assertEqual(True, io.getvalue().startswith('\nCommand\n az n1: short description\n long description')) @redirect_io def test_help_docstring_description_overrides_short_description(self): @@ -185,7 +185,7 @@ def test_handler(args): with self.assertRaises(SystemExit): app.execute('n1 -h'.split()) - self.assertEqual(True, io.getvalue().startswith('\nCommand\n n1\n line1\n line2')) + self.assertEqual(True, io.getvalue().startswith('\nCommand\n az n1\n line1\n line2')) @redirect_io @mock.patch('azure.cli.application.Application.register', return_value=None) @@ -228,7 +228,7 @@ def test_handler(args): app.execute('n1 -h'.split()) s = ''' Command - n1 + az n1 Arguments --foobar2 -fb2 [Required]: one line partial sentence @@ -236,10 +236,11 @@ def test_handler(args): --foobar -fb : one line partial sentence text, markdown, etc. - Values from: az vm list, default --foobar3 -fb3 : the foobar3 + +Global Arguments --help -h : show this help message and exit ''' self.assertEqual(s, io.getvalue()) @@ -291,7 +292,7 @@ def test_handler(args): app.execute('n1 -h'.split()) s = ''' Command - n1: this module does xyz one-line or so + az n1: this module does xyz one-line or so this module.... kjsdflkj... klsfkj paragraph1 this module.... kjsdflkj... klsfkj paragraph2 @@ -301,10 +302,12 @@ def test_handler(args): --foobar -fb : one line partial sentence text, markdown, etc. - Values from: az vm list, default + +Global Arguments --help -h : show this help message and exit + Examples foo example example details @@ -377,41 +380,42 @@ def test_handler(args): '.*Extra help param --foobar -fb.*', lambda: app.execute('n1 -h'.split())) -# Will uncomment when partial params don't bypass help (help behaviors implementation) task #115631559 -# @redirect_io -# def test_help_with_param_specified(self): -# app = Application(Configuration([])) -# def test_handler(args): -# pass - -# cmd_table = { -# test_handler: { -# 'name': 'n1', -# 'arguments': [ -# {'name': '--arg -a', 'required': False}, -# {'name': '-b', 'required': False} -# ] -# } -# } -# config = Configuration([]) - #config.get_command_table = lambda: cmd_table - #app = Application(config) + @redirect_io + @mock.patch('azure.cli.application.Application.register', return_value=None) + def test_help_with_param_specified(self, _): + app = Application(Configuration([])) + def test_handler(args): + pass -# with self.assertRaises(SystemExit): -# cmd_result = app.execute('n1 --arg -h'.split()) + cmd_table = { + test_handler: { + 'name': 'n1', + 'arguments': [ + {'name': '--arg -a', 'required': False}, + {'name': '-b', 'required': False} + ] + } + } + config = Configuration([]) + config.get_command_table = lambda: cmd_table + app = Application(config) -# s = ''' -#Command -# n1 + with self.assertRaises(SystemExit): + cmd_result = app.execute('n1 --arg foo -h'.split()) -#Arguments -# --arg -a + s = ''' +Command + az n1 -# -b +Arguments + --arg -a + -b -#''' +Global Arguments + --help -h: show this help message and exit +''' -# self.assertEqual(s, io.getvalue()) + self.assertEqual(s, io.getvalue()) @redirect_io def test_help_group_children(self): @@ -443,39 +447,42 @@ def test_handler2(args): with self.assertRaises(SystemExit): app.execute('group1 -h'.split()) - s = '\nGroup\n group1\n\nSub-Commands\n group2\n group3\n\n' + s = '\nGroup\n az group1\n\nSub-Commands\n group2\n group3\n\n' self.assertEqual(s, io.getvalue()) - # Will uncomment when all errors are shown at once (help behaviors implementation) task #115631559 - #@redirect_io - #def test_help_extra_missing_params(self): - # app = Application(Configuration([])) - # def test_handler(args): - # pass - - # cmd_table = { - # test_handler: { - # 'name': 'n1', - # 'arguments': [ - # {'name': '--foobar -fb', 'required': False}, - # {'name': '--foobar2 -fb2', 'required': True} - # ] - # } - # } - # config = Configuration([]) - #config.get_command_table = lambda: cmd_table - #app = Application(config) - - # with self.assertRaises(SystemExit): - # app.execute('n1 -fb a --foobar3 bad'.split()) - - # with open(r'C:\temp\value.txt', 'w') as f: - # f.write(io.getvalue()) - - # self.assertTrue('required' in io.getvalue() - # and '--foobar/-fb' not in io.getvalue() - # and '--foobar2/-fb' in io.getvalue() - # and 'unrecognized arguments: --foobar3' in io.getvalue()) + @redirect_io + def test_help_extra_missing_params(self): + app = Application(Configuration([])) + def test_handler(args): + pass + + cmd_table = { + test_handler: { + 'name': 'n1', + 'arguments': [ + {'name': '--foobar -fb', 'required': False}, + {'name': '--foobar2 -fb2', 'required': True} + ] + } + } + config = Configuration([]) + config.get_command_table = lambda: cmd_table + app = Application(config) + + # there is an argparse bug on <2.7.10 where SystemExit is not thrown on missing required param + if sys.version_info < (2, 7, 10): + app.execute('n1 -fb a --foobar value'.split()) + app.execute('n1 -fb a --foobar2 value --foobar3 extra'.split()) + else: + with self.assertRaises(SystemExit): + app.execute('n1 -fb a --foobar value'.split()) + with self.assertRaises(SystemExit): + app.execute('n1 -fb a --foobar2 value --foobar3 extra'.split()) + + self.assertTrue('required' in io.getvalue() + and '--foobar/-fb' not in io.getvalue() + and '--foobar2/-fb2' in io.getvalue() + and 'unrecognized arguments: --foobar3 extra' in io.getvalue()) @redirect_io def test_help_group_help(self): @@ -520,19 +527,74 @@ def test_handler(args): app.execute('test_group1 test_group2 --help'.split()) s = ''' Group - test_group1 test_group2: this module does xyz one-line or so + az test_group1 test_group2: this module does xyz one-line or so this module.... kjsdflkj... klsfkj paragraph1 this module.... kjsdflkj... klsfkj paragraph2 Sub-Commands n1: this module does xyz one-line or so + Examples foo example example details ''' self.assertEqual(s, io.getvalue()) + @redirect_io + @mock.patch('azure.cli.application.Application.register', return_value=None) + @mock.patch('azure.cli.extensions.register_extensions', return_value=None) + def test_help_global_params(self, mock_register_extensions, _): + def register_globals(global_group): + global_group.add_argument('--query2', dest='_jmespath_query', metavar='JMESPATH', + help='JMESPath query string. See http://jmespath.org/ ' + 'for more information and examples.') + + mock_register_extensions.return_value = None + mock_register_extensions.side_effect = lambda app: \ + app._event_handlers[app.GLOBAL_PARSER_CREATED].append(register_globals) + + def test_handler(args): + pass + + cmd_table = { + test_handler: { + 'name': 'n1', + 'help_file': ''' + long-summary: | + line1 + line2 + ''', + 'arguments': [ + {'name': '--arg -a', 'required': False}, + {'name': '-b', 'required': False} + ] + } + } + config = Configuration([]) + config.get_command_table = lambda: cmd_table + app = Application(config) + + with self.assertRaises(SystemExit): + app.execute('n1 -h'.split()) + + s = """ +Command + az n1 + line1 + line2 + +Arguments + --arg -a + -b + +Global Arguments + --help -h: show this help message and exit + --query2 : JMESPath query string. See http://jmespath.org/ for more information and examples. +""" + + self.assertEqual(s, io.getvalue()) + if __name__ == '__main__': unittest.main()