1 from __future__ import annotations 2 3 import dataclasses 4 import decimal 5 import json 6 import typing as t 7 import uuid 8 import weakref 9 from datetime import date 10 11 from werkzeug.http import http_date 12 13 if t.TYPE_CHECKING: # pragma: no cover 14 from werkzeug.sansio.response import Response 15 16 from ..sansio.app import App 17 18 19 class 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 108 def _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) # type: ignore[arg-type] 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 124 class 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 ) 216