Use Werkzeug's JSONMixin class and update tests

Flask's JSONMixin class was moved to Werkzeug>=0.15

Co-authored-by: Jeffrey Eliasen <jeff@jke.net>
This commit is contained in:
EtiennePelletier 2019-05-06 14:59:43 -04:00 committed by David Lord
parent d0bfb065e5
commit 8590d65a57
No known key found for this signature in database
GPG key ID: 7A1C87E3F5BC42A8
5 changed files with 18 additions and 131 deletions

View file

@ -9,6 +9,8 @@ Version 1.1.0
Unreleased Unreleased
- Bump minimum Werkzeug version to >= 0.15.
- Drop support for Python 3.4.
- :meth:`flask.RequestContext.copy` includes the current session - :meth:`flask.RequestContext.copy` includes the current session
object in the request context copy. This prevents ``session`` object in the request context copy. This prevents ``session``
pointing to an out-of-date object. (`#2935`_) pointing to an out-of-date object. (`#2935`_)
@ -27,12 +29,17 @@ Unreleased
- :attr:`Flask.jinja_options` is a ``dict`` instead of an - :attr:`Flask.jinja_options` is a ``dict`` instead of an
``ImmutableDict`` to allow easier configuration. Changes must still ``ImmutableDict`` to allow easier configuration. Changes must still
be made before creating the environment. :pr:`3190` be made before creating the environment. :pr:`3190`
- Flask's ``JSONMixin`` for the request and response wrappers was
moved into Werkzeug. Use Werkzeug's version with Flask-specific
support. This bumps the Werkzeug dependency to >= 0.15.
:issue:`3125`
.. _#2935: https://github.com/pallets/flask/issues/2935 .. _#2935: https://github.com/pallets/flask/issues/2935
.. _#2957: https://github.com/pallets/flask/issues/2957 .. _#2957: https://github.com/pallets/flask/issues/2957
.. _#2994: https://github.com/pallets/flask/pull/2994 .. _#2994: https://github.com/pallets/flask/pull/2994
.. _#3059: https://github.com/pallets/flask/pull/3059 .. _#3059: https://github.com/pallets/flask/pull/3059
.. _#3179: https://github.com/pallets/flask/pull/3179 .. _#3179: https://github.com/pallets/flask/pull/3179
.. _#3125: https://github.com/pallets/flask/pull/3125
Version 1.0.3 Version 1.0.3

View file

@ -11,99 +11,17 @@
from werkzeug.exceptions import BadRequest from werkzeug.exceptions import BadRequest
from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase
from werkzeug.wrappers.json import JSONMixin as _JSONMixin
from flask import json from flask import json
from flask.globals import current_app from flask.globals import current_app
class JSONMixin(object): class JSONMixin(_JSONMixin):
"""Common mixin for both request and response objects to provide JSON json_module = json
parsing capabilities.
.. versionadded:: 1.0
"""
_cached_json = (Ellipsis, Ellipsis)
@property
def is_json(self):
"""Check if the mimetype indicates JSON data, either
:mimetype:`application/json` or :mimetype:`application/*+json`.
.. versionadded:: 0.11
"""
mt = self.mimetype
return (
mt == "application/json"
or (mt.startswith("application/"))
and mt.endswith("+json")
)
@property
def json(self):
"""This will contain the parsed JSON data if the mimetype indicates
JSON (:mimetype:`application/json`, see :meth:`is_json`), otherwise it
will be ``None``.
"""
return self.get_json()
def _get_data_for_json(self, cache):
return self.get_data(cache=cache)
def get_json(self, force=False, silent=False, cache=True):
"""Parse and return the data as JSON. If the mimetype does not
indicate JSON (:mimetype:`application/json`, see
:meth:`is_json`), this returns ``None`` unless ``force`` is
true. If parsing fails, :meth:`on_json_loading_failed` is called
and its return value is used as the return value.
:param force: Ignore the mimetype and always try to parse JSON.
:param silent: Silence parsing errors and return ``None``
instead.
:param cache: Store the parsed JSON to return for subsequent
calls.
"""
if cache and self._cached_json[silent] is not Ellipsis:
return self._cached_json[silent]
if not (force or self.is_json):
return None
data = self._get_data_for_json(cache=cache)
try:
rv = json.loads(data)
except ValueError as e:
if silent:
rv = None
if cache:
normal_rv, _ = self._cached_json
self._cached_json = (normal_rv, rv)
else:
rv = self.on_json_loading_failed(e)
if cache:
_, silent_rv = self._cached_json
self._cached_json = (rv, silent_rv)
else:
if cache:
self._cached_json = (rv, rv)
return rv
def on_json_loading_failed(self, e): def on_json_loading_failed(self, e):
"""Called if :meth:`get_json` parsing fails and isn't silenced. If if current_app and current_app.debug:
this method returns a value, it is used as the return value for
:meth:`get_json`. The default implementation raises a
:class:`BadRequest` exception.
.. versionchanged:: 0.10
Raise a :exc:`BadRequest` error instead of returning an error
message as JSON. If you want that behavior you can add it by
subclassing.
.. versionadded:: 0.8
"""
if current_app is not None and current_app.debug:
raise BadRequest("Failed to decode JSON object: {0}".format(e)) raise BadRequest("Failed to decode JSON object: {0}".format(e))
raise BadRequest() raise BadRequest()

