Repository: python-cmd2/cmd2 (python)

Issue:
Fix several hack classes built to simulate file descriptors
We have several hack classes built to simulate regular file descriptors. There are at least:

- transcript.py::OutputTrap
- conftest.py::StdOut
- pyscript_bridge.py::CopyStream

These should really be replaced with `StringIO` objects.



Pull request:
Fixed several hack classes built to simulate file descriptors
Now there is a single class, ``StdSim`` in utils.py, which is intended to simulate stdout and stderr file objects.

This class replaced the following:
- pyscript_bridge.py::CopyStream
- transcript.py::OutputTrap
- conftest.py::StdOut

The new class should also fix a bug related to a ``bytes`` object being written to stdout versus a ``str``.

This closes #497 

TODO:
------
- [x] Add unit test(s) related to sending bytes to a ``StdSim``

Patch:
diff --git a/cmd2/pyscript_bridge.py b/cmd2/pyscript_bridge.py
index 3f58ab84..d8fa5fb9 100644
--- a/cmd2/pyscript_bridge.py
+++ b/cmd2/pyscript_bridge.py
@@ -12,15 +12,15 @@ import functools
 import sys
 from typing import List, Callable
 
+from .argparse_completer import _RangeAction
+from .utils import namedtuple_with_defaults, StdSim
+
 # Python 3.4 require contextlib2 for temporarily redirecting stderr and stdout
 if sys.version_info < (3, 5):
     from contextlib2 import redirect_stdout, redirect_stderr
 else:
     from contextlib import redirect_stdout, redirect_stderr
 
-from .argparse_completer import _RangeAction
-from .utils import namedtuple_with_defaults
-
 
 class CommandResult(namedtuple_with_defaults('CommandResult', ['stdout', 'stderr', 'data'])):
     """Encapsulates the results from a command.
@@ -38,37 +38,12 @@ class CommandResult(namedtuple_with_defaults('CommandResult', ['stdout', 'stderr
         return not self.stderr and self.data is not None
 
 
-class CopyStream(object):
-    """Copies all data written to a stream"""
-    def __init__(self, inner_stream, echo: bool = False) -> None:
-        self.buffer = ''
-        self.inner_stream = inner_stream
-        self.echo = echo
-
-    def write(self, s):
-        self.buffer += s
-        if self.echo:
-            self.inner_stream.write(s)
-
-    def read(self):
-        raise NotImplementedError
-
-    def clear(self):
-        self.buffer = ''
-
-    def __getattr__(self, item: str):
-        if item in self.__dict__:
-            return self.__dict__[item]
-        else:
-            return getattr(self.inner_stream, item)
-
-
 def _exec_cmd(cmd2_app, func: Callable, echo: bool):
     """Helper to encapsulate executing a command and capturing the results"""
-    copy_stdout = CopyStream(sys.stdout, echo)
-    copy_stderr = CopyStream(sys.stderr, echo)
+    copy_stdout = StdSim(sys.stdout, echo)
+    copy_stderr = StdSim(sys.stderr, echo)
 
-    copy_cmd_stdout = CopyStream(cmd2_app.stdout, echo)
+    copy_cmd_stdout = StdSim(cmd2_app.stdout, echo)
 
     cmd2_app._last_result = None
 
@@ -81,9 +56,9 @@ def _exec_cmd(cmd2_app, func: Callable, echo: bool):
         cmd2_app.stdout = copy_cmd_stdout.inner_stream
 
     # if stderr is empty, set it to None
-    stderr = copy_stderr.buffer if copy_stderr.buffer else None
+    stderr = copy_stderr.getvalue() if copy_stderr.getvalue() else None
 
-    outbuf = copy_cmd_stdout.buffer if copy_cmd_stdout.buffer else copy_stdout.buffer
+    outbuf = copy_cmd_stdout.getvalue() if copy_cmd_stdout.getvalue() else copy_stdout.getvalue()
     result = CommandResult(stdout=outbuf, stderr=stderr, data=cmd2_app._last_result)
     return result
 
diff --git a/cmd2/transcript.py b/cmd2/transcript.py
index 8df58634..2d94f4e4 100644
--- a/cmd2/transcript.py
+++ b/cmd2/transcript.py
@@ -44,7 +44,7 @@ class Cmd2TestCase(unittest.TestCase):
 
             # Trap stdout
             self._orig_stdout = self.cmdapp.stdout
-            self.cmdapp.stdout = OutputTrap()
+            self.cmdapp.stdout = utils.StdSim(self.cmdapp.stdout)
 
     def runTest(self):  # was testall
         if self.cmdapp:
@@ -203,24 +203,3 @@ class Cmd2TestCase(unittest.TestCase):
         if self.cmdapp:
             # Restore stdout
             self.cmdapp.stdout = self._orig_stdout
-
-class OutputTrap(object):
-    """Instantiate an OutputTrap to divert/capture ALL stdout output.
-    For use in transcript testing.
-    """
-
-    def __init__(self):
-        self.contents = ''
-
-    def write(self, txt: str):
-        """Add text to the internal contents."""
-        self.contents += txt
-
-    def read(self) -> str:
-        """Read from the internal contents and then clear them out.
-
-        :return: str - text from the internal contents
-        """
-        result = self.contents
-        self.contents = ''
-        return result
diff --git a/cmd2/utils.py b/cmd2/utils.py
index 735221c8..bdb488cc 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -246,3 +246,61 @@ def natural_sort(list_to_sort: List[str]) -> List[str]:
     :return: the list sorted naturally
     """
     return sorted(list_to_sort, key=natural_keys)
+
+
+class StdSim(object):
+    """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) -> None:
+            self.byte_buf = b''
+            self.inner_stream = inner_stream
+            self.echo = echo
+
+        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) -> None:
+        self.buffer = self.ByteBuf(inner_stream, echo)
+        self.inner_stream = inner_stream
+
+    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)))
+        b = s.encode()
+        self.buffer.write(b)
+
+    def getvalue(self) -> str:
+        """Get the internal contents as a str.
+
+        :return string from the internal contents
+        """
+        return self.buffer.byte_buf.decode()
+
+    def read(self) -> str:
+        """Read from the internal contents as a str and then clear them out.
+
+        :return: string from the internal contents
+        """
+        result = self.getvalue()
+        self.clear()
+        return result
+
+    def clear(self) -> None:
+        """Clear the internal contents."""
+        self.buffer.byte_buf = b''
+
+    def __getattr__(self, item: str):
+        if item in self.__dict__:
+            return self.__dict__[item]
+        else:
+            return getattr(self.inner_stream, item)


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index a85135b9..561f281b 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -12,6 +12,7 @@ from unittest import mock
 from pytest import fixture
 
 import cmd2
+from cmd2.utils import StdSim
 
 # Prefer statically linked gnureadline if available (for macOS compatibility due to issues with libedit)
 try:
@@ -115,21 +116,6 @@ timing: False             # Report execution times
 """.format(color_str)
 
 
-class StdOut(object):
-    """ Toy class for replacing self.stdout in cmd2.Cmd instances for unit testing. """
-    def __init__(self):
-        self.buffer = ''
-
-    def write(self, s):
-        self.buffer += s
-
-    def read(self):
-        raise NotImplementedError
-
-    def clear(self):
-        self.buffer = ''
-
-
 def normalize(block):
     """ Normalize a block of text to perform comparison.
 
@@ -142,10 +128,10 @@ def normalize(block):
 
 
 def run_cmd(app, cmd):
-    """ Clear StdOut buffer, run the command, extract the buffer contents, """
+    """ Clear StdSim buffer, run the command, extract the buffer contents, """
     app.stdout.clear()
     app.onecmd_plus_hooks(cmd)
-    out = app.stdout.buffer
+    out = app.stdout.getvalue()
     app.stdout.clear()
     return normalize(out)
 
@@ -153,7 +139,7 @@ def run_cmd(app, cmd):
 @fixture
 def base_app():
     c = cmd2.Cmd()
-    c.stdout = StdOut()
+    c.stdout = StdSim(c.stdout)
     return c
 
 
diff --git a/tests/test_argparse.py b/tests/test_argparse.py
index 469cbe76..bf20710b 100644
--- a/tests/test_argparse.py
+++ b/tests/test_argparse.py
@@ -6,9 +6,9 @@ import argparse
 import pytest
 
 import cmd2
-from unittest import mock
+from cmd2.utils import StdSim
 
-from .conftest import run_cmd, StdOut
+from .conftest import run_cmd
 
 # Prefer statically linked gnureadline if available (for macOS compatibility due to issues with libedit)
 try:
@@ -115,7 +115,7 @@ class ArgparseApp(cmd2.Cmd):
 @pytest.fixture
 def argparse_app():
     app = ArgparseApp()
-    app.stdout = StdOut()
+    app.stdout = StdSim(app.stdout)
     return app
 
 
@@ -217,12 +217,12 @@ class SubcommandApp(cmd2.Cmd):
             func(self, args)
         else:
             # No subcommand was provided, so call help
-            self.do_help('base')
+            self.do_help(['base'])
 
 @pytest.fixture
 def subcommand_app():
     app = SubcommandApp()
-    app.stdout = StdOut()
+    app.stdout = StdSim(app.stdout)
     return app
 
 
diff --git a/tests/test_autocompletion.py b/tests/test_autocompletion.py
index 8aa26e0e..8035587a 100644
--- a/tests/test_autocompletion.py
+++ b/tests/test_autocompletion.py
@@ -1,3 +1,4 @@
+# coding=utf-8
 """
 Unit/functional testing for argparse completer in cmd2
 
@@ -5,16 +6,17 @@ Copyright 2018 Eric Lin <anselor@gmail.com>
 Released under MIT license, see LICENSE file
 """
 import pytest
-from .conftest import run_cmd, normalize, StdOut, complete_tester
+
+from cmd2.utils import StdSim
+from .conftest import run_cmd, normalize, complete_tester
 
 from examples.tab_autocompletion import TabCompleteExample
 
 @pytest.fixture
 def cmd2_app():
-    c = TabCompleteExample()
-    c.stdout = StdOut()
-
-    return c
+    app = TabCompleteExample()
+    app.stdout = StdSim(app.stdout)
+    return app
 
 
 SUGGEST_HELP = '''Usage: suggest -t {movie, show} [-h] [-d DURATION{1..2}]
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 1e7e2c3f..4b0687c9 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -25,7 +25,7 @@ import cmd2
 from cmd2 import clipboard
 from cmd2 import utils
 from .conftest import run_cmd, normalize, BASE_HELP, BASE_HELP_VERBOSE, \
-    HELP_HISTORY, SHORTCUTS_TXT, SHOW_TXT, SHOW_LONG, StdOut
+    HELP_HISTORY, SHORTCUTS_TXT, SHOW_TXT, SHOW_LONG
 
 
 def test_version(base_app):
@@ -930,7 +930,7 @@ def test_base_cmdloop_with_queue():
     app.use_rawinput = True
     intro = 'Hello World, this is an intro ...'
     app.cmdqueue.append('quit\n')
-    app.stdout = StdOut()
+    app.stdout = utils.StdSim(app.stdout)
 
     # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args
     testargs = ["prog"]
@@ -938,7 +938,7 @@ def test_base_cmdloop_with_queue():
     with mock.patch.object(sys, 'argv', testargs):
         # Run the command loop with custom intro
         app.cmdloop(intro=intro)
-    out = app.stdout.buffer
+    out = app.stdout.getvalue()
     assert out == expected
 
 
@@ -947,7 +947,7 @@ def test_base_cmdloop_without_queue():
     app = cmd2.Cmd()
     app.use_rawinput = True
     app.intro = 'Hello World, this is an intro ...'
-    app.stdout = StdOut()
+    app.stdout = utils.StdSim(app.stdout)
 
     # Mock out the input call so we don't actually wait for a user's response on stdin
     m = mock.MagicMock(name='input', return_value='quit')
@@ -959,7 +959,7 @@ def test_base_cmdloop_without_queue():
     with mock.patch.object(sys, 'argv', testargs):
         # Run the command loop
         app.cmdloop()
-    out = app.stdout.buffer
+    out = app.stdout.getvalue()
     assert out == expected
 
 
@@ -969,7 +969,7 @@ def test_cmdloop_without_rawinput():
     app.use_rawinput = False
     app.echo = False
     app.intro = 'Hello World, this is an intro ...'
-    app.stdout = StdOut()
+    app.stdout = utils.StdSim(app.stdout)
 
     # Mock out the input call so we don't actually wait for a user's response on stdin
     m = mock.MagicMock(name='input', return_value='quit')
@@ -981,7 +981,7 @@ def test_cmdloop_without_rawinput():
     with mock.patch.object(sys, 'argv', testargs):
         # Run the command loop
         app.cmdloop()
-    out = app.stdout.buffer
+    out = app.stdout.getvalue()
     assert out == expected
 
 
@@ -996,7 +996,7 @@ class HookFailureApp(cmd2.Cmd):
 @pytest.fixture
 def hook_failure():
     app = HookFailureApp()
-    app.stdout = StdOut()
+    app.stdout = utils.StdSim(app.stdout)
     return app
 
 def test_precmd_hook_success(base_app):
@@ -1020,7 +1020,7 @@ class SayApp(cmd2.Cmd):
 def say_app():
     app = SayApp()
     app.allow_cli_args = False
-    app.stdout = StdOut()
+    app.stdout = utils.StdSim(app.stdout)
     return app
 
 def test_interrupt_quit(say_app):
@@ -1034,7 +1034,7 @@ def test_interrupt_quit(say_app):
     say_app.cmdloop()
 
     # And verify the expected output to stdout
-    out = say_app.stdout.buffer
+    out = say_app.stdout.getvalue()
     assert out == 'hello\n'
 
 def test_interrupt_noquit(say_app):
@@ -1048,7 +1048,7 @@ def test_interrupt_noquit(say_app):
     say_app.cmdloop()
 
     # And verify the expected output to stdout
-    out = say_app.stdout.buffer
+    out = say_app.stdout.getvalue()
     assert out == 'hello\n^C\ngoodbye\n'
 
 
@@ -1060,7 +1060,7 @@ class ShellApp(cmd2.Cmd):
 @pytest.fixture
 def shell_app():
     app = ShellApp()
-    app.stdout = StdOut()
+    app.stdout = utils.StdSim(app.stdout)
     return app
 
 def test_default_to_shell_unknown(shell_app):
@@ -1140,7 +1140,7 @@ class HelpApp(cmd2.Cmd):
 @pytest.fixture
 def help_app():
     app = HelpApp()
-    app.stdout = StdOut()
+    app.stdout = utils.StdSim(app.stdout)
     return app
 
 def test_custom_command_help(help_app):
@@ -1203,7 +1203,7 @@ class HelpCategoriesApp(cmd2.Cmd):
 @pytest.fixture
 def helpcat_app():
     app = HelpCategoriesApp()
-    app.stdout = StdOut()
+    app.stdout = utils.StdSim(app.stdout)
     return app
 
 def test_help_cat_base(helpcat_app):
@@ -1296,7 +1296,7 @@ class SelectApp(cmd2.Cmd):
 @pytest.fixture
 def select_app():
     app = SelectApp()
-    app.stdout = StdOut()
+    app.stdout = utils.StdSim(app.stdout)
     return app
 
 def test_select_options(select_app):
@@ -1461,7 +1461,7 @@ class MultilineApp(cmd2.Cmd):
 @pytest.fixture
 def multiline_app():
     app = MultilineApp()
-    app.stdout = StdOut()
+    app.stdout = utils.StdSim(app.stdout)
     return app
 
 def test_multiline_complete_empty_statement_raises_exception(multiline_app):
@@ -1522,7 +1522,7 @@ class CommandResultApp(cmd2.Cmd):
 @pytest.fixture
 def commandresult_app():
     app = CommandResultApp()
-    app.stdout = StdOut()
+    app.stdout = utils.StdSim(app.stdout)
     return app
 
 def test_commandresult_truthy(commandresult_app):
@@ -1628,7 +1628,7 @@ def piped_rawinput_true(capsys, echo, command):
     # run the cmdloop, which should pull input from our mock
     app._cmdloop()
     out, err = capsys.readouterr()
-    return (app, out)
+    return app, out
 
 # using the decorator puts the original input function back when this unit test returns
 @mock.patch('builtins.input', mock.MagicMock(name='input', side_effect=['set', EOFError]))
@@ -1658,7 +1658,7 @@ def piped_rawinput_false(capsys, echo, command):
     app.echo = echo
     app._cmdloop()
     out, err = capsys.readouterr()
-    return (app, out)
+    return app, out
 
 def test_pseudo_raw_input_piped_rawinput_false_echo_true(capsys):
     command = 'set'
@@ -1715,28 +1715,28 @@ def test_empty_stdin_input():
 def test_poutput_string(base_app):
     msg = 'This is a test'
     base_app.poutput(msg)
-    out = base_app.stdout.buffer
+    out = base_app.stdout.getvalue()
     expected = msg + '\n'
     assert out == expected
 
 def test_poutput_zero(base_app):
     msg = 0
     base_app.poutput(msg)
-    out = base_app.stdout.buffer
+    out = base_app.stdout.getvalue()
     expected = str(msg) + '\n'
     assert out == expected
 
 def test_poutput_empty_string(base_app):
     msg = ''
     base_app.poutput(msg)
-    out = base_app.stdout.buffer
+    out = base_app.stdout.getvalue()
     expected = msg
     assert out == expected
 
 def test_poutput_none(base_app):
     msg = None
     base_app.poutput(msg)
-    out = base_app.stdout.buffer
+    out = base_app.stdout.getvalue()
     expected = ''
     assert out == expected
 
@@ -1826,7 +1826,7 @@ def test_ppaged(base_app):
     msg = 'testing...'
     end = '\n'
     base_app.ppaged(msg)
-    out = base_app.stdout.buffer
+    out = base_app.stdout.getvalue()
     assert out == msg + end
 
 # we override cmd.parseline() so we always get consistent
@@ -1859,14 +1859,14 @@ def test_readline_remove_history_item(base_app):
 def test_onecmd_raw_str_continue(base_app):
     line = "help"
     stop = base_app.onecmd(line)
-    out = base_app.stdout.buffer
+    out = base_app.stdout.getvalue()
     assert not stop
     assert out.strip() == BASE_HELP.strip()
 
 def test_onecmd_raw_str_quit(base_app):
     line = "quit"
     stop = base_app.onecmd(line)
-    out = base_app.stdout.buffer
+    out = base_app.stdout.getvalue()
     assert stop
     assert out == ''
 
@@ -1996,7 +1996,7 @@ def test_exit_code_default(exit_code_repl):
     # Create a cmd2.Cmd() instance and make sure basic settings are like we want for test
     app = exit_code_repl
     app.use_rawinput = True
-    app.stdout = StdOut()
+    app.stdout = utils.StdSim(app.stdout)
 
     # Mock out the input call so we don't actually wait for a user's response on stdin
     m = mock.MagicMock(name='input', return_value='exit')
@@ -2008,14 +2008,14 @@ def test_exit_code_default(exit_code_repl):
     with mock.patch.object(sys, 'argv', testargs):
         # Run the command loop
         app.cmdloop()
-    out = app.stdout.buffer
+    out = app.stdout.getvalue()
     assert out == expected
 
 def test_exit_code_nonzero(exit_code_repl):
     # Create a cmd2.Cmd() instance and make sure basic settings are like we want for test
     app = exit_code_repl
     app.use_rawinput = True
-    app.stdout = StdOut()
+    app.stdout = utils.StdSim(app.stdout)
 
     # Mock out the input call so we don't actually wait for a user's response on stdin
     m = mock.MagicMock(name='input', return_value='exit 23')
@@ -2028,5 +2028,5 @@ def test_exit_code_nonzero(exit_code_repl):
         # Run the command loop
         with pytest.raises(SystemExit):
             app.cmdloop()
-    out = app.stdout.buffer
+    out = app.stdout.getvalue()
     assert out == expected
diff --git a/tests/test_completion.py b/tests/test_completion.py
index 00a120cc..40299954 100644
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -15,7 +15,7 @@ import sys
 import pytest
 import cmd2
 from cmd2 import utils
-from .conftest import complete_tester, StdOut
+from .conftest import complete_tester
 from examples.subcommands import SubcommandsExample
 
 # List of strings used with completion functions
@@ -114,13 +114,6 @@ def test_complete_bogus_command(cmd2_app):
     assert first_match is None
 
 
-def test_cmd2_command_completion_single(cmd2_app):
-    text = 'hel'
-    line = text
-    endidx = len(line)
-    begidx = endidx - len(text)
-    assert cmd2_app.completenames(text, line, begidx, endidx) == ['help']
-
 def test_cmd2_command_completion_multiple(cmd2_app):
     text = 'h'
     line = text
@@ -694,8 +687,7 @@ def test_add_opening_quote_delimited_space_in_prefix(cmd2_app):
 @pytest.fixture
 def sc_app():
     c = SubcommandsExample()
-    c.stdout = StdOut()
-
+    c.stdout = utils.StdSim(c.stdout)
     return c
 
 def test_cmd2_subcommand_completion_single_end(sc_app):
@@ -843,7 +835,7 @@ class SubcommandsWithUnknownExample(cmd2.Cmd):
             func(self, args)
         else:
             # No subcommand was provided, so call help
-            self.do_help('base')
+            self.do_help(['base'])
 
 
 @pytest.fixture
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index e401e837..20f2f32c 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -5,18 +5,14 @@ Test plugin infrastructure and hooks.
 Copyright 2018 Jared Crapo <jared@kotfu.net>
 Released under MIT license, see LICENSE file
 """
