Skip to content

Commit fee5fbf

Browse files
committed
PYTHON-5061 - Add an API to extend the bson TypeRegistry
1 parent 106343a commit fee5fbf

File tree

4 files changed

+202
-74
lines changed

4 files changed

+202
-74
lines changed

bson/codec_options.py

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,7 @@ class TypeCodec(TypeEncoder, TypeDecoder):
108108

109109
class TypeRegistry:
110110
"""Encapsulates type codecs used in encoding and / or decoding BSON, as
111-
well as the fallback encoder. Type registries cannot be modified after
112-
instantiation.
111+
well as the fallback encoder.
113112
114113
``TypeRegistry`` can be initialized with an iterable of type codecs, and
115114
a callable for the fallback encoder::
@@ -147,18 +146,33 @@ def __init__(
147146
raise TypeError("fallback_encoder %r is not a callable" % (fallback_encoder))
148147

149148
for codec in self.__type_codecs:
150-
is_valid_codec = False
151-
if isinstance(codec, TypeEncoder):
152-
self._validate_type_encoder(codec)
153-
is_valid_codec = True
154-
self._encoder_map[codec.python_type] = codec.transform_python
155-
if isinstance(codec, TypeDecoder):
156-
is_valid_codec = True
157-
self._decoder_map[codec.bson_type] = codec.transform_bson
158-
if not is_valid_codec:
159-
raise TypeError(
160-
f"Expected an instance of {TypeEncoder.__name__}, {TypeDecoder.__name__}, or {TypeCodec.__name__}, got {codec!r} instead"
161-
)
149+
self.add_codec(codec)
150+
151+
def add_codec(self, codec: _Codec) -> None:
152+
"""Adds a type codec to the registry.
153+
154+
:param codec: A type codec instance.
155+
If a codec already exists that transforms the same python or BSON type,
156+
it will be overwritten by ``codec``.
157+
A TypeError will be raised if ``codec`` modifies the encoding behavior
158+
of a built-in :mod:`bson` type.
159+
160+
.. versionadded:: 4.14
161+
"""
162+
is_valid_codec = False
163+
if isinstance(codec, TypeEncoder):
164+
self._validate_type_encoder(codec)
165+
is_valid_codec = True
166+
self._encoder_map[codec.python_type] = codec.transform_python
167+
if isinstance(codec, TypeDecoder):
168+
is_valid_codec = True
169+
self._decoder_map[codec.bson_type] = codec.transform_bson
170+
if is_valid_codec and codec not in self.__type_codecs:
171+
self.__type_codecs.append(codec)
172+
elif not is_valid_codec:
173+
raise TypeError(
174+
f"Expected an instance of {TypeEncoder.__name__}, {TypeDecoder.__name__}, or {TypeCodec.__name__}, got {codec!r} instead"
175+
)
162176

163177
def _validate_type_encoder(self, codec: _Codec) -> None:
164178
from bson import _BUILT_IN_TYPES

doc/changelog.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
Changelog
22
=========
33

4+
Changes in Version 4.14.0 (XXXX/XX/XX)
5+
--------------------------------------
6+
PyMongo 4.14 brings a number of changes including:
7+
8+
- Added :meth:`bson.codec_options.TypeRegistry.add_codec` to allow users to extend :class:`bson.codec_options.TypeRegistry`
9+
instances with additional type codecs after instantiation.
10+
11+
412
Changes in Version 4.13.0 (2025/05/14)
513
--------------------------------------
614

test/asynchronous/test_custom_types.py

Lines changed: 83 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -592,46 +592,99 @@ def test_type_registry_eq(self):
592592
codec_instances_2 = [codec() for codec in self.codecs]
593593
self.assertNotEqual(TypeRegistry(codec_instances), TypeRegistry(codec_instances_2))
594594

595-
def test_builtin_types_override_fails(self):
596-
def run_test(base, attrs):
597-
msg = (
598-
r"TypeEncoders cannot change how built-in types "
599-
r"are encoded \(encoder .* transforms type .*\)"
600-
)
601-
for pytype in _BUILT_IN_TYPES:
602-
attrs.update({"python_type": pytype, "transform_python": lambda x: x})
603-
codec = type("testcodec", (base,), attrs)
604-
codec_instance = codec()
595+
def _run_builtin_types_override_fails_test(self, base, attrs, add_codec):
596+
msg = (
597+
r"TypeEncoders cannot change how built-in types "
598+
r"are encoded \(encoder .* transforms type .*\)"
599+
)
600+
for pytype in _BUILT_IN_TYPES:
601+
attrs.update({"python_type": pytype, "transform_python": lambda x: x})
602+
codec = type("testcodec", (base,), attrs)
603+
codec_instance = codec()
604+
with self.assertRaisesRegex(TypeError, msg):
605+
TypeRegistry(
606+
[
607+
codec_instance,
608+
]
609+
)
610+
611+
# Test only some subtypes as not all can be subclassed.
612+
if pytype in [
613+
bool,
614+
type(None),
615+
RE_TYPE,
616+
]:
617+
continue
618+
619+
class MyType(pytype): # type: ignore
620+
pass
621+
622+
attrs.update({"python_type": MyType, "transform_python": lambda x: x})
623+
codec = type("testcodec", (base,), attrs)
624+
codec_instance = codec()
625+
if add_codec:
626+
type_registry = TypeRegistry()
627+
with self.assertRaisesRegex(TypeError, msg):
628+
type_registry.add_codec(codec_instance)
629+
else:
605630
with self.assertRaisesRegex(TypeError, msg):
606631
TypeRegistry(
607632
[
608633
codec_instance,
609634
]
610635
)
611636

612-
# Test only some subtypes as not all can be subclassed.
613-
if pytype in [
614-
bool,
615-
type(None),
616-
RE_TYPE,
617-
]:
618-
continue
637+
def test_builtin_types_override_fails(self):
638+
self._run_builtin_types_override_fails_test(TypeEncoder, {}, add_codec=False)
639+
self._run_builtin_types_override_fails_test(
640+
TypeCodec, {"bson_type": Decimal128, "transform_bson": lambda x: x}, add_codec=False
641+
)
619642

620-
class MyType(pytype): # type: ignore
621-
pass
643+
def test_add_codec(self):
644+
codec_instances = [codec() for codec in self.codecs]
622645

623-
attrs.update({"python_type": MyType, "transform_python": lambda x: x})
624-
codec = type("testcodec", (base,), attrs)
625-
codec_instance = codec()
626-
with self.assertRaisesRegex(TypeError, msg):
627-
TypeRegistry(
628-
[
629-
codec_instance,
630-
]
631-
)
646+
def assert_proper_initialization(type_registry, codec_instances):
647+
self.assertEqual(
648+
type_registry._encoder_map,
649+
{
650+
self.types[0]: codec_instances[0].transform_python,
651+
},
652+
)
653+
self.assertEqual(
654+
type_registry._decoder_map,
655+
{int: codec_instances[0].transform_bson},
656+
)
657+
658+
type_registry = TypeRegistry(codec_instances[:1])
659+
assert_proper_initialization(type_registry, codec_instances)
632660

633-
run_test(TypeEncoder, {})
634-
run_test(TypeCodec, {"bson_type": Decimal128, "transform_bson": lambda x: x})
661+
type_registry.add_codec(codec_instances[1])
662+
self.assertEqual(
663+
type_registry._encoder_map,
664+
{
665+
self.types[0]: codec_instances[0].transform_python,
666+
self.types[1]: codec_instances[1].transform_python,
667+
},
668+
)
669+
self.assertEqual(
670+
type_registry._decoder_map,
671+
{int: codec_instances[0].transform_bson, str: codec_instances[1].transform_bson},
672+
)
673+
674+
def test_add_codec_fails(self):
675+
err_msg = "Expected an instance of TypeEncoder, TypeDecoder, or TypeCodec, got .* instead"
676+
type_registry = TypeRegistry([codec() for codec in self.codecs])
677+
with self.assertRaisesRegex(TypeError, err_msg):
678+
type_registry.add_codec(self.codecs[0]) # type: ignore[arg-type]
679+
680+
with self.assertRaisesRegex(TypeError, err_msg):
681+
type_registry.add_codec(str) # type: ignore[arg-type]
682+
683+
def test_builtin_types_override_add_codec_fails(self):
684+
self._run_builtin_types_override_fails_test(TypeEncoder, {}, add_codec=True)
685+
self._run_builtin_types_override_fails_test(
686+
TypeCodec, {"bson_type": Decimal128, "transform_bson": lambda x: x}, add_codec=True
687+
)
635688

636689

637690
class TestCollectionWCustomType(AsyncIntegrationTest):

test/test_custom_types.py

Lines changed: 83 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -592,46 +592,99 @@ def test_type_registry_eq(self):
592592
codec_instances_2 = [codec() for codec in self.codecs]
593593
self.assertNotEqual(TypeRegistry(codec_instances), TypeRegistry(codec_instances_2))
594594

595-
def test_builtin_types_override_fails(self):
596-
def run_test(base, attrs):
597-
msg = (
598-
r"TypeEncoders cannot change how built-in types "
599-
r"are encoded \(encoder .* transforms type .*\)"
600-
)
601-
for pytype in _BUILT_IN_TYPES:
602-
attrs.update({"python_type": pytype, "transform_python": lambda x: x})
603-
codec = type("testcodec", (base,), attrs)
604-
codec_instance = codec()
595+
def _run_builtin_types_override_fails_test(self, base, attrs, add_codec):
596+
msg = (
597+
r"TypeEncoders cannot change how built-in types "
598+
r"are encoded \(encoder .* transforms type .*\)"
599+
)
600+
for pytype in _BUILT_IN_TYPES:
601+
attrs.update({"python_type": pytype, "transform_python": lambda x: x})
602+
codec = type("testcodec", (base,), attrs)
603+
codec_instance = codec()
604+
with self.assertRaisesRegex(TypeError, msg):
605+
TypeRegistry(
606+
[
607+
codec_instance,
608+
]
609+
)
610+
611+
# Test only some subtypes as not all can be subclassed.
612+
if pytype in [
613+
bool,
614+
type(None),
615+
RE_TYPE,
616+
]:
617+
continue
618+
619+
class MyType(pytype): # type: ignore
620+
pass
621+
622+
attrs.update({"python_type": MyType, "transform_python": lambda x: x})
623+
codec = type("testcodec", (base,), attrs)
624+
codec_instance = codec()
625+
if add_codec:
626+
type_registry = TypeRegistry()
627+
with self.assertRaisesRegex(TypeError, msg):
628+
type_registry.add_codec(codec_instance)
629+
else:
605630
with self.assertRaisesRegex(TypeError, msg):
606631
TypeRegistry(
607632
[
608633
codec_instance,
609634
]
610635
)
611636

612-
# Test only some subtypes as not all can be subclassed.
613-
if pytype in [
614-
bool,
615-
type(None),
616-
RE_TYPE,
617-
]:
618-
continue
637+
def test_builtin_types_override_fails(self):
638+
self._run_builtin_types_override_fails_test(TypeEncoder, {}, add_codec=False)
639+
self._run_builtin_types_override_fails_test(
640+
TypeCodec, {"bson_type": Decimal128, "transform_bson": lambda x: x}, add_codec=False
641+
)
619642

620-
class MyType(pytype): # type: ignore
621-
pass
643+
def test_add_codec(self):
644+
codec_instances = [codec() for codec in self.codecs]
622645

623-
attrs.update({"python_type": MyType, "transform_python": lambda x: x})
624-
codec = type("testcodec", (base,), attrs)
625-
codec_instance = codec()
626-
with self.assertRaisesRegex(TypeError, msg):
627-
TypeRegistry(
628-
[
629-
codec_instance,
630-
]
631-
)
646+
def assert_proper_initialization(type_registry, codec_instances):
647+
self.assertEqual(
648+
type_registry._encoder_map,
649+
{
650+
self.types[0]: codec_instances[0].transform_python,
651+
},
652+
)
653+
self.assertEqual(
654+
type_registry._decoder_map,
655+
{int: codec_instances[0].transform_bson},
656+
)
657+
658+
type_registry = TypeRegistry(codec_instances[:1])
659+
assert_proper_initialization(type_registry, codec_instances)
632660

633-
run_test(TypeEncoder, {})
634-
run_test(TypeCodec, {"bson_type": Decimal128, "transform_bson": lambda x: x})
661+
type_registry.add_codec(codec_instances[1])
662+
self.assertEqual(
663+
type_registry._encoder_map,
664+
{
665+
self.types[0]: codec_instances[0].transform_python,
666+
self.types[1]: codec_instances[1].transform_python,
667+
},
668+
)
669+
self.assertEqual(
670+
type_registry._decoder_map,
671+
{int: codec_instances[0].transform_bson, str: codec_instances[1].transform_bson},
672+
)
673+
674+
def test_add_codec_fails(self):
675+
err_msg = "Expected an instance of TypeEncoder, TypeDecoder, or TypeCodec, got .* instead"
676+
type_registry = TypeRegistry([codec() for codec in self.codecs])
677+
with self.assertRaisesRegex(TypeError, err_msg):
678+
type_registry.add_codec(self.codecs[0]) # type: ignore[arg-type]
679+
680+
with self.assertRaisesRegex(TypeError, err_msg):
681+
type_registry.add_codec(str) # type: ignore[arg-type]
682+
683+
def test_builtin_types_override_add_codec_fails(self):
684+
self._run_builtin_types_override_fails_test(TypeEncoder, {}, add_codec=True)
685+
self._run_builtin_types_override_fails_test(
686+
TypeCodec, {"bson_type": Decimal128, "transform_bson": lambda x: x}, add_codec=True
687+
)
635688

636689

637690
class TestCollectionWCustomType(IntegrationTest):

0 commit comments

Comments
 (0)