Merge pull request #3181 from EtiennePelletier/Flask-3125

Use Werkzeug's JSONMixin class (moved from Flask) and update tests
This commit is contained in:
David Lord 2019-05-18 21:10:12 -07:00 committed by GitHub
commit 5a49b96d0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 18 additions and 131 deletions

View File

@ -9,6 +9,8 @@ Version 1.1.0
Unreleased
- Bump minimum Werkzeug version to >= 0.15.
- Drop support for Python 3.4.
- :meth:`flask.RequestContext.copy` includes the current session
object in the request context copy. This prevents ``session``
pointing to an out-of-date object. (`#2935`_)
@ -27,12 +29,17 @@ Unreleased
- :attr:`Flask.jinja_options` is a ``dict`` instead of an
``ImmutableDict`` to allow easier configuration. Changes must still
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
.. _#2957: https://github.com/pallets/flask/issues/2957
.. _#2994: https://github.com/pallets/flask/pull/2994
.. _#3059: https://github.com/pallets/flask/pull/3059
.. _#3179: https://github.com/pallets/flask/pull/3179
.. _#3125: https://github.com/pallets/flask/pull/3125
Version 1.0.3

View File

@ -11,99 +11,17 @@
from werkzeug.exceptions import BadRequest
from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase
from werkzeug.wrappers.json import JSONMixin as _JSONMixin
from flask import json
from flask.globals import current_app
class JSONMixin(object):
"""Common mixin for both request and response objects to provide 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
class JSONMixin(_JSONMixin):
json_module = json
def on_json_loading_failed(self, e):
"""Called if :meth:`get_json` parsing fails and isn't silenced. If
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:
if current_app and current_app.debug:
raise BadRequest("Failed to decode JSON object: {0}".format(e))
raise BadRequest()

View File

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

View File

@ -92,33 +92,9 @@ class TestJSON(object):
assert json.detect_encoding(data) == encoding
assert json.loads(data) == value
def test_ignore_cached_json(self, app):
with app.test_request_context(
"/", method="POST", data="malformed", content_type="application/json"
):
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
@pytest.mark.parametrize("debug", (True, False))
def test_bad_request_debug_message(self, app, client, debug):
app.config["DEBUG"] = debug
app.config["TRAP_BAD_REQUEST_ERRORS"] = False
@app.route("/json", methods=["POST"])
@ -128,22 +104,8 @@ class TestJSON(object):
rv = client.post("/json", data=None, content_type="application/json")
assert rv.status_code == 400
assert b"Failed to decode JSON object" in rv.data
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
contains = b"Failed to decode JSON object" in rv.data
assert contains == debug
def test_json_bad_requests(self, app, client):
@app.route("/json", methods=["POST"])

View File

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