blob: 111e5f602ad7e17fb07a285b0bdd7449dab25a02 [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
Martin v. Löwis281b2c62003-04-18 21:04:39 +000015import re
Martin v. Löwis9c5ea502003-05-01 05:05:09 +000016import sys
Martin v. Löwis281b2c62003-04-18 21:04:39 +000017
Andrew M. Kuchling33ad28b2004-08-31 11:38:12 +000018from SimpleXMLRPCServer import (SimpleXMLRPCServer,
19 SimpleXMLRPCRequestHandler,
20 CGIXMLRPCRequestHandler,
21 resolve_dotted_attribute)
Martin v. Löwis281b2c62003-04-18 21:04:39 +000022
23class ServerHTMLDoc(pydoc.HTMLDoc):
24 """Class used to generate pydoc HTML document for a server"""
Tim Peters0eadaac2003-04-24 16:02:54 +000025
Martin v. Löwis281b2c62003-04-18 21:04:39 +000026 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)
Tim Peters0eadaac2003-04-24 16:02:54 +000066
Martin v. Löwis281b2c62003-04-18 21:04:39 +000067 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)
Tim Peters0eadaac2003-04-24 16:02:54 +000075
Martin v. Löwis281b2c62003-04-18 21:04:39 +000076 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
Raymond Hettingerf7153662005-02-07 14:16:21 +000094 if isinstance(object, tuple):
Martin v. Löwis281b2c62003-04-18 21:04:39 +000095 argspec = object[0] or argspec
96 docstring = object[1] or ""
97 else:
98 docstring = pydoc.getdoc(object)
Tim Peters0eadaac2003-04-24 16:02:54 +000099
Martin v. Löwis281b2c62003-04-18 21:04:39 +0000100 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]
Tim Peters0eadaac2003-04-24 16:02:54 +0000115
116 head = '<big><big><strong>%s</strong></big></big>' % server_name
Martin v. Löwis281b2c62003-04-18 21:04:39 +0000117 result = self.heading(head, '#ffffff', '#7799ee')
Tim Peters0eadaac2003-04-24 16:02:54 +0000118
Martin v. Löwis281b2c62003-04-18 21:04:39 +0000119 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 """
Tim Peters0eadaac2003-04-24 16:02:54 +0000139
Martin v. Löwis281b2c62003-04-18 21:04:39 +0000140 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."""
Tim Peters0eadaac2003-04-24 16:02:54 +0000173
Martin v. Löwis281b2c62003-04-18 21:04:39 +0000174 methods = {}
175
176 for method_name in self.system_listMethods():
Guido van Rossume2b70bc2006-08-18 22:13:04 +0000177 if method_name in self.funcs:
Martin v. Löwis281b2c62003-04-18 21:04:39 +0000178 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 )
Tim Peters0eadaac2003-04-24 16:02:54 +0000211
Martin v. Löwis281b2c62003-04-18 21:04:39 +0000212 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 """
Thomas Wouters4d70c3d2006-06-08 14:42:34 +0000230 # Check that the path is legal
231 if not self.is_rpc_path_valid():
232 self.report_404()
233 return
Tim Peters0eadaac2003-04-24 16:02:54 +0000234
Martin v. Löwis281b2c62003-04-18 21:04:39 +0000235 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,
Guido van Rossumd8faa362007-04-27 19:54:29 +0000255 logRequests=1, allow_none=False, encoding=None,
256 bind_and_activate=True):
257 SimpleXMLRPCServer.__init__(self, addr, requestHandler, logRequests,
258 allow_none, encoding, bind_and_activate)
Martin v. Löwis281b2c62003-04-18 21:04:39 +0000259 XMLRPCDocGenerator.__init__(self)
Tim Peters0eadaac2003-04-24 16:02:54 +0000260
Martin v. Löwis281b2c62003-04-18 21:04:39 +0000261class DocCGIXMLRPCRequestHandler( CGIXMLRPCRequestHandler,
262 XMLRPCDocGenerator):
263 """Handler for XML-RPC data and documentation requests passed through
264 CGI"""
265
266 def handle_get(self):
267 """Handles the HTTP GET request.
268
269 Interpret all HTTP GET requests as requests for server
270 documentation.
271 """
272
273 response = self.generate_html_documentation()
274
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000275 print('Content-Type: text/html')
276 print('Content-Length: %d' % len(response))
277 print()
Martin v. Löwis9c5ea502003-05-01 05:05:09 +0000278 sys.stdout.write(response)
Martin v. Löwis281b2c62003-04-18 21:04:39 +0000279
280 def __init__(self):
281 CGIXMLRPCRequestHandler.__init__(self)
282 XMLRPCDocGenerator.__init__(self)
283
284if __name__ == '__main__':
285 def deg_to_rad(deg):
286 """deg_to_rad(90) => 1.5707963267948966
287
288 Converts an angle in degrees to an angle in radians"""
289 import math
Tim Peters0eadaac2003-04-24 16:02:54 +0000290 return deg * math.pi / 180
291
Martin v. Löwis281b2c62003-04-18 21:04:39 +0000292 server = DocXMLRPCServer(("localhost", 8000))
293
294 server.set_server_title("Math Server")
295 server.set_server_name("Math XML-RPC Server")
296 server.set_server_documentation("""This server supports various mathematical functions.
297
298You can use it from Python as follows:
299
300>>> from xmlrpclib import ServerProxy
301>>> s = ServerProxy("http://localhost:8000")
302>>> s.deg_to_rad(90.0)
3031.5707963267948966""")
304
305 server.register_function(deg_to_rad)
306 server.register_introspection_functions()
307
Tim Peters0eadaac2003-04-24 16:02:54 +0000308 server.serve_forever()