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.
Related Information
- Catching errors thread on PyQt mailing list
Sorry to be commenting on such an old article, but I’d like to share a bit of knowledge I’ve picked up. First of all, I really wish I’d seen this article a few months ago. One problem with using the decorator solution (which was the first thing I tried) is that you have to manage decorators. If you decorate a helper function on accident, your exception can get caught prematurely, and processes you want to stop will continue to run. You can fix this by separating your event functions from your helper functions, but then you have to make sure you remember to decorate all of your event functions (i.e. buttons) from your helper functions.
Thanks for the feedback. I’m glad you found the article helpful and that you were willing to share your tips for my readers.