Skip to content

Commit 7e22492

Browse files
authored
Merge pull request #122 from dcrosta/support-jsonify
Add `jsonify()` support using PyMongo's `bson.json_util`
2 parents 543950e + a9c5e09 commit 7e22492

File tree

5 files changed

+201
-34
lines changed

5 files changed

+201
-34
lines changed

docs/index.rst

+12-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ 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+
.. autoclass:: flask_pymongo.helpers.JSONEncoder
8587

8688
Configuration
8789
-------------
@@ -92,6 +94,13 @@ You can configure Flask-PyMongo either by passing a `MongoDB URI
9294
``MONGO_URI`` `Flask configuration variable
9395
<http://flask.pocoo.org/docs/1.0/config/>`_
9496

97+
The :class:`~flask_pymongo.PyMongo` instnace also accepts these additional
98+
customization options:
99+
100+
* ``json_options``, a :class:`~bson.json_util.JSONOptions` instance which
101+
controls the JSON serialization of MongoDB objects when used with
102+
:func:`~flask.json.jsonify`.
103+
95104
You may also pass additional keyword arguments to the ``PyMongo``
96105
constructor. These are passed directly through to the underlying
97106
:class:`~pymongo.mongo_client.MongoClient` object.
@@ -179,6 +188,8 @@ Changes:
179188
MongoDB 3.2 support.
180189
- `#130 <https://github.com/dcrosta/flask-pymongo/pull/130>`_ Fix
181190
quickstart example in README (Emmanuel Arias).
191+
- `#62 <https://github.com/dcrosta/flask-pymongo/issues/62>`_ Add
192+
support for :func:`~flask.json.jsonify()`.
182193

183194
- 2.3.0: April 24, 2019
184195

flask_pymongo/__init__.py

+5-33
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,17 @@
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
3433
from flask import abort, current_app, request
3534
from gridfs import GridFS, NoFile
3635
from pymongo import uri_parser
37-
from werkzeug.routing import BaseConverter
3836
from werkzeug.wsgi import wrap_file
3937
import pymongo
4038

39+
from flask_pymongo.helpers import BSONObjectIdConverter, JSONEncoder
4140
from flask_pymongo.wrappers import MongoClient
4241

4342

@@ -59,35 +58,6 @@
5958
"""Ascending sort order."""
6059

6160

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-
9161
class PyMongo(object):
9262

9363
"""Manages MongoDB connections for your Flask app.
@@ -102,9 +72,10 @@ class PyMongo(object):
10272
10373
"""
10474

105-
def __init__(self, app=None, uri=None, *args, **kwargs):
75+
def __init__(self, app=None, uri=None, json_options=None, *args, **kwargs):
10676
self.cx = None
10777
self.db = None
78+
self._json_encoder = partial(JSONEncoder, json_options=json_options)
10879

10980
if app is not None:
11081
self.init_app(app, uri, *args, **kwargs)
@@ -156,6 +127,7 @@ def init_app(self, app, uri=None, *args, **kwargs):
156127
self.db = self.cx[database_name]
157128

158129
app.url_map.converters["ObjectId"] = BSONObjectIdConverter
130+
app.json_encoder = self._json_encoder
159131

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

flask_pymongo/helpers.py

