summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2012-05-03 02:43:08 -0400
committerChris McDonough <chrism@plope.com>2012-05-03 02:43:08 -0400
commitad42cef589f37b998e804e780e92e52544ecfb16 (patch)
treeec5f73051a34d4835442c82c115fb91bb452d0ef
parent004882434aa166a58c3b2148322e08ce61ec4cb7 (diff)
parente012aa12760f6c29bfc9967c50a51d3f47db47da (diff)
downloadpyramid-ad42cef589f37b998e804e780e92e52544ecfb16.tar.gz
pyramid-ad42cef589f37b998e804e780e92e52544ecfb16.tar.bz2
pyramid-ad42cef589f37b998e804e780e92e52544ecfb16.zip
Merge branch 'mmerickel-feature.json-api'
-rw-r--r--CHANGES.txt3
-rw-r--r--docs/narr/renderers.rst41
-rw-r--r--pyramid/interfaces.py6
-rw-r--r--pyramid/renderers.py105
-rw-r--r--pyramid/tests/test_renderers.py48
5 files changed, 145 insertions, 58 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 34d60090d..7c2af4451 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -17,6 +17,9 @@ Features
values natively serializable by ``json.dumps`` (such as ints, lists,
dictionaries, strings, and so forth).
+- The JSON renderer now allows for the definition of custom type adapters to
+ convert unknown objects to JSON serializations.
+
- As of this release, the ``request_method`` predicate, when used, will also
imply that ``HEAD`` is implied when you use ``GET``. For example, using
``@view_config(request_method='GET')`` is equivalent to using
diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst
index 02063a112..57b5bc65b 100644
--- a/docs/narr/renderers.rst
+++ b/docs/narr/renderers.rst
@@ -182,10 +182,10 @@ using the API of the ``request.response`` attribute. See
JSON Renderer
~~~~~~~~~~~~~
-The ``json`` renderer renders view callable results to :term:`JSON`. It
-passes the return value through the ``json.dumps`` standard library function,
-and wraps the result in a response object. It also sets the response
-content-type to ``application/json``.
+The ``json`` renderer renders view callable results to :term:`JSON`. By
+default, it passes the return value through the ``json.dumps`` standard
+library function, and wraps the result in a response object. It also sets
+the response content-type to ``application/json``.
Here's an example of a view that returns a dictionary. Since the ``json``
renderer is specified in the configuration for this view, the view will
@@ -209,11 +209,11 @@ representing the JSON serialization of the return value:
'{"content": "Hello!"}'
The return value needn't be a dictionary, but the return value must contain
-values serializable by ``json.dumps``.
+values serializable by the configured serializer (by default ``json.dumps``).
.. note::
- Extra arguments can be passed to ``json.dumps`` by overriding the default
+ Extra arguments can be passed to the serializer by overriding the default
``json`` renderer. See :class:`pyramid.renderers.JSON` and
:ref:`adding_and_overriding_renderers` for more information.
@@ -240,8 +240,9 @@ Serializing Custom Objects
Custom objects can be made easily JSON-serializable in Pyramid by defining a
``__json__`` method on the object's class. This method should return values
-natively serializable by ``json.dumps`` (such as ints, lists, dictionaries,
-strings, and so forth).
+natively JSON-serializable (such as ints, lists, dictionaries, strings, and
+so forth). It should accept a single additional argument, ``request``, which
+will be the active request object at render time.
.. code-block:: python
:linenos:
@@ -252,7 +253,7 @@ strings, and so forth).
def __init__(self, x):
self.x = x
- def __json__(self):
+ def __json__(self, request):
return {'x':self.x}
@view_config(renderer='json')
@@ -267,27 +268,29 @@ possible (or at least not reasonable) to add a custom ``__json__`` method to
to their classes in order to influence serialization. If the object passed
to the renderer is not a serializable type, and has no ``__json__`` method,
usually a :exc:`TypeError` will be raised during serialization. You can
-change this behavior by creating a JSON renderer with a "default" function
-which tries to "sniff" at the object, and returns a valid serialization (a
-string) or raises a TypeError if it can't determine what to do with the
-object. A short example follows:
+change this behavior by creating a custom JSON renderer and adding adapters
+to handle custom types. The renderer will attempt to adapt non-serializable
+objects using the registered adapters. A short example follows:
.. code-block:: python
:linenos:
from pyramid.renderers import JSON
- def default(obj):
- if isinstance(obj, datetime.datetime):
- return obj.isoformat()
- raise TypeError('%r is not serializable % (obj,))
-
- json_renderer = JSON(default=default)
+ json_renderer = JSON()
+ def datetime_adapter(obj, request):
+ return obj.isoformat()
+ json_renderer.add_adapter(datetime.datetime, datetime_adapter)
# then during configuration ....
config = Configurator()
config.add_renderer('json', json_renderer)
+The adapter should accept two arguments: the object needing to be serialized
+and ``request``, which will be the current request object at render time.
+The adapter should raise a :exc:`TypeError` if it can't determine what to do
+with the object.
+
See :class:`pyramid.renderers.JSON` and
:ref:`adding_and_overriding_renderers` for more information.
diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py
index 5d9d29afa..1445ee394 100644
--- a/pyramid/interfaces.py
+++ b/pyramid/interfaces.py
@@ -1105,6 +1105,12 @@ class IAssetDescriptor(Interface):
Returns True if asset exists, otherwise returns False.
"""
+class IJSONAdapter(Interface):
+ """
+ Marker interface for objects that can convert an arbitrary object
+ into a JSON-serializable primitive.
+ """
+
# configuration phases: a lower phase number means the actions associated
# with this phase will be executed earlier than those with later phase
# numbers. The default phase number is 0, FTR.
diff --git a/pyramid/renderers.py b/pyramid/renderers.py
index b393a40a6..bdef6f561 100644
--- a/pyramid/renderers.py
+++ b/pyramid/renderers.py
@@ -3,11 +3,16 @@ import os
import pkg_resources
import threading
-from zope.interface import implementer
+from zope.interface import (
+ implementer,
+ providedBy,
+ )
+from zope.interface.registry import Components
from pyramid.interfaces import (
IChameleonLookup,
IChameleonTranslate,
+ IJSONAdapter,
IRendererGlobalsFactory,
IRendererFactory,
IResponseFactory,
@@ -157,6 +162,8 @@ def string_renderer_factory(info):
return value
return _render
+_marker = object()
+
class JSON(object):
""" Renderer that returns a JSON-encoded string.
@@ -183,27 +190,53 @@ class JSON(object):
def myview(request):
return {'greeting':'Hello world'}
+ Custom objects can be serialized using the renderer by either
+ implementing the ``__json__`` magic method, or by registering
+ adapters with the renderer. See
+ :ref:`json_serializing_custom_objects` for more information.
+
+ The default serializer uses ``json.JSONEncoder``. A different
+ serializer can be specified via the ``serializer`` argument.
+ Custom serializers should accept the object, a callback
+ ``default``, and any extra ``kw`` keyword argments passed during
+ renderer construction.
+
.. note::
This feature is new in Pyramid 1.4. Prior to 1.4 there was
no public API for supplying options to the underlying
- :func:`json.dumps` without defining a custom renderer.
-
- You can pass a ``default`` argument to this class' constructor (which
- should be a function) to customize what happens when it attempts to
- serialize types unrecognized by the base ``json`` module. See
- :ref:`json_serializing_custom_objects` for more information.
+ serializer without defining a custom renderer.
"""
- def __init__(self, **kw):
- """ Any keyword arguments will be passed to the ``json.dumps``
- function. A notable exception is the keyword argument ``default``,
- which is wrapped in a function that sniffs for ``__json__``
- attributes before it is passed along to ``json.dumps``"""
- # we wrap the default callback with our own to get __json__ attr
- # sniffing
- self._default = kw.pop('default', None)
+ def __init__(self, serializer=json.dumps, adapters=(), **kw):
+ """ Any keyword arguments will be passed to the ``serializer``
+ function."""
+ self.serializer = serializer
self.kw = kw
+ self.components = Components()
+ for type, adapter in adapters:
+ self.add_adapter(type, adapter)
+
+ def add_adapter(self, type_or_iface, adapter):
+ """ When an object of type (or interface) ``type_or_iface`` fails to
+ automatically encode using the serializer, the renderer will use the
+ adapter ``adapter`` to convert it into a JSON-serializable object.
+ The adapter must accept two arguments: the object and the currently
+ active request.
+
+ .. code-block:: python
+
+ class Foo(object):
+ x = 5
+
+ def foo_adapter(obj, request):
+ return obj.x
+
+ renderer = JSON(indent=4)
+ renderer.add_adapter(Foo, foo_adapter)
+ """
+ self.components.registerAdapter(adapter, (type_or_iface,),
+ IJSONAdapter)
def __call__(self, info):
""" Returns a plain JSON-encoded string with content-type
@@ -216,22 +249,21 @@ class JSON(object):
ct = response.content_type
if ct == response.default_content_type:
response.content_type = 'application/json'
- return self._dumps(value)
- return _render
- def _default_encode(self, obj):
- if hasattr(obj, '__json__'):
- return obj.__json__()
-
- if self._default is not None:
- return self._default(obj)
- raise TypeError('%r is not JSON serializable' % (obj,))
-
- def _dumps(self, obj):
- """ Encode a Python object to a JSON string.
-
- By default, this uses the :func:`json.dumps` from the stdlib."""
- return json.dumps(obj, default=self._default_encode, **self.kw)
+ def default(obj):
+ if hasattr(obj, '__json__'):
+ return obj.__json__(request)
+ obj_iface = providedBy(obj)
+ adapters = self.components.adapters
+ result = adapters.lookup((obj_iface,), IJSONAdapter,
+ default=_marker)
+ if result is _marker:
+ raise TypeError('%r is not JSON serializable' % (obj,))
+ return result(obj, request)
+
+ return self.serializer(value, default=default, **self.kw)
+
+ return _render
json_renderer_factory = JSON() # bw compat
@@ -307,7 +339,18 @@ class JSONP(JSON):
plain-JSON encoded string with content-type ``application/json``"""
def _render(value, system):
request = system['request']
- val = self._dumps(value)
+
+ def default(obj):
+ if hasattr(obj, '__json__'):
+ return obj.__json__(request)
+
+ result = self.components.queryAdapter(obj, IJSONAdapter,
+ default=_marker)
+ if result is not _marker:
+ return result
+ raise TypeError('%r is not JSON serializable' % (obj,))
+
+ val = self.serializer(value, default=default, **self.kw)
callback = request.GET.get(self.param_name)
if callback is None:
ct = 'application/json'
diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py
index 55ed3f7fd..495d7dc23 100644
--- a/pyramid/tests/test_renderers.py
+++ b/pyramid/tests/test_renderers.py
@@ -369,28 +369,60 @@ class TestJSON(unittest.TestCase):
renderer({'a':1}, {'request':request})
self.assertEqual(request.response.content_type, 'text/mishmash')
- def test_with_custom_encoder(self):
+ def test_with_custom_adapter(self):
+ request = testing.DummyRequest()
+ from datetime import datetime
+ def adapter(obj, req):
+ self.assertEqual(req, request)
+ return obj.isoformat()
+ now = datetime.utcnow()
+ renderer = self._makeOne()
+ renderer.add_adapter(datetime, adapter)
+ result = renderer(None)({'a':now}, {'request':request})
+ self.assertEqual(result, '{"a": "%s"}' % now.isoformat())
+
+ def test_with_custom_adapter2(self):
+ request = testing.DummyRequest()
from datetime import datetime
- def default(obj):
+ def adapter(obj, req):
+ self.assertEqual(req, request)
return obj.isoformat()
now = datetime.utcnow()
- renderer = self._makeOne(default=default)(None)
- result = renderer({'a':now}, {})
+ renderer = self._makeOne(adapters=((datetime, adapter),))
+ result = renderer(None)({'a':now}, {'request':request})
self.assertEqual(result, '{"a": "%s"}' % now.isoformat())
- def test_with_object_encoder(self):
+ def test_with_custom_serializer(self):
+ class Serializer(object):
+ def __call__(self, obj, **kw):
+ self.obj = obj
+ self.kw = kw
+ return 'foo'
+ serializer = Serializer()
+ renderer = self._makeOne(serializer=serializer, baz=5)
+ obj = {'a':'b'}
+ result = renderer(None)(obj, {})
+ self.assertEqual(result, 'foo')
+ self.assertEqual(serializer.obj, obj)
+ self.assertEqual(serializer.kw['baz'], 5)
+ self.assertTrue('default' in serializer.kw)
+
+ def test_with_object_adapter(self):
+ request = testing.DummyRequest()
+ outerself = self
class MyObject(object):
def __init__(self, x):
self.x = x
- def __json__(self):
+ def __json__(self, req):
+ outerself.assertEqual(req, request)
return {'x': self.x}
objects = [MyObject(1), MyObject(2)]
renderer = self._makeOne()(None)
- result = renderer(objects, {})
+ result = renderer(objects, {'request':request})
self.assertEqual(result, '[{"x": 1}, {"x": 2}]')
- def test_with_object_encoder_no___json__(self):
+ def test_with_object_adapter_no___json__(self):
class MyObject(object):
def __init__(self, x):
self.x = x