diff --git a/CHANGES.rst b/CHANGES.rst index bdf0c896..ddfead62 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -23,10 +23,13 @@ Unreleased handle ``RoutingExcpetion``, which is used internally during routing. This fixes the unexpected behavior that had been introduced in 1.0. (`#2986`_) +- Passing the ``json`` argument to ``app.test_client`` does not + push/pop an extra app context. (`#2900`_) .. _#2766: https://github.com/pallets/flask/issues/2766 .. _#2765: https://github.com/pallets/flask/pull/2765 .. _#2825: https://github.com/pallets/flask/pull/2825 +.. _#2900: https://github.com/pallets/flask/issues/2900 .. _#2933: https://github.com/pallets/flask/issues/2933 .. _#2986: https://github.com/pallets/flask/pull/2986 diff --git a/flask/json/__init__.py b/flask/json/__init__.py index fbe6b92f..c24286c9 100644 --- a/flask/json/__init__.py +++ b/flask/json/__init__.py @@ -89,33 +89,37 @@ class JSONDecoder(_json.JSONDecoder): """ -def _dump_arg_defaults(kwargs): +def _dump_arg_defaults(kwargs, app=None): """Inject default arguments for dump functions.""" - if current_app: - bp = current_app.blueprints.get(request.blueprint) if request else None + if app is None: + app = current_app + + if app: + bp = app.blueprints.get(request.blueprint) if request else None kwargs.setdefault( - 'cls', - bp.json_encoder if bp and bp.json_encoder - else current_app.json_encoder + 'cls', bp.json_encoder if bp and bp.json_encoder else app.json_encoder ) - if not current_app.config['JSON_AS_ASCII']: + if not app.config['JSON_AS_ASCII']: kwargs.setdefault('ensure_ascii', False) - kwargs.setdefault('sort_keys', current_app.config['JSON_SORT_KEYS']) + kwargs.setdefault('sort_keys', app.config['JSON_SORT_KEYS']) else: kwargs.setdefault('sort_keys', True) kwargs.setdefault('cls', JSONEncoder) -def _load_arg_defaults(kwargs): +def _load_arg_defaults(kwargs, app=None): """Inject default arguments for load functions.""" - if current_app: - bp = current_app.blueprints.get(request.blueprint) if request else None + if app is None: + app = current_app + + if app: + bp = app.blueprints.get(request.blueprint) if request else None kwargs.setdefault( 'cls', bp.json_decoder if bp and bp.json_decoder - else current_app.json_decoder + else app.json_decoder ) else: kwargs.setdefault('cls', JSONDecoder) @@ -164,17 +168,28 @@ def detect_encoding(data): return 'utf-8' -def dumps(obj, **kwargs): - """Serialize ``obj`` to a JSON formatted ``str`` by using the application's - configured encoder (:attr:`~flask.Flask.json_encoder`) if there is an - application on the stack. +def dumps(obj, app=None, **kwargs): + """Serialize ``obj`` to a JSON-formatted string. If there is an + app context pushed, use the current app's configured encoder + (:attr:`~flask.Flask.json_encoder`), or fall back to the default + :class:`JSONEncoder`. - This function can return ``unicode`` strings or ascii-only bytestrings by - default which coerce into unicode strings automatically. That behavior by - default is controlled by the ``JSON_AS_ASCII`` configuration variable - and can be overridden by the simplejson ``ensure_ascii`` parameter. + Takes the same arguments as the built-in :func:`json.dumps`, and + does some extra configuration based on the application. If the + simplejson package is installed, it is preferred. + + :param obj: Object to serialize to JSON. + :param app: App instance to use to configure the JSON encoder. + Uses ``current_app`` if not given, and falls back to the default + encoder when not in an app context. + :param kwargs: Extra arguments passed to :func:`json.dumps`. + + .. versionchanged:: 1.0.3 + + ``app`` can be passed directly, rather than requiring an app + context for configuration. """ - _dump_arg_defaults(kwargs) + _dump_arg_defaults(kwargs, app=app) encoding = kwargs.pop('encoding', None) rv = _json.dumps(obj, **kwargs) if encoding is not None and isinstance(rv, text_type): @@ -182,21 +197,37 @@ def dumps(obj, **kwargs): return rv -def dump(obj, fp, **kwargs): +def dump(obj, fp, app=None, **kwargs): """Like :func:`dumps` but writes into a file object.""" - _dump_arg_defaults(kwargs) + _dump_arg_defaults(kwargs, app=app) encoding = kwargs.pop('encoding', None) if encoding is not None: fp = _wrap_writer_for_text(fp, encoding) _json.dump(obj, fp, **kwargs) -def loads(s, **kwargs): - """Unserialize a JSON object from a string ``s`` by using the application's - configured decoder (:attr:`~flask.Flask.json_decoder`) if there is an - application on the stack. +def loads(s, app=None, **kwargs): + """Deserialize an object from a JSON-formatted string ``s``. If + there is an app context pushed, use the current app's configured + decoder (:attr:`~flask.Flask.json_decoder`), or fall back to the + default :class:`JSONDecoder`. + + Takes the same arguments as the built-in :func:`json.loads`, and + does some extra configuration based on the application. If the + simplejson package is installed, it is preferred. + + :param s: JSON string to deserialize. + :param app: App instance to use to configure the JSON decoder. + Uses ``current_app`` if not given, and falls back to the default + encoder when not in an app context. + :param kwargs: Extra arguments passed to :func:`json.dumps`. + + .. versionchanged:: 1.0.3 + + ``app`` can be passed directly, rather than requiring an app + context for configuration. """ - _load_arg_defaults(kwargs) + _load_arg_defaults(kwargs, app=app) if isinstance(s, bytes): encoding = kwargs.pop('encoding', None) if encoding is None: @@ -205,10 +236,9 @@ def loads(s, **kwargs): return _json.loads(s, **kwargs) -def load(fp, **kwargs): - """Like :func:`loads` but reads from a file object. - """ - _load_arg_defaults(kwargs) +def load(fp, app=None, **kwargs): + """Like :func:`loads` but reads from a file object.""" + _load_arg_defaults(kwargs, app=app) if not PY2: fp = _wrap_reader_for_text(fp, kwargs.pop('encoding', None) or 'utf-8') return _json.load(fp, **kwargs) diff --git a/flask/testing.py b/flask/testing.py index 4bf0ebc1..114c5cc3 100644 --- a/flask/testing.py +++ b/flask/testing.py @@ -73,14 +73,10 @@ def make_test_environ_builder( sep = b'?' if isinstance(url.query, bytes) else '?' path += sep + url.query + # TODO use EnvironBuilder.json_dumps once we require Werkzeug 0.15 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')) + assert 'data' not in kwargs, "Client cannot provide both 'json' and 'data'." + kwargs['data'] = json_dumps(kwargs.pop('json'), app=app) if 'content_type' not in kwargs: kwargs['content_type'] = 'application/json' diff --git a/tests/test_testing.py b/tests/test_testing.py index 14c66324..8c41d1fc 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -14,11 +14,17 @@ import pytest import flask import werkzeug +from flask import appcontext_popped from flask._compat import text_type from flask.cli import ScriptInfo from flask.json import jsonify from flask.testing import make_test_environ_builder, FlaskCliRunner +try: + import blinker +except ImportError: + blinker = None + def test_environ_defaults_from_config(app, client): app.config['SERVER_NAME'] = 'example.com:1234' @@ -306,6 +312,27 @@ def test_json_request_and_response(app, client): assert rv.get_json() == json_data +@pytest.mark.skipif(blinker is None, reason="blinker is not installed") +def test_client_json_no_app_context(app, client): + @app.route("/hello", methods=["POST"]) + def hello(): + return "Hello, {}!".format(flask.request.json["name"]) + + class Namespace(object): + count = 0 + + def add(self, app): + self.count += 1 + + ns = Namespace() + + with appcontext_popped.connected_to(ns.add, app): + rv = client.post("/hello", json={"name": "Flask"}) + + assert rv.get_data(as_text=True) == "Hello, Flask!" + assert ns.count == 1 + + def test_subdomain(): app = flask.Flask(__name__, subdomain_matching=True) app.config['SERVER_NAME'] = 'example.com'