diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 819a3a9..2a08a98 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -109,3 +109,6 @@ Contributors - Chris McDonough, 2010/11/08 - Tres Seaver, 2010/11/09 + +- Guillaume Gauvrit, 2012/12/08 + diff --git a/docs/index.rst b/docs/index.rst index e68b21a..c0dfc89 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -161,6 +161,34 @@ client: >>> s.say_goodbye() Goodbye, cruel world + +.. _configuration: + +Configuration XML-RPC +~~~~~~~~~~~~~~~~~~~~~ + +XML RPC serialization can be configured as in the `Python Standard +Library `_. + + - ``nil`` xml rpc extension + - cast every xmlrpc ``datetime.iso8601`` to python `datetime.datetime` type + - use an xml encoding in the response + +These parameters are set in the :mod:`pyramid` .ini configuration file +like below: + +.. code-block:: ini + + [app:main] + + pyramid.includes = + pyramid_xmlrpc + + xmlrpc.encoding = utf-8 + xmlrpc.allow_none = True + xmlrpc.datetime = True + + .. _api: API Documentation for :mod:`pyramid_xmlrpc` diff --git a/pyramid_xmlrpc/__init__.py b/pyramid_xmlrpc/__init__.py index 8943685..8ddd2b5 100644 --- a/pyramid_xmlrpc/__init__.py +++ b/pyramid_xmlrpc/__init__.py @@ -1,28 +1,35 @@ import xmlrpclib import webob -def xmlrpc_marshal(data): +from pyramid.settings import asbool + + +def xmlrpc_marshal(data, allow_none=False, encoding=None): """ Marshal a Python data structure into an XML document suitable for use as an XML-RPC response and return the document. If ``data`` is an ``xmlrpclib.Fault`` instance, it will be marshalled into a suitable XML-RPC fault response.""" if isinstance(data, xmlrpclib.Fault): - return xmlrpclib.dumps(data) + return xmlrpclib.dumps(data, allow_none=allow_none, encoding=encoding) else: - return xmlrpclib.dumps((data,), methodresponse=True) + return xmlrpclib.dumps((data,), methodresponse=True, + allow_none=allow_none, + encoding=encoding) + -def xmlrpc_response(data): +def xmlrpc_response(data, allow_none=False, encoding=None): """ Marshal a Python data structure into a webob ``Response`` object with a body that is an XML document suitable for use as an XML-RPC response with a content-type of ``text/xml`` and return the response.""" - xml = xmlrpc_marshal(data) + xml = xmlrpc_marshal(data, allow_none=allow_none, encoding=encoding) response = webob.Response(xml) response.content_type = 'text/xml' response.content_length = len(xml) return response -def parse_xmlrpc_request(request): + +def parse_xmlrpc_request(request, use_datetime=False): """ Deserialize the body of a request from an XML-RPC request document into a set of params and return a two-tuple. The first element in the tuple is the method params as a sequence, the @@ -30,9 +37,10 @@ def parse_xmlrpc_request(request): if request.content_length > (1 << 23): # protect from DOS (> 8MB body) raise ValueError('Body too large (%s bytes)' % request.content_length) - params, method = xmlrpclib.loads(request.body) + params, method = xmlrpclib.loads(request.body, use_datetime) return params, method + def xmlrpc_view(wrapped): """ This decorator turns functions which accept params and return Python structures into functions suitable for use as Pyramid views that speak @@ -83,23 +91,27 @@ def say(context, what): In other words do *not* decorate it in :func:`~pyramid_xmlrpc.xmlrpc_view`, then :class:`~pyramid.view.view_config`; it won't work. """ - + def _curried(context, request): params, method = parse_xmlrpc_request(request) value = wrapped(context, *params) return xmlrpc_response(value) _curried.__name__ = wrapped.__name__ - _curried.__grok_module__ = wrapped.__module__ + _curried.__grok_module__ = wrapped.__module__ return _curried - + + class XMLRPCView: """A base class for a view that serves multiple methods by XML-RPC. Subclass and add your methods as described in the documentation. """ + allow_none = False + charset = None + use_datetime = False - def __init__(self,context,request): + def __init__(self, context, request): self.context = context self.request = request @@ -113,7 +125,16 @@ def __call__(self): .. warning:: Do not override this method in any subclass if you want XML-RPC to continute to work! - + """ - params, method = parse_xmlrpc_request(self.request) - return xmlrpc_response(getattr(self,method)(*params)) + params, method = parse_xmlrpc_request(self.request, self.use_datetime) + return xmlrpc_response(getattr(self, method)(*params), self.allow_none, + self.charset) + + +def includeme(config): + settings = config.registry.settings + XMLRPCView.allow_none = asbool(settings.get('xmlrpc.allow_none', False)) + XMLRPCView.use_datetime = asbool(settings.get('xmlrpc.use_datetime', + False)) + XMLRPCView.charset = settings.get('xmlrpc.charset') diff --git a/pyramid_xmlrpc/tests.py b/pyramid_xmlrpc/tests.py index e58809a..6a41792 100644 --- a/pyramid_xmlrpc/tests.py +++ b/pyramid_xmlrpc/tests.py @@ -1,11 +1,14 @@ +# -*- coding: utf-8 -*- import unittest from pyramid import testing + class TestXMLRPCMarshal(unittest.TestCase): + def _callFUT(self, value): from pyramid_xmlrpc import xmlrpc_marshal return xmlrpc_marshal(value) - + def test_xmlrpc_marshal_normal(self): data = 1 marshalled = self._callFUT(data) @@ -19,11 +22,13 @@ def test_xmlrpc_marshal_fault(self): data = self._callFUT(fault) self.assertEqual(data, xmlrpclib.dumps(fault)) + class TestXMLRPResponse(unittest.TestCase): - def _callFUT(self, value): + + def _callFUT(self, value, allow_none=False, charset=None): from pyramid_xmlrpc import xmlrpc_response - return xmlrpc_response(value) - + return xmlrpc_response(value, allow_none, charset) + def test_xmlrpc_response(self): import xmlrpclib data = 1 @@ -33,11 +38,29 @@ def test_xmlrpc_response(self): methodresponse=True)) self.assertEqual(response.content_length, len(response.body)) self.assertEqual(response.status, '200 OK') - + + def test_xmlrpc_response_nil(self): + import xmlrpclib + data = None + self.assertRaises(TypeError, self._callFUT, data) + response = self._callFUT(data, allow_none=True).body + self.assertIsNone(xmlrpclib.loads(response)[0][0]) + + def test_xmlrpc_response_charset(self): + import xmlrpclib + data = u"é" + self.assertRaises(UnicodeEncodeError, self._callFUT, data, False, + "us-ascii") + response = self._callFUT(data, charset="iso-8859-1").body + self.assertEqual(response.split('>', 1)[0], + "', 1)[0], + "