Skip to content

Commit a38e3f2

Browse files
authored
Merge pull request #405 from python-cmd2/pyscript
Pyscript updates
2 parents 8125d45 + 4699451 commit a38e3f2

23 files changed

+127
-36
lines changed

cmd2/cmd2.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2630,9 +2630,21 @@ def do_ipy(self, arg):
26302630
Run python code from external files with ``run filename.py``
26312631
End with ``Ctrl-D`` (Unix) / ``Ctrl-Z`` (Windows), ``quit()``, '`exit()``.
26322632
"""
2633-
banner = 'Entering an embedded IPython shell type quit() or <Ctrl>-d to exit ...'
2634-
exit_msg = 'Leaving IPython, back to {}'.format(sys.argv[0])
2635-
embed(banner1=banner, exit_msg=exit_msg)
2633+
from .pyscript_bridge import PyscriptBridge
2634+
bridge = PyscriptBridge(self)
2635+
2636+
if self.locals_in_py:
2637+
def load_ipy(self, app):
2638+
banner = 'Entering an embedded IPython shell type quit() or <Ctrl>-d to exit ...'
2639+
exit_msg = 'Leaving IPython, back to {}'.format(sys.argv[0])
2640+
embed(banner1=banner, exit_msg=exit_msg)
2641+
load_ipy(self, bridge)
2642+
else:
2643+
def load_ipy(app):
2644+
banner = 'Entering an embedded IPython shell type quit() or <Ctrl>-d to exit ...'
2645+
exit_msg = 'Leaving IPython, back to {}'.format(sys.argv[0])
2646+
embed(banner1=banner, exit_msg=exit_msg)
2647+
load_ipy(bridge)
26362648

26372649
history_parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter)
26382650
history_parser_group = history_parser.add_mutually_exclusive_group()

cmd2/pyscript_bridge.py

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@
88
"""
99

1010
import argparse
11-
from collections import namedtuple
1211
import functools
1312
import sys
14-
from typing import List, Tuple
13+
from typing import List, Tuple, Callable
1514

1615
# Python 3.4 require contextlib2 for temporarily redirecting stderr and stdout
1716
if sys.version_info < (3, 5):
@@ -41,54 +40,78 @@ def __bool__(self):
4140

4241
class CopyStream(object):
4342
"""Copies all data written to a stream"""
44-
def __init__(self, inner_stream):
43+
def __init__(self, inner_stream, echo: bool = False):
4544
self.buffer = ''
4645
self.inner_stream = inner_stream
46+
self.echo = echo
4747

4848
def write(self, s):
4949
self.buffer += s
50-
self.inner_stream.write(s)
50+
if self.echo:
51+
self.inner_stream.write(s)
5152

5253
def read(self):
5354
raise NotImplementedError
5455

5556
def clear(self):
5657
self.buffer = ''
5758

59+
def __getattr__(self, item: str):
60+
if item in self.__dict__:
61+
return self.__dict__[item]
62+
else:
63+
return getattr(self.inner_stream, item)
64+
5865

59-
def _exec_cmd(cmd2_app, func):
66+
def _exec_cmd(cmd2_app, func: Callable, echo: bool):
6067
"""Helper to encapsulate executing a command and capturing the results"""
61-
copy_stdout = CopyStream(sys.stdout)
62-
copy_stderr = CopyStream(sys.stderr)
68+
copy_stdout = CopyStream(sys.stdout, echo)
69+
copy_stderr = CopyStream(sys.stderr, echo)
70+
71+
copy_cmd_stdout = CopyStream(cmd2_app.stdout, echo)
6372

6473
cmd2_app._last_result = None
6574

66-
with redirect_stdout(copy_stdout):
67-
with redirect_stderr(copy_stderr):
68-
func()
75+
try:
76+
cmd2_app.stdout = copy_cmd_stdout
77+
with redirect_stdout(copy_stdout):
78+
with redirect_stderr(copy_stderr):
79+
func()
80+
finally:
81+
cmd2_app.stdout = copy_cmd_stdout.inner_stream
6982

7083
# if stderr is empty, set it to None
71-
stderr = copy_stderr if copy_stderr.buffer else None
84+
stderr = copy_stderr.buffer if copy_stderr.buffer else None
7285

73-
result = CommandResult(stdout=copy_stdout.buffer, stderr=stderr, data=cmd2_app._last_result)
86+
outbuf = copy_cmd_stdout.buffer if copy_cmd_stdout.buffer else copy_stdout.buffer
87+
result = CommandResult(stdout=outbuf, stderr=stderr, data=cmd2_app._last_result)
7488
return result
7589

7690

7791
class ArgparseFunctor:
7892
"""
7993
Encapsulates translating python object traversal
8094
"""
81-
def __init__(self, cmd2_app, item, parser):
95+
def __init__(self, echo: bool, cmd2_app, command_name: str, parser: argparse.ArgumentParser):
96+
self._echo = echo
8297
self._cmd2_app = cmd2_app
83-
self._item = item
98+
self._command_name = command_name
8499
self._parser = parser
85100

86101
# Dictionary mapping command argument name to value
87102
self._args = {}
88103
# argparse object for the current command layer
89104
self.__current_subcommand_parser = parser
90105

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

103126
raise AttributeError(item)
104-
# return super().__getattr__(item)
105127

106128
def __call__(self, *args, **kwargs):
107129
"""
@@ -182,7 +204,7 @@ def __call__(self, *args, **kwargs):
182204

183205
def _run(self):
184206
# look up command function
185-
func = getattr(self._cmd2_app, 'do_' + self._item)
207+
func = getattr(self._cmd2_app, 'do_' + self._command_name)
186208

187209
# reconstruct the cmd2 command from the python call
188210
cmd_str = ['']
@@ -238,16 +260,16 @@ def traverse_parser(parser):
238260

239261
traverse_parser(self._parser)
240262

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

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

245266
class PyscriptBridge(object):
246267
"""Preserves the legacy 'cmd' interface for pyscript while also providing a new python API wrapper for
247268
application commands."""
248269
def __init__(self, cmd2_app):
249270
self._cmd2_app = cmd2_app
250271
self._last_result = None
272+
self.cmd_echo = False
251273

252274
def __getattr__(self, item: str):
253275
"""Check if the attribute is a command. If so, return a callable."""
@@ -261,13 +283,19 @@ def __getattr__(self, item: str):
261283
except AttributeError:
262284
# Command doesn't, we will accept parameters in the form of a command string
263285
def wrap_func(args=''):
264-
return _exec_cmd(self._cmd2_app, functools.partial(func, args))
286+
return _exec_cmd(self._cmd2_app, functools.partial(func, args), self.cmd_echo)
265287
return wrap_func
266288
else:
267289
# Command does use argparse, return an object that can traverse the argparse subcommands and arguments
268-
return ArgparseFunctor(self._cmd2_app, item, parser)
290+
return ArgparseFunctor(self.cmd_echo, self._cmd2_app, item, parser)
269291

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

272-
def __call__(self, args):
273-
return _exec_cmd(self._cmd2_app, functools.partial(self._cmd2_app.onecmd_plus_hooks, args + '\n'))
300+
def __call__(self, args: str):
301+
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')

0 commit comments

Comments
 (0)