mirror of https://github.com/chriskiehl/Gooey.git
Browse Source
Removed dependency on argparse for building the GUI - now uses json. Fixed bugs in components resizing. Changed gooey_decorator to use the new json backed GUI builder
pull/61/head
Removed dependency on argparse for building the GUI - now uses json. Fixed bugs in components resizing. Changed gooey_decorator to use the new json backed GUI builder
pull/61/head
15 changed files with 631 additions and 84 deletions
Split View
Diff Options
-
203gooey/argparse_to_json.py
-
73gooey/argparse_to_json_unittest.py
-
20gooey/dev_utils/ast_inspector.py
-
6gooey/gooey_decorator.py
-
39gooey/gui/advanced_config.py
-
14gooey/gui/base_window.py
-
79gooey/gui/build_spec_validator.py
-
87gooey/gui/componenets2_runner.py
-
38gooey/gui/component_builder.py
-
77gooey/gui/components2.py
-
1gooey/gui/display_main.py
-
3gooey/gui/runtime_display_panel.py
-
2gooey/gui/widget_pack.py
-
27gooey/mockapplications/mockapp.py
-
46gooey/mockapplications/qwindelzorf _example.py
@ -0,0 +1,203 @@ |
|||
""" |
|||
Converts argparse parser actions into json "Build Specs" |
|||
""" |
|||
|
|||
from argparse import ( |
|||
_CountAction, |
|||
_HelpAction, |
|||
_StoreConstAction, |
|||
_StoreFalseAction, |
|||
_StoreTrueAction |
|||
) |
|||
import itertools |
|||
|
|||
|
|||
VALID_WIDGETS = ( |
|||
'@FileChooser', |
|||
'@DirChooser', |
|||
'@DateChooser', |
|||
'@TextField', |
|||
'@Dropdown', |
|||
'@Counter', |
|||
'@RadioGroup' |
|||
) |
|||
|
|||
|
|||
def convert(argparser): |
|||
|
|||
|
|||
mutually_exclusive_group = [ |
|||
mutex_action |
|||
for group_actions in argparser._mutually_exclusive_groups |
|||
for mutex_action in group_actions._group_actions] |
|||
|
|||
base_actions = [action for action in argparser._actions |
|||
if action not in mutually_exclusive_group] |
|||
|
|||
positional_args = get_required_and_positional_args(base_actions) |
|||
|
|||
choice_args = get_optionals_with_choices(base_actions) |
|||
standard_args = get_optionals_without_choices(base_actions) |
|||
counter_args = get_counter_style_optionals(base_actions) |
|||
radio_args = get_mutually_exclusive_optionals(mutually_exclusive_group) |
|||
checkable_args = get_flag_style_optionals(base_actions) |
|||
|
|||
return { |
|||
'required': positional_args, |
|||
'optional': list(itertools.chain( |
|||
get_optionals_with_choices(base_actions), |
|||
get_optionals_without_choices(base_actions), |
|||
get_counter_style_optionals(base_actions), |
|||
get_mutually_exclusive_optionals(mutually_exclusive_group), |
|||
get_flag_style_optionals(base_actions) |
|||
)), |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
|
|||
def get_required_and_positional_args(actions): |
|||
""" |
|||
Extracts positional or required args from the actions list |
|||
In argparse, positionals are defined by either an empty option_strings |
|||
or by the option_strings parameters being sans a leading hyphen |
|||
""" |
|||
filtered_actions = [action for action in actions |
|||
if not action.option_strings |
|||
or action.required == True] |
|||
|
|||
return [as_json(action, default_widget='TextField') |
|||
for action in filtered_actions] |
|||
|
|||
|
|||
def get_optionals_with_choices(actions): |
|||
""" |
|||
All optional arguments which are constrained |
|||
to specific choices. |
|||
""" |
|||
filtered_actions = [action |
|||
for action in actions |
|||
if action.choices] |
|||
|
|||
return [as_json(action, default_widget='Dropdown') |
|||
for action in filtered_actions] |
|||
|
|||
|
|||
def get_optionals_without_choices(actions): |
|||
""" |
|||
All actions which are: |
|||
(a) Optional, but without required choices |
|||
(b) Not of a "boolean" type (storeTrue, etc..) |
|||
(c) Of type _AppendAction |
|||
|
|||
e.g. anything which has an argument style like: |
|||
>>> -f myfilename.txt |
|||
""" |
|||
boolean_actions = ( |
|||
_StoreConstAction, _StoreFalseAction, |
|||
_StoreTrueAction |
|||
) |
|||
filtered_actions = [ |
|||
action |
|||
for action in actions |
|||
if action.option_strings |
|||
and not action.choices |
|||
and not isinstance(action, _CountAction) |
|||
and not isinstance(action, _HelpAction) |
|||
and type(action) not in boolean_actions |
|||
] |
|||
|
|||
return [as_json(action, default_widget='TextField') |
|||
for action in filtered_actions] |
|||
|
|||
|
|||
def get_flag_style_optionals(actions): |
|||
""" |
|||
Gets all instances of "flag" type options. |
|||
i.e. options which either store a const, or |
|||
store boolean style options (e.g. StoreTrue). |
|||
Types: |
|||
_StoreTrueAction |
|||
_StoreFalseAction |
|||
_StoreConst |
|||
""" |
|||
filtered_actions = [ |
|||
action |
|||
for action in actions |
|||
if isinstance(action, _StoreTrueAction) |
|||
or isinstance(action, _StoreFalseAction) |
|||
or isinstance(action, _StoreConstAction) |
|||
] |
|||
|
|||
return [as_json(action, default_widget='CheckBox') |
|||
for action in filtered_actions] |
|||
|
|||
|
|||
def get_counter_style_optionals(actions): |
|||
""" |
|||
Returns all instances of type _CountAction |
|||
""" |
|||
filtered_actions = [action |
|||
for action in actions |
|||
if isinstance(action, _CountAction)] |
|||
|
|||
_json_options = [as_json(action, default_widget='Dropdown') |
|||
for action in filtered_actions] |
|||
|
|||
# Counter should show as Dropdowns, so pre-populare with numeric choices |
|||
for opt in _json_options: |
|||
opt['choices'] = range(10) |
|||
|
|||
return _json_options |
|||
|
|||
|
|||
def get_mutually_exclusive_optionals(mutex_group): |
|||
if not mutex_group: |
|||
return [] |
|||
|
|||
options = [ |
|||
{ |
|||
'display_name': 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', |
|||
'data': options |
|||
}] |
|||
|
|||
|
|||
def as_json(action, default_widget): |
|||
option_strings = action.option_strings |
|||
_type = option_strings[-1] if option_strings else None |
|||
return { |
|||
'type': widget_type(_type) if is_widget_spec(_type) else default_widget, |
|||
'data' : { |
|||
'display_name': action.dest, |
|||
'help': action.help, |
|||
'nargs': action.nargs or '', |
|||
'commands': action.option_strings, |
|||
'choices': action.choices or [], |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
def is_widget_spec(option_string): |
|||
return option_string and option_string in VALID_WIDGETS |
|||
|
|||
def widget_type(option_string): |
|||
return option_string[1:] |
|||
|
|||
|
|||
|
|||
|
|||
|
|||
|
|||
|
@ -0,0 +1,73 @@ |
|||
import argparse |
|||
import unittest |
|||
import json |
|||
from argparse_to_json import * |
|||
|
|||
class TestArgparseToJson(unittest.TestCase): |
|||
|
|||
def setUp(self): |
|||
my_cool_parser = argparse.ArgumentParser(description='description') |
|||
my_cool_parser.add_argument("filename", help='filename help msg') # positional |
|||
my_cool_parser.add_argument("outfile", help="Name of the file where you'll save the output") # positional |
|||
my_cool_parser.add_argument('-c', '--countdown', default=10, type=int, help='sets the time to count down from') |
|||
my_cool_parser.add_argument("-s", "--showtime", action="store_true", help="display the countdown timer") |
|||
my_cool_parser.add_argument("-d", "--delay", action="store_true", help="Delay execution for a bit") |
|||
my_cool_parser.add_argument('--verbose', '-v', action='count') |
|||
my_cool_parser.add_argument("-o", "--obfuscate", action="store_true", help="obfuscate the countdown timer!") |
|||
my_cool_parser.add_argument('-r', '--recursive', choices=['yes', 'no'], help='Recurse into subfolders') |
|||
my_cool_parser.add_argument("-w", "--writelog", default="No, NOT whatevs", help="write log to some file or something") |
|||
my_cool_parser.add_argument("-e", "--expandAll", action="store_true", help="expand all processes") |
|||
verbosity = my_cool_parser.add_mutually_exclusive_group() |
|||
verbosity.add_argument('-t', '--verboze', dest='verboze', action="store_true", help="Show more details") |
|||
verbosity.add_argument('-q', '--quiet', dest='quiet', action="store_true", help="Only output on error") |
|||
|
|||
self.parser = my_cool_parser |
|||
|
|||
self.mutually_exclusive_group = [ |
|||
mutex_action |
|||
for group_actions in self.parser._mutually_exclusive_groups |
|||
for mutex_action in group_actions._group_actions] |
|||
|
|||
self.base_actions = [action for action in self.parser._actions |
|||
if action not in self.mutually_exclusive_group] |
|||
|
|||
def test_get_optionals_with_choices(self): |
|||
target_arg = self.find_arg_by_option(self.base_actions, '--recursive') |
|||
json_result = get_optionals_with_choices(self.base_actions) |
|||
self._test_parser_to_json_mapping(target_arg, json_result[0], 'Dropdown') |
|||
|
|||
def test_get_optionals_without_choices(self): |
|||
target_arg = self.find_arg_by_option(self.base_actions, '--showtime') |
|||
json_result = get_optionals_without_choices(self.base_actions) |
|||
self._test_parser_to_json_mapping(target_arg, json_result[0], 'TextField') |
|||
|
|||
def test_get_counter_style_optionals(self): |
|||
target_arg = self.find_arg_by_option(self.base_actions, '--verbose') |
|||
json_result = get_counter_style_optionals(self.base_actions) |
|||
self._test_parser_to_json_mapping(target_arg, json_result[0], 'Dropdown') |
|||
|
|||
def test_get_mutually_exclusive_optionals(self): |
|||
target_arg = self.find_arg_by_option(self.mutually_exclusive_group, '--verboze') |
|||
json_result = get_mutually_exclusive_optionals(self.mutually_exclusive_group)[0] |
|||
data = json_result['data'][0] |
|||
self.assertEqual('RadioGroup', json_result['type']) |
|||
self.assertEqual(target_arg.choices, data['choices']) |
|||
self.assertEqual(target_arg.help, data['help']) |
|||
self.assertEqual(target_arg.option_strings, data['commands']) |
|||
self.assertEqual(target_arg.dest, data['display_name']) |
|||
|
|||
|
|||
def _test_parser_to_json_mapping(self, target_arg, json_string, expected_type): |
|||
self.assertEqual(expected_type, json_string['type']) |
|||
self.assertEqual(target_arg.choices, json_string['data']['choices']) |
|||
self.assertEqual(target_arg.help, json_string['data']['help']) |
|||
self.assertEqual(target_arg.option_strings, json_string['data']['commands']) |
|||
self.assertEqual(target_arg.dest, json_string['data']['display_name']) |
|||
|
|||
|
|||
def find_arg_by_option(self, group, option_string): |
|||
for arg in group: |
|||
if option_string in arg.option_strings: |
|||
return arg |
|||
|
|||
|
@ -0,0 +1,79 @@ |
|||
''' |
|||
Validates that the json has meaningful keys |
|||
''' |
|||
|
|||
import itertools |
|||
|
|||
|
|||
a = { |
|||
'required' : [ |
|||
{ |
|||
'component': 'TextField', |
|||
'data': { |
|||
'display_name': 'filename', |
|||
'help_text': 'path to file you want to process', |
|||
'command_args': ['-f', '--infile'] |
|||
} |
|||
}, |
|||
{ |
|||
'component': 'FileChooser', |
|||
'data': { |
|||
'display_name': 'Output Location', |
|||
'help_text': 'Where to save the file', |
|||
'command_args': ['-o', '--outfile'] |
|||
} |
|||
} |
|||
], |
|||
'optional' : [ |
|||
{ |
|||
'component': 'RadioGroup', |
|||
'data': [ |
|||
{ |
|||
'display_name': 'Output Location', |
|||
'help_text': 'Where to save the file', |
|||
'command_args': ['-o', '--outfile'] |
|||
}, { |
|||
'display_name': 'Output Location', |
|||
'help_text': 'Where to save the file', |
|||
'command_args': ['-o', '--outfile'] |
|||
} |
|||
] |
|||
} |
|||
] |
|||
} |
|||
|
|||
VALID_WIDGETS = ( |
|||
'FileChooser', |
|||
'DirChooser', |
|||
'DateChooser', |
|||
'TextField', |
|||
'Dropdown', |
|||
'Counter', |
|||
'RadioGroup' |
|||
) |
|||
|
|||
|
|||
class MalformedBuildSpecException(Exception): |
|||
pass |
|||
|
|||
def validate(json_string): |
|||
required = json_string.get('required') |
|||
optional = json_string.get('optional') |
|||
|
|||
if not required or not optional: |
|||
raise MalformedBuildSpecException("All objects must be children of 'required,' or 'optional'") |
|||
|
|||
objects = [item for key in json_string for item in json_string[key]] |
|||
|
|||
for obj in objects: |
|||
if obj['component'] not in VALID_WIDGETS: |
|||
raise MalformedBuildSpecException("Invalid Component name: {0}".format(obj['component'])) |
|||
|
|||
|
|||
if __name__ == '__main__': |
|||
|
|||
validate(a) |
|||
|
|||
|
|||
|
|||
|
@ -1,41 +1,76 @@ |
|||
__author__ = 'Chris' |
|||
|
|||
import wx |
|||
import components2 |
|||
from wx.lib.scrolledpanel import ScrolledPanel |
|||
|
|||
|
|||
class MyFrame(wx.Frame): |
|||
class TestPanel(ScrolledPanel): |
|||
def __init__(self, parent): |
|||
wx.Frame.__init__(self, parent, title="test", size=(320, 240)) |
|||
self.SetBackgroundColour('#ffffff') |
|||
ScrolledPanel.__init__(self, parent) |
|||
self.SetupScrolling(scroll_x=False) |
|||
|
|||
self.textctrls = [wx.TextCtrl(self) for _ in range(4)] |
|||
|
|||
sizer = wx.BoxSizer(wx.VERTICAL) |
|||
# f = components2.RadioGroup({ |
|||
# 'title': 'cool title', |
|||
# 'help_msg': 'cool help msg that is super long and intense andd has lots of words!', 'nargs': '+', |
|||
# 'option_strings': ['-f', '--fudger'], |
|||
# 'choices': ['choice 1', 'choice 2', 'choice 3'] |
|||
# }) |
|||
f = components2.RadioGroup(data={ |
|||
'group_name': 'My Options', |
|||
'buttons': [ |
|||
{'name': 'verbose', |
|||
'help': "cool help msg that is super long and intense and has lots of words!", |
|||
'option': '-v' |
|||
},{ |
|||
'name': 'quiet', |
|||
'help': "Only output on error", |
|||
'option': '-q' |
|||
} |
|||
] |
|||
}) |
|||
sizer.Add(f.build(self), 0, wx.EXPAND) |
|||
hsizer = wx.BoxSizer(wx.HORIZONTAL) |
|||
for textctrl in self.textctrls: |
|||
hsizer.Add(textctrl, 1, wx.EXPAND) |
|||
|
|||
sizer.Add(hsizer, 0, wx.EXPAND) |
|||
self.SetSizer(sizer) |
|||
|
|||
class MyFrame(wx.Frame): |
|||
def __init__(self, parent): |
|||
wx.Frame.__init__(self, parent, title="test", size=(320, 240)) |
|||
self.SetBackgroundColour('#ffffff') |
|||
self.panel = TestPanel(self) |
|||
self.Show() |
|||
|
|||
if __name__ == '__main__': |
|||
app = wx.App(False) |
|||
frame = MyFrame(None) |
|||
frame.Show(True) |
|||
MyFrame(None) |
|||
app.MainLoop() |
|||
|
|||
|
|||
|
|||
|
|||
# a = { |
|||
# 'required' : [ |
|||
# { |
|||
# 'component': 'TextField', |
|||
# 'data': { |
|||
# 'display_name': 'filename', |
|||
# 'help_text': 'path to file you want to process', |
|||
# 'command_args': ['-f', '--infile'] |
|||
# } |
|||
# }, |
|||
# { |
|||
# 'component': 'FileChooser', |
|||
# 'data': { |
|||
# 'display_name': 'Output Location', |
|||
# 'help_text': 'Where to save the file', |
|||
# 'command_args': ['-o', '--outfile'] |
|||
# } |
|||
# } |
|||
# ], |
|||
# 'optional' : [ |
|||
# { |
|||
# 'component': 'RadioGroup', |
|||
# 'data': [ |
|||
# { |
|||
# 'display_name': 'Output Location', |
|||
# 'help_text': 'Where to save the file', |
|||
# 'command_args': ['-o', '--outfile'] |
|||
# }, { |
|||
# 'display_name': 'Output Location', |
|||
# 'help_text': 'Where to save the file', |
|||
# 'command_args': ['-o', '--outfile'] |
|||
# } |
|||
# ] |
|||
# } |
|||
# ] |
|||
# } |
|||
# |
|||
# ] |
|||
# } |
|||
|
@ -0,0 +1,38 @@ |
|||
import itertools |
|||
from gooey.gui import components2 |
|||
|
|||
|
|||
class ComponentBuilder(object): |
|||
def __init__(self, build_spec): |
|||
self.build_spec = build_spec |
|||
_required_specs = self.build_spec.get('required', None) |
|||
_optional_specs = self.build_spec.get('optional', None) |
|||
|
|||
self.required_args = self.build_widget(_required_specs) if _required_specs else None |
|||
optionals = self.build_widget(_optional_specs) if _optional_specs else None |
|||
if _optional_specs: |
|||
self.flags = [widget for widget in optionals if isinstance(widget, components2.CheckBox)] |
|||
self.general_options = [widget for widget in optionals if not isinstance(widget, components2.CheckBox)] |
|||
else: |
|||
self.flags = [] |
|||
self.general_options = [] |
|||
|
|||
def build_widget(self, build_spec): |
|||
assembled_widgets = [] |
|||
for spec in build_spec: |
|||
widget_type = spec['type'] |
|||
properties = spec['data'] |
|||
|
|||
Component = getattr(components2, widget_type) |
|||
assembled_widgets.append(Component(data=properties)) |
|||
return assembled_widgets |
|||
|
|||
|
|||
def __iter__(self): |
|||
''' |
|||
return an iterator for all of the contained gui |
|||
''' |
|||
return itertools.chain(self.required_args, |
|||
self.flags, |
|||
self.general_options) |
|||
|
Write
Preview
Loading…
Cancel
Save