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. 12
      flask/testing.py
  4. 125
      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 - 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
-------------- --------------
@ -126,11 +133,6 @@ Released on December 21st 2016, codename Punsch.
``application/octet-stream``. See pull request ``#1988``. ``application/octet-stream``. See pull request ``#1988``.
- Make ``flask.safe_join`` able to join multiple paths like ``os.path.join`` - Make ``flask.safe_join`` able to join multiple paths like ``os.path.join``
(pull request ``#1730``). (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 - Revert a behavior change that made the dev server crash instead of returning
a Internal Server Error (pull request ``#2006``). a Internal Server Error (pull request ``#2006``).
- Correctly invoke response handlers for both regular request dispatching as - Correctly invoke response handlers for both regular request dispatching as

25
docs/testing.rst

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

12
flask/testing.py

@ -23,7 +23,7 @@ except ImportError:
def make_test_environ_builder( 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 *args, **kwargs
): ):
"""Creates a new test builder with some application defaults thrown in.""" """Creates a new test builder with some application defaults thrown in."""
@ -54,12 +54,14 @@ def make_test_environ_builder(
path += sep + url.query path += sep + url.query
if 'json' in kwargs: if 'json' in kwargs:
if 'data' in kwargs: assert 'data' not in kwargs, (
raise ValueError('Client cannot provide both `json` and `data`') "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: if 'content_type' not in kwargs:
kwargs['content_type'] = 'application/json' kwargs['content_type'] = 'application/json'

125
flask/wrappers.py

@ -8,111 +8,106 @@
: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 _app_ctx_stack from flask.globals import current_app
class JSONMixin(object): class JSONMixin(object):
"""Common mixin for both request and response objects to provide JSON """Common mixin for both request and response objects to provide JSON
parsing capabilities. parsing capabilities.
.. versionadded:: 0.12 .. versionadded:: 1.0
""" """
_cached_json = Ellipsis
@property @property
def is_json(self): def is_json(self):
"""Indicates if this request/response is in JSON format or not. By """Check if the mimetype indicates JSON data, either
default it is considered to include JSON data if the mimetype is
:mimetype:`application/json` or :mimetype:`application/*+json`. :mimetype:`application/json` or :mimetype:`application/*+json`.
.. versionadded:: 1.0 .. versionadded:: 0.11
""" """
mt = self.mimetype mt = self.mimetype
if mt == 'application/json': return (
return True mt == 'application/json'
if mt.startswith('application/') and mt.endswith('+json'): or (mt.startswith('application/')) and mt.endswith('+json')
return True )
return False
@property @property
def json(self): def json(self):
"""If this request/response is in JSON format then this property will """This will contain the parsed JSON data if the mimetype indicates
contain the parsed JSON data. Otherwise it will be ``None``. 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( warn(DeprecationWarning(
'json is deprecated. Use get_json() instead.'), stacklevel=2) "'json' is deprecated. Use 'get_json()' instead."
), stacklevel=2)
return self.get_json() return self.get_json()
def _get_data_for_json(self, cache): def _get_data_for_json(self, cache):
getter = getattr(self, 'get_data', None) return self.get_data(cache=cache)
if getter is not None:
return getter(cache=cache)
return self.data
def get_json(self, force=False, silent=False, cache=True): def get_json(self, force=False, silent=False, cache=True):
"""Parses the JSON request/response data and returns it. By default """Parse and return the data as JSON. If the mimetype does not indicate
this function will return ``None`` if the mimetype is not JSON (:mimetype:`application/json`, see :meth:`is_json`), this returns
:mimetype:`application/json` but this can be overridden by the ``None`` unless ``force`` is true. If parsing fails,
``force`` parameter. If parsing fails the :meth:`on_json_loading_failed` is called and its return value is used
:meth:`on_json_loading_failed` method on the request object will be as the return value.
invoked.
:param force: Ignore the mimetype and always try to parse JSON.
:param force: if set to ``True`` the mimetype is ignored. :param silent: Silence parsing errors and return ``None`` instead.
:param silent: if set to ``True`` this method will fail silently :param cache: Store the parsed JSON to return for subsequent calls.
and return ``None``.
:param cache: if set to ``True`` the parsed JSON data is remembered
on the object.
""" """
try: if cache and self._cached_json is not Ellipsis:
return getattr(self, '_cached_json') return self._cached_json
except AttributeError:
pass
if not (force or self.is_json): if not (force or self.is_json):
return None return None
# We accept MIME charset header against the specification as certain # We accept MIME charset against the specification as certain clients
# clients have been using this in the past. For responses, we assume # have used this in the past. For responses, we assume that if the
# that if the response charset was set explicitly then the data had # charset is set then the data has been encoded correctly as well.
# been encoded correctly as well.
charset = self.mimetype_params.get('charset') charset = self.mimetype_params.get('charset')
try: try:
data = self._get_data_for_json(cache) data = self._get_data_for_json(cache=cache)
if charset is not None: rv = json.loads(data, encoding=charset)
rv = json.loads(data, encoding=charset)
else:
rv = json.loads(data)
except ValueError as e: except ValueError as e:
if silent: if silent:
rv = None rv = None
else: else:
rv = self.on_json_loading_failed(e) rv = self.on_json_loading_failed(e)
if cache: if cache:
self._cached_json = rv self._cached_json = rv
return rv return rv
def on_json_loading_failed(self, e): def on_json_loading_failed(self, e):
"""Called if decoding of the JSON data failed. The return value of """Called if :meth:`get_json` parsing fails and isn't silenced. If
this method is used by :meth:`get_json` when an error occurred. The this method returns a value, it is used as the return value for
default implementation just raises a :class:`BadRequest` exception. :meth:`get_json`. The default implementation raises a
:class:`BadRequest` exception.
.. versionchanged:: 0.10 .. versionchanged:: 0.10
Removed buggy previous behavior of generating a random JSON Raise a :exc:`BadRequest` error instead of returning an error
response. If you want that behavior back you can trivially message as JSON. If you want that behavior you can add it by
add it by subclassing. subclassing.
.. versionadded:: 0.8 .. versionadded:: 0.8
""" """
ctx = _app_ctx_stack.top if current_app is not None and current_app.debug:
if ctx is not None and ctx.app.debug:
raise BadRequest('Failed to decode JSON object: {0}'.format(e)) raise BadRequest('Failed to decode JSON object: {0}'.format(e))
raise BadRequest() raise BadRequest()
@ -153,9 +148,8 @@ class Request(RequestBase, JSONMixin):
@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 = _app_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):
@ -191,9 +185,12 @@ class Request(RequestBase, JSONMixin):
# 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 = _app_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)
@ -206,5 +203,13 @@ class Response(ResponseBase, JSONMixin):
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()

2
tests/test_testing.py

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

Loading…
Cancel
Save