sirk390 / wxasync

asyncio support for wxpython
MIT License
76 stars 9 forks source link

shutdown of wxasync raises exception if ever open a frame #3

Closed abulka closed 5 years ago

abulka commented 5 years ago

My wxasync based app, (wxPython 4.04, Python 3.7.1) is generating exceptions as I shut down. The error happens only if my app ever opens another frame (like the wxPython PrintFramework window or a Help window). I don't get the error immediately though - only when exiting the main app and shutting down the main app/frame.

If I never open another frame from my main wxPython app, then the shutdown happens cleanly.

The two exceptions seem to be related, in that a bound object cannot be found. I haven't had time to create a simple repro case yet.

Traceback (most recent call last):
  File "/home/andy/.pyenv/versions/3.7.1/lib/python3.7/site-packages/wxasync.py", line 51, in <lambda>
    object.Bind(event_binder, lambda event: self.OnEvent(event, object, event_binder.typeId), id=id, id2=id2)
  File "/home/andy/.pyenv/versions/3.7.1/lib/python3.7/site-packages/wxasync.py", line 64, in OnEvent
    for asyncallback in self.BoundObjects[obj][type]:
KeyError: <wx._core.Frame object at 0x7fec2cca5c18>

and

Traceback (most recent call last):
  File "/home/andy/.pyenv/versions/3.7.1/lib/python3.7/site-packages/wxasync.py", line 49, in <lambda>
    object.Bind(wx.EVT_WINDOW_DESTROY, lambda event: self.OnDestroy(event, object))
  File "/home/andy/.pyenv/versions/3.7.1/lib/python3.7/site-packages/wxasync.py", line 85, in OnDestroy
    del self.BoundObjects[obj]
sirk390 commented 5 years ago

Thanks Andy for all the good feedback and issues. If you can provide me a link to your app, it would help to understand the problem better. Currently, I could fix it by checking if the object is in the structure before deleting it, but that feels a bit like coding by trial and error.

abulka commented 5 years ago

I've gotten a small repro case going, which should be easier to debug. Its the same repro case I created for #2 with an additional button for popping up the frame.

  1. Click the button to open the frame.
  2. Close the popup frame.
  3. Click the button to open the frame again.
  4. Close the popup frame.
  5. Notice the exception.
  6. Close the main frame.
  7. Notice the exception.

import wx
from wxasync import AsyncBind, WxAsyncApp, StartCoroutine
import asyncio
from asyncio.events import get_event_loop
import time

import wx.lib.newevent
SomeNewEvent, EVT_SOME_NEW_EVENT = wx.lib.newevent.NewEvent()
SomeNewEventAsync, EVT_SOME_NEW_EVENT_ASYNC = wx.lib.newevent.NewEvent()

