diff --git a/gooey/python_bindings/argparse_to_json.py b/gooey/python_bindings/argparse_to_json.py index 7878979..63e1f85 100644 --- a/gooey/python_bindings/argparse_to_json.py +++ b/gooey/python_bindings/argparse_to_json.py @@ -9,8 +9,10 @@ from argparse import ( _StoreConstAction, _StoreFalseAction, _StoreTrueAction, - ArgumentParser) + ArgumentParser, _SubParsersAction) +from collections import OrderedDict +from functools import partial VALID_WIDGETS = ( 'FileChooser', @@ -29,16 +31,35 @@ VALID_WIDGETS = ( class UnknownWidgetType(Exception): pass +class UnsupportedConfiguration(Exception): + pass + def convert(parser): widget_dict = getattr(parser, 'widgets', {}) + actions = parser._actions + + if has_subparsers(actions): + if has_required(actions): + raise UnsupportedConfiguration("Gooey doesn't currently support required arguments when subprocesers are present.") + layout_type = 'column' + layout_data = {name: process(sub_parser, widget_dict) for name, sub_parser in get_subparser(actions).choices.iteritems()} + else: + layout_type = 'standard' + layout_data = process(parser, widget_dict) + return { + 'layout_type': layout_type, + 'widgets': layout_data + } + + +def process(parser, widget_dict): mutually_exclusive_group = [ mutex_action for group_actions in parser._mutually_exclusive_groups for mutex_action in group_actions._group_actions] - base_actions = [action for action in parser._actions if action not in mutually_exclusive_group and action.dest != 'help'] @@ -46,24 +67,24 @@ def convert(parser): required_actions = filter(is_required, base_actions) optional_actions = filter(is_optional, base_actions) - return { - 'required': list(categorize(required_actions, widget_dict)), - 'optional': list(categorize(optional_actions, widget_dict)) + build_radio_group(mutually_exclusive_group) - } + return list(categorize(required_actions, widget_dict, required=True)) + \ + list(categorize(optional_actions, widget_dict)) + \ + build_radio_group(mutually_exclusive_group) -def categorize(actions, widget_dict): +def categorize(actions, widget_dict, required=False): + _get_widget = partial(get_widget, widgets=widget_dict) for action in actions: if is_standard(action): - yield as_json(action, get_widget(action, widget_dict) or 'TextField') + yield as_json(action, _get_widget(action) or 'TextField', required) elif is_choice(action): - yield as_json(action, get_widget(action, widget_dict) or 'Dropdown') + yield as_json(action, _get_widget(action) or 'Dropdown', required) + elif is_flag(action): + yield as_json(action, _get_widget(action) or 'CheckBox', required) elif is_counter(action): - _json = as_json(action, get_widget(action, widget_dict) or 'Dropdown') - # prefill the 'counter' dropdown + _json = as_json(action, _get_widget(action) or 'Dropdown', required) + # pre-fill the 'counter' dropdown _json['choices'] = range(1, 11) yield _json - elif is_flag(action): - yield as_json(action, get_widget(action, widget_dict) or 'CheckBox') else: raise UnknownWidgetType(action) @@ -74,7 +95,19 @@ def get_widget(action, widgets): def is_required(action): '''_actions which are positional or possessing the `required` flag ''' - return not action.option_strings or action.required == True + return not action.option_strings and not isinstance(action, _SubParsersAction) or action.required == True + +def has_required(actions): + return filter(None, filter(is_required, actions)) + +def is_subparser(action): + return isinstance(action,_SubParsersAction) + +def has_subparsers(actions): + return filter(is_subparser, actions) + +def get_subparser(actions): + return filter(is_subparser, actions)[0] def is_optional(action): '''_actions not positional or possessing the `required` flag''' @@ -124,17 +157,18 @@ def build_radio_group(mutex_group): return [{ 'type': 'RadioGroup', 'group_name': 'Choose Option', + 'required': False, 'data': options }] -def as_json(action, widget): +def as_json(action, widget, required): if widget not in VALID_WIDGETS: raise UnknownWidgetType('Widget Type {0} is unrecognized'.format(widget)) - option_strings = action.option_strings return { 'type': widget, + 'required': required, 'data': { 'display_name': action.dest, 'help': action.help, @@ -146,4 +180,3 @@ def as_json(action, widget): - diff --git a/gooey/tests/argparse_to_json_unittest.py b/gooey/tests/argparse_to_json_unittest.py index f50bf52..0cc080d 100644 --- a/gooey/tests/argparse_to_json_unittest.py +++ b/gooey/tests/argparse_to_json_unittest.py @@ -3,7 +3,7 @@ from gooey.python_bindings.argparse_to_json import * @pytest.fixture -def parser(): +def empty_parser(): return argparse.ArgumentParser(description='description') @pytest.fixture @@ -26,6 +26,27 @@ def complete_parser(): verbosity.add_argument('-j', '--jj', action="store_true", help="hhh") return parser +@pytest.fixture +def subparser(): + parser = argparse.ArgumentParser(description='qidev') + parser.add_argument('--verbose', help='be verbose', dest='verbose', action='store_true', default=False) + subs = parser.add_subparsers(help='commands', dest='command') + + config_parser = subs.add_parser('config', help='configure defaults for qidev') + config_parser.add_argument('field', help='the field to configure', type=str) + config_parser.add_argument('value', help='set field to value', type=str) + + # ######################################################## + connect_parser = subs.add_parser('connect', help='connect to a robot (ip/hostname)') + connect_parser.add_argument('hostname', help='hostname or IP address of the robot', type=str) + + # ######################################################## + install_parser = subs.add_parser('install', help='package and install a project directory on a robot') + install_parser.add_argument('path', help='path to the project directory (containing manifest.xml', type=str) + install_parser.add_argument('--ip', nargs='*', type=str, dest='ip', help='specify hostname(es)/IP address(es)') + return parser + + @pytest.fixture def exclusive_group(): parser = argparse.ArgumentParser(description='description') @@ -39,6 +60,50 @@ def exclusive_group(): return mutually_exclusive_group + + +def test_parser_converts_to_correct_type(empty_parser, complete_parser, subparser): + assert convert(subparser)['layout_type'] == 'column' + assert convert(empty_parser)['layout_type'] == 'standard' + assert convert(complete_parser)['layout_type'] == 'standard' + + +def test_convert_std_parser(complete_parser): + result = convert(complete_parser) + assert result['layout_type'] == 'standard' + assert result['widgets'] + assert isinstance(result['widgets'], list) + + entry = result['widgets'][0] + assert 'type' in entry + assert 'required' in entry + assert 'data' in entry + + required = filter(lambda x: x['required'], result['widgets']) + optional = filter(lambda x: not x['required'], result['widgets']) + assert len(required) == 4 + assert len(optional) == 8 + + +def test_convert_sub_parser(subparser): + result = convert(subparser) + assert result['layout_type'] == 'column' + assert result['widgets'] + assert isinstance(result['widgets'], dict) + assert len(result['widgets']) == 3 + + +def test_has_required(empty_parser, complete_parser, subparser): + assert has_required(complete_parser._actions) + assert not has_required(empty_parser._actions) + assert not has_required(subparser._actions) + + +def test_has_subparsers(subparser, complete_parser): + assert has_subparsers(subparser._actions) + assert not has_subparsers(complete_parser._actions) + + def test_is_required(complete_parser): required = filter(is_required, complete_parser._actions) assert len(required) == 4 @@ -53,31 +118,32 @@ def test_is_optional(complete_parser): assert 'req' not in action.dest -def test_is_choice(parser): - parser.add_argument('--dropdown', choices=[1,2]) - assert is_choice(get_action(parser, 'dropdown')) +def test_is_choice(empty_parser): + empty_parser.add_argument('--dropdown', choices=[1,2]) + assert is_choice(get_action(empty_parser, 'dropdown')) - parser.add_argument('--storetrue', action='store_true') - assert not is_choice(get_action(parser, 'storetrue')) + empty_parser.add_argument('--storetrue', action='store_true') + assert not is_choice(get_action(empty_parser, 'storetrue')) # make sure positionals are caught as well (issue #85) - parser.add_argument('positional', choices=[1, 2]) - assert is_choice(get_action(parser, 'positional')) + empty_parser.add_argument('positional', choices=[1, 2]) + assert is_choice(get_action(empty_parser, 'positional')) + +def test_is_standard(empty_parser): + empty_parser.add_argument('--count', action='count') + assert not is_standard(get_action(empty_parser, 'count')) -def test_is_standard(parser): - parser.add_argument('--count', action='count') - assert not is_standard(get_action(parser, 'count')) + empty_parser.add_argument('--store', action='store') + assert is_standard(get_action(empty_parser, 'store')) - parser.add_argument('--store', action='store') - assert is_standard(get_action(parser, 'store')) -def test_is_counter(parser): - parser.add_argument('--count', action='count') - assert is_counter(get_action(parser, 'count')) +def test_is_counter(empty_parser): + empty_parser.add_argument('--count', action='count') + assert is_counter(get_action(empty_parser, 'count')) - parser.add_argument('--dropdown', choices=[1,2]) - assert not is_counter(get_action(parser, 'dropdown')) + empty_parser.add_argument('--dropdown', choices=[1,2]) + assert not is_counter(get_action(empty_parser, 'dropdown')) def test_mutually(exclusive_group): @@ -97,9 +163,16 @@ def get_action(parser, dest): if action.dest == dest: return action + def find_arg_by_option(group, option_string): for arg in group: if option_string in arg.option_strings: return arg + + + + + +