Thursday, October 15, 2009

Redirecting python logging to a wx.TextCtrl

Today I was googling around for a bit of code that seems to lack a published solution. I wanted to simulate a console window in wxPython for logging purposes. The closest examples I could find were for redirecting stdout to a wx.TextCtrl (http://www.blog.pythonlibrary.org/2009/01/01/wxpython-redirecting-stdout-stderr/). Eventually I managed to find a partial solution here http://www.velocityreviews.com/forums/t515815-wxpython-redirect-the-stdout-to-a-textctrl.html. Anyway, I thought I'd share my working modification:

import wx
import logging

class WxLog(logging.Handler):
def __init__(self, ctrl):
logging.Handler.__init__(self)
self.ctrl = ctrl
def emit(self, record):
self.ctrl.AppendText(self.format(record)+"\n")

class MainFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, title="logging test")
self.level = 4
log = wx.TextCtrl(self, style=wx.TE_MULTILINE)
criticalbtn = wx.Button(self, -1 , 'critical')
errorbtn = wx.Button(self, -1 , 'error')
warnbtn = wx.Button(self, -1 , 'warn')
infobtn = wx.Button(self, -1 , 'info')
debugbtn = wx.Button(self, -1 , 'debug')
togglebtn = wx.Button(self, -1 , 'toggle level')
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(log, 1, wx.EXPAND)
sizer.Add(criticalbtn)
sizer.Add(errorbtn)
sizer.Add(warnbtn)
sizer.Add(infobtn)
sizer.Add(debugbtn)
sizer.Add(togglebtn)
self.SetSizer(sizer)

self.logr = logging.getLogger('')
self.logr.setLevel(logging.DEBUG)
hdlr = WxLog(log)
#hdlr.setFormatter(logging.Formatter('%(levelname)s | %(name)s |%(message)s [@ %(asctime)s in %(filename)s:%(lineno)d]'))
hdlr.setFormatter(logging.Formatter('%(levelname)s |%(message)s'))
self.logr.addHandler(hdlr)

self.Bind(wx.EVT_BUTTON, self.on_toggle, togglebtn)
self.Bind(wx.EVT_BUTTON, self.on_critical, criticalbtn)
self.Bind(wx.EVT_BUTTON, self.on_error, errorbtn)
self.Bind(wx.EVT_BUTTON, self.on_warn, warnbtn)
self.Bind(wx.EVT_BUTTON, self.on_info, infobtn)
self.Bind(wx.EVT_BUTTON, self.on_debug, debugbtn)

def on_toggle(self, evt):
self.level = (self.level+1) % 5
levels = [logging.CRITICAL, logging.ERROR, logging.WARN, logging.INFO, logging.DEBUG]
self.logr.setLevel(levels[self.level])
self.logr.critical('Logging level set to: %s: %s'%(self.level, logging.getLevelName(levels[self.level])))

def on_critical(self, evt):
self.logr.critical('Test message.')

def on_error(self, evt):
self.logr.error('Test message.')

def on_warn(self, evt):
self.logr.warn('Test message.')

def on_info(self, evt):
self.logr.info('Test message.')

def on_debug(self, evt):
self.logr.debug('Test message.')

if __name__ =="__main__":
app = wx.App(0)
frame = MainFrame()
frame.Show()
app.MainLoop()

UPDATE:
I discovered that the above method will not work if you have multiple threads using the same logger. This can be remedied by using a generic handler that simply calls a function and updating your wx.TextCtrl on idle.

class FuncLog(logging.Handler):
'''
A logging handler that sends logs to an update function
'''
def __init__(self, update):
logging.Handler.__init__(self)
self.update = update

def emit(self, record):
self.update(self.format(record))


class MainGUI(wx.Frame):
def __init__(self, parent, **kwargs):
...
self.console = wx.TextCtrl(self, -1, '', style=wx.TE_MULTILINE|wx.TE_READONLY)
...
self.logr = logging.getLogger()
# recent logs will be stored in this string
self.log_text = ''
def update(x):
self.log_text += x+'\n'
hdlr = FuncLog(update)
self.logr.addHandler(hdlr)
...
self.Bind(wx.EVT_IDLE, self.on_idle)

def on_idle(self, evt=None):
if self.log_text != '':
self.console.AppendText(self.log_text)
self.log_text = '

Enjoy!

No comments: