diff --git a/.evergreen/generated_configs/tasks.yml b/.evergreen/generated_configs/tasks.yml index eb5b49e6f..8bc758890 100644 --- a/.evergreen/generated_configs/tasks.yml +++ b/.evergreen/generated_configs/tasks.yml @@ -178,6 +178,7 @@ tasks: vars: TEST_NAME: kms SUB_TEST_NAME: gcp + tags: [] - name: test-gcpkms-fail commands: - func: run server @@ -185,12 +186,14 @@ tasks: vars: TEST_NAME: kms SUB_TEST_NAME: gcp-fail + tags: [pr] - name: test-azurekms commands: - func: run tests vars: TEST_NAME: kms SUB_TEST_NAME: azure + tags: [] - name: test-azurekms-fail commands: - func: run server @@ -198,6 +201,7 @@ tasks: vars: TEST_NAME: kms SUB_TEST_NAME: azure-fail + tags: [pr] # Mod wsgi tests - name: mod-wsgi-replica-set-python3.9 diff --git a/.evergreen/generated_configs/variants.yml b/.evergreen/generated_configs/variants.yml index 4ec9f5a3a..673bb111c 100644 --- a/.evergreen/generated_configs/variants.yml +++ b/.evergreen/generated_configs/variants.yml @@ -22,6 +22,7 @@ buildvariants: VERSION: latest NO_EXT: "1" REQUIRE_FIPS: "1" + tags: [] - name: other-hosts-rhel8-zseries-latest tasks: - name: .test-no-toolchain @@ -32,6 +33,7 @@ buildvariants: expansions: VERSION: latest NO_EXT: "1" + tags: [] - name: other-hosts-rhel8-power8-latest tasks: - name: .test-no-toolchain @@ -42,6 +44,7 @@ buildvariants: expansions: VERSION: latest NO_EXT: "1" + tags: [] - name: other-hosts-rhel8-arm64-latest tasks: - name: .test-no-toolchain @@ -52,6 +55,7 @@ buildvariants: expansions: VERSION: latest NO_EXT: "1" + tags: [] - name: other-hosts-amazon2023-latest tasks: - name: .test-no-toolchain @@ -62,6 +66,7 @@ buildvariants: expansions: VERSION: latest NO_EXT: "1" + tags: [pr] # Atlas connect tests - name: atlas-connect-rhel8 @@ -70,6 +75,7 @@ buildvariants: display_name: Atlas connect RHEL8 run_on: - rhel87-small + tags: [pr] # Atlas data lake tests - name: atlas-data-lake-ubuntu-22 @@ -80,6 +86,7 @@ buildvariants: - ubuntu2204-small expansions: TEST_NAME: data_lake + tags: [pr] # Aws auth tests - name: auth-aws-ubuntu-20 @@ -88,18 +95,21 @@ buildvariants: display_name: Auth AWS Ubuntu-20 run_on: - ubuntu2004-small + tags: [] - name: auth-aws-win64 tasks: - name: .auth-aws !.auth-aws-ecs display_name: Auth AWS Win64 run_on: - windows-64-vsMulti-small + tags: [] - name: auth-aws-macos tasks: - name: .auth-aws !.auth-aws-web-identity !.auth-aws-ecs !.auth-aws-ec2 display_name: Auth AWS macOS run_on: - macos-14 + tags: [pr] # Aws lambda tests - name: faas-lambda @@ -288,6 +298,7 @@ buildvariants: - rhel87-small expansions: PYTHON_BINARY: /opt/python/3.13t/bin/python3 + tags: [pr] - name: free-threaded-macos-python3.13t tasks: - name: .free-threading @@ -296,6 +307,7 @@ buildvariants: - macos-14 expansions: PYTHON_BINARY: /Library/Frameworks/PythonT.Framework/Versions/3.13/bin/python3t + tags: [] - name: free-threaded-macos-arm64-python3.13t tasks: - name: .free-threading @@ -304,6 +316,7 @@ buildvariants: - macos-14-arm64 expansions: PYTHON_BINARY: /Library/Frameworks/PythonT.Framework/Versions/3.13/bin/python3t + tags: [] # Green framework tests - name: green-eventlet-rhel8 @@ -372,6 +385,7 @@ buildvariants: - rhel87-small expansions: TEST_NAME: mockupdb + tags: [pr] # Mod wsgi tests - name: mod_wsgi-ubuntu-22 @@ -398,6 +412,7 @@ buildvariants: display_name: No server RHEL8 run_on: - rhel87-small + tags: [pr] # Ocsp tests - name: ocsp-rhel8 @@ -427,21 +442,29 @@ buildvariants: # Oidc auth tests - name: auth-oidc-ubuntu-22 tasks: - - name: .auth_oidc + - name: .auth_oidc_remote display_name: Auth OIDC Ubuntu-22 run_on: - ubuntu2204-small batchtime: 10080 + - name: auth-oidc-local-ubuntu-22 + tasks: + - name: "!.auth_oidc_remote .auth_oidc" + display_name: Auth OIDC Local Ubuntu-22 + run_on: + - ubuntu2204-small + batchtime: 10080 + tags: [pr] - name: auth-oidc-macos tasks: - - name: .auth_oidc !.auth_oidc_remote + - name: "!.auth_oidc_remote .auth_oidc" display_name: Auth OIDC macOS run_on: - macos-14 batchtime: 10080 - name: auth-oidc-win64 tasks: - - name: .auth_oidc !.auth_oidc_remote + - name: "!.auth_oidc_remote .auth_oidc" display_name: Auth OIDC Win64 run_on: - windows-64-vsMulti-small diff --git a/.evergreen/scripts/generate_config.py b/.evergreen/scripts/generate_config.py index 8bb4d5b21..9f42fb0a4 100644 --- a/.evergreen/scripts/generate_config.py +++ b/.evergreen/scripts/generate_config.py @@ -112,10 +112,13 @@ def create_free_threaded_variants() -> list[BuildVariant]: # TODO: PYTHON-5027 continue tasks = [".free-threading"] + tags = [] + if host_name == "rhel8": + tags.append("pr") host = HOSTS[host_name] python = "3.13t" display_name = get_variant_name("Free-threaded", host, python=python) - variant = create_variant(tasks, display_name, python=python, host=host) + variant = create_variant(tasks, display_name, tags=tags, python=python, host=host) variants.append(variant) return variants @@ -329,7 +332,7 @@ def create_atlas_data_lake_variants(): tasks = [".test-no-orchestration"] expansions = dict(TEST_NAME="data_lake") display_name = get_variant_name("Atlas Data Lake", host) - return [create_variant(tasks, display_name, host=host, expansions=expansions)] + return [create_variant(tasks, display_name, tags=["pr"], host=host, expansions=expansions)] def create_mod_wsgi_variants(): @@ -370,9 +373,9 @@ def create_oidc_auth_variants(): variants = [] for host_name in ["ubuntu22", "macos", "win64"]: if host_name == "ubuntu22": - tasks = [".auth_oidc"] + tasks = [".auth_oidc_remote"] else: - tasks = [".auth_oidc !.auth_oidc_remote"] + tasks = ["!.auth_oidc_remote .auth_oidc"] host = HOSTS[host_name] variants.append( create_variant( @@ -382,6 +385,18 @@ def create_oidc_auth_variants(): batchtime=BATCHTIME_WEEK, ) ) + # Add a specific local test to run on PRs. + if host_name == "ubuntu22": + tasks = ["!.auth_oidc_remote .auth_oidc"] + variants.append( + create_variant( + tasks, + get_variant_name("Auth OIDC Local", host), + tags=["pr"], + host=host, + batchtime=BATCHTIME_WEEK, + ) + ) return variants @@ -406,6 +421,7 @@ def create_mockupdb_variants(): [".test-no-orchestration"], get_variant_name("MockupDB", host), host=host, + tags=["pr"], expansions=expansions, ) ] @@ -430,6 +446,7 @@ def create_atlas_connect_variants(): create_variant( [".test-no-orchestration"], get_variant_name("Atlas connect", host), + tags=["pr"], host=DEFAULT_HOST, ) ] @@ -469,8 +486,10 @@ def create_aws_auth_variants(): for host_name in ["ubuntu20", "win64", "macos"]: expansions = dict() tasks = [".auth-aws"] + tags = [] if host_name == "macos": tasks = [".auth-aws !.auth-aws-web-identity !.auth-aws-ecs !.auth-aws-ec2"] + tags = ["pr"] elif host_name == "win64": tasks = [".auth-aws !.auth-aws-ecs"] host = HOSTS[host_name] @@ -478,6 +497,7 @@ def create_aws_auth_variants(): tasks, get_variant_name("Auth AWS", host), host=host, + tags=tags, expansions=expansions, ) variants.append(variant) @@ -487,7 +507,7 @@ def create_aws_auth_variants(): def create_no_server_variants(): host = HOSTS["rhel8"] name = get_variant_name("No server", host=host) - return [create_variant([".test-no-orchestration"], name, host=host)] + return [create_variant([".test-no-orchestration"], name, host=host, tags=["pr"])] def create_alternative_hosts_variants(): @@ -512,14 +532,18 @@ def create_alternative_hosts_variants(): expansions = dict(VERSION="latest") handle_c_ext(C_EXTS[0], expansions) host = HOSTS[host_name] + tags = [] if "fips" in host_name.lower(): expansions["REQUIRE_FIPS"] = "1" + if "amazon" in host_name.lower(): + tags.append("pr") variants.append( create_variant( [".test-no-toolchain"], display_name=get_variant_name("Other hosts", host, version=version), batchtime=batchtime, host=host, + tags=tags, expansions=expansions, ) ) @@ -693,16 +717,18 @@ def create_kms_tasks(): for success in [True, False]: name = f"test-{kms_type}kms" sub_test_name = kms_type + tags = [] if not success: name += "-fail" sub_test_name += "-fail" + tags.append("pr") commands = [] if not success: commands.append(FunctionCall(func="run server")) test_vars = dict(TEST_NAME="kms", SUB_TEST_NAME=sub_test_name) test_func = FunctionCall(func="run tests", vars=test_vars) commands.append(test_func) - tasks.append(EvgTask(name=name, commands=commands)) + tasks.append(EvgTask(name=name, tags=tags, commands=commands)) return tasks @@ -756,6 +782,7 @@ def create_oidc_tasks(): if sub_test != "default": tags.append("auth_oidc_remote") tasks.append(EvgTask(name=task_name, tags=tags, commands=[test_func])) + return tasks @@ -802,6 +829,8 @@ def _create_ocsp_tasks(algo, variant, server_type, base_task_name): tags = ["ocsp", f"ocsp-{algo}", version] if "disableStapling" not in variant: tags.append("ocsp-staple") + if algo == "valid-cert-server-staples" and version == "latest": + tags.append("pr") task_name = get_task_name( f"test-ocsp-{algo}-{base_task_name}", python=python, version=version diff --git a/README.md b/README.md index 962d0d958..9bee92c56 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ The PyMongo distribution contains tools for interacting with MongoDB database from Python. The `bson` package is an implementation of the [BSON format](http://bsonspec.org) for Python. The `pymongo` package is -a native Python driver for MongoDB. The `gridfs` package is a +a native Python driver for MongoDB, offering both synchronous and asynchronous APIs. The `gridfs` package is a [gridfs](https://github.com/mongodb/specifications/blob/master/source/gridfs/gridfs-spec.md/) implementation on top of `pymongo`. diff --git a/bson/binary.py b/bson/binary.py index 48f1f5851..6698e55cc 100644 --- a/bson/binary.py +++ b/bson/binary.py @@ -462,10 +462,6 @@ class Binary(bytes): raise ValueError(f"{padding=}. It must be in [0,1, ..7].") if padding and not vector: raise ValueError("Empty vector with non-zero padding.") - if padding and not (vector[-1] & ((1 << padding) - 1)) == 0: # type: ignore - raise ValueError( - "If padding p is provided, all bits in the final byte lower than p must be 0." - ) elif dtype == BinaryVectorDtype.FLOAT32: # pack floats as float32 format_str = "f" if padding: @@ -494,11 +490,6 @@ class Binary(bytes): dtype = BinaryVectorDtype(dtype) n_values = len(self) - position - if padding and dtype != BinaryVectorDtype.PACKED_BIT: - raise ValueError( - f"Corrupt data. Padding ({padding}) must be 0 for all but PACKED_BIT dtypes. ({dtype=})" - ) - if dtype == BinaryVectorDtype.INT8: dtype_format = "b" format_string = f"<{n_values}{dtype_format}" @@ -522,12 +513,6 @@ class Binary(bytes): dtype_format = "B" format_string = f"<{n_values}{dtype_format}" unpacked_uint8s = list(struct.unpack_from(format_string, self, position)) - if padding and not n_values: - raise ValueError("Corrupt data. Vector has a padding P, but no data.") - if padding and n_values and not (unpacked_uint8s[-1] & ((1 << padding) - 1)) == 0: - raise ValueError( - "Corrupt data. Vector has a padding P, but bits in the final byte lower than P are non-zero." - ) return BinaryVector(unpacked_uint8s, dtype, padding) else: diff --git a/doc/changelog.rst b/doc/changelog.rst index 87918639c..9b0275dbd 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -1,6 +1,21 @@ Changelog ========= +Changes in Version 4.13.0 (2025/05/14) +-------------------------------------- + +PyMongo 4.13 brings a number of changes including: + +- Fixed a bug where :class:`pymongo.write_concern.WriteConcern` repr was not eval-able + when using ``w="majority"``. + +Issues Resolved +............... + +See the `PyMongo 4.13 release notes in JIRA`_ for the list of resolved issues +in this release. + +.. _PyMongo 4.13 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=42509 Changes in Version 4.12.1 (2025/04/29) -------------------------------------- diff --git a/doc/faq.rst b/doc/faq.rst index 7656481d8..cb67ea7fe 100644 --- a/doc/faq.rst +++ b/doc/faq.rst @@ -170,12 +170,9 @@ PyMongo supports CPython 3.9+ and PyPy3.10+. See the :doc:`python3` for details. Does PyMongo support asynchronous frameworks like Gevent, asyncio, Tornado, or Twisted? --------------------------------------------------------------------------------------- +As of PyMongo v4.13, PyMongo fully supports asyncio and `Tornado `_. See `the official docs `_ for more details. -PyMongo fully supports :doc:`Gevent `. - -To use MongoDB with `asyncio `_ -or `Tornado `_, see the -`Motor `_ project. +PyMongo also fully supports :doc:`Gevent `. For `Twisted `_, see `TxMongo `_. Its stated mission is to keep feature diff --git a/doc/tools.rst b/doc/tools.rst index a3f167d02..5a9297ad6 100644 --- a/doc/tools.rst +++ b/doc/tools.rst @@ -163,6 +163,9 @@ These are alternatives to PyMongo. * `Motor `_ is a full-featured, non-blocking MongoDB driver for Python Tornado applications. + As of PyMongo v4.13, Motor's features have been merged into PyMongo via the new AsyncMongoClient API. + As a result of this merger, Motor will be officially deprecated on May 14th, 2026. + For more information, see `the official PyMongo docs `_. * `TxMongo `_ is an asynchronous Twisted Python driver for MongoDB. * `MongoMock `_ is a small diff --git a/pymongo/asynchronous/pool.py b/pymongo/asynchronous/pool.py index f4d5b174f..9a39883fc 100644 --- a/pymongo/asynchronous/pool.py +++ b/pymongo/asynchronous/pool.py @@ -131,6 +131,7 @@ class AsyncConnection: :param pool: a Pool instance :param address: the server's (host, port) :param id: the id of this socket in it's pool + :param is_sdam: SDAM connections do not call hello on creation """ def __init__( @@ -139,11 +140,13 @@ class AsyncConnection: pool: Pool, address: tuple[str, int], id: int, + is_sdam: bool, ): self.pool_ref = weakref.ref(pool) self.conn = conn self.address = address self.id = id + self.is_sdam = is_sdam self.closed = False self.last_checkin_time = time.monotonic() self.performed_handshake = False @@ -711,13 +714,13 @@ class Pool: self, address: _Address, options: PoolOptions, - handshake: bool = True, + is_sdam: bool = False, client_id: Optional[ObjectId] = None, ): """ :param address: a (hostname, port) tuple :param options: a PoolOptions instance - :param handshake: whether to call hello for each new AsyncConnection + :param is_sdam: whether to call hello for each new AsyncConnection """ if options.pause_enabled: self.state = PoolState.PAUSED @@ -746,14 +749,14 @@ class Pool: self.pid = os.getpid() self.address = address self.opts = options - self.handshake = handshake + self.is_sdam = is_sdam # Don't publish events or logs in Monitor pools. self.enabled_for_cmap = ( - self.handshake + not self.is_sdam and self.opts._event_listeners is not None and self.opts._event_listeners.enabled_for_cmap ) - self.enabled_for_logging = self.handshake + self.enabled_for_logging = not self.is_sdam # The first portion of the wait queue. # Enforces: maxPoolSize @@ -1058,14 +1061,14 @@ class Pool: raise - conn = AsyncConnection(networking_interface, self, self.address, conn_id) # type: ignore[arg-type] + conn = AsyncConnection(networking_interface, self, self.address, conn_id, self.is_sdam) # type: ignore[arg-type] async with self.lock: self.active_contexts.add(conn.cancel_context) self.active_contexts.discard(tmp_context) if tmp_context.cancelled: conn.cancel_context.cancel() try: - if self.handshake: + if not self.is_sdam: await conn.hello() self.is_writable = conn.is_writable if handler: diff --git a/pymongo/asynchronous/topology.py b/pymongo/asynchronous/topology.py index 99b30fed1..052f91afe 100644 --- a/pymongo/asynchronous/topology.py +++ b/pymongo/asynchronous/topology.py @@ -985,7 +985,7 @@ class Topology: ) return self._settings.pool_class( - address, monitor_pool_options, handshake=False, client_id=self._topology_id + address, monitor_pool_options, is_sdam=True, client_id=self._topology_id ) def _error_message(self, selector: Callable[[Selection], Selection]) -> str: diff --git a/pymongo/network_layer.py b/pymongo/network_layer.py index 3fa180bf7..6f1bb9a35 100644 --- a/pymongo/network_layer.py +++ b/pymongo/network_layer.py @@ -357,7 +357,12 @@ def receive_data(conn: Connection, length: int, deadline: Optional[float]) -> me except socket.timeout: if conn.cancel_context.cancelled: raise _OperationCancelled("operation cancelled") from None - if _PYPY: + if ( + _PYPY + or not conn.is_sdam + and deadline is not None + and deadline - time.monotonic() < 0 + ): # We reached the true deadline. raise continue diff --git a/pymongo/synchronous/pool.py b/pymongo/synchronous/pool.py index 44aec31a8..505f58c60 100644 --- a/pymongo/synchronous/pool.py +++ b/pymongo/synchronous/pool.py @@ -131,6 +131,7 @@ class Connection: :param pool: a Pool instance :param address: the server's (host, port) :param id: the id of this socket in it's pool + :param is_sdam: SDAM connections do not call hello on creation """ def __init__( @@ -139,11 +140,13 @@ class Connection: pool: Pool, address: tuple[str, int], id: int, + is_sdam: bool, ): self.pool_ref = weakref.ref(pool) self.conn = conn self.address = address self.id = id + self.is_sdam = is_sdam self.closed = False self.last_checkin_time = time.monotonic() self.performed_handshake = False @@ -709,13 +712,13 @@ class Pool: self, address: _Address, options: PoolOptions, - handshake: bool = True, + is_sdam: bool = False, client_id: Optional[ObjectId] = None, ): """ :param address: a (hostname, port) tuple :param options: a PoolOptions instance - :param handshake: whether to call hello for each new Connection + :param is_sdam: whether to call hello for each new Connection """ if options.pause_enabled: self.state = PoolState.PAUSED @@ -744,14 +747,14 @@ class Pool: self.pid = os.getpid() self.address = address self.opts = options - self.handshake = handshake + self.is_sdam = is_sdam # Don't publish events or logs in Monitor pools. self.enabled_for_cmap = ( - self.handshake + not self.is_sdam and self.opts._event_listeners is not None and self.opts._event_listeners.enabled_for_cmap ) - self.enabled_for_logging = self.handshake + self.enabled_for_logging = not self.is_sdam # The first portion of the wait queue. # Enforces: maxPoolSize @@ -1054,14 +1057,14 @@ class Pool: raise - conn = Connection(networking_interface, self, self.address, conn_id) # type: ignore[arg-type] + conn = Connection(networking_interface, self, self.address, conn_id, self.is_sdam) # type: ignore[arg-type] with self.lock: self.active_contexts.add(conn.cancel_context) self.active_contexts.discard(tmp_context) if tmp_context.cancelled: conn.cancel_context.cancel() try: - if self.handshake: + if not self.is_sdam: conn.hello() self.is_writable = conn.is_writable if handler: diff --git a/pymongo/synchronous/topology.py b/pymongo/synchronous/topology.py index 10d41def6..28370d4ad 100644 --- a/pymongo/synchronous/topology.py +++ b/pymongo/synchronous/topology.py @@ -983,7 +983,7 @@ class Topology: ) return self._settings.pool_class( - address, monitor_pool_options, handshake=False, client_id=self._topology_id + address, monitor_pool_options, is_sdam=True, client_id=self._topology_id ) def _error_message(self, selector: Callable[[Selection], Selection]) -> str: diff --git a/pymongo/write_concern.py b/pymongo/write_concern.py index ff31c6730..1f9da7af2 100644 --- a/pymongo/write_concern.py +++ b/pymongo/write_concern.py @@ -127,7 +127,7 @@ class WriteConcern: def __repr__(self) -> str: return "WriteConcern({})".format( - ", ".join("{}={}".format(*kvt) for kvt in self.__document.items()) + ", ".join(f"{k}={v!r}" for k, v in self.__document.items()) ) def __eq__(self, other: Any) -> bool: diff --git a/test/asynchronous/utils.py b/test/asynchronous/utils.py index f653c575e..ca80d1f6d 100644 --- a/test/asynchronous/utils.py +++ b/test/asynchronous/utils.py @@ -159,6 +159,7 @@ class AsyncMockConnection: self.cancel_context = _CancellationContext() self.more_to_come = False self.id = random.randint(0, 100) + self.is_sdam = False self.server_connection_id = random.randint(0, 100) def close_conn(self, reason): @@ -172,7 +173,7 @@ class AsyncMockConnection: class AsyncMockPool: - def __init__(self, address, options, handshake=True, client_id=None): + def __init__(self, address, options, is_sdam=False, client_id=None): self.gen = _PoolGeneration() self._lock = _async_create_lock() self.opts = options diff --git a/test/bson_binary_vector/packed_bit.json b/test/bson_binary_vector/packed_bit.json index 3015acba6..a220e7e31 100644 --- a/test/bson_binary_vector/packed_bit.json +++ b/test/bson_binary_vector/packed_bit.json @@ -20,24 +20,6 @@ "padding": 0, "canonical_bson": "1600000005766563746F7200040000000910007F0700" }, - { - "description": "PACKED_BIT with padding", - "valid": true, - "vector": [127, 8], - "dtype_hex": "0x10", - "dtype_alias": "PACKED_BIT", - "padding": 3, - "canonical_bson": "1600000005766563746F7200040000000910037F0800" - }, - { - "description": "PACKED_BIT with inconsistent padding", - "valid": false, - "vector": [127, 7], - "dtype_hex": "0x10", - "dtype_alias": "PACKED_BIT", - "padding": 3, - "canonical_bson": "1600000005766563746F7200040000000910037F0700" - }, { "description": "Empty Vector PACKED_BIT", "valid": true, @@ -47,6 +29,15 @@ "padding": 0, "canonical_bson": "1400000005766563746F72000200000009100000" }, + { + "description": "PACKED_BIT with padding", + "valid": true, + "vector": [127, 7], + "dtype_hex": "0x10", + "dtype_alias": "PACKED_BIT", + "padding": 3, + "canonical_bson": "1600000005766563746F7200040000000910037F0700" + }, { "description": "Overflow Vector PACKED_BIT", "valid": false, diff --git a/test/test_bson.py b/test/test_bson.py index 522945d5f..1616c513c 100644 --- a/test/test_bson.py +++ b/test/test_bson.py @@ -739,7 +739,7 @@ class TestBSON(unittest.TestCase): """Tests of subtype 9""" # We start with valid cases, across the 3 dtypes implemented. # Work with a simple vector that can be interpreted as int8, float32, or ubyte - list_vector = [127, 8] + list_vector = [127, 7] # As INT8, vector has length 2 binary_vector = Binary.from_vector(list_vector, BinaryVectorDtype.INT8) vector = binary_vector.as_vector() @@ -764,18 +764,18 @@ class TestBSON(unittest.TestCase): uncompressed = "" for val in list_vector: uncompressed += format(val, "08b") - assert uncompressed[:-padding] == "0111111100001" + assert uncompressed[:-padding] == "0111111100000" # It is worthwhile explicitly showing the values encoded to BSON padded_doc = {"padded_vec": padded_vec} assert ( encode(padded_doc) - == b"\x1a\x00\x00\x00\x05padded_vec\x00\x04\x00\x00\x00\t\x10\x03\x7f\x08\x00" + == b"\x1a\x00\x00\x00\x05padded_vec\x00\x04\x00\x00\x00\t\x10\x03\x7f\x07\x00" ) # and dumped to json assert ( json_util.dumps(padded_doc) - == '{"padded_vec": {"$binary": {"base64": "EAN/CA==", "subType": "09"}}}' + == '{"padded_vec": {"$binary": {"base64": "EAN/Bw==", "subType": "09"}}}' ) # FLOAT32 is also implemented diff --git a/test/test_bson_binary_vector.py b/test/test_bson_binary_vector.py index afe01f42b..9bfdcbfb9 100644 --- a/test/test_bson_binary_vector.py +++ b/test/test_bson_binary_vector.py @@ -48,11 +48,11 @@ def create_test(case_spec): def run_test(self): for test_case in case_spec.get("tests", []): description = test_case["description"] - vector_exp = test_case.get("vector", None) + vector_exp = test_case.get("vector", []) dtype_hex_exp = test_case["dtype_hex"] dtype_alias_exp = test_case.get("dtype_alias") padding_exp = test_case.get("padding", 0) - canonical_bson_exp = test_case.get("canonical_bson", None) + canonical_bson_exp = test_case.get("canonical_bson") # Convert dtype hex string into bytes dtype_exp = BinaryVectorDtype(int(dtype_hex_exp, 16).to_bytes(1, byteorder="little")) @@ -85,25 +85,14 @@ def create_test(case_spec): self.assertEqual(cB_obs, canonical_bson_exp, description) else: - """ - #### To prove correct in an invalid case (`valid:false`), one MUST - - if the vector field is present, raise an exception when attempting to encode a document from the numeric values, - dtype, and padding. - - if the canonical_bson field is present, raise an exception when attempting to deserialize it into the corresponding - numeric values, as the field contains corrupted data. - """ - # Tests Binary.from_vector() - if vector_exp is not None: - with self.assertRaises((struct.error, ValueError), msg=description): - Binary.from_vector(vector_exp, dtype_exp, padding_exp) - - # Tests Binary.as_vector() - if canonical_bson_exp is not None: - with self.assertRaises((struct.error, ValueError), msg=description): - cB_exp = binascii.unhexlify(canonical_bson_exp.encode("utf8")) - decoded_doc = decode(cB_exp) - binary_obs = decoded_doc[test_key] - binary_obs.as_vector() + with self.assertRaises((struct.error, ValueError), msg=description): + # Tests Binary.from_vector + Binary.from_vector(vector_exp, dtype_exp, padding_exp) + # Tests Binary.as_vector + cB_exp = binascii.unhexlify(canonical_bson_exp.encode("utf8")) + decoded_doc = decode(cB_exp) + binary_obs = decoded_doc[test_key] + binary_obs.as_vector() return run_test diff --git a/test/test_topology.py b/test/test_topology.py index 22e94739e..141b2d7f2 100644 --- a/test/test_topology.py +++ b/test/test_topology.py @@ -121,7 +121,7 @@ class TestTopologyConfiguration(TopologyTest): self.assertEqual(1, monitor._pool.opts.socket_timeout) # The monitor, not its pool, is responsible for calling hello. - self.assertFalse(monitor._pool.handshake) + self.assertTrue(monitor._pool.is_sdam) class TestSingleServerTopology(TopologyTest): diff --git a/test/test_write_concern.py b/test/test_write_concern.py index e22c7e7a8..02a7cb6e5 100644 --- a/test/test_write_concern.py +++ b/test/test_write_concern.py @@ -67,6 +67,19 @@ class TestWriteConcern(unittest.TestCase): _fake_type = collections.namedtuple("NotAWriteConcern", ["document"]) # type: ignore self.assertNotEqual(WriteConcern(j=True), _fake_type({"j": True})) + def assertRepr(self, obj): + new_obj = eval(repr(obj)) + self.assertEqual(type(new_obj), type(obj)) + self.assertEqual(repr(new_obj), repr(obj)) + + def test_repr(self): + concern = WriteConcern(j=True, wtimeout=3000, w="majority", fsync=False) + self.assertRepr(concern) + self.assertEqual( + repr(concern), + "WriteConcern(wtimeout=3000, j=True, fsync=False, w='majority')", + ) + if __name__ == "__main__": unittest.main() diff --git a/test/utils.py b/test/utils.py index 3027ed751..25d95d1d3 100644 --- a/test/utils.py +++ b/test/utils.py @@ -157,6 +157,7 @@ class MockConnection: self.cancel_context = _CancellationContext() self.more_to_come = False self.id = random.randint(0, 100) + self.is_sdam = False self.server_connection_id = random.randint(0, 100) def close_conn(self, reason): @@ -170,7 +171,7 @@ class MockConnection: class MockPool: - def __init__(self, address, options, handshake=True, client_id=None): + def __init__(self, address, options, is_sdam=False, client_id=None): self.gen = _PoolGeneration() self._lock = _create_lock() self.opts = options