add json provider interface

This commit is contained in:
David Lord 2022-07-13 07:41:43 -07:00
parent c356c6da5f
commit 69f9845ef2
No known key found for this signature in database
GPG key ID: 7A1C87E3F5BC42A8
13 changed files with 662 additions and 282 deletions

View file

@ -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

View file

@ -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:

View file

@ -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
----------------------------- -----------------------------

View file

@ -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

View file

@ -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,

View file

@ -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:

View file

@ -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
View 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
)

View file

@ -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,

View file

@ -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:

View file

@ -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):

View file

@ -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("/")

View file

@ -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"'