wxPython And Twisted

This was copied from wxPython-users as a starting point for using wxPython and Twisted together. Feel free to expand and adapt this document as you gain your own experience with wxPython and Twisted.

David Bolen writes:

Mike Driscoll writes:

> I looked at Twisted one wxPython example, but it didn't really show
> how to get info from a twisted server, just how to call a wx method
> from its reactor.

I combined Twisted and wxPython all the time, and it works well.
Fundamentally you just need to select a mechanism for handling both
event loops and after that just write the usual wx or twisted code.

I've used each of the following mechanisms in the past:

1. Twisted and wxPython in separate threads (typically wx in the main
   thread when the application was GUI in nature).  Use any of Queues,
   reactor.callFromThread/deferToThread, wxPostEvent/wxCallAfter to
   shift requests between the two threads.

   This approach just uses standard wxPython and Twisted initialization
   sequences in separate threads.  The wx thread blocks in MainLoop and
   the Twisted thread in reactor.run()

2. Iterate the twisted reactor from within a wxPython idle or timer
   based loop.  This actually works quite well although there can be
   a small latency to servicing twisted events due to latency in the
   loop (I tended to use something in the 100-150ms range).  Generally
   I was working on GUI applications with relatively light networking
   loads (from a time critical nature) so a few hundred ms here or there
   never caused any problems.

   One example (with default reactor) using a timer would be in your wxApp:

   - In OnInit() or a separate method if you want to initialize it prior
     to calling MainLoop():

         reactor.startRunning(installSignalHandlers=0)
         timerid = wx.wxNewId()
         self.timer = wx.wxTimer(self, timerid)
         wx.EVT_TIMER(self, timerid, self.OnTimer)
         self.timer.Start(150, False)

   - Define the timer callback:

        def OnTimer(self, event):
            reactor.runUntilCurrent()
            reactor.doIteration(0)

3. Integrate the two loops using the threadedselect reactor, using
   wxCallAfter for the interleave waker.

   There's an example of this in the twisted examples, but essentially:

   - Install the threadedselect reactor:

         from twisted.internet import _threadedselect
         _threadedselect.install()
         from twisted.internet import reactor

     The last statement gets access to the reactor instance that was
     installed under the standard name.  Modules other than the one installing
     can just use the last statement per normal, but it's important to make
     sure no other module tries to import the reactor until you execute
     the above installation code, or else the regular select reactor will
     have already been installed.

   - Then somewhere during wxApp initialization (or really anywhere that
     you want to set things up), interleave the reactor:

         reactor.interleave(wx.CallAfter)

   - In your main window, add a shutdown trigger, so that calling reactor.stop
     will shut down the GUI side of the application (and you should use
     reactor.stop in any normal circumstance when you want to exit):

     For example, in your main window, you might:

         reactor.addSystemEventTrigger('after', 'shutdown', self.Close, true)

     to close the window when the reactor is done, and thus exit MainLoop.
     You could also probably use wxApp.ExitMainLoop as the callback although
     the recommendation is generally to close the top level window to exit.

I tend to reserve (1) for when the two purposes (network and GUI) are
more "ships in the night" that just happen to share a process but don't
tend to have a lot of tight coupling or cross-operations.

(2) was my most common method for a long time and it's convenient
because everything in a single thread means you can interleave twisted
and wxPython operations without any special handling at all.

I originally used (3) under OSX when integrating with a PyObjC
application, and after good results with that, started using it with
wxPython in other cases.  If only because I don't have to manually
iterate the reactor, I suppose it feels a little cleaner up in the
code level.  It also can be more responsive since the network select
has no latency (is just in a background thread) though actually
servicing a deferred callback or network I/O does go through the wx
event loop so can be delayed by other pending UI events.  The
remainder of the reactor processing is done in the main thread so you
can still inter-mix wx and twisted processing with no special
protections.

About the only thing I've never done is use the wxreactor in twisted.
When I first started it was still fairly buggy, and when that started
to clear up, I was already more than happy enough with (2).

-- David

A Simple Chat Application

To demonstrate using Twisted + wxPython together, here is a simple multi-client chat program. To begin, run python server.py to initially setup the server waiting as a background process. Then, python gui.py will launch the chat window.

I haven't coded this with the best programming practices -- but it should serve as a decent example :)

View a screenshot of the program in action

