Skip to content

Commit 631ed8a

Browse files
authored
Merge pull request #994 from python-cmd2/format_flags
Group flags in argparse tab completion
2 parents a8d3ab2 + 11117ce commit 631ed8a

File tree

3 files changed

+65
-27
lines changed

3 files changed

+65
-27
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
* Added user-settable option called `always_show_hint`. If True, then tab completion hints will always
44
display even when tab completion suggestions print. Arguments whose help or hint text is suppressed will
55
not display hints even when this setting is True.
6+
* argparse tab completion now groups flag names which run the same action. Optional flags are wrapped
7+
in brackets like it is done in argparse usage text.
68
* Bug Fixes
79
* Fixed issue where flag names weren't always sorted correctly in argparse tab completion
810

cmd2/argparse_completer.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,25 @@ def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matche
465465
if action.help != argparse.SUPPRESS:
466466
match_against.append(flag)
467467

468-
return basic_complete(text, line, begidx, endidx, match_against)
468+
matches = basic_complete(text, line, begidx, endidx, match_against)
469+
470+
# Build a dictionary linking actions with their matched flag names
471+
matched_actions = dict() # type: Dict[argparse.Action, List[str]]
472+
for flag in matches:
473+
action = self._flag_to_action[flag]
474+
matched_actions.setdefault(action, [])
475+
matched_actions[action].append(flag)
476+
477+
# For tab completion suggestions, group matched flags by action
478+
for action, option_strings in matched_actions.items():
479+
flag_text = ', '.join(option_strings)
480+
481+
# Mark optional flags with brackets
482+
if not action.required:
483+
flag_text = '[' + flag_text + ']'
484+
self._cmd2_app.display_matches.append(flag_text)
485+
486+
return matches
469487

470488
def _format_completions(self, arg_state: _ArgumentState, completions: List[Union[str, CompletionItem]]) -> List[str]:
471489
# Check if the results are CompletionItems and that there aren't too many to display

tests/test_argparse_completer.py

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ def do_music(self, args: argparse.Namespace) -> None:
9292
flag_parser.add_argument('-c', '--count_flag', help='count flag', action='count')
9393
flag_parser.add_argument('-s', '--suppressed_flag', help=argparse.SUPPRESS, action='store_true')
9494
flag_parser.add_argument('-r', '--remainder_flag', nargs=argparse.REMAINDER, help='a remainder flag')
95+
flag_parser.add_argument('-q', '--required_flag', required=True, help='a required flag', action='store_true')
9596

9697
@with_argparser(flag_parser)
9798
def do_flag(self, args: argparse.Namespace) -> None:
@@ -100,6 +101,7 @@ def do_flag(self, args: argparse.Namespace) -> None:
100101
# Uses non-default flag prefix value (+)
101102
plus_flag_parser = Cmd2ArgumentParser(prefix_chars='+')
102103
plus_flag_parser.add_argument('+n', '++normal_flag', help='a normal flag', action='store_true')
104+
plus_flag_parser.add_argument('+q', '++required_flag', required=True, help='a required flag', action='store_true')
103105

104106
@with_argparser(plus_flag_parser)
105107
def do_plus_flag(self, args: argparse.Namespace) -> None:
@@ -356,61 +358,77 @@ def test_subcommand_completions(ac_app, subcommand, text, completions):
356358
assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key)
357359

358360

