Index: twisted/web/xmlrpc.py
===================================================================
--- twisted/web/xmlrpc.py	(revision 35733)
+++ twisted/web/xmlrpc.py	(working copy)
@@ -318,7 +318,241 @@
     xmlrpc_methodSignature.signature = [['array', 'string'],
                                         ['string', 'string']]
 
+    @withRequest
+    def xmlrpc_multicall(self, request, procedureList):
+        """
+        Execute several RPC methods in a single XMLRPC request using the
+        multicall object.
 
+        Example:
+            On the server side, just load the instrospection so your
+            server has system.multicall. Then on the client:
+
+            from twisted.web.xmlrpc import Proxy
+            from twisted.web.xmlrpc import MultiCall
+
+            proxy = Proxy('url of your server')
+
+            multiRPC = Multicall( proxy )
+            # queue a few calls
+            multiRPC.system.listMethods()
+            multiRPC.system.methodHelp('system.listMethods')
+            multiRPC.system.methodSignature('system.listMethods')
+
+            def handleResults(results):
+                for success, result in results:
+                    print result
+
+            multiRPC().addCallback(handleResults)
+
+        @param request: the http C{request} object, obtained 
+            via the @withRequest decorator 
+        @type request: L{http.Request}
+
+        @param procedureList: A list of dictionaries, each representing an
+            individual rpc call, containing the C{methodName} and the 
+            C{params}
+        @type procedureList: list
+
+        @return: L{defer.DeferredList} of the deferreds for each procedure
+            in procedure list
+        @rtype: L{defer.DeferredList}
+        """
+        def callError(error):
+            """ 
+            errorback to handle individual call errors
+
+            Individual errors in a multicall are returned as
+            dictionaries. See U{http://www.xmlrpc.com/discuss/msgReader$1208}.
+
+            @param result: C{failure}
+            @type result: L{Failure}
+
+            @rtype: dict
+            @return: a dict with keys C{faultCode} and C{faultString}
+            """ 
+            log.err(error.value)
+            return {'faultCode':   self.FAILURE,
+                    'faultString': (error.value.faultString
+                        if isinstance(error.value, Fault)
+                        else getattr(error.value, 'message', ''))}
+            
+        def prepareCallResponse(result):
+            """
+            callback to convert a call C{response} to a list
+
+            The xmlrpc multicall spec expects a list wrapping 
+            each call response. 
+            See U{http://www.xmlrpc.com/discuss/msgReader$1208}.
+
+            @param result: C{response}
+            @type result: any python type
+
+            @rtype: list
+            @return: a list with response as element 0  
+            """
+            return [result]
+
+        def run(procedurePath, params):
+            """
+            run an individual procedure from the L{procedureList} and
+            returns a C{deferred}
+
+
+            @param procedurePath: string naming a procedure
+            @type procedurePath: str
+            
+            @param params: list of arguments to be passed to the procedure
+            @type params: list
+
+            @return: a C{deferred} object with prepareCallResponse and
+            callError attached.
+            @rtype: L{defer.Deferred}
+            """
+            try:
+                procedure = self._xmlrpc_parent.lookupProcedure(procedurePath)
+            except NoSuchFunction, e:
+                return defer.fail(e).addErrback(callError)
+            else:
+                if getattr(procedure, 'withRequest', False):
+                    call = defer.maybeDeferred(procedure, request, *params)
+                else:
+                    call = defer.maybeDeferred(procedure, *params)
+                
+                call.addCallback(prepareCallResponse)
+                call.addErrback(callError)
+                return call
+
+        results = [
+            run(procedure['methodName'], procedure['params'])
+            for procedure in procedureList]
+
+        return (defer.DeferredList(results)
+            .addCallback(lambda results: [r[1] for r in results]))
+
+    xmlrpc_multicall.signature = [['array', 'array']]
+  
+
+class _DeferredMultiCallProcedure(object):
+    '''
+    A helper object to store calls made on the
+    MultiCall object for batch execution
+    '''
+    def __init__(self, call_list, name):
+        self.__call_list = call_list
+        self.__name = name
+    
+    def __getattr__(self, name):
+        '''
+        magic to emulate x.y.name lookups for
+        a remote procedure
+        '''  
+        return _DeferredMultiCallProcedure(
+            self.__call_list, 
+            "%s.%s" % (self.__name, name)
+        )
+    
+    def __call__(self, *args):
+        '''
+        "calling" the RPC on the multicall queues a deferred, 
+        the procedure name, and its calling args. 
+
+        @return: a L{defer.Deferred} that will be fired when the
+            results for this RPC are processed
+        @rtype: L{defer.Deferred}
+        '''
+        d = defer.Deferred()
+        self.__call_list.append((d, self.__name, args))
+        return d
+
+class MultiCall(xmlrpclib.MultiCall):
+    """
+    @param server: a object used to boxcar method calls
+    @type server should be a twisted xmlrpc L{Proxy} object.
+
+    @return: a L{defer.DeferredList} of all the deferreds for
+        each queued rpc call
+    @rtype: L{defer.DeferredList}
+
+    Methods can be added to the MultiCall using normal
+    method call syntax e.g.:
+    
+    proxy = Proxy('http://advogato.org/XMLRPC')
+
+    multicall = MultiCall(proxy)
+    d1 = multicall.add(2,3)
+    d2 = multicall.add(5,6)
+    
+    or
+
+    d3 = multicall.callRemote('add', 2, 3)
+
+    To execute the multicall, call the MultiCall object 
+    and attach callbacks, errbacks to the returned
+    deferred e.g.:
+    
+    def printResults(results):
+        for result in results:
+            print result[1]
+
+    d = multicall()
+    d.addCallback(printResults)
+    """
+
+    def __getattr__(self, name):
+        ''' get a ref to a helper object to emulate
+        "RPC properties lookup" 
+        '''
+        return _DeferredMultiCallProcedure(self.__call_list, name)
+
+    def callRemote(self, method, *args):
+        ''' queue a call for c{method} on this multicall object
+        with given arguments.
+
+        @return: a L{defer.Deferred} that will fire with the method response,
+            or a failure if the method failed.
+        '''
+        return getattr(self, method)(*args)
+
+    def __call__(self):
+        """
+        execute the multicall, processing the deferreds for each
+        procedure once the results are ready
+
+        @return: a L{defer.DeferredList} that will fire all the queued deferreds
+        """
+        marshalled_list = []
+        deferreds = []
+        for deferred, name, args in self.__call_list:
+            marshalled_list.append({
+                'methodName': name,
+                'params': args})
+            deferreds.append(deferred)
+
+        def processResults(results, deferreds):
+            '''
+            callback to return an xmlrpclib
+            MultiCallIterator of the results
+            '''
+            for d, result in zip(deferreds, results):
+                if isinstance(result, dict):
+                    d.errback(Fault(result['faultCode'], 
+                        result['faultString']))
+                
+                elif isinstance(result, list):
+                    d.callback(result[0])
+
+                else:
+                    raise ValueError(
+                        "unexpected type in multicall result")
+
+        self.__server.callRemote(
+            'system.multicall', marshalled_list
+        ).addCallback(processResults, deferreds)
+
+        return defer.DeferredList(deferreds)
+
+
 def addIntrospection(xmlrpc):
     """
     Add Introspection support to an XMLRPC server.
Index: twisted/web/test/test_xmlrpc.py
===================================================================
--- twisted/web/test/test_xmlrpc.py	(revision 35733)
+++ twisted/web/test/test_xmlrpc.py	(working copy)
@@ -14,7 +14,7 @@
 from twisted.web import xmlrpc
 from twisted.web.xmlrpc import (
     XMLRPC, payloadTemplate, addIntrospection, _QueryFactory, Proxy,
-    withRequest)
+    withRequest, MultiCall)
 from twisted.web import server, static, client, error, http
 from twisted.internet import reactor, defer
 from twisted.internet.error import ConnectionDone
@@ -27,8 +27,8 @@
     sslSkip = "OpenSSL not present"
 else:
     sslSkip = None
+from twisted.internet.threads import deferToThread
 
-
 class AsyncXMLRPCTests(unittest.TestCase):
     """
     Tests for L{XMLRPC}'s support of Deferreds.
