diff options
-rwxr-xr-x | cmd2/cmd2.py | 18 | ||||
-rw-r--r-- | cmd2/pyscript_bridge.py | 65 | ||||
-rw-r--r-- | tests/pyscript/bar1.py | 1 | ||||
-rw-r--r-- | tests/pyscript/custom_echo.py | 2 | ||||
-rw-r--r-- | tests/pyscript/foo1.py | 1 | ||||
-rw-r--r-- | tests/pyscript/foo2.py | 1 | ||||
-rw-r--r-- | tests/pyscript/foo3.py | 1 | ||||
-rw-r--r-- | tests/pyscript/foo4.py | 1 | ||||
-rw-r--r-- | tests/pyscript/help.py | 3 | ||||
-rw-r--r-- | tests/pyscript/help_media.py | 1 | ||||
-rw-r--r-- | tests/pyscript/media_movies_add1.py | 1 | ||||
-rw-r--r-- | tests/pyscript/media_movies_add2.py | 1 | ||||
-rw-r--r-- | tests/pyscript/media_movies_list1.py | 3 | ||||
-rw-r--r-- | tests/pyscript/media_movies_list2.py | 3 | ||||
-rw-r--r-- | tests/pyscript/media_movies_list3.py | 3 | ||||
-rw-r--r-- | tests/pyscript/media_movies_list4.py | 1 | ||||
-rw-r--r-- | tests/pyscript/media_movies_list5.py | 1 | ||||
-rw-r--r-- | tests/pyscript/media_movies_list6.py | 1 | ||||
-rw-r--r-- | tests/pyscript/media_movies_list7.py | 1 | ||||
-rw-r--r-- | tests/pyscript/pyscript_dir1.py | 3 | ||||
-rw-r--r-- | tests/pyscript/pyscript_dir2.py | 3 | ||||
-rw-r--r-- | tests/scripts/recursive.py | 1 | ||||
-rw-r--r-- | tests/test_pyscript.py | 36 |
23 files changed, 122 insertions, 30 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..a1c367e2 100644 --- a/cmd2/pyscript_bridge.py +++ b/cmd2/pyscript_bridge.py @@ -41,13 +41,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): 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 +57,35 @@ class CopyStream(object): def clear(self): self.buffer = '' + def __getattr__(self, item): + 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, echo): """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,7 +93,8 @@ class ArgparseFunctor: """ Encapsulates translating python object traversal """ - def __init__(self, cmd2_app, item, parser): + def __init__(self, echo: bool, cmd2_app, item, parser): + self._echo = echo self._cmd2_app = cmd2_app self._item = item self._parser = parser @@ -88,6 +104,14 @@ class ArgparseFunctor: # argparse object for the current command layer self.__current_subcommand_parser = parser + 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): """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 @@ -101,7 +125,6 @@ class ArgparseFunctor: return self raise AttributeError(item) - # return super().__getattr__(item) def __call__(self, *args, **kwargs): """ @@ -238,9 +261,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 +270,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 +284,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')) + 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] - |