Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement xmlrpc options, such as allow_none #2

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions CONTRIBUTORS.txt
Expand Up @@ -109,3 +109,6 @@ Contributors
- Chris McDonough, 2010/11/08

- Tres Seaver, 2010/11/09

- Guillaume Gauvrit, 2012/12/08

28 changes: 28 additions & 0 deletions docs/index.rst
Expand Up @@ -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`
Expand Down
49 changes: 35 additions & 14 deletions 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
Expand Down Expand Up @@ -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

Expand All @@ -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')
151 changes: 139 additions & 12 deletions 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)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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)