summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xcmd2/cmd2.py18
-rw-r--r--cmd2/pyscript_bridge.py76
-rw-r--r--tests/pyscript/bar1.py1
-rw-r--r--tests/pyscript/custom_echo.py2
-rw-r--r--tests/pyscript/foo1.py1
-rw-r--r--tests/pyscript/foo2.py1
-rw-r--r--tests/pyscript/foo3.py1
-rw-r--r--tests/pyscript/foo4.py1
-rw-r--r--tests/pyscript/help.py3
-rw-r--r--tests/pyscript/help_media.py1
-rw-r--r--tests/pyscript/media_movies_add1.py1
-rw-r--r--tests/pyscript/media_movies_add2.py1
-rw-r--r--tests/pyscript/media_movies_list1.py3
-rw-r--r--tests/pyscript/media_movies_list2.py3
-rw-r--r--tests/pyscript/media_movies_list3.py3
-rw-r--r--tests/pyscript/media_movies_list4.py1
-rw-r--r--tests/pyscript/media_movies_list5.py1
-rw-r--r--tests/pyscript/media_movies_list6.py1
-rw-r--r--tests/pyscript/media_movies_list7.py1
-rw-r--r--tests/pyscript/pyscript_dir1.py3
-rw-r--r--tests/pyscript/pyscript_dir2.py3
-rw-r--r--tests/scripts/recursive.py1
-rw-r--r--tests/test_pyscript.py36
23 files changed, 127 insertions, 36 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 9cbbd639..a2d67def 100755
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -2630,9 +2630,21 @@ Paths or arguments that contain spaces must be enclosed in quotes
Run python code from external files with ``run filename.py``
End with ``Ctrl-D`` (Unix) / ``Ctrl-Z`` (Windows), ``quit()``, '`exit()``.
"""
- banner = 'Entering an embedded IPython shell type quit() or <Ctrl>-d to exit ...'
- exit_msg = 'Leaving IPython, back to {}'.format(sys.argv[0])
- embed(banner1=banner, exit_msg=exit_msg)
+ from .pyscript_bridge import PyscriptBridge
+ bridge = PyscriptBridge(self)
+
+ if self.locals_in_py:
+ def load_ipy(self, app):
+ banner = 'Entering an embedded IPython shell type quit() or <Ctrl>-d to exit ...'
+ exit_msg = 'Leaving IPython, back to {}'.format(sys.argv[0])
+ embed(banner1=banner, exit_msg=exit_msg)
+ load_ipy(self, bridge)
+ else:
+ def load_ipy(app):
+ banner = 'Entering an embedded IPython shell type quit() or <Ctrl>-d to exit ...'
+ exit_msg = 'Leaving IPython, back to {}'.format(sys.argv[0])
+ embed(banner1=banner, exit_msg=exit_msg)
+ load_ipy(bridge)
history_parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter)
history_parser_group = history_parser.add_mutually_exclusive_group()
diff --git a/cmd2/pyscript_bridge.py b/cmd2/pyscript_bridge.py
index 055ae4ae..277d8531 100644
--- a/cmd2/pyscript_bridge.py
+++ b/cmd2/pyscript_bridge.py
@@ -8,10 +8,9 @@ Released under MIT license, see LICENSE file
"""
import argparse
-from collections import namedtuple
import functools
import sys
-from typing import List, Tuple
+from typing import List, Tuple, Callable
# Python 3.4 require contextlib2 for temporarily redirecting stderr and stdout
if sys.version_info < (3, 5):
@@ -41,13 +40,15 @@ class CommandResult(namedtuple_with_defaults('CmdResult', ['stdout', 'stderr', '
class CopyStream(object):
"""Copies all data written to a stream"""
- def __init__(self, inner_stream):
+ def __init__(self, inner_stream, echo: bool = False):
self.buffer = ''
self.inner_stream = inner_stream
+ self.echo = echo
def write(self, s):
self.buffer += s
- self.inner_stream.write(s)
+ if self.echo:
+ self.inner_stream.write(s)
def read(self):
raise NotImplementedError
@@ -55,22 +56,35 @@ class CopyStream(object):
def clear(self):
self.buffer = ''
+ def __getattr__(self, item: str):
+ if item in self.__dict__:
+ return self.__dict__[item]
+ else:
+ return getattr(self.inner_stream, item)
+
-def _exec_cmd(cmd2_app, func):
+def _exec_cmd(cmd2_app, func: Callable, echo: bool):
"""Helper to encapsulate executing a command and capturing the results"""
- copy_stdout = CopyStream(sys.stdout)
- copy_stderr = CopyStream(sys.stderr)
+ copy_stdout = CopyStream(sys.stdout, echo)
+ copy_stderr = CopyStream(sys.stderr, echo)
+
+ copy_cmd_stdout = CopyStream(cmd2_app.stdout, echo)
cmd2_app._last_result = None
- with redirect_stdout(copy_stdout):
- with redirect_stderr(copy_stderr):
- func()
+ try:
+ cmd2_app.stdout = copy_cmd_stdout
+ with redirect_stdout(copy_stdout):
+ with redirect_stderr(copy_stderr):
+ func()
+ finally:
+ cmd2_app.stdout = copy_cmd_stdout.inner_stream
# if stderr is empty, set it to None
- stderr = copy_stderr if copy_stderr.buffer else None
+ stderr = copy_stderr.buffer if copy_stderr.buffer else None
- result = CommandResult(stdout=copy_stdout.buffer, stderr=stderr, data=cmd2_app._last_result)
+ outbuf = copy_cmd_stdout.buffer if copy_cmd_stdout.buffer else copy_stdout.buffer
+ result = CommandResult(stdout=outbuf, stderr=stderr, data=cmd2_app._last_result)
return result
@@ -78,9 +92,10 @@ class ArgparseFunctor:
"""
Encapsulates translating python object traversal
"""
- def __init__(self, cmd2_app, item, parser):
+ def __init__(self, echo: bool, cmd2_app, command_name: str, parser: argparse.ArgumentParser):
+ self._echo = echo
self._cmd2_app = cmd2_app
- self._item = item
+ self._command_name = command_name
self._parser = parser
# Dictionary mapping command argument name to value
@@ -88,7 +103,15 @@ class ArgparseFunctor:
# argparse object for the current command layer
self.__current_subcommand_parser = parser
- def __getattr__(self, item):
+ def __dir__(self):
+ """Returns a custom list of attribute names to match the sub-commands"""
+ commands = []
+ for action in self.__current_subcommand_parser._actions:
+ if not action.option_strings and isinstance(action, argparse._SubParsersAction):
+ commands.extend(action.choices)
+ return commands
+
+ def __getattr__(self, item: str):
"""Search for a subcommand matching this item and update internal state to track the traversal"""
# look for sub-command under the current command/sub-command layer
for action in self.__current_subcommand_parser._actions:
@@ -101,7 +124,6 @@ class ArgparseFunctor:
return self
raise AttributeError(item)
- # return super().__getattr__(item)
def __call__(self, *args, **kwargs):
"""
@@ -182,7 +204,7 @@ class ArgparseFunctor:
def _run(self):
# look up command function
- func = getattr(self._cmd2_app, 'do_' + self._item)
+ func = getattr(self._cmd2_app, 'do_' + self._command_name)
# reconstruct the cmd2 command from the python call
cmd_str = ['']
@@ -238,9 +260,8 @@ class ArgparseFunctor:
traverse_parser(self._parser)
- # print('Command: {}'.format(cmd_str[0]))
+ return _exec_cmd(self._cmd2_app, functools.partial(func, cmd_str[0]), self._echo)
- return _exec_cmd(self._cmd2_app, functools.partial(func, cmd_str[0]))
class PyscriptBridge(object):
"""Preserves the legacy 'cmd' interface for pyscript while also providing a new python API wrapper for
@@ -248,6 +269,7 @@ class PyscriptBridge(object):
def __init__(self, cmd2_app):
self._cmd2_app = cmd2_app
self._last_result = None
+ self.cmd_echo = False
def __getattr__(self, item: str):
"""Check if the attribute is a command. If so, return a callable."""
@@ -261,13 +283,19 @@ class PyscriptBridge(object):
except AttributeError:
# Command doesn't, we will accept parameters in the form of a command string
def wrap_func(args=''):
- return _exec_cmd(self._cmd2_app, functools.partial(func, args))
+ return _exec_cmd(self._cmd2_app, functools.partial(func, args), self.cmd_echo)
return wrap_func
else:
# Command does use argparse, return an object that can traverse the argparse subcommands and arguments
- return ArgparseFunctor(self._cmd2_app, item, parser)
+ return ArgparseFunctor(self.cmd_echo, self._cmd2_app, item, parser)
- raise AttributeError(item)
+ return super().__getattr__(item)
+
+ def __dir__(self):
+ """Return a custom set of attribute names to match the available commands"""
+ commands = list(self._cmd2_app.get_all_commands())
+ commands.insert(0, 'cmd_echo')
+ return commands
- def __call__(self, args):
- return _exec_cmd(self._cmd2_app, functools.partial(self._cmd2_app.onecmd_plus_hooks, args + '\n'))
+ def __call__(self, args: str):
+ return _exec_cmd(self._cmd2_app, functools.partial(self._cmd2_app.onecmd_plus_hooks, args + '\n'), self.cmd_echo)
diff --git a/tests/pyscript/bar1.py b/tests/pyscript/bar1.py
index c6276a87..521e2c29 100644
--- a/tests/pyscript/bar1.py
+++ b/tests/pyscript/bar1.py
@@ -1 +1,2 @@
+app.cmd_echo = True
app.bar('11', '22')
diff --git a/tests/pyscript/custom_echo.py b/tests/pyscript/custom_echo.py
new file mode 100644
index 00000000..14040e4c
--- /dev/null
+++ b/tests/pyscript/custom_echo.py
@@ -0,0 +1,2 @@
+custom.cmd_echo = True
+custom.echo('blah!')
diff --git a/tests/pyscript/foo1.py b/tests/pyscript/foo1.py
index 6e345d95..d9345354 100644
--- a/tests/pyscript/foo1.py
+++ b/tests/pyscript/foo1.py
@@ -1 +1,2 @@
+app.cmd_echo = True
app.foo('aaa', 'bbb', counter=3, trueval=True, constval=True)
diff --git a/tests/pyscript/foo2.py b/tests/pyscript/foo2.py
index d4df7616..d3600a60 100644
--- a/tests/pyscript/foo2.py
+++ b/tests/pyscript/foo2.py
@@ -1 +1,2 @@
+app.cmd_echo = True
app.foo('11', '22', '33', '44', counter=3, trueval=True, constval=True)
diff --git a/tests/pyscript/foo3.py b/tests/pyscript/foo3.py
index db69edaf..fc0e084a 100644
--- a/tests/pyscript/foo3.py
+++ b/tests/pyscript/foo3.py
@@ -1 +1,2 @@
+app.cmd_echo = True
app.foo('11', '22', '33', '44', '55', '66', counter=3, trueval=False, constval=False)
diff --git a/tests/pyscript/foo4.py b/tests/pyscript/foo4.py
index 88fd3ce8..e4b7d01c 100644
--- a/tests/pyscript/foo4.py
+++ b/tests/pyscript/foo4.py
@@ -1,3 +1,4 @@
+app.cmd_echo = True
result = app.foo('aaa', 'bbb', counter=3)
out_text = 'Fail'
if result:
diff --git a/tests/pyscript/help.py b/tests/pyscript/help.py
index 3f67793c..664c0488 100644
--- a/tests/pyscript/help.py
+++ b/tests/pyscript/help.py
@@ -1 +1,2 @@
-app.help() \ No newline at end of file
+app.cmd_echo = True
+app.help()
diff --git a/tests/pyscript/help_media.py b/tests/pyscript/help_media.py
index 78025bdd..d8d97c42 100644
--- a/tests/pyscript/help_media.py
+++ b/tests/pyscript/help_media.py
@@ -1 +1,2 @@
+app.cmd_echo = True
app.help('media')
diff --git a/tests/pyscript/media_movies_add1.py b/tests/pyscript/media_movies_add1.py
index a9139cb1..7249c0ef 100644
--- a/tests/pyscript/media_movies_add1.py
+++ b/tests/pyscript/media_movies_add1.py
@@ -1 +1,2 @@
+app.cmd_echo = True
app.media.movies.add('My Movie', 'PG-13', director=('George Lucas', 'J. J. Abrams'))
diff --git a/tests/pyscript/media_movies_add2.py b/tests/pyscript/media_movies_add2.py
index 5c4617ae..681095d7 100644
--- a/tests/pyscript/media_movies_add2.py
+++ b/tests/pyscript/media_movies_add2.py
@@ -1 +1,2 @@
+app.cmd_echo = True
app.media.movies.add('My Movie', 'PG-13', actor=('Mark Hamill'), director=('George Lucas', 'J. J. Abrams'))
diff --git a/tests/pyscript/media_movies_list1.py b/tests/pyscript/media_movies_list1.py
index 0124bbcb..edbc2021 100644
--- a/tests/pyscript/media_movies_list1.py
+++ b/tests/pyscript/media_movies_list1.py
@@ -1 +1,2 @@
-app.media.movies.list() \ No newline at end of file
+app.cmd_echo = True
+app.media.movies.list()
diff --git a/tests/pyscript/media_movies_list2.py b/tests/pyscript/media_movies_list2.py
index 83f6c8ff..5ad01b7b 100644
--- a/tests/pyscript/media_movies_list2.py
+++ b/tests/pyscript/media_movies_list2.py
@@ -1 +1,2 @@
-app.media().movies().list() \ No newline at end of file
+app.cmd_echo = True
+app.media().movies().list()
diff --git a/tests/pyscript/media_movies_list3.py b/tests/pyscript/media_movies_list3.py
index 4fcf1288..bdbdfceb 100644
--- a/tests/pyscript/media_movies_list3.py
+++ b/tests/pyscript/media_movies_list3.py
@@ -1 +1,2 @@
-app('media movies list') \ No newline at end of file
+app.cmd_echo = True
+app('media movies list')
diff --git a/tests/pyscript/media_movies_list4.py b/tests/pyscript/media_movies_list4.py
index 1165b0c5..5f7bdaa9 100644
--- a/tests/pyscript/media_movies_list4.py
+++ b/tests/pyscript/media_movies_list4.py
@@ -1 +1,2 @@
+app.cmd_echo = True
app.media.movies.list(actor='Mark Hamill')
diff --git a/tests/pyscript/media_movies_list5.py b/tests/pyscript/media_movies_list5.py
index 962b1516..fa4efa5b 100644
--- a/tests/pyscript/media_movies_list5.py
+++ b/tests/pyscript/media_movies_list5.py
@@ -1 +1,2 @@
+app.cmd_echo = True
app.media.movies.list(actor=('Mark Hamill', 'Carrie Fisher'))
diff --git a/tests/pyscript/media_movies_list6.py b/tests/pyscript/media_movies_list6.py
index 5f8d3654..ef1851cd 100644
--- a/tests/pyscript/media_movies_list6.py
+++ b/tests/pyscript/media_movies_list6.py
@@ -1 +1,2 @@
+app.cmd_echo = True
app.media.movies.list(rating='PG')
diff --git a/tests/pyscript/media_movies_list7.py b/tests/pyscript/media_movies_list7.py
index bb0e28bb..7c827b7f 100644
--- a/tests/pyscript/media_movies_list7.py
+++ b/tests/pyscript/media_movies_list7.py
@@ -1 +1,2 @@
+app.cmd_echo = True
app.media.movies.list(rating=('PG', 'PG-13'))
diff --git a/tests/pyscript/pyscript_dir1.py b/tests/pyscript/pyscript_dir1.py
new file mode 100644
index 00000000..14a70a31
--- /dev/null
+++ b/tests/pyscript/pyscript_dir1.py
@@ -0,0 +1,3 @@
+out = dir(app)
+out.sort()
+print(out)
diff --git a/tests/pyscript/pyscript_dir2.py b/tests/pyscript/pyscript_dir2.py
new file mode 100644
index 00000000..28c61c8e
--- /dev/null
+++ b/tests/pyscript/pyscript_dir2.py
@@ -0,0 +1,3 @@
+out = dir(app.media)
+out.sort()
+print(out)
diff --git a/tests/scripts/recursive.py b/tests/scripts/recursive.py
index 32c981b6..4c29d317 100644
--- a/tests/scripts/recursive.py
+++ b/tests/scripts/recursive.py
@@ -3,4 +3,5 @@
"""
Example demonstrating that running a Python script recursively inside another Python script isn't allowed
"""
+app.cmd_echo = True
app('pyscript ../script.py')
diff --git a/tests/test_pyscript.py b/tests/test_pyscript.py
index 8d0cefd8..73c1a62a 100644
--- a/tests/test_pyscript.py
+++ b/tests/test_pyscript.py
@@ -101,7 +101,14 @@ class PyscriptExample(Cmd):
@with_argparser(bar_parser)
def do_bar(self, args):
- print('bar ' + str(args.__dict__))
+ out = 'bar '
+ arg_dict = args.__dict__
+ keys = list(arg_dict.keys())
+ keys.sort()
+ out += '{'
+ for key in keys:
+ out += "'{}':'{}'".format(key, arg_dict[key])
+ print(out)
@pytest.fixture
@@ -160,7 +167,7 @@ def test_pyscript_help(ps_app, capsys, request, command, pyscript_file):
('foo aaa bbb -ccc -t -n', 'foo1.py'),
('foo 11 22 33 44 -ccc -t -n', 'foo2.py'),
('foo 11 22 33 44 55 66 -ccc', 'foo3.py'),
- ('bar 11 22', 'bar1.py')
+ ('bar 11 22', 'bar1.py'),
])
def test_pyscript_out(ps_app, capsys, request, command, pyscript_file):
test_dir = os.path.dirname(request.module.__file__)
@@ -204,11 +211,30 @@ def test_pyscript_results(ps_app, capsys, request, pyscript_file, exp_out):
assert exp_out in expected
-def test_pyscript_custom_name(ps_echo, capsys):
+@pytest.mark.parametrize('expected, pyscript_file', [
+ ("['_relative_load', 'alias', 'bar', 'cmd_echo', 'edit', 'eof', 'eos', 'foo', 'help', 'history', 'load', 'media', 'py', 'pyscript', 'quit', 'set', 'shell', 'shortcuts', 'unalias']",
+ 'pyscript_dir1.py'),
+ ("['movies', 'shows']", 'pyscript_dir2.py')
+])
+def test_pyscript_dir(ps_app, capsys, request, expected, pyscript_file):
+ test_dir = os.path.dirname(request.module.__file__)
+ python_script = os.path.join(test_dir, 'pyscript', pyscript_file)
+
+ run_cmd(ps_app, 'pyscript {}'.format(python_script))
+ out, _ = capsys.readouterr()
+ out = out.strip()
+ assert len(out) > 0
+ assert out == expected
+
+
+def test_pyscript_custom_name(ps_echo, capsys, request):
message = 'blah!'
- run_cmd(ps_echo, 'py custom.echo("{}")'.format(message))
+
+ test_dir = os.path.dirname(request.module.__file__)
+ python_script = os.path.join(test_dir, 'pyscript', 'custom_echo.py')
+
+ run_cmd(ps_echo, 'pyscript {}'.format(python_script))
expected, _ = capsys.readouterr()
assert len(expected) > 0
expected = expected.splitlines()
assert message == expected[0]
-