add json provider interface
This commit is contained in:
parent
c356c6da5f
commit
69f9845ef2
13 changed files with 662 additions and 282 deletions
15
CHANGES.rst
15
CHANGES.rst
|
|
@ -32,7 +32,22 @@ Unreleased
|
||||||
``Flask.aborter_class`` and ``Flask.make_aborter`` can be used
|
``Flask.aborter_class`` and ``Flask.make_aborter`` can be used
|
||||||
to customize this aborter. :issue:`4567`
|
to customize this aborter. :issue:`4567`
|
||||||
- ``flask.redirect`` will call ``app.redirect``. :issue:`4569`
|
- ``flask.redirect`` will call ``app.redirect``. :issue:`4569`
|
||||||
|
- ``flask.json`` is an instance of ``JSONProvider``. A different
|
||||||
|
provider can be set to use a different JSON library.
|
||||||
|
``flask.jsonify`` will call ``app.json.response``, other
|
||||||
|
functions in ``flask.json`` will call corresponding functions in
|
||||||
|
``app.json``. :pr:`4688`
|
||||||
|
|
||||||
|
- JSON configuration is moved to attributes on the default
|
||||||
|
``app.json`` provider. ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``,
|
||||||
|
``JSONIFY_MIMETYPE``, and ``JSONIFY_PRETTYPRINT_REGULAR`` are
|
||||||
|
deprecated. :pr:`4688`
|
||||||
|
- Setting custom ``json_encoder`` and ``json_decoder`` classes on the
|
||||||
|
app or a blueprint, and the corresponding ``json.JSONEncoder`` and
|
||||||
|
``JSONDecoder`` classes, are deprecated. JSON behavior can now be
|
||||||
|
overridden using the ``app.json`` provider interface. :pr:`4688`
|
||||||
|
- ``json.htmlsafe_dumps`` and ``json.htmlsafe_dump`` are deprecated,
|
||||||
|
the function is built-in to Jinja now. :pr:`4688`
|
||||||
- Refactor ``register_error_handler`` to consolidate error checking.
|
- Refactor ``register_error_handler`` to consolidate error checking.
|
||||||
Rewrite some error messages to be more consistent. :issue:`4559`
|
Rewrite some error messages to be more consistent. :issue:`4559`
|
||||||
- Use Blueprint decorators and functions intended for setup after
|
- Use Blueprint decorators and functions intended for setup after
|
||||||
|
|
|
||||||
30
docs/api.rst
30
docs/api.rst
|
|
@ -236,21 +236,15 @@ JSON Support
|
||||||
|
|
||||||
.. module:: flask.json
|
.. module:: flask.json
|
||||||
|
|
||||||
Flask uses the built-in :mod:`json` module for handling JSON. It will
|
Flask uses Python's built-in :mod:`json` module for handling JSON by
|
||||||
use the current blueprint's or application's JSON encoder and decoder
|
default. The JSON implementation can be changed by assigning a different
|
||||||
for easier customization. By default it handles some extra data types:
|
provider to :attr:`flask.Flask.json_provider_class` or
|
||||||
|
:attr:`flask.Flask.json`. The functions provided by ``flask.json`` will
|
||||||
|
use methods on ``app.json`` if an app context is active.
|
||||||
|
|
||||||
- :class:`datetime.datetime` and :class:`datetime.date` are serialized
|
Jinja's ``|tojson`` filter is configured to use the app's JSON provider.
|
||||||
to :rfc:`822` strings. This is the same as the HTTP date format.
|
The filter marks the output with ``|safe``. Use it to render data inside
|
||||||
- :class:`uuid.UUID` is serialized to a string.
|
HTML ``<script>`` tags.
|
||||||
- :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.
|
|
||||||
|
|
||||||
Jinja's ``|tojson`` filter is configured to use Flask's :func:`dumps`
|
|
||||||
function. The filter marks the output with ``|safe`` automatically. Use
|
|
||||||
the filter to render data inside ``<script>`` tags.
|
|
||||||
|
|
||||||
.. sourcecode:: html+jinja
|
.. sourcecode:: html+jinja
|
||||||
|
|
||||||
|
|
@ -269,6 +263,14 @@ the filter to render data inside ``<script>`` tags.
|
||||||
|
|
||||||
.. autofunction:: load
|
.. autofunction:: load
|
||||||
|
|
||||||
|
.. autoclass:: flask.json.provider.JSONProvider
|
||||||
|
:members:
|
||||||
|
:member-order: bysource
|
||||||
|
|
||||||
|
.. autoclass:: flask.json.provider.DefaultJSONProvider
|
||||||
|
:members:
|
||||||
|
:member-order: bysource
|
||||||
|
|
||||||
.. autoclass:: JSONEncoder
|
.. autoclass:: JSONEncoder
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -301,6 +301,10 @@ The following configuration values are used internally by Flask:
|
||||||
|
|
||||||
Default: ``True``
|
Default: ``True``
|
||||||
|
|
||||||
|
.. deprecated:: 2.2
|
||||||
|
Will be removed in Flask 2.3. Set ``app.json.ensure_ascii``
|
||||||
|
instead.
|
||||||
|
|
||||||
.. py:data:: JSON_SORT_KEYS
|
.. py:data:: JSON_SORT_KEYS
|
||||||
|
|
||||||
Sort the keys of JSON objects alphabetically. This is useful for caching
|
Sort the keys of JSON objects alphabetically. This is useful for caching
|
||||||
|
|
@ -310,19 +314,30 @@ The following configuration values are used internally by Flask:
|
||||||
|
|
||||||
Default: ``True``
|
Default: ``True``
|
||||||
|
|
||||||
|
.. deprecated:: 2.2
|
||||||
|
Will be removed in Flask 2.3. Set ``app.json.sort_keys``
|
||||||
|
instead.
|
||||||
|
|
||||||
.. py:data:: JSONIFY_PRETTYPRINT_REGULAR
|
.. py:data:: JSONIFY_PRETTYPRINT_REGULAR
|
||||||
|
|
||||||
``jsonify`` responses will be output with newlines, spaces, and indentation
|
:func:`~flask.jsonify` responses will be output with newlines,
|
||||||
for easier reading by humans. Always enabled in debug mode.
|
spaces, and indentation for easier reading by humans. Always enabled
|
||||||
|
in debug mode.
|
||||||
|
|
||||||
Default: ``False``
|
Default: ``False``
|
||||||
|
|
||||||
|
.. deprecated:: 2.2
|
||||||
|
Will be removed in Flask 2.3. Set ``app.json.compact`` instead.
|
||||||
|
|
||||||
.. py:data:: JSONIFY_MIMETYPE
|
.. py:data:: JSONIFY_MIMETYPE
|
||||||
|
|
||||||
The mimetype of ``jsonify`` responses.
|
The mimetype of ``jsonify`` responses.
|
||||||
|
|
||||||
Default: ``'application/json'``
|
Default: ``'application/json'``
|
||||||
|
|
||||||
|
.. deprecated:: 2.2
|
||||||
|
Will be removed in Flask 2.3. Set ``app.json.mimetype`` instead.
|
||||||
|
|
||||||
.. py:data:: TEMPLATES_AUTO_RELOAD
|
.. py:data:: TEMPLATES_AUTO_RELOAD
|
||||||
|
|
||||||
Reload templates when they are changed. If not set, it will be enabled in
|
Reload templates when they are changed. If not set, it will be enabled in
|
||||||
|
|
@ -387,6 +402,12 @@ The following configuration values are used internally by Flask:
|
||||||
.. versionchanged:: 2.2
|
.. versionchanged:: 2.2
|
||||||
Removed ``PRESERVE_CONTEXT_ON_EXCEPTION``.
|
Removed ``PRESERVE_CONTEXT_ON_EXCEPTION``.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.2
|
||||||
|
``JSON_AS_ASCII``, ``JSON_SORT_KEYS``,
|
||||||
|
``JSONIFY_MIMETYPE``, and ``JSONIFY_PRETTYPRINT_REGULAR`` will be
|
||||||
|
removed in Flask 2.3. The default ``app.json`` provider has
|
||||||
|
equivalent attributes instead.
|
||||||
|
|
||||||
|
|
||||||
Configuring from Python Files
|
Configuring from Python Files
|
||||||
-----------------------------
|
-----------------------------
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ from werkzeug.utils import redirect as _wz_redirect
|
||||||
from werkzeug.wrappers import Response as BaseResponse
|
from werkzeug.wrappers import Response as BaseResponse
|
||||||
|
|
||||||
from . import cli
|
from . import cli
|
||||||
from . import json
|
|
||||||
from . import typing as ft
|
from . import typing as ft
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .config import ConfigAttribute
|
from .config import ConfigAttribute
|
||||||
|
|
@ -50,7 +49,8 @@ from .helpers import get_env
|
||||||
from .helpers import get_flashed_messages
|
from .helpers import get_flashed_messages
|
||||||
from .helpers import get_load_dotenv
|
from .helpers import get_load_dotenv
|
||||||
from .helpers import locked_cached_property
|
from .helpers import locked_cached_property
|
||||||
from .json import jsonify
|
from .json.provider import DefaultJSONProvider
|
||||||
|
from .json.provider import JSONProvider
|
||||||
from .logging import create_logger
|
from .logging import create_logger
|
||||||
from .scaffold import _endpoint_from_view_func
|
from .scaffold import _endpoint_from_view_func
|
||||||
from .scaffold import _sentinel
|
from .scaffold import _sentinel
|
||||||
|
|
@ -315,15 +315,37 @@ class Flask(Scaffold):
|
||||||
#: ``USE_X_SENDFILE`` configuration key. Defaults to ``False``.
|
#: ``USE_X_SENDFILE`` configuration key. Defaults to ``False``.
|
||||||
use_x_sendfile = ConfigAttribute("USE_X_SENDFILE")
|
use_x_sendfile = ConfigAttribute("USE_X_SENDFILE")
|
||||||
|
|
||||||
#: The JSON encoder class to use. Defaults to :class:`~flask.json.JSONEncoder`.
|
#: The JSON encoder class to use. Defaults to
|
||||||
|
#: :class:`~flask.json.JSONEncoder`.
|
||||||
|
#:
|
||||||
|
#: .. deprecated:: 2.2
|
||||||
|
#: Will be removed in Flask 2.3. Customize
|
||||||
|
#: :attr:`json_provider_class` instead.
|
||||||
#:
|
#:
|
||||||
#: .. versionadded:: 0.10
|
#: .. versionadded:: 0.10
|
||||||
json_encoder = json.JSONEncoder
|
json_encoder: None = None
|
||||||
|
|
||||||
#: The JSON decoder class to use. Defaults to :class:`~flask.json.JSONDecoder`.
|
#: The JSON decoder class to use. Defaults to
|
||||||
|
#: :class:`~flask.json.JSONDecoder`.
|
||||||
|
#:
|
||||||
|
#: .. deprecated:: 2.2
|
||||||
|
#: Will be removed in Flask 2.3. Customize
|
||||||
|
#: :attr:`json_provider_class` instead.
|
||||||
#:
|
#:
|
||||||
#: .. versionadded:: 0.10
|
#: .. versionadded:: 0.10
|
||||||
json_decoder = json.JSONDecoder
|
json_decoder: None = None
|
||||||
|
|
||||||
|
json_provider_class: t.Type[JSONProvider] = DefaultJSONProvider
|
||||||
|
"""A subclass of :class:`~flask.json.provider.JSONProvider`. An
|
||||||
|
instance is created and assigned to :attr:`app.json` when creating
|
||||||
|
the app.
|
||||||
|
|
||||||
|
The default, :class:`~flask.json.provider.DefaultJSONProvider`, uses
|
||||||
|
Python's built-in :mod:`json` library. A different provider can use
|
||||||
|
a different JSON library.
|
||||||
|
|
||||||
|
.. versionadded:: 2.2
|
||||||
|
"""
|
||||||
|
|
||||||
#: Options that are passed to the Jinja environment in
|
#: Options that are passed to the Jinja environment in
|
||||||
#: :meth:`create_jinja_environment`. Changing these options after
|
#: :meth:`create_jinja_environment`. Changing these options after
|
||||||
|
|
@ -361,10 +383,10 @@ class Flask(Scaffold):
|
||||||
"TRAP_HTTP_EXCEPTIONS": False,
|
"TRAP_HTTP_EXCEPTIONS": False,
|
||||||
"EXPLAIN_TEMPLATE_LOADING": False,
|
"EXPLAIN_TEMPLATE_LOADING": False,
|
||||||
"PREFERRED_URL_SCHEME": "http",
|
"PREFERRED_URL_SCHEME": "http",
|
||||||
"JSON_AS_ASCII": True,
|
"JSON_AS_ASCII": None,
|
||||||
"JSON_SORT_KEYS": True,
|
"JSON_SORT_KEYS": None,
|
||||||
"JSONIFY_PRETTYPRINT_REGULAR": False,
|
"JSONIFY_PRETTYPRINT_REGULAR": None,
|
||||||
"JSONIFY_MIMETYPE": "application/json",
|
"JSONIFY_MIMETYPE": None,
|
||||||
"TEMPLATES_AUTO_RELOAD": None,
|
"TEMPLATES_AUTO_RELOAD": None,
|
||||||
"MAX_COOKIE_SIZE": 4093,
|
"MAX_COOKIE_SIZE": 4093,
|
||||||
}
|
}
|
||||||
|
|
@ -449,6 +471,22 @@ class Flask(Scaffold):
|
||||||
#: Moved from ``flask.abort``, which calls this object.
|
#: Moved from ``flask.abort``, which calls this object.
|
||||||
self.aborter = self.make_aborter()
|
self.aborter = self.make_aborter()
|
||||||
|
|
||||||
|
self.json: JSONProvider = self.json_provider_class(self)
|
||||||
|
"""Provides access to JSON methods. Functions in ``flask.json``
|
||||||
|
will call methods on this provider when the application context
|
||||||
|
is active. Used for handling JSON requests and responses.
|
||||||
|
|
||||||
|
An instance of :attr:`json_provider_class`. Can be customized by
|
||||||
|
changing that attribute on a subclass, or by assigning to this
|
||||||
|
attribute afterwards.
|
||||||
|
|
||||||
|
The default, :class:`~flask.json.provider.DefaultJSONProvider`,
|
||||||
|
uses Python's built-in :mod:`json` library. A different provider
|
||||||
|
can use a different JSON library.
|
||||||
|
|
||||||
|
.. versionadded:: 2.2
|
||||||
|
"""
|
||||||
|
|
||||||
#: A list of functions that are called by
|
#: A list of functions that are called by
|
||||||
#: :meth:`handle_url_build_error` when :meth:`.url_for` raises a
|
#: :meth:`handle_url_build_error` when :meth:`.url_for` raises a
|
||||||
#: :exc:`~werkzeug.routing.BuildError`. Each function is called
|
#: :exc:`~werkzeug.routing.BuildError`. Each function is called
|
||||||
|
|
@ -745,7 +783,7 @@ class Flask(Scaffold):
|
||||||
session=session,
|
session=session,
|
||||||
g=g,
|
g=g,
|
||||||
)
|
)
|
||||||
rv.policies["json.dumps_function"] = json.dumps
|
rv.policies["json.dumps_function"] = self.json.dumps
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
def create_global_jinja_loader(self) -> DispatchingJinjaLoader:
|
def create_global_jinja_loader(self) -> DispatchingJinjaLoader:
|
||||||
|
|
@ -1926,7 +1964,7 @@ class Flask(Scaffold):
|
||||||
)
|
)
|
||||||
status = headers = None
|
status = headers = None
|
||||||
elif isinstance(rv, (dict, list)):
|
elif isinstance(rv, (dict, list)):
|
||||||
rv = jsonify(rv)
|
rv = self.json.response(rv)
|
||||||
elif isinstance(rv, BaseResponse) or callable(rv):
|
elif isinstance(rv, BaseResponse) or callable(rv):
|
||||||
# evaluate a WSGI callable, or coerce a different response
|
# evaluate a WSGI callable, or coerce a different response
|
||||||
# class to the correct type
|
# class to the correct type
|
||||||
|
|
|
||||||
|
|
@ -174,10 +174,16 @@ class Blueprint(Scaffold):
|
||||||
|
|
||||||
#: Blueprint local JSON encoder class to use. Set to ``None`` to use
|
#: Blueprint local JSON encoder class to use. Set to ``None`` to use
|
||||||
#: the app's :class:`~flask.Flask.json_encoder`.
|
#: the app's :class:`~flask.Flask.json_encoder`.
|
||||||
json_encoder = None
|
#:
|
||||||
|
#: .. deprecated:: 2.2
|
||||||
|
#: Will be removed in Flask 2.3.
|
||||||
|
json_encoder: None = None
|
||||||
#: Blueprint local JSON decoder class to use. Set to ``None`` to use
|
#: Blueprint local JSON decoder class to use. Set to ``None`` to use
|
||||||
#: the app's :class:`~flask.Flask.json_decoder`.
|
#: the app's :class:`~flask.Flask.json_decoder`.
|
||||||
json_decoder = None
|
#:
|
||||||
|
#: .. deprecated:: 2.2
|
||||||
|
#: Will be removed in Flask 2.3.
|
||||||
|
json_decoder: None = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|
|
||||||
|
|
@ -307,6 +307,7 @@ class RequestContext:
|
||||||
self.app = app
|
self.app = app
|
||||||
if request is None:
|
if request is None:
|
||||||
request = app.request_class(environ)
|
request = app.request_class(environ)
|
||||||
|
request.json_module = app.json # type: ignore[misc]
|
||||||
self.request: Request = request
|
self.request: Request = request
|
||||||
self.url_adapter = None
|
self.url_adapter = None
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,12 @@
|
||||||
import dataclasses
|
from __future__ import annotations
|
||||||
import decimal
|
|
||||||
import json as _json
|
import json as _json
|
||||||
import typing as t
|
import typing as t
|
||||||
import uuid
|
|
||||||
from datetime import date
|
|
||||||
|
|
||||||
from jinja2.utils import htmlsafe_json_dumps as _jinja_htmlsafe_dumps
|
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 current_app
|
||||||
from ..globals import request
|
from .provider import _default
|
||||||
|
|
||||||
if t.TYPE_CHECKING: # pragma: no cover
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
from ..app import Flask
|
from ..app import Flask
|
||||||
|
|
@ -31,23 +28,30 @@ class JSONEncoder(_json.JSONEncoder):
|
||||||
|
|
||||||
Assign a subclass of this to :attr:`flask.Flask.json_encoder` or
|
Assign a subclass of this to :attr:`flask.Flask.json_encoder` or
|
||||||
:attr:`flask.Blueprint.json_encoder` to override the default.
|
:attr:`flask.Blueprint.json_encoder` to override the default.
|
||||||
|
|
||||||
|
.. deprecated:: 2.2
|
||||||
|
Will be removed in Flask 2.3. Use ``app.json`` instead.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs) -> None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'JSONEncoder' is deprecated and will be removed in"
|
||||||
|
" Flask 2.3. Use 'Flask.json' to provide an alternate"
|
||||||
|
" JSON implementation instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=3,
|
||||||
|
)
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
def default(self, o: t.Any) -> t.Any:
|
def default(self, o: t.Any) -> t.Any:
|
||||||
"""Convert ``o`` to a JSON serializable type. See
|
"""Convert ``o`` to a JSON serializable type. See
|
||||||
:meth:`json.JSONEncoder.default`. Python does not support
|
:meth:`json.JSONEncoder.default`. Python does not support
|
||||||
overriding how basic types like ``str`` or ``list`` are
|
overriding how basic types like ``str`` or ``list`` are
|
||||||
serialized, they are handled before this method.
|
serialized, they are handled before this method.
|
||||||
"""
|
"""
|
||||||
if isinstance(o, date):
|
return _default(o)
|
||||||
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):
|
class JSONDecoder(_json.JSONDecoder):
|
||||||
|
|
@ -58,144 +62,193 @@ class JSONDecoder(_json.JSONDecoder):
|
||||||
|
|
||||||
Assign a subclass of this to :attr:`flask.Flask.json_decoder` or
|
Assign a subclass of this to :attr:`flask.Flask.json_decoder` or
|
||||||
:attr:`flask.Blueprint.json_decoder` to override the default.
|
:attr:`flask.Blueprint.json_decoder` to override the default.
|
||||||
|
|
||||||
|
.. deprecated:: 2.2
|
||||||
|
Will be removed in Flask 2.3. Use ``app.json`` instead.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs) -> None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
def _dump_arg_defaults(
|
warnings.warn(
|
||||||
kwargs: t.Dict[str, t.Any], app: t.Optional["Flask"] = None
|
"'JSONDecoder' is deprecated and will be removed in"
|
||||||
) -> None:
|
" Flask 2.3. Use 'Flask.json' to provide an alternate"
|
||||||
"""Inject default arguments for dump functions."""
|
" JSON implementation instead.",
|
||||||
if app is None:
|
DeprecationWarning,
|
||||||
app = current_app
|
stacklevel=3,
|
||||||
|
)
|
||||||
if app:
|
super().__init__(**kwargs)
|
||||||
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(
|
def dumps(obj: t.Any, *, app: Flask | None = None, **kwargs: t.Any) -> str:
|
||||||
kwargs: t.Dict[str, t.Any], app: t.Optional["Flask"] = None
|
"""Serialize data as JSON.
|
||||||
) -> None:
|
|
||||||
"""Inject default arguments for load functions."""
|
|
||||||
if app is None:
|
|
||||||
app = current_app
|
|
||||||
|
|
||||||
if app:
|
If :data:`~flask.current_app` is available, it will use its
|
||||||
cls = app.json_decoder
|
:meth:`app.json.dumps() <flask.json.provider.JSONProvider.dumps>`
|
||||||
bp = app.blueprints.get(request.blueprint) if request else None # type: ignore
|
method, otherwise it will use :func:`json.dumps`.
|
||||||
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
|
:param obj: The data to serialize.
|
||||||
# faster on PyPy.
|
:param kwargs: Arguments passed to the ``dumps`` implementation.
|
||||||
if cls not in {JSONDecoder, _json.JSONDecoder}:
|
|
||||||
kwargs.setdefault("cls", cls)
|
|
||||||
|
|
||||||
|
.. versionchanged:: 2.2
|
||||||
|
Calls ``current_app.json.dumps``, allowing an app to override
|
||||||
|
the behavior.
|
||||||
|
|
||||||
def dumps(obj: t.Any, app: t.Optional["Flask"] = None, **kwargs: t.Any) -> str:
|
.. versionchanged:: 2.2
|
||||||
"""Serialize an object to a string of JSON.
|
The ``app`` parameter will be removed in Flask 2.3.
|
||||||
|
|
||||||
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
|
.. versionchanged:: 2.0.2
|
||||||
:class:`decimal.Decimal` is supported by converting to a string.
|
:class:`decimal.Decimal` is supported by converting to a string.
|
||||||
|
|
||||||
.. versionchanged:: 2.0
|
.. versionchanged:: 2.0
|
||||||
``encoding`` is deprecated and will be removed in Flask 2.1.
|
``encoding`` will be removed in Flask 2.1.
|
||||||
|
|
||||||
.. versionchanged:: 1.0.3
|
.. versionchanged:: 1.0.3
|
||||||
``app`` can be passed directly, rather than requiring an app
|
``app`` can be passed directly, rather than requiring an app
|
||||||
context for configuration.
|
context for configuration.
|
||||||
"""
|
"""
|
||||||
_dump_arg_defaults(kwargs, app=app)
|
if app is not None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"The 'app' parameter is deprecated and will be removed in"
|
||||||
|
" Flask 2.3. Call 'app.json.dumps' directly instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
app = current_app
|
||||||
|
|
||||||
|
if app:
|
||||||
|
return app.json.dumps(obj, **kwargs)
|
||||||
|
|
||||||
|
kwargs.setdefault("default", _default)
|
||||||
return _json.dumps(obj, **kwargs)
|
return _json.dumps(obj, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def dump(
|
def dump(
|
||||||
obj: t.Any, fp: t.IO[str], app: t.Optional["Flask"] = None, **kwargs: t.Any
|
obj: t.Any, fp: t.IO[str], *, app: Flask | None = None, **kwargs: t.Any
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Serialize an object to JSON written to a file object.
|
"""Serialize data as JSON and write to a file.
|
||||||
|
|
||||||
Takes the same arguments as the built-in :func:`json.dump`, with
|
If :data:`~flask.current_app` is available, it will use its
|
||||||
some defaults from application configuration.
|
:meth:`app.json.dump() <flask.json.provider.JSONProvider.dump>`
|
||||||
|
method, otherwise it will use :func:`json.dump`.
|
||||||
|
|
||||||
:param obj: Object to serialize to JSON.
|
:param obj: The data to serialize.
|
||||||
:param fp: File object to write JSON to.
|
:param fp: A file opened for writing text. Should use the UTF-8
|
||||||
:param app: Use this app's config instead of the active app context
|
encoding to be valid JSON.
|
||||||
or defaults.
|
:param kwargs: Arguments passed to the ``dump`` implementation.
|
||||||
:param kwargs: Extra arguments passed to :func:`json.dump`.
|
|
||||||
|
.. versionchanged:: 2.2
|
||||||
|
Calls ``current_app.json.dump``, allowing an app to override
|
||||||
|
the behavior.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.2
|
||||||
|
The ``app`` parameter will be removed in Flask 2.3.
|
||||||
|
|
||||||
.. versionchanged:: 2.0
|
.. versionchanged:: 2.0
|
||||||
Writing to a binary file, and the ``encoding`` argument, is
|
Writing to a binary file, and the ``encoding`` argument, will be
|
||||||
deprecated and will be removed in Flask 2.1.
|
removed in Flask 2.1.
|
||||||
"""
|
"""
|
||||||
_dump_arg_defaults(kwargs, app=app)
|
if app is not None:
|
||||||
_json.dump(obj, fp, **kwargs)
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"The 'app' parameter is deprecated and will be removed in"
|
||||||
|
" Flask 2.3. Call 'app.json.dump' directly instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
app = current_app
|
||||||
|
|
||||||
|
if app:
|
||||||
|
app.json.dump(obj, fp, **kwargs)
|
||||||
|
else:
|
||||||
|
kwargs.setdefault("default", _default)
|
||||||
|
_json.dump(obj, fp, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def loads(
|
def loads(s: str | bytes, *, app: Flask | None = None, **kwargs: t.Any) -> t.Any:
|
||||||
s: t.Union[str, bytes],
|
"""Deserialize data as JSON.
|
||||||
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
|
If :data:`~flask.current_app` is available, it will use its
|
||||||
some defaults from application configuration.
|
:meth:`app.json.loads() <flask.json.provider.JSONProvider.loads>`
|
||||||
|
method, otherwise it will use :func:`json.loads`.
|
||||||
|
|
||||||
:param s: JSON string to deserialize.
|
:param s: Text or UTF-8 bytes.
|
||||||
:param app: Use this app's config instead of the active app context
|
:param kwargs: Arguments passed to the ``loads`` implementation.
|
||||||
or defaults.
|
|
||||||
:param kwargs: Extra arguments passed to :func:`json.loads`.
|
.. versionchanged:: 2.2
|
||||||
|
Calls ``current_app.json.loads``, allowing an app to override
|
||||||
|
the behavior.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.2
|
||||||
|
The ``app`` parameter will be removed in Flask 2.3.
|
||||||
|
|
||||||
.. versionchanged:: 2.0
|
.. versionchanged:: 2.0
|
||||||
``encoding`` is deprecated and will be removed in Flask 2.1. The
|
``encoding`` will be removed in Flask 2.1. The data must be a
|
||||||
data must be a string or UTF-8 bytes.
|
string or UTF-8 bytes.
|
||||||
|
|
||||||
.. versionchanged:: 1.0.3
|
.. versionchanged:: 1.0.3
|
||||||
``app`` can be passed directly, rather than requiring an app
|
``app`` can be passed directly, rather than requiring an app
|
||||||
context for configuration.
|
context for configuration.
|
||||||
"""
|
"""
|
||||||
_load_arg_defaults(kwargs, app=app)
|
if app is not None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"The 'app' parameter is deprecated and will be removed in"
|
||||||
|
" Flask 2.3. Call 'app.json.loads' directly instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
app = current_app
|
||||||
|
|
||||||
|
if app:
|
||||||
|
return app.json.loads(s, **kwargs)
|
||||||
|
|
||||||
return _json.loads(s, **kwargs)
|
return _json.loads(s, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def load(fp: t.IO[str], app: t.Optional["Flask"] = None, **kwargs: t.Any) -> t.Any:
|
def load(fp: t.IO[t.AnyStr], *, app: Flask | None = None, **kwargs: t.Any) -> t.Any:
|
||||||
"""Deserialize an object from JSON read from a file object.
|
"""Deserialize data as JSON read from a file.
|
||||||
|
|
||||||
Takes the same arguments as the built-in :func:`json.load`, with
|
If :data:`~flask.current_app` is available, it will use its
|
||||||
some defaults from application configuration.
|
:meth:`app.json.load() <flask.json.provider.JSONProvider.load>`
|
||||||
|
method, otherwise it will use :func:`json.load`.
|
||||||
|
|
||||||
:param fp: File object to read JSON from.
|
:param fp: A file opened for reading text or UTF-8 bytes.
|
||||||
:param app: Use this app's config instead of the active app context
|
:param kwargs: Arguments passed to the ``load`` implementation.
|
||||||
or defaults.
|
|
||||||
:param kwargs: Extra arguments passed to :func:`json.load`.
|
.. versionchanged:: 2.2
|
||||||
|
Calls ``current_app.json.load``, allowing an app to override
|
||||||
|
the behavior.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.2
|
||||||
|
The ``app`` parameter will be removed in Flask 2.3.
|
||||||
|
|
||||||
.. versionchanged:: 2.0
|
.. versionchanged:: 2.0
|
||||||
``encoding`` is deprecated and will be removed in Flask 2.1. The
|
``encoding`` will be removed in Flask 2.1. The file must be text
|
||||||
file must be text mode, or binary mode with UTF-8 bytes.
|
mode, or binary mode with UTF-8 bytes.
|
||||||
"""
|
"""
|
||||||
_load_arg_defaults(kwargs, app=app)
|
if app is not None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"The 'app' parameter is deprecated and will be removed in"
|
||||||
|
" Flask 2.3. Call 'app.json.load' directly instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
app = current_app
|
||||||
|
|
||||||
|
if app:
|
||||||
|
return app.json.load(fp, **kwargs)
|
||||||
|
|
||||||
return _json.load(fp, **kwargs)
|
return _json.load(fp, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -211,6 +264,9 @@ def htmlsafe_dumps(obj: t.Any, **kwargs: t.Any) -> str:
|
||||||
double quoted; either use single quotes or the ``|forceescape``
|
double quoted; either use single quotes or the ``|forceescape``
|
||||||
filter.
|
filter.
|
||||||
|
|
||||||
|
.. deprecated:: 2.2
|
||||||
|
Will be removed in Flask 2.3. This is built-in to Jinja now.
|
||||||
|
|
||||||
.. versionchanged:: 2.0
|
.. versionchanged:: 2.0
|
||||||
Uses :func:`jinja2.utils.htmlsafe_json_dumps`. The returned
|
Uses :func:`jinja2.utils.htmlsafe_json_dumps`. The returned
|
||||||
value is marked safe by wrapping in :class:`~markupsafe.Markup`.
|
value is marked safe by wrapping in :class:`~markupsafe.Markup`.
|
||||||
|
|
@ -220,6 +276,14 @@ def htmlsafe_dumps(obj: t.Any, **kwargs: t.Any) -> str:
|
||||||
``<script>`` tags, and single-quoted attributes without further
|
``<script>`` tags, and single-quoted attributes without further
|
||||||
escaping.
|
escaping.
|
||||||
"""
|
"""
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'htmlsafe_dumps' is deprecated and will be removed in Flask"
|
||||||
|
" 2.3. Use 'jinja2.utils.htmlsafe_json_dumps' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
return _jinja_htmlsafe_dumps(obj, dumps=dumps, **kwargs)
|
return _jinja_htmlsafe_dumps(obj, dumps=dumps, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -227,77 +291,51 @@ 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
|
"""Serialize an object to JSON written to a file object, replacing
|
||||||
HTML-unsafe characters with Unicode escapes. See
|
HTML-unsafe characters with Unicode escapes. See
|
||||||
:func:`htmlsafe_dumps` and :func:`dumps`.
|
:func:`htmlsafe_dumps` and :func:`dumps`.
|
||||||
|
|
||||||
|
.. deprecated:: 2.2
|
||||||
|
Will be removed in Flask 2.3.
|
||||||
"""
|
"""
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'htmlsafe_dump' is deprecated and will be removed in Flask"
|
||||||
|
" 2.3. Use 'jinja2.utils.htmlsafe_json_dumps' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
fp.write(htmlsafe_dumps(obj, **kwargs))
|
fp.write(htmlsafe_dumps(obj, **kwargs))
|
||||||
|
|
||||||
|
|
||||||
def jsonify(*args: t.Any, **kwargs: t.Any) -> "Response":
|
def jsonify(*args: t.Any, **kwargs: t.Any) -> Response:
|
||||||
"""Serialize data to JSON and wrap it in a :class:`~flask.Response`
|
"""Serialize the given arguments as JSON, and return a
|
||||||
with the :mimetype:`application/json` mimetype.
|
:class:`~flask.Response` object with the ``application/json``
|
||||||
|
mimetype. A dict or list returned from a view will be converted to a
|
||||||
|
JSON response automatically without needing to call this.
|
||||||
|
|
||||||
Uses :func:`dumps` to serialize the data, but ``args`` and
|
This requires an active request or application context, and calls
|
||||||
``kwargs`` are treated as data rather than arguments to
|
:meth:`app.json.response() <flask.json.provider.JSONProvider.response>`.
|
||||||
:func:`json.dumps`.
|
|
||||||
|
|
||||||
1. Single argument: Treated as a single value.
|
In debug mode, the output is formatted with indentation to make it
|
||||||
2. Multiple arguments: Treated as a list of values.
|
easier to read. This may also be controlled by the provider.
|
||||||
``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
|
Either positional or keyword arguments can be given, not both.
|
||||||
|
If no arguments are given, ``None`` is serialized.
|
||||||
|
|
||||||
from flask import jsonify
|
:param args: A single value to serialize, or multiple values to
|
||||||
|
treat as a list to serialize.
|
||||||
|
:param kwargs: Treat as a dict to serialize.
|
||||||
|
|
||||||
@app.route("/users/me")
|
.. versionchanged:: 2.2
|
||||||
def get_current_user():
|
Calls ``current_app.json.response``, allowing an app to override
|
||||||
return jsonify(
|
the behavior.
|
||||||
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
|
.. versionchanged:: 2.0.2
|
||||||
:class:`decimal.Decimal` is supported by converting to a string.
|
:class:`decimal.Decimal` is supported by converting to a string.
|
||||||
|
|
||||||
.. versionchanged:: 0.11
|
.. versionchanged:: 0.11
|
||||||
Added support for serializing top-level arrays. This introduces
|
Added support for serializing top-level arrays. This was a
|
||||||
a security risk in ancient browsers. See :ref:`security-json`.
|
security risk in ancient browsers. See :ref:`security-json`.
|
||||||
|
|
||||||
.. versionadded:: 0.2
|
.. versionadded:: 0.2
|
||||||
"""
|
"""
|
||||||
indent = None
|
return current_app.json.response(*args, **kwargs)
|
||||||
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"],
|
|
||||||
)
|
|
||||||
|
|
|
||||||
310
src/flask/json/provider.py
Normal file
310
src/flask/json/provider.py
Normal file
|
|
@ -0,0 +1,310 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
|
import decimal
|
||||||
|
import json
|
||||||
|
import typing as t
|
||||||
|
import uuid
|
||||||
|
import weakref
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from werkzeug.http import http_date
|
||||||
|
|
||||||
|
from ..globals import request
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
|
from ..app import Flask
|
||||||
|
from ..wrappers import Response
|
||||||
|
|
||||||
|
|
||||||
|
class JSONProvider:
|
||||||
|
"""A standard set of JSON operations for an application. Subclasses
|
||||||
|
of this can be used to customize JSON behavior or use different
|
||||||
|
JSON libraries.
|
||||||
|
|
||||||
|
To implement a provider for a specific library, subclass this base
|
||||||
|
class and implement at least :meth:`dumps` and :meth:`loads`. All
|
||||||
|
other methods have default implementations.
|
||||||
|
|
||||||
|
To use a different provider, either subclass ``Flask`` and set
|
||||||
|
:attr:`~flask.Flask.json_provider_class` to a provider class, or set
|
||||||
|
:attr:`app.json <flask.Flask.json>` to an instance of the class.
|
||||||
|
|
||||||
|
:param app: An application instance. This will be stored as a
|
||||||
|
:class:`weakref.proxy` on the :attr:`_app` attribute.
|
||||||
|
|
||||||
|
.. versionadded:: 2.2
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, app: Flask) -> None:
|
||||||
|
self._app = weakref.proxy(app)
|
||||||
|
|
||||||
|
def dumps(self, obj: t.Any, **kwargs: t.Any) -> str:
|
||||||
|
"""Serialize data as JSON.
|
||||||
|
|
||||||
|
:param obj: The data to serialize.
|
||||||
|
:param kwargs: May be passed to the underlying JSON library.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def dump(self, obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None:
|
||||||
|
"""Serialize data as JSON and write to a file.
|
||||||
|
|
||||||
|
:param obj: The data to serialize.
|
||||||
|
:param fp: A file opened for writing text. Should use the UTF-8
|
||||||
|
encoding to be valid JSON.
|
||||||
|
:param kwargs: May be passed to the underlying JSON library.
|
||||||
|
"""
|
||||||
|
fp.write(self.dumps(obj, **kwargs))
|
||||||
|
|
||||||
|
def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any:
|
||||||
|
"""Deserialize data as JSON.
|
||||||
|
|
||||||
|
:param s: Text or UTF-8 bytes.
|
||||||
|
:param kwargs: May be passed to the underlying JSON library.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def load(self, fp: t.IO[t.AnyStr], **kwargs: t.Any) -> t.Any:
|
||||||
|
"""Deserialize data as JSON read from a file.
|
||||||
|
|
||||||
|
:param fp: A file opened for reading text or UTF-8 bytes.
|
||||||
|
:param kwargs: May be passed to the underlying JSON library.
|
||||||
|
"""
|
||||||
|
return self.loads(fp.read(), **kwargs)
|
||||||
|
|
||||||
|
def _prepare_response_obj(
|
||||||
|
self, args: t.Tuple[t.Any, ...], kwargs: t.Dict[str, t.Any]
|
||||||
|
) -> t.Any:
|
||||||
|
if args and kwargs:
|
||||||
|
raise TypeError("app.json.response() takes either args or kwargs, not both")
|
||||||
|
|
||||||
|
if not args and not kwargs:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if len(args) == 1:
|
||||||
|
return args[0]
|
||||||
|
|
||||||
|
return args or kwargs
|
||||||
|
|
||||||
|
def response(self, *args: t.Any, **kwargs: t.Any) -> Response:
|
||||||
|
"""Serialize the given arguments as JSON, and return a
|
||||||
|
:class:`~flask.Response` object with the ``application/json``
|
||||||
|
mimetype.
|
||||||
|
|
||||||
|
The :func:`~flask.json.jsonify` function calls this method for
|
||||||
|
the current application.
|
||||||
|
|
||||||
|
Either positional or keyword arguments can be given, not both.
|
||||||
|
If no arguments are given, ``None`` is serialized.
|
||||||
|
|
||||||
|
:param args: A single value to serialize, or multiple values to
|
||||||
|
treat as a list to serialize.
|
||||||
|
:param kwargs: Treat as a dict to serialize.
|
||||||
|
"""
|
||||||
|
obj = self._prepare_response_obj(args, kwargs)
|
||||||
|
return self._app.response_class(self.dumps(obj), mimetype="application/json")
|
||||||
|
|
||||||
|
|
||||||
|
def _default(o: t.Any) -> t.Any:
|
||||||
|
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__())
|
||||||
|
|
||||||
|
raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable")
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultJSONProvider(JSONProvider):
|
||||||
|
"""Provide JSON operations using Python's built-in :mod:`json`
|
||||||
|
library. Serializes the following additional data types:
|
||||||
|
|
||||||
|
- :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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
default: t.Callable[[t.Any], t.Any] = staticmethod(
|
||||||
|
_default
|
||||||
|
) # type: ignore[assignment]
|
||||||
|
"""Apply this function to any object that :meth:`json.dumps` does
|
||||||
|
not know how to serialize. It should return a valid JSON type or
|
||||||
|
raise a ``TypeError``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
ensure_ascii = True
|
||||||
|
"""Replace non-ASCII characters with escape sequences. This may be
|
||||||
|
more compatible with some clients, but can be disabled for better
|
||||||
|
performance and size.
|
||||||
|
"""
|
||||||
|
|
||||||
|
sort_keys = True
|
||||||
|
"""Sort the keys in any serialized dicts. This may be useful for
|
||||||
|
some caching situations, but can be disabled for better performance.
|
||||||
|
When enabled, keys must all be strings, they are not converted
|
||||||
|
before sorting.
|
||||||
|
"""
|
||||||
|
|
||||||
|
compact: bool | None = None
|
||||||
|
"""If ``True``, or ``None`` out of debug mode, the :meth:`response`
|
||||||
|
output will not add indentation, newlines, or spaces. If ``False``,
|
||||||
|
or ``None`` in debug mode, it will use a non-compact representation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
mimetype = "application/json"
|
||||||
|
"""The mimetype set in :meth:`response`."""
|
||||||
|
|
||||||
|
def dumps(self, obj: t.Any, **kwargs: t.Any) -> str:
|
||||||
|
"""Serialize data as JSON to a string.
|
||||||
|
|
||||||
|
Keyword arguments are passed to :func:`json.dumps`. Sets some
|
||||||
|
parameter defaults from the :attr:`default`,
|
||||||
|
:attr:`ensure_ascii`, and :attr:`sort_keys` attributes.
|
||||||
|
|
||||||
|
:param obj: The data to serialize.
|
||||||
|
:param kwargs: Passed to :func:`json.dumps`.
|
||||||
|
"""
|
||||||
|
cls = self._app.json_encoder
|
||||||
|
bp = self._app.blueprints.get(request.blueprint) if request else None
|
||||||
|
|
||||||
|
if bp is not None and bp.json_encoder is not None:
|
||||||
|
cls = bp.json_encoder
|
||||||
|
|
||||||
|
if cls is not None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"Setting 'json_encoder' on the app or a blueprint is"
|
||||||
|
" deprecated and will be removed in Flask 2.3."
|
||||||
|
" Customize 'app.json' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
kwargs.setdefault("cls", cls)
|
||||||
|
|
||||||
|
if "default" not in cls.__dict__:
|
||||||
|
kwargs.setdefault("default", self.default)
|
||||||
|
else:
|
||||||
|
kwargs.setdefault("default", self.default)
|
||||||
|
|
||||||
|
ensure_ascii = self._app.config["JSON_AS_ASCII"]
|
||||||
|
sort_keys = self._app.config["JSON_SORT_KEYS"]
|
||||||
|
|
||||||
|
if ensure_ascii is not None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"The 'JSON_AS_ASCII' config key is deprecated and will"
|
||||||
|
" be removed in Flask 2.3. Set 'app.json.ensure_ascii'"
|
||||||
|
" instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
ensure_ascii = self.ensure_ascii
|
||||||
|
|
||||||
|
if sort_keys is not None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"The 'JSON_SORT_KEYS' config key is deprecated and will"
|
||||||
|
" be removed in Flask 2.3. Set 'app.json.sort_keys'"
|
||||||
|
" instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
sort_keys = self.sort_keys
|
||||||
|
|
||||||
|
kwargs.setdefault("ensure_ascii", ensure_ascii)
|
||||||
|
kwargs.setdefault("sort_keys", sort_keys)
|
||||||
|
return json.dumps(obj, **kwargs)
|
||||||
|
|
||||||
|
def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any:
|
||||||
|
"""Deserialize data as JSON from a string or bytes.
|
||||||
|
|
||||||
|
:param s: Text or UTF-8 bytes.
|
||||||
|
:param kwargs: Passed to :func:`json.loads`.
|
||||||
|
"""
|
||||||
|
cls = self._app.json_decoder
|
||||||
|
bp = self._app.blueprints.get(request.blueprint) if request else None
|
||||||
|
|
||||||
|
if bp is not None and bp.json_decoder is not None:
|
||||||
|
cls = bp.json_decoder
|
||||||
|
|
||||||
|
if cls is not None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"Setting 'json_decoder' on the app or a blueprint is"
|
||||||
|
" deprecated and will be removed in Flask 2.3."
|
||||||
|
" Customize 'app.json' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
kwargs.setdefault("cls", cls)
|
||||||
|
|
||||||
|
return json.loads(s, **kwargs)
|
||||||
|
|
||||||
|
def response(self, *args: t.Any, **kwargs: t.Any) -> Response:
|
||||||
|
"""Serialize the given arguments as JSON, and return a
|
||||||
|
:class:`~flask.Response` object with it. The response mimetype
|
||||||
|
will be "application/json" and can be changed with
|
||||||
|
:attr:`mimetype`.
|
||||||
|
|
||||||
|
If :attr:`compact` is ``False`` or debug mode is enabled, the
|
||||||
|
output will be formatted to be easier to read.
|
||||||
|
|
||||||
|
Either positional or keyword arguments can be given, not both.
|
||||||
|
If no arguments are given, ``None`` is serialized.
|
||||||
|
|
||||||
|
:param args: A single value to serialize, or multiple values to
|
||||||
|
treat as a list to serialize.
|
||||||
|
:param kwargs: Treat as a dict to serialize.
|
||||||
|
"""
|
||||||
|
obj = self._prepare_response_obj(args, kwargs)
|
||||||
|
dump_args: t.Dict[str, t.Any] = {}
|
||||||
|
pretty = self._app.config["JSONIFY_PRETTYPRINT_REGULAR"]
|
||||||
|
mimetype = self._app.config["JSONIFY_MIMETYPE"]
|
||||||
|
|
||||||
|
if pretty is not None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"The 'JSONIFY_PRETTYPRINT_REGULAR' config key is"
|
||||||
|
" deprecated and will be removed in Flask 2.3. Set"
|
||||||
|
" 'app.json.compact' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
compact: bool | None = not pretty
|
||||||
|
else:
|
||||||
|
compact = self.compact
|
||||||
|
|
||||||
|
if (compact is None and self._app.debug) or compact is False:
|
||||||
|
dump_args.setdefault("indent", 2)
|
||||||
|
else:
|
||||||
|
dump_args.setdefault("separators", (",", ":"))
|
||||||
|
|
||||||
|
if mimetype is not None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"The 'JSONIFY_MIMETYPE' config key is deprecated and"
|
||||||
|
" will be removed in Flask 2.3. Set 'app.json.mimetype'"
|
||||||
|
" instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
mimetype = self.mimetype
|
||||||
|
|
||||||
|
return self._app.response_class(
|
||||||
|
f"{self.dumps(obj, **dump_args)}\n", mimetype=mimetype
|
||||||
|
)
|
||||||
|
|
@ -6,8 +6,6 @@ import sys
|
||||||
import typing as t
|
import typing as t
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from functools import update_wrapper
|
from functools import update_wrapper
|
||||||
from json import JSONDecoder
|
|
||||||
from json import JSONEncoder
|
|
||||||
|
|
||||||
from jinja2 import FileSystemLoader
|
from jinja2 import FileSystemLoader
|
||||||
from werkzeug.exceptions import default_exceptions
|
from werkzeug.exceptions import default_exceptions
|
||||||
|
|
@ -76,11 +74,17 @@ class Scaffold:
|
||||||
|
|
||||||
#: JSON encoder class used by :func:`flask.json.dumps`. If a
|
#: JSON encoder class used by :func:`flask.json.dumps`. If a
|
||||||
#: blueprint sets this, it will be used instead of the app's value.
|
#: blueprint sets this, it will be used instead of the app's value.
|
||||||
json_encoder: t.Optional[t.Type[JSONEncoder]] = None
|
#:
|
||||||
|
#: .. deprecated:: 2.2
|
||||||
|
#: Will be removed in Flask 2.3.
|
||||||
|
json_encoder: None = None
|
||||||
|
|
||||||
#: JSON decoder class used by :func:`flask.json.loads`. If a
|
#: JSON decoder class used by :func:`flask.json.loads`. If a
|
||||||
#: blueprint sets this, it will be used instead of the app's value.
|
#: blueprint sets this, it will be used instead of the app's value.
|
||||||
json_decoder: t.Optional[t.Type[JSONDecoder]] = None
|
#:
|
||||||
|
#: .. deprecated:: 2.2
|
||||||
|
#: Will be removed in Flask 2.3.
|
||||||
|
json_decoder: None = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ from werkzeug.wrappers import Request as BaseRequest
|
||||||
|
|
||||||
from .cli import ScriptInfo
|
from .cli import ScriptInfo
|
||||||
from .globals import _cv_request
|
from .globals import _cv_request
|
||||||
from .json import dumps as json_dumps
|
|
||||||
from .sessions import SessionMixin
|
from .sessions import SessionMixin
|
||||||
|
|
||||||
if t.TYPE_CHECKING: # pragma: no cover
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
|
|
@ -89,8 +88,7 @@ class EnvironBuilder(werkzeug.test.EnvironBuilder):
|
||||||
The serialization will be configured according to the config associated
|
The serialization will be configured according to the config associated
|
||||||
with this EnvironBuilder's ``app``.
|
with this EnvironBuilder's ``app``.
|
||||||
"""
|
"""
|
||||||
kwargs.setdefault("app", self.app)
|
return self.app.json.dumps(obj, **kwargs)
|
||||||
return json_dumps(obj, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class FlaskClient(Client):
|
class FlaskClient(Client):
|
||||||
|
|
@ -227,6 +225,7 @@ class FlaskClient(Client):
|
||||||
buffered=buffered,
|
buffered=buffered,
|
||||||
follow_redirects=follow_redirects,
|
follow_redirects=follow_redirects,
|
||||||
)
|
)
|
||||||
|
response.json_module = self.application.json # type: ignore[misc]
|
||||||
|
|
||||||
# Re-push contexts that were preserved during the request.
|
# Re-push contexts that were preserved during the request.
|
||||||
while self._new_contexts:
|
while self._new_contexts:
|
||||||
|
|
|
||||||
|
|
@ -1302,28 +1302,17 @@ def test_make_response_with_response_instance(app, req_ctx):
|
||||||
assert rv.headers["X-Foo"] == "bar"
|
assert rv.headers["X-Foo"] == "bar"
|
||||||
|
|
||||||
|
|
||||||
def test_jsonify_no_prettyprint(app, req_ctx):
|
@pytest.mark.parametrize("compact", [True, False])
|
||||||
app.config.update({"JSONIFY_PRETTYPRINT_REGULAR": False})
|
def test_jsonify_no_prettyprint(app, compact):
|
||||||
compressed_msg = b'{"msg":{"submsg":"W00t"},"msg2":"foobar"}\n'
|
app.json.compact = compact
|
||||||
uncompressed_msg = {"msg": {"submsg": "W00t"}, "msg2": "foobar"}
|
rv = app.json.response({"msg": {"submsg": "W00t"}, "msg2": "foobar"})
|
||||||
|
data = rv.data.strip()
|
||||||
rv = flask.make_response(flask.jsonify(uncompressed_msg), 200)
|
assert (b" " not in data) is compact
|
||||||
assert rv.data == compressed_msg
|
assert (b"\n" not in data) is compact
|
||||||
|
|
||||||
|
|
||||||
def test_jsonify_prettyprint(app, req_ctx):
|
|
||||||
app.config.update({"JSONIFY_PRETTYPRINT_REGULAR": True})
|
|
||||||
compressed_msg = {"msg": {"submsg": "W00t"}, "msg2": "foobar"}
|
|
||||||
pretty_response = (
|
|
||||||
b'{\n "msg": {\n "submsg": "W00t"\n }, \n "msg2": "foobar"\n}\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
rv = flask.make_response(flask.jsonify(compressed_msg), 200)
|
|
||||||
assert rv.data == pretty_response
|
|
||||||
|
|
||||||
|
|
||||||
def test_jsonify_mimetype(app, req_ctx):
|
def test_jsonify_mimetype(app, req_ctx):
|
||||||
app.config.update({"JSONIFY_MIMETYPE": "application/vnd.api+json"})
|
app.json.mimetype = "application/vnd.api+json"
|
||||||
msg = {"msg": {"submsg": "W00t"}}
|
msg = {"msg": {"submsg": "W00t"}}
|
||||||
rv = flask.make_response(flask.jsonify(msg), 200)
|
rv = flask.make_response(flask.jsonify(msg), 200)
|
||||||
assert rv.mimetype == "application/vnd.api+json"
|
assert rv.mimetype == "application/vnd.api+json"
|
||||||
|
|
@ -1333,15 +1322,15 @@ def test_json_dump_dataclass(app, req_ctx):
|
||||||
from dataclasses import make_dataclass
|
from dataclasses import make_dataclass
|
||||||
|
|
||||||
Data = make_dataclass("Data", [("name", str)])
|
Data = make_dataclass("Data", [("name", str)])
|
||||||
value = flask.json.dumps(Data("Flask"), app=app)
|
value = app.json.dumps(Data("Flask"))
|
||||||
value = flask.json.loads(value, app=app)
|
value = app.json.loads(value)
|
||||||
assert value == {"name": "Flask"}
|
assert value == {"name": "Flask"}
|
||||||
|
|
||||||
|
|
||||||
def test_jsonify_args_and_kwargs_check(app, req_ctx):
|
def test_jsonify_args_and_kwargs_check(app, req_ctx):
|
||||||
with pytest.raises(TypeError) as e:
|
with pytest.raises(TypeError) as e:
|
||||||
flask.jsonify("fake args", kwargs="fake")
|
flask.jsonify("fake args", kwargs="fake")
|
||||||
assert "behavior undefined" in str(e.value)
|
assert "args or kwargs" in str(e.value)
|
||||||
|
|
||||||
|
|
||||||
def test_url_generation(app, req_ctx):
|
def test_url_generation(app, req_ctx):
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from werkzeug.http import http_date
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
from flask import json
|
from flask import json
|
||||||
|
from flask.json.provider import DefaultJSONProvider
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("debug", (True, False))
|
@pytest.mark.parametrize("debug", (True, False))
|
||||||
|
|
@ -48,9 +49,8 @@ def test_json_custom_mimetypes(app, client):
|
||||||
"test_value,expected", [(True, '"\\u2603"'), (False, '"\u2603"')]
|
"test_value,expected", [(True, '"\\u2603"'), (False, '"\u2603"')]
|
||||||
)
|
)
|
||||||
def test_json_as_unicode(test_value, expected, app, app_ctx):
|
def test_json_as_unicode(test_value, expected, app, app_ctx):
|
||||||
|
app.json.ensure_ascii = test_value
|
||||||
app.config["JSON_AS_ASCII"] = test_value
|
rv = app.json.dumps("\N{SNOWMAN}")
|
||||||
rv = flask.json.dumps("\N{SNOWMAN}")
|
|
||||||
assert rv == expected
|
assert rv == expected
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -170,7 +170,7 @@ def test_jsonify_aware_datetimes(tz):
|
||||||
dt = datetime.datetime(2017, 1, 1, 12, 34, 56, tzinfo=tzinfo)
|
dt = datetime.datetime(2017, 1, 1, 12, 34, 56, tzinfo=tzinfo)
|
||||||
gmt = FixedOffset(hours=0, name="GMT")
|
gmt = FixedOffset(hours=0, name="GMT")
|
||||||
expected = dt.astimezone(gmt).strftime('"%a, %d %b %Y %H:%M:%S %Z"')
|
expected = dt.astimezone(gmt).strftime('"%a, %d %b %Y %H:%M:%S %Z"')
|
||||||
assert flask.json.JSONEncoder().encode(dt) == expected
|
assert flask.json.dumps(dt) == expected
|
||||||
|
|
||||||
|
|
||||||
def test_jsonify_uuid_types(app, client):
|
def test_jsonify_uuid_types(app, client):
|
||||||
|
|
@ -225,24 +225,25 @@ def test_json_customization(app, client):
|
||||||
def __init__(self, val):
|
def __init__(self, val):
|
||||||
self.val = val
|
self.val = val
|
||||||
|
|
||||||
class MyEncoder(flask.json.JSONEncoder):
|
def default(o):
|
||||||
def default(self, o):
|
if isinstance(o, X):
|
||||||
if isinstance(o, X):
|
return f"<{o.val}>"
|
||||||
return f"<{o.val}>"
|
|
||||||
return flask.json.JSONEncoder.default(self, o)
|
|
||||||
|
|
||||||
class MyDecoder(flask.json.JSONDecoder):
|
return DefaultJSONProvider.default(o)
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
kwargs.setdefault("object_hook", self.object_hook)
|
|
||||||
flask.json.JSONDecoder.__init__(self, *args, **kwargs)
|
|
||||||
|
|
||||||
|
class CustomProvider(DefaultJSONProvider):
|
||||||
def object_hook(self, obj):
|
def object_hook(self, obj):
|
||||||
if len(obj) == 1 and "_foo" in obj:
|
if len(obj) == 1 and "_foo" in obj:
|
||||||
return X(obj["_foo"])
|
return X(obj["_foo"])
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
app.json_encoder = MyEncoder
|
def loads(self, s, **kwargs):
|
||||||
app.json_decoder = MyDecoder
|
kwargs.setdefault("object_hook", self.object_hook)
|
||||||
|
return super().loads(s, **kwargs)
|
||||||
|
|
||||||
|
app.json = CustomProvider(app)
|
||||||
|
app.json.default = default
|
||||||
|
|
||||||
@app.route("/", methods=["POST"])
|
@app.route("/", methods=["POST"])
|
||||||
def index():
|
def index():
|
||||||
|
|
@ -256,49 +257,6 @@ def test_json_customization(app, client):
|
||||||
assert rv.data == b'"<42>"'
|
assert rv.data == b'"<42>"'
|
||||||
|
|
||||||
|
|
||||||
def test_blueprint_json_customization(app, client):
|
|
||||||
class X:
|
|
||||||
__slots__ = ("val",)
|
|
||||||
|
|
||||||
def __init__(self, val):
|
|
||||||
self.val = val
|
|
||||||
|
|
||||||
class MyEncoder(flask.json.JSONEncoder):
|
|
||||||
def default(self, o):
|
|
||||||
if isinstance(o, X):
|
|
||||||
return f"<{o.val}>"
|
|
||||||
|
|
||||||
return flask.json.JSONEncoder.default(self, o)
|
|
||||||
|
|
||||||
class MyDecoder(flask.json.JSONDecoder):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
kwargs.setdefault("object_hook", self.object_hook)
|
|
||||||
flask.json.JSONDecoder.__init__(self, *args, **kwargs)
|
|
||||||
|
|
||||||
def object_hook(self, obj):
|
|
||||||
if len(obj) == 1 and "_foo" in obj:
|
|
||||||
return X(obj["_foo"])
|
|
||||||
|
|
||||||
return obj
|
|
||||||
|
|
||||||
bp = flask.Blueprint("bp", __name__)
|
|
||||||
bp.json_encoder = MyEncoder
|
|
||||||
bp.json_decoder = MyDecoder
|
|
||||||
|
|
||||||
@bp.route("/bp", methods=["POST"])
|
|
||||||
def index():
|
|
||||||
return flask.json.dumps(flask.request.get_json()["x"])
|
|
||||||
|
|
||||||
app.register_blueprint(bp)
|
|
||||||
|
|
||||||
rv = client.post(
|
|
||||||
"/bp",
|
|
||||||
data=flask.json.dumps({"x": {"_foo": 42}}),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
assert rv.data == b'"<42>"'
|
|
||||||
|
|
||||||
|
|
||||||
def _has_encoding(name):
|
def _has_encoding(name):
|
||||||
try:
|
try:
|
||||||
import codecs
|
import codecs
|
||||||
|
|
@ -330,8 +288,7 @@ def test_modified_url_encoding(app, client):
|
||||||
|
|
||||||
def test_json_key_sorting(app, client):
|
def test_json_key_sorting(app, client):
|
||||||
app.debug = True
|
app.debug = True
|
||||||
|
assert app.json.sort_keys
|
||||||
assert app.config["JSON_SORT_KEYS"]
|
|
||||||
d = dict.fromkeys(range(20), "foo")
|
d = dict.fromkeys(range(20), "foo")
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ def test_path_is_url(app):
|
||||||
|
|
||||||
def test_environbuilder_json_dumps(app):
|
def test_environbuilder_json_dumps(app):
|
||||||
"""EnvironBuilder.json_dumps() takes settings from the app."""
|
"""EnvironBuilder.json_dumps() takes settings from the app."""
|
||||||
app.config["JSON_AS_ASCII"] = False
|
app.json.ensure_ascii = False
|
||||||
eb = EnvironBuilder(app, json="\u20ac")
|
eb = EnvironBuilder(app, json="\u20ac")
|
||||||
assert eb.input_stream.read().decode("utf8") == '"\u20ac"'
|
assert eb.input_stream.read().decode("utf8") == '"\u20ac"'
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue