Skip to content

Commit fc41d86

Browse files
committed
Exceptions occurring in tab completion functions are now printed to stderr before returning control back to readline
1 parent 4ebcf42 commit fc41d86

File tree

3 files changed

+41
-6
lines changed

3 files changed

+41
-6
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* `StdSim.buffer.write()` now flushes when the wrapped stream uses line buffering and the bytes being written
66
contain a newline or carriage return. This helps when `pyscript` is echoing the output of a shell command
77
since the output will print at the same frequency as when the command is run in a terminal.
8+
* Exceptions occurring in tab completion functions are now printed to stderr before returning control back to
9+
readline. This makes debugging a lot easier since readline suppresses these exceptions.
810
* **Python 3.4 EOL notice**
911
* Python 3.4 reached its [end of life](https://www.python.org/dev/peps/pep-0429/) on March 18, 2019
1012
* This is the last release of `cmd2` which will support Python 3.4

cmd2/cmd2.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1362,16 +1362,13 @@ def _display_matches_pyreadline(self, matches: List[str]) -> None: # pragma: no
13621362
# Display matches using actual display function. This also redraws the prompt and line.
13631363
orig_pyreadline_display(matches_to_display)
13641364

1365-
# ----- Methods which override stuff in cmd -----
1366-
1367-
def complete(self, text: str, state: int) -> Optional[str]:
1368-
"""Override of command method which returns the next possible completion for 'text'.
1365+
def _complete_worker(self, text: str, state: int) -> Optional[str]:
1366+
"""The actual worker function for tab completion which is called by complete() and returns
1367+
the next possible completion for 'text'.
13691368
13701369
If a command has not been entered, then complete against command list.
13711370
Otherwise try to call complete_<command> to get list of completions.
13721371
1373-
This method gets called directly by readline because it is set as the tab-completion function.
1374-
13751372
This completer function is called as complete(text, state), for state in 0, 1, 2, …, until it returns a
13761373
non-string value. It should return the next possible completion starting with text.
13771374
@@ -1581,6 +1578,24 @@ def complete(self, text: str, state: int) -> Optional[str]:
15811578
except IndexError:
15821579
return None
15831580

1581+
def complete(self, text: str, state: int) -> Optional[str]:
1582+
"""Override of cmd2's complete method which returns the next possible completion for 'text'
1583+
1584+
This method gets called directly by readline. Since readline suppresses any exception raised
1585+
in completer functions, they can be difficult to debug. Therefore this function wraps the
1586+
actual tab completion logic and prints to stderr any exception that occurs before returning
1587+
control to readline.
1588+
1589+
:param text: the current word that user is typing
1590+
:param state: non-negative integer
1591+
"""
1592+
# noinspection PyBroadException
1593+
try:
1594+
return self._complete_worker(text, state)
1595+
except Exception as e:
1596+
self.perror(e)
1597+
return None
1598+
15841599
def _autocomplete_default(self, text: str, line: str, begidx: int, endidx: int,
15851600
argparser: argparse.ArgumentParser) -> List[str]:
15861601
"""Default completion function for argparse commands."""

tests/test_completion.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ def complete_test_sort_key(self, text, line, begidx, endidx):
7777
num_strs = ['2', '11', '1']
7878
return self.basic_complete(text, line, begidx, endidx, num_strs)
7979

80+
def do_test_raise_exception(self, args):
81+
pass
82+
83+
def complete_test_raise_exception(self, text, line, begidx, endidx):
84+
raise IndexError("You are out of bounds!!")
85+
8086

8187
@pytest.fixture
8288
def cmd2_app():
@@ -120,6 +126,18 @@ def test_complete_bogus_command(cmd2_app):
120126
first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
121127
assert first_match is None
122128

129+
def test_complete_exception(cmd2_app, capsys):
130+
text = ''
131+
line = 'test_raise_exception {}'.format(text)
132+
endidx = len(line)
133+
begidx = endidx - len(text)
134+
135+
first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
136+
out, err = capsys.readouterr()
137+
138+
assert first_match is None
139+
assert "IndexError" in err
140+
123141
def test_complete_macro(base_app, request):
124142
# Create the macro
125143
out, err = run_cmd(base_app, 'macro create fake pyscript {1}')

0 commit comments

Comments
 (0)