Commit | Line | Data |
---|---|---|
86530b38 AT |
1 | """Self documenting XML-RPC Server. |
2 | ||
3 | This module can be used to create XML-RPC servers that | |
4 | serve pydoc-style documentation in response to HTTP | |
5 | GET requests. This documentation is dynamically generated | |
6 | based on the functions and methods registered with the | |
7 | server. | |
8 | ||
9 | This module is built upon the pydoc and SimpleXMLRPCServer | |
10 | modules. | |
11 | """ | |
12 | ||
13 | import pydoc | |
14 | import inspect | |
15 | import types | |
16 | import re | |
17 | import sys | |
18 | ||
19 | from SimpleXMLRPCServer import (SimpleXMLRPCServer, | |
20 | SimpleXMLRPCRequestHandler, | |
21 | CGIXMLRPCRequestHandler, | |
22 | resolve_dotted_attribute) | |
23 | ||
24 | class ServerHTMLDoc(pydoc.HTMLDoc): | |
25 | """Class used to generate pydoc HTML document for a server""" | |
26 | ||
27 | def markup(self, text, escape=None, funcs={}, classes={}, methods={}): | |
28 | """Mark up some plain text, given a context of symbols to look for. | |
29 | Each context dictionary maps object names to anchor names.""" | |
30 | escape = escape or self.escape | |
31 | results = [] | |
32 | here = 0 | |
33 | ||
34 | # XXX Note that this regular expressions does not allow for the | |
35 | # hyperlinking of arbitrary strings being used as method | |
36 | # names. Only methods with names consisting of word characters | |
37 | # and '.'s are hyperlinked. | |
38 | pattern = re.compile(r'\b((http|ftp)://\S+[\w/]|' | |
39 | r'RFC[- ]?(\d+)|' | |
40 | r'PEP[- ]?(\d+)|' | |
41 | r'(self\.)?((?:\w|\.)+))\b') | |
42 | while 1: | |
43 | match = pattern.search(text, here) | |
44 | if not match: break | |
45 | start, end = match.span() | |
46 | results.append(escape(text[here:start])) | |
47 | ||
48 | all, scheme, rfc, pep, selfdot, name = match.groups() | |
49 | if scheme: | |
50 | url = escape(all).replace('"', '"') | |
51 | results.append('<a href="%s">%s</a>' % (url, url)) | |
52 | elif rfc: | |
53 | url = 'http://www.rfc-editor.org/rfc/rfc%d.txt' % int(rfc) | |
54 | results.append('<a href="%s">%s</a>' % (url, escape(all))) | |
55 | elif pep: | |
56 | url = 'http://www.python.org/peps/pep-%04d.html' % int(pep) | |
57 | results.append('<a href="%s">%s</a>' % (url, escape(all))) | |
58 | elif text[end:end+1] == '(': | |
59 | results.append(self.namelink(name, methods, funcs, classes)) | |
60 | elif selfdot: | |
61 | results.append('self.<strong>%s</strong>' % name) | |
62 | else: | |
63 | results.append(self.namelink(name, classes)) | |
64 | here = end | |
65 | results.append(escape(text[here:])) | |
66 | return ''.join(results) | |
67 | ||
68 | def docroutine(self, object, name=None, mod=None, | |
69 | funcs={}, classes={}, methods={}, cl=None): | |
70 | """Produce HTML documentation for a function or method object.""" | |
71 | ||
72 | anchor = (cl and cl.__name__ or '') + '-' + name | |
73 | note = '' | |
74 | ||
75 | title = '<a name="%s"><strong>%s</strong></a>' % (anchor, 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, types.TupleType): | |
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 | head = '<big><big><strong>%s</strong></big></big>' % server_name | |
118 | result = self.heading(head, '#ffffff', '#7799ee') | |
119 | ||
120 | doc = self.markup(package_documentation, self.preformat, fdict) | |
121 | doc = doc and '<tt>%s</tt>' % doc | |
122 | result = result + '<p>%s</p>\n' % doc | |
123 | ||
124 | contents = [] | |
125 | method_items = methods.items() | |
126 | method_items.sort() | |
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 | ||
134 | class 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 self.funcs.has_key(method_name): | |
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 | ||
215 | class 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 | ||
232 | response = self.server.generate_html_documentation() | |
233 | self.send_response(200) | |
234 | self.send_header("Content-type", "text/html") | |
235 | self.send_header("Content-length", str(len(response))) | |
236 | self.end_headers() | |
237 | self.wfile.write(response) | |
238 | ||
239 | # shut down the connection | |
240 | self.wfile.flush() | |
241 | self.connection.shutdown(1) | |
242 | ||
243 | class 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): | |
253 | SimpleXMLRPCServer.__init__(self, addr, requestHandler, logRequests) | |
254 | XMLRPCDocGenerator.__init__(self) | |
255 | ||
256 | class DocCGIXMLRPCRequestHandler( CGIXMLRPCRequestHandler, | |
257 | XMLRPCDocGenerator): | |
258 | """Handler for XML-RPC data and documentation requests passed through | |
259 | CGI""" | |
260 | ||
261 | def handle_get(self): | |
262 | """Handles the HTTP GET request. | |
263 | ||
264 | Interpret all HTTP GET requests as requests for server | |
265 | documentation. | |
266 | """ | |
267 | ||
268 | response = self.generate_html_documentation() | |
269 | ||
270 | print 'Content-Type: text/html' | |
271 | print 'Content-Length: %d' % len(response) | |
272 | ||
273 | sys.stdout.write(response) | |
274 | ||
275 | def __init__(self): | |
276 | CGIXMLRPCRequestHandler.__init__(self) | |
277 | XMLRPCDocGenerator.__init__(self) | |
278 | ||
279 | if __name__ == '__main__': | |
280 | def deg_to_rad(deg): | |
281 | """deg_to_rad(90) => 1.5707963267948966 | |
282 | ||
283 | Converts an angle in degrees to an angle in radians""" | |
284 | import math | |
285 | return deg * math.pi / 180 | |
286 | ||
287 | server = DocXMLRPCServer(("localhost", 8000)) | |
288 | ||
289 | server.set_server_title("Math Server") | |
290 | server.set_server_name("Math XML-RPC Server") | |
291 | server.set_server_documentation("""This server supports various mathematical functions. | |
292 | ||
293 | You can use it from Python as follows: | |
294 | ||
295 | >>> from xmlrpclib import ServerProxy | |
296 | >>> s = ServerProxy("http://localhost:8000") | |
297 | >>> s.deg_to_rad(90.0) | |
298 | 1.5707963267948966""") | |
299 | ||
300 | server.register_function(deg_to_rad) | |
301 | server.register_introspection_functions() | |
302 | ||
303 | server.serve_forever() |