1"""Self documenting XML-RPC Server. 2 3This module can be used to create XML-RPC servers that 4serve pydoc-style documentation in response to HTTP 5GET requests. This documentation is dynamically generated 6based on the functions and methods registered with the 7server. 8 9This module is built upon the pydoc and SimpleXMLRPCServer 10modules. 11""" 12 13import pydoc 14import inspect 15import re 16import sys 17 18from SimpleXMLRPCServer import (SimpleXMLRPCServer, 19 SimpleXMLRPCRequestHandler, 20 CGIXMLRPCRequestHandler, 21 resolve_dotted_attribute) 22 23class ServerHTMLDoc(pydoc.HTMLDoc): 24 """Class used to generate pydoc HTML document for a server""" 25 26 def markup(self, text, escape=None, funcs={}, classes={}, methods={}): 27 """Mark up some plain text, given a context of symbols to look for. 28 Each context dictionary maps object names to anchor names.""" 29 escape = escape or self.escape 30 results = [] 31 here = 0 32 33 # XXX Note that this regular expression does not allow for the 34 # hyperlinking of arbitrary strings being used as method 35 # names. Only methods with names consisting of word characters 36 # and '.'s are hyperlinked. 37 pattern = re.compile(r'\b((http|ftp)://\S+[\w/]|' 38 r'RFC[- ]?(\d+)|' 39 r'PEP[- ]?(\d+)|' 40 r'(self\.)?((?:\w|\.)+))\b') 41 while 1: 42 match = pattern.search(text, here) 43 if not match: break 44 start, end = match.span() 45 results.append(escape(text[here:start])) 46 47 all, scheme, rfc, pep, selfdot, name = match.groups() 48 if scheme: 49 url = escape(all).replace('"', '"') 50 results.append('<a href="%s">%s</a>' % (url, url)) 51 elif rfc: 52 url = 'http://www.rfc-editor.org/rfc/rfc%d.txt' % int(rfc) 53 results.append('<a href="%s">%s</a>' % (url, escape(all))) 54 elif pep: 55 url = 'http://www.python.org/dev/peps/pep-%04d/' % int(pep) 56 results.append('<a href="%s">%s</a>' % (url, escape(all))) 57 elif text[end:end+1] == '(': 58 results.append(self.namelink(name, methods, funcs, classes)) 59 elif selfdot: 60 results.append('self.<strong>%s</strong>' % name) 61 else: 62 results.append(self.namelink(name, classes)) 63 here = end 64 results.append(escape(text[here:])) 65 return ''.join(results) 66 67 def docroutine(self, object, name, mod=None, 68 funcs={}, classes={}, methods={}, cl=None): 69 """Produce HTML documentation for a function or method object.""" 70 71 anchor = (cl and cl.__name__ or '') + '-' + name 72 note = '' 73 74 title = '<a name="%s"><strong>%s</strong></a>' % ( 75 self.escape(anchor), self.escape(name)) 76 77 if inspect.ismethod(object): 78 args, varargs, varkw, defaults = inspect.getargspec(object.im_func) 79 # exclude the argument bound to the instance, it will be 80 # confusing to the non-Python user 81 argspec = inspect.formatargspec ( 82 args[1:], 83 varargs, 84 varkw, 85 defaults, 86 formatvalue=self.formatvalue 87 ) 88 elif inspect.isfunction(object): 89 args, varargs, varkw, defaults = inspect.getargspec(object) 90 argspec = inspect.formatargspec( 91 args, varargs, varkw, defaults, formatvalue=self.formatvalue) 92 else: 93 argspec = '(...)' 94 95 if isinstance(object, tuple): 96 argspec = object[0] or argspec 97 docstring = object[1] or "" 98 else: 99 docstring = pydoc.getdoc(object) 100 101 decl = title + argspec + (note and self.grey( 102 '<font face="helvetica, arial">%s</font>' % note)) 103 104 doc = self.markup( 105 docstring, self.preformat, funcs, classes, methods) 106 doc = doc and '<dd><tt>%s</tt></dd>' % doc 107 return '<dl><dt>%s</dt>%s</dl>\n' % (decl, doc) 108 109 def docserver(self, server_name, package_documentation, methods): 110 """Produce HTML documentation for an XML-RPC server.""" 111 112 fdict = {} 113 for key, value in methods.items(): 114 fdict[key] = '#-' + key 115 fdict[value] = fdict[key] 116 117 server_name = self.escape(server_name) 118 head = '<big><big><strong>%s</strong></big></big>' % server_name 119 result = self.heading(head, '#ffffff', '#7799ee') 120 121 doc = self.markup(package_documentation, self.preformat, fdict) 122 doc = doc and '<tt>%s</tt>' % doc 123 result = result + '<p>%s</p>\n' % doc 124 125 contents = [] 126 method_items = sorted(methods.items()) 127 for key, value in method_items: 128 contents.append(self.docroutine(value, key, funcs=fdict)) 129 result = result + self.bigsection( 130 'Methods', '#ffffff', '#eeaa77', pydoc.join(contents)) 131 132 return result 133 134class XMLRPCDocGenerator: 135 """Generates documentation for an XML-RPC server. 136 137 This class is designed as mix-in and should not 138 be constructed directly. 139 """ 140 141 def __init__(self): 142 # setup variables used for HTML documentation 143 self.server_name = 'XML-RPC Server Documentation' 144 self.server_documentation = \ 145 "This server exports the following methods through the XML-RPC "\ 146 "protocol." 147 self.server_title = 'XML-RPC Server Documentation' 148 149 def set_server_title(self, server_title): 150 """Set the HTML title of the generated server documentation""" 151 152 self.server_title = server_title 153 154 def set_server_name(self, server_name): 155 """Set the name of the generated HTML server documentation""" 156 157 self.server_name = server_name 158 159 def set_server_documentation(self, server_documentation): 160 """Set the documentation string for the entire server.""" 161 162 self.server_documentation = server_documentation 163 164 def generate_html_documentation(self): 165 """generate_html_documentation() => html documentation for the server 166 167 Generates HTML documentation for the server using introspection for 168 installed functions and instances that do not implement the 169 _dispatch method. Alternatively, instances can choose to implement 170 the _get_method_argstring(method_name) method to provide the 171 argument string used in the documentation and the 172 _methodHelp(method_name) method to provide the help text used 173 in the documentation.""" 174 175 methods = {} 176 177 for method_name in self.system_listMethods(): 178 if method_name in self.funcs: 179 method = self.funcs[method_name] 180 elif self.instance is not None: 181 method_info = [None, None] # argspec, documentation 182 if hasattr(self.instance, '_get_method_argstring'): 183 method_info[0] = self.instance._get_method_argstring(method_name) 184 if hasattr(self.instance, '_methodHelp'): 185 method_info[1] = self.instance._methodHelp(method_name) 186 187 method_info = tuple(method_info) 188 if method_info != (None, None): 189 method = method_info 190 elif not hasattr(self.instance, '_dispatch'): 191 try: 192 method = resolve_dotted_attribute( 193 self.instance, 194 method_name 195 ) 196 except AttributeError: 197 method = method_info 198 else: 199 method = method_info 200 else: 201 assert 0, "Could not find method in self.functions and no "\ 202 "instance installed" 203 204 methods[method_name] = method 205 206 documenter = ServerHTMLDoc() 207 documentation = documenter.docserver( 208 self.server_name, 209 self.server_documentation, 210 methods 211 ) 212 213 return documenter.page(self.server_title, documentation) 214 215class DocXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): 216 """XML-RPC and documentation request handler class. 217 218 Handles all HTTP POST requests and attempts to decode them as 219 XML-RPC requests. 220 221 Handles all HTTP GET requests and interprets them as requests 222 for documentation. 223 """ 224 225 def do_GET(self): 226 """Handles the HTTP GET request. 227 228 Interpret all HTTP GET requests as requests for server 229 documentation. 230 """ 231 # Check that the path is legal 232 if not self.is_rpc_path_valid(): 233 self.report_404() 234 return 235 236 response = self.server.generate_html_documentation() 237 self.send_response(200) 238 self.send_header("Content-type", "text/html") 239 self.send_header("Content-length", str(len(response))) 240 self.end_headers() 241 self.wfile.write(response) 242 243class DocXMLRPCServer( SimpleXMLRPCServer, 244 XMLRPCDocGenerator): 245 """XML-RPC and HTML documentation server. 246 247 Adds the ability to serve server documentation to the capabilities 248 of SimpleXMLRPCServer. 249 """ 250 251 def __init__(self, addr, requestHandler=DocXMLRPCRequestHandler, 252 logRequests=1, allow_none=False, encoding=None, 253 bind_and_activate=True): 254 SimpleXMLRPCServer.__init__(self, addr, requestHandler, logRequests, 255 allow_none, encoding, bind_and_activate) 256 XMLRPCDocGenerator.__init__(self) 257 258class DocCGIXMLRPCRequestHandler( CGIXMLRPCRequestHandler, 259 XMLRPCDocGenerator): 260 """Handler for XML-RPC data and documentation requests passed through 261 CGI""" 262 263 def handle_get(self): 264 """Handles the HTTP GET request. 265 266 Interpret all HTTP GET requests as requests for server 267 documentation. 268 """ 269 270 response = self.generate_html_documentation() 271 272 print 'Content-Type: text/html' 273 print 'Content-Length: %d' % len(response) 274 print 275 sys.stdout.write(response) 276 277 def __init__(self): 278 CGIXMLRPCRequestHandler.__init__(self) 279 XMLRPCDocGenerator.__init__(self) 280