-
-from typing import Tuple
-
 import pytest
 
 import cmd2
 from cmd2 import plugin
 
-from .conftest import StdOut
 
 class Plugin:
-    "A mixin class for testing hook registration and calling"
+    """A mixin class for testing hook registration and calling"""
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         self.reset_counters()
@@ -35,19 +31,19 @@ class Plugin:
     #
     ###
     def prepost_hook_one(self) -> None:
-        "Method used for preloop or postloop hooks"
+        """Method used for preloop or postloop hooks"""
         self.poutput("one")
 
     def prepost_hook_two(self) -> None:
-        "Another method used for preloop or postloop hooks"
+        """Another method used for preloop or postloop hooks"""
         self.poutput("two")
 
     def prepost_hook_too_many_parameters(self, param) -> None:
-        "A preloop or postloop hook with too many parameters"
+        """A preloop or postloop hook with too many parameters"""
         pass
 
     def prepost_hook_with_wrong_return_annotation(self) -> bool:
-        "A preloop or postloop hook with incorrect return type"
+        """A preloop or postloop hook with incorrect return type"""
         pass
 
     ###
@@ -56,7 +52,7 @@ class Plugin:
     #
     ###
     def preparse(self, line: str) -> str:
-        "Preparsing hook"
+        """Preparsing hook"""
         self.called_preparse += 1
         return line
 
