From ab8194e92b9c3728d8f86cb9c81de180b6884eee Mon Sep 17 00:00:00 2001 From: Eric Lin Date: Fri, 4 May 2018 18:05:00 -0400 Subject: [PATCH 1/3] Some more pyscripting tweaks. Fixed issue with capturing ppaged output. Added pyscript bridge to ipy command. Saving progress. --- cmd2/cmd2.py | 18 +++++++++++++++--- cmd2/pyscript_bridge.py | 21 +++++++++++++++++---- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index f4f30bd4c..63a3cbe34 100755 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -2927,9 +2927,21 @@ def do_ipy(self, arg): 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 -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 -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 -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 055ae4ae7..ecd2b622d 100644 --- a/cmd2/pyscript_bridge.py +++ b/cmd2/pyscript_bridge.py @@ -55,22 +55,35 @@ def read(self): 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): """Helper to encapsulate executing a command and capturing the results""" copy_stdout = CopyStream(sys.stdout) copy_stderr = CopyStream(sys.stderr) + copy_cmd_stdout = CopyStream(cmd2_app.stdout) + 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 - 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 From ff89bad1b0dd2a608081db5a8fa299ef43d66bc5 Mon Sep 17 00:00:00 2001 From: Eric Lin Date: Thu, 17 May 2018 18:08:52 -0400 Subject: [PATCH 2/3] 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. --- cmd2/pyscript_bridge.py | 46 +++++++++++++++++++--------- tests/pyscript/bar1.py | 1 + tests/pyscript/custom_echo.py | 2 ++ tests/pyscript/foo1.py | 1 + tests/pyscript/foo2.py | 1 + tests/pyscript/foo3.py | 1 + tests/pyscript/foo4.py | 1 + tests/pyscript/help.py | 3 +- tests/pyscript/help_media.py | 1 + tests/pyscript/media_movies_add1.py | 1 + tests/pyscript/media_movies_add2.py | 1 + tests/pyscript/media_movies_list1.py | 3 +- tests/pyscript/media_movies_list2.py | 3 +- tests/pyscript/media_movies_list3.py | 3 +- tests/pyscript/media_movies_list4.py | 1 + tests/pyscript/media_movies_list5.py | 1 + tests/pyscript/media_movies_list6.py | 1 + tests/pyscript/media_movies_list7.py | 1 + tests/pyscript/pyscript_dir1.py | 3 ++ tests/pyscript/pyscript_dir2.py | 3 ++ tests/scripts/recursive.py | 1 + tests/test_pyscript.py | 36 +++++++++++++++++++--- 22 files changed, 91 insertions(+), 24 deletions(-) create mode 100644 tests/pyscript/custom_echo.py create mode 100644 tests/pyscript/pyscript_dir1.py create mode 100644 tests/pyscript/pyscript_dir2.py diff --git a/cmd2/pyscript_bridge.py b/cmd2/pyscript_bridge.py index ecd2b622d..a1c367e22 100644 --- a/cmd2/pyscript_bridge.py +++ b/cmd2/pyscript_bridge.py @@ -41,13 +41,15 @@ def __bool__(self): 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 @@ -62,12 +64,12 @@ def __getattr__(self, item): 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) + copy_cmd_stdout = CopyStream(cmd2_app.stdout, echo) cmd2_app._last_result = None @@ -80,7 +82,7 @@ def _exec_cmd(cmd2_app, func): 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 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) @@ -91,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 @@ -101,6 +104,14 @@ def __init__(self, cmd2_app, item, parser): # 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 @@ -114,7 +125,6 @@ def __getattr__(self, item): return self raise AttributeError(item) - # return super().__getattr__(item) def __call__(self, *args, **kwargs): """ @@ -251,9 +261,8 @@ def traverse_parser(parser): 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 @@ -261,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.""" @@ -274,13 +284,19 @@ def __getattr__(self, item: str): 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 c6276a875..521e2c298 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 000000000..14040e4c5 --- /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 6e345d95e..d93453548 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 d4df7616c..d3600a60c 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 db69edaf6..fc0e084a7 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 88fd3ce81..e4b7d01c8 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 3f67793c3..664c04880 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 78025bdd2..d8d97c42e 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 a9139cb14..7249c0eff 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 5c4617ae9..681095d70 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 0124bbcb8..edbc2021f 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 83f6c8fff..5ad01b7bf 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 4fcf12883..bdbdfcebd 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 1165b0c56..5f7bdaa96 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 962b15164..fa4efa5b0 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 5f8d36541..ef1851cd2 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 bb0e28bb3..7c827b7fb 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 000000000..14a70a316 --- /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 000000000..28c61c8ee --- /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 32c981b65..4c29d317a 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 8d0cefd8b..73c1a62a2 100644 --- a/tests/test_pyscript.py +++ b/tests/test_pyscript.py @@ -101,7 +101,14 @@ def do_foo(self, args): @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] - From 4699451df8d0b0ff87b2332ab26498e372309ec4 Mon Sep 17 00:00:00 2001 From: Eric Lin Date: Fri, 18 May 2018 10:40:28 -0400 Subject: [PATCH 3/3] Added type hinting. --- cmd2/pyscript_bridge.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/cmd2/pyscript_bridge.py b/cmd2/pyscript_bridge.py index a1c367e22..277d8531b 100644 --- a/cmd2/pyscript_bridge.py +++ b/cmd2/pyscript_bridge.py @@ -8,10 +8,9 @@ """ 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,7 +40,7 @@ def __bool__(self): class CopyStream(object): """Copies all data written to a stream""" - def __init__(self, inner_stream, echo): + def __init__(self, inner_stream, echo: bool = False): self.buffer = '' self.inner_stream = inner_stream self.echo = echo @@ -57,14 +56,14 @@ def read(self): def clear(self): self.buffer = '' - def __getattr__(self, item): + 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, echo): +def _exec_cmd(cmd2_app, func: Callable, echo: bool): """Helper to encapsulate executing a command and capturing the results""" copy_stdout = CopyStream(sys.stdout, echo) copy_stderr = CopyStream(sys.stderr, echo) @@ -93,10 +92,10 @@ class ArgparseFunctor: """ Encapsulates translating python object traversal """ - def __init__(self, echo: bool, 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 @@ -112,7 +111,7 @@ def __dir__(self): commands.extend(action.choices) return commands - def __getattr__(self, item): + 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: @@ -205,7 +204,7 @@ def __call__(self, *args, **kwargs): 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 = [''] @@ -298,5 +297,5 @@ def __dir__(self): commands.insert(0, 'cmd_echo') return commands - def __call__(self, args): + def __call__(self, args: str): return _exec_cmd(self._cmd2_app, functools.partial(self._cmd2_app.onecmd_plus_hooks, args + '\n'), self.cmd_echo)