Skip to content

Commit 65f85f6

Browse files
committed
PYTHON-1769 Re-define TypeCodecBase as an AbstractBaseClass
1 parent fe30705 commit 65f85f6

File tree

5 files changed

+280
-185
lines changed

5 files changed

+280
-185
lines changed

bson/codec_options.py

Lines changed: 48 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@
1616

1717
import datetime
1818

19+
from abc import abstractmethod
1920
from collections import namedtuple
2021

21-
from bson.py3compat import abc, string_type
22+
from bson.py3compat import ABC, abc, abstractproperty, string_type
2223
from bson.binary import (ALL_UUID_REPRESENTATIONS,
2324
PYTHON_LEGACY,
2425
UUID_REPRESENTATION_NAMES)
@@ -33,33 +34,53 @@ def _raw_document_class(document_class):
3334
return marker == _RAW_BSON_DOCUMENT_MARKER
3435

3536

36-
class TypeCodecBase(object):
37+
class TypeEncoder(ABC):
3738
"""Base class for defining type codec classes which describe how a
38-
custom type can be transformed to/from one of the types BSON already
39-
understands, and can encode/decode.
39+
custom type can be transformed to one of the types BSON understands.
4040
41-
Codec classes must implement the ``python_type`` property, and the
42-
``transform_python`` method to support encoding, or the ``bson_type``
43-
property and ``transform_bson`` method to support decoding. Note that a
44-
single codec class may support both encoding and decoding.
41+
Codec classes must implement the ``python_type`` attribute, and the
42+
``transform_python`` method to support encoding.
4543
"""
46-
@property
44+
@abstractproperty
4745
def python_type(self):
4846
"""The Python type to be converted into something serializable."""
49-
raise NotImplementedError
47+
pass
48+
49+
@abstractmethod
50+
def transform_python(self, value):
51+
"""Convert the given Python object into something serializable."""
52+
pass
5053

51-
@property
54+
55+
class TypeDecoder(ABC):
56+
"""Base class for defining type codec classes which describe how a
57+
BSON type can be transformed to a custom type.
58+
59+
Codec classes must implement the ``bson_type`` attribute, and the
60+
``transform_bson`` method to support decoding.
61+
"""
62+
@abstractproperty
5263
def bson_type(self):
5364
"""The BSON type to be converted into our own type."""
54-
raise NotImplementedError
65+
pass
5566

67+
@abstractmethod
5668
def transform_bson(self, value):
5769
"""Convert the given BSON value into our own type."""
58-
raise NotImplementedError
70+
pass
5971

60-
def transform_python(self, value):
61-
"""Convert the given Python object into something serializable."""
62-
raise NotImplementedError
72+
73+
class TypeCodec(TypeEncoder, TypeDecoder):
74+
"""Base class for defining type codec classes which describe how a
75+
custom type can be transformed to/from one of the types BSON already
76+
understands, and can encode/decode.
77+
78+
Codec classes must implement the ``python_type`` attribute, and the
79+
``transform_python`` method to support encoding, as well as the
80+
``bson_type`` attribute, and the ``transform_bson`` method to support
81+
decoding.
82+
"""
83+
pass
6384

6485

6586
class TypeRegistry(object):
@@ -95,23 +116,18 @@ def __init__(self, type_codecs=None, fallback_encoder=None):
95116
fallback_encoder))
96117

97118
for codec in self.__type_codecs:
98-
if not isinstance(codec, TypeCodecBase):
119+
is_valid_codec = False
120+
if isinstance(codec, TypeEncoder):
121+
is_valid_codec = True
122+
self._encoder_map[codec.python_type] = codec.transform_python
123+
if isinstance(codec, TypeDecoder):
124+
is_valid_codec = True
125+
self._decoder_map[codec.bson_type] = codec.transform_bson
126+
if not is_valid_codec:
99127
raise TypeError(
100-
"Expected an instance of %s, got %r instead" % (
101-
TypeCodecBase.__name__, codec))
102-
try:
103-
python_type = codec.python_type
104-
except NotImplementedError:
105-
pass
106-
else:
107-
self._encoder_map[python_type] = codec.transform_python
108-
109-
try:
110-
bson_type = codec.bson_type
111-
except NotImplementedError:
112-
pass
113-
else:
114-
self._decoder_map[bson_type] = codec.transform_bson
128+
"Expected an instance of %s, %s, or %s, got %r instead" % (
129+
TypeEncoder.__name__, TypeDecoder.__name__,
130+
TypeCodec.__name__, codec))
115131

116132
def __repr__(self):
117133
return ('%s(type_codecs=%r, fallback_encoder=%r)' % (

bson/py3compat.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,12 @@
2222
import codecs
2323
import collections.abc as abc
2424
import _thread as thread
25+
from abc import ABC, abstractmethod
2526
from io import BytesIO as StringIO
2627

28+
def abstractproperty(func):
29+
return property(abstractmethod(func))
30+
2731
MAXSIZE = sys.maxsize
2832

2933
imap = map
@@ -60,13 +64,16 @@ def _unicode(s):
6064
else:
6165
import collections as abc
6266
import thread
67+
from abc import ABCMeta, abstractproperty
6368

6469
from itertools import imap
6570
try:
6671
from cStringIO import StringIO
6772
except ImportError:
6873
from StringIO import StringIO
6974

75+
ABC = ABCMeta('ABC', (object,), {})
76+
7077
MAXSIZE = sys.maxint
7178

7279
def b(s):

doc/examples/custom_type.rst

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ Custom Type Example
22
===================
33

44
This is an example of using a custom type with PyMongo. The example here shows
5-
how to subclass :class:`~bson.codec_options.TypeCodecBase` to write a type
5+
how to subclass :class:`~bson.codec_options.TypeCodec` to write a type
66
codec, which is used to populate a :class:`~bson.codec_options.TypeRegistry`.
77
The type registry can then be used to create a custom-type-aware
88
:class:`~pymongo.collection.Collection`. Read and write operations
@@ -51,39 +51,39 @@ The Type Codec
5151

5252
In order to encode custom types, we must first define a **type codec** for our
5353
type. A type codec describes how an instance of a custom type can be
54-
*transformed* to/from one of the types :mod:`~bson` already understands, and
55-
can encode/decode. Type codecs must inherit from
56-
:class:`~bson.codec_options.TypeCodecBase`. In order to encode a custom type,
57-
a codec must implement the ``python_type`` property and the
58-
``transform_python`` method. Similarly, in order to decode a custom type,
59-
a codec must implement the ``bson_type`` property and the ``transform_bson``
60-
method. Note that a type codec need not support both encoding and decoding.
54+
*transformed* to and/or from one of the types :mod:`~bson` already understands.
55+
Depending on the desired functionality, users must choose from the following
56+
base classes when defining type codecs:
57+
58+
* :class:`~bson.codec_options.TypeEncoder`: subclass this to define a codec that
59+
encodes a custom Python type to a known BSON type. Users must implement the
60+
``python_type`` property/attribute and the ``transform_python`` method.
61+
* :class:`~bson.codec_options.TypeDecoder`: subclass this to define a codec that
62+
decodes a specified BSON type into a custom Python type. Users must implement
63+
the ``bson_type`` property/attribute and the ``transform_bson`` method.
64+
* :class:`~bson.codec_options.TypeCodec`: subclass this to define a codec that
65+
can both encode from and decode to a custom type. Users must implement the
66+
``python_type`` and ``bson_type`` properties/attributes, as well as the
67+
``transform_python`` and ``transform_bson`` methods.
6168

6269

6370
The type codec for our custom type simply needs to define how a
6471
:py:class:`~decimal.Decimal` instance can be converted into a
65-
:class:`~bson.decimal128.Decimal128` instance and vice-versa:
72+
:class:`~bson.decimal128.Decimal128` instance and vice-versa. Since we are
73+
interested in both encoding and decoding our custom type, we use the
74+
``TypeCodec`` base class to define our codec:
6675

6776
.. doctest::
6877

6978
>>> from bson.decimal128 import Decimal128
70-
>>> from bson.codec_options import TypeCodecBase
71-
>>> class DecimalCodec(TypeCodecBase):
72-
... @property
73-
... def python_type(self):
74-
... """The Python type acted upon by this type codec."""
75-
... return Decimal
76-
...
79+
>>> from bson.codec_options import TypeCodec
80+
>>> class DecimalCodec(TypeCodec):
81+
... python_type = Decimal # the Python type acted upon by this type codec
82+
... bson_type = Decimal128 # the BSON type acted upon by this type codec
7783
... def transform_python(self, value):
7884
... """Function that transforms a custom type value into a type
7985
... that BSON can encode."""
8086
... return Decimal128(value)
81-
...
82-
... @property
83-
... def bson_type(self):
84-
... """The BSON type acted upon by this type codec."""
85-
... return Decimal128
86-
...
8787
... def transform_bson(self, value):
8888
... """Function that transforms a vanilla BSON type value into our
8989
... custom type."""

test/test_bson.py

Lines changed: 1 addition & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,13 @@
3434
Regex)
3535
from bson.binary import Binary, UUIDLegacy
3636
from bson.code import Code
37-
from bson.codec_options import CodecOptions, TypeCodecBase, TypeRegistry
37+
from bson.codec_options import CodecOptions
3838
from bson.int64 import Int64
3939
from bson.objectid import ObjectId
4040
from bson.dbref import DBRef
4141
from bson.py3compat import abc, iteritems, PY3, StringIO, text_type
4242
from bson.son import SON
4343
from bson.timestamp import Timestamp
44-
from bson.tz_util import FixedOffset
4544
from bson.errors import (InvalidBSON,
4645
InvalidDocument,
4746
InvalidStringData)
@@ -906,121 +905,6 @@ def test_bad_id_keys(self):
906905
BSON.encode({"_id": {'$oid': "52d0b971b3ba219fdeb4170e"}})
907906

908907

909-
class TestTypeRegistry(unittest.TestCase):
910-
@classmethod
911-
def setUpClass(cls):
912-
class MyIntType(object):
913-
def __init__(self, x):
914-
assert isinstance(x, int)
915-
self.x = x
916-
917-
class MyStrType(object):
918-
def __init__(self, x):
919-
assert isinstance(x, str)
920-
self.x = x
921-
922-
class MyIntCodec(TypeCodecBase):
923-
@property
924-
def python_type(self):
925-
return MyIntType
926-
927-
@property
928-
def bson_type(self):
929-
return int
930-
931-
def transform_python(self, value):
932-
return value.x
933-
934-
def transform_bson(self, value):
935-
return MyIntType(value)
936-
937-
class MyStrCodec(TypeCodecBase):
938-
@property
939-
def python_type(self):
940-
return MyStrType
941-
942-
@property
943-
def bson_type(self):
944-
return str
945-
946-
def transform_python(self, value):
947-
return value.x
948-
949-
def transform_bson(self, value):
950-
return MyStrType(value)
951-
952-
def fallback_encoder(value):
953-
return value
954-
955-
cls.types = (MyIntType, MyStrType)
956-
cls.codecs = (MyIntCodec, MyStrCodec)
957-
cls.fallback_encoder = fallback_encoder
958-
959-
def test_simple(self):
960-
codec_instances = [codec() for codec in self.codecs]
961-
def assert_proper_initialization(type_registry, codec_instances):
962-
self.assertEqual(type_registry._encoder_map, {
963-
self.types[0]: codec_instances[0].transform_python,
964-
self.types[1]: codec_instances[1].transform_python})
965-
self.assertEqual(type_registry._decoder_map, {
966-
int: codec_instances[0].transform_bson,
967-
str: codec_instances[1].transform_bson})
968-
self.assertEqual(
969-
type_registry._fallback_encoder, self.fallback_encoder)
970-
971-
type_registry = TypeRegistry(codec_instances, self.fallback_encoder)
972-
assert_proper_initialization(type_registry, codec_instances)
973-
974-
type_registry = TypeRegistry(
975-
fallback_encoder=self.fallback_encoder, type_codecs=codec_instances)
976-
assert_proper_initialization(type_registry, codec_instances)
977-
978-
# Ensure codec list held by the type registry doesn't change if we
979-
# mutate the initial list.
980-
codec_instances_copy = list(codec_instances)
981-
codec_instances.pop(0)
982-
self.assertListEqual(
983-
type_registry._TypeRegistry__type_codecs, codec_instances_copy)
984-
985-
def test_initialize_fail(self):
986-
err_msg = "Expected an instance of TypeCodecBase, got .* instead"
987-
with self.assertRaisesRegex(TypeError, err_msg):
988-
TypeRegistry(self.codecs)
989-
990-
with self.assertRaisesRegex(TypeError, err_msg):
991-
TypeRegistry([type('AnyType', (object,), {})()])
992-
993-
err_msg = "fallback_encoder %r is not a callable" % (True,)
994-
with self.assertRaisesRegex(TypeError, err_msg):
995-
TypeRegistry([], True)
996-
997-
err_msg = "fallback_encoder %r is not a callable" % ('hello',)
998-
with self.assertRaisesRegex(TypeError, err_msg):
999-
TypeRegistry(fallback_encoder='hello')
1000-
1001-
def test_not_implemented(self):
1002-
type_registry = TypeRegistry([type("codec1", (TypeCodecBase, ), {})(),
1003-
type("codec2", (TypeCodecBase, ), {})()])
1004-
self.assertEqual(type_registry._encoder_map, {})
1005-
self.assertEqual(type_registry._decoder_map, {})
1006-
1007-
def test_type_registry_repr(self):
1008-
codec_instances = [codec() for codec in self.codecs]
1009-
type_registry = TypeRegistry(codec_instances)
1010-
r = ("TypeRegistry(type_codecs=%r, fallback_encoder=%r)" % (
1011-
codec_instances, None))
1012-
self.assertEqual(r, repr(type_registry))
1013-
1014-
def test_type_registry_eq(self):
1015-
codec_instances = [codec() for codec in self.codecs]
1016-
self.assertEqual(
1017-
TypeRegistry(codec_instances), TypeRegistry(codec_instances))
1018-
1019-
codec_instances_2 = [codec() for codec in self.codecs]
1020-
self.assertNotEqual(
1021-
TypeRegistry(codec_instances), TypeRegistry(codec_instances_2))
1022-
1023-
1024908
class TestCodecOptions(unittest.TestCase):
1025909
def test_document_class(self):
1026910
self.assertRaises(TypeError, CodecOptions, document_class=object)

0 commit comments

Comments
 (0)