From 72e1c2e7fc9d6af7fce3ecd9b61b83fa26ab31b6 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Mon, 24 Mar 2025 11:03:32 -0400 Subject: [PATCH 1/3] PYTHON-5121 - Use canonical Extended JSON for BSON binary vector spec tests --- test/bson_binary_vector/float32.json | 2 +- test/test_bson_binary_vector.py | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/test/bson_binary_vector/float32.json b/test/bson_binary_vector/float32.json index 845f504ff3..72dafce10f 100644 --- a/test/bson_binary_vector/float32.json +++ b/test/bson_binary_vector/float32.json @@ -32,7 +32,7 @@ { "description": "Infinity Vector FLOAT32", "valid": true, - "vector": ["-inf", 0.0, "inf"], + "vector": [{"$numberDouble": "-Infinity"}, 0.0, {"$numberDouble": "Infinity"} ], "dtype_hex": "0x27", "dtype_alias": "FLOAT32", "padding": 0, diff --git a/test/test_bson_binary_vector.py b/test/test_bson_binary_vector.py index a49f515fea..c9ca2abaee 100644 --- a/test/test_bson_binary_vector.py +++ b/test/test_bson_binary_vector.py @@ -27,6 +27,15 @@ _TEST_PATH = Path(__file__).parent / "bson_binary_vector" +def convert_extended_json(vector) -> float: + if isinstance(vector, dict) and "$numberDouble" in vector: + if vector["$numberDouble"] == "Infinity": + return float("inf") + elif vector["$numberDouble"] == "-Infinity": + return float("-inf") + return float(vector) + + class TestBSONBinaryVector(unittest.TestCase): """Runs Binary Vector subtype tests. @@ -62,9 +71,9 @@ def run_test(self): cB_exp = binascii.unhexlify(canonical_bson_exp.encode("utf8")) decoded_doc = decode(cB_exp) binary_obs = decoded_doc[test_key] - # Handle special float cases like '-inf' + # Handle special extended JSON cases like 'Infinity' if dtype_exp in [BinaryVectorDtype.FLOAT32]: - vector_exp = [float(x) for x in vector_exp] + vector_exp = [convert_extended_json(x) for x in vector_exp] # Test round-tripping canonical bson. self.assertEqual(encode(decoded_doc), cB_exp, description) From 29332c312ef90702539f411e108849e868d1255b Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Mon, 24 Mar 2025 14:16:27 -0400 Subject: [PATCH 2/3] Add json_util.load for extended JSON --- bson/json_util.py | 23 +++++++++++++++++++++++ test/test_bson_binary_vector.py | 16 ++-------------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/bson/json_util.py b/bson/json_util.py index ecae103b55..3cbbcf83de 100644 --- a/bson/json_util.py +++ b/bson/json_util.py @@ -507,6 +507,29 @@ def loads(s: Union[str, bytes, bytearray], *args: Any, **kwargs: Any) -> Any: return json.loads(s, *args, **kwargs) +def load(fp: Any, *args: Any, **kwargs: Any) -> Any: + """Helper function that wraps :func:`json.load`. + + Automatically passes the object_hook for BSON type conversion. + + Raises ``TypeError``, ``ValueError``, ``KeyError``, or + :exc:`~bson.errors.InvalidId` on invalid MongoDB Extended JSON. + + :param json_options: A :class:`JSONOptions` instance used to modify the + decoding of MongoDB Extended JSON types. Defaults to + :const:`DEFAULT_JSON_OPTIONS`. + + .. versionadded:: 4.12 + """ + json_options = kwargs.pop("json_options", DEFAULT_JSON_OPTIONS) + # Execution time optimization if json_options.document_class is dict + if json_options.document_class is dict: + kwargs["object_hook"] = lambda obj: object_hook(obj, json_options) + else: + kwargs["object_pairs_hook"] = lambda pairs: object_pairs_hook(pairs, json_options) + return json.load(fp, *args, **kwargs) + + def _json_convert(obj: Any, json_options: JSONOptions = DEFAULT_JSON_OPTIONS) -> Any: """Recursive helper method that converts BSON types so they can be converted into json. diff --git a/test/test_bson_binary_vector.py b/test/test_bson_binary_vector.py index c9ca2abaee..3911d99ca0 100644 --- a/test/test_bson_binary_vector.py +++ b/test/test_bson_binary_vector.py @@ -21,21 +21,12 @@ from pathlib import Path from test import unittest -from bson import decode, encode +from bson import decode, encode, json_util from bson.binary import Binary, BinaryVectorDtype _TEST_PATH = Path(__file__).parent / "bson_binary_vector" -def convert_extended_json(vector) -> float: - if isinstance(vector, dict) and "$numberDouble" in vector: - if vector["$numberDouble"] == "Infinity": - return float("inf") - elif vector["$numberDouble"] == "-Infinity": - return float("-inf") - return float(vector) - - class TestBSONBinaryVector(unittest.TestCase): """Runs Binary Vector subtype tests. @@ -71,9 +62,6 @@ def run_test(self): cB_exp = binascii.unhexlify(canonical_bson_exp.encode("utf8")) decoded_doc = decode(cB_exp) binary_obs = decoded_doc[test_key] - # Handle special extended JSON cases like 'Infinity' - if dtype_exp in [BinaryVectorDtype.FLOAT32]: - vector_exp = [convert_extended_json(x) for x in vector_exp] # Test round-tripping canonical bson. self.assertEqual(encode(decoded_doc), cB_exp, description) @@ -113,7 +101,7 @@ def run_test(self): def create_tests(): for filename in _TEST_PATH.glob("*.json"): with codecs.open(str(filename), encoding="utf-8") as test_file: - test_method = create_test(json.load(test_file)) + test_method = create_test(json_util.load(test_file)) setattr(TestBSONBinaryVector, "test_" + filename.stem, test_method) From c819378094d8fbc326a28f5591eec0122295791d Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Mon, 24 Mar 2025 15:40:46 -0400 Subject: [PATCH 3/3] Remove json_util.load() --- bson/json_util.py | 23 ----------------------- test/test_bson_binary_vector.py | 3 +-- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/bson/json_util.py b/bson/json_util.py index 3cbbcf83de..ecae103b55 100644 --- a/bson/json_util.py +++ b/bson/json_util.py @@ -507,29 +507,6 @@ def loads(s: Union[str, bytes, bytearray], *args: Any, **kwargs: Any) -> Any: return json.loads(s, *args, **kwargs) -def load(fp: Any, *args: Any, **kwargs: Any) -> Any: - """Helper function that wraps :func:`json.load`. - - Automatically passes the object_hook for BSON type conversion. - - Raises ``TypeError``, ``ValueError``, ``KeyError``, or - :exc:`~bson.errors.InvalidId` on invalid MongoDB Extended JSON. - - :param json_options: A :class:`JSONOptions` instance used to modify the - decoding of MongoDB Extended JSON types. Defaults to - :const:`DEFAULT_JSON_OPTIONS`. - - .. versionadded:: 4.12 - """ - json_options = kwargs.pop("json_options", DEFAULT_JSON_OPTIONS) - # Execution time optimization if json_options.document_class is dict - if json_options.document_class is dict: - kwargs["object_hook"] = lambda obj: object_hook(obj, json_options) - else: - kwargs["object_pairs_hook"] = lambda pairs: object_pairs_hook(pairs, json_options) - return json.load(fp, *args, **kwargs) - - def _json_convert(obj: Any, json_options: JSONOptions = DEFAULT_JSON_OPTIONS) -> Any: """Recursive helper method that converts BSON types so they can be converted into json. diff --git a/test/test_bson_binary_vector.py b/test/test_bson_binary_vector.py index 3911d99ca0..9bfdcbfb9a 100644 --- a/test/test_bson_binary_vector.py +++ b/test/test_bson_binary_vector.py @@ -16,7 +16,6 @@ import binascii import codecs -import json import struct from pathlib import Path from test import unittest @@ -101,7 +100,7 @@ def run_test(self): def create_tests(): for filename in _TEST_PATH.glob("*.json"): with codecs.open(str(filename), encoding="utf-8") as test_file: - test_method = create_test(json_util.load(test_file)) + test_method = create_test(json_util.loads(test_file.read())) setattr(TestBSONBinaryVector, "test_" + filename.stem, test_method)