Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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``.
Expand Down
38 changes: 28 additions & 10 deletions cmd2/pyscript_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -67,25 +85,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
47 changes: 25 additions & 22 deletions cmd2/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand All @@ -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.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)

Expand Down Expand Up @@ -337,6 +322,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 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
Expand Down
20 changes: 2 additions & 18 deletions docs/freefeatures.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for cleaning up this outdated documentation

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:

Expand Down
22 changes: 22 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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''