View file

@ -36,7 +36,7 @@ setup(
platforms="any", platforms="any",
python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*", python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*",
install_requires=[ install_requires=[
"Werkzeug>=0.14", "Werkzeug>=0.15",
"Jinja2>=2.10.1", "Jinja2>=2.10.1",
"itsdangerous>=0.24", "itsdangerous>=0.24",
"click>=5.1", "click>=5.1",

View file

@ -92,33 +92,9 @@ class TestJSON(object):
assert json.detect_encoding(data) == encoding assert json.detect_encoding(data) == encoding
assert json.loads(data) == value assert json.loads(data) == value
def test_ignore_cached_json(self, app): @pytest.mark.parametrize("debug", (True, False))
with app.test_request_context( def test_bad_request_debug_message(self, app, client, debug):
"/", method="POST", data="malformed", content_type="application/json" app.config["DEBUG"] = debug
):
assert flask.request.get_json(silent=True, cache=True) is None
with pytest.raises(BadRequest):
flask.request.get_json(silent=False, cache=False)
def test_different_silent_on_bad_request(self, app):
with app.test_request_context(
"/", method="POST", data="malformed", content_type="application/json"
):
assert flask.request.get_json(silent=True) is None
with pytest.raises(BadRequest):
flask.request.get_json(silent=False)
def test_different_silent_on_normal_request(self, app):
with app.test_request_context("/", method="POST", json={"foo": "bar"}):
silent_rv = flask.request.get_json(silent=True)
normal_rv = flask.request.get_json(silent=False)
assert silent_rv is normal_rv
assert normal_rv["foo"] == "bar"
def test_post_empty_json_adds_exception_to_response_content_in_debug(
self, app, client
):
app.config["DEBUG"] = True
app.config["TRAP_BAD_REQUEST_ERRORS"] = False app.config["TRAP_BAD_REQUEST_ERRORS"] = False
@app.route("/json", methods=["POST"]) @app.route("/json", methods=["POST"])
@ -128,22 +104,8 @@ class TestJSON(object):
rv = client.post("/json", data=None, content_type="application/json") rv = client.post("/json", data=None, content_type="application/json")
assert rv.status_code == 400 assert rv.status_code == 400
assert b"Failed to decode JSON object" in rv.data contains = b"Failed to decode JSON object" in rv.data
assert contains == debug
def test_post_empty_json_wont_add_exception_to_response_if_no_debug(
self, app, client
):
app.config["DEBUG"] = False
app.config["TRAP_BAD_REQUEST_ERRORS"] = False
@app.route("/json", methods=["POST"])
def post_json():
flask.request.get_json()
return None
rv = client.post("/json", data=None, content_type="application/json")
assert rv.status_code == 400
assert b"Failed to decode JSON object" not in rv.data
def test_json_bad_requests(self, app, client): def test_json_bad_requests(self, app, client):
@app.route("/json", methods=["POST"]) @app.route("/json", methods=["POST"])

View file

@ -14,7 +14,7 @@ deps =
blinker blinker
python-dotenv python-dotenv
lowest: Werkzeug==0.14 lowest: Werkzeug==0.15
lowest: Jinja2==2.10 lowest: Jinja2==2.10
lowest: itsdangerous==0.24 lowest: itsdangerous==0.24
lowest: Click==5.1 lowest: Click==5.1