mirror of https://github.com/chriskiehl/Gooey.git
6 changed files with 451 additions and 219 deletions
Unified View
Diff Options
-
471gooey/python_bindings/argparse_to_json.py
-
118gooey/python_bindings/config_generator.py
-
5gooey/python_bindings/constants.py
-
29gooey/python_bindings/gooey_decorator.py
-
47gooey/python_bindings/gooey_parser.py
-
0gooey/python_bindings/parser/gooey_parser.py
@ -1,237 +1,362 @@ |
|||||
""" |
""" |
||||
Converts argparse parser actions into json "Build Specs" |
Converts argparse parser actions into json "Build Specs" |
||||
""" |
""" |
||||
|
import functools |
||||
|
import pprint |
||||
import argparse |
import argparse |
||||
import os |
import os |
||||
|
import sys |
||||
from argparse import ( |
from argparse import ( |
||||
_CountAction, |
_CountAction, |
||||
_HelpAction, |
_HelpAction, |
||||
_StoreConstAction, |
_StoreConstAction, |
||||
_StoreFalseAction, |
_StoreFalseAction, |
||||
_StoreTrueAction, |
_StoreTrueAction, |
||||
ArgumentParser, |
_SubParsersAction) |
||||
_SubParsersAction) |
|
||||
|
|
||||
from collections import OrderedDict |
from collections import OrderedDict |
||||
from functools import partial |
from functools import partial |
||||
from itertools import chain |
from uuid import uuid4 |
||||
|
|
||||
import sys |
from gooey.util.functional import merge, getin |
||||
|
|
||||
VALID_WIDGETS = ( |
VALID_WIDGETS = ( |
||||
'FileChooser', |
'FileChooser', |
||||
'MultiFileChooser', |
'MultiFileChooser', |
||||
'FileSaver', |
'FileSaver', |
||||
'DirChooser', |
'DirChooser', |
||||
'DateChooser', |
'DateChooser', |
||||
'TextField', |
'TextField', |
||||
'Dropdown', |
'Dropdown', |
||||
'Counter', |
'Counter', |
||||
'RadioGroup', |
'RadioGroup', |
||||
'CheckBox', |
'CheckBox', |
||||
'MultiDirChooser', |
'MultiDirChooser', |
||||
'Textarea', |
'Textarea', |
||||
'PasswordField', |
'PasswordField', |
||||
'Listbox' |
'Listbox' |
||||
) |
) |
||||
|
|
||||
|
|
||||
class UnknownWidgetType(Exception): |
class UnknownWidgetType(Exception): |
||||
pass |
pass |
||||
|
|
||||
|
|
||||
class UnsupportedConfiguration(Exception): |
class UnsupportedConfiguration(Exception): |
||||
pass |
pass |
||||
|
|
||||
|
|
||||
{ |
group_defaults = { |
||||
'siege': { |
'columns': 2, |
||||
'command': 'siege', |
'padding': 10, |
||||
'display_name': 'Siege', |
'show_border': False |
||||
'contents': [] |
|
||||
} |
|
||||
} |
} |
||||
|
|
||||
|
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 { |
def convert(parser, **kwargs): |
||||
'layout_type': layout_type, |
assert_subparser_constraints(parser) |
||||
'widgets': layout_data |
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): |
def apply_default_rewrites(spec): |
||||
supplied_widget = widgets.get(action.dest, None) |
top_level_subgroups = list(spec['widgets'].keys()) |
||||
type_arg_widget = 'FileChooser' if action.type == argparse.FileType else None |
for subgroup in top_level_subgroups: |
||||
return supplied_widget or type_arg_widget or None |
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): |
def is_required(action): |
||||
''' |
''' |
||||
_actions possessing the `required` flag and not implicitly optional |
_actions possessing the `required` flag and not implicitly optional |
||||
through `nargs` being '*' or '?' |
through `nargs` being '*' or '?' |
||||
''' |
''' |
||||
return not isinstance(action, _SubParsersAction) and (action.required == True and action.nargs not in ['*', '?']) |
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): |
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): |
def is_subparser(action): |
||||
return isinstance(action,_SubParsersAction) |
return isinstance(action, _SubParsersAction) |
||||
|
|
||||
|
|
||||
def has_subparsers(actions): |
def has_subparsers(actions): |
||||
return list(filter(is_subparser, actions)) |
return list(filter(is_subparser, actions)) |
||||
|
|
||||
|
|
||||
def get_subparser(actions): |
def get_subparser(actions): |
||||
return list(filter(is_subparser, actions))[0] |
return list(filter(is_subparser, actions))[0] |
||||
|
|
||||
|
|
||||
def is_optional(action): |
def is_optional(action): |
||||
''' |
''' |
||||
_actions either not possessing the `required` flag or implicitly optional through `nargs` being '*' or '?' |
_actions either not possessing the `required` flag or implicitly optional |
||||
''' |
through `nargs` being '*' or '?' |
||||
return (not action.required) or action.nargs in ['*', '?'] |
''' |
||||
|
return (not action.required) or action.nargs in ['*', '?'] |
||||
|
|
||||
|
|
||||
def is_choice(action): |
def is_choice(action): |
||||
''' action with choices supplied ''' |
''' action with choices supplied ''' |
||||
return action.choices |
return action.choices |
||||
|
|
||||
|
|
||||
def is_standard(action): |
def is_standard(action): |
||||
""" actions which are general "store" instructions. |
""" actions which are general "store" instructions. |
||||
e.g. anything which has an argument style like: |
e.g. anything which has an argument style like: |
||||
$ script.py -f myfilename.txt |
$ script.py -f myfilename.txt |
||||
""" |
""" |
||||
boolean_actions = ( |
boolean_actions = ( |
||||
_StoreConstAction, _StoreFalseAction, |
_StoreConstAction, _StoreFalseAction, |
||||
_StoreTrueAction |
_StoreTrueAction |
||||
) |
) |
||||
return (not action.choices |
return (not action.choices |
||||
and not isinstance(action, _CountAction) |
and not isinstance(action, _CountAction) |
||||
and not isinstance(action, _HelpAction) |
and not isinstance(action, _HelpAction) |
||||
and type(action) not in boolean_actions) |
and type(action) not in boolean_actions) |
||||
|
|
||||
|
|
||||
def is_flag(action): |
def is_flag(action): |
||||
""" _actions which are either storeconst, store_bool, etc.. """ |
""" _actions which are either storeconst, store_bool, etc.. """ |
||||
action_types = [_StoreTrueAction, _StoreFalseAction, _StoreConstAction] |
action_types = [_StoreTrueAction, _StoreFalseAction, _StoreConstAction] |
||||
return any(list(map(lambda Action: isinstance(action, Action), action_types))) |
return any(list(map(lambda Action: isinstance(action, Action), action_types))) |
||||
|
|
||||
|
|
||||
def is_counter(action): |
def is_counter(action): |
||||
""" _actions which are of type _CountAction """ |
""" _actions which are of type _CountAction """ |
||||
return isinstance(action, _CountAction) |
return isinstance(action, _CountAction) |
||||
|
|
||||
|
|
||||
def is_default_progname(name, subparser): |
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): |
def choose_name(name, subparser): |
||||
return name if is_default_progname(name, subparser) else subparser.prog |
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 |
|
||||
] |
|
||||
|
|
||||
|
|
||||
|
def build_radio_group(mutex_group, widget_group, options): |
||||
return { |
return { |
||||
|
'id': str(uuid4()), |
||||
'type': 'RadioGroup', |
'type': 'RadioGroup', |
||||
|
'cli_type': 'optional', |
||||
'group_name': 'Choose Option', |
'group_name': 'Choose Option', |
||||
'required': False, |
'required': mutex_group.required, |
||||
'data': options |
'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): |
def action_to_json(action, widget, options): |
||||
if widget not in VALID_WIDGETS: |
if action.required: |
||||
raise UnknownWidgetType('Widget Type {0} is unrecognized'.format(widget)) |
# check that it's present and not just spaces |
||||
|
validator = 'user_input and not user_input.isspace()' |
||||
return { |
error_msg = 'This field is required' |
||||
'type': widget, |
else: |
||||
'required': required, |
# not required; do nothing; |
||||
'data': { |
validator = 'True' |
||||
'display_name': action.metavar or action.dest, |
error_msg = '' |
||||
'help': action.help, |
base = merge(item_default, { |
||||
'nargs': action.nargs or '', |
'validator': { |
||||
'commands': action.option_strings, |
'test': validator, |
||||
'choices': action.choices or [], |
'message': error_msg |
||||
'default': clean_default(widget, action.default) |
}, |
||||
|
}) |
||||
|
|
||||
|
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. |
def choose_cli_type(action): |
||||
function references supplied as arguments to the |
return 'positional' \ |
||||
`default` parameter in Argparse cause errors in Gooey. |
if action.required and not action.option_strings \ |
||||
''' |
else 'optional' |
||||
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 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 |
@ -0,0 +1,5 @@ |
|||||
|
|
||||
|
SIDEBAR = 'SIDEBAR' |
||||
|
TABBED = 'TABBED' |
||||
|
INLINE = 'INLINE' |
||||
|
HIDDEN = 'HIDDEN' |
xxxxxxxxxx