Skip to content

Commit 6652557

Browse files
committed
Some fixes to autocompleter to make it easier to do delimited and file completion. Saving state - more to come.
1 parent 8125d45 commit 6652557

File tree

4 files changed

+64
-16
lines changed

4 files changed

+64
-16
lines changed

cmd2/argparse_completer.py

Lines changed: 40 additions & 10 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:

examples/tab_autocompletion.py

Lines changed: 19 additions & 1 deletion
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,18 @@ 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+
movie_file_action = vid_movies_load_parser.add_argument('movie_file', help='Movie database')
239+
228240
# tag the action objects with completion providers. This can be a collection or a callable
229241
setattr(director_action, argparse_completer.ACTION_ARG_CHOICES, static_list_directors)
230-
setattr(actor_action, argparse_completer.ACTION_ARG_CHOICES, instance_query_actors)
242+
setattr(actor_action, argparse_completer.ACTION_ARG_CHOICES, 'instance_query_actors')
243+
244+
# tag the file property with a custom completion function 'delimeter_complete' provided by cmd2.
245+
setattr(movie_file_action, argparse_completer.ACTION_ARG_CHOICES,
246+
('delimiter_complete',
247+
{'delimiter': '/',
248+
'match_against': file_list}))
231249

232250
vid_movies_delete_parser = vid_movies_commands_subparsers.add_parser('delete')
233251

tests/test_autocompletion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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)