def handle_option_merge(group_defaults, incoming_options, title): """ Merges a set of group defaults with incoming options. A bunch of ceremony here is to ensure backwards compatibility with the old num_required_cols and num_optional_cols decorator args. They are used as the seed values for the new group defaults which keeps the old behavior _mostly_ in tact. Known failure points: * Using custom groups / names. No 'positional arguments' group means no required_cols arg being honored * Non-positional args marked as required. It would take group shuffling along the lines of that required to make mutually exclusive groups show in the correct place. In short, not worth the complexity for a legacy feature that's been succeeded by a much more powerful alternative. """ if title == 'positional arguments': # the argparse default 'required' bucket req_cols = getin(group_defaults, ['legacy', 'required_cols'], 2) new_defaults = assoc(group_defaults, 'columns', req_cols) return merge(new_defaults, incoming_options) else: opt_cols = getin(group_defaults, ['legacy', 'optional_cols'], 2) new_defaults = assoc(group_defaults, 'columns', opt_cols) return merge(new_defaults, incoming_options)
def action_to_json(action, widget, options): dropdown_types = {'Listbox', 'Dropdown', 'Counter'} if action.required: # Text fields get a default check that user input is present # and not just spaces, dropdown types get a simplified # is-it-present style check validator = ('user_input and not user_input.isspace()' if widget not in dropdown_types else 'user_input') error_msg = 'This field is required' else: # not required; do nothing; validator = 'True' error_msg = '' base = merge( item_default, { 'validator': { 'type': 'ExpressionValidator', 'test': validator, 'message': error_msg }, }) 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) return { 'id': action.option_strings[0] if action.option_strings else action.dest, 'type': widget, 'cli_type': choose_cli_type(action), 'required': action.required, 'data': { 'display_name': action.metavar or action.dest, 'help': action.help, 'required': action.required, 'nargs': action.nargs or '', 'commands': action.option_strings, 'choices': list(map(str, action.choices)) if action.choices else [], 'default': default, 'dest': action.dest, }, 'options': final_options }
def collect_errors(parser, error_registry: Dict[str, Exception], args: Dict[str, Try]) -> Dict[str, str]: """ Merges all the errors from the Args mapping and error registry into a final dict. """ # As is a theme throughout this module, to avoid Argparse # short-circuiting during parse-time, we pass a placeholder string # for required positional arguments which haven't yet been provided # by the user. So, what's happening here is that we're now collecting # all the args which have the placeholders so that we can flag them # all as required and missing. # Again, to be hyper clear, this is about being able to collect ALL # errors, versus just ONE error (Argparse default). required_but_missing = { k: 'This field is required' for k, v in args.items() if isinstance(v, Success) and v.value == VALUE_PLACEHOLDER } mutexes_required_but_missing = collect_mutex_errors(parser, args) errors = { k: str(v.error) for k, v in args.items() if v is not None and isinstance(v, Failure) } # Secondary errors are those which get frustratingly applied by # Argparse in a way which can't be easily tracked with patching # or higher order functions. See: `check_value` for more details. secondary = {k: str(e) for k, e in error_registry.items() if e} return merge(required_but_missing, errors, secondary, mutexes_required_but_missing)
def validateForm(self) -> Try[Mapping[str, str]]: # or Exception config = self.navbar.getActiveConfig() localErrors: Mapping[str, str] = config.getErrors() dynamicResult: Try[Mapping[str, str]] = self.fetchDynamicValidations() combineErrors = lambda m: merge(localErrors, m) return dynamicResult.map(combineErrors)
def build_app(build_spec): app = wx.App(False) i18n.load(build_spec['language_dir'], build_spec['language'], build_spec['encoding']) imagesPaths = image_repository.loadImages(build_spec['image_dir']) gapp = GooeyApplication(merge(build_spec, imagesPaths)) gapp.Show() return (app, gapp)
def getValue(self): for button, widget in zip(self.radioButtons, self.widgets): if button.GetValue(): # is Checked return merge(widget.getValue(), {'id': self._id}) else: # just return the first widget's value even though it's # not active so that the expected interface is satisfied return self.widgets[0].getValue()
def getValue(self): for button, widget in zip(self.radioButtons, self.widgets): if button.GetValue(): # is Checked return merge(widget.getValue(), {'id': self._id}) else: # just return the first widget's value even though it's # not active so that the expected interface is satisfied return self.widgets[0].getValue()
def build_app(build_spec): app = wx.App(False) i18n.load(build_spec['language_dir'], build_spec['language'], build_spec['encoding']) imagesPaths = image_repository.loadImages(build_spec['image_dir']) gapp = GooeyApplication(merge(build_spec, imagesPaths)) # wx.lib.inspection.InspectionTool().Show() gapp.Show() return app
def categorize2(groups, widget_dict, options): defaults = {'label_color': '#000000', 'description_color': '#363636'} return [{ 'name': group['name'], 'items': list(categorize(group['items'], widget_dict, options)), 'groups': categorize2(group['groups'], widget_dict, options), 'description': group['description'], 'options': merge(defaults, group['options']) } for group in groups]
def action_to_json(action, widget, options): dropdown_types = {'Listbox', 'Dropdown', 'Counter'} if action.required: # Text fields get a default check that user input is present # and not just spaces, dropdown types get a simplified # is-it-present style check validator = ('user_input and not user_input.isspace()' if widget not in dropdown_types else 'user_input') error_msg = 'This field is required' else: # not required; do nothing; validator = 'True' error_msg = '' base = merge(item_default, { 'validator': { 'test': validator, 'message': error_msg }, }) default = coerce_default(action.default, widget) return { 'id': action.option_strings[0] if action.option_strings else action.dest, 'type': widget, 'cli_type': choose_cli_type(action), 'required': action.required, 'data': { 'display_name': action.metavar or action.dest, 'help': action.help, 'required': action.required, 'nargs': action.nargs or '', 'commands': action.option_strings, 'choices': list(map(str, action.choices)) if action.choices else [], 'default': default, 'dest': action.dest, }, 'options': merge(base, options.get(action.dest) or {}) }
def action_to_json(action, widget, options): if action.required: # check that it's present and not just spaces validator = 'user_input and not user_input.isspace()' error_msg = 'This field is required' else: # not required; do nothing; validator = 'True' error_msg = '' base = merge(item_default, { 'validator': { 'test': validator, 'message': error_msg }, }) return { 'id': action.option_strings[0] if action.option_strings else action.dest, 'type': widget, 'cli_type': choose_cli_type(action), 'required': action.required, 'data': { 'display_name': action.metavar or action.dest, 'help': action.help, 'required': action.required, 'nargs': action.nargs or '', 'commands': action.option_strings, 'choices': action.choices or [], 'default': clean_default(action.default), 'dest': action.dest, }, 'options': merge(base, options.get(action.dest) or {}) }
def _build_app(build_spec, app): """ Note: this method is broken out with app as an argument to facilitate testing. """ # use actual program name instead of script file name in macOS menu app.SetAppDisplayName(build_spec['program_name']) i18n.load(build_spec['language_dir'], build_spec['language'], build_spec['encoding']) imagesPaths = image_repository.loadImages(build_spec['image_dir']) gapp = GooeyApplication(merge(build_spec, imagesPaths)) # wx.lib.inspection.InspectionTool().Show() gapp.Show() return (app, gapp)
def build_radio_group(mutex_group, widget_group, options): return { 'id': str(uuid4()), 'type': 'RadioGroup', 'cli_type': 'optional', 'group_name': 'Choose Option', 'required': mutex_group.required, 'options': merge(item_default, getattr(mutex_group, 'gooey_options', {})), 'data': { 'commands': [action.option_strings for action in mutex_group._group_actions], 'widgets': list(categorize(mutex_group._group_actions, widget_group, options)) } }
def _build_app(build_spec, app) -> Tuple[Any, wx.Frame]: """ Note: this method is broken out with app as an argument to facilitate testing. """ # use actual program name instead of script file name in macOS menu app.SetAppDisplayName(build_spec['program_name']) i18n.load(build_spec['language_dir'], build_spec['language'], build_spec['encoding']) imagesPaths = image_repository.loadImages(build_spec['image_dir']) gapp2 = render(create_element(RGooey, merge(build_spec, imagesPaths)), None) # wx.lib.inspection.InspectionTool().Show() # gapp.Show() gapp2.Show() return (app, gapp2)
def extract_groups(action_group): ''' Recursively extract argument groups and associated actions from ParserGroup objects ''' return { 'name': action_group.title, 'description': action_group.description, 'items': [ action for action in action_group._group_actions if not is_help_message(action) ], 'groups': [extract_groups(group) for group in action_group._action_groups], 'options': merge(group_defaults, getattr(action_group, 'gooey_options', {})) }
def run_integration(module, assertionFunction, **kwargs): """ Integration test harness. WXPython is *super* finicky when it comes to integration tests. It needs the main Python thread for its app loop, which means we have to integration test on a separate thread. The causes further strangeness in how Unittest and WXPython interact. In short, each test must be in its own module and thus import its own wx instance, and be run in its own "space." So long as the above is satisfied, then integration tests can run reliably. """ from gooey.gui import application options = merge( { 'image_dir': '::gooey/default', 'language_dir': getResourcePath('languages'), 'show_success_modal': False }, kwargs) module_path = os.path.abspath(module.__file__) parser = module.get_parser() build_spec = config_generator.create_from_parser(parser, module_path, **options) time.sleep(2) app = application.build_app(build_spec=build_spec) executor = futures.ThreadPoolExecutor(max_workers=1) # executor runs in parallel and will submit a wx.Destroy request # when done making its assertions testResult = executor.submit(assertionFunction, app, build_spec) # main loop blocks the main thread app.MainLoop() # .result() blocks as well while we wait for the thread to finish # any waiting it may be doing. testResult.result() del app
def gooey_params(**kwargs) -> GooeyParams: """ Builds the full GooeyParams object from an arbitrary subset of supplied values """ return GooeyParams(**{ # type: ignore 'show_preview_warning': kwargs.get('show_preview_warning', True), 'language': kwargs.get('language', 'english'), 'target': kwargs.get('target'), 'dump_build_config': kwargs.get('dump_build_config', False), 'load_build_config': kwargs.get('load_build_config'), 'use_cmd_args': kwargs.get('use_cmd_args', False), 'suppress_gooey_flag': kwargs.get('suppress_gooey_flag') or False, # TODO: I should not read from the environment. # remains here for legacy reasons pending refactor 'program_name': kwargs.get('program_name') or os.path.basename(sys.argv[0]).replace('.py', ''), 'program_description': kwargs.get('program_description') or '', 'sidebar_title': kwargs.get('sidebar_title', 'Actions'), 'default_size': kwargs.get('default_size', (610, 530)), 'auto_start': kwargs.get('auto_start', False), 'advanced': kwargs.get('advanced', True), 'run_validators': kwargs.get('run_validators', True), 'encoding': kwargs.get('encoding', 'utf-8'), 'show_stop_warning': kwargs.get('show_stop_warning', True), 'show_success_modal': kwargs.get('show_success_modal', True), 'show_failure_modal': kwargs.get('show_failure_modal', True), 'force_stop_is_error': kwargs.get('force_stop_is_error', True), 'poll_external_updates': kwargs.get('poll_external_updates', False), 'return_to_config': kwargs.get('return_to_config', False), 'show_restart_button': kwargs.get('show_restart_button', True), 'requires_shell': kwargs.get('requires_shell', True), 'menu': kwargs.get('menu', []), 'clear_before_run': kwargs.get('clear_before_run', False), 'fullscreen': kwargs.get('fullscreen', False), 'use_legacy_titles': kwargs.get('use_legacy_titles', True), 'required_cols': kwargs.get('required_cols', 2), 'optional_cols': kwargs.get('optional_cols', 2), 'manual_start': False, 'monospace_display': kwargs.get('monospace_display', False), 'image_dir': kwargs.get('image_dir', '::gooey/default'), # TODO: this directory resolution shouldn't happen here! # TODO: leaving due to legacy for now 'language_dir': kwargs.get('language_dir', getResourcePath('languages')), 'progress_regex': kwargs.get('progress_regex'), 'progress_expr': kwargs.get('progress_expr'), 'hide_progress_msg': kwargs.get('hide_progress_msg', False), 'timing_options': merge({ 'show_time_remaining': False, 'hide_time_remaining_on_complete': True }, kwargs.get('timing_options', {})), 'disable_progress_bar_animation': kwargs.get('disable_progress_bar_animation', False), 'disable_stop_button': kwargs.get('disable_stop_button'), 'shutdown_signal': kwargs.get('shutdown_signal', signal.SIGTERM), 'use_events': parse_events(kwargs.get('use_events', [])), 'navigation': kwargs.get('navigation', constants.SIDEBAR), 'show_sidebar': kwargs.get('show_sidebar', False), 'tabbed_groups': kwargs.get('tabbed_groups', False), 'group_by_type': kwargs.get('group_by_type', True), 'body_bg_color': kwargs.get('body_bg_color', '#f0f0f0'), 'header_bg_color': kwargs.get('header_bg_color', '#ffffff'), 'header_height': kwargs.get('header_height', 90), 'header_show_title': kwargs.get('header_show_title', True), 'header_show_subtitle': kwargs.get('header_show_subtitle', True), 'header_image_center': kwargs.get('header_image_center', False), 'footer_bg_color': kwargs.get('footer_bg_color', '#f0f0f0'), 'sidebar_bg_color': kwargs.get('sidebar_bg_color', '#f2f2f2'), 'terminal_panel_color': kwargs.get('terminal_panel_color', '#F0F0F0'), 'terminal_font_color': kwargs.get('terminal_font_color', '#000000'), 'terminal_font_family': kwargs.get('terminal_font_family', None), 'terminal_font_weight': _get_font_weight(kwargs), 'terminal_font_size': kwargs.get('terminal_font_size', None), 'richtext_controls': kwargs.get('richtext_controls', False), 'error_color': kwargs.get('error_color', '#ea7878'), # TODO: remove. Only useful for testing 'cli': kwargs.get('cli', sys.argv), })
def PasswordInput(_, parent, *args, **kwargs): style = {'style': wx.TE_PASSWORD} return TextInput(parent, *args, **merge(kwargs, style))
def MultilineTextInput(_, parent, *args, **kwargs): style = {'style': wx.TE_MULTILINE} return TextInput(parent, *args, **merge(kwargs, style))
def __init__(self, *args, **kwargs): defaults = {'label': _('choose_colour'), 'style': wx.TE_RICH} super(ColourChooser, self).__init__(*args, **merge(kwargs, defaults))
def __init__(self, *args, **kwargs): defaults = {'label': _('choose_time')} super(TimeChooser, self).__init__(*args, **merge(kwargs, defaults))
def __init__(self, *args, **kwargs): defaults = {'label': 'Choose Date'} super(DateChooser, self).__init__(*args, **merge(kwargs, defaults))
def loadImages(targetDir): defaultImages = resolvePaths(getResourcePath('images'), filenames) return { 'images': merge(defaultImages, collectOverrides(targetDir, filenames)) }
def Gooey( f=None, advanced=True, language='english', auto_start=False, # TODO: add this to the docs. Used to be `show_config=True` target=None, program_name=None, program_description=None, default_size=(610, 530), use_legacy_titles=True, required_cols=2, optional_cols=2, dump_build_config=False, load_build_config=None, monospace_display=False, # TODO: add this to the docs image_dir='::gooey/default', language_dir=getResourcePath('languages'), progress_regex=None, # TODO: add this to the docs progress_expr=None, # TODO: add this to the docs hide_progress_msg=False, # TODO: add this to the docs disable_progress_bar_animation=False, disable_stop_button=False, group_by_type=True, header_height=80, navigation='SIDEBAR', # TODO: add this to the docs tabbed_groups=False, **kwargs): ''' Decorator for client code's main function. Serializes argparse data to JSON for use with the Gooey front end ''' params = merge(locals(), locals()['kwargs']) def build(payload): def run_gooey(self, args=None, namespace=None): # This import is delayed so it is not in the --ignore-gooey codepath. from gooey.gui import application source_path = sys.argv[0] build_spec = None if load_build_config: try: exec_dir = os.path.dirname(sys.argv[0]) open_path = os.path.join(exec_dir, load_build_config) build_spec = json.load(open(open_path, "r")) except Exception as e: print( 'Exception loading Build Config from {0}: {1}'.format( load_build_config, e)) sys.exit(1) if not build_spec: build_spec = config_generator.create_from_parser( self, source_path, payload_name=payload.__name__, **params) if dump_build_config: config_path = os.path.join(os.path.dirname(sys.argv[0]), 'gooey_config.json') print('Writing Build Config to: {}'.format(config_path)) with open(config_path, 'w') as f: f.write(json.dumps(build_spec, indent=2)) application.run(build_spec) def inner2(*args, **kwargs): ArgumentParser.original_parse_args = ArgumentParser.parse_args ArgumentParser.parse_args = run_gooey return payload(*args, **kwargs) inner2.__name__ = payload.__name__ return inner2 def run_without_gooey(func): return lambda *args, **kwargs: func(*args, **kwargs) if IGNORE_COMMAND in sys.argv: sys.argv.remove(IGNORE_COMMAND) if callable(f): return run_without_gooey(f) return run_without_gooey if callable(f): return build(f) return build
def PasswordInput(_, parent, *args, **kwargs): style = {'style': wx.TE_PASSWORD} return TextInput(parent, *args, **merge(kwargs, style))
def __init__(self, *args, **kwargs): defaults = {'label': _('choose_date')} super(DateChooser, self).__init__(*args, **merge(kwargs, defaults))
def MultilineTextInput(_, parent, *args, **kwargs): style = {'style': wx.TE_MULTILINE} return TextInput(parent, *args, **merge(kwargs, style))
def loadImages(targetDir): defaultImages = resolvePaths(getResourcePath('images'), filenames) return {'images': merge(defaultImages, collectOverrides(targetDir, filenames))}
def Gooey(f=None, advanced=True, language='english', auto_start=False, # TODO: add this to the docs. Used to be `show_config=True` target=None, program_name=None, program_description=None, default_size=(610, 530), use_legacy_titles=True, required_cols=2, optional_cols=2, dump_build_config=False, load_build_config=None, monospace_display=False, # TODO: add this to the docs image_dir='::gooey/default', language_dir=getResourcePath('languages'), progress_regex=None, # TODO: add this to the docs progress_expr=None, # TODO: add this to the docs disable_progress_bar_animation=False, disable_stop_button=False, group_by_type=True, header_height=80, navigation='SIDEBAR', # TODO: add this to the docs tabbed_groups=False, **kwargs): ''' Decorator for client code's main function. Serializes argparse data to JSON for use with the Gooey front end ''' params = merge(locals(), locals()['kwargs']) def build(payload): def run_gooey(self, args=None, namespace=None): source_path = sys.argv[0] build_spec = None if load_build_config: try: build_spec = json.load(open(load_build_config, "r")) except Exception as e: print( 'Exception loading Build Config from {0}: {1}'.format(load_build_config, e)) sys.exit(1) if not build_spec: build_spec = config_generator.create_from_parser( self, source_path, payload_name=payload.__name__, **params) if dump_build_config: config_path = os.path.join(os.getcwd(), 'gooey_config.json') print('Writing Build Config to: {}'.format(config_path)) with open(config_path, 'w') as f: f.write(json.dumps(build_spec, indent=2)) application.run(build_spec) def inner2(*args, **kwargs): ArgumentParser.original_parse_args = ArgumentParser.parse_args ArgumentParser.parse_args = run_gooey return payload(*args, **kwargs) inner2.__name__ = payload.__name__ return inner2 def run_without_gooey(func): return lambda: func() if IGNORE_COMMAND in sys.argv: sys.argv.remove(IGNORE_COMMAND) if callable(f): return run_without_gooey(f) return run_without_gooey if callable(f): return build(f) return build