Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
f29d6af
Initial publishing of autocompleter. #349
anselor Apr 12, 2018
f6b6a3c
Merge branch 'python3' into autocompleter
anselor Apr 14, 2018
e20f7ec
Started working on an example for autocompleter usage.
anselor Apr 14, 2018
874f884
Merge remote-tracking branch 'origin/master' into autocompleter
anselor Apr 15, 2018
8d4f841
* AutoCompleter
anselor Apr 15, 2018
bfc190e
Merge remote-tracking branch 'origin/python3' into autocompleter
anselor Apr 16, 2018
c99b9a2
Matched changes in the python3 branch.
anselor Apr 16, 2018
21454b2
Changed setup.py requirement for pyperclip to >= 1.5.27 instead of 1.6.0
tleonhardt Apr 16, 2018
bb5e358
Added more advanced/complex autocompleter examples.
anselor Apr 16, 2018
81adb04
Added unit tests for AutoCompleter.
anselor Apr 17, 2018
50c96bc
Tweaks for Python 3.4
anselor Apr 17, 2018
cd60899
Try adding typing for Python 3.4
anselor Apr 17, 2018
d5d42fb
More changes for Python 3.4
anselor Apr 17, 2018
07c283e
Added check for allow_abbrev attribute before accessing it (for Pytho…
anselor Apr 17, 2018
dac2680
Created a common prompt reprint implementation for all supported plat…
anselor Apr 17, 2018
dadcd27
Marked the 2 tests failing the windows unit test as skip for windows.…
anselor Apr 17, 2018
9e24c98
Added common file to provide readline utility functions
kmvanbrunt Apr 17, 2018
94da51a
Merge branch 'autocompleter' of github.com:python-cmd2/cmd2 into auto…
kmvanbrunt Apr 17, 2018
de471a8
Some minor tweaks to AutoCompleter handling a collection of index-bas…
anselor Apr 17, 2018
6585622
Bringing back color. Updated dependencies to include colorama.
anselor Apr 17, 2018
93cc246
Added check for whether the terminal is present before reprinting the…
anselor Apr 18, 2018
a448192
Merge remote-tracking branch 'origin/master' into autocompleter
anselor Apr 18, 2018
8a5e2ff
Should fix linux import
anselor Apr 18, 2018
736cdda
Adding back Pyperclip imports that got mixed up in merge
anselor Apr 18, 2018
9452dfa
Tweaked AutoCompleter.ACArgumentParser's constructor to pass through …
anselor Apr 18, 2018
2428482
Changed coverage to look in cmd2 directory.
anselor Apr 18, 2018
94bea93
Adjusted some coverage configuration. Found and fixed bug in help com…
anselor Apr 18, 2018
154fa93
Adds main.py to run base cmd2 directly for development testing purposes.
anselor Apr 18, 2018
4918300
Updated main.py debug/test app to have execute permissions and includ…
tleonhardt Apr 18, 2018
ff66a95
Merged ReadTheDocs build fix from master
tleonhardt Apr 18, 2018
c218633
Addresses comments on #362
anselor Apr 19, 2018
df09c85
Identified and marked a few blocks of code that can't be reached duri…
anselor Apr 19, 2018
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
2 changes: 1 addition & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# .coveragerc to control coverage.py
[run]
# Source
source = cmd2.py
source = cmd2/
Copy link
Member

Choose a reason for hiding this comment

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

Thanks for fixing the code coverage analysis configuration now that cmd2 is a multi-file package

# (boolean, default False): whether to measure branch coverage in addition to statement coverage.
branch = False

Expand Down
830 changes: 830 additions & 0 deletions cmd2/argparse_completer.py

Large diffs are not rendered by default.

114 changes: 44 additions & 70 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,38 @@
import unittest
from code import InteractiveConsole

try:
from enum34 import Enum
except ImportError:
from enum import Enum

import pyparsing
import pyperclip

# Set up readline
from .rl_utils import rl_force_redisplay, readline, rl_type, RlType

if rl_type == RlType.PYREADLINE:

# Save the original pyreadline display completion function since we need to override it and restore it
# noinspection PyProtectedMember
orig_pyreadline_display = readline.rl.mode._display_completions

elif rl_type == RlType.GNU:

# We need wcswidth to calculate display width of tab completions
from wcwidth import wcswidth

# Get the readline lib so we can make changes to it
import ctypes
from .rl_utils import readline_lib

# Save address that rl_basic_quote_characters is pointing to since we need to override and restore it
rl_basic_quote_characters = ctypes.c_char_p.in_dll(readline_lib, "rl_basic_quote_characters")
orig_rl_basic_quote_characters_addr = ctypes.cast(rl_basic_quote_characters, ctypes.c_void_p).value

# Newer versions of pyperclip are released as a single file, but older versions had a more complicated structure
try:
from pyperclip.exceptions import PyperclipException
except ImportError:
# noinspection PyUnresolvedReferences
from pyperclip import PyperclipException

# Collection is a container that is sizable and iterable
# It was introduced in Python 3.6. We will try to import it, otherwise use our implementation
try:
Expand Down Expand Up @@ -96,47 +113,6 @@ def __subclasshook__(cls, C):
except ImportError:
ipython_available = False

# Prefer statically linked gnureadline if available (for macOS compatibility due to issues with libedit)
try:
import gnureadline as readline
except ImportError:
# Try to import readline, but allow failure for convenience in Windows unit testing
# Note: If this actually fails, you should install readline on Linux or Mac or pyreadline on Windows
try:
# noinspection PyUnresolvedReferences
import readline
except ImportError:
pass

# Check what implementation of readline we are using
class RlType(Enum):
GNU = 1
PYREADLINE = 2
NONE = 3

rl_type = RlType.NONE

if 'pyreadline' in sys.modules:
rl_type = RlType.PYREADLINE

# Save the original pyreadline display completion function since we need to override it and restore it
# noinspection PyProtectedMember
orig_pyreadline_display = readline.rl.mode._display_completions

elif 'gnureadline' in sys.modules or 'readline' in sys.modules:
rl_type = RlType.GNU

# We need wcswidth to calculate display width of tab completions
from wcwidth import wcswidth

# Load the readline lib so we can make changes to it
import ctypes
readline_lib = ctypes.CDLL(readline.__file__)

# Save address that rl_basic_quote_characters is pointing to since we need to override and restore it
rl_basic_quote_characters = ctypes.c_char_p.in_dll(readline_lib, "rl_basic_quote_characters")
orig_rl_basic_quote_characters_addr = ctypes.cast(rl_basic_quote_characters, ctypes.c_void_p).value

__version__ = '0.9.0'

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

# If there are subcommands, store their names in a list to support tab-completion of subcommand names
if argparser._subparsers is not None:
subcommand_names = argparser._subparsers._group_actions[0]._name_parser_map.keys()
cmd_wrapper.__dict__['subcommand_names'] = subcommand_names
# Key is subcommand name and value is completer function
subcommands = collections.OrderedDict()

# Get all subcommands and check if they have completer functions
for name, parser in argparser._subparsers._group_actions[0]._name_parser_map.items():
if 'completer' in parser._defaults:
completer = parser._defaults['completer']
else:
completer = None
subcommands[name] = completer

cmd_wrapper.__dict__['subcommands'] = subcommands

return cmd_wrapper

Expand Down Expand Up @@ -1605,7 +1591,7 @@ def _redirect_complete(self, text, line, begidx, endidx, compfunc):
return compfunc(text, line, begidx, endidx)

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

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

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

# rl_forced_update_display() is the proper way to redraw the prompt and line, but we
# have to use ctypes to do it since Python's readline API does not wrap the function
readline_lib.rl_forced_update_display()

# Since we updated the display, readline asks that rl_display_fixed be set for efficiency
display_fixed = ctypes.c_int.in_dll(readline_lib, "rl_display_fixed")
display_fixed.value = 1
# Redraw prompt and input line
rl_force_redisplay()

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

# Display the matches
# 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 -----
Expand Down Expand Up @@ -3363,7 +3344,7 @@ def do_load(self, arglist):
# self._script_dir list when done.
with open(expanded_path, encoding='utf-8') as target:
self.cmdqueue = target.read().splitlines() + ['eos'] + self.cmdqueue
except IOError as e:
except IOError as e: # pragma: no cover
self.perror('Problem accessing script from {}:\n{}'.format(expanded_path, e))
return

Expand All @@ -3390,7 +3371,7 @@ def is_text_file(file_path):
# noinspection PyUnusedLocal
if sum(1 for line in f) > 0:
valid_text_file = True
except IOError:
except IOError: # pragma: no cover
pass
except UnicodeDecodeError:
# The file is not ASCII. Check if it is UTF-8.
Expand All @@ -3400,7 +3381,7 @@ def is_text_file(file_path):
# noinspection PyUnusedLocal
if sum(1 for line in f) > 0:
valid_text_file = True
except IOError:
except IOError: # pragma: no cover
pass
except UnicodeDecodeError:
# Not UTF-8
Expand Down Expand Up @@ -4066,10 +4047,3 @@ def __bool__(self):
return not self.err


if __name__ == '__main__':
# If run as the main application, simply start a bare-bones cmd2 application with only built-in functionality.

# Set "use_ipython" to True to include the ipy command if IPython is installed, which supports advanced interactive
# debugging of your application via introspection on self.
app = Cmd(use_ipython=False)
app.cmdloop()
66 changes: 66 additions & 0 deletions cmd2/rl_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# coding=utf-8
"""
Imports the proper readline for the platform and provides utility functions for it
"""
import sys

try:
from enum34 import Enum
except ImportError:
from enum import Enum

# Prefer statically linked gnureadline if available (for macOS compatibility due to issues with libedit)
try:
import gnureadline as readline
except ImportError:
# Try to import readline, but allow failure for convenience in Windows unit testing
# Note: If this actually fails, you should install readline on Linux or Mac or pyreadline on Windows
try:
# noinspection PyUnresolvedReferences
import readline
except ImportError: # pragma: no cover
pass


class RlType(Enum):
"""Readline library types we recognize"""
GNU = 1
PYREADLINE = 2
NONE = 3


# Check what implementation of readline we are using

rl_type = RlType.NONE

# The order of this check matters since importing pyreadline will also show readline in the modules list
if 'pyreadline' in sys.modules:
rl_type = RlType.PYREADLINE

elif 'gnureadline' in sys.modules or 'readline' in sys.modules:
rl_type = RlType.GNU

# Load the readline lib so we can access members of it
import ctypes
readline_lib = ctypes.CDLL(readline.__file__)


def rl_force_redisplay() -> None:
"""
Causes readline to redraw prompt and input line
"""
if not sys.stdout.isatty():
return

if rl_type == RlType.GNU: # pragma: no cover
Copy link
Member

Choose a reason for hiding this comment

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

This is covered on macOS when the gnureadline module is installed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The reason I blocked this out is because it can't be covered by the automated tests because there's isn't a terminal.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess it depends on where we care about seeing the coverage.

Copy link
Member

Choose a reason for hiding this comment

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

Oh good point. I think it is reasonable to block it out if we know it won't be hit by any of our CI systems.

# rl_forced_update_display() is the proper way to redraw the prompt and line, but we
# have to use ctypes to do it since Python's readline API does not wrap the function
readline_lib.rl_forced_update_display()

# After manually updating the display, readline asks that rl_display_fixed be set to 1 for efficiency
display_fixed = ctypes.c_int.in_dll(readline_lib, "rl_display_fixed")
display_fixed.value = 1

elif rl_type == RlType.PYREADLINE: # pragma: no cover
Copy link
Member

Choose a reason for hiding this comment

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

This is covered on Windows when the pyreadline module is installed, which is a conditional dependency on Windows so it should be. So this should be covered by our AppVeyor CI builds.

# noinspection PyProtectedMember
readline.rl.mode._print_prompt()
1 change: 1 addition & 0 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pyparsing
pyperclip
wcwidth
colorama
Copy link
Member

Choose a reason for hiding this comment

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

Thanks for adding this. We need to keep this up-to-date so that the documentation builds correctly on ReadTheDocs

Loading