diff --git a/AUTHORS.md b/AUTHORS.md index 8e227b68..94a27003 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -4,6 +4,7 @@ Authors A huge thanks to all of our contributors: +- Alec Reiter - Alex Gaynor - Alex M - Alex Morken diff --git a/flask_restful/representations/json.py b/flask_restful/representations/json.py index ce6a77a8..f96189e4 100644 --- a/flask_restful/representations/json.py +++ b/flask_restful/representations/json.py @@ -3,27 +3,21 @@ from json import dumps -# This dictionary contains any kwargs that are to be passed to the json.dumps -# function, used below. -settings = {} - - def output_json(data, code, headers=None): """Makes a Flask response with a JSON encoded body""" + settings = current_app.config.get('RESTFUL_JSON', {}) + # If we're in debug mode, and the indent is not set, we set it to a # reasonable value here. Note that this won't override any existing value # that was set. We also set the "sort_keys" value. - local_settings = settings.copy() if current_app.debug: - local_settings.setdefault('indent', 4) - local_settings.setdefault('sort_keys', True) + settings.setdefault('indent', 4) + settings.setdefault('sort_keys', True) - # We also add a trailing newline to the dumped JSON if the indent value is - # set - this makes using `curl` on the command line much nicer. - dumped = dumps(data, **local_settings) - if 'indent' in local_settings: - dumped += '\n' + # always end the json dumps with a new line + # see https://github.com/mitsuhiko/flask/pull/1262 + dumped = dumps(data, **settings) + "\n" resp = make_response(dumped, code) resp.headers.extend(headers or {}) diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 12d2afe5..00000000 --- a/tests/conftest.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python - -import pytest - -from flask import Flask -from flask_restful import Api - - -@pytest.fixture -def app(): - return Flask(__name__) - - -@pytest.fixture -def api(app): - return Api(app) diff --git a/tests/test_api.py b/tests/test_api.py index 69dff2f9..d8d5f2e1 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -3,10 +3,10 @@ from flask import Flask, Blueprint, redirect, views from flask.signals import got_request_exception, signals_available try: - from mock import Mock, patch + from mock import Mock except: # python3 - from unittest.mock import Mock, patch + from unittest.mock import Mock import flask import werkzeug from werkzeug.exceptions import HTTPException, Unauthorized, BadRequest, NotFound @@ -14,7 +14,7 @@ import flask_restful import flask_restful.fields from flask_restful import OrderedDict -from json import dumps, loads +from json import dumps, loads, JSONEncoder #noinspection PyUnresolvedReferences from nose.tools import assert_equals, assert_true, assert_false # you need it for tests in form of continuations import six @@ -115,7 +115,7 @@ def test_handle_error_does_not_swallow_exceptions(self): with app.test_request_context('/foo'): resp = api.handle_error(exception) self.assertEquals(resp.status_code, 400) - self.assertEquals(resp.get_data(), b'{"message": "x"}') + self.assertEquals(resp.get_data(), b'{"message": "x"}\n') def test_marshal(self): @@ -359,7 +359,9 @@ def test_handle_server_error(self): with app.test_request_context("/foo"): resp = api.handle_error(Exception()) self.assertEquals(resp.status_code, 500) - self.assertEquals(resp.data.decode(), dumps({"message": "Internal Server Error"})) + self.assertEquals(resp.data.decode(), dumps({ + "message": "Internal Server Error" + }) + "\n") def test_handle_error_with_code(self): app = Flask(__name__) @@ -372,7 +374,7 @@ def test_handle_error_with_code(self): with app.test_request_context("/foo"): resp = api.handle_error(exception) self.assertEquals(resp.status_code, 500) - self.assertEquals(resp.data.decode(), dumps({"foo": "bar"})) + self.assertEquals(resp.data.decode(), dumps({"foo": "bar"}) + "\n") def test_handle_auth(self): app = Flask(__name__) @@ -381,7 +383,7 @@ def test_handle_auth(self): with app.test_request_context("/foo"): resp = api.handle_error(Unauthorized()) self.assertEquals(resp.status_code, 401) - expected_data = dumps({'message': Unauthorized.description}) + expected_data = dumps({'message': Unauthorized.description}) + "\n" self.assertEquals(resp.data.decode(), expected_data) self.assertTrue('WWW-Authenticate' in resp.headers) @@ -453,7 +455,7 @@ def test_handle_error(self): self.assertEquals(resp.status_code, 400) self.assertEquals(resp.data.decode(), dumps({ 'message': BadRequest.description, - })) + }) + "\n") def test_handle_smart_errors(self): app = Flask(__name__) @@ -464,6 +466,13 @@ def test_handle_smart_errors(self): api.add_resource(view, '/fee', endpoint='bir') api.add_resource(view, '/fii', endpoint='ber') + with app.test_request_context("/faaaaa"): + resp = api.handle_error(NotFound()) + self.assertEquals(resp.status_code, 404) + self.assertEquals(resp.data.decode(), dumps({ + "message": NotFound.description, + }) + "\n") + with app.test_request_context("/fOo"): resp = api.handle_error(NotFound()) self.assertEquals(resp.status_code, 404) @@ -476,7 +485,7 @@ def test_handle_smart_errors(self): self.assertEquals(resp.status_code, 404) self.assertEquals(resp.data.decode(), dumps({ "message": NotFound.description - })) + }) + "\n") def test_error_router_falls_back_to_original(self): """Verify that if an exception occurs in the Flask-RESTful error handler, @@ -576,9 +585,9 @@ def get(self): with app.test_client() as client: foo1 = client.get('/foo') - self.assertEquals(foo1.data, b'"foo1"') + self.assertEquals(foo1.data, b'"foo1"\n') foo2 = client.get('/foo/toto') - self.assertEquals(foo2.data, b'"foo1"') + self.assertEquals(foo2.data, b'"foo1"\n') def test_add_resource(self): app = Mock(flask.Flask) @@ -632,7 +641,7 @@ def get(self): with app.test_client() as client: foo = client.get('/foo') - self.assertEquals(foo.data, b'"wonderful slurm"') + self.assertEquals(foo.data, b'"wonderful slurm"\n') def test_output_unpack(self): @@ -646,7 +655,7 @@ def make_empty_response(): wrapper = api.output(make_empty_response) resp = wrapper() self.assertEquals(resp.status_code, 200) - self.assertEquals(resp.data.decode(), '{"foo": "bar"}') + self.assertEquals(resp.data.decode(), '{"foo": "bar"}\n') def test_output_func(self): @@ -800,34 +809,69 @@ def get(self): # Assert our trailing newline. self.assertTrue(foo.data.endswith(b'\n')) - def test_will_pass_options_to_json(self): + def test_read_json_settings_from_config(self): + class TestConfig(object): + RESTFUL_JSON = {'indent': 2, + 'sort_keys': True, + 'separators': (', ', ': ')} app = Flask(__name__) + app.config.from_object(TestConfig) api = flask_restful.Api(app) - class Foo1(flask_restful.Resource): + class Foo(flask_restful.Resource): def get(self): - return {'foo': 'bar'} + return {'foo': 'bar', 'baz': 'qux'} - api.add_resource(Foo1, '/foo', endpoint='bar') + api.add_resource(Foo, '/foo') + + with app.test_client() as client: + data = client.get('/foo').data + + expected = b'{\n "baz": "qux", \n "foo": "bar"\n}\n' + + self.assertEquals(data, expected) + + + def test_use_custom_jsonencoder(self): + class CabageEncoder(JSONEncoder): + def default(self, obj): + return 'cabbage' - # We patch the representations module here, with two things: - # 1. Set the settings dict() with some value - # 2. Patch the json.dumps function in the module with a Mock object. + class TestConfig(object): + RESTFUL_JSON = {'cls': CabageEncoder} - from flask_restful.representations import json as json_rep - json_dumps_mock = Mock(return_value='bar') - new_settings = {'indent': 123} + app = Flask(__name__) + app.config.from_object(TestConfig) + api = flask_restful.Api(app) + + class Cabbage(flask_restful.Resource): + def get(self): + return {'frob': object()} + + api.add_resource(Cabbage, '/cabbage') - with patch.multiple(json_rep, dumps=json_dumps_mock, - settings=new_settings): - with app.test_client() as client: - client.get('/foo') + with app.test_client() as client: + data = client.get('/cabbage').data + + expected = b'{"frob": "cabbage"}\n' + self.assertEquals(data, expected) + + def test_json_with_no_settings(self): + app = Flask(__name__) + api = flask_restful.Api(app) + + class Foo(flask_restful.Resource): + def get(self): + return {'foo': 'bar'} + + api.add_resource(Foo, '/foo') + + with app.test_client() as client: + data = client.get('/foo').data - # Assert that the function was called with the above settings. - data, kwargs = json_dumps_mock.call_args - self.assertTrue(json_dumps_mock.called) - self.assertEqual(123, kwargs['indent']) + expected = b'{"foo": "bar"}\n' + self.assertEquals(data, expected) def test_redirect(self): app = Flask(__name__) @@ -858,7 +902,7 @@ def get(self): app = app.test_client() resp = app.get('/api') self.assertEquals(resp.status_code, 200) - self.assertEquals(resp.data.decode('utf-8'), '{"foo": 3.0}') + self.assertEquals(resp.data.decode('utf-8'), '{"foo": 3.0}\n') def test_custom_error_message(self): errors = {