Index: doc/howto/xmlrpc.html =================================================================== RCS file: /cvs/Twisted/doc/howto/xmlrpc.html,v retrieving revision 1.11 diff -u -r1.11 xmlrpc.html --- doc/howto/xmlrpc.html 19 Jul 2003 12:42:34 -0000 1.11 +++ doc/howto/xmlrpc.html 26 Aug 2003 05:33:34 -0000 @@ -86,6 +86,57 @@ xmlquote.rpy +

Using XML-RPC sub-handlers

+ +

XML-RPC resource can be nested so that one handler calls another if +a method with a given prefix is called. For example, to add support for +an XML-RPC method called 'date.time' to the Example +class, you could do the following:

+ +
+
+import time
+from twisted.web import xmlrpc, server
+
+class Example(xmlrpc.XMLRPC):
+    """An example object to be published."""
+    
+    def xmlrpc_echo(self, x):
+        """Return all passed args."""
+        return x
+    
+    def xmlrpc_add(self, a, b):
+        """Return sum of arguments."""
+        return a + b
+
+class Date(xmlrpc.XMLRPC):
+    """Serve the XML-RPC 'time' method."""
+    
+    def xmlrpc_time(self):
+        """Return UNIX time."""
+        return time.time()
+
+def main():
+    from twisted.internet.app import Application
+    app = Application("xmlrpc")
+    r = Example()
+    date = Date()
+    r.putSubHandler('date', date)
+    app.listenTCP(7080, server.Site(r))
+    return app
+
+application = main()
+
+if __name__ == '__main__':
+    application.run(save=0)
+
+ +

By default, a period ('.') separates the prefix from the method +name, but you can use a different character by overriding the XMLRPC.separator data member in your base +XML-RPC server. XML-RPC servers may be nested to arbitrary depths +using this method.

+

SOAP Support

