One of my friends on the wxPython Google Group asked how to catch any exception that happens in wxPython. The problem is complicated somewhat because wxPython is a wrapper on top of a C++ library (wxWidgets). You can read about the issue on the wxPython wiki. Several wxPython users mentioned using Python’s sys.excepthook to catch the errors. So I decided to write up an example showing how that worked based on something that Andrea Gavana posted on the aforementioned thread. We will also look at the solution that is in that wiki link.


Catching all the Errors with sys.excepthook

It ended up being a bit more work than I expected as I ended up needing to import Python’s traceback module and I decided I wanted to display the error, so I also created a dialog. Let’s take a look at the code:

import sys
import traceback
import wx
import wx.lib.agw.genericmessagedialog as GMD
 
########################################################################
class Panel(wx.Panel):
    """"""
 
    #----------------------------------------------------------------------
    def __init__(self, parent):
        """Constructor"""
        wx.Panel.__init__(self, parent)
 
        btn = wx.Button(self, label="Raise Exception")
        btn.Bind(wx.EVT_BUTTON, self.onExcept)
 
    #----------------------------------------------------------------------
    def onExcept(self, event):
        """
        Raise an error
        """
        1/0
 
########################################################################
class Frame(wx.Frame):
    """"""
 
    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        wx.Frame.__init__(self, None, title="Exceptions")
        sys.excepthook = MyExceptionHook
        panel = Panel(self)
        self.Show()
 
########################################################################
class ExceptionDialog(GMD.GenericMessageDialog):
    """"""
 
    #----------------------------------------------------------------------
    def __init__(self, msg):
        """Constructor"""
        GMD.GenericMessageDialog.__init__(self, None, msg, "Exception!",
                                          wx.OK|wx.ICON_ERROR)
 
 
#----------------------------------------------------------------------
def MyExceptionHook(etype, value, trace):
    """
    Handler for all unhandled exceptions.
 
    :param `etype`: the exception type (`SyntaxError`, `ZeroDivisionError`, etc...);
    :type `etype`: `Exception`
    :param string `value`: the exception error message;
    :param string `trace`: the traceback header, if any (otherwise, it prints the
     standard Python header: ``Traceback (most recent call last)``.
    """
    frame = wx.GetApp().GetTopWindow()
    tmp = traceback.format_exception(etype, value, trace)
    exception = "".join(tmp)
 
    dlg = ExceptionDialog(exception)
    dlg.ShowModal()
    dlg.Destroy()    
 
#----------------------------------------------------------------------
if __name__ == "__main__":
    app = wx.App(False)
    frame = Frame()
    app.MainLoop()

This code is a bit involved so we’ll spend a little time on each section. The Panel code has one button on it that will call a method that will cause a ZeroDivisionError. In the Frame class, we set sys.excepthook to a custom function, MyExceptionHook. Let’s take a look at that:

#----------------------------------------------------------------------
def MyExceptionHook(etype, value, trace):
    """
    Handler for all unhandled exceptions.
 
    :param `etype`: the exception type (`SyntaxError`, `ZeroDivisionError`, etc...);
    :type `etype`: `Exception`
    :param string `value`: the exception error message;
    :param string `trace`: the traceback header, if any (otherwise, it prints the
     standard Python header: ``Traceback (most recent call last)``.
    """
    frame = wx.GetApp().GetTopWindow()
    tmp = traceback.format_exception(etype, value, trace)
    exception = "".join(tmp)
 
    dlg = ExceptionDialog(exception)
    dlg.ShowModal()
    dlg.Destroy()

This function accepts 3 arguments: etype, value and the traceback. We use the traceback module to put those pieces together to get us a full traceback that we can pass to our message dialog.


Using the Original Error Catching Method

Robin Dunn (creator of wxPython) mentioned that there was a solution on the wiki in that same thread above and that he’d like to see it used as a decorator. Here is my implementation:

import logging
import wx
import wx.lib.agw.genericmessagedialog as GMD
 
########################################################################
class ExceptionLogging(object):
 
    #----------------------------------------------------------------------
    def __init__(self, fn):
        self.fn = fn
 
        # create logging instance
        self.log = logging.getLogger("wxErrors")
        self.log.setLevel(logging.INFO)
 
        # create a logging file handler / formatter
        log_fh = logging.FileHandler("error.log")
        formatter = logging.Formatter("%(asctime)s - %(name)s - %(message)s")
        log_fh.setFormatter(formatter)
        self.log.addHandler(log_fh)
 
    #----------------------------------------------------------------------
    def __call__(self,evt):
        try:
            self.fn(self, evt)
        except Exception, e:
            self.log.exception("Exception")
 
########################################################################
class Panel(wx.Panel):
    """"""
 
    #----------------------------------------------------------------------
    def __init__(self, parent):
        """Constructor"""
        wx.Panel.__init__(self, parent)
 
        btn = wx.Button(self, label="Raise Exception")
        btn.Bind(wx.EVT_BUTTON, self.onExcept)
 
    #----------------------------------------------------------------------
    @ExceptionLogging
    def onExcept(self, event):
        """
        Raise an error
        """
        1/0
 
########################################################################
class Frame(wx.Frame):
    """"""
 
    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        wx.Frame.__init__(self, None, title="Exceptions")
        panel = Panel(self)
        self.Show()
 
#----------------------------------------------------------------------
if __name__ == "__main__":
    app = wx.App(False)
    frame = Frame()
    app.MainLoop()

We use the custom exception class to log errors. To apply that class to our event handlers, we decorate them with the class using @classname, which in this case translates into @ExceptionLogging. Thus whenever this event handler is called, it is run through the decorator which wraps the event handler in a try/except and logs all exceptions to disk. I’m not entirely sure if both of the methods mentioned in this article can catch the same errors or not. Feel free to let me know in the comments.

Print Friendly