Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions cmd2/argcomplete_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@
import argcomplete
except ImportError: # pragma: no cover
# not installed, skip the rest of the file
pass

DEFAULT_COMPLETER = None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anselor Would you please review the changes I made to your PR. These changes were to support older versions of argcomplete which have a few API differences including:

  • FilesCompleter is not accessible at the top level
  • Fewer keyword arguments to an __init__ method

These older versions of argcomplete would commonly be found on older versions, in particular Python 3.4.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me.

else:
# argcomplete is installed

# Newer versions of argcomplete have FilesCompleter at top level, older versions only have it under completers
try:
DEFAULT_COMPLETER = argcomplete.FilesCompleter()
except AttributeError:
DEFAULT_COMPLETER = argcomplete.completers.FilesCompleter()

from contextlib import redirect_stdout
import copy
from io import StringIO
Expand Down Expand Up @@ -102,7 +107,7 @@ class CompletionFinder(argcomplete.CompletionFinder):

def __call__(self, argument_parser, completer=None, always_complete_options=True, exit_method=os._exit, output_stream=None,
exclude=None, validator=None, print_suppressed=False, append_space=None,
default_completer=argcomplete.FilesCompleter()):
default_completer=DEFAULT_COMPLETER):
"""
:param argument_parser: The argument parser to autocomplete on
:type argument_parser: :class:`argparse.ArgumentParser`
Expand Down Expand Up @@ -140,9 +145,14 @@ def __call__(self, argument_parser, completer=None, always_complete_options=True
added to argcomplete.safe_actions, if their values are wanted in the ``parsed_args`` completer argument, or
their execution is otherwise desirable.
"""
self.__init__(argument_parser, always_complete_options=always_complete_options, exclude=exclude,
validator=validator, print_suppressed=print_suppressed, append_space=append_space,
default_completer=default_completer)
# Older versions of argcomplete have fewer keyword arguments
if sys.version_info >= (3, 5):
self.__init__(argument_parser, always_complete_options=always_complete_options, exclude=exclude,
validator=validator, print_suppressed=print_suppressed, append_space=append_space,
default_completer=default_completer)
else:
self.__init__(argument_parser, always_complete_options=always_complete_options, exclude=exclude,
validator=validator, print_suppressed=print_suppressed)

if "_ARGCOMPLETE" not in os.environ:
# not an argument completion invocation
Expand Down
55 changes: 44 additions & 11 deletions cmd2/argparse_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,23 +472,43 @@ def _complete_for_arg(self, action: argparse.Action,
if action.dest in self._arg_choices:
arg_choices = self._arg_choices[action.dest]

if isinstance(arg_choices, tuple) and len(arg_choices) > 0 and callable(arg_choices[0]):
completer = arg_choices[0]
# if arg_choices is a tuple
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything looks OK to me

# Let's see if it's a custom completion function. If it is, return what it provides
# To do this, we make sure the first element is either a callable
# or it's the name of a callable in the application
if isinstance(arg_choices, tuple) and len(arg_choices) > 0 and \
(callable(arg_choices[0]) or
(isinstance(arg_choices[0], str) and hasattr(self._cmd2_app, arg_choices[0]) and
callable(getattr(self._cmd2_app, arg_choices[0]))
)
):

if callable(arg_choices[0]):
completer = arg_choices[0]
elif isinstance(arg_choices[0], str) and callable(getattr(self._cmd2_app, arg_choices[0])):
completer = getattr(self._cmd2_app, arg_choices[0])

# extract the positional and keyword arguments from the tuple
list_args = None
kw_args = None
for index in range(1, len(arg_choices)):
if isinstance(arg_choices[index], list) or isinstance(arg_choices[index], tuple):
list_args = arg_choices[index]
elif isinstance(arg_choices[index], dict):
kw_args = arg_choices[index]
if list_args is not None and kw_args is not None:
return completer(text, line, begidx, endidx, *list_args, **kw_args)
elif list_args is not None:
return completer(text, line, begidx, endidx, *list_args)
elif kw_args is not None:
return completer(text, line, begidx, endidx, **kw_args)
else:
return completer(text, line, begidx, endidx)
try:
# call the provided function differently depending on the provided positional and keyword arguments
if list_args is not None and kw_args is not None:
return completer(text, line, begidx, endidx, *list_args, **kw_args)
elif list_args is not None:
return completer(text, line, begidx, endidx, *list_args)
elif kw_args is not None:
return completer(text, line, begidx, endidx, **kw_args)
else:
return completer(text, line, begidx, endidx)
except TypeError:
# assume this is due to an incorrect function signature, return nothing.
return []
else:
return AutoCompleter.basic_complete(text, line, begidx, endidx,
self._resolve_choices_for_arg(action, used_values))
Expand All @@ -499,6 +519,16 @@ def _resolve_choices_for_arg(self, action: argparse.Action, used_values=()) -> L
if action.dest in self._arg_choices:
args = self._arg_choices[action.dest]

# is the argument a string? If so, see if we can find an attribute in the
# application matching the string.
if isinstance(args, str):
try:
args = getattr(self._cmd2_app, args)
except AttributeError:
# Couldn't find anything matching the name
return []

# is the provided argument a callable. If so, call it
if callable(args):
try:
if self._cmd2_app is not None:
Expand Down Expand Up @@ -535,7 +565,10 @@ def _print_action_help(self, action: argparse.Action) -> None:

prefix = '{}{}'.format(flags, param)
else:
prefix = '{}'.format(str(action.dest).upper())
if action.dest != SUPPRESS:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this PR close Issue #407? If so, you should mark it as doing so in the description so that it auto-closes on merge.

prefix = '{}'.format(str(action.dest).upper())
else:
prefix = ''

prefix = ' {0: <{width}} '.format(prefix, width=20)
pref_len = len(prefix)
Expand Down
4 changes: 2 additions & 2 deletions cmd2/pyscript_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ def process_flag(action, value):
if action.option_strings:
cmd_str[0] += '{} '.format(action.option_strings[0])

if isinstance(value, List) or isinstance(value, Tuple):
if isinstance(value, List) or isinstance(value, tuple):
for item in value:
item = str(item).strip()
if ' ' in item:
Expand All @@ -250,7 +250,7 @@ def traverse_parser(parser):
cmd_str[0] += '{} '.format(self._args[action.dest])
traverse_parser(action.choices[self._args[action.dest]])
elif isinstance(action, argparse._AppendAction):
if isinstance(self._args[action.dest], List) or isinstance(self._args[action.dest], Tuple):
if isinstance(self._args[action.dest], list) or isinstance(self._args[action.dest], tuple):
for values in self._args[action.dest]:
process_flag(action, values)
else:
Expand Down
31 changes: 29 additions & 2 deletions examples/tab_autocompletion.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,15 @@ def __init__(self):
},
}

file_list = \
[
'/home/user/file.db',
'/home/user/file space.db',
'/home/user/another.db',
'/home/other user/maps.db',
'/home/other user/tests.db'
]

def instance_query_actors(self) -> List[str]:
"""Simulating a function that queries and returns a completion values"""
return actors
Expand Down Expand Up @@ -225,9 +234,23 @@ def _do_vid_media_shows(self, args) -> None:
required=True)
actor_action = vid_movies_add_parser.add_argument('actor', help='Actors', nargs='*')

vid_movies_load_parser = vid_movies_commands_subparsers.add_parser('load')
vid_movie_file_action = vid_movies_load_parser.add_argument('movie_file', help='Movie database')

vid_movies_read_parser = vid_movies_commands_subparsers.add_parser('read')
vid_movie_fread_action = vid_movies_read_parser.add_argument('movie_file', help='Movie database')

# tag the action objects with completion providers. This can be a collection or a callable
setattr(director_action, argparse_completer.ACTION_ARG_CHOICES, static_list_directors)
setattr(actor_action, argparse_completer.ACTION_ARG_CHOICES, instance_query_actors)
setattr(actor_action, argparse_completer.ACTION_ARG_CHOICES, 'instance_query_actors')

# tag the file property with a custom completion function 'delimeter_complete' provided by cmd2.
setattr(vid_movie_file_action, argparse_completer.ACTION_ARG_CHOICES,
('delimiter_complete',
{'delimiter': '/',
'match_against': file_list}))
setattr(vid_movie_fread_action, argparse_completer.ACTION_ARG_CHOICES,
('path_complete', [False, False]))

vid_movies_delete_parser = vid_movies_commands_subparsers.add_parser('delete')

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

movies_delete_parser = movies_commands_subparsers.add_parser('delete')

movies_load_parser = movies_commands_subparsers.add_parser('load')
movie_file_action = movies_load_parser.add_argument('movie_file', help='Movie database')

shows_parser = media_types_subparsers.add_parser('shows')
shows_parser.set_defaults(func=_do_media_shows)

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

Expand Down
4 changes: 2 additions & 2 deletions tests/test_autocompletion.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ def test_autocomp_subcmd_nested(cmd2_app):

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


def test_autocomp_subcmd_flag_choices_append(cmd2_app):
Expand Down Expand Up @@ -246,7 +246,7 @@ def test_autcomp_pos_consumed(cmd2_app):

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

Expand Down
8 changes: 4 additions & 4 deletions tests/test_bashcompletion.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,15 +139,13 @@ def test_invalid_ifs(parser1, mock):
@pytest.mark.parametrize('comp_line, exp_out, exp_err', [
('media ', 'movies\013shows', ''),
('media mo', 'movies', ''),
('media movies list -a "J', '"John Boyega"\013"Jake Lloyd"', ''),
('media movies list ', '', ''),
('media movies add ', '\013\013 ', '''
Hint:
TITLE Movie Title'''),
('media movies list -a "J', '"John Boyega"\013"Jake Lloyd"', ''),
('media movies list ', '', '')
])
def test_commands(parser1, capfd, mock, comp_line, exp_out, exp_err):
completer = CompletionFinder()

mock.patch.dict(os.environ, {'_ARGCOMPLETE': '1',
'_ARGCOMPLETE_IFS': '\013',
'COMP_TYPE': '63',
Expand All @@ -157,6 +155,8 @@ def test_commands(parser1, capfd, mock, comp_line, exp_out, exp_err):
mock.patch.object(os, 'fdopen', my_fdopen)

with pytest.raises(SystemExit):
completer = CompletionFinder()

choices = {'actor': query_actors, # function
}
autocompleter = AutoCompleter(parser1, arg_choices=choices)
Expand Down