From 116771edd2698bf65040ed04a5603530de93f8e1 Mon Sep 17 00:00:00 2001 From: Jack McKew Date: Fri, 10 Jul 2020 11:30:14 +1000 Subject: [PATCH] Elapsed / Remaining Time on Progress Bar parent 00e0f3e7c6434240ffe02522a0be2daf8c5b456c author Jack McKew 1594344614 +1000 committer Jack McKew 1594346638 +1000 Elapsed / Remaining Time on Progress Bar parent 00e0f3e7c6434240ffe02522a0be2daf8c5b456c author Fizban 1593182730 +1000 committer Fizban 1593246420 +1000 parent 00e0f3e7c6434240ffe02522a0be2daf8c5b456c author Fizban 1593182730 +1000 committer Fizban 1593246359 +1000 Elapsed / Remaining Time on Progress Bar Elapsed / Remaining Time on Progress Bar Time Remaining Text for Progress Time remaining somewhat working Time format working Add tests and pythn 2.7 compat Incase python 2 import perf counter in func Fix flickering text and align Remove transactUI Add new decorators Update time remaining notes in README Amend tests with new arguments Remove unused import Separate into time module Move to dictionary structure options Explicitly show time text Amend tests for separate module Integration test - missing on complete tests Remove old code Elapsed / Remaining Time on Progress Bar parent 00e0f3e7c6434240ffe02522a0be2daf8c5b456c author Fizban 1593182730 +1000 committer Fizban 1593246420 +1000 parent 00e0f3e7c6434240ffe02522a0be2daf8c5b456c author Fizban 1593182730 +1000 committer Fizban 1593246359 +1000 Elapsed / Remaining Time on Progress Bar Elapsed / Remaining Time on Progress Bar Time Remaining Text for Progress Time remaining somewhat working Time format working Add tests and pythn 2.7 compat Incase python 2 import perf counter in func Fix flickering text and align Remove transactUI Add new decorators Update time remaining notes in README Amend tests with new arguments Remove unused import Separate into time module Move to dictionary structure options Explicitly show time text Amend tests for separate module Remove old code Pass testdata as dict instead of kwargs Merge dictionaries for defaults Test almost working Delete settings.json Revert "Test almost working" This reverts commit f17d50681cae664719f67a7e8cc0b1feaf1ac4c7. Back to working state Remove unhelpful test Update docs Working without updated test Add tests for time remaining text Elapsed / Remaining Time on Progress Bar parent 00e0f3e7c6434240ffe02522a0be2daf8c5b456c author Jack McKew 1594344614 +1000 committer Jack McKew 1594346638 +1000 Elapsed / Remaining Time on Progress Bar parent 00e0f3e7c6434240ffe02522a0be2daf8c5b456c author Fizban 1593182730 +1000 committer Fizban 1593246420 +1000 parent 00e0f3e7c6434240ffe02522a0be2daf8c5b456c author Fizban 1593182730 +1000 committer Fizban 1593246359 +1000 Elapsed / Remaining Time on Progress Bar Elapsed / Remaining Time on Progress Bar Time Remaining Text for Progress Time remaining somewhat working Time format working Add tests and pythn 2.7 compat Incase python 2 import perf counter in func Fix flickering text and align Remove transactUI Add new decorators Update time remaining notes in README Amend tests with new arguments Remove unused import Separate into time module Move to dictionary structure options Explicitly show time text Amend tests for separate module Integration test - missing on complete tests Remove old code Elapsed / Remaining Time on Progress Bar parent 00e0f3e7c6434240ffe02522a0be2daf8c5b456c author Fizban 1593182730 +1000 committer Fizban 1593246420 +1000 parent 00e0f3e7c6434240ffe02522a0be2daf8c5b456c author Fizban 1593182730 +1000 committer Fizban 1593246359 +1000 Elapsed / Remaining Time on Progress Bar Elapsed / Remaining Time on Progress Bar Time Remaining Text for Progress Time remaining somewhat working Time format working Add tests and pythn 2.7 compat Incase python 2 import perf counter in func Fix flickering text and align Remove transactUI Add new decorators Update time remaining notes in README Amend tests with new arguments Remove unused import Separate into time module Move to dictionary structure options Explicitly show time text Amend tests for separate module Remove old code Pass testdata as dict instead of kwargs Merge dictionaries for defaults Test almost working Delete settings.json Revert "Test almost working" This reverts commit f17d50681cae664719f67a7e8cc0b1feaf1ac4c7. Back to working state Remove unhelpful test Update docs Working without updated test Add tests for time remaining text Remove artifact from squashing Amend artifacts Handle no progress and tidy up footer label --- README.md | 118 +++++++++++++--------- gooey/gui/components/footer.py | 19 +++- gooey/gui/containers/application.py | 15 +++ gooey/gui/events.py | 1 + gooey/gui/processor.py | 4 +- gooey/gui/util/functional.py | 24 +++++ gooey/gui/util/time.py | 96 ++++++++++++++++++ gooey/python_bindings/config_generator.py | 3 + gooey/python_bindings/gooey_decorator.py | 4 + gooey/tests/test_processor.py | 7 +- gooey/tests/test_time_remaining.py | 63 ++++++++++++ gooey/tests/test_util.py | 32 ++++++ 12 files changed, 329 insertions(+), 57 deletions(-) create mode 100644 gooey/gui/util/time.py create mode 100644 gooey/tests/test_time_remaining.py create mode 100644 gooey/tests/test_util.py diff --git a/README.md b/README.md index 9d0ba88..20bd817 100644 --- a/README.md +++ b/README.md @@ -18,32 +18,33 @@ Table of Contents ----------------- - [Gooey](#gooey) -- [Table of contents](#table-of-contents) -- [Latest Update](#latest-update) -- [Quick Start](#quick-start) - - [Installation Instructions](#installation-instructions) + - [Table of Contents](#table-of-contents) + - [Quick Start](#quick-start) + - [Installation instructions](#installation-instructions) - [Usage](#usage) - [Examples](#examples) -- [What It Is](#what-is-it) -- [Why Is It](#why) -- [Who is this for](#who-is-this-for) -- [How does it work](#how-does-it-work) -- [Internationalization](#internationalization) -- [Global Configuration](#global-configuration) -- [Layout Customization](#layout-customization) -- [Run Modes](#run-modes) - - [Full/Advanced](#advanced) + - [What is it?](#what-is-it) + - [Why?](#why) + - [Who is this for?](#who-is-this-for) + - [How does it work?](#how-does-it-work) + - [Mappings:](#mappings) + - [GooeyParser](#gooeyparser) + - [Internationalization](#internationalization) + - [Global Configuration](#global-configuration) + - [Layout Customization](#layout-customization) + - [Run Modes](#run-modes) + - [Advanced](#advanced) - [Basic](#basic) - [No Config](#no-config) -- [Menus](#menus) -- [Input Validation](#input-validation) -- [Using Dynamic Values](#using-dynamic-values) -- [Showing Progress](#showing-progress) -- [Customizing Icons](#customizing-icons) -- [Packaging](#packaging) -- [Screenshots](#screenshots) -- [Contributing](#wanna-help) -- [Image Credits](#image-credits) + - [Menus](#menus) + - [Input Validation](#input-validation) + - [Using Dynamic Values](#using-dynamic-values) + - [Showing Progress](#showing-progress) + - [Elapsed / Remaining Time](#elapsed--remaining-time) + - [Customizing Icons](#customizing-icons) + - [Packaging](#packaging) + - [Screenshots](#screenshots) + - [Wanna help?](#wanna-help) @@ -156,16 +157,16 @@ At run-time, it parses your Python script for all references to `ArgumentParser` Gooey does its best to choose sensible defaults based on the options it finds. Currently, `ArgumentParser._actions` are mapped to the following `WX` components. -| Parser Action | Widget | Example | -|:----------------------|-----------|------| -| store | TextCtrl | | -| store_const | CheckBox | | -| store_true| CheckBox | | -| store_False | CheckBox| | -| append | TextCtrl | | -| count| DropDown                  | | -| Mutually Exclusive Group | RadioGroup | -|choice                                             | DropDown | | +| Parser Action | Widget | Example | +| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| store | TextCtrl | | +| store_const | CheckBox | | +| store_true | CheckBox | | +| store_False | CheckBox | | +| append | TextCtrl | | +| count | DropDown                  | | +| Mutually Exclusive Group | RadioGroup | | +| choice                                              | DropDown | | ### GooeyParser @@ -199,14 +200,14 @@ However, by dropping in `GooeyParser` and supplying a `widget` name, you can dis **Custom Widgets:** -| Widget | Example | -|----------------|------------------------------| -| DirChooser, FileChooser, MultiFileChooser, FileSaver, MultiFileSaver |

| -| DateChooser/TimeChooser                                             |

Please note that for both of these widgets the values passed to the application will always be in [ISO format](https://www.wxpython.org/Phoenix/docs/html/wx.DateTime.html#wx.DateTime.FormatISOTime) while localized values may appear in some parts of the GUI depending on end-user settings.

| -| PasswordField |

| -| Listbox | ![image](https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/31590191-fadd06f2-b1c0-11e7-9a49-7cbf0c6d33d1.png) | -| BlockCheckbox | ![image](https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/46922288-9296f200-cfbb-11e8-8b0d-ddde08064247.png)
The default InlineCheck box can look less than ideal if a large help text block is present. `BlockCheckbox` moves the text block to the normal position and provides a short-form `block_label` for display next to the control. Use `gooey_options.checkbox_label` to control the label text | -| ColourChooser                                             |

| +| Widget | Example | +| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| DirChooser, FileChooser, MultiFileChooser, FileSaver, MultiFileSaver |

| +| DateChooser/TimeChooser                                              |

Please note that for both of these widgets the values passed to the application will always be in [ISO format](https://www.wxpython.org/Phoenix/docs/html/wx.DateTime.html#wx.DateTime.FormatISOTime) while localized values may appear in some parts of the GUI depending on end-user settings.

| +| PasswordField |

| +| Listbox | ![image](https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/31590191-fadd06f2-b1c0-11e7-9a49-7cbf0c6d33d1.png) | +| BlockCheckbox | ![image](https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/46922288-9296f200-cfbb-11e8-8b0d-ddde08064247.png)
The default InlineCheck box can look less than ideal if a large help text block is present. `BlockCheckbox` moves the text block to the normal position and provides a short-form `block_label` for display next to the control. Use `gooey_options.checkbox_label` to control the label text | +| ColourChooser                                              |

| @@ -271,6 +272,9 @@ Just about everything in Gooey's overall look and feel can be customized by pass | progress_expr | A python expression applied to any matches found via the `progress_regex`. See: [Showing Progress](#showing-progress) for a detailed how-to | | hide_progress_msg | Option to hide textual progress updates which match the `progress_regex`. See: [Showing Progress](#showing-progress) for a detailed how-to | | disable_progress_bar_animation | Disable the progress bar | +| timing_options | This contains the options for displaying time remaining and elapsed time, to be used with `progress_regex` and `progress_expr`. [Elapsed / Remaining Time](#elapsed--remaining-time). Contained as a dictionary with the options `show_time_remaining` and `hide_time_remaining_on_complete`. Eg: `timing_options={'show_time_remaining':True,'hide_time_remaining_on_complete':True}` | +| show_time_remaining | Disable the time remaining text see [Elapsed / Remaining Time](#elapsed--remaining-time) | +| hide_time_remaining_on_complete | Hide time remaining on complete screen see [Elapsed / Remaining Time](#elapsed--remaining-time) | | requires_shell | Controls whether or not the `shell` argument is used when invoking your program. [More info here](https://stackoverflow.com/questions/3172470/actual-meaning-of-shell-true-in-subprocess#3172488) | | navigation | Sets the "navigation" style of Gooey's top level window.
Options:
TABBEDSIDEBAR
| | sidebar_title | Controls the heading title above the SideBar's navigation pane. Defaults to: "Actions" | @@ -302,9 +306,9 @@ You can achieve fairly flexible layouts with Gooey by using a few simple customi At the highest level, you have several overall layout options controllable via various arguments to the Gooey decorator. -| `show_sidebar=True` | `show_sidebar=False` | `navigation='TABBED'` | `tabbed_groups=True` | -|---------------------|----------------------|----------------------|------------------------| -| | | | | +| `show_sidebar=True` | `show_sidebar=False` | `navigation='TABBED'` | `tabbed_groups=True` | +| -------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| | | | | **Grouping Inputs** @@ -690,6 +694,22 @@ progress: 3/100 There are lots of options for telling Gooey about progress as your program is running. Checkout the [Gooey Examples](https://github.com/chriskiehl/GooeyExamples) repository for more detailed usage and examples! +### Elapsed / Remaining Time + +Gooey also supports tracking elapsed / remaining time when progress is used! This is done in a similar manner to that of the project [tqdm](https://github.com/tqdm/tqdm). This can be enabled with `timing_options`, the `timing_options` argument takes in a dictionary with the keys `show_time_remaining` and `hide_time_remaining_on_complete`. The default behavior is True for `show_time_remaining` and False for `hide_time_remaining_on_complete`. This will only work when `progress_regex` and `progress_expr` are used. + +```python +@Gooey(progress_regex=r"^progress: (?P\d+)/(?P\d+)$", + progress_expr="current / total * 100", + timing_options = { + 'show_time_remaining':True, + 'hide_time_remaining_on_complete':True, + }) +``` + + +![Elapsed/Remaining Time](https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/gooey-estimated-finish.gif) + -------------------------------------- @@ -723,13 +743,13 @@ Detailed step by step instructions can be found [here](http://chriskiehl.com/art Screenshots ------------ -| Flat Layout | Column Layout |Success Screen | Error Screen | Warning Dialog | -|-------------|---------------|---------------|--------------|----------------| -| | | | | | +| Flat Layout | Column Layout | Success Screen | Error Screen | Warning Dialog | +| ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| | | | | | -| Custom Groups | Tabbed Groups | Tabbed Navigation | Sidebar Navigation | Input Validation | -|-------------|---------------|---------------|--------------|----------------| -| | | | | | +| Custom Groups | Tabbed Groups | Tabbed Navigation | Sidebar Navigation | Input Validation | +| -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| | | | | | diff --git a/gooey/gui/components/footer.py b/gooey/gui/components/footer.py index f964c7c..adfec4b 100644 --- a/gooey/gui/components/footer.py +++ b/gooey/gui/components/footer.py @@ -18,6 +18,7 @@ class Footer(wx.Panel): self.buildSpec = buildSpec self.SetMinSize((30, 53)) + self.SetDoubleBuffered(True) # components self.cancel_button = None self.start_button = None @@ -39,6 +40,18 @@ class Footer(wx.Panel): self.Bind(wx.EVT_LEFT_DOWN, notifyMouseEvent) + def updateTimeRemaining(self,*args,**kwargs): + estimate_time_remaining = kwargs.get('estimatedRemaining') + elapsed_time_value = kwargs.get('elapsed_time') + if elapsed_time_value is None: + return + elif estimate_time_remaining is not None: + self.time_remaining_text.SetLabel(f"{elapsed_time_value}<{estimate_time_remaining}") + return + else: + self.time_remaining_text.SetLabel(f"{elapsed_time_value}") + + def updateProgressBar(self, *args, **kwargs): ''' value, disable_animation=False @@ -85,6 +98,8 @@ class Footer(wx.Panel): self.progress_bar = wx.Gauge(self, range=100) + self.time_remaining_text = wx.StaticText(self) + self.buttons = [self.cancel_button, self.start_button, self.stop_button, self.close_button, self.restart_button, self.edit_button] @@ -103,7 +118,8 @@ class Footer(wx.Panel): h_sizer.Add(self.progress_bar, 1, wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 20) - self.progress_bar.Hide() + + h_sizer.Add(self.time_remaining_text,0,wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 20) h_sizer.AddStretchSpacer(1) h_sizer.Add(self.cancel_button, 0,wx.RIGHT, 20) @@ -119,6 +135,7 @@ class Footer(wx.Panel): self.edit_button.Hide() self.restart_button.Hide() self.close_button.Hide() + self.progress_bar.Hide() v_sizer.AddStretchSpacer(1) self.SetSizer(v_sizer) diff --git a/gooey/gui/containers/application.py b/gooey/gui/containers/application.py index 991ba2e..78d08c9 100644 --- a/gooey/gui/containers/application.py +++ b/gooey/gui/containers/application.py @@ -20,6 +20,7 @@ from gooey.gui.components.sidebar import Sidebar from gooey.gui.components.tabbar import Tabbar from gooey.gui.lang.i18n import _ from gooey.gui.processor import ProcessController +from gooey.gui.util.time import Timing from gooey.gui.pubsub import pub from gooey.gui.util import wx_util from gooey.gui.util.wx_util import transactUI @@ -45,6 +46,7 @@ class GooeyApplication(wx.Frame): self.footer = Footer(self, buildSpec) self.console = Console(self, buildSpec) self.layoutComponent() + self.timer = Timing(self) self.clientRunner = ProcessController( self.buildSpec.get('progress_regex'), @@ -63,6 +65,7 @@ class GooeyApplication(wx.Frame): pub.subscribe(events.CONSOLE_UPDATE, self.console.logOutput) pub.subscribe(events.EXECUTION_COMPLETE, self.onComplete) pub.subscribe(events.PROGRESS_UPDATE, self.footer.updateProgressBar) + pub.subscribe(events.TIME_UPDATE, self.footer.updateTimeRemaining) # Top level wx close event self.Bind(wx.EVT_CLOSE, self.onClose) @@ -91,6 +94,7 @@ class GooeyApplication(wx.Frame): else: config.displayErrors() self.Layout() + def onEdit(self): @@ -236,6 +240,7 @@ class GooeyApplication(wx.Frame): self.header.setSubtitle(self.buildSpec['program_description']) self.footer.showButtons('cancel_button', 'start_button') self.footer.progress_bar.Show(False) + self.footer.time_remaining_text.Show(False) def showConsole(self): @@ -246,6 +251,10 @@ class GooeyApplication(wx.Frame): self.header.setSubtitle(_('running_msg')) self.footer.showButtons('stop_button') self.footer.progress_bar.Show(True) + self.footer.time_remaining_text.Show(False) + if self.buildSpec.get('timing_options')['show_time_remaining']: + self.timer.start() + self.footer.time_remaining_text.Show(True) if not self.buildSpec['progress_regex']: self.footer.progress_bar.Pulse() @@ -258,6 +267,12 @@ class GooeyApplication(wx.Frame): else ['edit_button', 'close_button']) self.footer.showButtons(*buttons) self.footer.progress_bar.Show(False) + if self.buildSpec.get('timing_options')['show_time_remaining']: + self.timer.stop() + self.footer.time_remaining_text.Show(True) + if self.buildSpec.get('timing_options')['hide_time_remaining_on_complete']: + self.footer.time_remaining_text.Show(False) + def showSuccess(self): diff --git a/gooey/gui/events.py b/gooey/gui/events.py index 47916db..bca961c 100644 --- a/gooey/gui/events.py +++ b/gooey/gui/events.py @@ -21,6 +21,7 @@ LIST_BOX = wx.Window.NewControlId() CONSOLE_UPDATE = wx.Window.NewControlId() EXECUTION_COMPLETE = wx.Window.NewControlId() PROGRESS_UPDATE = wx.Window.NewControlId() +TIME_UPDATE = wx.Window.NewControlId() USER_INPUT = wx.Window.NewControlId() diff --git a/gooey/gui/processor.py b/gooey/gui/processor.py index c7bc756..1310cf9 100644 --- a/gooey/gui/processor.py +++ b/gooey/gui/processor.py @@ -13,8 +13,7 @@ from gooey.util.functional import unit, bind class ProcessController(object): - def __init__(self, progress_regex, progress_expr, hide_progress_msg, - encoding, shell=True): + def __init__(self, progress_regex, progress_expr, hide_progress_msg,encoding, shell=True): self._process = None self.progress_regex = progress_regex self.progress_expr = progress_expr @@ -69,6 +68,7 @@ class ProcessController(object): if not line: break _progress = self._extract_progress(line) + pub.send_message(events.PROGRESS_UPDATE, progress=_progress) if _progress is None or self.hide_progress_msg is False: pub.send_message(events.CONSOLE_UPDATE, diff --git a/gooey/gui/util/functional.py b/gooey/gui/util/functional.py index e6212fa..d0d2a70 100644 --- a/gooey/gui/util/functional.py +++ b/gooey/gui/util/functional.py @@ -1,6 +1,30 @@ +''' +Utils for functional methodologies throughout Gooey +''' +def merge_dictionaries(x,y): + """ + Merge 2 dictionaries with y taking overwriting x if a key collision is found + This is mainly useful for maintaining the dictionary arguments to allow for more expressive & extensible arguments. + https://stackoverflow.com/questions/38987/how-do-i-merge-two-dictionaries-in-a-single-expression-in-python-taking-union-o + Args: + x (dict): Input dictionary + y (dict): Input dictionary + Returns: + The combined dictionary of x & y with y taking preference on the occasion of key collision + """ + if x is None: + x = {} + if y is None: + y = {} + try: + return {**x,**y} + except: + z = x.copy() + z.update(y) + return z diff --git a/gooey/gui/util/time.py b/gooey/gui/util/time.py new file mode 100644 index 0000000..89fb98b --- /dev/null +++ b/gooey/gui/util/time.py @@ -0,0 +1,96 @@ +""" +Module for evaluating time elapsed & time remaining from progress +""" +import wx +from gooey.gui.pubsub import pub +from gooey.gui import events + +class Timing(object): + + def __init__(self, parent): + self.startTime = 0 + self.estimatedRemaining = None + self.wxTimer = wx.Timer(parent) + self.parent = parent + parent.Bind(wx.EVT_TIMER, self.publishTime, self.wxTimer) + + pub.subscribe(events.PROGRESS_UPDATE, self._updateEstimate) + + def _updateEstimate(self, *args, **kwargs): + prog = kwargs.get('progress') + if(not prog): + self.estimatedRemaining = None + return + if(prog > 0): + self.estimatedRemaining = estimate_time_remaining(prog,self.startTime) + + def publishTime(self, *args, **kwargs): + pub.send_message( + events.TIME_UPDATE, + start=self.startTime, + current=get_current_time(), + elapsed_time=format_interval(get_elapsed_time(self.startTime)), + estimatedRemaining=format_interval(self.estimatedRemaining)) + + def start(self): + self.startTime = get_current_time() + self.estimatedRemaining = None + self.wxTimer.Start() + + def stop(self): + self.wxTimer.Stop() + +def format_interval(timeValue): + """ + Formats a number of seconds as a clock time, [H:]MM:SS + Parameters + ---------- + t : int + Number of seconds. + Returns + ------- + out : str + [H:]MM:SS + """ + # https://github.com/tqdm/tqdm/blob/0cd9448b2bc08125e74538a2aea6af42ee1a7b6f/tqdm/std.py#L228 + try: + mins, s = divmod(int(timeValue), 60) + h, m = divmod(mins, 60) + if h: + return '{0:d}:{1:02d}:{2:02d}'.format(h, m, s) + else: + return '{0:02d}:{1:02d}'.format(m, s) + except: + return None + +def get_elapsed_time(startTime): + """ + Get elapsed time in form of seconds. Provide a start time in seconds as float. + + Args: + startTime (float): Start time to compare against in seconds. + + Returns: + float: Time between start time and now + """ + return get_current_time() - startTime + +def estimate_time_remaining(progress,startTime): + # https://github.com/tqdm/tqdm/blob/0cd9448b2bc08125e74538a2aea6af42ee1a7b6f/tqdm/std.py#L392 + # https://github.com/tqdm/tqdm/blob/0cd9448b2bc08125e74538a2aea6af42ee1a7b6f/tqdm/std.py#L417 + _rate = progress / get_elapsed_time(startTime) + return ((100 - progress) / _rate) + +def get_current_time(): + """ + Returns a float of the current time in seconds. Attempt to import perf_counter (more accurate in 3.4+), otherwise utilise timeit. + + Returns: + float: Current time in seconds from performance counter. + """ + try: + from time import perf_counter + return perf_counter() + except: + import timeit + return timeit.default_timer() diff --git a/gooey/python_bindings/config_generator.py b/gooey/python_bindings/config_generator.py index 5ee0b89..5b48a4c 100644 --- a/gooey/python_bindings/config_generator.py +++ b/gooey/python_bindings/config_generator.py @@ -5,6 +5,8 @@ import textwrap from gooey.python_bindings import argparse_to_json from gooey.gui.util.quoting import quote from gooey.python_bindings import constants +from gooey.python_bindings import gooey_decorator +from gooey.gui.util.functional import merge_dictionaries default_layout = { 'widgets': [{ @@ -69,6 +71,7 @@ def create_from_parser(parser, source_path, **kwargs): 'progress_regex': kwargs.get('progress_regex'), 'progress_expr': kwargs.get('progress_expr'), 'hide_progress_msg': kwargs.get('hide_progress_msg', False), + 'timing_options': merge_dictionaries(gooey_decorator.defaults.get('timing_options'),kwargs.get('timing_options')), 'disable_progress_bar_animation': kwargs.get('disable_progress_bar_animation'), 'disable_stop_button': kwargs.get('disable_stop_button'), diff --git a/gooey/python_bindings/gooey_decorator.py b/gooey/python_bindings/gooey_decorator.py index e8610d6..17d3f3a 100644 --- a/gooey/python_bindings/gooey_decorator.py +++ b/gooey/python_bindings/gooey_decorator.py @@ -48,6 +48,10 @@ defaults = { 'navigation': 'SIDEBAR', # TODO: add this to the docs 'tabbed_groups': False, 'use_cmd_args': False, + 'timing_options': { + 'show_time_remaining': False, + 'hide_time_remaining_on_complete': True + } } # TODO: kwargs all the things diff --git a/gooey/tests/test_processor.py b/gooey/tests/test_processor.py index c0a7530..0fd8666 100644 --- a/gooey/tests/test_processor.py +++ b/gooey/tests/test_processor.py @@ -15,7 +15,6 @@ class TestProcessor(unittest.TestCase): processor = ProcessController(r"total: (\d+)%$", None, False, 'utf-8') self.assertEqual(processor._extract_progress(b'my cool total: 100%'), 100) - def test_extract_progress_returns_none_if_no_regex_supplied(self): processor = ProcessController(None, None, False, 'utf-8') self.assertIsNone(processor._extract_progress(b'Total progress: 100%')) @@ -29,14 +28,12 @@ class TestProcessor(unittest.TestCase): def test_eval_progress(self): # given a match in the string, should eval the result regex = r'(\d+)/(\d+)$' - processor = ProcessController(regex, r'x[0] / x[1]', False, 'utf-8') + processor = ProcessController(regex, r'x[0] / x[1]', False,False, 'utf-8') match = re.search(regex, '50/50') self.assertEqual(processor._eval_progress(match), 1.0) - - def test_eval_progress_returns_none_on_failure(self): # given a match in the string, should eval the result regex = r'(\d+)/(\d+)$' - processor = ProcessController(regex, r'x[0] *^/* x[1]', False, 'utf-8') + processor = ProcessController(regex, r'x[0] *^/* x[1]', False, False,'utf-8') match = re.search(regex, '50/50') self.assertIsNone(processor._eval_progress(match)) diff --git a/gooey/tests/test_time_remaining.py b/gooey/tests/test_time_remaining.py new file mode 100644 index 0000000..6a26089 --- /dev/null +++ b/gooey/tests/test_time_remaining.py @@ -0,0 +1,63 @@ +import time +import unittest +from argparse import ArgumentParser +from itertools import * + +from tests.harness import instrumentGooey + +class TestFooterTimeRemaining(unittest.TestCase): + + def make_parser(self): + parser = ArgumentParser(description='description') + return parser + + def test_time_remaining_visibility(self): + for testdata in self.testcases(): + with self.subTest(testdata): + with instrumentGooey(self.make_parser(), timing_options=testdata) as (app, gooeyApp): + + gooeyApp.showConsole() + footer = gooeyApp.footer + + + self.assertEqual( + footer.time_remaining_text.Shown, + testdata.get('show_time_remaining',False) + ) + + def test_time_remaining_visibility_on_complete(self): + for testdata in self.testcases(): + with self.subTest(testdata): + with instrumentGooey(self.make_parser(), timing_options=testdata) as (app, gooeyApp): + + gooeyApp.showComplete() + footer = gooeyApp.footer + + + if not testdata.get('show_time_remaining') and testdata: + self.assertEqual( + footer.time_remaining_text.Shown, + testdata.get('hide_time_remaining_on_complete',True) + ) + else: + return True + + def testcases(self): + """ + Generate a powerset of all possible combinations of + the header parameters (empty, some present, all present, all combos) + """ + iterable = product(['show_time_remaining', 'hide_time_remaining_on_complete'], [True, False]) + allCombinations = list(powerset(iterable)) + return [{k: v for k,v in args} + for args in allCombinations] + + +def powerset(iterable): + "powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)" + s = list(iterable) + return chain.from_iterable(combinations(s, r) for r in range(len(s)+1)) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/gooey/tests/test_util.py b/gooey/tests/test_util.py new file mode 100644 index 0000000..b0f7e36 --- /dev/null +++ b/gooey/tests/test_util.py @@ -0,0 +1,32 @@ +import re +import unittest + +from gooey.gui.util.time import get_current_time,get_elapsed_time,estimate_time_remaining,format_interval + + +class TestTimeUtil(unittest.TestCase): + def test_time_elapsed(self): + # Check that time elapsed is greater than zero + _start_time = get_current_time() + elapsed = get_elapsed_time(_start_time) + self.assertGreater(elapsed,0) + + def test_time_remaining(self): + # Check that time elapsed is greater than zero + _start_time = get_current_time() + remaining = estimate_time_remaining(30,_start_time) + self.assertGreater(remaining,0) + + def test_current_time(self): + # Test that current time is greater than zero + _start_time = get_current_time() + self.assertGreater(_start_time,0) + + + def test_format_interval(self): + # Test same as TQDM https://github.com/tqdm/tqdm/blob/0cd9448b2bc08125e74538a2aea6af42ee1a7b6f/tqdm/tests/tests_tqdm.py#L234 + # but in unittest form + + self.assertEqual(format_interval(60), '01:00') + self.assertEqual(format_interval(6160), '1:42:40') + self.assertEqual(format_interval(238113), '66:08:33')