Skip to content

Commit ff89bad

Browse files
committed
Suppresses stdout and stderr output by default when calling an application command from pyscript.
Added support for tab completing application commands in ipython shell Updated unit tests scripts to set cmd_echo to True to validate command output.
1 parent 371284d commit ff89bad

22 files changed

+91
-24
lines changed

cmd2/pyscript_bridge.py

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,15 @@ def __bool__(self):
4141

4242
class CopyStream(object):
4343
"""Copies all data written to a stream"""
44-
def __init__(self, inner_stream):
44+
def __init__(self, inner_stream, echo):
4545
self.buffer = ''
4646
self.inner_stream = inner_stream
47+
self.echo = echo
4748

4849
def write(self, s):
4950
self.buffer += s
50-
self.inner_stream.write(s)
51+
if self.echo:
52+
self.inner_stream.write(s)
5153

5254
def read(self):
5355
raise NotImplementedError
@@ -62,12 +64,12 @@ def __getattr__(self, item):
6264
return getattr(self.inner_stream, item)
6365

6466

65-
def _exec_cmd(cmd2_app, func):
67+
def _exec_cmd(cmd2_app, func, echo):
6668
"""Helper to encapsulate executing a command and capturing the results"""
67-
copy_stdout = CopyStream(sys.stdout)
68-
copy_stderr = CopyStream(sys.stderr)
69+
copy_stdout = CopyStream(sys.stdout, echo)
70+
copy_stderr = CopyStream(sys.stderr, echo)
6971

70-
copy_cmd_stdout = CopyStream(cmd2_app.stdout)
72+
copy_cmd_stdout = CopyStream(cmd2_app.stdout, echo)
7173

7274
cmd2_app._last_result = None
7375

@@ -80,7 +82,7 @@ def _exec_cmd(cmd2_app, func):
8082
cmd2_app.stdout = copy_cmd_stdout.inner_stream
8183

8284
# if stderr is empty, set it to None
83-
stderr = copy_stderr if copy_stderr.buffer else None
85+
stderr = copy_stderr.buffer if copy_stderr.buffer else None
8486

8587
outbuf = copy_cmd_stdout.buffer if copy_cmd_stdout.buffer else copy_stdout.buffer
8688
result = CommandResult(stdout=outbuf, stderr=stderr, data=cmd2_app._last_result)
@@ -91,7 +93,8 @@ class ArgparseFunctor:
9193
"""
9294
Encapsulates translating python object traversal
9395
"""
94-
def __init__(self, cmd2_app, item, parser):
96+
def __init__(self, echo: bool, cmd2_app, item, parser):
97+
self._echo = echo
9598
self._cmd2_app = cmd2_app
9699
self._item = item
97100
self._parser = parser
@@ -101,6 +104,14 @@ def __init__(self, cmd2_app, item, parser):
101104
# argparse object for the current command layer
102105
self.__current_subcommand_parser = parser
103106

107+
def __dir__(self):
108+
"""Returns a custom list of attribute names to match the sub-commands"""
109+
commands = []
110+
for action in self.__current_subcommand_parser._actions:
111+
if not action.option_strings and isinstance(action, argparse._SubParsersAction):
112+
commands.extend(action.choices)
113+
return commands
114+
104115
def __getattr__(self, item):
105116
"""Search for a subcommand matching this item and update internal state to track the traversal"""
106117
# look for sub-command under the current command/sub-command layer
@@ -114,7 +125,6 @@ def __getattr__(self, item):
114125
return self
115126

116127
raise AttributeError(item)
117-
# return super().__getattr__(item)
118128

119129
def __call__(self, *args, **kwargs):
120130
"""
@@ -251,16 +261,16 @@ def traverse_parser(parser):
251261

252262
traverse_parser(self._parser)
253263

254-
# print('Command: {}'.format(cmd_str[0]))
264+
return _exec_cmd(self._cmd2_app, functools.partial(func, cmd_str[0]), self._echo)
255265

256-
return _exec_cmd(self._cmd2_app, functools.partial(func, cmd_str[0]))
257266

258267
class PyscriptBridge(object):
259268
"""Preserves the legacy 'cmd' interface for pyscript while also providing a new python API wrapper for
260269
application commands."""
261270
def __init__(self, cmd2_app):
262271
self._cmd2_app = cmd2_app
263272
self._last_result = None
273+
self.cmd_echo = False
264274

265275
def __getattr__(self, item: str):
266276
"""Check if the attribute is a command. If so, return a callable."""
@@ -274,13 +284,19 @@ def __getattr__(self, item: str):
274284
except AttributeError:
275285
# Command doesn't, we will accept parameters in the form of a command string
276286
def wrap_func(args=''):
277-
return _exec_cmd(self._cmd2_app, functools.partial(func, args))
287+
return _exec_cmd(self._cmd2_app, functools.partial(func, args), self.cmd_echo)
278288
return wrap_func
279289
else:
280290
# Command does use argparse, return an object that can traverse the argparse subcommands and arguments
281-
return ArgparseFunctor(self._cmd2_app, item, parser)
291+
return ArgparseFunctor(self.cmd_echo, self._cmd2_app, item, parser)
282292

283-
raise AttributeError(item)
293+
return super().__getattr__(item)
294+
295+
def __dir__(self):
296+
"""Return a custom set of attribute names to match the available commands"""
297+
commands = list(self._cmd2_app.get_all_commands())
298+
commands.insert(0, 'cmd_echo')
299+
return commands
284300

285301
def __call__(self, args):
286-
return _exec_cmd(self._cmd2_app, functools.partial(self._cmd2_app.onecmd_plus_hooks, args + '\n'))
302+
return _exec_cmd(self._cmd2_app, functools.partial(self._cmd2_app.onecmd_plus_hooks, args + '\n'), self.cmd_echo)

tests/pyscript/bar1.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
app.cmd_echo = True
12
app.bar('11', '22')

tests/pyscript/custom_echo.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
custom.cmd_echo = True
2+
custom.echo('blah!')

tests/pyscript/foo1.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
app.cmd_echo = True
12
app.foo('aaa', 'bbb', counter=3, trueval=True, constval=True)

tests/pyscript/foo2.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
app.cmd_echo = True
12
app.foo('11', '22', '33', '44', counter=3, trueval=True, constval=True)

tests/pyscript/foo3.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
app.cmd_echo = True
12
app.foo('11', '22', '33', '44', '55', '66', counter=3, trueval=False, constval=False)

tests/pyscript/foo4.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
app.cmd_echo = True
12
result = app.foo('aaa', 'bbb', counter=3)
23
out_text = 'Fail'
34
if result:

tests/pyscript/help.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
app.help()
1+
app.cmd_echo = True
2+
app.help()

tests/pyscript/help_media.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
app.cmd_echo = True
12
app.help('media')
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
app.cmd_echo = True
12
app.media.movies.add('My Movie', 'PG-13', director=('George Lucas', 'J. J. Abrams'))

0 commit comments

Comments
 (0)