Skip to content

Commit fe511e6

Browse files
committed
Add jsonify() support using PyMongo's bson.json_util
Closes #62
1 parent d2d1835 commit fe511e6

File tree

5 files changed

+192
-34
lines changed

5 files changed

+192
-34
lines changed

docs/index.rst

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,21 @@ Flask-PyMongo provides helpers for some common tasks:
8181

8282
.. automethod:: flask_pymongo.PyMongo.save_file
8383

84-
.. autoclass:: flask_pymongo.BSONObjectIdConverter
84+
.. autoclass:: flask_pymongo.helpers.BSONObjectIdConverter
85+
86+
The :class:`~flask_pymongo.helpers.BSONObjectIdConverter` is
87+
automatically installed on the :class:`~flask_pymongo.PyMongo`
88+
instnace at creation time.
89+
90+
.. autoclass:: flask_pymongo.helpers.JSONEncoder
91+
92+
A :class:`~flask_pymongo.helpers.JSONEncoder` is automatically
93+
automatically installed on the :class:`~flask_pymongo.PyMongo`
94+
instance at creation time, using
95+
:const:`~bson.json_util.RELAXED_JSON_OPTIONS`. You can change
96+
the :class:`~bson.json_util.JSONOptions` in use by passing
97+
``json_options`` to the :class:`~flask_pymongo.PyMongo`
98+
constructor.
8599

86100
Configuration
87101
-------------
@@ -92,6 +106,13 @@ You can configure Flask-PyMongo either by passing a `MongoDB URI
92106
``MONGO_URI`` `Flask configuration variable
93107
<http://flask.pocoo.org/docs/1.0/config/>`_
94108

109+
The :class:`~flask_pymongo.PyMongo` instnace also accepts these additional
110+
customization options:
111+
112+
* ``json_options``, a :class:`~bson.json_util.JSONOptions` instance which
113+
controls the JSON serialization of MongoDB objects when used with
114+
:func:`~flask.json.jsonify`.
115+
95116
You may also pass additional keyword arguments to the ``PyMongo``
96117
constructor. These are passed directly through to the underlying
97118
:class:`~pymongo.mongo_client.MongoClient` object.
@@ -173,6 +194,11 @@ History and Contributors
173194

174195
Changes:
175196

197+
- 2.4.0: TBD
198+
199+
- `#62 <https://github.com/dcrosta/flask-pymongo/issues/62>`_ Add
200+
support for :func:`~flask.json.jsonify()`.
201+
176202
- 2.3.0: April 24, 2019
177203

178204
- Update version compatibility matrix in tests, drop official support for

flask_pymongo/__init__.py

Lines changed: 13 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,18 @@
2626

2727
__all__ = ("PyMongo", "ASCENDING", "DESCENDING")
2828

29+
from functools import partial
2930
from mimetypes import guess_type
3031
import sys
3132

32-
from bson.errors import InvalidId
33-
from bson.objectid import ObjectId
33+
from bson.json_util import RELAXED_JSON_OPTIONS
3434
from flask import abort, current_app, request
3535
from gridfs import GridFS, NoFile
3636
from pymongo import uri_parser
37-
from werkzeug.routing import BaseConverter
3837
from werkzeug.wsgi import wrap_file
3938
import pymongo
4039

40+
from flask_pymongo.helpers import BSONObjectIdConverter, JSONEncoder
4141
from flask_pymongo.wrappers import MongoClient
4242

4343

@@ -59,35 +59,6 @@
5959
"""Ascending sort order."""
6060

6161

62-
class BSONObjectIdConverter(BaseConverter):
63-
64-
"""A simple converter for the RESTful URL routing system of Flask.
65-
66-
.. code-block:: python
67-
68-
@app.route("/<ObjectId:task_id>")
69-
def show_task(task_id):
70-
task = mongo.db.tasks.find_one_or_404(task_id)
71-
return render_template("task.html", task=task)
72-
73-
Valid object ID strings are converted into
74-
:class:`~bson.objectid.ObjectId` objects; invalid strings result
75-
in a 404 error. The converter is automatically registered by the
76-
initialization of :class:`~flask_pymongo.PyMongo` with keyword
77-
:attr:`ObjectId`.
78-
79-
"""
80-
81-
def to_python(self, value):
82-
try:
83-
return ObjectId(value)
84-
except InvalidId:
85-
raise abort(404)
86-
87-
def to_url(self, value):
88-
return str(value)
89-
90-
9162
class PyMongo(object):
9263

9364
"""Manages MongoDB connections for your Flask app.
@@ -102,9 +73,17 @@ class PyMongo(object):
10273
10374
"""
10475

105-
def __init__(self, app=None, uri=None, *args, **kwargs):
76+
def __init__(
77+
self,
78+
app=None,
79+
uri=None,
80+
json_options=RELAXED_JSON_OPTIONS,
81+
*args,
82+
**kwargs # noqa: C816
83+
):
10684
self.cx = None
10785
self.db = None
86+
self._json_encoder = partial(JSONEncoder, json_options=json_options)
10887

10988
if app is not None:
11089
self.init_app(app, uri, *args, **kwargs)
@@ -156,6 +135,7 @@ def init_app(self, app, uri=None, *args, **kwargs):
156135
self.db = self.cx[database_name]
157136

158137
app.url_map.converters["ObjectId"] = BSONObjectIdConverter
138+
app.json_encoder = self._json_encoder
159139

160140
# view helpers
161141
def send_file(self, filename, base="fs", version=-1, cache_for=31536000):

