Skip to content

Commit d9cd632

Browse files
authored
Merge pull request #642 from python-cmd2/store_output
StdSim.pause_storage
2 parents 3c7361d + 16a337d commit d9cd632

File tree

5 files changed

+79
-50
lines changed

5 files changed

+79
-50
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
* Added **-v**, **--verbose** flag
1212
* display history and include expanded commands if they differ from the typed command
1313
* Added ``matches_sort_key`` to override the default way tab completion matches are sorted
14+
* Added ``StdSim.pause_storage`` member which when True will cause ``StdSim`` to not save the output sent to it.
15+
See documentation for ``CommandResult`` in ``pyscript_bridge.py`` for reasons pausing the storage can be useful.
1416
* Potentially breaking changes
1517
* Made ``cmd2_app`` a positional and required argument of ``AutoCompleter`` since certain functionality now
1618
requires that it can't be ``None``.

cmd2/pyscript_bridge.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,27 @@ class CommandResult(namedtuple_with_defaults('CommandResult', ['stdout', 'stderr
2525
Named tuple attributes
2626
----------------------
2727
stdout: str - Output captured from stdout while this command is executing
28-
stderr: str - Output captured from stderr while this command is executing. None if no error captured
28+
stderr: str - Output captured from stderr while this command is executing. None if no error captured.
2929
data - Data returned by the command.
3030
31+
Any combination of these fields can be used when developing a scripting API for a given command.
32+
By default stdout and stderr will be captured for you. If there is additional command specific data,
33+
then write that to cmd2's _last_result member. That becomes the data member of this tuple.
34+
35+
In some cases, the data member may contain everything needed for a command and storing stdout
36+
and stderr might just be a duplication of data that wastes memory. In that case, the StdSim can
37+
be told not to store output with its pause_storage member. While this member is True, any output
38+
sent to StdSim won't be saved in its buffer.
39+
40+
The code would look like this:
41+
if isinstance(self.stdout, StdSim):
42+
self.stdout.pause_storage = True
43+
44+
if isinstance(sys.stderr, StdSim):
45+
sys.stderr.pause_storage = True
46+
47+
See StdSim class in utils.py for more information
48+
3149
NOTE: Named tuples are immutable. So the contents are there for access, not for modification.
3250
"""
3351
def __bool__(self) -> bool:
@@ -67,25 +85,25 @@ def __call__(self, command: str, echo: Optional[bool] = None) -> CommandResult:
6785
if echo is None:
6886
echo = self.cmd_echo
6987

70-
copy_stdout = StdSim(sys.stdout, echo)
71-
copy_stderr = StdSim(sys.stderr, echo)
72-
88+
# This will be used to capture _cmd2_app.stdout and sys.stdout
7389
copy_cmd_stdout = StdSim(self._cmd2_app.stdout, echo)
7490

91+
# This will be used to capture sys.stderr
92+
copy_stderr = StdSim(sys.stderr, echo)
93+
7594
self._cmd2_app._last_result = None
7695

7796
try:
7897
self._cmd2_app.stdout = copy_cmd_stdout
79-
with redirect_stdout(copy_stdout):
98+
with redirect_stdout(copy_cmd_stdout):
8099
with redirect_stderr(copy_stderr):
81100
# Include a newline in case it's a multiline command
82101
self._cmd2_app.onecmd_plus_hooks(command + '\n')
83102
finally:
84103
self._cmd2_app.stdout = copy_cmd_stdout.inner_stream
85104

86-
# if stderr is empty, set it to None
87-
stderr = copy_stderr.getvalue() if copy_stderr.getvalue() else None
88-
89-
outbuf = copy_cmd_stdout.getvalue() if copy_cmd_stdout.getvalue() else copy_stdout.getvalue()
90-
result = CommandResult(stdout=outbuf, stderr=stderr, data=self._cmd2_app._last_result)
105+
# Save the output. If stderr is empty, set it to None.
106+
result = CommandResult(stdout=copy_cmd_stdout.getvalue(),
107+
stderr=copy_stderr.getvalue() if copy_stderr.getvalue() else None,
108+
data=self._cmd2_app._last_result)
91109
return result

cmd2/utils.py

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -261,28 +261,10 @@ def natural_sort(list_to_sort: Iterable[str]) -> List[str]:
261261

262262

263263
class StdSim(object):
264-
"""Class to simulate behavior of sys.stdout or sys.stderr.
265-
264+
"""
265+
Class to simulate behavior of sys.stdout or sys.stderr.
266266
Stores contents in internal buffer and optionally echos to the inner stream it is simulating.
267267
"""
268-
class ByteBuf(object):
269-
"""Inner class which stores an actual bytes buffer and does the actual output if echo is enabled."""
270-
def __init__(self, inner_stream, echo: bool = False,
271-
encoding: str = 'utf-8', errors: str = 'replace') -> None:
272-
self.byte_buf = b''
273-
self.inner_stream = inner_stream
274-
self.echo = echo
275-
self.encoding = encoding
276-
self.errors = errors
277-
278-
def write(self, b: bytes) -> None:
279-
"""Add bytes to internal bytes buffer and if echo is True, echo contents to inner stream."""
280-
if not isinstance(b, bytes):
281-
raise TypeError('a bytes-like object is required, not {}'.format(type(b)))
282-
self.byte_buf += b
283-
if self.echo:
284-
self.inner_stream.buffer.write(b)
285-
286268
def __init__(self, inner_stream, echo: bool = False,
287269
encoding: str = 'utf-8', errors: str = 'replace') -> None:
288270
"""
@@ -292,17 +274,20 @@ def __init__(self, inner_stream, echo: bool = False,
292274
:param encoding: codec for encoding/decoding strings (defaults to utf-8)
293275
:param errors: how to handle encoding/decoding errors (defaults to replace)
294276
"""
295-
self.buffer = self.ByteBuf(inner_stream, echo)
296277
self.inner_stream = inner_stream
297278
self.echo = echo
298279
self.encoding = encoding
299280
self.errors = errors
281+
self.pause_storage = False
282+
self.buffer = ByteBuf(self)
300283

301284
def write(self, s: str) -> None:
302285
"""Add str to internal bytes buffer and if echo is True, echo contents to inner stream"""
303286
if not isinstance(s, str):
304287
raise TypeError('write() argument must be str, not {}'.format(type(s)))
305-
self.buffer.byte_buf += s.encode(encoding=self.encoding, errors=self.errors)
288+
289+
if not self.pause_storage:
290+
self.buffer.byte_buf += s.encode(encoding=self.encoding, errors=self.errors)
306291
if self.echo:
307292
self.inner_stream.write(s)
308293

@@ -337,6 +322,24 @@ def __getattr__(self, item: str):
337322
return getattr(self.inner_stream, item)
338323

339324

325+
class ByteBuf(object):
326+
"""
327+
Used by StdSim to write binary data and stores the actual bytes written
328+
"""
329+
def __init__(self, std_sim_instance: StdSim) -> None:
330+
self.byte_buf = b''
331+
self.std_sim_instance = std_sim_instance
332+
333+
def write(self, b: bytes) -> None:
334+
"""Add bytes to internal bytes buffer and if echo is True, echo contents to inner stream."""
335+
if not isinstance(b, bytes):
336+
raise TypeError('a bytes-like object is required, not {}'.format(type(b)))
337+
if not self.std_sim_instance.pause_storage:
338+
self.byte_buf += b
339+
if self.std_sim_instance.echo:
340+
self.std_sim_instance.inner_stream.buffer.write(b)
341+
342+
340343
def unquote_redirection_tokens(args: List[str]) -> None:
341344
"""
342345
Unquote redirection tokens in a list of command-line arguments

docs/freefeatures.rst

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -130,29 +130,13 @@ debugging your application. To prevent users from enabling this ability
130130
manually you'll need to remove ``locals_in_py`` from the ``settable`` dictionary.
131131

132132
The ``app`` object (or your custom name) provides access to application commands
133-
through either raw commands or through a python API wrapper. For example, any
134-
application command call be called with ``app("<command>")``. All application
135-
commands are accessible as python objects and functions matching the command
136-
name. For example, the following are equivalent:
133+
through raw commands. For example, any application command call be called with
134+
``app("<command>")``.
137135

138136
::
139137

140138
>>> app('say --piglatin Blah')
141139
lahBay
142-
>>> app.say("Blah", piglatin=True)
143-
lahBay
144-
145-
146-
Sub-commands are also supported. The following pairs are equivalent:
147-
148-
::
149-
150-
>>> app('command subcmd1 subcmd2 param1 --myflag --otherflag 3')
151-
>>> app.command.subcmd1.subcmd2('param1', myflag=True, otherflag=3)
152-
153-
>>> app('command subcmd1 param1 subcmd2 param2 --myflag --otherflag 3')
154-
>>> app.command.subcmd1('param1').subcmd2('param2', myflag=True, otherflag=3)
155-
156140

157141
More Python examples:
158142

tests/test_utils.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,25 @@ def test_stdsim_getattr_noexist(stdout_sim):
194194
# Here the StdSim getattr is allowing us to access methods defined by the inner stream
195195
assert not stdout_sim.isatty()
196196

197+
def test_stdsim_pause_storage(stdout_sim):
198+
# Test pausing storage for string data
199+
my_str = 'Hello World'
200+
201+
stdout_sim.pause_storage = False
202+
stdout_sim.write(my_str)
203+
assert stdout_sim.read() == my_str
204+
205+
stdout_sim.pause_storage = True
206+
stdout_sim.write(my_str)
207+
assert stdout_sim.read() == ''
208+
209+
# Test pausing storage for binary data
210+
b_str = b'Hello World'
211+
212+
stdout_sim.pause_storage = False
213+
stdout_sim.buffer.write(b_str)
214+
assert stdout_sim.readbytes() == b_str
215+
216+
stdout_sim.pause_storage = True
217+
stdout_sim.buffer.write(b_str)
218+
assert stdout_sim.getbytes() == b''

0 commit comments

Comments
 (0)