diff options
author | Eric Lin <anselor@gmail.com> | 2018-04-28 16:08:18 -0400 |
---|---|---|
committer | Eric Lin <anselor@gmail.com> | 2018-04-28 16:08:18 -0400 |
commit | ab7ac493950040565d933ed02deaeb778ef977e3 (patch) | |
tree | d6523277fb996b019a6a43714cfbb1ad27106704 | |
parent | 54f9a7a6f691367642ac47913ff5163e1e5155a7 (diff) | |
download | cmd2-git-ab7ac493950040565d933ed02deaeb778ef977e3.tar.gz |
Added support for translating function positional and keyword arguments into argparse command positional and flag arguments.
Added initial set of tests
-rw-r--r-- | cmd2/pyscript_bridge.py | 64 | ||||
-rwxr-xr-x | examples/tab_autocompletion.py | 2 | ||||
-rw-r--r-- | tests/pyscript/help.py | 1 | ||||
-rw-r--r-- | tests/pyscript/help_media.py | 1 | ||||
-rw-r--r-- | tests/pyscript/media_movies_list1.py | 1 | ||||
-rw-r--r-- | tests/pyscript/media_movies_list2.py | 1 | ||||
-rw-r--r-- | tests/pyscript/media_movies_list3.py | 1 | ||||
-rw-r--r-- | tests/pyscript/media_movies_list4.py | 1 | ||||
-rw-r--r-- | tests/test_pyscript.py | 115 |
9 files changed, 177 insertions, 10 deletions
diff --git a/cmd2/pyscript_bridge.py b/cmd2/pyscript_bridge.py index d48317c3..a9bbc311 100644 --- a/cmd2/pyscript_bridge.py +++ b/cmd2/pyscript_bridge.py @@ -1,7 +1,14 @@ -"""Bridges calls made inside of pyscript with the Cmd2 host app while maintaining a reasonable -degree of isolation between the two""" +""" +Bridges calls made inside of pyscript with the Cmd2 host app while maintaining a reasonable +degree of isolation between the two + +Copyright 2018 Eric Lin <anselor@gmail.com> +Released under MIT license, see LICENSE file +""" import argparse +from typing import List, Tuple + class ArgparseFunctor: def __init__(self, cmd2_app, item, parser): @@ -9,11 +16,14 @@ class ArgparseFunctor: self._item = item self._parser = parser + # Dictionary mapping command argument name to value self._args = {} + # argparse object for the current command layer self.__current_subcommand_parser = parser def __getattr__(self, item): - # look for sub-command + """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: if not action.option_strings and isinstance(action, argparse._SubParsersAction): if item in action.choices: @@ -22,20 +32,30 @@ class ArgparseFunctor: self.__current_subcommand_parser = action.choices[item] self._args[action.dest] = item return self + return super().__getatttr__(item) def __call__(self, *args, **kwargs): + """ + Process the arguments at this layer of the argparse command tree. If there are more sub-commands, + return self to accept the next sub-command name. If there are no more sub-commands, execute the + sub-command with the given parameters. + """ next_pos_index = 0 has_subcommand = False consumed_kw = [] + + # Iterate through the current sub-command's arguments in order for action in self.__current_subcommand_parser._actions: # is this a flag option? if action.option_strings: + # this is a flag argument, search for the argument by name in the parameters if action.dest in kwargs: self._args[action.dest] = kwargs[action.dest] consumed_kw.append(action.dest) else: + # This is a positional argument, search the positional arguments passed in. if not isinstance(action, argparse._SubParsersAction): if next_pos_index < len(args): self._args[action.dest] = args[next_pos_index] @@ -43,6 +63,7 @@ class ArgparseFunctor: else: has_subcommand = True + # Check if there are any extra arguments we don't know how to handle for kw in kwargs: if kw not in consumed_kw: raise TypeError('{}() got an unexpected keyword argument \'{}\''.format( @@ -60,19 +81,38 @@ class ArgparseFunctor: # reconstruct the cmd2 command from the python call cmd_str = [''] + def process_flag(action, value): + # was the argument a flag? + if action.option_strings: + cmd_str[0] += '{} '.format(action.option_strings[0]) + + if isinstance(value, List) or isinstance(value, Tuple): + for item in value: + item = str(item).strip() + if ' ' in item: + item = '"{}"'.format(item) + cmd_str[0] += '{} '.format(item) + else: + value = str(value).strip() + if ' ' in value: + value = '"{}"'.format(value) + cmd_str[0] += '{} '.format(value) + def traverse_parser(parser): for action in parser._actions: # was something provided for the argument if action.dest in self._args: if isinstance(action, argparse._SubParsersAction): + 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): + for values in self._args[action.dest]: + process_flag(action, values) + else: + process_flag(action, self._args[action.dest]) else: - # was the argument a flag? - if action.option_strings: - cmd_str[0] += '{} '.format(action.option_strings[0]) - - # TODO: Handle 'narg' and 'append' options - cmd_str[0] += '"{}" '.format(self._args[action.dest]) + process_flag(action, self._args[action.dest]) traverse_parser(self._parser) @@ -81,23 +121,29 @@ class ArgparseFunctor: class PyscriptBridge(object): + """Preserves the legacy 'cmd' interface for pyscript while also providing a new python API wrapper for + application commands.""" def __init__(self, cmd2_app): self._cmd2_app = cmd2_app self._last_result = None def __getattr__(self, item: str): + """Check if the attribute is a command. If so, return a callable.""" commands = self._cmd2_app.get_all_commands() if item in commands: func = getattr(self._cmd2_app, 'do_' + item) try: + # See if the command uses argparse parser = getattr(func, 'argparser') except AttributeError: + # Command doesn't, we will accept parameters in the form of a command string def wrap_func(args=''): func(args) return self._cmd2_app._last_result 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 super().__getattr__(item) diff --git a/examples/tab_autocompletion.py b/examples/tab_autocompletion.py index a1a8daee..078faac2 100755 --- a/examples/tab_autocompletion.py +++ b/examples/tab_autocompletion.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # coding=utf-8 """ A example usage of the AutoCompleter diff --git a/tests/pyscript/help.py b/tests/pyscript/help.py new file mode 100644 index 00000000..3f67793c --- /dev/null +++ b/tests/pyscript/help.py @@ -0,0 +1 @@ +app.help()
\ No newline at end of file diff --git a/tests/pyscript/help_media.py b/tests/pyscript/help_media.py new file mode 100644 index 00000000..78025bdd --- /dev/null +++ b/tests/pyscript/help_media.py @@ -0,0 +1 @@ +app.help('media') diff --git a/tests/pyscript/media_movies_list1.py b/tests/pyscript/media_movies_list1.py new file mode 100644 index 00000000..0124bbcb --- /dev/null +++ b/tests/pyscript/media_movies_list1.py @@ -0,0 +1 @@ +app.media.movies.list()
\ No newline at end of file diff --git a/tests/pyscript/media_movies_list2.py b/tests/pyscript/media_movies_list2.py new file mode 100644 index 00000000..83f6c8ff --- /dev/null +++ b/tests/pyscript/media_movies_list2.py @@ -0,0 +1 @@ +app.media().movies().list()
\ No newline at end of file diff --git a/tests/pyscript/media_movies_list3.py b/tests/pyscript/media_movies_list3.py new file mode 100644 index 00000000..4fcf1288 --- /dev/null +++ b/tests/pyscript/media_movies_list3.py @@ -0,0 +1 @@ +app('media movies list')
\ No newline at end of file diff --git a/tests/pyscript/media_movies_list4.py b/tests/pyscript/media_movies_list4.py new file mode 100644 index 00000000..0124bbcb --- /dev/null +++ b/tests/pyscript/media_movies_list4.py @@ -0,0 +1 @@ +app.media.movies.list()
\ No newline at end of file diff --git a/tests/test_pyscript.py b/tests/test_pyscript.py new file mode 100644 index 00000000..36970ddf --- /dev/null +++ b/tests/test_pyscript.py @@ -0,0 +1,115 @@ +""" +Unit/functional testing for argparse completer in cmd2 + +Copyright 2018 Eric Lin <anselor@gmail.com> +Released under MIT license, see LICENSE file +""" +import os +import pytest +from cmd2.cmd2 import Cmd, with_argparser +from cmd2 import argparse_completer +from .conftest import run_cmd, normalize, StdOut, complete_tester + +class PyscriptExample(Cmd): + ratings_types = ['G', 'PG', 'PG-13', 'R', 'NC-17'] + + def _do_media_movies(self, args) -> None: + if not args.command: + self.do_help('media movies') + else: + print('media movies ' + str(args.__dict__)) + + def _do_media_shows(self, args) -> None: + if not args.command: + self.do_help('media shows') + + if not args.command: + self.do_help('media shows') + else: + print('media shows ' + str(args.__dict__)) + + media_parser = argparse_completer.ACArgumentParser(prog='media') + + media_types_subparsers = media_parser.add_subparsers(title='Media Types', dest='type') + + movies_parser = media_types_subparsers.add_parser('movies') + movies_parser.set_defaults(func=_do_media_movies) + + movies_commands_subparsers = movies_parser.add_subparsers(title='Commands', dest='command') + + movies_list_parser = movies_commands_subparsers.add_parser('list') + + movies_list_parser.add_argument('-t', '--title', help='Title Filter') + movies_list_parser.add_argument('-r', '--rating', help='Rating Filter', nargs='+', + choices=ratings_types) + movies_list_parser.add_argument('-d', '--director', help='Director Filter') + movies_list_parser.add_argument('-a', '--actor', help='Actor Filter', action='append') + + movies_add_parser = movies_commands_subparsers.add_parser('add') + movies_add_parser.add_argument('title', help='Movie Title') + movies_add_parser.add_argument('rating', help='Movie Rating', choices=ratings_types) + movies_add_parser.add_argument('-d', '--director', help='Director', nargs=(1, 2), required=True) + movies_add_parser.add_argument('actor', help='Actors', nargs='*') + + movies_delete_parser = movies_commands_subparsers.add_parser('delete') + + shows_parser = media_types_subparsers.add_parser('shows') + shows_parser.set_defaults(func=_do_media_shows) + + shows_commands_subparsers = shows_parser.add_subparsers(title='Commands', dest='command') + + shows_list_parser = shows_commands_subparsers.add_parser('list') + + @with_argparser(media_parser) + def do_media(self, args): + """Media management command demonstrates multiple layers of subcommands being handled by AutoCompleter""" + func = getattr(args, 'func', None) + if func is not None: + # Call whatever subcommand function was selected + func(self, args) + else: + # No subcommand was provided, so call help + self.do_help('media') + + +@pytest.fixture +def ps_app(): + c = PyscriptExample() + c.stdout = StdOut() + + return c + + +@pytest.mark.parametrize('command, pyscript_file', [ + ('help', 'help.py'), + ('help media', 'help_media.py'), +]) +def test_pyscript_help(ps_app, capsys, request, command, pyscript_file): + test_dir = os.path.dirname(request.module.__file__) + python_script = os.path.join(test_dir, 'pyscript', pyscript_file) + expected = run_cmd(ps_app, command) + + assert len(expected) > 0 + assert len(expected[0]) > 0 + out = run_cmd(ps_app, 'pyscript {}'.format(python_script)) + assert len(out) > 0 + assert out == expected + + +@pytest.mark.parametrize('command, pyscript_file', [ + ('media movies list', 'media_movies_list1.py'), + ('media movies list', 'media_movies_list2.py'), + ('media movies list', 'media_movies_list3.py'), +]) +def test_pyscript_out(ps_app, capsys, request, command, pyscript_file): + test_dir = os.path.dirname(request.module.__file__) + python_script = os.path.join(test_dir, 'pyscript', pyscript_file) + run_cmd(ps_app, command) + expected, _ = capsys.readouterr() + + assert len(expected) > 0 + run_cmd(ps_app, 'pyscript {}'.format(python_script)) + out, _ = capsys.readouterr() + assert len(out) > 0 + assert out == expected + |