Skip to content

Commit

Permalink
don't push app context for test client json
Browse files Browse the repository at this point in the history
  • Loading branch information
davidism committed May 17, 2019
1 parent f2c8540 commit a4f0f19
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 39 deletions.
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
94 changes: 62 additions & 32 deletions flask/json/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -164,39 +168,66 @@ 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`.
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
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.
``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):
rv = rv.encode(encoding)
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:
Expand All @@ -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)
Expand Down
10 changes: 3 additions & 7 deletions flask/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
27 changes: 27 additions & 0 deletions tests/test_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,17 @@
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'
Expand Down Expand Up @@ -306,6 +312,27 @@ def echo():
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'
Expand Down

0 comments on commit a4f0f19

Please sign in to comment.