Welcome, guest | Sign In | My Account | Store | Cart

This expands on Uwe C. Schroeder's recipe titled "Using wxPython with Twisted Python" to show one way to implement a modal progress bar using Twisted to make an XML-RPC call.

Python, 218 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# In the public domain
# Author: Andrew Dalke <dalke@dalkescientific.com
#
# This is an example of a wxPython-based modal progress bar
# which uses Twisted to do XML-RPC requests from the state name
# demo server.  It's easily extensible to other tasks.

from __future__ import generators
from wxPython.wx import *

from twisted.internet import reactor
from twisted.web.xmlrpc import Proxy

# returned from the main modal
COMPLETED, CANCELED, ERROR = range(3)

# returned from the (sub)modal when there's an XML-RPC error
RETRY, SKIP, STOP = range(20, 23)

# The progress modal does N "tasks".  Each task has a 
# "start" method, which returns a deferred.  The deferred
# is chained to call "good" with the result or "bad"
# with the failure.  The "bad" method may return one of
# RETRY, SKIP, or STOP to tell the modal how to recover.
class Task:
    def start(self):
        """returns a defered"""
        raise NotImplementedError
    def good(self, result):
        pass
    def bad(self, fail):
        pass

# To show examples of error modes
TEST_ERRORS = 1

# A Task to get the state name corresponding to the given number.
class StateTask(Task):
    def __init__(self, i):
        self.i = i

    def start(self):
        proxy = Proxy("http://beatty.userland.com/RPC2")
        if TEST_ERRORS:
            i = self.i
            if i == 3:
                proxy = Proxy("http://illegal-host_name/")
            elif i == 6:
                proxy = Proxy("http://beatty.userland.com/")
            elif i == 8:
                proxy = Proxy("http://beatty.userland.com/testing_xmlrpc_error_case")
            
        return proxy.callRemote('examples.getStateName', self.i)

    def good(self, result):
        print "state", self.i, "is", result

    def bad(self, fail):
        # pop up a submodal
        status = wxMessageBox("Cannot get name for state %d.  Try again?\n"
                              "\n"
                              "The problem is: %s" % (self.i, fail.getErrorMessage()),
                              "Connection problem",
                              wxCANCEL | wxYES_NO | wxICON_QUESTION)
        if status == wxYES:
            return RETRY
        elif status == wxNO:
            return SKIP
        elif status == wxCANCEL:
            return STOP
        else:
            raise AssertionError(status)

# The progress dialog must be passed a "task list" object which
# implements len() (needed to know how many steps to show) and
# does forward iteration.
class StateTaskList:
    def __init__(self, min=0, max=50):
        self.min = min
        self.max = max
    def __iter__(self):
        for i in range(self.min, self.max):
            yield StateTask(i)
            
    def __len__(self):
        return self.max - self.min
        

class Progress(wxDialog):
    def __init__(self, parent, ID, title, tasks,
                 pos=wxDefaultPosition, size=wxDefaultSize,
                 style=wxDEFAULT_DIALOG_STYLE):
        wxDialog.__init__(self, parent, ID, title, pos, size, style)

        n = len(tasks)
        self.task_iter = iter(tasks)

        sizer = wxBoxSizer(wxVERTICAL)

        self.gauge = wxGauge(self, -1, 100, size = (300, -1))
        sizer.Add(self.gauge, 0, wxALIGN_CENTER|wxALL, 5)

        box = wxBoxSizer(wxHORIZONTAL)
        spacer = wxStaticText(self, -1, "")
        box.Add(spacer, 1, wxALIGN_CENTRE|wxALL|wxGROW)
        btn = wxButton(self, wxID_CANCEL, " Cancel ")
        box.Add(btn, 0, wxALIGN_CENTRE|wxALL, 5)
        spacer = wxStaticText(self, -1, "")
        box.Add(spacer, 1, wxALIGN_CENTRE|wxALL|wxGROW)

        sizer.AddSizer(box, 0, wxALIGN_CENTER_VERTICAL|wxALL|wxGROW, 5)

        EVT_BUTTON(self, wxID_CANCEL, self.OnCancel)

        self.SetSizer(sizer)
        self.SetAutoLayout(true)
        sizer.Fit(self)

        self.i = 0
        self.Start(n)

        self._canceled = 0
        self.Feed()

    def OnCancel(self, event):
        self._canceled = 1
        self.EndModal(CANCELED)

    def Feed(self):
        # Get the next task and start it up
        try:
            task = self.task_iter.next()
        except StopIteration:
            self.End()
            return
        self.StartTask(task)

    def StartTask(self, task):
        defered = task.start()
        def do_good(result):
            self.Good(task, result)
        def do_bad(fail):
            self.Bad(task, fail)
        defered.addCallbacks(do_good, do_bad)

    def Good(self, task, result):
        if self._canceled:
            return
        task.good(result)
        self.Update(1)
        self.Feed()

    def Bad(self, task, fail):
        if self._canceled:
            return
        try_again = task.bad(fail)
        if try_again == RETRY:
            self.StartTask(task)
        elif try_again == SKIP:
            self.Update(1)
            self.Feed()
        elif try_again == STOP:
            self._canceled = 1
            self.EndModal(ERROR)
        else:
            raise AssertionError(try_again)

    def Start(self, count):
        self.gauge.SetRange(count)
        self.gauge.SetValue(0)

    def Update(self, incr):
        # Increment the counter.
        self.i += incr
        self.gauge.SetValue(self.i)
        
    def End(self):
        self.gauge.SetValue(self.gauge.GetRange())
        self.EndModal(COMPLETED)

# Thanks to Uwe C. Schroeder and his "Using wxPython with Twisted
# Python" recipe at aspn.ActiveState.com

class MyApp(wxApp):
    def OnInit(self):
        # Twisted Reactor code
        reactor.startRunning()
        EVT_TIMER(self, 999999, self.OnTimer)
        self.timer = wxTimer(self, 999999)
        self.timer.Start(150, False)

        return true

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

    def __del__(self):
        self.timer.Stop()
        reactor.stop()
        wxApp.__del__(self)

def main():
    app = MyApp(0)
    win = Progress(None, -1, "Processing ...", StateTaskList(1, 10))

    status = win.ShowModal()
    if status == COMPLETED:
        print "All done"
    elif status == CANCELED:
        print "Okay, I stopped."
    elif status == ERROR:
        print "What happened?"
    else:
        raise AssertionError(status)
    
if __name__ == "__main__":
    main()

I wanted a simple GUI client for making a set of remote procedure calls using XML-RPC. Python has the standard library 'xmlrpclib' for doing this, but it blocks in the request. Since the call may take several minutes to complete, I want to give the user the chance to cancel the operation, which means no blocking.

There are several ways to do this, as described in detail on the wxPython wiki at http://wiki.wxpython.org/index.cgi/LongRunningTasks wxYield and idle events wouldn't help, since my code blocks on the socket. Threads might work, except that the libraries i'm using elsewhere in the project might not be thread safe.

I instead chose to give Twisted a try. Twisted has problems working with wxPython because both projects want to manage the main event loop. Luckily, Uwe C. Schroeder posted a recipe which makes them work together. This recipe expands on that to show how the different pieces can all fit together.

8 comments

Dave Richards 20 years, 10 months ago  # | flag

for the record... Twisted now comes with built in wxsupport. Use:

from twisted.internet import wxsupport

and

wxsupport.install(app)
reactor.run()

where app is your wxApp subclass. I don't know if there is any difference between this and your code, but wxsupport has always worked fine for me.

Cory Dodt 20 years, 9 months ago  # | flag

Event loop incompatibilities on Windows. The problem Uwe's recipe addresses (and this one expands on) is that, on MS Windows, Modal dialogs (and therefore also menus) use a whole new event loop, which Twisted doesn't know about. Therefore, Twisted events stall waiting for modal dialogs to close.

Uwe's recipe effectively turns wxsupport inside-out, and pumps Twisted events from inside the wx event loop instead of vice-versa. This is often good enough. Disadvantage: Twisted may not be as responsive; this would probably not effect the performance of many kinds of network clients, but it would certainly hurt you if you tried to use this recipe with a server attached to a wx GUI.

Another solution, as discussed, is to put the whole wxPython app inside another thread; yet another is to avoid menus and modals in your wx Application. There is no "perfect" solution yet.

The only real solution to this problem is a better win32eventreactor inside Twisted, to work with both kinds of events. (It doesn't exist yet, as of this writing.)

B T 20 years, 7 months ago  # | flag

error running this sample. I get an unhandled Twisted exception running this sample code on Windows:

  File "C:\...\twisted-wxpython.py", line 187, in OnInit
    reactor.startRunning()
  File "C:\Python22\Lib\site-packages\twisted\internet\default.py", line 115, in startRunning
    self._handleSignals()
  File "C:\Python22\Lib\site-packages\twisted\internet\default.py", line 87, in _handleSignals
    signal.signal(signal.SIGINT, self.sigInt)
SystemError: error return without exception set
alex epshteyn 20 years, 6 months ago  # | flag

same problem when using wxsupport. I tried bot the sample and simply running an application using wxsupport, but seem to be getting the same exception. I am using Twisted 1.0.7rc1 with wxPython 2.41 for Python 2.3 on Windows 2000.

Here is the snippet of offending code (class names changed):

if __name__ == '__main__':
    import myGUI
    app = myGUI.MyApp(0)
    wxsupport.install(app)
    f = Factory()
    f.protocol = MyProtocol
    reactor.listenTCP(7929, f)
    reactor.run()

And here is the traceback:

Traceback (most recent call last):
  File "callDirector.py", line 206, in ?
    reactor.run()
  File "C:\Python23\Lib\site-packages\twisted\internet\default.py", line 121, in run
    self.startRunning(installSignalHandlers=installSignalHandlers)
  File "C:\Python23\Lib\site-packages\twisted\internet\default.py", line 115, in startRunning
    self._handleSignals()
  File "C:\Python23\Lib\site-packages\twisted\internet\default.py", line 87, in _handleSignals
    signal.signal(signal.SIGINT, self.sigInt)
SystemError: error return without exception set

I wonder if anyone can shed some light on:

1) What is the problem?

2) What platforms / versions of wxPython/Python wxsupport is expected to work on? The problem with menus and modal dialogs is actually not an issue for us(we'll do without), but the exception obviously is :-)

carlos choy 20 years, 5 months ago  # | flag

Twisted expects to handle signal handlers cleanly. Signal handlers are such things as ctrl-C, ctrl-Break, etc. They are handled on the main thread.

Eliminate the trapping of signal handlers by not using reactor.run(), but instead use reactor.run(installSignalHandlers=0).

You may want to consider putting reactor on its own thread and leaving wxPython on the main thread.

Cory Dodt 20 years, 3 months ago  # | flag

Twisted 1.1.1 has wxreactor. This recipe is no longer necessary. Itamar Shtull-Trauring added twisted.internet.wxreactor, which is a full-fledged reactor and runs each of Twisted and wx in timeslices. Modal dialogs (and menus, which are secretly modal dialogs) will now work on MSWindows with no additional code.

Update your wx projects to use wxreactor and rejoice! :-)

Matthew Sherborne 19 years, 8 months ago  # | flag

New Solution. For me, wxreactor doesn't work on linux and wxsupport doesn't work on windows XP. And neither work with modal dialogs.

I've posted another solution which I think is quite neat...

It's long because there's a demo chat app in there... http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/286201

Jean-Paul Calderone 16 years ago  # | flag

twisted.internet.wxreactor works on Windows, Linux, and OS X now.