diff options
author | Todd Leonhardt <todd.leonhardt@gmail.com> | 2019-03-18 22:49:42 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-03-18 22:49:42 -0400 |
commit | 2f24a8ad3eeb2fdf699d1e2a9d4f05429fe879c4 (patch) | |
tree | b836270cf61175d1e4a434fd8cae08f0ffd998a2 | |
parent | 96d176cc3d8198913693a42c7dd983cf69a165bd (diff) | |
parent | 57dd827963491439e40eb5dfe20811c14ea757ff (diff) | |
download | cmd2-git-2f24a8ad3eeb2fdf699d1e2a9d4f05429fe879c4.tar.gz |
Merge branch 'master' into load_generate_transcript
-rw-r--r-- | CHANGELOG.md | 8 | ||||
-rwxr-xr-x | README.md | 14 | ||||
-rw-r--r-- | cmd2/clipboard.py | 2 | ||||
-rw-r--r-- | cmd2/cmd2.py | 138 | ||||
-rw-r--r-- | cmd2/history.py | 4 | ||||
-rw-r--r-- | cmd2/parsing.py | 43 | ||||
-rw-r--r-- | cmd2/pyscript_bridge.py | 1 | ||||
-rw-r--r-- | cmd2/rl_utils.py | 15 | ||||
-rw-r--r-- | cmd2/utils.py | 16 | ||||
-rw-r--r-- | docs/settingchanges.rst | 10 | ||||
-rw-r--r-- | docs/unfreefeatures.rst | 5 | ||||
-rwxr-xr-x | examples/arg_print.py | 9 | ||||
-rwxr-xr-x | examples/async_printing.py | 1 | ||||
-rwxr-xr-x | examples/cmd_as_argument.py | 13 | ||||
-rwxr-xr-x | examples/colors.py | 11 | ||||
-rwxr-xr-x | examples/decorator_example.py | 10 | ||||
-rwxr-xr-x | examples/example.py | 11 | ||||
-rwxr-xr-x | examples/hooks.py | 2 | ||||
-rwxr-xr-x | examples/pirate.py | 14 | ||||
-rwxr-xr-x | examples/plumbum_colors.py | 11 | ||||
-rw-r--r-- | tests/test_cmd2.py | 9 | ||||
-rw-r--r-- | tests/test_completion.py | 4 | ||||
-rw-r--r-- | tests/test_transcript.py | 3 |
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 @@ -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' |