@@ -686,7 +686,8 @@
                  'deferFault', 'dict', 'echo', 'fail', 'fault',
                  'pair', 'system.listMethods',
                  'system.methodHelp',
-                 'system.methodSignature', 'withRequest'])
+                 'system.methodSignature', 'system.multicall', 
+                 'withRequest'])
 
         d = self.proxy().callRemote("system.listMethods")
         d.addCallback(cbMethods)
@@ -720,6 +721,143 @@
         return defer.DeferredList(dl, fireOnOneErrback=True)
 
 
+class XMLRPCTestMultiCall(XMLRPCTestCase):
+
+    def setUp(self):
+        xmlrpc = Test()
+        addIntrospection(xmlrpc)
+        self.p = reactor.listenTCP(0, server.Site(xmlrpc),interface="127.0.0.1")
+        self.port = self.p.getHost().port
+        self.factories = []
+
+
+    def test_multicall(self):
+        '''
+        test a suscessfull multicall
+        '''
+        inputs = range(5)
+        m = MultiCall(self.proxy())
+        for x in inputs:
+            m.echo(x)
+
+        def testResults(results):
+            self.assertEqual(inputs, [x[1] for x in results])
+
+        return m().addCallback(testResults)
+
+
+    def test_multicall_callRemote(self):
+        '''
+        test a suscessfull multicall using
+        multicall.callRemote instead of attribute lookups
+        '''
+        inputs = range(5)
+        m = MultiCall(self.proxy())
+        for x in inputs:
+            m.callRemote('echo', x)
+
+        def testResults(results):
+            self.assertEqual(inputs, [x[1] for x in results])
+
+        return m().addCallback(testResults)
+
+
+    def test_multicall_with_callbacks(self):
+        ''' 
+        test correct execution of callbacks added to
+        multicall returned deferred for each individual queued
+        call
+        '''
+        inputs = range(5)
+        m = MultiCall(self.proxy())
+        for x in inputs:
+            d = m.echo(x)
+            d.addCallback( lambda x : x*x )
+
+        def testResults(results):
+            self.assertEqual([ x*x for x in inputs], [x[1] for x in results])
+
+        return m().addCallback(testResults)
+
+    def test_multicall_errorback(self):
+        ''' 
+        test  an error (an invalid (not found) method ) 
+        does not propagate if properly handled in the errorback
+        of an individual deferred
+        '''
+        def trapFoo(error):
+            error.trap(xmlrpclib.Fault)
+            self.assertEqual(error.value.faultString,
+                'procedure foo not found',
+                'check we have a failure message'
+                ) 
+            self.flushLoggedErrors(xmlrpc.NoSuchFunction)
+
+
+        m = MultiCall(self.proxy())
+        m.echo(1)
+        # method not present on server
+        m.foo().addErrback(trapFoo)
+        m.echo(2)
+
+        def handleErrors(error):
+            error.trap(xmlrpclib.Fault)
+            self.assertEqual(error.value.faultString,
+                'xmlrpc_echo() takes exactly 2 arguments (4 given)')
+            self.flushLoggedErrors(TypeError)
+
+        m.echo(1,2,3).addErrback(handleErrors)
+
+        def testResults(results):
+            ''' 
+            the errorback should have trapped the error
+            '''
+            self.assertEqual(results[1], (True, None),
+            'failure trapped in errorback does not propagate to deferredList results')
+
+        return m().addCallback(testResults)
+
+    def test_multicall_withRequest(self):
+        '''
+        Test that methods decorated with @withRequest are handled correctly
+        '''
+        m = MultiCall(self.proxy())
+        m.echo(1)
+        # method decorated with withRequest
+        msg = 'hoho'
+        m.withRequest(msg)
+        m.echo(2)
+
+        def testResults(results):
+            '''
+            test that a withRequest decorated method was properly handled
+            '''
+            self.assertEqual(results[1][1], 
+                'POST %s' % msg, 'check withRequest decorated result')
+        return m().addCallback(testResults)
+
+    def test_multicall_with_xmlrpclib(self):
+        '''
+        check that the sever's response is also compatible with xmlrpclib
+        MultiCall client
+        '''
+        inputs = range(5)
+        def doMulticall(inputs):
+            m = xmlrpclib.MultiCall(
+                xmlrpclib.ServerProxy("http://127.0.0.1:%d" % self.port)
+            )
+            for x in inputs:
+                m.echo(x)
+            return m()
+
+        def testResults(iterator):
+            self.assertEqual(
+                inputs, 
+                list(iterator), 
+                'xmlrpclib multicall can talk to the twisted multicall')
+
+        return deferToThread(doMulticall, inputs).addCallback(testResults)
+
 class XMLRPCClientErrorHandling(unittest.TestCase):
     """
     Test error handling on the xmlrpc client.
Index: doc/web/howto/xmlrpc.xhtml
===================================================================
--- doc/web/howto/xmlrpc.xhtml	(revision 35733)
+++ doc/web/howto/xmlrpc.xhtml	(working copy)
@@ -234,6 +234,8 @@
 no <code class="python">help</code> attribute is specified, the
 method's documentation string is used instead.</p>
 
+
+ 
 <h2>SOAP Support</h2>
 
 <p>From the point of view of a Twisted developer, there is little difference
@@ -295,6 +297,82 @@
 [8, 15]
 </pre>
 
+<h3>Using multicall Objects</h3>
+<p>Another informal pattern commonly used along with xmlrpc
+introspection is the use of a multicall object to boxcar several 
+rpc calls in a single http request. 
+
+An instance of a multicall object 
+is created on the client side and several RPC calls are queued by 
+calling them on this multicall instance. Every RPC called on the 
+multicall returns a deferred that you can use to attach callbacks 
+to each queued rpc response.
+
+Executing the multicall instance itself triggers a request to 
+<code>system.multicall</code> with a list of dictionaries representing
+each of the procedures to call, returning a <code>defer.DeferredList</code>
+of the deferreds corresponding to each individual call. 
+
+Let's see an 
+example to talk to the server above using a multicall:</p>
+
+<pre class="python">
+from twisted.web.xmlrpc import Proxy, MultiCall, NoSuchFunction
+from twisted.internet import reactor
+
+def printResults(results):
+    for result in results:
+        print result[1]
+    reactor.stop()
+
+proxy = Proxy('http://127.0.0.1:7080')
+
+multiRPC = MultiCall(proxy)
+
+# queue a few echo calls, with a callback to square the results
+for x in range(10):
+    multiRPC.echo(x).addCallback(lamdba x: x*x)
+
+# you can of course use the <code>callRemote</code> method instead
+# for a better twisted-like feeling
+for x in range(10,20):
+    multiRPC.callRemote('echo', x).addCallback(lamdba x: x*x)
+
+# now queue a few more echo calls with a callback that will 
+# indicate the echoed letter and their index in the original string
+for index, x in enumerate('hello'):
+    multiRPC.echo(x).addCallback(lambda result, index: '%s found at %d' % (result, index))
+
+# individual deferreds ideally should also handle errors
+def handleErrors(error):
+    error.trap(xmlrpclib.Fault)
+    print "yay! The server said %s" % error.value.faultString
+multiRPC.foo().addErrback(NoSuchFunction)
+
+# finally execute the multiRPC instance, to send the request, 
+# then handle the returned DeferredList 
+# results with the printResults callback
+multiRPC().addCallback(printResults)
+reactor.run()
+</pre>
+
+The <code>system.multicall</code> method is also 100% compatible with the xmlrpclib
+MultiCall, so you can also use the blocking xmlrpclib as the client:
+
+<pre>
+from xmlrpclib import ServerProxy, Multicall
+
+proxy = ServerProxy('http://127.0.0.1:7080')
+
+multiRPC = MultiCall(proxy)
+for x in range(10):
+    multiRPC.echo(x)
+
+for result in multiRPC():
+    print result * result
+</pre>
+
+
 <h2>Serving SOAP and XML-RPC simultaneously</h2>
 
 <p><code class="API">twisted.web.xmlrpc.XMLRPC</code> and <code
