Browse Source

Pluggable widget grouping

wx2.0
chriskiehl 7 years ago
parent
commit
117942d6b9
11 changed files with 892 additions and 353 deletions
  1. 197
      gooey/gui/components/widgets/base.py
  2. 10
      gooey/gui/containers/application.py
  3. 150
      gooey/gui/processor.py
  4. 75
      gooey/gui/util/formatters.py
  5. 4
      gooey/gui/util/functional.py
  6. 513
      gooey/python_bindings/argparse_to_json.py
  7. 4
      gooey/python_bindings/config_generator.py
  8. 16
      gooey/python_bindings/gooey_decorator.py
  9. 188
      gooey/python_bindings/gooey_parser.py
  10. 30
      gooey/python_bindings/groupings.py
  11. 58
      gooey/util/__init__.py

197
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('<b>{}</b>'.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__

10
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)

150
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

75
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

4
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

513
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

4
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:

16
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

188
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)

30
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

58
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))
Loading…
Cancel
Save