Skip to content

Commit

Permalink
Added wrapper module around simplejson/json for much simplified custo…
Browse files Browse the repository at this point in the history
…mization.
  • Loading branch information
mitsuhiko committed Oct 7, 2012
1 parent 301e244 commit b146d82
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 81 deletions.
3 changes: 3 additions & 0 deletions CHANGES
Expand Up @@ -17,6 +17,9 @@ Release date to be decided.
- ``tojson`` filter now does not escape script blocks in HTML5 parsers. - ``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 - Flask will now raise an error if you attempt to register a new function
on an already used endpoint. 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 Version 0.9
----------- -----------
Expand Down
72 changes: 50 additions & 22 deletions docs/api.rst
Expand Up @@ -312,43 +312,71 @@ Message Flashing


.. autofunction:: get_flashed_messages .. 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

This comment has been minimized.

Copy link
@yuvallanger

yuvallanger Dec 9, 2014

Something about the wording here sounds weird. Could anyone fix this?

This comment has been minimized.

Copy link
@untitaker

untitaker via email Dec 9, 2014

Contributor
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 You can instead just do this::
using to parse and serialize JSON. So instead of doing this yourself::


try: from flask import json
import simplejson as json
except ImportError:
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 .. sourcecode:: html+jinja
as filter called ``|tojson`` in Jinja2. Note that inside `script`
tags no escaping must take place, so make sure to disable escaping <script type=text/javascript>
with ``|safe`` if you intend to use it inside `script` tags: 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> .. autofunction:: dump
doSomethingWith({{ user.username|tojson|safe }});
</script>


Note that the ``|tojson`` filter escapes forward slashes properly. .. autofunction:: loads

.. autofunction:: load

.. autoclass:: JSONEncoder
:members:

.. autoclass:: JSONDecoder
:members:


Template Rendering Template Rendering
------------------ ------------------


.. currentmodule:: flask

.. autofunction:: render_template .. autofunction:: render_template


.. autofunction:: render_template_string .. autofunction:: render_template_string
Expand Down
14 changes: 9 additions & 5 deletions flask/__init__.py
Expand Up @@ -20,9 +20,8 @@


from .app import Flask, Request, Response from .app import Flask, Request, Response
from .config import Config from .config import Config
from .helpers import url_for, jsonify, flash, \ from .helpers import url_for, flash, send_file, send_from_directory, \
send_file, send_from_directory, get_flashed_messages, \ get_flashed_messages, get_template_attribute, make_response, safe_join, \
get_template_attribute, make_response, safe_join, \
stream_with_context stream_with_context
from .globals import current_app, g, request, session, _request_ctx_stack, \ from .globals import current_app, g, request, session, _request_ctx_stack, \
_app_ctx_stack _app_ctx_stack
Expand All @@ -37,8 +36,13 @@
request_finished, got_request_exception, request_tearing_down, \ request_finished, got_request_exception, request_tearing_down, \
appcontext_tearing_down appcontext_tearing_down


# only import json if it's available # We're not exposing the actual json module but a convenient wrapper around
from .helpers import json # 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 # backwards compat, goes away in 1.0
from .sessions import SecureCookieSession as Session from .sessions import SecureCookieSession as Session
Expand Down
16 changes: 13 additions & 3 deletions flask/app.py
Expand Up @@ -24,8 +24,8 @@
MethodNotAllowed, BadRequest MethodNotAllowed, BadRequest


from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \
locked_cached_property, _tojson_filter, _endpoint_from_view_func, \ locked_cached_property, _endpoint_from_view_func, find_package
find_package from . import json
from .wrappers import Request, Response from .wrappers import Request, Response
from .config import ConfigAttribute, Config from .config import ConfigAttribute, Config
from .ctx import RequestContext, AppContext, _RequestGlobals from .ctx import RequestContext, AppContext, _RequestGlobals
Expand Down Expand Up @@ -238,6 +238,16 @@ class Flask(_PackageBoundObject):
'-' * 80 '-' * 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. #: Options that are passed directly to the Jinja2 environment.
jinja_options = ImmutableDict( jinja_options = ImmutableDict(
extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_'] extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_']
Expand Down Expand Up @@ -637,7 +647,7 @@ def create_jinja_environment(self):
url_for=url_for, url_for=url_for,
get_flashed_messages=get_flashed_messages get_flashed_messages=get_flashed_messages
) )
rv.filters['tojson'] = _tojson_filter rv.filters['tojson'] = json.htmlsafe_dumps
return rv return rv


def create_global_jinja_loader(self): def create_global_jinja_loader(self):
Expand Down
3 changes: 1 addition & 2 deletions flask/exceptions.py
Expand Up @@ -9,7 +9,7 @@
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.
""" """
from werkzeug.exceptions import HTTPException, BadRequest from werkzeug.exceptions import HTTPException, BadRequest
from .helpers import json from . import json




class JSONHTTPException(HTTPException): class JSONHTTPException(HTTPException):
Expand Down Expand Up @@ -39,7 +39,6 @@ def get_headers(self, environ):
class JSONBadRequest(JSONHTTPException, BadRequest): class JSONBadRequest(JSONHTTPException, BadRequest):
"""Represents an HTTP ``400 Bad Request`` error whose body contains an """Represents an HTTP ``400 Bad Request`` error whose body contains an
error message in JSON format instead of HTML format (as in the superclass). error message in JSON format instead of HTML format (as in the superclass).
""" """


#: The description of the error which occurred as a string. #: The description of the error which occurred as a string.
Expand Down
46 changes: 0 additions & 46 deletions flask/helpers.py
Expand Up @@ -23,10 +23,6 @@
from werkzeug.urls import url_quote from werkzeug.urls import url_quote
from functools import update_wrapper 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.datastructures import Headers
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
Expand All @@ -43,17 +39,6 @@
current_app, request 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 # sentinel
_missing = object() _missing = object()


Expand Down Expand Up @@ -146,37 +131,6 @@ def generator():
return wrapped_g 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): def make_response(*args):
"""Sometimes it is necessary to set additional headers in a view. Because """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 views do not have to return response objects but can return a value that
Expand Down
146 changes: 146 additions & 0 deletions flask/json.py
@@ -0,0 +1,146 @@
# -*- 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')

0 comments on commit b146d82

Please sign in to comment.