Skip to content

Commit 06787bc

Browse files
committed
Updated the py console to tab complete Python identifiers and have its own history
1 parent 9cbfa4d commit 06787bc

File tree

1 file changed

+161
-18
lines changed

1 file changed

+161
-18
lines changed

cmd2.py

Lines changed: 161 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,38 @@ class RlType(Enum):
154154
# noinspection PyProtectedMember
155155
orig_pyreadline_display = readline.rl.mode._display_completions
156156

157+
############################################################################################################
158+
# pyreadline is incomplete in terms of the Python readline API. Add the missing functions we need.
159+
############################################################################################################
160+
# readline.redisplay()
161+
try:
162+
getattr(readline, 'redisplay')
163+
except AttributeError:
164+
# noinspection PyProtectedMember
165+
readline.redisplay = readline.rl.mode._update_line
166+
167+
# readline.remove_history_item()
168+
try:
169+
getattr(readline, 'remove_history_item')
170+
except AttributeError:
171+
# noinspection PyProtectedMember
172+
def pyreadline_remove_history_item(pos):
173+
"""
174+
An implementation of remove_history_item() for pyreadline
175+
:param pos: The 0-based position in history to remove
176+
"""
177+
# Save of the current location of the history cursor
178+
saved_cursor = readline.rl.mode._history.history_cursor
179+
180+
# Delete the history item
181+
del (readline.rl.mode._history.history[pos])
182+
183+
# Update the cursor if needed
184+
if saved_cursor > pos:
185+
readline.rl.mode._history.history_cursor -= 1
186+
187+
readline.remove_history_item = pyreadline_remove_history_item
188+
157189
elif 'gnureadline' in sys.modules or 'readline' in sys.modules:
158190
# We don't support libedit
159191
if 'libedit' not in readline.__doc__:
@@ -166,11 +198,17 @@ class RlType(Enum):
166198
import ctypes
167199
readline_lib = ctypes.CDLL(readline.__file__)
168200

201+
rl_basic_quote_characters = ctypes.c_char_p.in_dll(readline_lib, "rl_basic_quote_characters")
202+
orig_rl_basic_quotes = ctypes.cast(rl_basic_quote_characters, ctypes.c_void_p).value
203+
169204
if rl_type == RlType.NONE:
170205
rl_warning = "Readline features including tab completion have been disabled since no \n" \
171206
"supported version of readline was found. To resolve this, install \n" \
172207
"pyreadline on Windows or gnureadline on Mac.\n\n"
173208
sys.stderr.write(rl_warning)
209+
else:
210+
# Used by rlcompleter in Python console loaded by py command
211+
orig_rl_delims = readline.get_completer_delims()
174212

