From c2d1de68d247668f55523582b35849d8c61b2144 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 6 Mar 2019 22:44:35 -0500 Subject: [PATCH 1/4] Added way to turn off storing stdout and stderr in StdSim --- cmd2/pyscript_bridge.py | 37 +++++++++++++++++++------- cmd2/utils.py | 58 +++++++++++++++++++++++++---------------- 2 files changed, 63 insertions(+), 32 deletions(-) diff --git a/cmd2/pyscript_bridge.py b/cmd2/pyscript_bridge.py index 6c14ff1d9..d2e52a308 100644 --- a/cmd2/pyscript_bridge.py +++ b/cmd2/pyscript_bridge.py @@ -25,9 +25,26 @@ 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 set_store_output() method. + + The code would look like this: + if isinstance(self.stdout, StdSim): + self.stdout.set_store_output(False) + + if isinstance(sys.stderr, StdSim): + sys.stderr.set_store_output(False) + + 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 +84,25 @@ def __call__(self, command: str, echo: Optional[bool] = None) -> CommandResult: 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 098ed41df..ca68bff7d 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 @@ def __init__(self, inner_stream, echo: bool = False, :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.__store_output = True + 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 self.__store_output: + self.buffer.byte_buf += s.encode(encoding=self.encoding, errors=self.errors) if self.echo: self.inner_stream.write(s) @@ -330,6 +315,17 @@ def clear(self) -> None: """Clear the internal contents""" self.buffer.byte_buf = b'' + def get_store_output(self) -> bool: + return self.__store_output + + def set_store_output(self, store_output: bool) -> None: + """ + Set whether output should be saved in buffer.byte_buf + :param store_output: Store output if True, otherwise do not and clear the buffer + """ + self.__store_output = self.buffer.store_output = store_output + self.clear() + def __getattr__(self, item: str): if item in self.__dict__: return self.__dict__[item] @@ -337,6 +333,24 @@ def __getattr__(self, item: str): 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 self.std_sim_instance.get_store_output(): + 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 From 2ea9658fae7ce882bfc3266415fb634c79b4db09 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 9 Mar 2019 19:38:22 -0500 Subject: [PATCH 2/4] Replaced StdSim.__store_output with StdSim.pause_storage --- cmd2/pyscript_bridge.py | 7 ++++--- cmd2/utils.py | 17 +++-------------- tests/test_utils.py | 22 ++++++++++++++++++++++ 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/cmd2/pyscript_bridge.py b/cmd2/pyscript_bridge.py index d2e52a308..f3ce841df 100644 --- a/cmd2/pyscript_bridge.py +++ b/cmd2/pyscript_bridge.py @@ -34,14 +34,15 @@ class CommandResult(namedtuple_with_defaults('CommandResult', ['stdout', 'stderr 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 set_store_output() method. + 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.set_store_output(False) + self.stdout.pause_storage = True if isinstance(sys.stderr, StdSim): - sys.stderr.set_store_output(False) + sys.stderr.pause_storage = True See StdSim class in utils.py for more information diff --git a/cmd2/utils.py b/cmd2/utils.py index ca68bff7d..a8760a65e 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -278,7 +278,7 @@ def __init__(self, inner_stream, echo: bool = False, self.echo = echo self.encoding = encoding self.errors = errors - self.__store_output = True + self.pause_storage = False self.buffer = ByteBuf(self) def write(self, s: str) -> None: @@ -286,7 +286,7 @@ def write(self, s: str) -> None: if not isinstance(s, str): raise TypeError('write() argument must be str, not {}'.format(type(s))) - if self.__store_output: + 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) @@ -315,17 +315,6 @@ def clear(self) -> None: """Clear the internal contents""" self.buffer.byte_buf = b'' - def get_store_output(self) -> bool: - return self.__store_output - - def set_store_output(self, store_output: bool) -> None: - """ - Set whether output should be saved in buffer.byte_buf - :param store_output: Store output if True, otherwise do not and clear the buffer - """ - self.__store_output = self.buffer.store_output = store_output - self.clear() - def __getattr__(self, item: str): if item in self.__dict__: return self.__dict__[item] @@ -345,7 +334,7 @@ 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 self.std_sim_instance.get_store_output(): + 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) diff --git a/tests/test_utils.py b/tests/test_utils.py index 75d4479a6..307f69dab 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'' From e21d11db137a34d919737506e8ecc49eb0d2f4e3 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sun, 10 Mar 2019 01:45:42 -0500 Subject: [PATCH 3/4] Added StdSim.pause_storage to CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcf90db8c..37ccc9a38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,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``. From 16a337dfa4b96d57f0dc9e2e31e2eb99330f673b Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sun, 10 Mar 2019 14:01:52 -0400 Subject: [PATCH 4/4] Removed obsolete documentation --- docs/freefeatures.rst | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/docs/freefeatures.rst b/docs/freefeatures.rst index 5c246798c..bcb9c0e70 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("")``. 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("")``. :: >>> 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: