From 4c61c29b650b2d43ca2476bf62eefdeb5dceb4ef Mon Sep 17 00:00:00 2001 From: chriskiehl Date: Sat, 27 Jan 2018 13:18:42 -0800 Subject: [PATCH] partial re-write to allow better user customization --- gooey/python_bindings/argparse_to_json.py | 471 ++++++++++++------- gooey/python_bindings/config_generator.py | 118 +++-- gooey/python_bindings/constants.py | 5 + gooey/python_bindings/gooey_decorator.py | 29 +- gooey/python_bindings/gooey_parser.py | 47 +- gooey/python_bindings/parser/gooey_parser.py | 0 6 files changed, 451 insertions(+), 219 deletions(-) create mode 100644 gooey/python_bindings/constants.py create mode 100644 gooey/python_bindings/parser/gooey_parser.py diff --git a/gooey/python_bindings/argparse_to_json.py b/gooey/python_bindings/argparse_to_json.py index f867081..de3b081 100644 --- a/gooey/python_bindings/argparse_to_json.py +++ b/gooey/python_bindings/argparse_to_json.py @@ -1,237 +1,362 @@ """ Converts argparse parser actions into json "Build Specs" """ - +import functools +import pprint import argparse import os +import sys from argparse import ( - _CountAction, - _HelpAction, - _StoreConstAction, - _StoreFalseAction, - _StoreTrueAction, - ArgumentParser, - _SubParsersAction) - + _CountAction, + _HelpAction, + _StoreConstAction, + _StoreFalseAction, + _StoreTrueAction, + _SubParsersAction) from collections import OrderedDict from functools import partial -from itertools import chain +from uuid import uuid4 -import sys +from gooey.util.functional import merge, getin VALID_WIDGETS = ( - 'FileChooser', - 'MultiFileChooser', - 'FileSaver', - 'DirChooser', - 'DateChooser', - 'TextField', - 'Dropdown', - 'Counter', - 'RadioGroup', - 'CheckBox', - 'MultiDirChooser', - 'Textarea', - 'PasswordField', - 'Listbox' + 'FileChooser', + 'MultiFileChooser', + 'FileSaver', + 'DirChooser', + 'DateChooser', + 'TextField', + 'Dropdown', + 'Counter', + 'RadioGroup', + 'CheckBox', + 'MultiDirChooser', + 'Textarea', + 'PasswordField', + 'Listbox' ) class UnknownWidgetType(Exception): - pass + pass + class UnsupportedConfiguration(Exception): - pass + pass -{ - 'siege': { - 'command': 'siege', - 'display_name': 'Siege', - 'contents': [] - } + +group_defaults = { + 'columns': 2, + 'padding': 10, + 'show_border': False } +item_default = { + 'error_color': '#ea7878', + 'validator': { + 'type': 'local', + 'test': 'lambda x: True', + 'message': '' + }, + 'external_validator': { + 'cmd': '', + } +} -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 subparsers are present.") - layout_type = 'column' - layout_data = OrderedDict( - (choose_name(name, sub_parser), { - 'command': name, - 'contents': process(sub_parser, getattr(sub_parser, 'widgets', {})) - }) for name, sub_parser in get_subparser(actions).choices.items()) - - else: - layout_type = 'standard' - layout_data = OrderedDict([ - ('primary', { - 'command': None, - 'contents': process(parser, widget_dict) - }) - ]) - return { - 'layout_type': layout_type, - 'widgets': layout_data - } +def convert(parser, **kwargs): + assert_subparser_constraints(parser) + x = { + 'layout': 'standard', + 'widgets': OrderedDict( + (choose_name(name, sub_parser), { + 'command': name, + 'contents': process(sub_parser, + getattr(sub_parser, 'widgets', {}), + getattr(sub_parser, 'options', {})) + }) for name, sub_parser in iter_parsers(parser)) + } + if kwargs.get('use_legacy_titles'): + return apply_default_rewrites(x) + return x + + +def process(parser, widget_dict, options): + mutex_groups = parser._mutually_exclusive_groups + raw_action_groups = [extract_groups(group) for group in parser._action_groups + if group._group_actions] + corrected_action_groups = reapply_mutex_groups(mutex_groups, raw_action_groups) + + return categorize2(strip_empty(corrected_action_groups), widget_dict, options) + +def strip_empty(groups): + return [group for group in groups if group['items']] + + +def assert_subparser_constraints(parser): + if has_subparsers(parser._actions): + if has_required(parser._actions): + raise UnsupportedConfiguration( + "Gooey doesn't currently support top level required arguments " + "when subparsers are present.") + + +def iter_parsers(parser): + ''' Iterate over name, parser pairs ''' + try: + return get_subparser(parser._actions).choices.items() + except: + return iter([('::gooey/default', parser)]) + + +def extract_groups(action_group): + ''' + Recursively extract argument groups and associated actions + from ParserGroup objects + ''' + return { + 'name': action_group.title, + 'description': action_group.description, + 'items': [action for action in action_group._group_actions + if not is_help_message(action)], + 'groups': [extract_groups(group) + for group in action_group._action_groups], + 'options': merge(group_defaults, + getattr(action_group, 'gooey_options', {})) + } -def process(parser, widget_dict): - mutually_exclusive_groups = [ - [mutex_action for mutex_action in group_actions._group_actions] - for group_actions in parser._mutually_exclusive_groups] - - group_options = list(chain(*mutually_exclusive_groups)) - - base_actions = [action for action in parser._actions - if action not in group_options - and action.dest != 'help'] - - required_actions = filter(is_required, base_actions) - optional_actions = filter(is_optional, base_actions) - - return list(categorize(required_actions, widget_dict, required=True)) + \ - list(categorize(optional_actions, widget_dict)) + \ - list(map(build_radio_group, mutually_exclusive_groups)) - -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) or 'TextField', required) - elif is_choice(action): - 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) or 'Counter', required) - # pre-fill the 'counter' dropdown - _json['data']['choices'] = list(map(str, range(1, 11))) - yield _json - else: - raise UnknownWidgetType(action) -def get_widget(action, widgets): - supplied_widget = widgets.get(action.dest, None) - type_arg_widget = 'FileChooser' if action.type == argparse.FileType else None - return supplied_widget or type_arg_widget or None +def apply_default_rewrites(spec): + top_level_subgroups = list(spec['widgets'].keys()) + + for subgroup in top_level_subgroups: + path = ['widgets', subgroup, 'contents'] + contents = getin(spec, path) + for group in contents: + if group['name'] == 'positional arguments': + group['name'] = 'Required Arguments' + if group['name'] == 'optional arguments': + group['name'] = 'Optional Arguments' + return spec + + +def contains_actions(a, b): + ''' check if any actions(a) are present in actions(b) ''' + return set(a).intersection(set(b)) + + +def reapply_mutex_groups(mutex_groups, action_groups): + # argparse stores mutually exclusive groups independently + # of all other groups. So, they must be manually re-combined + # with the groups/subgroups to which they were originally declared + # in order to have them appear in the correct location in the UI. + # + # Order is attempted to be preserved by inserting the MutexGroup + # into the _actions list at the first occurrence of any item + # where the two groups intersect + def swap_actions(actions): + for mutexgroup in mutex_groups: + mutex_actions = mutexgroup._group_actions + if contains_actions(mutex_actions, actions): + # make a best guess as to where we should store the group + targetindex = actions.index(mutexgroup._group_actions[0]) + # insert the _ArgumentGroup container + actions[targetindex] = mutexgroup + # remove the duplicated individual actions + return [action for action in actions + if action not in mutex_actions] + return actions + + return [group.update({'items': swap_actions(group['items'])}) or group + for group in action_groups] + + +def categorize2(groups, widget_dict, options): + return [{ + 'name': group['name'], + 'items': list(categorize(group['items'], widget_dict, options)), + 'groups': categorize2(group['groups'], widget_dict, options), + 'description': group['description'], + 'options': group['options'] + } for group in groups] + + +def categorize(actions, widget_dict, options): + _get_widget = partial(get_widget, widget_dict) + for action in actions: + + if is_mutex(action): + yield build_radio_group(action, widget_dict, options) + + elif is_standard(action): + yield action_to_json(action, _get_widget(action, 'TextField'), options) + + elif is_choice(action): + yield action_to_json(action, _get_widget(action, 'Dropdown'), options) + + elif is_flag(action): + yield action_to_json(action, _get_widget(action, 'CheckBox'), options) + + elif is_counter(action): + _json = action_to_json(action, _get_widget(action, 'Counter'), options) + # pre-fill the 'counter' dropdown + _json['data']['choices'] = list(map(str, range(1, 11))) + yield _json + else: + raise UnknownWidgetType(action) + + +def get_widget(widgets, action, default): + supplied_widget = widgets.get(action.dest, None) + type_arg_widget = 'FileChooser' if action.type == argparse.FileType else None + return supplied_widget or type_arg_widget or default + def is_required(action): - ''' - _actions possessing the `required` flag and not implicitly optional - through `nargs` being '*' or '?' - ''' - return not isinstance(action, _SubParsersAction) and (action.required == True and action.nargs not in ['*', '?']) + ''' + _actions possessing the `required` flag and not implicitly optional + through `nargs` being '*' or '?' + ''' + return not isinstance(action, _SubParsersAction) and ( + action.required == True and action.nargs not in ['*', '?']) + + +def is_mutex(action): + return isinstance(action, argparse._MutuallyExclusiveGroup) + def has_required(actions): - return list(filter(None, list(filter(is_required, actions)))) + return list(filter(None, list(filter(is_required, actions)))) + def is_subparser(action): - return isinstance(action,_SubParsersAction) + return isinstance(action, _SubParsersAction) + def has_subparsers(actions): return list(filter(is_subparser, actions)) + def get_subparser(actions): return list(filter(is_subparser, actions))[0] + def is_optional(action): - ''' - _actions either not possessing the `required` flag or implicitly optional through `nargs` being '*' or '?' - ''' - return (not action.required) or action.nargs in ['*', '?'] + ''' + _actions either not possessing the `required` flag or implicitly optional + through `nargs` being '*' or '?' + ''' + return (not action.required) or action.nargs in ['*', '?'] + def is_choice(action): - ''' action with choices supplied ''' - return action.choices + ''' action with choices supplied ''' + return action.choices + def is_standard(action): - """ actions which are general "store" instructions. - e.g. anything which has an argument style like: - $ script.py -f myfilename.txt - """ - boolean_actions = ( - _StoreConstAction, _StoreFalseAction, - _StoreTrueAction - ) - return (not action.choices - and not isinstance(action, _CountAction) - and not isinstance(action, _HelpAction) - and type(action) not in boolean_actions) + """ actions which are general "store" instructions. + e.g. anything which has an argument style like: + $ script.py -f myfilename.txt + """ + boolean_actions = ( + _StoreConstAction, _StoreFalseAction, + _StoreTrueAction + ) + return (not action.choices + and not isinstance(action, _CountAction) + and not isinstance(action, _HelpAction) + and type(action) not in boolean_actions) + def is_flag(action): - """ _actions which are either storeconst, store_bool, etc.. """ - action_types = [_StoreTrueAction, _StoreFalseAction, _StoreConstAction] - return any(list(map(lambda Action: isinstance(action, Action), action_types))) + """ _actions which are either storeconst, store_bool, etc.. """ + action_types = [_StoreTrueAction, _StoreFalseAction, _StoreConstAction] + return any(list(map(lambda Action: isinstance(action, Action), action_types))) + def is_counter(action): - """ _actions which are of type _CountAction """ - return isinstance(action, _CountAction) + """ _actions which are of type _CountAction """ + return isinstance(action, _CountAction) + def is_default_progname(name, subparser): - return subparser.prog == '{} {}'.format(os.path.split(sys.argv[0])[-1], name) + return subparser.prog == '{} {}'.format(os.path.split(sys.argv[0])[-1], name) + + +def is_help_message(action): + return isinstance(action, _HelpAction) + def choose_name(name, subparser): - return name if is_default_progname(name, subparser) else subparser.prog - -def build_radio_group(mutex_group): - if not mutex_group: - return [] - - options = [ - { - 'display_name': mutex_arg.metavar or mutex_arg.dest, - 'help': mutex_arg.help, - 'nargs': mutex_arg.nargs or '', - 'commands': mutex_arg.option_strings, - 'choices': mutex_arg.choices, - } for mutex_arg in mutex_group - ] + return name if is_default_progname(name, subparser) else subparser.prog + +def build_radio_group(mutex_group, widget_group, options): return { + 'id': str(uuid4()), 'type': 'RadioGroup', + 'cli_type': 'optional', 'group_name': 'Choose Option', - 'required': False, - 'data': options + 'required': mutex_group.required, + 'options': getattr(mutex_group, 'gooey_options', {}), + 'data': { + 'commands': [action.option_strings for action in mutex_group._group_actions], + 'widgets': list(categorize(mutex_group._group_actions, widget_group, options)) + } } -def as_json(action, widget, required): - if widget not in VALID_WIDGETS: - raise UnknownWidgetType('Widget Type {0} is unrecognized'.format(widget)) - - return { - 'type': widget, - 'required': required, - 'data': { - 'display_name': action.metavar or action.dest, - 'help': action.help, - 'nargs': action.nargs or '', - 'commands': action.option_strings, - 'choices': action.choices or [], - 'default': clean_default(widget, action.default) +def action_to_json(action, widget, options): + if action.required: + # check that it's present and not just spaces + validator = 'user_input and not user_input.isspace()' + error_msg = 'This field is required' + else: + # not required; do nothing; + validator = 'True' + error_msg = '' + + base = merge(item_default, { + 'validator': { + 'test': validator, + 'message': error_msg + }, + }) + + return { + 'id': action.option_strings[0] if action.option_strings else action.dest, + 'type': widget, + 'cli_type': choose_cli_type(action), + 'required': action.required, + 'data': { + 'display_name': action.metavar or action.dest, + 'help': action.help, + 'required': action.required, + 'nargs': action.nargs or '', + 'commands': action.option_strings, + 'choices': action.choices or [], + 'default': clean_default(action.default), + 'dest': action.dest, + }, + 'options': merge(base, options.get(action.dest) or {}) } - } -def clean_default(widget_type, default): - ''' - Attemps to safely coalesce the default value down to - a valid JSON type. - See: Issue #147. - function references supplied as arguments to the - `default` parameter in Argparse cause errors in Gooey. - ''' - if widget_type != 'CheckBox': - return default.__name__ if callable(default) else default - # checkboxes must be handled differently, as they - # must be forced down to a boolean value - return default if isinstance(default, bool) else False +def choose_cli_type(action): + return 'positional' \ + if action.required and not action.option_strings \ + else 'optional' +def clean_default(default): + ''' + Attemps to safely coalesce the default value down to + a valid JSON type. + See: Issue #147. + function references supplied as arguments to the + `default` parameter in Argparse cause errors in Gooey. + ''' + return default.__name__ if callable(default) else default diff --git a/gooey/python_bindings/config_generator.py b/gooey/python_bindings/config_generator.py index 8277020..9de01fe 100644 --- a/gooey/python_bindings/config_generator.py +++ b/gooey/python_bindings/config_generator.py @@ -1,12 +1,28 @@ import os import sys -from gooey.gui.windows import layouts +import warnings + from gooey.python_bindings import argparse_to_json from gooey.gui.util.quoting import quote +from gooey.python_bindings import constants + +default_layout = { + 'widgets': [{ + 'type': 'CommandField', + 'required': True, + 'data': { + 'display_name': 'Enter Commands', + 'help': 'Enter command line arguments', + 'nargs': '', + 'commands': '', + 'choices': [], + 'default': None, + } + }], +} def create_from_parser(parser, source_path, **kwargs): - auto_start = kwargs.get('auto_start', False) run_cmd = kwargs.get('target') if run_cmd is None: @@ -15,32 +31,76 @@ def create_from_parser(parser, source_path, **kwargs): else: run_cmd = '{} -u {}'.format(quote(sys.executable), quote(source_path)) - build_spec = { - 'language': kwargs.get('language', 'english'), - 'target': run_cmd, - 'program_name': kwargs.get('program_name') or os.path.basename(sys.argv[0]).replace('.py', ''), - 'program_description': kwargs.get('program_description', ''), - 'auto_start': kwargs.get('auto_start', False), - 'show_advanced': kwargs.get('advanced', True), - 'default_size': kwargs.get('default_size', (610, 530)), - 'num_required_cols': kwargs.get('required_cols', 1), - 'num_optional_cols': kwargs.get('optional_cols', 3), - 'manual_start': False, - 'layout_type': 'flat', - 'monospace_display': kwargs.get('monospace_display', False), - 'image_dir': kwargs.get('image_dir'), - 'language_dir': kwargs.get('language_dir'), - 'progress_regex': kwargs.get('progress_regex'), - 'progress_expr': kwargs.get('progress_expr'), - 'disable_progress_bar_animation': kwargs.get('disable_progress_bar_animation'), - 'disable_stop_button': kwargs.get('disable_stop_button'), - 'group_by_type': kwargs.get('group_by_type', True) - } - - if not auto_start: - build_spec['program_description'] = parser.description or build_spec['program_description'] - - layout_data = argparse_to_json.convert(parser) if build_spec['show_advanced'] else layouts.basic_config.items() - build_spec.update(layout_data) + build_spec = { + 'language': kwargs.get('language', 'english'), + 'target': run_cmd, + 'program_name': kwargs.get('program_name') or os.path.basename(sys.argv[0]).replace('.py', ''), + 'program_description': kwargs.get('program_description') or '', + 'sidebar_title': kwargs.get('sidebar_title', 'Actions'), + 'default_size': kwargs.get('default_size', (610, 530)), + 'auto_start': kwargs.get('auto_start', False), + 'show_advanced': kwargs.get('advanced', True), + 'run_validators': kwargs.get('run_validators', True), + 'encoding': kwargs.get('encoding', 'utf-8'), + 'show_stop_warning': kwargs.get('show_stop_warning', True), + 'show_success_modal': kwargs.get('show_success_modal', True), + 'force_stop_is_error': kwargs.get('force_stop_is_error', True), + 'poll_external_updates':kwargs.get('poll_external_updates', False), + + # Legacy/Backward compatibility interop + 'use_legacy_titles': kwargs.get('use_legacy_titles', True), + 'num_required_cols': kwargs.get('required_cols', 1), + 'num_optional_cols': kwargs.get('optional_cols', 3), + 'manual_start': False, + 'monospace_display': kwargs.get('monospace_display', False), + + 'image_dir': kwargs.get('image_dir'), + 'language_dir': kwargs.get('language_dir'), + 'progress_regex': kwargs.get('progress_regex'), + 'progress_expr': kwargs.get('progress_expr'), + 'disable_progress_bar_animation': kwargs.get('disable_progress_bar_animation'), + 'disable_stop_button': kwargs.get('disable_stop_button'), + + # Layouts + 'navigation': kwargs.get('navigation', constants.SIDEBAR), + 'show_sidebar': kwargs.get('show_sidebar', False), + 'tabbed_groups': kwargs.get('tabbed_groups', False), + 'group_by_type': kwargs.get('group_by_type', True), + + # styles + 'body_bg_color': kwargs.get('body_bg_color', '#f0f0f0'), + 'header_bg_color': kwargs.get('header_bg_color', '#ffffff'), + 'header_height': kwargs.get('header_height', 90), + 'header_show_title': kwargs.get('header_show_title', True), + 'header_show_subtitle': kwargs.get('header_show_subtitle', True), + 'header_image_center': kwargs.get('header_image_center', False), + 'footer_bg_color': kwargs.get('footer_bg_color', '#f0f0f0'), + 'sidebar_bg_color': kwargs.get('sidebar_bg_color', '#f2f2f2'), + # font family, weight, and size are determined at runtime + 'terminal_panel_color': kwargs.get('terminal_panel_color', '#F0F0F0'), + 'terminal_font_color': kwargs.get('terminal_font_color', '#000000'), + 'terminal_font_family': kwargs.get('terminal_font_family', None), + 'terminal_font_weight': kwargs.get('terminal_font_weight', None), + 'terminal_font_size': kwargs.get('terminal_font_size', None), + 'error_color': kwargs.get('error_color', '#ea7878') + } + + if build_spec['monospace_display']: + warnings.warn('Gooey Option `monospace_display` is a legacy option.\n' + 'See the terminal_font_x options for more flexible control ' + 'over Gooey\'s text formatting') + + + build_spec['program_description'] = parser.description or build_spec['program_description'] + + layout_data = (argparse_to_json.convert(parser, **build_spec) + if build_spec['show_advanced'] + else default_layout.items()) + + build_spec.update(layout_data) + + if len(build_spec['widgets']) > 1: + # there are subparsers involved + build_spec['show_sidebar'] = True return build_spec diff --git a/gooey/python_bindings/constants.py b/gooey/python_bindings/constants.py new file mode 100644 index 0000000..3641020 --- /dev/null +++ b/gooey/python_bindings/constants.py @@ -0,0 +1,5 @@ + +SIDEBAR = 'SIDEBAR' +TABBED = 'TABBED' +INLINE = 'INLINE' +HIDDEN = 'HIDDEN' diff --git a/gooey/python_bindings/gooey_decorator.py b/gooey/python_bindings/gooey_decorator.py index c6a04fe..275a252 100644 --- a/gooey/python_bindings/gooey_decorator.py +++ b/gooey/python_bindings/gooey_decorator.py @@ -12,11 +12,13 @@ import sys from argparse import ArgumentParser from gooey.gui import application -from gooey.gui.util.freeze import get_resource_path +from gooey.gui.util.freeze import getResourcePath +from gooey.util.functional import merge from . import config_generator IGNORE_COMMAND = '--ignore-gooey' +# TODO: kwargs all the things def Gooey(f=None, advanced=True, language='english', @@ -25,24 +27,29 @@ def Gooey(f=None, program_name=None, program_description=None, default_size=(610, 530), + use_legacy_titles=True, required_cols=2, optional_cols=2, dump_build_config=False, load_build_config=None, - monospace_display=False, # TODO: add this to the docs - image_dir='default', - language_dir=get_resource_path('languages'), - progress_regex=None, # TODO: add this to the docs - progress_expr=None, # TODO: add this to the docs + monospace_display=False, # TODO: add this to the docs + image_dir='::gooey/default', + language_dir=getResourcePath('languages'), + progress_regex=None, # TODO: add this to the docs + progress_expr=None, # TODO: add this to the docs disable_progress_bar_animation=False, disable_stop_button=False, - group_by_type=True): # TODO: add this to the docs + group_by_type=True, + header_height=80, + navigation='SIDEBAR', # TODO: add this to the docs + tabbed_groups=False, + **kwargs): ''' Decorator for client code's main function. Serializes argparse data to JSON for use with the Gooey front end ''' - params = locals() + params = merge(locals(), locals()['kwargs']) def build(payload): def run_gooey(self, args=None, namespace=None): @@ -57,7 +64,11 @@ def Gooey(f=None, sys.exit(1) if not build_spec: - build_spec = config_generator.create_from_parser(self, source_path, payload_name=payload.__name__, **params) + build_spec = config_generator.create_from_parser( + self, + source_path, + payload_name=payload.__name__, + **params) if dump_build_config: config_path = os.path.join(os.getcwd(), 'gooey_config.json') diff --git a/gooey/python_bindings/gooey_parser.py b/gooey/python_bindings/gooey_parser.py index 0106259..322aac8 100644 --- a/gooey/python_bindings/gooey_parser.py +++ b/gooey/python_bindings/gooey_parser.py @@ -8,39 +8,64 @@ class GooeySubParser(_SubParsersAction): def __init__(self, *args, **kwargs): super(GooeySubParser, self).__init__(*args, **kwargs) -# TODO: dedupe code + +# TODO: figure out how to correctly dispatch all of these +# so that the individual wrappers aren't needed class GooeyArgumentGroup(_ArgumentGroup): - def __init__(self, parser, widgets, *args, **kwargs): + def __init__(self, parser, widgets, options, *args, **kwargs): self.parser = parser self.widgets = widgets + self.options = options super(GooeyArgumentGroup, self).__init__(self.parser, *args, **kwargs) def add_argument(self, *args, **kwargs): widget = kwargs.pop('widget', None) metavar = kwargs.pop('metavar', None) + options = kwargs.pop('gooey_options', None) super(GooeyArgumentGroup, self).add_argument(*args, **kwargs) self.parser._actions[-1].metavar = metavar self.widgets[self.parser._actions[-1].dest] = widget + self.options[self.parser._actions[-1].dest] = options + + def add_argument_group(self, *args, **kwargs): + options = kwargs.pop('gooey_options', {}) + group = GooeyArgumentGroup(self.parser, self.widgets, self.options, *args, **kwargs) + group.gooey_options = options + self._action_groups.append(group) + return group + + def add_mutually_exclusive_group(self, *args, **kwargs): + options = kwargs.pop('gooey_options', {}) + container = self + group = GooeyMutuallyExclusiveGroup(container, self.parser, self.widgets, self.options, *args, **kwargs) + group.gooey_options = options + self.parser._mutually_exclusive_groups.append(group) + return group class GooeyMutuallyExclusiveGroup(_MutuallyExclusiveGroup): - def __init__(self, parser, widgets, *args, **kwargs): + def __init__(self, container, parser, widgets, options, *args, **kwargs): self.parser = parser self.widgets = widgets - super(GooeyMutuallyExclusiveGroup, self).__init__(self.parser, *args, **kwargs) + self.options = options + super(GooeyMutuallyExclusiveGroup, self).__init__(container, *args, **kwargs) def add_argument(self, *args, **kwargs): widget = kwargs.pop('widget', None) metavar = kwargs.pop('metavar', None) + options = kwargs.pop('gooey_options', None) super(GooeyMutuallyExclusiveGroup, self).add_argument(*args, **kwargs) self.parser._actions[-1].metavar = metavar self.widgets[self.parser._actions[-1].dest] = widget + self.options[self.parser._actions[-1].dest] = options + class GooeyParser(object): def __init__(self, **kwargs): self.__dict__['parser'] = ArgumentParser(**kwargs) self.widgets = {} + self.options = {} @property def _mutually_exclusive_groups(self): @@ -57,6 +82,7 @@ class GooeyParser(object): def add_argument(self, *args, **kwargs): widget = kwargs.pop('widget', None) metavar = kwargs.pop('metavar', None) + options = kwargs.pop('gooey_options', None) if widget and widget == 'Listbox': if not 'nargs' in kwargs or kwargs['nargs'] not in ['*', '+']: @@ -67,15 +93,20 @@ class GooeyParser(object): self.parser.add_argument(*args, **kwargs) self.parser._actions[-1].metavar = metavar self.widgets[self.parser._actions[-1].dest] = widget + self.options[self.parser._actions[-1].dest] = options - def add_mutually_exclusive_group(self, **kwargs): - group = GooeyMutuallyExclusiveGroup(self.parser, self.widgets, **kwargs) + def add_mutually_exclusive_group(self, *args, **kwargs): + options = kwargs.pop('gooey_options', {}) + group = GooeyMutuallyExclusiveGroup(self, self.parser, self.widgets, self.options, *args, **kwargs) + group.gooey_options = options self.parser._mutually_exclusive_groups.append(group) return group def add_argument_group(self, *args, **kwargs): - group = GooeyArgumentGroup(self.parser, self.widgets, **kwargs) - self.parser.add_argument_group(*args, **kwargs) + options = kwargs.pop('gooey_options', {}) + group = GooeyArgumentGroup(self.parser, self.widgets, self.options, *args, **kwargs) + group.gooey_options = options + self.parser._action_groups.append(group) return group def parse_args(self, args=None, namespace=None): diff --git a/gooey/python_bindings/parser/gooey_parser.py b/gooey/python_bindings/parser/gooey_parser.py new file mode 100644 index 0000000..e69de29