This commit is contained in:
Justine Pelletreau
2022-03-16 15:02:53 +01:00
commit e5adfa2d64
1404 changed files with 270042 additions and 0 deletions

View File

@ -0,0 +1,363 @@
import decimal
import io
import json as _json
import typing as t
import uuid
import warnings
from datetime import date
from jinja2.utils import htmlsafe_json_dumps as _jinja_htmlsafe_dumps
from werkzeug.http import http_date
from ..globals import current_app
from ..globals import request
if t.TYPE_CHECKING:
from ..app import Flask
from ..wrappers import Response
try:
import dataclasses
except ImportError:
# Python < 3.7
dataclasses = None # type: ignore
class JSONEncoder(_json.JSONEncoder):
"""The default JSON encoder. Handles extra types compared to the
built-in :class:`json.JSONEncoder`.
- :class:`datetime.datetime` and :class:`datetime.date` are
serialized to :rfc:`822` strings. This is the same as the HTTP
date format.
- :class:`uuid.UUID` is serialized to a string.
- :class:`dataclasses.dataclass` is passed to
:func:`dataclasses.asdict`.
- :class:`~markupsafe.Markup` (or any object with a ``__html__``
method) will call the ``__html__`` method to get a string.
Assign a subclass of this to :attr:`flask.Flask.json_encoder` or
:attr:`flask.Blueprint.json_encoder` to override the default.
"""
def default(self, o: t.Any) -> t.Any:
"""Convert ``o`` to a JSON serializable type. See
:meth:`json.JSONEncoder.default`. Python does not support
overriding how basic types like ``str`` or ``list`` are
serialized, they are handled before this method.
"""
if isinstance(o, date):
return http_date(o)
if isinstance(o, (decimal.Decimal, uuid.UUID)):
return str(o)
if dataclasses and dataclasses.is_dataclass(o):
return dataclasses.asdict(o)
if hasattr(o, "__html__"):
return str(o.__html__())
return super().default(o)
class JSONDecoder(_json.JSONDecoder):
"""The default JSON decoder.
This does not change any behavior from the built-in
:class:`json.JSONDecoder`.
Assign a subclass of this to :attr:`flask.Flask.json_decoder` or
:attr:`flask.Blueprint.json_decoder` to override the default.
"""
def _dump_arg_defaults(
kwargs: t.Dict[str, t.Any], app: t.Optional["Flask"] = None
) -> None:
"""Inject default arguments for dump functions."""
if app is None:
app = current_app
if app:
cls = app.json_encoder
bp = app.blueprints.get(request.blueprint) if request else None # type: ignore
if bp is not None and bp.json_encoder is not None:
cls = bp.json_encoder
# Only set a custom encoder if it has custom behavior. This is
# faster on PyPy.
if cls is not _json.JSONEncoder:
kwargs.setdefault("cls", cls)
kwargs.setdefault("cls", cls)
kwargs.setdefault("ensure_ascii", app.config["JSON_AS_ASCII"])
kwargs.setdefault("sort_keys", app.config["JSON_SORT_KEYS"])
else:
kwargs.setdefault("sort_keys", True)
kwargs.setdefault("cls", JSONEncoder)
def _load_arg_defaults(
kwargs: t.Dict[str, t.Any], app: t.Optional["Flask"] = None
) -> None:
"""Inject default arguments for load functions."""
if app is None:
app = current_app
if app:
cls = app.json_decoder
bp = app.blueprints.get(request.blueprint) if request else None # type: ignore
if bp is not None and bp.json_decoder is not None:
cls = bp.json_decoder
# Only set a custom decoder if it has custom behavior. This is
# faster on PyPy.
if cls not in {JSONDecoder, _json.JSONDecoder}:
kwargs.setdefault("cls", cls)
def dumps(obj: t.Any, app: t.Optional["Flask"] = None, **kwargs: t.Any) -> str:
"""Serialize an object to a string of JSON.
Takes the same arguments as the built-in :func:`json.dumps`, with
some defaults from application configuration.
:param obj: Object to serialize to JSON.
:param app: Use this app's config instead of the active app context
or defaults.
:param kwargs: Extra arguments passed to :func:`json.dumps`.
.. versionchanged:: 2.0.2
:class:`decimal.Decimal` is supported by converting to a string.
.. versionchanged:: 2.0
``encoding`` is deprecated and will be removed in Flask 2.1.
.. 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:
warnings.warn(
"'encoding' is deprecated and will be removed in Flask 2.1.",
DeprecationWarning,
stacklevel=2,
)
if isinstance(rv, str):
return rv.encode(encoding) # type: ignore
return rv
def dump(
obj: t.Any, fp: t.IO[str], app: t.Optional["Flask"] = None, **kwargs: t.Any
) -> None:
"""Serialize an object to JSON written to a file object.
Takes the same arguments as the built-in :func:`json.dump`, with
some defaults from application configuration.
:param obj: Object to serialize to JSON.
:param fp: File object to write JSON to.
:param app: Use this app's config instead of the active app context
or defaults.
:param kwargs: Extra arguments passed to :func:`json.dump`.
.. versionchanged:: 2.0
Writing to a binary file, and the ``encoding`` argument, is
deprecated and will be removed in Flask 2.1.
"""
_dump_arg_defaults(kwargs, app=app)
encoding = kwargs.pop("encoding", None)
show_warning = encoding is not None
try:
fp.write("")
except TypeError:
show_warning = True
fp = io.TextIOWrapper(fp, encoding or "utf-8") # type: ignore
if show_warning:
warnings.warn(
"Writing to a binary file, and the 'encoding' argument, is"
" deprecated and will be removed in Flask 2.1.",
DeprecationWarning,
stacklevel=2,
)
_json.dump(obj, fp, **kwargs)
def loads(s: str, app: t.Optional["Flask"] = None, **kwargs: t.Any) -> t.Any:
"""Deserialize an object from a string of JSON.
Takes the same arguments as the built-in :func:`json.loads`, with
some defaults from application configuration.
:param s: JSON string to deserialize.
:param app: Use this app's config instead of the active app context
or defaults.
:param kwargs: Extra arguments passed to :func:`json.loads`.
.. versionchanged:: 2.0
``encoding`` is deprecated and will be removed in Flask 2.1. The
data must be a string or UTF-8 bytes.
.. versionchanged:: 1.0.3
``app`` can be passed directly, rather than requiring an app
context for configuration.
"""
_load_arg_defaults(kwargs, app=app)
encoding = kwargs.pop("encoding", None)
if encoding is not None:
warnings.warn(
"'encoding' is deprecated and will be removed in Flask 2.1."
" The data must be a string or UTF-8 bytes.",
DeprecationWarning,
stacklevel=2,
)
if isinstance(s, bytes):
s = s.decode(encoding)
return _json.loads(s, **kwargs)
def load(fp: t.IO[str], app: t.Optional["Flask"] = None, **kwargs: t.Any) -> t.Any:
"""Deserialize an object from JSON read from a file object.
Takes the same arguments as the built-in :func:`json.load`, with
some defaults from application configuration.
:param fp: File object to read JSON from.
:param app: Use this app's config instead of the active app context
or defaults.
:param kwargs: Extra arguments passed to :func:`json.load`.
.. versionchanged:: 2.0
``encoding`` is deprecated and will be removed in Flask 2.1. The
file must be text mode, or binary mode with UTF-8 bytes.
"""
_load_arg_defaults(kwargs, app=app)
encoding = kwargs.pop("encoding", None)
if encoding is not None:
warnings.warn(
"'encoding' is deprecated and will be removed in Flask 2.1."
" The file must be text mode, or binary mode with UTF-8"
" bytes.",
DeprecationWarning,
stacklevel=2,
)
if isinstance(fp.read(0), bytes):
fp = io.TextIOWrapper(fp, encoding) # type: ignore
return _json.load(fp, **kwargs)
def htmlsafe_dumps(obj: t.Any, **kwargs: t.Any) -> str:
"""Serialize an object to a string of JSON with :func:`dumps`, then
replace HTML-unsafe characters with Unicode escapes and mark the
result safe with :class:`~markupsafe.Markup`.
This is available in templates as the ``|tojson`` filter.
The returned string is safe to render in HTML documents and
``<script>`` tags. The exception is in HTML attributes that are
double quoted; either use single quotes or the ``|forceescape``
filter.
.. versionchanged:: 2.0
Uses :func:`jinja2.utils.htmlsafe_json_dumps`. The returned
value is marked safe by wrapping in :class:`~markupsafe.Markup`.
.. versionchanged:: 0.10
Single quotes are escaped, making this safe to use in HTML,
``<script>`` tags, and single-quoted attributes without further
escaping.
"""
return _jinja_htmlsafe_dumps(obj, dumps=dumps, **kwargs)
def htmlsafe_dump(obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None:
"""Serialize an object to JSON written to a file object, replacing
HTML-unsafe characters with Unicode escapes. See
:func:`htmlsafe_dumps` and :func:`dumps`.
"""
fp.write(htmlsafe_dumps(obj, **kwargs))
def jsonify(*args: t.Any, **kwargs: t.Any) -> "Response":
"""Serialize data to JSON and wrap it in a :class:`~flask.Response`
with the :mimetype:`application/json` mimetype.
Uses :func:`dumps` to serialize the data, but ``args`` and
``kwargs`` are treated as data rather than arguments to
:func:`json.dumps`.
1. Single argument: Treated as a single value.
2. Multiple arguments: Treated as a list of values.
``jsonify(1, 2, 3)`` is the same as ``jsonify([1, 2, 3])``.
3. Keyword arguments: Treated as a dict of values.
``jsonify(data=data, errors=errors)`` is the same as
``jsonify({"data": data, "errors": errors})``.
4. Passing both arguments and keyword arguments is not allowed as
it's not clear what should happen.
.. code-block:: python
from flask import jsonify
@app.route("/users/me")
def get_current_user():
return jsonify(
username=g.user.username,
email=g.user.email,
id=g.user.id,
)
Will return a JSON response like this:
.. code-block:: javascript
{
"username": "admin",
"email": "admin@localhost",
"id": 42
}
The default output omits indents and spaces after separators. In
debug mode or if :data:`JSONIFY_PRETTYPRINT_REGULAR` is ``True``,
the output will be formatted to be easier to read.
.. versionchanged:: 2.0.2
:class:`decimal.Decimal` is supported by converting to a string.
.. versionchanged:: 0.11
Added support for serializing top-level arrays. This introduces
a security risk in ancient browsers. See :ref:`security-json`.
.. 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(
f"{dumps(data, indent=indent, separators=separators)}\n",
mimetype=current_app.config["JSONIFY_MIMETYPE"],
)