359-
@pytest.mark.parametrize('command_and_args, text, completions', [
361+
@pytest.mark.parametrize('command_and_args, text, completion_matches, display_matches', [
360362
# Complete all flags (suppressed will not show)
361-
('flag', '-', ['--append_const_flag', '--append_flag', '--count_flag', '--help', '--normal_flag',
362-
'--remainder_flag', '-a', '-c', '-h', '-n', '-o', '-r']),
363-
('flag', '--', ['--append_const_flag', '--append_flag', '--count_flag', '--help',
364-
'--normal_flag', '--remainder_flag']),
363+
('flag', '-',
364+
['--append_const_flag', '--append_flag', '--count_flag', '--help', '--normal_flag',
365+
'--remainder_flag', '--required_flag', '-a', '-c', '-h', '-n', '-o', '-q', '-r'],
366+
['-q, --required_flag', '[-o, --append_const_flag]', '[-a, --append_flag]', '[-c, --count_flag]', '[-h, --help]',
367+
'[-n, --normal_flag]', '[-r, --remainder_flag]']),
368+
('flag', '--',
369+
['--append_const_flag', '--append_flag', '--count_flag', '--help',
370+
'--normal_flag', '--remainder_flag', '--required_flag'],
371+
['--required_flag', '[--append_const_flag]', '[--append_flag]', '[--count_flag]', '[--help]',
372+
'[--normal_flag]', '[--remainder_flag]']),
365373
366374
# Complete individual flag
367-
('flag', '-n', ['-n ']),
368-
('flag', '--n', ['--normal_flag ']),
375+
('flag', '-n', ['-n '], ['[-n]']),
376+
('flag', '--n', ['--normal_flag '], ['[--normal_flag]']),
369377
370378
# No flags should complete until current flag has its args
371-
('flag --append_flag', '-', []),
379+
('flag --append_flag', '-', [], []),
372380
373381
# Complete REMAINDER flag name
374-
('flag', '-r', ['-r ']),
375-
('flag', '--r', ['--remainder_flag ']),
382+
('flag', '-r', ['-r '], ['[-r]']),
383+
('flag', '--rem', ['--remainder_flag '], ['[--remainder_flag]']),
376384
377385
# No flags after a REMAINDER should complete
378-
('flag -r value', '-', []),
379-
('flag --remainder_flag value', '--', []),
386+
('flag -r value', '-', [], []),
387+
('flag --remainder_flag value', '--', [], []),
380388
381389
# Suppressed flag should not complete
382-
('flag', '-s', []),
383-
('flag', '--s', []),
390+
('flag', '-s', [], []),
391+
('flag', '--s', [], []),
384392
385393
# A used flag should not show in completions
386-
('flag -n', '--', ['--append_const_flag', '--append_flag', '--count_flag', '--help', '--remainder_flag']),
394+
('flag -n', '--',
395+
['--append_const_flag', '--append_flag', '--count_flag', '--help', '--remainder_flag', '--required_flag'],
396+
['--required_flag', '[--append_const_flag]', '[--append_flag]', '[--count_flag]', '[--help]', '[--remainder_flag]']),
387397
388398
# Flags with actions set to append, append_const, and count will always show even if they've been used
389-
('flag --append_const_flag -c --append_flag value', '--', ['--append_const_flag', '--append_flag', '--count_flag',
390-
'--help', '--normal_flag', '--remainder_flag']),
399+
('flag --append_const_flag -c --append_flag value', '--',
400+
['--append_const_flag', '--append_flag', '--count_flag', '--help',
401+
'--normal_flag', '--remainder_flag', '--required_flag'],
402+
['--required_flag', '[--append_const_flag]', '[--append_flag]', '[--count_flag]', '[--help]',
403+
'[--normal_flag]', '[--remainder_flag]']),
391404
392405
# Non-default flag prefix character (+)
393-
('plus_flag', '+', ['++help', '++normal_flag', '+h', '+n']),
394-
('plus_flag', '++', ['++help', '++normal_flag']),
406+
('plus_flag', '+',
407+
['++help', '++normal_flag', '+h', '+n', '+q', '++required_flag'],
408+
['+q, ++required_flag', '[+h, ++help]', '[+n, ++normal_flag]']),
409+
('plus_flag', '++',
410+
['++help', '++normal_flag', '++required_flag'],
411+
['++required_flag', '[++help]', '[++normal_flag]']),
395412
396413
# Flag completion should not occur after '--' since that tells argparse all remaining arguments are non-flags
397-
('flag --', '--', []),
398-
('flag --help --', '--', []),
399-
('plus_flag --', '++', []),
400-
('plus_flag ++help --', '++', [])
414+
('flag --', '--', [], []),
415+
('flag --help --', '--', [], []),
416+
('plus_flag --', '++', [], []),
417+
('plus_flag ++help --', '++', [], [])
401418
])
402-
def test_autcomp_flag_completion(ac_app, command_and_args, text, completions):
419+
def test_autcomp_flag_completion(ac_app, command_and_args, text, completion_matches, display_matches):
403420
line = '{} {}'.format(command_and_args, text)
404421
endidx = len(line)
405422
begidx = endidx - len(text)
406423

407424
first_match = complete_tester(text, line, begidx, endidx, ac_app)
408-
if completions:
425+
if completion_matches:
409426
assert first_match is not None
410427
else:
411428
assert first_match is None
412429

413-
assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key)
430+
assert (ac_app.completion_matches == sorted(completion_matches, key=ac_app.default_sort_key) and
431+
ac_app.display_matches == sorted(display_matches, key=ac_app.default_sort_key))
414432

415433

416434
@pytest.mark.parametrize('flag, text, completions', [

0 commit comments

Comments
 (0)