From ab8194e92b9c3728d8f86cb9c81de180b6884eee Mon Sep 17 00:00:00 2001 From: Eric Lin Date: Fri, 4 May 2018 18:05:00 -0400 Subject: Some more pyscripting tweaks. Fixed issue with capturing ppaged output. Added pyscript bridge to ipy command. Saving progress. --- cmd2/pyscript_bridge.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) (limited to 'cmd2/pyscript_bridge.py') diff --git a/cmd2/pyscript_bridge.py b/cmd2/pyscript_bridge.py index 055ae4ae..ecd2b622 100644 --- a/cmd2/pyscript_bridge.py +++ b/cmd2/pyscript_bridge.py @@ -55,22 +55,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): """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 -- cgit v1.2.1 From ff89bad1b0dd2a608081db5a8fa299ef43d66bc5 Mon Sep 17 00:00:00 2001 From: Eric Lin Date: Thu, 17 May 2018 18:08:52 -0400 Subject: 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 +++++++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 15 deletions(-) (limited to 'cmd2/pyscript_bridge.py') diff --git a/cmd2/pyscript_bridge.py b/cmd2/pyscript_bridge.py index ecd2b622..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 @@ -62,12 +64,12 @@ class CopyStream(object): 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 @@ 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 @@ -114,7 +125,6 @@ class ArgparseFunctor: return self raise AttributeError(item) - # return super().__getattr__(item) def __call__(self, *args, **kwargs): """ @@ -251,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 @@ -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 @@ 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) -- cgit v1.2.1 From 4699451df8d0b0ff87b2332ab26498e372309ec4 Mon Sep 17 00:00:00 2001 From: Eric Lin Date: Fri, 18 May 2018 10:40:28 -0400 Subject: Added type hinting. --- cmd2/pyscript_bridge.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) (limited to 'cmd2/pyscript_bridge.py') diff --git a/cmd2/pyscript_bridge.py b/cmd2/pyscript_bridge.py index a1c367e2..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,7 +40,7 @@ class CommandResult(namedtuple_with_defaults('CmdResult', ['stdout', 'stderr', ' 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 @@ class CopyStream(object): 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 @@ class ArgparseFunctor: 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 @@ 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 = [''] @@ -298,5 +297,5 @@ class PyscriptBridge(object): 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) -- cgit v1.2.1 From 1c1da0273faf3ab2316b4af9281cf15869b3da20 Mon Sep 17 00:00:00 2001 From: Eric Lin Date: Sat, 19 May 2018 15:54:08 -0400 Subject: Changed some unit tests to use pytest-mock instead of mocker/monkeypatch because they were failing for me. Added detection of ==SUPPRESS== in subcommand group names to avoid printing it in the help hint. Added some examples to tab_autocompletion to demonstrate how to tie in to cmd2 path_complete --- cmd2/pyscript_bridge.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'cmd2/pyscript_bridge.py') diff --git a/cmd2/pyscript_bridge.py b/cmd2/pyscript_bridge.py index 277d8531..196be82b 100644 --- a/cmd2/pyscript_bridge.py +++ b/cmd2/pyscript_bridge.py @@ -230,7 +230,7 @@ class ArgparseFunctor: if action.option_strings: cmd_str[0] += '{} '.format(action.option_strings[0]) - if isinstance(value, List) or isinstance(value, Tuple): + if isinstance(value, List) or isinstance(value, tuple): for item in value: item = str(item).strip() if ' ' in item: @@ -250,7 +250,7 @@ class ArgparseFunctor: cmd_str[0] += '{} '.format(self._args[action.dest]) traverse_parser(action.choices[self._args[action.dest]]) elif isinstance(action, argparse._AppendAction): - if isinstance(self._args[action.dest], List) or isinstance(self._args[action.dest], Tuple): + if isinstance(self._args[action.dest], list) or isinstance(self._args[action.dest], tuple): for values in self._args[action.dest]: process_flag(action, values) else: -- cgit v1.2.1