Ticket #5732: multicall2.diff

File multicall2.diff, 19.5 KB (added by braudel, 3 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