Skip to content

Commit 8367d49

Browse files
committed
Added json helpers for Binary and Code types PYTHON-362
1 parent 1913ae0 commit 8367d49

File tree

3 files changed

+146
-48
lines changed

3 files changed

+146
-48
lines changed

bson/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,3 +565,11 @@ def has_c():
565565
.. versionadded:: 1.9
566566
"""
567567
return _use_c
568+
569+
570+
def has_uuid():
571+
"""Is the uuid module available?
572+
573+
.. versionadded:: 2.2.1+
574+
"""
575+
return _use_uuid

bson/json_util.py

Lines changed: 90 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,43 @@
1414

1515
"""Tools for using Python's :mod:`json` module with BSON documents.
1616
17-
This module provides two methods: `object_hook` and `default`. These
18-
names are pretty terrible, but match the names used in Python's `json
19-
library <http://docs.python.org/library/json.html>`_. They allow for
20-
specialized encoding and decoding of BSON documents into `Mongo
21-
Extended JSON
17+
This module provides two helper methods `dumps` and `loads` that wrap the
18+
native :mod:`json` methods and provide explicit BSON conversion to and from
19+
json. This allows for specialized encoding and decoding of BSON documents
20+
into `Mongo Extended JSON
2221
<http://www.mongodb.org/display/DOCS/Mongo+Extended+JSON>`_'s *Strict*
2322
mode. This lets you encode / decode BSON documents to JSON even when
2423
they use special BSON types.
2524
2625
Example usage (serialization)::
2726
28-
>>> json.dumps(..., default=json_util.default)
27+
.. doctest::
28+
29+
>>> from bson import Binary, Code
30+
>>> from bson.json_util import dumps
31+
>>> dumps([{'foo': [1, 2]},
32+
... {'bar': {'hello': 'world'}},
33+
... {'code': Code("function x() { return 1; }")},
34+
... {'bin': Binary("\x00\x01\x02\x03\x04")}])
35+
'[{"foo": [1, 2]}, {"bar": {"hello": "world"}}, {"code": {"$scope": {}, "$code": "function x() { return 1; }"}}, {"bin": {"$type": 0, "$binary": "AAECAwQ=\\n"}}]'
2936
3037
Example usage (deserialization)::
3138
32-
>>> json.loads(..., object_hook=json_util.object_hook)
39+
.. doctest::
40+
41+
>>> from bson.json_util import loads
42+
>>> loads('[{"foo": [1, 2]}, {"bar": {"hello": "world"}}, {"code": {"$scope": {}, "$code": "function x() { return 1; }"}}, {"bin": {"$type": 0, "$binary": "AAECAwQ=\\n"}}]')
43+
[{u'foo': [1, 2]}, {u'bar': {u'hello': u'world'}}, {u'code': Code('function x() { return 1; }', {})}, {u'bin': Binary('\x00\x01\x02\x03\x04', 0)}]
3344
34-
Currently this does not handle special encoding and decoding for
35-
:class:`~bson.binary.Binary` and :class:`~bson.code.Code` instances.
45+
Alternatively, you can manually pass the `default` to :func:`json.dumps`.
46+
It won't handle :class:`~bson.binary.Binary` and :class:`~bson.code.Code`
47+
instances (as they are extended strings you can't provide custom defaults),
48+
but it will be faster as there is less recursion.
49+
50+
.. versionchanged:: 2.2.1+
51+
Added dumps and loads helpers to automatically handle conversion to and
52+
from json and supports :class:`~bson.binary.Binary` and
53+
:class:`~bson.code.Code`
3654
3755
.. versionchanged:: 1.9
3856
Handle :class:`uuid.UUID` instances, whenever possible.
@@ -50,34 +68,72 @@
5068
Added support for encoding/decoding datetimes and regular expressions.
5169
"""
5270

71+
import base64
5372
import calendar
5473
import datetime
5574
import re
75+
76+
json_lib = True
5677
try:
57-
import uuid
58-
_use_uuid = True
78+
import json
5979
except ImportError:
60-
_use_uuid = False
80+
try:
81+
import simplejson as json
82+
except ImportError:
83+
json_lib = False
6184

85+
import bson
6286
from bson import EPOCH_AWARE
87+
from bson.binary import Binary
88+
from bson.code import Code
6389
from bson.dbref import DBRef
6490
from bson.max_key import MaxKey
6591
from bson.min_key import MinKey
6692
from bson.objectid import ObjectId
6793
from bson.timestamp import Timestamp
68-
from bson.tz_util import utc
6994

70-
# TODO support Binary and Code
71-
# Binary and Code are tricky because they subclass str so json thinks it can
72-
# handle them. Not sure what the proper way to get around this is...
73-
#
74-
# One option is to just add some other method that users need to call _before_
75-
# calling json.dumps or json.loads. That is pretty terrible though...
95+
from bson.py3compat import PY3, binary_type, string_types
7696

7797
# TODO share this with bson.py?
7898
_RE_TYPE = type(re.compile("foo"))
7999

80100

101+
def dumps(obj, *args, **kwargs):
102+
"""Helper function that wraps :class:`json.dumps`.
103+
104+
Recursive function that handles all BSON types incuding
105+
:class:`~bson.binary.Binary` and :class:`~bson.code.Code`.
106+
"""
107+
if not json_lib:
108+
raise Exception("No json library available")
109+
return json.dumps(_json_convert(obj), *args, **kwargs)
110+
111+
112+
def loads(s, *args, **kwargs):
113+
"""Helper function that wraps :class:`json.loads`.
114+
115+
Automatically passes the object_hook for BSON type conversion.
116+
"""
117+
if not json_lib:
118+
raise Exception("No json library available")
119+
kwargs['object_hook'] = object_hook
120+
return json.loads(s, *args, **kwargs)
121+
122+
123+
def _json_convert(obj):
124+
"""Recursive helper method that converts BSON types so they can be
125+
converted into json.
126+
"""
127+
if hasattr(obj, 'iteritems') or hasattr(obj, 'items'): # PY3 support
128+
return dict(((k, _json_convert(v)) for k, v in obj.iteritems()))
129+
elif hasattr(obj, '__iter__') and not isinstance(obj, string_types):
130+
return list((_json_convert(v) for v in obj))
131+
try:
132+
return default(obj)
133+
except TypeError:
134+
return obj
135+
136+
81137
def object_hook(dct):
82138
if "$oid" in dct:
83139
return ObjectId(str(dct["$oid"]))
@@ -97,8 +153,12 @@ def object_hook(dct):
97153
return MinKey()
98154
if "$maxKey" in dct:
99155
return MaxKey()
100-
if _use_uuid and "$uuid" in dct:
101-
return uuid.UUID(dct["$uuid"])
156+
if "$binary" in dct:
157+
return Binary(base64.b64decode(dct["$binary"].encode()), dct["$type"])
158+
if "$code" in dct:
159+
return Code(dct["$code"], dct.get("$scope"))
160+
if bson.has_uuid() and "$uuid" in dct:
161+
return bson.uuid.UUID(dct["$uuid"])
102162
return dct
103163

104164

@@ -128,6 +188,14 @@ def default(obj):
128188
return {"$maxKey": 1}
129189
if isinstance(obj, Timestamp):
130190
return {"t": obj.time, "i": obj.inc}
131-
if _use_uuid and isinstance(obj, uuid.UUID):
191+
if isinstance(obj, Code):
192+
return {'$code': "%s" % obj, '$scope': obj.scope}
193+
if isinstance(obj, Binary):
194+
return {'$binary': base64.b64encode(obj).decode(),
195+
'$type': obj.subtype}
196+
if PY3 and isinstance(obj, binary_type):
197+
return {'$binary': base64.b64encode(obj).decode(),
198+
'$type': 0}
199+
if bson.has_uuid() and isinstance(obj, bson.uuid.UUID):
132200
return {"$uuid": obj.hex}
133201
raise TypeError("%r is not JSON serializable" % obj)

test/test_json_util.py

Lines changed: 48 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,44 +18,38 @@
1818
import datetime
1919
import re
2020
import sys
21-
json_lib = True
22-
try:
23-
import json
24-
except ImportError:
25-
try:
26-
import simplejson as json
27-
except ImportError:
28-
json_lib = False
29-
try:
30-
import uuid
31-
should_test_uuid = True
32-
except ImportError:
33-
should_test_uuid = False
3421

3522
from nose.plugins.skip import SkipTest
3623

3724
sys.path[0:0] = [""]
3825

26+
import bson
27+
from bson.py3compat import b
28+
from bson import json_util
29+
from bson.binary import Binary, MD5_SUBTYPE
30+
from bson.code import Code
3931
from bson.dbref import DBRef
40-
from bson.json_util import default, object_hook
41-
from bson.min_key import MinKey
4232
from bson.max_key import MaxKey
33+
from bson.min_key import MinKey
4334
from bson.objectid import ObjectId
4435
from bson.timestamp import Timestamp
4536
from bson.tz_util import utc
4637

38+
from test.test_connection import get_connection
39+
4740
PY3 = sys.version_info[0] == 3
4841

4942

5043
class TestJsonUtil(unittest.TestCase):
5144

5245
def setUp(self):
53-
if not json_lib:
46+
if not json_util.json_lib:
5447
raise SkipTest()
5548

49+
self.db = get_connection().pymongo_test
50+
5651
def round_tripped(self, doc):
57-
return json.loads(json.dumps(doc, default=default),
58-
object_hook=object_hook)
52+
return json_util.loads(json_util.dumps(doc))
5953

6054
def round_trip(self, doc):
6155
self.assertEqual(doc, self.round_tripped(doc))
@@ -76,11 +70,11 @@ def test_dbref(self):
7670
#
7771
# self.assertEqual("{\"ref\": {\"$ref\": \"foo\", \"$id\": 5}}",
7872
# json.dumps({"ref": DBRef("foo", 5)},
79-
# default=default))
73+
# default=json_util.default))
8074
# self.assertEqual("{\"ref\": {\"$ref\": \"foo\",
8175
# \"$id\": 5, \"$db\": \"bar\"}}",
8276
# json.dumps({"ref": DBRef("foo", 5, "bar")},
83-
# default=default))
77+
# default=json_util.default))
8478

8579
def test_datetime(self):
8680
# only millis, not micros
@@ -92,27 +86,55 @@ def test_regex(self):
9286
self.assertEqual("a*b", res.pattern)
9387
if PY3:
9488
# re.UNICODE is a default in python 3.
95-
self.assertEqual(re.IGNORECASE|re.UNICODE, res.flags)
89+
self.assertEqual(re.IGNORECASE | re.UNICODE, res.flags)
9690
else:
9791
self.assertEqual(re.IGNORECASE, res.flags)
9892

9993
def test_minkey(self):
10094
self.round_trip({"m": MinKey()})
10195

10296
def test_maxkey(self):
103-
self.round_trip({"m": MinKey()})
97+
self.round_trip({"m": MaxKey()})
10498

10599
def test_timestamp(self):
106-
res = json.dumps({"ts": Timestamp(4, 13)}, default=default)
107-
dct = json.loads(res)
100+
res = json_util.json.dumps({"ts": Timestamp(4, 13)},
101+
default=json_util.default)
102+
dct = json_util.json.loads(res)
108103
self.assertEqual(dct['ts']['t'], 4)
109104
self.assertEqual(dct['ts']['i'], 13)
110105

111106
def test_uuid(self):
112-
if not should_test_uuid:
107+
if not bson.has_uuid():
113108
raise SkipTest()
114109
self.round_trip(
115-
{'uuid': uuid.UUID('f47ac10b-58cc-4372-a567-0e02b2c3d479')})
110+
{'uuid': bson.uuid.UUID(
111+
'f47ac10b-58cc-4372-a567-0e02b2c3d479')})
112+
113+
def test_binary(self):
114+
self.round_trip({"bin": Binary(b("\x00\x01\x02\x03\x04"))})
115+
self.round_trip({
116+
"md5": Binary(b(' n7\x18\xaf\t/\xd1\xd1/\x80\xca\xe7q\xcc\xac'),
117+
MD5_SUBTYPE)})
118+
119+
def test_code(self):
120+
self.round_trip({"code": Code("function x() { return 1; }")})
121+
self.round_trip({"code": Code("function y() { return z; }", z=2)})
122+
123+
def test_cursor(self):
124+
db = self.db
125+
126+
db.drop_collection("test")
127+
docs = [
128+
{'foo': [1, 2]},
129+
{'bar': {'hello': 'world'}},
130+
{'code': Code("function x() { return 1; }")},
131+
{'bin': Binary(b("\x00\x01\x02\x03\x04"))}
132+
]
133+
134+
db.test.insert(docs)
135+
reloaded_docs = json_util.loads(json_util.dumps(db.test.find()))
136+
for doc in docs:
137+
self.assertTrue(doc in reloaded_docs)
116138

117139
if __name__ == "__main__":
118140
unittest.main()

0 commit comments

Comments
 (0)