+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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+
import pymongo
36+
37+
if pymongo.version_tuple >= (3, 5, 0):
38+
from bson.json_util import RELAXED_JSON_OPTIONS
39+
DEFAULT_JSON_OPTIONS = RELAXED_JSON_OPTIONS
40+
else:
41+
DEFAULT_JSON_OPTIONS = None
42+
43+
44+
def _iteritems(obj):
45+
if hasattr(obj, "iteritems"):
46+
return obj.iteritems()
47+
elif hasattr(obj, "items"):
48+
return obj.items()
49+
else:
50+
raise TypeError("{!r} missing iteritems() and items()".format(obj))
51+
52+
53+
class BSONObjectIdConverter(BaseConverter):
54+
55+
"""A simple converter for the RESTful URL routing system of Flask.
56+
57+
.. code-block:: python
58+
59+
@app.route("/<ObjectId:task_id>")
60+
def show_task(task_id):
61+
task = mongo.db.tasks.find_one_or_404(task_id)
62+
return render_template("task.html", task=task)
63+
64+
Valid object ID strings are converted into
65+
:class:`~bson.objectid.ObjectId` objects; invalid strings result
66+
in a 404 error. The converter is automatically registered by the
67+
initialization of :class:`~flask_pymongo.PyMongo` with keyword
68+
:attr:`ObjectId`.
69+
70+
The :class:`~flask_pymongo.helpers.BSONObjectIdConverter` is
71+
automatically installed on the :class:`~flask_pymongo.PyMongo`
72+
instnace at creation time.
73+
74+
"""
75+
76+
def to_python(self, value):
77+
try:
78+
return ObjectId(value)
79+
except InvalidId:
80+
raise abort(404)
81+
82+
def to_url(self, value):
83+
return str(value)
84+
85+
86+
class JSONEncoder(flask_json.JSONEncoder):
87+
88+
"""A JSON encoder that uses :mod:`bson.json_util` for MongoDB documents.
89+
90+
.. code-block:: python
91+
92+
@app.route("/cart/<ObjectId:cart_id>")
93+
def json_route(cart_id):
94+
results = mongo.db.carts.find({"_id": cart_id})
95+
return jsonify(results)
96+
97+
# returns a Response with JSON body and application/json content-type:
98+
# '[{"count":12,"item":"egg"},{"count":1,"item":"apple"}]'
99+
100+
Since this uses PyMongo's JSON tools, certain types may serialize
101+
differently than you expect. See :class:`~bson.json_util.JSONOptions`
102+
for details on the particular serialization that will be used.
103+
104+
A :class:`~flask_pymongo.helpers.JSONEncoder` is automatically
105+
automatically installed on the :class:`~flask_pymongo.PyMongo`
106+
instance at creation time, using
107+
:const:`~bson.json_util.RELAXED_JSON_OPTIONS`. You can change the
108+
:class:`~bson.json_util.JSONOptions` in use by passing
109+
``json_options`` to the :class:`~flask_pymongo.PyMongo`
110+
constructor.
111+
112+
.. note::
113+
114+
:class:`~bson.json_util.JSONOptions` is only supported as of
115+
PyMongo version 3.4. For older versions of PyMongo, you will
116+
have less control over the JSON format that results from calls
117+
to :func:`~flask.json.jsonify`.
118+
119+
.. versionadded:: 2.4.0
120+
121+
"""
122+
123+
def __init__(self, json_options, *args, **kwargs):
124+
if json_options is None:
125+
json_options = DEFAULT_JSON_OPTIONS
126+
if json_options is not None:
127+
self._default_kwargs = {"json_options": json_options}
128+
else:
129+
self._default_kwargs = {}
130+
131+
super(JSONEncoder, self).__init__(*args, **kwargs)
132+
133+
def default(self, obj):
134+
"""Serialize MongoDB object types using :mod:`bson.json_util`.
135+
136+
Falls back to Flask's default JSON serialization for all other types.
137+
138+
This may raise ``TypeError`` for object types not recignozed.
139+
140+
.. versionadded:: 2.4.0
141+
142+
"""
143+
if hasattr(obj, "iteritems") or hasattr(obj, "items"):
144+
return SON((k, self.default(v)) for k, v in iteritems(obj))
145+
elif hasattr(obj, "__iter__") and not isinstance(obj, string_types):
146+
return [self.default(v) for v in obj]
147+
else:
148+
try:
149+
return json_util.default(obj, **self._default_kwargs)
150+
except TypeError:
151+
# PyMongo couldn't convert into a serializable object, and
152+
# the Flask default JSONEncoder won't; so we return the
153+
# object itself and let stdlib json handle it if possible
154+
return obj

flask_pymongo/tests/test_json.py

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

setup.py

+1
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)