diff --git a/CHANGES.rst b/CHANGES.rst index 0942aea6ce..c77f8fa5c8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,11 +20,16 @@ Unreleased (`#3059`_) - :func:`send_file` supports :class:`~io.BytesIO` partial content. (`#2957`_) +- Use Werkzeug's JSONMixin and delete Flask's JSONMixin class; + Bump minimum dependency versions to the latest stable versions: + Werkzeug >= 0.15 + (`#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 +.. _#3125: https://github.com/pallets/flask/pull/3125 Version 1.0.3 diff --git a/flask/wrappers.py b/flask/wrappers.py index 12eff2c7e9..de1fd82586 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -11,103 +11,12 @@ from werkzeug.exceptions import BadRequest from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase +from werkzeug.wrappers.json import 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 - - 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: - raise BadRequest('Failed to decode JSON object: {0}'.format(e)) - - raise BadRequest() - - class Request(RequestBase, JSONMixin): """The request object used by default in Flask. Remembers the matched endpoint and view arguments. diff --git a/setup.py b/setup.py index 0bc1c90b98..d6e4936038 100755 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ 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 8f2fc1c97b..80c4353fc8 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -122,19 +122,6 @@ def post_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 - def test_json_bad_requests(self, app, client): @app.route('/json', methods=['POST']) @@ -288,64 +275,17 @@ def test_template_escaping(self, app, req_ctx): assert rv == '' def test_json_customization(self, app, client): - class X(object): - def __init__(self, val): - self.val = val - - class MyEncoder(flask.json.JSONEncoder): - def default(self, o): - if isinstance(o, X): - return '<%d>' % 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 - - app.json_encoder = MyEncoder - app.json_decoder = MyDecoder - @app.route('/', methods=['POST']) def index(): return flask.json.dumps(flask.request.get_json()['x']) - rv = client.post('/', data=flask.json.dumps({ - 'x': {'_foo': 42} - }), content_type='application/json') - assert rv.data == b'"<42>"' - - def test_blueprint_json_customization(self, app, client): - class X(object): - def __init__(self, val): - self.val = val - - class MyEncoder(flask.json.JSONEncoder): - def default(self, o): - if isinstance(o, X): - return '<%d>' % o.val + payload = {'x': {'_foo': 42}} - 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 + rv = client.post('/', data=flask.json.dumps(payload), content_type='application/json') + assert rv.data == json.dumps(payload['x']).encode() + def test_blueprint_json_customization(self, app, client): bp = flask.Blueprint('bp', __name__) - bp.json_encoder = MyEncoder - bp.json_decoder = MyDecoder @bp.route('/bp', methods=['POST']) def index(): @@ -353,10 +293,10 @@ def index(): app.register_blueprint(bp) - rv = client.post('/bp', data=flask.json.dumps({ - 'x': {'_foo': 42} - }), content_type='application/json') - assert rv.data == b'"<42>"' + payload = {'x': {'_foo': 42}} + + rv = client.post('/bp', data=flask.json.dumps(payload), content_type='application/json') + assert rv.data == json.dumps(payload['x']).encode() def test_modified_url_encoding(self, app, client): class ModifiedRequest(flask.Request):