summaryrefslogtreecommitdiff
path: root/cmd2/cmd2.py
diff options
context:
space:
mode:
authorkmvanbrunt <kmvanbrunt@gmail.com>2018-05-26 01:17:48 -0400
committerGitHub <noreply@github.com>2018-05-26 01:17:48 -0400
commit6b6a36c41bd4d1ce3c7b8fbe46d935e599ccf744 (patch)
treebf57a15570248a2e3fbfb058a817d6c743cc9167 /cmd2/cmd2.py
parentcad21a60fa92ebe4a7c177142d273f9f7497967b (diff)
parent4c67d3372db924e87be920d89aadecb946584b31 (diff)
downloadcmd2-git-6b6a36c41bd4d1ce3c7b8fbe46d935e599ccf744.tar.gz
Merge pull request #416 from python-cmd2/pyshell_readline
Pyshell readline
Diffstat (limited to 'cmd2/cmd2.py')
-rw-r--r--[-rwxr-xr-x]cmd2/cmd2.py149
1 files changed, 130 insertions, 19 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index f480b3ae..817a1f21 100755..100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -58,6 +58,9 @@ if rl_type == RlType.NONE:
else:
from .rl_utils import rl_force_redisplay, readline
+ # Used by rlcompleter in Python console loaded by py command
+ orig_rl_delims = readline.get_completer_delims()
+
if rl_type == RlType.PYREADLINE:
# Save the original pyreadline display completion function since we need to override it and restore it
@@ -73,6 +76,9 @@ else:
import ctypes
from .rl_utils import readline_lib
+ rl_basic_quote_characters = ctypes.c_char_p.in_dll(readline_lib, "rl_basic_quote_characters")
+ orig_rl_basic_quotes = ctypes.cast(rl_basic_quote_characters, ctypes.c_void_p).value
+
from .argparse_completer import AutoCompleter, ACArgumentParser
# Newer versions of pyperclip are released as a single file, but older versions had a more complicated structure
@@ -416,6 +422,7 @@ class Cmd(cmd.Cmd):
self.initial_stdout = sys.stdout
self.history = History()
self.pystate = {}
+ self.py_history = []
self.pyscript_name = 'app'
self.keywords = self.reserved_words + [fname[3:] for fname in dir(self) if fname.startswith('do_')]
self.statement_parser = StatementParser(
@@ -476,7 +483,7 @@ class Cmd(cmd.Cmd):
############################################################################################################
# The following variables are used by tab-completion functions. They are reset each time complete() is run
- # using set_completion_defaults() and it is up to completer functions to set them before returning results.
+ # in reset_completion_defaults() and it is up to completer functions to set them before returning results.
############################################################################################################
# If true and a single match is returned to complete(), then a space will be appended
@@ -643,7 +650,7 @@ class Cmd(cmd.Cmd):
# ----- Methods related to tab completion -----
- def set_completion_defaults(self):
+ def reset_completion_defaults(self):
"""
Resets tab completion settings
Needs to be called each time readline runs tab completion
@@ -1285,7 +1292,7 @@ class Cmd(cmd.Cmd):
import functools
if state == 0 and rl_type != RlType.NONE:
unclosed_quote = ''
- self.set_completion_defaults()
+ self.reset_completion_defaults()
# lstrip the original line
orig_line = readline.get_line_buffer()
@@ -2026,12 +2033,10 @@ class Cmd(cmd.Cmd):
# Set GNU readline's rl_basic_quote_characters to NULL so it won't automatically add a closing quote
# We don't need to worry about setting rl_completion_suppress_quote since we never declared
# rl_completer_quote_characters.
- basic_quote_characters = ctypes.c_char_p.in_dll(readline_lib, "rl_basic_quote_characters")
- old_basic_quote_characters = ctypes.cast(basic_quote_characters, ctypes.c_void_p).value
- basic_quote_characters.value = None
+ old_basic_quotes = ctypes.cast(rl_basic_quote_characters, ctypes.c_void_p).value
+ rl_basic_quote_characters.value = None
old_completer = readline.get_completer()
- old_delims = readline.get_completer_delims()
readline.set_completer(self.complete)
# Break words on whitespace and quotes when tab completing
@@ -2041,6 +2046,7 @@ class Cmd(cmd.Cmd):
# If redirection is allowed, then break words on those characters too
completer_delims += ''.join(constants.REDIRECTION_CHARS)
+ old_delims = readline.get_completer_delims()
readline.set_completer_delims(completer_delims)
# Enable tab completion
@@ -2077,7 +2083,7 @@ class Cmd(cmd.Cmd):
if rl_type == RlType.GNU:
readline.set_completion_display_matches_hook(None)
- basic_quote_characters.value = old_basic_quote_characters
+ rl_basic_quote_characters.value = old_basic_quotes
elif rl_type == RlType.PYREADLINE:
readline.rl.mode._display_completions = orig_pyreadline_display
@@ -2507,7 +2513,30 @@ Usage: Usage: unalias [-a] name [name ...]
index_dict = {1: self.shell_cmd_complete}
return self.index_based_complete(text, line, begidx, endidx, index_dict, self.path_complete)
- # noinspection PyBroadException
+ @staticmethod
+ def _reset_py_display() -> None:
+ """
+ Resets the dynamic objects in the sys module that the py and ipy consoles fight over.
+ When a Python console starts it adopts certain display settings if they've already been set.
+ If an ipy console has previously been run, then py uses its settings and ends up looking
+ like an ipy console in terms of prompt and exception text. This method forces the Python
+ console to create its own display settings since they won't exist.
+
+ IPython does not have this problem since it always overwrites the display settings when it
+ is run. Therefore this method only needs to be called before creating a Python console.
+ """
+ # Delete any prompts that have been set
+ attributes = ['ps1', 'ps2', 'ps3']
+ for cur_attr in attributes:
+ try:
+ del sys.__dict__[cur_attr]
+ except KeyError:
+ pass
+
+ # Reset functions
+ sys.displayhook = sys.__displayhook__
+ sys.excepthook = sys.__excepthook__
+
def do_py(self, arg):
"""
Invoke python command, shell, or script
@@ -2524,6 +2553,7 @@ Usage: Usage: unalias [-a] name [name ...]
return
self._in_py = True
+ # noinspection PyBroadException
try:
arg = arg.strip()
@@ -2539,6 +2569,7 @@ Usage: Usage: unalias [-a] name [name ...]
except IOError as e:
self.perror(e)
+ # noinspection PyUnusedLocal
def onecmd_plus_hooks(cmd_plus_args):
"""Run a cmd2.Cmd command from a Python script or the interactive Python console.
@@ -2561,6 +2592,8 @@ Usage: Usage: unalias [-a] name [name ...]
if arg:
interp.runcode(arg)
+
+ # If there are no args, then we will open an interactive Python console
else:
# noinspection PyShadowingBuiltins
def quit():
@@ -2570,20 +2603,98 @@ Usage: Usage: unalias [-a] name [name ...]
self.pystate['quit'] = quit
self.pystate['exit'] = quit
- keepstate = None
+ # Set up readline for Python console
+ if rl_type != RlType.NONE:
+ # Save cmd2 history
+ saved_cmd2_history = []
+ for i in range(1, readline.get_current_history_length() + 1):
+ saved_cmd2_history.append(readline.get_history_item(i))
+
+ readline.clear_history()
+
+ # Restore py's history
+ for item in self.py_history:
+ readline.add_history(item)
+
+ if self.use_rawinput and self.completekey:
+ # Set up tab completion for the Python console
+ # rlcompleter relies on the default settings of the Python readline module
+ if rl_type == RlType.GNU:
+ old_basic_quotes = ctypes.cast(rl_basic_quote_characters, ctypes.c_void_p).value
+ rl_basic_quote_characters.value = orig_rl_basic_quotes
+
+ if 'gnureadline' in sys.modules:
+ # rlcompleter imports readline by name, so it won't use gnureadline
+ # Force rlcompleter to use gnureadline instead so it has our settings and history
+ saved_readline = None
+ if 'readline' in sys.modules:
+ saved_readline = sys.modules['readline']
+
+ sys.modules['readline'] = sys.modules['gnureadline']
+
+ old_delims = readline.get_completer_delims()
+ readline.set_completer_delims(orig_rl_delims)
+
+ # rlcompleter will not need cmd2's custom display function
+ # This will be restored by cmd2 the next time complete() is called
+ if rl_type == RlType.GNU:
+ readline.set_completion_display_matches_hook(None)
+ elif rl_type == RlType.PYREADLINE:
+ readline.rl.mode._display_completions = self._display_matches_pyreadline
+
+ # Save off the current completer and set a new one in the Python console
+ # Make sure it tab completes from its locals() dictionary
+ old_completer = readline.get_completer()
+ interp.runcode("from rlcompleter import Completer")
+ interp.runcode("import readline")
+ interp.runcode("readline.set_completer(Completer(locals()).complete)")
+
+ # Set up sys module for the Python console
+ self._reset_py_display()
+ keepstate = Statekeeper(sys, ('stdin', 'stdout'))
+ sys.stdout = self.stdout
+ sys.stdin = self.stdin
+
+ cprt = 'Type "help", "copyright", "credits" or "license" for more information.'
+ docstr = self.do_py.__doc__.replace('pyscript_name', self.pyscript_name)
+
try:
- cprt = 'Type "help", "copyright", "credits" or "license" for more information.'
- keepstate = Statekeeper(sys, ('stdin', 'stdout'))
- sys.stdout = self.stdout
- sys.stdin = self.stdin
- docstr = self.do_py.__doc__.replace('pyscript_name', self.pyscript_name)
- interp.interact(banner="Python %s on %s\n%s\n(%s)\n%s" %
- (sys.version, sys.platform, cprt, self.__class__.__name__,
- docstr))
+ interp.interact(banner="Python {} on {}\n{}\n({})\n{}".
+ format(sys.version, sys.platform, cprt, self.__class__.__name__, docstr))
except EmbeddedConsoleExit:
pass
- if keepstate is not None:
+
+ finally:
keepstate.restore()
+
+ # Set up readline for cmd2
+ if rl_type != RlType.NONE:
+ # Save py's history
+ self.py_history.clear()
+ for i in range(1, readline.get_current_history_length() + 1):
+ self.py_history.append(readline.get_history_item(i))
+
+ readline.clear_history()
+
+ # Restore cmd2's history
+ for item in saved_cmd2_history:
+ readline.add_history(item)
+
+ if self.use_rawinput and self.completekey:
+ # Restore cmd2's tab completion settings
+ readline.set_completer(old_completer)
+ readline.set_completer_delims(old_delims)
+
+ if rl_type == RlType.GNU:
+ rl_basic_quote_characters.value = old_basic_quotes
+
+ if 'gnureadline' in sys.modules:
+ # Restore what the readline module pointed to
+ if saved_readline is None:
+ del(sys.modules['readline'])
+ else:
+ sys.modules['readline'] = saved_readline
+
except Exception:
pass
finally: