Skip to content

Commit 75400bc

Browse files
committed
Fix bug where tab completion didn't work in embedded Python shell when LibEdit was insalled instead of readline
1 parent a07e57c commit 75400bc

File tree

3 files changed

+80
-3
lines changed

3 files changed

+80
-3
lines changed

cmd2/cmd2.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ class _SavedCmd2Env:
183183

184184
def __init__(self) -> None:
185185
self.history: list[str] = []
186+
self.completer: Callable[[str, int], str | None] | None = None
186187

187188

188189
# Contains data about a disabled command which is used to restore its original functions when the command is enabled
@@ -4593,7 +4594,7 @@ def _reset_py_display() -> None:
45934594
sys.displayhook = sys.__displayhook__
45944595
sys.excepthook = sys.__excepthook__
45954596

4596-
def _set_up_py_shell_env(self, _interp: InteractiveConsole) -> _SavedCmd2Env:
4597+
def _set_up_py_shell_env(self, interp: InteractiveConsole) -> _SavedCmd2Env:
45974598
"""Set up interactive Python shell environment.
45984599
45994600
:return: Class containing saved up cmd2 environment.
@@ -4603,13 +4604,39 @@ def _set_up_py_shell_env(self, _interp: InteractiveConsole) -> _SavedCmd2Env:
46034604
# Set up sys module for the Python console
46044605
self._reset_py_display()
46054606

4607+
# Enable tab completion if readline is available
4608+
try:
4609+
import readline
4610+
import rlcompleter
4611+
except ImportError:
4612+
pass
4613+
else:
4614+
# Save the current completer
4615+
cmd2_env.completer = readline.get_completer()
4616+
4617+
# Set the completer to use the interpreter's locals
4618+
readline.set_completer(rlcompleter.Completer(interp.locals).complete)
4619+
4620+
# Use the correct binding based on whether LibEdit or Readline is being used
4621+
if 'libedit' in (readline.__doc__ or ''):
4622+
readline.parse_and_bind("bind ^I rl_complete")
4623+
else:
4624+
readline.parse_and_bind("tab: complete")
4625+
46064626
return cmd2_env
46074627

4608-
def _restore_cmd2_env(self, _cmd2_env: _SavedCmd2Env) -> None:
4628+
def _restore_cmd2_env(self, cmd2_env: _SavedCmd2Env) -> None:
46094629
"""Restore cmd2 environment after exiting an interactive Python shell.
46104630
46114631
:param cmd2_env: the environment settings to restore
46124632
"""
4633+
# Restore the readline completer
4634+
try:
4635+
import readline
4636+
except ImportError:
4637+
pass
4638+
else:
4639+
readline.set_completer(cmd2_env.completer)
46134640

46144641
def _run_python(self, *, pyscript: str | None = None) -> bool | None:
46154642
"""Run an interactive Python shell or execute a pyscript file.

examples/basic_completion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434
class BasicCompletion(cmd2.Cmd):
3535
def __init__(self) -> None:
36-
super().__init__(complete_style=CompleteStyle.MULTI_COLUMN)
36+
super().__init__(complete_style=CompleteStyle.MULTI_COLUMN, include_py=True)
3737

3838
def do_flag_based(self, statement: cmd2.Statement) -> None:
3939
"""Tab completes arguments based on a preceding flag using flag_based_complete

tests/test_py_completion.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import sys
2+
from code import InteractiveConsole
3+
from unittest import mock
4+
5+
6+
def test_py_completion_setup_readline(base_app):
7+
# Mock readline and rlcompleter
8+
mock_readline = mock.MagicMock()
9+
mock_readline.__doc__ = 'GNU Readline'
10+
mock_rlcompleter = mock.MagicMock()
11+
12+
with mock.patch.dict(sys.modules, {'readline': mock_readline, 'rlcompleter': mock_rlcompleter}):
13+
interp = InteractiveConsole()
14+
base_app._set_up_py_shell_env(interp)
15+
16+
# Verify completion setup for GNU Readline
17+
mock_readline.parse_and_bind.assert_called_with("tab: complete")
18+
mock_readline.set_completer.assert_called()
19+
20+
21+
def test_py_completion_setup_libedit(base_app):
22+
# Mock readline and rlcompleter
23+
mock_readline = mock.MagicMock()
24+
mock_readline.__doc__ = 'libedit'
25+
mock_rlcompleter = mock.MagicMock()
26+
27+
with mock.patch.dict(sys.modules, {'readline': mock_readline, 'rlcompleter': mock_rlcompleter}):
28+
interp = InteractiveConsole()
29+
base_app._set_up_py_shell_env(interp)
30+
31+
# Verify completion setup for LibEdit
32+
mock_readline.parse_and_bind.assert_called_with("bind ^I rl_complete")
33+
mock_readline.set_completer.assert_called()
34+
35+
36+
def test_py_completion_restore(base_app):
37+
# Mock readline
38+
mock_readline = mock.MagicMock()
39+
original_completer = mock.Mock()
40+
mock_readline.get_completer.return_value = original_completer
41+
42+
with mock.patch.dict(sys.modules, {'readline': mock_readline, 'rlcompleter': mock.MagicMock()}):
43+
interp = InteractiveConsole()
44+
env = base_app._set_up_py_shell_env(interp)
45+
46+
# Restore and verify
47+
base_app._restore_cmd2_env(env)
48+
49+
# set_completer is called twice: once in setup, once in restore
50+
mock_readline.set_completer.assert_called_with(original_completer)

0 commit comments

Comments
 (0)