class TestFrame(wx.Frame):
    def __init__(self, parent=None):
        super(TestFrame, self).__init__(parent)
        vbox = wx.BoxSizer(wx.VERTICAL)
        button1 =  wx.Button(self, label="AsyncBind (original wxasync example)")
        button2 =  wx.Button(self, label="Start/stop clock via StartCoroutine()")
        button3 =  wx.Button(self, label="Emit Custom Event (sync)")
        button4 =  wx.Button(self, label="Emit Custom Event (async)")
        button5 =  wx.Button(self, label="Show Frame")
        self.edit =  wx.StaticText(self, style=wx.ALIGN_CENTRE_HORIZONTAL|wx.ST_NO_AUTORESIZE)
        self.edit_timer =  wx.StaticText(self, style=wx.ALIGN_CENTRE_HORIZONTAL|wx.ST_NO_AUTORESIZE)
        vbox.Add(button1, 2, wx.EXPAND|wx.ALL)
        vbox.Add(button2, 2, wx.EXPAND|wx.ALL)
        vbox.Add(button3, 2, wx.EXPAND|wx.ALL)
        vbox.Add(button4, 2, wx.EXPAND|wx.ALL)
        vbox.Add(button5, 2, wx.EXPAND|wx.ALL)
        vbox.AddStretchSpacer(1)
        vbox.Add(self.edit, 1, wx.EXPAND|wx.ALL)
        vbox.Add(self.edit_timer, 1, wx.EXPAND|wx.ALL)
        self.SetSizer(vbox)
        self.Layout()
        self.clock_on = False

        """Original direct binding - works ok"""
        AsyncBind(wx.EVT_BUTTON, self.async_callback, button1)

        """
        Regular method calls StartCoroutine() - works ok
        No need for async/await syntax except on the final async method! Turtle avoidance success 
        because no need for async/await syntax turtles all the way up the calling chain.
        PROVISO: WxAsyncApp() object must be created first, frame creation within OnInit fails.
        """
        self.Bind(wx.EVT_BUTTON, self.regular_func_starts_coroutine, button2)

        """
        Regular method broadcast a custom event - works ok 
        But this is the synchronous version.  Its the async version we want to get working.
        """
        self.Bind(wx.EVT_BUTTON, self.regular_func_raises_custom_event, button3)
        self.Bind(EVT_SOME_NEW_EVENT, self.callback)  # bind custom event to synchronous handler - works OK

        """
        Regular method broadcast a custom event - doesn't work
        bind custom event to asynchronous handler - doesn't work
        """
        self.Bind(wx.EVT_BUTTON, self.regular_func_raises_custom_async_event, button4)
        AsyncBind(EVT_SOME_NEW_EVENT_ASYNC, self.async_callback, self)  # don't specify id

        """Show popup frame"""
        self.Bind(wx.EVT_BUTTON, self.on_show_frame, button5)

    def regular_func_raises_custom_event(self, event):
        print("trigger demo via custom event")
        # Create and post the event
        evt = SomeNewEvent(attr1="hello", attr2=654)
        wx.PostEvent(self, evt)

    def regular_func_raises_custom_async_event(self, event):
        print("trigger async demo via custom event")
        # Create and post the event
        evt = SomeNewEventAsync(attr1="hello", attr2=654)
        wx.PostEvent(self, evt)

    def regular_func_starts_coroutine(self, event):
        self.clock_on = not self.clock_on
        if self.clock_on:
            print(f"triggering an async call via StartCoroutine()")
            StartCoroutine(self.update_clock, self)
        else:
            print("clock flag off, coroutine will stop looping, drop through and complete")

    def on_show_frame(self, event):  # not used
        """manually build a frame with inner html window, no sizer involved"""
        class MyPopupFrame(wx.Frame):
            def __init__(self, parent, title):
                super(MyPopupFrame, self).__init__(parent, title=title)
        frm = MyPopupFrame(parent=self, title="Simple Popup Frame")
        frm.Show()

    def callback(self, event):
        self.edit.SetLabel("Button clicked (synchronous)")
        wx.SafeYield()
        time.sleep(1)
        self.edit.SetLabel("Working (synchronous)")
        wx.SafeYield()
        time.sleep(1)
        self.edit.SetLabel("Completed (synchronous)")
        wx.SafeYield()
        time.sleep(1)
        self.edit.SetLabel("")

    async def async_callback(self, event):
        self.edit.SetLabel("Button clicked")
        await asyncio.sleep(1)
        self.edit.SetLabel("Working")
        await asyncio.sleep(1)
        self.edit.SetLabel("Completed")
        await asyncio.sleep(1)
        self.edit.SetLabel("")

    async def update_clock(self):
        while self.clock_on:
            self.edit_timer.SetLabel(time.strftime('%H:%M:%S'))
            await asyncio.sleep(0.5)
        self.edit_timer.SetLabel("")

app = WxAsyncApp()
frame = TestFrame()
frame.Show()
app.SetTopWindow(frame)
loop = get_event_loop()
loop.run_until_complete(app.MainLoop())
sirk390 commented 5 years ago

I've added both parameters object and source in AsyncBind similar to when you do "object.Bind(EVT_BUTTON, source=self.button)"

Can you check if this commit works to fix you exceptions messages? https://github.com/sirk390/wxasync/commit/816344de3df88806cbc7e64a92ce9e479965013b

abulka commented 5 years ago

Nice, seems to be fixed now, thank you.

An official PyPi release with these changes would be great at some stage. :-)