gui.py

   1 import wx
   2 from twisted.internet import wxreactor
   3 wxreactor.install()
   4 
   5 # import twisted reactor *only after* installing wxreactor
   6 from twisted.internet import reactor, protocol
   7 from twisted.protocols import basic
   8 
   9 
  10 class ChatFrame(wx.Frame):
  11     def __init__(self):
  12         wx.Frame.__init__(self, parent=None, title="Twisted Chat")
  13         self.protocol = None  # twisted Protocol
  14 
  15         sizer = wx.BoxSizer(wx.VERTICAL)
  16         self.text = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.TE_READONLY)
  17         self.ctrl = wx.TextCtrl(self, style=wx.TE_PROCESS_ENTER, size=(300, 25))
  18 
  19         sizer.Add(self.text, 5, wx.EXPAND)
  20         sizer.Add(self.ctrl, 0, wx.EXPAND)
  21         self.SetSizer(sizer)
  22         self.ctrl.Bind(wx.EVT_TEXT_ENTER, self.send)
  23 
  24     def send(self, evt):
  25         self.protocol.sendLine(str(self.ctrl.GetValue()))
  26         self.ctrl.SetValue("")
  27 
  28 
  29 class DataForwardingProtocol(basic.LineReceiver):
  30     def __init__(self):
  31         self.output = None
  32 
  33     def dataReceived(self, data):
  34         gui = self.factory.gui
  35 
  36         gui.protocol = self
  37         if gui:
  38             val = gui.text.GetValue()
  39             gui.text.SetValue(val + data)
  40             gui.text.SetInsertionPointEnd()
  41 
  42     def connectionMade(self):
  43         self.output = self.factory.gui.text  # redirect Twisted's output
  44 
  45 
  46 class ChatFactory(protocol.ClientFactory):
  47     def __init__(self, gui):
  48         self.gui = gui
  49         self.protocol = DataForwardingProtocol
  50 
  51     def clientConnectionLost(self, transport, reason):
  52         reactor.stop()
  53 
  54     def clientConnectionFailed(self, transport, reason):
  55         reactor.stop()
  56 
  57 
  58 if __name__ == '__main__':
  59     app = wx.App(False)
  60     frame = ChatFrame()
  61     frame.Show()
  62     reactor.registerWxApp(app)
  63     reactor.connectTCP("localhost", 5001, ChatFactory(frame))
  64     reactor.run()

server.py

   1 from twisted.internet import reactor, protocol
   2 from twisted.protocols import basic
   3 import time
   4 
   5 def t():
   6     return "["+ time.strftime("%H:%M:%S") +"] "
   7 
   8 class EchoProtocol(basic.LineReceiver):
   9     name = "Unnamed"
  10 
  11     def connectionMade(self):
  12         self.sendLine("Welcome, what is your name?")
  13         self.sendLine("")
  14         self.count = 0
  15         self.factory.clients.append(self)
  16         print t() + "+ Connection from: "+ self.transport.getPeer().host
  17 
  18     def connectionLost(self, reason):
  19         self.sendMsg("- %s left." % self.name)
  20         print t() + "- Connection lost: "+ self.name
  21         self.factory.clients.remove(self)
  22 
  23     def lineReceived(self, line):
  24         if line == 'quit':
  25             self.sendLine("Goodbye.")
  26             self.transport.loseConnection()
  27             return
  28         elif line == "userlist":
  29             self.chatters()
  30             return
  31         if not self.count:
  32             self.username(line)
  33         else:
  34             self.sendMsg(self.name +": " + line)
  35 
  36     def username(self, line):
  37         for x in self.factory.clients:
  38             if x.name == line:
  39                 self.sendLine("This username is taken; please choose another")
  40                 return
  41 
  42         self.name = line
  43         self.chatters()
  44         self.sendLine("Chat away!")
  45         self.sendLine("")
  46         self.count += 1
  47         self.sendMsg("+ %s joined." % self.name)
  48         print '%s~ %s is now known as %s' % (t(), self.transport.getPeer().host, self.name)
  49 
  50     def chatters(self):
  51         x = len(self.factory.clients) - 1
  52         s = 'is' if x == 1 else 'are'
  53         p = 'person' if x == 1 else 'people'
  54         self.sendLine("There %s %i %s connected:" % (s, x, p) )
  55 
  56         for client in self.factory.clients:
  57             if client is not self:
  58                 self.sendLine(client.name)
  59         self.sendLine("")
  60 
  61     def sendMsg(self, message):
  62         for client in self.factory.clients:
  63             client.sendLine(t() + message)
  64 
  65 
  66 class EchoServerFactory(protocol.ServerFactory):
  67     protocol  = EchoProtocol
  68     clients = []
  69 
  70 if __name__ == "__main__":
  71     reactor.listenTCP(5001, EchoServerFactory())
  72     reactor.run()

Code Explanation

Client

For the gui, we simply set up two text boxes: one that is read-only, and expands inside the sizer to take up all the remaining space. The other text control is for typing in your chat messages, and is pretty small. On a text event, we send a "line" to our Twisted protocol, the DataForwardingProtocol.

When a connection is initially made to the server, the DataForwardingProtocol's attribute, "output" is changed to the read-only text control. Twisted will redirect all output received from the server into this text control (and scrolls the control to the bottom with SetInsertionPointEnd)

The ChatFactory is used when connecting to Twisted's reactor. It stores a reference to the gui, so that it can update its text box, and also sets its protocol to the DataForwardingProtocol. This tells twisted which Protocol handles any data received from the server.

Server

This is some basic code. When the script runs, it creates an instance of the EchoFactory, and listens over TCP at localhost, port 5001 for incoming connections. The functions connectionMade, connectionLost and lineReceived overwrite the Factory's default methods. What the server does is track each client that connects in a Python list, and then whenever a line is received from a connected client, it simply "echoes" the received message back to all registered clients.

Each client is prompted for their username, which must be unique. This allows the server to identify each client, and to echo their name, alongside their message.

wxPythonAndTwisted (last edited 2010-01-17 04:08:47 by 79-65-133-135)