Implement xmlrpc options, such as allow_none #2

Open
wants to merge 4 commits into from
View
3 CONTRIBUTORS.txt
@@ -109,3 +109,6 @@ Contributors
- Chris McDonough, 2010/11/08
- Tres Seaver, 2010/11/09
+
+- Guillaume Gauvrit, 2012/12/08
+
View
28 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 <http://docs.python.org/2/library/xmlrpclib.html>`_.
+
+ - ``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`
View
49 pyramid_xmlrpc/__init__.py
@@ -1,38 +1,46 @@
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
second element in the tuple is the method name."""
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')
View
151 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],
+ "<?xml version='1.0' encoding='iso-8859-1'?")
+
+
class TestParseXMLRPCRequest(unittest.TestCase):
- def _callFUT(self, request):
+
+ def _callFUT(self, request, use_datetime=0):
from pyramid_xmlrpc import parse_xmlrpc_request
- return parse_xmlrpc_request(request)
+ return parse_xmlrpc_request(request, use_datetime)
def test_normal(self):
import xmlrpclib
@@ -55,7 +78,23 @@ def test_toobig(self):
request.content_length = 1 << 24
self.assertRaises(ValueError, self._callFUT, request)
+ def test_datetime(self):
+ import datetime
+ import xmlrpclib
+ from pyramid_xmlrpc import parse_xmlrpc_request
+ param = datetime.datetime.now()
+ packet = xmlrpclib.dumps((param,), methodname='__call__')
+ request = testing.DummyRequest()
+ request.body = packet
+ request.content_length = len(packet)
+ params, method = self._callFUT(request)
+ self.assertEqual(params[0].__class__, xmlrpclib.DateTime)
+ params, method = self._callFUT(request, use_datetime=True)
+ self.assertEqual(params[0].__class__, datetime.datetime)
+
+
class TestDecorator(unittest.TestCase):
+
def _callFUT(self, unwrapped):
from pyramid_xmlrpc import xmlrpc_view
return xmlrpc_view(unwrapped)
@@ -76,15 +115,17 @@ def unwrapped(context, what):
request.content_length = len(packet)
response = wrapped(context, request)
self.assertEqual(response.body, xmlrpclib.dumps((param,),
- methodresponse=True))
+ methodresponse=True))
+
class TestBaseClass(unittest.TestCase):
def test_normal(self):
-
+
from pyramid_xmlrpc import XMLRPCView
+
class Test(XMLRPCView):
- def a_method(self,param):
+ def a_method(self, param):
return param
# set up a request
@@ -97,7 +138,7 @@ def a_method(self,param):
# instantiate the view
context = testing.DummyModel()
- instance = Test(context,request)
+ instance = Test(context, request)
# these are fair game for the methods to use if they want
self.failUnless(instance.context is context)
@@ -106,4 +147,90 @@ def a_method(self,param):
# exercise it
response = instance()
self.assertEqual(response.body, xmlrpclib.dumps((param,),
- methodresponse=True))
+ methodresponse=True))
+
+ def test_marshalling_none(self):
+ from pyramid_xmlrpc import XMLRPCView
+
+ class Test(XMLRPCView):
+ allow_none = True
+
+ def a_method(self, param):
+ return None
+
+ import xmlrpclib
+ packet = xmlrpclib.dumps((None,), methodname='a_method',
+ allow_none=True)
+ request = testing.DummyRequest()
+ request.body = packet
+ request.content_length = len(packet)
+
+ # instantiate the view
+ context = testing.DummyModel()
+ instance = Test(context, request)
+ # exercise it
+ response = instance()
+ self.assertEqual(response.body, xmlrpclib.dumps((None,),
+ allow_none=True,
+ methodresponse=True))
+
+ def test_parse_datetime(self):
+ from pyramid_xmlrpc import XMLRPCView
+
+ class Test(XMLRPCView):
+ use_datetime = True
+
+ def a_method(self, param):
+ Test.datetime = param
+ return param
+
+ import xmlrpclib
+ import datetime
+ packet = xmlrpclib.dumps((datetime.datetime.now(),),
+ methodname='a_method')
+ request = testing.DummyRequest()
+ request.body = packet
+ request.content_length = len(packet)
+
+ # instantiate the view
+ context = testing.DummyModel()
+ instance = Test(context, request)
+ # exercise it
+ response = instance()
+ self.assertEqual(Test.datetime.__class__, datetime.datetime)
+
+ def test_charset(self):
+ from pyramid_xmlrpc import XMLRPCView
+
+ class Test(XMLRPCView):
+ charset = 'iso-8859-1'
+
+ def a_method(self, param):
+ return param
+
+ import xmlrpclib
+ packet = xmlrpclib.dumps(('param',), methodname='a_method')
+ request = testing.DummyRequest()
+ request.body = packet
+ request.content_length = len(packet)
+
+ # instantiate the view
+ context = testing.DummyModel()
+ instance = Test(context, request)
+ # exercise it
+ response = instance()
+ self.assertEqual(response.body.split('>', 1)[0],
+ "<?xml version='1.0' encoding='iso-8859-1'?")
+
+
+class TestConfig(unittest.TestCase):
+
+ def test_includeme(self):
+ from pyramid_xmlrpc import includeme, XMLRPCView
+
+ settings = {'xmlrpc.charset': 'iso-8859-15',
+ 'xmlrpc.allow_none': 'true'}
+ self.config = testing.setUp(settings=settings)
+ self.config.include(includeme)
+ self.assertEqual(XMLRPCView.charset, 'iso-8859-15')
+ self.assertTrue(XMLRPCView.allow_none)