# coding=utf-8 # flake8: noqa E302 """ Unit/functional testing for argparse completer in cmd2 """ import argparse import numbers from typing import ( List, ) import pytest import cmd2 from cmd2 import ( Cmd2ArgumentParser, CompletionError, CompletionItem, with_argparser, ) from cmd2.utils import ( StdSim, ) from .conftest import ( complete_tester, normalize, run_cmd, ) # Data and functions for testing standalone choice_provider and completer standalone_choices = ['standalone', 'provider'] standalone_completions = ['standalone', 'completer'] # noinspection PyUnusedLocal def standalone_choice_provider(cli: cmd2.Cmd) -> List[str]: return standalone_choices def standalone_completer(cli: cmd2.Cmd, text: str, line: str, begidx: int, endidx: int) -> List[str]: return cli.basic_complete(text, line, begidx, endidx, standalone_completions) # noinspection PyMethodMayBeStatic,PyUnusedLocal,PyProtectedMember class ArgparseCompleterTester(cmd2.Cmd): """Cmd2 app that exercises ArgparseCompleter class""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) ############################################################################################################ # Begin code related to help and command name completion ############################################################################################################ # Top level parser for music command music_parser = Cmd2ArgumentParser(description='Manage music') # Add subcommands to music music_subparsers = music_parser.add_subparsers() music_create_parser = music_subparsers.add_parser('create', help='create music') # Add subcommands to music -> create music_create_subparsers = music_create_parser.add_subparsers() music_create_jazz_parser = music_create_subparsers.add_parser('jazz', help='create jazz') music_create_rock_parser = music_create_subparsers.add_parser('rock', help='create rocks') @with_argparser(music_parser) def do_music(self, args: argparse.Namespace) -> None: pass ############################################################################################################ # Begin code related to flag completion ############################################################################################################ # Uses default flag prefix value (-) flag_parser = Cmd2ArgumentParser() flag_parser.add_argument('-n', '--normal_flag', help='a normal flag', action='store_true') flag_parser.add_argument('-a', '--append_flag', help='append flag', action='append') flag_parser.add_argument('-o', '--append_const_flag', help='append const flag', action='append_const', const=True) flag_parser.add_argument('-c', '--count_flag', help='count flag', action='count') flag_parser.add_argument('-s', '--suppressed_flag', help=argparse.SUPPRESS, action='store_true') flag_parser.add_argument('-r', '--remainder_flag', nargs=argparse.REMAINDER, help='a remainder flag') flag_parser.add_argument('-q', '--required_flag', required=True, help='a required flag', action='store_true') @with_argparser(flag_parser) def do_flag(self, args: argparse.Namespace) -> None: pass # Uses non-default flag prefix value (+) plus_flag_parser = Cmd2ArgumentParser(prefix_chars='+') plus_flag_parser.add_argument('+n', '++normal_flag', help='a normal flag', action='store_true') plus_flag_parser.add_argument('+q', '++required_flag', required=True, help='a required flag', action='store_true') @with_argparser(plus_flag_parser) def do_plus_flag(self, args: argparse.Namespace) -> None: pass ############################################################################################################ # Begin code related to testing choices and choices_provider parameters ############################################################################################################ STR_METAVAR = "HEADLESS" TUPLE_METAVAR = ('arg1', 'others') CUSTOM_DESC_HEADER = "Custom Header" # Lists used in our tests (there is a mix of sorted and unsorted on purpose) non_negative_int_choices = [1, 2, 3, 0, 22] int_choices = [-1, 1, -2, 2, 0, -12] static_choices_list = ['static', 'choices', 'stop', 'here'] choices_from_provider = ['choices', 'provider', 'probably', 'improved'] completion_item_choices = [CompletionItem('choice_1', 'A description'), CompletionItem('choice_2', 'Another description')] def choices_provider(self) -> List[str]: """Method that provides choices""" return self.choices_from_provider def completion_item_method(self) -> List[CompletionItem]: """Choices method that returns CompletionItems""" items = [] for i in range(0, 10): main_str = 'main_str{}'.format(i) items.append(CompletionItem(main_str, desc='blah blah')) return items choices_parser = Cmd2ArgumentParser() # Flag args for choices command. Include string and non-string arg types. choices_parser.add_argument("-l", "--list", help="a flag populated with a choices list", choices=static_choices_list) choices_parser.add_argument( "-p", "--provider", help="a flag populated with a choices provider", choices_provider=choices_provider ) choices_parser.add_argument( '-d', "--desc_header", help='this arg has a descriptive header', choices_provider=completion_item_method, descriptive_header=CUSTOM_DESC_HEADER, ) choices_parser.add_argument( '-n', "--no_header", help='this arg has no descriptive header', choices_provider=completion_item_method, metavar=STR_METAVAR, ) choices_parser.add_argument( '-t', "--tuple_metavar", help='this arg has tuple for a metavar', choices_provider=completion_item_method, metavar=TUPLE_METAVAR, nargs=argparse.ONE_OR_MORE, ) choices_parser.add_argument('-i', '--int', type=int, help='a flag with an int type', choices=int_choices) choices_parser.add_argument('--completion_items', help='choices are CompletionItems', choices=completion_item_choices) # Positional args for choices command choices_parser.add_argument("list_pos", help="a positional populated with a choices list", choices=static_choices_list) choices_parser.add_argument( "method_pos", help="a positional populated with a choices provider", choices_provider=choices_provider ) choices_parser.add_argument( 'non_negative_int', type=int, help='a positional with non-negative int choices', choices=non_negative_int_choices ) choices_parser.add_argument('empty_choices', help='a positional with empty choices', choices=[]) @with_argparser(choices_parser) def do_choices(self, args: argparse.Namespace) -> None: pass ############################################################################################################ # Begin code related to testing completer parameter ############################################################################################################ completions_for_flag = ['completions', 'flag', 'fairly', 'complete'] completions_for_pos_1 = ['completions', 'positional_1', 'probably', 'missed', 'spot'] completions_for_pos_2 = ['completions', 'positional_2', 'probably', 'missed', 'me'] def flag_completer(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: return self.basic_complete(text, line, begidx, endidx, self.completions_for_flag) def pos_1_completer(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: return self.basic_complete(text, line, begidx, endidx, self.completions_for_pos_1) def pos_2_completer(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: return self.basic_complete(text, line, begidx, endidx, self.completions_for_pos_2) completer_parser = Cmd2ArgumentParser() # Flag args for completer command completer_parser.add_argument("-c", "--completer", help="a flag using a completer", completer=flag_completer) # Positional args for completer command completer_parser.add_argument("pos_1", help="a positional using a completer method", completer=pos_1_completer) completer_parser.add_argument("pos_2", help="a positional using a completer method", completer=pos_2_completer) @with_argparser(completer_parser) def do_completer(self, args: argparse.Namespace) -> None: pass ############################################################################################################ # Begin code related to nargs ############################################################################################################ set_value_choices = ['set', 'value', 'choices'] one_or_more_choices = ['one', 'or', 'more', 'choices'] optional_choices = ['a', 'few', 'optional', 'choices'] range_choices = ['some', 'range', 'choices'] remainder_choices = ['remainder', 'choices'] positional_choices = ['the', 'positional', 'choices'] nargs_parser = Cmd2ArgumentParser() # Flag args for nargs command nargs_parser.add_argument("--set_value", help="a flag with a set value for nargs", nargs=2, choices=set_value_choices) nargs_parser.add_argument( "--one_or_more", help="a flag wanting one or more args", nargs=argparse.ONE_OR_MORE, choices=one_or_more_choices ) nargs_parser.add_argument( "--optional", help="a flag with an optional value", nargs=argparse.OPTIONAL, choices=optional_choices ) # noinspection PyTypeChecker nargs_parser.add_argument("--range", help="a flag with nargs range", nargs=(1, 2), choices=range_choices) nargs_parser.add_argument( "--remainder", help="a flag wanting remaining", nargs=argparse.REMAINDER, choices=remainder_choices ) nargs_parser.add_argument("normal_pos", help="a remainder positional", nargs=2, choices=positional_choices) nargs_parser.add_argument( "remainder_pos", help="a remainder positional", nargs=argparse.REMAINDER, choices=remainder_choices ) @with_argparser(nargs_parser) def do_nargs(self, args: argparse.Namespace) -> None: pass ############################################################################################################ # Begin code related to testing tab hints ############################################################################################################ hint_parser = Cmd2ArgumentParser() hint_parser.add_argument('-f', '--flag', help='a flag arg') hint_parser.add_argument('-s', '--suppressed_help', help=argparse.SUPPRESS) hint_parser.add_argument('-t', '--suppressed_hint', help='a flag arg', suppress_tab_hint=True) hint_parser.add_argument('hint_pos', help='here is a hint\nwith new lines') hint_parser.add_argument('no_help_pos') @with_argparser(hint_parser) def do_hint(self, args: argparse.Namespace) -> None: pass ############################################################################################################ # Begin code related to CompletionError ############################################################################################################ def completer_raise_error(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: """Raises CompletionError""" raise CompletionError('completer broke something') def choice_raise_error(self) -> List[str]: """Raises CompletionError""" raise CompletionError('choice broke something') comp_error_parser = Cmd2ArgumentParser() comp_error_parser.add_argument('completer_pos', help='positional arg', completer=completer_raise_error) comp_error_parser.add_argument('--choice', help='flag arg', choices_provider=choice_raise_error) @with_argparser(comp_error_parser) def do_raise_completion_error(self, args: argparse.Namespace) -> None: pass ############################################################################################################ # Begin code related to receiving arg_tokens ############################################################################################################ def choices_takes_arg_tokens(self, arg_tokens: argparse.Namespace) -> List[str]: """Choices function that receives arg_tokens from ArgparseCompleter""" return [arg_tokens['parent_arg'][0], arg_tokens['subcommand'][0]] def completer_takes_arg_tokens( self, text: str, line: str, begidx: int, endidx: int, arg_tokens: argparse.Namespace ) -> List[str]: """Completer function that receives arg_tokens from ArgparseCompleter""" match_against = [arg_tokens['parent_arg'][0], arg_tokens['subcommand'][0]] return self.basic_complete(text, line, begidx, endidx, match_against) arg_tokens_parser = Cmd2ArgumentParser() arg_tokens_parser.add_argument('parent_arg', help='arg from a parent parser') # Create a subcommand for to exercise receiving parent_tokens and subcommand name in arg_tokens arg_tokens_subparser = arg_tokens_parser.add_subparsers(dest='subcommand') arg_tokens_subcmd_parser = arg_tokens_subparser.add_parser('subcmd') arg_tokens_subcmd_parser.add_argument('choices_pos', choices_provider=choices_takes_arg_tokens) arg_tokens_subcmd_parser.add_argument('completer_pos', completer=completer_takes_arg_tokens) # Used to override parent_arg in arg_tokens_parser arg_tokens_subcmd_parser.add_argument('--parent_arg') @with_argparser(arg_tokens_parser) def do_arg_tokens(self, args: argparse.Namespace) -> None: pass ############################################################################################################ # Begin code related to mutually exclusive groups ############################################################################################################ mutex_parser = Cmd2ArgumentParser() mutex_group = mutex_parser.add_mutually_exclusive_group(required=True) mutex_group.add_argument('optional_pos', help='the optional positional', nargs=argparse.OPTIONAL) mutex_group.add_argument('-f', '--flag', help='the flag arg') mutex_group.add_argument('-o', '--other_flag', help='the other flag arg') mutex_parser.add_argument('last_arg', help='the last arg') @with_argparser(mutex_parser) def do_mutex(self, args: argparse.Namespace) -> None: pass ############################################################################################################ # Begin code related to standalone functions ############################################################################################################ standalone_parser = Cmd2ArgumentParser() standalone_parser.add_argument('--provider', help='standalone provider', choices_provider=standalone_choice_provider) standalone_parser.add_argument('--completer', help='standalone completer', completer=standalone_completer) @with_argparser(standalone_parser) def do_standalone(self, args: argparse.Namespace) -> None: pass @pytest.fixture def ac_app(): app = ArgparseCompleterTester() app.stdout = StdSim(app.stdout) return app @pytest.mark.parametrize('command', ['music', 'music create', 'music create rock', 'music create jazz']) def test_help(ac_app, command): out1, err1 = run_cmd(ac_app, '{} -h'.format(command)) out2, err2 = run_cmd(ac_app, 'help {}'.format(command)) assert out1 == out2 def test_bad_subcommand_help(ac_app): # These should give the same output because the second one isn't using a # real subcommand, so help will be called on the music command instead. out1, err1 = run_cmd(ac_app, 'help music') out2, err2 = run_cmd(ac_app, 'help music fake') assert out1 == out2 @pytest.mark.parametrize( 'command, text, completions', [ ('', 'mus', ['music ']), ('music', 'cre', ['create ']), ('music', 'creab', []), ('music create', '', ['jazz', 'rock']), ('music crea', 'jazz', []), ('music create', 'foo', []), ('fake create', '', []), ('music fake', '', []), ], ) def test_complete_help(ac_app, command, text, completions): line = 'help {} {}'.format(command, text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, ac_app) if completions: assert first_match is not None else: assert first_match is None assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key) @pytest.mark.parametrize( 'subcommand, text, completions', [('create', '', ['jazz', 'rock']), ('create', 'ja', ['jazz ']), ('create', 'foo', []), ('creab', 'ja', [])], ) def test_subcommand_completions(ac_app, subcommand, text, completions): line = 'music {} {}'.format(subcommand, text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, ac_app) if completions: assert first_match is not None else: assert first_match is None assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key) @pytest.mark.parametrize( 'command_and_args, text, completion_matches, display_matches', [ # Complete all flags (suppressed will not show) ( 'flag', '-', [ '--append_const_flag', '--append_flag', '--count_flag', '--help', '--normal_flag', '--remainder_flag', '--required_flag', '-a', '-c', '-h', '-n', '-o', '-q', '-r', ], [ '-q, --required_flag', '[-o, --append_const_flag]', '[-a, --append_flag]', '[-c, --count_flag]', '[-h, --help]', '[-n, --normal_flag]', '[-r, --remainder_flag]', ], ), ( 'flag', '--', [ '--append_const_flag', '--append_flag', '--count_flag', '--help', '--normal_flag', '--remainder_flag', '--required_flag', ], [ '--required_flag', '[--append_const_flag]', '[--append_flag]', '[--count_flag]', '[--help]', '[--normal_flag]', '[--remainder_flag]', ], ), # Complete individual flag ('flag', '-n', ['-n '], ['[-n]']), ('flag', '--n', ['--normal_flag '], ['[--normal_flag]']), # No flags should complete until current flag has its args ('flag --append_flag', '-', [], []), # Complete REMAINDER flag name ('flag', '-r', ['-r '], ['[-r]']), ('flag', '--rem', ['--remainder_flag '], ['[--remainder_flag]']), # No flags after a REMAINDER should complete ('flag -r value', '-', [], []), ('flag --remainder_flag value', '--', [], []), # Suppressed flag should not complete ('flag', '-s', [], []), ('flag', '--s', [], []), # A used flag should not show in completions ( 'flag -n', '--', ['--append_const_flag', '--append_flag', '--count_flag', '--help', '--remainder_flag', '--required_flag'], [ '--required_flag', '[--append_const_flag]', '[--append_flag]', '[--count_flag]', '[--help]', '[--remainder_flag]', ], ), # Flags with actions set to append, append_const, and count will always show even if they've been used ( 'flag --append_const_flag -c --append_flag value', '--', [ '--append_const_flag', '--append_flag', '--count_flag', '--help', '--normal_flag', '--remainder_flag', '--required_flag', ], [ '--required_flag', '[--append_const_flag]', '[--append_flag]', '[--count_flag]', '[--help]', '[--normal_flag]', '[--remainder_flag]', ], ), # Non-default flag prefix character (+) ( 'plus_flag', '+', ['++help', '++normal_flag', '+h', '+n', '+q', '++required_flag'], ['+q, ++required_flag', '[+h, ++help]', '[+n, ++normal_flag]'], ), ( 'plus_flag', '++', ['++help', '++normal_flag', '++required_flag'], ['++required_flag', '[++help]', '[++normal_flag]'], ), # Flag completion should not occur after '--' since that tells argparse all remaining arguments are non-flags ('flag --', '--', [], []), ('flag --help --', '--', [], []), ('plus_flag --', '++', [], []), ('plus_flag ++help --', '++', [], []), ], ) def test_autcomp_flag_completion(ac_app, command_and_args, text, completion_matches, display_matches): line = '{} {}'.format(command_and_args, text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, ac_app) if completion_matches: assert first_match is not None else: assert first_match is None assert ac_app.completion_matches == sorted( completion_matches, key=ac_app.default_sort_key ) and ac_app.display_matches == sorted(display_matches, key=ac_app.default_sort_key) @pytest.mark.parametrize( 'flag, text, completions', [ ('-l', '', ArgparseCompleterTester.static_choices_list), ('--list', 's', ['static', 'stop']), ('-p', '', ArgparseCompleterTester.choices_from_provider), ('--provider', 'pr', ['provider', 'probably']), ('-i', '', ArgparseCompleterTester.int_choices), ('--int', '1', ['1 ']), ('--int', '-', [-1, -2, -12]), ('--int', '-1', [-1, -12]), ], ) def test_autocomp_flag_choices_completion(ac_app, flag, text, completions): line = 'choices {} {}'.format(flag, text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, ac_app) if completions: assert first_match is not None else: assert first_match is None # Numbers will be sorted in ascending order and then converted to strings by ArgparseCompleter if completions and all(isinstance(x, numbers.Number) for x in completions): completions.sort() completions = [str(x) for x in completions] else: completions.sort(key=ac_app.default_sort_key) assert ac_app.completion_matches == completions @pytest.mark.parametrize( 'pos, text, completions', [ (1, '', ArgparseCompleterTester.static_choices_list), (1, 's', ['static', 'stop']), (2, '', ArgparseCompleterTester.choices_from_provider), (2, 'pr', ['provider', 'probably']), (3, '', ArgparseCompleterTester.non_negative_int_choices), (3, '2', [2, 22]), (4, '', []), ], ) def test_autocomp_positional_choices_completion(ac_app, pos, text, completions): # Generate line were preceding positionals are already filled line = 'choices {} {}'.format('foo ' * (pos - 1), text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, ac_app) if completions: assert first_match is not None else: assert first_match is None # Numbers will be sorted in ascending order and then converted to strings by ArgparseCompleter if completions and all(isinstance(x, numbers.Number) for x in completions): completions.sort() completions = [str(x) for x in completions] else: completions.sort(key=ac_app.default_sort_key) assert ac_app.completion_matches == completions def test_flag_sorting(ac_app): # This test exercises the case where a positional arg has non-negative integers for its choices. # ArgparseCompleter will sort these numerically before converting them to strings. As a result, # cmd2.matches_sorted gets set to True. If no completion matches are returned and the entered # text looks like the beginning of a flag (e.g -), then ArgparseCompleter will try to complete # flag names next. Before it does this, cmd2.matches_sorted is reset to make sure the flag names # get sorted correctly. option_strings = [] for action in ac_app.choices_parser._actions: option_strings.extend(action.option_strings) option_strings.sort(key=ac_app.default_sort_key) text = '-' line = 'choices arg1 arg2 arg3 {}'.format(text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, ac_app) assert first_match is not None and ac_app.completion_matches == option_strings @pytest.mark.parametrize( 'flag, text, completions', [('-c', '', ArgparseCompleterTester.completions_for_flag), ('--completer', 'f', ['flag', 'fairly'])], ) def test_autocomp_flag_completers(ac_app, flag, text, completions): line = 'completer {} {}'.format(flag, text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, ac_app) if completions: assert first_match is not None else: assert first_match is None assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key) @pytest.mark.parametrize( 'pos, text, completions', [ (1, '', ArgparseCompleterTester.completions_for_pos_1), (1, 'p', ['positional_1', 'probably']), (2, '', ArgparseCompleterTester.completions_for_pos_2), (2, 'm', ['missed', 'me']), ], ) def test_autocomp_positional_completers(ac_app, pos, text, completions): # Generate line were preceding positionals are already filled line = 'completer {} {}'.format('foo ' * (pos - 1), text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, ac_app) if completions: assert first_match is not None else: assert first_match is None assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key) def test_autocomp_blank_token(ac_app): """Force a blank token to make sure ArgparseCompleter consumes them like argparse does""" from cmd2.argparse_completer import ( ArgparseCompleter, ) blank = '' # Blank flag arg will be consumed. Therefore we expect to be completing the first positional. text = '' line = 'completer -c {} {}'.format(blank, text) endidx = len(line) begidx = endidx - len(text) completer = ArgparseCompleter(ac_app.completer_parser, ac_app) tokens = ['-c', blank, text] completions = completer.complete(text, line, begidx, endidx, tokens) assert sorted(completions) == sorted(ArgparseCompleterTester.completions_for_pos_1) # Blank arg for first positional will be consumed. Therefore we expect to be completing the second positional. text = '' line = 'completer {} {}'.format(blank, text) endidx = len(line) begidx = endidx - len(text) completer = ArgparseCompleter(ac_app.completer_parser, ac_app) tokens = [blank, text] completions = completer.complete(text, line, begidx, endidx, tokens) assert sorted(completions) == sorted(ArgparseCompleterTester.completions_for_pos_2) @pytest.mark.parametrize( 'num_aliases, show_description', [ # The number of completion results determines if the description field of CompletionItems gets displayed # in the tab completions. The count must be greater than 1 and less than ac_app.max_completion_items, # which defaults to 50. (1, False), (5, True), (100, False), ], ) def test_completion_items(ac_app, num_aliases, show_description): # Create aliases for i in range(0, num_aliases): run_cmd(ac_app, 'alias create fake_alias{} help'.format(i)) assert len(ac_app.aliases) == num_aliases text = 'fake_alias' line = 'alias list {}'.format(text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, ac_app) assert first_match is not None assert len(ac_app.completion_matches) == num_aliases assert len(ac_app.display_matches) == num_aliases assert bool(ac_app.formatted_completions) == show_description if show_description: # If show_description is True, the table will show both the alias name and result first_result_line = normalize(ac_app.formatted_completions)[1] assert 'fake_alias0' in first_result_line assert 'help' in first_result_line def test_completion_item_choices(ac_app): text = '' line = 'choices --completion_items {}'.format(text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, ac_app) assert first_match is not None assert len(ac_app.completion_matches) == len(ac_app.completion_item_choices) assert len(ac_app.display_matches) == len(ac_app.completion_item_choices) # Make sure a completion table was created first_result_line = normalize(ac_app.formatted_completions)[1] assert 'choice_1' in first_result_line assert 'A description' in first_result_line @pytest.mark.parametrize( 'args, completions', [ # Flag with nargs = 2 ('--set_value', ArgparseCompleterTester.set_value_choices), ('--set_value set', ['value', 'choices']), # Both args are filled. At positional arg now. ('--set_value set value', ArgparseCompleterTester.positional_choices), # Using the flag again will reset the choices available ('--set_value set value --set_value', ArgparseCompleterTester.set_value_choices), # Flag with nargs = ONE_OR_MORE ('--one_or_more', ArgparseCompleterTester.one_or_more_choices), ('--one_or_more one', ['or', 'more', 'choices']), # Flag with nargs = OPTIONAL ('--optional', ArgparseCompleterTester.optional_choices), # Only one arg allowed for an OPTIONAL. At positional now. ('--optional optional', ArgparseCompleterTester.positional_choices), # Flag with nargs range (1, 2) ('--range', ArgparseCompleterTester.range_choices), ('--range some', ['range', 'choices']), # Already used 2 args so at positional ('--range some range', ArgparseCompleterTester.positional_choices), # Flag with nargs = REMAINDER ('--remainder', ArgparseCompleterTester.remainder_choices), ('--remainder remainder ', ['choices ']), # No more flags can appear after a REMAINDER flag) ('--remainder choices --set_value', ['remainder ']), # Double dash ends the current flag ('--range choice --', ArgparseCompleterTester.positional_choices), # Double dash ends a REMAINDER flag ('--remainder remainder --', ArgparseCompleterTester.positional_choices), # No more flags after a double dash ('-- --one_or_more ', ArgparseCompleterTester.positional_choices), # Consume positional ('', ArgparseCompleterTester.positional_choices), ('positional', ['the', 'choices']), # Intermixed flag and positional ('positional --set_value', ArgparseCompleterTester.set_value_choices), ('positional --set_value set', ['choices', 'value']), # Intermixed flag and positional with flag finishing ('positional --set_value set value', ['the', 'choices']), ('positional --range choice --', ['the', 'choices']), # REMAINDER positional ('the positional', ArgparseCompleterTester.remainder_choices), ('the positional remainder', ['choices ']), ('the positional remainder choices', []), # REMAINDER positional. Flags don't work in REMAINDER ('the positional --set_value', ArgparseCompleterTester.remainder_choices), ('the positional remainder --set_value', ['choices ']), ], ) def test_autcomp_nargs(ac_app, args, completions): text = '' line = 'nargs {} {}'.format(args, text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, ac_app) if completions: assert first_match is not None else: assert first_match is None assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key) @pytest.mark.parametrize( 'command_and_args, text, is_error', [ # Flag is finished before moving on ('hint --flag foo --', '', False), ('hint --flag foo --help', '', False), ('hint --flag foo', '--', False), ('nargs --one_or_more one --', '', False), ('nargs --one_or_more one or --set_value', '', False), ('nargs --one_or_more one or more', '--', False), ('nargs --set_value set value --', '', False), ('nargs --set_value set value --one_or_more', '', False), ('nargs --set_value set value', '--', False), ('nargs --set_val set value', '--', False), # This exercises our abbreviated flag detection ('nargs --range choices --', '', False), ('nargs --range choices range --set_value', '', False), ('nargs --range range', '--', False), # Flag is not finished before moving on ('hint --flag --', '', True), ('hint --flag --help', '', True), ('hint --flag', '--', True), ('nargs --one_or_more --', '', True), ('nargs --one_or_more --set_value', '', True), ('nargs --one_or_more', '--', True), ('nargs --set_value set --', '', True), ('nargs --set_value set --one_or_more', '', True), ('nargs --set_value set', '--', True), ('nargs --set_val set', '--', True), # This exercises our abbreviated flag detection ('nargs --range --', '', True), ('nargs --range --set_value', '', True), ('nargs --range', '--', True), ], ) def test_unfinished_flag_error(ac_app, command_and_args, text, is_error, capsys): line = '{} {}'.format(command_and_args, text) endidx = len(line) begidx = endidx - len(text) complete_tester(text, line, begidx, endidx, ac_app) out, err = capsys.readouterr() assert is_error == all(x in out for x in ["Error: argument", "expected"]) def test_completion_items_arg_header(ac_app): # Test when metavar is None text = '' line = 'choices --desc_header {}'.format(text) endidx = len(line) begidx = endidx - len(text) complete_tester(text, line, begidx, endidx, ac_app) assert "DESC_HEADER" in normalize(ac_app.formatted_completions)[0] # Test when metavar is a string text = '' line = 'choices --no_header {}'.format(text) endidx = len(line) begidx = endidx - len(text) complete_tester(text, line, begidx, endidx, ac_app) assert ac_app.STR_METAVAR in normalize(ac_app.formatted_completions)[0] # Test when metavar is a tuple text = '' line = 'choices --tuple_metavar {}'.format(text) endidx = len(line) begidx = endidx - len(text) # We are completing the first argument of this flag. The first element in the tuple should be the column header. complete_tester(text, line, begidx, endidx, ac_app) assert ac_app.TUPLE_METAVAR[0].upper() in normalize(ac_app.formatted_completions)[0] text = '' line = 'choices --tuple_metavar token_1 {}'.format(text) endidx = len(line) begidx = endidx - len(text) # We are completing the second argument of this flag. The second element in the tuple should be the column header. complete_tester(text, line, begidx, endidx, ac_app) assert ac_app.TUPLE_METAVAR[1].upper() in normalize(ac_app.formatted_completions)[0] text = '' line = 'choices --tuple_metavar token_1 token_2 {}'.format(text) endidx = len(line) begidx = endidx - len(text) # We are completing the third argument of this flag. It should still be the second tuple element # in the column header since the tuple only has two strings in it. complete_tester(text, line, begidx, endidx, ac_app) assert ac_app.TUPLE_METAVAR[1].upper() in normalize(ac_app.formatted_completions)[0] def test_completion_items_descriptive_header(ac_app): from cmd2.argparse_completer import ( DEFAULT_DESCRIPTIVE_HEADER, ) # This argument provided a descriptive header text = '' line = 'choices --desc_header {}'.format(text) endidx = len(line) begidx = endidx - len(text) complete_tester(text, line, begidx, endidx, ac_app) assert ac_app.CUSTOM_DESC_HEADER in normalize(ac_app.formatted_completions)[0] # This argument did not provide a descriptive header, so it should be DEFAULT_DESCRIPTIVE_HEADER text = '' line = 'choices --no_header {}'.format(text) endidx = len(line) begidx = endidx - len(text) complete_tester(text, line, begidx, endidx, ac_app) assert DEFAULT_DESCRIPTIVE_HEADER in normalize(ac_app.formatted_completions)[0] @pytest.mark.parametrize( 'command_and_args, text, has_hint', [ # Normal cases ('hint', '', True), ('hint --flag', '', True), ('hint --suppressed_help', '', False), ('hint --suppressed_hint', '', False), # Hint because flag does not have enough values to be considered finished ('nargs --one_or_more', '-', True), # This flag has reached its minimum value count and therefore a new flag could start. # However the flag can still consume values and the text is not a single prefix character. # Therefor a hint will be shown. ('nargs --one_or_more choices', 'bad_completion', True), # Like the previous case, but this time text is a single prefix character which will cause flag # name completion to occur instead of a hint for the current flag. ('nargs --one_or_more choices', '-', False), # Hint because this is a REMAINDER flag and therefore no more flag name completions occur. ('nargs --remainder', '-', True), # No hint for the positional because text is a single prefix character which results in flag name completion ('hint', '-', False), # Hint because this is a REMAINDER positional and therefore no more flag name completions occur. ('nargs the choices', '-', True), ('nargs the choices remainder', '-', True), ], ) def test_autocomp_hint(ac_app, command_and_args, text, has_hint, capsys): line = '{} {}'.format(command_and_args, text) endidx = len(line) begidx = endidx - len(text) complete_tester(text, line, begidx, endidx, ac_app) out, err = capsys.readouterr() if has_hint: assert "Hint:\n" in out else: assert not out def test_autocomp_hint_no_help_text(ac_app, capsys): text = '' line = 'hint foo {}'.format(text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, ac_app) out, err = capsys.readouterr() assert first_match is None assert ( not out == ''' Hint: NO_HELP_POS ''' ) @pytest.mark.parametrize( 'args, text', [ # Exercise a flag arg and choices function that raises a CompletionError ('--choice ', 'choice'), # Exercise a positional arg and completer that raises a CompletionError ('', 'completer'), ], ) def test_completion_error(ac_app, capsys, args, text): line = 'raise_completion_error {} {}'.format(args, text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, ac_app) out, err = capsys.readouterr() assert first_match is None assert "{} broke something".format(text) in out @pytest.mark.parametrize( 'command_and_args, completions', [ # Exercise a choices function that receives arg_tokens dictionary ('arg_tokens choice subcmd', ['choice', 'subcmd']), # Exercise a completer that receives arg_tokens dictionary ('arg_tokens completer subcmd fake', ['completer', 'subcmd']), # Exercise overriding parent_arg from the subcommand ('arg_tokens completer subcmd --parent_arg override fake', ['override', 'subcmd']), ], ) def test_arg_tokens(ac_app, command_and_args, completions): text = '' line = '{} {}'.format(command_and_args, text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, ac_app) if completions: assert first_match is not None else: assert first_match is None assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key) @pytest.mark.parametrize( 'command_and_args, text, output_contains, first_match', [ # Group isn't done. Hint will show for optional positional and no completions returned ('mutex', '', 'the optional positional', None), # Group isn't done. Flag name will still complete. ('mutex', '--fl', '', '--flag '), # Group isn't done. Flag hint will show. ('mutex --flag', '', 'the flag arg', None), # Group finished by optional positional. No flag name will complete. ('mutex pos_val', '--fl', '', None), # Group finished by optional positional. Error will display trying to complete the flag's value. ('mutex pos_val --flag', '', 'f/--flag: not allowed with argument optional_pos', None), # Group finished by --flag. Optional positional will be skipped and last_arg will show its hint. ('mutex --flag flag_val', '', 'the last arg', None), # Group finished by --flag. Other flag name won't complete. ('mutex --flag flag_val', '--oth', '', None), # Group finished by --flag. Error will display trying to complete other flag's value. ('mutex --flag flag_val --other', '', '-o/--other_flag: not allowed with argument -f/--flag', None), # Group finished by --flag. That same flag can be used again so it's hint will show. ('mutex --flag flag_val --flag', '', 'the flag arg', None), ], ) def test_complete_mutex_group(ac_app, command_and_args, text, output_contains, first_match, capsys): line = '{} {}'.format(command_and_args, text) endidx = len(line) begidx = endidx - len(text) assert first_match == complete_tester(text, line, begidx, endidx, ac_app) out, err = capsys.readouterr() assert output_contains in out def test_single_prefix_char(): from cmd2.argparse_completer import ( _single_prefix_char, ) parser = Cmd2ArgumentParser(prefix_chars='-+') # Invalid assert not _single_prefix_char('', parser) assert not _single_prefix_char('--', parser) assert not _single_prefix_char('-+', parser) assert not _single_prefix_char('++has space', parser) assert not _single_prefix_char('foo', parser) # Valid assert _single_prefix_char('-', parser) assert _single_prefix_char('+', parser) def test_looks_like_flag(): from cmd2.argparse_completer import ( _looks_like_flag, ) parser = Cmd2ArgumentParser() # Does not start like a flag assert not _looks_like_flag('', parser) assert not _looks_like_flag('non-flag', parser) assert not _looks_like_flag('-', parser) assert not _looks_like_flag('--has space', parser) assert not _looks_like_flag('-2', parser) # Does start like a flag assert _looks_like_flag('--', parser) assert _looks_like_flag('-flag', parser) assert _looks_like_flag('--flag', parser) def test_complete_command_no_tokens(ac_app): from cmd2.argparse_completer import ( ArgparseCompleter, ) parser = Cmd2ArgumentParser() ac = ArgparseCompleter(parser, ac_app) completions = ac.complete(text='', line='', begidx=0, endidx=0, tokens=[]) assert not completions def test_complete_command_help_no_tokens(ac_app): from cmd2.argparse_completer import ( ArgparseCompleter, ) parser = Cmd2ArgumentParser() ac = ArgparseCompleter(parser, ac_app) completions = ac.complete_subcommand_help(text='', line='', begidx=0, endidx=0, tokens=[]) assert not completions @pytest.mark.parametrize('flag, completions', [('--provider', standalone_choices), ('--completer', standalone_completions)]) def test_complete_standalone(ac_app, flag, completions): text = '' line = 'standalone {} {}'.format(flag, text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, ac_app) assert first_match is not None assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key)