diff --git a/gooey/_tmp/example_disable_stop.py b/gooey/_tmp/example_disable_stop.py new file mode 100644 index 0000000..b6c12e2 --- /dev/null +++ b/gooey/_tmp/example_disable_stop.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals +from __future__ import print_function +import sys +from time import sleep +from gooey import Gooey, GooeyParser + + +@Gooey(progress_regex=r"^progress: (\d+)%$", + disable_stop_button=True) +def main(): + parser = GooeyParser(prog="example_progress_bar_1") + _ = parser.parse_args(sys.argv[1:]) + + for i in range(100): + print("progress: {}%".format(i+1)) + sys.stdout.flush() + sleep(0.1) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/gooey/_tmp/example_progress_bar_1.py b/gooey/_tmp/example_progress_bar_1.py new file mode 100644 index 0000000..7969ea3 --- /dev/null +++ b/gooey/_tmp/example_progress_bar_1.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals +from __future__ import print_function +import sys +from time import sleep +from gooey import Gooey, GooeyParser + + +@Gooey(progress_regex=r"^progress: (\d+)%$") +def main(): + parser = GooeyParser(prog="example_progress_bar_1") + _ = parser.parse_args(sys.argv[1:]) + + for i in range(100): + print("progress: {}%".format(i+1)) + sys.stdout.flush() + sleep(0.1) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/gooey/_tmp/example_progress_bar_2.py b/gooey/_tmp/example_progress_bar_2.py new file mode 100644 index 0000000..6ead7e2 --- /dev/null +++ b/gooey/_tmp/example_progress_bar_2.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals +from __future__ import print_function +import sys +from time import sleep +from gooey import Gooey, GooeyParser + + +@Gooey(progress_regex=r"^progress: (\d+)/(\d+)$", + progress_expr="x[0] / x[1] * 100", + disable_progress_bar_animation=True) +def main(): + parser = GooeyParser(prog="example_progress_bar_2") + parser.add_argument("steps", type=int, default=15) + parser.add_argument("delay", type=int, default=1) + args = parser.parse_args(sys.argv[1:]) + + for i in range(args.steps): + print("progress: {}/{}".format(i+1, args.steps)) + sys.stdout.flush() + sleep(args.delay) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/gooey/_tmp/example_progress_bar_3.py b/gooey/_tmp/example_progress_bar_3.py new file mode 100644 index 0000000..e3d062f --- /dev/null +++ b/gooey/_tmp/example_progress_bar_3.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals +from __future__ import print_function +import sys +from time import sleep +from gooey import Gooey, GooeyParser + + +@Gooey(progress_regex=r"^progress: (?P\d+)/(?P\d+)$", + progress_expr="current / total * 100") +def main(): + parser = GooeyParser(prog="example_progress_bar_3") + parser.add_argument("steps", type=int, default=15) + parser.add_argument("delay", type=int, default=1) + args = parser.parse_args(sys.argv[1:]) + + for i in range(args.steps): + print("progress: {}/{}".format(i+1, args.steps)) + sys.stdout.flush() + sleep(args.delay) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/gooey/_tmp/example_progress_bar_4.py b/gooey/_tmp/example_progress_bar_4.py new file mode 100644 index 0000000..fca9b99 --- /dev/null +++ b/gooey/_tmp/example_progress_bar_4.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals +from __future__ import print_function +import sys +from time import sleep +from gooey import Gooey, GooeyParser + + +@Gooey(progress_regex=r"^progress: (-?\d+)%$", + disable_progress_bar_animation=True) +def main(): + parser = GooeyParser(prog="example_progress_bar_1") + _ = parser.parse_args(sys.argv[1:]) + + print("Step 1") + + for i in range(1, 101): + print("progress: {}%".format(i)) + sys.stdout.flush() + sleep(0.05) + + print("Step 2") + + print("progress: -1%") # pulse + sys.stdout.flush() + sleep(3) + + print("Step 3") + + for i in range(1, 101): + print("progress: {}%".format(i)) + sys.stdout.flush() + sleep(0.05) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/gooey/gui/controller.py b/gooey/gui/controller.py index e5a5647..c0cb18c 100644 --- a/gooey/gui/controller.py +++ b/gooey/gui/controller.py @@ -5,6 +5,8 @@ Created on Dec 22, 2013 ''' import wx +import os +import re import sys import subprocess @@ -14,12 +16,13 @@ from multiprocessing.dummy import Pool from gooey.gui import events from gooey.gui.lang import i18n from gooey.gui.windows import views +from gooey.gui.util.taskkill import taskkill + YES = 5103 NO = 5104 - class Controller(object): ''' Main controller for the gui. @@ -34,9 +37,11 @@ class Controller(object): ''' self.core_gui = base_frame self.build_spec = build_spec + self._process = None # wire up all the observers pub.subscribe(self.on_cancel, events.WINDOW_CANCEL) + pub.subscribe(self.on_stop, events.WINDOW_STOP) pub.subscribe(self.on_start, events.WINDOW_START) pub.subscribe(self.on_restart, events.WINDOW_RESTART) pub.subscribe(self.on_close, events.WINDOW_CLOSE) @@ -46,8 +51,9 @@ class Controller(object): pub.send_message(events.WINDOW_CHANGE, view_name=views.CONFIG_SCREEN) def on_close(self): - self.core_gui.Destroy() - sys.exit() + if self.ask_stop(): + self.core_gui.Destroy() + sys.exit() def on_restart(self): self.on_start() @@ -70,12 +76,41 @@ class Controller(object): return self.show_dialog(i18n._('error_title'), i18n._('error_required_fields'), wx.ICON_ERROR) cmd_line_args = self.core_gui.GetOptions() - command = '{0} --ignore-gooey {1}'.format(self.build_spec['target'], cmd_line_args) + command = '{} --ignore-gooey {}'.format(self.build_spec['target'], cmd_line_args) pub.send_message(events.WINDOW_CHANGE, view_name=views.RUNNING_SCREEN) self.run_client_code(command) + def on_stop(self): + self.ask_stop() + + def ask_stop(self): + if not self.running(): + return True + if self.build_spec['disable_stop_button']: + return False + msg = i18n._('sure_you_want_to_stop') + dlg = wx.MessageDialog(None, msg, i18n._('stop_task'), wx.YES_NO) + result = dlg.ShowModal() + dlg.Destroy() + if result == YES: + self.stop() + return True + return False + + def stop(self): + if self.running(): + taskkill(self._process.pid) + + def running(self): + return self._process and self._process.poll() is None + def run_client_code(self, command): - p = subprocess.Popen(command, bufsize=1, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + env = os.environ.copy() + env["GOOEY"] = "1" + print "run command:", command + p = subprocess.Popen(command, bufsize=1, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, shell=True, env=env) + self._process = p pool = Pool(1) pool.apply_async(self.read_stdout, (p, self.process_result)) @@ -85,16 +120,52 @@ class Controller(object): if not line: break wx.CallAfter(self.core_gui.PublishConsoleMsg, line) + progress = self.progress_from_line(line) + if progress is not None: + wx.CallAfter(self.core_gui.UpdateProgressBar, progress) wx.CallAfter(callback, process) + def progress_from_line(self, text): + progress_regex = self.build_spec['progress_regex'] + if not progress_regex: + return None + match = re.search(progress_regex, text.strip()) + if not match: + return None + progress_expr = self.build_spec['progress_expr'] + if progress_expr: + return self._eval_progress(match, progress_expr) + else: + return self._search_progress(match) + + def _search_progress(self, match): + try: + return int(float(match.group(1))) + except: + return None + + def _eval_progress(self, match, eval_expr): + def safe_float(x): + try: + return float(x) + except ValueError: + return x + _locals = {k: safe_float(v) for k, v in match.groupdict().items()} + if "x" not in _locals: + _locals["x"] = [safe_float(x) for x in match.groups()] + try: + return int(float(eval(eval_expr, {}, _locals))) + except: + return None + def process_result(self, process): - _stdout, _stderr = process.communicate() + _stdout, _ = process.communicate() if process.returncode == 0: pub.send_message(events.WINDOW_CHANGE, view_name=views.SUCCESS_SCREEN) self.success_dialog() else: pub.send_message(events.WINDOW_CHANGE, view_name=views.ERROR_SCREEN) - self.error_dialog(_stderr) + self.error_dialog() def skipping_config(self): return self.build_spec['manual_start'] @@ -108,8 +179,8 @@ class Controller(object): def success_dialog(self): self.show_dialog(i18n._("execution_finished"), i18n._('success_message'), wx.ICON_INFORMATION) - def error_dialog(self, error_msg): - self.show_dialog(i18n._('error_title'), i18n._('uh_oh').format(error_msg), wx.ICON_ERROR) + def error_dialog(self): + self.show_dialog(i18n._('error_title'), i18n._('uh_oh'), wx.ICON_ERROR) def show_dialog(self, title, content, style): a = wx.MessageDialog(None, content, title, style) diff --git a/gooey/gui/util/quoting.py b/gooey/gui/util/quoting.py index f97fd31..179fbed 100644 --- a/gooey/gui/util/quoting.py +++ b/gooey/gui/util/quoting.py @@ -1,5 +1,6 @@ import re import sys +import re if sys.platform.startswith("win"): @@ -8,3 +9,7 @@ if sys.platform.startswith("win"): else: # POSIX shell def quote(value): return "'{}'".format('{}'.format(value).replace("'", "'\\''")) + + +def maybe_quote(string): + return '"{}"'.format(string) if not re.match(r'^".*"$', string) else string diff --git a/gooey/gui/util/taskkill.py b/gooey/gui/util/taskkill.py new file mode 100644 index 0000000..a48f676 --- /dev/null +++ b/gooey/gui/util/taskkill.py @@ -0,0 +1,11 @@ +import sys +import os +import signal + + +if sys.platform.startswith("win"): + def taskkill(pid): + os.system('taskkill /F /PID {:d} /T >NUL 2>NUL'.format(pid)) +else: # POSIX + def taskkill(pid): + os.kill(pid, signal.SIGTERM) diff --git a/gooey/gui/widgets/components.py b/gooey/gui/widgets/components.py index eab4ee0..ca875be 100644 --- a/gooey/gui/widgets/components.py +++ b/gooey/gui/widgets/components.py @@ -243,7 +243,6 @@ DirChooser = lambda data: BaseGuiComponent(data=data, widget_pack=widget_ FileSaver = lambda data: BaseGuiComponent(data=data, widget_pack=widget_pack.FileSaverPayload()) DateChooser = lambda data: BaseGuiComponent(data=data, widget_pack=widget_pack.DateChooserPayload()) TextField = lambda data: BaseGuiComponent(data=data, widget_pack=widget_pack.TextInputPayload()) -CommandField = lambda data: BaseGuiComponent(data=data, widget_pack=widget_pack.TextInputPayload(no_qouting=True)) Dropdown = lambda data: BaseGuiComponent(data=data, widget_pack=widget_pack.DropdownPayload()) Counter = lambda data: BaseGuiComponent(data=data, widget_pack=widget_pack.CounterPayload()) MultiDirChooser = lambda data: BaseGuiComponent(data=data, widget_pack=widget_pack.MultiDirChooserPayload()) diff --git a/gooey/gui/widgets/widget_pack.py b/gooey/gui/widgets/widget_pack.py index b12697a..b197522 100644 --- a/gooey/gui/widgets/widget_pack.py +++ b/gooey/gui/widgets/widget_pack.py @@ -65,7 +65,7 @@ class BaseChooser(WidgetPack): widget_sizer = wx.BoxSizer(wx.HORIZONTAL) widget_sizer.Add(self.text_box, 1, wx.EXPAND) widget_sizer.AddSpacer(10) - widget_sizer.Add(self.button, 0) + widget_sizer.Add(self.button, 0, wx.ALIGN_CENTER_VERTICAL) parent.Bind(wx.EVT_BUTTON, self.onButton, self.button) return widget_sizer diff --git a/gooey/gui/windows/base_window.py b/gooey/gui/windows/base_window.py index 70889d2..0a7d9b4 100644 --- a/gooey/gui/windows/base_window.py +++ b/gooey/gui/windows/base_window.py @@ -3,6 +3,8 @@ Created on Jan 19, 2014 @author: Chris ''' +import sys + import wx from gooey.gui.pubsub import pub @@ -40,8 +42,7 @@ class BaseWindow(wx.Frame): self._init_controller() self.registerControllers() self.Bind(wx.EVT_SIZE, self.onResize) - - self.Bind(wx.EVT_CLOSE, lambda x: pub.send_message(str(events.WINDOW_CLOSE))) + self.Bind(wx.EVT_CLOSE, self.onClose) def _init_properties(self): self.SetTitle(self.build_spec['program_name']) @@ -60,6 +61,12 @@ class BaseWindow(wx.Frame): self.runtime_display = RuntimeDisplay(self, self.build_spec) self.foot_panel = footer.Footer(self) + + if self.build_spec['disable_stop_button']: + self.foot_panel.stop_button.Disable() + else: + self.foot_panel.stop_button.Enable() + self.panels = [self.head_panel, self.config_panel, self.foot_panel] def _do_layout(self): @@ -137,9 +144,32 @@ class BaseWindow(wx.Frame): def onResize(self, evt): evt.Skip() + def onClose(self, evt): + if evt.CanVeto(): + evt.Veto() + pub.send_message(str(events.WINDOW_CLOSE)) + def PublishConsoleMsg(self, text): self.runtime_display.cmd_textbox.AppendText(text) + def UpdateProgressBar(self, value): + pb = self.foot_panel.progress_bar + if value < 0: + pb.Pulse() + else: + value = min(int(value), pb.GetRange()) + if pb.GetValue() != value: + # Windows 7 progress bar animation hack + # http://stackoverflow.com/questions/5332616/disabling-net-progressbar-animation-when-changing-value + if self.build_spec["disable_progress_bar_animation"] \ + and sys.platform.startswith("win"): + if pb.GetRange() == value: + pb.SetValue(value) + pb.SetValue(value-1) + else: + pb.SetValue(value+1) + pb.SetValue(value) + if __name__ == '__main__': pass diff --git a/gooey/gui/windows/footer.py b/gooey/gui/windows/footer.py index 43a9156..d32c8d3 100644 --- a/gooey/gui/windows/footer.py +++ b/gooey/gui/windows/footer.py @@ -10,7 +10,7 @@ import wx.animate from gooey.gui.pubsub import pub from gooey.gui.lang import i18n -from gooey.gui import imageutil, image_repository, events +from gooey.gui import imageutil, events class AbstractFooter(wx.Panel): @@ -27,7 +27,7 @@ class AbstractFooter(wx.Panel): # components self.cancel_button = None self.start_button = None - self.running_animation = None + self.progress_bar = None self.close_button = None self.stop_button = None self.restart_button = None @@ -50,7 +50,7 @@ class AbstractFooter(wx.Panel): self.restart_button = self.button(i18n._('restart'), wx.ID_OK, event_id=int(events.WINDOW_RESTART)) self.edit_button = self.button(i18n._('edit'), wx.ID_OK, event_id=int(events.WINDOW_EDIT)) - self.running_animation = wx.animate.GIFAnimationCtrl(self, -1, image_repository.loading_icon) + self.progress_bar = wx.Gauge(self, range=100) self.buttons = [self.cancel_button, self.start_button, self.stop_button, self.close_button, self.restart_button, self.edit_button] @@ -59,19 +59,18 @@ class AbstractFooter(wx.Panel): self.hide_all_buttons() self.cancel_button.Show() self.start_button.Show() - self.running_animation.Stop() self.Layout() def running(): self.hide_all_buttons() - self.running_animation.Show() - self.running_animation.Play() + self.stop_button.Show() + self.progress_bar.Show() + self.progress_bar.Pulse() self.Layout() def success(): self.hide_all_buttons() - self.running_animation.Stop() - self.running_animation.Hide() + self.progress_bar.Hide() self.edit_button.Show() self.restart_button.Show() self.close_button.Show() @@ -96,14 +95,16 @@ class AbstractFooter(wx.Panel): v_sizer = wx.BoxSizer(wx.VERTICAL) h_sizer = wx.BoxSizer(wx.HORIZONTAL) + h_sizer.Add(self.progress_bar, 0, wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 20) + self.progress_bar.Hide() + h_sizer.AddStretchSpacer(1) h_sizer.Add(self.cancel_button, 0, wx.ALIGN_RIGHT | wx.RIGHT, 20) h_sizer.Add(self.start_button, 0, wx.ALIGN_RIGHT | wx.RIGHT, 20) + h_sizer.Add(self.stop_button, 0, wx.ALIGN_RIGHT | wx.RIGHT, 20) v_sizer.AddStretchSpacer(1) v_sizer.Add(h_sizer, 0, wx.ALIGN_CENTER_VERTICAL | wx.EXPAND) - v_sizer.Add(self.running_animation, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT | wx.RIGHT, 20) - self.running_animation.Hide() h_sizer.Add(self.edit_button, 0, wx.ALIGN_RIGHT | wx.RIGHT, 10) h_sizer.Add(self.restart_button, 0, wx.ALIGN_RIGHT | wx.RIGHT, 10) diff --git a/gooey/gui/windows/layouts.py b/gooey/gui/windows/layouts.py index ae8deca..519b692 100644 --- a/gooey/gui/windows/layouts.py +++ b/gooey/gui/windows/layouts.py @@ -11,7 +11,7 @@ from gooey.gui.util.quoting import quote basic_config = { 'required': [{ - 'type': 'CommandField', + 'type': 'TextField', 'data': { 'display_name': 'Enter Commands', 'help': 'Enter command line arguments', diff --git a/gooey/languages/eng.py b/gooey/languages/eng.py index c79a2c7..04ef61c 100644 --- a/gooey/languages/eng.py +++ b/gooey/languages/eng.py @@ -25,17 +25,18 @@ if __name__ == '__main__': 'required_args_msg': 'Required Arguments', 'optional_args_msg': 'Optional Arguments', # popup dialogs "sure_you_want_to_exit": "Are you sure you want to exit?", - 'close_program': 'Close Program?', + 'close_program': 'Close program?', + 'sure_you_want_to_stop': 'Are you sure you want to stop the task? ' + + '\nInterruption can corrupt your data!', + 'stop_task': 'Stop task?', 'status': 'Status', 'uh_oh': ''' -Uh oh! Looks like there was a problem. -Copy the below error to let your developer know what went wrong. - -{} - ''', +Uh oh! Looks like there was a problem. +Copy the text from status window to let your developer know what went wrong. +''', 'error_title': "Error", - 'execution_finished': 'Execution Finished', - 'success_message': 'Program completed Sucessfully!', + 'execution_finished': 'Execution finished', + 'success_message': 'Program completed sucessfully!', } diff --git a/gooey/languages/english.json b/gooey/languages/english.json index a393ee7..a389ca2 100644 --- a/gooey/languages/english.json +++ b/gooey/languages/english.json @@ -1,17 +1,18 @@ { - "cancel": "Cancel", - "close": "Close", - "close_program": "Close Program?", - "error_title": "Error", - "execution_finished": "Execution Finished", + "cancel": "Cancel", + "close": "Close", + "close_program": "Close program?", + "stop_task": "Stop task?", + "error_title": "Error", + "execution_finished": "Execution finished", "finished_msg": "All done! You may now safely close the program.", "finished_error": "An error has occurred.", - "finished_title": "Finished", - "optional_args_msg": "Optional Arguments", - "required_args_msg": "Required Arguments", - "running_msg": "Please wait while the application performs its tasks. \nThis may take a few moments", - "running_title": "Running", - "settings_title": "Settings", + "finished_title": "Finished", + "optional_args_msg": "Optional Arguments", + "required_args_msg": "Required Arguments", + "running_msg": "Please wait while the application performs its tasks. \nThis may take a few moments", + "running_title": "Running", + "settings_title": "Settings", "simple_config": "Enter Command Line Arguments", "error_required_fields": "Must fill in all fields in the Required section!", "start": "Start", @@ -19,8 +20,9 @@ "status": "Status", "restart": "Restart", "edit": "Edit", - "success_message": "Program completed Sucessfully!\nPress the OK button to exit", - "sure_you_want_to_exit": "Are you sure you want to exit?", - "uh_oh": "\nUh oh! Looks like there was a problem. \nCopy the below error to let your developer know what went wrong.\n\n{} \t\t\n\t\t", + "success_message": "Program completed sucessfully!", + "sure_you_want_to_exit": "Are you sure you want to exit?", + "sure_you_want_to_stop": "Are you sure you want to stop the task? \nInterruption can corrupt your data!", + "uh_oh": "\nUh oh! Looks like there was a problem. \nCopy the text from status window to let your developer know what went wrong.\n", "browse": "Browse" } diff --git a/gooey/python_bindings/config_generator.py b/gooey/python_bindings/config_generator.py index 23fae2f..6316d94 100644 --- a/gooey/python_bindings/config_generator.py +++ b/gooey/python_bindings/config_generator.py @@ -28,7 +28,10 @@ def create_from_parser(parser, source_path, **kwargs): 'monospace_display': kwargs.get('monospace_display', False), 'image_dir': kwargs.get('image_dir'), 'language_dir': kwargs.get('language_dir'), - + 'progress_regex': kwargs.get('progress_regex'), + 'progress_expr': kwargs.get('progress_expr'), + 'disable_progress_bar_animation': kwargs.get('disable_progress_bar_animation'), + 'disable_stop_button': kwargs.get('disable_stop_button'), } if show_config: diff --git a/gooey/python_bindings/docopt_to_json.py b/gooey/python_bindings/docopt_to_json.py index fb0c763..93868d3 100644 --- a/gooey/python_bindings/docopt_to_json.py +++ b/gooey/python_bindings/docopt_to_json.py @@ -28,7 +28,7 @@ Options: # types? - +import re from docopt import docopt, Option, Argument @@ -51,7 +51,7 @@ class MyOption(Option): else: argcount = 1 if argcount: - matched = re.findall('\[default: (.*)\]', description, flags=re.I) + matched = re.findall(r'\[default: (.*)\]', description, flags=re.I) value = matched[0] if matched else None return class_(short, long, argcount, value, description=description.strip()) diff --git a/gooey/python_bindings/gooey_decorator.py b/gooey/python_bindings/gooey_decorator.py index 11e91b1..eea7672 100644 --- a/gooey/python_bindings/gooey_decorator.py +++ b/gooey/python_bindings/gooey_decorator.py @@ -6,6 +6,7 @@ Created on Jan 24, 2014 TODO: this ''' +import sys import os import json import atexit @@ -13,7 +14,6 @@ import tempfile from . import source_parser from . import config_generator -import sys from gooey.gui import application @@ -35,7 +35,11 @@ def Gooey(f=None, load_build_config=None, monospace_display=False, image_dir='default', - language_dir=get_resource_path('languages')): + language_dir=get_resource_path('languages'), + progress_regex=None, + progress_expr=None, + disable_progress_bar_animation=False, + disable_stop_button=False): ''' Decorator for client code's main function. Serializes argparse data to JSON for use with the Gooey front end @@ -60,7 +64,7 @@ def Gooey(f=None, if dump_build_config: config_path = os.path.join(os.getcwd(), 'gooey_config.json') - print( 'Writing Build Config to: {}'.format(config_path)) + 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) diff --git a/gooey/python_bindings/gooey_parser.py b/gooey/python_bindings/gooey_parser.py index 334e606..aee804e 100644 --- a/gooey/python_bindings/gooey_parser.py +++ b/gooey/python_bindings/gooey_parser.py @@ -1,4 +1,5 @@ from argparse import ArgumentParser, _SubParsersAction, _MutuallyExclusiveGroup +from gooey.gui.lang.i18n import _ class GooeySubParser(_SubParsersAction):