diff --git a/Doc/library/xmlrpc.server.rst b/Doc/library/xmlrpc.server.rst --- a/Doc/library/xmlrpc.server.rst +++ b/Doc/library/xmlrpc.server.rst @@ -61,6 +61,19 @@ The *use_builtin_types* flag was added. +.. class:: WSGIXMLRPCRequestHandler(allow_none=False, encoding=None,\ + use_builtin_types=False) + + Create a WSGI application to handle XML-RPC requests in a WSGI environment. + The *allow_none* and *encoding* parameters are passed on to + :mod:`xmlrpc.client` and control the XML-RPC responses that will be returned + from the server. The *use_builtin_types* parameter is passed to the + :func:`~xmlrpc.client.loads` function and controls which types are processed + when date/times values or binary data are received; it defaults to false. + + .. versionadded:: 3.4 + + .. class:: SimpleXMLRPCRequestHandler() Create a new request handler instance. This request handler supports ``POST`` @@ -246,13 +259,103 @@ handler.handle_request() +WSGIXMLRPCRequestHandler +------------------------ +The :class:`WSGIXMLRPCRequestHandler` can be used to handle XML-RPC requests as +a WSGI application. + +.. method:: WSGIXMLRPCRequestHandler.register_function(function, name=None) + + Register a function that can respond to XML-RPC requests. If *name* is + given, it will be the method name associated with function, otherwise + *function.__name__* will be used. *name* can be either a normal or Unicode + string, and may contain characters not legal in Python identifiers, + including the period character. + + +.. method:: WSGIXMLRPCRequestHandler.register_instance(instance) + + Register an object which is used to expose method names which have not been + registered using :meth:`register_function`. If instance contains a + :meth:`_dispatch` method, it is called with the requested method name and + the parameters from the request; the return value is returned to the client + as the result. If instance does not have a :meth:`_dispatch` method, it is + searched for an attribute matching the name of the requested method; if the + requested method name contains periods, each component of the method name + is searched for individually, with the effect that a simple hierarchical + search is performed. The value found from this search is then called with + the parameters from the request, and the return value is passed back to + the client. + + +.. method:: WSGIXMLRPCRequestHandler.register_introspection_functions() + + Register the XML-RPC introspection functions ``system.listMethods``, + ``system.methodHelp`` and ``system.methodSignature``. + + +.. method:: WSGIXMLRPCRequestHandler.register_multicall_functions() + + Register the XML-RPC multicall function ``system.multicall``. + +WSGIXMLRPCRequestHandler Example +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Example:: + + from xmlrpc.server import WSGIXMLRPCRequestHandler + from wsgiref.simple_server import make_server + + # Create the WSGI application + wsgi_app = WSGIXMLRPCRequestHandler() + + wsgi_app.register_introspection_functions() + + # Register pow() function; this will use the value of + # pow.__name__ as the name, which is just 'pow'. + wsgi_app.register_function(pow) + + # Register a function under a different name + def adder_function(x,y): + return x + y + + wsgi_app.register_function(adder_function, 'add') + + # Register an instance; all the methods of the instance are + # published as XML-RPC methods (in this case, just 'mul'). + class MyFuncs: + def mul(self, x, y): + return x * y + + wsgi_app.register_instance(MyFuncs()) + + + #Create a WSGI Server + server = make_server('',8000, wsgi_app) + + # Run the server's main loop + server.serve_forever() + +The following client code will call the methods made available by the preceding +server:: + + import xmlrpc.client + + s = xmlrpc.client.ServerProxy('http://localhost:8000') + print(s.pow(2,3)) # Returns 2**3 = 8 + print(s.add(2,3)) # Returns 5 + print(s.mul(5,2)) # Returns 5*2 = 10 + + # Print list of available methods + print(s.system.listMethods()) + Documenting XMLRPC server ------------------------- These classes extend the above classes to serve HTML documentation in response -to HTTP GET requests. Servers can either be free standing, using -:class:`DocXMLRPCServer`, or embedded in a CGI environment, using -:class:`DocCGIXMLRPCRequestHandler`. +to HTTP GET requests. Servers can be free standing using +:class:`DocXMLRPCServer`, embedded in a CGI environment using +:class:`DocCGIXMLRPCRequestHandler`, or run as a WSGI application using +:class:`DocWSGIXMLRPCRequestHandler`. .. class:: DocXMLRPCServer(addr, requestHandler=DocXMLRPCRequestHandler,\ @@ -271,6 +374,9 @@ Create a new instance to handle XML-RPC requests in a CGI environment. +.. class:: DocWSGIXMLRPCRequestHandler() + + Create a documenting WSGI application to handle XML-RPC requests. .. class:: DocXMLRPCRequestHandler() @@ -287,7 +393,7 @@ The :class:`DocXMLRPCServer` class is derived from :class:`SimpleXMLRPCServer` and provides a means of creating self-documenting, stand alone XML-RPC -servers. HTTP POST requests are handled as XML-RPC method calls. HTTP GET +servers. HTTP POST requests are handled as XML-RPC method calls. HTTP GET requests are handled by generating pydoc-style HTML documentation. This allows a server to provide its own web-based documentation. @@ -336,3 +442,32 @@ Set the description used in the generated HTML documentation. This description will appear as a paragraph, below the server name, in the documentation. + + +DocWSGIXMLRequestHandler +------------------------ + +The :class:`DocWSGIXMLRPCRequestHandler` class is derived from +:class:`WSGIXMLRPCRequestHandler` and provides a means of creating a +self-documenting, XML-RPC WSGI application. HTTP POST requests are handled as XML-RPC +method calls. HTTP GET requests are handled by generating pydoc-style HTML +documentation. This allows a server to provide its own web-based documentation. + +.. method:: DocCGIXMLRPCRequestHandler.set_server_title(server_title) + + Set the title used in the generated HTML documentation. This title will be used + inside the HTML "title" element. + + +.. method:: DocCGIXMLRPCRequestHandler.set_server_name(server_name) + + Set the name used in the generated HTML documentation. This name will appear at + the top of the generated documentation inside a "h1" element. + + +.. method:: DocCGIXMLRPCRequestHandler.set_server_documentation(server_documentation) + + Set the description used in the generated HTML documentation. This description + will appear as a paragraph, below the server name, in the documentation. + + diff --git a/Lib/test/test_xmlrpc.py b/Lib/test/test_xmlrpc.py --- a/Lib/test/test_xmlrpc.py +++ b/Lib/test/test_xmlrpc.py @@ -12,6 +12,7 @@ import io import contextlib from test import support +from io import StringIO try: import gzip @@ -1046,6 +1047,84 @@ len(content)) +class WSGIHandlerTestCase(unittest.TestCase): + + class MockStartResponse(): + """ A Mock start_response implementation """ + + def __call__(self, status, response_headers, exc_info=None): + self.status = status + self.response_headers = response_headers + + def setUp(self): + self.wsgi = xmlrpc.server.WSGIXMLRPCRequestHandler() + self.test_data = ("400 Bad request" + "

