blob: 86ed32b6fc193248bb791c68e01ac95fd0e745bc [file] [log] [blame]
Tor Norbye3a2425a2013-11-04 10:16:08 -08001"""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 expressions 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/peps/pep-%04d.html' % 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=None, 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>' % (anchor, name)
75
76 if inspect.ismethod(object):
77 args, varargs, varkw, defaults = inspect.getargspec(object.im_func)
78 # exclude the argument bound to the instance, it will be
79 # confusing to the non-Python user
80 argspec = inspect.formatargspec (
81 args[1:],
82 varargs,
83 varkw,
84 defaults,
85 formatvalue=self.formatvalue
86 )
87 elif inspect.isfunction(object):
88 args, varargs, varkw, defaults = inspect.getargspec(object)
89 argspec = inspect.formatargspec(
90 args, varargs, varkw, defaults, formatvalue=self.formatvalue)
91 else:
92 argspec = '(...)'
93
94 if isinstance(object, tuple):
95 argspec = object[0] or argspec
96 docstring = object[1] or ""
97 else:
98 docstring = pydoc.getdoc(object)
99
100 decl = title + argspec + (note and self.grey(
101 '<font face="helvetica, arial">%s</font>' % note))
102
103 doc = self.markup(
104 docstring, self.preformat, funcs, classes, methods)
105 doc = doc and '<dd><tt>%s</tt></dd>' % doc
106 return '<dl><dt>%s</dt>%s</dl>\n' % (decl, doc)
107
108 def docserver(self, server_name, package_documentation, methods):
109 """Produce HTML documentation for an XML-RPC server."""
110
111 fdict = {}
112 for key, value in methods.items():
113 fdict[key] = '#-' + key
114 fdict[value] = fdict[key]
115
116 head = '<big><big><strong>%s</strong></big></big>' % server_name
117 result = self.heading(head, '#ffffff', '#7799ee')
118
119 doc = self.markup(package_documentation, self.preformat, fdict)
120 doc = doc and '<tt>%s</tt>' % doc
121 result = result + '<p>%s</p>\n' % doc
122
123 contents = []
124 method_items = methods.items()
125 method_items.sort()
126 for key, value in method_items:
127 contents.append(self.docroutine(value, key, funcs=fdict))
128 result = result + self.bigsection(
129 'Methods', '#ffffff', '#eeaa77', pydoc.join(contents))
130
131 return result
132
133class XMLRPCDocGenerator:
134 """Generates documentation for an XML-RPC server.
135
136 This class is designed as mix-in and should not
137 be constructed directly.
138 """
139
140 def __init__(self):
141 # setup variables used for HTML documentation
142 self.server_name = 'XML-RPC Server Documentation'
143 self.server_documentation = \
144 "This server exports the following methods through the XML-RPC "\
145 "protocol."
146 self.server_title = 'XML-RPC Server Documentation'
147
148 def set_server_title(self, server_title):
149 """Set the HTML title of the generated server documentation"""
150
151 self.server_title = server_title
152
153 def set_server_name(self, server_name):
154 """Set the name of the generated HTML server documentation"""
155
156 self.server_name = server_name
157
158 def set_server_documentation(self, server_documentation):
159 """Set the documentation string for the entire server."""
160
161 self.server_documentation = server_documentation
162
163 def generate_html_documentation(self):
164 """generate_html_documentation() => html documentation for the server
165
166 Generates HTML documentation for the server using introspection for
167 installed functions and instances that do not implement the
168 _dispatch method. Alternatively, instances can choose to implement
169 the _get_method_argstring(method_name) method to provide the
170 argument string used in the documentation and the
171 _methodHelp(method_name) method to provide the help text used
172 in the documentation."""
173
174 methods = {}
175
176 for method_name in self.system_listMethods():
177 if self.funcs.has_key(method_name):
178 method = self.funcs[method_name]
179 elif self.instance is not None:
180 method_info = [None, None] # argspec, documentation
181 if hasattr(self.instance, '_get_method_argstring'):
182 method_info[0] = self.instance._get_method_argstring(method_name)
183 if hasattr(self.instance, '_methodHelp'):
184 method_info[1] = self.instance._methodHelp(method_name)
185
186 method_info = tuple(method_info)
187 if method_info != (None, None):
188 method = method_info
189 elif not hasattr(self.instance, '_dispatch'):
190 try:
191 method = resolve_dotted_attribute(
192 self.instance,
193 method_name
194 )
195 except AttributeError:
196 method = method_info
197 else:
198 method = method_info
199 else:
200 assert 0, "Could not find method in self.functions and no "\
201 "instance installed"
202
203 methods[method_name] = method
204
205 documenter = ServerHTMLDoc()
206 documentation = documenter.docserver(
207 self.server_name,
208 self.server_documentation,
209 methods
210 )
211
212 return documenter.page(self.server_title, documentation)
213
214class DocXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
215 """XML-RPC and documentation request handler class.
216
217 Handles all HTTP POST requests and attempts to decode them as
218 XML-RPC requests.
219
220 Handles all HTTP GET requests and interprets them as requests
221 for documentation.
222 """
223
224 def do_GET(self):
225 """Handles the HTTP GET request.
226
227 Interpret all HTTP GET requests as requests for server
228 documentation.
229 """
230 # Check that the path is legal
231 if not self.is_rpc_path_valid():
232 self.report_404()
233 return
234
235 response = self.server.generate_html_documentation()
236 self.send_response(200)
237 self.send_header("Content-type", "text/html")
238 self.send_header("Content-length", str(len(response)))
239 self.end_headers()
240 self.wfile.write(response)
241
242 # shut down the connection
243 self.wfile.flush()
244 self.connection.shutdown(1)
245
246class DocXMLRPCServer( SimpleXMLRPCServer,
247 XMLRPCDocGenerator):
248 """XML-RPC and HTML documentation server.
249
250 Adds the ability to serve server documentation to the capabilities
251 of SimpleXMLRPCServer.
252 """
253
254 def __init__(self, addr, requestHandler=DocXMLRPCRequestHandler,
255 logRequests=1):
256 SimpleXMLRPCServer.__init__(self, addr, requestHandler, logRequests)
257 XMLRPCDocGenerator.__init__(self)
258
259class DocCGIXMLRPCRequestHandler( CGIXMLRPCRequestHandler,
260 XMLRPCDocGenerator):
261 """Handler for XML-RPC data and documentation requests passed through
262 CGI"""
263
264 def handle_get(self):
265 """Handles the HTTP GET request.
266
267 Interpret all HTTP GET requests as requests for server
268 documentation.
269 """
270
271 response = self.generate_html_documentation()
272
273 print 'Content-Type: text/html'
274 print 'Content-Length: %d' % len(response)
275 print
276 sys.stdout.write(response)
277
278 def __init__(self):
279 CGIXMLRPCRequestHandler.__init__(self)
280 XMLRPCDocGenerator.__init__(self)
281
282if __name__ == '__main__':
283 def deg_to_rad(deg):
284 """deg_to_rad(90) => 1.5707963267948966
285
286 Converts an angle in degrees to an angle in radians"""
287 import math
288 return deg * math.pi / 180
289
290 server = DocXMLRPCServer(("localhost", 8000))
291
292 server.set_server_title("Math Server")
293 server.set_server_name("Math XML-RPC Server")
294 server.set_server_documentation("""This server supports various mathematical functions.
295
296You can use it from Python as follows:
297
298>>> from xmlrpclib import ServerProxy
299>>> s = ServerProxy("http://localhost:8000")
300>>> s.deg_to_rad(90.0)
3011.5707963267948966""")
302
303 server.register_function(deg_to_rad)
304 server.register_introspection_functions()
305
306 server.serve_forever()