From 5c6d85864724b93b961bbeee45aaab91501b2b8d Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 12 Oct 2018 19:13:22 -0400 Subject: [PATCH 1/6] Non-argparse commands called as methods of PyscriptBridge now go through onecmd_plus_hooks --- cmd2/pyscript_bridge.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cmd2/pyscript_bridge.py b/cmd2/pyscript_bridge.py index 3c5c61f27..90c46c28a 100644 --- a/cmd2/pyscript_bridge.py +++ b/cmd2/pyscript_bridge.py @@ -310,7 +310,12 @@ def __getattr__(self, item: str): else: # Command doesn't use argparse, 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), self.cmd_echo) + command = item + if args: + command += ' ' + args + return _exec_cmd(self._cmd2_app, + functools.partial(self._cmd2_app.onecmd_plus_hooks, command + '\n'), + self.cmd_echo) return wrap_func else: From b50dfc5dc0c30c6af8e2bcf2dbf058cadf1c9834 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 12 Oct 2018 19:41:54 -0400 Subject: [PATCH 2/6] Argparse commands called as methods of PyscriptBridge now go through onecmd_plus_hooks --- cmd2/pyscript_bridge.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/cmd2/pyscript_bridge.py b/cmd2/pyscript_bridge.py index 90c46c28a..feb50babe 100644 --- a/cmd2/pyscript_bridge.py +++ b/cmd2/pyscript_bridge.py @@ -199,20 +199,21 @@ def _run(self): self._command_name)) # reconstruct the cmd2 command from the python call - cmd_str = [''] + command = self._command_name + ' ' def process_argument(action, value): + nonlocal command if isinstance(action, argparse._CountAction): if isinstance(value, int): for _ in range(value): - cmd_str[0] += '{} '.format(action.option_strings[0]) + command += '{} '.format(action.option_strings[0]) return else: raise TypeError('Expected int for ' + action.dest) if isinstance(action, argparse._StoreConstAction) or isinstance(action, argparse._AppendConstAction): if value: # Nothing else to append to the command string, just the flag is enough. - cmd_str[0] += '{} '.format(action.option_strings[0]) + command += '{} '.format(action.option_strings[0]) return else: # value is not True so we default to false, which means don't include the flag @@ -220,7 +221,7 @@ def process_argument(action, value): # was the argument a flag? if action.option_strings: - cmd_str[0] += '{} '.format(action.option_strings[0]) + command += '{} '.format(action.option_strings[0]) is_remainder_arg = action.dest == self._remainder_arg @@ -231,14 +232,14 @@ def process_argument(action, value): raise ValueError('{} appears to be a flag and should be supplied as a keyword argument ' 'to the function.'.format(item)) item = quote_string_if_needed(item) - cmd_str[0] += '{} '.format(item) + command += '{} '.format(item) # If this is a flag parameter that can accept a variable number of arguments and we have not # reached the max number, add a list completion suffix to tell argparse to move to the next # parameter if action.option_strings and isinstance(action, _RangeAction) and action.nargs_max is not None and \ action.nargs_max > len(value): - cmd_str[0] += '{0}{0} '.format(self._parser.prefix_chars[0]) + command += '{0}{0} '.format(self._parser.prefix_chars[0]) else: value = str(value).strip() @@ -246,18 +247,19 @@ def process_argument(action, value): raise ValueError('{} appears to be a flag and should be supplied as a keyword argument ' 'to the function.'.format(value)) value = quote_string_if_needed(value) - cmd_str[0] += '{} '.format(value) + command += '{} '.format(value) # If this is a flag parameter that can accept a variable number of arguments and we have not # reached the max number, add a list completion suffix to tell argparse to move to the next # parameter if action.option_strings and isinstance(action, _RangeAction) and action.nargs_max is not None and \ action.nargs_max > 1: - cmd_str[0] += '{0}{0} '.format(self._parser.prefix_chars[0]) + command += '{0}{0} '.format(self._parser.prefix_chars[0]) def process_action(action): + nonlocal command if isinstance(action, argparse._SubParsersAction): - cmd_str[0] += '{} '.format(self._args[action.dest]) + command += '{} '.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): @@ -284,8 +286,9 @@ def traverse_parser(parser): process_action(action) traverse_parser(self._parser) - - return _exec_cmd(self._cmd2_app, functools.partial(func, cmd_str[0]), self._echo) + return _exec_cmd(self._cmd2_app, + functools.partial(self._cmd2_app.onecmd_plus_hooks, command.strip() + '\n'), + self._echo) class PyscriptBridge(object): @@ -310,9 +313,7 @@ def __getattr__(self, item: str): else: # Command doesn't use argparse, we will accept parameters in the form of a command string def wrap_func(args=''): - command = item - if args: - command += ' ' + args + command = (item + ' ' + args).strip() return _exec_cmd(self._cmd2_app, functools.partial(self._cmd2_app.onecmd_plus_hooks, command + '\n'), self.cmd_echo) From d3d75f28101d3af09a692eb044cf40a73404c678 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 13 Oct 2018 17:45:01 -0400 Subject: [PATCH 3/6] Refactored --- CHANGELOG.md | 1 + cmd2/pyscript_bridge.py | 55 ++++++++++++++++++++++------------------- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a8affabd..b08732414 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * Enhancements * All platforms now depend on [wcwidth](https://pypi.python.org/pypi/wcwidth) to assist with asynchronous alerts. * Macros now accept extra arguments when called. These will be tacked onto the resolved command. + * All cmd2 commands run via py now go through onecmd_plus_hooks. ## 0.9.5 (October 11, 2018) * Bug Fixes diff --git a/cmd2/pyscript_bridge.py b/cmd2/pyscript_bridge.py index feb50babe..7551ed882 100644 --- a/cmd2/pyscript_bridge.py +++ b/cmd2/pyscript_bridge.py @@ -45,8 +45,14 @@ def __bool__(self) -> bool: return not self.stderr -def _exec_cmd(cmd2_app, func: Callable, echo: bool) -> CommandResult: - """Helper to encapsulate executing a command and capturing the results""" +def _exec_cmd(cmd2_app, command: str, echo: bool) -> CommandResult: + """ + Helper to encapsulate executing a command and capturing the results + :param cmd2_app: cmd2 app that will run the command + :param command: command line being run + :param echo: if True, output will be echoed to stdout/stderr while the command runs + :return: result of the command + """ copy_stdout = StdSim(sys.stdout, echo) copy_stderr = StdSim(sys.stderr, echo) @@ -58,7 +64,8 @@ def _exec_cmd(cmd2_app, func: Callable, echo: bool) -> CommandResult: cmd2_app.stdout = copy_cmd_stdout with redirect_stdout(copy_stdout): with redirect_stderr(copy_stderr): - func() + # Include a newline in case it's a multiline command + cmd2_app.onecmd_plus_hooks(command + '\n') finally: cmd2_app.stdout = copy_cmd_stdout.inner_stream @@ -199,21 +206,21 @@ def _run(self): self._command_name)) # reconstruct the cmd2 command from the python call - command = self._command_name + ' ' + command = self._command_name def process_argument(action, value): nonlocal command if isinstance(action, argparse._CountAction): if isinstance(value, int): for _ in range(value): - command += '{} '.format(action.option_strings[0]) + command += ' {}'.format(action.option_strings[0]) return else: raise TypeError('Expected int for ' + action.dest) if isinstance(action, argparse._StoreConstAction) or isinstance(action, argparse._AppendConstAction): if value: # Nothing else to append to the command string, just the flag is enough. - command += '{} '.format(action.option_strings[0]) + command += ' {}'.format(action.option_strings[0]) return else: # value is not True so we default to false, which means don't include the flag @@ -221,7 +228,7 @@ def process_argument(action, value): # was the argument a flag? if action.option_strings: - command += '{} '.format(action.option_strings[0]) + command += ' {}'.format(action.option_strings[0]) is_remainder_arg = action.dest == self._remainder_arg @@ -232,14 +239,14 @@ def process_argument(action, value): raise ValueError('{} appears to be a flag and should be supplied as a keyword argument ' 'to the function.'.format(item)) item = quote_string_if_needed(item) - command += '{} '.format(item) + command += ' {}'.format(item) # If this is a flag parameter that can accept a variable number of arguments and we have not # reached the max number, add a list completion suffix to tell argparse to move to the next # parameter if action.option_strings and isinstance(action, _RangeAction) and action.nargs_max is not None and \ action.nargs_max > len(value): - command += '{0}{0} '.format(self._parser.prefix_chars[0]) + command += ' {0}{0}'.format(self._parser.prefix_chars[0]) else: value = str(value).strip() @@ -247,19 +254,19 @@ def process_argument(action, value): raise ValueError('{} appears to be a flag and should be supplied as a keyword argument ' 'to the function.'.format(value)) value = quote_string_if_needed(value) - command += '{} '.format(value) + command += ' {}'.format(value) # If this is a flag parameter that can accept a variable number of arguments and we have not # reached the max number, add a list completion suffix to tell argparse to move to the next # parameter if action.option_strings and isinstance(action, _RangeAction) and action.nargs_max is not None and \ action.nargs_max > 1: - command += '{0}{0} '.format(self._parser.prefix_chars[0]) + command += ' {0}{0}'.format(self._parser.prefix_chars[0]) def process_action(action): nonlocal command if isinstance(action, argparse._SubParsersAction): - command += '{} '.format(self._args[action.dest]) + command += ' {}'.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): @@ -286,9 +293,7 @@ def traverse_parser(parser): process_action(action) traverse_parser(self._parser) - return _exec_cmd(self._cmd2_app, - functools.partial(self._cmd2_app.onecmd_plus_hooks, command.strip() + '\n'), - self._echo) + return _exec_cmd(self._cmd2_app, command, self._echo) class PyscriptBridge(object): @@ -313,10 +318,10 @@ def __getattr__(self, item: str): else: # Command doesn't use argparse, we will accept parameters in the form of a command string def wrap_func(args=''): - command = (item + ' ' + args).strip() - return _exec_cmd(self._cmd2_app, - functools.partial(self._cmd2_app.onecmd_plus_hooks, command + '\n'), - self.cmd_echo) + command = item + if args: + command += ' ' + args + return _exec_cmd(self._cmd2_app, command, self.cmd_echo) return wrap_func else: @@ -329,17 +334,15 @@ def __dir__(self): attributes.insert(0, 'cmd_echo') return attributes - def __call__(self, args: str, echo: Optional[bool]=None) -> CommandResult: + def __call__(self, command: str, echo: Optional[bool]=None) -> CommandResult: """ Provide functionality to call application commands by calling PyscriptBridge ex: app('help') - :param args: The string being passed to the command - :param echo: If True, output will be echoed while the command runs - This temporarily overrides the value of self.cmd_echo + :param command: command line being run + :param echo: if True, output will be echoed to stdout/stderr while the command runs + this temporarily overrides the value of self.cmd_echo """ if echo is None: echo = self.cmd_echo - return _exec_cmd(self._cmd2_app, - functools.partial(self._cmd2_app.onecmd_plus_hooks, args + '\n'), - echo) + return _exec_cmd(self._cmd2_app, command, echo) From 058db757857977baaa764ce5422ba7038d678857 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 13 Oct 2018 17:48:51 -0400 Subject: [PATCH 4/6] Added bug fixes to change log --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b08732414..c97852842 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ * All platforms now depend on [wcwidth](https://pypi.python.org/pypi/wcwidth) to assist with asynchronous alerts. * Macros now accept extra arguments when called. These will be tacked onto the resolved command. * All cmd2 commands run via py now go through onecmd_plus_hooks. +* Bug Fixes + * Fixed bug introduced in 0.9.5 caused by backing up and restoring self.prompt in pseudo_raw_input(). + As part of this fix, continuation prompts will not be redrawn with `async_update_prompt` or `async_alert`. ## 0.9.5 (October 11, 2018) * Bug Fixes From 7786b667a4dcd6f4f1b441c876c0228a3f7ca45f Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 13 Oct 2018 18:03:39 -0400 Subject: [PATCH 5/6] Moved bug fixes to top --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c97852842..0b33bd1a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,11 @@ -## 0.9.6 (TBD) +## 0.9.6 (October 13, 2018) +* Bug Fixes + * Fixed bug introduced in 0.9.5 caused by backing up and restoring `self.prompt` in `pseudo_raw_input`. + As part of this fix, continuation prompts will not be redrawn with `async_update_prompt` or `async_alert`. * Enhancements * All platforms now depend on [wcwidth](https://pypi.python.org/pypi/wcwidth) to assist with asynchronous alerts. * Macros now accept extra arguments when called. These will be tacked onto the resolved command. * All cmd2 commands run via py now go through onecmd_plus_hooks. -* Bug Fixes - * Fixed bug introduced in 0.9.5 caused by backing up and restoring self.prompt in pseudo_raw_input(). - As part of this fix, continuation prompts will not be redrawn with `async_update_prompt` or `async_alert`. ## 0.9.5 (October 11, 2018) * Bug Fixes From be82ee5d66f18343cc055b1daf84a0b14454eec7 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 13 Oct 2018 18:10:48 -0400 Subject: [PATCH 6/6] Removed venv --- .gitignore | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 9954fca63..140c73b2a 100644 --- a/.gitignore +++ b/.gitignore @@ -26,8 +26,5 @@ htmlcov dmypy.json dmypy.sock -# cmd2 history file used in main.py +# cmd2 history file used in hello_cmd2.py cmd2_history.txt - -# Virtual environment -venv