Repository: python-cmd2/cmd2
Pull request #642 (2019-03-10 00:39:23)

StdSim.pause_storage
Added a way to pause storing output in StdSim.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cc63af6b..e7a985b7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,8 @@
         * Added **-v**, **--verbose** flag
             * display history and include expanded commands if they differ from the typed command
     * Added ``matches_sort_key`` to override the default way tab completion matches are sorted
+    * Added ``StdSim.pause_storage`` member which when True will cause ``StdSim`` to not save the output sent to it.
+      See documentation for ``CommandResult`` in ``pyscript_bridge.py`` for reasons pausing the storage can be useful.
 * Potentially breaking changes
     * Made ``cmd2_app`` a positional and required argument of ``AutoCompleter`` since certain functionality now
     requires that it can't be ``None``.
diff --git a/cmd2/pyscript_bridge.py b/cmd2/pyscript_bridge.py
index 6c14ff1d..f3ce841d 100644
--- a/cmd2/pyscript_bridge.py
+++ b/cmd2/pyscript_bridge.py
@@ -25,9 +25,27 @@ class CommandResult(namedtuple_with_defaults('CommandResult', ['stdout', 'stderr
     Named tuple attributes
     ----------------------
     stdout: str - Output captured from stdout while this command is executing
-    stderr: str - Output captured from stderr while this command is executing. None if no error captured
+    stderr: str - Output captured from stderr while this command is executing. None if no error captured.
     data - Data returned by the command.
 
+    Any combination of these fields can be used when developing a scripting API for a given command.
+    By default stdout and stderr will be captured for you. If there is additional command specific data,
+    then write that to cmd2's _last_result member. That becomes the data member of this tuple.
+
+    In some cases, the data member may contain everything needed for a command and storing stdout
+    and stderr might just be a duplication of data that wastes memory. In that case, the StdSim can
+    be told not to store output with its pause_storage member. While this member is True, any output
+    sent to StdSim won't be saved in its buffer.
+
+    The code would look like this:
+        if isinstance(self.stdout, StdSim):
+            self.stdout.pause_storage = True
+
+        if isinstance(sys.stderr, StdSim):
+            sys.stderr.pause_storage = True
+
+    See StdSim class in utils.py for more information
+
     NOTE: Named tuples are immutable.  So the contents are there for access, not for modification.
     """
     def __bool__(self) -> bool:
@@ -67,25 +85,25 @@ class PyscriptBridge(object):
         if echo is None:
             echo = self.cmd_echo
 
-        copy_stdout = StdSim(sys.stdout, echo)
-        copy_stderr = StdSim(sys.stderr, echo)
-
+        # This will be used to capture _cmd2_app.stdout and sys.stdout
         copy_cmd_stdout = StdSim(self._cmd2_app.stdout, echo)
 
+        # This will be used to capture sys.stderr
+        copy_stderr = StdSim(sys.stderr, echo)
+
         self._cmd2_app._last_result = None
 
         try:
             self._cmd2_app.stdout = copy_cmd_stdout
-            with redirect_stdout(copy_stdout):
+            with redirect_stdout(copy_cmd_stdout):
                 with redirect_stderr(copy_stderr):
                     # Include a newline in case it's a multiline command
                     self._cmd2_app.onecmd_plus_hooks(command + '\n')
         finally:
             self._cmd2_app.stdout = copy_cmd_stdout.inner_stream
 
-        # if stderr is empty, set it to None
-        stderr = copy_stderr.getvalue() if copy_stderr.getvalue() else None
-
-        outbuf = copy_cmd_stdout.getvalue() if copy_cmd_stdout.getvalue() else copy_stdout.getvalue()
-        result = CommandResult(stdout=outbuf, stderr=stderr, data=self._cmd2_app._last_result)
+        # Save the output. If stderr is empty, set it to None.
+        result = CommandResult(stdout=copy_cmd_stdout.getvalue(),
+                               stderr=copy_stderr.getvalue() if copy_stderr.getvalue() else None,
+                               data=self._cmd2_app._last_result)
         return result
diff --git a/cmd2/utils.py b/cmd2/utils.py
index 098ed41d..a8760a65 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -261,28 +261,10 @@ def natural_sort(list_to_sort: Iterable[str]) -> List[str]:
 
 
 class StdSim(object):
-    """Class to simulate behavior of sys.stdout or sys.stderr.
-
+    """
+    Class to simulate behavior of sys.stdout or sys.stderr.
     Stores contents in internal buffer and optionally echos to the inner stream it is simulating.
     """
-    class ByteBuf(object):
-        """Inner class which stores an actual bytes buffer and does the actual output if echo is enabled."""
-        def __init__(self, inner_stream, echo: bool = False,
-                     encoding: str = 'utf-8', errors: str = 'replace') -> None:
-            self.byte_buf = b''
-            self.inner_stream = inner_stream
-            self.echo = echo
-            self.encoding = encoding
-            self.errors = errors
-
-        def write(self, b: bytes) -> None:
-            """Add bytes to internal bytes buffer and if echo is True, echo contents to inner stream."""
-            if not isinstance(b, bytes):
-                raise TypeError('a bytes-like object is required, not {}'.format(type(b)))
-            self.byte_buf += b
-            if self.echo:
-                self.inner_stream.buffer.write(b)
-
     def __init__(self, inner_stream, echo: bool = False,
                  encoding: str = 'utf-8', errors: str = 'replace') -> None:
         """
@@ -292,17 +274,20 @@ class StdSim(object):
         :param encoding: codec for encoding/decoding strings (defaults to utf-8)
         :param errors: how to handle encoding/decoding errors (defaults to replace)
         """
-        self.buffer = self.ByteBuf(inner_stream, echo)
         self.inner_stream = inner_stream
         self.echo = echo
         self.encoding = encoding
         self.errors = errors
+        self.pause_storage = False
+        self.buffer = ByteBuf(self)
 
     def write(self, s: str) -> None:
         """Add str to internal bytes buffer and if echo is True, echo contents to inner stream"""
         if not isinstance(s, str):
             raise TypeError('write() argument must be str, not {}'.format(type(s)))
-        self.buffer.byte_buf += s.encode(encoding=self.encoding, errors=self.errors)
+
+        if not self.pause_storage:
+            self.buffer.byte_buf += s.encode(encoding=self.encoding, errors=self.errors)
         if self.echo:
             self.inner_stream.write(s)
 
@@ -337,6 +322,24 @@ class StdSim(object):
             return getattr(self.inner_stream, item)
 
 
+class ByteBuf(object):
+    """
+    Used by StdSim to write binary data and stores the actual bytes written
+    """
+    def __init__(self, std_sim_instance: StdSim) -> None:
+        self.byte_buf = b''
+        self.std_sim_instance = std_sim_instance
+
+    def write(self, b: bytes) -> None:
+        """Add bytes to internal bytes buffer and if echo is True, echo contents to inner stream."""
+        if not isinstance(b, bytes):
+            raise TypeError('a bytes-like object is required, not {}'.format(type(b)))
+        if not self.std_sim_instance.pause_storage:
+            self.byte_buf += b
+        if self.std_sim_instance.echo:
+            self.std_sim_instance.inner_stream.buffer.write(b)
+
+
 def unquote_redirection_tokens(args: List[str]) -> None:
     """
     Unquote redirection tokens in a list of command-line arguments
diff --git a/docs/freefeatures.rst b/docs/freefeatures.rst
index 5c246798..bcb9c0e7 100644
--- a/docs/freefeatures.rst
+++ b/docs/freefeatures.rst
@@ -130,29 +130,13 @@ debugging your application.  To prevent users from enabling this ability
 manually you'll need to remove ``locals_in_py`` from the ``settable`` dictionary.
 
 The ``app`` object (or your custom name) provides access to application commands
-through either raw commands or through a python API wrapper.  For example, any
-application command call be called with ``app("<command>")``. All application
-commands are accessible as python objects and functions matching the command
-name.  For example, the following are equivalent:
+through raw commands.  For example, any application command call be called with
+``app("<command>")``.
 
 ::
 
     >>> app('say --piglatin Blah')
     lahBay
-    >>> app.say("Blah", piglatin=True)
-    lahBay
-
-
-Sub-commands are also supported. The following pairs are equivalent:
-
-::
-
-    >>> app('command subcmd1 subcmd2 param1 --myflag --otherflag 3')
-    >>> app.command.subcmd1.subcmd2('param1', myflag=True, otherflag=3)
-
-    >>> app('command subcmd1 param1 subcmd2 param2 --myflag --otherflag 3')
-    >>> app.command.subcmd1('param1').subcmd2('param2', myflag=True, otherflag=3)
-
 
 More Python examples:
 


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 75d4479a..307f69da 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -194,3 +194,25 @@ def test_stdsim_getattr_noexist(stdout_sim):
     # Here the StdSim getattr is allowing us to access methods defined by the inner stream
     assert not stdout_sim.isatty()
 
+def test_stdsim_pause_storage(stdout_sim):
+    # Test pausing storage for string data
+    my_str = 'Hello World'
+
+    stdout_sim.pause_storage = False
+    stdout_sim.write(my_str)
+    assert stdout_sim.read() == my_str
+
+    stdout_sim.pause_storage = True
+    stdout_sim.write(my_str)
+    assert stdout_sim.read() == ''
+
+    # Test pausing storage for binary data
+    b_str = b'Hello World'
+
+    stdout_sim.pause_storage = False
+    stdout_sim.buffer.write(b_str)
+    assert stdout_sim.readbytes() == b_str
+
+    stdout_sim.pause_storage = True
+    stdout_sim.buffer.write(b_str)
+    assert stdout_sim.getbytes() == b''