@@ -66,44 +62,44 @@ class Plugin:
     #
     ###
     def postparse_hook(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
-        "A postparsing hook"
+        """A postparsing hook"""
         self.called_postparsing += 1
         return data
 
     def postparse_hook_stop(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
-        "A postparsing hook with requests application exit"
+        """A postparsing hook with requests application exit"""
         self.called_postparsing += 1
         data.stop = True
         return data
 
     def postparse_hook_emptystatement(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
-        "A postparsing hook with raises an EmptyStatement exception"
+        """A postparsing hook with raises an EmptyStatement exception"""
         self.called_postparsing += 1
         raise cmd2.EmptyStatement
 
     def postparse_hook_exception(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
-        "A postparsing hook which raises an exception"
+        """A postparsing hook which raises an exception"""
         self.called_postparsing += 1
         raise ValueError
 
     def postparse_hook_too_many_parameters(self, data1, data2) -> cmd2.plugin.PostparsingData:
-        "A postparsing hook with too many parameters"
+        """A postparsing hook with too many parameters"""
         pass
 
     def postparse_hook_undeclared_parameter_annotation(self, data) -> cmd2.plugin.PostparsingData:
-        "A postparsing hook with an undeclared parameter type"
+        """A postparsing hook with an undeclared parameter type"""
         pass
 
     def postparse_hook_wrong_parameter_annotation(self, data: str) -> cmd2.plugin.PostparsingData:
-        "A postparsing hook with the wrong parameter type"
+        """A postparsing hook with the wrong parameter type"""
         pass
 
     def postparse_hook_undeclared_return_annotation(self, data: cmd2.plugin.PostparsingData):
-        "A postparsing hook with an undeclared return type"
+        """A postparsing hook with an undeclared return type"""
         pass
 
     def postparse_hook_wrong_return_annotation(self, data: cmd2.plugin.PostparsingData) -> str:
-        "A postparsing hook with the wrong return type"
+        """A postparsing hook with the wrong return type"""
         pass
 
     ###
@@ -112,43 +108,43 @@ class Plugin:
     #
     ###
     def precmd(self, statement: cmd2.Statement) -> cmd2.Statement:
-        "Override cmd.Cmd method"
+        """Override cmd.Cmd method"""
         self.called_precmd += 1
         return statement
 
     def precmd_hook(self, data: plugin.PrecommandData) -> plugin.PrecommandData:
-        "A precommand hook"
+        """A precommand hook"""
         self.called_precmd += 1
         return data
 
     def precmd_hook_emptystatement(self, data: plugin.PrecommandData) -> plugin.PrecommandData:
-        "A precommand hook which raises an EmptyStatement exception"
+        """A precommand hook which raises an EmptyStatement exception"""
         self.called_precmd += 1
         raise cmd2.EmptyStatement
 
     def precmd_hook_exception(self, data: plugin.PrecommandData) -> plugin.PrecommandData:
-        "A precommand hook which raises an exception"
+        """A precommand hook which raises an exception"""
         self.called_precmd += 1
         raise ValueError
 
     def precmd_hook_not_enough_parameters(self) -> plugin.PrecommandData:
-        "A precommand hook with no parameters"
+        """A precommand hook with no parameters"""
         pass
 
     def precmd_hook_too_many_parameters(self, one: plugin.PrecommandData, two: str) -> plugin.PrecommandData:
-        "A precommand hook with too many parameters"
+        """A precommand hook with too many parameters"""
         return one
 
     def precmd_hook_no_parameter_annotation(self, data) -> plugin.PrecommandData:
-        "A precommand hook with no type annotation on the parameter"
+        """A precommand hook with no type annotation on the parameter"""
         return data
 
     def precmd_hook_wrong_parameter_annotation(self, data: str) -> plugin.PrecommandData:
-        "A precommand hook with the incorrect type annotation on the parameter"
+        """A precommand hook with the incorrect type annotation on the parameter"""
         return data
 
     def precmd_hook_no_return_annotation(self, data: plugin.PrecommandData):
-        "A precommand hook with no type annotation on the return value"
+        """A precommand hook with no type annotation on the return value"""
         return data
 
     def precmd_hook_wrong_return_annotation(self, data: plugin.PrecommandData) -> cmd2.Statement:
@@ -160,38 +156,38 @@ class Plugin:
     #
     ###
     def postcmd(self, stop: bool, statement: cmd2.Statement) -> bool:
-        "Override cmd.Cmd method"
+        """Override cmd.Cmd method"""
         self.called_postcmd += 1
         return stop
 
     def postcmd_hook(self, data: plugin.PostcommandData) -> plugin.PostcommandData:
-        "A postcommand hook"
+        """A postcommand hook"""
         self.called_postcmd += 1
         return data
 
     def postcmd_hook_exception(self, data: plugin.PostcommandData) -> plugin.PostcommandData:
-        "A postcommand hook with raises an exception"
+        """A postcommand hook with raises an exception"""
         self.called_postcmd += 1
         raise ZeroDivisionError
 
     def postcmd_hook_not_enough_parameters(self) -> plugin.PostcommandData:
-        "A precommand hook with no parameters"
+        """A precommand hook with no parameters"""
         pass
 
     def postcmd_hook_too_many_parameters(self, one: plugin.PostcommandData, two: str) -> plugin.PostcommandData:
-        "A precommand hook with too many parameters"
+        """A precommand hook with too many parameters"""
         return one
 
     def postcmd_hook_no_parameter_annotation(self, data) -> plugin.PostcommandData:
-        "A precommand hook with no type annotation on the parameter"
+        """A precommand hook with no type annotation on the parameter"""
         return data
 
     def postcmd_hook_wrong_parameter_annotation(self, data: str) -> plugin.PostcommandData:
-        "A precommand hook with the incorrect type annotation on the parameter"
+        """A precommand hook with the incorrect type annotation on the parameter"""
         return data
 
     def postcmd_hook_no_return_annotation(self, data: plugin.PostcommandData):
-        "A precommand hook with no type annotation on the return value"
+        """A precommand hook with no type annotation on the return value"""
         return data
 
     def postcmd_hook_wrong_return_annotation(self, data: plugin.PostcommandData) -> cmd2.Statement:
@@ -208,13 +204,13 @@ class Plugin:
         return data
 
     def cmdfinalization_hook_stop(self, data: cmd2.plugin.CommandFinalizationData) -> cmd2.plugin.CommandFinalizationData:
-        "A postparsing hook which requests application exit"
+        """A postparsing hook which requests application exit"""
         self.called_cmdfinalization += 1
         data.stop = True
         return data
 
     def cmdfinalization_hook_exception(self, data: cmd2.plugin.CommandFinalizationData) -> cmd2.plugin.CommandFinalizationData:
-        "A postparsing hook which raises an exception"
+        """A postparsing hook which raises an exception"""
         self.called_cmdfinalization += 1
         raise ValueError
 
@@ -244,7 +240,7 @@ class Plugin:
 
 
 class PluggedApp(Plugin, cmd2.Cmd):
-    "A sample app with a plugin mixed in"
+    """A sample app with a plugin mixed in"""
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
 
diff --git a/tests/test_pyscript.py b/tests/test_pyscript.py
index 73c1a62a..256e63a7 100644
--- a/tests/test_pyscript.py
+++ b/tests/test_pyscript.py
@@ -1,3 +1,4 @@
+# coding=utf-8
 """
 Unit/functional testing for argparse completer in cmd2
 
@@ -8,24 +9,25 @@ import os
 import pytest
 from cmd2.cmd2 import Cmd, with_argparser
 from cmd2 import argparse_completer
-from .conftest import run_cmd, StdOut
-from cmd2.utils import namedtuple_with_defaults
+from .conftest import run_cmd
+from cmd2.utils import namedtuple_with_defaults, StdSim
+
 
 class PyscriptExample(Cmd):
     ratings_types = ['G', 'PG', 'PG-13', 'R', 'NC-17']
 
     def _do_media_movies(self, args) -> None:
         if not args.command:
-            self.do_help('media movies')
+            self.do_help(['media movies'])
         else:
             print('media movies ' + str(args.__dict__))
 
     def _do_media_shows(self, args) -> None:
         if not args.command:
-            self.do_help('media shows')
+            self.do_help(['media shows'])
 
         if not args.command:
-            self.do_help('media shows')
+            self.do_help(['media shows'])
         else:
             print('media shows ' + str(args.__dict__))
 
@@ -70,7 +72,7 @@ class PyscriptExample(Cmd):
             func(self, args)
         else:
             # No subcommand was provided, so call help
-            self.do_help('media')
+            self.do_help(['media'])
 
     foo_parser = argparse_completer.ACArgumentParser(prog='foo')
     foo_parser.add_argument('-c', dest='counter', action='count')
@@ -114,8 +116,7 @@ class PyscriptExample(Cmd):
 @pytest.fixture
 def ps_app():
     c = PyscriptExample()
-    c.stdout = StdOut()
-
+    c.stdout = StdSim(c.stdout)
     return c
 
 
@@ -131,8 +132,7 @@ class PyscriptCustomNameExample(Cmd):
 @pytest.fixture
 def ps_echo():
     c = PyscriptCustomNameExample()
-    c.stdout = StdOut()
-
+    c.stdout = StdSim(c.stdout)
     return c
 
 
diff --git a/tests/test_transcript.py b/tests/test_transcript.py
index 3caf6a37..f854241b 100644
--- a/tests/test_transcript.py
+++ b/tests/test_transcript.py
@@ -16,8 +16,10 @@ from unittest import mock
 import pytest
 
 import cmd2
-from .conftest import run_cmd, StdOut
+from .conftest import run_cmd
 from cmd2 import transcript
+from cmd2.utils import StdSim
+
 
 class CmdLineApp(cmd2.Cmd):
 
@@ -83,9 +85,9 @@ def test_commands_at_invocation():
     expected = "This is an intro banner ...\nhello\nGracie\n"
     with mock.patch.object(sys, 'argv', testargs):
         app = CmdLineApp()
-        app.stdout = StdOut()
+        app.stdout = StdSim(app.stdout)
         app.cmdloop()
-        out = app.stdout.buffer
+        out = app.stdout.getvalue()
         assert out == expected
 
 @pytest.mark.parametrize('filename,feedback_to_output', [
@@ -129,7 +131,7 @@ def test_transcript(request, capsys, filename, feedback_to_output):
 
 def test_history_transcript(request, capsys):
     app = CmdLineApp()
-    app.stdout = StdOut()
+    app.stdout = StdSim(app.stdout)
     run_cmd(app, 'orate this is\na /multiline/\ncommand;\n')
     run_cmd(app, 'speak /tmp/file.txt is not a regex')
 
@@ -150,13 +152,13 @@ this is a \/multiline\/ command
 
     # read in the transcript created by the history command
     with open(history_fname) as f:
-        transcript = f.read()
+        xscript = f.read()
 
-    assert transcript == expected
+    assert xscript == expected
 
 def test_history_transcript_bad_filename(request, capsys):
     app = CmdLineApp()
-    app.stdout = StdOut()
+    app.stdout = StdSim(app.stdout)
     run_cmd(app, 'orate this is\na /multiline/\ncommand;\n')
     run_cmd(app, 'speak /tmp/file.txt is not a regex')
 
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 8c8daa39..53031567 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -5,6 +5,10 @@ Unit testing for cmd2/utils.py module.
 Copyright 2018 Todd Leonhardt <todd.leonhardt@gmail.com>
 Released under MIT license, see LICENSE file
 """
+import sys
+
+import pytest
+
 from colorama import Fore
 import cmd2.utils as cu
 
@@ -110,4 +114,55 @@ def test_quot_string_if_needed_no():
     assert cu.quote_string_if_needed(your_str) == your_str
 
 
+@pytest.fixture
+def stdout_sim():
+    stdsim = cu.StdSim(sys.stdout)
+    return stdsim
+
+def test_stdsim_write_str(stdout_sim):
+    my_str = 'Hello World'
+    stdout_sim.write(my_str)
+    assert stdout_sim.getvalue() == my_str
+
+def test_stdsim_write_bytes(stdout_sim):
+    b_str = b'Hello World'
+    with pytest.raises(TypeError):
+        stdout_sim.write(b_str)
+
+def test_stdsim_buffer_write_bytes(stdout_sim):
+    b_str = b'Hello World'
+    stdout_sim.buffer.write(b_str)
+    assert stdout_sim.getvalue() == b_str.decode()
+
+def test_stdsim_buffer_write_str(stdout_sim):
+    my_str = 'Hello World'
+    with pytest.raises(TypeError):
+        stdout_sim.buffer.write(my_str)
+
+def test_stdsim_read(stdout_sim):
+    my_str = 'Hello World'
+    stdout_sim.write(my_str)
+    # getvalue() returns the value and leaves it unaffected internally
+    assert stdout_sim.getvalue() == my_str
+    # read() returns the value and then clears the internal buffer
+    assert stdout_sim.read() == my_str
+    assert stdout_sim.getvalue() == ''
+
+def test_stdsim_clear(stdout_sim):
+    my_str = 'Hello World'
+    stdout_sim.write(my_str)
+    assert stdout_sim.getvalue() == my_str
+    stdout_sim.clear()
+    assert stdout_sim.getvalue() == ''
+
+def test_stdsim_getattr_exist(stdout_sim):
+    # Here the StdSim getattr is allowing us to access methods within StdSim
+    my_str = 'Hello World'
+    stdout_sim.write(my_str)
+    val_func = getattr(stdout_sim, 'getvalue')
+    assert val_func() == my_str
+
+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()