diff options
| author | Chris McDonough <chrism@plope.com> | 2012-05-03 02:43:08 -0400 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2012-05-03 02:43:08 -0400 |
| commit | ad42cef589f37b998e804e780e92e52544ecfb16 (patch) | |
| tree | ec5f73051a34d4835442c82c115fb91bb452d0ef | |
| parent | 004882434aa166a58c3b2148322e08ce61ec4cb7 (diff) | |
| parent | e012aa12760f6c29bfc9967c50a51d3f47db47da (diff) | |
| download | pyramid-ad42cef589f37b998e804e780e92e52544ecfb16.tar.gz pyramid-ad42cef589f37b998e804e780e92e52544ecfb16.tar.bz2 pyramid-ad42cef589f37b998e804e780e92e52544ecfb16.zip | |
Merge branch 'mmerickel-feature.json-api'
| -rw-r--r-- | CHANGES.txt | 3 | ||||
| -rw-r--r-- | docs/narr/renderers.rst | 41 | ||||
| -rw-r--r-- | pyramid/interfaces.py | 6 | ||||
| -rw-r--r-- | pyramid/renderers.py | 105 | ||||
| -rw-r--r-- | pyramid/tests/test_renderers.py | 48 |
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 |
