Browse Source

1.0.7 release features

* new widgets: IntegerField, DecimalField, Slider
* new validator: RegexValidator
* new Option Helpers: from gooey import options
* new option: placeholder
pull/659/head
Chris 3 years ago
parent
commit
65e71d16c2
23 changed files with 947 additions and 92 deletions
  1. 1
      gooey/__init__.py
  2. 4
      gooey/gui/components/filtering/prefix_filter.py
  3. 5
      gooey/gui/components/footer.py
  4. 0
      gooey/gui/components/options/__init__.py
  5. 300
      gooey/gui/components/options/options.py
  6. 201
      gooey/gui/components/options/validators.py
  7. 2
      gooey/gui/components/widgets/__init__.py
  8. 21
      gooey/gui/components/widgets/bases.py
  9. 6
      gooey/gui/components/widgets/choosers.py
  10. 5
      gooey/gui/components/widgets/core/chooser.py
  11. 3
      gooey/gui/components/widgets/core/text_input.py
  12. 50
      gooey/gui/components/widgets/numeric_fields.py
  13. 74
      gooey/gui/components/widgets/options.py
  14. 28
      gooey/gui/components/widgets/slider.py
  15. 8
      gooey/gui/components/widgets/textfield.py
  16. 36
      gooey/python_bindings/argparse_to_json.py
  17. 89
      gooey/tests/test_numeric_inputs.py
  18. 54
      gooey/tests/test_options.py
  19. 32
      gooey/tests/test_password.py
  20. 51
      gooey/tests/test_slider.py
  21. 32
      gooey/tests/test_textarea.py
  22. 32
      gooey/tests/test_textfield.py
  23. 5
      gooey/util/functional.py

1
gooey/__init__.py

@ -4,4 +4,5 @@ from gooey.python_bindings.gooey_parser import GooeyParser
from gooey.gui.util.freeze import localResourcePath as local_resource_path
from gooey.python_bindings import constants
from gooey.gui.components.filtering.prefix_filter import PrefixTokenizers
from gooey.gui.components.options import options
__version__ = '1.0.6'

4
gooey/gui/components/filtering/prefix_filter.py

