diff options
author | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2019-03-10 14:54:06 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-03-10 14:54:06 -0400 |
commit | d9cd632651d01f87bd599feb75653cd0dde9497e (patch) | |
tree | 277bf52a39593799d919dc2bbb1dd054b514d448 | |
parent | 3c7361db4cc0cd2b0c8445902a1a3eed8b2ae225 (diff) | |
parent | 16a337dfa4b96d57f0dc9e2e31e2eb99330f673b (diff) | |
download | cmd2-git-d9cd632651d01f87bd599feb75653cd0dde9497e.tar.gz |
Merge pull request #642 from python-cmd2/store_output
StdSim.pause_storage
-rw-r--r-- | CHANGELOG.md | 2 | ||||
-rw-r--r-- | cmd2/pyscript_bridge.py | 38 | ||||
-rw-r--r-- | cmd2/utils.py | 47 | ||||
-rw-r--r-- | docs/freefeatures.rst | 20 | ||||
-rw-r--r-- | tests/test_utils.py | 22 |
5 files changed, 79 insertions, 50 deletions
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: 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'' |