diff --git a/bson/json_util.py b/bson/json_util.py index 194a3611c..ea3e3dcd3 100644 --- a/bson/json_util.py +++ b/bson/json_util.py @@ -83,7 +83,7 @@ except ImportError: json_lib = False import bson -from bson import EPOCH_AWARE, RE_TYPE +from bson import EPOCH_AWARE, RE_TYPE, SON from bson.binary import Binary from bson.code import Code from bson.dbref import DBRef @@ -111,6 +111,10 @@ def dumps(obj, *args, **kwargs): Recursive function that handles all BSON types including :class:`~bson.binary.Binary` and :class:`~bson.code.Code`. + + .. versionchanged:: 2.7 + Preserves order when rendering SON, Timestamp, Code, Binary, and DBRef + instances. """ if not json_lib: raise Exception("No json library available") @@ -143,7 +147,7 @@ def _json_convert(obj): converted into json. """ if hasattr(obj, 'iteritems') or hasattr(obj, 'items'): # PY3 support - return dict(((k, _json_convert(v)) for k, v in obj.iteritems())) + return SON(((k, _json_convert(v)) for k, v in obj.iteritems())) elif hasattr(obj, '__iter__') and not isinstance(obj, string_types): return list((_json_convert(v) for v in obj)) try: @@ -214,22 +218,23 @@ def default(obj): flags += "u" if obj.flags & re.VERBOSE: flags += "x" - return {"$regex": obj.pattern, - "$options": flags} + return SON([("$regex", obj.pattern), ("$options", flags)]) if isinstance(obj, MinKey): return {"$minKey": 1} if isinstance(obj, MaxKey): return {"$maxKey": 1} if isinstance(obj, Timestamp): - return {"t": obj.time, "i": obj.inc} + return SON([("t", obj.time), ("i", obj.inc)]) if isinstance(obj, Code): - return {'$code': "%s" % obj, '$scope': obj.scope} + return SON([('$code', "%s" % obj), ('$scope', obj.scope)]) if isinstance(obj, Binary): - return {'$binary': base64.b64encode(obj).decode(), - '$type': "%02x" % obj.subtype} + return SON([ + ('$binary', base64.b64encode(obj).decode()), + ('$type', "%02x" % obj.subtype)]) if PY3 and isinstance(obj, binary_type): - return {'$binary': base64.b64encode(obj).decode(), - '$type': "00"} + return SON([ + ('$binary', base64.b64encode(obj).decode()), + ('$type', "00")]) if bson.has_uuid() and isinstance(obj, bson.uuid.UUID): return {"$uuid": obj.hex} raise TypeError("%r is not JSON serializable" % obj) diff --git a/test/test_json_util.py b/test/test_json_util.py index 034dd337b..929f00c8a 100644 --- a/test/test_json_util.py +++ b/test/test_json_util.py @@ -69,7 +69,11 @@ class TestJsonUtil(unittest.TestCase): self.round_trip({"ref": DBRef("foo", 5)}) self.round_trip({"ref": DBRef("foo", 5, "db")}) self.round_trip({"ref": DBRef("foo", ObjectId())}) - self.round_trip({"ref": DBRef("foo", ObjectId(), "db")}) + + # Check order. + self.assertEqual( + '{"$ref": "collection", "$id": 1, "$db": "db"}', + json_util.dumps(DBRef('collection', 1, 'db'))) def test_datetime(self): # only millis, not micros @@ -126,6 +130,15 @@ class TestJsonUtil(unittest.TestCase): '{"r": {"$regex": ".*", "$options": "ilm"}}', compile_re=False)['r']) + # Check order. + self.assertEqual( + '{"$regex": ".*", "$options": "mx"}', + json_util.dumps(Regex('.*', re.M | re.X))) + + self.assertEqual( + '{"$regex": ".*", "$options": "mx"}', + json_util.dumps(re.compile('.*', re.M | re.X))) + def test_minkey(self): self.round_trip({"m": MinKey()}) @@ -135,6 +148,7 @@ class TestJsonUtil(unittest.TestCase): def test_timestamp(self): res = json_util.json.dumps({"ts": Timestamp(4, 13)}, default=json_util.default) + self.assertEqual('{"ts": {"t": 4, "i": 13}}', res) dct = json_util.json.loads(res) self.assertEqual(dct['ts']['t'], 4) self.assertEqual(dct['ts']['i'], 13) @@ -164,7 +178,10 @@ class TestJsonUtil(unittest.TestCase): json_util.loads('{"bin": {"$type": 0, "$binary": "AAECAwQ="}}')) json_bin_dump = json_util.dumps(md5_type_dict) - self.assertTrue('"$type": "05"' in json_bin_dump) + self.assertEqual( + '{"md5": {"$binary": "IG43GK8JL9HRL4DK53HMrA==", "$type": "05"}}', + json_bin_dump) + self.assertEqual(md5_type_dict, json_util.loads('{"md5": {"$type": 5, "$binary":' ' "IG43GK8JL9HRL4DK53HMrA=="}}')) @@ -186,7 +203,13 @@ class TestJsonUtil(unittest.TestCase): def test_code(self): self.round_trip({"code": Code("function x() { return 1; }")}) - self.round_trip({"code": Code("function y() { return z; }", z=2)}) + + code = Code("return z", z=2) + res = json_util.dumps(code) + self.assertEqual(code, json_util.loads(res)) + + # Check order. + self.assertEqual('{"$code": "return z", "$scope": {"z": 2}}', res) def test_cursor(self): db = self.db