Skip to content

Commit 9b43502

Browse files
committed
Added code to handle -- in argparse completer
1 parent f38e100 commit 9b43502

File tree

6 files changed

+101
-57
lines changed

6 files changed

+101
-57
lines changed

cmd2/argparse_completer.py

100755100644
Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,8 @@ def register_custom_actions(parser: argparse.ArgumentParser) -> None:
209209
parser.register('action', 'append', _AppendRangeAction)
210210

211211

212-
def token_resembles_flag(token: str, parser: argparse.ArgumentParser) -> bool:
213-
"""Determine if a token looks like a flag. Based on argparse._parse_optional()."""
212+
def is_potential_flag(token: str, parser: argparse.ArgumentParser) -> bool:
213+
"""Determine if a token looks like a potential flag. Based on argparse._parse_optional()."""
214214
# if it's an empty string, it was meant to be a positional
215215
if not token:
216216
return False
@@ -340,6 +340,10 @@ def complete_command(self, tokens: List[str], text: str, line: str, begidx: int,
340340
# Skip any flags or flag parameter tokens
341341
next_pos_arg_index = 0
342342

343+
# This gets set to True when flags will no longer be processed as argparse flags
344+
# That can happen when -- is used or an argument with nargs=argparse.REMAINDER is used
345+
skip_remaining_flags = False
346+
343347
pos_arg = AutoCompleter._ArgumentState()
344348
pos_action = None
345349

@@ -363,7 +367,7 @@ def consume_flag_argument() -> None:
363367
"""Consuming token as a flag argument"""
364368
# we're consuming flag arguments
365369
# if the token does not look like a new flag, then count towards flag arguments
366-
if not token_resembles_flag(token, self._parser) and flag_action is not None:
370+
if not is_potential_flag(token, self._parser) and flag_action is not None:
367371
flag_arg.count += 1
368372

369373
# does this complete a option item for the flag
@@ -432,8 +436,20 @@ def process_action_nargs(action: argparse.Action, arg_state: AutoCompleter._Argu
432436

433437
for idx, token in enumerate(tokens):
434438
is_last_token = idx >= len(tokens) - 1
439+
435440
# Only start at the start token index
436441
if idx >= self._token_start_index:
442+
443+
# all args after -- are non-flags
444+
if remainder['arg'] is None and token == '--':
445+
flag_action = None
446+
flag_arg.reset()
447+
if is_last_token:
448+
break
449+
else:
450+
skip_remaining_flags = True
451+
continue
452+
437453
# If a remainder action is found, force all future tokens to go to that
438454
if remainder['arg'] is not None:
439455
if remainder['action'] == pos_action:
@@ -442,23 +458,25 @@ def process_action_nargs(action: argparse.Action, arg_state: AutoCompleter._Argu
442458
elif remainder['action'] == flag_action:
443459
consume_flag_argument()
444460
continue
461+
445462
current_is_positional = False
446463
# Are we consuming flag arguments?
447464
if not flag_arg.needed:
448-
# Special case when each of the following is true:
449-
# - We're not in the middle of consuming flag arguments
450-
# - The current positional argument count has hit the max count
451-
# - The next positional argument is a REMAINDER argument
452-
# Argparse will now treat all future tokens as arguments to the positional including tokens that
453-
# look like flags so the completer should skip any flag related processing once this happens
454-
skip_flag = False
455-
if (pos_action is not None) and pos_arg.count >= pos_arg.max and \
456-
next_pos_arg_index < len(self._positional_actions) and \
457-
self._positional_actions[next_pos_arg_index].nargs == argparse.REMAINDER:
458-
skip_flag = True
465+
466+
if not skip_remaining_flags:
467+
# Special case when each of the following is true:
468+
# - We're not in the middle of consuming flag arguments
469+
# - The current positional argument count has hit the max count
470+
# - The next positional argument is a REMAINDER argument
471+
# Argparse will now treat all future tokens as arguments to the positional including tokens that
472+
# look like flags so the completer should skip any flag related processing once this happens
473+
if (pos_action is not None) and pos_arg.count >= pos_arg.max and \
474+
next_pos_arg_index < len(self._positional_actions) and \
475+
self._positional_actions[next_pos_arg_index].nargs == argparse.REMAINDER:
476+
skip_remaining_flags = True
459477

460478
# At this point we're no longer consuming flag arguments. Is the current argument a potential flag?
461-
if token_resembles_flag(token, self._parser) and not skip_flag:
479+
if is_potential_flag(token, self._parser) and not skip_remaining_flags:
462480
# reset some tracking values
463481
flag_arg.reset()
464482
# don't reset positional tracking because flags can be interspersed anywhere between positionals
@@ -524,22 +542,25 @@ def process_action_nargs(action: argparse.Action, arg_state: AutoCompleter._Argu
524542
else:
525543
consume_flag_argument()
526544

545+
if remainder['arg'] is not None:
546+
skip_remaining_flags = True
547+
527548
# don't reset this if we're on the last token - this allows completion to occur on the current token
528-
if not is_last_token and flag_arg.min is not None:
549+
elif not is_last_token and flag_arg.min is not None:
529550
flag_arg.needed = flag_arg.count < flag_arg.min
530551

531552
# Here we're done parsing all of the prior arguments. We know what the next argument is.
532553

554+
completion_results = []
555+
533556
# if we don't have a flag to populate with arguments and the last token starts with
534557
# a flag prefix then we'll complete the list of flag options
535-
completion_results = []
536558
if not flag_arg.needed and len(tokens[-1]) > 0 and tokens[-1][0] in self._parser.prefix_chars and \
537-
remainder['arg'] is None:
559+
not skip_remaining_flags:
538560
return AutoCompleter.basic_complete(text, line, begidx, endidx,
539561
[flag for flag in self._flags if flag not in matched_flags])
540562
# we're not at a positional argument, see if we're in a flag argument
541563
elif not current_is_positional:
542-
# current_items = []
543564
if flag_action is not None:
544565
consumed = consumed_arg_values[flag_action.dest]\
545566
if flag_action.dest in consumed_arg_values else []

cmd2/pyscript_bridge.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import sys
1313
from typing import List, Callable, Optional
1414

15-
from .argparse_completer import _RangeAction, token_resembles_flag
15+
from .argparse_completer import _RangeAction, is_potential_flag
1616
from .utils import namedtuple_with_defaults, StdSim, quote_string_if_needed
1717

1818
# Python 3.4 require contextlib2 for temporarily redirecting stderr and stdout
@@ -225,7 +225,7 @@ def process_argument(action, value):
225225
if isinstance(value, List) or isinstance(value, tuple):
226226
for item in value:
227227
item = str(item).strip()
228-
if token_resembles_flag(item, self._parser):
228+
if is_potential_flag(item, self._parser):
229229
raise ValueError('{} appears to be a flag and should be supplied as a keyword argument '
230230
'to the function.'.format(item))
231231
item = quote_string_if_needed(item)
@@ -240,7 +240,7 @@ def process_argument(action, value):
240240

241241
else:
242242
value = str(value).strip()
243-
if token_resembles_flag(value, self._parser):
243+
if is_potential_flag(value, self._parser):
244244
raise ValueError('{} appears to be a flag and should be supplied as a keyword argument '
245245
'to the function.'.format(value))
246246
value = quote_string_if_needed(value)

examples/tab_autocompletion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ def do_hybrid_suggest(self, args):
163163

164164
# This variant demonstrates the AutoCompleter working with the orginial argparse.
165165
# Base argparse is unable to specify narg ranges. Autocompleter will keep expecting additional arguments
166-
# for the -d/--duration flag until you specify a new flaw or end the list it with '--'
166+
# for the -d/--duration flag until you specify a new flag or end processing of flags with '--'
167167

168168
suggest_parser_orig = argparse.ArgumentParser()
169169

tests/test_acargparse.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
Released under MIT license, see LICENSE file
66
"""
77
import pytest
8-
from cmd2.argparse_completer import ACArgumentParser, token_resembles_flag
8+
from cmd2.argparse_completer import ACArgumentParser, is_potential_flag
99

1010

1111
def test_acarg_narg_empty_tuple():
@@ -53,16 +53,16 @@ def test_acarg_narg_tuple_zero_to_one():
5353
parser.add_argument('tuple', nargs=(0, 1))
5454

5555

56-
def test_token_resembles_flag():
56+
def test_is_potential_flag():
5757
parser = ACArgumentParser()
5858

5959
# Not valid flags
60-
assert not token_resembles_flag('', parser)
61-
assert not token_resembles_flag('non-flag', parser)
62-
assert not token_resembles_flag('-', parser)
63-
assert not token_resembles_flag('--has space', parser)
64-
assert not token_resembles_flag('-2', parser)
60+
assert not is_potential_flag('', parser)
61+
assert not is_potential_flag('non-flag', parser)
62+
assert not is_potential_flag('-', parser)
63+
assert not is_potential_flag('--has space', parser)
64+
assert not is_potential_flag('-2', parser)
6565

6666
# Valid flags
67-
assert token_resembles_flag('-flag', parser)
68-
assert token_resembles_flag('--flag', parser)
67+
assert is_potential_flag('-flag', parser)
68+
assert is_potential_flag('--flag', parser)

tests/test_autocompletion.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,51 @@ def test_autcomp_custom_func_list_and_dict_arg(cmd2_app):
279279
cmd2_app.completion_matches == ['S01E02', 'S01E03', 'S02E01', 'S02E03']
280280

281281

282+
def test_argparse_remainder_completion(cmd2_app):
283+
import cmd2
284+
import argparse
285+
286+
# First test a positional with nargs=argparse.REMAINDER
287+
text = '--h'
288+
line = 'help command subcommand {}'.format(text)
289+
endidx = len(line)
290+
begidx = endidx - len(text)
291+
292+
# --h should not complete into --help because we are in the argparse.REMAINDER sections
293+
assert complete_tester(text, line, begidx, endidx, cmd2_app) is None
294+
295+
# Now test a flag with nargs=argparse.REMAINDER
296+
parser = argparse.ArgumentParser()
297+
parser.add_argument('-f', nargs=argparse.REMAINDER)
298+
299+
# Overwrite eof's parser for this test
300+
cmd2.Cmd.do_eof.argparser = parser
301+
302+
text = '--h'
303+
line = 'eof -f {}'.format(text)
304+
endidx = len(line)
305+
begidx = endidx - len(text)
306+
307+
# --h should not complete into --help because we are in the argparse.REMAINDER sections
308+
assert complete_tester(text, line, begidx, endidx, cmd2_app) is None
309+
310+
311+
def test_completion_after_double_dash(cmd2_app):
312+
# Test -- as the last token before an argparse.REMAINDER sections
313+
text = '--'
314+
line = 'help {}'.format(text)
315+
endidx = len(line)
316+
begidx = endidx - len(text)
317+
318+
# Since -- is the last token in a non-remainder section, then it should show flag choices
319+
first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
320+
assert first_match is not None and '--help' in cmd2_app.completion_matches
321+
322+
# Test -- to end all flag completion
323+
text = '--'
324+
line = 'help -- {}'.format(text)
325+
endidx = len(line)
326+
begidx = endidx - len(text)
327+
328+
# Since -- appeared before the -- being completed, no more flags should be completed
329+
assert complete_tester(text, line, begidx, endidx, cmd2_app) is None

tests/test_completion.py

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -716,31 +716,6 @@ def test_add_opening_quote_delimited_space_in_prefix(cmd2_app):
716716
os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix and \
717717
cmd2_app.display_matches == expected_display
718718

719-
def test_argparse_remainder_completion(cmd2_app):
720-
# First test a positional with nargs=argparse.REMAINDER
721-
text = '--h'
722-
line = 'help command subcommand {}'.format(text)
723-
endidx = len(line)
724-
begidx = endidx - len(text)
725-
726-
# --h should not complete into --help because we are in the argparse.REMAINDER sections
727-
assert complete_tester(text, line, begidx, endidx, cmd2_app) is None
728-
729-
# Now test a flag with nargs=argparse.REMAINDER
730-
parser = argparse.ArgumentParser()
731-
parser.add_argument('-f', nargs=argparse.REMAINDER)
732-
733-
# Overwrite eof's parser for this test
734-
cmd2.Cmd.do_eof.argparser = parser
735-
736-
text = '--h'
737-
line = 'eof -f {}'.format(text)
738-
endidx = len(line)
739-
begidx = endidx - len(text)
740-
741-
# --h should not complete into --help because we are in the argparse.REMAINDER sections
742-
assert complete_tester(text, line, begidx, endidx, cmd2_app) is None
743-
744719
@pytest.fixture
745720
def sc_app():
746721
c = SubcommandsExample()

0 commit comments

Comments
 (0)