Skip to content

Commit c2594ff

Browse files
authored
Merge pull request #409 from python-cmd2/autocompleter
Autocompleter
2 parents a38e3f2 + 34c7e65 commit c2594ff

File tree

6 files changed

+97
-27
lines changed

6 files changed

+97
-27
lines changed

cmd2/argcomplete_bridge.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@
66
import argcomplete
77
except ImportError: # pragma: no cover
88
# not installed, skip the rest of the file
9-
pass
10-
9+
DEFAULT_COMPLETER = None
1110
else:
1211
# argcomplete is installed
1312

13+
# Newer versions of argcomplete have FilesCompleter at top level, older versions only have it under completers
14+
try:
15+
DEFAULT_COMPLETER = argcomplete.FilesCompleter()
16+
except AttributeError:
17+
DEFAULT_COMPLETER = argcomplete.completers.FilesCompleter()
18+
1419
from contextlib import redirect_stdout
1520
import copy
1621
from io import StringIO
@@ -102,7 +107,7 @@ class CompletionFinder(argcomplete.CompletionFinder):
102107

103108
def __call__(self, argument_parser, completer=None, always_complete_options=True, exit_method=os._exit, output_stream=None,
104109
exclude=None, validator=None, print_suppressed=False, append_space=None,
105-
default_completer=argcomplete.FilesCompleter()):
110+
default_completer=DEFAULT_COMPLETER):
106111
"""
107112
:param argument_parser: The argument parser to autocomplete on
108113
:type argument_parser: :class:`argparse.ArgumentParser`
@@ -140,9 +145,14 @@ def __call__(self, argument_parser, completer=None, always_complete_options=True
140145
added to argcomplete.safe_actions, if their values are wanted in the ``parsed_args`` completer argument, or
141146
their execution is otherwise desirable.
142147
"""
143-
self.__init__(argument_parser, always_complete_options=always_complete_options, exclude=exclude,
144-
validator=validator, print_suppressed=print_suppressed, append_space=append_space,
145-
default_completer=default_completer)
148+
# Older versions of argcomplete have fewer keyword arguments
149+
if sys.version_info >= (3, 5):
150+
self.__init__(argument_parser, always_complete_options=always_complete_options, exclude=exclude,
151+
validator=validator, print_suppressed=print_suppressed, append_space=append_space,
152+
default_completer=default_completer)
153+
else:
154+
self.__init__(argument_parser, always_complete_options=always_complete_options, exclude=exclude,
155+
validator=validator, print_suppressed=print_suppressed)
146156

147157
if "_ARGCOMPLETE" not in os.environ:
148158
# not an argument completion invocation

