From 1b58e1378404deed196f92b0e77b24275649a541 Mon Sep 17 00:00:00 2001 From: ziirish Date: Mon, 6 May 2019 13:59:13 +0200 Subject: [PATCH] let the user choose a custom json serializer through the RESTPLUS_JSON_SERIALIZER config option --- CHANGELOG.rst | 2 ++ doc/quickstart.rst | 60 +++++++++++++++++++++++++++++++ flask_restplus/representations.py | 19 +++++++++- requirements/test.pip | 1 + tests/test_representations.py | 22 ++++++++++++ 5 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 tests/test_representations.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 88b1ba4e..d950d887 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,8 @@ Changelog Current ------- +- **Behavior**: + * ``ujson`` is not the default json serializer anymore. A new configuration option is available instead: ``RESTPLUS_JSON_SERIALIZER`` (:issue:`507`, :issue:`587`, :issue:`589`, :pr:`637`) - Add new `Wildcard` fields (:pr:`255`) - Fix ABC deprecation warnings (:pr:`580`) - Fix `@api.expect(..., validate=False)` decorators for an :class:`Api` where `validate=True` is set on the constructor (:issue:`609`, :pr:`610`) diff --git a/doc/quickstart.rst b/doc/quickstart.rst index fcb74502..7098672b 100644 --- a/doc/quickstart.rst +++ b/doc/quickstart.rst @@ -308,6 +308,66 @@ parameter to some classes or function to force order preservation: - globally on :class:`Namespace`: ``ns = Namespace(ordered=True)`` - locally on :func:`marshal`: ``return marshal(data, fields, ordered=True)`` +Configuration +------------- + +The following configuration options exist for Flask-RESTPlus: + +============================ =============== ================================== + OPTION DEFAULT VALUE DESCRIPTION +============================ =============== ================================== +``BUNDLE_ERRORS`` ``False`` Bundle all the validation errors + instead of returning only the + first one encountered. + See the `Error Handling + `__ + section of the documentation for + details. +``RESTPLUS_VALIDATE`` ``False`` Whether to enforce payload + validation by default when using + the ``@api.expect()`` decorator. + See the `@api.expect() + `__ + documentation for details. +``RESTPLUS_MASK_HEADER`` ``X-Fields`` Choose the name of the *Header* + that will contain the masks to + apply to your answer. + See the `Fields masks `__ + documentation for details. +``RESTPLUS_MASK_SWAGGER`` ``True`` Whether to enable the mask + documentation in your swagger or + not. + See the `mask usage + `__ documentation + for details. +``RESTPLUS_JSON`` ``{}`` Dictionary of options to pass to + the json *serializer* (by default + ``json.dumps``). +``RESTPLUS_JSON_SERIALIZER`` ``None`` Here you can choose your + own/preferred json *serializer*. + You can either specify the name + of the module (example: ``ujson``) + or you can give the full name of + your *serializer* (example: + ``ujson.dumps``). + + .. note:: + If you only specify the module + name the default Flask-RESTPLUS + behavior is to import its + ``dumps`` method. + + + .. warning:: + We only officially support + python's builtin + ``json.dumps``. + Please keep in mind some + serializers may behave + differently depending on the + input types (floats, dates, + etc.). +============================ =============== ================================== Full example ------------ diff --git a/flask_restplus/representations.py b/flask_restplus/representations.py index ff1b3782..4fec9bfe 100644 --- a/flask_restplus/representations.py +++ b/flask_restplus/representations.py @@ -1,15 +1,32 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals, absolute_import +import importlib + from json import dumps from flask import make_response, current_app +serializer = None + def output_json(data, code, headers=None): '''Makes a Flask response with a JSON encoded body''' + global serializer + settings = current_app.config.get('RESTPLUS_JSON', {}) + custom_serializer = current_app.config.get('RESTPLUS_JSON_SERIALIZER', None) + + # If the user wants to use a custom serializer, let it be + if serializer is None and custom_serializer: + try: + serializer = importlib.import_module("{}.dumps".format(custom_serializer) if '.' not in custom_serializer + else custom_serializer) + except ImportError: + serializer = dumps + elif serializer is None: + serializer = dumps # 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 @@ -19,7 +36,7 @@ def output_json(data, code, headers=None): # always end the json dumps with a new line # see https://github.com/mitsuhiko/flask/pull/1262 - dumped = dumps(data, **settings) + "\n" + dumped = serializer(data, **settings) + "\n" resp = make_response(dumped, code) resp.headers.extend(headers or {}) diff --git a/requirements/test.pip b/requirements/test.pip index 043c0e09..d223625e 100644 --- a/requirements/test.pip +++ b/requirements/test.pip @@ -9,3 +9,4 @@ pytest-mock==1.6.3 pytest-profiling==1.2.11 pytest-sugar==0.9.0 tzlocal +ujson diff --git a/tests/test_representations.py b/tests/test_representations.py new file mode 100644 index 00000000..201027dd --- /dev/null +++ b/tests/test_representations.py @@ -0,0 +1,22 @@ +import flask_restplus.representations as rep + +from json import dumps, loads +from ujson import dumps as udumps, loads as uloads + +from flask import current_app + + +def test_representations(api): + payload = { + 'id': 1, + 'name': 'toto', + 'address': 'test', + } + r = rep.output_json(payload, 200) + assert loads(r.get_data(True)) == loads(dumps(payload)) + # now reset serializer + rep.serializer = None + # then enforce a custom serializer + current_app.config['RESTPLUS_JSON_SERIALIZER'] = 'ujson' + r2 = rep.output_json(payload, 200) + assert uloads(r2.get_data(True)) == uloads(udumps(payload))