Ticket #5732: multicall2.diff

File multicall2.diff, 19.5 KB (added by braudel, 2 years ago)

Corrected errors in documentation, docstrings, etc...

  • twisted/web/xmlrpc.py

     
    319319                                        ['string', 'string']] 
    320320 
    321321 
     322    @withRequest 
     323    def xmlrpc_multicall(self, request, procedureList): 
     324        """ 
     325        Execute several RPC methods in a single XMLRPC request using the 
     326        multicall object. 
     327 
     328        Example:: 
     329            On the server side, just load the instrospection so your 
     330            server has system.multicall. Then on the client:: 
     331 
     332                from twisted.web.xmlrpc import Proxy 
     333                from twisted.web.xmlrpc import MultiCall 
     334 
     335                proxy = Proxy('url of your server') 
     336 
     337                multiRPC = MultiCall( proxy ) 
     338                # queue a few calls 
     339                multiRPC.system.listMethods() 
     340                multiRPC.system.methodHelp('system.listMethods') 
     341                multiRPC.system.methodSignature('system.listMethods') 
     342 
     343                def handleResults(results): 
     344                    for success, result in results: 
     345                        print result 
     346 
     347                multiRPC().addCallback(handleResults) 
     348 
     349        @param request: The http C{request} object, obtained 
     350            via the @withRequest decorator.  
     351        @type request: L{http.Request} 
     352 
     353        @param procedureList: A list of dictionaries, each representing an 
     354            individual rpc call, containing the C{methodName} and the 
     355            C{params}. 
     356        @type procedureList: list 
     357 
     358        @return: L{defer.DeferredList} of the deferreds for each procedure 
     359            in procedure list. 
     360        @rtype: L{defer.DeferredList} 
     361        @since: 12.3 
     362        """ 
     363        def callError(error): 
     364            """ 
     365            Errorback to handle individual call errors. 
     366 
     367            Individual errors in a multicall are returned as 
     368            dictionaries. See U{http://www.xmlrpc.com/discuss/msgReader$1208}. 
     369 
     370            @param result: C{failure} 
     371            @type result: L{Failure} 
     372 
     373            @rtype: dict 
     374            @return: A dict with keys C{faultCode} and C{faultString}. 
     375            """ 
     376            log.err(error.value) 
     377            return {'faultCode':   self.FAILURE, 
     378                    'faultString': (error.value.faultString 
     379                        if isinstance(error.value, Fault) 
     380                        else getattr(error.value, 'message', ''))} 
     381 
     382        def prepareCallResponse(result): 
     383            """ 
     384            Callback to convert a call C{response} to a list. 
     385 
     386            The xmlrpc multicall spec expects a list wrapping 
     387            each call response. 
     388            See U{http://www.xmlrpc.com/discuss/msgReader$1208}. 
     389 
     390            @param result: C{response} 
     391            @type result: Any python type. 
     392 
     393            @rtype: list. 
     394            @return: A list with response as element 0. 
     395            """ 
     396            return [result] 
     397 
     398        def run(procedurePath, params): 
     399            """ 
     400            Run an individual procedure from the L{procedureList} and 
     401            returns a C{deferred}. 
     402 
     403            @param procedurePath: String naming a procedure. 
     404            @type procedurePath: str 
     405 
     406            @param params: List of arguments to be passed to the procedure. 
     407            @type params: list 
     408 
     409            @return: A C{deferred} object with prepareCallResponse and 
     410            callError attached. 
     411            @rtype: L{defer.Deferred} 
     412            """ 
     413            try: 
     414                procedure = self._xmlrpc_parent.lookupProcedure(procedurePath) 
     415            except NoSuchFunction, e: 
     416                return defer.fail(e).addErrback(callError) 
     417            else: 
     418                if getattr(procedure, 'withRequest', False): 
     419                    call = defer.maybeDeferred(procedure, request, *params) 
     420                else: 
     421                    call = defer.maybeDeferred(procedure, *params) 
     422 
     423                call.addCallback(prepareCallResponse) 
     424                call.addErrback(callError) 
     425                return call 
     426 
     427        results = [ 
     428            run(procedure['methodName'], procedure['params']) 
     429            for procedure in procedureList] 
     430 
     431        return (defer.DeferredList(results) 
     432            .addCallback(lambda results: [r[1] for r in results])) 
     433 
     434    xmlrpc_multicall.signature = [['array', 'array']] 
     435 
     436 
     437 
     438class _DeferredMultiCallProcedure(object): 
     439    """ 
     440    A helper object to store calls made on the 
     441    MultiCall object for batch execution. 
     442    @since: 12.3 
     443    """ 
     444    def __init__(self, call_list, name): 
     445        self.__call_list = call_list 
     446        self.__name = name 
     447 
     448 
     449    def __getattr__(self, name): 
     450        """ 
     451        Magic to emulate x.y.name lookups for 
     452        a remote procedure. 
     453        """ 
     454        return _DeferredMultiCallProcedure( 
     455            self.__call_list, 
     456            "%s.%s" % (self.__name, name) 
     457        ) 
     458 
     459 
     460    def __call__(self, *args): 
     461        """ 
     462        "Calling" an RPC on the multicall queues a deferred, 
     463        the procedure name, and its calling args. 
     464 
     465        @return: A L{defer.Deferred} that will be fired with the 
     466            results for this RPC. 
     467        @rtype: L{defer.Deferred} 
     468        """ 
     469        d = defer.Deferred() 
     470        self.__call_list.append((d, self.__name, args)) 
     471        return d 
     472 
     473 
     474 
     475class MultiCall(xmlrpclib.MultiCall): 
     476    """ 
     477    @param server: An object used to boxcar method calls 
     478    @type server: L{Proxy}. 
     479 
     480    @return: A L{defer.DeferredList} of all the deferreds for 
     481        each queued rpc call. 
     482    @rtype: L{defer.DeferredList} 
     483    @since: 12.3 
     484 
     485    Methods can be added to the MultiCall using normal 
     486    method call syntax e.g.:: 
     487 
     488        proxy = Proxy('http://advogato.org/XMLRPC') 
     489 
     490        multicall = MultiCall(proxy) 
     491        d1 = multicall.add(2,3) 
     492        d2 = multicall.add(5,6) 
     493 
     494    Or using the classic twisted L{Proxy} api:: 
     495 
     496        d3 = multicall.callRemote('add', 2, 3) 
     497 
     498    To execute the multicall, call the MultiCall object 
     499    and attach callbacks, errbacks to the returned 
     500    deferred.List e.g.:: 
     501 
     502        def printResults(results): 
     503            for result in results: 
     504                print result[1] 
     505 
     506        d = multicall() 
     507        d.addCallback(printResults) 
     508    """ 
     509    def __getattr__(self, name): 
     510        """ Get a ref to a helper object to emulate 
     511        RPC 'attributes lookup'. 
     512        """ 
     513        return _DeferredMultiCallProcedure(self.__call_list, name) 
     514 
     515 
     516    def callRemote(self, method, *args): 
     517        """ 
     518        Queue a call for C{method} on this multicall object 
     519        with the given arguments. 
     520 
     521        @return: A L{defer.Deferred} that will fire with the method response, 
     522            or a failure if the method raised a L{Fault}. 
     523        """ 
     524        return getattr(self, method)(*args) 
     525 
     526 
     527    def __call__(self): 
     528        """ 
     529        Execute the multicall, processing the deferreds for each 
     530        procedure once the results are ready. 
     531 
     532        @return: A L{defer.DeferredList} that will fire all the queued deferreds. 
     533        """ 
     534        marshalled_list = [] 
     535        deferreds = [] 
     536        for deferred, name, args in self.__call_list: 
     537            marshalled_list.append({ 
     538                'methodName': name, 
     539                'params': args}) 
     540            deferreds.append(deferred) 
     541 
     542        def processResults(results, deferreds): 
     543            """ 
     544            Callback to trigger the deferreds with their 
     545            corresponding RPC's results. 
     546            """ 
     547            for d, result in zip(deferreds, results): 
     548                if isinstance(result, dict): 
     549                    d.errback(Fault(result['faultCode'], 
     550                        result['faultString'])) 
     551 
     552                elif isinstance(result, list): 
     553                    d.callback(result[0]) 
     554 
     555                else: 
     556                    raise ValueError( 
     557                        "Unexpected type in multicall result.") 
     558 
     559        self.__server.callRemote( 
     560            'system.multicall', marshalled_list 
     561        ).addCallback(processResults, deferreds) 
     562 
     563        return defer.DeferredList(deferreds) 
     564 
     565 
    322566def addIntrospection(xmlrpc): 
    323567    """ 
    324568    Add Introspection support to an XMLRPC server. 
     
    587831__all__ = [ 
    588832    "XMLRPC", "Handler", "NoSuchFunction", "Proxy", 
    589833 
    590     "Fault", "Binary", "Boolean", "DateTime"] 
     834    "Fault", "Binary", "Boolean", "DateTime", "MultiCall"] 
  • twisted/web/test/test_xmlrpc.py

     
    1414from twisted.web import xmlrpc 
    1515from twisted.web.xmlrpc import ( 
    1616    XMLRPC, payloadTemplate, addIntrospection, _QueryFactory, Proxy, 
    17     withRequest) 
     17    withRequest, MultiCall) 
    1818from twisted.web import server, static, client, error, http 
    1919from twisted.internet import reactor, defer 
    2020from twisted.internet.error import ConnectionDone 
     
    2828else: 
    2929    sslSkip = None 
    3030 
    31  
    3231class AsyncXMLRPCTests(unittest.TestCase): 
    3332    """ 
    3433    Tests for L{XMLRPC}'s support of Deferreds. 
     
    667666        return d 
    668667 
    669668 
     669 
    670670class XMLRPCTestIntrospection(XMLRPCTestCase): 
    671671 
    672672    def setUp(self): 
     
    686686                 'deferFault', 'dict', 'echo', 'fail', 'fault', 
    687687                 'pair', 'system.listMethods', 
    688688                 'system.methodHelp', 
    689                  'system.methodSignature', 'withRequest']) 
     689                 'system.methodSignature', 'system.multicall',  
     690                 'withRequest']) 
    690691 
    691692        d = self.proxy().callRemote("system.listMethods") 
    692693        d.addCallback(cbMethods) 
     
    720721        return defer.DeferredList(dl, fireOnOneErrback=True) 
    721722 
    722723 
     724 
     725class FakeProxy(object): 
     726    """ 
     727    Fake twisted XMLRPC Proxy client to run tests without using 
     728    the network 
     729    """ 
     730    def __init__(self, resource): 
     731        self.resource = resource 
     732 
     733 
     734    def callRemote(self, methodName, *args): 
     735        """ 
     736        emulate twisted.web.xmlrpc.Proxy.callRemote 
     737        """ 
     738        # build request 
     739        request = DummyRequest(['']) 
     740        request.method = 'POST' 
     741        request.content = StringIO( 
     742            payloadTemplate % (methodName, xmlrpclib.dumps(args))) 
     743         
     744        def returnResponse( requestResponse ): 
     745            results = xmlrpclib.loads(requestResponse)[0] 
     746            if len(results) == 1: 
     747                results = results[0] 
     748            return results 
     749 
     750        # look mom no network! 
     751        self.resource.render(request) 
     752 
     753        return (defer.succeed("".join(request.written)) 
     754            .addCallback(returnResponse)) 
     755 
     756 
     757 
     758class XMLRPCTestMultiCall(unittest.TestCase): 
     759    """ 
     760    Tests for xmlrpc multicalls 
     761    """ 
     762    def setUp(self): 
     763        self.resource = Test() 
     764        addIntrospection(self.resource) 
     765        self.proxy = FakeProxy(self.resource) 
     766 
     767 
     768    def test_multicall(self): 
     769        """ 
     770        test a suscessfull multicall 
     771        """ 
     772        inputs = range(5) 
     773        m = MultiCall(self.proxy) 
     774        for x in inputs: 
     775            m.echo(x) 
     776 
     777        def testResults(results): 
     778            self.assertEqual(inputs, [x[1] for x in results]) 
     779 
     780        resultsDeferred = m().addCallback(testResults) 
     781        self.assertTrue(resultsDeferred.called)  
     782 
     783 
     784    def test_multicall_callRemote(self): 
     785        """ 
     786        test a suscessfull multicall using 
     787        multicall.callRemote instead of attribute lookups 
     788        """ 
     789        inputs = range(5) 
     790        m = MultiCall(self.proxy) 
     791        for x in inputs: 
     792            m.callRemote('echo', x) 
     793 
     794        def testResults(results): 
     795            self.assertEqual(inputs, [x[1] for x in results]) 
     796 
     797        resultsDeferred = m().addCallback(testResults) 
     798        self.assertTrue(resultsDeferred.called) 
     799 
     800 
     801    def test_multicall_with_callbacks(self): 
     802        """  
     803        test correct execution of callbacks added to the 
     804        multicall's returned deferreds for each individual queued 
     805        call 
     806        """ 
     807        inputs = range(5) 
     808        m = MultiCall(self.proxy) 
     809        for x in inputs: 
     810            d = m.echo(x) 
     811            d.addCallback( lambda x : x*x ) 
     812 
     813        def testResults(results): 
     814            self.assertEqual([ x*x for x in inputs], [x[1] for x in results]) 
     815 
     816        resultsDeferred = m().addCallback(testResults) 
     817        self.assertTrue(resultsDeferred.called) 
     818 
     819 
     820    def test_multicall_errorback(self): 
     821        """  
     822        test that an error (an invalid - not found - method)  
     823        does not propagate if properly handled in the errorback 
     824        of an individual deferred 
     825        """ 
     826        def trapFoo(error): 
     827            error.trap(xmlrpclib.Fault) 
     828            self.assertEqual(error.value.faultString, 
     829                'procedure foo not found', 
     830                'check we have a failure message' 
     831                )  
     832            self.flushLoggedErrors(xmlrpc.NoSuchFunction) 
     833 
     834 
     835        m = MultiCall(self.proxy) 
     836        m.echo(1) 
     837        # method not present on server 
     838        m.foo().addErrback(trapFoo) 
     839        m.echo(2) 
     840 
     841        def handleErrors(error): 
     842            error.trap(xmlrpclib.Fault) 
     843            self.assertEqual(error.value.faultString, 
     844                'xmlrpc_echo() takes exactly 2 arguments (4 given)') 
     845            self.flushLoggedErrors(TypeError) 
     846 
     847        m.echo(1,2,3).addErrback(handleErrors) 
     848 
     849        def testResults(results): 
     850            """  
     851            the errorback should have trapped the error 
     852            """ 
     853            self.assertEqual(results[1], (True, None), 
     854            'failure trapped in errorback does not propagate to deferredList results') 
     855 
     856        resultsDeferred = m().addCallback(testResults) 
     857        self.assertTrue(resultsDeferred.called) 
     858 
     859 
     860    def test_multicall_withRequest(self): 
     861        """ 
     862        Test that methods decorated with @withRequest are handled correctly 
     863        """ 
     864        m = MultiCall(self.proxy) 
     865        m.echo(1) 
     866        # method decorated with withRequest 
     867        msg = 'hoho' 
     868        m.withRequest(msg) 
     869        m.echo(2) 
     870 
     871        def testResults(results): 
     872            """ 
     873            test that a withRequest decorated method was properly handled 
     874            """ 
     875            self.assertEqual(results[1][1],  
     876                'POST %s' % msg, 'check withRequest decorated result') 
     877 
     878        resultsDeferred = m().addCallback(testResults) 
     879        self.assertTrue(resultsDeferred.called) 
     880 
     881 
     882    def test_multicall_with_xmlrpclib(self): 
     883        """ 
     884        check that the sever's response is also compatible with xmlrpclib 
     885        MultiCall client 
     886        """ 
     887        class PatchedXmlrpclibProxy(object): 
     888            """ 
     889            A proxy that more closely resembles xmlrpclib.ServerProxy 
     890            """ 
     891            def __init__(self, resource): 
     892                self.resource = resource 
     893 
     894            def __request(self, methodName, params): 
     895                """ 
     896                Patched xmlrpclib.ServerProxy.__request to emulate 
     897                RPC call without using the network 
     898                """ 
     899                request = DummyRequest(['']) 
     900                request.method = 'POST' 
     901                request.content = StringIO( 
     902                    payloadTemplate % (methodName, xmlrpclib.dumps(params))) 
     903 
     904                self.resource.render(request) 
     905                response =  xmlrpclib.loads("".join(request.written))[0] 
     906                if len(response) == 1: 
     907                    response = response[0] 
     908                return response 
     909 
     910            def __getattr__(self, name): 
     911                """ 
     912                magic method dispatcher 
     913                """ 
     914                return xmlrpclib._Method(self.__request, name) 
     915 
     916        inputs = range(5) 
     917        m = xmlrpclib.MultiCall( 
     918            PatchedXmlrpclibProxy(self.resource)) 
     919        for x in inputs: 
     920            m.echo(x) 
     921 
     922        self.assertEqual( 
     923                inputs,  
     924                list(m()),  
     925                'xmlrpclib multicall can talk to the twisted multicall') 
     926 
     927 
     928 
    723929class XMLRPCClientErrorHandling(unittest.TestCase): 
    724930    """ 
    725931    Test error handling on the xmlrpc client. 
  • doc/web/howto/xmlrpc.xhtml

     
    234234no <code class="python">help</code> attribute is specified, the 
    235235method's documentation string is used instead.</p> 
    236236 
     237 
     238  
    237239<h2>SOAP Support</h2> 
    238240 
    239241<p>From the point of view of a Twisted developer, there is little difference 
     
    295297[8, 15] 
    296298</pre> 
    297299 
     300<h3>Using multicall Objects</h3> 
     301<p>Another informal pattern commonly used along with xmlrpc 
     302introspection is the use of a multicall object to boxcar several  
     303rpc calls in a single http request.  
     304 
     305An instance of a <code base="twisted.web.xmlrpc" class="API">MultiCall</code> object is created on the client side and several RPC calls are queued by  
     306calling them on this <code base="twisted.web.xmlrpc" class="API">MultiCall</code> instance. Every RPC called on the <code base="twisted.web.xmlrpc" class="API">MultiCall</code> returns a <code class="API" base="twisted.internet.defer">Deferred</code> that you can use to attach callbacks to each queued rpc response. 
     307 
     308Executing the <code base="twisted.web.xmlrpc" class="API">MultiCall</code> instance itself triggers a request to  
     309<code class="python">system.multicall</code> (<code class="API" base="twisted.web.xmlrpc">XMLRPCIntrospection.multicall</code>) with a list of dictionaries representing each of the procedures to call, returning a <code class="API" base="twisted.internet.defer">DeferredList</code> of the deferreds corresponding to each individual call.  
     310 
     311Let's see an example to talk to the server above using a <code base="twisted.web.xmlrpc" class="API">MultiCall</code>:</p> 
     312 
     313<pre class="python"> 
     314from twisted.web.xmlrpc import Proxy, MultiCall, NoSuchFunction 
     315from twisted.internet import reactor 
     316 
     317def printResults(results): 
     318    for result in results: 
     319        print result[1] 
     320    reactor.stop() 
     321 
     322proxy = Proxy('http://127.0.0.1:7080') 
     323 
     324multiRPC = MultiCall(proxy) 
     325 
     326# queue a few echo calls, with a callback to square the results 
     327for x in range(10): 
     328    multiRPC.echo(x).addCallback(lambda x: x*x) 
     329 
     330# you can of course use the <code>callRemote</code> method instead 
     331# for a better twisted-like feeling 
     332for x in range(10,20): 
     333    multiRPC.callRemote('echo', x).addCallback(lambda x: x*x) 
     334 
     335# now queue a few more echo calls with a callback that will  
     336# indicate the echoed letter and their index in the original string 
     337for index, x in enumerate('hello'): 
     338    multiRPC.echo(x).addCallback(lambda result, index: '%s found at %d' % (result, index)) 
     339 
     340# individual deferreds ideally should also handle errors 
     341def handleErrors(error): 
     342    error.trap(xmlrpclib.Fault) 
     343    print "yay! The server said %s" % error.value.faultString 
     344multiRPC.foo().addErrback(handleErrors) 
     345 
     346# finally execute the multiRPC instance, to send the request,  
     347# then handle the returned DeferredList  
     348# results with the printResults callback 
     349multiRPC().addCallback(printResults) 
     350reactor.run() 
     351</pre> 
     352 
     353The <code class="API">twisted.web.XMLRPCIntrospection.multicall</code> method is also 100% compatible with the xmlrpclib MultiCall, so you can also use xmlrpclib as the client: 
     354 
     355<pre class="python"> 
     356from xmlrpclib import ServerProxy, Multicall 
     357 
     358proxy = ServerProxy('http://127.0.0.1:7080') 
     359 
     360multiRPC = MultiCall(proxy) 
     361for x in range(10): 
     362    multiRPC.echo(x) 
     363 
     364for result in multiRPC(): 
     365    print result * result 
     366</pre> 
     367 
     368 
    298369<h2>Serving SOAP and XML-RPC simultaneously</h2> 
    299370 
    300371<p><code class="API">twisted.web.xmlrpc.XMLRPC</code> and <code