Skip to content

Commit

Permalink
notify the user in case we couldn't load the custom serializer throug…
Browse files Browse the repository at this point in the history
…h a warnings.warn(UserWarning)
  • Loading branch information
ziirish committed May 27, 2019
1 parent 813ac5d commit bb1006c
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 47 deletions.
12 changes: 7 additions & 5 deletions doc/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -360,11 +360,13 @@ The following configuration options exist for Flask-RESTPlus:

.. note::
Flask-RESTPlus will always
silently fallback to the
default ``json.dumps``
*serializer* if it cannot
manage to import the one
you configured.
fallback to the default
``json.dumps`` *serializer*
if it cannot manage to import
the one you configured.
In such case, a
``UserWarning`` will be
raised.


.. warning::
Expand Down
3 changes: 2 additions & 1 deletion flask_restplus/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from .postman import PostmanCollectionV1
from .resource import Resource
from .swagger import Swagger
from .utils import default_id, camel_to_dash, unpack
from .utils import default_id, camel_to_dash, preload_serializer, unpack
from .representations import output_json
from ._http import HTTPStatus

Expand Down Expand Up @@ -209,6 +209,7 @@ def _init_app(self, app):
self._validate = self._validate if self._validate is not None else app.config.get('RESTPLUS_VALIDATE', False)
app.config.setdefault('RESTPLUS_MASK_HEADER', 'X-Fields')
app.config.setdefault('RESTPLUS_MASK_SWAGGER', True)
preload_serializer(app)

def __getattr__(self, name):
try:
Expand Down
34 changes: 5 additions & 29 deletions flask_restplus/representations.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, absolute_import

import importlib

from json import dumps

from flask import make_response, current_app

DEFAULT_SERIALIZER = 'dumps'
serializer = None

from json import dumps

def _importer(mod_name, func_name=DEFAULT_SERIALIZER, default=dumps):
imported = importlib.import_module(mod_name)
return getattr(imported, func_name, default)
from .utils import preload_serializer


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 = _importer(custom_serializer)
except ImportError:
if '.' in custom_serializer:
mod, func = custom_serializer.rsplit('.', 1)
try:
serializer = _importer(mod, func)
except ImportError:
pass

# fallback, no serializer found so far, use the default one
serializer = current_app.config.get('RESTPLUS_CACHED_SERIALIZER')
if serializer is None:
serializer = dumps
preload_serializer(current_app)
serializer = current_app.config.get('RESTPLUS_CACHED_SERIALIZER')

# 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
Expand Down
48 changes: 47 additions & 1 deletion flask_restplus/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
from __future__ import unicode_literals

import re
import importlib
import warnings

from collections import OrderedDict
from copy import deepcopy
from json import dumps
from six import iteritems

from ._http import HTTPStatus
Expand All @@ -14,7 +17,50 @@
ALL_CAP_RE = re.compile('([a-z0-9])([A-Z])')


__all__ = ('merge', 'camel_to_dash', 'default_id', 'not_none', 'not_none_sorted', 'unpack')
__all__ = ('preload_serializer', 'importer', 'merge', 'camel_to_dash', 'default_id', 'not_none', 'not_none_sorted', 'unpack')


def preload_serializer(app):
'''
Preload the json serializer for the given ``app``.
:param flask.Flask app: The flask application object
'''
custom_serializer = app.config.get('RESTPLUS_JSON_SERIALIZER', None)
serializer = None

# If the user wants to use a custom serializer, let it be
if custom_serializer:
try:
serializer = importer(custom_serializer, 'dumps')
except ImportError:
if '.' in custom_serializer:
mod, func = custom_serializer.rsplit('.', 1)
try:
serializer = importer(mod, func)
except ImportError:
warnings.warn("Unable to load custom serializer '{}', falling back to "
"'json.dumps'".format(custom_serializer),
UserWarning)

# fallback, no serializer found so far, use the default one
if serializer is None:
serializer = dumps
app.config['RESTPLUS_CACHED_SERIALIZER'] = serializer


def importer(mod_name, obj_name, default=None):
'''
Import the given ``obj_name`` from the given ``mod_name``.
:param str mod_name: Module from which to import the ``obj_name``
:param str obj_name: Object to import from ``mod_name``
:param object default: Default object to return
:return: Imported object
'''
imported = importlib.import_module(mod_name)
return getattr(imported, obj_name, default)


def merge(first, second):
Expand Down
27 changes: 16 additions & 11 deletions tests/test_representations.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import pytest

import flask_restplus.representations as rep
from flask_restplus.utils import preload_serializer

from json import dumps, loads
from ujson import dumps as udumps, loads as uloads
Expand All @@ -11,31 +14,33 @@


def test_representations_serialization_output_correct(app):
print(app.config)
r = rep.output_json(payload, 200)
assert loads(r.get_data(True)) == loads(dumps(payload))


def test_config_custom_serializer_is_module(app):
# now reset serializer
rep.serializer = None
# then enforce a custom serializer
def test_config_custom_serializer_is_module(app, api):
# enforce a custom serializer
app.config['RESTPLUS_JSON_SERIALIZER'] = 'ujson'
# now reset serializer
preload_serializer(app)
r2 = rep.output_json(payload, 200)
assert uloads(r2.get_data(True)) == uloads(udumps(payload))
assert rep.serializer == udumps
assert app.config.get('RESTPLUS_CACHED_SERIALIZER') == udumps


def test_config_custom_serializer_is_function(app):
def test_config_custom_serializer_is_function(app, api):
# test other config syntax
rep.serializer = None
app.config['RESTPLUS_JSON_SERIALIZER'] = 'ujson.dumps'
preload_serializer(app)
rep.output_json(payload, 200)
assert rep.serializer == udumps
assert app.config.get('RESTPLUS_CACHED_SERIALIZER') == udumps


def test_config_custom_serializer_fallback(app):
def test_config_custom_serializer_fallback(app, api):
# test fallback
rep.serializer = None
app.config['RESTPLUS_JSON_SERIALIZER'] = 'ujson.lol.dumps'
with pytest.warns(UserWarning):
preload_serializer(app)
rep.output_json(payload, 200)
assert rep.serializer == dumps
assert app.config.get('RESTPLUS_CACHED_SERIALIZER') == dumps

0 comments on commit bb1006c

Please sign in to comment.