summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin Van Brunt <kmvanbrunt@gmail.com>2019-07-18 14:48:25 -0400
committerKevin Van Brunt <kmvanbrunt@gmail.com>2019-07-18 14:48:25 -0400
commit28acfbff590749c3356db528036877f16ffec4cf (patch)
tree1b27bed107731195001528a078382d5d285b4f65
parent462cdbe59dc8410b68e413cb6b5902623f16c805 (diff)
downloadcmd2-git-28acfbff590749c3356db528036877f16ffec4cf.tar.gz
Added sigint protection while entering/leaving cmd2 and Python interactive shells.
-rw-r--r--cmd2/cmd2.py299
1 files changed, 182 insertions, 117 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 1f07f2cb..b13af6ea 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -38,6 +38,7 @@ import pickle
import re
import sys
import threading
+from code import InteractiveConsole
from collections import namedtuple
from contextlib import redirect_stdout
from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Tuple, Type, Union
@@ -280,6 +281,24 @@ def with_argparser(argparser: argparse.ArgumentParser, *,
return arg_decorator
+class _SavedReadlineSettings:
+ """readline settings that are backed up when switching between readline environments"""
+ def __init__(self):
+ self.completer = None
+ self.delims = ''
+ self.basic_quotes = None
+
+
+class _SavedCmd2Env:
+ """cmd2 environment settings that are backed up when entering an interactive Python shell"""
+ def __init__(self):
+ self.readline_settings = _SavedReadlineSettings()
+ self.readline_module = None
+ self.history = []
+ self.sys_stdout = None
+ self.sys_stdin = None
+
+
class EmbeddedConsoleExit(SystemExit):
"""Custom exception class for use with the py command."""
pass
@@ -2218,15 +2237,13 @@ class Cmd(cmd.Cmd):
return line.rstrip('\r\n')
- def _cmdloop(self) -> None:
- """Repeatedly issue a prompt, accept input, parse an initial prefix
- off the received input, and dispatch to action methods, passing them
- the remainder of the line as argument.
-
- This serves the same role as cmd.cmdloop().
+ def _set_up_cmd2_readline(self) -> _SavedReadlineSettings:
"""
- # An almost perfect copy from Cmd; however, the pseudo_raw_input portion
- # has been split out so that it can be called separately
+ Set up readline with cmd2-specific settings
+ :return: Class containing saved readline settings
+ """
+ readline_settings = _SavedReadlineSettings()
+
if self.use_rawinput and self.completekey and rl_type != RlType.NONE:
# Set up readline for our tab completion needs
@@ -2234,10 +2251,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.
- saved_basic_quotes = ctypes.cast(rl_basic_quote_characters, ctypes.c_void_p).value
+ readline_settings.basic_quotes = ctypes.cast(rl_basic_quote_characters, ctypes.c_void_p).value
rl_basic_quote_characters.value = None
- saved_completer = readline.get_completer()
+ readline_settings.completer = readline.get_completer()
readline.set_completer(self.complete)
# Break words on whitespace and quotes when tab completing
@@ -2247,13 +2264,46 @@ class Cmd(cmd.Cmd):
# If redirection is allowed, then break words on those characters too
completer_delims += ''.join(constants.REDIRECTION_CHARS)
- saved_delims = readline.get_completer_delims()
+ readline_settings.delims = readline.get_completer_delims()
readline.set_completer_delims(completer_delims)
# Enable tab completion
readline.parse_and_bind(self.completekey + ": complete")
+ return readline_settings
+
+ def _restore_readline(self, readline_settings: _SavedReadlineSettings):
+ """
+ Restore readline settings
+ :param readline_settings: the readline settings to restore
+ """
+ if self.use_rawinput and self.completekey and rl_type != RlType.NONE:
+
+ # Restore what we changed in readline
+ readline.set_completer(readline_settings.completer)
+ readline.set_completer_delims(readline_settings.delims)
+
+ if rl_type == RlType.GNU:
+ readline.set_completion_display_matches_hook(None)
+ rl_basic_quote_characters.value = readline_settings.basic_quotes
+ elif rl_type == RlType.PYREADLINE:
+ # noinspection PyUnresolvedReferences
+ readline.rl.mode._display_completions = orig_pyreadline_display
+
+ def _cmdloop(self) -> None:
+ """Repeatedly issue a prompt, accept input, parse an initial prefix
+ off the received input, and dispatch to action methods, passing them
+ the remainder of the line as argument.
+
+ This serves the same role as cmd.cmdloop().
+ """
+ readline_settings = None
+
try:
+ # Get sigint protection while we set up readline for cmd2
+ with self.sigint_protection:
+ readline_settings = self._set_up_cmd2_readline()
+
# Run startup commands
stop = self.runcmds_plus_hooks(self._startup_commands)
self._startup_commands.clear()
@@ -2272,18 +2322,10 @@ class Cmd(cmd.Cmd):
# Run the command along with all associated pre and post hooks
stop = self.onecmd_plus_hooks(line)
finally:
- if self.use_rawinput and self.completekey and rl_type != RlType.NONE:
-
- # Restore what we changed in readline
- readline.set_completer(saved_completer)
- readline.set_completer_delims(saved_delims)
-
- if rl_type == RlType.GNU:
- readline.set_completion_display_matches_hook(None)
- rl_basic_quote_characters.value = saved_basic_quotes
- elif rl_type == RlType.PYREADLINE:
- # noinspection PyUnresolvedReferences
- readline.rl.mode._display_completions = orig_pyreadline_display
+ # Get sigint protection while we restore readline settings
+ with self.sigint_protection:
+ if readline_settings is not None:
+ self._restore_readline(readline_settings)
# ----- Alias sub-command functions -----
@@ -2890,10 +2932,10 @@ class Cmd(cmd.Cmd):
response, len(fulloptions)))
return result
- def _cmdenvironment(self) -> str:
- """Get a summary report of read-only settings which the user cannot modify at runtime.
+ def _get_read_only_settings(self) -> str:
+ """Return a summary report of read-only settings which the user cannot modify at runtime.
- :return: summary report of read-only settings which the user cannot modify at runtime
+ :return: The report string
"""
read_only_settings = """
Commands may be terminated with: {}
@@ -2924,7 +2966,7 @@ class Cmd(cmd.Cmd):
# If user has requested to see all settings, also show read-only settings
if args.all:
- self.poutput('\nRead only settings:{}'.format(self._cmdenvironment()))
+ self.poutput('\nRead only settings:{}'.format(self._get_read_only_settings()))
else:
self.perror("Parameter '{}' not supported (type 'set' for list of parameters).".format(param))
@@ -3032,6 +3074,108 @@ class Cmd(cmd.Cmd):
sys.displayhook = sys.__displayhook__
sys.excepthook = sys.__excepthook__
+ def _set_up_py_shell_env(self, interp: InteractiveConsole) -> _SavedCmd2Env:
+ """
+ Set up interactive Python shell environment
+ :return: Class containing saved up cmd2 environment
+ """
+ cmd2_env = _SavedCmd2Env()
+
+ # Set up readline for Python shell
+ if rl_type != RlType.NONE:
+ # Save cmd2 history
+ for i in range(1, readline.get_current_history_length() + 1):
+ # noinspection PyArgumentList
+ cmd2_env.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:
+ cmd2_env.readline_settings.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
+ if 'readline' in sys.modules:
+ cmd2_env.readline_module = sys.modules['readline']
+
+ sys.modules['readline'] = sys.modules['gnureadline']
+
+ cmd2_env.readline_settings.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:
+ # noinspection PyUnresolvedReferences
+ readline.rl.mode._display_completions = orig_pyreadline_display
+
+ # Save off the current completer and set a new one in the Python console
+ # Make sure it tab completes from its locals() dictionary
+ cmd2_env.readline_settings.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()
+
+ cmd2_env.sys_stdout = sys.stdout
+ sys.stdout = self.stdout
+
+ cmd2_env.sys_stdin = sys.stdin
+ sys.stdin = self.stdin
+
+ return cmd2_env
+
+ def _restore_cmd2_env(self, cmd2_env: _SavedCmd2Env) -> None:
+ """
+ Restore cmd2 environment after exiting an interactive Python shell
+ :param cmd2_env: the environment settings to restore
+ """
+ sys.stdout = cmd2_env.sys_stdout
+ sys.stdin = cmd2_env.sys_stdin
+
+ # 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):
+ # noinspection PyArgumentList
+ self._py_history.append(readline.get_history_item(i))
+
+ readline.clear_history()
+
+ # Restore cmd2's history
+ for item in cmd2_env.history:
+ readline.add_history(item)
+
+ if self.use_rawinput and self.completekey:
+ # Restore cmd2's tab completion settings
+ readline.set_completer(cmd2_env.readline_settings.completer)
+ readline.set_completer_delims(cmd2_env.readline_settings.delims)
+
+ if rl_type == RlType.GNU:
+ rl_basic_quote_characters.value = cmd2_env.readline_settings.basic_quotes
+
+ if 'gnureadline' in sys.modules:
+ # Restore what the readline module pointed to
+ if cmd2_env.readline_module is None:
+ del(sys.modules['readline'])
+ else:
+ sys.modules['readline'] = cmd2_env.readline_module
+
py_description = ("Invoke Python command or shell\n"
"\n"
"Note that, when invoking a command directly from the command line, this shell\n"
@@ -3092,7 +3236,6 @@ class Cmd(cmd.Cmd):
del self._pystate['self']
localvars = self._pystate
- from code import InteractiveConsole
interp = InteractiveConsole(locals=localvars)
interp.runcode('import sys, os;sys.path.insert(0, os.getcwd())')
@@ -3113,73 +3256,22 @@ class Cmd(cmd.Cmd):
# We don't care about any exception that happened in the interactive console
pass
- # If there are no args, then we will open an interactive Python console
+ # If there are no args, then we will open an interactive Python shell
else:
- # 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):
- # noinspection PyArgumentList
- 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:
- saved_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']
-
- saved_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:
- # noinspection PyUnresolvedReferences
- readline.rl.mode._display_completions = orig_pyreadline_display
-
- # Save off the current completer and set a new one in the Python console
- # Make sure it tab completes from its locals() dictionary
- saved_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()
-
- saved_sys_stdout = sys.stdout
- sys.stdout = self.stdout
-
- saved_sys_stdin = sys.stdin
- sys.stdin = self.stdin
-
cprt = 'Type "help", "copyright", "credits" or "license" for more information.'
instructions = ('End with `Ctrl-D` (Unix) / `Ctrl-Z` (Windows), `quit()`, `exit()`.\n'
'Non-Python commands can be issued with: {}("your command")\n'
'Run Python code from external script files with: run("script.py")'
.format(self.pyscript_name))
+ cmd2_env = None
+
# noinspection PyBroadException
try:
+ # Get sigint protection while we set up the Python shell environment
+ with self.sigint_protection:
+ cmd2_env = self._set_up_py_shell_env(interp)
+
interp.interact(banner="Python {} on {}\n{}\n\n{}\n".
format(sys.version, sys.platform, cprt, instructions))
except BaseException:
@@ -3187,37 +3279,10 @@ class Cmd(cmd.Cmd):
pass
finally:
- sys.stdout = saved_sys_stdout
- sys.stdin = saved_sys_stdin
-
- # 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):
- # noinspection PyArgumentList
- 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(saved_completer)
- readline.set_completer_delims(saved_delims)
-
- if rl_type == RlType.GNU:
- rl_basic_quote_characters.value = saved_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
+ # Get sigint protection while we restore cmd2 environment settings
+ with self.sigint_protection:
+ if cmd2_env is not None:
+ self._restore_cmd2_env(cmd2_env)
except KeyboardInterrupt:
pass