chriskiehl / Gooey

Turn (almost) any Python command line program into a full GUI application with one line

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

wxAssertionError with dynamic values on 1.2.1-release with Python3.9.14/MacOS12.0.1

abrichr opened this issue · comments

  • OS: MacOS 12.0.1
  • Python Version: Python 3.9.14
  • Gooey Version: 1.2.1-release (git branch) / 1.2.0-ALPHA (gooey.__version__)
  • Thorough description of problem :

Steps to reproduce:

  1. run python minimum_working_example.py on MacOS
  2. enter a value for the parameter "foo" in the UI
  3. click "start"
  4. observe exception:
Traceback (most recent call last):
...
...lib/python3.9/site-packages/gooey/gui/util/time.py", line 38, in start
    self.wxTimer.Start()
wx._core.wxAssertionError: C++ assertion ""m_milli > 0"" failed at /Users/robind/projects/bb2/dist-osx-py39/build/ext/wxWidgets/src/osx/core/timer.cpp(69) in Start(): invalid value for timer timeout
  • Expected Behavior: No exception is raised on MacOS; monkeypatches are not required
  • Actual Behavior: An exception is raised on MacOS; monkeypatches are required
  • A minimal code example:

minimum_working_example.py:

from typing import Mapping, Any, Optional

from gooey import Gooey, GooeyParser, Events, types


def monkeypatch_frame():
    # https://github.com/chriskiehl/Gooey/issues/826#issuecomment-1240180894
    from rewx import widgets
    from rewx.widgets import set_basic_props
    from rewx.dispatch import update
    import wx
    import sys

    @update.register(wx.Frame)
    def frame(element, instance: wx.Frame):
        props = element['props']
        set_basic_props(instance, props)
        if 'title' in props:
            instance.SetTitle(props['title'])
        if 'show' in props:
            instance.Show(props['show'])
        if 'icon_uri' in props:
            icon = wx.Icon(props['icon_uri'])
            instance.SetIcon(icon)
            if sys.platform != 'win32':
                # OSX needs to have its taskbar icon explicitly set
                # bizarrely, wx requires the TaskBarIcon to be attached to the Frame
                # as instance data (self.). Otherwise, it will not render correctly.
                try:
                    frame.taskbarIcon = wx.adv.TaskBarIcon(iconType=wx.adv.TBI_DOCK)
                except wx._core.wxAssertionError:
                    pass
                frame.taskbarIcon.SetIcon(icon)
        else:
            instance.SetIcon(wx.Icon(os.path.join(dirname, 'icon.png')))
        if 'on_close' in props:
            instance.Bind(wx.EVT_CLOSE, props['on_close'])
        return instance
    widgets.frame = frame


def monkeypatch_communicate():
    # https://github.com/chriskiehl/Gooey/issues/813#issuecomment-1113911923
    import sys
    if sys.platform != 'win32':
        import subprocess
        from subprocess import CalledProcessError
        from json import JSONDecodeError

        from gooey.gui import seeder
        from gooey.python_bindings.types import Try, Success, Failure
        from gooey.python_bindings.coms import deserialize_inbound


        def communicate(cmd, encoding) -> Try:
            """
            Invoke the processes specified by `cmd`.
            Assumes that the process speaks JSON over stdout. Non-json response
            are treated as an error.

            Implementation Note: I don't know why, but `Popen` is like ~5-6x faster
            than `check_output`. in practice, it means waiting for ~1/10th
            of a second rather than ~7/10ths of a second. A
            difference which is pretty weighty when there's a
            user waiting on the other end.
            """

            cmd = cmd.replace("'", "").split()
            try:
                proc = subprocess.Popen(
                    cmd,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                )
                out, err = proc.communicate()
                if out and proc.poll() == 0:
                    return Success(deserialize_inbound(out, encoding))
                else:
                    return Failure(CalledProcessError(proc.returncode, cmd, output=out, stderr=err))
            except JSONDecodeError as e:
                return Failure(e)
        seeder.communicate = communicate

