diff options
| author | Michael Merickel <michael@merickel.org> | 2012-04-18 00:50:10 -0500 |
|---|---|---|
| committer | Michael Merickel <michael@merickel.org> | 2012-04-18 00:50:10 -0500 |
| commit | 677216d2c4ddc5f0df857b8f9e8fa6ccfd5fd55a (patch) | |
| tree | 22f5fa97aeaf3583d07bc596159aee670343857d | |
| parent | c9ec6bd5130642095d77e66e672734296c6a513e (diff) | |
| download | pyramid-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.rst | 32 | ||||
| -rw-r--r-- | pyramid/interfaces.py | 6 | ||||
| -rw-r--r-- | pyramid/renderers.py | 82 | ||||
| -rw-r--r-- | pyramid/tests/test_renderers.py | 37 |
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 |
