summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Merickel <michael@merickel.org>2012-04-18 00:50:10 -0500
committerMichael Merickel <michael@merickel.org>2012-04-18 00:50:10 -0500
commit677216d2c4ddc5f0df857b8f9e8fa6ccfd5fd55a (patch)
tree22f5fa97aeaf3583d07bc596159aee670343857d
parentc9ec6bd5130642095d77e66e672734296c6a513e (diff)
downloadpyramid-677216d2c4ddc5f0df857b8f9e8fa6ccfd5fd55a.tar.gz
pyramid-677216d2c4ddc5f0df857b8f9e8fa6ccfd5fd55a.tar.bz2
pyramid-677216d2c4ddc5f0df857b8f9e8fa6ccfd5fd55a.zip
reverted back to using a component registry during json encoding
-rw-r--r--docs/narr/renderers.rst32
-rw-r--r--pyramid/interfaces.py6
-rw-r--r--pyramid/renderers.py82
-rw-r--r--pyramid/tests/test_renderers.py37
4 files changed, 108 insertions, 49 deletions
diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst
index 02063a112..c36caeb87 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,8 @@ 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).
.. code-block:: python
:linenos:
@@ -267,22 +267,18 @@ 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. It will raise a :exc:`TypeError` if it
+can't determine what to do with the object. 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()
+ json_renderer.add_adapter(datetime.datetime, lambda x: x.isoformat())
# then during configuration ....
config = Configurator()
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..efd7cdf42 100644
--- a/pyramid/renderers.py
+++ b/pyramid/renderers.py
@@ -4,10 +4,12 @@ import pkg_resources
import threading
from zope.interface import implementer
+from zope.interface.registry import Components
from pyramid.interfaces import (
IChameleonLookup,
IChameleonTranslate,
+ IJSONAdapter,
IRendererGlobalsFactory,
IRendererFactory,
IResponseFactory,
@@ -157,6 +159,8 @@ def string_renderer_factory(info):
return value
return _render
+_marker = object()
+
class JSON(object):
""" Renderer that returns a JSON-encoded string.
@@ -183,27 +187,52 @@ 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.
+
+ .. code-block:: python
+
+ class Foo(object):
+ x = 5
+
+ def foo_adapter(obj):
+ return obj.x
+
+ renderer = JSON(indent=4)
+ renderer.adapt(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,23 +245,26 @@ class JSON(object):
ct = response.content_type
if ct == response.default_content_type:
response.content_type = 'application/json'
- return self._dumps(value)
+ return self.serializer(value, default=self.default, **self.kw)
return _render
- def _default_encode(self, obj):
+ def default(self, obj):
+ """ Returns a JSON-serializable representation of ``obj``. If
+ no representation can be found, a ``TypeError`` is raised.
+
+ If the object implements the ``__json__`` magic method, it will
+ be preferred. Otherwise, attempt to adapt ``obj`` into a
+ serializable type using one of the registered adapters.
+ """
if hasattr(obj, '__json__'):
return obj.__json__()
- if self._default is not None:
- return self._default(obj)
+ result = self.components.queryAdapter(obj, IJSONAdapter,
+ default=_marker)
+ if result is not _marker:
+ return result
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)
-
json_renderer_factory = JSON() # bw compat
class JSONP(JSON):
@@ -307,7 +339,7 @@ class JSONP(JSON):
plain-JSON encoded string with content-type ``application/json``"""
def _render(value, system):
request = system['request']
- val = self._dumps(value)
+ val = self.serializer(value, default=self.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..55c5c2f5a 100644
--- a/pyramid/tests/test_renderers.py
+++ b/pyramid/tests/test_renderers.py
@@ -369,16 +369,41 @@ 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):
from datetime import datetime
- def default(obj):
+ def adapter(obj):
return obj.isoformat()
now = datetime.utcnow()
- renderer = self._makeOne(default=default)(None)
- result = renderer({'a':now}, {})
+ renderer = self._makeOne()
+ renderer.add_adapter(datetime, adapter)
+ result = renderer(None)({'a':now}, {})
self.assertEqual(result, '{"a": "%s"}' % now.isoformat())
- def test_with_object_encoder(self):
+ def test_with_custom_adapter2(self):
+ from datetime import datetime
+ def adapter(obj):
+ return obj.isoformat()
+ now = datetime.utcnow()
+ renderer = self._makeOne(adapters=((datetime, adapter),))
+ result = renderer(None)({'a':now}, {})
+ self.assertEqual(result, '{"a": "%s"}' % now.isoformat())
+
+ 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):
class MyObject(object):
def __init__(self, x):
self.x = x
@@ -390,7 +415,7 @@ class TestJSON(unittest.TestCase):
result = renderer(objects, {})
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