# coding=utf-8 # flake8: noqa E302 """ Unit/functional testing for argparse completer in cmd2 """ import argparse from typing import List import pytest import cmd2 from cmd2 import with_argparser, CompletionItem from cmd2.utils import StdSim, basic_complete from .conftest import run_cmd, complete_tester # Lists used in our tests static_int_choices_list = [-12, -1, -2, 0, 1, 2] static_choices_list = ['static', 'choices', 'stop', 'here'] choices_from_function = ['choices', 'function', 'chatty', 'smith'] choices_from_method = ['choices', 'method', 'most', 'improved'] 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'] completions_from_function = ['completions', 'function', 'fairly', 'complete'] completions_from_method = ['completions', 'method', 'missed', 'spot'] def choices_function() -> List[str]: """Function that provides choices""" return choices_from_function def completer_function(text: str, line: str, begidx: int, endidx: int) -> List[str]: """Tab completion function""" return basic_complete(text, line, begidx, endidx, completions_from_function) # noinspection PyMethodMayBeStatic,PyUnusedLocal class AutoCompleteTester(cmd2.Cmd): """Cmd2 app that exercises AutoCompleter class""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) ############################################################################################################ # Begin code related to help and command name completion ############################################################################################################ def _music_create(self, args: argparse.Namespace) -> None: """Implements the 'music create' command""" self.poutput('music create') def _music_create_jazz(self, args: argparse.Namespace) -> None: """Implements the 'music create jazz' command""" self.poutput('music create jazz') def _music_create_rock(self, args: argparse.Namespace) -> None: """Implements the 'music create rock' command""" self.poutput('music create rock') # Top level parser for music command music_parser = cmd2.ArgParser(description='Manage music', prog='music') # Add sub-commands to music music_subparsers = music_parser.add_subparsers() # music -> create music_create_parser = music_subparsers.add_parser('create', help='Create music') music_create_parser.set_defaults(func=_music_create) # Add sub-commands to music -> create music_create_subparsers = music_create_parser.add_subparsers() # music -> create -> jazz music_create_jazz_parser = music_create_subparsers.add_parser('jazz', help='Create jazz') music_create_jazz_parser.set_defaults(func=_music_create_jazz) # music -> create -> rock music_create_rock_parser = music_create_subparsers.add_parser('rock', help='Create rocks') music_create_rock_parser.set_defaults(func=_music_create_rock) @with_argparser(music_parser) def do_music(self, args: argparse.Namespace) -> None: """Music command""" func = getattr(args, 'func', None) if func is not None: # Call whatever sub-command function was selected func(self, args) else: # No sub-command was provided, so call help # noinspection PyTypeChecker self.do_help('music') ############################################################################################################ # Begin code related to flag completion ############################################################################################################ # Uses default flag prefix value (-) flag_parser = cmd2.ArgParser() 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') @with_argparser(flag_parser) def do_flag(self, args: argparse.Namespace) -> None: pass # Uses non-default flag prefix value (+) plus_flag_parser = cmd2.ArgParser(prefix_chars='+') plus_flag_parser.add_argument('+n', '++normal_flag', help='A normal 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, choices_function, and choices_method parameters ############################################################################################################ def choices_method(self) -> List[str]: """Method that provides choices""" return choices_from_method 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 = cmd2.ArgParser() # 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("-f", "--function", help="a flag populated with a choices function", choices_function=choices_function) choices_parser.add_argument("-m", "--method", help="a flag populated with a choices method", choices_method=choices_method) choices_parser.add_argument('-n', "--no_header", help='this arg has a no descriptive header', choices_method=completion_item_method) choices_parser.add_argument('-i', '--int', type=int, help='a flag with an int type', choices=static_int_choices_list) # 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("function_pos", help="a positional populated with a choices function", choices_function=choices_function) choices_parser.add_argument("method_pos", help="a positional populated with a choices method", choices_method=choices_method) @with_argparser(choices_parser) def do_choices(self, args: argparse.Namespace) -> None: pass ############################################################################################################ # Begin code related to testing completer_function and completer_method parameters ############################################################################################################ def completer_method(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: """Tab completion method""" return basic_complete(text, line, begidx, endidx, completions_from_method) completer_parser = cmd2.ArgParser() # Flag args for completer command completer_parser.add_argument("-f", "--function", help="a flag using a completer function", completer_function=completer_function) completer_parser.add_argument("-m", "--method", help="a flag using a completer method", completer_method=completer_method) # Positional args for completer command completer_parser.add_argument("function_pos", help="a positional using a completer function", completer_function=completer_function) completer_parser.add_argument("method_pos", help="a positional using a completer method", completer_method=completer_method) @with_argparser(completer_parser) def do_completer(self, args: argparse.Namespace) -> None: pass ############################################################################################################ # Begin code related to nargs ############################################################################################################ nargs_parser = cmd2.ArgParser() # 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) 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 = cmd2.ArgParser() 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 @pytest.fixture def ac_app(): app = AutoCompleteTester() 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 @pytest.mark.parametrize('command, text, completions', [ ('', 'mu', ['music ']), ('music', 'cre', ['create ']), ('music create', '', ['jazz', 'rock']) ]) 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.matches_sort_key) @pytest.mark.parametrize('command_and_args, text, completions', [ # Complete all flags (suppressed will not show) ('flag', '-', ['--append_const_flag', '--append_flag', '--count_flag', '--help', '--normal_flag', '--remainder_flag', '-a', '-c', '-h', '-n', '-o', '-r']), ('flag', '--', ['--append_const_flag', '--append_flag', '--count_flag', '--help', '--normal_flag', '--remainder_flag']), # Complete individual flag ('flag', '-n', ['-n ']), ('flag', '--n', ['--normal_flag ']), # No flags should complete until current flag has its args ('flag --append_flag', '-', []), # Complete REMAINDER flag name ('flag', '-r', ['-r ']), ('flag', '--r', ['--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']), # 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']), # Non-default flag prefix character (+) ('plus_flag', '+', ['++help', '++normal_flag', '+h', '+n']), ('plus_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, completions): 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.matches_sort_key) @pytest.mark.parametrize('flag, text, completions', [ ('-l', '', static_choices_list), ('--list', 's', ['static', 'stop']), ('-f', '', choices_from_function), ('--function', 'ch', ['choices', 'chatty']), ('-m', '', choices_from_method), ('--method', 'm', ['method', 'most']), ('-i', '', [str(i) for i in static_int_choices_list]), ('--int', '1', ['1 ']), ('--int', '-', ['-12', '-1', '-2']), ('--int', '-1', ['-12', '-1']) ]) 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 assert ac_app.completion_matches == sorted(completions, key=ac_app.matches_sort_key) @pytest.mark.parametrize('pos, text, completions', [ (1, '', static_choices_list), (1, 's', ['static', 'stop']), (2, '', choices_from_function), (2, 'ch', ['choices', 'chatty']), (3, '', choices_from_method), (3, 'm', ['method', 'most']) ]) 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 assert ac_app.completion_matches == sorted(completions, key=ac_app.matches_sort_key) @pytest.mark.parametrize('flag, text, completions', [ ('-f', '', completions_from_function), ('--function', 'f', ['function', 'fairly']), ('-m', '', completions_from_method), ('--method', 'm', ['method', 'missed']) ]) 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.matches_sort_key) @pytest.mark.parametrize('pos, text, completions', [ (1, '', completions_from_function), (1, 'c', ['completions', 'complete']), (2, '', completions_from_method), (2, 'm', ['method', 'missed']) ]) 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.matches_sort_key) def test_autocomp_blank_token(ac_app): """Force a blank token to make sure AutoCompleter consumes them like argparse does""" from cmd2.argparse_completer import AutoCompleter blank = '' # Blank flag arg text = '' line = 'completer -m {} {}'.format(blank, text) endidx = len(line) begidx = endidx - len(text) completer = AutoCompleter(ac_app.completer_parser, ac_app) tokens = ['completer', '-f', blank, text] completions = completer.complete_command(tokens, text, line, begidx, endidx) assert completions == completions_from_function # Blank positional arg text = '' line = 'completer {} {}'.format(blank, text) endidx = len(line) begidx = endidx - len(text) completer = AutoCompleter(ac_app.completer_parser, ac_app) tokens = ['completer', blank, text] completions = completer.complete_command(tokens, text, line, begidx, endidx) assert completions == completions_from_method @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{} help'.format(i)) assert len(ac_app.aliases) == num_aliases text = 'fake' 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 # If show_description is True, the alias's value will be in the display text assert ('help' in ac_app.display_matches[0]) == show_description @pytest.mark.parametrize('args, completions', [ # Flag with nargs = 2 ('--set_value', set_value_choices), ('--set_value set', ['value', 'choices']), # Both args are filled. At positional arg now. ('--set_value set value', positional_choices), # Using the flag again will reset the choices available ('--set_value set value --set_value', set_value_choices), # Flag with nargs = ONE_OR_MORE ('--one_or_more', one_or_more_choices), ('--one_or_more one', ['or', 'more', 'choices']), # Flag with nargs = OPTIONAL ('--optional', optional_choices), # Only one arg allowed for an OPTIONAL. At positional now. ('--optional optional', positional_choices), # Flag with nargs range (1, 2) ('--range', range_choices), ('--range some', ['range', 'choices']), # Already used 2 args so at positional ('--range some range', positional_choices), # Flag with nargs = REMAINDER ('--remainder', 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 --', positional_choices), # Double dash ends a REMAINDER flag ('--remainder remainder --', positional_choices), # No more flags after a double dash ('-- --one_or_more ', positional_choices), # Consume positional ('', positional_choices), ('positional', ['the', 'choices']), # Intermixed flag and positional ('positional --set_value', 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', remainder_choices), ('the positional remainder', ['choices ']), ('the positional remainder choices', []), # REMAINDER positional. Flags don't work in REMAINDER ('the positional --set_value', remainder_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.matches_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() if is_error: assert "Flag requires" in out else: assert "Flag requires" not in out def test_completion_items_default_header(ac_app): from cmd2.argparse_completer import DEFAULT_DESCRIPTIVE_HEADER text = '' line = 'choices -n {}'.format(text) endidx = len(line) begidx = endidx - len(text) # This positional argument did not provide a descriptive header, so it should be DEFAULT_DESCRIPTIVE_HEADER complete_tester(text, line, begidx, endidx, ac_app) assert DEFAULT_DESCRIPTIVE_HEADER in ac_app.completion_header def test_autocomp_hint_flag(ac_app, capsys): text = '' line = 'hint --flag {}'.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 out == ''' Hint: -f, --flag FLAG a flag arg ''' def test_autocomp_hint_suppressed_help(ac_app, capsys): text = '' line = 'hint --suppressed_help {}'.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 def test_autocomp_hint_suppressed_hint(ac_app, capsys): text = '' line = 'hint --suppressed_hint {}'.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 def test_autocomp_hint_pos(ac_app, capsys): text = '' line = 'hint {}'.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 out == ''' Hint: HINT_POS here is a hint with new lines ''' def test_autocomp_hint_no_help(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 ''' def test_single_prefix_char(): from cmd2.argparse_completer import single_prefix_char parser = cmd2.ArgParser(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_starts_like_flag(): from cmd2.argparse_completer import starts_like_flag parser = cmd2.ArgParser() # Does not start like a flag assert not starts_like_flag('', parser) assert not starts_like_flag('non-flag', parser) assert not starts_like_flag('-', parser) assert not starts_like_flag('--has space', parser) assert not starts_like_flag('-2', parser) # Does start like a flag assert starts_like_flag('--', parser) assert starts_like_flag('-flag', parser) assert starts_like_flag('--flag', parser) def test_complete_command_no_tokens(ac_app): from cmd2.argparse_completer import AutoCompleter parser = cmd2.ArgParser() ac = AutoCompleter(parser, ac_app) completions = ac.complete_command(tokens=[], text='', line='', begidx=0, endidx=0) assert not completions def test_complete_command_help_no_tokens(ac_app): from cmd2.argparse_completer import AutoCompleter parser = cmd2.ArgParser() ac = AutoCompleter(parser, ac_app) completions = ac.complete_command_help(tokens=[], text='', line='', begidx=0, endidx=0) assert not completions