Browse Source

clean up JSON code and docs

pull/2358/head
David Lord 7 years ago
parent
commit
e97253e4c1
No known key found for this signature in database
GPG Key ID: 7A1C87E3F5BC42A8
  1. 12
      CHANGES
  2. 25
      docs/testing.rst
  3. 10
      flask/testing.py
  4. 123
      flask/wrappers.py
  5. 2
      tests/test_testing.py

12
CHANGES

@ -70,6 +70,12 @@ Major release, unreleased
- Only open the session if the request has not been pushed onto the context
stack yet. This allows ``stream_with_context`` generators to access the same
session that the containing view uses. (`#2354`_)
- Add ``json`` keyword argument for the test client request methods. This will
dump the given object as JSON and set the appropriate content type.
(`#2358`_)
- Extract JSON handling to a mixin applied to both the request and response
classes used by Flask. This adds the ``is_json`` and ``get_json`` methods to
the response to make testing JSON response much easier. (`#2358`_)
.. _#1489: https://github.com/pallets/flask/pull/1489
.. _#1621: https://github.com/pallets/flask/pull/1621
@ -91,6 +97,7 @@ Major release, unreleased
.. _#2348: https://github.com/pallets/flask/pull/2348
.. _#2352: https://github.com/pallets/flask/pull/2352
.. _#2354: https://github.com/pallets/flask/pull/2354
.. _#2358: https://github.com/pallets/flask/pull/2358
Version 0.12.2
--------------
@ -126,11 +133,6 @@ Released on December 21st 2016, codename Punsch.
``application/octet-stream``. See pull request ``#1988``.
- Make ``flask.safe_join`` able to join multiple paths like ``os.path.join``
(pull request ``#1730``).
- Added `json` keyword argument to :meth:`flask.testing.FlaskClient.open`
(and related ``get``, ``post``, etc.), which makes it more convenient to
send JSON requests from the test client.
- Added ``is_json`` and ``get_json`` to :class:``flask.wrappers.Response``
in order to make it easier to build assertions when testing JSON responses.
- Revert a behavior change that made the dev server crash instead of returning
a Internal Server Error (pull request ``#2006``).
- Correctly invoke response handlers for both regular request dispatching as

25
docs/testing.rst

@ -382,28 +382,27 @@ Testing JSON APIs
.. versionadded:: 1.0
Flask has great support for JSON, and is a popular choice for building REST
APIs. Testing both JSON requests and responses using the test client is very
convenient::
Flask has great support for JSON, and is a popular choice for building JSON
APIs. Making requests with JSON data and examining JSON data in responses is
very convenient::
from flask import jsonify
from flask import request, jsonify
@app.route('/api/auth')
def auth():
json_data = request.get_json()
email = json_data['email']
password = json_data['password']
return jsonify(token=generate_token(email, password))
with app.test_client() as c:
email = 'john@example.com'
password = 'secret'
resp = c.post('/api/auth', json={'login': email, 'password': password})
json_data = resp.get_json()
rv = c.post('/api/auth', json={
'username': 'flask', 'password': 'secret'
})
json_data = rv.get_json()
assert verify_token(email, json_data['token'])
Note that if the ``json`` argument is provided then the test client will put
JSON-serialized data in the request body, and also set the
``Content-Type: application/json`` HTTP header.
Passing the ``json`` argument in the test client methods sets the request data
to the JSON-serialized object and sets the content type to
``application/json``. You can get the JSON data from the request or response
with ``get_json``.

10
flask/testing.py

@ -23,7 +23,7 @@ except ImportError:
def make_test_environ_builder(
app, path='/', base_url=None, subdomain=None, url_scheme=None, json=None,
app, path='/', base_url=None, subdomain=None, url_scheme=None,
*args, **kwargs
):
"""Creates a new test builder with some application defaults thrown in."""
@ -54,12 +54,14 @@ def make_test_environ_builder(
path += sep + url.query
if 'json' in kwargs:
if 'data' in kwargs:
raise ValueError('Client cannot provide both `json` and `data`')
assert 'data' not in kwargs, (
"Client cannot provide both 'json' and 'data'."
)
# push a context so flask.json can use app's json attributes
with app.app_context():
kwargs['data'] = json_dumps(kwargs.pop('json'))
# Only set Content-Type when not explicitly provided
if 'content_type' not in kwargs:
kwargs['content_type'] = 'application/json'

123
flask/wrappers.py

@ -8,111 +8,106 @@
:copyright: (c) 2015 by Armin Ronacher.
:license: BSD, see LICENSE for more details.
"""
from warnings import warn
from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase
from werkzeug.exceptions import BadRequest
from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase
from . import json
from .globals import _app_ctx_stack
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:: 0.12
.. versionadded:: 1.0
"""
_cached_json = Ellipsis
@property
def is_json(self):
"""Indicates if this request/response is in JSON format or not. By
default it is considered to include JSON data if the mimetype is
"""Check if the mimetype indicates JSON data, either
:mimetype:`application/json` or :mimetype:`application/*+json`.
.. versionadded:: 1.0
.. versionadded:: 0.11
"""
mt = self.mimetype
if mt == 'application/json':
return True
if mt.startswith('application/') and mt.endswith('+json'):
return True
return False
return (
mt == 'application/json'
or (mt.startswith('application/')) and mt.endswith('+json')
)
@property
def json(self):
"""If this request/response is in JSON format then this property will
contain the parsed JSON data. Otherwise it will be ``None``.
"""This will contain the parsed JSON data if the mimetype indicates
JSON (:mimetype:`application/json`, see :meth:`is_json`), otherwise it
will be ``None``.
The :meth:`get_json` method should be used instead.
.. deprecated:: 1.0
Use :meth:`get_json` instead.
"""
from warnings import warn
warn(DeprecationWarning(
'json is deprecated. Use get_json() instead.'), stacklevel=2)
"'json' is deprecated. Use 'get_json()' instead."
), stacklevel=2)
return self.get_json()
def _get_data_for_json(self, cache):
getter = getattr(self, 'get_data', None)
if getter is not None:
return getter(cache=cache)
return self.data
return self.get_data(cache=cache)
def get_json(self, force=False, silent=False, cache=True):
"""Parses the JSON request/response data and returns it. By default
this function will return ``None`` if the mimetype is not
:mimetype:`application/json` but this can be overridden by the
``force`` parameter. If parsing fails the
:meth:`on_json_loading_failed` method on the request object will be
invoked.
:param force: if set to ``True`` the mimetype is ignored.
:param silent: if set to ``True`` this method will fail silently
and return ``None``.
:param cache: if set to ``True`` the parsed JSON data is remembered
on the object.
"""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.
"""
try:
return getattr(self, '_cached_json')
except AttributeError:
pass
if cache and self._cached_json is not Ellipsis:
return self._cached_json
if not (force or self.is_json):
return None
# We accept MIME charset header against the specification as certain
# clients have been using this in the past. For responses, we assume
# that if the response charset was set explicitly then the data had
# been encoded correctly as well.
# We accept MIME charset against the specification as certain clients
# have used this in the past. For responses, we assume that if the
# charset is set then the data has been encoded correctly as well.
charset = self.mimetype_params.get('charset')
try:
data = self._get_data_for_json(cache)
if charset is not None:
data = self._get_data_for_json(cache=cache)
rv = json.loads(data, encoding=charset)
else:
rv = json.loads(data)
except ValueError as e:
if silent:
rv = None
else:
rv = self.on_json_loading_failed(e)
if cache:
self._cached_json = rv
return rv
def on_json_loading_failed(self, e):
"""Called if decoding of the JSON data failed. The return value of
this method is used by :meth:`get_json` when an error occurred. The
default implementation just raises a :class:`BadRequest` exception.
"""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
Removed buggy previous behavior of generating a random JSON
response. If you want that behavior back you can trivially
add it by subclassing.
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
"""
ctx = _app_ctx_stack.top
if ctx is not None and ctx.app.debug:
if current_app is not None and current_app.debug:
raise BadRequest('Failed to decode JSON object: {0}'.format(e))
raise BadRequest()
@ -153,9 +148,8 @@ class Request(RequestBase, JSONMixin):
@property
def max_content_length(self):
"""Read-only view of the ``MAX_CONTENT_LENGTH`` config key."""
ctx = _app_ctx_stack.top
if ctx is not None:
return ctx.app.config['MAX_CONTENT_LENGTH']
if current_app:
return current_app.config['MAX_CONTENT_LENGTH']
@property
def endpoint(self):
@ -191,9 +185,12 @@ class Request(RequestBase, JSONMixin):
# In debug mode we're replacing the files multidict with an ad-hoc
# subclass that raises a different error for key errors.
ctx = _app_ctx_stack.top
if ctx is not None and ctx.app.debug and \
self.mimetype != 'multipart/form-data' and not self.files:
if (
current_app
and current_app.debug
and self.mimetype != 'multipart/form-data'
and not self.files
):
from .debughelpers import attach_enctype_error_multidict
attach_enctype_error_multidict(self)
@ -206,5 +203,13 @@ class Response(ResponseBase, JSONMixin):
If you want to replace the response object used you can subclass this and
set :attr:`~flask.Flask.response_class` to your subclass.
.. versionchanged:: 1.0
JSON support is added to the response, like the request. This is useful
when testing to get the test client response data as JSON.
"""
default_mimetype = 'text/html'
def _get_data_for_json(self, cache):
return self.get_data()

2
tests/test_testing.py

@ -267,7 +267,7 @@ def test_full_url_request(app, client):
def test_json_request_and_response(app, client):
@app.route('/echo', methods=['POST'])
def echo():
return jsonify(flask.request.json)
return jsonify(flask.request.get_json())
with client:
json_data = {'drink': {'gin': 1, 'tonic': True}, 'price': 10}

Loading…
Cancel
Save