aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/Flask/py3/flask/json/__init__.py
blob: ccb9efb174cd9f9e2c7da47d9ae467f099c7eed3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
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"],
    )