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')