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 25 Aug 2003 07:23:47 -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,36 @@ FAILURE = 666 NOT_FOUND = 23 - + + # the doc string is part of the test def xmlrpc_add(self, a, b): + """This function add two integers. + + @type a: int + @type b: int + @rtype: int + """ return a + b + # the doc string is part of the test + def xmlrpc_pair(self, string, num): + """This function puts the two arguments in an array. + + @type string: string + @type num: int + @rtype: array + """ + return [string, num] + + # 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 @@ -72,16 +93,18 @@ 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("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 +120,46 @@ 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', '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, '') + + 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']) + + 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 25 Aug 2003 07:24:02 -0000 @@ -27,6 +27,9 @@ """ from __future__ import nested_scopes +import inspect +import re + # System Imports import xmlrpclib import urlparse @@ -34,7 +37,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 +92,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 putSubHander. 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 +104,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 +137,7 @@ self._cbRender, request ) return server.NOT_DONE_YET - + def _cbRender(self, result, request): if isinstance(result, Handler): result = result.result @@ -136,23 +157,100 @@ 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. + + To enable the methodSignature method, add @type and @rtype information + to all xmlrpc-served methods. + """ + + 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 + + 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, '__doc__', None) or '' + + def xmlrpc_methodSignature(self, method): + """Return a list of type signatures. + + Each type signature is an array 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. + + @type method: string + @rtype: array + """ + method = self.xmlrpc_parent._getFunction(method) + doc = getattr(method, '__doc__', None) or '' + if not doc: return '' + + signature = [] + match = re.search(r'@rtype\s*:\s*(\w+)', doc) + if not match: return '' + signature.append(match.group(1)) + + args, varargs, varkw, _ = inspect.getargspec(method) + for arg in args[1:]: + pat = r'@type\s*%s\s*:\s*(\w+)' % arg + match = re.search(pat, doc) + if not match: return '' + signature.append(match.group(1)) + + return signature + class QueryProtocol(http.HTTPClient): @@ -240,7 +338,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