diff options
| author | Chris McDonough <chrism@plope.com> | 2012-03-29 04:22:32 -0400 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2012-03-29 04:22:32 -0400 |
| commit | c24be2c07cad921c411d105a17a0d6cf9583b7df (patch) | |
| tree | 226eca892311341011e5cdcaf5e4861e65e9d6ae | |
| parent | 4321a443da2443e3097af254f285a50d81449b0f (diff) | |
| parent | de797c4cefb03f16cfe3505c85d94c0af24eb066 (diff) | |
| download | pyramid-c24be2c07cad921c411d105a17a0d6cf9583b7df.tar.gz pyramid-c24be2c07cad921c411d105a17a0d6cf9583b7df.tar.bz2 pyramid-c24be2c07cad921c411d105a17a0d6cf9583b7df.zip | |
Merge branch 'wwitzel3-json-api'
| -rw-r--r-- | CHANGES.txt | 21 | ||||
| -rw-r--r-- | CONTRIBUTORS.txt | 2 | ||||
| -rw-r--r-- | docs/api/renderers.rst | 4 | ||||
| -rw-r--r-- | docs/narr/renderers.rst | 63 | ||||
| -rw-r--r-- | pyramid/renderers.py | 128 | ||||
| -rw-r--r-- | pyramid/tests/test_renderers.py | 47 |
6 files changed, 222 insertions, 43 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 859dc7b74..0714f6940 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,22 +1,13 @@ Next release ============ -Bug Fixes ---------- +Features +-------- -- Add ``REMOTE_ADDR`` to the ``prequest`` WSGI environ dict for benefit of - the debug toolbar, which effectively requires it to be present to work - properly. - -- When an asset specification was used as a Mako ``renderer=`` argument and a - ``mako.modules_directory`` was specified, Pyramid would fail to render the - template and instead would raise an error when attempting to write the file - to the modules directory. Example symptom: ``WindowsError: [Error 267] The - directory name is invalid: - 'c:\\docume~1\\chrism\\locals~1\\temp\\tmp9jtjix\\pyramid.tests:fixtures'``. - We now replace the colon in the Mako module filename with a dollar sign, so - it can work on Windows. See https://github.com/Pylons/pyramid/issues/512 - for more information. +- 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). 1.3 (2012-03-21) ================ diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index a402d49e6..c00170d09 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -166,3 +166,5 @@ Contributors - Paul M. Winkler, 2012/02/22 - Martijn Pieters, 2012/03/02 + +- Steve Piercy, 2012/03/27 diff --git a/docs/api/renderers.rst b/docs/api/renderers.rst index 312aa0b31..ab182365e 100644 --- a/docs/api/renderers.rst +++ b/docs/api/renderers.rst @@ -11,8 +11,12 @@ .. autofunction:: render_to_response +.. autoclass:: JSON + .. autoclass:: JSONP +.. autoclass:: ObjectJSONEncoder + .. attribute:: null_renderer An object that can be used in advanced integration cases as input to the diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index 76035cbdf..34bee3c7f 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -177,8 +177,8 @@ using the API of the ``request.response`` attribute. See .. index:: pair: renderer; JSON -``json``: JSON Renderer -~~~~~~~~~~~~~~~~~~~~~~~ +JSON Renderer +~~~~~~~~~~~~~ The ``json`` renderer renders view callable results to :term:`JSON`. It passes the return value through the ``json.dumps`` standard library function, @@ -207,7 +207,13 @@ 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 :func:`json.dumps`. +values serializable by ``json.dumps``. + +.. note:: + + Extra arguments can be passed to ``json.dumps`` by overriding the default + ``json`` renderer. See :class:`pyramid.renderers.JSON` and + :ref:`adding_and_overriding_renderers` for more information. You can configure a view to use the JSON renderer by naming ``json`` as the ``renderer`` argument of a view configuration, e.g. by using @@ -221,18 +227,61 @@ You can configure a view to use the JSON renderer by naming ``json`` as the context='myproject.resources.Hello', renderer='json') - Views which use the JSON renderer can vary non-body response attributes by using the api of the ``request.response`` attribute. See :ref:`request_response_attr`. +.. _json_serializing_custom_objects: + +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). + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + + class MyObject(object): + def __init__(self, x): + self.x = x + + def __json__(self): + return {'x':self.x} + + @view_config(renderer='json') + def objects(request): + return [MyObject(1), MyObject(2)] + + # the JSON value returned by ``objects`` will be: + # [{"x": 1}, {"x": 2}] + +.. note:: + + Honoring the ``__json__`` method of custom objects is a feature new in + Pyramid 1.4. + +.. warning:: + + The machinery which performs the ``__json__`` method-calling magic is in + the :class:`pyramid.renderers.ObjectJSONEncoder` class. This class will + be used for encoding any non-basic Python object when you use the default + ```json`` or ``jsonp`` renderers. But if you later define your own custom + JSON renderer and pass it a "cls" argument signifying a different encoder, + the encoder you pass will override Pyramid's use of + :class:`pyramid.renderers.ObjectJSONEncoder`. + .. index:: pair: renderer; JSONP .. _jsonp_renderer: JSONP Renderer --------------- +~~~~~~~~~~~~~~ .. note:: This feature is new in Pyramid 1.1. @@ -297,6 +346,10 @@ The string ``callback=?`` above in the the ``url`` param to the JQuery a JSONP request; the ``callback`` parameter will be automatically filled in for you and used. +The same custom-object serialization scheme defined used for a "normal" JSON +renderer in :ref:`json_serializing_custom_objects` can be used when passing +values to a JSONP renderer too. + .. index:: pair: renderer; chameleon diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 14941c61a..0adadf726 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -144,17 +144,6 @@ def get_renderer(renderer_name, package=None): # concrete renderer factory implementations (also API) -def json_renderer_factory(info): - def _render(value, system): - request = system.get('request') - if request is not None: - response = request.response - ct = response.content_type - if ct == response.default_content_type: - response.content_type = 'application/json' - return json.dumps(value) - return _render - def string_renderer_factory(info): def _render(value, system): if not isinstance(value, string_types): @@ -168,7 +157,101 @@ def string_renderer_factory(info): return value return _render -class JSONP(object): +class ObjectJSONEncoder(json.JSONEncoder): + """ The default JSON object encoder (a subclass of json.Encoder) used by + :class:`pyramid.renderers.JSON` and :class:`pyramid.renderers.JSONP`. It + is used when an object returned from a view and presented to a JSON-based + renderer is not a builtin Python type otherwise serializable to JSON. + + This ``json.Encoder`` subclass overrides the ``json.Encoder.default`` + method. The overridden method looks for a ``__json__`` attribute on the + object it is passed. If it's found, the encoder will assume it's + callable, and will call it with no arguments to obtain a value. The + overridden ``default`` method will then return that value (which must be + a JSON-serializable basic Python type). + + If the object passed to the overridden ``default`` method has no + ``__json__`` attribute, the ``json.JSONEncoder.default`` method is called + with the object that it was passed (which will end up raising a + :exc:`TypeError`, as it would with any other unserializable type). + + This class will be used only when you set a JSON or JSONP + renderer and you do not define your own custom encoder class. + + .. note:: This feature is new in Pyramid 1.4. + """ + + def default(self, obj): + if hasattr(obj, '__json__'): + return obj.__json__() + return json.JSONEncoder.default(self, obj) + +class JSON(object): + """ Renderer that returns a JSON-encoded string. + + Configure a custom JSON renderer using the + :meth:`pyramid.config.Configurator.add_renderer` API at application + startup time: + + .. code-block:: python + + from pyramid.config import Configurator + + config = Configurator() + config.add_renderer('myjson', JSON(indent=4, cls=MyJSONEncoder)) + + Once this renderer is registered via + :meth:`~pyramid.config.Configurator.add_renderer` as above, you can use + ``myjson`` as the ``renderer=`` parameter to ``@view_config`` or + :meth:`pyramid.config.Configurator.add_view``: + + .. code-block:: python + + from pyramid.view import view_config + + @view_config(renderer='myjson') + def myview(request): + return {'greeting':'Hello world'} + + .. 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. + + """ + + def __init__(self, **kw): + """ Any keyword arguments will be forwarded to + :func:`json.dumps`. + """ + self.kw = kw + + def __call__(self, info): + """ Returns a plain JSON-encoded string with content-type + ``application/json``. The content-type may be overridden by + setting ``request.response.content_type``.""" + def _render(value, system): + request = system.get('request') + if request is not None: + response = request.response + ct = response.content_type + if ct == response.default_content_type: + response.content_type = 'application/json' + return self.value_to_json(value) + return _render + + def value_to_json(self, value): + """ Convert a Python object to a JSON string. + + By default, this uses the :func:`json.dumps` from the stdlib.""" + if not self.kw.get('cls'): + self.kw['cls'] = ObjectJSONEncoder + return json.dumps(value, **self.kw) + +json_renderer_factory = JSON() # bw compat + +class JSONP(JSON): """ `JSONP <http://en.wikipedia.org/wiki/JSONP>`_ renderer factory helper which implements a hybrid json/jsonp renderer. JSONP is useful for making cross-domain AJAX requests. @@ -184,6 +267,20 @@ class JSONP(object): config = Configurator() config.add_renderer('jsonp', JSONP(param_name='callback')) + The class also accepts arbitrary keyword arguments; all keyword arguments + except ``param_name`` are passed to the ``json.dumps`` function as + keyword arguments: + + .. code-block:: python + + from pyramid.config import Configurator + + config = Configurator() + config.add_renderer('jsonp', JSONP(param_name='callback', indent=4)) + + .. note:: The ability of this class to accept a ``**kw`` in its + constructor is new as of Pyramid 1.4. + Once this renderer is registered via :meth:`~pyramid.config.Configurator.add_renderer` as above, you can use ``jsonp`` as the ``renderer=`` parameter to ``@view_config`` or @@ -210,9 +307,10 @@ class JSONP(object): See also: :ref:`jsonp_renderer`. """ - - def __init__(self, param_name='callback'): + + def __init__(self, param_name='callback', **kw): self.param_name = param_name + JSON.__init__(self, **kw) def __call__(self, info): """ Returns JSONP-encoded string with content-type @@ -221,7 +319,7 @@ class JSONP(object): plain-JSON encoded string with content-type ``application/json``""" def _render(value, system): request = system['request'] - val = json.dumps(value) + val = self.value_to_json(value) 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 b32e68e25..f03c7acda 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -340,35 +340,66 @@ class TestChameleonRendererLookup(unittest.TestCase): self.assertNotEqual(reg.queryUtility(ITemplateRenderer, name=spec), None) -class Test_json_renderer_factory(unittest.TestCase): +class TestJSON(unittest.TestCase): def setUp(self): self.config = testing.setUp() def tearDown(self): testing.tearDown() - - def _callFUT(self, name): - from pyramid.renderers import json_renderer_factory - return json_renderer_factory(name) + + def _makeOne(self, **kw): + from pyramid.renderers import JSON + return JSON(**kw) def test_it(self): - renderer = self._callFUT(None) + renderer = self._makeOne()(None) result = renderer({'a':1}, {}) self.assertEqual(result, '{"a": 1}') def test_with_request_content_type_notset(self): request = testing.DummyRequest() - renderer = self._callFUT(None) + renderer = self._makeOne()(None) renderer({'a':1}, {'request':request}) self.assertEqual(request.response.content_type, 'application/json') def test_with_request_content_type_set(self): request = testing.DummyRequest() request.response.content_type = 'text/mishmash' - renderer = self._callFUT(None) + renderer = self._makeOne()(None) renderer({'a':1}, {'request':request}) self.assertEqual(request.response.content_type, 'text/mishmash') + def test_with_custom_encoder(self): + from datetime import datetime + from json import JSONEncoder + class MyEncoder(JSONEncoder): + def default(self, obj): + return obj.isoformat() + now = datetime.utcnow() + renderer = self._makeOne(cls=MyEncoder)(None) + result = renderer({'a':now}, {}) + self.assertEqual(result, '{"a": "%s"}' % now.isoformat()) + + def test_with_object_encoder(self): + class MyObject(object): + def __init__(self, x): + self.x = x + def __json__(self): + return {'x': self.x} + + objects = [MyObject(1), MyObject(2)] + renderer = self._makeOne()(None) + result = renderer(objects, {}) + self.assertEqual(result, '[{"x": 1}, {"x": 2}]') + + def test_with_object_encoder_no___json__(self): + class MyObject(object): + def __init__(self, x): + self.x = x + objects = [MyObject(1), MyObject(2)] + renderer = self._makeOne()(None) + self.assertRaises(TypeError, renderer, objects, {}) + class Test_string_renderer_factory(unittest.TestCase): def _callFUT(self, name): from pyramid.renderers import string_renderer_factory |
