Skip to content

Commit 49a3290

Browse files
authored
Merge pull request #362 from python-cmd2/autocompleter
Autocompleter
2 parents 7560b0a + df09c85 commit 49a3290

File tree

13 files changed

+1920
-125
lines changed

13 files changed

+1920
-125
lines changed

.coveragerc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# .coveragerc to control coverage.py
22
[run]
33
# Source
4-
source = cmd2.py
4+
source = cmd2/
55
# (boolean, default False): whether to measure branch coverage in addition to statement coverage.
66
branch = False
77

cmd2/argparse_completer.py

Lines changed: 830 additions & 0 deletions
Large diffs are not rendered by default.

cmd2/cmd2.py

Lines changed: 44 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -45,21 +45,38 @@
4545
import unittest
4646
from code import InteractiveConsole
4747

48-
try:
49-
from enum34 import Enum
50-
except ImportError:
51-
from enum import Enum
52-
5348
import pyparsing
5449
import pyperclip
5550

51+
# Set up readline
52+
from .rl_utils import rl_force_redisplay, readline, rl_type, RlType
53+
54+
if rl_type == RlType.PYREADLINE:
55+
56+
# Save the original pyreadline display completion function since we need to override it and restore it
57+
# noinspection PyProtectedMember
58+
orig_pyreadline_display = readline.rl.mode._display_completions
59+
60+
elif rl_type == RlType.GNU:
61+
62+
# We need wcswidth to calculate display width of tab completions
63+
from wcwidth import wcswidth
64+
65+
# Get the readline lib so we can make changes to it
66+
import ctypes
67+
from .rl_utils import readline_lib
68+
69+
# Save address that rl_basic_quote_characters is pointing to since we need to override and restore it
70+
rl_basic_quote_characters = ctypes.c_char_p.in_dll(readline_lib, "rl_basic_quote_characters")
71+
orig_rl_basic_quote_characters_addr = ctypes.cast(rl_basic_quote_characters, ctypes.c_void_p).value
72+
5673
# Newer versions of pyperclip are released as a single file, but older versions had a more complicated structure
5774
try:
5875
from pyperclip.exceptions import PyperclipException
5976
except ImportError:
6077
# noinspection PyUnresolvedReferences
6178
from pyperclip import PyperclipException
62-
79+
6380
# Collection is a container that is sizable and iterable
6481
# It was introduced in Python 3.6. We will try to import it, otherwise use our implementation
6582
try:
@@ -96,47 +113,6 @@ def __subclasshook__(cls, C):
96113
except ImportError:
97114
ipython_available = False
98115

99-
# Prefer statically linked gnureadline if available (for macOS compatibility due to issues with libedit)
100-
try:
101-
import gnureadline as readline
102-
except ImportError:
103-
# Try to import readline, but allow failure for convenience in Windows unit testing
104-
# Note: If this actually fails, you should install readline on Linux or Mac or pyreadline on Windows
105-
try:
106-
# noinspection PyUnresolvedReferences
107-
import readline
108-
except ImportError:
109-
pass
110-
111-
# Check what implementation of readline we are using
112-
class RlType(Enum):
113-
GNU = 1
114-
PYREADLINE = 2
115-
NONE = 3
116-
117-
rl_type = RlType.NONE
118-
119-
if 'pyreadline' in sys.modules:
120-
rl_type = RlType.PYREADLINE
121-
122-
# Save the original pyreadline display completion function since we need to override it and restore it
123-
# noinspection PyProtectedMember
124-
orig_pyreadline_display = readline.rl.mode._display_completions
125-
126-
elif 'gnureadline' in sys.modules or 'readline' in sys.modules:
127-
rl_type = RlType.GNU
128-
129-
# We need wcswidth to calculate display width of tab completions
130-
from wcwidth import wcswidth
131-
132-
# Load the readline lib so we can make changes to it
133-
import ctypes
134-
readline_lib = ctypes.CDLL(readline.__file__)
135-
136-
# Save address that rl_basic_quote_characters is pointing to since we need to override and restore it
137-
rl_basic_quote_characters = ctypes.c_char_p.in_dll(readline_lib, "rl_basic_quote_characters")
138-
orig_rl_basic_quote_characters_addr = ctypes.cast(rl_basic_quote_characters, ctypes.c_void_p).value
139-
140116
__version__ = '0.9.0'
141117

142118
# Pyparsing enablePackrat() can greatly speed up parsing, but problems have been seen in Python 3 in the past
@@ -295,8 +271,18 @@ def cmd_wrapper(instance, cmdline):
295271

296272
# If there are subcommands, store their names in a list to support tab-completion of subcommand names
297273
if argparser._subparsers is not None:
298-
subcommand_names = argparser._subparsers._group_actions[0]._name_parser_map.keys()
299-
cmd_wrapper.__dict__['subcommand_names'] = subcommand_names
274+
# Key is subcommand name and value is completer function
275+
subcommands = collections.OrderedDict()
276+
277+
# Get all subcommands and check if they have completer functions
278+
for name, parser in argparser._subparsers._group_actions[0]._name_parser_map.items():
279+
if 'completer' in parser._defaults:
280+
completer = parser._defaults['completer']
281+
else:
282+
completer = None
283+
subcommands[name] = completer
284+
285+
cmd_wrapper.__dict__['subcommands'] = subcommands
300286

301287
return cmd_wrapper
302288

@@ -1605,7 +1591,7 @@ def _redirect_complete(self, text, line, begidx, endidx, compfunc):
16051591
return compfunc(text, line, begidx, endidx)
16061592

16071593
@staticmethod
1608-
def _pad_matches_to_display(matches_to_display):
1594+
def _pad_matches_to_display(matches_to_display): # pragma: no cover
16091595
"""
16101596
Adds padding to the matches being displayed as tab completion suggestions.
16111597
The default padding of readline/pyreadine is small and not visually appealing
@@ -1627,7 +1613,7 @@ def _pad_matches_to_display(matches_to_display):
16271613

16281614
return [cur_match + padding for cur_match in matches_to_display], len(padding)
16291615

1630-
def _display_matches_gnu_readline(self, substitution, matches, longest_match_length):
1616+
def _display_matches_gnu_readline(self, substitution, matches, longest_match_length): # pragma: no cover
16311617
"""
16321618
Prints a match list using GNU readline's rl_display_match_list()
16331619
This exists to print self.display_matches if it has data. Otherwise matches prints.
@@ -1675,15 +1661,10 @@ def _display_matches_gnu_readline(self, substitution, matches, longest_match_len
16751661
# rl_display_match_list(strings_array, number of completion matches, longest match length)
16761662
readline_lib.rl_display_match_list(strings_array, len(encoded_matches), longest_match_length)
16771663

1678-
# rl_forced_update_display() is the proper way to redraw the prompt and line, but we
1679-
# have to use ctypes to do it since Python's readline API does not wrap the function
1680-
readline_lib.rl_forced_update_display()
1681-
1682-
# Since we updated the display, readline asks that rl_display_fixed be set for efficiency
1683-
display_fixed = ctypes.c_int.in_dll(readline_lib, "rl_display_fixed")
1684-
display_fixed.value = 1
1664+
# Redraw prompt and input line
1665+
rl_force_redisplay()
16851666

1686-
def _display_matches_pyreadline(self, matches):
1667+
def _display_matches_pyreadline(self, matches): # pragma: no cover
16871668
"""
16881669
Prints a match list using pyreadline's _display_completions()
16891670
This exists to print self.display_matches if it has data. Otherwise matches prints.
@@ -1701,7 +1682,7 @@ def _display_matches_pyreadline(self, matches):
17011682
# Add padding for visual appeal
17021683
matches_to_display, _ = self._pad_matches_to_display(matches_to_display)
17031684

1704-
# Display the matches
1685+
# Display matches using actual display function. This also redraws the prompt and line.
17051686
orig_pyreadline_display(matches_to_display)
17061687

17071688
# ----- Methods which override stuff in cmd -----
@@ -3363,7 +3344,7 @@ def do_load(self, arglist):
33633344
# self._script_dir list when done.
33643345
with open(expanded_path, encoding='utf-8') as target:
33653346
self.cmdqueue = target.read().splitlines() + ['eos'] + self.cmdqueue
3366-
except IOError as e:
3347+
except IOError as e: # pragma: no cover
33673348
self.perror('Problem accessing script from {}:\n{}'.format(expanded_path, e))
33683349
return
33693350

@@ -3390,7 +3371,7 @@ def is_text_file(file_path):
33903371
# noinspection PyUnusedLocal
33913372
if sum(1 for line in f) > 0:
33923373
valid_text_file = True
3393-
except IOError:
3374+
except IOError: # pragma: no cover
33943375
pass
33953376
except UnicodeDecodeError:
33963377
# The file is not ASCII. Check if it is UTF-8.
@@ -3400,7 +3381,7 @@ def is_text_file(file_path):
34003381
# noinspection PyUnusedLocal
34013382
if sum(1 for line in f) > 0:
34023383
valid_text_file = True
3403-
except IOError:
3384+
except IOError: # pragma: no cover
34043385
pass
34053386
except UnicodeDecodeError:
34063387
# Not UTF-8
@@ -4066,10 +4047,3 @@ def __bool__(self):
40664047
return not self.err
40674048

40684049

4069-
if __name__ == '__main__':
4070-
# If run as the main application, simply start a bare-bones cmd2 application with only built-in functionality.
4071-
4072-
# Set "use_ipython" to True to include the ipy command if IPython is installed, which supports advanced interactive
4073-
# debugging of your application via introspection on self.
4074-
app = Cmd(use_ipython=False)
4075-
app.cmdloop()

cmd2/rl_utils.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# coding=utf-8
2+
"""
3+
Imports the proper readline for the platform and provides utility functions for it
4+
"""
5+
import sys
6+
7+
try:
8+
from enum34 import Enum
9+
except ImportError:
10+
from enum import Enum
11+
12+
# Prefer statically linked gnureadline if available (for macOS compatibility due to issues with libedit)
13+
try:
14+
import gnureadline as readline
15+
except ImportError:
16+
# Try to import readline, but allow failure for convenience in Windows unit testing
17+
# Note: If this actually fails, you should install readline on Linux or Mac or pyreadline on Windows
18+
try:
19+
# noinspection PyUnresolvedReferences
20+
import readline
21+
except ImportError: # pragma: no cover
22+
pass
23+
24+
25+
class RlType(Enum):
26+
"""Readline library types we recognize"""
27+
GNU = 1
28+
PYREADLINE = 2
29+
NONE = 3
30+
31+
32+
# Check what implementation of readline we are using
33+
34+
rl_type = RlType.NONE
35+
36+
# The order of this check matters since importing pyreadline will also show readline in the modules list
37+
if 'pyreadline' in sys.modules:
38+
rl_type = RlType.PYREADLINE
39+
40+
elif 'gnureadline' in sys.modules or 'readline' in sys.modules:
41+
rl_type = RlType.GNU
42+
43+
# Load the readline lib so we can access members of it
44+
import ctypes
45+
readline_lib = ctypes.CDLL(readline.__file__)
46+
47+
48+
def rl_force_redisplay() -> None:
49+
"""
50+
Causes readline to redraw prompt and input line
51+
"""
52+
if not sys.stdout.isatty():
53+
return
54+
55+
if rl_type == RlType.GNU: # pragma: no cover
56+
# rl_forced_update_display() is the proper way to redraw the prompt and line, but we
57+
# have to use ctypes to do it since Python's readline API does not wrap the function
58+
readline_lib.rl_forced_update_display()
59+
60+
# After manually updating the display, readline asks that rl_display_fixed be set to 1 for efficiency
61+
display_fixed = ctypes.c_int.in_dll(readline_lib, "rl_display_fixed")
62+
display_fixed.value = 1
63+
64+
elif rl_type == RlType.PYREADLINE: # pragma: no cover
65+
# noinspection PyProtectedMember
66+
readline.rl.mode._print_prompt()

docs/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pyparsing
22
pyperclip
33
wcwidth
4+
colorama

0 commit comments

Comments
 (0)