• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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