monkeypatch_frame()
monkeypatch_communicate()


def dump_state(name, args, state):
    import pickle
    with open(f"{name}.pkl", "wb") as f:
        pickle.dump({"args": args, "state": state}, f)


def on_success(args: Mapping[str, Any], state: types.PublicGooeyState) -> Optional[types.PublicGooeyState]:
    """
    You can do anything you want in the handler including 
    returning an updated UI state for your next run!   
    """ 
    dump_state("success", args, state)
    """
    {'args': Namespace(foo='asdf'),
     'state': {'active_form': [{'enabled': True,
                                'error': '',
                                'id': 'foo',
                                'placeholder': '',
                                'type': 'TextField',
                                'value': 'asdf',
                                'visible': True}]}}
    """
    state["active_form"][0]["value"] = "updated in on_success"
    return state

    
def on_error(args: Mapping[str, Any], state: types.PublicGooeyState) -> Optional[types.PublicGooeyState]:
    """
    You can do anything you want in the handler including 
    returning an updated UI state for your next run!   
    """ 
    dump_state("failure", args, state)
    state["active_form"][0]["value"] = "updated in on_failure"
    return state    


@Gooey(
    use_events=[
        Events.ON_SUCCESS,
        Events.ON_ERROR,
        Events.VALIDATE_FORM,
    ],
)
def main():
    parser = GooeyParser(
        on_success=on_success,
        on_error=on_error,
    )
    parser.add_argument("foo")
    parser.parse_args()
    
    # comment out for on_success; otherwise on_failure is called
    #raise Exception("this is an exception")


if __name__ == "__main__":
    main()

To clarify:

  • on_success() is called if no Exception is raised after the call to parser.parse_args(), otherwise on_failure() is called
  • no additional Exceptions are raised on Windows
  • the following additional Exception is raised on MacOS when a parameter value is supplied and the start button is pressed, regardless of success or failure: wx._core.wxAssertionError: C++ assertion ""m_milli > 0"" failed at /Users/robind/projects/bb2/dist-osx-py39/build/ext/wxWidgets/src/osx/core/timer.cpp(69) in Start(): invalid value for timer timeout
  • the state is successfully modified in both on_success and on_failure
  • if Events.VALIDATE_FORM is not included in the use_events parameter to Gooey() and a value for the parameter is not supplied in the UI, an error is printed in the UI on both platforms:
usage: minimum_working_example.py [-h] [--ignore-gooey] foo
ui_min.py: error: the following arguments are required: foo
Command '['/.../bin/python', '-u', 'minimum_working_example.py', '--gooey-state', 'eyJhY3RpdmVfZm9ybSI6IFt7ImlkIjogImZvbyIsICJ0eXBlIjogIlRleHRGaWVsZCIsICJ2YWx1ZSI6ICIiLCAicGxhY2Vob2xkZXIiOiAiIiwgImVycm9yIjogIiIsICJlbmFibGVkIjogdHJ1ZSwgInZpc2libGUiOiB0cnVlfV19', '--gooey-run-is-failure']' returned non-zero exit status 2.b''b'usage: minimum_working_example.py [-h] [--gooey-state GOOEY_STATE] [--gooey-run-is-success]\n                 [--gooey-run-is-failure]\n                 foo\minimum_working_example.py: error: the following arguments are required: foo\n'
+=======================+
|Gooey Unexpected Error!|
+=======================+

Gooey encountered an unexpected error while trying to communicate 
with your program to process one of the ('VALIDATE_FORM', 'ON_SUCCESS', 'ON_ERROR') events.

These features are new and experimental! You may have encountered a bug! 

You can open a ticket with a small reproducible example here
https://github.com/chriskiehl/Gooey/issues

Apologies for the verbosity, but the intended behavior and required configuration is not clear from the documentation. Perhaps this could be improved as well.

And of course ideally the monkeypatches from #826 (comment) and #813 (comment) would not be required