Skip to content

Commit 21b59b5

Browse files
authored
Merge pull request #507 from MongoEngine/implement_json_provider
Added: Support of Json Provider extension (Flask 2.2+); Compatibility with future Flask 2.3+ (#498)
2 parents 95b5e3f + ff64eec commit 21b59b5

File tree

4 files changed

+116
-19
lines changed

4 files changed

+116
-19
lines changed

docs/api/base.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ flask_mongoengine.json module
2828
-----------------------------
2929

3030
.. automodule:: flask_mongoengine.json
31+
:exclude-members: MongoEngineJSONProvider
3132

3233
flask_mongoengine.pagination module
3334
-----------------------------------

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@
240240

241241
intersphinx_mapping = {
242242
"python": ("https://docs.python.org/3", None),
243-
"flask": ("https://flask.palletsprojects.com/en/2.1.x/", None),
243+
"flask": ("https://flask.palletsprojects.com/en/2.2.x/", None),
244244
"werkzeug": ("https://werkzeug.palletsprojects.com/en/2.1.x/", None),
245245
"pymongo": ("https://pymongo.readthedocs.io/en/stable/", None),
246246
"mongoengine": ("https://docs.mongoengine.org/", None),

flask_mongoengine/json.py

Lines changed: 86 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,101 @@
1+
"""Flask application JSON extension functions."""
2+
from functools import lru_cache
3+
14
from bson import DBRef, ObjectId, json_util
2-
from flask.json import JSONEncoder
35
from mongoengine.base import BaseDocument
46
from mongoengine.queryset import QuerySet
57
from pymongo.command_cursor import CommandCursor
68

79

10+
@lru_cache(maxsize=1)
11+
def use_json_provider() -> bool:
12+
"""Split Flask before 2.2.0 and after, to use/not use JSON provider approach."""
13+
from flask import __version__
14+
15+
version = list(__version__.split("."))
16+
return int(version[0]) > 2 or (int(version[0]) == 2 and int(version[1]) > 1)
17+
18+
19+
# noinspection PyProtectedMember
20+
def _convert_mongo_objects(obj):
21+
"""Convert objects, related to Mongo database to JSON."""
22+
converted = None
23+
if isinstance(obj, BaseDocument):
24+
converted = json_util._json_convert(obj.to_mongo())
25+
elif isinstance(obj, QuerySet):
26+
converted = json_util._json_convert(obj.as_pymongo())
27+
elif isinstance(obj, CommandCursor):
28+
converted = json_util._json_convert(obj)
29+
elif isinstance(obj, DBRef):
30+
converted = obj.id
31+
elif isinstance(obj, ObjectId):
32+
converted = obj.__str__()
33+
return converted
34+
35+
836
def _make_encoder(superclass):
37+
"""Extend Flask JSON Encoder 'default' method with support of Mongo objects."""
38+
import warnings
39+
40+
warnings.warn(
41+
(
42+
"JSONEncoder/JSONDecoder are deprecated in Flask 2.2 and will be removed "
43+
"in Flask 2.3."
44+
),
45+
DeprecationWarning,
46+
stacklevel=2,
47+
)
48+
949
class MongoEngineJSONEncoder(superclass):
1050
"""
1151
A JSONEncoder which provides serialization of MongoEngine
1252
documents and queryset objects.
1353
"""
1454

1555
def default(self, obj):
16-
if isinstance(obj, BaseDocument):
17-
return json_util._json_convert(obj.to_mongo())
18-
elif isinstance(obj, QuerySet):
19-
return json_util._json_convert(obj.as_pymongo())
20-
elif isinstance(obj, CommandCursor):
21-
return json_util._json_convert(obj)
22-
elif isinstance(obj, DBRef):
23-
return obj.id
24-
elif isinstance(obj, ObjectId):
25-
return obj.__str__()
26-
return superclass.default(self, obj)
56+
"""Extend JSONEncoder default method, with Mongo objects."""
57+
if isinstance(
58+
obj,
59+
(BaseDocument, QuerySet, CommandCursor, DBRef, ObjectId),
60+
):
61+
return _convert_mongo_objects(obj)
62+
return super().default(obj)
2763

2864
return MongoEngineJSONEncoder
2965

3066

31-
MongoEngineJSONEncoder = _make_encoder(JSONEncoder)
67+
def _update_json_provider(superclass):
68+
"""Extend Flask Provider 'default' static method with support of Mongo objects."""
69+
70+
class MongoEngineJSONProvider(superclass):
71+
"""A JSON Provider update for Flask 2.2.0+"""
72+
73+
@staticmethod
74+
def default(obj):
75+
"""Extend JSONProvider default static method, with Mongo objects."""
76+
if isinstance(
77+
obj,
78+
(BaseDocument, QuerySet, CommandCursor, DBRef, ObjectId),
79+
):
80+
return _convert_mongo_objects(obj)
81+
return super().default(obj)
82+
83+
return MongoEngineJSONProvider
84+
85+
86+
# Compatibility code for Flask 2.2.0+ support
87+
MongoEngineJSONEncoder = None
88+
MongoEngineJSONProvider = None
89+
90+
if use_json_provider():
91+
from flask.json.provider import DefaultJSONProvider
92+
93+
MongoEngineJSONProvider = _update_json_provider(DefaultJSONProvider)
94+
else:
95+
from flask.json import JSONEncoder
96+
97+
MongoEngineJSONEncoder = _make_encoder(JSONEncoder)
98+
# End of compatibility code
3299

33100

34101
def override_json_encoder(app):
@@ -42,4 +109,9 @@ def override_json_encoder(app):
42109
NOTE: This does not cover situations where users override
43110
an instance's json_encoder after calling init_app.
44111
"""
45-
app.json_encoder = _make_encoder(app.json_encoder)
112+
113+
if use_json_provider():
114+
app.json_provider_class = _update_json_provider(app.json_provider_class)
115+
app.json = app.json_provider_class(app)
116+
else:
117+
app.json_encoder = _make_encoder(app.json_encoder)

tests/test_json.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1+
"""Extension of app JSON capabilities."""
12
import flask
23
import pytest
34

45
from flask_mongoengine import MongoEngine
6+
from flask_mongoengine.json import use_json_provider
57

68

79
@pytest.fixture()
810
def extended_db(app):
9-
app.json_encoder = DummyEncoder
11+
"""Provider config fixture."""
12+
if use_json_provider():
13+
app.json_provider_class = DummyProvider
14+
else:
15+
app.json_encoder = DummyEncoder
1016
app.config["MONGODB_SETTINGS"] = [
1117
{
1218
"db": "flask_mongoengine_test_db",
@@ -43,11 +49,29 @@ class DummyEncoder(flask.json.JSONEncoder):
4349
"""
4450

4551

52+
DummyProvider = None
53+
if use_json_provider():
54+
55+
class DummyProvider(flask.json.provider.DefaultJSONProvider):
56+
"""Dummy Provider, to test correct MRO in new flask versions."""
57+
58+
59+
@pytest.mark.skipif(condition=use_json_provider(), reason="New flask use other test")
4660
@pytest.mark.usefixtures("extended_db")
47-
def test_inheritance(app):
61+
def test_inheritance_old_flask(app):
4862
assert issubclass(app.json_encoder, DummyEncoder)
4963
json_encoder_name = app.json_encoder.__name__
5064

51-
# Since the class is dynamically derrived, must compare class names
52-
# rather than class objects.
5365
assert json_encoder_name == "MongoEngineJSONEncoder"
66+
67+
68+
@pytest.mark.skipif(
69+
condition=not use_json_provider(), reason="Old flask use other test"
70+
)
71+
@pytest.mark.usefixtures("extended_db")
72+
def test_inheritance(app):
73+
assert issubclass(app.json_provider_class, DummyProvider)
74+
json_provider_class = app.json_provider_class.__name__
75+
76+
assert json_provider_class == "MongoEngineJSONProvider"
77+
assert isinstance(app.json, DummyProvider)

0 commit comments

Comments
 (0)