Problem with event watcher

Hi, Posts :wave:

While playing with an event watcher, I ran into a mysterious crash. The first code works fine, but the second code has a problem. If you close the Event Watcher window and press the button, the program will crash silently. I couldn’t find out where this problem comes from. Am I overlooking something?

  1. The following code works fine.
import wx
import wx.lib.eventwatcher as ew

class TestPanel(wx.Panel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        self.btn = wx.Button(self, label="Hello, wxPython!")
        
        self.btn.Bind(wx.EVT_BUTTON, print)

if __name__ == "__main__":
    app = wx.App()
    frm = wx.Frame(None)
    frm.panel = TestPanel(frm)
    watcher = ew.EventWatcher(frm) # no crash after closing window
    watcher.watch(frm.panel.btn)
    watcher.Show()
    frm.Show()
    app.MainLoop()
  1. The following code crashes.
import wx
import wx.lib.eventwatcher as ew

class TestPanel(wx.Panel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        self.btn = wx.Button(self, label="Hello, wxPython!")
        
        watcher = ew.EventWatcher(None) # silent crash after closing window
        watcher.watch(self.btn)
        watcher.Show()
        ## close -> unwatch => crash
        
        self.btn.Bind(wx.EVT_BUTTON, print)

if __name__ == "__main__":
    app = wx.App()
    frm = wx.Frame(None)
    frm.panel = TestPanel(frm)
    frm.Show()
    app.MainLoop()

I think this is a bug since 4.1 and reported it to GitHub as an issue.

to mimic the binding order of the first example, which looks quite natural to me, try

import wx
import wx.lib.eventwatcher as ew

class TestPanel(wx.Panel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.btn = wx.Button(self, label="Hello, wxPython!")
        self.btn.Bind(wx.EVT_BUTTON, print)

        watcher = ew.EventWatcher(args[0]) # silent crash after closing window
        watcher.watch(self.btn)
        watcher.Show()
        ## close -> unwatch => crash

if __name__ == "__main__":
    app = wx.App()
    frm = wx.Frame(None)
    frm.panel = TestPanel(frm)
    frm.Show()
    app.MainLoop()

Hi, da-dada
After I posted, I noticed that this code works, as you suggested. But if the bind and unibind changed like this,

watcher.watch(self.btn) # bind many events including wx.EVT_BUTTON
self.btn.Bind(wx.EVT_BUTTON, print)
watcher.unwatch() # unbind events 

then, It will crash. I thought the crash was due to the event watcher binding hundreds of events, but the cause is actually due to the unbinding order.
I think this is not serious normally, but crucial when frequent binding and unbinding of events occurs. It will pose a potential bug unless if we deal with the message chain like a FIFO list.

I’ve been down to the river at Touchstone’s and the assistant there advised me to try

import wx
import wx.lib.eventwatcher as ew

class TestPanel(wx.Panel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.btn = wx.Button(self, label="Hello, wxPython!")

        watcher = ew.EventWatcher(args[0]) # silent crash after closing window
        watcher.watch(self.btn)
        watcher.Show()
        ## close -> unwatch => crash

        self.Bind(wx.EVT_BUTTON, print, self.btn)

if __name__ == "__main__":
    app = wx.App()
    frm = wx.Frame(None)
    frm.panel = TestPanel(frm)
    frm.Show()
    app.MainLoop(

here is a more focused snippet which parades the sophistication of Unbind :rofl:

import wx

class Gui(wx.Frame):

    def __init__(self, parent):
        super().__init__(parent)

        btn = wx.Button(self, label='Button')

        btn.Bind(wx.EVT_BUTTON, self.evt_trap)
        btn.Bind(wx.EVT_BUTTON, self.evt_btn)
        # does  n o t   work, unless 'source' is given at Bind
        btn.Unbind(wx.EVT_BUTTON, handler=self.evt_trap)
        # works
        # btn.Unbind(wx.EVT_BUTTON, source=btn, handler=self.evt_trap)

        self.SetTitle('Unbind trap: just press Button')
        self.Centre()

        self.Show()

    def evt_btn(self, evt):
        print('evt_btn')

    def evt_trap(self, evt):
        print('evt_trap')

if __name__ == '__main__':
    app = wx.App()
    Gui(None)
    app.MainLoop()
1 Like

GREAT! :+1: It works! Thank you very much for this suggestion.
I will use this until the next release.

Thanks are also due to @swt2c, who made a PR for this issue.

Hi, da-dada!
I have to report bad news. The following code looked like working to me, but actually, it doesn’t unbind the handler!

To confirm it, please add evt.Skip() to both handlers, or check the returned value btn.Unbind being False.

I must say that we got trapped by evt_trap!!!
:rofl:
Anyway, Thank you for taking your time!
I updated my test code as follows. Note that Unbind returns False, that means it failed to unbind.

Code Example (click to expand)
import wx

class TestPanel(wx.Panel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        self.btn = wx.Button(self, label="Hello, wxPython!")
        
        self.btn.Bind(wx.EVT_BUTTON, self.onButton1)
        self.btn.Bind(wx.EVT_BUTTON, self.onButton2)
        
        if 0:
            ## no problem
            self.btn.Unbind(wx.EVT_BUTTON, handler=self.onButton2)
            self.btn.Unbind(wx.EVT_BUTTON, handler=self.onButton1)
        elif 0:
            ## Reversing the unbind order will break the linkage of the event chain
            self.btn.Unbind(wx.EVT_BUTTON, handler=self.onButton1)
            self.btn.Unbind(wx.EVT_BUTTON, handler=self.onButton2)
        else:
            ## Unbind returns False(s)
            ret1 = self.btn.Unbind(wx.EVT_BUTTON, source=self.btn, handler=self.onButton1)
            ret2 = self.btn.Unbind(wx.EVT_BUTTON, source=self.btn, handler=self.onButton2)
            print("ret1, ret2 =", ret1, ret2)
    
    def onButton1(self, evt):
        print(1)
        evt.Skip()
    
    def onButton2(self, evt):
        print(2)
        evt.Skip()

app = wx.App()
frm = wx.Frame(None)
frm.panel = TestPanel(frm)
frm.Show()
app.MainLoop()

Well, I have to admit I just watched the crash! But even without crash it must look terrible down there…
Nevertheless, I’ve never used Unbind (I’m always glad if my Binds work ok). What use case have you got in mind? (the eventwatcher is useful even without clearing)

By for now, eventwatcher is the only use-case for me.
If we want to debug the code, we would do like this,

import pdb; pdb.set_trace()

however, it is hard to track running objects. You know, we already have a nice inspection tool.

from wx.lib.inspection import InspectionTool
it = InspectionTool()
it.Show(target)

and from InspectionTool, the eventwatcher can be launched to watch events of the target. But if the target is more specific, it is more useful to launch the eventwatcher directly embedding the following code,

import wx.lib.eventwatcher as ew
watcher = ew.EventWatcher(None)
watcher.watch(target)
watcher.Show()

Now let’s get back to the first post, the second case; I ran into the crash.