View File

@ -0,0 +1,312 @@
"""
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 see an example that adds support for
:class:`~collections.OrderedDict`. Dicts don't have an order in 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``.
.. code-block:: python
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)
"""
import typing as t
from base64 import b64decode
from base64 import b64encode
from datetime import datetime
from uuid import UUID
from markupsafe import Markup
from werkzeug.http import http_date
from werkzeug.http import parse_date
from ..json import dumps
from ..json import loads
class JSONTag:
"""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: t.Optional[str] = None
def __init__(self, serializer: "TaggedJSONSerializer") -> None:
"""Create a tagger for the given serializer."""
self.serializer = serializer
def check(self, value: t.Any) -> bool:
"""Check if the given value should be tagged by this tag."""
raise NotImplementedError
def to_json(self, value: t.Any) -> t.Any:
"""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: t.Any) -> t.Any:
"""Convert the JSON representation back to the correct type. The tag
will already be removed."""
raise NotImplementedError
def tag(self, value: t.Any) -> t.Any:
"""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: t.Any) -> bool:
return (
isinstance(value, dict)
and len(value) == 1
and next(iter(value)) in self.serializer.tags
)
def to_json(self, value: t.Any) -> t.Any:
key = next(iter(value))
return {f"{key}__": self.serializer.tag(value[key])}
def to_python(self, value: t.Any) -> t.Any:
key = next(iter(value))
return {key[:-2]: value[key]}
class PassDict(JSONTag):
__slots__ = ()
def check(self, value: t.Any) -> bool:
return isinstance(value, dict)
def to_json(self, value: t.Any) -> t.Any:
# JSON objects may only have string keys, so don't bother tagging the
# key here.
return {k: self.serializer.tag(v) for k, v in value.items()}
tag = to_json
class TagTuple(JSONTag):
__slots__ = ()
key = " t"
def check(self, value: t.Any) -> bool:
return isinstance(value, tuple)
def to_json(self, value: t.Any) -> t.Any:
return [self.serializer.tag(item) for item in value]
def to_python(self, value: t.Any) -> t.Any:
return tuple(value)
class PassList(JSONTag):
__slots__ = ()
def check(self, value: t.Any) -> bool:
return isinstance(value, list)
def to_json(self, value: t.Any) -> t.Any:
return [self.serializer.tag(item) for item in value]
tag = to_json
class TagBytes(JSONTag):
__slots__ = ()
key = " b"
def check(self, value: t.Any) -> bool:
return isinstance(value, bytes)
def to_json(self, value: t.Any) -> t.Any:
return b64encode(value).decode("ascii")
def to_python(self, value: t.Any) -> t.Any:
return b64decode(value)
class TagMarkup(JSONTag):
"""Serialize anything matching the :class:`~markupsafe.Markup` API by
having a ``__html__`` method to the result of that method. Always
deserializes to an instance of :class:`~markupsafe.Markup`."""
__slots__ = ()
key = " m"
def check(self, value: t.Any) -> bool:
return callable(getattr(value, "__html__", None))
def to_json(self, value: t.Any) -> t.Any:
return str(value.__html__())
def to_python(self, value: t.Any) -> t.Any:
return Markup(value)
class TagUUID(JSONTag):
__slots__ = ()
key = " u"
def check(self, value: t.Any) -> bool:
return isinstance(value, UUID)
def to_json(self, value: t.Any) -> t.Any:
return value.hex
def to_python(self, value: t.Any) -> t.Any:
return UUID(value)
class TagDateTime(JSONTag):
__slots__ = ()
key = " d"
def check(self, value: t.Any) -> bool:
return isinstance(value, datetime)
def to_json(self, value: t.Any) -> t.Any:
return http_date(value)
def to_python(self, value: t.Any) -> t.Any:
return parse_date(value)
class TaggedJSONSerializer:
"""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:`~markupsafe.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) -> None:
self.tags: t.Dict[str, JSONTag] = {}
self.order: t.List[JSONTag] = []
for cls in self.default_tags:
self.register(cls)
def register(
self,
tag_class: t.Type[JSONTag],
force: bool = False,
index: t.Optional[int] = None,
) -> 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(f"Tag '{key}' is already registered.")
self.tags[key] = tag
if index is None:
self.order.append(tag)
else:
self.order.insert(index, tag)
def tag(self, value: t.Any) -> t.Dict[str, t.Any]:
"""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: t.Dict[str, t.Any]) -> t.Any:
"""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: t.Any) -> str:
"""Tag the value and dump it to a compact JSON string."""
return dumps(self.tag(value), separators=(",", ":"))
def loads(self, value: str) -> t.Any:
"""Load data from a JSON string and deserialized any tagged objects."""
return loads(value, object_hook=self.untag)