summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md2
-rw-r--r--cmd2/argparse_completer.py50
-rw-r--r--cmd2/cmd2.py27
-rwxr-xr-xexamples/tab_autocomp_dynamic.py2
-rwxr-xr-xexamples/tab_autocompletion.py2
-rw-r--r--tests/test_autocompletion.py19
-rw-r--r--tests/test_completion.py18
7 files changed, 74 insertions, 46 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4967891a..ce72a858 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,8 @@
since the output will print at the same frequency as when the command is run in a terminal.
* **ACArgumentParser** no longer prints complete help text when a parsing error occurs since long help messages
scroll the actual error message off the screen.
+ * Exceptions occurring in tab completion functions are now printed to stderr before returning control back to
+ readline. This makes debugging a lot easier since readline suppresses these exceptions.
* **Python 3.4 EOL notice**
* Python 3.4 reached its [end of life](https://www.python.org/dev/peps/pep-0429/) on March 18, 2019
* This is the last release of `cmd2` which will support Python 3.4
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py
index ac65185b..edfaeec4 100644
--- a/cmd2/argparse_completer.py
+++ b/cmd2/argparse_completer.py
@@ -667,7 +667,7 @@ class AutoCompleter(object):
if callable(arg_choices[0]):
completer = arg_choices[0]
- elif isinstance(arg_choices[0], str) and callable(getattr(self._cmd2_app, arg_choices[0])):
+ else:
completer = getattr(self._cmd2_app, arg_choices[0])
# extract the positional and keyword arguments from the tuple
@@ -678,19 +678,16 @@ class AutoCompleter(object):
list_args = arg_choices[index]
elif isinstance(arg_choices[index], dict):
kw_args = arg_choices[index]
- 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 []
+
+ # 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)
else:
return self._cmd2_app.basic_complete(text, line, begidx, endidx,
self._resolve_choices_for_arg(action, used_values))
@@ -704,32 +701,17 @@ class AutoCompleter(object):
# 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 []
+ args = getattr(self._cmd2_app, args)
# is the provided argument a callable. If so, call it
if callable(args):
try:
- try:
- args = args(self._cmd2_app)
- except TypeError:
- args = args()
+ args = args(self._cmd2_app)
except TypeError:
- return []
-
- try:
- iter(args)
- except TypeError:
- pass
- else:
- # filter out arguments we already used
- args = [arg for arg in args if arg not in used_values]
+ args = args()
- if len(args) > 0:
- return args
+ # filter out arguments we already used
+ return [arg for arg in args if arg not in used_values]
return []
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index a7b60b1a..3c1c8d2c 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -1362,16 +1362,13 @@ class Cmd(cmd.Cmd):
# Display matches using actual display function. This also redraws the prompt and line.
orig_pyreadline_display(matches_to_display)
- # ----- Methods which override stuff in cmd -----
-
- def complete(self, text: str, state: int) -> Optional[str]:
- """Override of command method which returns the next possible completion for 'text'.
+ def _complete_worker(self, text: str, state: int) -> Optional[str]:
+ """The actual worker function for tab completion which is called by complete() and returns
+ the next possible completion for 'text'.
If a command has not been entered, then complete against command list.
Otherwise try to call complete_<command> to get list of completions.
- This method gets called directly by readline because it is set as the tab-completion function.
-
This completer function is called as complete(text, state), for state in 0, 1, 2, …, until it returns a
non-string value. It should return the next possible completion starting with text.
@@ -1581,6 +1578,24 @@ class Cmd(cmd.Cmd):
except IndexError:
return None
+ def complete(self, text: str, state: int) -> Optional[str]:
+ """Override of cmd2's complete method which returns the next possible completion for 'text'
+
+ This method gets called directly by readline. Since readline suppresses any exception raised
+ in completer functions, they can be difficult to debug. Therefore this function wraps the
+ actual tab completion logic and prints to stderr any exception that occurs before returning
+ control to readline.
+
+ :param text: the current word that user is typing
+ :param state: non-negative integer
+ """
+ # noinspection PyBroadException
+ try:
+ return self._complete_worker(text, state)
+ except Exception as e:
+ self.perror(e)
+ return None
+
def _autocomplete_default(self, text: str, line: str, begidx: int, endidx: int,
argparser: argparse.ArgumentParser) -> List[str]:
"""Default completion function for argparse commands."""
diff --git a/examples/tab_autocomp_dynamic.py b/examples/tab_autocomp_dynamic.py
index bedc9d4b..93b72442 100755
--- a/examples/tab_autocomp_dynamic.py
+++ b/examples/tab_autocomp_dynamic.py
@@ -69,7 +69,7 @@ class TabCompleteExample(cmd2.Cmd):
setattr(director_action, argparse_completer.ACTION_ARG_CHOICES, TabCompleteExample.static_list_directors)
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.
+ # tag the file property with a custom completion function 'delimiter_complete' provided by cmd2.
setattr(vid_movie_file_action, argparse_completer.ACTION_ARG_CHOICES,
('delimiter_complete',
{'delimiter': '/',
diff --git a/examples/tab_autocompletion.py b/examples/tab_autocompletion.py
index aa28fc10..3f06a274 100755
--- a/examples/tab_autocompletion.py
+++ b/examples/tab_autocompletion.py
@@ -255,7 +255,7 @@ class TabCompleteExample(cmd2.Cmd):
setattr(director_action, argparse_completer.ACTION_ARG_CHOICES, static_list_directors)
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.
+ # tag the file property with a custom completion function 'delimiter_complete' provided by cmd2.
setattr(vid_movie_file_action, argparse_completer.ACTION_ARG_CHOICES,
('delimiter_complete',
{'delimiter': '/',
diff --git a/tests/test_autocompletion.py b/tests/test_autocompletion.py
index a5dafd2d..005eee81 100644
--- a/tests/test_autocompletion.py
+++ b/tests/test_autocompletion.py
@@ -229,7 +229,7 @@ def test_autocomp_subcmd_flag_comp_list_attr(cmd2_app):
assert first_match is not None and first_match == '"Gareth Edwards'
-def test_autcomp_pos_consumed(cmd2_app):
+def test_autocomp_pos_consumed(cmd2_app):
text = ''
line = 'library movie add SW_EP01 {}'.format(text)
endidx = len(line)
@@ -239,7 +239,7 @@ def test_autcomp_pos_consumed(cmd2_app):
assert first_match is None
-def test_autcomp_pos_after_flag(cmd2_app):
+def test_autocomp_pos_after_flag(cmd2_app):
text = 'Joh'
line = 'video movies add -d "George Lucas" -- "Han Solo" PG "Emilia Clarke" "{}'.format(text)
endidx = len(line)
@@ -250,7 +250,7 @@ def test_autcomp_pos_after_flag(cmd2_app):
cmd2_app.completion_matches == ['John Boyega" ']
-def test_autcomp_custom_func_list_arg(cmd2_app):
+def test_autocomp_custom_func_list_arg(cmd2_app):
text = 'SW_'
line = 'library show add {}'.format(text)
endidx = len(line)
@@ -261,7 +261,7 @@ def test_autcomp_custom_func_list_arg(cmd2_app):
cmd2_app.completion_matches == ['SW_CW', 'SW_REB', 'SW_TCW']
-def test_autcomp_custom_func_list_and_dict_arg(cmd2_app):
+def test_autocomp_custom_func_list_and_dict_arg(cmd2_app):
text = ''
line = 'library show add SW_REB {}'.format(text)
endidx = len(line)
@@ -272,6 +272,17 @@ def test_autcomp_custom_func_list_and_dict_arg(cmd2_app):
cmd2_app.completion_matches == ['S01E02', 'S01E03', 'S02E01', 'S02E03']
+def test_autocomp_custom_func_dict_arg(cmd2_app):
+ text = '/home/user/'
+ line = 'video movies load {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match is not None and \
+ cmd2_app.completion_matches == ['/home/user/another.db', '/home/user/file space.db', '/home/user/file.db']
+
+
def test_argparse_remainder_flag_completion(cmd2_app):
import cmd2
import argparse
diff --git a/tests/test_completion.py b/tests/test_completion.py
index 23843012..158856ec 100644
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -77,6 +77,12 @@ class CompletionsExample(cmd2.Cmd):
num_strs = ['2', '11', '1']
return self.basic_complete(text, line, begidx, endidx, num_strs)
+ def do_test_raise_exception(self, args):
+ pass
+
+ def complete_test_raise_exception(self, text, line, begidx, endidx):
+ raise IndexError("You are out of bounds!!")
+
@pytest.fixture
def cmd2_app():
@@ -120,6 +126,18 @@ def test_complete_bogus_command(cmd2_app):
first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
assert first_match is None
+def test_complete_exception(cmd2_app, capsys):
+ text = ''
+ line = 'test_raise_exception {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ out, err = capsys.readouterr()
+
+ assert first_match is None
+ assert "IndexError" in err
+
def test_complete_macro(base_app, request):
# Create the macro
out, err = run_cmd(base_app, 'macro create fake pyscript {1}')