Repository: python-cmd2/cmd2

Issue:
self.statement_parser was changed to self._statement_parser even though Docs suggest to use it.
self.statement_parser was changed to self._statement_parser, but in the docs, we have put it as a part of the processing hook (see [Postparsing Hooks](https://cmd2.readthedocs.io/en/latest/features/hooks.html) section)


Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1261bc49..6b7bc5c3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@
         * `pyscript` - replaced by `run_pyscript`
     * We apologize for any inconvenience, but the new names are more self-descriptive
         * Lots of end users were confused particularly about what exactly `load` should be loading
+* Breaking Changes
+    * Restored `cmd2.Cmd.statement_parser` to be a public attribute (no underscore) 
+        * Since it can be useful for creating [post-parsing hooks](https://cmd2.readthedocs.io/en/latest/features/hooks.html#postparsing-hooks)
+    
 
 ## 0.9.14 (June 29, 2019)
 * Enhancements
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 26636c41..3aa4beb3 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -146,9 +146,9 @@ def with_argument_list(*args: List[Callable], preserve_quotes: bool = False) ->
     def arg_decorator(func: Callable):
         @functools.wraps(func)
         def cmd_wrapper(cmd2_instance, statement: Union[Statement, str]):
-            _, parsed_arglist = cmd2_instance._statement_parser.get_command_arg_list(command_name,
-                                                                                     statement,
-                                                                                     preserve_quotes)
+            _, parsed_arglist = cmd2_instance.statement_parser.get_command_arg_list(command_name,
+                                                                                    statement,
+                                                                                    preserve_quotes)
 
             return func(cmd2_instance, parsed_arglist)
 
@@ -185,9 +185,9 @@ def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser, *,
     def arg_decorator(func: Callable):
         @functools.wraps(func)
         def cmd_wrapper(cmd2_instance, statement: Union[Statement, str]):
-            statement, parsed_arglist = cmd2_instance._statement_parser.get_command_arg_list(command_name,
-                                                                                             statement,
-                                                                                             preserve_quotes)
+            statement, parsed_arglist = cmd2_instance.statement_parser.get_command_arg_list(command_name,
+                                                                                            statement,
+                                                                                            preserve_quotes)
 
             if ns_provider is None:
                 namespace = None
@@ -243,9 +243,9 @@ def with_argparser(argparser: argparse.ArgumentParser, *,
     def arg_decorator(func: Callable):
         @functools.wraps(func)
         def cmd_wrapper(cmd2_instance, statement: Union[Statement, str]):
-            statement, parsed_arglist = cmd2_instance._statement_parser.get_command_arg_list(command_name,
-                                                                                             statement,
-                                                                                             preserve_quotes)
+            statement, parsed_arglist = cmd2_instance.statement_parser.get_command_arg_list(command_name,
+                                                                                            statement,
+                                                                                            preserve_quotes)
 
             if ns_provider is None:
                 namespace = None
@@ -393,10 +393,10 @@ class Cmd(cmd.Cmd):
         if shortcuts is None:
             shortcuts = constants.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.statement_parser = StatementParser(allow_redirection=allow_redirection,
+                                                terminators=terminators,
+                                                multiline_commands=multiline_commands,
+                                                shortcuts=shortcuts)
 
         # True if running inside a Python script or interactive console, False otherwise
         self._in_py = False
@@ -561,17 +561,17 @@ class Cmd(cmd.Cmd):
     @property
     def aliases(self) -> Dict[str, str]:
         """Read-only property to access the aliases stored in the StatementParser."""
-        return self._statement_parser.aliases
+        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
+        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
+        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
@@ -1385,7 +1385,7 @@ class Cmd(cmd.Cmd):
             # from text and update the indexes. This only applies if we are at the the beginning of the line.
             shortcut_to_restore = ''
             if begidx == 0:
-                for (shortcut, _) in self._statement_parser.shortcuts:
+                for (shortcut, _) in self.statement_parser.shortcuts:
                     if text.startswith(shortcut):
                         # Save the shortcut to restore later
                         shortcut_to_restore = shortcut
@@ -1399,7 +1399,7 @@ class Cmd(cmd.Cmd):
             if begidx > 0:
 
                 # Parse the command line
-                statement = self._statement_parser.parse_command_only(line)
+                statement = self.statement_parser.parse_command_only(line)
                 command = statement.command
                 expanded_line = statement.command_and_args
 
@@ -1678,7 +1678,7 @@ class Cmd(cmd.Cmd):
         :param line: line read by readline
         :return: tuple containing (command, args, line)
         """
-        statement = self._statement_parser.parse_command_only(line)
+        statement = self.statement_parser.parse_command_only(line)
         return statement.command, statement.args, statement.command_and_args
 
     def onecmd_plus_hooks(self, line: str, pyscript_bridge_call: bool = False) -> bool:
@@ -1842,7 +1842,7 @@ class Cmd(cmd.Cmd):
         """
         while True:
             try:
-                statement = self._statement_parser.parse(line)
+                statement = self.statement_parser.parse(line)
                 if statement.multiline_command and statement.terminator:
                     # we have a completed multiline command, we are done
                     break
@@ -1853,7 +1853,7 @@ class Cmd(cmd.Cmd):
             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)
+                statement = self.statement_parser.parse_command_only(line)
                 if not statement.multiline_command:
                     # not a multiline command, so raise the exception
                     raise
@@ -1877,7 +1877,7 @@ class Cmd(cmd.Cmd):
                     raise ex
                 else:
                     self.poutput('^C')
-                    statement = self._statement_parser.parse('')
+                    statement = self.statement_parser.parse('')
                     break
             finally:
                 self._at_continuation_prompt = False
@@ -2290,7 +2290,7 @@ class Cmd(cmd.Cmd):
         """Create or overwrite an alias"""
 
         # Validate the alias name
-        valid, errmsg = self._statement_parser.is_valid_command(args.name)
+        valid, errmsg = self.statement_parser.is_valid_command(args.name)
         if not valid:
             self.perror("Invalid alias name: {}".format(errmsg))
             return
@@ -2301,7 +2301,7 @@ class Cmd(cmd.Cmd):
 
         # Unquote redirection and terminator tokens
         tokens_to_unquote = constants.REDIRECTION_TOKENS
-        tokens_to_unquote.extend(self._statement_parser.terminators)
+        tokens_to_unquote.extend(self.statement_parser.terminators)
         utils.unquote_specific_tokens(args.command_args, tokens_to_unquote)
 
         # Build the alias value string
@@ -2421,7 +2421,7 @@ class Cmd(cmd.Cmd):
         """Create or overwrite a macro"""
 
         # Validate the macro name
-        valid, errmsg = self._statement_parser.is_valid_command(args.name)
+        valid, errmsg = self.statement_parser.is_valid_command(args.name)
         if not valid:
             self.perror("Invalid macro name: {}".format(errmsg))
             return
@@ -2436,7 +2436,7 @@ class Cmd(cmd.Cmd):
 
         # Unquote redirection and terminator tokens
         tokens_to_unquote = constants.REDIRECTION_TOKENS
-        tokens_to_unquote.extend(self._statement_parser.terminators)
+        tokens_to_unquote.extend(self.statement_parser.terminators)
         utils.unquote_specific_tokens(args.command_args, tokens_to_unquote)
 
         # Build the macro value string
@@ -2830,7 +2830,7 @@ class Cmd(cmd.Cmd):
     @with_argparser(ACArgumentParser())
     def do_shortcuts(self, _: argparse.Namespace) -> None:
         """List available shortcuts"""
-        result = "\n".join('%s: %s' % (sc[0], sc[1]) for sc in sorted(self._statement_parser.shortcuts))
+        result = "\n".join('%s: %s' % (sc[0], sc[1]) for sc in sorted(self.statement_parser.shortcuts))
         self.poutput("Shortcuts for other commands:\n{}".format(result))
 
     @with_argparser(ACArgumentParser(epilog=INTERNAL_COMMAND_EPILOG))
@@ -2899,7 +2899,7 @@ class Cmd(cmd.Cmd):
         read_only_settings = """
         Commands may be terminated with: {}
         Output redirection and pipes allowed: {}"""
-        return read_only_settings.format(str(self._statement_parser.terminators), self.allow_redirection)
+        return read_only_settings.format(str(self.statement_parser.terminators), self.allow_redirection)
 
     def _show(self, args: argparse.Namespace, parameter: str = '') -> None:
         """Shows current settings of parameters.
diff --git a/cmd2/parsing.py b/cmd2/parsing.py
index 86087db1..2e94516a 100644
--- a/cmd2/parsing.py
+++ b/cmd2/parsing.py
@@ -324,7 +324,7 @@ class StatementParser:
         This string is suitable for inclusion in an error message of your
         choice:
 
-        valid, errmsg = _statement_parser.is_valid_command('>')
+        valid, errmsg = statement_parser.is_valid_command('>')
         if not valid:
             errmsg = "Alias {}".format(errmsg)
         """
diff --git a/examples/hooks.py b/examples/hooks.py
index acd427cd..409c8979 100755
--- a/examples/hooks.py
+++ b/examples/hooks.py
@@ -61,7 +61,7 @@ class CmdLineApp(cmd2.Cmd):
         command_pattern = re.compile(r'^([^\s\d]+)(\d+)')
         match = command_pattern.search(command)
         if match:
-            data.statement = self._statement_parser.parse("{} {} {}".format(
+            data.statement = self.statement_parser.parse("{} {} {}".format(
                 match.group(1),
                 match.group(2),
                 '' if data.statement.args is None else data.statement.args
@@ -71,7 +71,7 @@ class CmdLineApp(cmd2.Cmd):
     def downcase_hook(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
         """A hook to make uppercase commands lowercase."""
         command = data.statement.command.lower()
-        data.statement = self._statement_parser.parse("{} {}".format(
+        data.statement = self.statement_parser.parse("{} {}".format(
             command,
             '' if data.statement.args is None else data.statement.args
         ))
@@ -85,7 +85,7 @@ class CmdLineApp(cmd2.Cmd):
             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)
+                data.statement = self.statement_parser.parse(raw)
         return data
 
     @cmd2.with_argument_list


Test patch:
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index c9a41033..9ffe547a 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -109,7 +109,7 @@ def test_base_show_readonly(base_app):
     expected = normalize(SHOW_TXT + '\nRead only settings:' + """
         Commands may be terminated with: {}
         Output redirection and pipes allowed: {}
-""".format(base_app._statement_parser.terminators, base_app.allow_redirection))
+""".format(base_app.statement_parser.terminators, base_app.allow_redirection))
     assert out == expected
 
 
@@ -532,7 +532,7 @@ def test_feedback_to_output_false(base_app):
 
 def test_disallow_redirection(base_app):
     # Set allow_redirection to False
-    base_app._statement_parser.allow_redirection = False
+    base_app.statement_parser.allow_redirection = False
 
     filename = 'test_allow_redirect.txt'
 
diff --git a/tests/test_completion.py b/tests/test_completion.py
index eea34ba6..5cfc741c 100644
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -703,7 +703,7 @@ def test_tokens_for_completion_quoted_redirect(cmd2_app):
     endidx = len(line)
     begidx = endidx - len(text)
 
-    cmd2_app._statement_parser.redirection = True
+    cmd2_app.statement_parser.redirection = True
     expected_tokens = ['command', '>file']
     expected_raw_tokens = ['command', '">file']
 
@@ -717,7 +717,7 @@ def test_tokens_for_completion_redirect_off(cmd2_app):
     endidx = len(line)
     begidx = endidx - len(text)
 
-    cmd2_app._statement_parser.allow_redirection = False
+    cmd2_app.statement_parser.allow_redirection = False
     expected_tokens = ['command', '>file']
     expected_raw_tokens = ['command', '>file']