400 Bad request

") + + def tearDown(self): + self.wsgi = None + + def test_wsgi_head(self): + start_response = self.MockStartResponse() + environ = {} + environ['REQUEST_METHOD'] = 'HEAD' + + response = self.wsgi(environ, start_response) + self.assertEqual(start_response.status, '400 Bad Request') + self.assertTrue(len(start_response.response_headers) > 0) + headers = dict(start_response.response_headers) + self.assertTrue('Content-Length' in headers) + self.assertEqual(headers['Content-Length'], str(len(self.test_data))) + + def test_wsgi_non_post(self): + start_response = self.MockStartResponse() + + methods = ['GET', 'PUT', 'DELETE'] + for method in methods: + environ = {} + environ['REQUEST_METHOD'] = method + + response = self.wsgi(environ, start_response) + + self.assertEqual(start_response.status, '400 Bad Request') + self.assertEqual(response, [self.test_data]) + + def test_wsgi_xmlrpc_response(self): + data = """ + + test_method + + + foo + + + bar + + + + """ + start_response = self.MockStartResponse() + environ = {} + body = StringIO() + body.write(data) + environ['REQUEST_METHOD'] = 'POST' + environ['wsgi.input'] = body + environ['CONTENT_LENGTH'] = str(len(data)) + + response = self.wsgi(environ, start_response) + data = response[0] + headers = dict(start_response.response_headers) + + #Test that we get an XML Response back + self.assertEqual(start_response.status, '200 OK') + self.assertEqual(str(len(data)), headers['Content-Length']) + self.assertEqual(headers['Content-Type'], 'text/xml') + + #Test that we got an exception since test_method is not registered + self.assertRaises(xmlrpclib.Fault, xmlrpclib.loads, data) + + + class UseBuiltinTypesTestCase(unittest.TestCase): def test_use_builtin_types(self): @@ -1086,7 +1165,7 @@ SimpleServerTestCase, KeepaliveServerTestCase1, KeepaliveServerTestCase2, GzipServerTestCase, MultiPathServerTestCase, ServerProxyTestCase, FailingServerTestCase, - CGIHandlerTestCase) + CGIHandlerTestCase, WSGIHandlerTestCase) if __name__ == "__main__": diff --git a/Lib/xmlrpc/server.py b/Lib/xmlrpc/server.py --- a/Lib/xmlrpc/server.py +++ b/Lib/xmlrpc/server.py @@ -698,6 +698,39 @@ self.handle_xmlrpc(request_text) +class WSGIXMLRPCRequestHandler(SimpleXMLRPCDispatcher): + """ + Provides a simple WSGI application to handle XML-RPC requests in + a WSGI environment. + """ + + def __init__(self, allow_none=False, encoding=None, + use_builtin_types=False): + SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding, + use_builtin_types) + + def __call__(self, environ, start_response): + """WSGI Interface""" + if environ['REQUEST_METHOD'] != 'POST': + status = '400 Bad Request' + headers = [('Content-Type', 'text/html')] + data = ('400 Bad request' + '

400 Bad request

') + headers.append(('Content-Length', str(len(data)))) + start_response(status, headers) + if environ['REQUEST_METHOD'] == 'HEAD': + return [] + return [data] + + l = int(environ['CONTENT_LENGTH']) + request = environ['wsgi.input'].read(l) + response = self._marshaled_dispatch(request) + headers = [('Content-Type', 'text/xml')] + headers.append(('Content-Length', str(len(response)))) + start_response('200 OK', headers) + return [response] + + # ----------------------------------------------------------------------------- # Self documenting XML-RPC Server. @@ -963,6 +996,32 @@ XMLRPCDocGenerator.__init__(self) +class DocWSGIXMLRPCRequestHandler(WSGIXMLRPCRequestHandler, + XMLRPCDocGenerator): + """Handler for XML-RPC data and documentation requests for WSGI""" + + def __init__(self): + WSGIXMLRPCRequestHandler.__init__(self) + XMLRPCDocGenerator.__init__(self) + + def __call__(self, environ, start_response): + """Handles the HTTP GET request. + + Interpret all HTTP GET requests as requests for server + documentation. + """ + + if environ['REQUEST_METHOD'] == 'GET': + response = self.generate_html_documentation().encode('utf-8') + status = '200 OK' + headers = [('Content-Type', 'text/html'), + ('Content-Length', str(len(response)))] + start_response(status, headers) + return [response] + + return WSGIXMLRPCRequestHandler.__call__(self, environ, start_response) + + if __name__ == '__main__': server = SimpleXMLRPCServer(("localhost", 8000)) server.register_function(pow) diff --git a/Misc/ACKS b/Misc/ACKS --- a/Misc/ACKS +++ b/Misc/ACKS @@ -948,6 +948,7 @@ Heikki Partanen Harri Pasanen Gaƫl Pasgrimaud +Sanjeev Paskaradevan Ashish Nitin Patil Randy Pausch Samuele Pedroni