Commits

Armin Ronacher committed b146d82

Added wrapper module around simplejson/json for much simplified customization.

  • Participants
  • Parent commits 301e244

Comments (0)

Files changed (9)

 - ``tojson`` filter now does not escape script blocks in HTML5 parsers.
 - Flask will now raise an error if you attempt to register a new function
   on an already used endpoint.
+- Added wrapper module around simplejson and added default serialization
+  of datetime objects.  This allows much easier customization of how
+  JSON is handled by Flask or any Flask extension.
 
 Version 0.9
 -----------

File docs/api.rst

 
 .. autofunction:: get_flashed_messages
 
-Returning JSON
---------------
+JSON Support
+------------
 
-.. autofunction:: jsonify
+.. module:: flask.json
+
+Flask uses ``simplejson`` for the JSON implementation.  Since simplejson
+is provided both by the standard library as well as extension Flask will
+try simplejson first and then fall back to the stdlib json module.  On top
+of that it will delegate access to the current application's JSOn encoders
+and decoders for easier customization.
+
+So for starters instead of doing::
 
-.. data:: json
+    try:
+        import simplejson as json
+    except ImportError:
+        import json
 
-    If JSON support is picked up, this will be the module that Flask is
-    using to parse and serialize JSON.  So instead of doing this yourself::
+You can instead just do this::
 
-        try:
-            import simplejson as json
-        except ImportError:
-            import json
+    from flask import json
 
-    You can instead just do this::
+For usage examples, read the :mod:`json` documentation in the standard
+lirbary.  The following extensions are by default applied to the stdlib's
+JSON module:
 
-        from flask import json
+1.  ``datetime`` objects are serialized as :rfc:`822` strings.
+2.  Any object with an ``__html__`` method (like :class:`~flask.Markup`)
+    will ahve that method called and then the return value is serialized
+    as string.
 
-    For usage examples, read the :mod:`json` documentation.
+The :func:`~htmlsafe_dumps` function of this json module is also available
+as filter called ``|tojson`` in Jinja2.  Note that inside `script`
+tags no escaping must take place, so make sure to disable escaping
+with ``|safe`` if you intend to use it inside `script` tags:
 
-    The :func:`~json.dumps` function of this json module is also available
-    as filter called ``|tojson`` in Jinja2.  Note that inside `script`
-    tags no escaping must take place, so make sure to disable escaping
-    with ``|safe`` if you intend to use it inside `script` tags:
+.. sourcecode:: html+jinja
+
+    <script type=text/javascript>
+        doSomethingWith({{ user.username|tojson|safe }});
+    </script>
+
+Note that the ``|tojson`` filter escapes forward slashes properly.
+
+.. autofunction:: jsonify
 
-    .. sourcecode:: html+jinja
+.. autofunction:: dumps
 
-        <script type=text/javascript>
-            doSomethingWith({{ user.username|tojson|safe }});
-        </script>
+.. autofunction:: dump
 
-    Note that the ``|tojson`` filter escapes forward slashes properly.
+.. autofunction:: loads
+
+.. autofunction:: load
+
+.. autoclass:: JSONEncoder
+   :members:
+
+.. autoclass:: JSONDecoder
+   :members:
 
 Template Rendering
 ------------------
 
+.. currentmodule:: flask
+
 .. autofunction:: render_template
 
 .. autofunction:: render_template_string

File flask/__init__.py

 
 from .app import Flask, Request, Response
 from .config import Config
-from .helpers import url_for, jsonify, flash, \
-    send_file, send_from_directory, get_flashed_messages, \
-    get_template_attribute, make_response, safe_join, \
+from .helpers import url_for, flash, send_file, send_from_directory, \
+    get_flashed_messages, get_template_attribute, make_response, safe_join, \
     stream_with_context
 from .globals import current_app, g, request, session, _request_ctx_stack, \
      _app_ctx_stack
      request_finished, got_request_exception, request_tearing_down, \
      appcontext_tearing_down
 
-# only import json if it's available
-from .helpers import json
+# We're not exposing the actual json module but a convenient wrapper around
+# it.
+from . import json
+
+# This was the only thing that flask used to export at one point and it had
+# a more generic name.
+jsonify = json.jsonify
 
 # backwards compat, goes away in 1.0
 from .sessions import SecureCookieSession as Session

File flask/app.py

      MethodNotAllowed, BadRequest
 
 from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \
