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 27 Aug 2003 06:17:49 -0000 @@ -24,49 +24,45 @@ arguments determine what arguments it will accept from XML-RPC clients. The result is what will be returned to the clients.

-

Methods published via XML-RPC can return all the basic XML-RPC types, -such as strings, lists and so on (just return a regular python integer, etc). -They can also return Failure instances to indicate -an error has occurred, or Binary, -Boolean or -DateTime instances (all of these -are the same as the respective classes in xmlrpclib. In addition, XML-RPC -published methods can return Deferred -instances whose results are one of -the above. This allows you to return results that can't be calculated immediately, -such as database queries. See the Deferred documentation for more details.

- -

XMLRPC instances are Resource objects, and they can thus be published using -a Site. The following example has two methods published via XML-RPC, -add(a, b) and echo(x). You can run it directly -or with twistd -y script.py

+

Methods published via XML-RPC can return all the basic XML-RPC +types, such as strings, lists and so on (just return a regular python +integer, etc). They can also return Failure instances to indicate an +error has occurred, or Binary, Boolean or DateTime instances (all of these are the same as +the respective classes in xmlrpclib. In addition, XML-RPC published +methods can return Deferred instances whose results are one of the +above. This allows you to return results that can't be calculated +immediately, such as database queries. See the Deferred documentation for more details.

+ +

XMLRPC instances +are Resource objects, and they can thus be published using a Site. The +following example has two methods published via XML-RPC, add(a, +b) and echo(x). You can run it directly or with +twistd -y script.py

 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
 
-
-def main():
-    from twisted.internet.app import Application
-    app = Application("xmlrpc")
-    r = Example()
-    app.listenTCP(7080, server.Site(r))
-    return app
-
-application = main()
-
 if __name__ == '__main__':
-    application.run(save=0)
+    from twisted.internet import reactor
+    r = Example()
+    reactor.listenTCP(7080, server.Site(r))
+    reactor.run()
 

After we run this command, we can connect with a client and send commands @@ -86,6 +82,98 @@ 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 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()
+
+if __name__ == '__main__':
+    from twisted.internet import reactor
+    r = Example()
+    date = Date()
+    r.putSubHandler('date', date)
+    reactor.listenTCP(7080, server.Site(r))
+    reactor.run()
+
+ +

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.

+ +

Adding XML-RPC Introspection support

+ +

XML-RPC has an informal Introspection API that specifies three +methods in a system sub-handler which allow a client to query +a server about the server's API. Adding Introspection support to the +Example class is easy using the +XMLRPCIntrospection +class:

+ +
+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
+
+    xmlrpc_echo.signature = [['string', 'string'],
+                             ['int', 'int'],
+                             ['double', 'double'],
+                             ['array', 'array'],
+                             ['struct', 'struct']]
+
+    def xmlrpc_add(self, a, b):
+        """Return sum of arguments."""
+        return a + b
+
+    xmlrpc_add.signature = [['int', 'int', 'int'],
+                            ['double', 'double', 'double']]
+    xmlrpc_add.help = "Add the arguments and return the sum."
+
+if __name__ == '__main__':
+    from twisted.internet import reactor
+    r = Example()
+    xmlrpc.addIntrospection(r)
+    reactor.listenTCP(7080, server.Site(r))
+    reactor.run()
+
+ +

Note the method attributes help and +signature which are used by the Introspection +API methods system.methodHelp and +system.methodSignature respectively. If no +help attribute is specified, +the method's documentation string is used instead.

+

SOAP Support

From the point of view, of a Twisted developer, there is little difference @@ -166,4 +254,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 27 Aug 2003 06:17:52 -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, addIntrospection 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() + addIntrospection(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 27 Aug 2003 06:17:53 -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,102 @@ 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): + """Implement Introspection support for an XMLRPC server. + + @param parent: the XMLRPC server to add Introspection support to. + """ + + XMLRPC.__init__(self) + self._xmlrpc_parent = parent + + 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']] + + +def addIntrospection(xmlrpc): + """Add Introspection support to an XMLRPC server. + + @param xmlrpc: The xmlrpc server to add Introspection support to. + """ + xmlrpc.putSubHandler('system', XMLRPCIntrospection(xmlrpc)) + class QueryProtocol(http.HTTPClient): @@ -240,7 +337,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