summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2012-03-29 04:22:32 -0400
committerChris McDonough <chrism@plope.com>2012-03-29 04:22:32 -0400
commitc24be2c07cad921c411d105a17a0d6cf9583b7df (patch)
tree226eca892311341011e5cdcaf5e4861e65e9d6ae
parent4321a443da2443e3097af254f285a50d81449b0f (diff)
parentde797c4cefb03f16cfe3505c85d94c0af24eb066 (diff)
downloadpyramid-c24be2c07cad921c411d105a17a0d6cf9583b7df.tar.gz
pyramid-c24be2c07cad921c411d105a17a0d6cf9583b7df.tar.bz2
pyramid-c24be2c07cad921c411d105a17a0d6cf9583b7df.zip
Merge branch 'wwitzel3-json-api'
-rw-r--r--CHANGES.txt21
-rw-r--r--CONTRIBUTORS.txt2
-rw-r--r--docs/api/renderers.rst4
-rw-r--r--docs/narr/renderers.rst63
-rw-r--r--pyramid/renderers.py128
-rw-r--r--pyramid/tests/test_renderers.py47
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