-    locked_cached_property, _tojson_filter, _endpoint_from_view_func, \
-    find_package
+    locked_cached_property, _endpoint_from_view_func, find_package
+from . import json
 from .wrappers import Request, Response
 from .config import ConfigAttribute, Config
 from .ctx import RequestContext, AppContext, _RequestGlobals
         '-' * 80
     )
 
+    #: The JSON encoder class to use.  Defaults to :class:`~flask.json.JSONEncoder`.
+    #:
+    #: .. versionadded:: 0.10
+    json_encoder = json.JSONEncoder
+
+    #: The JSON decoder class to use.  Defaults to :class:`~flask.json.JSONDecoder`.
+    #:
+    #: .. versionadded:: 0.10
+    json_decoder = json.JSONDecoder
+
     #: Options that are passed directly to the Jinja2 environment.
     jinja_options = ImmutableDict(
         extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_']
             url_for=url_for,
             get_flashed_messages=get_flashed_messages
         )
-        rv.filters['tojson'] = _tojson_filter
+        rv.filters['tojson'] = json.htmlsafe_dumps
         return rv
 
     def create_global_jinja_loader(self):

File flask/exceptions.py

     :license: BSD, see LICENSE for more details.
 """
 from werkzeug.exceptions import HTTPException, BadRequest
-from .helpers import json
+from . import json
 
 
 class JSONHTTPException(HTTPException):
 class JSONBadRequest(JSONHTTPException, BadRequest):
     """Represents an HTTP ``400 Bad Request`` error whose body contains an
     error message in JSON format instead of HTML format (as in the superclass).
-
     """
 
     #: The description of the error which occurred as a string.

File flask/helpers.py

 from werkzeug.urls import url_quote
 from functools import update_wrapper
 
-# Use the same json implementation as itsdangerous on which we
-# depend anyways.
-from itsdangerous import simplejson as json
-
 
 from werkzeug.datastructures import Headers
 from werkzeug.exceptions import NotFound
      current_app, request
 
 
-# figure out if simplejson escapes slashes.  This behavior was changed
-# from one version to another without reason.
-_slash_escape = '\\/' not in json.dumps('/')
-
-def _tojson_filter(*args, **kwargs):
-    rv = json.dumps(*args, **kwargs)
-    if _slash_escape:
-        rv = rv.replace('/', '\\/')
-    return rv.replace('<!', '<\\u0021')
-
-
 # sentinel
 _missing = object()
 
     return wrapped_g
 
 
-def jsonify(*args, **kwargs):
-    """Creates a :class:`~flask.Response` with the JSON representation of
-    the given arguments with an `application/json` mimetype.  The arguments
-    to this function are the same as to the :class:`dict` constructor.
-
-    Example usage::
-
-        @app.route('/_get_current_user')
-        def get_current_user():
-            return jsonify(username=g.user.username,
-                           email=g.user.email,
-                           id=g.user.id)
-
-    This will send a JSON response like this to the browser::
-
-        {
-            "username": "admin",
-            "email": "admin@localhost",
-            "id": 42
-        }
-
-    This requires Python 2.6 or an installed version of simplejson.  For
-    security reasons only objects are supported toplevel.  For more
-    information about this, have a look at :ref:`json-security`.
-
-    .. versionadded:: 0.2
-    """
-    return current_app.response_class(json.dumps(dict(*args, **kwargs),
-        indent=None if request.is_xhr else 2), mimetype='application/json')
-
-
 def make_response(*args):
     """Sometimes it is necessary to set additional headers in a view.  Because
     views do not have to return response objects but can return a value that

File flask/json.py

+# -*- coding: utf-8 -*-
+"""
+    flask.jsonimpl
+    ~~~~~~~~~~~~~~
+
+    Implementation helpers for the JSON support in Flask.
+
+    :copyright: (c) 2012 by Armin Ronacher.
+    :license: BSD, see LICENSE for more details.
+"""
+from datetime import datetime
+from .globals import current_app, request
+
+from werkzeug.http import http_date
+
+# Use the same json implementation as itsdangerous on which we
+# depend anyways.
+from itsdangerous import simplejson as _json
+
+
+# figure out if simplejson escapes slashes.  This behavior was changed
+# from one version to another without reason.
+_slash_escape = '\\/' not in _json.dumps('/')
+
+
+__all__ = ['dump', 'dumps', 'load', 'loads', 'htmlsafe_dump',
+           'htmlsafe_dumps', 'JSONDecoder', 'JSONEncoder',
+           'jsonify']
+
+
+class JSONEncoder(_json.JSONEncoder):
+    """The default Flask JSON encoder.  This one extends the default simplejson
+    encoder by also supporting ``datetime`` objects as well as ``Markup``
+    objects which are serialized as RFC 822 datetime strings (same as the HTTP
+    date format).  In order to support more data types override the
+    :meth:`default` method.
+    """
+
+    def default(self, o):
+        """Implement this method in a subclass such that it returns a
+        serializable object for ``o``, or calls the base implementation (to
+        raise a ``TypeError``).
+
+        For example, to support arbitrary iterators, you could implement
+        default like this::
+
+            def default(self, o):
+                try:
+                    iterable = iter(o)
+                except TypeError:
+                    pass
+                else:
+                    return list(iterable)
+                return JSONEncoder.default(self, o)
+        """
+        if isinstance(o, datetime):
+            return http_date(o)
+        if hasattr(o, '__html__'):
+            return unicode(o.__html__())
+        return _json.JSONEncoder.default(self, o)
+
+
+class JSONDecoder(_json.JSONDecoder):
+    """The default JSON decoder.  This one does not change the behavior from
+    the default simplejson encoder.  Consult the :mod:`json` documentation
+    for more information.  This decoder is not only used for the load
+    functions of this module but also :attr:`~flask.Request`.
+    """
+
+
+def dumps(obj, **kwargs):
+    """Serialize ``obj`` to a JSON formatted ``str`` by using the application's
+    configured encoder (:attr:`~flask.Flask.json_encoder`).
+    """
+    kwargs.setdefault('cls', current_app.json_encoder)
+    return _json.dumps(obj, **kwargs)
+
+
+def dump(obj, fp, **kwargs):
+    """Like :func:`dumps` but writes into a file object."""
+    kwargs.setdefault('cls', current_app.json_encoder)
+    return _json.dump(obj, fp, **kwargs)
+
+
+def loads(s, **kwargs):
+    """Unserialize a JSON object from a string ``s`` by using the application's
+    configured decoder (:attr:`~flask.Flask.json_decoder`).
+    """
+    kwargs.setdefault('cls', current_app.json_decoder)
+    return _json.loads(s, **kwargs)
+
+
+def load(fp, **kwargs):
+    """Like :func:`loads` but reads from a file object.
+    """
+    kwargs.setdefault('cls', current_app.json_decoder)
+    return _json.load(fp, **kwargs)
+
+
+def htmlsafe_dumps(obj, **kwargs):
+    """Works exactly like :func:`dumps` but is safe for use in ``<script>``
+    tags.  It accepts the same arguments and returns a JSON string.  Note that
+    this is available in templates through the ``|tojson`` filter but it will
+    have to be wrapped in ``|safe`` unless **true** XHTML is being used.
+    """
+    rv = dumps(obj, **kwargs)
+    if _slash_escape:
+        rv = rv.replace('/', '\\/')
+    return rv.replace('<!', '<\\u0021')
+
+
+def htmlsafe_dump(obj, fp, **kwargs):
+    """Like :func:`htmlsafe_dumps` but writes into a file object."""
+    fp.write(htmlsafe_dumps(obj, **kwargs))
+
+
+def jsonify(*args, **kwargs):
+    """Creates a :class:`~flask.Response` with the JSON representation of
+    the given arguments with an `application/json` mimetype.  The arguments
+    to this function are the same as to the :class:`dict` constructor.
+
+    Example usage::
+
+        @app.route('/_get_current_user')
+        def get_current_user():
+            return jsonify(username=g.user.username,
+                           email=g.user.email,
+                           id=g.user.id)
+
+    This will send a JSON response like this to the browser::
+
+        {
+            "username": "admin",
+            "email": "admin@localhost",
+            "id": 42
+        }
+
+    This requires Python 2.6 or an installed version of simplejson.  For
+    security reasons only objects are supported toplevel.  For more
+    information about this, have a look at :ref:`json-security`.
+
+    .. versionadded:: 0.2
+    """
+    return current_app.response_class(dumps(dict(*args, **kwargs),
+        indent=None if request.is_xhr else 2),
+        mimetype='application/json')

File flask/sessions.py

 from datetime import datetime
 from werkzeug.http import http_date, parse_date
 from werkzeug.datastructures import CallbackDict
-from .helpers import json
-from . import Markup
+from . import Markup, json
 
 from itsdangerous import URLSafeTimedSerializer, BadSignature
 

File flask/wrappers.py

 
 from .exceptions import JSONBadRequest
 from .debughelpers import attach_enctype_error_multidict
-from .helpers import json
+from . import json
 from .globals import _request_ctx_stack