1from __future__ import annotations
2
3import dataclasses
4import decimal
5import json
6import typing as t
7import uuid
8import weakref
9from datetime import date
10
11from werkzeug.http import http_date
12
13if t.TYPE_CHECKING: # pragma: no cover
14 from werkzeug.sansio.response import Response
15
16 from ..sansio.app import App
17
18
19class JSONProvider:
20 """A standard set of JSON operations for an application. Subclasses
21 of this can be used to customize JSON behavior or use different
22 JSON libraries.
23
24 To implement a provider for a specific library, subclass this base
25 class and implement at least :meth:`dumps` and :meth:`loads`. All
26 other methods have default implementations.
27
28 To use a different provider, either subclass ``Flask`` and set
29 :attr:`~flask.Flask.json_provider_class` to a provider class, or set
30 :attr:`app.json <flask.Flask.json>` to an instance of the class.
31
32 :param app: An application instance. This will be stored as a
33 :class:`weakref.proxy` on the :attr:`_app` attribute.
34
35 .. versionadded:: 2.2
36 """
37
38 def __init__(self, app: App) -> None:
39 self._app: App = weakref.proxy(app)
40
41 def dumps(self, obj: t.Any, **kwargs: t.Any) -> str:
42 """Serialize data as JSON.
43
44 :param obj: The data to serialize.
45 :param kwargs: May be passed to the underlying JSON library.
46 """
47 raise NotImplementedError
48
49 def dump(self, obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None:
50 """Serialize data as JSON and write to a file.
51
52 :param obj: The data to serialize.
53 :param fp: A file opened for writing text. Should use the UTF-8
54 encoding to be valid JSON.
55 :param kwargs: May be passed to the underlying JSON library.
56 """
57 fp.write(self.dumps(obj, **kwargs))
58
59 def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any:
60 """Deserialize data as JSON.
61
62 :param s: Text or UTF-8 bytes.
63 :param kwargs: May be passed to the underlying JSON library.
64 """
65 raise NotImplementedError
66
67 def load(self, fp: t.IO[t.AnyStr], **kwargs: t.Any) -> t.Any:
68 """Deserialize data as JSON read from a file.
69
70 :param fp: A file opened for reading text or UTF-8 bytes.
71 :param kwargs: May be passed to the underlying JSON library.
72 """
73 return self.loads(fp.read(), **kwargs)
74
75 def _prepare_response_obj(
76 self, args: tuple[t.Any, ...], kwargs: dict[str, t.Any]
77 ) -> t.Any:
78 if args and kwargs:
79 raise TypeError("app.json.response() takes either args or kwargs, not both")
80
81 if not args and not kwargs:
82 return None
83
84 if len(args) == 1:
85 return args[0]
86
87 return args or kwargs
88
89 def response(self, *args: t.Any, **kwargs: t.Any) -> Response:
90 """Serialize the given arguments as JSON, and return a
91 :class:`~flask.Response` object with the ``application/json``
92 mimetype.
93
94 The :func:`~flask.json.jsonify` function calls this method for
95 the current application.
96
97 Either positional or keyword arguments can be given, not both.
98 If no arguments are given, ``None`` is serialized.
99
100 :param args: A single value to serialize, or multiple values to
101 treat as a list to serialize.
102 :param kwargs: Treat as a dict to serialize.
103 """
104 obj = self._prepare_response_obj(args, kwargs)
105 return self._app.response_class(self.dumps(obj), mimetype="application/json")
106
107
108def _default(o: t.Any) -> t.Any:
109 if isinstance(o, date):
110 return http_date(o)
111
112 if isinstance(o, (decimal.Decimal, uuid.UUID)):
113 return str(o)
114
115 if dataclasses and dataclasses.is_dataclass(o):
116 return dataclasses.asdict(o)
117
118 if hasattr(o, "__html__"):
119 return str(o.__html__())
120
121 raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable")
122
123
124class DefaultJSONProvider(JSONProvider):
125 """Provide JSON operations using Python's built-in :mod:`json`
126 library. Serializes the following additional data types:
127
128 - :class:`datetime.datetime` and :class:`datetime.date` are
129 serialized to :rfc:`822` strings. This is the same as the HTTP
130 date format.
131 - :class:`uuid.UUID` is serialized to a string.
132 - :class:`dataclasses.dataclass` is passed to
133 :func:`dataclasses.asdict`.
134 - :class:`~markupsafe.Markup` (or any object with a ``__html__``
135 method) will call the ``__html__`` method to get a string.
136 """
137
138 default: t.Callable[[t.Any], t.Any] = staticmethod(_default) # type: ignore[assignment]
139 """Apply this function to any object that :meth:`json.dumps` does
140 not know how to serialize. It should return a valid JSON type or
141 raise a ``TypeError``.
142 """
143
144 ensure_ascii = True
145 """Replace non-ASCII characters with escape sequences. This may be
146 more compatible with some clients, but can be disabled for better
147 performance and size.
148 """
149
150 sort_keys = True
151 """Sort the keys in any serialized dicts. This may be useful for
152 some caching situations, but can be disabled for better performance.
153 When enabled, keys must all be strings, they are not converted
154 before sorting.
155 """
156
157 compact: bool | None = None
158 """If ``True``, or ``None`` out of debug mode, the :meth:`response`
159 output will not add indentation, newlines, or spaces. If ``False``,
160 or ``None`` in debug mode, it will use a non-compact representation.
161 """
162
163 mimetype = "application/json"
164 """The mimetype set in :meth:`response`."""
165
166 def dumps(self, obj: t.Any, **kwargs: t.Any) -> str:
167 """Serialize data as JSON to a string.
168
169 Keyword arguments are passed to :func:`json.dumps`. Sets some
170 parameter defaults from the :attr:`default`,
171 :attr:`ensure_ascii`, and :attr:`sort_keys` attributes.
172
173 :param obj: The data to serialize.
174 :param kwargs: Passed to :func:`json.dumps`.
175 """
176 kwargs.setdefault("default", self.default)
177 kwargs.setdefault("ensure_ascii", self.ensure_ascii)
178 kwargs.setdefault("sort_keys", self.sort_keys)
179 return json.dumps(obj, **kwargs)
180
181 def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any:
182 """Deserialize data as JSON from a string or bytes.
183
184 :param s: Text or UTF-8 bytes.
185 :param kwargs: Passed to :func:`json.loads`.
186 """
187 return json.loads(s, **kwargs)
188
189 def response(self, *args: t.Any, **kwargs: t.Any) -> Response:
190 """Serialize the given arguments as JSON, and return a
191 :class:`~flask.Response` object with it. The response mimetype
192 will be "application/json" and can be changed with
193 :attr:`mimetype`.
194
195 If :attr:`compact` is ``False`` or debug mode is enabled, the
196 output will be formatted to be easier to read.
197
198 Either positional or keyword arguments can be given, not both.
199 If no arguments are given, ``None`` is serialized.
200
201 :param args: A single value to serialize, or multiple values to
202 treat as a list to serialize.
203 :param kwargs: Treat as a dict to serialize.
204 """
205 obj = self._prepare_response_obj(args, kwargs)
206 dump_args: dict[str, t.Any] = {}
207
208 if (self.compact is None and self._app.debug) or self.compact is False:
209 dump_args.setdefault("indent", 2)
210 else:
211 dump_args.setdefault("separators", (",", ":"))
212
213 return self._app.response_class(
214 f"{self.dumps(obj, **dump_args)}\n", mimetype=self.mimetype
215 )