summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin Van Brunt <kmvanbrunt@gmail.com>2019-03-10 14:54:06 -0400
committerGitHub <noreply@github.com>2019-03-10 14:54:06 -0400
commitd9cd632651d01f87bd599feb75653cd0dde9497e (patch)
tree277bf52a39593799d919dc2bbb1dd054b514d448
parent3c7361db4cc0cd2b0c8445902a1a3eed8b2ae225 (diff)
parent16a337dfa4b96d57f0dc9e2e31e2eb99330f673b (diff)
downloadcmd2-git-d9cd632651d01f87bd599feb75653cd0dde9497e.tar.gz
Merge pull request #642 from python-cmd2/store_output
StdSim.pause_storage
-rw-r--r--CHANGELOG.md2
-rw-r--r--cmd2/pyscript_bridge.py38
-rw-r--r--cmd2/utils.py47
-rw-r--r--docs/freefeatures.rst20
-rw-r--r--tests/test_utils.py22
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''