blob: adb8a93349a616b2a92cbbc9f3713948032537e5 [file] [log] [blame]
Martin v. Löwis281b2c62003-04-18 21:04:39 +00001"""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 types
16import re
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, types.TupleType):
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
231 response = self.server.generate_html_documentation()
232 self.send_response(200)
233 self.send_header("Content-type", "text/html")
234 self.send_header("Content-length", str(len(response)))
235 self.end_headers()
236 self.wfile.write(response)
237
238 # shut down the connection
239 self.wfile.flush()
240 self.connection.shutdown(1)
241
242class DocXMLRPCServer( SimpleXMLRPCServer,
243 XMLRPCDocGenerator):
244 """XML-RPC and HTML documentation server.
245
246 Adds the ability to serve server documentation to the capabilities
247 of SimpleXMLRPCServer.
248 """
249
250 def __init__(self, addr, requestHandler=DocXMLRPCRequestHandler,
251 logRequests=1):
252 SimpleXMLRPCServer.__init__(self, addr, requestHandler, logRequests)
253 XMLRPCDocGenerator.__init__(self)
254
255class DocCGIXMLRPCRequestHandler( CGIXMLRPCRequestHandler,
256 XMLRPCDocGenerator):
257 """Handler for XML-RPC data and documentation requests passed through
258 CGI"""
259
260 def handle_get(self):
261 """Handles the HTTP GET request.
262
263 Interpret all HTTP GET requests as requests for server
264 documentation.
265 """
266
267 response = self.generate_html_documentation()
268
269 print 'Content-Type: text/html'
270 print 'Content-Length: %d' % len(response)
271 print
272 print response
273
274 def __init__(self):
275 CGIXMLRPCRequestHandler.__init__(self)
276 XMLRPCDocGenerator.__init__(self)
277
278if __name__ == '__main__':
279 def deg_to_rad(deg):
280 """deg_to_rad(90) => 1.5707963267948966
281
282 Converts an angle in degrees to an angle in radians"""
283 import math
284 return deg * math.pi / 180
285
286 server = DocXMLRPCServer(("localhost", 8000))
287
288 server.set_server_title("Math Server")
289 server.set_server_name("Math XML-RPC Server")
290 server.set_server_documentation("""This server supports various mathematical functions.
291
292You can use it from Python as follows:
293
294>>> from xmlrpclib import ServerProxy
295>>> s = ServerProxy("http://localhost:8000")
296>>> s.deg_to_rad(90.0)
2971.5707963267948966""")
298
299 server.register_function(deg_to_rad)
300 server.register_introspection_functions()
301
302 server.serve_forever()