summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTodd Leonhardt <todd.leonhardt@gmail.com>2019-03-18 22:49:42 -0400
committerGitHub <noreply@github.com>2019-03-18 22:49:42 -0400
commit2f24a8ad3eeb2fdf699d1e2a9d4f05429fe879c4 (patch)
treeb836270cf61175d1e4a434fd8cae08f0ffd998a2
parent96d176cc3d8198913693a42c7dd983cf69a165bd (diff)
parent57dd827963491439e40eb5dfe20811c14ea757ff (diff)
downloadcmd2-git-2f24a8ad3eeb2fdf699d1e2a9d4f05429fe879c4.tar.gz
Merge branch 'master' into load_generate_transcript
-rw-r--r--CHANGELOG.md8
-rwxr-xr-xREADME.md14
-rw-r--r--cmd2/clipboard.py2
-rw-r--r--cmd2/cmd2.py138
-rw-r--r--cmd2/history.py4
-rw-r--r--cmd2/parsing.py43
-rw-r--r--cmd2/pyscript_bridge.py1
-rw-r--r--cmd2/rl_utils.py15
-rw-r--r--cmd2/utils.py16
-rw-r--r--docs/settingchanges.rst10
-rw-r--r--docs/unfreefeatures.rst5
-rwxr-xr-xexamples/arg_print.py9
-rwxr-xr-xexamples/async_printing.py1
-rwxr-xr-xexamples/cmd_as_argument.py13
-rwxr-xr-xexamples/colors.py11
-rwxr-xr-xexamples/decorator_example.py10
-rwxr-xr-xexamples/example.py11
-rwxr-xr-xexamples/hooks.py2
-rwxr-xr-xexamples/pirate.py14
-rwxr-xr-xexamples/plumbum_colors.py11
-rw-r--r--tests/test_cmd2.py9
-rw-r--r--tests/test_completion.py4
-rw-r--r--tests/test_transcript.py3
23 files changed, 190 insertions, 164 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 01b30027..694d2786 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,4 @@
-## 0.9.12 (TBD, 2019)
+## 0.9.12 (March TBD, 2019)
* Enhancements
* Added ability to include command name placeholders in the message printed when trying to run a disabled command.
* See docstring for ``disable_command()`` or ``disable_category()`` for more details.
@@ -15,7 +15,11 @@
* ``do_help()`` - when no help information can be found
* ``default()`` - in all cases since this is called when an invalid command name is run
* ``_report_disabled_command_usage()`` - in all cases since this is called when a disabled command is run
- * Removed *** from beginning of error messages printed by `do_help()` and `default()`.
+ * Removed *** from beginning of error messages printed by `do_help()` and `default()`
+ * Significantly refactored ``cmd.Cmd`` class so that all class attributes got converted to instance attributes, also:
+ * Added ``allow_redirection``, ``terminators``, ``multiline_commands``, and ``shortcuts`` as optional arguments
+ to ``cmd.Cmd.__init__()`
+ * A few instance attributes were moved inside ``StatementParser`` and properties were created for accessing them
## 0.9.11 (March 13, 2019)
* Bug Fixes
diff --git a/README.md b/README.md
index 6b33ab84..1cc045f2 100755
--- a/README.md
+++ b/README.md
@@ -185,9 +185,9 @@ Instructions for implementing each feature follow.
- Multi-line commands
- Any command accepts multi-line input when its name is listed in `Cmd.multiline_commands`.
- The program will keep expecting input until a line ends with any of the characters
- in `Cmd.terminators` . The default terminators are `;` and `/n` (empty newline).
+ Any command accepts multi-line input when its name is listed the `multiline_commands` optional argument to
+ `cmd2.Cmd.__init`. The program will keep expecting input until a line ends with any of the characters listed in the
+ `terminators` optional argument to `cmd2.Cmd.__init__()` . The default terminators are `;` and `/n` (empty newline).
- Special-character shortcut commands (beyond cmd's "@" and "!")
@@ -239,14 +239,12 @@ class CmdLineApp(cmd2.Cmd):
MUMBLE_LAST = ['right?']
def __init__(self):
- self.multiline_commands = ['orate']
self.maxrepeats = 3
-
- # Add stuff to shortcuts before calling base class initializer
- self.shortcuts.update({'&': 'speak'})
+ shortcuts = dict(self.DEFAULT_SHORTCUTS)
+ shortcuts.update({'&': 'speak'})
# Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
- super().__init__(use_ipython=False)
+ super().__init__(use_ipython=False, multiline_commands=['orate'], shortcuts=shortcuts)
# Make maxrepeats settable at runtime
self.settable['maxrepeats'] = 'max repetitions for speak command'
diff --git a/cmd2/clipboard.py b/cmd2/clipboard.py
index e0d1fc03..b2331649 100644
--- a/cmd2/clipboard.py
+++ b/cmd2/clipboard.py
@@ -10,7 +10,7 @@ import pyperclip
try:
from pyperclip.exceptions import PyperclipException
except ImportError: # pragma: no cover
- # noinspection PyUnresolvedReferences
+ # noinspection PyUnresolvedReferences,PyProtectedMember
from pyperclip import PyperclipException
# Can we access the clipboard? Should always be true on Windows and Mac, but only sometimes on Linux
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 893a63dd..f46ce496 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -68,7 +68,7 @@ else:
if rl_type == RlType.PYREADLINE:
# Save the original pyreadline display completion function since we need to override it and restore it
- # noinspection PyProtectedMember
+ # noinspection PyProtectedMember,PyUnresolvedReferences
orig_pyreadline_display = readline.rl.mode._display_completions
elif rl_type == RlType.GNU:
@@ -104,6 +104,7 @@ except ImportError:
# Python 3.4 require contextlib2 for temporarily redirecting stderr and stdout
if sys.version_info < (3, 5):
+ # noinspection PyUnresolvedReferences
from contextlib2 import redirect_stdout
else:
from contextlib import redirect_stdout
@@ -310,52 +311,14 @@ class Cmd(cmd.Cmd):
Line-oriented command interpreters are often useful for test harnesses, internal tools, and rapid prototypes.
"""
- # Attributes used to configure the StatementParser, best not to change these at runtime
- multiline_commands = []
- shortcuts = {'?': 'help', '!': 'shell', '@': 'load', '@@': '_relative_load'}
- terminators = [constants.MULTILINE_TERMINATOR]
-
- # Attributes which are NOT dynamically settable at runtime
- allow_cli_args = True # Should arguments passed on the command-line be processed as commands?
- allow_redirection = True # Should output redirection and pipes be allowed
- default_to_shell = False # Attempt to run unrecognized commands as shell commands
- quit_on_sigint = False # Quit the loop on interrupt instead of just resetting prompt
- reserved_words = []
-
- # Attributes which ARE dynamically settable at runtime
- colors = constants.COLORS_TERMINAL
- continuation_prompt = '> '
- debug = False
- echo = False
- editor = os.environ.get('EDITOR')
- if not editor:
- if sys.platform[:3] == 'win':
- editor = 'notepad'
- else:
- # Favor command-line editors first so we don't leave the terminal to edit
- for editor in ['vim', 'vi', 'emacs', 'nano', 'pico', 'gedit', 'kate', 'subl', 'geany', 'atom']:
- if utils.which(editor):
- break
- feedback_to_output = False # Do not include nonessentials in >, | output by default (things like timing)
- locals_in_py = False
- quiet = False # Do not suppress nonessential output
- timing = False # Prints elapsed time for each command
-
- # To make an attribute settable with the "do_set" command, add it to this ...
- settable = {'colors': 'Allow colorized output (valid values: Terminal, Always, Never)',
- 'continuation_prompt': 'On 2nd+ line of input',
- 'debug': 'Show full error stack on error',
- 'echo': 'Echo command issued into output',
- 'editor': 'Program used by ``edit``',
- 'feedback_to_output': 'Include nonessentials in `|`, `>` results',
- 'locals_in_py': 'Allow access to your application in py via self',
- 'prompt': 'The prompt issued to solicit input',
- 'quiet': "Don't print nonessential feedback",
- 'timing': 'Report execution times'}
+ DEFAULT_SHORTCUTS = {'?': 'help', '!': 'shell', '@': 'load', '@@': '_relative_load'}
+ DEFAULT_EDITOR = utils.find_editor()
def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, persistent_history_file: str = '',
persistent_history_length: int = 1000, startup_script: Optional[str] = None, use_ipython: bool = False,
- transcript_files: Optional[List[str]] = None) -> None:
+ transcript_files: Optional[List[str]] = None, allow_redirection: bool = True,
+ multiline_commands: Optional[List[str]] = None, terminators: Optional[List[str]] = None,
+ shortcuts: Optional[Dict[str, str]] = None) -> None:
"""An easy but powerful framework for writing line-oriented command interpreters, extends Python's cmd package.
:param completekey: (optional) readline name of a completion key, default to Tab
@@ -366,6 +329,9 @@ class Cmd(cmd.Cmd):
:param startup_script: (optional) file path to a a script to load and execute at startup
:param use_ipython: (optional) should the "ipy" command be included for an embedded IPython shell
:param transcript_files: (optional) allows running transcript tests when allow_cli_args is False
+ :param allow_redirection: (optional) should output redirection and pipes be allowed
+ :param multiline_commands: (optional) list of commands allowed to accept multi-line input
+ :param shortcuts: (optional) dictionary containing shortcuts for commands
"""
# If use_ipython is False, make sure the do_ipy() method doesn't exit
if not use_ipython:
@@ -384,6 +350,34 @@ class Cmd(cmd.Cmd):
# Call super class constructor
super().__init__(completekey=completekey, stdin=stdin, stdout=stdout)
+ # Attributes which should NOT be dynamically settable at runtime
+ self.allow_cli_args = True # Should arguments passed on the command-line be processed as commands?
+ self.default_to_shell = False # Attempt to run unrecognized commands as shell commands
+ self.quit_on_sigint = False # Quit the loop on interrupt instead of just resetting prompt
+
+ # Attributes which ARE dynamically settable at runtime
+ self.colors = constants.COLORS_TERMINAL
+ self.continuation_prompt = '> '
+ self.debug = False
+ self.echo = False
+ self.editor = self.DEFAULT_EDITOR
+ self.feedback_to_output = False # Do not include nonessentials in >, | output by default (things like timing)
+ self.locals_in_py = False
+ self.quiet = False # Do not suppress nonessential output
+ self.timing = False # Prints elapsed time for each command
+
+ # To make an attribute settable with the "do_set" command, add it to this ...
+ self.settable = {'colors': 'Allow colorized output (valid values: Terminal, Always, Never)',
+ 'continuation_prompt': 'On 2nd+ line of input',
+ 'debug': 'Show full error stack on error',
+ 'echo': 'Echo command issued into output',
+ 'editor': 'Program used by ``edit``',
+ 'feedback_to_output': 'Include nonessentials in `|`, `>` results',
+ 'locals_in_py': 'Allow access to your application in py via self',
+ 'prompt': 'The prompt issued to solicit input',
+ 'quiet': "Don't print nonessential feedback",
+ 'timing': 'Report execution times'}
+
# Commands to exclude from the help menu and tab completion
self.hidden_commands = ['eof', 'eos', '_relative_load']
@@ -391,24 +385,21 @@ class Cmd(cmd.Cmd):
self.exclude_from_history = '''history edit eof eos'''.split()
# Command aliases and macros
- self.aliases = dict()
self.macros = dict()
- self._finalize_app_parameters()
-
self.initial_stdout = sys.stdout
self.history = History()
self.pystate = {}
self.py_history = []
self.pyscript_name = 'app'
- self.keywords = self.reserved_words + self.get_all_commands()
- self.statement_parser = StatementParser(
- allow_redirection=self.allow_redirection,
- terminators=self.terminators,
- multiline_commands=self.multiline_commands,
- aliases=self.aliases,
- shortcuts=self.shortcuts,
- )
+
+ if shortcuts is None:
+ shortcuts = self.DEFAULT_SHORTCUTS
+ shortcuts = sorted(shortcuts.items(), reverse=True)
+ self.statement_parser = StatementParser(allow_redirection=allow_redirection,
+ terminators=terminators,
+ multiline_commands=multiline_commands,
+ shortcuts=shortcuts)
self._transcript_files = transcript_files
# Used to enable the ability for a Python script to quit the application
@@ -568,10 +559,25 @@ class Cmd(cmd.Cmd):
"""
return utils.strip_ansi(self.prompt)
- def _finalize_app_parameters(self) -> None:
- """Finalize the shortcuts"""
- # noinspection PyUnresolvedReferences
- self.shortcuts = sorted(self.shortcuts.items(), reverse=True)
+ @property
+ def aliases(self) -> Dict[str, str]:
+ """Read-only property to access the aliases stored in the StatementParser."""
+ return self.statement_parser.aliases
+
+ @property
+ def shortcuts(self) -> Tuple[Tuple[str, str]]:
+ """Read-only property to access the shortcuts stored in the StatementParser."""
+ return self.statement_parser.shortcuts
+
+ @property
+ def allow_redirection(self) -> bool:
+ """Getter for the allow_redirection property that determines whether or not redirection of stdout is allowed."""
+ return self.statement_parser.allow_redirection
+
+ @allow_redirection.setter
+ def allow_redirection(self, value: bool) -> None:
+ """Setter for the allow_redirection property that determines whether or not redirection of stdout is allowed."""
+ self.statement_parser.allow_redirection = value
def decolorized_write(self, fileobj: IO, msg: str) -> None:
"""Write a string to a fileobject, stripping ANSI escape sequences if necessary
@@ -728,6 +734,7 @@ class Cmd(cmd.Cmd):
if rl_type == RlType.GNU:
readline.set_completion_display_matches_hook(self._display_matches_gnu_readline)
elif rl_type == RlType.PYREADLINE:
+ # noinspection PyUnresolvedReferences
readline.rl.mode._display_completions = self._display_matches_pyreadline
def tokens_for_completion(self, line: str, begidx: int, endidx: int) -> Tuple[List[str], List[str]]:
@@ -1355,6 +1362,7 @@ class Cmd(cmd.Cmd):
# Print the header if one exists
if self.completion_header:
+ # noinspection PyUnresolvedReferences
readline.rl.mode.console.write('\n' + self.completion_header)
# Display matches using actual display function. This also redraws the prompt and line.
@@ -2203,6 +2211,7 @@ class Cmd(cmd.Cmd):
readline.set_completion_display_matches_hook(None)
rl_basic_quote_characters.value = old_basic_quotes
elif rl_type == RlType.PYREADLINE:
+ # noinspection PyUnresolvedReferences
readline.rl.mode._display_completions = orig_pyreadline_display
self.cmdqueue.clear()
@@ -2822,7 +2831,8 @@ class Cmd(cmd.Cmd):
Commands may be terminated with: {}
Arguments at invocation allowed: {}
Output redirection and pipes allowed: {}"""
- return read_only_settings.format(str(self.terminators), self.allow_cli_args, self.allow_redirection)
+ return read_only_settings.format(str(self.statement_parser.terminators), self.allow_cli_args,
+ self.allow_redirection)
def show(self, args: argparse.Namespace, parameter: str = '') -> None:
"""Shows current settings of parameters.
@@ -3047,6 +3057,7 @@ class Cmd(cmd.Cmd):
# 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()
@@ -3079,6 +3090,7 @@ class Cmd(cmd.Cmd):
if rl_type == RlType.GNU:
readline.set_completion_display_matches_hook(None)
elif rl_type == RlType.PYREADLINE:
+ # noinspection PyUnresolvedReferences
readline.rl.mode._display_completions = self._display_matches_pyreadline
# Save off the current completer and set a new one in the Python console
@@ -3116,6 +3128,7 @@ class Cmd(cmd.Cmd):
# 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()
@@ -3193,10 +3206,12 @@ class Cmd(cmd.Cmd):
exit_msg = 'Leaving IPython, back to {}'.format(sys.argv[0])
if self.locals_in_py:
- def load_ipy(self, app):
+ # noinspection PyUnusedLocal
+ def load_ipy(cmd2_instance, app):
embed(banner1=banner, exit_msg=exit_msg)
load_ipy(self, bridge)
else:
+ # noinspection PyUnusedLocal
def load_ipy(app):
embed(banner1=banner, exit_msg=exit_msg)
load_ipy(bridge)
@@ -3614,6 +3629,7 @@ class Cmd(cmd.Cmd):
if rl_type == RlType.GNU:
sys.stderr.write(terminal_str)
elif rl_type == RlType.PYREADLINE:
+ # noinspection PyUnresolvedReferences
readline.rl.mode.console.write(terminal_str)
# Redraw the prompt and input lines
diff --git a/cmd2/history.py b/cmd2/history.py
index 729cc6e3..7cc36bfc 100644
--- a/cmd2/history.py
+++ b/cmd2/history.py
@@ -128,12 +128,12 @@ class History(list):
# \s*$ match any whitespace at the end of the input. This is here so
# you don't have to trim the input
#
- spanpattern = re.compile(r'^\s*(?P<start>-?[1-9]{1}\d*)?(?P<separator>:|(\.{2,}))?(?P<end>-?[1-9]{1}\d*)?\s*$')
+ spanpattern = re.compile(r'^\s*(?P<start>-?[1-9]\d*)?(?P<separator>:|(\.{2,}))?(?P<end>-?[1-9]\d*)?\s*$')
def span(self, span: str) -> List[HistoryItem]:
"""Return an index or slice of the History list,
- :param raw: string containing an index or a slice
+ :param span: string containing an index or a slice
:return: a list of HistoryItems
This method can accommodate input in any of these forms:
diff --git a/cmd2/parsing.py b/cmd2/parsing.py
index 514f5faf..2dc698b0 100644
--- a/cmd2/parsing.py
+++ b/cmd2/parsing.py
@@ -5,7 +5,7 @@
import os
import re
import shlex
-from typing import Dict, List, Tuple, Union
+from typing import Dict, Iterable, List, Optional, Tuple, Union
import attr
@@ -242,31 +242,42 @@ class StatementParser:
Shortcuts is a list of tuples with each tuple containing the shortcut and
the expansion.
"""
- def __init__(
- self,
- allow_redirection: bool = True,
- terminators: List[str] = None,
- multiline_commands: List[str] = None,
- aliases: Dict[str, str] = None,
- shortcuts: List[Tuple[str, str]] = None,
- ):
+ def __init__(self,
+ allow_redirection: bool = True,
+ terminators: Optional[Iterable[str]] = None,
+ multiline_commands: Optional[Iterable[str]] = None,
+ aliases: Optional[Dict[str, str]] = None,
+ shortcuts: Optional[Iterable[Tuple[str, str]]] = None) -> None:
+ """Initialize an instance of StatementParser.
+
+ The following will get converted to an immutable tuple before storing internally:
+ * terminators
+ * multiline commands
+ * shortcuts
+
+ :param allow_redirection: (optional) should redirection and pipes be allowed?
+ :param terminators: (optional) iterable containing strings which should terminate multiline commands
+ :param multiline_commands: (optional) iterable containing the names of commands that accept multiline input
+ :param aliases: (optional) dictionary contaiing aliases
+ :param shortcuts (optional) an iterable of tuples with each tuple containing the shortcut and the expansion
+ """
self.allow_redirection = allow_redirection
if terminators is None:
- self.terminators = [constants.MULTILINE_TERMINATOR]
+ self.terminators = (constants.MULTILINE_TERMINATOR,)
else:
- self.terminators = terminators
+ self.terminators = tuple(terminators)
if multiline_commands is None:
- self.multiline_commands = []
+ self.multiline_commands = tuple()
else:
- self.multiline_commands = multiline_commands
+ self.multiline_commands = tuple(multiline_commands)
if aliases is None:
- self.aliases = {}
+ self.aliases = dict()
else:
self.aliases = aliases
if shortcuts is None:
- self.shortcuts = []
+ self.shortcuts = tuple()
else:
- self.shortcuts = shortcuts
+ self.shortcuts = tuple(shortcuts)
# commands have to be a word, so make a regular expression
# that matches the first word in the line. This regex has three
diff --git a/cmd2/pyscript_bridge.py b/cmd2/pyscript_bridge.py
index f3ce841d..e1568b7c 100644
--- a/cmd2/pyscript_bridge.py
+++ b/cmd2/pyscript_bridge.py
@@ -14,6 +14,7 @@ from .utils import namedtuple_with_defaults, StdSim
# Python 3.4 require contextlib2 for temporarily redirecting stderr and stdout
if sys.version_info < (3, 5):
+ # noinspection PyUnresolvedReferences
from contextlib2 import redirect_stdout, redirect_stderr
else:
from contextlib import redirect_stdout, redirect_stderr
diff --git a/cmd2/rl_utils.py b/cmd2/rl_utils.py
index fdddca0b..b5ba8e4a 100644
--- a/cmd2/rl_utils.py
+++ b/cmd2/rl_utils.py
@@ -7,6 +7,7 @@ import sys
# Prefer statically linked gnureadline if available (for macOS compatibility due to issues with libedit)
try:
+ # noinspection PyPackageRequirements
import gnureadline as readline
except ImportError:
# Try to import readline, but allow failure for convenience in Windows unit testing
@@ -41,7 +42,7 @@ if 'pyreadline' in sys.modules:
# Check if we are running in a terminal
if sys.stdout.isatty(): # pragma: no cover
- # noinspection PyPep8Naming
+ # noinspection PyPep8Naming,PyUnresolvedReferences
def enable_win_vt100(handle: HANDLE) -> bool:
"""
Enables VT100 character sequences in a Windows console
@@ -71,7 +72,9 @@ if 'pyreadline' in sys.modules:
# Enable VT100 sequences for stdout and stderr
STD_OUT_HANDLE = -11
STD_ERROR_HANDLE = -12
+ # noinspection PyUnresolvedReferences
vt100_stdout_support = enable_win_vt100(readline.rl.console.GetStdHandle(STD_OUT_HANDLE))
+ # noinspection PyUnresolvedReferences
vt100_stderr_support = enable_win_vt100(readline.rl.console.GetStdHandle(STD_ERROR_HANDLE))
vt100_support = vt100_stdout_support and vt100_stderr_support
@@ -82,14 +85,14 @@ if 'pyreadline' in sys.modules:
try:
getattr(readline, 'redisplay')
except AttributeError:
- # noinspection PyProtectedMember
+ # noinspection PyProtectedMember,PyUnresolvedReferences
readline.redisplay = readline.rl.mode._update_line
# readline.remove_history_item()
try:
getattr(readline, 'remove_history_item')
except AttributeError:
- # noinspection PyProtectedMember
+ # noinspection PyProtectedMember,PyUnresolvedReferences
def pyreadline_remove_history_item(pos: int) -> None:
"""
An implementation of remove_history_item() for pyreadline
@@ -121,7 +124,7 @@ elif 'gnureadline' in sys.modules or 'readline' in sys.modules:
vt100_support = True
-# noinspection PyProtectedMember
+# noinspection PyProtectedMember,PyUnresolvedReferences
def rl_force_redisplay() -> None: # pragma: no cover
"""
Causes readline to display the prompt and input text wherever the cursor is and start
@@ -144,7 +147,7 @@ def rl_force_redisplay() -> None: # pragma: no cover
readline.rl.mode._update_line()
-# noinspection PyProtectedMember
+# noinspection PyProtectedMember, PyUnresolvedReferences
def rl_get_point() -> int: # pragma: no cover
"""
Returns the offset of the current cursor position in rl_line_buffer
@@ -159,7 +162,7 @@ def rl_get_point() -> int: # pragma: no cover
return 0
-# noinspection PyProtectedMember
+# noinspection PyProtectedMember, PyUnresolvedReferences
def rl_set_prompt(prompt: str) -> None: # pragma: no cover
"""
Sets readline's prompt
diff --git a/cmd2/utils.py b/cmd2/utils.py
index a8760a65..f3c29227 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -5,6 +5,7 @@
import collections
import os
import re
+import sys
import unicodedata
from typing import Any, Iterable, List, Optional, Union
@@ -88,6 +89,7 @@ def namedtuple_with_defaults(typename: str, field_names: Union[str, List[str]],
Node(val=4, left=None, right=7)
"""
T = collections.namedtuple(typename, field_names)
+ # noinspection PyProtectedMember,PyUnresolvedReferences
T.__new__.__defaults__ = (None,) * len(T._fields)
if isinstance(default_values, collections.Mapping):
prototype = T(**default_values)
@@ -350,3 +352,17 @@ def unquote_redirection_tokens(args: List[str]) -> None:
unquoted_arg = strip_quotes(arg)
if unquoted_arg in constants.REDIRECTION_TOKENS:
args[i] = unquoted_arg
+
+
+def find_editor() -> str:
+ """Find a reasonable editor to use by default for the system that the cmd2 application is running on."""
+ editor = os.environ.get('EDITOR')
+ if not editor:
+ if sys.platform[:3] == 'win':
+ editor = 'notepad'
+ else:
+ # Favor command-line editors first so we don't leave the terminal to edit
+ for editor in ['vim', 'vi', 'emacs', 'nano', 'pico', 'gedit', 'kate', 'subl', 'geany', 'atom']:
+ if which(editor):
+ break
+ return editor
diff --git a/docs/settingchanges.rst b/docs/settingchanges.rst
index e1c437e4..b9ad4a22 100644
--- a/docs/settingchanges.rst
+++ b/docs/settingchanges.rst
@@ -33,18 +33,16 @@ To define more shortcuts, update the dict ``App.shortcuts`` with the
class App(Cmd2):
def __init__(self):
- # Make sure you update the shortcuts attribute before calling the super class __init__
- self.shortcuts.update({'*': 'sneeze', '~': 'squirm'})
-
- # Make sure to call this super class __init__ after updating shortcuts
- cmd2.Cmd.__init__(self)
+ shortcuts = dict(self.DEFAULT_SHORTCUTS)
+ shortcuts.update({'*': 'sneeze', '~': 'squirm'})
+ cmd2.Cmd.__init__(self, shortcuts=shortcuts)
.. warning::
Shortcuts need to be created by updating the ``shortcuts`` dictionary attribute prior to calling the
``cmd2.Cmd`` super class ``__init__()`` method. Moreover, that super class init method needs to be called after
updating the ``shortcuts`` attribute This warning applies in general to many other attributes which are not
- settable at runtime such as ``multiline_commands``, etc.
+ settable at runtime.
Aliases
diff --git a/docs/unfreefeatures.rst b/docs/unfreefeatures.rst
index 97953215..071a15b2 100644
--- a/docs/unfreefeatures.rst
+++ b/docs/unfreefeatures.rst
@@ -7,12 +7,11 @@ Multiline commands
Command input may span multiple lines for the
commands whose names are listed in the
-parameter ``app.multiline_commands``. These
+``multiline_commands`` argument to ``cmd2.Cmd.__init__()``. These
commands will be executed only
after the user has entered a *terminator*.
By default, the command terminator is
-``;``; replacing or appending to the list
-``app.terminators`` allows different
+``;``; specifying the ``terminators`` optional argument to ``cmd2.Cmd.__init__()`` allows different
terminators. A blank line
is *always* considered a command terminator
(cannot be overridden).
diff --git a/examples/arg_print.py b/examples/arg_print.py
index 18d21787..edcc8444 100755
--- a/examples/arg_print.py
+++ b/examples/arg_print.py
@@ -19,12 +19,9 @@ class ArgumentAndOptionPrinter(cmd2.Cmd):
def __init__(self):
# Create command shortcuts which are typically 1 character abbreviations which can be used in place of a command
- self.shortcuts.update({'$': 'aprint', '%': 'oprint'})
-
- # Make sure to call this super class __init__ *after* setting and/or updating shortcuts
- super().__init__()
- # NOTE: It is critical that the super class __init__ method be called AFTER updating certain parameters which
- # are not settable at runtime. This includes the shortcuts, multiline_commands, etc.
+ shortcuts = dict(self.DEFAULT_SHORTCUTS)
+ shortcuts.update({'$': 'aprint', '%': 'oprint'})
+ super().__init__(shortcuts=shortcuts)
def do_aprint(self, statement):
"""Print the argument string this basic command is called with."""
diff --git a/examples/async_printing.py b/examples/async_printing.py
index dddbc352..60119a9c 100755
--- a/examples/async_printing.py
+++ b/examples/async_printing.py
@@ -32,7 +32,6 @@ class AlerterApp(cmd2.Cmd):
def __init__(self, *args, **kwargs) -> None:
""" Initializer """
-
super().__init__(*args, **kwargs)
self.prompt = "(APR)> "
diff --git a/examples/cmd_as_argument.py b/examples/cmd_as_argument.py
index dcec81c8..df7e1d76 100755
--- a/examples/cmd_as_argument.py
+++ b/examples/cmd_as_argument.py
@@ -29,16 +29,13 @@ class CmdLineApp(cmd2.Cmd):
MUMBLE_LAST = ['right?']
def __init__(self):
- self.allow_cli_args = False
- self.multiline_commands = ['orate']
- self.maxrepeats = 3
-
- # Add stuff to shortcuts before calling base class initializer
- self.shortcuts.update({'&': 'speak'})
-
+ shortcuts = dict(self.DEFAULT_SHORTCUTS)
+ shortcuts.update({'&': 'speak'})
# Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
- super().__init__(use_ipython=False)
+ super().__init__(use_ipython=False, multiline_commands=['orate'], shortcuts=shortcuts)
+ self.allow_cli_args = False
+ self.maxrepeats = 3
# Make maxrepeats settable at runtime
self.settable['maxrepeats'] = 'max repetitions for speak command'
diff --git a/examples/colors.py b/examples/colors.py
index 62df54e6..ea0bca39 100755
--- a/examples/colors.py
+++ b/examples/colors.py
@@ -63,15 +63,12 @@ class CmdLineApp(cmd2.Cmd):
MUMBLE_LAST = ['right?']
def __init__(self):
- self.multiline_commands = ['orate']
- self.maxrepeats = 3
-
- # Add stuff to shortcuts before calling base class initializer
- self.shortcuts.update({'&': 'speak'})
-
+ shortcuts = dict(self.DEFAULT_SHORTCUTS)
+ shortcuts.update({'&': 'speak'})
# Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
- super().__init__(use_ipython=True)
+ super().__init__(use_ipython=True, multiline_commands=['orate'], shortcuts=shortcuts)
+ self.maxrepeats = 3
# Make maxrepeats settable at runtime
self.settable['maxrepeats'] = 'max repetitions for speak command'
diff --git a/examples/decorator_example.py b/examples/decorator_example.py
index 79bd7633..d8088c0a 100755
--- a/examples/decorator_example.py
+++ b/examples/decorator_example.py
@@ -20,13 +20,13 @@ import cmd2
class CmdLineApp(cmd2.Cmd):
""" Example cmd2 application. """
def __init__(self, ip_addr=None, port=None, transcript_files=None):
- self.multiline_commands = ['orate']
- self.shortcuts.update({'&': 'speak'})
- self.maxrepeats = 3
-
+ shortcuts = dict(self.DEFAULT_SHORTCUTS)
+ shortcuts.update({'&': 'speak'})
# Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
- super().__init__(use_ipython=False, transcript_files=transcript_files)
+ super().__init__(use_ipython=False, transcript_files=transcript_files, multiline_commands=['orate'],
+ shortcuts=shortcuts)
+ self.maxrepeats = 3
# Make maxrepeats settable at runtime
self.settable['maxrepeats'] = 'Max number of `--repeat`s allowed'
diff --git a/examples/example.py b/examples/example.py
index 04727ec6..9f9c0304 100755
--- a/examples/example.py
+++ b/examples/example.py
@@ -27,16 +27,13 @@ class CmdLineApp(cmd2.Cmd):
MUMBLE_LAST = ['right?']
def __init__(self):
- self.multiline_commands = ['orate']
- self.maxrepeats = 3
-
- # Add stuff to shortcuts before calling base class initializer
- self.shortcuts.update({'&': 'speak'})
-
+ shortcuts = dict(self.DEFAULT_SHORTCUTS)
+ shortcuts.update({'&': 'speak'})
# Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
- super().__init__(use_ipython=False)
+ super().__init__(use_ipython=False, multiline_commands=['orate'], shortcuts=shortcuts)
# Make maxrepeats settable at runtime
+ self.maxrepeats = 3
self.settable['maxrepeats'] = 'max repetitions for speak command'
speak_parser = argparse.ArgumentParser()
diff --git a/examples/hooks.py b/examples/hooks.py
index b6f6263e..dd21e58a 100755
--- a/examples/hooks.py
+++ b/examples/hooks.py
@@ -87,7 +87,7 @@ class CmdLineApp(cmd2.Cmd):
func = self.cmd_func(data.statement.command)
if func is None:
# check if the entered command might be an abbreviation
- possible_cmds = [cmd for cmd in self.keywords if cmd.startswith(data.statement.command)]
+ possible_cmds = [cmd for cmd in self.get_all_commands() if cmd.startswith(data.statement.command)]
if len(possible_cmds) == 1:
raw = data.statement.raw.replace(data.statement.command, possible_cmds[0], 1)
data.statement = self.statement_parser.parse(raw)
diff --git a/examples/pirate.py b/examples/pirate.py
index 32330404..994ca245 100755
--- a/examples/pirate.py
+++ b/examples/pirate.py
@@ -11,6 +11,7 @@ import argparse
from colorama import Fore
import cmd2
+from cmd2.constants import MULTILINE_TERMINATOR
COLORS = {
'black': Fore.BLACK,
@@ -27,17 +28,14 @@ COLORS = {
class Pirate(cmd2.Cmd):
"""A piratical example cmd2 application involving looting and drinking."""
def __init__(self):
+ """Initialize the base class as well as this one"""
+ shortcuts = dict(self.DEFAULT_SHORTCUTS)
+ shortcuts.update({'~': 'sing'})
+ super().__init__(multiline_commands=['sing'], terminators=[MULTILINE_TERMINATOR, '...'], shortcuts=shortcuts)
+
self.default_to_shell = True
- self.multiline_commands = ['sing']
- self.terminators = self.terminators + ['...']
self.songcolor = Fore.BLUE
- # Add stuff to shortcuts before calling base class initializer
- self.shortcuts.update({'~': 'sing'})
-
- """Initialize the base class as well as this one"""
- super().__init__()
-
# Make songcolor settable at runtime
self.settable['songcolor'] = 'Color to ``sing`` in (black/red/green/yellow/blue/magenta/cyan/white)'
diff --git a/examples/plumbum_colors.py b/examples/plumbum_colors.py
index 3e5031d7..6daa5312 100755
--- a/examples/plumbum_colors.py
+++ b/examples/plumbum_colors.py
@@ -66,15 +66,12 @@ class CmdLineApp(cmd2.Cmd):
MUMBLE_LAST = ['right?']
def __init__(self):
- self.multiline_commands = ['orate']
- self.maxrepeats = 3
-
- # Add stuff to shortcuts before calling base class initializer
- self.shortcuts.update({'&': 'speak'})
-
+ shortcuts = dict(self.DEFAULT_SHORTCUTS)
+ shortcuts.update({'&': 'speak'})
# Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
- super().__init__(use_ipython=True)
+ super().__init__(use_ipython=True, multiline_commands=['orate'], shortcuts=shortcuts)
+ self.maxrepeats = 3
# Make maxrepeats settable at runtime
self.settable['maxrepeats'] = 'max repetitions for speak command'
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 6733be19..2137b564 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -105,7 +105,7 @@ def test_base_show_readonly(base_app):
Commands may be terminated with: {}
Arguments at invocation allowed: {}
Output redirection and pipes allowed: {}
-""".format(base_app.terminators, base_app.allow_cli_args, base_app.allow_redirection))
+""".format(base_app.statement_parser.terminators, base_app.allow_cli_args, base_app.allow_redirection))
assert out == expected
@@ -558,9 +558,9 @@ def test_feedback_to_output_false(base_app, capsys):
os.remove(filename)
-def test_allow_redirection(base_app):
+def test_disallow_redirection(base_app):
# Set allow_redirection to False
- base_app.allow_redirection = False
+ base_app.statement_parser.allow_redirection = False
filename = 'test_allow_redirect.txt'
@@ -1265,8 +1265,7 @@ def test_which_editor_bad():
class MultilineApp(cmd2.Cmd):
def __init__(self, *args, **kwargs):
- self.multiline_commands = ['orate']
- super().__init__(*args, **kwargs)
+ super().__init__(*args, multiline_commands=['orate'], **kwargs)
orate_parser = argparse.ArgumentParser()
orate_parser.add_argument('-s', '--shout', action="store_true", help="N00B EMULATION MODE")
diff --git a/tests/test_completion.py b/tests/test_completion.py
index da7fae65..47a7a9d6 100644
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -649,7 +649,7 @@ def test_tokens_for_completion_quoted_redirect(cmd2_app):
endidx = len(line)
begidx = endidx - len(text)
- cmd2_app.allow_redirection = True
+ cmd2_app.statement_parser.redirection = True
expected_tokens = ['command', '>file']
expected_raw_tokens = ['command', '">file']
@@ -663,7 +663,7 @@ def test_tokens_for_completion_redirect_off(cmd2_app):
endidx = len(line)
begidx = endidx - len(text)
- cmd2_app.allow_redirection = False
+ cmd2_app.statement_parser.allow_redirection = False
expected_tokens = ['command', '>file']
expected_raw_tokens = ['command', '>file']
diff --git a/tests/test_transcript.py b/tests/test_transcript.py
index df7a7cf9..709648fc 100644
--- a/tests/test_transcript.py
+++ b/tests/test_transcript.py
@@ -29,10 +29,9 @@ class CmdLineApp(cmd2.Cmd):
MUMBLE_LAST = ['right?']
def __init__(self, *args, **kwargs):
- self.multiline_commands = ['orate']
self.maxrepeats = 3
- super().__init__(*args, **kwargs)
+ super().__init__(*args, multiline_commands=['orate'], **kwargs)
# Make maxrepeats settable at runtime
self.settable['maxrepeats'] = 'Max number of `--repeat`s allowed'