From fca40dbcd5bc961cf20a6d8fa533ce7ea139249b Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 19 Apr 2019 15:46:53 -0700 Subject: [PATCH] closes #408 - allow hiding of widgets in UI --- gooey/gui/components/config.py | 2 + gooey/python_bindings/constraints.py | 49 ++++++++++++++ gooey/python_bindings/gooey_parser.py | 20 ++++-- gooey/tests/test_constraints.py | 98 +++++++++++++++++++++++++++ gooey/tests/test_language_parity.py | 41 ----------- 5 files changed, 163 insertions(+), 47 deletions(-) create mode 100644 gooey/python_bindings/constraints.py create mode 100644 gooey/tests/test_constraints.py delete mode 100644 gooey/tests/test_language_parity.py diff --git a/gooey/gui/components/config.py b/gooey/gui/components/config.py index d260f67..999e225 100644 --- a/gooey/gui/components/config.py +++ b/gooey/gui/components/config.py @@ -134,6 +134,8 @@ class ConfigPage(ScrolledPanel): sizer = wx.BoxSizer(wx.HORIZONTAL) for item in uigroup: widget = self.reifyWidget(parent, item) + if not getin(item, ['options', 'visible'], True): + widget.Hide() # !Mutate the reifiedWidgets instance variable in place self.reifiedWidgets.append(widget) sizer.Add(widget, 1, wx.ALL | wx.EXPAND, 5) diff --git a/gooey/python_bindings/constraints.py b/gooey/python_bindings/constraints.py new file mode 100644 index 0000000..20de5d6 --- /dev/null +++ b/gooey/python_bindings/constraints.py @@ -0,0 +1,49 @@ +""" +Basic constraints to ensure GooeyParser is fed all the info it needs +for various widget classes. + +TODO: this should all live in the build_config stage here where it is used +within the GooeyParser directly. As is, logic is fragmented across files. Some +assertions happen in argparse_to_json, while others happen in GooeyParser. + +Whenever refactoring happens, these should be removed from GooeyParser. +""" +from textwrap import dedent + +def is_required(action): + return action.required + +def is_hidden(options): + return not options.get('visible', True) + +def has_validator(options): + return bool(options.get('validator')) + +def has_default(action): + return bool(action.default) + +def assert_visibility_requirements(action, options): + if action.required and is_hidden(options) \ + and not (has_validator(options) or has_default(action)): + raise ValueError(dedent( + ''' + When using Gooey's hidden field functionality, you must either ' + + (a) provide a default value, or ' + (b) provide a custom validator' + + Without one of those, your users will be unable to advance past + the configuration screen as they cannot interact with your + hidden field, and the default validator requires something to + be present for fields marked as `required`. + ''' + )) + +def assert_listbox_constraints(widget, **kwargs): + if widget and widget == 'Listbox': + if not 'nargs' in kwargs or kwargs['nargs'] not in ['*', '+']: + raise ValueError( + 'Gooey\'s Listbox widget requires that nargs be specified.\n' + 'Nargs must be set to either `*` or `+` (e.g. nargs="*")' + ) + diff --git a/gooey/python_bindings/gooey_parser.py b/gooey/python_bindings/gooey_parser.py index 322aac8..1ecb8fa 100644 --- a/gooey/python_bindings/gooey_parser.py +++ b/gooey/python_bindings/gooey_parser.py @@ -1,5 +1,6 @@ from argparse import ArgumentParser, _SubParsersAction from argparse import _MutuallyExclusiveGroup, _ArgumentGroup +from textwrap import dedent from gooey.gui.lang.i18n import _ @@ -84,16 +85,16 @@ class GooeyParser(object): metavar = kwargs.pop('metavar', None) options = kwargs.pop('gooey_options', None) - if widget and widget == 'Listbox': - if not 'nargs' in kwargs or kwargs['nargs'] not in ['*', '+']: - raise ValueError( - 'Gooey\'s Listbox widget requires that nargs be specified.\n' - 'Nargs must be set to either `*` or `+` (e.g. nargs="*")' - ) self.parser.add_argument(*args, **kwargs) self.parser._actions[-1].metavar = metavar self.widgets[self.parser._actions[-1].dest] = widget self.options[self.parser._actions[-1].dest] = options + self._validate_constraints( + self.parser._actions[-1], + widget, + options or {}, + **kwargs + ) def add_mutually_exclusive_group(self, *args, **kwargs): options = kwargs.pop('gooey_options', {}) @@ -143,6 +144,13 @@ class GooeyParser(object): # return the created parsers action return action + def _validate_constraints(self, parser_action, widget, options, **kwargs): + from gooey.python_bindings import constraints + constraints.assert_listbox_constraints(widget, **kwargs) + constraints.assert_visibility_requirements(parser_action, options) + + + def __getattr__(self, item): return getattr(self.parser, item) diff --git a/gooey/tests/test_constraints.py b/gooey/tests/test_constraints.py new file mode 100644 index 0000000..f5c1231 --- /dev/null +++ b/gooey/tests/test_constraints.py @@ -0,0 +1,98 @@ +import unittest + +from gooey import GooeyParser + + +class TestConstraints(unittest.TestCase): + + def test_listbox_constraints(self): + """ + Listbox widgets must be provided a nargs option + """ + + # Trying to create a listbox widget without specifying nargs + # throws an error + with self.assertRaises(ValueError): + parser = GooeyParser() + parser.add_argument('one', choices=['one', 'two'], widget='Listbox') + + # Listbox with an invalid nargs value throws an error + with self.assertRaises(ValueError): + parser = GooeyParser() + parser.add_argument( + 'one', choices=['one', 'two'], widget='Listbox', nargs='?') + + # Listbox with an invalid nargs value throws an error + with self.assertRaises(ValueError): + parser = GooeyParser() + parser.add_argument( + 'one', choices=['one', 'two'], widget='Listbox', nargs=3) + + # valid nargs throw no errors + for narg in ['*', '+']: + parser = GooeyParser() + parser.add_argument( + 'one', choices=['one', 'two'], widget='Listbox', nargs=narg) + + + + def test_visibility_constraint(self): + """ + When visible=False in Gooey config, the user MUST supply either + a custom validator or a default value. + """ + # added without issue + parser = GooeyParser() + parser.add_argument('one') + + # still fine + parser = GooeyParser() + parser.add_argument('one', gooey_options={'visible': True}) + + # trying to hide an input without a default or custom validator + # results in an error + with self.assertRaises(ValueError): + parser = GooeyParser() + parser.add_argument('one', gooey_options={'visible': False}) + + # explicit default=None; still error + with self.assertRaises(ValueError): + parser = GooeyParser() + parser.add_argument( + 'one', + default=None, + gooey_options={'visible': False}) + + # default = empty string. Still error + with self.assertRaises(ValueError): + parser = GooeyParser() + parser.add_argument( + 'one', + default='', + gooey_options={'visible': False}) + + # default = valid string. No Error + parser = GooeyParser() + parser.add_argument( + 'one', + default='Hello', + gooey_options={'visible': False}) + + # No default, but custom validator: Success + parser = GooeyParser() + parser.add_argument( + 'one', + gooey_options={ + 'visible': False, + 'validator': {'test': 'true'} + }) + + # default AND validator, still fine + parser = GooeyParser() + parser.add_argument( + 'one', + default='Hai', + gooey_options={ + 'visible': False, + 'validator': {'test': 'true'} + }) \ No newline at end of file diff --git a/gooey/tests/test_language_parity.py b/gooey/tests/test_language_parity.py deleted file mode 100644 index 5410db1..0000000 --- a/gooey/tests/test_language_parity.py +++ /dev/null @@ -1,41 +0,0 @@ -import os -import unittest -import json -from collections import OrderedDict -from gooey import languages - -from gooey.gui.processor import ProcessController - - -class TestLanguageParity(unittest.TestCase): - """ - Checks that all language files have the same set of keys so that non-english - languages don't silently break as features are added to Gooey. - """ - - def test_languageParity(self): - langDir = os.path.dirname(languages.__file__) - englishFile = os.path.join(langDir, 'english.json') - - english = self.readFile(englishFile) - jsonFiles = [(path, self.readFile(os.path.join(langDir, path))) - for path in os.listdir(langDir) - if path.endswith('json') and 'poooo' not in path and '2' not in path] - - allKeys = set(english.keys()) - for name, contents in jsonFiles: - missing = allKeys.difference(set(contents.keys())) - self.assertEqual( - set(), - missing, - "{} language file is missing keys: [{}]".format(name, missing) - ) - - - def readFile(self, path): - with open(path, 'r', encoding='utf-8') as f: - return json.loads(f.read()) - - -if __name__ == '__main__': - unittest.main()