diff options
author | Eric Lin <anselor@gmail.com> | 2018-04-24 16:17:25 -0400 |
---|---|---|
committer | Eric Lin <anselor@gmail.com> | 2018-04-24 16:17:25 -0400 |
commit | 0a1c41ce7048b45fc7ef9b0176d988c26861224e (patch) | |
tree | df27473d38566e24dbf6bb5179cf32bb5f966193 | |
parent | f11b06374aaf56b755de33a763220140d36eab64 (diff) | |
download | cmd2-git-0a1c41ce7048b45fc7ef9b0176d988c26861224e.tar.gz |
Initial approach to the pyscript revamp.
Doesn't handle all argparse argument options yet (nargs, append, flag, probably more)
For #368
-rwxr-xr-x | cmd2/argparse_completer.py | 8 | ||||
-rwxr-xr-x | cmd2/cmd2.py | 7 | ||||
-rw-r--r-- | cmd2/pyscript_bridge.py | 107 |
3 files changed, 119 insertions, 3 deletions
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index 03f2d965..8b5246e8 100755 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -75,6 +75,7 @@ from .rl_utils import rl_force_redisplay # define the completion choices for the argument. You may provide a Collection or a Function. ACTION_ARG_CHOICES = 'arg_choices' + class _RangeAction(object): def __init__(self, nargs: Union[int, str, Tuple[int, int], None]): self.nargs_min = None @@ -103,6 +104,7 @@ class _RangeAction(object): self.nargs_adjusted = nargs +# noinspection PyShadowingBuiltins,PyShadowingBuiltins class _StoreRangeAction(argparse._StoreAction, _RangeAction): def __init__(self, option_strings, @@ -131,6 +133,7 @@ class _StoreRangeAction(argparse._StoreAction, _RangeAction): metavar=metavar) +# noinspection PyShadowingBuiltins,PyShadowingBuiltins class _AppendRangeAction(argparse._AppendAction, _RangeAction): def __init__(self, option_strings, @@ -433,7 +436,6 @@ class AutoCompleter(object): return self.basic_complete(text, line, begidx, endidx, completers.keys()) return [] - @staticmethod def _process_action_nargs(action: argparse.Action, arg_state: _ArgumentState) -> None: if isinstance(action, _RangeAction): @@ -571,6 +573,7 @@ class AutoCompleter(object): ############################################################################### +# noinspection PyCompatibility,PyShadowingBuiltins,PyShadowingBuiltins class ACHelpFormatter(argparse.HelpFormatter): """Custom help formatter to configure ordering of help text""" @@ -631,6 +634,7 @@ class ACHelpFormatter(argparse.HelpFormatter): # End cmd2 customization # helper for wrapping lines + # noinspection PyMissingOrEmptyDocstring,PyShadowingNames def get_lines(parts, indent, prefix=None): lines = [] line = [] @@ -722,6 +726,7 @@ class ACHelpFormatter(argparse.HelpFormatter): else: result = default_metavar + # noinspection PyMissingOrEmptyDocstring def format(tuple_size): if isinstance(result, tuple): return result @@ -748,6 +753,7 @@ class ACHelpFormatter(argparse.HelpFormatter): return text.splitlines() +# noinspection PyCompatibility class ACArgumentParser(argparse.ArgumentParser): """Custom argparse class to override error method to change default help text.""" diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 8d8a5b07..4437426e 100755 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -2877,13 +2877,13 @@ Usage: Usage: unalias [-a] name [name ...] Non-python commands can be issued with ``cmd("your command")``. Run python code from external script files with ``run("script.py")`` """ + from .pyscript_bridge import PyscriptBridge if self._in_py: self.perror("Recursively entering interactive Python consoles is not allowed.", traceback_war=False) return self._in_py = True try: - self.pystate['self'] = self arg = arg.strip() # Support the run command even if called prior to invoking an interactive interpreter @@ -2906,8 +2906,11 @@ Usage: Usage: unalias [-a] name [name ...] """ return self.onecmd_plus_hooks(cmd_plus_args + '\n') + bridge = PyscriptBridge(self) + self.pystate['self'] = bridge self.pystate['run'] = run - self.pystate['cmd'] = onecmd_plus_hooks + self.pystate['cmd'] = bridge + self.pystate['app'] = bridge localvars = (self.locals_in_py and self.pystate) or {} interp = InteractiveConsole(locals=localvars) diff --git a/cmd2/pyscript_bridge.py b/cmd2/pyscript_bridge.py new file mode 100644 index 00000000..88e12bfb --- /dev/null +++ b/cmd2/pyscript_bridge.py @@ -0,0 +1,107 @@ +"""Bridges calls made inside of pyscript with the Cmd2 host app while maintaining a reasonable +degree of isolation between the two""" + +import argparse + +class ArgparseFunctor: + def __init__(self, cmd2_app, item, parser): + self._cmd2_app = cmd2_app + self._item = item + self._parser = parser + + self._args = {} + self.__current_subcommand_parser = parser + + def __getattr__(self, item): + # look for sub-command + for action in self.__current_subcommand_parser._actions: + if not action.option_strings and isinstance(action, argparse._SubParsersAction): + if item in action.choices: + # item matches the a sub-command, save our position in argparse, + # save the sub-command, return self to allow next level of traversal + self.__current_subcommand_parser = action.choices[item] + self._args[action.dest] = item + return self + return super().__getatttr__(item) + + def __call__(self, *args, **kwargs): + next_pos_index = 0 + + has_subcommand = False + consumed_kw = [] + for action in self.__current_subcommand_parser._actions: + # is this a flag option? + if action.option_strings: + if action.dest in kwargs: + self._args[action.dest] = kwargs[action.dest] + consumed_kw.append(action.dest) + else: + if not isinstance(action, argparse._SubParsersAction): + if next_pos_index < len(args): + self._args[action.dest] = args[next_pos_index] + next_pos_index += 1 + else: + has_subcommand = True + + for kw in kwargs: + if kw not in consumed_kw: + raise TypeError('{}() got an unexpected keyword argument \'{}\''.format( + self.__current_subcommand_parser.prog, kw)) + + if has_subcommand: + return self + else: + return self._run() + + def _run(self): + # look up command function + func = getattr(self._cmd2_app, 'do_' + self._item) + + # reconstruct the cmd2 command from the python call + cmd_str = [''] + + def traverse_parser(parser): + for action in parser._actions: + # was something provided for the argument + if action.dest in self._args: + # was the argument a flag? + # TODO: Handle 'narg' and 'append' options + if action.option_strings: + cmd_str[0] += '"{}" "{}" '.format(action.option_strings[0], self._args[action.dest]) + else: + cmd_str[0] += '"{}" '.format(self._args[action.dest]) + + if isinstance(action, argparse._SubParsersAction): + traverse_parser(action.choices[self._args[action.dest]]) + traverse_parser(self._parser) + + func(cmd_str[0]) + return self._cmd2_app._last_result + + +class PyscriptBridge(object): + def __init__(self, cmd2_app): + self._cmd2_app = cmd2_app + self._last_result = None + + def __getattr__(self, item: str): + commands = self._cmd2_app.get_all_commands() + if item in commands: + func = getattr(self._cmd2_app, 'do_' + item) + + try: + parser = getattr(func, 'argparser') + except AttributeError: + def wrap_func(args=''): + func(args) + return self._cmd2_app._last_result + return wrap_func + else: + return ArgparseFunctor(self._cmd2_app, item, parser) + + return super().__getattr__(item) + + def __call__(self, args): + self._cmd2_app.onecmd_plus_hooks(args + '\n') + self._last_result = self._cmd2_app._last_result + return self._cmd2_app._last_result |