From d214709eecf2208b5edb6c52af69a0d76973e595 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Tue, 18 Feb 2020 12:10:34 -0500 Subject: Fixed issue where argparse completion errors were being rewrapped as _ActionCompletionError in some cases --- cmd2/argparse_completer.py | 23 +++++++++++++++------- cmd2/cmd2.py | 2 +- tests/test_argparse_completer.py | 42 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index a0c19959..add8868c 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -95,8 +95,13 @@ class _ArgumentState: self.max = self.action.nargs +class _ArgparseCompletionError(CompletionError): + """CompletionError specific to argparse-based tab completion""" + pass + + # noinspection PyProtectedMember -class _ActionCompletionError(CompletionError): +class _ActionCompletionError(_ArgparseCompletionError): def __init__(self, arg_action: argparse.Action, completion_error: CompletionError) -> None: """ Adds action-specific information to a CompletionError. These are raised when @@ -107,19 +112,19 @@ class _ActionCompletionError(CompletionError): # Indent all lines of completion_error indented_error = textwrap.indent(str(completion_error), ' ') - error = ("\nError tab completing {}:\n" - "{}\n".format(argparse._get_action_name(arg_action), indented_error)) + error = ("Error tab completing {}:\n" + "{}".format(argparse._get_action_name(arg_action), indented_error)) super().__init__(ansi.style_error(error)) # noinspection PyProtectedMember -class _UnfinishedFlagError(CompletionError): +class _UnfinishedFlagError(_ArgparseCompletionError): def __init__(self, flag_arg_state: _ArgumentState) -> None: """ CompletionError which occurs when the user has not finished the current flag :param flag_arg_state: information about the unfinished flag action """ - error = "\nError: argument {}: {} ({} entered)\n".\ + error = "Error: argument {}: {} ({} entered)".\ format(argparse._get_action_name(flag_arg_state.action), generate_range_error(flag_arg_state.min, flag_arg_state.max), flag_arg_state.count) @@ -127,7 +132,7 @@ class _UnfinishedFlagError(CompletionError): # noinspection PyProtectedMember -class _NoResultsError(CompletionError): +class _NoResultsError(_ArgparseCompletionError): def __init__(self, parser: argparse.ArgumentParser, arg_action: argparse.Action) -> None: """ CompletionError which occurs when there are no results. If hinting is allowed, then its message will @@ -145,7 +150,7 @@ class _NoResultsError(CompletionError): formatter.start_section("Hint") formatter.add_argument(arg_action) formatter.end_section() - hint_str = '\n' + formatter.format_help() + hint_str = formatter.format_help() super().__init__(hint_str) @@ -416,6 +421,8 @@ class ArgparseCompleter: try: completion_results = self._complete_for_arg(flag_arg_state.action, text, line, begidx, endidx, consumed_arg_values) + except _ArgparseCompletionError as ex: + raise ex except CompletionError as ex: raise _ActionCompletionError(flag_arg_state.action, ex) @@ -439,6 +446,8 @@ class ArgparseCompleter: try: completion_results = self._complete_for_arg(pos_arg_state.action, text, line, begidx, endidx, consumed_arg_values) + except _ArgparseCompletionError as ex: + raise ex except CompletionError as ex: raise _ActionCompletionError(pos_arg_state.action, ex) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 49273b51..60d5463a 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1420,7 +1420,7 @@ class Cmd(cmd.Cmd): err_str = str(e) if err_str: # Don't print error and redraw the prompt unless the error has length - ansi.style_aware_write(sys.stdout, err_str + '\n') + ansi.style_aware_write(sys.stdout, '\n' + err_str + '\n') rl_force_redisplay() return None except Exception as e: diff --git a/tests/test_argparse_completer.py b/tests/test_argparse_completer.py index 2619d053..83cee30f 100644 --- a/tests/test_argparse_completer.py +++ b/tests/test_argparse_completer.py @@ -30,6 +30,8 @@ positional_choices = ['the', 'positional', 'choices'] completions_from_function = ['completions', 'function', 'fairly', 'complete'] completions_from_method = ['completions', 'method', 'missed', 'spot'] +AP_COMP_ERROR_TEXT = "SHOULD ONLY BE THIS TEXT" + def choices_function() -> List[str]: """Function that provides choices""" @@ -53,7 +55,7 @@ def completer_takes_arg_tokens(text: str, line: str, begidx: int, endidx: int, return basic_complete(text, line, begidx, endidx, match_against) -# noinspection PyMethodMayBeStatic,PyUnusedLocal +# noinspection PyMethodMayBeStatic,PyUnusedLocal,PyProtectedMember class AutoCompleteTester(cmd2.Cmd): """Cmd2 app that exercises ArgparseCompleter class""" def __init__(self, *args, **kwargs): @@ -181,6 +183,7 @@ class AutoCompleteTester(cmd2.Cmd): 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, @@ -231,6 +234,24 @@ class AutoCompleteTester(cmd2.Cmd): def do_raise_completion_error(self, args: argparse.Namespace) -> None: pass + ############################################################################################################ + # Begin code related to _ArgparseCompletionError + ############################################################################################################ + def raise_argparse_completion_error(self): + """Raises ArgparseCompletionError to make sure it gets raised as is""" + from cmd2.argparse_completer import _ArgparseCompletionError + raise _ArgparseCompletionError(AP_COMP_ERROR_TEXT) + + ap_comp_error_parser = Cmd2ArgumentParser() + ap_comp_error_parser.add_argument('pos_ap_comp_err', help='pos ap completion error', + choices_method=raise_argparse_completion_error) + ap_comp_error_parser.add_argument('--flag_ap_comp_err', help='flag ap completion error', + choices_method=raise_argparse_completion_error) + + @with_argparser(ap_comp_error_parser) + def do_raise_ap_completion_error(self, args: argparse.Namespace) -> None: + pass + ############################################################################################################ # Begin code related to receiving arg_tokens ############################################################################################################ @@ -772,6 +793,25 @@ def test_completion_error(ac_app, capsys, args, text): assert "{} broke something".format(text) in out +@pytest.mark.parametrize('arg', [ + # Exercise positional arg that raises _ArgparseCompletionError + '', + + # Exercise flag arg that raises _ArgparseCompletionError + '--flag_ap_comp_err' +]) +def test_argparse_completion_error(ac_app, capsys, arg): + text = '' + line = 'raise_ap_completion_error {} {}'.format(arg, text) + endidx = len(line) + begidx = endidx - len(text) + + first_match = complete_tester(text, line, begidx, endidx, ac_app) + assert first_match is None + out, err = capsys.readouterr() + assert out.strip() == AP_COMP_ERROR_TEXT + + @pytest.mark.parametrize('command_and_args, completions', [ # Exercise a choices function that receives arg_tokens dictionary ('arg_tokens choice subcmd', ['choice', 'subcmd']), -- cgit v1.2.1