Hello community,

here is the log from the commit of package python-cmd2 for openSUSE:Factory 
checked in at 2019-09-23 12:09:41
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-cmd2 (Old)
 and      /work/SRC/openSUSE:Factory/.python-cmd2.new.7948 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-cmd2"

Mon Sep 23 12:09:41 2019 rev:27 rq:730677 version:0.9.16

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-cmd2/python-cmd2.changes  2019-08-07 
13:56:46.420855928 +0200
+++ /work/SRC/openSUSE:Factory/.python-cmd2.new.7948/python-cmd2.changes        
2019-09-23 12:09:44.109882338 +0200
@@ -1,0 +2,8 @@
+Fri Sep 13 10:57:37 UTC 2019 - Tomáš Chvátal <tchva...@suse.com>
+
+- Update to 0.9.16:
+  * Fixed inconsistent parsing/tab completion behavio
+  * Create directory for the persistent history file if it does not already 
exist
+  * Aliases and macros can no longer have the same name as a command
+
+-------------------------------------------------------------------

Old:
----
  cmd2-0.9.15.tar.gz

New:
----
  cmd2-0.9.16.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-cmd2.spec ++++++
--- /var/tmp/diff_new_pack.L8fhY9/_old  2019-09-23 12:09:45.257882148 +0200
+++ /var/tmp/diff_new_pack.L8fhY9/_new  2019-09-23 12:09:45.261882148 +0200
@@ -19,7 +19,7 @@
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 %define         skip_python2 1
 Name:           python-cmd2
-Version:        0.9.15
+Version:        0.9.16
 Release:        0
 Summary:        Extra features for standard library's cmd module
 License:        MIT
@@ -35,11 +35,11 @@
 Requires:       python-colorama >= 0.3.7
 Requires:       python-pyperclip >= 1.6
 Requires:       python-wcwidth >= 0.1.7
-%if %python3_version_nodots < 35
+BuildArch:      noarch
+%if %{python3_version_nodots} < 35
 Requires:       python-contextlib2
 Requires:       python-typing
 %endif
-BuildArch:      noarch
 # SECTION Test requirements
 BuildRequires:  %{python_module attrs >= 16.3.0}
 BuildRequires:  %{python_module colorama >= 0.3.7}

++++++ cmd2-0.9.15.tar.gz -> cmd2-0.9.16.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cmd2-0.9.15/CHANGELOG.md new/cmd2-0.9.16/CHANGELOG.md
--- old/cmd2-0.9.15/CHANGELOG.md        2019-07-25 03:46:19.000000000 +0200
+++ new/cmd2-0.9.16/CHANGELOG.md        2019-08-08 03:55:37.000000000 +0200
@@ -1,3 +1,16 @@
+## 0.9.16 (August 7, 2019)
+* Bug Fixes
+    * Fixed inconsistent parsing/tab completion behavior based on the value of 
`allow_redirection`. This flag is
+    only meant to be a security setting that prevents redirection of stdout 
and should not alter parsing logic.
+* Enhancements
+    * Raise `TypeError` if trying to set choices/completions on argparse 
action that accepts no arguments
+    * Create directory for the persistent history file if it does not already 
exist
+    * Added `set_choices_function()`, `set_choices_method()`, 
`set_completer_function()`, and `set_completer_method()`
+    to support cases where this functionality needs to be added to an argparse 
action outside of the normal
+    `parser.add_argument()` call.
+* Breaking Changes
+    * Aliases and macros can no longer have the same name as a command
+
 ## 0.9.15 (July 24, 2019)
 * Bug Fixes
     * Fixed exception caused by tab completing after an invalid subcommand was 
entered
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cmd2-0.9.15/PKG-INFO new/cmd2-0.9.16/PKG-INFO
--- old/cmd2-0.9.15/PKG-INFO    2019-07-25 04:11:23.000000000 +0200
+++ new/cmd2-0.9.16/PKG-INFO    2019-08-08 04:00:03.000000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: cmd2
-Version: 0.9.15
+Version: 0.9.16
 Summary: cmd2 - quickly build feature-rich and user-friendly interactive 
command line applications in Python
 Home-page: https://github.com/python-cmd2/cmd2
 Author: Catherine Devlin
@@ -196,7 +196,7 @@
         
             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).
