comparison planemo/lib/python3.7/site-packages/future/backports/xmlrpc/server.py @ 0:d30785e31577 draft

"planemo upload commit 6eee67778febed82ddd413c3ca40b3183a3898f1"
author guerler
date Fri, 31 Jul 2020 00:18:57 -0400
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:d30785e31577
1 r"""
2 Ported using Python-Future from the Python 3.3 standard library.
3
4 XML-RPC Servers.
5
6 This module can be used to create simple XML-RPC servers
7 by creating a server and either installing functions, a
8 class instance, or by extending the SimpleXMLRPCServer
9 class.
10
11 It can also be used to handle XML-RPC requests in a CGI
12 environment using CGIXMLRPCRequestHandler.
13
14 The Doc* classes can be used to create XML-RPC servers that
15 serve pydoc-style documentation in response to HTTP
16 GET requests. This documentation is dynamically generated
17 based on the functions and methods registered with the
18 server.
19
20 A list of possible usage patterns follows:
21
22 1. Install functions:
23
24 server = SimpleXMLRPCServer(("localhost", 8000))
25 server.register_function(pow)
26 server.register_function(lambda x,y: x+y, 'add')
27 server.serve_forever()
28
29 2. Install an instance:
30
31 class MyFuncs:
32 def __init__(self):
33 # make all of the sys functions available through sys.func_name
34 import sys
35 self.sys = sys
36 def _listMethods(self):
37 # implement this method so that system.listMethods
38 # knows to advertise the sys methods
39 return list_public_methods(self) + \
40 ['sys.' + method for method in list_public_methods(self.sys)]
41 def pow(self, x, y): return pow(x, y)
42 def add(self, x, y) : return x + y
43
44 server = SimpleXMLRPCServer(("localhost", 8000))
45 server.register_introspection_functions()
46 server.register_instance(MyFuncs())
47 server.serve_forever()
48
49 3. Install an instance with custom dispatch method:
50
51 class Math:
52 def _listMethods(self):
53 # this method must be present for system.listMethods
54 # to work
55 return ['add', 'pow']
56 def _methodHelp(self, method):
57 # this method must be present for system.methodHelp
58 # to work
59 if method == 'add':
60 return "add(2,3) => 5"
61 elif method == 'pow':
62 return "pow(x, y[, z]) => number"
63 else:
64 # By convention, return empty
65 # string if no help is available
66 return ""
67 def _dispatch(self, method, params):
68 if method == 'pow':
69 return pow(*params)
70 elif method == 'add':
71 return params[0] + params[1]
72 else:
73 raise ValueError('bad method')
74
75 server = SimpleXMLRPCServer(("localhost", 8000))
76 server.register_introspection_functions()
77 server.register_instance(Math())
78 server.serve_forever()
79
80 4. Subclass SimpleXMLRPCServer:
81
82 class MathServer(SimpleXMLRPCServer):
83 def _dispatch(self, method, params):
84 try:
85 # We are forcing the 'export_' prefix on methods that are
86 # callable through XML-RPC to prevent potential security
87 # problems
88 func = getattr(self, 'export_' + method)
89 except AttributeError:
90 raise Exception('method "%s" is not supported' % method)
91 else:
92 return func(*params)
93
94 def export_add(self, x, y):
95 return x + y
96
97 server = MathServer(("localhost", 8000))
98 server.serve_forever()
99
100 5. CGI script:
101
102 server = CGIXMLRPCRequestHandler()
103 server.register_function(pow)
104 server.handle_request()
105 """
106
107 from __future__ import absolute_import, division, print_function, unicode_literals
108 from future.builtins import int, str
109
110 # Written by Brian Quinlan (brian@sweetapp.com).
111 # Based on code written by Fredrik Lundh.
112
113 from future.backports.xmlrpc.client import Fault, dumps, loads, gzip_encode, gzip_decode
114 from future.backports.http.server import BaseHTTPRequestHandler
115 import future.backports.http.server as http_server
116 from future.backports import socketserver
117 import sys
118 import os
119 import re
120 import pydoc
121 import inspect
122 import traceback
123 try:
124 import fcntl
125 except ImportError:
126 fcntl = None
127
128 def resolve_dotted_attribute(obj, attr, allow_dotted_names=True):
129 """resolve_dotted_attribute(a, 'b.c.d') => a.b.c.d
130
131 Resolves a dotted attribute name to an object. Raises
132 an AttributeError if any attribute in the chain starts with a '_'.
133
134 If the optional allow_dotted_names argument is false, dots are not
135 supported and this function operates similar to getattr(obj, attr).
136 """
137
138 if allow_dotted_names:
139 attrs = attr.split('.')
140 else:
141 attrs = [attr]
142
143 for i in attrs:
144 if i.startswith('_'):
145 raise AttributeError(
146 'attempt to access private attribute "%s"' % i
147 )
148 else:
149 obj = getattr(obj,i)
150 return obj
151
152 def list_public_methods(obj):
153 """Returns a list of attribute strings, found in the specified
154 object, which represent callable attributes"""
155
156 return [member for member in dir(obj)
157 if not member.startswith('_') and
158 callable(getattr(obj, member))]
159
160 class SimpleXMLRPCDispatcher(object):
161 """Mix-in class that dispatches XML-RPC requests.
162
163 This class is used to register XML-RPC method handlers
164 and then to dispatch them. This class doesn't need to be
165 instanced directly when used by SimpleXMLRPCServer but it
166 can be instanced when used by the MultiPathXMLRPCServer
167 """
168
169 def __init__(self, allow_none=False, encoding=None,
170 use_builtin_types=False):
171 self.funcs = {}
172 self.instance = None
173 self.allow_none = allow_none
174 self.encoding = encoding or 'utf-8'
175 self.use_builtin_types = use_builtin_types
176
177 def register_instance(self, instance, allow_dotted_names=False):
178 """Registers an instance to respond to XML-RPC requests.
179
180 Only one instance can be installed at a time.
181
182 If the registered instance has a _dispatch method then that
183 method will be called with the name of the XML-RPC method and
184 its parameters as a tuple
185 e.g. instance._dispatch('add',(2,3))
186
187 If the registered instance does not have a _dispatch method
188 then the instance will be searched to find a matching method
189 and, if found, will be called. Methods beginning with an '_'
190 are considered private and will not be called by
191 SimpleXMLRPCServer.
192
193 If a registered function matches a XML-RPC request, then it
194 will be called instead of the registered instance.
195
196 If the optional allow_dotted_names argument is true and the
197 instance does not have a _dispatch method, method names
198 containing dots are supported and resolved, as long as none of
199 the name segments start with an '_'.
200
201 *** SECURITY WARNING: ***
202
203 Enabling the allow_dotted_names options allows intruders
204 to access your module's global variables and may allow
205 intruders to execute arbitrary code on your machine. Only
206 use this option on a secure, closed network.
207
208 """
209
210 self.instance = instance
211 self.allow_dotted_names = allow_dotted_names
212
213 def register_function(self, function, name=None):
214 """Registers a function to respond to XML-RPC requests.
215
216 The optional name argument can be used to set a Unicode name
217 for the function.
218 """
219
220 if name is None:
221 name = function.__name__
222 self.funcs[name] = function
223
224 def register_introspection_functions(self):
225 """Registers the XML-RPC introspection methods in the system
226 namespace.
227
228 see http://xmlrpc.usefulinc.com/doc/reserved.html
229 """
230
231 self.funcs.update({'system.listMethods' : self.system_listMethods,
232 'system.methodSignature' : self.system_methodSignature,
233 'system.methodHelp' : self.system_methodHelp})
234
235 def register_multicall_functions(self):
236 """Registers the XML-RPC multicall method in the system
237 namespace.
238
239 see http://www.xmlrpc.com/discuss/msgReader$1208"""
240
241 self.funcs.update({'system.multicall' : self.system_multicall})
242
243 def _marshaled_dispatch(self, data, dispatch_method = None, path = None):
244 """Dispatches an XML-RPC method from marshalled (XML) data.
245
246 XML-RPC methods are dispatched from the marshalled (XML) data
247 using the _dispatch method and the result is returned as
248 marshalled data. For backwards compatibility, a dispatch
249 function can be provided as an argument (see comment in
250 SimpleXMLRPCRequestHandler.do_POST) but overriding the
251 existing method through subclassing is the preferred means
252 of changing method dispatch behavior.
253 """
254
255 try:
256 params, method = loads(data, use_builtin_types=self.use_builtin_types)
257
258 # generate response
259 if dispatch_method is not None:
260 response = dispatch_method(method, params)
261 else:
262 response = self._dispatch(method, params)
263 # wrap response in a singleton tuple
264 response = (response,)
265 response = dumps(response, methodresponse=1,
266 allow_none=self.allow_none, encoding=self.encoding)
267 except Fault as fault:
268 response = dumps(fault, allow_none=self.allow_none,
269 encoding=self.encoding)
270 except:
271 # report exception back to server
272 exc_type, exc_value, exc_tb = sys.exc_info()
273 response = dumps(
274 Fault(1, "%s:%s" % (exc_type, exc_value)),
275 encoding=self.encoding, allow_none=self.allow_none,
276 )
277
278 return response.encode(self.encoding)
279
280 def system_listMethods(self):
281 """system.listMethods() => ['add', 'subtract', 'multiple']
282
283 Returns a list of the methods supported by the server."""
284
285 methods = set(self.funcs.keys())
286 if self.instance is not None:
287 # Instance can implement _listMethod to return a list of
288 # methods
289 if hasattr(self.instance, '_listMethods'):
290 methods |= set(self.instance._listMethods())
291 # if the instance has a _dispatch method then we
292 # don't have enough information to provide a list
293 # of methods
294 elif not hasattr(self.instance, '_dispatch'):
295 methods |= set(list_public_methods(self.instance))
296 return sorted(methods)
297
298 def system_methodSignature(self, method_name):
299 """system.methodSignature('add') => [double, int, int]
300
301 Returns a list describing the signature of the method. In the
302 above example, the add method takes two integers as arguments
303 and returns a double result.
304
305 This server does NOT support system.methodSignature."""
306
307 # See http://xmlrpc.usefulinc.com/doc/sysmethodsig.html
308
309 return 'signatures not supported'
310
311 def system_methodHelp(self, method_name):
312 """system.methodHelp('add') => "Adds two integers together"
313
314 Returns a string containing documentation for the specified method."""
315
316 method = None
317 if method_name in self.funcs:
318 method = self.funcs[method_name]
319 elif self.instance is not None:
320 # Instance can implement _methodHelp to return help for a method
321 if hasattr(self.instance, '_methodHelp'):
322 return self.instance._methodHelp(method_name)
323 # if the instance has a _dispatch method then we
324 # don't have enough information to provide help
325 elif not hasattr(self.instance, '_dispatch'):
326 try:
327 method = resolve_dotted_attribute(
328 self.instance,
329 method_name,
330 self.allow_dotted_names
331 )
332 except AttributeError:
333 pass
334
335 # Note that we aren't checking that the method actually
336 # be a callable object of some kind
337 if method is None:
338 return ""
339 else:
340 return pydoc.getdoc(method)
341
342 def system_multicall(self, call_list):
343 """system.multicall([{'methodName': 'add', 'params': [2, 2]}, ...]) => \
344 [[4], ...]
345
346 Allows the caller to package multiple XML-RPC calls into a single
347 request.
348
349 See http://www.xmlrpc.com/discuss/msgReader$1208
350 """
351
352 results = []
353 for call in call_list:
354 method_name = call['methodName']
355 params = call['params']
356
357 try:
358 # XXX A marshalling error in any response will fail the entire
359 # multicall. If someone cares they should fix this.
360 results.append([self._dispatch(method_name, params)])
361 except Fault as fault:
362 results.append(
363 {'faultCode' : fault.faultCode,
364 'faultString' : fault.faultString}
365 )
366 except:
367 exc_type, exc_value, exc_tb = sys.exc_info()
368 results.append(
369 {'faultCode' : 1,
370 'faultString' : "%s:%s" % (exc_type, exc_value)}
371 )
372 return results
373
374 def _dispatch(self, method, params):
375 """Dispatches the XML-RPC method.
376
377 XML-RPC calls are forwarded to a registered function that
378 matches the called XML-RPC method name. If no such function
379 exists then the call is forwarded to the registered instance,
380 if available.
381
382 If the registered instance has a _dispatch method then that
383 method will be called with the name of the XML-RPC method and
384 its parameters as a tuple
385 e.g. instance._dispatch('add',(2,3))
386
387 If the registered instance does not have a _dispatch method
388 then the instance will be searched to find a matching method
389 and, if found, will be called.
390
391 Methods beginning with an '_' are considered private and will
392 not be called.
393 """
394
395 func = None
396 try:
397 # check to see if a matching function has been registered
398 func = self.funcs[method]
399 except KeyError:
400 if self.instance is not None:
401 # check for a _dispatch method
402 if hasattr(self.instance, '_dispatch'):
403 return self.instance._dispatch(method, params)
404 else:
405 # call instance method directly
406 try:
407 func = resolve_dotted_attribute(
408 self.instance,
409 method,
410 self.allow_dotted_names
411 )
412 except AttributeError:
413 pass
414
415 if func is not None:
416 return func(*params)
417 else:
418 raise Exception('method "%s" is not supported' % method)
419
420 class SimpleXMLRPCRequestHandler(BaseHTTPRequestHandler):
421 """Simple XML-RPC request handler class.
422
423 Handles all HTTP POST requests and attempts to decode them as
424 XML-RPC requests.
425 """
426
427 # Class attribute listing the accessible path components;
428 # paths not on this list will result in a 404 error.
429 rpc_paths = ('/', '/RPC2')
430
431 #if not None, encode responses larger than this, if possible
432 encode_threshold = 1400 #a common MTU
433
434 #Override form StreamRequestHandler: full buffering of output
435 #and no Nagle.
436 wbufsize = -1
437 disable_nagle_algorithm = True
438
439 # a re to match a gzip Accept-Encoding
440 aepattern = re.compile(r"""
441 \s* ([^\s;]+) \s* #content-coding
442 (;\s* q \s*=\s* ([0-9\.]+))? #q
443 """, re.VERBOSE | re.IGNORECASE)
444
445 def accept_encodings(self):
446 r = {}
447 ae = self.headers.get("Accept-Encoding", "")
448 for e in ae.split(","):
449 match = self.aepattern.match(e)
450 if match:
451 v = match.group(3)
452 v = float(v) if v else 1.0
453 r[match.group(1)] = v
454 return r
455
456 def is_rpc_path_valid(self):
457 if self.rpc_paths:
458 return self.path in self.rpc_paths
459 else:
460 # If .rpc_paths is empty, just assume all paths are legal
461 return True
462
463 def do_POST(self):
464 """Handles the HTTP POST request.
465
466 Attempts to interpret all HTTP POST requests as XML-RPC calls,
467 which are forwarded to the server's _dispatch method for handling.
468 """
469
470 # Check that the path is legal
471 if not self.is_rpc_path_valid():
472 self.report_404()
473 return
474
475 try:
476 # Get arguments by reading body of request.
477 # We read this in chunks to avoid straining
478 # socket.read(); around the 10 or 15Mb mark, some platforms
479 # begin to have problems (bug #792570).
480 max_chunk_size = 10*1024*1024
481 size_remaining = int(self.headers["content-length"])
482 L = []
483 while size_remaining:
484 chunk_size = min(size_remaining, max_chunk_size)
485 chunk = self.rfile.read(chunk_size)
486 if not chunk:
487 break
488 L.append(chunk)
489 size_remaining -= len(L[-1])
490 data = b''.join(L)
491
492 data = self.decode_request_content(data)
493 if data is None:
494 return #response has been sent
495
496 # In previous versions of SimpleXMLRPCServer, _dispatch
497 # could be overridden in this class, instead of in
498 # SimpleXMLRPCDispatcher. To maintain backwards compatibility,
499 # check to see if a subclass implements _dispatch and dispatch
500 # using that method if present.
501 response = self.server._marshaled_dispatch(
502 data, getattr(self, '_dispatch', None), self.path
503 )
504 except Exception as e: # This should only happen if the module is buggy
505 # internal error, report as HTTP server error
506 self.send_response(500)
507
508 # Send information about the exception if requested
509 if hasattr(self.server, '_send_traceback_header') and \
510 self.server._send_traceback_header:
511 self.send_header("X-exception", str(e))
512 trace = traceback.format_exc()
513 trace = str(trace.encode('ASCII', 'backslashreplace'), 'ASCII')
514 self.send_header("X-traceback", trace)
515
516 self.send_header("Content-length", "0")
517 self.end_headers()
518 else:
519 self.send_response(200)
520 self.send_header("Content-type", "text/xml")
521 if self.encode_threshold is not None:
522 if len(response) > self.encode_threshold:
523 q = self.accept_encodings().get("gzip", 0)
524 if q:
525 try:
526 response = gzip_encode(response)
527 self.send_header("Content-Encoding", "gzip")
528 except NotImplementedError:
529 pass
530 self.send_header("Content-length", str(len(response)))
531 self.end_headers()
532 self.wfile.write(response)
533
534 def decode_request_content(self, data):
535 #support gzip encoding of request
536 encoding = self.headers.get("content-encoding", "identity").lower()
537 if encoding == "identity":
538 return data
539 if encoding == "gzip":
540 try:
541 return gzip_decode(data)
542 except NotImplementedError:
543 self.send_response(501, "encoding %r not supported" % encoding)
544 except ValueError:
545 self.send_response(400, "error decoding gzip content")
546 else:
547 self.send_response(501, "encoding %r not supported" % encoding)
548 self.send_header("Content-length", "0")
549 self.end_headers()
550
551 def report_404 (self):
552 # Report a 404 error
553 self.send_response(404)
554 response = b'No such page'
555 self.send_header("Content-type", "text/plain")
556 self.send_header("Content-length", str(len(response)))
557 self.end_headers()
558 self.wfile.write(response)
559
560 def log_request(self, code='-', size='-'):
561 """Selectively log an accepted request."""
562
563 if self.server.logRequests:
564 BaseHTTPRequestHandler.log_request(self, code, size)
565
566 class SimpleXMLRPCServer(socketserver.TCPServer,
567 SimpleXMLRPCDispatcher):
568 """Simple XML-RPC server.
569
570 Simple XML-RPC server that allows functions and a single instance
571 to be installed to handle requests. The default implementation
572 attempts to dispatch XML-RPC calls to the functions or instance
573 installed in the server. Override the _dispatch method inherited
574 from SimpleXMLRPCDispatcher to change this behavior.
575 """
576
577 allow_reuse_address = True
578
579 # Warning: this is for debugging purposes only! Never set this to True in
580 # production code, as will be sending out sensitive information (exception
581 # and stack trace details) when exceptions are raised inside
582 # SimpleXMLRPCRequestHandler.do_POST
583 _send_traceback_header = False
584
585 def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler,
586 logRequests=True, allow_none=False, encoding=None,
587 bind_and_activate=True, use_builtin_types=False):
588 self.logRequests = logRequests
589
590 SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding, use_builtin_types)
591 socketserver.TCPServer.__init__(self, addr, requestHandler, bind_and_activate)
592
593 # [Bug #1222790] If possible, set close-on-exec flag; if a
594 # method spawns a subprocess, the subprocess shouldn't have
595 # the listening socket open.
596 if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'):
597 flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD)
598 flags |= fcntl.FD_CLOEXEC
599 fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)
600
601 class MultiPathXMLRPCServer(SimpleXMLRPCServer):
602 """Multipath XML-RPC Server
603 This specialization of SimpleXMLRPCServer allows the user to create
604 multiple Dispatcher instances and assign them to different
605 HTTP request paths. This makes it possible to run two or more
606 'virtual XML-RPC servers' at the same port.
607 Make sure that the requestHandler accepts the paths in question.
608 """
609 def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler,
610 logRequests=True, allow_none=False, encoding=None,
611 bind_and_activate=True, use_builtin_types=False):
612
613 SimpleXMLRPCServer.__init__(self, addr, requestHandler, logRequests, allow_none,
614 encoding, bind_and_activate, use_builtin_types)
615 self.dispatchers = {}
616 self.allow_none = allow_none
617 self.encoding = encoding or 'utf-8'
618
619 def add_dispatcher(self, path, dispatcher):
620 self.dispatchers[path] = dispatcher
621 return dispatcher
622
623 def get_dispatcher(self, path):
624 return self.dispatchers[path]
625
626 def _marshaled_dispatch(self, data, dispatch_method = None, path = None):
627 try:
628 response = self.dispatchers[path]._marshaled_dispatch(
629 data, dispatch_method, path)
630 except:
631 # report low level exception back to server
632 # (each dispatcher should have handled their own
633 # exceptions)
634 exc_type, exc_value = sys.exc_info()[:2]
635 response = dumps(
636 Fault(1, "%s:%s" % (exc_type, exc_value)),
637 encoding=self.encoding, allow_none=self.allow_none)
638 response = response.encode(self.encoding)
639 return response
640
641 class CGIXMLRPCRequestHandler(SimpleXMLRPCDispatcher):
642 """Simple handler for XML-RPC data passed through CGI."""
643
644 def __init__(self, allow_none=False, encoding=None, use_builtin_types=False):
645 SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding, use_builtin_types)
646
647 def handle_xmlrpc(self, request_text):
648 """Handle a single XML-RPC request"""
649
650 response = self._marshaled_dispatch(request_text)
651
652 print('Content-Type: text/xml')
653 print('Content-Length: %d' % len(response))
654 print()
655 sys.stdout.flush()
656 sys.stdout.buffer.write(response)
657 sys.stdout.buffer.flush()
658
659 def handle_get(self):
660 """Handle a single HTTP GET request.
661
662 Default implementation indicates an error because
663 XML-RPC uses the POST method.
664 """
665
666 code = 400
667 message, explain = BaseHTTPRequestHandler.responses[code]
668
669 response = http_server.DEFAULT_ERROR_MESSAGE % \
670 {
671 'code' : code,
672 'message' : message,
673 'explain' : explain
674 }
675 response = response.encode('utf-8')
676 print('Status: %d %s' % (code, message))
677 print('Content-Type: %s' % http_server.DEFAULT_ERROR_CONTENT_TYPE)
678 print('Content-Length: %d' % len(response))
679 print()
680 sys.stdout.flush()
681 sys.stdout.buffer.write(response)
682 sys.stdout.buffer.flush()
683
684 def handle_request(self, request_text=None):
685 """Handle a single XML-RPC request passed through a CGI post method.
686
687 If no XML data is given then it is read from stdin. The resulting
688 XML-RPC response is printed to stdout along with the correct HTTP
689 headers.
690 """
691
692 if request_text is None and \
693 os.environ.get('REQUEST_METHOD', None) == 'GET':
694 self.handle_get()
695 else:
696 # POST data is normally available through stdin
697 try:
698 length = int(os.environ.get('CONTENT_LENGTH', None))
699 except (ValueError, TypeError):
700 length = -1
701 if request_text is None:
702 request_text = sys.stdin.read(length)
703
704 self.handle_xmlrpc(request_text)
705
706
707 # -----------------------------------------------------------------------------
708 # Self documenting XML-RPC Server.
709
710 class ServerHTMLDoc(pydoc.HTMLDoc):
711 """Class used to generate pydoc HTML document for a server"""
712
713 def markup(self, text, escape=None, funcs={}, classes={}, methods={}):
714 """Mark up some plain text, given a context of symbols to look for.
715 Each context dictionary maps object names to anchor names."""
716 escape = escape or self.escape
717 results = []
718 here = 0
719
720 # XXX Note that this regular expression does not allow for the
721 # hyperlinking of arbitrary strings being used as method
722 # names. Only methods with names consisting of word characters
723 # and '.'s are hyperlinked.
724 pattern = re.compile(r'\b((http|ftp)://\S+[\w/]|'
725 r'RFC[- ]?(\d+)|'
726 r'PEP[- ]?(\d+)|'
727 r'(self\.)?((?:\w|\.)+))\b')
728 while 1:
729 match = pattern.search(text, here)
730 if not match: break
731 start, end = match.span()
732 results.append(escape(text[here:start]))
733
734 all, scheme, rfc, pep, selfdot, name = match.groups()
735 if scheme:
736 url = escape(all).replace('"', '"')
737 results.append('<a href="%s">%s</a>' % (url, url))
738 elif rfc:
739 url = 'http://www.rfc-editor.org/rfc/rfc%d.txt' % int(rfc)
740 results.append('<a href="%s">%s</a>' % (url, escape(all)))
741 elif pep:
742 url = 'http://www.python.org/dev/peps/pep-%04d/' % int(pep)
743 results.append('<a href="%s">%s</a>' % (url, escape(all)))
744 elif text[end:end+1] == '(':
745 results.append(self.namelink(name, methods, funcs, classes))
746 elif selfdot:
747 results.append('self.<strong>%s</strong>' % name)
748 else:
749 results.append(self.namelink(name, classes))
750 here = end
751 results.append(escape(text[here:]))
752 return ''.join(results)
753
754 def docroutine(self, object, name, mod=None,
755 funcs={}, classes={}, methods={}, cl=None):
756 """Produce HTML documentation for a function or method object."""
757
758 anchor = (cl and cl.__name__ or '') + '-' + name
759 note = ''
760
761 title = '<a name="%s"><strong>%s</strong></a>' % (
762 self.escape(anchor), self.escape(name))
763
764 if inspect.ismethod(object):
765 args = inspect.getfullargspec(object)
766 # exclude the argument bound to the instance, it will be
767 # confusing to the non-Python user
768 argspec = inspect.formatargspec (
769 args.args[1:],
770 args.varargs,
771 args.varkw,
772 args.defaults,
773 annotations=args.annotations,
774 formatvalue=self.formatvalue
775 )
776 elif inspect.isfunction(object):
777 args = inspect.getfullargspec(object)
778 argspec = inspect.formatargspec(
779 args.args, args.varargs, args.varkw, args.defaults,
780 annotations=args.annotations,
781 formatvalue=self.formatvalue)
782 else:
783 argspec = '(...)'
784
785 if isinstance(object, tuple):
786 argspec = object[0] or argspec
787 docstring = object[1] or ""
788 else:
789 docstring = pydoc.getdoc(object)
790
791 decl = title + argspec + (note and self.grey(
792 '<font face="helvetica, arial">%s</font>' % note))
793
794 doc = self.markup(
795 docstring, self.preformat, funcs, classes, methods)
796 doc = doc and '<dd><tt>%s</tt></dd>' % doc
797 return '<dl><dt>%s</dt>%s</dl>\n' % (decl, doc)
798
799 def docserver(self, server_name, package_documentation, methods):
800 """Produce HTML documentation for an XML-RPC server."""
801
802 fdict = {}
803 for key, value in methods.items():
804 fdict[key] = '#-' + key
805 fdict[value] = fdict[key]
806
807 server_name = self.escape(server_name)
808 head = '<big><big><strong>%s</strong></big></big>' % server_name
809 result = self.heading(head, '#ffffff', '#7799ee')
810
811 doc = self.markup(package_documentation, self.preformat, fdict)
812 doc = doc and '<tt>%s</tt>' % doc
813 result = result + '<p>%s</p>\n' % doc
814
815 contents = []
816 method_items = sorted(methods.items())
817 for key, value in method_items:
818 contents.append(self.docroutine(value, key, funcs=fdict))
819 result = result + self.bigsection(
820 'Methods', '#ffffff', '#eeaa77', ''.join(contents))
821
822 return result
823
824 class XMLRPCDocGenerator(object):
825 """Generates documentation for an XML-RPC server.
826
827 This class is designed as mix-in and should not
828 be constructed directly.
829 """
830
831 def __init__(self):
832 # setup variables used for HTML documentation
833 self.server_name = 'XML-RPC Server Documentation'
834 self.server_documentation = \
835 "This server exports the following methods through the XML-RPC "\
836 "protocol."
837 self.server_title = 'XML-RPC Server Documentation'
838
839 def set_server_title(self, server_title):
840 """Set the HTML title of the generated server documentation"""
841
842 self.server_title = server_title
843
844 def set_server_name(self, server_name):
845 """Set the name of the generated HTML server documentation"""
846
847 self.server_name = server_name
848
849 def set_server_documentation(self, server_documentation):
850 """Set the documentation string for the entire server."""
851
852 self.server_documentation = server_documentation
853
854 def generate_html_documentation(self):
855 """generate_html_documentation() => html documentation for the server
856
857 Generates HTML documentation for the server using introspection for
858 installed functions and instances that do not implement the
859 _dispatch method. Alternatively, instances can choose to implement
860 the _get_method_argstring(method_name) method to provide the
861 argument string used in the documentation and the
862 _methodHelp(method_name) method to provide the help text used
863 in the documentation."""
864
865 methods = {}
866
867 for method_name in self.system_listMethods():
868 if method_name in self.funcs:
869 method = self.funcs[method_name]
870 elif self.instance is not None:
871 method_info = [None, None] # argspec, documentation
872 if hasattr(self.instance, '_get_method_argstring'):
873 method_info[0] = self.instance._get_method_argstring(method_name)
874 if hasattr(self.instance, '_methodHelp'):
875 method_info[1] = self.instance._methodHelp(method_name)
876
877 method_info = tuple(method_info)
878 if method_info != (None, None):
879 method = method_info
880 elif not hasattr(self.instance, '_dispatch'):
881 try:
882 method = resolve_dotted_attribute(
883 self.instance,
884 method_name
885 )
886 except AttributeError:
887 method = method_info
888 else:
889 method = method_info
890 else:
891 assert 0, "Could not find method in self.functions and no "\
892 "instance installed"
893
894 methods[method_name] = method
895
896 documenter = ServerHTMLDoc()
897 documentation = documenter.docserver(
898 self.server_name,
899 self.server_documentation,
900 methods
901 )
902
903 return documenter.page(self.server_title, documentation)
904
905 class DocXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
906 """XML-RPC and documentation request handler class.
907
908 Handles all HTTP POST requests and attempts to decode them as
909 XML-RPC requests.
910
911 Handles all HTTP GET requests and interprets them as requests
912 for documentation.
913 """
914
915 def do_GET(self):
916 """Handles the HTTP GET request.
917
918 Interpret all HTTP GET requests as requests for server
919 documentation.
920 """
921 # Check that the path is legal
922 if not self.is_rpc_path_valid():
923 self.report_404()
924 return
925
926 response = self.server.generate_html_documentation().encode('utf-8')
927 self.send_response(200)
928 self.send_header("Content-type", "text/html")
929 self.send_header("Content-length", str(len(response)))
930 self.end_headers()
931 self.wfile.write(response)
932
933 class DocXMLRPCServer( SimpleXMLRPCServer,
934 XMLRPCDocGenerator):
935 """XML-RPC and HTML documentation server.
936
937 Adds the ability to serve server documentation to the capabilities
938 of SimpleXMLRPCServer.
939 """
940
941 def __init__(self, addr, requestHandler=DocXMLRPCRequestHandler,
942 logRequests=True, allow_none=False, encoding=None,
943 bind_and_activate=True, use_builtin_types=False):
944 SimpleXMLRPCServer.__init__(self, addr, requestHandler, logRequests,
945 allow_none, encoding, bind_and_activate,
946 use_builtin_types)
947 XMLRPCDocGenerator.__init__(self)
948
949 class DocCGIXMLRPCRequestHandler( CGIXMLRPCRequestHandler,
950 XMLRPCDocGenerator):
951 """Handler for XML-RPC data and documentation requests passed through
952 CGI"""
953
954 def handle_get(self):
955 """Handles the HTTP GET request.
956
957 Interpret all HTTP GET requests as requests for server
958 documentation.
959 """
960
961 response = self.generate_html_documentation().encode('utf-8')
962
963 print('Content-Type: text/html')
964 print('Content-Length: %d' % len(response))
965 print()
966 sys.stdout.flush()
967 sys.stdout.buffer.write(response)
968 sys.stdout.buffer.flush()
969
970 def __init__(self):
971 CGIXMLRPCRequestHandler.__init__(self)
972 XMLRPCDocGenerator.__init__(self)
973
974
975 if __name__ == '__main__':
976 import datetime
977
978 class ExampleService:
979 def getData(self):
980 return '42'
981
982 class currentTime:
983 @staticmethod
984 def getCurrentTime():
985 return datetime.datetime.now()
986
987 server = SimpleXMLRPCServer(("localhost", 8000))
988 server.register_function(pow)
989 server.register_function(lambda x,y: x+y, 'add')
990 server.register_instance(ExampleService(), allow_dotted_names=True)
991 server.register_multicall_functions()
992 print('Serving XML-RPC on localhost port 8000')
993 print('It is advisable to run this example server within a secure, closed network.')
994 try:
995 server.serve_forever()
996 except KeyboardInterrupt:
997 print("\nKeyboard interrupt received, exiting.")
998 server.server_close()
999 sys.exit(0)