diff --git a/CHANGES.rst b/CHANGES.rst index 0942aea6..a383f3f4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -30,7 +30,7 @@ Unreleased Version 1.0.3 ------------- -Unreleased +Released 2019-05-17 - :func:`send_file` encodes filenames as ASCII instead of Latin-1 (ISO-8859-1). This fixes compatibility with Gunicorn, which is @@ -46,10 +46,13 @@ Unreleased handle ``RoutingException``, 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 @@ -57,7 +60,7 @@ Unreleased Version 1.0.2 ------------- -Released on May 2nd 2018 +Released 2018-05-02 - Fix more backwards compatibility issues with merging slashes between a blueprint prefix and route. (`#2748`_) @@ -71,7 +74,7 @@ Released on May 2nd 2018 Version 1.0.1 ------------- -Released on April 29th 2018 +Released 2018-04-29 - Fix registering partials (with no ``__name__``) as view functions. (`#2730`_) @@ -97,7 +100,7 @@ Released on April 29th 2018 Version 1.0 ----------- -Released on April 26th 2018 +Released 2018-04-26 - **Python 2.6 and 3.3 are no longer supported.** (`pallets/meta#24`_) - Bump minimum dependency versions to the latest stable versions: diff --git a/docs/testing.rst b/docs/testing.rst index a18aca6c..ed9f0b03 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -357,14 +357,15 @@ This however does not make it possible to also modify the session or to access the session before a request was fired. Starting with Flask 0.8 we provide a so called “session transaction” which simulates the appropriate calls to open a session in the context of the test client and to modify -it. At the end of the transaction the session is stored. This works -independently of the session backend used:: +it. At the end of the transaction the session is stored and ready to be +used by the test client. This works independently of the session backend used:: with app.test_client() as c: with c.session_transaction() as sess: sess['a_key'] = 'a value' - # once this is reached the session was stored + # once this is reached the session was stored and ready to be used by the client + c.get(...) 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 diff --git a/flask/blueprints.py b/flask/blueprints.py index 42702d6d..dc193a8d 100644 --- a/flask/blueprints.py +++ b/flask/blueprints.py @@ -10,7 +10,6 @@ :license: BSD, see LICENSE for more details. """ from functools import update_wrapper -from werkzeug.urls import url_join from .helpers import _PackageBoundObject, _endpoint_from_view_func diff --git a/flask/cli.py b/flask/cli.py index 6bb01a1f..d96f16f6 100644 --- a/flask/cli.py +++ b/flask/cli.py @@ -650,11 +650,9 @@ def show_server_banner(env, debug, app_import_path, eager_loading): if env == "production": click.secho( - " WARNING: Do not use the development server in a production" - " environment.", - fg="red", - ) - click.secho(" Use a production WSGI server instead.", dim=True) + ' WARNING: This is a development server. ' + 'Do not use it in a production deployment.', fg='red') + click.secho(' Use a production WSGI server instead.', dim=True) if debug is not None: click.echo(" * Debug mode: {0}".format("on" if debug else "off")) diff --git a/flask/json/__init__.py b/flask/json/__init__.py index 80019250..ac9847dd 100644 --- a/flask/json/__init__.py +++ b/flask/json/__init__.py @@ -97,31 +97,35 @@ 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, + "cls", bp.json_decoder if bp and bp.json_decoder else app.json_decoder ) else: kwargs.setdefault("cls", JSONDecoder) @@ -170,17 +174,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): @@ -188,21 +203,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: @@ -211,10 +242,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 514a63b4..6c9de313 100644 --- a/flask/testing.py +++ b/flask/testing.py @@ -71,12 +71,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")) + 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 ad5ed0cb..a6b9d70e 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"