forked from orbit-oss/flask
Merge pull request #2358 from davidism/json-mixin
JSON support for test client and Response
This commit is contained in:
commit
63129e8426
7 changed files with 187 additions and 106 deletions
1
AUTHORS
1
AUTHORS
|
|
@ -9,6 +9,7 @@ Development Lead
|
||||||
Patches and Suggestions
|
Patches and Suggestions
|
||||||
```````````````````````
|
```````````````````````
|
||||||
|
|
||||||
|
- Adam Byrtek
|
||||||
- Adam Zapletal
|
- Adam Zapletal
|
||||||
- Ali Afshar
|
- Ali Afshar
|
||||||
- Chris Edgemon
|
- Chris Edgemon
|
||||||
|
|
|
||||||
7
CHANGES
7
CHANGES
|
|
@ -70,6 +70,12 @@ Major release, unreleased
|
||||||
- Only open the session if the request has not been pushed onto the context
|
- 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
|
stack yet. This allows ``stream_with_context`` generators to access the same
|
||||||
session that the containing view uses. (`#2354`_)
|
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
|
.. _#1489: https://github.com/pallets/flask/pull/1489
|
||||||
.. _#1621: https://github.com/pallets/flask/pull/1621
|
.. _#1621: https://github.com/pallets/flask/pull/1621
|
||||||
|
|
@ -91,6 +97,7 @@ Major release, unreleased
|
||||||
.. _#2348: https://github.com/pallets/flask/pull/2348
|
.. _#2348: https://github.com/pallets/flask/pull/2348
|
||||||
.. _#2352: https://github.com/pallets/flask/pull/2352
|
.. _#2352: https://github.com/pallets/flask/pull/2352
|
||||||
.. _#2354: https://github.com/pallets/flask/pull/2354
|
.. _#2354: https://github.com/pallets/flask/pull/2354
|
||||||
|
.. _#2358: https://github.com/pallets/flask/pull/2358
|
||||||
|
|
||||||
Version 0.12.2
|
Version 0.12.2
|
||||||
--------------
|
--------------
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ Response Objects
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
.. autoclass:: flask.Response
|
.. autoclass:: flask.Response
|
||||||
:members: set_cookie, data, mimetype
|
:members: set_cookie, data, mimetype, is_json, get_json
|
||||||
|
|
||||||
.. attribute:: headers
|
.. attribute:: headers
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -375,3 +375,34 @@ independently of the session backend used::
|
||||||
Note that in this case you have to use the ``sess`` object instead of the
|
Note that in this case you have to use the ``sess`` object instead of the
|
||||||
:data:`flask.session` proxy. The object however itself will provide the
|
:data:`flask.session` proxy. The object however itself will provide the
|
||||||
same interface.
|
same interface.
|
||||||
|
|
||||||
|
|
||||||
|
Testing JSON APIs
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
.. versionadded:: 1.0
|
||||||
|
|
||||||
|
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 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:
|
||||||
|
rv = c.post('/api/auth', json={
|
||||||
|
'username': 'flask', 'password': 'secret'
|
||||||
|
})
|
||||||
|
json_data = rv.get_json()
|
||||||
|
assert verify_token(email, json_data['token'])
|
||||||
|
|
||||||
|
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``.
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import werkzeug
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from werkzeug.test import Client, EnvironBuilder
|
from werkzeug.test import Client, EnvironBuilder
|
||||||
from flask import _request_ctx_stack
|
from flask import _request_ctx_stack
|
||||||
|
from flask.json import dumps as json_dumps
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from werkzeug.urls import url_parse
|
from werkzeug.urls import url_parse
|
||||||
|
|
@ -52,6 +53,18 @@ def make_test_environ_builder(
|
||||||
sep = b'?' if isinstance(url.query, bytes) else '?'
|
sep = b'?' if isinstance(url.query, bytes) else '?'
|
||||||
path += sep + url.query
|
path += sep + url.query
|
||||||
|
|
||||||
|
if 'json' in kwargs:
|
||||||
|
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'))
|
||||||
|
|
||||||
|
if 'content_type' not in kwargs:
|
||||||
|
kwargs['content_type'] = 'application/json'
|
||||||
|
|
||||||
return EnvironBuilder(path, base_url, *args, **kwargs)
|
return EnvironBuilder(path, base_url, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,24 +8,110 @@
|
||||||
:copyright: (c) 2015 by Armin Ronacher.
|
:copyright: (c) 2015 by Armin Ronacher.
|
||||||
:license: BSD, see LICENSE for more details.
|
: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.exceptions import BadRequest
|
||||||
|
from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase
|
||||||
|
|
||||||
from . import json
|
from flask import json
|
||||||
from .globals import _request_ctx_stack
|
from flask.globals import current_app
|
||||||
|
|
||||||
_missing = object()
|
|
||||||
|
|
||||||
|
|
||||||
def _get_data(req, cache):
|
class JSONMixin(object):
|
||||||
getter = getattr(req, 'get_data', None)
|
"""Common mixin for both request and response objects to provide JSON
|
||||||
if getter is not None:
|
parsing capabilities.
|
||||||
return getter(cache=cache)
|
|
||||||
return req.data
|
.. versionadded:: 1.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
_cached_json = 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``.
|
||||||
|
|
||||||
|
.. deprecated:: 1.0
|
||||||
|
Use :meth:`get_json` instead.
|
||||||
|
"""
|
||||||
|
warn(DeprecationWarning(
|
||||||
|
"'json' is deprecated. Use 'get_json()' instead."
|
||||||
|
), stacklevel=2)
|
||||||
|
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 is not Ellipsis:
|
||||||
|
return self._cached_json
|
||||||
|
|
||||||
|
if not (force or self.is_json):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 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=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 :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):
|
class Request(RequestBase, JSONMixin):
|
||||||
"""The request object used by default in Flask. Remembers the
|
"""The request object used by default in Flask. Remembers the
|
||||||
matched endpoint and view arguments.
|
matched endpoint and view arguments.
|
||||||
|
|
||||||
|
|
@ -62,9 +148,8 @@ class Request(RequestBase):
|
||||||
@property
|
@property
|
||||||
def max_content_length(self):
|
def max_content_length(self):
|
||||||
"""Read-only view of the ``MAX_CONTENT_LENGTH`` config key."""
|
"""Read-only view of the ``MAX_CONTENT_LENGTH`` config key."""
|
||||||
ctx = _request_ctx_stack.top
|
if current_app:
|
||||||
if ctx is not None:
|
return current_app.config['MAX_CONTENT_LENGTH']
|
||||||
return ctx.app.config['MAX_CONTENT_LENGTH']
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def endpoint(self):
|
def endpoint(self):
|
||||||
|
|
@ -95,106 +180,22 @@ class Request(RequestBase):
|
||||||
if self.url_rule and '.' in self.url_rule.endpoint:
|
if self.url_rule and '.' in self.url_rule.endpoint:
|
||||||
return self.url_rule.endpoint.rsplit('.', 1)[0]
|
return self.url_rule.endpoint.rsplit('.', 1)[0]
|
||||||
|
|
||||||
@property
|
|
||||||
def json(self):
|
|
||||||
"""If the request has a JSON mimetype like :mimetype:`application/json`
|
|
||||||
(see :meth:`is_json`), this will contain the parsed JSON data.
|
|
||||||
Otherwise this will be ``None``.
|
|
||||||
|
|
||||||
The :meth:`get_json` method should be used instead.
|
|
||||||
"""
|
|
||||||
from warnings import warn
|
|
||||||
warn(DeprecationWarning('json is deprecated. '
|
|
||||||
'Use get_json() instead.'), stacklevel=2)
|
|
||||||
return self.get_json()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_json(self):
|
|
||||||
"""Indicates if this request is JSON or not. By default a request
|
|
||||||
is considered to include JSON data if the mimetype is
|
|
||||||
:mimetype:`application/json` or :mimetype:`application/*+json`.
|
|
||||||
|
|
||||||
.. versionadded:: 0.11
|
|
||||||
"""
|
|
||||||
mt = self.mimetype
|
|
||||||
if mt == 'application/json':
|
|
||||||
return True
|
|
||||||
if mt.startswith('application/') and mt.endswith('+json'):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_json(self, force=False, silent=False, cache=True):
|
|
||||||
"""Parses the incoming JSON request data and returns it. By default
|
|
||||||
this function will return ``None`` if the request does not use a JSON
|
|
||||||
mimetype like :mimetype:`application/json`. See :meth:`is_json`. 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 request.
|
|
||||||
"""
|
|
||||||
rv = getattr(self, '_cached_json', _missing)
|
|
||||||
# We return cached JSON only when the cache is enabled.
|
|
||||||
if cache and rv is not _missing:
|
|
||||||
return rv
|
|
||||||
|
|
||||||
if not (force or self.is_json):
|
|
||||||
return None
|
|
||||||
|
|
||||||
# We accept a request charset against the specification as
|
|
||||||
# certain clients have been using this in the past. This
|
|
||||||
# fits our general approach of being nice in what we accept
|
|
||||||
# and strict in what we send out.
|
|
||||||
request_charset = self.mimetype_params.get('charset')
|
|
||||||
try:
|
|
||||||
data = _get_data(self, cache)
|
|
||||||
if request_charset is not None:
|
|
||||||
rv = json.loads(data, encoding=request_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.
|
|
||||||
|
|
||||||
.. 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.
|
|
||||||
|
|
||||||
.. versionadded:: 0.8
|
|
||||||
"""
|
|
||||||
ctx = _request_ctx_stack.top
|
|
||||||
if ctx is not None and ctx.app.config.get('DEBUG', False):
|
|
||||||
raise BadRequest('Failed to decode JSON object: {0}'.format(e))
|
|
||||||
raise BadRequest()
|
|
||||||
|
|
||||||
def _load_form_data(self):
|
def _load_form_data(self):
|
||||||
RequestBase._load_form_data(self)
|
RequestBase._load_form_data(self)
|
||||||
|
|
||||||
# In debug mode we're replacing the files multidict with an ad-hoc
|
# In debug mode we're replacing the files multidict with an ad-hoc
|
||||||
# subclass that raises a different error for key errors.
|
# subclass that raises a different error for key errors.
|
||||||
ctx = _request_ctx_stack.top
|
if (
|
||||||
if ctx is not None and ctx.app.debug and \
|
current_app
|
||||||
self.mimetype != 'multipart/form-data' and not self.files:
|
and current_app.debug
|
||||||
|
and self.mimetype != 'multipart/form-data'
|
||||||
|
and not self.files
|
||||||
|
):
|
||||||
from .debughelpers import attach_enctype_error_multidict
|
from .debughelpers import attach_enctype_error_multidict
|
||||||
attach_enctype_error_multidict(self)
|
attach_enctype_error_multidict(self)
|
||||||
|
|
||||||
|
|
||||||
class Response(ResponseBase):
|
class Response(ResponseBase, JSONMixin):
|
||||||
"""The response object that is used by default in Flask. Works like the
|
"""The response object that is used by default in Flask. Works like the
|
||||||
response object from Werkzeug but is set to have an HTML mimetype by
|
response object from Werkzeug but is set to have an HTML mimetype by
|
||||||
default. Quite often you don't have to create this object yourself because
|
default. Quite often you don't have to create this object yourself because
|
||||||
|
|
@ -202,5 +203,13 @@ class Response(ResponseBase):
|
||||||
|
|
||||||
If you want to replace the response object used you can subclass this and
|
If you want to replace the response object used you can subclass this and
|
||||||
set :attr:`~flask.Flask.response_class` to your subclass.
|
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'
|
default_mimetype = 'text/html'
|
||||||
|
|
||||||
|
def _get_data_for_json(self, cache):
|
||||||
|
return self.get_data()
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import flask
|
||||||
import werkzeug
|
import werkzeug
|
||||||
|
|
||||||
from flask._compat import text_type
|
from flask._compat import text_type
|
||||||
|
from flask.json import jsonify
|
||||||
|
|
||||||
|
|
||||||
def test_environ_defaults_from_config(app, client):
|
def test_environ_defaults_from_config(app, client):
|
||||||
|
|
@ -263,6 +264,25 @@ def test_full_url_request(app, client):
|
||||||
assert 'vodka' in flask.request.args
|
assert 'vodka' in flask.request.args
|
||||||
|
|
||||||
|
|
||||||
|
def test_json_request_and_response(app, client):
|
||||||
|
@app.route('/echo', methods=['POST'])
|
||||||
|
def echo():
|
||||||
|
return jsonify(flask.request.get_json())
|
||||||
|
|
||||||
|
with client:
|
||||||
|
json_data = {'drink': {'gin': 1, 'tonic': True}, 'price': 10}
|
||||||
|
rv = client.post('/echo', json=json_data)
|
||||||
|
|
||||||
|
# Request should be in JSON
|
||||||
|
assert flask.request.is_json
|
||||||
|
assert flask.request.get_json() == json_data
|
||||||
|
|
||||||
|
# Response should be in JSON
|
||||||
|
assert rv.status_code == 200
|
||||||
|
assert rv.is_json
|
||||||
|
assert rv.get_json() == json_data
|
||||||
|
|
||||||
|
|
||||||
def test_subdomain(app, client):
|
def test_subdomain(app, client):
|
||||||
app.config['SERVER_NAME'] = 'example.com'
|
app.config['SERVER_NAME'] = 'example.com'
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue