From 34a68691ae5291b9a45bcf4b19f8ea5c4edcf3a1 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Tue, 27 Mar 2012 23:55:23 -0700 Subject: signed contributors agreement --- CONTRIBUTORS.txt | 2 ++ 1 file changed, 2 insertions(+) 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 -- cgit v1.2.3 From d81ea33ac67ac750053acbfd12616db0130de3c8 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 10 Jan 2012 22:52:24 -0600 Subject: intermediate commit --- docs/api/renderers.rst | 2 ++ docs/narr/renderers.rst | 10 +++--- pyramid/renderers.py | 80 +++++++++++++++++++++++++++++++++-------- pyramid/tests/test_renderers.py | 29 ++++++++++----- 4 files changed, 94 insertions(+), 27 deletions(-) diff --git a/docs/api/renderers.rst b/docs/api/renderers.rst index 312aa0b31..ea000ad02 100644 --- a/docs/api/renderers.rst +++ b/docs/api/renderers.rst @@ -11,6 +11,8 @@ .. autofunction:: render_to_response +.. autoclass:: JSON + .. autoclass:: JSONP .. attribute:: null_renderer diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index 76035cbdf..47182c09e 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,10 @@ 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 :func:`json.dumps`. Extra arguments can be passed +to :func:`json.dumps` by overriding the default 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,7 +224,6 @@ 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`. diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 14941c61a..401d1fdd4 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,67 @@ def string_renderer_factory(info): return value return _render -class JSONP(object): +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.3. Prior to 1.3 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.""" + return json.dumps(value, **self.kw) + +json_renderer_factory = JSON() # bw compat + +class JSONP(JSON): """ `JSONP `_ renderer factory helper which implements a hybrid json/jsonp renderer. JSONP is useful for making cross-domain AJAX requests. @@ -210,9 +259,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 +271,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..c2450b875 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -340,35 +340,48 @@ 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): + if isinstance(obj, datetime): + return obj.isoformat() + return super(JSONEncoder, self).default(obj) + now = datetime.utcnow() + renderer = self._makeOne(cls=MyEncoder)(None) + result = renderer({'a':now}, {}) + self.assertEqual(result, '{"a": "%s"}' % now.isoformat()) + class Test_string_renderer_factory(unittest.TestCase): def _callFUT(self, name): from pyramid.renderers import string_renderer_factory -- cgit v1.2.3 From ba60524b56a639ecad42f85b63af2120d9d96cdc Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Wed, 28 Mar 2012 12:03:52 -0400 Subject: JSON-API rework and Object.__json__ support --- docs/api/renderers.rst | 2 ++ docs/narr/renderers.rst | 7 +++++++ pyramid/renderers.py | 21 +++++++++++++++++++++ pyramid/tests/test_renderers.py | 12 ++++++++++++ 4 files changed, 42 insertions(+) diff --git a/docs/api/renderers.rst b/docs/api/renderers.rst index ea000ad02..ab182365e 100644 --- a/docs/api/renderers.rst +++ b/docs/api/renderers.rst @@ -15,6 +15,8 @@ .. 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 47182c09e..52e97d091 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -212,6 +212,13 @@ to :func:`json.dumps` by overriding the default renderer. See :class:`pyramid.renderers.JSON` and :ref:`_adding_and_overriding_renderers` for more information. +Custom objects can be easily serialized by defining a :func:`__json__` method +on the object. This method should return values serializable by +:func:`json_dumps`. By defining this method and using a :term:`JSON` +renderer the :class:`pyramid.renderers.ObjectJSONEncoder` class will be used +for encoding your object. If you later define your own custom encoder it will +override :class:`pyramid.renderers.ObjectJSONEncoder`. + 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 :meth:`~pyramid.config.Configurator.add_view`: diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 401d1fdd4..859fd3953 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -157,6 +157,25 @@ def string_renderer_factory(info): return value return _render +class ObjectJSONEncoder(json.JSONEncoder): + """ Encoder that is used by :class:`pyramid.renderers.JSON` and + :class:`pyramid.renderers.JSONP`. + + This encoder will look for a :meth:`__json__` method on an object + and use the return value when encoding the object to json + using :meth:`json_dumps`. + + 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.3. + """ + + def default(self, obj): + if hasattr(obj, '__json__') and callable(obj.__json__): + return obj.__json__() + return json.JSONEncoder.default(self, obj) + class JSON(object): """ Renderer that returns a JSON-encoded string. @@ -213,6 +232,8 @@ class JSON(object): """ 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 diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py index c2450b875..9d9e608f1 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -382,6 +382,18 @@ class TestJSON(unittest.TestCase): 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}]') + class Test_string_renderer_factory(unittest.TestCase): def _callFUT(self, name): from pyramid.renderers import string_renderer_factory -- cgit v1.2.3 From de797c4cefb03f16cfe3505c85d94c0af24eb066 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 29 Mar 2012 04:21:30 -0400 Subject: - Coverage and docs updates for custom JSON class. - Fork point: master now represents a future 1.4 release. --- CHANGES.txt | 21 ++++--------- docs/narr/renderers.rst | 68 +++++++++++++++++++++++++++++++++-------- pyramid/renderers.py | 47 ++++++++++++++++++++++------ pyramid/tests/test_renderers.py | 12 ++++++-- 4 files changed, 108 insertions(+), 40 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/docs/narr/renderers.rst b/docs/narr/renderers.rst index 52e97d091..34bee3c7f 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -207,17 +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`. Extra arguments can be passed -to :func:`json.dumps` by overriding the default renderer. See -:class:`pyramid.renderers.JSON` and -:ref:`_adding_and_overriding_renderers` for more information. - -Custom objects can be easily serialized by defining a :func:`__json__` method -on the object. This method should return values serializable by -:func:`json_dumps`. By defining this method and using a :term:`JSON` -renderer the :class:`pyramid.renderers.ObjectJSONEncoder` class will be used -for encoding your object. If you later define your own custom encoder it will -override :class:`pyramid.renderers.ObjectJSONEncoder`. +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 @@ -235,13 +231,57 @@ 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. @@ -306,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 859fd3953..0adadf726 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -158,21 +158,31 @@ def string_renderer_factory(info): return _render class ObjectJSONEncoder(json.JSONEncoder): - """ Encoder that is used by :class:`pyramid.renderers.JSON` and - :class:`pyramid.renderers.JSONP`. + """ 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 encoder will look for a :meth:`__json__` method on an object - and use the return value when encoding the object to json - using :meth:`json_dumps`. + 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.3. + .. note:: This feature is new in Pyramid 1.4. """ def default(self, obj): - if hasattr(obj, '__json__') and callable(obj.__json__): + if hasattr(obj, '__json__'): return obj.__json__() return json.JSONEncoder.default(self, obj) @@ -203,9 +213,12 @@ class JSON(object): def myview(request): return {'greeting':'Hello world'} - .. note:: This feature is new in Pyramid 1.3. Prior to 1.3 there was - no public API for supplying options to the underlying - :func:`json.dumps` without defining a custom renderer. + .. 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): @@ -254,6 +267,20 @@ class JSONP(JSON): 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 diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py index 9d9e608f1..f03c7acda 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -374,9 +374,7 @@ class TestJSON(unittest.TestCase): from json import JSONEncoder class MyEncoder(JSONEncoder): def default(self, obj): - if isinstance(obj, datetime): - return obj.isoformat() - return super(JSONEncoder, self).default(obj) + return obj.isoformat() now = datetime.utcnow() renderer = self._makeOne(cls=MyEncoder)(None) result = renderer({'a':now}, {}) @@ -394,6 +392,14 @@ class TestJSON(unittest.TestCase): 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 -- cgit v1.2.3