Skip to content

Commit 3bcdde3

Browse files
committed
PYTHON-1785 Add bson.encode and bson.decode
1 parent 57c7f8c commit 3bcdde3

File tree

3 files changed

+87
-11
lines changed

3 files changed

+87
-11
lines changed

bson/__init__.py

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -903,7 +903,7 @@ def _millis_to_datetime(millis, opts):
903903
micros = diff * 1000
904904
if opts.tz_aware:
905905
dt = EPOCH_AWARE + datetime.timedelta(seconds=seconds,
906-
microseconds=micros)
906+
microseconds=micros)
907907
if opts.tzinfo:
908908
dt = dt.astimezone(opts.tzinfo)
909909
return dt
@@ -924,6 +924,65 @@ def _datetime_to_millis(dtm):
924924
"codec_options must be an instance of CodecOptions")
925925

926926

927+
def encode(document, check_keys=False, codec_options=DEFAULT_CODEC_OPTIONS):
928+
"""Encode a document to BSON.
929+
930+
A document can be any mapping type (like :class:`dict`).
931+
932+
Raises :class:`TypeError` if `document` is not a mapping type,
933+
or contains keys that are not instances of
934+
:class:`basestring` (:class:`str` in python 3). Raises
935+
:class:`~bson.errors.InvalidDocument` if `document` cannot be
936+
converted to :class:`BSON`.
937+
938+
:Parameters:
939+
- `document`: mapping type representing a document
940+
- `check_keys` (optional): check if keys start with '$' or
941+
contain '.', raising :class:`~bson.errors.InvalidDocument` in
942+
either case
943+
- `codec_options` (optional): An instance of
944+
:class:`~bson.codec_options.CodecOptions`.
945+
946+
.. versionadded:: 3.9
947+
"""
948+
if not isinstance(codec_options, CodecOptions):
949+
raise _CODEC_OPTIONS_TYPE_ERROR
950+
951+
return _dict_to_bson(document, check_keys, codec_options)
952+
953+
954+
def decode(data, codec_options=DEFAULT_CODEC_OPTIONS):
955+
"""Decode BSON to a document.
956+
957+
By default, returns a BSON document represented as a Python
958+
:class:`dict`. To use a different :class:`MutableMapping` class,
959+
configure a :class:`~bson.codec_options.CodecOptions`::
960+
961+
>>> import collections # From Python standard library.
962+
>>> import bson
963+
>>> from bson.codec_options import CodecOptions
964+
>>> data = bson.encode({'a': 1})
965+
>>> decoded_doc = bson.decode(data)
966+
<type 'dict'>
967+
>>> options = CodecOptions(document_class=collections.OrderedDict)
968+
>>> decoded_doc = bson.decode(data, codec_options=options)
969+
>>> type(decoded_doc)
970+
<class 'collections.OrderedDict'>
971+
972+
:Parameters:
973+
- `data`: the BSON to decode. Any bytes-like object that implements
974+
the buffer protocol.
975+
- `codec_options` (optional): An instance of
976+
:class:`~bson.codec_options.CodecOptions`.
977+
978+
.. versionadded:: 3.9
979+
"""
980+
if not isinstance(codec_options, CodecOptions):
981+
raise _CODEC_OPTIONS_TYPE_ERROR
982+
983+
return _bson_to_dict(data, codec_options)
984+
985+
927986
def decode_all(data, codec_options=DEFAULT_CODEC_OPTIONS):
928987
"""Decode BSON data to multiple documents.
929988
@@ -935,7 +994,7 @@ def decode_all(data, codec_options=DEFAULT_CODEC_OPTIONS):
935994
- `codec_options` (optional): An instance of
936995
:class:`~bson.codec_options.CodecOptions`.
937996
938-
.. versionchanges:: 3.9
997+
.. versionchanged:: 3.9
939998
Supports bytes-like objects that implement the buffer protocol.
940999
9411000
.. versionchanged:: 3.0
@@ -1137,6 +1196,10 @@ def is_valid(bson):
11371196

11381197
class BSON(bytes):
11391198
"""BSON (Binary JSON) data.
1199+
1200+
.. warning:: Using this class to encode and decode BSON adds a performance
1201+
cost. For better performance use the module level functions
1202+
:func:`encode` and :func:`decode` instead.
11401203
"""
11411204

11421205
@classmethod
@@ -1163,10 +1226,7 @@ def encode(cls, document, check_keys=False,
11631226
.. versionchanged:: 3.0
11641227
Replaced `uuid_subtype` option with `codec_options`.
11651228
"""
1166-
if not isinstance(codec_options, CodecOptions):
1167-
raise _CODEC_OPTIONS_TYPE_ERROR
1168-
1169-
return cls(_dict_to_bson(document, check_keys, codec_options))
1229+
return cls(encode(document, check_keys, codec_options))
11701230

