]> git.saurik.com Git - wxWidgets.git/commitdiff
Added wx.lib.delayedresult from Oliver Schoenborn.
authorRobin Dunn <robin@alldunn.com>
Thu, 14 Sep 2006 21:06:57 +0000 (21:06 +0000)
committerRobin Dunn <robin@alldunn.com>
Thu, 14 Sep 2006 21:06:57 +0000 (21:06 +0000)
git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@41237 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775

wxPython/demo/DelayedResult.py [new file with mode: 0644]
wxPython/demo/Main.py
wxPython/docs/CHANGES.txt
wxPython/wx/lib/delayedresult.py [new file with mode: 0644]

diff --git a/wxPython/demo/DelayedResult.py b/wxPython/demo/DelayedResult.py
new file mode 100644 (file)
index 0000000..f72fab5
--- /dev/null
@@ -0,0 +1,236 @@
+"""
+This demonstrates a simple use of delayedresult: get/compute 
+something that takes a long time, without hanging the GUI while this
+is taking place. 
+
+The top button runs a small GUI that uses wx.lib.delayedresult.startWorker
+to wrap a long-running function into a separate thread. Just click
+Get, and move the slider, and click Get and Abort a few times, and
+observe that GUI responds. The key functions to look for in the code
+are startWorker() and __handleResult().
+
+The second button runs the same GUI, but without delayedresult.  Click
+Get: now the get/compute is taking place in main thread, so the GUI
+does not respond to user actions until worker function returns, it's
+not even possible to Abort.
+"""
+
+import wx
+from wx.lib.delayedresult import startWorker
+
+class FrameSimpleDelayedGlade(wx.Frame):
+    def __init__(self, *args, **kwds):
+        # begin wxGlade: FrameSimpleDelayed.__init__
+        kwds["style"] = wx.DEFAULT_FRAME_STYLE
+        wx.Frame.__init__(self, *args, **kwds)
+        self.checkboxUseDelayed = wx.CheckBox(self, -1, "Use delayedresult")
+        self.buttonGet = wx.Button(self, -1, "Get")
+        self.buttonAbort = wx.Button(self, -1, "Abort")
+        self.slider = wx.Slider(self, -1, 0, 0, 10, size=(100,-1), style=wx.SL_HORIZONTAL|wx.SL_AUTOTICKS)
+        self.textCtrlResult = wx.TextCtrl(self, -1, "", style=wx.TE_READONLY)
+
+        self.__set_properties()
+        self.__do_layout()
+
+        self.Bind(wx.EVT_BUTTON, self.handleGet, self.buttonGet)
+        self.Bind(wx.EVT_BUTTON, self.handleAbort, self.buttonAbort)
+        # end wxGlade
+
+    def __set_properties(self):
+        # begin wxGlade: FrameSimpleDelayed.__set_properties
+        self.SetTitle("Simple Examle of Delayed Result")
+        self.checkboxUseDelayed.SetValue(1)
+        self.checkboxUseDelayed.Enable(False)
+        self.buttonAbort.Enable(False)
+        # end wxGlade
+
+    def __do_layout(self):
+        # begin wxGlade: FrameSimpleDelayed.__do_layout
+        sizerFrame = wx.BoxSizer(wx.VERTICAL)
+        sizerGetResult = wx.BoxSizer(wx.HORIZONTAL)
+        sizerUseDelayed = wx.BoxSizer(wx.HORIZONTAL)
+        sizerUseDelayed.Add(self.checkboxUseDelayed, 0, wx.LEFT|wx.ALIGN_CENTER_VERTICAL|wx.ADJUST_MINSIZE, 5)
+        sizerFrame.Add(sizerUseDelayed, 1, wx.EXPAND, 0)
+        sizerGetResult.Add(self.buttonGet, 0, wx.ADJUST_MINSIZE, 0)
+        sizerGetResult.Add(self.buttonAbort, 0, wx.ADJUST_MINSIZE, 0)
+        sizerGetResult.Add(self.slider, 0, wx.ADJUST_MINSIZE, 0)
+        sizerGetResult.Add(self.textCtrlResult, 0, wx.ADJUST_MINSIZE, 0)
+        sizerFrame.Add(sizerGetResult, 1, wx.ALL|wx.EXPAND, 5)
+        self.SetAutoLayout(True)
+        self.SetSizer(sizerFrame)
+        sizerFrame.Fit(self)
+        sizerFrame.SetSizeHints(self)
+        self.Layout()
+        # end wxGlade
+
+
+class FrameSimpleDelayed(FrameSimpleDelayedGlade):
+    """This demos simplistic use of delayedresult module."""
+    
+    def __init__(self, *args, **kwargs):
+        self.jobID = 1
+        FrameSimpleDelayedGlade.__init__(self, *args, **kwargs)
+        self.Bind(wx.EVT_CLOSE, self.handleClose)
+    
+    def setLog(self, log):
+        self.log = log
+
+    def handleClose(self, event):
+        """Only needed because in demo, closing the window does not kill the 
+        app, so worker thread continues and sends result to dead frame; normally
+        your app would exit so this would not happen."""
+        if self.buttonAbort.IsEnabled():
+            self.Hide()
+            import time
+            time.sleep(5)
+        self.Destroy()
+            
+    def handleGet(self, event): 
+        """Compute result in separate thread, doesn't affect GUI response."""
+        self.buttonGet.Enable(False)
+        self.buttonAbort.Enable(True)
+
+        self.log( "Starting job %s in producer thread: GUI remains responsive" % self.jobID )
+        startWorker(self.__handleResult, self.__resultCreator, 
+                    wargs=(self.jobID,), jobID=self.jobID)
+                        
+    def __resultCreator(self, jobID):
+        """Pretend to be a complex worker function or something that takes 
+        long time to run due to network access etc. GUI will freeze if this 
+        method is not called in separate thread."""
+        import time
+        time.sleep(5)
+        return jobID
+
+    def handleAbort(self, event): 
+        """Abort actually just means 'ignore the result when it gets to 
+        handler, it is no longer relevant'. We just increase the job ID, 
+        this will let handler know that the result has been cancelled."""
+        self.log( "Aborting result for job %s" % self.jobID )
+        self.buttonGet.Enable(True)
+        self.buttonAbort.Enable(False)
+        self.jobID += 1
+        
+    def __handleResult(self, delayedResult):
+        # See if we still want the result for last job started
+        jobID = delayedResult.getJobID()
+        if jobID != self.jobID:
+            self.log( "Got obsolete result for job %s, ignored" % jobID )
+            return
+
+        # we do, get result:
+        try:
+            result = delayedResult.get()
+        except Exception, exc:
+            self.log( "Result for job %s raised exception: %s" % (jobID, exc) )
+            self.jobID += 1
+            return
+        
+        # output result
+        self.log( "Got result for job %s: %s" % (jobID, result) )
+        self.textCtrlResult.SetValue(str(result))
+        
+        # get ready for next job:
+        self.buttonGet.Enable(True)
+        self.buttonAbort.Enable(False)
+        self.jobID += 1
+
+
+class FrameSimpleDirect(FrameSimpleDelayedGlade):
+    """This does not use delayedresult so the GUI will freeze while
+    the GET is taking place."""
+    
+    def __init__(self, *args, **kwargs):
+        self.jobID = 1
+        FrameSimpleDelayedGlade.__init__(self, *args, **kwargs)
+        self.checkboxUseDelayed.SetValue(False)
+                
+    def setLog(self, log):
+        self.log = log
+        
+    def handleGet(self, event): 
+        """Use delayedresult, this will compute
+        result in separate thread, and won't affect GUI response. """
+        self.buttonGet.Enable(False)
+        self.buttonAbort.Enable(True)
+
+        self.log( "Doing job %s without delayedresult (same as GUI thread): GUI hangs (for a while)" % self.jobID )
+        result = self.__resultCreator(self.jobID)
+        self.__handleResult( result )
+
+    def __resultCreator(self, jobID):
+        """Pretend to be a complex worker function or something that takes 
+        long time to run due to network access etc. GUI will freeze if this 
+        method is not called in separate thread."""
+        import time
+        time.sleep(5)
+        return jobID
+
+    def handleAbort(self, event):
+        """can never be called"""
+        pass
+        
+    def __handleResult(self, result):
+        # output result
+        self.log( "Got result for job %s: %s" % (self.jobID, result) )
+        self.textCtrlResult.SetValue(str(result))
+        
+        # get ready for next job:
+        self.buttonGet.Enable(True)
+        self.buttonAbort.Enable(False)
+        self.jobID += 1
+
+
+#---------------------------------------------------------------------------
+#---------------------------------------------------------------------------
+
+class TestPanel(wx.Panel):
+    def __init__(self, parent, log):
+        self.log = log
+        wx.Panel.__init__(self, parent, -1)
+
+        vsizer = wx.BoxSizer(wx.VERTICAL)
+        b = wx.Button(self, -1, "Long-running function in separate thread")
+        vsizer.Add(b, 0, wx.ALL, 5)
+        self.Bind(wx.EVT_BUTTON, self.OnButton1, b)
+
+        b = wx.Button(self, -1, "Long-running function in GUI thread")
+        vsizer.Add(b, 0, wx.ALL, 5)
+        self.Bind(wx.EVT_BUTTON, self.OnButton2, b)
+
+        bdr = wx.BoxSizer()
+        bdr.Add(vsizer, 0, wx.ALL, 50)
+        self.SetSizer(bdr)
+        self.Layout()
+
+    def OnButton1(self, evt):
+        frame = FrameSimpleDelayed(self, title="Long-running function in separate thread")
+        frame.setLog(self.log.WriteText)
+        frame.Show()
+
+    def OnButton2(self, evt):
+        frame = FrameSimpleDirect(self, title="Long-running function in GUI thread")
+        frame.setLog(self.log.WriteText)
+        frame.Show()
+    
+
+#---------------------------------------------------------------------------
+
+
+def runTest(frame, nb, log):
+    win = TestPanel(nb, log)
+    return win
+
+
+#---------------------------------------------------------------------------
+
+
+overview = __doc__
+
+
+if __name__ == '__main__':
+   import sys,os
+   import run
+   run.main(['', os.path.basename(sys.argv[0])] + sys.argv[1:])
+
+
index 250c5b908ca7d58ca5bba0475ed2583e068b85cb..11ea138078b91f5bdd11e2e6fbcd14f0fca9d492 100644 (file)
@@ -61,6 +61,7 @@ _treeList = [
         'RawBitmapAccess',
         'DragScroller',
 ##        'AlphaDrawing',
         'RawBitmapAccess',
         'DragScroller',
 ##        'AlphaDrawing',
+        'DelayedResult',
         ]),
 
     # managed windows == things with a (optional) caption you can close
         ]),
 
     # managed windows == things with a (optional) caption you can close