+            `terminators` optional argument to `cmd2.Cmd.__init__()`  .  The 
default terminators are `;` and `\n` (empty newline).
         
         - Special-character shortcut commands (beyond cmd's "@" and "!")
         
@@ -216,11 +216,10 @@
         Tutorials
         ---------
         
-        A few tutorials on using cmd2 exist:
-        
-        * Florida PyCon 2017 talk: 
[slides](https://docs.google.com/presentation/d/1LRmpfBt3V-pYQfgQHdczf16F3hcXmhK83tl77R6IJtE),
 [video](https://www.youtube.com/watch?v=6m0RdpITaeY)
-        * PyCon 2010 talk by Catherine Devlin, the original author: 
[video](http://pyvideo.org/pycon-us-2010/pycon-2010--easy-command-line-applications-with-c.html)
-        * A nice brief step-by-step tutorial: 
[blog](https://kushaldas.in/posts/developing-command-line-interpreters-using-python-cmd2.html)
+        * PyOhio 2019 presentation: 
+            * [video](https://www.youtube.com/watch?v=pebeWrTqIIw)
+            * 
[slides](https://github.com/python-cmd2/talks/blob/master/PyOhio_2019/cmd2-PyOhio_2019.pdf)
+            * [example 
code](https://github.com/python-cmd2/talks/tree/master/PyOhio_2019/examples)
         
         
         Example Application
@@ -400,5 +399,5 @@
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
 Requires-Python: >=3.5
 Description-Content-Type: text/markdown
-Provides-Extra: test
 Provides-Extra: dev
+Provides-Extra: test
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cmd2-0.9.15/README.md new/cmd2-0.9.16/README.md
--- old/cmd2-0.9.15/README.md   2019-07-20 01:17:59.000000000 +0200
+++ new/cmd2-0.9.16/README.md   2019-08-03 19:14:02.000000000 +0200
@@ -188,7 +188,7 @@
 
     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).
+    `terminators` optional argument to `cmd2.Cmd.__init__()`  .  The default 
terminators are `;` and `\n` (empty newline).
 
 - Special-character shortcut commands (beyond cmd's "@" and "!")
 
@@ -208,11 +208,10 @@
 Tutorials
 ---------
 
-A few tutorials on using cmd2 exist:
-
-* Florida PyCon 2017 talk: 
[slides](https://docs.google.com/presentation/d/1LRmpfBt3V-pYQfgQHdczf16F3hcXmhK83tl77R6IJtE),
 [video](https://www.youtube.com/watch?v=6m0RdpITaeY)
-* PyCon 2010 talk by Catherine Devlin, the original author: 
[video](http://pyvideo.org/pycon-us-2010/pycon-2010--easy-command-line-applications-with-c.html)
-* A nice brief step-by-step tutorial: 
[blog](https://kushaldas.in/posts/developing-command-line-interpreters-using-python-cmd2.html)
+* PyOhio 2019 presentation: 
+    * [video](https://www.youtube.com/watch?v=pebeWrTqIIw)
+    * 
[slides](https://github.com/python-cmd2/talks/blob/master/PyOhio_2019/cmd2-PyOhio_2019.pdf)
+    * [example 
code](https://github.com/python-cmd2/talks/tree/master/PyOhio_2019/examples)
 
 
 Example Application
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cmd2-0.9.15/cmd2/argparse_custom.py 
new/cmd2-0.9.16/cmd2/argparse_custom.py
--- old/cmd2-0.9.15/cmd2/argparse_custom.py     2019-07-23 01:40:19.000000000 
+0200
+++ new/cmd2-0.9.16/cmd2/argparse_custom.py     2019-08-06 04:42:05.000000000 
+0200
@@ -94,6 +94,14 @@
     as dynamic. Therefore it is up to the developer to validate if the user 
has typed an acceptable value for these
     arguments.
 
+    The following functions exist in cases where you may want to manually add 
choice providing function/methods to
+    an existing argparse action. For instance, in __init__() of a custom 
action class.
+
+        set_choices_function(action, func)
+        set_choices_method(action, method)
+        set_completer_function(action, func)
+        set_completer_method(action, method)
+
 CompletionItem Class:
     This class was added to help in cases where uninformative data is being 
tab completed. For instance,
     tab completing ID numbers isn't very helpful to a user without context. 
Returning a list of CompletionItems
@@ -223,6 +231,9 @@
         self.description = desc
 
 
+############################################################################################################
+# Class and functions related to ChoicesCallable
+############################################################################################################
 class ChoicesCallable:
     """
     Enables using a callable as the choices provider for an argparse argument.
@@ -241,6 +252,48 @@
         self.to_call = to_call
 
 
+def _set_choices_callable(action: argparse.Action, choices_callable: 
ChoicesCallable) -> None:
+    """
+    Set the choices_callable attribute of an argparse Action
+    :param action: action being edited
+    :param choices_callable: the ChoicesCallable instance to use
+    :raises: TypeError if used on incompatible action type
+    """
+    # Verify consistent use of parameters
+    if action.choices is not None:
+        err_msg = ("None of the following parameters can be used alongside a 
choices parameter:\n"
+                   "choices_function, choices_method, completer_function, 
completer_method")
+        raise (TypeError(err_msg))
+    elif action.nargs == 0:
+        err_msg = ("None of the following parameters can be used on an action 
that takes no arguments:\n"
+                   "choices_function, choices_method, completer_function, 
completer_method")
+        raise (TypeError(err_msg))
+
+    setattr(action, ATTR_CHOICES_CALLABLE, choices_callable)
+
+
+def set_choices_function(action: argparse.Action, choices_function: 
Callable[[], Iterable[Any]]) -> None:
+    """Set choices_function on an argparse action"""
+    _set_choices_callable(action, ChoicesCallable(is_method=False, 
is_completer=False, to_call=choices_function))
+
+
+def set_choices_method(action: argparse.Action, choices_method: 
Callable[[Any], Iterable[Any]]) -> None:
+    """Set choices_method on an argparse action"""
+    _set_choices_callable(action, ChoicesCallable(is_method=True, 
is_completer=False, to_call=choices_method))
+
+
+def set_completer_function(action: argparse.Action,
+                           completer_function: Callable[[str, str, int, int], 
List[str]]) -> None:
+    """Set completer_function on an argparse action"""
+    _set_choices_callable(action, ChoicesCallable(is_method=False, 
is_completer=True, to_call=completer_function))
+
+
+def set_completer_method(action: argparse.Action,
+                         completer_method: Callable[[Any, str, str, int, int], 
List[str]]) -> None:
+    """Set completer_method on an argparse action"""
+    _set_choices_callable(action, ChoicesCallable(is_method=True, 
is_completer=True, to_call=completer_method))
+
+
 
############################################################################################################
 # Patch _ActionsContainer.add_argument with our wrapper to support more 
arguments
 
############################################################################################################
@@ -291,7 +344,17 @@
           See the header of this file for more information
 
     :return: the created argument action
+    :raises ValueError on incorrect parameter usage
     """
+    # Verify consistent use of arguments
+    choices_callables = [choices_function, choices_method, completer_function, 
completer_method]
+    num_params_set = len(choices_callables) - choices_callables.count(None)
+
+    if num_params_set > 1:
+        err_msg = ("Only one of the following parameters may be used at a 
time:\n"
+                   "choices_function, choices_method, completer_function, 
completer_method")
+        raise (ValueError(err_msg))
+
     # Pre-process special ranged nargs
     nargs_range = None
 
@@ -345,30 +408,17 @@
     # Create the argument using the original add_argument function
     new_arg = orig_actions_container_add_argument(self, *args, **kwargs)
 
-    # Verify consistent use of arguments
-    choice_params = [new_arg.choices, choices_function, choices_method, 
completer_function, completer_method]
-    num_set = len(choice_params) - choice_params.count(None)
-
-    if num_set > 1:
-        err_msg = ("Only one of the following may be used in an argparser 
argument at a time:\n"
-                   "choices, choices_function, choices_method, 
completer_function, completer_method")
-        raise (ValueError(err_msg))
-
     # Set the custom attributes
     setattr(new_arg, ATTR_NARGS_RANGE, nargs_range)
 
     if choices_function:
-        setattr(new_arg, ATTR_CHOICES_CALLABLE,
-                ChoicesCallable(is_method=False, is_completer=False, 
to_call=choices_function))
+        set_choices_function(new_arg, choices_function)
     elif choices_method:
-        setattr(new_arg, ATTR_CHOICES_CALLABLE,
-                ChoicesCallable(is_method=True, is_completer=False, 
to_call=choices_method))
+        set_choices_method(new_arg, choices_method)
     elif completer_function:
-        setattr(new_arg, ATTR_CHOICES_CALLABLE,
-                ChoicesCallable(is_method=False, is_completer=True, 
to_call=completer_function))
+        set_completer_function(new_arg, completer_function)
     elif completer_method:
-        setattr(new_arg, ATTR_CHOICES_CALLABLE,
-                ChoicesCallable(is_method=True, is_completer=True, 
to_call=completer_method))
+        set_completer_method(new_arg, completer_method)
 
     setattr(new_arg, ATTR_SUPPRESS_TAB_HINT, suppress_tab_hint)
     setattr(new_arg, ATTR_DESCRIPTIVE_COMPLETION_HEADER, descriptive_header)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cmd2-0.9.15/cmd2/cmd2.py new/cmd2-0.9.16/cmd2/cmd2.py
--- old/cmd2-0.9.15/cmd2/cmd2.py        2019-07-25 00:50:26.000000000 +0200
+++ new/cmd2-0.9.16/cmd2/cmd2.py        2019-08-08 02:38:15.000000000 +0200
@@ -353,9 +353,15 @@
                                commands to be run or, if -t is specified, 
transcript files to run.
                                This should be set to False if your application 
parses its own arguments.
         :param transcript_files: allow running transcript tests when 
allow_cli_args is False
-        :param allow_redirection: should output redirection and pipes be 
allowed
+        :param allow_redirection: should output redirection and pipes be 
allowed. this is only a security setting
+                                  and does not alter parsing behavior.
         :param multiline_commands: list of commands allowed to accept 
multi-line input
-        :param shortcuts: dictionary containing shortcuts for commands
+        :param terminators: list of characters that terminate a command. These 
are mainly intended for terminating
+                            multiline commands, but will also terminate 
single-line commands. If not supplied, then
+                            defaults to semicolon. If your app only contains 
single-line commands and you want
+                            terminators to be treated as literals by the 
parser, then set this to an empty list.
+        :param shortcuts: dictionary containing shortcuts for commands. If not 
supplied, then defaults to
+                          constants.DEFAULT_SHORTCUTS.
         """
         # If use_ipython is False, make sure the do_ipy() method doesn't exit
         if not use_ipython:
@@ -371,11 +377,14 @@
         # Call super class constructor
         super().__init__(completekey=completekey, stdin=stdin, stdout=stdout)
 
-        # Attributes which should NOT be dynamically settable at runtime
+        # Attributes which should NOT be dynamically settable via the set 
command at runtime
+        # To prevent a user from altering these with the py/ipy commands, 
remove locals_in_py from the
+        # settable dictionary during your applications's __init__ method.
         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
+        self.allow_redirection = allow_redirection  # Security setting to 
prevent redirection of stdout
 
-        # Attributes which ARE dynamically settable at runtime
+        # Attributes which ARE dynamically settable via the set command at 
runtime
         self.continuation_prompt = '> '
         self.debug = False
         self.echo = False
@@ -435,11 +444,16 @@
         # True if running inside a Python script or interactive console, False 
otherwise
         self._in_py = False
 
-        self.statement_parser = 
StatementParser(allow_redirection=allow_redirection,
-                                                terminators=terminators,
+        self.statement_parser = StatementParser(terminators=terminators,
                                                 
multiline_commands=multiline_commands,
                                                 shortcuts=shortcuts)
 
+        # Verify commands don't have invalid names (like starting with a 
shortcut)
+        for cur_cmd in self.get_all_commands():
+            valid, errmsg = self.statement_parser.is_valid_command(cur_cmd)
+            if not valid:
+                raise ValueError("Invalid command name {!r}: 
{}".format(cur_cmd, errmsg))
+
         # Stores results from the last command run to enable usage of results 
in a Python script or interactive console
         # Built-in commands don't make use of this.  It is purely there for 
user-defined commands and convenience.
         self.last_result = None
@@ -611,16 +625,6 @@
         """Read-only property to access the aliases stored in the 
StatementParser."""
         return self.statement_parser.aliases
 
-    @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 poutput(self, msg: Any, *, end: str = '\n') -> None:
         """Print message to self.stdout and appends a newline by default
 
@@ -686,7 +690,7 @@
             final_msg = ansi.style_error(final_msg)
 
         if not self.debug:
-            warning = "\nTo enable full traceback, run the following command:  
'set debug true'"
+            warning = "\nTo enable full traceback, run the following command: 
'set debug true'"
             final_msg += ansi.style_warning(warning)
 
         # Set apply_style to False since style has already been applied
@@ -826,61 +830,8 @@
                     # Return empty lists since this means the line is 
malformed.
                     return [], []
 
-        if self.allow_redirection:
-
-            # Since redirection is enabled, we need to treat redirection 
characters (|, <, >)
-            # as word breaks when they are in unquoted strings. Go through 
each token
-            # and further split them on these characters. Each run of redirect 
characters
-            # is treated as a single token.
-            raw_tokens = []
-
-            for cur_initial_token in initial_tokens:
-
-                # Save tokens up to 1 character in length or quoted tokens. No 
need to parse these.
-                if len(cur_initial_token) <= 1 or cur_initial_token[0] in 
constants.QUOTES:
-                    raw_tokens.append(cur_initial_token)
-                    continue
-
-                # Iterate over each character in this token
-                cur_index = 0
-                cur_char = cur_initial_token[cur_index]
-
-                # Keep track of the token we are building
-                cur_raw_token = ''
-
-                while True:
-                    if cur_char not in constants.REDIRECTION_CHARS:
-
-                        # Keep appending to cur_raw_token until we hit a 
redirect char
-                        while cur_char not in constants.REDIRECTION_CHARS:
-                            cur_raw_token += cur_char
-                            cur_index += 1
-                            if cur_index < len(cur_initial_token):
-                                cur_char = cur_initial_token[cur_index]
-                            else:
-                                break
-
-                    else:
-                        redirect_char = cur_char
-
-                        # Keep appending to cur_raw_token until we hit 
something other than redirect_char
-                        while cur_char == redirect_char:
-                            cur_raw_token += cur_char
-                            cur_index += 1
-                            if cur_index < len(cur_initial_token):
-                                cur_char = cur_initial_token[cur_index]
-                            else:
-                                break
-
-                    # Save the current token
-                    raw_tokens.append(cur_raw_token)
-                    cur_raw_token = ''
-
-                    # Check if we've viewed all characters
-                    if cur_index >= len(cur_initial_token):
-                        break
-        else:
-            raw_tokens = initial_tokens
+        # Further split tokens on punctuation characters
+        raw_tokens = self.statement_parser.split_on_punctuation(initial_tokens)
 
         # Save the unquoted tokens
         tokens = [utils.strip_quotes(cur_token) for cur_token in raw_tokens]
@@ -1223,72 +1174,70 @@
                          this will be called if we aren't completing for 
redirection
         :return: a list of possible tab completions
         """
-        if self.allow_redirection:
-
-            # Get all tokens through the one being completed. We want the raw 
tokens
-            # so we can tell if redirection strings are quoted and ignore them.
-            _, raw_tokens = self.tokens_for_completion(line, begidx, endidx)
-            if not raw_tokens:
-                return []
+        # Get all tokens through the one being completed. We want the raw 
tokens
+        # so we can tell if redirection strings are quoted and ignore them.
+        _, raw_tokens = self.tokens_for_completion(line, begidx, endidx)
+        if not raw_tokens:
+            return []
 
-            # Must at least have the command
-            if len(raw_tokens) > 1:
+        # Must at least have the command
+        if len(raw_tokens) > 1:
 
-                # True when command line contains any redirection tokens
-                has_redirection = False
+            # True when command line contains any redirection tokens
+            has_redirection = False
 
-                # Keep track of state while examining tokens
-                in_pipe = False
-                in_file_redir = False
-                do_shell_completion = False
-                do_path_completion = False
-                prior_token = None
-
-                for cur_token in raw_tokens:
-                    # Process redirection tokens
-                    if cur_token in constants.REDIRECTION_TOKENS:
-                        has_redirection = True
-
-                        # Check if we are at a pipe
-                        if cur_token == constants.REDIRECTION_PIPE:
-                            # Do not complete bad syntax (e.g cmd | |)
-                            if prior_token == constants.REDIRECTION_PIPE:
-                                return []
+            # Keep track of state while examining tokens
+            in_pipe = False
+            in_file_redir = False
+            do_shell_completion = False
+            do_path_completion = False
+            prior_token = None
+
+            for cur_token in raw_tokens:
+                # Process redirection tokens
+                if cur_token in constants.REDIRECTION_TOKENS:
+                    has_redirection = True
+
+                    # Check if we are at a pipe
+                    if cur_token == constants.REDIRECTION_PIPE:
+                        # Do not complete bad syntax (e.g cmd | |)
+                        if prior_token == constants.REDIRECTION_PIPE:
+                            return []
 
-                            in_pipe = True
-                            in_file_redir = False
+                        in_pipe = True
+                        in_file_redir = False
 
-                        # Otherwise this is a file redirection token
-                        else:
-                            if prior_token in constants.REDIRECTION_TOKENS or 
in_file_redir:
-                                # Do not complete bad syntax (e.g cmd | >) 
(e.g cmd > blah >)
-                                return []
+                    # Otherwise this is a file redirection token
+                    else:
+                        if prior_token in constants.REDIRECTION_TOKENS or 
in_file_redir:
+                            # Do not complete bad syntax (e.g cmd | >) (e.g 
cmd > blah >)
+                            return []
 
-                            in_pipe = False
-                            in_file_redir = True
+                        in_pipe = False
+                        in_file_redir = True
 
-                    # Not a redirection token
-                    else:
-                        do_shell_completion = False
-                        do_path_completion = False
+                # Not a redirection token
+                else:
+                    do_shell_completion = False
+                    do_path_completion = False
 
-                        if prior_token == constants.REDIRECTION_PIPE:
-                            do_shell_completion = True
-                        elif in_pipe or prior_token in 
(constants.REDIRECTION_OUTPUT, constants.REDIRECTION_APPEND):
-                            do_path_completion = True
+                    if prior_token == constants.REDIRECTION_PIPE:
+                        do_shell_completion = True
+                    elif in_pipe or prior_token in 
(constants.REDIRECTION_OUTPUT, constants.REDIRECTION_APPEND):
+                        do_path_completion = True
 
-                    prior_token = cur_token
+                prior_token = cur_token
 
-                if do_shell_completion:
-                    return self.shell_cmd_complete(text, line, begidx, endidx)
+            if do_shell_completion:
+                return self.shell_cmd_complete(text, line, begidx, endidx)
 
-                elif do_path_completion:
-                    return self.path_complete(text, line, begidx, endidx)
+            elif do_path_completion:
+                return self.path_complete(text, line, begidx, endidx)
 
-                # If there were redirection strings anywhere on the command 
line, then we
-                # are no longer tab completing for the current command
-                elif has_redirection:
-                    return []
+            # If there were redirection strings anywhere on the command line, 
then we
+            # are no longer tab completing for the current command
+            elif has_redirection:
+                return []
 
         # Call the command's completer function
         return compfunc(text, line, begidx, endidx)
@@ -1729,13 +1678,10 @@
         statement = self.statement_parser.parse_command_only(line)
         return statement.command, statement.args, statement.command_and_args
 
-    def onecmd_plus_hooks(self, line: str, *, expand: bool = True, 
add_to_history: bool = True,
-                          py_bridge_call: bool = False) -> bool:
+    def onecmd_plus_hooks(self, line: str, *, add_to_history: bool = True, 
py_bridge_call: bool = False) -> bool:
         """Top-level function called by cmdloop() to handle parsing a line and 
running the command and all of its hooks.
 
         :param line: command line to run
-        :param expand: If True, then aliases, macros, and shortcuts will be 
expanded.
-                       Set this to False if the command token should not be 
altered. Defaults to True.
         :param add_to_history: If True, then add this command to history. 
Defaults to True.
         :param py_bridge_call: This should only ever be set to True by 
PyBridge to signify the beginning
                                of an app() call from Python. It is used to 
enable/disable the storage of the
@@ -1746,7 +1692,7 @@
 
         stop = False
         try:
-            statement = self._input_line_to_statement(line, expand=expand)
+            statement = self._input_line_to_statement(line)
         except EmptyStatement:
             return self._run_cmdfinalization_hooks(stop, None)
         except ValueError as ex:
@@ -1861,15 +1807,12 @@
         except Exception as ex:
             self.pexcept(ex)
 
-    def runcmds_plus_hooks(self, cmds: List[Union[HistoryItem, str]], *,
-                           expand: bool = True, add_to_history: bool = True) 
-> bool:
+    def runcmds_plus_hooks(self, cmds: List[Union[HistoryItem, str]], *, 
add_to_history: bool = True) -> bool:
         """
         Used when commands are being run in an automated fashion like text 
scripts or history replays.
         The prompt and command line for each command will be printed if echo 
is True.
 
         :param cmds: commands to run
-        :param expand: If True, then aliases, macros, and shortcuts will be 
expanded.
-                       Set this to False if the command token should not be 
altered. Defaults to True.
         :param add_to_history: If True, then add these commands to history. 
Defaults to True.
         :return: True if running of commands should stop
         """
@@ -1880,12 +1823,12 @@
             if self.echo:
                 self.poutput('{}{}'.format(self.prompt, line))
 
-            if self.onecmd_plus_hooks(line, expand=expand, 
add_to_history=add_to_history):
+            if self.onecmd_plus_hooks(line, add_to_history=add_to_history):
                 return True
 
         return False
 
-    def _complete_statement(self, line: str, *, expand: bool = True) -> 
Statement:
+    def _complete_statement(self, line: str) -> Statement:
         """Keep accepting lines of input until the command is complete.
 
         There is some pretty hacky code here to handle some quirks of
@@ -1894,13 +1837,11 @@
         backwards compatibility with the standard library version of cmd.
 
         :param line: the line being parsed
-        :param expand: If True, then aliases and shortcuts will be expanded.
-                       Set this to False if the command token should not be 
altered. Defaults to True.
         :return: the completed Statement
         """
         while True:
             try:
-                statement = self.statement_parser.parse(line, expand=expand)
+                statement = self.statement_parser.parse(line)
                 if statement.multiline_command and statement.terminator:
                     # we have a completed multiline command, we are done
                     break
@@ -1911,7 +1852,7 @@
             except ValueError:
                 # we have unclosed quotation marks, lets parse only the command
                 # and see if it's a multiline
-                statement = self.statement_parser.parse_command_only(line, 
expand=expand)
+                statement = self.statement_parser.parse_command_only(line)
                 if not statement.multiline_command:
                     # not a multiline command, so raise the exception
                     raise
@@ -1948,13 +1889,11 @@
             raise EmptyStatement()
         return statement
 
-    def _input_line_to_statement(self, line: str, *, expand: bool = True) -> 
Statement:
+    def _input_line_to_statement(self, line: str) -> Statement:
         """
         Parse the user's input line and convert it to a Statement, ensuring 
that all macros are also resolved
 
         :param line: the line being parsed
-        :param expand: If True, then aliases, macros, and shortcuts will be 
expanded.
-                       Set this to False if the command token should not be 
altered. Defaults to True.
         :return: parsed command line as a Statement
         """
         used_macros = []
@@ -1963,14 +1902,14 @@
         # Continue until all macros are resolved
         while True:
             # Make sure all input has been read and convert it to a Statement
-            statement = self._complete_statement(line, expand=expand)
+            statement = self._complete_statement(line)
 
             # Save the fully entered line if this is the first loop iteration
             if orig_line is None:
                 orig_line = statement.raw
 
             # Check if this command matches a macro and wasn't already 
processed to avoid an infinite loop
-            if expand and statement.command in self.macros.keys() and 
statement.command not in used_macros:
+            if statement.command in self.macros.keys() and statement.command 
not in used_macros:
                 used_macros.append(statement.command)
                 line = self._resolve_macro(statement)
                 if line is None:
@@ -2184,22 +2123,19 @@
         return target if callable(getattr(self, target, None)) else ''
 
     # noinspection PyMethodOverriding
-    def onecmd(self, statement: Union[Statement, str], *,
-               expand: bool = True, add_to_history: bool = True) -> bool:
+    def onecmd(self, statement: Union[Statement, str], *, add_to_history: bool 
= True) -> bool:
         """ This executes the actual do_* method for a command.
 
         If the command provided doesn't exist, then it executes default() 
instead.
 
         :param statement: intended to be a Statement instance parsed command 
from the input stream, alternative
                           acceptance of a str is present only for backward 
compatibility with cmd
-        :param expand: If True, then aliases, macros, and shortcuts will be 
expanded.
-                       Set this to False if the command token should not be 
altered. Defaults to True.
         :param add_to_history: If True, then add this command to history. 
Defaults to True.
         :return: a flag indicating whether the interpretation of commands 
should stop
         """
         # For backwards compatibility with cmd, allow a str to be passed in
         if not isinstance(statement, Statement):
-            statement = self._input_line_to_statement(statement, expand=expand)
+            statement = self._input_line_to_statement(statement)
 
         func = self.cmd_func(statement.command)
         if func:
@@ -2308,12 +2244,11 @@
             readline_settings.completer = readline.get_completer()
             readline.set_completer(self.complete)
 
-            # Break words on whitespace and quotes when tab completing
-            completer_delims = " \t\n" + ''.join(constants.QUOTES)
-
-            if self.allow_redirection:
-                # If redirection is allowed, then break words on those 
characters too
-                completer_delims += ''.join(constants.REDIRECTION_CHARS)
+            # Set the readline word delimiters for completion
+            completer_delims = " \t\n"
+            completer_delims += ''.join(constants.QUOTES)
+            completer_delims += ''.join(constants.REDIRECTION_CHARS)
+            completer_delims += ''.join(self.statement_parser.terminators)
 
             readline_settings.delims = readline.get_completer_delims()
             readline.set_completer_delims(completer_delims)
@@ -2389,6 +2324,10 @@
             self.perror("Invalid alias name: {}".format(errmsg))
             return
 
+        if args.name in self.get_all_commands():
+            self.perror("Alias cannot have the same name as a command")
+            return
+
         if args.name in self.macros:
             self.perror("Alias cannot have the same name as a macro")
             return
@@ -2451,8 +2390,8 @@
     alias_create_description = "Create or overwrite an alias"
 
     alias_create_epilog = ("Notes:\n"
-                           "  If you want to use redirection, pipes, or 
terminators like ';' in the value\n"
-                           "  of the alias, then quote them.\n"
+                           "  If you want to use redirection, pipes, or 
terminators in the value of the\n"
+                           "  alias, then quote them.\n"
                            "\n"
                            "  Since aliases are resolved during parsing, tab 
completion will function as\n"
                            "  it would for the actual command the alias 
resolves to.\n"
@@ -2518,8 +2457,8 @@
             self.perror("Invalid macro name: {}".format(errmsg))
             return
 
-        if args.name in self.statement_parser.multiline_commands:
-            self.perror("Macro cannot have the same name as a multiline 
command")
+        if args.name in self.get_all_commands():
+            self.perror("Macro cannot have the same name as a command")
             return
 
         if args.name in self.aliases:
@@ -2657,8 +2596,8 @@
                            "\n"
                            "    macro create backup !cp \"{1}\" \"{1}.orig\"\n"
                            "\n"
-                           "  If you want to use redirection, pipes, or 
terminators like ';' in the value\n"
-                           "  of the macro, then quote them.\n"
+                           "  If you want to use redirection, pipes, or 
terminators in the value of the\n"
+                           "  macro, then quote them.\n"
                            "\n"
                            "    macro create show_results print_results -type 
{1} \"|\" less\n"
                            "\n"
@@ -2957,6 +2896,28 @@
              | a list of tuples -> interpreted as (value, text), so
                                    that the return value can differ from
                                    the text advertised to the user """
+
+        completion_disabled = False
+        orig_completer = None
+
+        def disable_completion():
+            """Turn off completion during the select input line"""
+            nonlocal orig_completer
+            nonlocal completion_disabled
+
+            if rl_type != RlType.NONE and not completion_disabled:
+                orig_completer = readline.get_completer()
+                readline.set_completer(lambda *args, **kwargs: None)
+                completion_disabled = True
+
+        def enable_completion():
+            """Restore tab completion when select is done reading input"""
+            nonlocal completion_disabled
+
+            if rl_type != RlType.NONE and completion_disabled:
+                readline.set_completer(orig_completer)
+                completion_disabled = False
+
         local_opts = opts
         if isinstance(opts, str):
             local_opts = list(zip(opts.split(), opts.split()))
@@ -2971,15 +2932,28 @@
                     fulloptions.append((opt[0], opt[0]))
         for (idx, (_, text)) in enumerate(fulloptions):
             self.poutput('  %2d. %s' % (idx + 1, text))
+
         while True:
             safe_prompt = rl_make_safe_prompt(prompt)
-            response = input(safe_prompt)
+
+            try:
+                with self.sigint_protection:
+                    disable_completion()
+                response = input(safe_prompt)
+            except EOFError:
+                response = ''
+                self.poutput('\n', end='')
+            finally:
+                with self.sigint_protection:
+                    enable_completion()
+
+            if not response:
+                continue
 
             if rl_type != RlType.NONE:
                 hlen = readline.get_current_history_length()
-                if hlen >= 1 and response != '':
+                if hlen >= 1:
                     readline.remove_history_item(hlen - 1)
-
             try:
                 choice = int(response)
                 if choice < 1:
@@ -2989,6 +2963,7 @@
             except (ValueError, IndexError):
                 self.poutput("{!r} isn't a valid choice. Pick a number between 
1 and {}:".format(
                     response, len(fulloptions)))
+
         return result
 
     def _get_read_only_settings(self) -> str:
@@ -3603,15 +3578,25 @@
 
         hist_file = os.path.abspath(os.path.expanduser(hist_file))
 
-        # first we try and unpickle the history file
-        history = History()
         # on Windows, trying to open a directory throws a permission
         # error, not a `IsADirectoryError`. So we'll check it ourselves.
         if os.path.isdir(hist_file):
-            msg = "persistent history file '{}' is a directory"
+            msg = "Persistent history file '{}' is a directory"
             self.perror(msg.format(hist_file))
             return
 
+        # Create the directory for the history file if it doesn't already exist
+        hist_file_dir = os.path.dirname(hist_file)
+        try:
+            os.makedirs(hist_file_dir, exist_ok=True)
+        except OSError as ex:
+            msg = "Error creating persistent history file directory '{}': 
{}".format(hist_file_dir, ex)
+            self.pexcept(msg)
+            return
+
+        # first we try and unpickle the history file
+        history = History()
+
         try:
             with open(hist_file, 'rb') as fobj:
                 history = pickle.load(fobj)
@@ -3619,7 +3604,7 @@
             # If any non-operating system error occurs when attempting to 
unpickle, just use an empty history
             pass
         except OSError as ex:
-            msg = "can not read persistent history file '{}': {}"
+            msg = "Can not read persistent history file '{}': {}"
             self.pexcept(msg.format(hist_file, ex))
             return
 
@@ -3655,7 +3640,7 @@
             with open(self.persistent_history_file, 'wb') as fobj:
                 pickle.dump(self.history, fobj)
         except OSError as ex:
-            msg = "can not write persistent history file '{}': {}"
+            msg = "Can not write persistent history file '{}': {}"
             self.pexcept(msg.format(self.persistent_history_file, ex))
 
     def _generate_transcript(self, history: List[Union[HistoryItem, str]], 
transcript_file: str) -> None:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cmd2-0.9.15/cmd2/parsing.py 
new/cmd2-0.9.16/cmd2/parsing.py
--- old/cmd2-0.9.15/cmd2/parsing.py     2019-07-25 00:50:26.000000000 +0200
+++ new/cmd2-0.9.16/cmd2/parsing.py     2019-08-07 15:12:28.000000000 +0200
@@ -185,7 +185,7 @@
         """Combine command and args with a space separating them.
 
         Quoted arguments remain quoted. Output redirection and piping are
-        excluded, as are any multiline command terminators.
+        excluded, as are any command terminators.
         """
         if self.command and self.args:
             rtn = '{} {}'.format(self.command, self.args)
@@ -245,7 +245,6 @@
     the expansion.
     """
     def __init__(self,
-                 allow_redirection: bool = True,
                  terminators: Optional[Iterable[str]] = None,
                  multiline_commands: Optional[Iterable[str]] = None,
                  aliases: Optional[Dict[str, str]] = None,
@@ -257,13 +256,11 @@
         * multiline commands
         * shortcuts
 
-        :param allow_redirection: should redirection and pipes be allowed?
-        :param terminators: iterable containing strings which should terminate 
multiline commands
+        :param terminators: iterable containing strings which should terminate 
commands
         :param multiline_commands: iterable containing the names of commands 
that accept multiline input
         :param aliases: dictionary containing aliases
         :param shortcuts: dictionary containing shortcuts
         """
-        self.allow_redirection = allow_redirection
         if terminators is None:
             self.terminators = (constants.MULTILINE_TERMINATOR,)
         else:
@@ -359,20 +356,17 @@
                 errmsg = ''
         return valid, errmsg
 
-    def tokenize(self, line: str, *, expand: bool = True) -> List[str]:
+    def tokenize(self, line: str) -> List[str]:
         """
         Lex a string into a list of tokens. Shortcuts and aliases are expanded 
and comments are removed
 
         :param line: the command line being lexed
-        :param expand: If True, then aliases and shortcuts will be expanded.
-                       Set this to False if the command token should not be 
altered. Defaults to True.
         :return: A list of tokens
         :raises ValueError if there are unclosed quotation marks.
         """
 
         # expand shortcuts and aliases
-        if expand:
-            line = self._expand(line)
+        line = self._expand(line)
 
         # check if this line is a comment
         if line.lstrip().startswith(constants.COMMENT_CHAR):
@@ -382,18 +376,16 @@
         tokens = shlex_split(line)
 
         # custom lexing
-        tokens = self._split_on_punctuation(tokens)
+        tokens = self.split_on_punctuation(tokens)
         return tokens
 
-    def parse(self, line: str, *, expand: bool = True) -> Statement:
+    def parse(self, line: str) -> Statement:
         """
         Tokenize the input and parse it into a Statement object, stripping
         comments, expanding aliases and shortcuts, and extracting output
         redirection directives.
 
         :param line: the command line being parsed
-        :param expand: If True, then aliases and shortcuts will be expanded.
-                       Set this to False if the command token should not be 
altered. Defaults to True.
         :return: the created Statement
         :raises ValueError if there are unclosed quotation marks
         """
@@ -410,7 +402,7 @@
         arg_list = []
 
         # lex the input into a list of tokens
-        tokens = self.tokenize(line, expand=expand)
+        tokens = self.tokenize(line)
 
         # of the valid terminators, find the first one to occur in the input
         terminator_pos = len(tokens) + 1
@@ -532,7 +524,7 @@
                               output_to=output_to)
         return statement
 
-    def parse_command_only(self, rawinput: str, *, expand: bool = True) -> 
Statement:
+    def parse_command_only(self, rawinput: str) -> Statement:
         """Partially parse input into a Statement object.
 
         The command is identified, and shortcuts and aliases are expanded.
@@ -557,15 +549,12 @@
         whitespace.
 
         :param rawinput: the command line as entered by the user
-        :param expand: If True, then aliases and shortcuts will be expanded.
-                       Set this to False if the command token should not be 
altered. Defaults to True.
         :return: the created Statement
         """
         line = rawinput
 
         # expand shortcuts and aliases
-        if expand:
-            line = self._expand(rawinput)
+        line = self._expand(rawinput)
 
         command = ''
         args = ''
@@ -619,7 +608,7 @@
         """
         # Check if to_parse needs to be converted to a Statement
         if not isinstance(to_parse, Statement):
-            to_parse = self.parse(command_name + ' ' + to_parse, expand=False)
+            to_parse = self.parse(command_name + ' ' + to_parse)
 
         if preserve_quotes:
             return to_parse, to_parse.arg_list
@@ -678,7 +667,7 @@
 
         return command, args
 
-    def _split_on_punctuation(self, tokens: List[str]) -> List[str]:
+    def split_on_punctuation(self, tokens: List[str]) -> List[str]:
         """Further splits tokens from a command line using punctuation 
characters
 
         Punctuation characters are treated as word breaks when they are in
@@ -690,8 +679,7 @@
         """
         punctuation = []
         punctuation.extend(self.terminators)
-        if self.allow_redirection:
-            punctuation.extend(constants.REDIRECTION_CHARS)
+        punctuation.extend(constants.REDIRECTION_CHARS)
 
         punctuated_tokens = []
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cmd2-0.9.15/cmd2.egg-info/PKG-INFO 
new/cmd2-0.9.16/cmd2.egg-info/PKG-INFO
--- old/cmd2-0.9.15/cmd2.egg-info/PKG-INFO      2019-07-25 04:11:22.000000000 
+0200
+++ new/cmd2-0.9.16/cmd2.egg-info/PKG-INFO      2019-08-08 04:00:02.000000000 
+0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: cmd2
-Version: 0.9.15
+Version: 0.9.16
 Summary: cmd2 - quickly build feature-rich and user-friendly interactive 
command line applications in Python
 Home-page: https://github.com/python-cmd2/cmd2
 Author: Catherine Devlin
@@ -196,7 +196,7 @@
         
             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).
+            `terminators` optional argument to `cmd2.Cmd.__init__()`  .  The 
default terminators are `;` and `\n` (empty newline).
         
         - Special-character shortcut commands (beyond cmd's "@" and "!")
         
@@ -216,11 +216,10 @@
         Tutorials
         ---------
         
-        A few tutorials on using cmd2 exist:
-        
-        * Florida PyCon 2017 talk: 
[slides](https://docs.google.com/presentation/d/1LRmpfBt3V-pYQfgQHdczf16F3hcXmhK83tl77R6IJtE),
 [video](https://www.youtube.com/watch?v=6m0RdpITaeY)
-        * PyCon 2010 talk by Catherine Devlin, the original author: 
[video](http://pyvideo.org/pycon-us-2010/pycon-2010--easy-command-line-applications-with-c.html)
-        * A nice brief step-by-step tutorial: 
[blog](https://kushaldas.in/posts/developing-command-line-interpreters-using-python-cmd2.html)
+        * PyOhio 2019 presentation: 
+            * [video](https://www.youtube.com/watch?v=pebeWrTqIIw)
+            * 
[slides](https://github.com/python-cmd2/talks/blob/master/PyOhio_2019/cmd2-PyOhio_2019.pdf)
+            * [example 
code](https://github.com/python-cmd2/talks/tree/master/PyOhio_2019/examples)
         
         
         Example Application
@@ -400,5 +399,5 @@
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
 Requires-Python: >=3.5
 Description-Content-Type: text/markdown
-Provides-Extra: test
 Provides-Extra: dev
+Provides-Extra: test
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/cmd2-0.9.15/docs/features/shortcuts_aliases_macros.rst 
new/cmd2-0.9.16/docs/features/shortcuts_aliases_macros.rst
--- old/cmd2-0.9.15/docs/features/shortcuts_aliases_macros.rst  2019-07-19 
03:01:42.000000000 +0200
+++ new/cmd2-0.9.16/docs/features/shortcuts_aliases_macros.rst  2019-08-08 
02:38:15.000000000 +0200
@@ -38,6 +38,7 @@
   updating the ``shortcuts`` attribute  This warning applies in general to many
   other attributes which are not settable at runtime.
 
+Note: Command, alias, and macro names cannot start with a shortcut
 
 Aliases
 -------
@@ -65,6 +66,7 @@
 
 For more details run: ``help alias delete``
 
+Note: Aliases cannot have the same name as a command or macro
 
 Macros
 ------
@@ -93,3 +95,5 @@
 For more details on listing macros run: ``help macro list``
 
 For more details on deleting macros run: ``help macro delete``
+
+Note: Macros cannot have the same name as a command or alias
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cmd2-0.9.15/examples/async_printing.py 
new/cmd2-0.9.16/examples/async_printing.py
--- old/cmd2-0.9.15/examples/async_printing.py  2019-07-19 03:01:42.000000000 
+0200
+++ new/cmd2-0.9.16/examples/async_printing.py  2019-08-02 02:50:24.000000000 
+0200
@@ -139,12 +139,26 @@
         return alert_str
 
     def _generate_colored_prompt(self) -> str:
-        """Randomly generates a colored prompt
+        """
+        Randomly generates a colored prompt
         :return: the new prompt
         """
-        fg_color = random.choice(list(ansi.FG_COLORS.keys()))
-        bg_color = random.choice(list(ansi.BG_COLORS.keys()))
-        return ansi.style(self.visible_prompt.rstrip(), fg=fg_color, 
bg=bg_color) + ' '
+        rand_num = random.randint(1, 20)
+
+        status_color = 'reset'
+
+        if rand_num == 1:
+            status_color = 'bright_red'
+        elif rand_num == 2:
+            status_color = 'bright_yellow'
+        elif rand_num == 3:
+            status_color = 'cyan'
+        elif rand_num == 4:
+            status_color = 'bright_green'
+        elif rand_num == 5:
+            status_color = 'bright_blue'
+
+        return ansi.style(self.visible_prompt, fg=status_color)
 
     def _alerter_thread_func(self) -> None:
         """ Prints alerts and updates the prompt any time the prompt is 
showing """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cmd2-0.9.15/tests/test_argparse_custom.py 
new/cmd2-0.9.16/tests/test_argparse_custom.py
--- old/cmd2-0.9.15/tests/test_argparse_custom.py       2019-07-19 
03:01:42.000000000 +0200
+++ new/cmd2-0.9.16/tests/test_argparse_custom.py       2019-08-06 
04:42:05.000000000 +0200
@@ -39,25 +39,49 @@
     pass
 
 
-@pytest.mark.parametrize('args, is_valid', [
-    ({'choices': []}, True),
+@pytest.mark.parametrize('kwargs, is_valid', [
     ({'choices_function': fake_func}, True),
     ({'choices_method': fake_func}, True),
     ({'completer_function': fake_func}, True),
     ({'completer_method': fake_func}, True),
-    ({'choices': [], 'choices_function': fake_func}, False),
-    ({'choices': [], 'choices_method': fake_func}, False),
+    ({'choices_function': fake_func, 'choices_method': fake_func}, False),
     ({'choices_method': fake_func, 'completer_function': fake_func}, False),
-    ({'choices_method': fake_func, 'completer_method': fake_func}, False),
+    ({'completer_function': fake_func, 'completer_method': fake_func}, False),
 ])
-def test_apcustom_invalid_args(args, is_valid):
+def test_apcustom_choices_callable_count(kwargs, is_valid):
     parser = Cmd2ArgumentParser(prog='test')
     try:
-        parser.add_argument('name', **args)
+        parser.add_argument('name', **kwargs)
         assert is_valid
     except ValueError as ex:
         assert not is_valid
-        assert 'Only one of the following may be used' in str(ex)
+        assert 'Only one of the following parameters' in str(ex)
+
+
+@pytest.mark.parametrize('kwargs', [
+    ({'choices_function': fake_func}),
+    ({'choices_method': fake_func}),
+    ({'completer_function': fake_func}),
+    ({'completer_method': fake_func})
+])
+def test_apcustom_no_choices_callables_alongside_choices(kwargs):
+    with pytest.raises(TypeError) as excinfo:
+        parser = Cmd2ArgumentParser(prog='test')
+        parser.add_argument('name', choices=['my', 'choices', 'list'], 
**kwargs)
+    assert 'None of the following parameters can be used alongside a choices 
parameter' in str(excinfo.value)
+
+
+@pytest.mark.parametrize('kwargs', [
+    ({'choices_function': fake_func}),
+    ({'choices_method': fake_func}),
+    ({'completer_function': fake_func}),
+    ({'completer_method': fake_func})
+])
+def test_apcustom_no_choices_callables_when_nargs_is_0(kwargs):
+    with pytest.raises(TypeError) as excinfo:
+        parser = Cmd2ArgumentParser(prog='test')
+        parser.add_argument('name', action='store_true', **kwargs)
+    assert 'None of the following parameters can be used on an action that 
takes no arguments' in str(excinfo.value)
 
 
 def test_apcustom_usage():
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cmd2-0.9.15/tests/test_cmd2.py 
new/cmd2-0.9.16/tests/test_cmd2.py
--- old/cmd2-0.9.15/tests/test_cmd2.py  2019-07-25 00:50:26.000000000 +0200
+++ new/cmd2-0.9.16/tests/test_cmd2.py  2019-08-08 02:38:15.000000000 +0200
@@ -87,6 +87,10 @@
     expected = normalize(SHORTCUTS_TXT)
     assert out == expected
 
+def test_command_starts_with_shortcut():
+    with pytest.raises(ValueError) as excinfo:
+        app = cmd2.Cmd(shortcuts={'help': 'fake'})
+    assert "Invalid command name 'help'" in str(excinfo.value)
 
 def test_base_show(base_app):
     # force editor to be 'vim' so test is repeatable across platforms
@@ -558,7 +562,7 @@
 
 def test_disallow_redirection(base_app):
     # Set allow_redirection to False
-    base_app.statement_parser.allow_redirection = False
+    base_app.allow_redirection = False
 
     filename = 'test_allow_redirect.txt'
 
@@ -639,7 +643,7 @@
 
     expected_text = normalize("""
 EXCEPTION of type '{}' occurred with message: 'Please use 'set editor' to 
specify your text editing program of choice.'
-To enable full traceback, run the following command:  'set debug true'
+To enable full traceback, run the following command: 'set debug true'
 """.format(expected_exception))
 
     return expected_text
@@ -1098,6 +1102,7 @@
     arg = 'Sauce? '
     calls = [mock.call(arg), mock.call(arg)]
     m.assert_has_calls(calls)
+    assert m.call_count == 2
 
     # And verify the expected output to stdout
     assert out == expected
@@ -1122,6 +1127,7 @@
     arg = 'Sauce? '
     calls = [mock.call(arg), mock.call(arg)]
     m.assert_has_calls(calls)
+    assert m.call_count == 2
 
     # And verify the expected output to stdout
     assert out == expected
@@ -1181,6 +1187,19 @@
     # And verify the expected output to stdout
     assert out == expected
 
+def test_select_eof(select_app):
+    # Ctrl-D during select causes an EOFError that just reprompts the user
+    m = mock.MagicMock(name='input', side_effect=[EOFError, 2])
+    builtins.input = m
+
+    food = 'fish'
+    out, err = run_cmd(select_app, "eat {}".format(food))
+
+    # Make sure our mock was called exactly twice with the expected arguments
+    arg = 'Sauce? '
+    calls = [mock.call(arg), mock.call(arg)]
+    m.assert_has_calls(calls)
+    assert m.call_count == 2
 
 class HelpNoDocstringApp(cmd2.Cmd):
     greet_parser = argparse.ArgumentParser()
@@ -1625,6 +1644,10 @@
     out, err = run_cmd(base_app, 'alias create {} help'.format(alias_name))
     assert "Invalid alias name" in err[0]
 
+def test_alias_create_with_command_name(base_app):
+    out, err = run_cmd(base_app, 'alias create help stuff')
+    assert "Alias cannot have the same name as a command" in err[0]
+
 def test_alias_create_with_macro_name(base_app):
     macro = "my_macro"
     run_cmd(base_app, 'macro create {} help'.format(macro))
@@ -1713,19 +1736,16 @@
     out, err = run_cmd(base_app, 'macro create {} help'.format(macro_name))
     assert "Invalid macro name" in err[0]
 
+def test_macro_create_with_command_name(base_app):
+    out, err = run_cmd(base_app, 'macro create help stuff')
+    assert "Macro cannot have the same name as a command" in err[0]
+
 def test_macro_create_with_alias_name(base_app):
     macro = "my_macro"
     run_cmd(base_app, 'alias create {} help'.format(macro))
     out, err = run_cmd(base_app, 'macro create {} help'.format(macro))
     assert "Macro cannot have the same name as an alias" in err[0]
 
-def test_macro_create_with_command_name(multiline_app):
-    out, err = run_cmd(multiline_app, 'macro create help stuff')
-    assert out == normalize("Macro 'help' created")
-
-    out, err = run_cmd(multiline_app, 'macro create orate stuff')
-    assert "Macro cannot have the same name as a multiline command" in err[0]
-
 def test_macro_create_with_args(base_app):
     # Create the macro
     out, err = run_cmd(base_app, 'macro create fake {1} {2}')
@@ -1843,37 +1863,6 @@
 
     assert exception is not None
 
-def test_input_line_to_statement_expand(base_app):
-    # Enable/Disable expansion of shortcuts
-    line = '!ls'
-    statement = base_app._input_line_to_statement(line, expand=True)
-    assert statement.command == 'shell'
-
-    statement = base_app._input_line_to_statement(line, expand=False)
-    assert statement.command == '!ls'
-
-    # Enable/Disable expansion of aliases
-    run_cmd(base_app, 'alias create help macro')
-
-    line = 'help'
-    statement = base_app._input_line_to_statement(line, expand=True)
-    assert statement.command == 'macro'
-
-    statement = base_app._input_line_to_statement(line, expand=False)
-    assert statement.command == 'help'
-
-    run_cmd(base_app, 'alias delete help')
-
-    # Enable/Disable expansion of macros
-    run_cmd(base_app, 'macro create help alias')
-
-    line = 'help'
-    statement = base_app._input_line_to_statement(line, expand=True)
-    assert statement.command == 'alias'
-
-    statement = base_app._input_line_to_statement(line, expand=False)
-    assert statement.command == 'help'
-
 def test_ppaged(outsim_app):
     msg = 'testing...'
     end = '\n'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cmd2-0.9.15/tests/test_completion.py 
new/cmd2-0.9.16/tests/test_completion.py
--- old/cmd2-0.9.15/tests/test_completion.py    2019-07-24 03:41:45.000000000 
+0200
+++ new/cmd2-0.9.16/tests/test_completion.py    2019-08-03 17:16:28.000000000 
+0200
@@ -689,46 +689,32 @@
     assert expected_tokens == tokens
     assert expected_raw_tokens == raw_tokens
 
-def test_tokens_for_completion_redirect(cmd2_app):
-    text = '>>file'
-    line = 'command | < {}'.format(text)
+def test_tokens_for_completion_punctuation(cmd2_app):
+    """Test that redirectors and terminators are word delimiters"""
+    text = 'file'
+    line = 'command | < ;>>{}'.format(text)
     endidx = len(line)
     begidx = endidx - len(text)
 
-    cmd2_app.allow_redirection = True
-    expected_tokens = ['command', '|', '<', '>>', 'file']
-    expected_raw_tokens = ['command', '|', '<', '>>', 'file']
+    expected_tokens = ['command', '|', '<', ';', '>>', 'file']
+    expected_raw_tokens = ['command', '|', '<', ';', '>>', 'file']
 
     tokens, raw_tokens = cmd2_app.tokens_for_completion(line, begidx, endidx)
     assert expected_tokens == tokens
     assert expected_raw_tokens == raw_tokens
 
-def test_tokens_for_completion_quoted_redirect(cmd2_app):
+def test_tokens_for_completion_quoted_punctuation(cmd2_app):
+    """Test that quoted punctuation characters are not word delimiters"""
     text = '>file'
     line = 'command "{}'.format(text)
     endidx = len(line)
     begidx = endidx - len(text)
 
-    cmd2_app.statement_parser.redirection = True
     expected_tokens = ['command', '>file']
     expected_raw_tokens = ['command', '">file']
 
     tokens, raw_tokens = cmd2_app.tokens_for_completion(line, begidx, endidx)
     assert expected_tokens == tokens
-    assert expected_raw_tokens == raw_tokens
-
-def test_tokens_for_completion_redirect_off(cmd2_app):
-    text = '>file'
-    line = 'command {}'.format(text)
-    endidx = len(line)
-    begidx = endidx - len(text)
-
-    cmd2_app.statement_parser.allow_redirection = False
-    expected_tokens = ['command', '>file']
-    expected_raw_tokens = ['command', '>file']
-
-    tokens, raw_tokens = cmd2_app.tokens_for_completion(line, begidx, endidx)
-    assert expected_tokens == tokens
     assert expected_raw_tokens == raw_tokens
 
 def test_add_opening_quote_basic_no_text(cmd2_app):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cmd2-0.9.15/tests/test_history.py 
new/cmd2-0.9.16/tests/test_history.py
--- old/cmd2-0.9.15/tests/test_history.py       2019-07-25 00:50:26.000000000 
+0200
+++ new/cmd2-0.9.16/tests/test_history.py       2019-08-02 14:21:44.000000000 
+0200
@@ -268,7 +268,6 @@
 def parser():
     from cmd2.parsing import StatementParser
     parser = StatementParser(
-        allow_redirection=True,
         terminators=[';', '&'],
         multiline_commands=['multiline'],
         aliases={'helpalias': 'help',
@@ -638,6 +637,36 @@
         _, err = capsys.readouterr()
         assert 'is a directory' in err
 
+def test_history_can_create_directory(mocker):
+    # Mock out atexit.register so the persistent file doesn't written when 
this function
+    # exists because we will be deleting the directory it needs to go to.
+    mock_register = mocker.patch('atexit.register')
+
+    # Create a temp path for us to use and let it get deleted
+    with tempfile.TemporaryDirectory() as test_dir:
+        pass
+    assert not os.path.isdir(test_dir)
+
+    # Add some subdirectories for the complete history file directory
+    hist_file_dir = os.path.join(test_dir, 'subdir1', 'subdir2')
+    hist_file = os.path.join(hist_file_dir, 'hist_file')
+
+    # Make sure cmd2 creates the history file directory
+    cmd2.Cmd(persistent_history_file=hist_file)
+    assert os.path.isdir(hist_file_dir)
+
+    # Cleanup
+    os.rmdir(hist_file_dir)
+
+def test_history_cannot_create_directory(mocker, capsys):
+    mock_open = mocker.patch('os.makedirs')
+    mock_open.side_effect = OSError
+
+    hist_file_path = os.path.join('fake_dir', 'file')
+    cmd2.Cmd(persistent_history_file=hist_file_path)
+    _, err = capsys.readouterr()
+    assert 'Error creating persistent history file directory' in err
+
 def test_history_file_permission_error(mocker, capsys):
     mock_open = mocker.patch('builtins.open')
     mock_open.side_effect = PermissionError
@@ -645,7 +674,7 @@
     cmd2.Cmd(persistent_history_file='/tmp/doesntmatter')
     out, err = capsys.readouterr()
     assert not out
-    assert 'can not read' in err
+    assert 'Can not read' in err
 
 def test_history_file_conversion_no_truncate_on_init(hist_file, capsys):
     # make sure we don't truncate the plain text history file on init
@@ -720,4 +749,4 @@
     app._persist_history()
     out, err = capsys.readouterr()
     assert not out
-    assert 'can not write' in err
+    assert 'Can not write' in err
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cmd2-0.9.15/tests/test_parsing.py 
new/cmd2-0.9.16/tests/test_parsing.py
--- old/cmd2-0.9.15/tests/test_parsing.py       2019-07-19 03:01:42.000000000 
+0200
+++ new/cmd2-0.9.16/tests/test_parsing.py       2019-08-07 15:12:28.000000000 
+0200
@@ -13,7 +13,6 @@
 @pytest.fixture
 def parser():
     parser = StatementParser(
-        allow_redirection=True,
         terminators=[';', '&'],
         multiline_commands=['multiline'],
         aliases={'helpalias': 'help',
@@ -605,19 +604,11 @@
     ('l', 'shell', 'ls -al')
 ])
 def test_parse_alias_and_shortcut_expansion(parser, line, command, args):
-    # Test first with expansion
     statement = parser.parse(line)
     assert statement.command == command
     assert statement == args
     assert statement.args == statement
 
-    # Now allow no expansion
-    tokens = shlex_split(line)
-    statement = parser.parse(line, expand=False)
-    assert statement.command == tokens[0]
-    assert shlex_split(statement) == tokens[1:]
-    assert statement.args == statement
-
 def test_parse_alias_on_multiline_command(parser):
     line = 'anothermultiline has > inside an unfinished command'
     statement = parser.parse(line)


Reply via email to