summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEric Lin <anselor@gmail.com>2018-04-28 16:08:18 -0400
committerEric Lin <anselor@gmail.com>2018-04-28 16:08:18 -0400
commitab7ac493950040565d933ed02deaeb778ef977e3 (patch)
treed6523277fb996b019a6a43714cfbb1ad27106704
parent54f9a7a6f691367642ac47913ff5163e1e5155a7 (diff)
downloadcmd2-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.py64
-rwxr-xr-xexamples/tab_autocompletion.py2
-rw-r--r--tests/pyscript/help.py1
-rw-r--r--tests/pyscript/help_media.py1
-rw-r--r--tests/pyscript/media_movies_list1.py1
-rw-r--r--tests/pyscript/media_movies_list2.py1
-rw-r--r--tests/pyscript/media_movies_list3.py1
-rw-r--r--tests/pyscript/media_movies_list4.py1
-rw-r--r--tests/test_pyscript.py115
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
+