@@ -215,6 +216,7 @@ _treeList = [
 
     # ditto
     ('Process and Events', [
 
     # ditto
     ('Process and Events', [
+        'DelayedResult',
         'EventManager',
         'KeyEvents',
         'Process',
         'EventManager',
         'KeyEvents',
         'Process',
@@ -1143,8 +1145,11 @@ class wxPythonDemo(wx.Frame):
         icon = images.getWXPdemoIcon()
         self.SetIcon(icon)
 
         icon = images.getWXPdemoIcon()
         self.SetIcon(icon)
 
-        self.tbicon = DemoTaskBarIcon(self)
-
+        try:
+            self.tbicon = DemoTaskBarIcon(self)
+        except:
+            self.tbicon = None
+            
         wx.CallAfter(self.ShowTip)
 
         self.otherWin = None
         wx.CallAfter(self.ShowTip)
 
         self.otherWin = None
@@ -1627,7 +1632,8 @@ class wxPythonDemo(wx.Frame):
         self.demoPage = None
         self.codePage = None
         self.mainmenu = None
         self.demoPage = None
         self.codePage = None
         self.mainmenu = None
-        self.tbicon.Destroy()
+        if self.tbicon is not None:
+            self.tbicon.Destroy()
         self.Destroy()
 
 
         self.Destroy()
 
 
index 4b2254aef3535a9fbd83d3a167dc663610ed4276..9777ae59cc4ca0116bb8e1f2245fabad3821f33b 100644 (file)
@@ -249,6 +249,9 @@ Updated the C++ code for wx.gizmos.TreeListCtrl from the wxCode
 project.  This has resulted in some minor API changes, most of which
 were worked around in the wrapper code.  
 
 project.  This has resulted in some minor API changes, most of which
 were worked around in the wrapper code.  
 
+Added wx.lib.delayedresult from Oliver Schoenborn.
+
+
 
 
 
 
 
 
diff --git a/wxPython/wx/lib/delayedresult.py b/wxPython/wx/lib/delayedresult.py
new file mode 100644 (file)
index 0000000..8370ac5
--- /dev/null
@@ -0,0 +1,376 @@
+"""
+This module supports the thread-safe, asynchronous transmission of data 
+('delayed results') from a worker (non-GUI) thread to the main thread. Ie you don't 
+need to mutex lock any data, the worker thread doesn't wait (or even check) 
+for the result to be received, and the main thread doesn't wait for the 
+worker thread to send the result. Instead, the consumer will be called 
+automatically by the wx app when the worker thread result is available. 
+
+In most cases you just need to use startWorker() with the correct parameters
+(your worker function and your 'consumer' in the simplest of cases). The 
+only requirement on consumer is that it must accept a DelayedResult instance 
+as first arg. 
+
+In the following example, this will call consumer(delayedResult) with the 
+return value from workerFn::
+
+    from delayedresult import startWorker
+    startWorker(consumer, workerFn)
+
+More advanced uses: 
+
+- The other parameters to startWorker()
+- Derive from Producer to override _extraInfo (e.g. to provide traceback info)
+- Create your own worker-function-thread wrapper instead of using Producer
+- Create your own Handler-like wrapper to pre- or post-process the result 
+  (see PreProcessChain)
+- Derive from Sender to use your own way of making result hop over the 
+  "thread boundary" (from non-main thread to main thread), e.g. using Queue
+
+Thanks to Josiah Carlson for critical feedback/ideas that helped me 
+improve this module. 
+
+:Copyright: (c) 2006 by Oliver Schoenborn
+:License: wxWidgets license
+:Version: 1.0
+
+"""
+
+__author__  = 'Oliver Schoenborn at utoronto dot ca'
+__version__ = '1.0'
+
+__all__ = ('Sender', 'SenderNoWx', 'SenderWxEvent', 'SenderCallAfter', 
+    'Handler', 'DelayedResult', 'Producer', 'startWorker', 'PreProcessChain')
+
+
+import wx
+import threading
+
+
+class Struct:
+    """
+    An object that has attributes built from the dictionary given in 
+    constructor. So ss=Struct(a=1, b='b') will satisfy assert ss.a == 1
+    and assert ss.b == 'b'.
+    """
+    
+    def __init__(self, **kwargs):
+        self.__dict__.update( kwargs )
+
+
+class Handler:
+    """
+    Bind some of the arguments and keyword arguments of a callable ('listener'). 
+    Then when the Handler instance is called (e.g. handler(result, **kwargs))
+    the result is passed as first argument to callable, the kwargs is 
+    combined with those given at construction, and the args are those
+    given at construction. Its return value is returned.
+    """
+    def __init__(self, listener, *args, **kwargs ):
+        """Bind args and kwargs to listener. """
+        self.__listener = listener
+        self.__args = args
+        self.__kwargs = kwargs
+        
+    def __call__(self, result, **moreKwargs):
+        """Listener is assumed to take result as first arg, then *args, 
+        then the combination of moreKwargs and the kwargs given at construction."""
+        if moreKwargs:
+            moreKwargs.update(self.__kwargs)
+        else:
+            moreKwargs = self.__kwargs
+        return self.__listener(result, *self.__args, **moreKwargs)
+
+        
+class Sender:
+    """
+    Base class for various kinds of senders. A sender sends a result
+    produced by a worker funtion to a result handler (listener). Note
+    that each sender can be given a "job id". This can be anything
+    (number, string, id, and object, etc) and is not used, it is 
+    simply added as attribute whenever a DelayedResult is created. 
+    This allows you to know, if desired, what result corresponds to 
+    which sender. Note that uniqueness is not necessary. 
+    
+    Derive from this class if none of the existing derived classes
+    are adequate, and override _sendImpl(). 
+    """
+    
+    def __init__(self, jobID=None):
+        """The optional jobID can be anything that you want to use to 
+        track which sender particular results come from. """
+        self.__jobID = jobID
+
+    def getJobID(self):
+        """Return the jobID given at construction"""
+        return self.__jobID
+    
+    def sendResult(self, result):
+        """This will send the result to handler, using whatever 
+        technique the derived class uses. """
+        delayedResult = DelayedResult(result, jobID=self.__jobID)
+        self._sendImpl(delayedResult)
+
+    def sendException(self, exception, extraInfo = None):
+        """Use this when the worker function raised an exception.
+        The *exception* is the instance of Exception caught. The extraInfo
+        could be anything you want (e.g. locals or traceback etc), 
+        it will be added to the exception as attribute 'extraInfo'. The
+        exception will be raised when DelayedResult.get() is called."""
+        assert exception is not None
+        delayedResult = DelayedResult(extraInfo, 
+            exception=exception, jobID=self.__jobID)
+        self._sendImpl(delayedResult)
+
+    def _sendImpl(self, delayedResult):
+        msg = '_sendImpl() must be implemented in %s' % self.__class__
+        raise NotImplementedError(msg)
+        
+    
+class SenderNoWx( Sender ):
+    """
+    Sender that works without wx. The results are sent directly, ie
+    the consumer will get them "in the worker thread". So it should 
+    only be used for testing. 
+    """
+    def __init__(self, consumer, jobID=None, args=(), kwargs={}):
+        """The consumer can be any callable of the form 
+        callable(result, *args, **kwargs)"""
+        Sender.__init__(self, jobID)
+        if args or kwargs:
+            self.__consumer = Handler(consumer, *args, **kwargs)
+        else: 
+            self.__consumer = consumer
+            
+    def _sendImpl(self, delayedResult):
+        self.__consumer(delayedResult)
+        
+
+class SenderWxEvent( Sender ):
+    """
+    This sender sends the delayed result produced in the worker thread
+    to an event handler in the main thread, via a wx event of class 
+    *eventClass*. The result is an attribute of the event (default: 
+    "delayedResult". 
+    """
+    def __init__(self, handler, eventClass, resultAttr="delayedResult", 
+        jobID=None, **kwargs):
+        """The handler must derive from wx.EvtHandler. The event class 
+        is typically the first item in the pair returned by 
+        wx.lib.newevent.NewEvent(). You can use the *resultAttr* 
+        to change the attribute name of the generated event's 
+        delayed result. """
+        Sender.__init__(self, jobID)
+        if not isinstance(handler, wx.EvtHandler):
+            msg = 'SenderWxEvent(handler=%s, ...) not allowed,' % type(handler)
+            msg = '%s handler must derive from wx.EvtHandler' % msg
+            raise ValueError(msg)
+        self.__consumer = Struct(handler=handler, eventClass=eventClass, 
+                                 resultAttr=resultAttr, kwargs=kwargs)
+        
+    def _sendImpl(self, delayedResult):
+        """Must not modify the consumer (that was created at construction) 
+        since might be shared by several senders, each sending from 
+        separate threads."""
+        consumer = self.__consumer
+        kwargs = consumer.kwargs.copy()
+        kwargs[ consumer.resultAttr ] = delayedResult
+        event = consumer.eventClass(** kwargs)
+        wx.PostEvent(consumer.handler, event)
+
+
+class SenderCallAfter( Sender ):
+    """
+    This sender sends the delayed result produced in the worker thread
+    to a callable in the main thread, via wx.CallAfter. 
+    """
+    def __init__(self, listener, jobID=None, args=(), kwargs={}):
+        Sender.__init__(self, jobID)
+        if args or kwargs:
+            self.__consumer = Handler(listener, *args, **kwargs)
+        else:
+            self.__consumer = listener
+            
+    def _sendImpl(self, delayedResult):
+        wx.CallAfter(self.__consumer, delayedResult)
+        
+
+class DelayedResult:
+    """
+    Represent the actual delayed result coming from the non-main thread. 
+    An instance of this is given to the result handler. This result is 
+    either a (reference to a) the value sent, or an exception. 
+    If the latter, the exception is raised when the get() method gets
+    called. 
+    """
+    
+    def __init__(self, result, jobID=None, exception = None):
+        """You should never have to call this yourself. A DelayedResult 
+        is created by a concrete Sender for you."""
+        self.__result = result
+        self.__exception = exception
+        self.__jobID = jobID
+
+    def getJobID(self):
+        """Return the jobID given when Sender initialized, 
+        or None if none given. """
+        return self.__jobID 
+    
+    def get(self):
+        """Get the result. If an exception was sent instead of a result, 
+        (via Sender's sendExcept()), that **exception is raised**.
+        Otherwise the result is simply returned. """
+        if self.__exception: # exception was raised!
+            self.__exception.extraInfo = self.__result
+            raise self.__exception
+        
+        return self.__result
+
+
+class Producer(threading.Thread):
+    """
+    Represent the worker thread that produces delayed results. 
+    It causes the given function to run in a separate thread, 
+    and a sender to be used to send the return value of the function.
+    As with any threading.Thread, instantiate and call start().
+    """
+    
+    def __init__(self, sender, workerFn, args=(), kwargs={}, 
+                 name=None, group=None, daemon=False, 
+                 sendReturn=True, senderArg=None):
+        """The sender will send the return value of 
+        workerFn(*args, **kwargs) to the main thread. The name and group 
+        are same as threading.Thread constructor parameters. Daemon causes 
+        setDaemon() to be called. If sendReturn is False, then the return 
+        value of workerFn() will not be sent. If senderArg is given, it 
+        must be the name of the keyword arg to use to pass the sender into 
+        the workerFn, so the function can send (typically many) results."""
+        if senderArg:
+            kwargs[senderArg] = sender
+        def wrapper():
+            try: 
+                result = workerFn(*args, **kwargs)
+            except Exception, exc:
+                extraInfo = self._extraInfo(exc)
+                sender.sendException(exc, extraInfo)
+            else:
+                if sendReturn:
+                    sender.sendResult(result)
+            
+        threading.Thread.__init__(self, name=name, group=group, target=wrapper)
+        if daemon:
+            self.setDaemon(daemon)
+    
+    def _extraInfo(self, exception):
+        """This method could be overridden in a derived class to provide 
+        extra information when an exception is being sent instead of a 
+        result. """
+        return None
+
+
+def startWorker(
+    consumer, workerFn, 
+    cargs=(), ckwargs={}, 
+    wargs=(), wkwargs={},
+    jobID=None, group=None, daemon=False, 
+    sendReturn=True, senderArg=None):
+    """
+    Convenience function to send data produced by workerFn(*wargs, **wkwargs) 
+    running in separate thread, to a consumer(*cargs, **ckwargs) running in
+    the main thread. This function merely creates a SenderCallAfter (or a
+    SenderWxEvent, if consumer derives from wx.EvtHandler), and a Producer,
+    and returns immediately after starting the Producer thread. The jobID
+    is used for the Sender and as name for the Producer thread. Returns the 
+    thread created, in case caller needs join/etc.
+    """
+    
+    if isinstance(consumer, wx.EvtHandler):
+        eventClass = cargs[0]
+        sender = SenderWxEvent(consumer, eventClass, jobID=jobID, **ckwargs)
+    else:
+        sender = SenderCallAfter(consumer, jobID, args=cargs, kwargs=ckwargs)
+        
+    thread = Producer(
+        sender, workerFn, args=wargs, kwargs=wkwargs, 
+        name=jobID, group=group, daemon=daemon, 
+        senderArg=senderArg, sendReturn=sendReturn)
+        
+    thread.start() 
+    return thread
+
+
+class PreProcessChain:
+    """
+    Represent a 'delayed result pre-processing chain', a kind of Handler. 
+    Useful when lower-level objects need to apply a sequence of transformations 
+    to the delayed result before handing it over to a final handler. 
+    This allows the starter of the worker function to not know 
+    anything about the lower-level objects. 
+    """
+    def __init__(self, handler, *args, **kwargs):
+        """Wrap handler(result, *args, **kwargs) so that the result 
+        it receives has been transformed by us. """
+        if handler is None:# assume rhs is a chain
+            self.__chain = args[0]
+        else:
+            if args or kwargs:
+                handler = Handler(handler, *args, **kwargs)
+            self.__chain = [handler]
+
+    def addSub(self, callable, *args, **kwargs):
+        """Add a sub-callable, ie a callable(result, *args, **kwargs)
+        that returns a transformed result to the previously added
+        sub-callable (or the handler given at construction, if this is 
+        the first call to addSub). """
+        self.__chain.append( Handler(callable, *args, **kwargs) )
+        
+    def clone(self):
+        """Clone the chain. Shallow only. Useful when several threads 
+        must be started but have different sub-callables. """
+        return PreProcessChain(None, self.__chain[:] )
+    
+    def cloneAddSub(self, callable, *args, **kwargs):
+        """Convenience method that first clones self, then calls addSub() 
+        on that clone with given arguments. """
+        cc = self.clone()
+        cc.addSub(callable, *args, **kwargs)
+        
+    def count(self):
+        """How many pre-processors in the chain"""
+        return len(self.__chain)
+    
+    class Traverser:
+        """
+        Traverses the chain of pre-processors it is given, transforming
+        the original delayedResult along the way. The return value of each 
+        callable added via addSub() is given to the previous addSub() callable,
+        until the handler is reached. 
+        """
+        def __init__(self, delayedResult, chain):
+            self.__dr = delayedResult
+            self.__chain = chain
+            
+        def get(self):
+            """This makes handler think we are a delayedResult."""
+            if not self.__chain:
+                return self.__dr.get()
+            
+            handler = self.__chain[0]
+            del self.__chain[0]
+            return handler(self)
+        
+        def getJobID(self):
+            """Return the job id for the delayedResult we transform."""
+            return self.__dr.getJobID()
+
+
+    def __call__(self, delayedResult):
+        """This makes us a Handler. We just call handler(Traverser). The
+        handler will think it is getting a delayed result, but in fact 
+        will be getting an instance of Traverser, which will take care
+        of properly applying the chain of transformations to delayedResult."""
+        chainTrav = self.Traverser(delayedResult, self.__chain[1:])
+        handler = self.__chain[0]
+        handler( chainTrav )
+        
+