From 117942d6b9d5af95e82969105307033e25781d28 Mon Sep 17 00:00:00 2001 From: chriskiehl Date: Sun, 13 Aug 2017 19:56:24 -0700 Subject: [PATCH] Pluggable widget grouping --- gooey/gui/components/widgets/base.py | 197 +++++++++ gooey/gui/containers/application.py | 10 + gooey/gui/processor.py | 150 ++++--- gooey/gui/util/formatters.py | 75 ++++ gooey/gui/util/functional.py | 4 + gooey/python_bindings/argparse_to_json.py | 513 ++++++++++++++-------- gooey/python_bindings/config_generator.py | 4 +- gooey/python_bindings/gooey_decorator.py | 16 +- gooey/python_bindings/gooey_parser.py | 188 ++++---- gooey/python_bindings/groupings.py | 30 ++ gooey/util/__init__.py | 58 +++ 11 files changed, 892 insertions(+), 353 deletions(-) create mode 100644 gooey/gui/components/widgets/base.py create mode 100644 gooey/gui/containers/application.py create mode 100644 gooey/gui/util/formatters.py create mode 100644 gooey/python_bindings/groupings.py create mode 100644 gooey/util/__init__.py diff --git a/gooey/gui/components/widgets/base.py b/gooey/gui/components/widgets/base.py new file mode 100644 index 0000000..7e488ee --- /dev/null +++ b/gooey/gui/components/widgets/base.py @@ -0,0 +1,197 @@ +import wx + +from gooey.gui.util import wx_util + + + + +class BaseWidget(wx.Panel): + + def __init__(self, parent, *args, **kwargs ): + super(BaseWidget, self).__init__(*args, **kwargs) + + + def arrange(self, label, text): + raise NotImplementedError + + def getWidget(self, ): + return self.widget_class(self) + + def connectSignal(self): + raise NotImplementedError + + def getSublayout(self, *args, **kwargs): + raise NotImplementedError + + def setValue(self, value): + raise NotImplementedError + + def receiveChange(self, *args, **kwargs): + raise NotImplementedError + + def dispatchChange(self, value, **kwargs): + raise NotImplementedError + + def formatOutput(self, metatdata, value): + raise NotImplementedError + + + + +class TextContainer(BaseWidget): + ''' + + type: "N" + validator: "x > 10 and x < 11" + + ''' + widget_class = None + + def __init__(self, parent, widgetInfo, *args, **kwargs): + super(TextContainer, self).__init__(parent, *args, **kwargs) + + self._id = widgetInfo['id'] + self._meta = widgetInfo['data'] + self.label = wx.StaticText('{}'.format(widgetInfo['data']['display_name'])) + self.help_text = wx.StaticText(widgetInfo['data']['help']) + self.error_text = wx.StaticText(widgetInfo['data']['help']) + self.widget = self.getWidget() + self.layout = self.arrange(self.label, self.help_text) + self.value = Subject() + self.connectSignal() + + def arrange(self, label, text): + layout = QVBoxLayout() + layout.addWidget(label, alignment=Qt.AlignTop) + if text: + layout.addWidget(text) + else: + layout.addStretch(1) + layout.addLayout(self.getSublayout()) + return layout + + def getWidget(self,): + return self.widget_class(self) + + def connectSignal(self): + self.widget.textChanged.connect(self.dispatchChange) + + def getSublayout(self, *args, **kwargs): + raise NotImplementedError + + def setValue(self, value): + raise NotImplementedError + + def receiveChange(self, metatdata, value): + raise NotImplementedError + + def dispatchChange(self, value, **kwargs): + raise NotImplementedError + + def formatOutput(self, metadata, value): + raise NotImplementedError + + + + +class BaseGuiComponent(object): + widget_class = None + + def __init__(self, parent, title, msg, choices=None): + ''' + :param data: field info (title, help, etc..) + :param widget_pack: internal wxWidgets to render + ''' + # parent + self.parent = parent + + # Widgets + self.title = None + self.help_msg = None + self.choices = choices + + # Internal WidgetPack set in subclasses + + self.do_layout(parent, title, msg) + + def do_layout(self, parent, title, msg): + self.panel = wx.Panel(parent) + + self.widget_pack = self.widget_class() + + self.title = self.format_title(self.panel, title) + self.help_msg = self.format_help_msg(self.panel, msg) + self.help_msg.SetMinSize((0, -1)) + core_widget_set = self.widget_pack.build(self.panel, {}, self.choices) + + vertical_container = wx.BoxSizer(wx.VERTICAL) + + vertical_container.Add(self.title) + vertical_container.AddSpacer(2) + + if self.help_msg.GetLabelText(): + vertical_container.Add(self.help_msg, 1, wx.EXPAND) + vertical_container.AddSpacer(2) + else: + vertical_container.AddStretchSpacer(1) + + vertical_container.Add(core_widget_set, 0, wx.EXPAND) + self.panel.SetSizer(vertical_container) + + return self.panel + + def bind(self, *args, **kwargs): + print(self.widget_pack.widget.Bind(*args, **kwargs)) + + def get_title(self): + return self.title.GetLabel() + + def set_title(self, text): + self.title.SetLabel(text) + + def get_help_msg(self): + return self.help_msg.GetLabelText() + + def set_label_text(self, text): + self.help_msg.SetLabel(text) + + def format_help_msg(self, parent, msg): + base_text = wx.StaticText(parent, label=msg or '') + wx_util.dark_grey(base_text) + return base_text + + def format_title(self, parent, title): + text = wx.StaticText(parent, label=title) + wx_util.make_bold(text) + return text + + def onResize(self, evt): + # handle internal widgets + # self.panel.Freeze() + self._onResize(evt) + # propagate event to child widgets + self.widget_pack.onResize(evt) + evt.Skip() + # self.panel.Thaw() + + def _onResize(self, evt): + if not self.help_msg: + return + self.panel.Size = evt.GetSize() + container_width, _ = self.panel.Size + text_width, _ = self.help_msg.Size + + if text_width != container_width: + self.help_msg.SetLabel(self.help_msg.GetLabelText().replace('\n', ' ')) + self.help_msg.Wrap(container_width) + evt.Skip() + + def get_value(self): + return self.widget_pack.get_value() + + def set_value(self, val): + if val: + self.widget_pack.widget.SetValue(str(val)) + + def __repr__(self): + return self.__class__.__name__ diff --git a/gooey/gui/containers/application.py b/gooey/gui/containers/application.py new file mode 100644 index 0000000..6e0dc0a --- /dev/null +++ b/gooey/gui/containers/application.py @@ -0,0 +1,10 @@ + +import wx + + + +class MainWindow(wx.Frame): + + def __init__(self, *args, **kwargs): + super(MainWindow, self).__init__(*args, **kwargs) + diff --git a/gooey/gui/processor.py b/gooey/gui/processor.py index d2bcdb6..1487fe3 100644 --- a/gooey/gui/processor.py +++ b/gooey/gui/processor.py @@ -1,12 +1,10 @@ import os import re import subprocess - +import sys from functools import partial from multiprocessing.dummy import Pool -import sys - from gooey.gui.pubsub import pub from gooey.gui.util.casting import safe_float from gooey.gui.util.functional import unit, bind @@ -14,85 +12,85 @@ from gooey.gui.util.taskkill import taskkill class ProcessController(object): - def __init__(self, progress_regex, progress_expr): - self._process = None - self.progress_regex = progress_regex - self.progress_expr = progress_expr - - def was_success(self): - self._process.communicate() - return self._process.returncode == 0 + def __init__(self, progress_regex, progress_expr): + self._process = None + self.progress_regex = progress_regex + self.progress_expr = progress_expr - def poll(self): - if not self._process: - raise Exception('Not started!') - self._process.poll() + def was_success(self): + self._process.communicate() + return self._process.returncode == 0 - def stop(self): - if self.running(): - taskkill(self._process.pid) + def poll(self): + if not self._process: + raise Exception('Not started!') + self._process.poll() - def running(self): - return self._process and self.poll() is None + def stop(self): + if self.running(): + taskkill(self._process.pid) - def run(self, command): - env = os.environ.copy() - env["GOOEY"] = "1" - try: - self._process = subprocess.Popen( - command.encode(sys.getfilesystemencoding()), - bufsize=1, stdout=subprocess.PIPE, stdin=subprocess.PIPE, - stderr=subprocess.STDOUT, shell=True, env=env) - except: - self._process = subprocess.Popen( - command, - bufsize=1, stdout=subprocess.PIPE, stdin=subprocess.PIPE, - stderr=subprocess.STDOUT, shell=True, env=env) - Pool(1).apply_async(self._forward_stdout, (self._process,)) + def running(self): + return self._process and self.poll() is None - def _forward_stdout(self, process): - ''' - Reads the stdout of `process` and forwards lines and progress - to any interested subscribers - ''' - while True: - line = process.stdout.readline() - if not line: - break - pub.send_message('console_update', msg=line) - pub.send_message('progress_update', progress=self._extract_progress(line)) - pub.send_message('execution_complete') + def run(self, command): + env = os.environ.copy() + env["GOOEY"] = "1" + try: + self._process = subprocess.Popen( + command.encode(sys.getfilesystemencoding()), + bufsize=1, stdout=subprocess.PIPE, stdin=subprocess.PIPE, + stderr=subprocess.STDOUT, shell=True, env=env) + except: + self._process = subprocess.Popen( + command, + bufsize=1, stdout=subprocess.PIPE, stdin=subprocess.PIPE, + stderr=subprocess.STDOUT, shell=True, env=env) + Pool(1).apply_async(self._forward_stdout, (self._process,)) - def _extract_progress(self, text): - ''' - Finds progress information in the text using the - user-supplied regex and calculation instructions - ''' - # monad-ish dispatch to avoid the if/else soup - find = partial(re.search, string=text.strip()) - regex = unit(self.progress_regex) - match = bind(regex, find) - result = bind(match, self._calculate_progress) - return result + def _forward_stdout(self, process): + ''' + Reads the stdout of `process` and forwards lines and progress + to any interested subscribers + ''' + while True: + line = process.stdout.readline() + if not line: + break + pub.send_message('console_update', msg=line) + pub.send_message('progress_update', + progress=self._extract_progress(line)) + pub.send_message('execution_complete') - def _calculate_progress(self, match): - ''' - Calculates the final progress value found by the regex - ''' - if not self.progress_expr: - return safe_float(match.group(1)) - else: - return self._eval_progress(match) + def _extract_progress(self, text): + ''' + Finds progress information in the text using the + user-supplied regex and calculation instructions + ''' + # monad-ish dispatch to avoid the if/else soup + find = partial(re.search, string=text.strip()) + regex = unit(self.progress_regex) + match = bind(regex, find) + result = bind(match, self._calculate_progress) + return result - def _eval_progress(self, match): - ''' - Runs the user-supplied progress calculation rule - ''' - _locals = {k: safe_float(v) for k, v in match.groupdict().items()} - if "x" not in _locals: - _locals["x"] = [safe_float(x) for x in match.groups()] - try: - return int(eval(self.progress_expr, {}, _locals)) - except: - return None + def _calculate_progress(self, match): + ''' + Calculates the final progress value found by the regex + ''' + if not self.progress_expr: + return safe_float(match.group(1)) + else: + return self._eval_progress(match) + def _eval_progress(self, match): + ''' + Runs the user-supplied progress calculation rule + ''' + _locals = {k: safe_float(v) for k, v in match.groupdict().items()} + if "x" not in _locals: + _locals["x"] = [safe_float(x) for x in match.groups()] + try: + return int(eval(self.progress_expr, {}, _locals)) + except: + return None diff --git a/gooey/gui/util/formatters.py b/gooey/gui/util/formatters.py new file mode 100644 index 0000000..2497a3c --- /dev/null +++ b/gooey/gui/util/formatters.py @@ -0,0 +1,75 @@ +import os + +from gooey.gui.util.quoting import quote + + +def checkbox(metadata, value): + return metadata['commands'][0] if value else None + + +def radioGroup(metadata, value): + # TODO + try: + return self.commands[self._value.index(True)][0] + except ValueError: + return None + + +def multiFileChooser(metadata, value): + paths = ' '.join(quote(x) for x in value.split(os.pathsep) if x) + if metadata['commands'] and paths: + return u'{} {}'.format(metadata['commands'][0], paths) + return paths or None + + +def textArea(metadata, value): + if metadata['commands'] and value: + return '{} {}'.format(metadata['commands'][0], quote(value.encode('unicode_escape'))) + else: + return quote(value.encode('unicode_escape')) if value else '' + + +def commandField(metadata, value): + if metadata['commands'] and value: + return u'{} {}'.format(metadata['commands'][0], value) + else: + return value or None + + +def counter(metatdata, value): + ''' + Returns + str(option_string * DropDown Value) + e.g. + -vvvvv + ''' + if not str(value).isdigit(): + return None + arg = str(metatdata['commands'][0]).replace('-', '') + repeated_args = arg * int(value) + return '-' + repeated_args + + +def dropdown(metadata, value): + if value == 'Select Option': + return None + elif metadata['commands'] and value: + return u'{} {}'.format(metadata['commands'][0], quote(value)) + else: + return quote(value) if value else '' + + +def general(metadata, value): + if metadata['commands'] and value: + if not metadata['nargs']: + v = quote(value) + else: + v = value + return u'{0} {1}'.format(metadata['commands'][0], v) + else: + if not value: + return None + elif not metadata['nargs']: + return quote(value) + else: + return value diff --git a/gooey/gui/util/functional.py b/gooey/gui/util/functional.py index 8d0c8d4..9f1c9d7 100644 --- a/gooey/gui/util/functional.py +++ b/gooey/gui/util/functional.py @@ -3,8 +3,12 @@ Simple monad-ish bindings ''' + def unit(val): return val + def bind(val, f): return f(val) if val else None + + diff --git a/gooey/python_bindings/argparse_to_json.py b/gooey/python_bindings/argparse_to_json.py index 878909d..131b89d 100644 --- a/gooey/python_bindings/argparse_to_json.py +++ b/gooey/python_bindings/argparse_to_json.py @@ -4,233 +4,380 @@ Converts argparse parser actions into json "Build Specs" import argparse import os +import sys +from _sha256 import sha256 from argparse import ( - _CountAction, - _HelpAction, - _StoreConstAction, - _StoreFalseAction, - _StoreTrueAction, - ArgumentParser, - _SubParsersAction) - -from collections import OrderedDict -from functools import partial -from itertools import chain + _CountAction, + _HelpAction, + _StoreConstAction, + _StoreFalseAction, + _StoreTrueAction, + _SubParsersAction) +from collections import defaultdict +from functools import reduce +from operator import itemgetter + +from gooey.util import bootlegCurry, apply_transforms, merge, partition_by +from python_bindings.groupings import requiredAndOptional +from util import excluding, indentity + + +__ALL__ = ( + 'convert', + 'UnknownWidgetType', + 'UnsupportedConfiguration' +) -import sys VALID_WIDGETS = ( - 'FileChooser', - 'MultiFileChooser', - 'FileSaver', - 'DirChooser', - 'DateChooser', - 'TextField', - 'Dropdown', - 'Counter', - 'RadioGroup', - 'CheckBox', - 'MultiDirChooser', - 'Textarea', - 'PasswordField' + 'FileChooser', + 'MultiFileChooser', + 'FileSaver', + 'DirChooser', + 'DateChooser', + 'TextField', + 'Dropdown', + 'Counter', + 'RadioGroup', + 'CheckBox', + 'MultiDirChooser', + 'Textarea', + 'PasswordField' ) class UnknownWidgetType(Exception): - pass + pass + class UnsupportedConfiguration(Exception): - pass + pass -{ - 'siege': { - 'command': 'siege', - 'display_name': 'Siege', - 'contents': [] - } -} 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), { + ''' + Convert an ArgParse instance into a JSON representation for Gooey + ''' + metadata = getattr(parser, 'metadata', {}) + toplevel_groups = get_toplevel_groups(parser) + + transforms = ( + flatten_actions, + apply_identifiers, + apply_gooey_metadata(metadata), + clean_default_values, + clean_types, + make_json_friendly, + group_mutex_groups, + # requiredAndOptional + ) + + final_groups = [] + for group in toplevel_groups: + final_groups.append(merge( + excluding(group, 'parser'), + {'items': reduce(apply_transforms, transforms, group['parser'])} + )) + + return { + 'layout': 'column' if len(final_groups) > 1 else 'standard', + 'widgets': final_groups + } + + + + +def fingerprint(obj): + ''' + Generate a deterministic identifier for a given dict or object + ''' + if isinstance(obj, dict): + data = obj.items() + else: + data = obj.__dict__.items() + hash = sha256(''.join(map(str, sorted(data))).encode('utf-8')) + return hash.hexdigest()[:8] + + + +def flatten_actions(parser): + ''' + Turn all of the parser actions into a flattened list of dicts + tagged with group and mutex info + ''' + mutex_groups = {} + for index, group in enumerate(parser._mutually_exclusive_groups): + for action in group._group_actions: + mutex_groups[fingerprint(action)] = group.title or index + + actions = [] + for index, group in enumerate(parser._action_groups): + for order, action in enumerate(group._group_actions): + hash = fingerprint(action) + + record = merge(action.__dict__, { + 'group_name': group.title or index, + 'mutex_group': mutex_groups.get(hash, None), + 'argparse_type': type(action), + 'order': order + }) + actions.append(record) + + return actions + + +def apply_identifiers(actions): + ''' Add a unique identifier to each action ''' + return [merge(action, {'id': fingerprint(action)}) for action in actions] + + +@bootlegCurry +def apply_gooey_metadata(metadata, actions): + def add_metadata(metadata, action): + ''' + Extends the action dict with widget, validatation, + and any additional metadata required for the GUI + ''' + widgets = metadata.get('widgets', {}) + validators = metadata.get('validators', {}) + + defaults = ( + (is_standard, 'TextField', indentity), + (is_choice, 'Dropdown', indentity), + (is_flag, 'CheckBox', indentity), + (is_counter, 'Counter', build_choice_array) + ) + + for predicate, default, finalizer in defaults: + if predicate(action): + widget = {'widget': get_widget(action, widgets) or default} + validator = {'validator': get_validator(action, validators) or 'true'} + return finalizer(merge(action, widget, validator)) + + # if we fell out of the loop, a bad type was supplied by the user + raise UnknownWidgetType(action) + + return [add_metadata(metadata, action) for action in actions] + + +def group_mutex_groups(actions): + ''' + Wrap any mutexes up into their own sub-groups while taking + special care to keep the ordering of the actions + ''' + groups = defaultdict(list) + + for action in actions: + groups[action['mutex_group']].append(action) + + output = [] + for mutex_name, stuff in groups.items(): + if mutex_name is not None: + output.append({ + 'name': mutex_name, + 'type': 'MutualExclusiveGroup', + 'items': stuff, + 'order': stuff[0]['order'] + }) + else: + output.extend(stuff) + + return sorted(output, key=lambda x: x['order']) + + + + + + + + + + + + + + +def extract_subparser_details(parser): + group_actions = parser._subparsers._group_actions[0] + choice_actions = group_actions.choices.items() + + return [{ + 'name': choose_name(name, item.parser), 'command': name, - 'contents': process(sub_parser, getattr(sub_parser, 'widgets', {})) - }) for name, sub_parser in get_subparser(actions).choices.items()) + 'parser': item.parser + } for name, item in choice_actions] + + +def clean_default_values(actions): + return [merge(action, { + 'default': clean_default(action['argparse_type'], action['default']) + }) for action in actions] - else: - layout_type = 'standard' - layout_data = OrderedDict([ - ('primary', { + +def clean_types(actions): + ''' clean any user supplied type objects so they don't cause json explosions''' + return [merge(action, {'type': action['type'].__name__ if callable(action['type']) else ''}) + for action in actions] + + +def wrap_parser(parser): + ''' + Wrap a non-subparser ArgumentParser in + a list of dicts to match the shape of subprocessor items + ''' + return [{ + 'name': 'primary', 'command': None, - 'contents': process(parser, widget_dict) - }) - ]) - - return { - 'layout_type': layout_type, - 'widgets': layout_data - } - - -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 + 'parser': parser + }] + + +def get_toplevel_groups(parser): + ''' + Get the top-level ArgumentParser groups/subparsers + ''' + if parser._subparsers: + return extract_subparser_details(parser) else: - raise UnknownWidgetType(action) + return wrap_parser(parser) + + +def make_json_friendly(actions): + ''' + Remove any non-primitive argparse values from the dict + that would cause serialization problems + ''' + return [excluding(item, 'argparse_type', 'container') for item in actions] + + + + + +def get_validator(action, validators): + # TODO + pass + + + +def validate_subparser_constraints(parser): + if parser._subparsers and has_required(parser._actions): + raise UnsupportedConfiguration( + "Gooey doesn't currently support required arguments when subparsers are present.") + 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 + 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 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 ['*', '?']) 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) -def has_subparsers(actions): - return list(filter(is_subparser, actions)) +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 ['*', '?']) -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['argparse_type'], _CountAction) + and not isinstance(action['argparse_type'], _HelpAction) + and ['argparse_type'] 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) 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 is_default_progname(name, subparser): + return subparser.prog == '{} {}'.format(os.path.split(sys.argv[0])[-1], name) + + +def build_choice_array(action): + ''' Generate a 1-10 choices array ''' + return merge(action, {'choices': list(map(str, range(1, 11)))}) + 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 { - 'type': 'RadioGroup', - 'group_name': 'Choose Option', - 'required': False, - 'data': 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) + 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 { + 'type': 'RadioGroup', + 'group_name': 'Choose Option', + 'required': False, + 'data': options } - } + + 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 + ''' + 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 + diff --git a/gooey/python_bindings/config_generator.py b/gooey/python_bindings/config_generator.py index 8277020..808a753 100644 --- a/gooey/python_bindings/config_generator.py +++ b/gooey/python_bindings/config_generator.py @@ -5,6 +5,7 @@ from gooey.python_bindings import argparse_to_json from gooey.gui.util.quoting import quote + def create_from_parser(parser, source_path, **kwargs): auto_start = kwargs.get('auto_start', False) @@ -34,7 +35,8 @@ def create_from_parser(parser, source_path, **kwargs): '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) + 'group_by_type': kwargs.get('group_by_type', True), + 'validate_inputs': kwargs.get('validate_inputs', False) } if not auto_start: diff --git a/gooey/python_bindings/gooey_decorator.py b/gooey/python_bindings/gooey_decorator.py index c6a04fe..d7e1bc0 100644 --- a/gooey/python_bindings/gooey_decorator.py +++ b/gooey/python_bindings/gooey_decorator.py @@ -14,29 +14,31 @@ from argparse import ArgumentParser from gooey.gui import application from gooey.gui.util.freeze import get_resource_path from . import config_generator +from gooey.python_bindings.groupings import requiredAndOptional IGNORE_COMMAND = '--ignore-gooey' def Gooey(f=None, - advanced=True, - language='english', - auto_start=False, # TODO: add this to the docs. Used to be `show_config=True` - target=None, program_name=None, program_description=None, default_size=(610, 530), 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'), + auto_start=False, # TODO: add this to the docs. Used to be `show_config=True` + advanced=True, + target=None, + language='english', + dump_build_config=False, + load_build_config=None, 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 + validate_inputs=True, + group_by=requiredAndOptional): # TODO: add this to the docs ''' Decorator for client code's main function. Serializes argparse data to JSON for use with the Gooey front end diff --git a/gooey/python_bindings/gooey_parser.py b/gooey/python_bindings/gooey_parser.py index aee804e..4657dbc 100644 --- a/gooey/python_bindings/gooey_parser.py +++ b/gooey/python_bindings/gooey_parser.py @@ -1,96 +1,112 @@ from argparse import ArgumentParser, _SubParsersAction, _MutuallyExclusiveGroup + from gooey.gui.lang.i18n import _ class GooeySubParser(_SubParsersAction): - def __init__(self, *args, **kwargs): - super(GooeySubParser, self).__init__(*args, **kwargs) + def __init__(self, *args, **kwargs): + super(GooeySubParser, self).__init__(*args, **kwargs) -class GooeyMutuallyExclusiveGroup(_MutuallyExclusiveGroup): - def __init__(self, parser, widgets, *args, **kwargs): - self.parser = parser - self.widgets = widgets - super(GooeyMutuallyExclusiveGroup, self).__init__(self.parser, *args, **kwargs) - def add_argument(self, *args, **kwargs): - widget = kwargs.pop('widget', None) - metavar = kwargs.pop('metavar', None) - super(GooeyMutuallyExclusiveGroup, self).add_argument(*args, **kwargs) - self.parser._actions[-1].metavar = metavar - self.widgets[self.parser._actions[-1].dest] = widget +class GooeyMutuallyExclusiveGroup(_MutuallyExclusiveGroup): + def __init__(self, parser, metadata, *args, **kwargs): + self.parser = parser + self.metadata = metadata + title = kwargs.pop('title', None) # todo inc group num? + super(GooeyMutuallyExclusiveGroup, self).__init__(self.parser, *args, **kwargs) + self.title = title + + def add_argument(self, *args, **kwargs): + widget = kwargs.pop('widget', None) + validator = kwargs.pop('validate', None) + metavar = kwargs.pop('metavar', None) + super(GooeyMutuallyExclusiveGroup, self).add_argument(*args, **kwargs) + self.parser._actions[-1].metavar = metavar + self.metadata['widgets'][self.parser._actions[-1].dest] = widget + self.metadata['validators'][self.parser._actions[-1].dest] = validator class GooeyParser(object): - def __init__(self, **kwargs): - self.__dict__['parser'] = ArgumentParser(**kwargs) - self.widgets = {} - - @property - def _mutually_exclusive_groups(self): - return self.parser._mutually_exclusive_groups - - @property - def _actions(self): - return self.parser._actions - - @property - def description(self): - return self.parser.description - - def add_argument(self, *args, **kwargs): - widget = kwargs.pop('widget', None) - metavar = kwargs.pop('metavar', None) - self.parser.add_argument(*args, **kwargs) - self.parser._actions[-1].metavar = metavar - self.widgets[self.parser._actions[-1].dest] = widget - - # def add_mutually_exclusive_group(self, **kwargs): - # return self.parser.add_mutually_exclusive_group(**kwargs) - - def add_mutually_exclusive_group(self, **kwargs): - group = GooeyMutuallyExclusiveGroup(self.parser, self.widgets, **kwargs) - self.parser._mutually_exclusive_groups.append(group) - return group - - def add_argument_group(self, *args, **kwargs): - return self.parser.add_argument_group(*args, **kwargs) - - def parse_args(self, args=None, namespace=None): - return self.parser.parse_args(args, namespace) - - def add_subparsers(self, **kwargs): - if self._subparsers is not None: - self.error(_('cannot have multiple subparser arguments')) - - # add the parser class to the arguments if it's not present - kwargs.setdefault('parser_class', type(self)) - - if 'title' in kwargs or 'description' in kwargs: - title = _(kwargs.pop('title', 'subcommands')) - description = _(kwargs.pop('description', None)) - self._subparsers = self.add_argument_group(title, description) - else: - self._subparsers = self._positionals - - # prog defaults to the usage message of this parser, skipping - # optional arguments and with no "usage:" prefix - if kwargs.get('prog') is None: - formatter = self._get_formatter() - positionals = self._get_positional_actions() - groups = self._mutually_exclusive_groups - formatter.add_usage(self.usage, positionals, groups, '') - kwargs['prog'] = formatter.format_help().strip() - - # create the parsers action and add it to the positionals list - parsers_class = self._pop_action_class(kwargs, 'parsers') - action = parsers_class(option_strings=[], **kwargs) - self._subparsers._add_action(action) - - # return the created parsers action - return action - - def __getattr__(self, item): - return getattr(self.parser, item) - - def __setattr__(self, key, value): - return setattr(self.parser, key, value) + def __init__(self, **kwargs): + self.__dict__['parser'] = ArgumentParser(**kwargs) + # self.widgets = {} + # self.validators = {} + self.metadata = { + 'widgets': {}, + 'validators': {} + } + + @property + def _mutually_exclusive_groups(self): + return self.parser._mutually_exclusive_groups + + @property + def _actions(self): + return self.parser._actions + + @property + def description(self): + return self.parser.description + + def add_argument(self, *args, **kwargs): + widget = kwargs.pop('widget', None) + validator = kwargs.pop('validate', None) + metavar = kwargs.pop('metavar', None) + self.parser.add_argument(*args, **kwargs) + self.parser._actions[-1].metavar = metavar + self.metadata['widgets'][self.parser._actions[-1].dest] = widget + self.metadata['validators'][self.parser._actions[-1].dest] = validator + + + def add_mutually_exclusive_group(self, **kwargs): + group = GooeyMutuallyExclusiveGroup(self.parser, self.metadata, **kwargs) + self.parser._mutually_exclusive_groups.append(group) + return group + + + def add_argument_group(self, *args, **kwargs): + return self.parser.add_argument_group(*args, **kwargs) + + + def parse_args(self, args=None, namespace=None): + return self.parser.parse_args(args, namespace) + + + def add_subparsers(self, **kwargs): + if self._subparsers is not None: + self.error(_('cannot have multiple subparser arguments')) + + # add the parser class to the arguments if it's not present + kwargs.setdefault('parser_class', type(self)) + + if 'title' in kwargs or 'description' in kwargs: + title = _(kwargs.pop('title', 'subcommands')) + description = _(kwargs.pop('description', None)) + self._subparsers = self.add_argument_group(title, description) + else: + self._subparsers = self._positionals + + # prog defaults to the usage message of this parser, skipping + # optional arguments and with no "usage:" prefix + if kwargs.get('prog') is None: + formatter = self._get_formatter() + positionals = self._get_positional_actions() + groups = self._mutually_exclusive_groups + formatter.add_usage(self.usage, positionals, groups, '') + kwargs['prog'] = formatter.format_help().strip() + + # create the parsers action and add it to the positionals list + parsers_class = self._pop_action_class(kwargs, 'parsers') + action = parsers_class(option_strings=[], **kwargs) + self._subparsers._add_action(action) + + # return the created parsers action + return action + + + def __getattr__(self, item): + return getattr(self.parser, item) + + + def __setattr__(self, key, value): + return setattr(self.parser, key, value) diff --git a/gooey/python_bindings/groupings.py b/gooey/python_bindings/groupings.py new file mode 100644 index 0000000..0db083a --- /dev/null +++ b/gooey/python_bindings/groupings.py @@ -0,0 +1,30 @@ +from collections import OrderedDict + + +def positional(actions): + groups = OrderedDict([('Positional Arguments', []), ('Optional Arguments', [])]) + for action in actions: + if action['group_name'] == 'Positional Arguments': + groups['Positional Arguments'].append(action) + else: + groups['Optional Arguments'].append(action) + return groups + + +def requiredAndOptional(actions): + groups = OrderedDict([('Required', []), ('Optional', [])]) + for action in actions: + if action['required']: + groups['Required'].append(action) + else: + groups['Optional'].append(action) + return groups + + +def argparseGroups(actions): + groups = OrderedDict() + for action in actions: + if action['group_name'] not in groups: + groups[action['group_name']] = [] + groups[action['group_name']].append(action) + return groups diff --git a/gooey/util/__init__.py b/gooey/util/__init__.py new file mode 100644 index 0000000..59bfc03 --- /dev/null +++ b/gooey/util/__init__.py @@ -0,0 +1,58 @@ +from functools import reduce +from inspect import signature + + + +def apply_transforms(data, func): + return func(data) + + +def bootlegCurry(f): + ''' + a bootleg curry. + ''' + def _curry(f, remaining): + def inner(*args): + if len(args) >= remaining: + return f(*args) + else: + newfunc = lambda *rem: f(*args, *rem) + return _curry(newfunc, remaining - len(args)) + return inner + return _curry(f, len(signature(f).parameters)) + + +def excluding(item_dict, *to_exclude): + excluded = set(to_exclude) + return {key: val for key, val in item_dict.items() + if key not in excluded} + + +def indentity(x): + return x + + +def merge(*args): + return reduce(lambda acc, val: acc.update(val) or acc, args, {}) + + +def partition_by(f, coll): + a = [] + b = [] + for item in coll: + bucket = a if f(item) else b + bucket.append(item) + return a, b + + + + +if __name__ == '__main__': + pass + # a = { + # 'a': 111, + # 'b': 111, + # 'c': 111, + # 1: 111 + # } + # print(excluding(a, 'a', 'c', 1))