You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

351 lines
11KB

  1. import io
  2. import json as _json
  3. import typing as t
  4. import uuid
  5. import warnings
  6. from datetime import date
  7. from jinja2.utils import htmlsafe_json_dumps as _jinja_htmlsafe_dumps
  8. from werkzeug.http import http_date
  9. from ..globals import current_app
  10. from ..globals import request
  11. if t.TYPE_CHECKING:
  12. from ..app import Flask
  13. from ..wrappers import Response
  14. try:
  15. import dataclasses
  16. except ImportError:
  17. # Python < 3.7
  18. dataclasses = None # type: ignore
  19. class JSONEncoder(_json.JSONEncoder):
  20. """The default JSON encoder. Handles extra types compared to the
  21. built-in :class:`json.JSONEncoder`.
  22. - :class:`datetime.datetime` and :class:`datetime.date` are
  23. serialized to :rfc:`822` strings. This is the same as the HTTP
  24. date format.
  25. - :class:`uuid.UUID` is serialized to a string.
  26. - :class:`dataclasses.dataclass` is passed to
  27. :func:`dataclasses.asdict`.
  28. - :class:`~markupsafe.Markup` (or any object with a ``__html__``
  29. method) will call the ``__html__`` method to get a string.
  30. Assign a subclass of this to :attr:`flask.Flask.json_encoder` or
  31. :attr:`flask.Blueprint.json_encoder` to override the default.
  32. """
  33. def default(self, o: t.Any) -> t.Any:
  34. """Convert ``o`` to a JSON serializable type. See
  35. :meth:`json.JSONEncoder.default`. Python does not support
  36. overriding how basic types like ``str`` or ``list`` are
  37. serialized, they are handled before this method.
  38. """
  39. if isinstance(o, date):
  40. return http_date(o)
  41. if isinstance(o, uuid.UUID):
  42. return str(o)
  43. if dataclasses and dataclasses.is_dataclass(o):
  44. return dataclasses.asdict(o)
  45. if hasattr(o, "__html__"):
  46. return str(o.__html__())
  47. return super().default(o)
  48. class JSONDecoder(_json.JSONDecoder):
  49. """The default JSON decoder.
  50. This does not change any behavior from the built-in
  51. :class:`json.JSONDecoder`.
  52. Assign a subclass of this to :attr:`flask.Flask.json_decoder` or
  53. :attr:`flask.Blueprint.json_decoder` to override the default.
  54. """
  55. def _dump_arg_defaults(
  56. kwargs: t.Dict[str, t.Any], app: t.Optional["Flask"] = None
  57. ) -> None:
  58. """Inject default arguments for dump functions."""
  59. if app is None:
  60. app = current_app
  61. if app:
  62. cls = app.json_encoder
  63. bp = app.blueprints.get(request.blueprint) if request else None # type: ignore
  64. if bp is not None and bp.json_encoder is not None:
  65. cls = bp.json_encoder
  66. kwargs.setdefault("cls", cls)
  67. kwargs.setdefault("ensure_ascii", app.config["JSON_AS_ASCII"])
  68. kwargs.setdefault("sort_keys", app.config["JSON_SORT_KEYS"])
  69. else:
  70. kwargs.setdefault("sort_keys", True)
  71. kwargs.setdefault("cls", JSONEncoder)
  72. def _load_arg_defaults(
  73. kwargs: t.Dict[str, t.Any], app: t.Optional["Flask"] = None
  74. ) -> None:
  75. """Inject default arguments for load functions."""
  76. if app is None:
  77. app = current_app
  78. if app:
  79. cls = app.json_decoder
  80. bp = app.blueprints.get(request.blueprint) if request else None # type: ignore
  81. if bp is not None and bp.json_decoder is not None:
  82. cls = bp.json_decoder
  83. kwargs.setdefault("cls", cls)
  84. else:
  85. kwargs.setdefault("cls", JSONDecoder)
  86. def dumps(obj: t.Any, app: t.Optional["Flask"] = None, **kwargs: t.Any) -> str:
  87. """Serialize an object to a string of JSON.
  88. Takes the same arguments as the built-in :func:`json.dumps`, with
  89. some defaults from application configuration.
  90. :param obj: Object to serialize to JSON.
  91. :param app: Use this app's config instead of the active app context
  92. or defaults.
  93. :param kwargs: Extra arguments passed to :func:`json.dumps`.
  94. .. versionchanged:: 2.0
  95. ``encoding`` is deprecated and will be removed in Flask 2.1.
  96. .. versionchanged:: 1.0.3
  97. ``app`` can be passed directly, rather than requiring an app
  98. context for configuration.
  99. """
  100. _dump_arg_defaults(kwargs, app=app)
  101. encoding = kwargs.pop("encoding", None)
  102. rv = _json.dumps(obj, **kwargs)
  103. if encoding is not None:
  104. warnings.warn(
  105. "'encoding' is deprecated and will be removed in Flask 2.1.",
  106. DeprecationWarning,
  107. stacklevel=2,
  108. )
  109. if isinstance(rv, str):
  110. return rv.encode(encoding) # type: ignore
  111. return rv
  112. def dump(
  113. obj: t.Any, fp: t.IO[str], app: t.Optional["Flask"] = None, **kwargs: t.Any
  114. ) -> None:
  115. """Serialize an object to JSON written to a file object.
  116. Takes the same arguments as the built-in :func:`json.dump`, with
  117. some defaults from application configuration.
  118. :param obj: Object to serialize to JSON.
  119. :param fp: File object to write JSON to.
  120. :param app: Use this app's config instead of the active app context
  121. or defaults.
  122. :param kwargs: Extra arguments passed to :func:`json.dump`.
  123. .. versionchanged:: 2.0
  124. Writing to a binary file, and the ``encoding`` argument, is
  125. deprecated and will be removed in Flask 2.1.
  126. """
  127. _dump_arg_defaults(kwargs, app=app)
  128. encoding = kwargs.pop("encoding", None)
  129. show_warning = encoding is not None
  130. try:
  131. fp.write("")
  132. except TypeError:
  133. show_warning = True
  134. fp = io.TextIOWrapper(fp, encoding or "utf-8") # type: ignore
  135. if show_warning:
  136. warnings.warn(
  137. "Writing to a binary file, and the 'encoding' argument, is"
  138. " deprecated and will be removed in Flask 2.1.",
  139. DeprecationWarning,
  140. stacklevel=2,
  141. )
  142. _json.dump(obj, fp, **kwargs)
  143. def loads(s: str, app: t.Optional["Flask"] = None, **kwargs: t.Any) -> t.Any:
  144. """Deserialize an object from a string of JSON.
  145. Takes the same arguments as the built-in :func:`json.loads`, with
  146. some defaults from application configuration.
  147. :param s: JSON string to deserialize.
  148. :param app: Use this app's config instead of the active app context
  149. or defaults.
  150. :param kwargs: Extra arguments passed to :func:`json.loads`.
  151. .. versionchanged:: 2.0
  152. ``encoding`` is deprecated and will be removed in Flask 2.1. The
  153. data must be a string or UTF-8 bytes.
  154. .. versionchanged:: 1.0.3
  155. ``app`` can be passed directly, rather than requiring an app
  156. context for configuration.
  157. """
  158. _load_arg_defaults(kwargs, app=app)
  159. encoding = kwargs.pop("encoding", None)
  160. if encoding is not None:
  161. warnings.warn(
  162. "'encoding' is deprecated and will be removed in Flask 2.1."
  163. " The data must be a string or UTF-8 bytes.",
  164. DeprecationWarning,
  165. stacklevel=2,
  166. )
  167. if isinstance(s, bytes):
  168. s = s.decode(encoding)
  169. return _json.loads(s, **kwargs)
  170. def load(fp: t.IO[str], app: t.Optional["Flask"] = None, **kwargs: t.Any) -> t.Any:
  171. """Deserialize an object from JSON read from a file object.
  172. Takes the same arguments as the built-in :func:`json.load`, with
  173. some defaults from application configuration.
  174. :param fp: File object to read JSON from.
  175. :param app: Use this app's config instead of the active app context
  176. or defaults.
  177. :param kwargs: Extra arguments passed to :func:`json.load`.
  178. .. versionchanged:: 2.0
  179. ``encoding`` is deprecated and will be removed in Flask 2.1. The
  180. file must be text mode, or binary mode with UTF-8 bytes.
  181. """
  182. _load_arg_defaults(kwargs, app=app)
  183. encoding = kwargs.pop("encoding", None)
  184. if encoding is not None:
  185. warnings.warn(
  186. "'encoding' is deprecated and will be removed in Flask 2.1."
  187. " The file must be text mode, or binary mode with UTF-8"
  188. " bytes.",
  189. DeprecationWarning,
  190. stacklevel=2,
  191. )
  192. if isinstance(fp.read(0), bytes):
  193. fp = io.TextIOWrapper(fp, encoding) # type: ignore
  194. return _json.load(fp, **kwargs)
  195. def htmlsafe_dumps(obj: t.Any, **kwargs: t.Any) -> str:
  196. """Serialize an object to a string of JSON with :func:`dumps`, then
  197. replace HTML-unsafe characters with Unicode escapes and mark the
  198. result safe with :class:`~markupsafe.Markup`.
  199. This is available in templates as the ``|tojson`` filter.
  200. The returned string is safe to render in HTML documents and
  201. ``<script>`` tags. The exception is in HTML attributes that are
  202. double quoted; either use single quotes or the ``|forceescape``
  203. filter.
  204. .. versionchanged:: 2.0
  205. Uses :func:`jinja2.utils.htmlsafe_json_dumps`. The returned
  206. value is marked safe by wrapping in :class:`~markupsafe.Markup`.
  207. .. versionchanged:: 0.10
  208. Single quotes are escaped, making this safe to use in HTML,
  209. ``<script>`` tags, and single-quoted attributes without further
  210. escaping.
  211. """
  212. return _jinja_htmlsafe_dumps(obj, dumps=dumps, **kwargs)
  213. def htmlsafe_dump(obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None:
  214. """Serialize an object to JSON written to a file object, replacing
  215. HTML-unsafe characters with Unicode escapes. See
  216. :func:`htmlsafe_dumps` and :func:`dumps`.
  217. """
  218. fp.write(htmlsafe_dumps(obj, **kwargs))
  219. def jsonify(*args: t.Any, **kwargs: t.Any) -> "Response":
  220. """Serialize data to JSON and wrap it in a :class:`~flask.Response`
  221. with the :mimetype:`application/json` mimetype.
  222. Uses :func:`dumps` to serialize the data, but ``args`` and
  223. ``kwargs`` are treated as data rather than arguments to
  224. :func:`json.dumps`.
  225. 1. Single argument: Treated as a single value.
  226. 2. Multiple arguments: Treated as a list of values.
  227. ``jsonify(1, 2, 3)`` is the same as ``jsonify([1, 2, 3])``.
  228. 3. Keyword arguments: Treated as a dict of values.
  229. ``jsonify(data=data, errors=errors)`` is the same as
  230. ``jsonify({"data": data, "errors": errors})``.
  231. 4. Passing both arguments and keyword arguments is not allowed as
  232. it's not clear what should happen.
  233. .. code-block:: python
  234. from flask import jsonify
  235. @app.route("/users/me")
  236. def get_current_user():
  237. return jsonify(
  238. username=g.user.username,
  239. email=g.user.email,
  240. id=g.user.id,
  241. )
  242. Will return a JSON response like this:
  243. .. code-block:: javascript
  244. {
  245. "username": "admin",
  246. "email": "admin@localhost",
  247. "id": 42
  248. }
  249. The default output omits indents and spaces after separators. In
  250. debug mode or if :data:`JSONIFY_PRETTYPRINT_REGULAR` is ``True``,
  251. the output will be formatted to be easier to read.
  252. .. versionchanged:: 0.11
  253. Added support for serializing top-level arrays. This introduces
  254. a security risk in ancient browsers. See :ref:`security-json`.
  255. .. versionadded:: 0.2
  256. """
  257. indent = None
  258. separators = (",", ":")
  259. if current_app.config["JSONIFY_PRETTYPRINT_REGULAR"] or current_app.debug:
  260. indent = 2
  261. separators = (", ", ": ")
  262. if args and kwargs:
  263. raise TypeError("jsonify() behavior undefined when passed both args and kwargs")
  264. elif len(args) == 1: # single args are passed directly to dumps()
  265. data = args[0]
  266. else:
  267. data = args or kwargs
  268. return current_app.response_class(
  269. f"{dumps(data, indent=indent, separators=separators)}\n",
  270. mimetype=current_app.config["JSONIFY_MIMETYPE"],
  271. )