flask_pymongo/helpers.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Copyright (c) 2011-2019, Dan Crosta
2+
# All rights reserved.
3+
#
4+
# Redistribution and use in source and binary forms, with or without
5+
# modification, are permitted provided that the following conditions are met:
6+
#
7+
# * Redistributions of source code must retain the above copyright notice,
8+
# this list of conditions and the following disclaimer.
9+
#
10+
# * Redistributions in binary form must reproduce the above copyright notice,
11+
# this list of conditions and the following disclaimer in the documentation
12+
# and/or other materials provided with the distribution.
13+
#
14+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
18+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
24+
# POSSIBILITY OF SUCH DAMAGE.
25+
26+
27+
__all__ = ("BSONObjectIdConverter", "JSONEncoder")
28+
29+
from bson import json_util, SON
30+
from bson.errors import InvalidId
31+
from bson.objectid import ObjectId
32+
from flask import abort, json as flask_json
33+
from six import iteritems, string_types
34+
from werkzeug.routing import BaseConverter
35+
36+
37+
def _iteritems(obj):
38+
if hasattr(obj, "iteritems"):
39+
return obj.iteritems()
40+
elif hasattr(obj, "items"):
41+
return obj.items()
42+
else:
43+
raise TypeError("{!r} missing iteritems() and items()".format(obj))
44+
45+
46+
class BSONObjectIdConverter(BaseConverter):
47+
48+
"""A simple converter for the RESTful URL routing system of Flask.
49+
50+
.. code-block:: python
51+
52+
@app.route("/<ObjectId:task_id>")
53+
def show_task(task_id):
54+
task = mongo.db.tasks.find_one_or_404(task_id)
55+
return render_template("task.html", task=task)
56+
57+
Valid object ID strings are converted into
58+
:class:`~bson.objectid.ObjectId` objects; invalid strings result
59+
in a 404 error. The converter is automatically registered by the
60+
initialization of :class:`~flask_pymongo.PyMongo` with keyword
61+
:attr:`ObjectId`.
62+
63+
"""
64+
65+
def to_python(self, value):
66+
try:
67+
return ObjectId(value)
68+
except InvalidId:
69+
raise abort(404)
70+
71+
def to_url(self, value):
72+
return str(value)
73+
74+
75+
class JSONEncoder(flask_json.JSONEncoder):
76+
77+
"""A JSON encoder that uses :mod:`bson.json_util` for MongoDB documents.
78+
79+
.. code-block:: python
80+
81+
@app.route("/cart/<ObjectId:cart_id>")
82+
def json_route(cart_id):
83+
results = mongo.db.carts.find({"_id": cart_id})
84+
return jsonify(results)
85+
86+
# returns a Response with body content:
87+
# '[{"count":12,"item":"egg"},{"count":1,"item":"apple"}]\\n'
88+
89+
.. note::
90+
91+
Since this uses PyMongo's JSON tools, certain types may
92+
serialize differently than you expect. See
93+
:class:`~bson.json_util.JSONOptions` for details on the
94+
particular serialization that will be used.
95+
96+
"""
97+
98+
def __init__(self, json_options, *args, **kwargs):
99+
self._json_options = json_options
100+
super(JSONEncoder, self).__init__(*args, **kwargs)
101+
102+
def default(self, obj):
103+
"""Serialize MongoDB object types using :mod:`bson.json_util`.
104+
105+
Falls back to Flask's default JSON serialization for all other types.
106+
107+
This may raise ``TypeError`` for object types not recignozed.
108+
109+
.. versionadded:: 2.4.0
110+
111+
"""
112+
if hasattr(obj, "iteritems") or hasattr(obj, "items"):
113+
return SON((k, self.default(v)) for k, v in iteritems(obj))
114+
elif hasattr(obj, "__iter__") and not isinstance(obj, string_types):
115+
return [self.default(v) for v in obj]
116+
else:
117+
try:
118+
return json_util.default(obj)
119+
except TypeError:
120+
# PyMongo couldn't convert into a serializable object, and
121+
# the Flask default JSONEncoder won't; so we return the
122+
# object itself and let stdlib json handle it if possible
123+
return obj

flask_pymongo/tests/test_json.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import json
2+
3+
from bson import ObjectId
4+
from flask import jsonify
5+
6+
from flask_pymongo.tests.util import FlaskPyMongoTest
7+
8+
9+
class JSONTest(FlaskPyMongoTest):
10+
11+
def test_it_encodes_json(self):
12+
resp = jsonify({"foo": "bar"})
13+
dumped = json.loads(resp.get_data())
14+
self.assertEqual(dumped, {"foo": "bar"})
15+
16+
def test_it_handles_pymongo_types(self):
17+
resp = jsonify({"id": ObjectId("5cf29abb5167a14c9e6e12c4")})
18+
dumped = json.loads(resp.get_data())
19+
self.assertEqual(dumped, {"id": {"$oid": "5cf29abb5167a14c9e6e12c4"}})
20+
21+
def test_it_jsonifies_a_cursor(self):
22+
self.mongo.db.rows.insert_many([{"foo": "bar"}, {"foo": "baz"}])
23+
24+
curs = self.mongo.db.rows.find(projection={"_id": False}).sort("foo")
25+
26+
resp = jsonify(curs)
27+
dumped = json.loads(resp.get_data())
28+
self.assertEqual([{"foo": "bar"}, {"foo": "baz"}], dumped)

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
install_requires=[
3333
"Flask>=0.11",
3434
"PyMongo>=3.3",
35+
"six",
3536
],
3637
classifiers=[
3738
"Environment :: Web Environment",

0 commit comments

Comments
 (0)