From 055708ec3ccbb9e7f4e9838cbb968d6fe2c0070b Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 19 Dec 2020 15:58:31 -0800 Subject: [PATCH] add support for initial_value gooey option --- docs/Gooey-Options.md | 16 ++++++- gooey/gui/components/options/options.py | 54 +++++++++++++++++---- gooey/gui/components/widgets/bases.py | 6 ++- gooey/python_bindings/argparse_to_json.py | 14 ++++-- gooey/tests/test_argparse_to_json.py | 2 +- gooey/tests/test_checkbox.py | 58 +++++++++++++++++++++++ gooey/tests/test_common.py | 54 +++++++++++++++++++++ gooey/tests/test_counter.py | 51 ++++++++++++++++++++ gooey/tests/test_dropdown.py | 40 ++++++++++++++-- gooey/tests/test_listbox.py | 58 +++++++++++++++++++++++ gooey/tests/test_numeric_inputs.py | 22 +++++++++ gooey/tests/test_slider.py | 13 +++++ gooey/tests/test_textfield.py | 29 ++++++++++++ 13 files changed, 394 insertions(+), 23 deletions(-) create mode 100644 gooey/tests/test_checkbox.py create mode 100644 gooey/tests/test_common.py create mode 100644 gooey/tests/test_counter.py create mode 100644 gooey/tests/test_listbox.py diff --git a/docs/Gooey-Options.md b/docs/Gooey-Options.md index 77f2780..1f60d5d 100644 --- a/docs/Gooey-Options.md +++ b/docs/Gooey-Options.md @@ -22,7 +22,8 @@ and with that, you're ready to rock. ## Overview -* Global Style Options +* Global Style/Layout Options +* Global Config Options * Custom Widget Options * Textarea * BlockCheckbox @@ -31,7 +32,7 @@ and with that, you're ready to rock. * Argument Group Options -## Global Widget Styles +## Global Style / Layout Options All widgets in Gooey (with the exception of RadioGroups) are made up of three basic components. @@ -72,6 +73,17 @@ parser.add_argument('-my-arg', gooey_options={ | full_width | bool | This is a layout hint for this widget. When `True` the widget will fill the entire available space within a given row. Otherwise, it will be sized based on the column rules provided elsewhere. | +## Global Config Options + +> new in 1.0.8 + +All widgets in Gooey accept an `initial_value` option to seed the UI. + +```python +parser.add_argument('-my-arg', widget='Textarea', gooey_options={ + 'initial_value': 'Hello world!' +}) +``` ## Individual Widget Options diff --git a/gooey/gui/components/options/options.py b/gooey/gui/components/options/options.py index 00101ab..b1ce974 100644 --- a/gooey/gui/components/options/options.py +++ b/gooey/gui/components/options/options.py @@ -11,6 +11,16 @@ def _include_layout_docs(f): return f +def _include_global_option_docs(f): + """ + Combines docstrings for options available to + all widget types. + """ + _doc = """:param initial_value: Sets the initial value in the UI. + """ + f.__doc__ = (f.__doc__ or '') + _doc + return f + def _include_chooser_msg_wildcard_docs(f): """ Combines the basic Chooser options (wildard, message) docsstring @@ -74,18 +84,22 @@ def LayoutOptions(label_color=None, return _clean(locals()) + @_include_layout_docs -def TextField(validator=None, **layout_options): +@_include_global_option_docs +def TextField(initial_value=None, validator=None, **layout_options): return _clean(locals()) @_include_layout_docs -def PasswordField(validator=None, **layout_options): +@_include_global_option_docs +def PasswordField(initial_value=None, validator=None, **layout_options): return _clean(locals()) @_include_layout_docs -def IntegerField(validator=None, min=0, max=100, increment=1, **layout_options): +@_include_global_option_docs +def IntegerField(initial_value=None, validator=None, min=0, max=100, increment=1, **layout_options): """ :param min: The minimum value allowed :param max: The maximum value allowed @@ -94,7 +108,8 @@ def IntegerField(validator=None, min=0, max=100, increment=1, **layout_options): return _clean(locals()) @_include_layout_docs -def Slider(validator=None, min=0, max=100, increment=1, **layout_options): +@_include_global_option_docs +def Slider(initial_value=None, validator=None, min=0, max=100, increment=1, **layout_options): """ :param min: The minimum value allowed :param max: The maximum value allowed @@ -104,7 +119,9 @@ def Slider(validator=None, min=0, max=100, increment=1, **layout_options): @_include_layout_docs +@_include_global_option_docs def DecimalField(validator=None, + initial_value=None, min=0.0, max=1.0, increment=0.01, @@ -120,7 +137,8 @@ def DecimalField(validator=None, @_include_layout_docs -def TextArea(height=None, readonly=False, validator=None, **layout_options): +@_include_global_option_docs +def TextArea(initial_value=None, height=None, readonly=False, validator=None, **layout_options): """ :param height: The height of the TextArea. :param readonly: Controls whether or not user's may modify the contents @@ -129,12 +147,14 @@ def TextArea(height=None, readonly=False, validator=None, **layout_options): @_include_layout_docs +@_include_global_option_docs def RichTextConsole(**layout_options): return _clean(locals()) @_include_layout_docs -def ListBox(height=None, **layout_options): +@_include_global_option_docs +def ListBox(initial_value=None, height=None, **layout_options): """ :param height: The height of the ListBox """ @@ -150,30 +170,36 @@ def MutexGroup(initial_selection=None, title=None, **layout_options): @_include_layout_docs -def Dropdown(**layout_options): +@_include_global_option_docs +def Dropdown(initial_value=None, **layout_options): return _clean(locals()) @_include_layout_docs -def Counter(**layout_options): +@_include_global_option_docs +def Counter(initial_value=None, **layout_options): return _clean(locals()) @_include_layout_docs -def CheckBox(**layout_options): +@_include_global_option_docs +def CheckBox(initial_value=None, **layout_options): return _clean(locals()) @_include_layout_docs -def BlockCheckBox(checkbox_label=None, **layout_options): +@_include_global_option_docs +def BlockCheckBox(initial_value=None, checkbox_label=None, **layout_options): return _clean(locals()) @_include_layout_docs +@_include_global_option_docs def FilterableDropdown(placeholder=None, empty_message=None, max_size=80, search_strategy=None, + initial_value=None, **layout_options): """ :param placeholder: Text to display when the user has provided no input @@ -205,21 +231,25 @@ def PrefixSearchStrategy( @_include_layout_docs +@_include_global_option_docs @_include_choose_dir_file_docs @_include_chooser_msg_wildcard_docs def FileChooser(wildcard=None, default_dir=None, default_file=None, message=None, + initial_value=None, **layout_options): return _clean(locals()) @_include_layout_docs +@_include_global_option_docs @_include_chooser_msg_wildcard_docs def DirectoryChooser(wildcard=None, default_path=None, message=None, + initial_value=None, **layout_options): """ :param default_path: The default path selected when the dialog spawns @@ -228,23 +258,27 @@ def DirectoryChooser(wildcard=None, @_include_layout_docs +@_include_global_option_docs @_include_choose_dir_file_docs @_include_chooser_msg_wildcard_docs def FileSaver(wildcard=None, default_dir=None, default_file=None, message=None, + initial_value=None, **layout_options): return _clean(locals()) @_include_layout_docs +@_include_global_option_docs @_include_choose_dir_file_docs @_include_chooser_msg_wildcard_docs def MultiFileSaver(wildcard=None, default_dir=None, default_file=None, message=None, + initial_value=None, **layout_options): return _clean(locals()) diff --git a/gooey/gui/components/widgets/bases.py b/gooey/gui/components/widgets/bases.py index cc22671..04b33db 100644 --- a/gooey/gui/components/widgets/bases.py +++ b/gooey/gui/components/widgets/bases.py @@ -76,8 +76,12 @@ class TextContainer(BaseWidget): self.SetSizer(self.layout) self.bindMouseEvents() self.Bind(wx.EVT_SIZE, self.onSize) + + # 1.0.7 initial_value should supersede default when both are present + if self._options.get('initial_value') is not None: + self.setValue(self._options['initial_value']) # Checking for None instead of truthiness means False-evaluaded defaults can be used. - if self._meta['default'] is not None: + elif self._meta['default'] is not None: self.setValue(self._meta['default']) if self._options.get('placeholder'): diff --git a/gooey/python_bindings/argparse_to_json.py b/gooey/python_bindings/argparse_to_json.py index e951874..576b173 100644 --- a/gooey/python_bindings/argparse_to_json.py +++ b/gooey/python_bindings/argparse_to_json.py @@ -429,10 +429,15 @@ def action_to_json(action, widget, options): }, }) - default = handle_default(action, widget) + if (options.get(action.dest) or {}).get('initial_value') != None: + value = options[action.dest]['initial_value'] + options[action.dest]['initial_value'] = handle_initial_values(action, widget, value) + default = handle_initial_values(action, widget, action.default) if default == argparse.SUPPRESS: default = None + + final_options = merge(base, options.get(action.dest) or {}) validate_gooey_options(action, widget, final_options) @@ -494,7 +499,6 @@ def coerce_default(default, widget): 'Dropdown': safe_string, 'Counter': safe_string } - # Issue #321: # Defaults for choice types must be coerced to strings # to be able to match the stringified `choices` used by `wx.ComboBox` @@ -505,7 +509,7 @@ def coerce_default(default, widget): return dispatcher.get(widget, identity)(cleaned) -def handle_default(action, widget): +def handle_initial_values(action, widget, value): handlers = [ [textinput_with_nargs_and_list_default, coerse_nargs_list], [is_widget('Listbox'), clean_list_defaults], @@ -514,8 +518,8 @@ def handle_default(action, widget): ] for matches, apply_coercion in handlers: if matches(action, widget): - return apply_coercion(action.default) - return clean_default(action.default) + return apply_coercion(value) + return clean_default(value) def coerse_nargs_list(default): diff --git a/gooey/tests/test_argparse_to_json.py b/gooey/tests/test_argparse_to_json.py index e1d1b4f..77723fa 100644 --- a/gooey/tests/test_argparse_to_json.py +++ b/gooey/tests/test_argparse_to_json.py @@ -190,7 +190,7 @@ class TestArgparse(unittest.TestCase): parser = ArgumentParser(prog='test_program') parser.add_argument('--foo', nargs=case['nargs'], default=case['default']) action = parser._actions[-1] - result = argparse_to_json.handle_default(action, case['w']) + result = argparse_to_json.handle_initial_values(action, case['w'], action.default) self.assertEqual(result, case['gooey_default']) def test_nargs(self): diff --git a/gooey/tests/test_checkbox.py b/gooey/tests/test_checkbox.py new file mode 100644 index 0000000..01e0f98 --- /dev/null +++ b/gooey/tests/test_checkbox.py @@ -0,0 +1,58 @@ +import unittest + +from tests.harness import instrumentGooey +from gooey import GooeyParser +from gooey.tests import * + + + +class TestCheckbox(unittest.TestCase): + + def makeParser(self, **kwargs): + parser = GooeyParser(description='description') + parser.add_argument( + '--widget', + action='store_true', + **kwargs) + return parser + + + def testInitialValue(self): + cases = [ + # `initial` should supersede `default` + {'inputs': {'default': False, + 'widget': 'CheckBox', + 'gooey_options': {'initial_value': True}}, + 'expect': True}, + + {'inputs': {'gooey_options': {'initial_value': True}, + 'widget': 'CheckBox'}, + 'expect': True}, + + {'inputs': {'gooey_options': {'initial_value': False}, + 'widget': 'CheckBox'}, + 'expect': False}, + + {'inputs': {'default': True, + 'widget': 'CheckBox', + 'gooey_options': {}}, + 'expect': True}, + + {'inputs': {'default': True, + 'widget': 'CheckBox'}, + 'expect': True}, + + {'inputs': {'widget': 'CheckBox'}, + 'expect': False} + ] + for case in cases: + with self.subTest(case): + parser = self.makeParser(**case['inputs']) + with instrumentGooey(parser) as (app, gooeyApp): + widget = gooeyApp.configs[0].reifiedWidgets[0] + self.assertEqual(widget.getValue()['rawValue'], case['expect']) + + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/gooey/tests/test_common.py b/gooey/tests/test_common.py new file mode 100644 index 0000000..d9db88c --- /dev/null +++ b/gooey/tests/test_common.py @@ -0,0 +1,54 @@ +import unittest +from collections import namedtuple + +from tests.harness import instrumentGooey +from gooey import GooeyParser +from gooey.tests import * + +Case = namedtuple('Case', 'inputs initialExpected') + + +class TestCommonProperties(unittest.TestCase): + """ + Test options and functionality + common across all widgets. + """ + + def makeParser(self, **kwargs): + parser = GooeyParser(description='description') + parser.add_argument('--widget', **kwargs) + return parser + + def testInitialValue(self): + widgets = ['ColourChooser', + 'CommandField', + 'DateChooser', 'DirChooser', 'FileChooser', 'FileSaver', + 'FilterableDropdown', 'MultiDirChooser', 'MultiFileChooser', + 'PasswordField', 'TextField', 'Textarea', 'TimeChooser'] + + cases = [ + # initial_value supersedes, default + Case( + {'default': 'default', 'gooey_options': {'initial_value': 'some val'}}, + 'some val'), + Case( + {'gooey_options': {'initial_value': 'some val'}}, + 'some val'), + Case( + {'default': 'default', 'gooey_options': {}}, + 'default'), + Case({'default': 'default'}, + 'default') + ] + + for widgetName in widgets: + with self.subTest(widgetName): + for case in cases: + parser = self.makeParser(widget=widgetName, **case.inputs) + with instrumentGooey(parser) as (app, gooeyApp): + widget = gooeyApp.configs[0].reifiedWidgets[0] + self.assertEqual(widget.getValue()['rawValue'], case.initialExpected) + + +if __name__ == '__main__': + unittest.main() diff --git a/gooey/tests/test_counter.py b/gooey/tests/test_counter.py new file mode 100644 index 0000000..0c7d857 --- /dev/null +++ b/gooey/tests/test_counter.py @@ -0,0 +1,51 @@ +import unittest + +from tests.harness import instrumentGooey +from gooey import GooeyParser +from gooey.tests import * + + + +class TestCounter(unittest.TestCase): + + def makeParser(self, **kwargs): + parser = GooeyParser(description='description') + parser.add_argument( + '--widget', + action='count', + widget="Counter", + **kwargs) + return parser + + + def testInitialValue(self): + cases = [ + # `initial` should supersede `default` + {'inputs': {'default': 1, + 'gooey_options': {'initial_value': 3}}, + 'expect': '3'}, + + {'inputs': {'gooey_options': {'initial_value': 1}}, + 'expect': '1'}, + + {'inputs': {'default': 2, + 'gooey_options': {}}, + 'expect': '2'}, + + {'inputs': {'default': 1}, + 'expect': '1'}, + + {'inputs': {}, + 'expect': None} + ] + for case in cases: + with self.subTest(case): + parser = self.makeParser(**case['inputs']) + with instrumentGooey(parser) as (app, gooeyApp): + widget = gooeyApp.configs[0].reifiedWidgets[0] + self.assertEqual(widget.getValue()['rawValue'], case['expect']) + + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/gooey/tests/test_dropdown.py b/gooey/tests/test_dropdown.py index 4c2f11f..9ccf52a 100644 --- a/gooey/tests/test_dropdown.py +++ b/gooey/tests/test_dropdown.py @@ -2,13 +2,14 @@ import unittest from argparse import ArgumentParser from unittest.mock import patch +from gooey import GooeyParser from tests.harness import instrumentGooey from gooey.tests import * class TestGooeyDropdown(unittest.TestCase): - def make_parser(self, **kwargs): - parser = ArgumentParser(description='description') + def makeParser(self, **kwargs): + parser = GooeyParser(description='description') parser.add_argument('--dropdown', **kwargs) return parser @@ -35,11 +36,10 @@ class TestGooeyDropdown(unittest.TestCase): # TODO: from dynamics just like it does in parser land. It doesn't currently # TODO: do this, so I'm manually casting it to strings for now. [[True, False], True, 'True', ['True', 'False'], 'True'] - ] for choices, default, initalSelection, dynamicUpdate, expectedFinalSelection in testcases: - parser = self.make_parser(choices=choices, default=default) + parser = self.makeParser(choices=choices, default=default) with instrumentGooey(parser) as (app, gooeyApp): dropdown = gooeyApp.configs[0].reifiedWidgets[0] # ensure that default values (when supplied) are selected in the UI @@ -56,5 +56,37 @@ class TestGooeyDropdown(unittest.TestCase): self.assertEqual(dropdown.widget.GetValue(), expectedFinalSelection) + def testInitialValue(self): + cases = [ + # `initial` should supersede `default` + {'inputs': {'default': 'b', + 'choices': ['a', 'b', 'c'], + 'gooey_options': {'initial_value': 'a'}}, + 'expect': 'a'}, + + {'inputs': {'choices': ['a', 'b', 'c'], + 'gooey_options': {'initial_value': 'a'}}, + 'expect': 'a'}, + + {'inputs': {'choices': ['a', 'b', 'c'], + 'default': 'b', + 'gooey_options': {}}, + 'expect': 'b'}, + + {'inputs': {'choices': ['a', 'b', 'c'], + 'default': 'b'}, + 'expect': 'b'}, + + {'inputs': {'choices': ['a', 'b', 'c']}, + 'expect': None} + ] + for case in cases: + with self.subTest(case): + parser = self.makeParser(**case['inputs']) + with instrumentGooey(parser) as (app, gooeyApp): + widget = gooeyApp.configs[0].reifiedWidgets[0] + self.assertEqual(widget.getValue()['rawValue'], case['expect']) + + if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/gooey/tests/test_listbox.py b/gooey/tests/test_listbox.py new file mode 100644 index 0000000..2000b0b --- /dev/null +++ b/gooey/tests/test_listbox.py @@ -0,0 +1,58 @@ +import unittest + +from tests.harness import instrumentGooey +from gooey import GooeyParser +from gooey.tests import * + + + +class TestListbox(unittest.TestCase): + + def makeParser(self, **kwargs): + parser = GooeyParser(description='description') + parser.add_argument( + '--widget', + widget="Listbox", + nargs="*", + **kwargs) + return parser + + def testInitialValue(self): + cases = [ + # `initial` should supersede `default` + {'inputs': {'default': 'b', + 'choices': ['a', 'b', 'c'], + 'gooey_options': {'initial_value': 'a'}}, + 'expect': ['a']}, + + {'inputs': {'choices': ['a', 'b', 'c'], + 'gooey_options': {'initial_value': 'a'}}, + 'expect': ['a']}, + + {'inputs': {'choices': ['a', 'b', 'c'], + 'gooey_options': {'initial_value': ['a', 'c']}}, + 'expect': ['a', 'c']}, + + {'inputs': {'choices': ['a', 'b', 'c'], + 'default': 'b', + 'gooey_options': {}}, + 'expect': ['b']}, + + {'inputs': {'choices': ['a', 'b', 'c'], + 'default': 'b'}, + 'expect': ['b']}, + + {'inputs': {'choices': ['a', 'b', 'c']}, + 'expect': []} + ] + for case in cases: + with self.subTest(case): + parser = self.makeParser(**case['inputs']) + with instrumentGooey(parser) as (app, gooeyApp): + widget = gooeyApp.configs[0].reifiedWidgets[0] + self.assertEqual(widget.getValue()['rawValue'], case['expect']) + + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/gooey/tests/test_numeric_inputs.py b/gooey/tests/test_numeric_inputs.py index 322c85f..e7ee931 100644 --- a/gooey/tests/test_numeric_inputs.py +++ b/gooey/tests/test_numeric_inputs.py @@ -27,11 +27,33 @@ class TestNumbericInputs(unittest.TestCase): # the default and all works as expected. # this is a TODO for validation [{'default': 81234, 'widget': 'IntegerField', 'gooey_options': {'max': 99999}}, 81234], + # Initial Value cases + [{'widget': 'IntegerField', 'gooey_options': {'initial_value': 0}}, 0], + [{'widget': 'IntegerField', 'gooey_options': {'initial_value': 10}}, 10], + [{'widget': 'IntegerField', 'gooey_options': {'initial_value': 76}}, 76], + # note that WX caps the value + # unless explicitly widened via gooey_options + [{'widget': 'IntegerField', 'gooey_options': {'initial_value': 81234}}, 100], + # here we set the max to something higher than + # the default and all works as expected. + # this is a TODO for validation + [{'widget': 'IntegerField', 'gooey_options': {'initial_value': 81234, 'max': 99999}}, 81234], [{'widget': 'DecimalField'}, 0], [{'default': 0, 'widget': 'DecimalField'}, 0], [{'default': 81234, 'widget': 'DecimalField'}, 100], [{'default': 81234, 'widget': 'DecimalField', 'gooey_options': {'max': 99999}}, 81234], + # Initial Value cases + [{'widget': 'DecimalField', 'gooey_options': {'initial_value': 0}}, 0], + [{'widget': 'DecimalField', 'gooey_options': {'initial_value': 10}}, 10], + [{'widget': 'DecimalField', 'gooey_options': {'initial_value': 76}}, 76], + # note that WX caps the value + # unless explicitly widened via gooey_options + [{'widget': 'DecimalField', 'gooey_options': {'initial_value': 81234}}, 100], + # here we set the max to something higher than + # the default and all works as expected. + # this is a TODO for validation + [{'widget': 'DecimalField', 'gooey_options': {'initial_value': 81234, 'max': 99999}}, 81234], ] for inputs, expected in cases: with self.subTest(inputs): diff --git a/gooey/tests/test_slider.py b/gooey/tests/test_slider.py index 4461f12..2d685ce 100644 --- a/gooey/tests/test_slider.py +++ b/gooey/tests/test_slider.py @@ -26,6 +26,19 @@ class TestGooeySlider(unittest.TestCase): # the default and all works as expected. # this is a TODO for validation [{'default': 81234, 'gooey_options': {'max': 99999}}, 81234], + + # Initial Value cases + [{}, 0], + [{'gooey_options': {'initial_value': 0}}, 0], + [{'gooey_options': {'initial_value': 10}}, 10], + [{'gooey_options': {'initial_value': 76}}, 76], + # note that WX caps the value + # unless explicitly widened via gooey_options + [{'gooey_options': {'initial_value': 81234}}, 100], + # here we set the max to something higher than + # the default and all works as expected. + # this is a TODO for validation + [{'gooey_options': {'initial_value': 81234, 'max': 99999}}, 81234], ] for inputs, expected in cases: with self.subTest(inputs): diff --git a/gooey/tests/test_textfield.py b/gooey/tests/test_textfield.py index 3e7aeab..0313fd1 100644 --- a/gooey/tests/test_textfield.py +++ b/gooey/tests/test_textfield.py @@ -1,9 +1,12 @@ import unittest +from collections import namedtuple from tests.harness import instrumentGooey from gooey import GooeyParser from gooey.tests import * +Case = namedtuple('Case', 'inputs initialExpected expectedAfterClearing') + class TestTextField(unittest.TestCase): def makeParser(self, **kwargs): @@ -28,5 +31,31 @@ class TestTextField(unittest.TestCase): + + def testDefaultAndInitialValue(self): + cases = [ + # initial_value takes precedence when both are present + Case( + {'default': 'default_val', 'gooey_options': {'initial_value': 'some val'}}, + 'some val', + None), + # when no default is present + # Case({'gooey_options': {'initial_value': 'some val'}}, + # 'some val', + # ''), + # [{'default': 'default', 'gooey_options': {}}, + # 'default'], + # [{'default': 'default'}, + # 'default'], + ] + for case in cases: + parser = self.makeParser(**case.inputs) + with instrumentGooey(parser) as (app, gooeyApp): + widget = gooeyApp.configs[0].reifiedWidgets[0] + self.assertEqual(widget.getValue()['rawValue'], case.initialExpected) + widget.setValue('') + print(widget.getValue()) + self.assertEqual(widget.getValue()['cmd'], case.expectedAfterClearing) + if __name__ == '__main__': unittest.main() \ No newline at end of file