aboutsummaryrefslogtreecommitdiffstats
path: root/python/flask/json
diff options
context:
space:
mode:
Diffstat (limited to 'python/flask/json')
-rw-r--r--python/flask/json/__init__.py357
-rw-r--r--python/flask/json/tag.py300
2 files changed, 657 insertions, 0 deletions
diff --git a/python/flask/json/__init__.py b/python/flask/json/__init__.py
new file mode 100644
index 0000000..c24286c
--- /dev/null
+++ b/python/flask/json/__init__.py
@@ -0,0 +1,357 @@
+# -*- coding: utf-8 -*-
+"""
+flask.json
+~~~~~~~~~~
+
+:copyright: © 2010 by the Pallets team.
+:license: BSD, see LICENSE for more details.
+"""
+import codecs
+import io
+import uuid
+from datetime import date, datetime
+from flask.globals import current_app, request
+from flask._compat import text_type, PY2
+
+from werkzeug.http import http_date
+from jinja2 import Markup
+
+# Use the same json implementation as itsdangerous on which we
+# depend anyways.
+from itsdangerous import json 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']
+
+
+def _wrap_reader_for_text(fp, encoding):
+ if isinstance(fp.read(0), bytes):
+ fp = io.TextIOWrapper(io.BufferedReader(fp), encoding)
+ return fp
+
+
+def _wrap_writer_for_text(fp, encoding):
+ try:
+ fp.write('')
+ except TypeError:
+ fp = io.TextIOWrapper(fp, encoding)
+ return fp
+
+
+class JSONEncoder(_json.JSONEncoder):
+ """The default Flask JSON encoder. This one extends the default simplejson
+ encoder by also supporting ``datetime`` objects, ``UUID`` 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 :exc:`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.utctimetuple())
+ if isinstance(o, date):
+ return http_date(o.timetuple())
+ if isinstance(o, uuid.UUID):
+ return str(o)
+ if hasattr(o, '__html__'):
+ return text_type(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 decoder. 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 _dump_arg_defaults(kwargs, app=None):
+ """Inject default arguments for dump functions."""
+ if app is None:
+ app = current_app
+
+ if app:
+ bp = app.blueprints.get(request.blueprint) if request else None
+ kwargs.setdefault(
+ 'cls', bp.json_encoder if bp and bp.json_encoder else app.json_encoder
+ )
+
+ if not app.config['JSON_AS_ASCII']:
+ kwargs.setdefault('ensure_ascii', False)
+
+ kwargs.setdefault('sort_keys', app.config['JSON_SORT_KEYS'])
+ else:
+ kwargs.setdefault('sort_keys', True)
+ kwargs.setdefault('cls', JSONEncoder)
+
+
+def _load_arg_defaults(kwargs, app=None):
+ """Inject default arguments for load functions."""
+ if app is None:
+ app = current_app
+
+ if app:
+ bp = app.blueprints.get(request.blueprint) if request else None
+ kwargs.setdefault(
+ 'cls',
+ bp.json_decoder if bp and bp.json_decoder
+ else app.json_decoder
+ )
+ else:
+ kwargs.setdefault('cls', JSONDecoder)
+
+
+def detect_encoding(data):
+ """Detect which UTF codec was used to encode the given bytes.
+
+ The latest JSON standard (:rfc:`8259`) suggests that only UTF-8 is
+ accepted. Older documents allowed 8, 16, or 32. 16 and 32 can be big
+ or little endian. Some editors or libraries may prepend a BOM.
+
+ :param data: Bytes in unknown UTF encoding.
+ :return: UTF encoding name
+ """
+ head = data[:4]
+
+ if head[:3] == codecs.BOM_UTF8:
+ return 'utf-8-sig'
+
+ if b'\x00' not in head:
+ return 'utf-8'
+
+ if head in (codecs.BOM_UTF32_BE, codecs.BOM_UTF32_LE):
+ return 'utf-32'
+
+ if head[:2] in (codecs.BOM_UTF16_BE, codecs.BOM_UTF16_LE):
+ return 'utf-16'
+
+ if len(head) == 4:
+ if head[:3] == b'\x00\x00\x00':
+ return 'utf-32-be'
+
+ if head[::2] == b'\x00\x00':
+ return 'utf-16-be'
+
+ if head[1:] == b'\x00\x00\x00':
+ return 'utf-32-le'
+
+ if head[1::2] == b'\x00\x00':
+ return 'utf-16-le'
+
+ if len(head) == 2:
+ return 'utf-16-be' if head.startswith(b'\x00') else 'utf-16-le'
+
+ return 'utf-8'
+
+
+def dumps(obj, app=None, **kwargs):
+ """Serialize ``obj`` to a JSON-formatted string. If there is an
+ app context pushed, use the current app's configured encoder
+ (:attr:`~flask.Flask.json_encoder`), or fall back to the default
+ :class:`JSONEncoder`.
+
+ Takes the same arguments as the built-in :func:`json.dumps`, and
+ does some extra configuration based on the application. If the
+ simplejson package is installed, it is preferred.
+
+ :param obj: Object to serialize to JSON.
+ :param app: App instance to use to configure the JSON encoder.
+ Uses ``current_app`` if not given, and falls back to the default
+ encoder when not in an app context.
+ :param kwargs: Extra arguments passed to :func:`json.dumps`.
+
+ .. versionchanged:: 1.0.3
+
+ ``app`` can be passed directly, rather than requiring an app
+ context for configuration.
+ """
+ _dump_arg_defaults(kwargs, app=app)
+ encoding = kwargs.pop('encoding', None)
+ rv = _json.dumps(obj, **kwargs)
+ if encoding is not None and isinstance(rv, text_type):
+ rv = rv.encode(encoding)
+ return rv
+
+
+def dump(obj, fp, app=None, **kwargs):
+ """Like :func:`dumps` but writes into a file object."""
+ _dump_arg_defaults(kwargs, app=app)
+ encoding = kwargs.pop('encoding', None)
+ if encoding is not None:
+ fp = _wrap_writer_for_text(fp, encoding)
+ _json.dump(obj, fp, **kwargs)
+
+
+def loads(s, app=None, **kwargs):
+ """Deserialize an object from a JSON-formatted string ``s``. If
+ there is an app context pushed, use the current app's configured
+ decoder (:attr:`~flask.Flask.json_decoder`), or fall back to the
+ default :class:`JSONDecoder`.
+
+ Takes the same arguments as the built-in :func:`json.loads`, and
+ does some extra configuration based on the application. If the
+ simplejson package is installed, it is preferred.
+
+ :param s: JSON string to deserialize.
+ :param app: App instance to use to configure the JSON decoder.
+ Uses ``current_app`` if not given, and falls back to the default
+ encoder when not in an app context.
+ :param kwargs: Extra arguments passed to :func:`json.dumps`.
+
+ .. versionchanged:: 1.0.3
+
+ ``app`` can be passed directly, rather than requiring an app
+ context for configuration.
+ """
+ _load_arg_defaults(kwargs, app=app)
+ if isinstance(s, bytes):
+ encoding = kwargs.pop('encoding', None)
+ if encoding is None:
+ encoding = detect_encoding(s)
+ s = s.decode(encoding)
+ return _json.loads(s, **kwargs)
+
+
+def load(fp, app=None, **kwargs):
+ """Like :func:`loads` but reads from a file object."""
+ _load_arg_defaults(kwargs, app=app)
+ if not PY2:
+ fp = _wrap_reader_for_text(fp, kwargs.pop('encoding', None) or 'utf-8')
+ 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 which will
+ also mark the result as safe. Due to how this function escapes certain
+ characters this is safe even if used outside of ``<script>`` tags.
+
+ The following characters are escaped in strings:
+
+ - ``<``
+ - ``>``
+ - ``&``
+ - ``'``
+
+ This makes it safe to embed such strings in any place in HTML with the
+ notable exception of double quoted attributes. In that case single
+ quote your attributes or HTML escape it in addition.
+
+ .. versionchanged:: 0.10
+ This function's return value is now always safe for HTML usage, even
+ if outside of script tags or if used in XHTML. This rule does not
+ hold true when using this function in HTML attributes that are double
+ quoted. Always single quote attributes if you use the ``|tojson``
+ filter. Alternatively use ``|tojson|forceescape``.
+ """
+ rv = dumps(obj, **kwargs) \
+ .replace(u'<', u'\\u003c') \
+ .replace(u'>', u'\\u003e') \
+ .replace(u'&', u'\\u0026') \
+ .replace(u"'", u'\\u0027')
+ if not _slash_escape:
+ rv = rv.replace('\\/', '/')
+ return rv
+
+
+def htmlsafe_dump(obj, fp, **kwargs):
+ """Like :func:`htmlsafe_dumps` but writes into a file object."""
+ fp.write(text_type(htmlsafe_dumps(obj, **kwargs)))
+
+
+def jsonify(*args, **kwargs):
+ """This function wraps :func:`dumps` to add a few enhancements that make
+ life easier. It turns the JSON output into a :class:`~flask.Response`
+ object with the :mimetype:`application/json` mimetype. For convenience, it
+ also converts multiple arguments into an array or multiple keyword arguments
+ into a dict. This means that both ``jsonify(1,2,3)`` and
+ ``jsonify([1,2,3])`` serialize to ``[1,2,3]``.
+
+ For clarity, the JSON serialization behavior has the following differences
+ from :func:`dumps`:
+
+ 1. Single argument: Passed straight through to :func:`dumps`.
+ 2. Multiple arguments: Converted to an array before being passed to
+ :func:`dumps`.
+ 3. Multiple keyword arguments: Converted to a dict before being passed to
+ :func:`dumps`.
+ 4. Both args and kwargs: Behavior undefined and will throw an exception.
+
+ Example usage::
+
+ from flask import jsonify
+
+ @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
+ }
+
+
+ .. versionchanged:: 0.11
+ Added support for serializing top-level arrays. This introduces a
+ security risk in ancient browsers. See :ref:`json-security` for details.
+
+ This function's response will be pretty printed if the
+ ``JSONIFY_PRETTYPRINT_REGULAR`` config parameter is set to True or the
+ Flask app is running in debug mode. Compressed (not pretty) formatting
+ currently means no indents and no spaces after separators.
+
+ .. versionadded:: 0.2
+ """
+
+ indent = None
+ separators = (',', ':')
+
+ if current_app.config['JSONIFY_PRETTYPRINT_REGULAR'] or current_app.debug:
+ indent = 2
+ separators = (', ', ': ')
+
+ if args and kwargs:
+ raise TypeError('jsonify() behavior undefined when passed both args and kwargs')
+ elif len(args) == 1: # single args are passed directly to dumps()
+ data = args[0]
+ else:
+ data = args or kwargs
+
+ return current_app.response_class(
+ dumps(data, indent=indent, separators=separators) + '\n',
+ mimetype=current_app.config['JSONIFY_MIMETYPE']
+ )
+
+
+def tojson_filter(obj, **kwargs):
+ return Markup(htmlsafe_dumps(obj, **kwargs))
diff --git a/python/flask/json/tag.py b/python/flask/json/tag.py
new file mode 100644
index 0000000..11c966c
--- /dev/null
+++ b/python/flask/json/tag.py
@@ -0,0 +1,300 @@
+# -*- coding: utf-8 -*-
+"""
+Tagged JSON
+~~~~~~~~~~~
+
+A compact representation for lossless serialization of non-standard JSON types.
+:class:`~flask.sessions.SecureCookieSessionInterface` uses this to serialize
+the session data, but it may be useful in other places. It can be extended to
+support other types.
+
+.. autoclass:: TaggedJSONSerializer
+ :members:
+
+.. autoclass:: JSONTag
+ :members:
+
+Let's seen an example that adds support for :class:`~collections.OrderedDict`.
+Dicts don't have an order in Python or JSON, so to handle this we will dump
+the items as a list of ``[key, value]`` pairs. Subclass :class:`JSONTag` and
+give it the new key ``' od'`` to identify the type. The session serializer
+processes dicts first, so insert the new tag at the front of the order since
+``OrderedDict`` must be processed before ``dict``. ::
+
+ from flask.json.tag import JSONTag
+
+ class TagOrderedDict(JSONTag):
+ __slots__ = ('serializer',)
+ key = ' od'
+
+ def check(self, value):
+ return isinstance(value, OrderedDict)
+
+ def to_json(self, value):
+ return [[k, self.serializer.tag(v)] for k, v in iteritems(value)]
+
+ def to_python(self, value):
+ return OrderedDict(value)
+
+ app.session_interface.serializer.register(TagOrderedDict, index=0)
+
+:copyright: © 2010 by the Pallets team.
+:license: BSD, see LICENSE for more details.
+"""
+
+from base64 import b64decode, b64encode
+from datetime import datetime
+from uuid import UUID
+
+from jinja2 import Markup
+from werkzeug.http import http_date, parse_date
+
+from flask._compat import iteritems, text_type
+from flask.json import dumps, loads
+
+
+class JSONTag(object):
+ """Base class for defining type tags for :class:`TaggedJSONSerializer`."""
+
+ __slots__ = ('serializer',)
+
+ #: The tag to mark the serialized object with. If ``None``, this tag is
+ #: only used as an intermediate step during tagging.
+ key = None
+
+ def __init__(self, serializer):
+ """Create a tagger for the given serializer."""
+ self.serializer = serializer
+
+ def check(self, value):
+ """Check if the given value should be tagged by this tag."""
+ raise NotImplementedError
+
+ def to_json(self, value):
+ """Convert the Python object to an object that is a valid JSON type.
+ The tag will be added later."""
+ raise NotImplementedError
+
+ def to_python(self, value):
+ """Convert the JSON representation back to the correct type. The tag
+ will already be removed."""
+ raise NotImplementedError
+
+ def tag(self, value):
+ """Convert the value to a valid JSON type and add the tag structure
+ around it."""
+ return {self.key: self.to_json(value)}
+
+
+class TagDict(JSONTag):
+ """Tag for 1-item dicts whose only key matches a registered tag.
+
+ Internally, the dict key is suffixed with `__`, and the suffix is removed
+ when deserializing.
+ """
+
+ __slots__ = ()
+ key = ' di'
+
+ def check(self, value):
+ return (
+ isinstance(value, dict)
+ and len(value) == 1
+ and next(iter(value)) in self.serializer.tags
+ )
+
+ def to_json(self, value):
+ key = next(iter(value))
+ return {key + '__': self.serializer.tag(value[key])}
+
+ def to_python(self, value):
+ key = next(iter(value))
+ return {key[:-2]: value[key]}
+
+
+class PassDict(JSONTag):
+ __slots__ = ()
+
+ def check(self, value):
+ return isinstance(value, dict)
+
+ def to_json(self, value):
+ # JSON objects may only have string keys, so don't bother tagging the
+ # key here.
+ return dict((k, self.serializer.tag(v)) for k, v in iteritems(value))
+
+ tag = to_json
+
+
+class TagTuple(JSONTag):
+ __slots__ = ()
+ key = ' t'
+
+ def check(self, value):
+ return isinstance(value, tuple)
+
+ def to_json(self, value):
+ return [self.serializer.tag(item) for item in value]
+
+ def to_python(self, value):
+ return tuple(value)
+
+
+class PassList(JSONTag):
+ __slots__ = ()
+
+ def check(self, value):
+ return isinstance(value, list)
+
+ def to_json(self, value):
+ return [self.serializer.tag(item) for item in value]
+
+ tag = to_json
+
+
+class TagBytes(JSONTag):
+ __slots__ = ()
+ key = ' b'
+
+ def check(self, value):
+ return isinstance(value, bytes)
+
+ def to_json(self, value):
+ return b64encode(value).decode('ascii')
+
+ def to_python(self, value):
+ return b64decode(value)
+
+
+class TagMarkup(JSONTag):
+ """Serialize anything matching the :class:`~flask.Markup` API by
+ having a ``__html__`` method to the result of that method. Always
+ deserializes to an instance of :class:`~flask.Markup`."""
+
+ __slots__ = ()
+ key = ' m'
+
+ def check(self, value):
+ return callable(getattr(value, '__html__', None))
+
+ def to_json(self, value):
+ return text_type(value.__html__())
+
+ def to_python(self, value):
+ return Markup(value)
+
+
+class TagUUID(JSONTag):
+ __slots__ = ()
+ key = ' u'
+
+ def check(self, value):
+ return isinstance(value, UUID)
+
+ def to_json(self, value):
+ return value.hex
+
+ def to_python(self, value):
+ return UUID(value)
+
+
+class TagDateTime(JSONTag):
+ __slots__ = ()
+ key = ' d'
+
+ def check(self, value):
+ return isinstance(value, datetime)
+
+ def to_json(self, value):
+ return http_date(value)
+
+ def to_python(self, value):
+ return parse_date(value)
+
+
+class TaggedJSONSerializer(object):
+ """Serializer that uses a tag system to compactly represent objects that
+ are not JSON types. Passed as the intermediate serializer to
+ :class:`itsdangerous.Serializer`.
+
+ The following extra types are supported:
+
+ * :class:`dict`
+ * :class:`tuple`
+ * :class:`bytes`
+ * :class:`~flask.Markup`
+ * :class:`~uuid.UUID`
+ * :class:`~datetime.datetime`
+ """
+
+ __slots__ = ('tags', 'order')
+
+ #: Tag classes to bind when creating the serializer. Other tags can be
+ #: added later using :meth:`~register`.
+ default_tags = [
+ TagDict, PassDict, TagTuple, PassList, TagBytes, TagMarkup, TagUUID,
+ TagDateTime,
+ ]
+
+ def __init__(self):
+ self.tags = {}
+ self.order = []
+
+ for cls in self.default_tags:
+ self.register(cls)
+
+ def register(self, tag_class, force=False, index=None):
+ """Register a new tag with this serializer.
+
+ :param tag_class: tag class to register. Will be instantiated with this
+ serializer instance.
+ :param force: overwrite an existing tag. If false (default), a
+ :exc:`KeyError` is raised.
+ :param index: index to insert the new tag in the tag order. Useful when
+ the new tag is a special case of an existing tag. If ``None``
+ (default), the tag is appended to the end of the order.
+
+ :raise KeyError: if the tag key is already registered and ``force`` is
+ not true.
+ """
+ tag = tag_class(self)
+ key = tag.key
+
+ if key is not None:
+ if not force and key in self.tags:
+ raise KeyError("Tag '{0}' is already registered.".format(key))
+
+ self.tags[key] = tag
+
+ if index is None:
+ self.order.append(tag)
+ else:
+ self.order.insert(index, tag)
+
+ def tag(self, value):
+ """Convert a value to a tagged representation if necessary."""
+ for tag in self.order:
+ if tag.check(value):
+ return tag.tag(value)
+
+ return value
+
+ def untag(self, value):
+ """Convert a tagged representation back to the original type."""
+ if len(value) != 1:
+ return value
+
+ key = next(iter(value))
+
+ if key not in self.tags:
+ return value
+
+ return self.tags[key].to_python(value[key])
+
+ def dumps(self, value):
+ """Tag the value and dump it to a compact JSON string."""
+ return dumps(self.tag(value), separators=(',', ':'))
+
+ def loads(self, value):
+ """Load data from a JSON string and deserialized any tagged objects."""
+ return loads(value, object_hook=self.untag)