From b146d8277ab90cf6d43ea54113383076e4fd0318 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 7 Oct 2012 23:31:48 +0200 Subject: [PATCH] Added wrapper module around simplejson/json for much simplified customization. --- CHANGES | 3 + docs/api.rst | 72 +++++++++++++++------- flask/__init__.py | 14 +++-- flask/app.py | 16 ++++- flask/exceptions.py | 3 +- flask/helpers.py | 46 -------------- flask/json.py | 146 ++++++++++++++++++++++++++++++++++++++++++++ flask/sessions.py | 3 +- flask/wrappers.py | 2 +- 9 files changed, 224 insertions(+), 81 deletions(-) create mode 100644 flask/json.py diff --git a/CHANGES b/CHANGES index b79c6814..2a87cbe2 100644 --- a/CHANGES +++ b/CHANGES @@ -17,6 +17,9 @@ Release date to be decided. - ``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 ----------- diff --git a/docs/api.rst b/docs/api.rst index 316c76ab..dbd1877e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -312,43 +312,71 @@ Message Flashing .. 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 + + + +Note that the ``|tojson`` filter escapes forward slashes properly. + +.. autofunction:: jsonify - .. sourcecode:: html+jinja +.. autofunction:: dumps - +.. 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 diff --git a/flask/__init__.py b/flask/__init__.py index fda94f39..6e7883fb 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -20,9 +20,8 @@ from jinja2 import Markup, escape 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 @@ -37,8 +36,13 @@ from .signals import signals_available, template_rendered, request_started, \ 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 diff --git a/flask/app.py b/flask/app.py index 327cb8cf..1763aa74 100644 --- a/flask/app.py +++ b/flask/app.py @@ -24,8 +24,8 @@ from werkzeug.exceptions import HTTPException, InternalServerError, \ 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 @@ -238,6 +238,16 @@ class Flask(_PackageBoundObject): '-' * 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_'] @@ -637,7 +647,7 @@ class Flask(_PackageBoundObject): 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): diff --git a/flask/exceptions.py b/flask/exceptions.py index 9ccdedab..83b9556b 100644 --- a/flask/exceptions.py +++ b/flask/exceptions.py @@ -9,7 +9,7 @@ :license: BSD, see LICENSE for more details. """ from werkzeug.exceptions import HTTPException, BadRequest -from .helpers import json +from . import json class JSONHTTPException(HTTPException): @@ -39,7 +39,6 @@ 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. diff --git a/flask/helpers.py b/flask/helpers.py index 8780ee20..3f06c116 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -23,10 +23,6 @@ from werkzeug.routing import BuildError 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 @@ -43,17 +39,6 @@ from .globals import session, _request_ctx_stack, _app_ctx_stack, \ 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('`` + 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('