11711231
def decode(self, codec_options=DEFAULT_CODEC_OPTIONS):
11721232
"""Decode this BSON data.
@@ -1208,10 +1268,7 @@ def decode(self, codec_options=DEFAULT_CODEC_OPTIONS):
12081268
12091269
.. _PYTHON-500: https://jira.mongodb.org/browse/PYTHON-500
12101270
"""
1211-
if not isinstance(codec_options, CodecOptions):
1212-
raise _CODEC_OPTIONS_TYPE_ERROR
1213-
1214-
return _bson_to_dict(self, codec_options)
1271+
return decode(self, codec_options)
12151272

12161273

12171274
def has_c():

doc/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ Version 3.9 adds support for MongoDB 4.2. Highlights include:
6060
:meth:`~pymongo.collection.Collection.find_one_and_update`,
6161
:meth:`~pymongo.operations.UpdateOne`, and
6262
:meth:`~pymongo.operations.UpdateMany`.
63+
- New BSON utility functions :func:`~bson.encode` and :func:`~bson.decode`
6364
- :class:`~bson.binary.Binary` now supports any bytes-like type that implements
6465
the buffer protocol.
6566
- Resume tokens can now be accessed from a ``ChangeStream`` cursor using the

test/test_bson.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@
2626

2727
import bson
2828
from bson import (BSON,
29+
decode,
2930
decode_all,
3031
decode_file_iter,
3132
decode_iter,
33+
encode,
3234
EPOCH_AWARE,
3335
is_valid,
3436
Regex)
@@ -124,6 +126,8 @@ def check_encode_then_decode(self, doc_class=dict):
124126

125127
def helper(doc):
126128
self.assertEqual(doc, (BSON.encode(doc_class(doc))).decode())
129+
self.assertEqual(doc, decode(encode(doc)))
130+
127131
helper({})
128132
helper({"test": u"hello"})
129133
self.assertTrue(isinstance(BSON.encode({"hello": "world"})
@@ -283,7 +287,7 @@ def test_basic_decode(self):
283287
b"\x6f\x20\x77\x6F\x72\x6C\x64\x00\x00"
284288
b"\x05\x00\x00\x00\x00"))))
285289

286-
def test_buffer_protocol(self):
290+
def test_decode_all_buffer_protocol(self):
287291
docs = [{'foo': 'bar'}, {}]
288292
bs = b"".join(map(BSON.encode, docs))
289293
self.assertEqual(docs, decode_all(bytearray(bs)))
@@ -297,6 +301,20 @@ def test_buffer_protocol(self):
297301
mm.seek(0)
298302
self.assertEqual(docs, decode_all(mm))
299303

304+
def test_decode_buffer_protocol(self):
305+
doc = {'foo': 'bar'}
306+
bs = encode(doc)
307+
self.assertEqual(doc, decode(bs))
308+
self.assertEqual(doc, decode(bytearray(bs)))
309+
self.assertEqual(doc, decode(memoryview(bs)))
310+
if PY3:
311+
import array
312+
import mmap
313+
self.assertEqual(doc, decode(array.array('B', bs)))
314+
with mmap.mmap(-1, len(bs)) as mm:
315+
mm.write(bs)
316+
mm.seek(0)
317+
self.assertEqual(doc, decode(mm))
300318

301319
def test_invalid_decodes(self):
302320
# Invalid object size (not enough bytes in document for even

0 commit comments

Comments
 (0)