wxPython: Catching Exceptions from Anywhere

The wxPython Google Group was discussing different methods of catching exceptions in wxPython the other day. If you use wxPython a lot, you will soon realize that some exceptions are difficult to catch. The wxPython Wiki explains why. Anyway, the fellows on the list were recommending the use of sys.excepthook. So I took one of the methods they mentioned and created a little example:

import sys
import traceback
import wx

########################################################################
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()

In this example, we create a panel with a button that will deliberately cause an exception to be raised. We catch the exception by redirecting sys.excepthook to our MyExceptionHook function. This function will format the traceback of the exception, format it to make it readable and then display a dialog with the exception information. Robin Dunn, creator of wxPython, thought it would be good if someone came up with a decorator that we could use to catch exception with that could then be added as an example to the wiki page.

My first idea for a decorator was the following:

import logging
import wx

########################################################################
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()

In this code, we create a class that creates a logging instance. Then we override the __call__ method to wrap a method call in an exception handler so we can catch exceptions. Basically what we're doing here is creating a class decorator. Next we decorate an event handler with our exception logging class. This wasn't exactly what Mr. Dunn wanted, as the decorator needed to be able to wrap other functions too. So I edited it a bit and came up with the following minor adjustment:

import logging
import wx

class ExceptionLogging(object):
    def __init__(self, fn, *args, **kwargs):
        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, *args, **kwargs):
        try:
            self.fn(self, *args, **kwargs)
        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()

This time the __call__ method can accept any number of arguments or keyword arguments, which gives it a bit more flexibility. This still wasn't what Robin Dunn wanted, so he wrote up the following example:

from __future__ import print_function

import logging
import wx

print(wx.version())

def exceptionLogger(func, loggerName=''):
    """
    A simple decorator that will catch and log any exceptions that may occur
    to the root logger.
    """
    assert callable(func)
    mylogger = logging.getLogger(loggerName)

    # wrap a new function around the callable
    def logger_func(*args, **kw):
        try:
            if not kw:
                return func(*args)
            return func(*args, **kw)
        except Exception:
            mylogger.exception('Exception in %s:', func.__name__)
            
    logger_func.__name__ = func.__name__
    logger_func.__doc__ = func.__doc__
    if hasattr(func, '__dict__'):
        logger_func.__dict__.update(func.__dict__)
    return logger_func    


def exceptionLog2Logger(loggerName):
    """
    A decorator that will catch and log any exceptions that may occur
    to the named logger.
    """
    import functools
    return functools.partial(exceptionLogger, loggerName=loggerName)    
    

########################################################################
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)
        
    #----------------------------------------------------------------------
    
    @exceptionLog2Logger('testLogger')
    def onExcept(self, event):
        """
        Raise an error
        """
        print(self, event)
        print(isinstance(self, wx.Panel))
        
        #trigger an exception
        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__":

    # set up the default logger
    log = logging.getLogger('testLogger')
    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)
    log.addHandler(log_fh)
    
    app = wx.App(False)
    frame = Frame()
    app.MainLoop()

This shows a couple of different decorator examples. This example demonstrates the more traditional methodology of decorator construction. It has a bit more metaprogramming in it though. The first example checks to make sure what is passed to it is actually callable. Then it creates a logger and wraps the callable with an exception handler. Before it returns the wrapped function, the wrapped function is modified so that it has the same name and docstring as the original function passed to it. I believe you could drop that and use functools.wraps instead, but being explicit is probably better in a tutorial.


Wrapping Up

Now you know how you catch exceptions in a couple of different ways. Hopefully you will find this helpful in your own application design. Have fun!

Copyright © 2024 Mouse Vs Python | Powered by Pythonlibrary