clean up JSON code and docs
This commit is contained in:
parent
dbc70c9274
commit
e97253e4c1
5 changed files with 91 additions and 83 deletions
12
CHANGES
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
|
||||
|
|
|
|||
|
|
@ -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``.
|
||||
|
|
|
|||
|
|
@ -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'."
|
||||
)
|
||||
|
||||
kwargs['data'] = json_dumps(kwargs.pop('json'))
|
||||
# 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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
"""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: 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.
|
||||
: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:
|
||||
rv = json.loads(data, encoding=charset)
|
||||
else:
|
||||
rv = json.loads(data)
|
||||
data = self._get_data_for_json(cache=cache)
|
||||
rv = json.loads(data, encoding=charset)
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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…
Add table
Add a link
Reference in a new issue