cmd2/argparse_completer.py

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -472,23 +472,43 @@ def _complete_for_arg(self, action: argparse.Action,
472472
if action.dest in self._arg_choices:
473473
arg_choices = self._arg_choices[action.dest]
474474

475-
if isinstance(arg_choices, tuple) and len(arg_choices) > 0 and callable(arg_choices[0]):
476-
completer = arg_choices[0]
475+
# if arg_choices is a tuple
476+
# Let's see if it's a custom completion function. If it is, return what it provides
477+
# To do this, we make sure the first element is either a callable
478+
# or it's the name of a callable in the application
479+
if isinstance(arg_choices, tuple) and len(arg_choices) > 0 and \
480+
(callable(arg_choices[0]) or
481+
(isinstance(arg_choices[0], str) and hasattr(self._cmd2_app, arg_choices[0]) and
482+
callable(getattr(self._cmd2_app, arg_choices[0]))
483+
)
484+
):
485+
486+
if callable(arg_choices[0]):
487+
completer = arg_choices[0]
488+
elif isinstance(arg_choices[0], str) and callable(getattr(self._cmd2_app, arg_choices[0])):
489+
completer = getattr(self._cmd2_app, arg_choices[0])
490+
491+
# extract the positional and keyword arguments from the tuple
477492
list_args = None
478493
kw_args = None
479494
for index in range(1, len(arg_choices)):
480495
if isinstance(arg_choices[index], list) or isinstance(arg_choices[index], tuple):
481496
list_args = arg_choices[index]
482497
elif isinstance(arg_choices[index], dict):
483498
kw_args = arg_choices[index]
484-
if list_args is not None and kw_args is not None:
485-
return completer(text, line, begidx, endidx, *list_args, **kw_args)
486-
elif list_args is not None:
487-
return completer(text, line, begidx, endidx, *list_args)
488-
elif kw_args is not None:
489-
return completer(text, line, begidx, endidx, **kw_args)
490-
else:
491-
return completer(text, line, begidx, endidx)
499+
try:
500+
# call the provided function differently depending on the provided positional and keyword arguments
501+
if list_args is not None and kw_args is not None:
502+
return completer(text, line, begidx, endidx, *list_args, **kw_args)
503+
elif list_args is not None:
504+
return completer(text, line, begidx, endidx, *list_args)
505+
elif kw_args is not None:
506+
return completer(text, line, begidx, endidx, **kw_args)
507+
else:
508+
return completer(text, line, begidx, endidx)
509+
except TypeError:
510+
# assume this is due to an incorrect function signature, return nothing.
511+
return []
492512
else:
493513
return AutoCompleter.basic_complete(text, line, begidx, endidx,
494514
self._resolve_choices_for_arg(action, used_values))
@@ -499,6 +519,16 @@ def _resolve_choices_for_arg(self, action: argparse.Action, used_values=()) -> L
499519
if action.dest in self._arg_choices:
500520
args = self._arg_choices[action.dest]
501521

522+
# is the argument a string? If so, see if we can find an attribute in the
523+
# application matching the string.
524+
if isinstance(args, str):
525+
try:
526+
args = getattr(self._cmd2_app, args)
527+
except AttributeError:
528+
# Couldn't find anything matching the name
529+
return []
530+
531+
# is the provided argument a callable. If so, call it
502532
if callable(args):
503533
try:
504534
if self._cmd2_app is not None:
@@ -535,7 +565,10 @@ def _print_action_help(self, action: argparse.Action) -> None:
535565

536566
prefix = '{}{}'.format(flags, param)
537567
else:
538-
prefix = '{}'.format(str(action.dest).upper())
568+
if action.dest != SUPPRESS:
569+
prefix = '{}'.format(str(action.dest).upper())
570+
else:
571+
prefix = ''
539572

540573
prefix = ' {0: <{width}} '.format(prefix, width=20)
541574
pref_len = len(prefix)

cmd2/pyscript_bridge.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ def process_flag(action, value):
230230
if action.option_strings:
231231
cmd_str[0] += '{} '.format(action.option_strings[0])
232232

233-
if isinstance(value, List) or isinstance(value, Tuple):
233+
if isinstance(value, List) or isinstance(value, tuple):
234234
for item in value:
235235
item = str(item).strip()
236236
if ' ' in item:
@@ -250,7 +250,7 @@ def traverse_parser(parser):
250250
cmd_str[0] += '{} '.format(self._args[action.dest])
251251
traverse_parser(action.choices[self._args[action.dest]])
252252
elif isinstance(action, argparse._AppendAction):
253-
if isinstance(self._args[action.dest], List) or isinstance(self._args[action.dest], Tuple):
253+
if isinstance(self._args[action.dest], list) or isinstance(self._args[action.dest], tuple):
254254
for values in self._args[action.dest]:
255255
process_flag(action, values)
256256
else:

examples/tab_autocompletion.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ def __init__(self):
9696
},
9797
}
9898

99+
file_list = \
100+
[
101+
'/home/user/file.db',
102+
'/home/user/file space.db',
103+
'/home/user/another.db',
104+
'/home/other user/maps.db',
105+
'/home/other user/tests.db'
106+
]
107+
99108
def instance_query_actors(self) -> List[str]:
100109
"""Simulating a function that queries and returns a completion values"""
101110
return actors
@@ -225,9 +234,23 @@ def _do_vid_media_shows(self, args) -> None:
225234
required=True)
226235
actor_action = vid_movies_add_parser.add_argument('actor', help='Actors', nargs='*')
227236

237+
vid_movies_load_parser = vid_movies_commands_subparsers.add_parser('load')
238+
vid_movie_file_action = vid_movies_load_parser.add_argument('movie_file', help='Movie database')
239+
240+
vid_movies_read_parser = vid_movies_commands_subparsers.add_parser('read')
241+
vid_movie_fread_action = vid_movies_read_parser.add_argument('movie_file', help='Movie database')
242+
228243
# tag the action objects with completion providers. This can be a collection or a callable
229244
setattr(director_action, argparse_completer.ACTION_ARG_CHOICES, static_list_directors)
230-
setattr(actor_action, argparse_completer.ACTION_ARG_CHOICES, instance_query_actors)
245+
setattr(actor_action, argparse_completer.ACTION_ARG_CHOICES, 'instance_query_actors')
246+
247+
# tag the file property with a custom completion function 'delimeter_complete' provided by cmd2.
248+
setattr(vid_movie_file_action, argparse_completer.ACTION_ARG_CHOICES,
249+
('delimiter_complete',
250+
{'delimiter': '/',
251+
'match_against': file_list}))
252+
setattr(vid_movie_fread_action, argparse_completer.ACTION_ARG_CHOICES,
253+
('path_complete', [False, False]))
231254

232255
vid_movies_delete_parser = vid_movies_commands_subparsers.add_parser('delete')
233256

@@ -306,6 +329,9 @@ def _do_media_shows(self, args) -> None:
306329

307330
movies_delete_parser = movies_commands_subparsers.add_parser('delete')
308331

332+
movies_load_parser = movies_commands_subparsers.add_parser('load')
333+
movie_file_action = movies_load_parser.add_argument('movie_file', help='Movie database')
334+
309335
shows_parser = media_types_subparsers.add_parser('shows')
310336
shows_parser.set_defaults(func=_do_media_shows)
311337

@@ -333,7 +359,8 @@ def do_media(self, args):
333359
def complete_media(self, text, line, begidx, endidx):
334360
""" Adds tab completion to media"""
335361
choices = {'actor': query_actors, # function
336-
'director': TabCompleteExample.static_list_directors # static list
362+
'director': TabCompleteExample.static_list_directors, # static list
363+
'movie_file': (self.path_complete, [False, False])
337364
}
338365
completer = argparse_completer.AutoCompleter(TabCompleteExample.media_parser, arg_choices=choices)
339366

tests/test_autocompletion.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ def test_autocomp_subcmd_nested(cmd2_app):
168168

169169
first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
170170
assert first_match is not None and \
171-
cmd2_app.completion_matches == ['add', 'delete', 'list']
171+
cmd2_app.completion_matches == ['add', 'delete', 'list', 'load']
172172

173173

174174
def test_autocomp_subcmd_flag_choices_append(cmd2_app):
@@ -246,7 +246,7 @@ def test_autcomp_pos_consumed(cmd2_app):
246246

247247
def test_autcomp_pos_after_flag(cmd2_app):
248248
text = 'Joh'
249-
line = 'media movies add -d "George Lucas" -- "Han Solo" PG "Emilia Clarke" "{}'.format(text)
249+
line = 'video movies add -d "George Lucas" -- "Han Solo" PG "Emilia Clarke" "{}'.format(text)
250250
endidx = len(line)
251251
begidx = endidx - len(text)
252252

tests/test_bashcompletion.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,15 +139,13 @@ def test_invalid_ifs(parser1, mock):
139139
@pytest.mark.parametrize('comp_line, exp_out, exp_err', [
140140
('media ', 'movies\013shows', ''),
141141
('media mo', 'movies', ''),
142+
('media movies list -a "J', '"John Boyega"\013"Jake Lloyd"', ''),
143+
('media movies list ', '', ''),
142144
('media movies add ', '\013\013 ', '''
143145
Hint:
144146
TITLE Movie Title'''),
145-
('media movies list -a "J', '"John Boyega"\013"Jake Lloyd"', ''),
146-
('media movies list ', '', '')
147147
])
148148
def test_commands(parser1, capfd, mock, comp_line, exp_out, exp_err):
149-
completer = CompletionFinder()
150-
151149
mock.patch.dict(os.environ, {'_ARGCOMPLETE': '1',
152150
'_ARGCOMPLETE_IFS': '\013',
153151
'COMP_TYPE': '63',
@@ -157,6 +155,8 @@ def test_commands(parser1, capfd, mock, comp_line, exp_out, exp_err):
157155
mock.patch.object(os, 'fdopen', my_fdopen)
158156

159157
with pytest.raises(SystemExit):
158+
completer = CompletionFinder()
159+
160160
choices = {'actor': query_actors, # function
161161
}
162162
autocompleter = AutoCompleter(parser1, arg_choices=choices)

0 commit comments

Comments
 (0)