diff --git a/CHANGES.rst b/CHANGES.rst index 0e8eeda9..ae46bf3b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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 diff --git a/flask/wrappers.py b/flask/wrappers.py index 66f0fea4..3e0336be 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -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() diff --git a/setup.py b/setup.py index c9cf21bd..082e64f0 100755 --- a/setup.py +++ b/setup.py @@ -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", diff --git a/tests/test_helpers.py b/tests/test_helpers.py index cddc7517..89e15493 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -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"]) diff --git a/tox.ini b/tox.ini index 618a6ff7..2be290a1 100644 --- a/tox.ini +++ b/tox.ini @@ -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