Skip to content

Commit d214709

Browse files
committed
Fixed issue where argparse completion errors were being rewrapped as _ActionCompletionError in some cases
1 parent e1dc763 commit d214709

File tree

3 files changed

+58
-9
lines changed

3 files changed

+58
-9
lines changed

cmd2/argparse_completer.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,13 @@ def __init__(self, arg_action: argparse.Action) -> None:
9595
self.max = self.action.nargs
9696

9797

98+
class _ArgparseCompletionError(CompletionError):
99+
"""CompletionError specific to argparse-based tab completion"""
100+
pass
101+
102+
98103
# noinspection PyProtectedMember
99-
class _ActionCompletionError(CompletionError):
104+
class _ActionCompletionError(_ArgparseCompletionError):
100105
def __init__(self, arg_action: argparse.Action, completion_error: CompletionError) -> None:
101106
"""
102107
Adds action-specific information to a CompletionError. These are raised when
@@ -107,27 +112,27 @@ def __init__(self, arg_action: argparse.Action, completion_error: CompletionErro
107112
# Indent all lines of completion_error
108113
indented_error = textwrap.indent(str(completion_error), ' ')
109114

110-
error = ("\nError tab completing {}:\n"
111-
"{}\n".format(argparse._get_action_name(arg_action), indented_error))
115+
error = ("Error tab completing {}:\n"
116+
"{}".format(argparse._get_action_name(arg_action), indented_error))
112117
super().__init__(ansi.style_error(error))
113118

114119

115120
# noinspection PyProtectedMember
116-
class _UnfinishedFlagError(CompletionError):
121+
class _UnfinishedFlagError(_ArgparseCompletionError):
117122
def __init__(self, flag_arg_state: _ArgumentState) -> None:
118123
"""
119124
CompletionError which occurs when the user has not finished the current flag
120125
:param flag_arg_state: information about the unfinished flag action
121126
"""
122-
error = "\nError: argument {}: {} ({} entered)\n".\
127+
error = "Error: argument {}: {} ({} entered)".\
123128
format(argparse._get_action_name(flag_arg_state.action),
124129
generate_range_error(flag_arg_state.min, flag_arg_state.max),
125130
flag_arg_state.count)
126131
super().__init__(ansi.style_error(error))
127132

128133

129134
# noinspection PyProtectedMember
130-
class _NoResultsError(CompletionError):
135+
class _NoResultsError(_ArgparseCompletionError):
131136
def __init__(self, parser: argparse.ArgumentParser, arg_action: argparse.Action) -> None:
132137
"""
133138
CompletionError which occurs when there are no results. If hinting is allowed, then its message will
@@ -145,7 +150,7 @@ def __init__(self, parser: argparse.ArgumentParser, arg_action: argparse.Action)
145150
formatter.start_section("Hint")
146151
formatter.add_argument(arg_action)
147152
formatter.end_section()
148-
hint_str = '\n' + formatter.format_help()
153+
hint_str = formatter.format_help()
149154
super().__init__(hint_str)
150155

151156

@@ -416,6 +421,8 @@ def update_mutex_groups(arg_action: argparse.Action) -> None:
416421
try:
417422
completion_results = self._complete_for_arg(flag_arg_state.action, text, line,
418423
begidx, endidx, consumed_arg_values)
424+
except _ArgparseCompletionError as ex:
425+
raise ex
419426
except CompletionError as ex:
420427
raise _ActionCompletionError(flag_arg_state.action, ex)
421428

@@ -439,6 +446,8 @@ def update_mutex_groups(arg_action: argparse.Action) -> None:
439446
try:
440447
completion_results = self._complete_for_arg(pos_arg_state.action, text, line,
441448
begidx, endidx, consumed_arg_values)
449+
except _ArgparseCompletionError as ex:
450+
raise ex
442451
except CompletionError as ex:
443452
raise _ActionCompletionError(pos_arg_state.action, ex)
444453

cmd2/cmd2.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1420,7 +1420,7 @@ def complete(self, text: str, state: int) -> Optional[str]:
14201420
err_str = str(e)
14211421
if err_str:
14221422
# Don't print error and redraw the prompt unless the error has length
1423-
ansi.style_aware_write(sys.stdout, err_str + '\n')
1423+
ansi.style_aware_write(sys.stdout, '\n' + err_str + '\n')
14241424
rl_force_redisplay()
14251425
return None
14261426
except Exception as e:

tests/test_argparse_completer.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
completions_from_function = ['completions', 'function', 'fairly', 'complete']
3131
completions_from_method = ['completions', 'method', 'missed', 'spot']
3232

33+
AP_COMP_ERROR_TEXT = "SHOULD ONLY BE THIS TEXT"
34+
3335

3436
def choices_function() -> List[str]:
3537
"""Function that provides choices"""
@@ -53,7 +55,7 @@ def completer_takes_arg_tokens(text: str, line: str, begidx: int, endidx: int,
5355
return basic_complete(text, line, begidx, endidx, match_against)
5456

5557

56-
# noinspection PyMethodMayBeStatic,PyUnusedLocal
58+
# noinspection PyMethodMayBeStatic,PyUnusedLocal,PyProtectedMember
5759
class AutoCompleteTester(cmd2.Cmd):
5860
"""Cmd2 app that exercises ArgparseCompleter class"""
5961
def __init__(self, *args, **kwargs):
@@ -181,6 +183,7 @@ def do_completer(self, args: argparse.Namespace) -> None:
181183
choices=one_or_more_choices)
182184
nargs_parser.add_argument("--optional", help="a flag with an optional value", nargs=argparse.OPTIONAL,
183185
choices=optional_choices)
186+
# noinspection PyTypeChecker
184187
nargs_parser.add_argument("--range", help="a flag with nargs range", nargs=(1, 2),
185188
choices=range_choices)
186189
nargs_parser.add_argument("--remainder", help="a flag wanting remaining", nargs=argparse.REMAINDER,
@@ -231,6 +234,24 @@ def choice_raise_error(self) -> List[str]:
231234
def do_raise_completion_error(self, args: argparse.Namespace) -> None:
232235
pass
233236

237+
############################################################################################################
238+
# Begin code related to _ArgparseCompletionError
239+
############################################################################################################
240+
def raise_argparse_completion_error(self):
241+
"""Raises ArgparseCompletionError to make sure it gets raised as is"""
242+
from cmd2.argparse_completer import _ArgparseCompletionError
243+
raise _ArgparseCompletionError(AP_COMP_ERROR_TEXT)
244+
245+
ap_comp_error_parser = Cmd2ArgumentParser()
246+
ap_comp_error_parser.add_argument('pos_ap_comp_err', help='pos ap completion error',
247+
choices_method=raise_argparse_completion_error)
248+
ap_comp_error_parser.add_argument('--flag_ap_comp_err', help='flag ap completion error',
249+
choices_method=raise_argparse_completion_error)
250+
251+
@with_argparser(ap_comp_error_parser)
252+
def do_raise_ap_completion_error(self, args: argparse.Namespace) -> None:
253+
pass
254+
234255
############################################################################################################
235256
# Begin code related to receiving arg_tokens
236257
############################################################################################################
@@ -772,6 +793,25 @@ def test_completion_error(ac_app, capsys, args, text):
772793
assert "{} broke something".format(text) in out
773794

774795

796+
@pytest.mark.parametrize('arg', [
797+
# Exercise positional arg that raises _ArgparseCompletionError
798+
'',
799+
800+
# Exercise flag arg that raises _ArgparseCompletionError
801+
'--flag_ap_comp_err'
802+
])
803+
def test_argparse_completion_error(ac_app, capsys, arg):
804+
text = ''
805+
line = 'raise_ap_completion_error {} {}'.format(arg, text)
806+
endidx = len(line)
807+
begidx = endidx - len(text)
808+
809+
first_match = complete_tester(text, line, begidx, endidx, ac_app)
810+
assert first_match is None
811+
out, err = capsys.readouterr()
812+
assert out.strip() == AP_COMP_ERROR_TEXT
813+
814+
775815
@pytest.mark.parametrize('command_and_args, completions', [
776816
# Exercise a choices function that receives arg_tokens dictionary
777817
('arg_tokens choice subcmd', ['choice', 'subcmd']),

0 commit comments

Comments
 (0)