Skip to content

Commit

Permalink
Fix pallets#3125 - Use Werkzeug's JSONMixin class (moved from Flask) …
Browse files Browse the repository at this point in the history
…and update tests
  • Loading branch information
EtiennePelletier committed May 6, 2019
1 parent 12ab9f9 commit 680352d
Show file tree
Hide file tree
Showing 4 changed files with 15 additions and 161 deletions.
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
93 changes: 1 addition & 92 deletions flask/wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
76 changes: 8 additions & 68 deletions tests/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
Expand Down Expand Up @@ -288,75 +275,28 @@ def test_template_escaping(self, app, req_ctx):
assert rv == '<a ng-data=\'{"x": ["foo", "bar", "baz\\u0027"]}\'></a>'

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():
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>"'
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):
Expand Down

0 comments on commit 680352d

Please sign in to comment.