mirror of https://github.com/chriskiehl/Gooey.git
Chris
4 years ago
23 changed files with 1149 additions and 87 deletions
Split View
Diff Options
-
49docs/Filterable-Dropdown-Guide.md
-
99docs/releases/1.0.7-release-notes.md
-
1gooey/__init__.py
-
16gooey/gui/components/filtering/prefix_filter.py
-
2gooey/gui/components/footer.py
-
0gooey/gui/components/options/__init__.py
-
42gooey/gui/components/options/layout.py
-
291gooey/gui/components/options/options.py
-
127gooey/gui/components/options/validation.py
-
199gooey/gui/components/options/validators.py
-
2gooey/gui/components/widgets/__init__.py
-
21gooey/gui/components/widgets/bases.py
-
5gooey/gui/components/widgets/core/chooser.py
-
3gooey/gui/components/widgets/core/text_input.py
-
50gooey/gui/components/widgets/numeric_fields.py
-
74gooey/gui/components/widgets/options.py
-
28gooey/gui/components/widgets/slider.py
-
6gooey/gui/components/widgets/textfield.py
-
33gooey/python_bindings/argparse_to_json.py
-
89gooey/tests/test_numeric_inputs.py
-
54gooey/tests/test_options.py
-
40gooey/tests/test_slider.py
-
5gooey/util/functional.py
@ -0,0 +1,49 @@ |
|||
# Customizing FilterableDropdown's search behavior |
|||
|
|||
|
|||
Out of the box, FilterableDropdown does a very simple 'startswith' style lookup to find candidates which match the user's input. However, this behavior can be customized using GooeyOptions to support all kinds of filtering strategies. |
|||
|
|||
For each example, we'll be starting with the following sample program. This uses just 4 choices to keep the different options easy to follow. However, `FilterableDropdown` is fully virtualized and can be used with 10s of thousands of choices. |
|||
|
|||
|
|||
```python |
|||
from gooey import Gooey, GooeyParser, PrefixTokenizers |
|||
|
|||
choices = [ |
|||
'Afghanistan Kabul', |
|||
'Albania Tirana', |
|||
'Japan Kyoto', |
|||
'Japan Tokyo' |
|||
] |
|||
|
|||
@Gooey(program_name='FilterableDropdown Demo', poll_external_updates=True) |
|||
def main(): |
|||
parser = GooeyParser(description="Example of the Filterable Dropdown") |
|||
parser.add_argument( |
|||
"-a", |
|||
"--myargument", |
|||
metavar='Country', |
|||
help='Search for a country', |
|||
choices=choices, |
|||
widget='FilterableDropdown', |
|||
gooey_options={ |
|||
'label_color': (255, 100, 100), |
|||
'placeholder': 'Start typing to view suggestions' |
|||
}) |
|||
args = parser.parse_args() |
|||
print(args) |
|||
``` |
|||
|
|||
|
|||
|
|||
|
|||
|
|||
|
|||
## Combining results |
|||
|
|||
## Suffix Trees |
|||
|
|||
|
|||
|
|||
|
|||
|
@ -0,0 +1,99 @@ |
|||
## Gooey 1.0.6 Released! |
|||
|
|||
|
|||
|
|||
Quality of Life improvements for Gooey Options. |
|||
|
|||
Goal is to enable more IDE auto-completion help as well as more REPL driven usefulness via help() and docstrings. |
|||
|
|||
Gooey now exposes a top-level set of public gooey_option data constructors. |
|||
|
|||
```python |
|||
from gooey import options |
|||
|
|||
parser.add_argument( |
|||
'--foo', |
|||
help='Some foo thing', |
|||
widget='FilterableDropdown', |
|||
gooey_options=options.FilterableDropdown( |
|||
placeholder='Search for a Foo', |
|||
search_strategy=options.PrefixSearchStrategy( |
|||
ignore_case=True |
|||
) |
|||
)) |
|||
``` |
|||
|
|||
Note that these are _just_ helpers for generating the right data shapes. They're still generating plain data behind the scenes and thus all existing gooey_options code remains 100% compatible. |
|||
|
|||
**Better Docs:** |
|||
|
|||
Which is to say, documentation which actually exists rather than _not_ exist. You can inspect the docs live in the REPL or by hopping to the symbol in editors which support such things. |
|||
|
|||
``` |
|||
>>> from gooey import options |
|||
>>> help(options.RadioGroup) |
|||
Help on function FileChooser in module __main__: |
|||
|
|||
FileChooser(wildcard=None, default_dir=None, default_file=None, message=None, **layout_options) |
|||
: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. |
|||
:param default_dir: The default directory selected when the dialog spawns |
|||
:param default_file: The default filename used in the dialog |
|||
|
|||
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. |
|||
``` |
|||
|
|||
|
|||
Previously, Gooey Options have been an opaque map. While great of openness / extenisibility, it's pretty terrible from a "what does this actually take..?" perspective. |
|||
|
|||
|
|||
Ideally, and eventually, we'll be able to completely type these options to increase visibility / usability even more. However, for backwards compatibility reasons, Gooey will continue to be sans types for the time being. |
|||
|
|||
|
|||
|
|||
### Gooey Options: placeholder |
|||
|
|||
Widgets with text inputs now all accept a `placeholder` Gooey option. |
|||
|
|||
```python |
|||
add_argument('--foo', widget='TextField', gooey_options=options.TextField( |
|||
placeholder='Type some text here!' |
|||
) |
|||
``` |
|||
|
|||
### New Widget: IntegerField |
|||
|
|||
### New Widget: DecimalField |
|||
|
|||
### New Widget: Slider |
|||
|
|||
|
|||
### New Validator option: RegexValidator |
|||
|
|||
|
|||
|
|||
|
|||
|
@ -0,0 +1,42 @@ |
|||
from pyrsistent import pmap |
|||
|
|||
from gooey.gui.components.options.validation import validate_color, _unit |
|||
|
|||
layout_validators = { |
|||
'label_color': validate_color, |
|||
'label_bg_color': validate_color, |
|||
'help_color': validate_color, |
|||
'help_bg_color': validate_color, |
|||
'error_color': validate_color, |
|||
'error_bg_color': validate_color, |
|||
# 'show_label': validate_bool, |
|||
# 'show_help': validate_bool, |
|||
# 'visible': validate_bool, |
|||
# 'full_width': validate_bool |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
def layout_options(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): |
|||
|
|||
options = pmap({k:v for k,v in locals().items() if v is not None}) |
|||
failures = {} |
|||
for k,v in options.items(): |
|||
validator = layout_validators.get(k, _unit) |
|||
err, success = validator(v) |
|||
if err: |
|||
failures[k] = err |
|||
|
|||
if failures: |
|||
raise ValueError(failures) |
|||
return options |
@ -0,0 +1,291 @@ |
|||
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 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} |
|||
|
@ -0,0 +1,127 @@ |
|||
from textwrap import dedent |
|||
|
|||
from pyrsistent import m, pmap, v |
|||
import re |
|||
|
|||
|
|||
def validate_and_raise(validators, some_map): |
|||
err, success = validate(validators, some_map) |
|||
if err: |
|||
raise ValueError(err) |
|||
return success |
|||
|
|||
def validate(validators, some_map): |
|||
failures = {} |
|||
for k, v in some_map.items(): |
|||
validator = validators.get(k, _unit) |
|||
err, success = validator(v) |
|||
if err: |
|||
failures[k] = err |
|||
|
|||
if failures: |
|||
return [[failures], None] |
|||
return [None, some_map] |
|||
|
|||
def _unit(value): |
|||
return [None, value] |
|||
|
|||
def is_tuple_or_list(value): |
|||
return isinstance(value, list) or isinstance(value, tuple) |
|||
|
|||
def is_str(value): |
|||
return isinstance(value, str) |
|||
|
|||
|
|||
def validate_str_or_coll(value): |
|||
error_msg = dedent(''' |
|||
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 ([None, value] |
|||
if is_str(value) or is_tuple_or_list(value) |
|||
else [[error_msg], None]) |
|||
|
|||
def validate_color(value): |
|||
def _validate(value): |
|||
if isinstance(value, str): |
|||
return isfoo(value) |
|||
else: |
|||
return validate_rgb(value) |
|||
return _flatmap(_validate, validate_str_or_coll(value)) |
|||
|
|||
def validate_rgb_vals(rgb_coll): |
|||
failures = [] |
|||
for val, channel in zip(rgb_coll, 'RGB'): |
|||
err, success = is_uint8(val) |
|||
if err: |
|||
failures += ['{} value: {}'.format(channel, msg) for msg in err] |
|||
return [failures, None] if failures else [None, rgb_coll] |
|||
|
|||
def is_uint8(value): |
|||
return _flatmap(is0to255, isInt(value)) |
|||
|
|||
def validate_rgb(value): |
|||
return _flatmap(validate_rgb_vals, three_channels(value)) |
|||
|
|||
|
|||
def three_channels(value): |
|||
return ([None, value] |
|||
if len(value) == 3 |
|||
else [['Colors in an RGB collection should be of the form [R,G,B] or (R,G,B)'], None]) |
|||
|
|||
def isfoo(value: str): |
|||
return ([None, value] |
|||
if re.match('^#[\dABCDEF]{6}$', value, flags=2) |
|||
else [['Invalid hexadecimal format. Expected: "#FFFFFF"'], None]) |
|||
|
|||
def is0to255(value: int): |
|||
return ([None, value] |
|||
if 0 <= value <= 255 |
|||
else [['Colors myst be 0-255'], None]) |
|||
|
|||
def isInt(value): |
|||
return ([None, value] |
|||
if isinstance(value, int) |
|||
else [['Invalid RGB value. Expected type int'], None]) |
|||
|
|||
|
|||
def _or(f, g): |
|||
def inner(value): |
|||
err1, val1 = f(value) |
|||
err2, val2 = g(value) |
|||
if val1 and val2: |
|||
return [None, val1] |
|||
elif err1 and err2: |
|||
return [err1 + err2, None] |
|||
elif err1: |
|||
return [err1, None] |
|||
else: |
|||
return err2 |
|||
return inner |
|||
|
|||
|
|||
def _and(f, g): |
|||
def inner(value): |
|||
return _flatmap(f, g(value)) |
|||
return inner |
|||
|
|||
def _flatmap(f, v): |
|||
err, value = v |
|||
if err: |
|||
return v |
|||
else: |
|||
return f(value) |
|||
|
|||
if __name__ == '__main__': |
|||
print(validate_color((1,'ergerg',1234))) |
|||
print(validate_color(1234)) |
|||
print(validate_color(123.234)) |
|||
print(validate_color('123.234')) |
|||
print(validate_color('FFFAAA')) |
|||
print(validate_color('#FFFAAA')) |
|||
print(validate_color([])) |
|||
print(validate_color(())) |
|||
print(validate_color((1,2))) |
|||
print(validate_color((1, 2, 1234))) |
@ -0,0 +1,199 @@ |
|||
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__': |
|||
# 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) |
|||
|
|||
|
@ -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) |
|||
|
|||
|
@ -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 |
@ -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, value) |
|||
|
|||
|
@ -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() |
@ -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) |
@ -0,0 +1,40 @@ |
|||
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) |
|||
|
|||
|
|||
|
|||
if __name__ == '__main__': |
|||
unittest.main() |
Write
Preview
Loading…
Cancel
Save