From the point of view, of a Twisted developer, there is little difference @@ -166,4 +217,3 @@ - Index: twisted/test/test_xmlrpc.py =================================================================== RCS file: /cvs/Twisted/twisted/test/test_xmlrpc.py,v retrieving revision 1.8 diff -u -r1.8 test_xmlrpc.py --- twisted/test/test_xmlrpc.py 24 Aug 2003 13:47:31 -0000 1.8 +++ twisted/test/test_xmlrpc.py 26 Aug 2003 05:33:37 -0000 @@ -18,6 +18,7 @@ # """Test XML-RPC support.""" + try: import xmlrpclib except ImportError: @@ -25,7 +26,7 @@ class XMLRPC: pass else: from twisted.web import xmlrpc - from twisted.web.xmlrpc import XMLRPC + from twisted.web.xmlrpc import XMLRPC, XMLRPCIntrospection from twisted.trial import unittest from twisted.web import server @@ -37,16 +38,31 @@ FAILURE = 666 NOT_FOUND = 23 - + + # the doc string is part of the test def xmlrpc_add(self, a, b): + """This function add two numbers.""" return a + b + xmlrpc_add.signature = [['int', 'int', 'int'], + ['double', 'double', 'double']] + + # the doc string is part of the test + def xmlrpc_pair(self, string, num): + """This function puts the two arguments in an array.""" + return [string, num] + + xmlrpc_pair.signature = [['array', 'string', 'int']] + + # the doc string is part of the test def xmlrpc_defer(self, x): + """Help for defer.""" return defer.succeed(x) def xmlrpc_deferFail(self): return defer.fail(ValueError()) + # don't add a doc string, it's part of the test def xmlrpc_fail(self): raise RuntimeError @@ -59,10 +75,17 @@ def xmlrpc_complex(self): return {"a": ["b", "c", 12, []], "D": "foo"} + def xmlrpc_dict(self, map, key): + return map[key] + + xmlrpc_dict.help = 'Help for dict.' + + class XMLRPCTestCase(unittest.TestCase): def setUp(self): - self.p = reactor.listenTCP(0, server.Site(Test()), interface="127.0.0.1") + self.p = reactor.listenTCP(0, server.Site(Test()), + interface="127.0.0.1") self.port = self.p.getHost()[2] def tearDown(self): @@ -72,16 +95,20 @@ def proxy(self): return xmlrpc.Proxy("http://localhost:%d/" % self.port) - + def testResults(self): x = self.proxy().callRemote("add", 2, 3) self.assertEquals(unittest.deferredResult(x), 5) x = self.proxy().callRemote("defer", "a") self.assertEquals(unittest.deferredResult(x), "a") + x = self.proxy().callRemote("dict", {"a" : 1}, "a") + self.assertEquals(unittest.deferredResult(x), 1) + x = self.proxy().callRemote("pair", 'a', 1) + self.assertEquals(unittest.deferredResult(x), ['a', 1]) x = self.proxy().callRemote("complex") self.assertEquals(unittest.deferredResult(x), {"a": ["b", "c", 12, []], "D": "foo"}) - + def testErrors(self): for code, methodName in [(666, "fail"), (666, "deferFail"), (12, "fault"), (23, "noSuchMethod"), @@ -97,6 +124,52 @@ class XMLRPCTestCase2(XMLRPCTestCase): """Test with proxy that doesn't add a slash.""" - + def proxy(self): return xmlrpc.Proxy("http://localhost:%d" % self.port) + + +class XMLRPCTestIntrospection(XMLRPCTestCase): + + def setUp(self): + xmlrpc = Test() + XMLRPCIntrospection(xmlrpc) + self.p = reactor.listenTCP(0, server.Site(xmlrpc),interface="127.0.0.1") + self.port = self.p.getHost()[2] + + def testListMethods(self): + d = self.proxy().callRemote("system.listMethods") + list = unittest.deferredResult(d) + list.sort() + self.failUnlessEqual(list, ['add', 'complex', 'defer', 'deferFail', + 'deferFault', 'dict', 'fail', 'fault', + 'pair', 'system.listMethods', + 'system.methodHelp', + 'system.methodSignature']) + + def testMethodHelp(self): + d = self.proxy().callRemote("system.methodHelp", 'defer') + help = unittest.deferredResult(d) + self.failUnlessEqual(help, 'Help for defer.') + + d = self.proxy().callRemote("system.methodHelp", 'fail') + help = unittest.deferredResult(d) + self.failUnlessEqual(help, '') + + d = self.proxy().callRemote("system.methodHelp", 'dict') + help = unittest.deferredResult(d) + self.failUnlessEqual(help, 'Help for dict.') + + def testMethodSignature(self): + d = self.proxy().callRemote("system.methodSignature", 'defer') + sig = unittest.deferredResult(d) + self.failUnlessEqual(sig, '') + + d = self.proxy().callRemote("system.methodSignature", 'add') + sig = unittest.deferredResult(d) + self.failUnlessEqual(sig, [['int', 'int', 'int'], + ['double', 'double', 'double']]) + + d = self.proxy().callRemote("system.methodSignature", 'pair') + sig = unittest.deferredResult(d) + self.failUnlessEqual(sig, [['array', 'string', 'int']]) Index: twisted/web/xmlrpc.py =================================================================== RCS file: /cvs/Twisted/twisted/web/xmlrpc.py,v retrieving revision 1.27 diff -u -r1.27 xmlrpc.py --- twisted/web/xmlrpc.py 27 Jul 2003 00:04:14 -0000 1.27 +++ twisted/web/xmlrpc.py 26 Aug 2003 05:33:38 -0000 @@ -34,7 +34,7 @@ # Sibling Imports from twisted.web import resource, server from twisted.internet import defer, protocol, reactor -from twisted.python import log +from twisted.python import log, reflect from twisted.protocols import http # These are deprecated, use the class level definitions @@ -89,6 +89,10 @@ Binary, Boolean, DateTime, Deferreds, or Handler instances. By default methods beginning with 'xmlrpc_' are published. + + Sub-handlers for prefixed methods (e.g., system.listMethods) + can be added with putSubHandler. By default, prefixes are + separated with a '.'. Override self.separator to change this. """ # Error codes for Twisted, if they conflict with yours then @@ -97,7 +101,21 @@ FAILURE = 8002 isLeaf = 1 - + separator = '.' + + def __init__(self): + resource.Resource.__init__(self) + self.subHandlers = {} + + def putSubHandler(self, prefix, handler): + self.subHandlers[prefix] = handler + + def getSubHandler(self, prefix): + return self.subHandlers.get(prefix, None) + + def getSubHandlerPrefixes(self): + return self.subHandlers.keys() + def render(self, request): request.content.seek(0, 0) args, functionPath = xmlrpclib.loads(request.content.read()) @@ -116,7 +134,7 @@ self._cbRender, request ) return server.NOT_DONE_YET - + def _cbRender(self, result, request): if isinstance(result, Handler): result = result.result @@ -136,23 +154,94 @@ return failure.value log.err(failure) return Fault(self.FAILURE, "error") - + def _getFunction(self, functionPath): """Given a string, return a function, or raise NoSuchFunction. - + This returned function will be called, and should return the result of the call, a Deferred, or a Fault instance. - + Override in subclasses if you want your own policy. The default policy is that given functionPath 'foo', return the method at self.xmlrpc_foo, i.e. getattr(self, "xmlrpc_" + functionPath). + If functionPath contains self.separator, the sub-handler for + the initial prefix is used to search for the remaining path. """ + if functionPath.find(self.separator) >= 0: + prefix, functionPath = functionPath.split(self.separator, 2) + handler = self.getSubHandler(prefix) + if handler is None: raise NoSuchFunction + return handler._getFunction(functionPath) + f = getattr(self, "xmlrpc_%s" % functionPath, None) if f and callable(f): return f else: raise NoSuchFunction + def _listFunctions(self): + """Return a list of the names of all xmlrpc methods.""" + return reflect.prefixedMethodNames(self.__class__, 'xmlrpc_') + + +class XMLRPCIntrospection(XMLRPC): + """Implement the XML-RPC Introspection API. + + By default, the methodHelp method returns the 'help' method attribute, + if it exists, otherwise the __doc__ method attribute, if it exists, + otherwise the empty string. + + To enable the methodSignature method, add a 'signature' method attribute + containing a list of lists. See methodSignature's documentation for the + format. Note the type strings should be XML-RPC types, not Python types. + """ + + def __init__(self, parent): + """Use this class to add Introspection support to an XMLRPC server. + + @param parent: the XMLRPC server to add Introspection support to. + """ + + XMLRPC.__init__(self) + self.xmlrpc_parent = parent + parent.putSubHandler('system', self) + + def xmlrpc_listMethods(self): + """Return a list of the method names implemented by this server.""" + functions = [] + todo = [(self.xmlrpc_parent, '')] + while todo: + obj, prefix = todo.pop(0) + functions.extend([ prefix + name for name in obj._listFunctions() ]) + todo.extend([ (obj.getSubHandler(name), + prefix + name + obj.separator) + for name in obj.getSubHandlerPrefixes() ]) + return functions + + xmlrpc_listMethods.signature = [['array']] + + def xmlrpc_methodHelp(self, method): + """Return a documentation string describing the use of the given method. + """ + method = self.xmlrpc_parent._getFunction(method) + return (getattr(method, 'help', None) + or getattr(method, '__doc__', None) or '') + + xmlrpc_methodHelp.signature = [['string', 'string']] + + def xmlrpc_methodSignature(self, method): + """Return a list of type signatures. + + Each type signature is a list of the form [rtype, type1, type2, ...] + where rtype is the return type and typeN is the type of the Nth + argument. If no signature information is available, the empty + string is returned. + """ + method = self.xmlrpc_parent._getFunction(method) + return getattr(method, 'signature', None) or '' + + xmlrpc_methodSignature.signature = [['array', 'string'], + ['string', 'string']] class QueryProtocol(http.HTTPClient): @@ -240,7 +329,8 @@ factory = QueryFactory(self.url, self.host, method, *args) if self.secure: from twisted.internet import ssl - reactor.connectSSL(self.host, self.port or 443, factory, ssl.ClientContextFactory()) + reactor.connectSSL(self.host, self.port or 443, + factory, ssl.ClientContextFactory()) else: reactor.connectTCP(self.host, self.port or 80, factory) return factory.deferred