175213
# BrokenPipeError and FileNotFoundError exist only in Python 3. Use IOError for Python 2.
176214
if six.PY3:
@@ -1065,6 +1103,7 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, persistent_histor
10651103
self.initial_stdout = sys.stdout
10661104
self.history = History()
10671105
self.pystate = {}
1106+
self.py_history = []
10681107
self.keywords = self.reserved_words + [fname[3:] for fname in dir(self) if fname.startswith('do_')]
10691108
self.parser_manager = ParserManager(redirector=self.redirector, terminators=self.terminators,
10701109
multilineCommands=self.multilineCommands,
@@ -1124,7 +1163,7 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, persistent_histor
11241163

11251164
############################################################################################################
11261165
# The following variables are used by tab-completion functions. They are reset each time complete() is run
1127-
# using set_completion_defaults() and it is up to completer functions to set them before returning results.
1166+
# using reset_completion_defaults() and it is up to completer functions to set them before returning results.
11281167
############################################################################################################
11291168

11301169
# If true and a single match is returned to complete(), then a space will be appended
@@ -1333,7 +1372,7 @@ def get_subcommand_completer(self, command, subcommand):
13331372

13341373
# ----- Methods related to tab completion -----
13351374

1336-
def set_completion_defaults(self):
1375+
def reset_completion_defaults(self):
13371376
"""
13381377
Resets tab completion settings
13391378
Needs to be called each time readline runs tab completion
@@ -1985,7 +2024,7 @@ def complete(self, text, state):
19852024
"""
19862025
if state == 0 and rl_type != RlType.NONE:
19872026
unclosed_quote = ''
1988-
self.set_completion_defaults()
2027+
self.reset_completion_defaults()
19892028

19902029
# lstrip the original line
19912030
orig_line = readline.get_line_buffer()
@@ -2729,12 +2768,10 @@ def _cmdloop(self):
27292768
# Set GNU readline's rl_basic_quote_characters to NULL so it won't automatically add a closing quote
27302769
# We don't need to worry about setting rl_completion_suppress_quote since we never declared
27312770
# rl_completer_quote_characters.
2732-
basic_quote_characters = ctypes.c_char_p.in_dll(readline_lib, "rl_basic_quote_characters")
2733-
old_basic_quote_characters = ctypes.cast(basic_quote_characters, ctypes.c_void_p).value
2734-
basic_quote_characters.value = None
2771+
old_basic_quotes = ctypes.cast(rl_basic_quote_characters, ctypes.c_void_p).value
2772+
rl_basic_quote_characters.value = None
27352773

27362774
old_completer = readline.get_completer()
2737-
old_delims = readline.get_completer_delims()
27382775
readline.set_completer(self.complete)
27392776

27402777
# Break words on whitespace and quotes when tab completing
@@ -2744,6 +2781,7 @@ def _cmdloop(self):
27442781
# If redirection is allowed, then break words on those characters too
27452782
completer_delims += ''.join(REDIRECTION_CHARS)
27462783

2784+
old_delims = readline.get_completer_delims()
27472785
readline.set_completer_delims(completer_delims)
27482786

27492787
# Enable tab completion
@@ -2780,7 +2818,7 @@ def _cmdloop(self):
27802818

27812819
if rl_type == RlType.GNU:
27822820
readline.set_completion_display_matches_hook(None)
2783-
basic_quote_characters.value = old_basic_quote_characters
2821+
rl_basic_quote_characters.value = old_basic_quotes
27842822
elif rl_type == RlType.PYREADLINE:
27852823
readline.rl.mode._display_completions = orig_pyreadline_display
27862824

@@ -3298,7 +3336,30 @@ def cmd_with_subs_completer(self, text, line, begidx, endidx):
32983336

32993337
return matches
33003338

3301-
# noinspection PyBroadException
3339+
@staticmethod
3340+
def _reset_py_display():
3341+
"""
3342+
Resets the dynamic objects in the sys module that the py and ipy consoles fight over.
3343+
When a Python console starts it adopts certain display settings if they've already been set.
3344+
If an ipy console has previously been run, then py uses its settings and ends up looking
3345+
like an ipy console in terms of prompt and exception text. This method forces the Python
3346+
console to create its own display settings since they won't exist.
3347+
3348+
IPython does not have this problem since it always overwrites the display settings when it
3349+
is run. Therefore this method only needs to be called before creating a Python console.
3350+
"""
3351+
# Delete any prompts that have been set
3352+
attributes = ['ps1', 'ps2', 'ps3']
3353+
for cur_attr in attributes:
3354+
try:
3355+
del sys.__dict__[cur_attr]
3356+
except KeyError:
3357+
pass
3358+
3359+
# Reset functions
3360+
sys.displayhook = sys.__displayhook__
3361+
sys.excepthook = sys.__excepthook__
3362+
33023363
def do_py(self, arg):
33033364
"""
33043365
Invoke python command, shell, or script
@@ -3314,6 +3375,7 @@ def do_py(self, arg):
33143375
return
33153376
self._in_py = True
33163377

3378+
# noinspection PyBroadException
33173379
try:
33183380
self.pystate['self'] = self
33193381
arg = arg.strip()
@@ -3347,6 +3409,8 @@ def onecmd_plus_hooks(cmd_plus_args):
33473409

33483410
if arg:
33493411
interp.runcode(arg)
3412+
3413+
# If there are no args, then we will open an interactive Python console
33503414
else:
33513415
# noinspection PyShadowingBuiltins
33523416
def quit():
@@ -3356,19 +3420,98 @@ def quit():
33563420
self.pystate['quit'] = quit
33573421
self.pystate['exit'] = quit
33583422

3359-
keepstate = None
3423+
# Set up readline for Python console
3424+
if rl_type != RlType.NONE:
3425+
# Save cmd2 history
3426+
saved_cmd2_history = []
3427+
for i in range(1, readline.get_current_history_length() + 1):
3428+
saved_cmd2_history.append(readline.get_history_item(i))
3429+
3430+
readline.clear_history()
3431+
3432+
# Restore py's history
3433+
for item in self.py_history:
3434+
readline.add_history(item)
3435+
3436+
if self.use_rawinput and self.completekey:
3437+
# Set up tab completion for the Python console
3438+
# rlcompleter relies on the default settings of the Python readline module
3439+
if rl_type == RlType.GNU:
3440+
old_basic_quotes = ctypes.cast(rl_basic_quote_characters, ctypes.c_void_p).value
3441+
rl_basic_quote_characters.value = orig_rl_basic_quotes
3442+
3443+
if 'gnureadline' in sys.modules:
3444+
# rlcompleter imports readline by name, so it won't use gnureadline
3445+
# Force rlcompleter to use gnureadline instead so it has our settings and history
3446+
saved_readline = None
3447+
if 'readline' in sys.modules:
3448+
saved_readline = sys.modules['readline']
3449+
3450+
sys.modules['readline'] = sys.modules['gnureadline']
3451+
3452+
old_delims = readline.get_completer_delims()
3453+
readline.set_completer_delims(orig_rl_delims)
3454+
3455+
# rlcompleter will not need cmd2's custom display function
3456+
# This will be restored by cmd2 the next time complete() is called
3457+
if rl_type == RlType.GNU:
3458+
readline.set_completion_display_matches_hook(None)
3459+
elif rl_type == RlType.PYREADLINE:
3460+
readline.rl.mode._display_completions = self._display_matches_pyreadline
3461+
3462+
# Save off the current completer and set a new one in the Python console
3463+
# Make sure it tab completes from its locals() dictionary
3464+
old_completer = readline.get_completer()
3465+
interp.runcode("from rlcompleter import Completer")
3466+
interp.runcode("import readline")
3467+
interp.runcode("readline.set_completer(Completer(locals()).complete)")
3468+
3469+
# Set up sys module for the Python console
3470+
self._reset_py_display()
3471+
keepstate = Statekeeper(sys, ('stdin', 'stdout'))
3472+
sys.stdout = self.stdout
3473+
sys.stdin = self.stdin
3474+
3475+
cprt = 'Type "help", "copyright", "credits" or "license" for more information.'
3476+
33603477
try:
3361-
cprt = 'Type "help", "copyright", "credits" or "license" for more information.'
3362-
keepstate = Statekeeper(sys, ('stdin', 'stdout'))
3363-
sys.stdout = self.stdout
3364-
sys.stdin = self.stdin
3365-
interp.interact(banner="Python %s on %s\n%s\n(%s)\n%s" %
3366-
(sys.version, sys.platform, cprt, self.__class__.__name__,
3367-
self.do_py.__doc__))
3478+
interp.interact(banner="Python {} on {}\n{}\n({})\n{}".format(sys.version, sys.platform,
3479+
cprt, self.__class__.__name__,
3480+
self.do_py.__doc__))
33683481
except EmbeddedConsoleExit:
33693482
pass
3370-
if keepstate is not None:
3483+
3484+
finally:
33713485
keepstate.restore()
3486+
3487+
# Set up readline for cmd2
3488+
if rl_type != RlType.NONE:
3489+
# Save py's history
3490+
del self.py_history[:]
3491+
for i in range(1, readline.get_current_history_length() + 1):
3492+
self.py_history.append(readline.get_history_item(i))
3493+
3494+
readline.clear_history()
3495+
3496+
# Restore cmd2's history
3497+
for item in saved_cmd2_history:
3498+
readline.add_history(item)
3499+
3500+
if self.use_rawinput and self.completekey:
3501+
# Restore cmd2's tab completion settings
3502+
readline.set_completer(old_completer)
3503+
readline.set_completer_delims(old_delims)
3504+
3505+
if rl_type == RlType.GNU:
3506+
rl_basic_quote_characters.value = old_basic_quotes
3507+
3508+
if 'gnureadline' in sys.modules:
3509+
# Restore what the readline module pointed to
3510+
if saved_readline is None:
3511+
del (sys.modules['readline'])
3512+
else:
3513+
sys.modules['readline'] = saved_readline
3514+
33723515
except Exception:
33733516
pass
33743517
finally:

0 commit comments

Comments
 (0)