Added wrapper module around simplejson/json for much simplified customization.
This commit is contained in:
parent
301e244df3
commit
b146d8277a
9 changed files with 227 additions and 84 deletions
3
CHANGES
3
CHANGES
|
|
@ -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
|
||||||
-----------
|
-----------
|
||||||
|
|
|
||||||
78
docs/api.rst
78
docs/api.rst
|
|
@ -312,43 +312,71 @@ Message Flashing
|
||||||
|
|
||||||
.. autofunction:: get_flashed_messages
|
.. autofunction:: get_flashed_messages
|
||||||
|
|
||||||
Returning JSON
|
JSON Support
|
||||||
--------------
|
------------
|
||||||
|
|
||||||
|
.. 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::
|
||||||
|
|
||||||
|
try:
|
||||||
|
import simplejson as json
|
||||||
|
except ImportError:
|
||||||
|
import json
|
||||||
|
|
||||||
|
You can instead just do this::
|
||||||
|
|
||||||
|
from flask import json
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
.. sourcecode:: html+jinja
|
||||||
|
|
||||||
|
<script type=text/javascript>
|
||||||
|
doSomethingWith({{ user.username|tojson|safe }});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
Note that the ``|tojson`` filter escapes forward slashes properly.
|
||||||
|
|
||||||
.. autofunction:: jsonify
|
.. autofunction:: jsonify
|
||||||
|
|
||||||
.. data:: json
|
.. autofunction:: dumps
|
||||||
|
|
||||||
If JSON support is picked up, this will be the module that Flask is
|
.. autofunction:: dump
|
||||||
using to parse and serialize JSON. So instead of doing this yourself::
|
|
||||||
|
|
||||||
try:
|
.. autofunction:: loads
|
||||||
import simplejson as json
|
|
||||||
except ImportError:
|
|
||||||
import json
|
|
||||||
|
|
||||||
You can instead just do this::
|
.. autofunction:: load
|
||||||
|
|
||||||
from flask import json
|
.. autoclass:: JSONEncoder
|
||||||
|
:members:
|
||||||
|
|
||||||
For usage examples, read the :mod:`json` documentation.
|
.. autoclass:: JSONDecoder
|
||||||
|
:members:
|
||||||
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.
|
|
||||||
|
|
||||||
Template Rendering
|
Template Rendering
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
.. currentmodule:: flask
|
||||||
|
|
||||||
.. autofunction:: render_template
|
.. autofunction:: render_template
|
||||||
|
|
||||||
.. autofunction:: render_template_string
|
.. autofunction:: render_template_string
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,8 @@ from jinja2 import Markup, escape
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -37,8 +36,13 @@ from .signals import signals_available, template_rendered, request_started, \
|
||||||
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
|
||||||
|
|
|
||||||
16
flask/app.py
16
flask/app.py
|
|
@ -24,8 +24,8 @@ from werkzeug.exceptions import HTTPException, InternalServerError, \
|
||||||
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
|
||||||
|
|
@ -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_']
|
||||||
|
|
@ -637,7 +647,7 @@ class Flask(_PackageBoundObject):
|
||||||
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):
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
@ -39,7 +39,6 @@ class JSONHTTPException(HTTPException):
|
||||||
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.
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,6 @@ from werkzeug.routing import BuildError
|
||||||
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
|
||||||
|
|
@ -43,17 +39,6 @@ from .globals import session, _request_ctx_stack, _app_ctx_stack, \
|
||||||
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()
|
||||||
|
|
||||||
|
|
@ -146,37 +131,6 @@ def stream_with_context(generator_or_function):
|
||||||
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
|
||||||
|
|
|
||||||
146
flask/json.py
Normal file
146
flask/json.py
Normal file
|
|
@ -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')
|
||||||
|
|
@ -13,8 +13,7 @@ import hashlib
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from werkzeug.http import http_date, parse_date
|
from werkzeug.http import http_date, parse_date
|
||||||
from werkzeug.datastructures import CallbackDict
|
from werkzeug.datastructures import CallbackDict
|
||||||
from .helpers import json
|
from . import Markup, json
|
||||||
from . import Markup
|
|
||||||
|
|
||||||
from itsdangerous import URLSafeTimedSerializer, BadSignature
|
from itsdangerous import URLSafeTimedSerializer, BadSignature
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ from werkzeug.utils import cached_property
|
||||||
|
|
||||||
from .exceptions import JSONBadRequest
|
from .exceptions import JSONBadRequest
|
||||||
from .debughelpers import attach_enctype_error_multidict
|
from .debughelpers import attach_enctype_error_multidict
|
||||||
from .helpers import json
|
from . import json
|
||||||
from .globals import _request_ctx_stack
|
from .globals import _request_ctx_stack
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue