Skip to content

Commit c380852

Browse files
authored
PYTHON-1337 Add __slots__ to commonly used bson classes (mongodb#739)
1 parent 4b44736 commit c380852

File tree

10 files changed

+158
-8
lines changed

10 files changed

+158
-8
lines changed

bson/_helpers.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Copyright 2021-present MongoDB, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Setstate and getstate functions for objects with __slots__, allowing
16+
compatibility with default pickling protocol
17+
"""
18+
19+
20+
def _setstate_slots(self, state):
21+
for slot, value in state.items():
22+
setattr(self, slot, value)
23+
24+
25+
def _mangle_name(name, prefix):
26+
if name.startswith("__"):
27+
prefix = "_"+prefix
28+
else:
29+
prefix = ""
30+
return prefix + name
31+
32+
33+
def _getstate_slots(self):
34+
prefix = self.__class__.__name__
35+
ret = dict()
36+
for name in self.__slots__:
37+
mangled_name = _mangle_name(name, prefix)
38+
if hasattr(self, mangled_name):
39+
ret[mangled_name] = getattr(self, mangled_name)
40+
return ret

bson/dbref.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
from copy import deepcopy
1818

1919
from bson.son import SON
20-
20+
from bson._helpers import _getstate_slots, _setstate_slots
2121

2222
class DBRef(object):
2323
"""A reference to a document stored in MongoDB.
2424
"""
25-
25+
__slots__ = "__collection", "__id", "__database", "__kwargs"
26+
__getstate__ = _getstate_slots
27+
__setstate__ = _setstate_slots
2628
# DBRef isn't actually a BSON "type" so this number was arbitrarily chosen.
2729
_type_marker = 100
2830

@@ -81,12 +83,6 @@ def __getattr__(self, key):
8183
except KeyError:
8284
raise AttributeError(key)
8385

84-
# Have to provide __setstate__ to avoid
85-
# infinite recursion since we override
86-
# __getattr__.
87-
def __setstate__(self, state):
88-
self.__dict__.update(state)
89-
9086
def as_doc(self):
9187
"""Get the SON document representation of this DBRef.
9288

bson/int64.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,12 @@ class Int64(int):
2424
:Parameters:
2525
- `value`: the numeric value to represent
2626
"""
27+
__slots__ = ()
2728

2829
_type_marker = 18
30+
31+
def __getstate__(self):
32+
return {}
33+
34+
def __setstate__(self, state):
35+
pass

bson/max_key.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,16 @@
1818

1919
class MaxKey(object):
2020
"""MongoDB internal MaxKey type."""
21+
__slots__ = ()
2122

2223
_type_marker = 127
2324

25+
def __getstate__(self):
26+
return {}
27+
28+
def __setstate__(self, state):
29+
pass
30+
2431
def __eq__(self, other):
2532
return isinstance(other, MaxKey)
2633

bson/min_key.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,16 @@
1818

1919
class MinKey(object):
2020
"""MongoDB internal MinKey type."""
21+
__slots__ = ()
2122

2223
_type_marker = 255
2324

25+
def __getstate__(self):
26+
return {}
27+
28+
def __setstate__(self, state):
29+
pass
30+
2431
def __eq__(self, other):
2532
return isinstance(other, MinKey)
2633

bson/regex.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import re
1919

2020
from bson.son import RE_TYPE
21+
from bson._helpers import _getstate_slots, _setstate_slots
2122

2223

2324
def str_flags_to_int(str_flags):
@@ -40,6 +41,11 @@ def str_flags_to_int(str_flags):
4041

4142
class Regex(object):
4243
"""BSON regular expression data."""
44+
__slots__ = ("pattern", "flags")
45+
46+
__getstate__ = _getstate_slots
47+
__setstate__ = _setstate_slots
48+
4349
_type_marker = 11
4450

4551
@classmethod

bson/timestamp.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,18 @@
1919
import datetime
2020

2121
from bson.tz_util import utc
22+
from bson._helpers import _getstate_slots, _setstate_slots
2223

2324
UPPERBOUND = 4294967296
2425

2526

2627
class Timestamp(object):
2728
"""MongoDB internal timestamps used in the opLog.
2829
"""
30+
__slots__ = ("__time", "__inc")
31+
32+
__getstate__ = _getstate_slots
33+
__setstate__ = _setstate_slots
2934

3035
_type_marker = 17
3136

doc/changelog.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,11 @@ Breaking Changes in 4.0
143143
opposed to
144144
the previous syntax which was simply ``if collection:`` or ``if database:``.
145145
You must now explicitly compare with None.
146+
- Classes :class:`~bson.int64.Int64`, :class:`~bson.min_key.MinKey`,
147+
:class:`~bson.max_key.MaxKey`, :class:`~bson.timestamp.Timestamp`,
148+
:class:`~bson.regex.Regex`, and :class:`~bson.dbref.DBRef` all implement
149+
``__slots__`` now. This means that their attributes are fixed, and new
150+
attributes cannot be added to them at runtime.
146151
- Empty projections (eg {} or []) for
147152
:meth:`~pymongo.collection.Collection.find`, and
148153
:meth:`~pymongo.collection.Collection.find_one`

doc/migrate-to-pymongo4.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,3 +840,12 @@ The default uuid_representation for :class:`~bson.codec_options.CodecOptions`,
840840
:data:`bson.binary.UuidRepresentation.UNSPECIFIED`. Attempting to encode a
841841
:class:`uuid.UUID` instance to BSON or JSON now produces an error by default.
842842
See :ref:`handling-uuid-data-example` for details.
843+
844+
Additional BSON classes implement ``__slots__``
845+
...............................................
846+
847+
:class:`~bson.int64.Int64`, :class:`~bson.min_key.MinKey`,
848+
:class:`~bson.max_key.MaxKey`, :class:`~bson.timestamp.Timestamp`,
849+
:class:`~bson.regex.Regex`, and :class:`~bson.dbref.DBRef` now implement
850+
``__slots__`` to reduce memory usage. This means that their attributes are fixed, and new
851+
attributes cannot be added to the object at runtime.

test/test_bson.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import sys
2626
import tempfile
2727
import uuid
28+
import pickle
2829

2930
from collections import abc, OrderedDict
3031
from io import BytesIO
@@ -1053,6 +1054,73 @@ def test_unicode_decode_error_handler(self):
10531054
self.assertRaises(InvalidBSON, decode, invalid_both, CodecOptions(
10541055
unicode_decode_error_handler="junk"))
10551056

1057+
def round_trip_pickle(self, obj, pickled_with_older):
1058+
pickled_with_older_obj = pickle.loads(pickled_with_older)
1059+
for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
1060+
pkl = pickle.dumps(obj, protocol=protocol)
1061+
obj2 = pickle.loads(pkl)
1062+
self.assertEqual(obj, obj2)
1063+
self.assertEqual(pickled_with_older_obj, obj2)
1064+
1065+
def test_regex_pickling(self):
1066+
reg = Regex(".?")
1067+
pickled_with_3 = (b'\x80\x04\x959\x00\x00\x00\x00\x00\x00\x00\x8c\n'
1068+
b'bson.regex\x94\x8c\x05Regex\x94\x93\x94)\x81\x94}'
1069+
b'\x94(\x8c\x07pattern\x94\x8c\x02.?\x94\x8c\x05flag'
1070+
b's\x94K\x00ub.')
1071+
self.round_trip_pickle(reg, pickled_with_3)
1072+
1073+
def test_timestamp_pickling(self):
1074+
ts = Timestamp(0, 1)
1075+
pickled_with_3 = (b'\x80\x04\x95Q\x00\x00\x00\x00\x00\x00\x00\x8c'
1076+
b'\x0ebson.timestamp\x94\x8c\tTimestamp\x94\x93\x94)'
1077+
b'\x81\x94}\x94('
1078+
b'\x8c\x10_Timestamp__time\x94K\x00\x8c'
1079+
b'\x0f_Timestamp__inc\x94K\x01ub.')
1080+
self.round_trip_pickle(ts, pickled_with_3)
1081+
1082+
def test_dbref_pickling(self):
1083+
dbr = DBRef("foo", 5)
1084+
pickled_with_3 = (b'\x80\x04\x95q\x00\x00\x00\x00\x00\x00\x00\x8c\n'
1085+
b'bson.dbref\x94\x8c\x05DBRef\x94\x93\x94)\x81\x94}'
1086+
b'\x94(\x8c\x12_DBRef__collection\x94\x8c\x03foo\x94'
1087+
b'\x8c\n_DBRef__id\x94K\x05\x8c\x10_DBRef__database'
1088+
b'\x94N\x8c\x0e_DBRef__kwargs\x94}\x94ub.')
1089+
self.round_trip_pickle(dbr, pickled_with_3)
1090+
1091+
dbr = DBRef("foo", 5, database='db', kwargs1=None)
1092+
pickled_with_3 = (b'\x80\x04\x95\x81\x00\x00\x00\x00\x00\x00\x00\x8c'
1093+
b'\nbson.dbref\x94\x8c\x05DBRef\x94\x93\x94)\x81\x94}'
1094+
b'\x94(\x8c\x12_DBRef__collection\x94\x8c\x03foo\x94'
1095+
b'\x8c\n_DBRef__id\x94K\x05\x8c\x10_DBRef__database'
1096+
b'\x94\x8c\x02db\x94\x8c\x0e_DBRef__kwargs\x94}\x94'
1097+
b'\x8c\x07kwargs1\x94Nsub.')
1098+
1099+
self.round_trip_pickle(dbr, pickled_with_3)
1100+
1101+
def test_minkey_pickling(self):
1102+
mink = MinKey()
1103+
pickled_with_3 = (b'\x80\x04\x95\x1e\x00\x00\x00\x00\x00\x00\x00\x8c'
1104+
b'\x0cbson.min_key\x94\x8c\x06MinKey\x94\x93\x94)'
1105+
b'\x81\x94.')
1106+
1107+
self.round_trip_pickle(mink, pickled_with_3)
1108+
1109+
def test_maxkey_pickling(self):
1110+
maxk = MaxKey()
1111+
pickled_with_3 = (b'\x80\x04\x95\x1e\x00\x00\x00\x00\x00\x00\x00\x8c'
1112+
b'\x0cbson.max_key\x94\x8c\x06MaxKey\x94\x93\x94)'
1113+
b'\x81\x94.')
1114+
1115+
self.round_trip_pickle(maxk, pickled_with_3)
1116+
1117+
def test_int64_pickling(self):
1118+
i64 = Int64(9)
1119+
pickled_with_3 = (b'\x80\x04\x95\x1e\x00\x00\x00\x00\x00\x00\x00\x8c\n'
1120+
b'bson.int64\x94\x8c\x05Int64\x94\x93\x94K\t\x85\x94'
1121+
b'\x81\x94.')
1122+
self.round_trip_pickle(i64, pickled_with_3)
1123+
10561124

10571125
if __name__ == "__main__":
10581126
unittest.main()

0 commit comments

Comments
 (0)