@@ -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+
157189elif '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+
169204if 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.
176214if 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