@ -18,7 +18,9 @@ class PrefixTokenizers:
def REGEX(cls, expression):
return expression
class OperatorType:
AND = 'AND'
OR = 'OR'
class SearchOptions:
def __init__(self,

5
gooey/gui/components/footer.py

@ -18,7 +18,10 @@ class Footer(wx.Panel):
self.buildSpec = buildSpec
self.SetMinSize((30, 53))
self.SetDoubleBuffered(True)
# TODO: The was set to True for the timer addition
# however, it leads to 'tearing' issues when resizing
# the GUI in windows. Disabling until I can dig into it.
self.SetDoubleBuffered(False)
# components
self.cancel_button = None
self.start_button = None

0
gooey/gui/components/options/__init__.py

300
gooey/gui/components/options/options.py

@ -0,0 +1,300 @@
from gooey.gui.components.filtering.prefix_filter import PrefixTokenizers
def _include_layout_docs(f):
"""
Combines the layout_options docsstring with the
wrapped function's doc string.
"""
f.__doc__ = (f.__doc__ or '') + LayoutOptions.__doc__
return f
def _include_chooser_msg_wildcard_docs(f):
"""
Combines the basic Chooser options (wildard, message) docsstring
with the wrapped function's doc string.
"""
_doc = """:param wildcard: Sets the wildcard, which can contain multiple file types, for
example: "BMP files (.bmp)|.bmp|GIF files (.gif)|.gif"
:param message: Sets the message that will be displayed on the dialog.
"""
f.__doc__ = (f.__doc__ or '') + _doc
return f
def _include_choose_dir_file_docs(f):
"""
Combines the basic Chooser options (wildard, message) docsstring
with the wrapped function's doc string.
"""
_doc = """:param default_dir: The default directory selected when the dialog spawns
:param default_file: The default filename used in the dialog
"""
f.__doc__ = (f.__doc__ or '') + _doc
return f
def LayoutOptions(label_color=None,
label_bg_color=None,
help_color=None,
help_bg_color=None,
error_color=None,
error_bg_color=None,
show_label=True,
show_help=True,
visible=True,
full_width=False):
"""
Layout Options:
---------------
Color options can be passed either as a hex string ('#ff0000') or as
a collection of RGB values (e.g. `[255, 0, 0]` or `(255, 0, 0)`)
:param label_color: The foreground color of the label text
:param label_bg_color: The background color of the label text.
:param help_color: The foreground color of the help text.
:param help_bg_color: The background color of the help text.
:param error_color: The foreground color of the error text (when visible).
:param error_bg_color: The background color of the error text (when visible).
:param show_label: Toggles whether or not to display the label text
:param show_help: Toggles whether or not to display the help text
:param visible: Hides the entire widget when False. Note: the widget
is still present in the UI and will still send along any
default values that have been provided in code. This option
is here for when you want to hide certain advanced / dangerous
inputs from your GUI users.
:param full_width: 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.
"""
return _clean(locals())
@_include_layout_docs
def TextField(validator=None, **layout_options):
return _clean(locals())
@_include_layout_docs
def PasswordField(validator=None, **layout_options):
return _clean(locals())
@_include_layout_docs
def IntegerField(validator=None, min=0, max=100, increment=1, **layout_options):
"""
:param min: The minimum value allowed
:param max: The maximum value allowed
:param increment: The step size of the spinner
"""
return _clean(locals())
@_include_layout_docs
def Slider(validator=None, min=0, max=100, increment=1, **layout_options):
"""
:param min: The minimum value allowed
:param max: The maximum value allowed
:param increment: The step size of the slider
"""
return _clean(locals())
@_include_layout_docs
def DecimalField(validator=None,
min=0.0,
max=1.0,
increment=0.01,
precision=2,
**layout_options):
"""
:param min: The minimum value allowed
:param max: The maximum value allowed
:param increment: The step size of the spinner
:param precision: The precision of the decimal (0-20)
"""
return _clean(locals())
@_include_layout_docs
def TextArea(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
"""
return _clean(locals())
@_include_layout_docs
def RichTextConsole(**layout_options):
return _clean(locals())
@_include_layout_docs
def ListBox(height=None, **layout_options):
"""
:param height: The height of the ListBox
"""
return _clean(locals())
# TODO: what are this guy's layout options..?
def MutexGroup(initial_selection=None, title=None, **layout_options):
"""
:param initial_selection: The index of the option which should be initially selected.
:param title: Adds the supplied title above the RadioGroup options (when present)
"""
return _clean(locals())
@_include_layout_docs
def Dropdown(**layout_options):
return _clean(locals())
@_include_layout_docs
def Counter(**layout_options):
return _clean(locals())
@_include_layout_docs
def CheckBox(**layout_options):
return _clean(locals())
@_include_layout_docs
def BlockCheckBox(checkbox_label=None, **layout_options):
return _clean(locals())
@_include_layout_docs
def FilterableDropdown(placeholder=None,
empty_message=None,
max_size=80,
search_strategy=None,
**layout_options):
"""
:param placeholder: Text to display when the user has provided no input
:param empty_message: Text to display if the user's query doesn't match anything
:param max_size: maximum height of the dropdown
:param search_strategy: see: PrefixSearchStrategy
"""
return _clean(locals())
def PrefixSearchStrategy(
choice_tokenizer=PrefixTokenizers.WORDS,
input_tokenizer=PrefixTokenizers.REGEX('\s'),
ignore_case=True,
operator='AND',
index_suffix=False):
"""
:param choice_tokenizer: See: PrefixTokenizers - sets the tokenization strategy
for the `choices`
:param input_tokenizer: See: PrefixTokenizers sets how the users's `input` get tokenized.
:param ignore_case: Controls whether or not to honor case while searching
:param operator: see: `OperatorType` - controls whether or not individual
search tokens
get `AND`ed or `OR`d together when evaluating a match.
:param index_suffix: When enabled, generates a suffix-tree to enable efficient
partial-matching against any of the tokens.
"""
return {**_clean(locals()), 'type': 'PrefixFilter'}
@_include_layout_docs
@_include_choose_dir_file_docs
@_include_chooser_msg_wildcard_docs
def FileChooser(wildcard=None,
default_dir=None,
default_file=None,
message=None,
**layout_options):
return _clean(locals())
@_include_layout_docs
@_include_chooser_msg_wildcard_docs
def DirectoryChooser(wildcard=None,
default_path=None,
message=None,
**layout_options):
"""
:param default_path: The default path selected when the dialog spawns
"""
return _clean(locals())
@_include_layout_docs
@_include_choose_dir_file_docs
@_include_chooser_msg_wildcard_docs
def FileSaver(wildcard=None,
default_dir=None,
default_file=None,
message=None,
**layout_options):
return _clean(locals())
@_include_layout_docs
@_include_choose_dir_file_docs
@_include_chooser_msg_wildcard_docs
def MultiFileSaver(wildcard=None,
default_dir=None,
default_file=None,
message=None,
**layout_options):
return _clean(locals())
def ExpressionValidator(test=None, message=None):
"""
Creates the data for a basic expression validator.
Your test function can be made up of any valid Python expression.
It receives the variable user_input as an argument against which to
perform its validation. Note that all values coming from Gooey
are in the form of a string, so you'll have to cast as needed
in order to perform your validation.
"""
return {**_clean(locals()), 'type': 'ExpressionValidator'}
def RegexValidator(test=None, message=None):
"""
Creates the data for a basic RegexValidator.
:param test: the regex expression. This should be the expression
directly (i.e. `test='\d+'`). Gooey will test
that the user's input satisfies this expression.
:param message: The message to display if the input doesn't match
the regex
"""
return {**_clean(locals()), 'type': 'RegexValidator'}
def ArgumentGroup(show_border=False,
show_underline=True,
label_color=None,
columns=None,
margin_top=None):
"""
:param show_border: When True a labeled border will surround all widgets added to this group.
:param show_underline: Controls whether or not to display the underline when using the default border style
:param label_color: The foreground color for the group name
:param columns: Controls the number of widgets on each row
:param margin_top: specifies the top margin in pixels for this group
"""
return _clean(locals())
def _clean(options):
cleaned = {k: v for k, v in options.items()
if v is not None and k is not "layout_options"}
return {**options.get('layout_options', {}), **cleaned}

201
gooey/gui/components/options/validators.py

@ -0,0 +1,201 @@
import re
from functools import wraps
from gooey.gui.components.filtering.prefix_filter import OperatorType
class SuperBool(object):
"""
A boolean which keeps with it the rationale
for when it is false.
"""
def __init__(self, value, rationale):
self.value = value
self.rationale = rationale
def __bool__(self):
return self.value
__nonzero__ = __bool__
def __str__(self):
return str(self.value)
def lift(f):
"""
Lifts a basic predicate to the SuperBool type
stealing the docstring as the rationale message.
This is largely just goofing around and experimenting
since it's a private internal API.
"""
@wraps(f)
def inner(value):
result = f(value)
return SuperBool(result, f.__doc__) if not isinstance(result, SuperBool) else result
return inner
@lift
def is_tuple_or_list(value):
"""Must be either a list or tuple"""
return isinstance(value, list) or isinstance(value, tuple)
@lift
def is_str(value):
"""Must be of type `str`"""
return isinstance(value, str)
@lift
def is_str_or_coll(value):
"""
Colors must be either a hex string or collection of RGB values.
e.g.
Hex string: #fff0ce
RGB Collection: [0, 255, 128] or (0, 255, 128)
"""
return bool(is_str(value)) or bool(is_tuple_or_list(value))
@lift
def has_valid_channel_values(rgb_coll):
"""Colors in an RGB collection must all be in the range 0-255"""
return all([is_0to255(c) and is_int(c) for c in rgb_coll])
@lift
def is_three_channeled(value):
"""Missing channels! Colors in an RGB collection should be of the form [R,G,B] or (R,G,B)"""
return len(value) == 3
@lift
def is_hex_string(value: str):
"""Invalid hexadecimal format. Expected: "#FFFFFF" """
return isinstance(value, str) and bool(re.match('^#[\dABCDEF]{6}$', value, flags=2))
@lift
def is_bool(value):
"""Must be of type Boolean"""
return isinstance(value, bool)
@lift
def non_empty_string(value):
"""Must be a non-empty non-blank string"""
return value and bool(value.strip())
@lift
def is_tokenization_operator(value):
"""Operator must be a valid OperatorType i.e. one of: (AND, OR)"""
return value in (OperatorType.AND, OperatorType.OR)
@lift
def is_tokenizer(value):
"""Tokenizers must be valid Regular expressions. see: options.PrefixTokenizers"""
return bool(non_empty_string(value))
@lift
def is_int(value):
"""Invalid type. Expected `int`"""
return isinstance(value, int)
@lift
def is_0to255(value):
"""RGB values must be in the range 0 - 255 (inclusive)"""
return 0 <= value <= 255
def is_0to20(value):
"""Precision values must be in the range 0 - 20 (inclusive)"""
return 0 <= value <= 20
@lift
def is_valid_color(value):
"""Must be either a valid hex string or RGB list"""
if is_str(value):
return is_hex_string(value)
elif is_tuple_or_list(value):
return (is_tuple_or_list(value)
and is_three_channeled(value)
and has_valid_channel_values(value))
else:
return is_str_or_coll(value)
validators = {
'label_color': is_valid_color,
'label_bg_color': is_valid_color,
'help_color': is_valid_color,
'help_bg_color': is_valid_color,
'error_color': is_valid_color,
'error_bg_color': is_valid_color,
'show_label': is_bool,
'show_help': is_bool,
'visible': is_bool,
'full_width': is_bool,
'height': is_int,
'readonly': is_bool,
'initial_selection': is_int,
'title': non_empty_string,
'checkbox_label': non_empty_string,
'placeholder': non_empty_string,
'empty_message': non_empty_string,
'max_size': is_int,
'choice_tokenizer': is_tokenizer,
'input_tokenizer': is_tokenizer,
'ignore_case': is_bool,
'operator': is_tokenization_operator,
'index_suffix': is_bool,
'wildcard': non_empty_string,
'default_dir': non_empty_string,
'default_file': non_empty_string,
'default_path': non_empty_string,
'message': non_empty_string,
'precision': is_0to20
}
def collect_errors(predicates, m):
return {
k:predicates[k](v).rationale
for k,v in m.items()
if k in predicates and not predicates[k](v)}
def validate(pred, value):
result = pred(value)
if not result:
raise ValueError(result.rationale)
if __name__ == '__main__':
# TODO: there should be tests
pass
# print(validateColor((1, 'ergerg', 1234)))
# print(validateColor(1234))
# print(validateColor(123.234))
# print(validateColor('123.234'))
# print(validateColor('FFFAAA'))
# print(validateColor('#FFFAAA'))
# print(validateColor([]))
# print(validateColor(()))
# print(validateColor((1, 2)))
# print(validateColor((1, 2, 1234)))
# print(is_lifted(lift(is_int)))
# print(is_lifted(is_int))
# print(OR(is_poop, is_int)('poop'))
# print(AND(is_poop, is_lower, is_lower)('pooP'))
# print(OR(is_poop, is_int))
# print(is_lifted(OR(is_poop, is_int)))
# print(validate(is_valid_color, [255, 255, 256]))
# print(is_valid_color('#fff000'))
# print(is_valid_color([255, 244, 256]))
# print(non_empty_string('asdf') and non_empty_string('asdf'))
# validate(is_valid_color, 1234)

2
gooey/gui/components/widgets/__init__.py

@ -12,3 +12,5 @@ from .counter import Counter
from .radio_group import RadioGroup
from .choosers import *
from .dropdown_filterable import FilterableDropdown
from .numeric_fields import IntegerField, DecimalField
from .slider import Slider

21
gooey/gui/components/widgets/bases.py

@ -1,3 +1,4 @@
import re
from functools import reduce
import wx
@ -28,6 +29,9 @@ class BaseWidget(wx.Panel):
def setValue(self, value):
raise NotImplementedError
def setPlaceholder(self, value):
raise NotImplementedError
def receiveChange(self, *args, **kwargs):
raise NotImplementedError
@ -75,6 +79,10 @@ class TextContainer(BaseWidget):
# Checking for None instead of truthiness means False-evaluaded defaults can be used.
if self._meta['default'] is not None:
self.setValue(self._meta['default'])
if self._options.get('placeholder'):
self.setPlaceholder(self._options.get('placeholder'))
self.onComponentInitialized()
def onComponentInitialized(self):
@ -153,9 +161,13 @@ class TextContainer(BaseWidget):
def getValue(self):
regexFunc = lambda x: bool(re.match(userValidator, x))
userValidator = getin(self._options, ['validator', 'test'], 'True')
message = getin(self._options, ['validator', 'message'], '')
testFunc = eval('lambda user_input: bool(%s)' % userValidator)
testFunc = regexFunc \
if getin(self._options, ['validator', 'type'], None) == 'RegexValidator'\
else eval('lambda user_input: bool(%s)' % userValidator)
satisfies = testFunc if self._meta['required'] else ifPresent(testFunc)
value = self.getWidgetValue()
@ -173,6 +185,10 @@ class TextContainer(BaseWidget):
def setValue(self, value):
self.widget.SetValue(value)
def setPlaceholder(self, value):
if getattr(self.widget, 'SetHint', None):
self.widget.SetHint(value)
def setErrorString(self, message):
self.error.SetLabel(message)
self.error.Wrap(self.Size.width)
@ -203,6 +219,9 @@ class BaseChooser(TextContainer):
def setValue(self, value):
self.widget.setValue(value)
def setPlaceholder(self, value):
self.widget.SetHint(value)
def getWidgetValue(self):
return self.widget.getValue()

6
gooey/gui/components/widgets/choosers.py

@ -16,12 +16,10 @@ __ALL__ = [
class FileChooser(BaseChooser):
# todo: allow wildcard from argparse
widget_class = core.FileChooser
class MultiFileChooser(BaseChooser):
# todo: allow wildcard from argparse
widget_class = core.MultiFileChooser
def formatOutput(self, metatdata, value):
@ -29,17 +27,14 @@ class MultiFileChooser(BaseChooser):
class FileSaver(BaseChooser):
# todo: allow wildcard
widget_class = core.FileSaver
class DirChooser(BaseChooser):
# todo: allow wildcard
widget_class = core.DirChooser
class MultiDirChooser(BaseChooser):
# todo: allow wildcard
widget_class = core.MultiDirChooser
def formatOutput(self, metadata, value):
@ -47,7 +42,6 @@ class MultiDirChooser(BaseChooser):
class DateChooser(BaseChooser):
# todo: allow wildcard
widget_class = core.DateChooser

5
gooey/gui/components/widgets/core/chooser.py

@ -22,7 +22,7 @@ class Chooser(wx.Panel):
TODO: oh, young me. DRY != Good Abstraction
TODO: this is another weird inheritance hierarchy that's hard
to follow. Why do subclasses rather into, not their parent
to follow. Why do subclasses reach into, not their parent
class, but their _physical_ UI parent to grab the Gooey Options?
All this could be simplified to make the data flow
more apparent.
@ -73,6 +73,9 @@ class Chooser(wx.Panel):
def setValue(self, value):
self.widget.setValue(value)
def SetHint(self, value):
self.widget.SetHint(value)
def getValue(self):
return self.widget.getValue()

3
gooey/gui/components/widgets/core/text_input.py

@ -32,6 +32,9 @@ class TextInput(wx.Panel):
def getValue(self):
return self.widget.GetValue()
def SetHint(self, value):
self.widget.SetHint(value)
def SetDropTarget(self, target):
self.widget.SetDropTarget(target)

50
gooey/gui/components/widgets/numeric_fields.py

@ -0,0 +1,50 @@
import wx
from gooey.gui import formatters
from gooey.gui.components.widgets.bases import TextContainer
class IntegerField(TextContainer):
"""
An integer input field
"""
widget_class = wx.SpinCtrl
def getWidget(self, *args, **options):
widget = self.widget_class(self,
value='',
min=self._options.get('min', 0),
max=self._options.get('max', 100))
return widget
def getWidgetValue(self):
return self.widget.GetValue()
def setValue(self, value):
self.widget.SetValue(int(value))
def formatOutput(self, metatdata, value):
# casting to string so that the generic formatter
# doesn't treat 0 as false/None
return formatters.general(metatdata, str(value))
class DecimalField(IntegerField):
"""
A decimal input field
"""
widget_class = wx.SpinCtrlDouble
def getWidget(self, *args, **options):
widget = self.widget_class(self,
value='',
min=self._options.get('min', 0),
max=self._options.get('max', 100),
inc=self._options.get('increment', 0.01))
widget.SetDigits(self._options.get('precision', widget.GetDigits()))
return widget
def setValue(self, value):
self.widget.SetValue(value)

74
gooey/gui/components/widgets/options.py

@ -1,74 +0,0 @@
import pygtrie as trie
from fuzzywuzzy import process
class BasicDisplayOptions(object):
pass
countries = ["Abkhazia -> Abkhazia", "Afghanistan",
"Albania", "Algeria", "Andorra", "Angola", "Antigua and Barbuda", "Argentina",
"Armenia", "Artsakh -> Artsakh", "Australia", "Austria", "Azerbaijan", "Bahamas, The",
"Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin", "Bhutan",
"Bolivia", "Bosnia and Herzegovina", "Botswana", "Brazil", "Brunei", "Bulgaria",
"Burkina Faso[j]", "Burma -> Myanmar", "Burundi", "Cambodia", "Cameroon", "Canada[k]",
"Cape Verde", "Central African Republic", "Chad", "Chile", "China",
"China, Republic of -> Taiwan", "Colombia", "Comoros",
"Congo, Democratic Republic of the[p]", "Congo, Republic of the",
"Cook Islands -> Cook Islands", "Costa Rica", "Croatia", "Cuba", "Cyprus",
"Czech Republic[r]", "Democratic People's Republic of Korea -> Korea, North",
"Democratic Republic of the Congo -> Congo, Democratic Republic of the", "Denmark",
"Djibouti", "Dominica", "Dominican Republic", "East Timor", "Ecuador", "Egypt",
"El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Eswatini -> Swaziland",
"Ethiopia", "Fiji", "Finland", "France", "Gabon", "Gambia, The", "Georgia", "Germany",
"Ghana", "Greece", "Grenada", "Guatemala", "Guinea", "Guinea", "Guyana", "Haiti",
"Holy See -> Vatican City", "Honduras", "Hungary", "Iceland[v]", "India", "Indonesia",
"Iran", "Iraq", "Ireland", "Israel", "Italy", "Ivory Coast", "Jamaica", "Japan",
"Jordan", "Kazakhstan", "Kenya", "Kiribati", "Korea, North", "Korea, South",
"Kosovo -> Kosovo", "Kuwait", "Kyrgyzstan", "Laos", "Latvia", "Lebanon", "Lesotho",
"Liberia", "Libya", "Liechtenstein", "Lithuania", "Luxembourg", "Madagascar", "Malawi",
"Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Mauritania", "Mauritius",
"Mexico", "Micronesia", "Moldova", "Monaco", "Mongolia", "Montenegro", "Morocco",
"Mozambique", "Myanmar", "Nagorno", "Namibia", "Nauru", "Nepal", "Netherlands",
"New Zealand", "Nicaragua", "Niger", "Nigeria", "Niue -> Niue",
"Northern Cyprus -> Northern Cyprus", "North Korea -> Korea, North", "North Macedonia",
"Norway", "Oman", "Pakistan", "Palau", "Palestine", "Panama", "Papua New Guinea",
"Paraguay", "Peru", "Philippines", "Poland", "Portugal",
"Pridnestrovie -> Transnistria", "Qatar", "Republic of Korea -> Korea, South",
"Republic of the Congo -> Congo, Republic of the", "Romania", "Russia", "Rwanda",
"Sahrawi Arab Democratic Republic -> Sahrawi Arab Democratic Republic",
"Saint Kitts and Nevis", "Saint Lucia", "Saint Vincent and the Grenadines", "Samoa",
"San Marino", "São Tomé and Príncipe", "Saudi Arabia", "Senegal", "Serbia",
"Seychelles", "Sierra Leone", "Singapore", "Slovakia", "Slovenia", "Solomon Islands",
"Somalia", "Somaliland -> Somaliland", "South Africa", "South Korea -> Korea, South",
"South Ossetia -> South Ossetia", "South Sudan", "Spain", "Sri Lanka", "Sudan",
"Sudan, South -> South Sudan", "Suriname", "Swaziland", "Sweden", "Switzerland",
"Syria", "Taiwan (Republic of China) -> Taiwan", "Tajikistan", "Tanzania", "Thailand",
"The Bahamas -> Bahamas, The", "The Gambia -> Gambia, The", "Timor", "Togo", "Tonga",
"Transnistria -> Transnistria", "Trinidad and Tobago", "Tunisia", "Turkey",
"Turkmenistan", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates",
"United Kingdom", "United States", "Uruguay", "Uzbekistan", "Vanuatu", "Vatican City",
"Venezuela", "Vietnam", "Yemen", "Zambia", "Zimbabwe",
"↑ UN member states and observer states ↑", "", "↓ Other states ↓", "Abkhazia",
"Artsakh", "Cook Islands", "Kosovo", "Niue", "Northern Cyprus",
"Sahrawi Arab Democratic Republic", "Somaliland", "South Ossetia", "Taiwan",
"Transnistria", "↑ Other states ↑"]
us_states = ["Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado",
"Connecticut", "Delaware", "Florida", "Georgia", "Hawaii", "Idaho", "Illinois",
"Indiana", "Iowa", "Kansas", "Kentucky[E]", "Louisiana", "Maine", "Maryland",
"Massachusetts", "Michigan", "Minnesota", "Mississippi", "Missouri", "Montana",
"Nebraska", "Nevada", "New Hampshire", "New Jersey", "New Mexico", "New York",
"North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon", "Pennsylvania",
"Rhode Island", "South Carolina", "South Dakota", "Tennessee", "Texas", "Utah",
"Vermont", "Virginia", "Washington", "West Virginia", "Wisconsin", "Wyoming"]
x = trie.Trie()
output = {}
for country in countries:
for i in country.split():
if not x.has_key(i):
x[i] = []
x[i].append(country)
a = 10

28
gooey/gui/components/widgets/slider.py

@ -0,0 +1,28 @@
import wx
from gooey.gui import formatters
from gooey.gui.components.widgets.bases import TextContainer
class Slider(TextContainer):
"""
An integer input field
"""
widget_class = wx.Slider
def getWidget(self, *args, **options):
widget = self.widget_class(self,
minValue=self._options.get('min', 0),
maxValue=self._options.get('max', 100),
style=wx.SL_MIN_MAX_LABELS | wx.SL_VALUE_LABEL)
return widget
def getWidgetValue(self):
return self.widget.GetValue()
def setValue(self, value):
self.widget.SetValue(value)
def formatOutput(self, metatdata, value):
return formatters.general(metatdata, str(value))

8
gooey/gui/components/widgets/textfield.py

@ -1,9 +1,6 @@
import wx
from gooey.gui import formatters
from gooey.gui.components.widgets.bases import TextContainer
from gooey.gui import formatters, events
from gooey.gui.components.widgets.core.text_input import TextInput
from gooey.gui.pubsub import pub
from gooey.util.functional import getin
class TextField(TextContainer):
@ -15,6 +12,9 @@ class TextField(TextContainer):
def setValue(self, value):
self.widget.setValue(str(value))
def setPlaceholder(self, value):
self.widget.SetHint(value)
def formatOutput(self, metatdata, value):
return formatters.general(metatdata, value)

36
gooey/python_bindings/argparse_to_json.py

@ -19,7 +19,8 @@ from uuid import uuid4
from gooey.python_bindings.gooey_parser import GooeyParser
from gooey.util.functional import merge, getin, identity, assoc
from gooey.gui.components.options.validators import validators
from gooey.gui.components.options.validators import collect_errors
VALID_WIDGETS = (
'FileChooser',
@ -38,7 +39,10 @@ VALID_WIDGETS = (
'Textarea',
'PasswordField',
'Listbox',
'FilterableDropdown'
'FilterableDropdown',
'IntegerField',
'DecimalField',
'Slider'
)
@ -419,6 +423,7 @@ def action_to_json(action, widget, options):
base = merge(item_default, {
'validator': {
'type': 'ExpressionValidator',
'test': validator,
'message': error_msg
},
@ -428,6 +433,9 @@ def action_to_json(action, widget, options):
if default == argparse.SUPPRESS:
default = None
final_options = merge(base, options.get(action.dest) or {})
validate_gooey_options(action, widget, final_options)
return {
'id': action.option_strings[0] if action.option_strings else action.dest,
'type': widget,
@ -443,11 +451,33 @@ def action_to_json(action, widget, options):
'default': default,
'dest': action.dest,
},
'options': merge(base, options.get(action.dest) or {})
'options': final_options
}
def validate_gooey_options(action, widget, options):
"""Very basic field validation / sanity checking for
the time being.
Future plans are to assert against the options and actions together
to facilitate checking that certain options like `initial_selection` in
RadioGroups map to a value which actually exists (rather than exploding
at runtime with an unhelpful error)
Additional problems with the current approach is that no feedback is given
as to WHERE the issue took place (in terms of stacktrace). Which means we should
probably explode in GooeyParser proper rather than trying to collect all the errors here.
It's not super ideal in that the user will need to run multiple times to
see all the issues, but, ultimately probably less annoying that trying to
debug which `gooey_option` key had an issue in a large program.
That said "better is the enemy of done." This is good enough for now. It'll be
a TODO: better validation
"""
errors = collect_errors(validators, options)
if errors:
from pprint import pformat
raise ValueError(str(action.dest) + str(pformat(errors)))
def choose_cli_type(action):

89
gooey/tests/test_numeric_inputs.py

@ -0,0 +1,89 @@
import unittest
from random import randint
from unittest.mock import patch
from tests.harness import instrumentGooey
from gooey import GooeyParser
from gooey.tests import *
class TestNumbericInputs(unittest.TestCase):
def makeParser(self, **kwargs):
parser = GooeyParser(description='description')
parser.add_argument('--input', **kwargs)
return parser
def testDefault(self):
cases = [
[{'widget': 'IntegerField'}, 0],
[{'default': 0, 'widget': 'IntegerField'}, 0],
[{'default': 10, 'widget': 'IntegerField'}, 10],
[{'default': 76, 'widget': 'IntegerField'}, 76],
# note that WX caps the value
# unless explicitly widened via gooey_options
[{'default': 81234, 'widget': 'IntegerField'}, 100],
# here we set the max to something higher than
# the default and all works as expected.
# this is a TODO for validation
[{'default': 81234, 'widget': 'IntegerField', 'gooey_options': {'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],
]
for inputs, expected in cases:
with self.subTest(inputs):
parser = self.makeParser(**inputs)
with instrumentGooey(parser) as (app, gooeyApp):
input = gooeyApp.configs[0].reifiedWidgets[0]
self.assertEqual(input.getValue()['rawValue'], expected)
def testGooeyOptions(self):
cases = [
{'widget': 'DecimalField', 'gooey_options': {'min': -100, 'max': 1234, 'increment': 1.240}},
{'widget': 'DecimalField', 'gooey_options': {'min': 1234, 'max': 3456, 'increment': 2.2}},
{'widget': 'IntegerField', 'gooey_options': {'min': -100, 'max': 1234}},
{'widget': 'IntegerField', 'gooey_options': {'min': 1234, 'max': 3456}}
];
using = {
'min': lambda widget: widget.GetMin(),
'max': lambda widget: widget.GetMax(),
'increment': lambda widget: widget.GetIncrement(),
}
for case in cases:
with self.subTest(case):
parser = self.makeParser(**case)
with instrumentGooey(parser) as (app, gooeyApp):
wxWidget = gooeyApp.configs[0].reifiedWidgets[0].widget
for option, value in case['gooey_options'].items():
self.assertEqual(using[option](wxWidget), value)
def testZerosAreReturned(self):
"""
Originally the formatter was dropping '0' due to
it being interpreted as falsey
"""
parser = self.makeParser(widget='IntegerField')
with instrumentGooey(parser) as (app, gooeyApp):
field = gooeyApp.configs[0].reifiedWidgets[0]
result = field.getValue()
self.assertEqual(result['rawValue'], 0)
self.assertIsNotNone(result['cmd'])
def testNoLossOfPrecision(self):
parser = self.makeParser(widget='DecimalField', default=12.23534, gooey_options={'precision': 20})
with instrumentGooey(parser) as (app, gooeyApp):
field = gooeyApp.configs[0].reifiedWidgets[0]
result = field.getValue()
self.assertEqual(result['rawValue'], 12.23534)
self.assertIsNotNone(result['cmd'])
if __name__ == '__main__':
unittest.main()

54
gooey/tests/test_options.py

@ -0,0 +1,54 @@
import unittest
from gooey.gui.components.options import options
class TestPrefixFilter(unittest.TestCase):
def test_doc_schenanigans(self):
"""Sanity check that my docstring wrappers all behave as expected"""
@options._include_layout_docs
def no_self_docstring():
pass
@options._include_layout_docs
def yes_self_docstring():
"""sup"""
pass
# gets attached to functions even if they don't have a docstring
self.assertIn(options.LayoutOptions.__doc__, no_self_docstring.__doc__)
# gets attached to the *end* of existing doc strings
self.assertTrue(yes_self_docstring.__doc__.startswith('sup'))
self.assertIn(options.LayoutOptions.__doc__, yes_self_docstring.__doc__)
def test_clean_method(self):
"""
_clean should drop any keys with None values
and flatten the layout_option kwargs to the root level
"""
result = options._clean({'a': None, 'b': 123, 'c': 0})
self.assertEqual(result, {'b': 123, 'c': 0})
result = options._clean({'root_level': 123, 'layout_options': {
'nested': 'hello',
'another': 1234
}})
self.assertEqual(result, {'root_level': 123, 'nested': 'hello', 'another': 1234})
def test_only_provided_arguments_included(self):
"""
More sanity checking that the internal use of locals()
does the Right Thing
"""
option = options.LayoutOptions(label_color='#ffffff')
self.assertIn('label_color', option)
option = options.LayoutOptions()
self.assertNotIn('label_color', option)
option = options.TextField(label_color='#ffffff')
self.assertIn('label_color', option)
option = options.TextField()
self.assertNotIn('label_color', option)

32
gooey/tests/test_password.py

@ -0,0 +1,32 @@
import unittest
from tests.harness import instrumentGooey
from gooey import GooeyParser
from gooey.tests import *
class TestPasswordField(unittest.TestCase):
def makeParser(self, **kwargs):
parser = GooeyParser(description='description')
parser.add_argument('--widget', widget="PasswordField", **kwargs)
return parser
def testPlaceholder(self):
cases = [
[{}, ''],
[{'placeholder': 'Hello'}, 'Hello']
]
for options, expected in cases:
parser = self.makeParser(gooey_options=options)
with instrumentGooey(parser) as (app, gooeyApp):
# because of how poorly designed the Gooey widgets are
# we have to reach down 3 levels in order to find the
# actual WX object we need to test.
widget = gooeyApp.configs[0].reifiedWidgets[0].widget
self.assertEqual(widget.widget.GetHint(), expected)
if __name__ == '__main__':
unittest.main()

51
gooey/tests/test_slider.py

@ -0,0 +1,51 @@
import unittest
from unittest.mock import patch
from tests.harness import instrumentGooey
from gooey import GooeyParser
from gooey.tests import *
class TestGooeySlider(unittest.TestCase):
def makeParser(self, **kwargs):
parser = GooeyParser(description='description')
parser.add_argument('--slider', widget="Slider", **kwargs)
return parser
def testSliderDefault(self):
cases = [
[{}, 0],
[{'default': 0}, 0],
[{'default': 10}, 10],
[{'default': 76}, 76],
# note that WX caps the value
# unless explicitly widened via gooey_options
[{'default': 81234}, 100],
# here we set the max to something higher than
# the default and all works as expected.
# this is a TODO for validation
[{'default': 81234, 'gooey_options': {'max': 99999}}, 81234],
]
for inputs, expected in cases:
with self.subTest(inputs):
parser = self.makeParser(**inputs)
with instrumentGooey(parser) as (app, gooeyApp):
slider = gooeyApp.configs[0].reifiedWidgets[0]
self.assertEqual(slider.getValue()['rawValue'], expected)
def testZerosAreReturned(self):
"""
Originally the formatter was dropping '0' due to
it being interpreted as falsey
"""
parser = self.makeParser()
with instrumentGooey(parser) as (app, gooeyApp):
field = gooeyApp.configs[0].reifiedWidgets[0]
result = field.getValue()
self.assertEqual(result['rawValue'], 0)
self.assertIsNotNone(result['cmd'])
if __name__ == '__main__':
unittest.main()

32
gooey/tests/test_textarea.py

@ -0,0 +1,32 @@
import unittest
from tests.harness import instrumentGooey
from gooey import GooeyParser
from gooey.tests import *
class TestTextarea(unittest.TestCase):
def makeParser(self, **kwargs):
parser = GooeyParser(description='description')
parser.add_argument('--widget', widget="Textarea", **kwargs)
return parser
def testPlaceholder(self):
cases = [
[{}, ''],
[{'placeholder': 'Hello'}, 'Hello']
]
for options, expected in cases:
parser = self.makeParser(gooey_options=options)
with instrumentGooey(parser) as (app, gooeyApp):
# because of how poorly designed the Gooey widgets are
# we have to reach down 3 levels in order to find the
# actual WX object we need to test.
widget = gooeyApp.configs[0].reifiedWidgets[0]
self.assertEqual(widget.widget.GetHint(), expected)
if __name__ == '__main__':
unittest.main()

32
gooey/tests/test_textfield.py

@ -0,0 +1,32 @@
import unittest
from tests.harness import instrumentGooey
from gooey import GooeyParser
from gooey.tests import *
class TestTextField(unittest.TestCase):
def makeParser(self, **kwargs):
parser = GooeyParser(description='description')
parser.add_argument('--widget', widget="TextField", **kwargs)
return parser
def testPlaceholder(self):
cases = [
[{}, ''],
[{'placeholder': 'Hello'}, 'Hello']
]
for options, expected in cases:
parser = self.makeParser(gooey_options=options)
with instrumentGooey(parser) as (app, gooeyApp):
# because of how poorly designed the Gooey widgets are
# we have to reach down 3 levels in order to find the
# actual WX object we need to test.
widget = gooeyApp.configs[0].reifiedWidgets[0].widget
self.assertEqual(widget.widget.GetHint(), expected)
if __name__ == '__main__':
unittest.main()

5
gooey/util/functional.py

@ -72,7 +72,10 @@ def zipmap(keys, vals):
def compact(coll):
"""Returns a new list with all falsy values removed"""
return list(filter(None, coll))
if isinstance(coll, dict):
return {k:v for k,v in coll.items() if v is not None}
else:
return list(filter(None, coll))
def ifPresent(f):

Loading…
Cancel
Save